From d061fdaef70711566a1290636a16b41bab607a93 Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Sat, 5 Sep 2026 23:03:43 +0800 Subject: [PATCH 001/165] docs(adr): reopen the Go runtime as an incremental sidecar takeover ADR-0008 records the owner decision to migrate the backend to Go as an incremental sidecar takeover, ending in a single static Go binary. - Reconcile the runtime-line policy in AGENTS.md, MAINTAINERS.md, and structure/06_docs-and-release.md with ADR-0008. - Add the first-increment plan (fresh go/ tree, ocx-sidecar, one read-only route, differential harness) under devlog/_plan. - Add Agent skills docs (issue tracker, triage labels, domain) and the Agent skills section in AGENTS.md. --- AGENTS.md | 29 +++++++-- MAINTAINERS.md | 12 ++-- .../260905_go_sidecar_takeover/000_plan.md | 61 +++++++++++++++++++ .../0008-go-runtime-incremental-takeover.md | 27 ++++++++ docs/agents/domain.md | 51 ++++++++++++++++ docs/agents/issue-tracker.md | 55 +++++++++++++++++ docs/agents/triage-labels.md | 15 +++++ structure/06_docs-and-release.md | 9 +-- 8 files changed, 246 insertions(+), 13 deletions(-) create mode 100644 devlog/_plan/260905_go_sidecar_takeover/000_plan.md create mode 100644 docs/adr/0008-go-runtime-incremental-takeover.md create mode 100644 docs/agents/domain.md create mode 100644 docs/agents/issue-tracker.md create mode 100644 docs/agents/triage-labels.md diff --git a/AGENTS.md b/AGENTS.md index 8d07b948ef..1cf18d753a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -16,8 +16,10 @@ Bun-native TypeScript with no separate server compile step. `tests/helpers/`, broader scenarios in `tests/e2e-style/`. - `gui/` — React + Vite dashboard; packaged output is served from `gui/dist`. - `docs-site/` — public docs (Astro + Starlight), deployed to GitHub Pages. -- `go/` — retired Go native-runtime experiment; kept only where the TypeScript - runtime still references it. New work does not go here. +- `go/` — the Go runtime under incremental takeover (see + `docs/adr/0008-go-runtime-incremental-takeover.md`). The retired `dev2-go` + port is archived at `lidge-jun/opencodex-go-archive` (tag `archive/dev2-go`) + and is reference material only, not a fork. - `structure/` — maintainer invariants and architecture notes; read before changing shared subsystems. - `scripts/` — release and maintenance tooling; `scripts/release.ts` is the @@ -241,6 +243,20 @@ They are not regressions; do not re-investigate them: Everything else passes (15480 pass / 16 skip / 5 fail as of 2.35.0). +## Agent skills + +### Issue tracker + +Issues live in this repo's GitHub Issues; agent-created issues must use the `.github/ISSUE_TEMPLATE/` forms (the `enforce-issue-quality` gate closes freeform issues). See `docs/agents/issue-tracker.md`. + +### Triage labels + +Default five-label vocabulary: `needs-triage`, `needs-info`, `ready-for-agent`, `ready-for-human`, `wontfix`. See `docs/agents/triage-labels.md`. + +### Domain docs + +Single-context: root `CONTEXT.md` (when present) plus root `docs/adr/`. See `docs/agents/domain.md`. + ## Issues and pull requests (agents) Agent-created issues and PRs must use the repository templates. The gates @@ -282,9 +298,12 @@ than nudged. from `dev` (releases, docs deploys). Do not open feature PRs against `main`. - `preview` — prerelease train (`x.y.z-preview.*` versions). -Bun-native TypeScript on `dev` is the only runtime line. If native code -returns, the expectation is an incremental module (for example Rust via N-API) -landing on `dev`, not a second full-runtime branch. +Bun-native TypeScript on `dev` is the only runtime line today; it is being +migrated to Go as an incremental sidecar takeover, per +`docs/adr/0008-go-runtime-incremental-takeover.md`. The Go sidecar takes over +routes one at a time behind the TypeScript front door, ending in a single +static Go binary. The retired `dev2-go` parallel line stays retired — no +second full-runtime branch is being reopened. Stacked child pull requests that target another **open** PR's head branch are an intentional review workflow, not an alternate integration line. The diff --git a/MAINTAINERS.md b/MAINTAINERS.md index f7183db6ef..c96ba758fa 100644 --- a/MAINTAINERS.md +++ b/MAINTAINERS.md @@ -98,7 +98,8 @@ and ported under `go/`. That policy is withdrawn as of 2026-07-30. The dual-track cost outran its return: the carry backlog never cleared (17 commits and 9 open `needs-go-port` issues at the time of the decision, against 594 commits of divergence), and dogfooding the Go runtime kept producing new -defects. Bun-native TypeScript on `dev` is the single runtime line again. +defects. Bun-native TypeScript on `dev` was the single runtime line again — +until ADR-0008 reopened Go work as a sidecar takeover. - The branch has been deleted from this repository. Its full history is published at @@ -108,9 +109,12 @@ defects. Bun-native TypeScript on `dev` is the single runtime line again. issues (#661, #663, #666, #670, #674, #678, #680, #685, #703) were closed as not planned, and the `needs-go-port` label no longer exists on the repository. -- Future native work is expected to be an incremental module landing on `dev` - (Rust via N-API is the current candidate), not a second integration branch. - Reopening a parallel runtime line is an owner decision. +- Future native work is expected to be an incremental module landing on `dev`, + not a second integration branch. At the time of retirement the candidate was + Rust via N-API; ADR-0008 supersedes that with a Go runtime reopened as an + incremental sidecar takeover — a fresh `go/` codebase, not a fork of this + archive and not a parallel line. Reopening a parallel runtime line remains an + owner decision. See `docs/adr/0008-go-runtime-incremental-takeover.md`. ## Maintainer changes diff --git a/devlog/_plan/260905_go_sidecar_takeover/000_plan.md b/devlog/_plan/260905_go_sidecar_takeover/000_plan.md new file mode 100644 index 0000000000..fa2c93a56d --- /dev/null +++ b/devlog/_plan/260905_go_sidecar_takeover/000_plan.md @@ -0,0 +1,61 @@ +# Go sidecar takeover — first increment: fresh `go/` + one read-only route + +Date: 2026-09-05 +Status: planned +ADR: [`docs/adr/0008-go-runtime-incremental-takeover.md`](../../docs/adr/0008-go-runtime-incremental-takeover.md) + +## Objective + +Land the first Go increment on `dev` per ADR-0008: a fresh Go module under +`go/` building an `ocx-sidecar` binary, spawned and supervised by the +TypeScript server, serving exactly one read-only management route +(`GET /api/system/health`) with byte-identical HTTP semantics, plus the +differential oracle harness that proves it. + +This increment migrates nothing else: no proxy hot path, no CLI, no write +route. Its only job is to prove the seam — TS front door → Go sidecar → +differential oracle — with zero user-visible change. + +## Shape + +- Go module at `go/` (`go.mod`, module path `github.com/lidge-jun/opencodex/go`), + a fresh codebase. Nothing is copied from `archive/dev2-go`; that archive is + reference material only (consulted, never forked). +- Binary: `go/cmd/ocx-sidecar` → `ocx-sidecar`, built `CGO_ENABLED=0`. +- The TS server spawns the sidecar as a child process and supervises it, + following the existing sidecar pattern (`openai-sidecar.ts` is the model). + It forwards only `GET /api/system/health` to the sidecar over the local HTTP + channel (`direct-local-http.ts`); every other route stays in-process TS. +- The sidecar serves the same JSON shape as the TS handler: + `{ status, service, version, uptime, pid }`. `status`, `service`, and + `version` must equal the TS values; `uptime` and `pid` are the sidecar's own + process values. + +## Differential oracle harness + +- A Bun test boots the TS server with the sidecar attached, then issues + `GET /api/system/health` twice — once to the TS in-process handler and once + to the Go sidecar — and asserts byte-identical responses after normalising + the declared volatile fields (`pid`, `uptime`). +- The normalisation set is explicit and declared, never ad-hoc, so no later + route can silently widen what "equal" means. +- This is the divergence class that sank `dev2-go` (Go runtime numbers rendered + under JavaScript labels); the harness must fail on any such drift, not log it. + +## Accept criteria + +- `go build ./...`, `go vet ./...`, and `go test ./...` clean under `go/`. +- The differential harness passes in CI: TS handler and Go sidecar agree on + status, headers, and the normalised body for `GET /api/system/health`. +- `bun run typecheck` and the existing Bun suite stay green — no TS behaviour + change. +- No route other than `GET /api/system/health` is affected, and the Go-owned + route is declared in the management route registry so the forwarding seam is + visible to the existing registry reconciliation test. + +## Open questions for the implementer + +- Nested `go/go.mod` (module `.../opencodex/go`) versus a root `go.mod`; nested + is assumed here to keep the Go tree self-contained under `go/`. +- Which sidecar supervision primitive to reuse (the exact sidecar spawner to + copy), settled during implementation against `openai-sidecar.ts`. diff --git a/docs/adr/0008-go-runtime-incremental-takeover.md b/docs/adr/0008-go-runtime-incremental-takeover.md new file mode 100644 index 0000000000..4adf895f95 --- /dev/null +++ b/docs/adr/0008-go-runtime-incremental-takeover.md @@ -0,0 +1,27 @@ +# Reopen the Go runtime line as an incremental sidecar takeover + +The `dev2-go` Go port was retired on 2026-07-30 because a parallel runtime line +could not keep up with `dev` (594 commits of divergence) and kept producing silent +dogfood defects. We reopen Go native work in a different shape: the backend migrates +to Go as an incremental sidecar takeover — the Bun/TypeScript server stays the front +door while a Go sidecar takes over routes one at a time, and the endpoint is a single +static Go binary (server, CLI, and the embedded dashboard) with a byte-identical HTTP +API and on-disk formats. The owner will maintain the Go side long-term, which is what +makes the reopened line sustainable where the parallel line was not. + +## Considered options + +- **Parallel Go runtime line** — the `dev2-go` shape; rejected as already-failed. +- **Rust via N-API incremental module** — the previously stated default; not chosen + because the owner prefers Go. +- **Big-bang rewrite** — rejected; a single cutover cannot be verified against the + live TS oracle. + +## Consequences + +- Fresh Go codebase; `archive/dev2-go` is reference material only, not a fork. +- Parity is proven by a differential harness: the same request is run against the TS + and Go implementations and the responses (including SSE frame sequences) must match. +- The Compatibility Lab migrates last and is an explicit cut candidate. +- The flip to a single binary happens only at 100% differential parity; until then the + TypeScript CLI and server remain the operating surface. diff --git a/docs/agents/domain.md b/docs/agents/domain.md new file mode 100644 index 0000000000..b548c538d6 --- /dev/null +++ b/docs/agents/domain.md @@ -0,0 +1,51 @@ +# Domain Docs + +How the engineering skills should consume this repo's domain documentation when exploring the codebase. + +## Before exploring, read these + +- **`CONTEXT.md`** at the repo root, or +- **`CONTEXT-MAP.md`** at the repo root if it exists — it points at one `CONTEXT.md` per context. Read each one relevant to the topic. +- **`docs/adr/`** — read ADRs that touch the area you're about to work in. In multi-context repos, also check `src//docs/adr/` for context-scoped decisions. + +If any of these files don't exist, **proceed silently**. Don't flag their absence; don't suggest creating them upfront. The `/domain-modeling` skill (reached via `/grill-with-docs` and `/improve-codebase-architecture`) creates them lazily when terms or decisions actually get resolved. + +## File structure + +Single-context repo (most repos): + +``` +/ +├── CONTEXT.md +├── docs/adr/ +│ ├── 0001-event-sourced-orders.md +│ └── 0002-postgres-for-write-model.md +└── src/ +``` + +Multi-context repo (presence of `CONTEXT-MAP.md` at the root): + +``` +/ +├── CONTEXT-MAP.md +├── docs/adr/ ← system-wide decisions +└── src/ + ├── ordering/ + │ ├── CONTEXT.md + │ └── docs/adr/ ← context-specific decisions + └── billing/ + ├── CONTEXT.md + └── docs/adr/ +``` + +## Use the glossary's vocabulary + +When your output names a domain concept (in an issue title, a refactor proposal, a hypothesis, a test name), use the term as defined in `CONTEXT.md`. Don't drift to synonyms the glossary explicitly avoids. + +If the concept you need isn't in the glossary yet, that's a signal — either you're inventing language the project doesn't use (reconsider) or there's a real gap (note it for `/domain-modeling`). + +## Flag ADR conflicts + +If your output contradicts an existing ADR, surface it explicitly rather than silently overriding: + +> _Contradicts ADR-0007 (event-sourced orders) — but worth reopening because…_ diff --git a/docs/agents/issue-tracker.md b/docs/agents/issue-tracker.md new file mode 100644 index 0000000000..a52d774eb5 --- /dev/null +++ b/docs/agents/issue-tracker.md @@ -0,0 +1,55 @@ +# Issue tracker: GitHub + +Issues and specs for this repo live as GitHub issues. Use the `gh` CLI for all operations. + +Infer the repo from `git remote -v` — `gh` does this automatically when run inside a clone. + +## Conventions + +- **Create an issue**: this repo requires a form template. Open issues through + the template chooser using one of `.github/ISSUE_TEMPLATE/` — + `bug_report.yml` (Bug report), `feature_request.yml` (Feature proposal), + `documentation.yml` (Documentation), or `provider_compatibility.yml` + (Provider or API compatibility) — and keep the form's `###` section headings + exactly as generated. The `enforce-issue-quality` gate closes freeform or + mismatched issues and blank issues are disabled, so a bare + `gh issue create --title … --body …` without the form headings will be + auto-closed. When scripting, reproduce the matching form's headings and + content in `--body` (or apply the matching kind label — `bug`, `enhancement`, + `documentation`, `provider-compatibility`). +- **Read an issue**: `gh issue view --comments`, filtering comments by `jq` and also fetching labels. +- **List issues**: `gh issue list --state open --json number,title,body,labels,comments --jq '[.[] | {number, title, body, labels: [.labels[].name], comments: [.comments[].body]}]'` with appropriate `--label` and `--state` filters. +- **Comment on an issue**: `gh issue comment --body "..."` +- **Apply / remove labels**: `gh issue edit --add-label "..."` / `--remove-label "..."` +- **Close**: `gh issue close --comment "..."` + +## Pull requests as a triage surface + +**PRs as a request surface: no.** _(Set to `yes` if this repo treats external PRs as feature requests; `/triage` reads this flag.)_ + +When set to `yes`, PRs run through the same labels and states as issues, using the `gh pr` equivalents: + +- **Read a PR**: `gh pr view --comments` and `gh pr diff ` for the diff. +- **List external PRs for triage**: `gh pr list --state open --json number,title,body,labels,author,authorAssociation,comments` then keep only `authorAssociation` of `CONTRIBUTOR`, `FIRST_TIME_CONTRIBUTOR`, or `NONE` (drop `OWNER`/`MEMBER`/`COLLABORATOR`). +- **Comment / label / close**: `gh pr comment`, `gh pr edit --add-label`/`--remove-label`, `gh pr close`. + +GitHub shares one number space across issues and PRs, so a bare `#42` may be either — resolve with `gh pr view 42` and fall back to `gh issue view 42`. + +## When a skill says "publish to the issue tracker" + +Create a GitHub issue using a form template (see Conventions). + +## When a skill says "fetch the relevant ticket" + +Run `gh issue view --comments`. + +## Wayfinding operations + +Used by `/wayfinder`. The **map** is a single issue with **child** issues as tickets. + +- **Map**: a single issue labelled `wayfinder:map`, holding the Notes / Decisions-so-far / Fog body. `gh issue create --label wayfinder:map`. +- **Child ticket**: an issue linked to the map as a GitHub sub-issue (`gh api` on the sub-issues endpoint). Where sub-issues aren't enabled, add the child to a task list in the map body and put `Part of #` at the top of the child body. Labels: `wayfinder:` (`research`/`prototype`/`grilling`/`task`). Once claimed, the ticket is assigned to the driving dev. +- **Blocking**: GitHub's **native issue dependencies** — the canonical, UI-visible representation. Add an edge with `gh api --method POST repos///issues//dependencies/blocked_by -F issue_id=`, where `` is the blocker's numeric **database id** (`gh api repos///issues/ --jq .id`, _not_ the `#number` or `node_id`). GitHub reports `issue_dependencies_summary.blocked_by` (open blockers only — the live gate). Where dependencies aren't available, fall back to a `Blocked by: #, #` line at the top of the child body. A ticket is unblocked when every blocker is closed. +- **Frontier query**: list the map's open children (`gh issue list --state open`, scoped to the map's sub-issues / task list), drop any with an open blocker (`issue_dependencies_summary.blocked_by > 0`, or an open issue in the `Blocked by` line) or an assignee; first in map order wins. +- **Claim**: `gh issue edit --add-assignee @me` — the session's first write. +- **Resolve**: `gh issue comment --body ""`, then `gh issue close `, then append a context pointer (gist + link) to the map's Decisions-so-far. diff --git a/docs/agents/triage-labels.md b/docs/agents/triage-labels.md new file mode 100644 index 0000000000..b716855d48 --- /dev/null +++ b/docs/agents/triage-labels.md @@ -0,0 +1,15 @@ +# Triage Labels + +The skills speak in terms of five canonical triage roles. This file maps those roles to the actual label strings used in this repo's issue tracker. + +| Label in mattpocock/skills | Label in our tracker | Meaning | +| -------------------------- | -------------------- | ---------------------------------------- | +| `needs-triage` | `needs-triage` | Maintainer needs to evaluate this issue | +| `needs-info` | `needs-info` | Waiting on reporter for more information | +| `ready-for-agent` | `ready-for-agent` | Fully specified, ready for an AFK agent | +| `ready-for-human` | `ready-for-human` | Requires human implementation | +| `wontfix` | `wontfix` | Will not be actioned | + +When a skill mentions a role (e.g. "apply the AFK-ready triage label"), use the corresponding label string from this table. + +Edit the right-hand column to match whatever vocabulary you actually use. diff --git a/structure/06_docs-and-release.md b/structure/06_docs-and-release.md index 6305785c38..b2f8e1015e 100644 --- a/structure/06_docs-and-release.md +++ b/structure/06_docs-and-release.md @@ -120,10 +120,11 @@ exists so the repository-shape source of truth does not omit the shape of its ow by maintainer-controlled promotion; `preview` carries the `x.y.z-preview.*` train. One documented exception: a stacked child PR may target another **open** PR's head branch as a review workflow, and is retargeted to `dev` once the parent lands or closes. -- Bun-native TypeScript on `dev` is the only runtime line. The former Go native-runtime experiment is - retired and archived, and no `go/` tree is tracked in this repository; a local `go/` directory is - untracked leftovers. If native code returns, the expectation is an incremental module landing on - `dev`, not a second full-runtime branch. +- Bun-native TypeScript on `dev` is the only runtime line today; it is being migrated to Go as an + incremental sidecar takeover per ADR-0008, ending in a single static Go binary. The former Go + native-runtime experiment (`dev2-go`) is retired and archived at `lidge-jun/opencodex-go-archive`; + a fresh `go/` tree is the target of new Go work, and the archive is reference material only, not a + fork. No second full-runtime branch is being reopened. - `devlog/` is a tracked directory in this repository — no submodule, no private mirror. Open units live in `devlog/_plan/`, closed units in `devlog/_fin/`, and external parity references in `devlog/_chase/` (the reference clones themselves are gitignored). From 4b8715a30918f42977b60a4b41fd1904f24efef5 Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Sat, 5 Sep 2026 23:36:50 +0800 Subject: [PATCH 002/165] =?UTF-8?q?feat(go):=20land=20the=20ADR-0008=20fir?= =?UTF-8?q?st=20increment=20=E2=80=94=20ocx-sidecar=20health=20seam=20+=20?= =?UTF-8?q?differential=20oracle?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement the first Go sidecar increment per ADR-0008 and devlog/_plan/260905_go_sidecar_takeover: a fresh in-tree Go module that serves exactly one read-only management route (GET /api/system/health) with byte-identical HTTP semantics to the in-process TypeScript handler, plus the differential oracle proving it. - go/: fresh module (module github.com/lidge-jun/opencodex/go) building the ocx-sidecar binary (CGO_ENABLED=0). The health payload struct field order and encoding/json number formatting are part of the byte contract with the Bun harness; key order, Content-Type, and compactness mirror jsonResponse. - src/server/go-sidecar.ts + go-sidecar-slot.ts: optional supervisor. The TS server spawns and supervises the child when OPENCODEX_GO_SIDECAR_BIN names a binary; readiness is a stdout handshake line. The core health route consults only a core-owned slot (AGENTS.md optional-subsystem pattern); the forwarder registers at activation and deregisters on stop or unexpected child exit, so a default install spawns nothing and every existing route is byte-identical. - tests/go-sidecar-parity.test.ts: boots the TS server with and without the sidecar and asserts the in-process handler and the Go sidecar agree on status, headers, and the normalised body, normalising exactly the declared volatile fields (pid, uptime). Skips with a visible reason when is absent; CI installs Go (the new job plus setup-go on the suite lanes). - .gitignore / tests/repo-hygiene.test.ts: reconcile the pre-ADR gitignore and hygiene guard that treated go/ as a retired, untrackable tree. go/ is tracked source again; go/bin build output stays ignored. - route-registry: annotate the Go-owned health route seam; the declared owner stays system-routes so registry reconciliation holds for the default install. - ci.yml: add the go/** scope to the shared CI allowlist, a dedicated job (build/vet/test + the differential oracle), and setup-go on the shard and macOS suite lanes; pin the allowlist sync in tests/ci-workflows.test.ts. --- .github/workflows/ci.yml | 64 ++++- .gitignore | 11 +- .../260905_go_sidecar_takeover/000_plan.md | 21 +- go/README.md | 45 +++ go/cmd/ocx-sidecar/main.go | 87 ++++++ go/go.mod | 3 + go/internal/sidecar/sidecar.go | 109 +++++++ go/internal/sidecar/sidecar_test.go | 160 +++++++++++ src/server/go-sidecar-slot.ts | 53 ++++ src/server/go-sidecar.ts | 266 ++++++++++++++++++ src/server/index.ts | 20 ++ src/server/management/route-registry.ts | 4 + src/server/management/system-routes.ts | 9 + tests/ci-workflows.test.ts | 3 +- tests/go-sidecar-parity.test.ts | 261 +++++++++++++++++ tests/repo-hygiene.test.ts | 27 +- 16 files changed, 1124 insertions(+), 19 deletions(-) create mode 100644 go/README.md create mode 100644 go/cmd/ocx-sidecar/main.go create mode 100644 go/go.mod create mode 100644 go/internal/sidecar/sidecar.go create mode 100644 go/internal/sidecar/sidecar_test.go create mode 100644 src/server/go-sidecar-slot.ts create mode 100644 src/server/go-sidecar.ts create mode 100644 tests/go-sidecar-parity.test.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3bb05cd81d..f7699f3a75 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,6 +31,7 @@ on: - "tests/**" - "scripts/**" - "gui/**" + - "go/**" - "assets/**" - ".gitattributes" - ".npmignore" @@ -179,6 +180,7 @@ jobs: - 'tests/**' - 'scripts/**' - 'gui/**' + - 'go/**' - 'assets/**' - '.gitattributes' - '.npmignore' @@ -282,6 +284,16 @@ jobs: - name: Setup project Bun uses: ./.github/actions/setup-project-bun + # The Go sidecar parity test (tests/go-sidecar-parity.test.ts) builds the + # sidecar with the local toolchain; whichever shard picks the file up runs + # the oracle only when `go` is on PATH. Keep it installed so the oracle + # never silently skips in the ordinary suite. + - name: Setup Go + uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5 + with: + go-version-file: go/go.mod + cache: false + # The GUI install is NOT optional here, however unrelated it looks to a # test shard. Several files under tests/ import JSX-bearing modules from # gui/src (ProviderRail and friends), and React is declared only in @@ -380,6 +392,48 @@ jobs: - name: Test api usage API run: bun test --isolate ./tests/api-usage.test.ts + # Go sidecar line (ADR-0008): the fresh in-tree Go module plus the differential + # oracle that proves the TS health handler and the ocx-sidecar agree on status, + # headers, and the normalised body for GET /api/system/health. Runs the Go + # toolchain gates (build/vet/test under go/) and then the Bun parity harness + # against the binary it builds. The module has no external dependencies, so + # module caching is disabled. + go: + name: go + needs: changes + if: github.event_name != 'pull_request' || needs.changes.outputs.ci == 'true' + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Checkout + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + with: + persist-credentials: false + + - name: Setup project Bun + uses: ./.github/actions/setup-project-bun + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Setup Go + uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5 + with: + go-version-file: go/go.mod + cache: false + + - name: Go build + run: cd go && go build ./... + + - name: Go vet + run: cd go && go vet ./... + + - name: Go test + run: cd go && go test ./... + + - name: Differential oracle + run: bun test --timeout 60000 tests/go-sidecar-parity.test.ts + # Everything that is not the suite: type safety, privacy, lint, build, smoke. # One runner, once per push. Splitting these across the shards would repeat a # fixed couple of minutes four times to save nothing. @@ -486,6 +540,14 @@ jobs: - name: Setup project Bun uses: ./.github/actions/setup-project-bun + # Same rationale as the Linux shards: this lane runs the whole suite, + # which includes the Go-sidecar differential oracle. + - name: Setup Go + uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5 + with: + go-version-file: go/go.mod + cache: false + - name: Install dependencies run: | bun install --frozen-lockfile @@ -805,7 +867,7 @@ jobs: # direct dependencies only, so a failing `select-windows-runner` would # otherwise reach this gate as nothing at all while its dependents report # `skipped` — which the gate is required to read as a deliberate skip. - needs: [changes, select-windows-runner, test, storage-policy, api-usage, gates, platform-macos, platform-windows, keyring-smoke, npm-global-smoke] + needs: [changes, select-windows-runner, test, storage-policy, api-usage, go, gates, platform-macos, platform-windows, keyring-smoke, npm-global-smoke] runs-on: ubuntu-latest timeout-minutes: 5 steps: diff --git a/.gitignore b/.gitignore index 19ab5348b8..e0982e1afb 100644 --- a/.gitignore +++ b/.gitignore @@ -59,9 +59,8 @@ devlog/**/security-advisory-draft* tests/.tmp-* .claude/ -# Retired Go native-runtime experiment. `go/` is not part of the build, the -# typecheck, or the test path, and nothing in `src/` imports it. A single file -# from it (go/internal/cli/config_parity.go) has now been committed by a broad -# `git add` three separate times and reached `dev` once — see -# tests/repo-hygiene.test.ts, which fails if any path here becomes tracked again. -go/ +# Go sidecar line (ADR-0008). `go/` is a tracked in-tree Go module under +# incremental takeover; only per-machine build output stays ignored so a stray +# `go build -o bin` cannot be committed. Nothing in `src/` imports `go/` at +# runtime — the TypeScript server spawns the built binary as a child process. +go/bin/ diff --git a/devlog/_plan/260905_go_sidecar_takeover/000_plan.md b/devlog/_plan/260905_go_sidecar_takeover/000_plan.md index fa2c93a56d..1090895a60 100644 --- a/devlog/_plan/260905_go_sidecar_takeover/000_plan.md +++ b/devlog/_plan/260905_go_sidecar_takeover/000_plan.md @@ -1,9 +1,28 @@ # Go sidecar takeover — first increment: fresh `go/` + one read-only route Date: 2026-09-05 -Status: planned +Status: implemented on `dev-go` (first increment landed per ADR-0008) ADR: [`docs/adr/0008-go-runtime-incremental-takeover.md`](../../docs/adr/0008-go-runtime-incremental-takeover.md) +## Delivery notes (dev-go) + +The open questions below were settled during implementation as follows: + +- **Nested `go/go.mod`** was used (module `github.com/lidge-jun/opencodex/go`), keeping the Go + tree self-contained under `go/`. +- **Supervision primitive**: `src/providers/openai-sidecar.ts` turned out to be credential + selection, not process supervision, so the implementer built the small supervisor in + `src/server/go-sidecar.ts` (spawn → ready-line handshake → register → child-exit + deregistration), reusing `direct-local-http.ts` for the forwarded request and + `optional-shutdown-hooks.ts` for teardown. +- Activation is env-gated (`OPENCODEX_GO_SIDECAR_BIN`), synchronous within the + `startServer` activation window, and default-OFF, so a default install is byte-identical to + a build without Go. +- The differential oracle lives in `tests/go-sidecar-parity.test.ts`; Go toolchain gates and + the oracle run in CI under the `go` job plus setup-go on the suite lanes. +- `.gitignore`/`tests/repo-hygiene.test.ts` were reconciled with ADR-0008: `go/` is tracked + source again, `go/bin/` build output stays ignored. + ## Objective Land the first Go increment on `dev` per ADR-0008: a fresh Go module under diff --git a/go/README.md b/go/README.md new file mode 100644 index 0000000000..98547fb852 --- /dev/null +++ b/go/README.md @@ -0,0 +1,45 @@ +# Go sidecar — ADR-0008 incremental takeover, first increment + +A fresh Go module under the nested `go/` tree (module +`github.com/lidge-jun/opencodex/go`), per +[`docs/adr/0008-go-runtime-incremental-takeover.md`](../docs/adr/0008-go-runtime-incremental-takeover.md) +and the first-increment plan in +[`devlog/_plan/260905_go_sidecar_takeover/`](../devlog/_plan/260905_go_sidecar_takeover/). + +Nothing here is copied from `archive/dev2-go`; that archive is reference +material only. This is a fresh codebase. + +## What lives here + +- `cmd/ocx-sidecar` — the sidecar binary. The TypeScript server spawns and + supervises it when the operator sets `OPENCODEX_GO_SIDECAR_BIN` to a built + binary path; it serves exactly one read-only management route, + `GET /api/system/health`, with byte-identical HTTP semantics to the + in-process TypeScript handler (see `src/server/go-sidecar.ts`). +- `internal/sidecar` — the handler plus its unit tests. The JSON key order and + number formatting in the health payload are part of the byte contract with + the Bun differential oracle (`tests/go-sidecar-parity.test.ts`). + +## Building + +```bash +go -C go build ./cmd/ocx-sidecar +go -C go vet ./... +go -C go test ./... +``` + +The differential harness builds the binary itself with `CGO_ENABLED=0`; CI +does the same (the `go` job in `.github/workflows/ci.yml`). The module has no +external dependencies, so there is no `go.sum`. + +## Wire contract with the TypeScript parent + +- The parent passes the installed package version in `OCX_SIDECAR_VERSION`; + the sidecar reports it verbatim as the `version` field (fallback `0.0.0`). +- After binding its loopback listener, the sidecar prints one readiness line on + stdout: `ocx-sidecar-ready http://127.0.0.1:`. The parent waits for + this line before forwarding the health route. +- `status`, `service`, and `version` must equal the TypeScript values; + `uptime` and `pid` are the sidecar's own process values. The differential + harness normalises exactly `pid` and `uptime` (declared in + `src/server/go-sidecar.ts`) and compares everything else byte-for-byte. diff --git a/go/cmd/ocx-sidecar/main.go b/go/cmd/ocx-sidecar/main.go new file mode 100644 index 0000000000..26bdce0f13 --- /dev/null +++ b/go/cmd/ocx-sidecar/main.go @@ -0,0 +1,87 @@ +// Command ocx-sidecar is the first Go-owned process of the incremental +// runtime takeover (ADR-0008). It is spawned and supervised by the +// TypeScript proxy front door and serves exactly one read-only management +// route, GET /api/system/health, with byte-identical HTTP semantics to the +// in-process TypeScript handler. See go/internal/sidecar for the contract. +// +// The binary is built CGO_ENABLED=0 and carries no state: everything it must +// echo from the parent (service label, package version) arrives through the +// environment at spawn time. +package main + +import ( + "context" + "fmt" + "net" + "net/http" + "os" + "os/signal" + "syscall" + "time" + + "github.com/lidge-jun/opencodex/go/internal/sidecar" +) + +func main() { + if err := run(); err != nil { + fmt.Fprintln(os.Stderr, "ocx-sidecar:", err) + os.Exit(1) + } +} + +func run() error { + // Bind first, announce second: the parent only starts forwarding once it + // has read the ready line, so announcing a listener that failed to bind + // would leave the front door waiting on a dead child. + listener, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + return fmt.Errorf("listen: %w", err) + } + addr := listener.Addr().String() + + cfg := sidecar.Config{ + Service: "opencodex", + Version: os.Getenv("OCX_SIDECAR_VERSION"), + StartedAt: time.Now(), + } + if cfg.Version == "" { + fmt.Fprintln(os.Stderr, "ocx-sidecar: warning: OCX_SIDECAR_VERSION is unset; reporting version 0.0.0") + } + + server := &http.Server{ + Handler: sidecar.NewHandler(cfg), + ReadHeaderTimeout: 5 * time.Second, + // Health responses are tiny; an idle client must not pin a socket. + IdleTimeout: 30 * time.Second, + } + + // The readiness contract: exactly one line on stdout, " http://:". + // The TypeScript supervisor (src/server/go-sidecar.ts) waits for this line before it + // registers the sidecar as the owner of GET /api/system/health. + fmt.Printf("%s http://%s\n", sidecar.ReadyLinePrefix, addr) + + serveErr := make(chan error, 1) + go func() { + serveErr <- server.Serve(listener) + }() + + // Terminate cleanly on SIGTERM/SIGINT so the supervising front door can + // stop the sidecar without a zombie or a half-written health response. + signals := make(chan os.Signal, 1) + signal.Notify(signals, syscall.SIGTERM, syscall.SIGINT) + select { + case sig := <-signals: + fmt.Fprintf(os.Stderr, "ocx-sidecar: received %s; shutting down\n", sig) + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + if err := server.Shutdown(ctx); err != nil { + return fmt.Errorf("shutdown: %w", err) + } + return nil + case err := <-serveErr: + if err == nil { + return nil + } + return fmt.Errorf("serve: %w", err) + } +} diff --git a/go/go.mod b/go/go.mod new file mode 100644 index 0000000000..5a6af1b9ca --- /dev/null +++ b/go/go.mod @@ -0,0 +1,3 @@ +module github.com/lidge-jun/opencodex/go + +go 1.24 diff --git a/go/internal/sidecar/sidecar.go b/go/internal/sidecar/sidecar.go new file mode 100644 index 0000000000..42eeab87d9 --- /dev/null +++ b/go/internal/sidecar/sidecar.go @@ -0,0 +1,109 @@ +// Package sidecar is the first Go-owned route of the incremental runtime +// takeover (ADR-0008, devlog/_plan/260905_go_sidecar_takeover). +// +// It owns exactly one management route -- GET /api/system/health -- and must +// reproduce the TypeScript handler's HTTP semantics byte-for-byte after the +// caller normalises the declared volatile fields (pid, uptime). The shape, +// key order, and number formatting of the JSON body are part of that contract: +// the Bun differential harness compares the normalised wire bodies, so this +// package's payload struct field order and its use of encoding/json (shortest +// round-trip number formatting, matching ECMAScript) are load-bearing, not +// cosmetic. +package sidecar + +import ( + "encoding/json" + "fmt" + "net/http" + "os" + "time" +) + +// Config carries the values the sidecar must echo from its TypeScript parent. +// Service and Version are labels owned by the parent process: they must equal +// the TS handler's values exactly, which is why the supervisor passes the +// installed package version at spawn time rather than letting the sidecar +// derive or guess it. Uptime and pid are NOT config: they are the sidecar's +// own process values, which is the divergence class the oracle exists to pin. +type Config struct { + // Service is the service label the TS handler reports ("opencodex"). + Service string + // Version is the installed package version the TS parent passes in + // OCX_SIDECAR_VERSION. Empty means the parent did not pass one; the + // sidecar then reports "0.0.0" exactly like the TS VERSION fallback, + // rather than inventing a value. + Version string + // StartedAt anchors the uptime clock; the handler reports + // time.Since(StartedAt).Seconds() at request time, mirroring + // process.uptime(). + StartedAt time.Time +} + +// healthPayload mirrors the JSON object literal in +// src/server/management/system-routes.ts. Field order is the byte contract: +// encoding/json emits struct fields in declaration order and the TS handler +// emits object keys in insertion order, and both orders must agree. +type healthPayload struct { + Status string `json:"status"` + Service string `json:"service"` + Version string `json:"version"` + Uptime float64 `json:"uptime"` + Pid int `json:"pid"` +} + +// NewHandler builds the sidecar's HTTP surface: exactly GET /api/system/health. +// Every other path or method falls through to Go's default ServeMux 404/405 so +// the sidecar never invents management surface of its own. The TypeScript front +// door only forwards the one route, so this handler never sees another request +// while the seam is wired correctly. +func NewHandler(cfg Config) http.Handler { + mux := http.NewServeMux() + mux.HandleFunc("GET /api/system/health", func(w http.ResponseWriter, r *http.Request) { + version := cfg.Version + if version == "" { + version = "0.0.0" + } + service := cfg.Service + if service == "" { + service = "opencodex" + } + startedAt := cfg.StartedAt + if startedAt.IsZero() { + startedAt = time.Now() + } + payload := healthPayload{ + Status: "ok", + Service: service, + Version: version, + Uptime: time.Since(startedAt).Seconds(), + Pid: os.Getpid(), + } + // Same header the TS handler produces via jsonResponse without a + // request/config pair: Content-Type application/json, nothing else. + // Header names are case-insensitive on the wire, but the harness + // compares them case-insensitively anyway. + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + // json.Marshal (not an Encoder): Encoder.Encode appends a trailing + // newline, and the TS handler emits none — the differential oracle + // compares bytes, so the newline would be a divergence. + raw, err := json.Marshal(payload) + if err != nil { + // Unreachable for this fixed struct, but never write a partial body. + fmt.Fprintf(os.Stderr, "ocx-sidecar: marshal health payload: %v\n", err) + return + } + if _, err := w.Write(raw); err != nil { + fmt.Fprintf(os.Stderr, "ocx-sidecar: write health payload: %v\n", err) + } + }) + return mux +} + +// ReadyLinePrefix is the stdout marker the TypeScript supervisor parses to +// learn the sidecar's bound address. The full line is +// " http://:"; see the supervisor's reader in +// src/server/go-sidecar.ts. Parsing is deliberately trivial (a space-separated +// http URL) so the parent never needs a JSON handshake to supervise a health +// sidecar. +const ReadyLinePrefix = "ocx-sidecar-ready" diff --git a/go/internal/sidecar/sidecar_test.go b/go/internal/sidecar/sidecar_test.go new file mode 100644 index 0000000000..bcf5b3ae7e --- /dev/null +++ b/go/internal/sidecar/sidecar_test.go @@ -0,0 +1,160 @@ +package sidecar + +import ( + "bytes" + "encoding/json" + "io" + "net/http" + "net/http/httptest" + "os" + "regexp" + "testing" + "time" +) + +// requestURLs each case against the handler and returns the raw response. +func do(t *testing.T, h http.Handler, method, path string) *http.Response { + t.Helper() + req := httptest.NewRequest(method, path, nil) + rec := httptest.NewRecorder() + h.ServeHTTP(rec, req) + return rec.Result() +} + +func TestHealthShape(t *testing.T) { + startedAt := time.Now().Add(-123 * time.Second) + h := NewHandler(Config{Service: "opencodex", Version: "2.42.0", StartedAt: startedAt}) + + resp := do(t, h, http.MethodGet, "/api/system/health") + if resp.StatusCode != http.StatusOK { + t.Fatalf("status = %d, want 200", resp.StatusCode) + } + if got := resp.Header.Get("Content-Type"); got != "application/json" { + t.Fatalf("Content-Type = %q, want application/json", got) + } + + raw, err := io.ReadAll(resp.Body) + if err != nil { + t.Fatal(err) + } + if len(raw) == 0 { + t.Fatal("empty body") + } + + // Key order is part of the byte contract with the TypeScript handler, so + // verify textual order directly against the raw body: each key must appear + // followed by its colon, and after the last key no earlier key may recur. + wantKeys := []string{"status", "service", "version", "uptime", "pid"} + probe := raw + for _, key := range wantKeys { + marker := []byte(`"` + key + `":`) + idx := bytes.Index(probe, marker) + if idx < 0 { + t.Fatalf("body missing key %q in order (raw %s)", key, raw) + } + probe = probe[idx+len(marker):] + } + for _, key := range wantKeys { + if bytes.Contains(probe, []byte(`"`+key+`":`)) { + t.Fatalf("body repeats key %q after pid (raw %s)", key, raw) + } + } + + var payload map[string]json.RawMessage + if err := json.Unmarshal(raw, &payload); err != nil { + t.Fatalf("body is not a JSON object: %v", err) + } + if string(payload["status"]) != `"ok"` { + t.Fatalf("status = %s, want \"ok\"", payload["status"]) + } + if string(payload["service"]) != `"opencodex"` { + t.Fatalf("service = %s, want \"opencodex\"", payload["service"]) + } + if string(payload["version"]) != `"2.42.0"` { + t.Fatalf("version = %s, want \"2.42.0\"", payload["version"]) + } + uptimeRaw := string(payload["uptime"]) + if !regexp.MustCompile(`^\d+(\.\d+)?([eE][+-]?\d+)?$`).MatchString(uptimeRaw) { + t.Fatalf("uptime = %q is not a JSON number", uptimeRaw) + } + if got := string(payload["pid"]); got == "" || got == "null" { + t.Fatalf("pid missing from body (raw %s)", raw) + } +} + +func TestHealthReportsOwnPidAndRoughlyCorrectUptime(t *testing.T) { + startedAt := time.Now().Add(-5 * time.Second) + h := NewHandler(Config{Service: "opencodex", Version: "9.9.9", StartedAt: startedAt}) + resp := do(t, h, http.MethodGet, "/api/system/health") + defer resp.Body.Close() + + var payload struct { + Status string `json:"status"` + Version string `json:"version"` + Uptime float64 `json:"uptime"` + Pid int `json:"pid"` + } + if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil { + t.Fatal(err) + } + if payload.Pid != os.Getpid() { + t.Fatalf("pid = %d, want own pid %d", payload.Pid, os.Getpid()) + } + // Uptime is anchored to StartedAt; allow the encode/decode round trip and + // a little scheduling slop, but it must be near the configured anchor + // rather than the process start time (which would be a much larger number + // under a long-lived test binary). + if payload.Uptime < 4 || payload.Uptime > 20 { + t.Fatalf("uptime = %v, want ~5s (anchored to StartedAt)", payload.Uptime) + } + if payload.Version != "9.9.9" { + t.Fatalf("version = %q, want the configured value", payload.Version) + } +} + +func TestHealthDefaults(t *testing.T) { + // Absent Service/Version must degrade to the TS-handler fallbacks rather + // than empty strings or a panic. + h := NewHandler(Config{}) + resp := do(t, h, http.MethodGet, "/api/system/health") + defer resp.Body.Close() + raw, err := io.ReadAll(resp.Body) + if err != nil { + t.Fatal(err) + } + for _, want := range []string{`"status":"ok"`, `"service":"opencodex"`, `"version":"0.0.0"`} { + if !bytes.Contains(raw, []byte(want)) { + t.Fatalf("body %s missing %s", raw, want) + } + } +} + +func TestHealthRouteSurfaceIsNarrow(t *testing.T) { + h := NewHandler(Config{Service: "opencodex", Version: "1.0.0"}) + + cases := []struct { + method string + path string + want int + }{ + {http.MethodPost, "/api/system/health", http.StatusMethodNotAllowed}, + {http.MethodGet, "/api/system/memory", http.StatusNotFound}, + {http.MethodGet, "/api/system", http.StatusNotFound}, + {http.MethodGet, "/healthz", http.StatusNotFound}, + {http.MethodGet, "/api/system/health/", http.StatusNotFound}, + {http.MethodGet, "/api/system/health?x=1", http.StatusOK}, + } + for _, tc := range cases { + resp := do(t, h, tc.method, tc.path) + resp.Body.Close() + if resp.StatusCode != tc.want { + t.Errorf("%s %s status = %d, want %d", tc.method, tc.path, resp.StatusCode, tc.want) + } + } +} + +func TestReadyLineConstant(t *testing.T) { + if ReadyLinePrefix != "ocx-sidecar-ready" { + t.Fatalf("ReadyLinePrefix = %q changed; the TS supervisor parses this exact token", ReadyLinePrefix) + } +} diff --git a/src/server/go-sidecar-slot.ts b/src/server/go-sidecar-slot.ts new file mode 100644 index 0000000000..b90f1f23e7 --- /dev/null +++ b/src/server/go-sidecar-slot.ts @@ -0,0 +1,53 @@ +/** + * Core-owned slot for the optional ADR-0008 Go sidecar health forwarder. + * + * The Go sidecar is an optional subsystem: a default install never spawns it + * and must execute none of its code. Core route files therefore hold only this + * slot — null on installs that never activate the sidecar — and the optional + * module (`src/server/go-sidecar.ts`) registers its forwarder here when it + * activates. This mirrors `passive-route-linker.ts`: an optional subsystem + * registers into a core-owned slot at activation instead of being imported. + * + * Contract for any registered forwarder: given the sidecar is attached, return + * the sidecar's `GET /api/system/health` Response; return null when there is + * nothing to forward (not attached, not reachable, or a supervision blip) so + * the caller serves the in-process handler exactly as before. The try/catch + * lives here so the guarantee belongs to the mechanism instead of being + * restated by every caller. + */ + +export type GoSidecarHealthForwarder = () => Promise; + +let forwarder: GoSidecarHealthForwarder | null = null; + +/** Install the forwarder. Returns a detach function. */ +export function setGoSidecarHealthForwarder(next: GoSidecarHealthForwarder): () => void { + forwarder = next; + return () => { + // Only detach our own registration: a later activation may have replaced it. + if (forwarder === next) forwarder = null; + }; +} + +/** + * Forward the health request to the attached Go sidecar, or null when no + * subsystem is active or the sidecar is unreachable. Never throws. + */ +export async function tryGoSidecarHealthForward(): Promise { + if (!forwarder) return null; + try { + return await forwarder(); + } catch { + return null; + } +} + +/** True when the optional subsystem has installed a forwarder. Test/diagnostic use. */ +export function hasGoSidecarHealthForwarder(): boolean { + return forwarder !== null; +} + +/** Test-only reset. */ +export function resetGoSidecarHealthForwarderForTests(): void { + forwarder = null; +} diff --git a/src/server/go-sidecar.ts b/src/server/go-sidecar.ts new file mode 100644 index 0000000000..9595884dea --- /dev/null +++ b/src/server/go-sidecar.ts @@ -0,0 +1,266 @@ +/** + * Optional Go sidecar supervision for the first incremental-takeover seam + * (ADR-0008, devlog/_plan/260905_go_sidecar_takeover). + * + * The TypeScript front door keeps owning `GET /api/system/health`; this module + * spawns, supervises, and forwards to a fresh Go binary (`go/cmd/ocx-sidecar`) + * that serves that one route with byte-identical HTTP semantics. The Go body + * carries the sidecar's own pid and uptime; status, service, and version equal + * the TypeScript values because the parent passes the package version at spawn + * time. + * + * Strictly optional and default-OFF. A process that never activates the + * sidecar (no `OPENCODEX_GO_SIDECAR_BIN`) executes no spawn and imports no + * runtime state beyond this module's own: core route files consult the + * core-owned slot in `./go-sidecar-slot.ts`, which is empty unless this module + * registered its forwarder at activation (the AGENTS.md optional-subsystem + * pattern, same shape as `passive-route-linker.ts`). + * + * The supervision model is deliberately small for a first increment: spawn, + * wait for the ready line, register the forwarder, and on an unexpected child + * exit deregister (falling back to the in-process TypeScript handler) and log. + * There is no respawn loop yet; that is a later increment once the seam has + * live evidence. + */ +import { existsSync } from "node:fs"; +import { directLocalHttpFetch } from "./direct-local-http"; +import { registerOptionalShutdownHook } from "../lib/optional-shutdown-hooks"; +import { setGoSidecarHealthForwarder } from "./go-sidecar-slot"; + +/** Environment variable naming the ocx-sidecar binary to spawn. */ +export const GO_SIDECAR_BIN_ENV = "OPENCODEX_GO_SIDECAR_BIN"; + +/** Environment variable the parent uses to pass the installed package version. */ +export const GO_SIDECAR_VERSION_ENV = "OCX_SIDECAR_VERSION"; + +/** Readiness marker the Go binary prints on stdout after binding. */ +export const GO_SIDECAR_READY_PREFIX = "ocx-sidecar-ready"; + +/** How long the front door waits for the child's ready line before giving up. */ +export const GO_SIDECAR_READY_TIMEOUT_MS = 10_000; + +/** + * The declared volatile field set of the health payload. Byte comparisons of + * the two implementations must normalise exactly these fields and nothing else, + * so the oracle cannot silently widen what "equal" means. Mirrored by the Bun + * differential harness in tests/go-sidecar-parity.test.ts. + */ +export const GO_SIDECAR_VOLATILE_FIELDS = ["pid", "uptime"] as const; + +type KillableChild = { + exited: Promise; + stdout: ReadableStream | null; + kill(): unknown; + unref?(): unknown; +}; + +let childProc: KillableChild | null = null; +let stopped = true; +let readyBaseUrl = ""; +let generation = 0; +let forwardDetach: (() => void) | null = null; + +/** Test-only reset so an isolated harness does not inherit a live child. */ +export function resetGoSidecarForTests(): void { + if (!stopped) stopSidecar(); +} + +/** Base URL of the attached sidecar, or null when none is attached and ready. */ +export function activeGoSidecarBaseUrl(): string | null { + return stopped ? null : readyBaseUrl || null; +} + +function parseReadyLine(line: string): string | null { + const trimmed = line.trim(); + const prefix = `${GO_SIDECAR_READY_PREFIX} http://`; + if (!trimmed.startsWith(prefix)) return null; + const url = trimmed.slice(prefix.length).trim(); + if (!url) return null; + try { + const parsed = new URL(`http://${url}`); + // Loopback-only by construction: the sidecar binds 127.0.0.1. Refuse any + // other host on the ready line instead of forwarding health to it. + if (parsed.hostname !== "127.0.0.1" && parsed.hostname !== "localhost") return null; + return `http://${parsed.host}`; + } catch { + return null; + } +} + +/** + * Read the child's stdout until its ready line or the deadline. Resolves to + * the parsed base URL, or null when the child exited or timed out without + * announcing. Called off the activation call stack, never inside the + * synchronous startServer window. + */ +async function waitForReadyLine(proc: KillableChild, timeoutMs: number): Promise { + if (!proc.stdout) return null; + const decoder = new TextDecoder(); + let buffer = ""; + const deadline = Date.now() + timeoutMs; + try { + const reader = proc.stdout.getReader(); + for (;;) { + const remaining = deadline - Date.now(); + if (remaining <= 0) { + await reader.cancel().catch(() => {}); + return null; + } + let timer: ReturnType | undefined; + const timedOut = new Promise<{ done: true; value: undefined }>(resolve => { + timer = setTimeout(() => resolve({ done: true, value: undefined }), Math.max(1, remaining)); + // The deadline must not keep the process alive if a read settles first. + if (typeof timer.unref === "function") timer.unref(); + }); + let outcome: { done: true; value?: undefined } | { done: false; value?: Uint8Array }; + try { + outcome = await Promise.race([ + reader.read(), + proc.exited.then(() => ({ done: true as const, value: undefined })), + timedOut, + ]); + } finally { + if (timer !== undefined) clearTimeout(timer); + } + if (outcome.done) { + await reader.cancel().catch(() => {}); + return null; + } + const chunk = outcome.value; + if (chunk === undefined || chunk.byteLength === 0) continue; + buffer += decoder.decode(chunk, { stream: true }); + const newline = buffer.indexOf("\n"); + if (newline >= 0) { + const parsed = parseReadyLine(buffer.slice(0, newline)); + if (parsed) { + // Detach; the rest of the child's stdout is not ours to interpret. + await reader.cancel().catch(() => {}); + return parsed; + } + } + } + } catch { + return null; + } +} + +function warnActivation(message: string): void { + console.warn(`[go-sidecar] ${message}; serving health in-process`); +} + +/** Forward one health request to a ready sidecar, or null on any failure. */ +async function forwardTo(baseUrl: string): Promise { + try { + const upstream = await directLocalHttpFetch(new URL("/api/system/health", baseUrl), { + headers: { accept: "application/json" }, + }); + if (!upstream.ok) return null; + return new Response(upstream.body, { + status: upstream.status, + headers: { "content-type": upstream.headers.get("content-type") ?? "application/json" }, + }); + } catch { + return null; + } +} + +function stopSidecar(): void { + if (stopped) return; + stopped = true; + if (forwardDetach) { + forwardDetach(); + forwardDetach = null; + } + const proc = childProc; + childProc = null; + readyBaseUrl = ""; + if (proc) { + try { + proc.kill(); + } catch { /* already exited */ } + try { + proc.unref?.(); + } catch { /* already gone */ } + } +} + +/** + * Activate the Go sidecar: spawn the binary named by OPENCODEX_GO_SIDECAR_BIN + * and start supervising it. Returns a stop handle when a child was spawned, or + * null when the env var is absent or the binary is unusable (a warned no-op, + * so a misconfigured opt-in never takes the proxy down at startup). + * + * Synchronous by contract: `startServer` calls this inside its synchronous + * activation window (see tests/core-lab-boundary.test.ts), so readiness is + * awaited off the call stack and the forwarder is registered into the + * core-owned slot (`go-sidecar-slot.ts`) once the ready line lands. Until then + * — and after any unexpected exit — the slot is empty and the in-process + * handler answers, byte-identically to a build without Go. + */ +export function activateGoSidecar(version: string): { stop(): void } | null { + const binary = process.env[GO_SIDECAR_BIN_ENV]?.trim(); + if (!binary) return null; + if (!existsSync(binary)) { + warnActivation(`${GO_SIDECAR_BIN_ENV}=${binary} does not exist`); + return null; + } + + let proc: KillableChild; + try { + proc = Bun.spawn([binary], { + stdin: "ignore", + stdout: "pipe", + stderr: "inherit", + windowsHide: true, + env: { + ...process.env, + [GO_SIDECAR_VERSION_ENV]: version, + }, + }); + } catch (error) { + warnActivation(`spawn failed: ${error instanceof Error ? error.message : String(error)}`); + return null; + } + + if (!stopped) stopSidecar(); + const myGeneration = ++generation; + stopped = false; + childProc = proc; + readyBaseUrl = ""; + // The optional-subsystem shutdown hook is keyed, so a re-activation replaces + // the previous registration instead of accumulating duplicate teardown. + registerOptionalShutdownHook("go-sidecar", stopSidecar); + + // Child-exit supervision: an unexpected exit deregisters the forwarder so + // the front door falls back to the in-process handler, and logs once. + const onExit = (reason: string): void => { + if (stopped || myGeneration !== generation) return; + stopSidecar(); + warnActivation(`sidecar ${reason}`); + }; + void proc.exited.then( + () => onExit("exited unexpectedly"), + () => onExit("terminated unexpectedly"), + ); + + void waitForReadyLine(proc, GO_SIDECAR_READY_TIMEOUT_MS).then(parsed => { + if (stopped || myGeneration !== generation) return; + if (!parsed) { + stopSidecar(); + warnActivation(`no ready line within ${GO_SIDECAR_READY_TIMEOUT_MS}ms`); + return; + } + if (stopped || myGeneration !== generation) return; + readyBaseUrl = parsed; + const baseUrl = parsed; + const detach = setGoSidecarHealthForwarder(() => forwardTo(baseUrl)); + if (stopped || myGeneration !== generation) { + detach(); + return; + } + forwardDetach = detach; + console.log(`[go-sidecar] ocx-sidecar attached at ${parsed}; GET /api/system/health served by Go`); + }); + + return { stop: stopSidecar }; +} diff --git a/src/server/index.ts b/src/server/index.ts index 17d662a020..1a70abf3a6 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -52,6 +52,7 @@ import { } from "../lib/app-owned-memory-stores"; import { acquireServerBackgroundLifecycle } from "./background-lifecycle"; import { activateLab, labActivationRequired } from "../lib/lab-activation"; +import { activateGoSidecar } from "./go-sidecar"; import { runOpenAiTierStartupMigration } from "../providers/openai-tier-startup"; import { runAlibabaRegionStartupMigration } from "../providers/alibaba-region-startup"; import { runModelRenameStartupMigration } from "../providers/model-rename-startup"; @@ -1017,6 +1018,10 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server | null = null; + // Set only when the optional Go sidecar activated (ADR-0008); consumed by the server.stop + // override below, which is built before activation runs. Null default keeps a process that + // never opted in from carrying any Go-sidecar state. + let goSidecarStop: (() => void) | null = null; try { backgroundLifecycle = acquireServerBackgroundLifecycle(applyPolicy); // External `ocx config set` / direct config.json edits run in other @@ -2349,6 +2354,11 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server { userCostOverlayReconciler?.stop(); }, + async () => { + // The Go sidecar (ADR-0008) is a child of this process; stopping the server must + // stop it too, or an opted-in proxy leaves a health sidecar behind. + goSidecarStop?.(); + }, ], async () => { try { @@ -2440,5 +2450,15 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server { "assets/**", "bin/**", "bun.lock", + "go/**", "gui/**", "package.json", "scripts/**", @@ -482,7 +483,7 @@ describe("GitHub Actions hardening", () => { expect(scopeIndex).toBeGreaterThan(filterIndex); const scopedCondition = "github.event_name != 'pull_request' || needs.changes.outputs.ci == 'true'"; - for (const jobName of ["test", "storage-policy", "gates", "platform-macos", "keyring-smoke"]) { + for (const jobName of ["test", "storage-policy", "go", "gates", "platform-macos", "keyring-smoke"]) { const job = ci.jobs?.[jobName] as { needs?: string; if?: string } | undefined; expect(`${jobName}:${job?.needs}`).toBe(`${jobName}:changes`); expect(`${jobName}:${job?.if}`).toBe(`${jobName}:${scopedCondition}`); diff --git a/tests/go-sidecar-parity.test.ts b/tests/go-sidecar-parity.test.ts new file mode 100644 index 0000000000..346ee7d387 --- /dev/null +++ b/tests/go-sidecar-parity.test.ts @@ -0,0 +1,261 @@ +import { describe, expect, test } from "bun:test"; +import { existsSync, mkdtempSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { SERVER_BUDGET_MS } from "./helpers/test-budget"; +import { saveConfig } from "../src/config"; +import { startServer } from "../src/server"; +import { VERSION } from "../src/server/management-api"; +import { + GO_SIDECAR_BIN_ENV, + GO_SIDECAR_VOLATILE_FIELDS, + activeGoSidecarBaseUrl, + resetGoSidecarForTests, +} from "../src/server/go-sidecar"; +import { removeTreeWithRetry } from "./helpers/remove-tree"; + +/** + * Differential oracle for the first ADR-0008 increment (devlog/_plan/260905_go_sidecar_takeover). + * + * The TS in-process health handler and the Go ocx-sidecar must agree on status, + * headers, and the normalised body for GET /api/system/health. "Normalised" is + * a DECLARED set — pid and uptime, the sidecar's own process values — and + * nothing else, so a later route cannot silently widen what parity means. The + * volatile field list lives in src/server/go-sidecar.ts and is mirrored here; + * both must stay in lockstep. + * + * The divergence class this pins is the one that sank dev2-go: Go runtime + * numbers rendered under JavaScript labels, or a shape that merely looks like + * the TS response. The assertion is byte identity of the wire bodies after the + * declared normalisation — a JSON re-parse would forgive key-order drift and + * float-formatting drift that a byte compare catches. + * + * The harness needs the Go toolchain to build the sidecar, and boots two real + * servers. Where `go` is unavailable (a contributor machine without Go), the + * whole file skips with a visible reason; CI installs Go (see the `go` job and + * the setup-go steps in .github/workflows/ci.yml), so the oracle runs there. + */ + +const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); +const previousBinEnv = process.env[GO_SIDECAR_BIN_ENV]; + +function goToolchainAvailable(): boolean { + const probe = Bun.spawnSync(["go", "version"], { stdout: "ignore", stderr: "ignore" }); + return probe.success; +} + +function buildSidecarBinary(): string { + const dir = mkdtempSync(join(tmpdir(), "ocx-go-sidecar-")); + const binPath = join(dir, process.platform === "win32" ? "ocx-sidecar.exe" : "ocx-sidecar"); + const build = Bun.spawnSync( + ["go", "build", "-o", binPath, "./cmd/ocx-sidecar"], + { + cwd: join(repoRoot, "go"), + env: { ...process.env, CGO_ENABLED: "0" }, + stdout: "pipe", + stderr: "pipe", + }, + ); + if (build.exitCode !== 0) { + throw new Error( + `go build ./cmd/ocx-sidecar failed (${build.exitCode}):\n${new TextDecoder().decode(build.stderr)}`, + ); + } + return binPath; +} + +const goAvailable = goToolchainAvailable(); +// Built once at load: the four shard lanes and the macOS lane share nothing, so +// each process that can run this file pays one small build. +const sidecarBinary: string | null = goAvailable ? buildSidecarBinary() : null; + +/** + * Normalise the declared volatile fields of a health body to a fixed token. + * Any other difference between two health bodies fails the byte comparison. + */ +function normaliseHealthBody(raw: string): string { + let out = raw; + for (const field of GO_SIDECAR_VOLATILE_FIELDS) { + out = out.replace( + new RegExp(`"${field}":-?\\d+(?:\\.\\d+)?(?:[eE][+-]?\\d+)?`, "g"), + `"${field}":0`, + ); + } + return out; +} + +interface HealthCapture { + status: number; + contentType: string | null; + body: string; + parsed: { status: string; service: string; version: string; uptime: number; pid: number }; +} + +async function captureHealth(server: { url: URL }, token: string): Promise { + const response = await fetch(new URL("/api/system/health", server.url), { + headers: { "x-opencodex-api-key": token }, + }); + const body = await response.text(); + return { + status: response.status, + contentType: response.headers.get("content-type"), + body, + parsed: JSON.parse(body) as HealthCapture["parsed"], + }; +} + +const previousHome = process.env.OPENCODEX_HOME; +const previousDataToken = process.env.OPENCODEX_API_AUTH_TOKEN; +const previousAdminToken = process.env.OPENCODEX_ADMIN_AUTH_TOKEN; +let testHome = ""; + +function configFixture() { + return { + port: 0, + hostname: "0.0.0.0", + defaultProvider: "test", + providers: { + test: { + adapter: "openai-chat", + baseUrl: "https://example.test/v1", + disabled: true, + models: ["gpt-test"], + }, + }, + }; +} + +function setUpFixture(): void { + testHome = mkdtempSync(join(tmpdir(), "ocx-go-sidecar-")); + process.env.OPENCODEX_HOME = testHome; + process.env.OPENCODEX_API_AUTH_TOKEN = "data-secret"; + process.env.OPENCODEX_ADMIN_AUTH_TOKEN = "admin-secret"; + saveConfig(configFixture()); +} + +function tearDownFixture(): void { + resetGoSidecarForTests(); + if (previousBinEnv === undefined) delete process.env[GO_SIDECAR_BIN_ENV]; + else process.env[GO_SIDECAR_BIN_ENV] = previousBinEnv; + if (previousHome === undefined) delete process.env.OPENCODEX_HOME; + else process.env.OPENCODEX_HOME = previousHome; + if (previousDataToken === undefined) delete process.env.OPENCODEX_API_AUTH_TOKEN; + else process.env.OPENCODEX_API_AUTH_TOKEN = previousDataToken; + if (previousAdminToken === undefined) delete process.env.OPENCODEX_ADMIN_AUTH_TOKEN; + else process.env.OPENCODEX_ADMIN_AUTH_TOKEN = previousAdminToken; + if (testHome) { + removeTreeWithRetry(testHome); + testHome = ""; + } +} + +async function waitFor(probe: () => T | null | undefined, timeoutMs: number): Promise { + const deadline = Date.now() + timeoutMs; + for (;;) { + const value = probe(); + if (value !== null && value !== undefined) return value; + if (Date.now() >= deadline) throw new Error(`condition not met within ${timeoutMs}ms`); + await Bun.sleep(50); + } +} + +function runFixtureTest(name: string, fn: (token: string) => Promise): void { + test( + name, + async () => { + setUpFixture(); + try { + await fn("admin-secret"); + } finally { + tearDownFixture(); + } + }, + SERVER_BUDGET_MS, + ); +} + +describe.skipIf(!goAvailable || sidecarBinary === null)("ocx-sidecar differential parity (ADR-0008)", () => { + test("the Go sidecar binary is buildable before the oracle runs", () => { + expect(sidecarBinary).toBeTruthy(); + expect(existsSync(sidecarBinary!)).toBe(true); + }); + + runFixtureTest("in-process handler and Go sidecar agree on status, headers, and normalised body", async (token) => { + // Server A: no sidecar attached — the route must answer exactly as a build + // that never heard of Go (zero behaviour change for the default install). + const serverA = startServer(0); + try { + const tsBody = await captureHealth(serverA, token); + expect(tsBody.status).toBe(200); + expect(tsBody.contentType).toBe("application/json"); + expect(tsBody.parsed).toMatchObject({ + status: "ok", + service: "opencodex", + version: VERSION, + }); + // The in-process handler reports the proxy's own pid and uptime. + expect(tsBody.parsed.pid).toBe(process.pid); + + // Server B: same config, sidecar attached via env. The front door must + // now forward GET /api/system/health to the Go child. + process.env[GO_SIDECAR_BIN_ENV] = sidecarBinary!; + const serverB = startServer(0); + try { + const sidecarUrl = await waitFor(() => activeGoSidecarBaseUrl(), 15_000); + const goBody = await captureHealth(serverB, token); + expect(goBody.status).toBe(200); + expect(goBody.contentType).toBe("application/json"); + expect(goBody.parsed).toMatchObject({ + status: "ok", + service: "opencodex", + version: VERSION, + }); + // The sidecar reports ITS OWN pid, not the proxy's: this is the seam. + expect(goBody.parsed.pid).not.toBe(process.pid); + expect(goBody.parsed.pid).toBeGreaterThan(0); + + // Byte parity after the declared normalisation: the oracle fails on + // drift rather than logging it. Raw bodies still differ (pid/uptime), + // so a vacuous equality is impossible. + expect(normaliseHealthBody(goBody.body)).toBe(normaliseHealthBody(tsBody.body)); + + // The front door must relay the Go bytes without alteration. The two + // requests land at different instants, so uptime (a volatile field) + // legitimately differs — normalise exactly the declared set, then + // require byte equality of everything the relay is allowed to touch. + const direct = await fetch(new URL("/api/system/health", sidecarUrl), { + headers: { accept: "application/json" }, + }); + expect(direct.status).toBe(200); + expect(normaliseHealthBody(await direct.text())).toBe(normaliseHealthBody(goBody.body)); + } finally { + await serverB.stop(true); + } + expect(activeGoSidecarBaseUrl()).toBeNull(); + } finally { + await serverA.stop(true); + } + }); + + runFixtureTest("a missing sidecar binary is a warned no-op, not a startup failure", async (token) => { + process.env[GO_SIDECAR_BIN_ENV] = join(testHome, "does-not-exist-ocx-sidecar"); + const server = startServer(0); + try { + const health = await captureHealth(server, token); + expect(health.status).toBe(200); + expect(health.parsed.pid).toBe(process.pid); + expect(health.parsed.version).toBe(VERSION); + expect(activeGoSidecarBaseUrl()).toBeNull(); + } finally { + await server.stop(true); + } + }); + + test("the declared volatile field set is exactly pid and uptime", () => { + // Keep the mirror honest: widening parity would silently tolerate drift in + // fields the TS handler owns (status/service/version), which is the exact + // divergence class the oracle exists to catch. + expect([...GO_SIDECAR_VOLATILE_FIELDS]).toEqual(["pid", "uptime"]); + }); +}); diff --git a/tests/repo-hygiene.test.ts b/tests/repo-hygiene.test.ts index c1de41184b..2aaf338631 100644 --- a/tests/repo-hygiene.test.ts +++ b/tests/repo-hygiene.test.ts @@ -20,14 +20,14 @@ const FORBIDDEN_TRACKED_DIRS = [".codexclaw", ".omo", ".claude", "node_modules", const FORBIDDEN_TRACKED_FILENAMES = [".DS_Store", "Thumbs.db"]; /** - * The retired Go native-runtime experiment. Nothing in `src/`, the build, the - * typecheck, or the test path reads from `go/`, so a tracked file there is always - * an accident — and this specific one is a repeat offender: `git add -A` pulled - * `go/internal/cli/config_parity.go` back into the index three times during the - * #820 campaign, and the third one rode a merge into `dev`. `.gitignore` cannot - * catch that on its own, because an already-tracked path ignores the rule. + * ADR-0008 reopened the Go runtime as an in-tree incremental takeover, so `go/` + * is now intentionally tracked source (the module plus the ocx-sidecar binary). + * What must stay OUT of the index is per-machine build output under `go/bin/` — + * a stray `go build -o bin` must never be committed. `.gitignore` carries that + * rule; the tests below assert it against the real index, because an + * already-tracked path ignores the ignore file. */ -const RETIRED_TRACKED_DIRS = ["go"]; +const GO_BUILD_OUTPUT_DIRS = ["go/bin"]; function trackedFiles(): string[] { const result = Bun.spawnSync(["git", "ls-files"], { cwd: repoRoot }); @@ -74,9 +74,16 @@ describe("repository hygiene", () => { expect(offenders).toEqual([]); }); - test("the retired Go runtime stays untracked", () => { + test("the ADR-0008 Go sidecar tree is tracked source", () => { + const goSources = trackedFiles().filter((path) => path.startsWith("go/")); + + expect(goSources.some((path) => path === "go/go.mod")).toBe(true); + expect(goSources.some((path) => path.endsWith(".go"))).toBe(true); + }); + + test("Go build output stays untracked", () => { const offenders = trackedFiles().filter((path) => - RETIRED_TRACKED_DIRS.some((dir) => path === dir || path.startsWith(`${dir}/`)), + GO_BUILD_OUTPUT_DIRS.some((dir) => path === dir || path.startsWith(`${dir}/`)), ); expect(offenders).toEqual([]); @@ -89,7 +96,7 @@ describe("repository hygiene", () => { expect(ignore).toContain(`${dir}/`); } - for (const dir of RETIRED_TRACKED_DIRS) { + for (const dir of GO_BUILD_OUTPUT_DIRS) { expect(ignore).toContain(`${dir}/`); } }); From 05c127563edecd2737e74d939971276bc7758c2e Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Sat, 5 Sep 2026 23:45:52 +0800 Subject: [PATCH 003/165] ci(go): force CGO_ENABLED=0 on the go job per the ADR-0008 static-build contract The plan states the sidecar is built static (CGO_ENABLED=0); the parity harness already sets it for its throwaway binary. Make the dedicated go job enforce the same flag so the CI artifact and the oracle build identically. --- .github/workflows/ci.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f7699f3a75..18d193048c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -404,6 +404,10 @@ jobs: if: github.event_name != 'pull_request' || needs.changes.outputs.ci == 'true' runs-on: ubuntu-latest timeout-minutes: 10 + env: + # The sidecar is built static per the ADR-0008 plan (CGO_ENABLED=0); the + # parity harness sets the same flag when it builds a throwaway binary. + CGO_ENABLED: "0" steps: - name: Checkout uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 From 905182cf357ab0e5a1b286bab3507bfc93d5cb4e Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Sun, 6 Sep 2026 00:03:37 +0800 Subject: [PATCH 004/165] docs(go): record the Lab migrate-vs-cut owner decision (ticket #9) as migrate Ticket #9 on the fork board asks for an explicit, owner-approved migrate-or-cut decision for the Compatibility Lab with a cost-vs-value basis, recorded so later Lab tickets (#19 activation gate + provider slot in Go, #33 routes migration + differential) can reference it. The owner ratified MIGRATE on 2026-09-06. The record captures the evidence that framed the choice and why migrate wins: the Lab is a shipped, GUI-exposed capability whose evidence provider feeds the synchronous routing assembler through the core-owned provider slot, so cutting would remove a routing control and change behavior for gated profiles rather than just retire an experiment. The 2026-07 decoupling campaign left the activation gate, passive-route linker, provider slot, and shutdown hooks as first-class seams, so the port is bounded; Lab stays increment 6, independently gated (spec #6), and cannot block increments 2-5 or the flip. The doc keeps the cut alternative alive as a reopenable revisit point with fresh adoption evidence before the Lab batch starts, per spec #6's estimate-cost-against-usage requirement. --- .../010_lab_migrate_vs_cut_decision.md | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 devlog/_plan/260905_go_sidecar_takeover/010_lab_migrate_vs_cut_decision.md diff --git a/devlog/_plan/260905_go_sidecar_takeover/010_lab_migrate_vs_cut_decision.md b/devlog/_plan/260905_go_sidecar_takeover/010_lab_migrate_vs_cut_decision.md new file mode 100644 index 0000000000..cccb2e23aa --- /dev/null +++ b/devlog/_plan/260905_go_sidecar_takeover/010_lab_migrate_vs_cut_decision.md @@ -0,0 +1,94 @@ +# 010 — Decision: migrate the Compatibility Lab, do not cut it + +Unit: `260905_go_sidecar_takeover` +Date: 2026-09-06 +Status: **decided — migrate** (owner, recorded on [ticket #9](https://github.com/waxiangzi/opencodex/issues/9)) +Parent spec: [#6 — Migrate the Compatibility Lab to Go (ADR-0008 increment 6)](https://github.com/waxiangzi/opencodex/issues/6) +ADR: [`docs/adr/0008-go-runtime-incremental-takeover.md`](../../../docs/adr/0008-go-runtime-incremental-takeover.md) + +## Decision + +The Compatibility Lab is **migrated to Go**, not cut. It remains the last surface to move +(ADR-0008 increment 6 per spec #6) and stays behind the same ownership seam, reproducing +its opt-in activation gate and provider slot with byte-identical behavior. Cutting is +recorded as an owner-rejected alternative: the Lab is **not** removed from the single +binary, and no discontinuation documentation will be written. + +Ticket #9 asked for an explicit migrate-or-cut decision with a cost-vs-value basis so the +choice is made on evidence rather than defaulted. The evidence is summarized below; the +owner weighed the same evidence and chose migrate. + +## Cost-vs-value basis + +### Cost (acknowledged, and why it does not decide the outcome) + +- `src/lab/` is 117 TypeScript files / ~21.4k LOC of production code, with ~14.5k LOC of + tests across 58 test files — the largest opt-in subsystem in the tree. +- It is not a thin route layer: SQLite-backed projection/ledger/event stores, a secure + artifact store, a signed community registry with origin/revocation handling, conformance + suites, a live sandbox runner with MCP loopback and credential leases, an automation + scheduler, and a query layer all sit behind the public routes. +- A Go port must reproduce the synchronous, gap-free activation guarantee and the + core-owned slot contract ([#19](https://github.com/waxiangzi/opencodex/issues/19)), + then prove byte-identical route behavior under the differential oracle + ([#33](https://github.com/waxiangzi/opencodex/issues/33)). +- Migrating is strictly more work than cutting. That alone was never the question: the + question is whether the Lab is a shipped capability worth keeping. + +### Value (why migrate wins) + +- **The Lab is a shipped, GUI-exposed capability, not an experiment.** The dashboard + exposes a routing control that requires compatibility evidence + (`routing.compatibility.enabled`, "require evidence"), a Compatibility Matrix view, a + Lab section, and i18n strings in several locales. Cutting the Lab would remove that + control and quietly change routing for every install whose profile is gated on + compatibility evidence. ADR-0008 states the single-binary endpoint cannot quietly drop + a documented opt-in surface; the same principle applies before the flip. +- **Routing depends on it.** The compatibility evidence provider feeds the synchronous + routing assembler (`routeModelInternal`) through the core-owned provider slot. A cut is + not a UI cleanup — it deletes an evidence source the policy path already consults for + gated profiles. +- **The seams make migration bounded.** The 2026-07 decoupling campaign + ([`devlog/_fin/260814_lab_core_decoupling/`](../../_fin/260814_lab_core_decoupling/)) + was expensive precisely because the Lab had leaked into the core import graph. Today the + activation gate, the passive-route linker, the provider slot, and the optional shutdown + hooks are first-class core seams, and the boundary is machine-enforced + (`tests/core-lab-boundary.test.ts`). Porting into those same seams is mechanical where + it was previously architectural. +- **Independent gating neutralizes the schedule risk.** Lab is increment 6, gated on its + own terms (spec #6): it cannot block the management read/write surfaces, the hot path, + the CLI, or the flip (#7), which only needs the Lab batch in a terminal state. +- **Cut still costs.** Cutting would require its own release-note and docs work + (spec #6 US6), a GUI/i18n removal sweep, and a deprecation window for existing users — + real work with a permanently lost capability at the end. + +## Scope for later Lab tickets + +This decision sets the direction for the Lab batch under spec #6: + +- [#19 — Lab activation gate + provider-slot in Go](https://github.com/waxiangzi/opencodex/issues/19): + build, not re-scope. Reproduce the opt-in activation gate and the provider-slot seam in + Go so the "one provider, no Lab" user still executes no Lab code. +- [#33 — Lab routes migration + differential](https://github.com/waxiangzi/opencodex/issues/33): + the migrate branch applies — Lab routes go Go-owned and differential-green. No cut + documentation is to be written. +- The TypeScript Lab remains the operating surface until its batch; no Lab code changes in + increments 1–5 and no user-visible change before increment 6. + +## Revisit + +The decision is recorded now so later Lab tickets have a stable reference, but it is not +permanent. Spec #6 requires the porting cost to be estimated against usage before the Lab +batch commits. If adoption evidence collected at that point shows the Lab is effectively +unused, the owner may reopen [ticket #9](https://github.com/waxiangzi/opencodex/issues/9) +and flip this record to cut — the flip (#7) depends only on the Lab batch reaching a +terminal state, so revisiting before the batch starts costs nothing. + +## References + +- ADR-0008 (go runtime incremental takeover) — Lab "migrates last", explicit cut candidate. +- Spec #6 (ADR-0008 increment 6) — Lab migrate/cut framing and independent gating. +- Tickets #9 (this decision), #19 (activation gate + provider slot in Go), #33 (routes + migration + differential). +- `devlog/_fin/260814_lab_core_decoupling/` — why the Lab is seam-gated today. +- `tests/core-lab-boundary.test.ts` — the machine-enforced core/Lab boundary. From 6a9b621c1461fc6e4e4cab12f6a6262642b555e4 Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Sun, 6 Sep 2026 00:27:44 +0800 Subject: [PATCH 005/165] =?UTF-8?q?feat(go):=20ticket=20#14=20=E2=80=94=20?= =?UTF-8?q?typed=20read/write=20ownership=20+=20one=20registry-driven=20fo?= =?UTF-8?q?rwarding=20branch?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Walk the ADR-0008 critical path #8 -> #10||#11 -> #12 -> #13 -> #14. Increment 1 already sat on dev-go; this run closed its remaining acceptance gaps and delivered #14 (2.1), the first increment-2 ticket, per devlog/_plan/260905_go_sidecar_takeover/020_ownership_plumbing.md. - route-registry: ManagementRoute is now a discriminated union so the `go` ownership marker can only sit on a read route (mutates: false) — the write arm refuses it at compile time. The health row flips its typed marker with the per-route volatile declaration (pid, uptime); GO_OWNED_MANAGEMENT_ROUTES is the derived migrated surface and findGoOwnedManagementRoute the dispatch lookup. - management-api: a single forwarding branch at the head of handleManagementAPI serves declared Go-owned routes from the attached ocx-sidecar (response relayed verbatim) and falls through to the in-process chain for everything else and every supervision state. The bespoke health forwarder consult is gone from system-routes.ts, whose in-process handler is now purely the fallback and differential oracle. - go-sidecar-slot / go-sidecar: the core-owned slot generalizes from a health-only forwarder to a route forwarder; the supervisor registers it at activation exactly as before. - parity harness: volatile normalisation now reads the route's declared go.volatileFields from the registry (single source, no mirrored constant), and gains a #11 crash-oracle — kill the sidecar, assert the forwarder deregisters and the next health response flips back to the proxy's own pid. - tests/go-ownership-plumbing.test.ts: registry invariants (writes cannot be Go-owned; volatile declared per route; migrated surface pinned) and dispatch behaviour under a fake forwarder, with no Go toolchain required. Verified: bun run typecheck; go build/vet/test under go/; focused suites; full Bun test suite green except the pre-existing release-version-line failure on this branch (package.json 2.42.0 equals the released tag while HEAD is not the tagged commit — present without this diff). --- go/README.md | 21 +- src/server/go-sidecar-slot.ts | 50 +++-- src/server/go-sidecar.ts | 42 ++-- src/server/management-api.ts | 27 +++ src/server/management/route-registry.ts | 104 ++++++++- src/server/management/system-routes.ts | 14 +- tests/go-ownership-plumbing.test.ts | 282 ++++++++++++++++++++++++ tests/go-sidecar-parity.test.ts | 85 +++++-- 8 files changed, 537 insertions(+), 88 deletions(-) create mode 100644 tests/go-ownership-plumbing.test.ts diff --git a/go/README.md b/go/README.md index 98547fb852..fbc3c914f0 100644 --- a/go/README.md +++ b/go/README.md @@ -13,11 +13,14 @@ material only. This is a fresh codebase. - `cmd/ocx-sidecar` — the sidecar binary. The TypeScript server spawns and supervises it when the operator sets `OPENCODEX_GO_SIDECAR_BIN` to a built - binary path; it serves exactly one read-only management route, - `GET /api/system/health`, with byte-identical HTTP semantics to the - in-process TypeScript handler (see `src/server/go-sidecar.ts`). + binary path; it serves the declared Go-owned management read routes with + byte-identical HTTP semantics to the in-process TypeScript handlers. Which + routes are Go-owned is DATA, not code: the ownership markers (and each + route's volatile-field declaration) live in + `src/server/management/route-registry.ts`, and the single forwarding branch + in `src/server/management-api.ts` reads them before asking the sidecar. - `internal/sidecar` — the handler plus its unit tests. The JSON key order and - number formatting in the health payload are part of the byte contract with + number formatting of the health payload are part of the byte contract with the Bun differential oracle (`tests/go-sidecar-parity.test.ts`). ## Building @@ -38,8 +41,8 @@ external dependencies, so there is no `go.sum`. the sidecar reports it verbatim as the `version` field (fallback `0.0.0`). - After binding its loopback listener, the sidecar prints one readiness line on stdout: `ocx-sidecar-ready http://127.0.0.1:`. The parent waits for - this line before forwarding the health route. -- `status`, `service`, and `version` must equal the TypeScript values; - `uptime` and `pid` are the sidecar's own process values. The differential - harness normalises exactly `pid` and `uptime` (declared in - `src/server/go-sidecar.ts`) and compares everything else byte-for-byte. + this line before registering the route forwarder. +- The migrated route's declared volatile fields (today: `pid`, `uptime` for + `GET /api/system/health`) are normalised by the differential oracle and + nothing else is: a later route cannot silently widen what parity means. The + declaration lives with the route in `route-registry.ts`, not here. diff --git a/src/server/go-sidecar-slot.ts b/src/server/go-sidecar-slot.ts index b90f1f23e7..a4846fd21c 100644 --- a/src/server/go-sidecar-slot.ts +++ b/src/server/go-sidecar-slot.ts @@ -1,27 +1,35 @@ /** - * Core-owned slot for the optional ADR-0008 Go sidecar health forwarder. + * Core-owned slot for the optional ADR-0008 Go sidecar route forwarder. * * The Go sidecar is an optional subsystem: a default install never spawns it - * and must execute none of its code. Core route files therefore hold only this - * slot — null on installs that never activate the sidecar — and the optional - * module (`src/server/go-sidecar.ts`) registers its forwarder here when it - * activates. This mirrors `passive-route-linker.ts`: an optional subsystem - * registers into a core-owned slot at activation instead of being imported. + * and must execute none of its code. Core route dispatch therefore holds only + * this slot — empty on installs that never activate the sidecar — and the + * optional module (`src/server/go-sidecar.ts`) registers its forwarder here + * when it activates. This mirrors `passive-route-linker.ts`: an optional + * subsystem registers into a core-owned slot at activation instead of being + * imported. * - * Contract for any registered forwarder: given the sidecar is attached, return - * the sidecar's `GET /api/system/health` Response; return null when there is - * nothing to forward (not attached, not reachable, or a supervision blip) so - * the caller serves the in-process handler exactly as before. The try/catch - * lives here so the guarantee belongs to the mechanism instead of being - * restated by every caller. + * Contract for any registered forwarder: given the sidecar is attached, relay + * the request (`method` + `pathAndSearch`) to the sidecar's loopback listener + * and return its Response; return null when there is nothing to forward (not + * attached, not reachable, a supervision blip, or a non-2xx upstream) so the + * caller serves the in-process handler exactly as before. The try/catch lives + * here so the guarantee belongs to the mechanism instead of being restated by + * every caller. + * + * Dispatch decides WHICH requests reach this slot: `management-api.ts` first + * looks up the route in the declared Go-owned surface (`route-registry.ts`) and + * only then asks the slot to forward, so the slot never invents surface of its + * own and a misconfigured opt-in cannot hijack a route that is not declared + * Go-owned. */ -export type GoSidecarHealthForwarder = () => Promise; +export type GoOwnedRouteForwarder = (method: string, pathAndSearch: string) => Promise; -let forwarder: GoSidecarHealthForwarder | null = null; +let forwarder: GoOwnedRouteForwarder | null = null; /** Install the forwarder. Returns a detach function. */ -export function setGoSidecarHealthForwarder(next: GoSidecarHealthForwarder): () => void { +export function setGoOwnedRouteForwarder(next: GoOwnedRouteForwarder): () => void { forwarder = next; return () => { // Only detach our own registration: a later activation may have replaced it. @@ -30,24 +38,24 @@ export function setGoSidecarHealthForwarder(next: GoSidecarHealthForwarder): () } /** - * Forward the health request to the attached Go sidecar, or null when no - * subsystem is active or the sidecar is unreachable. Never throws. + * Forward one declared Go-owned request to the attached sidecar, or null when + * no subsystem is active or the sidecar is unreachable. Never throws. */ -export async function tryGoSidecarHealthForward(): Promise { +export async function tryForwardGoOwnedRoute(method: string, pathAndSearch: string): Promise { if (!forwarder) return null; try { - return await forwarder(); + return await forwarder(method, pathAndSearch); } catch { return null; } } /** True when the optional subsystem has installed a forwarder. Test/diagnostic use. */ -export function hasGoSidecarHealthForwarder(): boolean { +export function hasGoOwnedRouteForwarder(): boolean { return forwarder !== null; } /** Test-only reset. */ -export function resetGoSidecarHealthForwarderForTests(): void { +export function resetGoOwnedRouteForwarderForTests(): void { forwarder = null; } diff --git a/src/server/go-sidecar.ts b/src/server/go-sidecar.ts index 9595884dea..10af379a51 100644 --- a/src/server/go-sidecar.ts +++ b/src/server/go-sidecar.ts @@ -2,12 +2,14 @@ * Optional Go sidecar supervision for the first incremental-takeover seam * (ADR-0008, devlog/_plan/260905_go_sidecar_takeover). * - * The TypeScript front door keeps owning `GET /api/system/health`; this module - * spawns, supervises, and forwards to a fresh Go binary (`go/cmd/ocx-sidecar`) - * that serves that one route with byte-identical HTTP semantics. The Go body - * carries the sidecar's own pid and uptime; status, service, and version equal - * the TypeScript values because the parent passes the package version at spawn - * time. + * The TypeScript front door keeps owning dispatch for every management route; + * this module spawns, supervises, and forwards declared Go-owned management routes to a fresh Go + * binary (`go/cmd/ocx-sidecar`) that serves them with byte-identical HTTP semantics. + * Which routes are forwarded is DATA, not code: the ownership markers live in + * `src/server/management/route-registry.ts`, and `management-api.ts` consults them + * before asking this module's forwarder. The Go body carries the sidecar's own pid and + * uptime; status, service, and version equal the TypeScript values because the parent + * passes the package version at spawn time. * * Strictly optional and default-OFF. A process that never activates the * sidecar (no `OPENCODEX_GO_SIDECAR_BIN`) executes no spawn and imports no @@ -16,7 +18,7 @@ * registered its forwarder at activation (the AGENTS.md optional-subsystem * pattern, same shape as `passive-route-linker.ts`). * - * The supervision model is deliberately small for a first increment: spawn, + * The supervision model is deliberately small for the first increments: spawn, * wait for the ready line, register the forwarder, and on an unexpected child * exit deregister (falling back to the in-process TypeScript handler) and log. * There is no respawn loop yet; that is a later increment once the seam has @@ -25,7 +27,7 @@ import { existsSync } from "node:fs"; import { directLocalHttpFetch } from "./direct-local-http"; import { registerOptionalShutdownHook } from "../lib/optional-shutdown-hooks"; -import { setGoSidecarHealthForwarder } from "./go-sidecar-slot"; +import { setGoOwnedRouteForwarder } from "./go-sidecar-slot"; /** Environment variable naming the ocx-sidecar binary to spawn. */ export const GO_SIDECAR_BIN_ENV = "OPENCODEX_GO_SIDECAR_BIN"; @@ -39,14 +41,6 @@ export const GO_SIDECAR_READY_PREFIX = "ocx-sidecar-ready"; /** How long the front door waits for the child's ready line before giving up. */ export const GO_SIDECAR_READY_TIMEOUT_MS = 10_000; -/** - * The declared volatile field set of the health payload. Byte comparisons of - * the two implementations must normalise exactly these fields and nothing else, - * so the oracle cannot silently widen what "equal" means. Mirrored by the Bun - * differential harness in tests/go-sidecar-parity.test.ts. - */ -export const GO_SIDECAR_VOLATILE_FIELDS = ["pid", "uptime"] as const; - type KillableChild = { exited: Promise; stdout: ReadableStream | null; @@ -79,7 +73,7 @@ function parseReadyLine(line: string): string | null { try { const parsed = new URL(`http://${url}`); // Loopback-only by construction: the sidecar binds 127.0.0.1. Refuse any - // other host on the ready line instead of forwarding health to it. + // other host on the ready line instead of forwarding management reads to it. if (parsed.hostname !== "127.0.0.1" && parsed.hostname !== "localhost") return null; return `http://${parsed.host}`; } catch { @@ -148,13 +142,17 @@ function warnActivation(message: string): void { console.warn(`[go-sidecar] ${message}; serving health in-process`); } -/** Forward one health request to a ready sidecar, or null on any failure. */ -async function forwardTo(baseUrl: string): Promise { +/** Forward one declared Go-owned route request to a ready sidecar, or null on any failure. */ +async function forwardTo(baseUrl: string, method: string, pathAndSearch: string): Promise { try { - const upstream = await directLocalHttpFetch(new URL("/api/system/health", baseUrl), { + const upstream = await directLocalHttpFetch(new URL(pathAndSearch, baseUrl), { + method, headers: { accept: "application/json" }, }); if (!upstream.ok) return null; + // Relay the sidecar's response with the in-process handler's header shape: + // Content-Type plus the body verbatim. The management-API CORS wrapper adds + // the shared headers downstream exactly as it does for an in-process route. return new Response(upstream.body, { status: upstream.status, headers: { "content-type": upstream.headers.get("content-type") ?? "application/json" }, @@ -253,13 +251,13 @@ export function activateGoSidecar(version: string): { stop(): void } | null { if (stopped || myGeneration !== generation) return; readyBaseUrl = parsed; const baseUrl = parsed; - const detach = setGoSidecarHealthForwarder(() => forwardTo(baseUrl)); + const detach = setGoOwnedRouteForwarder((method, pathAndSearch) => forwardTo(baseUrl, method, pathAndSearch)); if (stopped || myGeneration !== generation) { detach(); return; } forwardDetach = detach; - console.log(`[go-sidecar] ocx-sidecar attached at ${parsed}; GET /api/system/health served by Go`); + console.log(`[go-sidecar] ocx-sidecar attached at ${parsed}; declared Go-owned routes served by Go`); }); return { stop: stopSidecar }; diff --git a/src/server/management-api.ts b/src/server/management-api.ts index 57830feb26..dc4ce4ba0f 100644 --- a/src/server/management-api.ts +++ b/src/server/management-api.ts @@ -82,6 +82,8 @@ import type { CatalogDisposition, ConvergeCodex } from "../codex/convergence-typ import { normalizeCatalogDisposition } from "../codex/catalog-refresh-status"; import { managementBodyTooLargeResponse } from "./management/body"; import { handleSessionRoutes } from "./management/session-routes"; +import { findGoOwnedManagementRoute, type HttpMethod } from "./management/route-registry"; +import { tryForwardGoOwnedRoute } from "./go-sidecar-slot"; // installed npm version instead of a stale hardcode. export const VERSION = (() => { @@ -132,6 +134,19 @@ async function handleLabRoutesOnDemand(ctx: ManagementContext): Promise { + const declared = findGoOwnedManagementRoute(req.method as HttpMethod, url.pathname); + if (!declared) return null; + return tryForwardGoOwnedRoute(req.method, `${url.pathname}${url.search}`); +} + export async function handleManagementAPI( req: Request, url: URL, @@ -151,6 +166,18 @@ export async function handleManagementAPI( return jsonResponse({ error: "request body too large" }, 413, req, config); } } + + // ADR-0008 single forwarding branch (ticket #14). When the management-route registry + // declares this exact request Go-owned and the optional ocx-sidecar is attached, answer it + // from the Go binary with the response relayed verbatim. Returns null when the request is + // not part of the declared Go-owned surface or when no sidecar is attached, so dispatch + // falls through to the in-process handlers below: a default install and a supervision blip + // behave byte-identically to a build without Go, and the in-process handler stays the + // differential oracle. One branch, driven by data — migrating another read route flips the + // `go` marker in route-registry.ts and never edits this dispatch. + const goOwnedResponse = await tryForwardDeclaredGoOwnedRoute(req, url); + if (goOwnedResponse) return goOwnedResponse; + async function convergeCodexCatalog(): Promise { let convergenceInvoked = false; let managementConvergeCodex: ConvergeCodex | undefined; diff --git a/src/server/management/route-registry.ts b/src/server/management/route-registry.ts index 566c1c5846..e405a3cfd5 100644 --- a/src/server/management/route-registry.ts +++ b/src/server/management/route-registry.ts @@ -14,6 +14,13 @@ * never import anything from `src/lab/`. The `module` field names the owning file as text * for exactly this reason. * + * The file is also the ADR-0008 Go-ownership ledger. A read route declares itself + * Go-owned by carrying a `go` marker (see `GoOwnedRouteDeclaration`); the discriminated + * union makes the marker impossible on a write route, `GO_OWNED_MANAGEMENT_ROUTES` is the + * derived migrated surface, and the single forwarding branch in `management-api.ts` reads + * that data. Migrating a read route is a marker flip here plus the Go handler and oracle + * coverage -- never a dispatch edit. + * * Reconciliation lives in `tests/management-route-registry.test.ts`, which resolves * `(method, path)` pairs from source and fails loudly on a route whose method it cannot * determine. Adding a route without declaring it here fails that test. @@ -65,16 +72,69 @@ export type NonLiteralMechanism = | "ends-with" | "regex"; -export interface ManagementRoute { +/** + * ADR-0008 Go-ownership declaration carried by a read route that the Go + * ocx-sidecar has taken over (ticket #14: the ownership marker is typed so a + * write route cannot carry it, and migration is a flip of this data marker). + * + * The declaration is per route, not per payload family, because the volatile + * fields belong to the route's response contract: when the differential oracle + * compares the in-process TypeScript response against the Go-served response it + * normalises exactly these top-level JSON keys and nothing else, so a later + * route can never silently widen what "equal" means. + */ +export interface GoOwnedRouteDeclaration { + /** + * Top-level JSON body keys of the route's response that may legitimately + * differ between the two implementations (process-specific values such as + * `pid` or `uptime`). Must be non-empty: a route that migrates while + * declaring "nothing may differ" would demand byte equality the harness + * cannot actually check, which is a vacuous pass. + */ + readonly volatileFields: readonly string[]; +} + +interface ManagementWriteRoute { + /** The route mutates state; it must stay in TypeScript until increment 3. */ + readonly mutates: true; + /** + * A write route cannot be declared Go-owned. The read/write split is a + * discriminated union so the type system refuses `go` on this arm: only a + * read route (`mutates: false`) may carry a GoOwnedRouteDeclaration. + */ + readonly go?: never; +} + +interface ManagementReadRoute { + readonly mutates: false; + /** + * When present, the route is declared Go-owned: the single forwarding branch + * in `management-api.ts` serves it from the attached ocx-sidecar, and the + * in-process handler below remains the fallback and the differential oracle. + */ + readonly go?: GoOwnedRouteDeclaration; +} + +/** + * Every reachable management route. The union splits reads from writes so the + * `go` ownership marker exists only on the read arm: a write route cannot be + * migrated early, at compile time, without a comment or a cast to explain it. + */ +export type ManagementRoute = { readonly method: HttpMethod; readonly path: string; /** Owning source file, repo-relative without the `src/` prefix or `.ts` suffix. */ readonly module: string; - readonly mutates: boolean; /** Set when the route is not recoverable from an equality scan of its own file. */ readonly mechanism?: NonLiteralMechanism; readonly exempt?: RouteExemption; -} +} & (ManagementWriteRoute | ManagementReadRoute); + +/** A read route that carries a Go-ownership declaration (ADR-0008). */ +export type GoOwnedManagementRoute = ManagementRoute & { + readonly mutates: false; + readonly go: GoOwnedRouteDeclaration; +}; /** Every reachable management route. */ @@ -296,11 +356,13 @@ export const MANAGEMENT_ROUTES: readonly ManagementRoute[] = [ { method: "POST", path: "/api/storage/codex-logs/repair", module: "server/management/storage-log-guard-routes", mutates: true }, { method: "POST", path: "/api/storage/codex-logs/unprotect", module: "server/management/storage-log-guard-routes", mutates: true }, // server/management/system-routes - // ADR-0008 seam: GET /api/system/health is answered by the Go ocx-sidecar when the optional - // sidecar is attached (spawned from OPENCODEX_GO_SIDECAR_BIN); system-routes stays the - // declared owner so this row reconciles for the default install, where the route is served - // in-process exactly as before. - { method: "GET", path: "/api/system/health", module: "server/management/system-routes", mutates: false }, + // ADR-0008 ownership: GET /api/system/health is Go-owned (ticket #14). The typed marker below + // is the migration act -- the single forwarding branch in management-api.ts serves this route + // from the ocx-sidecar when it is attached (spawned from OPENCODEX_GO_SIDECAR_BIN) and the + // in-process handler otherwise, so system-routes stays the declared owner for the default + // install. volatileFields is the oracle's normalisation contract: pid and uptime are the + // sidecar's own process values; everything else must be byte-identical. + { method: "GET", path: "/api/system/health", module: "server/management/system-routes", mutates: false, go: { volatileFields: ["pid", "uptime"] } }, { method: "GET", path: "/api/system/memory", module: "server/management/system-routes", mutates: false }, { method: "GET", path: "/api/system/windows-replace-retries", module: "server/management/system-routes", mutates: false }, { method: "POST", path: "/api/system/restart", module: "server/management/system-routes", mutates: true }, @@ -324,3 +386,29 @@ export const MANAGEMENT_ROUTES: readonly ManagementRoute[] = [ { method: "GET", path: "/api/lab/artifacts/{digest}", module: "server/management/lab-routes", mutates: false, mechanism: "regex", exempt: { reason: "local-transport", why: "ocx lab reads the same rows from the local SQLite projection; src/cli/lab.ts imports ../lab/query directly and never fetches /api/lab." } }, { method: "POST", path: "/api/lab/automation/runs/{id}/cancel", module: "server/management/lab-automation-routes", mutates: true, mechanism: "regex", exempt: { reason: "deferred-verb", why: "Lab automation run cancellation has no CLI verb yet. A local SQLite read cannot drive it, so local-transport does not apply.", owner: "wp7", ownerDoc: "devlog/_plan/260828_ocx_agentic_control/060_phase_gui_parity.md" } }, ]; + +/** + * The declared Go-owned surface (ADR-0008): exactly the read routes whose + * `go` marker is flipped. This is the single data source the forwarding branch + * and the differential oracle read, so migrating another read route is a + * marker flip here plus the matching Go handler and oracle coverage -- never a + * second dispatch edit. Read-only by construction: the write arm of the union + * cannot carry a `go` declaration, and this filter re-checks at runtime. + */ +export const GO_OWNED_MANAGEMENT_ROUTES: readonly GoOwnedManagementRoute[] = MANAGEMENT_ROUTES.filter( + (route): route is GoOwnedManagementRoute => route.mutates === false && route.go !== undefined, +); + +/** + * Dispatch lookup for the single forwarding branch: the declared Go-owned + * route for an exact (method, pathname), or undefined when the request is not + * part of the migrated surface. Non-literal (parameterised) routes cannot be + * Go-owned today, which is why the lookup is an exact-match scan over a list + * that is at most a few entries long. + */ +export function findGoOwnedManagementRoute( + method: HttpMethod, + pathname: string, +): GoOwnedManagementRoute | undefined { + return GO_OWNED_MANAGEMENT_ROUTES.find(route => route.method === method && route.path === pathname); +} diff --git a/src/server/management/system-routes.ts b/src/server/management/system-routes.ts index 336c424bcb..23668ac9f0 100644 --- a/src/server/management/system-routes.ts +++ b/src/server/management/system-routes.ts @@ -44,7 +44,6 @@ import type { } from "../../codex/app-server-restart-service"; import type { ManagementContext } from "./context"; import { acceptSystemRestart } from "./system-restart"; -import { tryGoSidecarHealthForward } from "../go-sidecar-slot"; const ENDPOINT_SAMPLE_LIMIT = 60; @@ -55,13 +54,12 @@ export async function handleSystemRoutes(ctx: ManagementContext): Promise Promise): void { + test( + name, + async () => { + setUpFixture(); + try { + await fn("admin-secret"); + } finally { + tearDownFixture(); + } + }, + SERVER_BUDGET_MS, + ); +} + +async function getJson(token: string, server: { url: URL }, pathname: string): Promise<{ status: number; pid?: number; body: string }> { + const response = await fetch(new URL(pathname, server.url), { + headers: { "x-opencodex-api-key": token }, + }); + const body = await response.text(); + const parsed = JSON.parse(body) as Record; + return { status: response.status, pid: typeof parsed.pid === "number" ? parsed.pid : undefined, body }; +} + +// --------------------------------------------------------------------------- +// 1. Registry invariants: the marker is typed read-only and volatile is declared. +// --------------------------------------------------------------------------- + +describe("ADR-0008 ownership markers are typed read/write (ticket #14)", () => { + test("the declared Go-owned surface is exactly GET /api/system/health today", () => { + // Pin the migrated set so an accidental marker flip on another read route + // fails here instead of silently changing what the proxy serves. Adding a + // real migration updates this list deliberately. + const keys = GO_OWNED_MANAGEMENT_ROUTES.map(r => `${r.method} ${r.path}`); + expect(keys).toEqual(["GET /api/system/health"]); + const health = GO_OWNED_MANAGEMENT_ROUTES[0]!; + expect(health.mutates).toBe(false); + expect(health.module).toBe("server/management/system-routes"); + expect(health.go.volatileFields).toEqual(["pid", "uptime"]); + }); + + test("no write route can be Go-owned: runtime re-check of the union's read-only arm", () => { + // The discriminated union in route-registry.ts already refuses `go` on a + // write route at compile time. This is the runtime belt-and-braces check: + // it re-derives the marker set from MANAGEMENT_ROUTES and compares it with + // the exported view, so a cast or an array-level workaround cannot drift. + const marked = MANAGEMENT_ROUTES.filter( + (r): r is (typeof GO_OWNED_MANAGEMENT_ROUTES)[number] => r.mutates === false && r.go !== undefined, + ); + expect(marked).toEqual(GO_OWNED_MANAGEMENT_ROUTES); + for (const route of marked) { + expect(route.mutates).toBe(false); + } + // And explicitly: no mutating route anywhere in the table declares Go ownership. + const writesWithGo = MANAGEMENT_ROUTES.filter(r => r.mutates === true && "go" in r); + expect(writesWithGo).toEqual([]); + }); + + test("every Go-owned route declares a non-empty, duplicate-free volatile set", () => { + expect(GO_OWNED_MANAGEMENT_ROUTES.length).toBeGreaterThan(0); + for (const route of GO_OWNED_MANAGEMENT_ROUTES) { + expect(route.go.volatileFields.length, `${route.method} ${route.path}`).toBeGreaterThan(0); + expect(new Set(route.go.volatileFields).size).toBe(route.go.volatileFields.length); + } + }); + + test("the dispatch lookup is exact on method and path, and sees only the declared surface", () => { + expect(findGoOwnedManagementRoute("GET", "/api/system/health")).toBe(GO_OWNED_MANAGEMENT_ROUTES[0]); + expect(findGoOwnedManagementRoute("POST", "/api/system/health")).toBeUndefined(); + expect(findGoOwnedManagementRoute("GET", "/api/system/health/")).toBeUndefined(); + expect(findGoOwnedManagementRoute("GET", "/api/system/memory")).toBeUndefined(); + expect(findGoOwnedManagementRoute("GET", "/api/config")).toBeUndefined(); + }); + + test("the forwarding branch in management-api.ts names no route of its own", () => { + // "Migrating a read route is a marker flip, not dispatch edits": the single + // branch must be driven by the registry lookup, so it may not mention any + // concrete management path. If a future migration adds a literal here, that + // is a dispatch edit and this test fails. (handleSystemRoutes itself is + // legitimately in the handler chain below the branch; the branch must not + // special-case it.) + const src = readFileSync(join(repoRoot, "src/server/management-api.ts"), "utf8"); + expect(src).toContain("findGoOwnedManagementRoute"); + expect(src).toContain("tryForwardGoOwnedRoute"); + expect(src).not.toContain('"/api/system/health"'); + expect(src).not.toContain('"/api/system/memory"'); + expect(src).not.toContain("tryForwardGoOwnedRoute(\"/"); + }); +}); + +// --------------------------------------------------------------------------- +// 2. Dispatch behaviour: one registry-driven branch forwards declared Go-owned +// routes and leaves everything else (and every supervision state) in-process. +// --------------------------------------------------------------------------- + +describe("single forwarding branch serves declared Go-owned routes (ticket #14)", () => { + runFixtureTest("a registered forwarder answers the declared route and nothing else", async (token) => { + const calls: string[] = []; + const fakeBody = JSON.stringify({ + status: "ok", + service: "opencodex", + version: "go-sidecar", + uptime: 1, + pid: 987654, + }); + const detach = setGoOwnedRouteForwarder(async (method, pathAndSearch) => { + calls.push(`${method} ${pathAndSearch}`); + return new Response(fakeBody, { + status: 200, + headers: { "content-type": "application/json" }, + }); + }); + try { + expect(hasGoOwnedRouteForwarder()).toBe(true); + const server = startServer(0); + try { + // The declared Go-owned route is served by the "sidecar" (the fake). + const health = await getJson(token, server, "/api/system/health"); + expect(health.status).toBe(200); + expect(health.body).toBe(fakeBody); + expect(health.pid).toBe(987654); + + // A read route that is NOT declared Go-owned stays in-process: the + // branch forwards only what the registry declares. + const memory = await getJson(token, server, "/api/system/memory"); + expect(memory.status).toBe(200); + expect(memory.pid).toBe(process.pid); + expect(calls).toEqual(["GET /api/system/health"]); + } finally { + await server.stop(true); + } + } finally { + detach(); + } + expect(hasGoOwnedRouteForwarder()).toBe(false); + }); + + runFixtureTest("a forwarder returning null falls back to the in-process handler", async (token) => { + // This is the supervision-blip contract: when the sidecar is attached but + // unreachable, the route answers from TypeScript exactly as without Go. + const detach = setGoOwnedRouteForwarder(async () => null); + try { + const server = startServer(0); + try { + const health = await getJson(token, server, "/api/system/health"); + expect(health.status).toBe(200); + expect(health.pid).toBe(process.pid); + } finally { + await server.stop(true); + } + } finally { + detach(); + } + }); + + runFixtureTest("a throwing forwarder is contained by the slot, never by dispatch", async (token) => { + const detach = setGoOwnedRouteForwarder(async () => { + throw new Error("sidecar exploded"); + }); + try { + const server = startServer(0); + try { + const health = await getJson(token, server, "/api/system/health"); + expect(health.status).toBe(200); + expect(health.pid).toBe(process.pid); + } finally { + await server.stop(true); + } + } finally { + detach(); + } + }); + + runFixtureTest("no forwarder installed: default install answers in-process, unchanged", async (token) => { + // Zero behaviour change for a build that never heard of Go. + expect(hasGoOwnedRouteForwarder()).toBe(false); + const server = startServer(0); + try { + const health = await getJson(token, server, "/api/system/health"); + expect(health.status).toBe(200); + expect(health.pid).toBe(process.pid); + } finally { + await server.stop(true); + } + }); +}); diff --git a/tests/go-sidecar-parity.test.ts b/tests/go-sidecar-parity.test.ts index 346ee7d387..ac88eecb5c 100644 --- a/tests/go-sidecar-parity.test.ts +++ b/tests/go-sidecar-parity.test.ts @@ -7,23 +7,22 @@ import { SERVER_BUDGET_MS } from "./helpers/test-budget"; import { saveConfig } from "../src/config"; import { startServer } from "../src/server"; import { VERSION } from "../src/server/management-api"; +import { GO_OWNED_MANAGEMENT_ROUTES } from "../src/server/management/route-registry"; import { GO_SIDECAR_BIN_ENV, - GO_SIDECAR_VOLATILE_FIELDS, activeGoSidecarBaseUrl, resetGoSidecarForTests, } from "../src/server/go-sidecar"; import { removeTreeWithRetry } from "./helpers/remove-tree"; /** - * Differential oracle for the first ADR-0008 increment (devlog/_plan/260905_go_sidecar_takeover). + * Differential oracle for the ADR-0008 Go sidecar (devlog/_plan/260905_go_sidecar_takeover). * - * The TS in-process health handler and the Go ocx-sidecar must agree on status, - * headers, and the normalised body for GET /api/system/health. "Normalised" is - * a DECLARED set — pid and uptime, the sidecar's own process values — and - * nothing else, so a later route cannot silently widen what parity means. The - * volatile field list lives in src/server/go-sidecar.ts and is mirrored here; - * both must stay in lockstep. + * The TS in-process handlers and the Go ocx-sidecar must agree on status, + * headers, and the normalised body for every declared Go-owned route. + * "Normalised" is the DECLARED per-route volatile set from + * src/server/management/route-registry.ts (the `go.volatileFields` marker) and + * nothing else, so a later route cannot silently widen what parity means. * * The divergence class this pins is the one that sank dev2-go: Go runtime * numbers rendered under JavaScript labels, or a shape that merely looks like @@ -71,12 +70,23 @@ const goAvailable = goToolchainAvailable(); const sidecarBinary: string | null = goAvailable ? buildSidecarBinary() : null; /** - * Normalise the declared volatile fields of a health body to a fixed token. - * Any other difference between two health bodies fails the byte comparison. + * The declared Go-owned health route (ADR-0008): the single migrated route the + * oracle must prove today. Reads are the only surface that can be Go-owned, so + * this must exist whenever the harness runs. */ -function normaliseHealthBody(raw: string): string { +const goOwnedHealth = GO_OWNED_MANAGEMENT_ROUTES.find( + route => route.method === "GET" && route.path === "/api/system/health", +); + +/** + * Normalise the declared volatile fields of a route body to a fixed token. + * Any other difference between two bodies fails the byte comparison. The field + * list comes from the route's `go.volatileFields` marker (route-registry.ts): + * the oracle normalises exactly the declared set and nothing else. + */ +function normaliseBody(raw: string, volatileFields: readonly string[]): string { let out = raw; - for (const field of GO_SIDECAR_VOLATILE_FIELDS) { + for (const field of volatileFields) { out = out.replace( new RegExp(`"${field}":-?\\d+(?:\\.\\d+)?(?:[eE][+-]?\\d+)?`, "g"), `"${field}":0`, @@ -85,6 +95,8 @@ function normaliseHealthBody(raw: string): string { return out; } +const healthVolatileFields = goOwnedHealth?.go.volatileFields ?? []; + interface HealthCapture { status: number; contentType: string | null; @@ -176,9 +188,13 @@ function runFixtureTest(name: string, fn: (token: string) => Promise): voi } describe.skipIf(!goAvailable || sidecarBinary === null)("ocx-sidecar differential parity (ADR-0008)", () => { - test("the Go sidecar binary is buildable before the oracle runs", () => { + test("the Go sidecar binary is buildable and health is declared Go-owned before the oracle runs", () => { expect(sidecarBinary).toBeTruthy(); expect(existsSync(sidecarBinary!)).toBe(true); + // The harness must prove a declared surface: if the marker is ever removed + // the oracle would compare nothing and pass vacuously. + expect(goOwnedHealth).toBeDefined(); + expect(healthVolatileFields).toEqual(["pid", "uptime"]); }); runFixtureTest("in-process handler and Go sidecar agree on status, headers, and normalised body", async (token) => { @@ -218,7 +234,7 @@ describe.skipIf(!goAvailable || sidecarBinary === null)("ocx-sidecar differentia // Byte parity after the declared normalisation: the oracle fails on // drift rather than logging it. Raw bodies still differ (pid/uptime), // so a vacuous equality is impossible. - expect(normaliseHealthBody(goBody.body)).toBe(normaliseHealthBody(tsBody.body)); + expect(normaliseBody(goBody.body, healthVolatileFields)).toBe(normaliseBody(tsBody.body, healthVolatileFields)); // The front door must relay the Go bytes without alteration. The two // requests land at different instants, so uptime (a volatile field) @@ -228,7 +244,7 @@ describe.skipIf(!goAvailable || sidecarBinary === null)("ocx-sidecar differentia headers: { accept: "application/json" }, }); expect(direct.status).toBe(200); - expect(normaliseHealthBody(await direct.text())).toBe(normaliseHealthBody(goBody.body)); + expect(normaliseBody(await direct.text(), healthVolatileFields)).toBe(normaliseBody(goBody.body, healthVolatileFields)); } finally { await serverB.stop(true); } @@ -252,10 +268,39 @@ describe.skipIf(!goAvailable || sidecarBinary === null)("ocx-sidecar differentia } }); - test("the declared volatile field set is exactly pid and uptime", () => { - // Keep the mirror honest: widening parity would silently tolerate drift in - // fields the TS handler owns (status/service/version), which is the exact - // divergence class the oracle exists to catch. - expect([...GO_SIDECAR_VOLATILE_FIELDS]).toEqual(["pid", "uptime"]); + runFixtureTest("an unexpected sidecar exit deregisters the forwarder and health falls back in-process", async (token) => { + // #11: a crash must surface, not fall silent. The supervisor deregisters the + // forwarder on an unexpected child exit, so the next health response flips + // back to the PROXY's pid — observable through the exact route the sidecar + // was serving, with no gap where health is unanswered. + process.env[GO_SIDECAR_BIN_ENV] = sidecarBinary!; + const server = startServer(0); + try { + const sidecarUrl = await waitFor(() => activeGoSidecarBaseUrl(), 15_000); + const served = await captureHealth(server, token); + expect(served.parsed.pid).not.toBe(process.pid); + expect(served.parsed.pid).toBeGreaterThan(0); + + // The sidecar reports its own pid; kill that process to simulate a crash. + const childPid = served.parsed.pid; + let killed = false; + try { + process.kill(childPid, "SIGTERM"); + killed = true; + } catch { + killed = false; + } + expect(killed).toBe(true); + + // The supervisor observes the exit and empties the slot (base URL gone). + await waitFor(() => (activeGoSidecarBaseUrl() === null ? true : null), 10_000); + + // Health still answers — from the in-process handler, pid flipped back. + const after = await captureHealth(server, token); + expect(after.status).toBe(200); + expect(after.parsed.pid).toBe(process.pid); + } finally { + await server.stop(true); + } }); }); From 02bbba480f2d3188cb5d90dbbfc072c8168362b0 Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Sun, 6 Sep 2026 00:27:47 +0800 Subject: [PATCH 006/165] ci(go): cross-compile every release target in the go job (ticket #8) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #8's acceptance — builds CGO_ENABLED=0 on every release target — was only proven on the runner's native platform. The go job now loops the six release targets (linux/darwin/windows x amd64/arm64) and fails if any combination does not produce a binary, so a future cgo leak or build-tag mistake surfaces in CI rather than at release. All six combos build clean today. --- .github/workflows/ci.yml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 18d193048c..cee9d50b49 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -435,6 +435,21 @@ jobs: - name: Go test run: cd go && go test ./... + - name: Cross-compile every release target (CGO_ENABLED=0) + # Ticket #8 acceptance: the ocx-sidecar must build static on every + # release target (linux/darwin/windows x amd64/arm64), not only the + # native platform of this runner. A platform-specific dependency that + # leaks cgo (or a build tag mistake) would otherwise surface only at + # release time. The loop proves all six combinations produce a binary; + # artifacts land in /tmp and are discarded. + run: | + set -euo pipefail + cd go + for target in linux/amd64 linux/arm64 darwin/amd64 darwin/arm64 windows/amd64 windows/arm64; do + os="${target%/*}"; arch="${target#*/}" + GOOS="$os" GOARCH="$arch" CGO_ENABLED=0 go build -o "/tmp/ocx-sidecar-$os-$arch" ./cmd/ocx-sidecar + done + - name: Differential oracle run: bun test --timeout 60000 tests/go-sidecar-parity.test.ts From 148a51c6d69dc323b19fa0406dcc40ea12d4ae1a Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Sun, 6 Sep 2026 00:27:51 +0800 Subject: [PATCH 007/165] docs(go): record the #14 ownership-plumbing delivery and the #8/#11 closures devlog unit 260905_go_sidecar_takeover now documents the critical-path run: the two increment-1 acceptance gaps that gained machine checks (cross-platform build gate for #8, crash-fallback oracle for #11) and the increment-2 ticket #14 implementation (typed read/write ownership, single registry-driven forwarding branch, per-route volatile declarations). --- .../020_ownership_plumbing.md | 113 ++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 devlog/_plan/260905_go_sidecar_takeover/020_ownership_plumbing.md diff --git a/devlog/_plan/260905_go_sidecar_takeover/020_ownership_plumbing.md b/devlog/_plan/260905_go_sidecar_takeover/020_ownership_plumbing.md new file mode 100644 index 0000000000..76502450f1 --- /dev/null +++ b/devlog/_plan/260905_go_sidecar_takeover/020_ownership_plumbing.md @@ -0,0 +1,113 @@ +# 020 — Ticket #14 delivered: read/write ownership split + batch-migration plumbing + +Unit: `260905_go_sidecar_takeover` +Date: 2026-09-06 +Status: implemented on `dev-go` +Tickets: [#8](https://github.com/waxiangzi/opencodex/issues/8) (1.1) → +[#10](https://github.com/waxiangzi/opencodex/issues/10) (1.2) / +[#11](https://github.com/waxiangzi/opencodex/issues/11) (1.3) → +[#12](https://github.com/waxiangzi/opencodex/issues/12) (1.4) → +[#13](https://github.com/waxiangzi/opencodex/issues/13) (1.5) → +[#14](https://github.com/waxiangzi/opencodex/issues/14) (2.1) +Parent specs: [#1](https://github.com/waxiangzi/opencodex/issues/1) (increment 1), +[#2](https://github.com/waxiangzi/opencodex/issues/2) (increment 2) + +## What this run delivered + +The critical path was walked in dependency order. Increment 1 (#8 → #10 ∥ #11 → +#12 → #13) already sat on `dev-go` (commit `4b8715a30` + follow-ups); this run +audited each acceptance criterion against the tree, closed the two gaps that +were still machine-unproven, and then implemented ticket #14 — the first +increment-2 ticket, which generalises the one-route seam into the plumbing the +read-surface batches will use. + +### #8 (1.1) gap closed: cross-platform build is now a CI gate + +The Go job already ran `go build/vet/test`, but only on the runner's native +platform. #8's acceptance — "builds CGO_ENABLED=0 on every release target" — +needed a proof. The `go` job in `.github/workflows/ci.yml` now loops the six +release targets (`linux/darwin/windows` × `amd64/arm64`) with `CGO_ENABLED=0` +and fails if any combination does not produce a binary. All six build clean +today; a future cgo leak or build-tag mistake surfaces in CI, not at release. + +### #11 (1.3) gap closed: crash observability is now machine-checked + +#11's "crashes surface via health/status" had been argued in prose (warn log + +in-process fallback) but not proven. The differential oracle now kills the +sidecar child mid-run and asserts the full observable contract: the forwarder +deregisters (base URL goes null) and the next `GET /api/system/health` +answers from the in-process handler with the pid flipped back to the proxy's +own — an observable change on the exact route the sidecar was serving, with no +window where health goes unanswered. + +### #14 (2.1): typed read/write ownership + single registry-driven branch + +Ticket #14's three acceptance criteria, and how each is met: + +- **Write routes cannot be marked Go-owned by mistake.** `ManagementRoute` in + `src/server/management/route-registry.ts` is now a discriminated union: the + write arm (`mutates: true`) has no `go` marker and the read arm + (`mutates: false`) carries an optional `GoOwnedRouteDeclaration`. Writing + `go:` onto a write route is a compile error. A runtime re-check in + `tests/go-ownership-plumbing.test.ts` re-derives the marker set from + `MANAGEMENT_ROUTES` and compares it with the exported + `GO_OWNED_MANAGEMENT_ROUTES`, so a cast or array-level workaround cannot + drift. + +- **Migrating a read route is a marker flip, not dispatch edits.** The bespoke + health forwarder call is gone from `system-routes.ts`. Dispatch now has ONE + branch, at the head of `handleManagementAPI`, that looks the request up in + the declared Go-owned surface (`findGoOwnedManagementRoute`) and only then + asks the optional-subsystem slot (`go-sidecar-slot.ts`, generalized from a + health forwarder to a route forwarder) to relay it. The Go-owned surface is + derived DATA (`GO_OWNED_MANAGEMENT_ROUTES`); the branch names no route, and a + test pins that `management-api.ts` contains no management-path literal in the + forwarding path. The next read route migrates by flipping its marker plus a + Go handler plus oracle coverage — no second dispatch edit exists to write. + +- **Per-route volatile declarations supported.** Each Go-owned route declares + its volatile fields in the registry (`health` declares `["pid","uptime"]`). + The differential oracle consumes the declaration instead of a mirrored + constant in `go-sidecar.ts`, so the oracle normalises exactly the declared + set and a later route cannot silently widen what parity means. The harness + also pins that the declared Go-owned surface is exactly health today, so an + accidental flip fails loudly. + +Fallback semantics are unchanged end to end: forwarder absent (default +install), returning `null`, or throwing → the in-process handler answers, +byte-identically to a build without Go. The in-process handler remains the +differential oracle. + +## Files + +- `src/server/management/route-registry.ts` — typed read/write ownership, + `GO_OWNED_MANAGEMENT_ROUTES`, `findGoOwnedManagementRoute`. +- `src/server/go-sidecar-slot.ts` — generalized core-owned forwarder slot. +- `src/server/go-sidecar.ts` — supervisor registers the generic forwarder. +- `src/server/management-api.ts` — the single forwarding branch. +- `src/server/management/system-routes.ts` — health back to pure in-process + fallback/oracle. +- `tests/go-ownership-plumbing.test.ts` — new: registry invariants + dispatch + behaviour with a fake forwarder (no Go toolchain needed). +- `tests/go-sidecar-parity.test.ts` — registry-driven normalisation; crash + fallback oracle (#11). +- `.github/workflows/ci.yml` — cross-platform build gate (#8). +- `go/README.md` — ownership marker contract. + +## Verification + +- `bun run typecheck` green. +- `tests/go-ownership-plumbing.test.ts` (9 pass), `tests/go-sidecar-parity.test.ts` + (4 pass, incl. crash fallback), `management-route-registry`, `core-lab-boundary`, + `ci-workflows`, `repo-hygiene`, `cli-capabilities`, `route-explainability`, + `skill-ocx` all green. +- `go build ./...` / `go vet ./...` / `go test ./...` green under `go/`; + all six release-target cross-compiles succeed with `CGO_ENABLED=0`. + +## Next on the path + +Spec #2 (increment 2) migrates the read-only management surface in batches: +system memory, models, providers, usage, quotas, config, catalog. Each batch +route flips its marker in the registry, gains a Go handler in +`go/cmd/ocx-sidecar`, and joins the registry-driven oracle. The plumbing to do +that without re-proving the seam is what ticket #14 established. From d238bcc9f169cbc6af2ebb66416a1dea685be946 Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Sun, 6 Sep 2026 00:44:12 +0800 Subject: [PATCH 008/165] docs(go): close tickets #8-#14 and record the read-surface state-source gate Re-ran every machine gate backing the delivered increment (go build/vet/test, six-target CGO_ENABLED=0 cross-compile matrix, differential oracle, ownership plumbing, registry reconciliation, typecheck) and resolved issues #8-#14 in the tracker in dependency order, each with tree evidence in the closure comment. Auditing the newly unblocked frontier (#15-#19) against the actual handlers found a gate the batch texts do not state: a route is Go-servable byte-identically only when its body is a pure function of state the sidecar process can see. The system-memory and windows-replace-retries bodies are TS-process introspection (no on-disk counterpart); the dashboard-session half of #18 lives in an in-memory Map; #19 would gate nothing until the Lab routes port. Records the per-route classification and the recommended order: port #16's pure config-core first, defer process-derived system routes to the flip with a documented exemption, specify the principal-relay contract before #18. --- .../030_read_surface_state_source_gate.md | 157 ++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 devlog/_plan/260905_go_sidecar_takeover/030_read_surface_state_source_gate.md diff --git a/devlog/_plan/260905_go_sidecar_takeover/030_read_surface_state_source_gate.md b/devlog/_plan/260905_go_sidecar_takeover/030_read_surface_state_source_gate.md new file mode 100644 index 0000000000..539bae10c2 --- /dev/null +++ b/devlog/_plan/260905_go_sidecar_takeover/030_read_surface_state_source_gate.md @@ -0,0 +1,157 @@ +# 030 — Frontier closes #8–#14 and the read-surface state-source gate + +Unit: `260905_go_sidecar_takeover` +Date: 2026-09-06 +Status: recorded on `dev-go`; issues #8–#14 closed as delivered +Tickets: closes [#8](https://github.com/waxiangzi/opencodex/issues/8) (1.1) → +[#10](https://github.com/waxiangzi/opencodex/issues/10) (1.2) / +[#11](https://github.com/waxiangzi/opencodex/issues/11) (1.3) → +[#12](https://github.com/waxiangzi/opencodex/issues/12) (1.4) → +[#13](https://github.com/waxiangzi/opencodex/issues/13) (1.5) → +[#14](https://github.com/waxiangzi/opencodex/issues/14) (2.1), plus +[#9](https://github.com/waxiangzi/opencodex/issues/9) (6.1) — in dependency order +Parent specs: [#1](https://github.com/waxiangzi/opencodex/issues/1) (increment 1), +[#2](https://github.com/waxiangzi/opencodex/issues/2) (increment 2 — see the gate below) + +## What this run delivered + +The first seven tickets were code-complete on `dev-go` (commits `4b8715a30` … +`148a51c6d`, recorded in `000_plan.md`, `010_lab_migrate_vs_cut_decision.md`, and +`020_ownership_plumbing.md`) but had never been **resolved** in the issue +tracker, so the frontier query kept returning them as available work. This run +re-ran every machine gate that backs them — `go build ./...` / `go vet ./...` / +`go test ./...` under `go/`, the six-target `CGO_ENABLED=0` cross-compile matrix, +the differential oracle, the ownership-plumbing suite, the route-registry +reconciliation, and `bun run typecheck` — and closed issues #8–#14 in dependency +order, each with a comment citing the tree evidence and this doc. + +Closing them exposes the true frontier: #15, #16, #17, #18, and #19 are now all +unblocked (each lists only #14 and/or #9 as a blocker). Auditing those tickets +against the actual handlers found a gate the batch texts do not state: + +> **A management route can be Go-served byte-identically only when its body is a +> pure function of state the sidecar process can see** — the environment it was +> spawned with, on-disk files, the OS, or its own process. Routes whose bodies +> report the *TypeScript process's* live state (in-memory maps and caches, +> `bun:jsc` introspection, module-level counters, memoized discovery) cannot be +> reproduced by a separate sidecar process before the flip, no matter how +> faithfully their handlers are ported. + +The health route migrated in #14 precisely because it sits at the portable end +of that spectrum (env + the sidecar's own process, two declared volatile +fields). Several routes in the next batches sit at the opposite end. + +## State-source classification + +Each candidate read route should be classified by where its body comes from +before batch work starts: + +- **env/on-disk (portable today)** — config-file content merged with the + version the supervisor passed at spawn. Both processes can read it; this is + the class the ownership marker was built for. +- **OS-level (portable with care)** — process enumeration, platform probes. Go + can reproduce these, but the matching semantics must be proven byte-exact + against the live TS oracle. +- **TS-process live state (NOT portable pre-flip)** — in-memory session and + replay tables, runtime introspection, module counters, memoized discovery, + drain/lifecycle state. A sidecar physically cannot return these values. + +### Ticket #15 (system reads) — per-route verdicts + +| route | handler | source | Go-servable pre-flip? | +|---|---|---|---| +| `GET /api/system/health` | `system-routes.ts` (Go-owned since #14) | env + own process | yes — the #14 precedent | +| `GET /api/system/memory` | `system-routes.ts` | TS-process live: `process.memoryUsage`, `bun:jsc` heapStats, `responseStateMetrics`, memory-watchdog snapshot, relay inspection counters, active-turn/drain counters | no | +| `GET /api/system/windows-replace-retries` | `src/lib/windows-atomic-replace.ts` (`counters` module map) | TS-process live | no | +| `GET /api/system/codex-app-server` | `src/codex/app-server-restart-service.ts` `readCodexAppServerState` | OS process catalog | yes in principle; catalog-matching parity must be proven | + +So #15 as written — "system read routes are Go-owned and byte-identical" — +cannot be closed while the TS server owns the process state: two of its three +unmigrated routes report TS-process introspection with no on-disk counterpart. +Only `codex-app-server` is plausibly portable, and one route does not close a +three-route ticket. + +### Ticket #16 (config reads) — mixed, and the pure core is the prize + +The config-core read bodies are disk-derived, but the GET handlers mix in live +process work: + +- `GET /api/settings` returns a disk-derived core (`port`, `hostname`, + `streamMode`, memory budget, toggles) **plus** `codexRuntime`, resolved by + `resolveCodexRuntime()` — memoized in-process discovery that locates and + versions the installed Codex binaries (`config-routes.ts`) — **plus** cached + `startupHealth` and the serving process's browser `timeZone`. +- `GET /api/startup-health` serves a cache invalidated by install actions. +- `GET /api/diagnostics/project-config` serves `getCachedProjectConfigDiagnostics` + — a scan cache populated by process work. +- `GET /api/update/check` / `GET /api/update/status` consult the update job + module (`src/update/job.ts`), which is process state. +- `GET /api/sidecar-settings` mixes config with candidate-row/model resolution. + +The valuable and genuinely portable artifact here is the **disk-derived core** — +one shared Go config-parsing implementation that turns the same on-disk config +into the same DTO bytes (#16's second acceptance criterion, and the dependency +#20/#21/#24/#35 all list). Route-level parity for the live fields needs a +per-field owner decision in the style of health's `volatileFields`: declare the +field volatile, forward a parent snapshot, or defer the whole route to the flip. + +### Ticket #18 (auth/session) — the session half is not portable + +- **Admin token**: file/env → portable; Go can read the same + `OPENCODEX_HOME/admin-api-token` or `OPENCODEX_ADMIN_AUTH_TOKEN`. +- **Dashboard session**: `ManagementAuthState.sessions` is an **in-memory + `Map`** in the TS process (`src/server/management-auth.ts`, + `src/server/gui-session.ts`). Sessions are opaque tokens into that map — there + is no stateless signed cookie a second process could verify. +- **Local capability principals**: HMACs over the process pid/port/attestation + secret, plus **in-process replay tables** (`consumedLocalReadCapabilities`, + the `admitted*Requests` weak sets) that live and die with the TS process. + +So #18's "Go validates the dashboard session; under-privileged requests are +rejected identically" cannot be proven while the session table and replay caches +exist only in the TS process. What is missing is a **principal-relay contract**: +the front door already admits the request; for Go to re-validate rather than +trust the hop blindly (spec #3), the front door must hand the sidecar an +assertion of the admitted principal that Go can verify against a shared secret. +That contract has no ticket yet and no consumer until a mutating route actually +migrates — it should be specified as part of the write-surface work, not before. + +### Ticket #19 (Lab gate) — premature + +The Go codebase contains no Lab subsystem yet, so a Go activation gate would +gate nothing until the Lab routes port (#33). #9's migrate-vs-cut decision is +recorded as **migrate** (`010_lab_migrate_vs_cut_decision.md`); the natural +reading is that the Go gate + provider-slot seam are built *as part of* the Lab +increment so the seam has something to activate. #19's acceptance wording ("an +opt-in activation gate exists in Go") is satisfiable only vacuously today. + +## Consequence and recommended order + +The next batch implementer should not take "every read route Go-owned and +byte-identical" as a literal instruction: several routes report process state +that a sidecar cannot know. Concretely: + +1. **Port #16's pure config-core first.** A shared Go config-parsing package is + the load-bearing artifact the rest of the program lists as a dependency. + Prove it byte-identical on the disk-derived subset of config read bodies and + resolve each live field (`codexRuntime`, `startupHealth`, updater state) by + one of the three per-field options above, recorded where the health route's + `volatileFields` live. +2. **Before #15, get an owner decision on the process-derived system routes.** + `memory` and `windows-replace-retries` cannot be Go-served pre-flip; the + registry's `exempt` mechanism already models a documented deferral, and a + defer-to-flip exemption is more honest than a sidecar inventing values. +3. **Specify the principal-relay contract before #18.** It becomes real work + only when a mutating route is being ported. +4. **Fold #19 into the Lab increment (#33)** so the gate has something to gate. + +## Verification + +- `go build ./...`, `go vet ./...`, `go test ./...` green under `go/`. +- Six-target cross-compile matrix (`linux/darwin/windows` × `amd64/arm64`, + `CGO_ENABLED=0`) produces all six binaries (#8 acceptance). +- `bun test tests/go-sidecar-parity.test.ts`: 4 pass — byte parity with the + declared normalisation, missing-binary no-op, crash fallback (#10/#11/#13). +- `bun test tests/go-ownership-plumbing.test.ts tests/management-route-registry.test.ts + tests/ci-workflows.test.ts tests/repo-hygiene.test.ts`: 170 pass (#12/#13/#14). +- `bun run typecheck` green. From 5dc6cae002cffc3e2c8cf83ebb7ba0294cd4ce39 Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Sun, 6 Sep 2026 01:20:51 +0800 Subject: [PATCH 009/165] =?UTF-8?q?feat(go):=20ticket=20#16=20=E2=80=94=20?= =?UTF-8?q?shared=20Go=20config=20parsing=20+=20strict=20shadow-call-setti?= =?UTF-8?q?ngs=20read=20route?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit First vertical slice of the config read batch (spec #2). The registry's go.volatileFields marker may now be EMPTY: that declares a strict route whose body is a pure function of shared state and must be byte-identical with no normalisation — the strongest oracle contract, not a vacuous one (the previous non-empty rule only ever made sense for process-value routes). - go/internal/config: shared Go config reader (OPENCODEX_HOME/config.json, json.Number preserves on-disk literals, never rewrites the file). The load-bearing artifact #20/#21/#24/#35 all depend on. - GET /api/shadow-call-settings is now Go-owned: marker flip + sidecar handler projecting shadowCallIntercept through the exact TS rules (enabled === true, model ?? "", shadowSourceModels trim/non-empty/default gpt-5.6-luna). In-process handler remains fallback and oracle. - Oracle: two strict-parity cases (default + configured body, raw bytes equal; relay alters nothing). go-ownership/route-registry/hygiene/ci/cli/explainability 199 pass; go build/vet/test green; typecheck green. Issue #16 stays open (1/9 routes); remaining routes and per-route status in devlog 031. --- .../031_config_read_first_slice.md | 92 ++++++++ go/README.md | 32 ++- go/cmd/ocx-sidecar/main.go | 10 +- go/internal/config/config.go | 211 ++++++++++++++++++ go/internal/config/config_test.go | 145 ++++++++++++ go/internal/sidecar/sidecar.go | 117 +++++++--- go/internal/sidecar/sidecar_test.go | 92 ++++++++ src/server/management/route-registry.ts | 10 +- tests/go-ownership-plumbing.test.ts | 49 +++- tests/go-sidecar-parity.test.ts | 121 +++++++++- 10 files changed, 817 insertions(+), 62 deletions(-) create mode 100644 devlog/_plan/260905_go_sidecar_takeover/031_config_read_first_slice.md create mode 100644 go/internal/config/config.go create mode 100644 go/internal/config/config_test.go diff --git a/devlog/_plan/260905_go_sidecar_takeover/031_config_read_first_slice.md b/devlog/_plan/260905_go_sidecar_takeover/031_config_read_first_slice.md new file mode 100644 index 0000000000..ffe08a76c5 --- /dev/null +++ b/devlog/_plan/260905_go_sidecar_takeover/031_config_read_first_slice.md @@ -0,0 +1,92 @@ +# 031 — Ticket #16 first vertical slice: shared Go config parsing + strict shadow-call-settings parity + +Unit: `260905_go_sidecar_takeover` +Date: 2026-09-06 +Status: recorded on `dev-go`; ticket #16 OPEN (1 of 9 config read routes migrated) +Tickets: [#16](https://github.com/waxiangzi/opencodex/issues/16) (config read routes batch, spec #2) +Parent spec: [#2](https://github.com/waxiangzi/opencodex/issues/2) (increment 2) +Owner decision (2026-09-06): implement #16 next, per the ordering recommended in +`030_read_surface_state_source_gate.md` — port the pure config core first and +resolve live-field routes per-field. + +## What this run delivered + +The first vertical slice of #16, proven end to end through the differential +oracle: a shared Go config reader, one config read route served byte-identically +from it, and the registry/oracle semantics that make "this route has no volatile +field" a real, checkable contract. + +- **`go/internal/config`** — the shared Go config parser (the artifact #20/#21/ + #24/#35 all list as a dependency). It reads the operator's `config.json` from + the same place the TypeScript runtime reads it (`OPENCODEX_HOME`, defaulting + to `~/.opencodex`), decodes numbers with `json.Number` so a value echoed into + a response keeps its on-disk literal, and never rewrites or moves the file. It + mirrors only the TS-side normalisation the Go-owned route bodies depend on, + and keeps the raw decoded document (`Raw`) so later routes can project from + it without a full schema port. +- **`GET /api/shadow-call-settings` is now Go-owned** — a marker flip in + `route-registry.ts` plus a Go handler in `go/internal/sidecar` that projects + the `shadowCallIntercept` section through the exact TS rules + (`sci.enabled === true`, `sci.model ?? ""`, `shadowSourceModels` trim / + non-empty / default filtering, default `["gpt-5.6-luna"]`). The in-process TS + handler remains the fallback and the differential oracle. +- **Strict-byte ownership semantics.** The `go.volatileFields` marker may now be + EMPTY: that declares a route whose body is a pure function of shared state + and must be byte-identical with NO normalisation — the strongest contract the + oracle can impose, not a vacuous one (the raw wire bodies are compared). The + previous "must be non-empty" rule had it backwards: it only ever made sense + for routes that legitimately report the serving process's own values. The + interface doc, `go-ownership-plumbing.test.ts`, the parity oracle, and + `go/README.md` all state the new contract. + +## Why this route first + +Of #16's nine config read routes, this is the only one whose body is a pure +function of the on-disk config with no live-process dependency — the smallest +byte-parity target that exercises the whole seam (config file → Go parser → +Go handler → registry flip → oracle row). Empirical check first: a probe +through `saveConfig`/`loadConfig` confirmed `shadowCallIntercept` round-trips +unnormalised (values verbatim, empty strings kept), so TS in-memory == file +content for this section and byte parity is well-defined. + +## Verification + +- `go build ./...`, `go vet ./...`, `go test ./...` green under `go/` + (new `internal/config` package: 8 tests; `internal/sidecar`: 10 tests incl. + shape/order, defaults, coercions, narrow surface). +- `bun test tests/go-sidecar-parity.test.ts`: 6 pass — health volatile parity, + missing-binary no-op, crash fallback, PLUS two new strict cases: + shadow-call-settings default body and configured body are byte-identical + with no normalisation, and the front door relays the Go bytes unaltered. +- `bun test tests/go-ownership-plumbing.test.ts tests/management-route-registry.test.ts + tests/repo-hygiene.test.ts tests/ci-workflows.test.ts tests/cli-capabilities.test.ts + tests/route-explainability.test.ts`: 199 pass. +- `bun run typecheck` green. + +## Per-route status of #16 (config read routes, 1 of 9 migrated) + +| route | body source | status | +|---|---|---| +| `/api/shadow-call-settings` | pure config | **Go-owned, strict parity** | +| `/api/config` | pure fn of config, but a LARGE projection | next sub-increment: provider redaction policy (`providerEditorConfigDTO`), registry notes, xai opt-in, cost-overlay sanitisation, service-tier projection, key order | +| `/api/settings` | disk core + live fields | mixed: `codexRuntime` (memoized discovery), cached `startupHealth`, process `timeZone` — port the disk core; live fields need the per-field owner decision | +| `/api/sidecar-settings` | config core + candidate/model helpers | classify the helpers when attempted | +| `/api/startup-health` | in-process cache (install/repair actions) | defer — no on-disk counterpart | +| `/api/diagnostics/project-config` | in-process scan cache | defer — no on-disk counterpart | +| `/api/update/check` / `/api/update/status` | updater job module state | defer | +| `/api/windows-tray` | platform probe / static platform string | OS-level; low value; defer | + +The `/api/settings` and `/api/config` disk-derived cores are the two large +sub-increments left before #16's write of shared parsing is exercised broadly; +the cache/process routes stay TypeScript-owned until the flip, consistent with +`030_read_surface_state_source_gate.md`. Ticket #16 remains open. + +## Files + +- `go/internal/config/config.go`, `config_test.go` — new shared config reader. +- `go/internal/sidecar/sidecar.go`, `sidecar_test.go` — new route + shared + `respondJSON`; unit tests. +- `go/cmd/ocx-sidecar/main.go`, `go/README.md` — doc updates. +- `src/server/management/route-registry.ts` — strict-volatile semantics + marker. +- `tests/go-ownership-plumbing.test.ts`, `tests/go-sidecar-parity.test.ts` — + two-route surface pins + strict-parity oracle cases. diff --git a/go/README.md b/go/README.md index fbc3c914f0..ee405f24ad 100644 --- a/go/README.md +++ b/go/README.md @@ -20,8 +20,13 @@ material only. This is a fresh codebase. `src/server/management/route-registry.ts`, and the single forwarding branch in `src/server/management-api.ts` reads them before asking the sidecar. - `internal/sidecar` — the handler plus its unit tests. The JSON key order and - number formatting of the health payload are part of the byte contract with - the Bun differential oracle (`tests/go-sidecar-parity.test.ts`). + number formatting of each payload are part of the byte contract with the Bun + differential oracle (`tests/go-sidecar-parity.test.ts`). +- `internal/config` — the shared Go config reader (ticket #16). It parses the + operator's `config.json` (OPENCODEX_HOME, defaulting to `~/.opencodex`) + exactly where the TypeScript runtime keeps it, so a Go-served read route + answers from the real on-disk state; route bodies are pure functions of the + subsection they read. ## Building @@ -42,7 +47,22 @@ external dependencies, so there is no `go.sum`. - After binding its loopback listener, the sidecar prints one readiness line on stdout: `ocx-sidecar-ready http://127.0.0.1:`. The parent waits for this line before registering the route forwarder. -- The migrated route's declared volatile fields (today: `pid`, `uptime` for - `GET /api/system/health`) are normalised by the differential oracle and - nothing else is: a later route cannot silently widen what parity means. The - declaration lives with the route in `route-registry.ts`, not here. +- The migrated route's declared volatile fields are normalised by the + differential oracle and nothing else is: a later route cannot silently widen + what parity means. The declaration lives with the route in + `route-registry.ts`, not here. Today: `GET /api/system/health` declares + `["pid", "uptime"]` (the sidecar reports its own process values), and + `GET /api/shadow-call-settings` declares an EMPTY set — its body is a pure + function of `config.json`, so the oracle compares raw bytes with no + normalisation at all. + +## Config read routes (ticket #16) + +`GET /api/shadow-call-settings` is the first config read route served from Go. +The sidecar reads the same `config.json` the TypeScript in-process handler's +config snapshot came from (`internal/config`), then projects the +`shadowCallIntercept` section through the same rules the TS handler applies +(`shadowSourceModels` defaults, trim/non-empty filtering, `sci.model ?? ""`). +The config is read per request; the sidecar carries no state. The in-process +TS handler remains the fallback and the differential oracle, so a default +install and a supervision blip behave byte-identically to a build without Go. diff --git a/go/cmd/ocx-sidecar/main.go b/go/cmd/ocx-sidecar/main.go index 26bdce0f13..cb3155065e 100644 --- a/go/cmd/ocx-sidecar/main.go +++ b/go/cmd/ocx-sidecar/main.go @@ -1,12 +1,14 @@ // Command ocx-sidecar is the first Go-owned process of the incremental // runtime takeover (ADR-0008). It is spawned and supervised by the -// TypeScript proxy front door and serves exactly one read-only management -// route, GET /api/system/health, with byte-identical HTTP semantics to the -// in-process TypeScript handler. See go/internal/sidecar for the contract. +// TypeScript proxy front door and serves the declared Go-owned read-only +// management routes (today: GET /api/system/health and +// GET /api/shadow-call-settings) with byte-identical HTTP semantics to the +// in-process TypeScript handlers. See go/internal/sidecar for the contract. // // The binary is built CGO_ENABLED=0 and carries no state: everything it must // echo from the parent (service label, package version) arrives through the -// environment at spawn time. +// environment at spawn time, and the config read route reads the operator's +// config.json from the same OPENCODEX_HOME the parent was launched with. package main import ( diff --git a/go/internal/config/config.go b/go/internal/config/config.go new file mode 100644 index 0000000000..361ad74cf4 --- /dev/null +++ b/go/internal/config/config.go @@ -0,0 +1,211 @@ +// Package config is the shared Go config reader of the incremental runtime +// takeover (ADR-0008, devlog/_plan/260905_go_sidecar_takeover, ticket #16). +// +// It reads the same on-disk config the TypeScript runtime reads +// (OPENCODEX_HOME/config.json, defaulting to ~/.opencodex/config.json) so a +// Go-served management read route can answer from the operator's real state +// instead of a snapshot invented inside the sidecar. The TypeScript side +// validates and normalises the file through its zod pipeline on load +// (src/config.ts); this package deliberately mirrors only the parts of that +// pipeline that the Go-owned read routes depend on, and it does NOT rewrite or +// move the file. Divergence is confined to configs that are invalid enough for +// TypeScript to salvage or back up, which the differential oracle never feeds. +// +// The route bodies this package feeds (today: GET /api/shadow-call-settings) +// are pure functions of the config subsection they read, so byte parity with +// the in-process TypeScript handler holds as long as both processes read the +// same file content. Numbers are decoded with json.Number so a value echoed +// into a response keeps its exact on-disk literal instead of being reformatted +// through float64, which is what byte parity requires for config-derived DTOs. +package config + +import ( + "encoding/json" + "errors" + "io" + "log" + "os" + "path/filepath" + "strings" +) + +// DefaultShadowSourceModels mirrors DEFAULT_SHADOW_SOURCE_MODELS in +// src/lib/shadow-call.ts. It is the value the shadow-call settings read route +// reports when the config carries no usable sourceModels override, and it must +// stay in lockstep with that constant — the differential oracle compares bytes. +var DefaultShadowSourceModels = []string{"gpt-5.6-luna"} + +// Dir resolves the config directory exactly like getConfigDir in +// src/config/paths.ts: OPENCODEX_HOME when set (trimmed, a leading ~ expanded), +// otherwise /.opencodex. +func Dir() (string, error) { + raw := strings.TrimSpace(os.Getenv("OPENCODEX_HOME")) + if raw == "" { + home, err := os.UserHomeDir() + if err != nil { + return "", err + } + return filepath.Join(home, ".opencodex"), nil + } + if raw == "~" { + home, err := os.UserHomeDir() + if err != nil { + return "", err + } + return home, nil + } + if strings.HasPrefix(raw, "~/") || strings.HasPrefix(raw, `~\`) { + home, err := os.UserHomeDir() + if err != nil { + return "", err + } + return filepath.Join(home, raw[2:]), nil + } + return filepath.Clean(raw), nil +} + +// Path returns the config file path (getConfigPath in src/config/paths.ts). +func Path() (string, error) { + dir, err := Dir() + if err != nil { + return "", err + } + return filepath.Join(dir, "config.json"), nil +} + +// Config is the parsed config.json. It holds exactly the subsections the +// Go-owned read routes consume; unknown top-level keys are preserved in Raw so +// a later route can project from them without a schema re-read. This is a +// foundation, not a full schema port: TS-side validation/normalisation is +// replicated only where a Go-owned route body depends on it (see package doc). +type Config struct { + // ShadowCallIntercept mirrors config.shadowCallIntercept (the optional + // shadow/helper-call rewrite section). Nil when absent from the file. + ShadowCallIntercept *ShadowCallIntercept + // Raw is the whole file decoded with numbers preserved as json.Number. + Raw map[string]any +} + +// ShadowCallIntercept mirrors the shadowCallIntercept subsection of OcxConfig +// (src/types/config.ts). Values are kept as decoded JSON (not narrowed to the +// expected types) because the TypeScript runtime stores whatever the file +// carried and the read route's projection is where type coercion happens. +type ShadowCallIntercept struct { + // Enabled mirrors sci.enabled: the route reports exactly sci.enabled === true. + Enabled any `json:"enabled"` + // Model mirrors sci.model, retained as the decoded JSON value so a string + // stays a string and an absent/null key stays distinguishable. + Model any `json:"model"` + // SourceModels mirrors sci.sourceModels (decoded array or nil). + SourceModels any `json:"sourceModels"` +} + +// Load reads and decodes config.json. A missing file yields an empty Config +// (the TypeScript runtime defaults on ENOENT and getDefaultConfig carries no +// shadowCallIntercept). A malformed file yields an empty Config plus the +// decode error: the TypeScript runtime backs the file up and defaults, and the +// Go side must not move user files, so it only logs. +func Load() (*Config, error) { + path, err := Path() + if err != nil { + return nil, err + } + return LoadFromPath(path) +} + +// LoadFromDir is Load with an explicit config directory (test seam and +// supervisor-injected homes). +func LoadFromDir(dir string) (*Config, error) { + return LoadFromPath(filepath.Join(dir, "config.json")) +} + +// LoadFromPath is the raw loader; the path comes from Path() or LoadFromDir. +func LoadFromPath(path string) (*Config, error) { + file, err := os.Open(path) + if err != nil { + if errors.Is(err, os.ErrNotExist) { + return &Config{Raw: map[string]any{}}, nil + } + return &Config{Raw: map[string]any{}}, err + } + defer file.Close() + return decode(file) +} + +func decode(reader io.Reader) (*Config, error) { + decoder := json.NewDecoder(reader) + decoder.UseNumber() + raw := map[string]any{} + if err := decoder.Decode(&raw); err != nil { + log.Printf("ocx-sidecar: config.json is not valid JSON; treating it as empty: %v", err) + return &Config{Raw: map[string]any{}}, err + } + cfg := &Config{Raw: raw} + if section, ok := raw["shadowCallIntercept"]; ok { + if obj, ok := section.(map[string]any); ok { + cfg.ShadowCallIntercept = &ShadowCallIntercept{ + Enabled: obj["enabled"], + Model: obj["model"], + SourceModels: obj["sourceModels"], + } + } + } + return cfg, nil +} + +// ShadowCallSettings is the projection the shadow-call settings read route +// emits (src/server/management/config-routes.ts, GET /api/shadow-call-settings). +type ShadowCallSettings struct { + Enabled bool + Model any + SourceModels []string +} + +// ShadowCallSettingsView mirrors the TypeScript handler's projection: +// enabled = sci.enabled === true, model = sci.model ?? "" (so absent or null +// becomes the empty string), and sourceModels = shadowSourceModels(sci. +// sourceModels) from src/lib/shadow-call.ts — non-string entries are dropped, +// entries are trimmed, and an empty result falls back to the default list. +func (c *Config) ShadowCallSettingsView() ShadowCallSettings { + // Absent section and null model both project to the empty string (the TS + // handler's `sci.model ?? ""`), so Model starts as "" and only a present, + // non-null value replaces it. + out := ShadowCallSettings{Model: ""} + sci := c.ShadowCallIntercept + if sci == nil { + out.SourceModels = defaultSourceModels() + return out + } + out.Enabled = sci.Enabled == true + if sci.Model != nil { + out.Model = sci.Model + } + out.SourceModels = normalizeSourceModels(sci.SourceModels) + return out +} + +func normalizeSourceModels(configured any) []string { + normalized := []string{} + if array, ok := configured.([]any); ok { + for _, entry := range array { + value, ok := entry.(string) + if !ok { + continue + } + trimmed := strings.TrimSpace(value) + if trimmed != "" { + normalized = append(normalized, trimmed) + } + } + } + if len(normalized) == 0 { + return defaultSourceModels() + } + return normalized +} + +func defaultSourceModels() []string { + // Fresh slice per call: a caller must not be able to mutate the shared + // default and skew a later response. + return append([]string(nil), DefaultShadowSourceModels...) +} diff --git a/go/internal/config/config_test.go b/go/internal/config/config_test.go new file mode 100644 index 0000000000..3325a754ab --- /dev/null +++ b/go/internal/config/config_test.go @@ -0,0 +1,145 @@ +package config + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +func writeFixture(t *testing.T, dir, content string) string { + t.Helper() + path := filepath.Join(dir, "config.json") + if err := os.WriteFile(path, []byte(content), 0o600); err != nil { + t.Fatal(err) + } + return path +} + +func TestLoadMissingFileIsEmpty(t *testing.T) { + dir := t.TempDir() + cfg, err := LoadFromDir(dir) + if err != nil { + t.Fatalf("LoadFromDir on an empty dir returned an error: %v", err) + } + if cfg.ShadowCallIntercept != nil { + t.Fatalf("expected no shadowCallIntercept for a missing file, got %+v", cfg.ShadowCallIntercept) + } +} + +func TestLoadMalformedJSONDefaultsWithoutMovingTheFile(t *testing.T) { + dir := t.TempDir() + path := writeFixture(t, dir, "{not json") + cfg, err := LoadFromDir(dir) + if err == nil { + t.Fatal("expected a decode error for malformed JSON") + } + if cfg.ShadowCallIntercept != nil { + t.Fatalf("expected no shadowCallIntercept after a decode failure, got %+v", cfg.ShadowCallIntercept) + } + // The TS side backs the file up on an invalid parse; the Go side must never + // move or rewrite user files, so the original bytes stay untouched. + raw, readErr := os.ReadFile(path) + if readErr != nil { + t.Fatal(readErr) + } + if string(raw) != "{not json" { + t.Fatalf("malformed config file was modified; got %q", raw) + } +} + +func TestShadowCallSettingsAbsentSection(t *testing.T) { + dir := t.TempDir() + writeFixture(t, dir, `{"port": 18080}`) + cfg, err := LoadFromDir(dir) + if err != nil { + t.Fatal(err) + } + view := cfg.ShadowCallSettingsView() + if view.Enabled { + t.Fatal("enabled must be false when the section is absent") + } + if view.Model != "" { + t.Fatalf("model must be the empty string when the section is absent, got %#v", view.Model) + } + if got := strings.Join(view.SourceModels, ","); got != "gpt-5.6-luna" { + t.Fatalf("sourceModels = %q, want the TS default gpt-5.6-luna", got) + } +} + +func TestShadowCallSettingsProjectionMatchesTypeScript(t *testing.T) { + dir := t.TempDir() + // Includes the coercions the TS handler performs: enabled only when === true, + // model kept verbatim (spaces included, null collapses to ""), sourceModels + // filtered to non-empty trimmed strings with non-string entries dropped. + writeFixture(t, dir, `{ + "shadowCallIntercept": { + "enabled": true, + "model": " gpt-5.5 ", + "sourceModels": [" gpt-5.4-mini ", "", 42, "gpt-6-terra"] + } +}`) + cfg, err := LoadFromDir(dir) + if err != nil { + t.Fatal(err) + } + view := cfg.ShadowCallSettingsView() + if !view.Enabled { + t.Fatal("enabled must be true") + } + if view.Model != " gpt-5.5 " { + t.Fatalf("model must be echoed verbatim, got %#v", view.Model) + } + if got := strings.Join(view.SourceModels, ","); got != "gpt-5.4-mini,gpt-6-terra" { + t.Fatalf("sourceModels = %q, want gpt-5.4-mini,gpt-6-terra (42 and the empty entry dropped)", got) + } +} + +func TestShadowCallSettingsEnabledNonBooleanAndNullModel(t *testing.T) { + dir := t.TempDir() + writeFixture(t, dir, `{ + "shadowCallIntercept": { "enabled": "yes", "model": null, "sourceModels": [] } +}`) + cfg, err := LoadFromDir(dir) + if err != nil { + t.Fatal(err) + } + view := cfg.ShadowCallSettingsView() + if view.Enabled { + t.Fatal(`enabled must be false for the string "yes" (sci.enabled === true)`) + } + if view.Model != "" { + t.Fatalf("null model must collapse to the empty string, got %#v", view.Model) + } + // An empty configured array falls back to the default list. + if got := strings.Join(view.SourceModels, ","); got != "gpt-5.6-luna" { + t.Fatalf("sourceModels = %q, want the default after an empty array", got) + } +} + +func TestDirHonoursOpenCodexHome(t *testing.T) { + t.Setenv("OPENCODEX_HOME", "/tmp/ocx-home-probe") + dir, err := Dir() + if err != nil { + t.Fatal(err) + } + if dir != "/tmp/ocx-home-probe" { + t.Fatalf("Dir() = %q, want the OPENCODEX_HOME value", dir) + } +} + +func TestDirFallsBackToHomeDotOpenCodex(t *testing.T) { + t.Setenv("OPENCODEX_HOME", "") + dir, err := Dir() + if err != nil { + t.Fatal(err) + } + home, err := os.UserHomeDir() + if err != nil { + t.Fatal(err) + } + want := filepath.Join(home, ".opencodex") + if dir != want { + t.Fatalf("Dir() = %q, want %q", dir, want) + } +} diff --git a/go/internal/sidecar/sidecar.go b/go/internal/sidecar/sidecar.go index 42eeab87d9..70e558552d 100644 --- a/go/internal/sidecar/sidecar.go +++ b/go/internal/sidecar/sidecar.go @@ -1,14 +1,14 @@ -// Package sidecar is the first Go-owned route of the incremental runtime -// takeover (ADR-0008, devlog/_plan/260905_go_sidecar_takeover). +// Package sidecar serves the declared Go-owned management read routes of the +// incremental runtime takeover (ADR-0008, devlog/_plan/260905_go_sidecar_takeover). // -// It owns exactly one management route -- GET /api/system/health -- and must -// reproduce the TypeScript handler's HTTP semantics byte-for-byte after the -// caller normalises the declared volatile fields (pid, uptime). The shape, -// key order, and number formatting of the JSON body are part of that contract: -// the Bun differential harness compares the normalised wire bodies, so this -// package's payload struct field order and its use of encoding/json (shortest -// round-trip number formatting, matching ECMAScript) are load-bearing, not -// cosmetic. +// Today it owns GET /api/system/health (volatile pid/uptime normalised by the +// oracle) and GET /api/shadow-call-settings (a pure function of config.json, +// compared with no normalisation at all). Each handler must reproduce the +// TypeScript handler's HTTP semantics byte-for-byte: the shape, key order, and +// number formatting of the JSON body are part of the contract. The Bun +// differential harness compares the wire bodies, so this package's payload +// struct field order and its use of encoding/json (shortest round-trip number +// formatting, matching ECMAScript) are load-bearing, not cosmetic. package sidecar import ( @@ -17,6 +17,8 @@ import ( "net/http" "os" "time" + + "github.com/lidge-jun/opencodex/go/internal/config" ) // Config carries the values the sidecar must echo from its TypeScript parent. @@ -37,6 +39,12 @@ type Config struct { // time.Since(StartedAt).Seconds() at request time, mirroring // process.uptime(). StartedAt time.Time + // ConfigDir overrides the config directory for routes that read the + // operator's config.json (GET /api/shadow-call-settings). Empty defers to + // the environment: OPENCODEX_HOME, then ~/.opencodex -- the same + // resolution as src/config/paths.ts in the parent. Set explicitly only by + // unit tests; the supervisor inherits OPENCODEX_HOME at spawn time. + ConfigDir string } // healthPayload mirrors the JSON object literal in @@ -51,11 +59,11 @@ type healthPayload struct { Pid int `json:"pid"` } -// NewHandler builds the sidecar's HTTP surface: exactly GET /api/system/health. -// Every other path or method falls through to Go's default ServeMux 404/405 so -// the sidecar never invents management surface of its own. The TypeScript front -// door only forwards the one route, so this handler never sees another request -// while the seam is wired correctly. +// NewHandler builds the sidecar's HTTP surface: exactly the declared Go-owned +// read routes. Every other path or method falls through to Go's default +// ServeMux 404/405 so the sidecar never invents management surface of its own. +// The TypeScript front door only forwards declared routes, so this handler +// never sees another request while the seam is wired correctly. func NewHandler(cfg Config) http.Handler { mux := http.NewServeMux() mux.HandleFunc("GET /api/system/health", func(w http.ResponseWriter, r *http.Request) { @@ -78,32 +86,79 @@ func NewHandler(cfg Config) http.Handler { Uptime: time.Since(startedAt).Seconds(), Pid: os.Getpid(), } - // Same header the TS handler produces via jsonResponse without a - // request/config pair: Content-Type application/json, nothing else. - // Header names are case-insensitive on the wire, but the harness - // compares them case-insensitively anyway. - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - // json.Marshal (not an Encoder): Encoder.Encode appends a trailing - // newline, and the TS handler emits none — the differential oracle - // compares bytes, so the newline would be a divergence. - raw, err := json.Marshal(payload) + respondJSON(w, payload, "health") + }) + + // GET /api/shadow-call-settings (ticket #16): the body is a pure function + // of the operator's config.json shadowCallIntercept section, so the sidecar + // reads the same file the in-process TS handler's config snapshot came from + // and projects it through the same rules. Unlike health there is no + // process-specific value, so the differential oracle compares these bytes + // with NO normalisation — any drift is a real divergence. The config is + // read per request (the sidecar carries no state) from cfg.ConfigDir or the + // OPENCODEX_HOME / ~/.opencodex resolution the TS parent uses. + mux.HandleFunc("GET /api/shadow-call-settings", func(w http.ResponseWriter, r *http.Request) { + loaded, err := loadSidecarConfig(cfg.ConfigDir) if err != nil { - // Unreachable for this fixed struct, but never write a partial body. - fmt.Fprintf(os.Stderr, "ocx-sidecar: marshal health payload: %v\n", err) - return + // A missing or unreadable config is an empty Config (the TS runtime + // defaults too); a malformed file was already logged by the loader. + loaded = &config.Config{Raw: map[string]any{}} } - if _, err := w.Write(raw); err != nil { - fmt.Fprintf(os.Stderr, "ocx-sidecar: write health payload: %v\n", err) + view := loaded.ShadowCallSettingsView() + payload := shadowCallSettingsPayload{ + Enabled: view.Enabled, + Model: view.Model, + SourceModels: view.SourceModels, } + respondJSON(w, payload, "shadow-call-settings") }) return mux } +// loadSidecarConfig is the config-file loader used by the shadow-call route. +// An explicit dir (unit tests) wins; otherwise the same OPENCODEX_HOME then +// ~/.opencodex resolution the TS parent uses at spawn. +func loadSidecarConfig(configDir string) (*config.Config, error) { + if configDir != "" { + return config.LoadFromDir(configDir) + } + return config.Load() +} + +// respondJSON writes a fixed-shape payload exactly the way the TS handlers +// emit jsonResponse: Content-Type application/json, a 200 status, and the +// marshalled bytes with NO trailing newline (Encoder.Encode would append one +// and the differential oracle compares bytes). On the impossible marshal error +// it logs and writes nothing rather than emitting a partial body. +func respondJSON(w http.ResponseWriter, payload any, owner string) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + raw, err := json.Marshal(payload) + if err != nil { + fmt.Fprintf(os.Stderr, "ocx-sidecar: marshal %s payload: %v\n", owner, err) + return + } + if _, err := w.Write(raw); err != nil { + fmt.Fprintf(os.Stderr, "ocx-sidecar: write %s payload: %v\n", owner, err) + } +} + +// shadowCallSettingsPayload mirrors the JSON object literal in +// src/server/management/config-routes.ts (GET /api/shadow-call-settings). Field +// order is the byte contract, matching the TS handler's insertion order. Model +// is the raw decoded value (normally a string; a non-string value in the file +// is echoed as-is, exactly like the TS projection sci.model ?? "" collapses +// only null/absent). +type shadowCallSettingsPayload struct { + Enabled bool `json:"enabled"` + Model any `json:"model"` + SourceModels []string `json:"sourceModels"` +} + // ReadyLinePrefix is the stdout marker the TypeScript supervisor parses to // learn the sidecar's bound address. The full line is // " http://:"; see the supervisor's reader in // src/server/go-sidecar.ts. Parsing is deliberately trivial (a space-separated -// http URL) so the parent never needs a JSON handshake to supervise a health +// http URL) so the parent never needs a JSON handshake to supervise the // sidecar. const ReadyLinePrefix = "ocx-sidecar-ready" diff --git a/go/internal/sidecar/sidecar_test.go b/go/internal/sidecar/sidecar_test.go index bcf5b3ae7e..801d0506d8 100644 --- a/go/internal/sidecar/sidecar_test.go +++ b/go/internal/sidecar/sidecar_test.go @@ -7,6 +7,7 @@ import ( "net/http" "net/http/httptest" "os" + "path/filepath" "regexp" "testing" "time" @@ -158,3 +159,94 @@ func TestReadyLineConstant(t *testing.T) { t.Fatalf("ReadyLinePrefix = %q changed; the TS supervisor parses this exact token", ReadyLinePrefix) } } + +// writeConfigFile writes a config.json fixture into dir and returns its path. +func writeConfigFile(t *testing.T, dir, content string) string { + t.Helper() + path := filepath.Join(dir, "config.json") + if err := os.WriteFile(path, []byte(content), 0o600); err != nil { + t.Fatal(err) + } + return path +} + +func TestShadowCallSettingsShapeAndOrder(t *testing.T) { + dir := t.TempDir() + writeConfigFile(t, dir, `{"shadowCallIntercept": {"enabled": true, "model": "gpt-5.5", "sourceModels": ["gpt-5.4-mini"]}}`) + h := NewHandler(Config{ConfigDir: dir}) + + resp := do(t, h, http.MethodGet, "/api/shadow-call-settings") + if resp.StatusCode != http.StatusOK { + t.Fatalf("status = %d, want 200", resp.StatusCode) + } + if got := resp.Header.Get("Content-Type"); got != "application/json" { + t.Fatalf("Content-Type = %q, want application/json", got) + } + raw, err := io.ReadAll(resp.Body) + if err != nil { + t.Fatal(err) + } + // Byte contract with the TS handler: the raw body must be exactly this + // string (key order enabled, model, sourceModels; no trailing newline). + want := `{"enabled":true,"model":"gpt-5.5","sourceModels":["gpt-5.4-mini"]}` + if string(raw) != want { + t.Fatalf("body = %s, want %s", raw, want) + } +} + +func TestShadowCallSettingsDefaultsWithoutConfig(t *testing.T) { + // No config.json at all: the body must match the TS handler reading a + // default config (enabled false, model "", default source list). + h := NewHandler(Config{ConfigDir: t.TempDir()}) + resp := do(t, h, http.MethodGet, "/api/shadow-call-settings") + defer resp.Body.Close() + raw, err := io.ReadAll(resp.Body) + if err != nil { + t.Fatal(err) + } + want := `{"enabled":false,"model":"","sourceModels":["gpt-5.6-luna"]}` + if string(raw) != want { + t.Fatalf("body = %s, want %s", raw, want) + } +} + +func TestShadowCallSettingsCoercions(t *testing.T) { + dir := t.TempDir() + // Mirrors the TS projection: enabled only when strictly true, model echoed + // verbatim when a string, empty entries dropped from sourceModels. + writeConfigFile(t, dir, `{"shadowCallIntercept": {"enabled": "yes", "model": " gpt-5.5 ", "sourceModels": [" ", "gpt-6-terra", ""]}}`) + h := NewHandler(Config{ConfigDir: dir}) + resp := do(t, h, http.MethodGet, "/api/shadow-call-settings") + defer resp.Body.Close() + raw, err := io.ReadAll(resp.Body) + if err != nil { + t.Fatal(err) + } + want := `{"enabled":false,"model":" gpt-5.5 ","sourceModels":["gpt-6-terra"]}` + if string(raw) != want { + t.Fatalf("body = %s, want %s", raw, want) + } +} + +func TestShadowCallSettingsSurfaceIsNarrow(t *testing.T) { + h := NewHandler(Config{ConfigDir: t.TempDir()}) + cases := []struct { + method string + path string + want int + }{ + {http.MethodPost, "/api/shadow-call-settings", http.StatusMethodNotAllowed}, + {http.MethodGet, "/api/config", http.StatusNotFound}, + {http.MethodGet, "/api/settings", http.StatusNotFound}, + {http.MethodGet, "/api/shadow-call-settings/", http.StatusNotFound}, + {http.MethodGet, "/api/shadow-call-settings?x=1", http.StatusOK}, + } + for _, tc := range cases { + resp := do(t, h, tc.method, tc.path) + resp.Body.Close() + if resp.StatusCode != tc.want { + t.Errorf("%s %s status = %d, want %d", tc.method, tc.path, resp.StatusCode, tc.want) + } + } +} + diff --git a/src/server/management/route-registry.ts b/src/server/management/route-registry.ts index e405a3cfd5..972cd840af 100644 --- a/src/server/management/route-registry.ts +++ b/src/server/management/route-registry.ts @@ -212,7 +212,15 @@ export const MANAGEMENT_ROUTES: readonly ManagementRoute[] = [ { method: "GET", path: "/api/config", module: "server/management/config-routes", mutates: false }, { method: "GET", path: "/api/diagnostics/project-config", module: "server/management/config-routes", mutates: false }, { method: "GET", path: "/api/settings", module: "server/management/config-routes", mutates: false }, - { method: "GET", path: "/api/shadow-call-settings", module: "server/management/config-routes", mutates: false }, + // ADR-0008 ownership: GET /api/shadow-call-settings is Go-owned (ticket #16). + // The body is a PURE function of config.json's shadowCallIntercept section + // (no process values), so it declares an EMPTY volatile set: the differential + // oracle compares its raw bytes with no normalisation — the strictest + // contract it can impose. The Go handler (go/internal/sidecar) reads the same + // OPENCODEX_HOME/config.json the in-process TS handler's snapshot came from + // (go/internal/config) and projects it through the same rules + // (shadowSourceModels defaults, trim/non-empty filtering, sci.model ?? ""). + { method: "GET", path: "/api/shadow-call-settings", module: "server/management/config-routes", mutates: false, go: { volatileFields: [] } }, { method: "GET", path: "/api/sidecar-settings", module: "server/management/config-routes", mutates: false }, { method: "GET", path: "/api/startup-health", module: "server/management/config-routes", mutates: false }, { method: "GET", path: "/api/update/check", module: "server/management/config-routes", mutates: false }, diff --git a/tests/go-ownership-plumbing.test.ts b/tests/go-ownership-plumbing.test.ts index 977618f836..3bdd585c4a 100644 --- a/tests/go-ownership-plumbing.test.ts +++ b/tests/go-ownership-plumbing.test.ts @@ -27,9 +27,11 @@ import { removeTreeWithRetry } from "./helpers/remove-tree"; * 1. REGISTRY INVARIANTS — the typed `go` ownership marker lives only on read * routes (the discriminated union in route-registry.ts refuses it on a write * route at compile time; these tests re-check at runtime), every Go-owned - * route declares a non-empty volatile set for the differential oracle, and - * the derived GO_OWNED_MANAGEMENT_ROUTES view stays in lockstep with the - * markers in MANAGEMENT_ROUTES. + * route declares a duplicate-free volatile set for the differential oracle + * (EMPTY when the route is strict: its body is a pure function of shared + * state and must be byte-identical with no normalisation), and the derived + * GO_OWNED_MANAGEMENT_ROUTES view stays in lockstep with the markers in + * MANAGEMENT_ROUTES. * * 2. DISPATCH BEHAVIOUR — one forwarding branch, driven by the registry data * (not by per-route code), serves declared Go-owned routes from the sidecar @@ -123,16 +125,28 @@ async function getJson(token: string, server: { url: URL }, pathname: string): P // --------------------------------------------------------------------------- describe("ADR-0008 ownership markers are typed read/write (ticket #14)", () => { - test("the declared Go-owned surface is exactly GET /api/system/health today", () => { + test("the declared Go-owned surface is health (volatile) and shadow-call-settings (strict) today", () => { // Pin the migrated set so an accidental marker flip on another read route // fails here instead of silently changing what the proxy serves. Adding a - // real migration updates this list deliberately. - const keys = GO_OWNED_MANAGEMENT_ROUTES.map(r => `${r.method} ${r.path}`); - expect(keys).toEqual(["GET /api/system/health"]); - const health = GO_OWNED_MANAGEMENT_ROUTES[0]!; + // real migration updates this list deliberately. Health reports the serving + // process's own pid/uptime and declares them volatile; shadow-call-settings + // is a pure function of config.json and declares NO volatile field, which + // means the oracle compares its bytes with no normalisation at all. + const byPath = new Map(GO_OWNED_MANAGEMENT_ROUTES.map(r => [r.path, r])); + expect([...byPath.keys()].sort()).toEqual([ + "/api/shadow-call-settings", + "/api/system/health", + ]); + const health = byPath.get("/api/system/health")!; + expect(health.method).toBe("GET"); expect(health.mutates).toBe(false); expect(health.module).toBe("server/management/system-routes"); expect(health.go.volatileFields).toEqual(["pid", "uptime"]); + const shadowCall = byPath.get("/api/shadow-call-settings")!; + expect(shadowCall.method).toBe("GET"); + expect(shadowCall.mutates).toBe(false); + expect(shadowCall.module).toBe("server/management/config-routes"); + expect(shadowCall.go.volatileFields).toEqual([]); }); test("no write route can be Go-owned: runtime re-check of the union's read-only arm", () => { @@ -152,20 +166,31 @@ describe("ADR-0008 ownership markers are typed read/write (ticket #14)", () => { expect(writesWithGo).toEqual([]); }); - test("every Go-owned route declares a non-empty, duplicate-free volatile set", () => { + test("every Go-owned route declares a duplicate-free volatile set (empty = strict byte equality)", () => { expect(GO_OWNED_MANAGEMENT_ROUTES.length).toBeGreaterThan(0); for (const route of GO_OWNED_MANAGEMENT_ROUTES) { - expect(route.go.volatileFields.length, `${route.method} ${route.path}`).toBeGreaterThan(0); - expect(new Set(route.go.volatileFields).size).toBe(route.go.volatileFields.length); + // An EMPTY volatile set is the strict contract: the route body must be + // byte-identical between the TS handler and the Go sidecar with no + // normalisation. A non-empty set names exactly the keys that may differ + // (process values). Either way, no key may be listed twice. + expect(Array.isArray(route.go.volatileFields), `${route.method} ${route.path}`).toBe(true); + expect(new Set(route.go.volatileFields).size, `${route.method} ${route.path}`).toBe(route.go.volatileFields.length); } }); test("the dispatch lookup is exact on method and path, and sees only the declared surface", () => { - expect(findGoOwnedManagementRoute("GET", "/api/system/health")).toBe(GO_OWNED_MANAGEMENT_ROUTES[0]); + const health = GO_OWNED_MANAGEMENT_ROUTES.find(r => r.path === "/api/system/health"); + expect(health).toBeDefined(); + expect(findGoOwnedManagementRoute("GET", "/api/system/health")).toBe(health); expect(findGoOwnedManagementRoute("POST", "/api/system/health")).toBeUndefined(); expect(findGoOwnedManagementRoute("GET", "/api/system/health/")).toBeUndefined(); expect(findGoOwnedManagementRoute("GET", "/api/system/memory")).toBeUndefined(); expect(findGoOwnedManagementRoute("GET", "/api/config")).toBeUndefined(); + const shadowCall = GO_OWNED_MANAGEMENT_ROUTES.find(r => r.path === "/api/shadow-call-settings"); + expect(shadowCall).toBeDefined(); + expect(findGoOwnedManagementRoute("GET", "/api/shadow-call-settings")).toBe(shadowCall); + expect(findGoOwnedManagementRoute("PUT", "/api/shadow-call-settings")).toBeUndefined(); + expect(findGoOwnedManagementRoute("GET", "/api/shadow-call-settings/")).toBeUndefined(); }); test("the forwarding branch in management-api.ts names no route of its own", () => { diff --git a/tests/go-sidecar-parity.test.ts b/tests/go-sidecar-parity.test.ts index ac88eecb5c..22a338835c 100644 --- a/tests/go-sidecar-parity.test.ts +++ b/tests/go-sidecar-parity.test.ts @@ -19,15 +19,19 @@ import { removeTreeWithRetry } from "./helpers/remove-tree"; * Differential oracle for the ADR-0008 Go sidecar (devlog/_plan/260905_go_sidecar_takeover). * * The TS in-process handlers and the Go ocx-sidecar must agree on status, - * headers, and the normalised body for every declared Go-owned route. - * "Normalised" is the DECLARED per-route volatile set from - * src/server/management/route-registry.ts (the `go.volatileFields` marker) and - * nothing else, so a later route cannot silently widen what parity means. + * headers, and the body for every declared Go-owned route. The per-route + * volatile set from src/server/management/route-registry.ts (the + * `go.volatileFields` marker) is exactly what may legitimately differ — and it + * may be EMPTY, which declares a strict route whose raw bytes must be equal + * with no normalisation at all (a pure config read, e.g. + * /api/shadow-call-settings). Nothing outside the declared set is ever + * forgiven, so a later route cannot silently widen what parity means. * * The divergence class this pins is the one that sank dev2-go: Go runtime * numbers rendered under JavaScript labels, or a shape that merely looks like * the TS response. The assertion is byte identity of the wire bodies after the - * declared normalisation — a JSON re-parse would forgive key-order drift and + * declared normalisation (or raw byte identity for strict routes with an empty + * volatile set) — a JSON re-parse would forgive key-order drift and * float-formatting drift that a byte compare catches. * * The harness needs the Go toolchain to build the sidecar, and boots two real @@ -70,14 +74,21 @@ const goAvailable = goToolchainAvailable(); const sidecarBinary: string | null = goAvailable ? buildSidecarBinary() : null; /** - * The declared Go-owned health route (ADR-0008): the single migrated route the - * oracle must prove today. Reads are the only surface that can be Go-owned, so - * this must exist whenever the harness runs. + * The declared Go-owned health route (ADR-0008, ticket #14): volatile pid and + * uptime, normalised by the oracle. The strict shadow-call-settings config read + * (ticket #16) is pure and declares no volatile field. The oracle must prove + * both; if a marker is ever removed it would compare nothing and pass + * vacuously. */ const goOwnedHealth = GO_OWNED_MANAGEMENT_ROUTES.find( route => route.method === "GET" && route.path === "/api/system/health", ); +/** The strict config read route (ticket #16): pure function of config.json. */ +const goOwnedShadowCallSettings = GO_OWNED_MANAGEMENT_ROUTES.find( + route => route.method === "GET" && route.path === "/api/shadow-call-settings", +); + /** * Normalise the declared volatile fields of a route body to a fixed token. * Any other difference between two bodies fails the byte comparison. The field @@ -104,6 +115,21 @@ interface HealthCapture { parsed: { status: string; service: string; version: string; uptime: number; pid: number }; } +async function captureJson(server: { url: URL }, token: string, pathname: string): Promise<{ status: number; contentType: string | null; body: string }> { + const response = await fetch(new URL(pathname, server.url), { + headers: { "x-opencodex-api-key": token }, + }); + return { + status: response.status, + contentType: response.headers.get("content-type"), + body: await response.text(), + }; +} + +async function captureShadowCall(server: { url: URL }, token: string) { + return captureJson(server, token, "/api/shadow-call-settings"); +} + async function captureHealth(server: { url: URL }, token: string): Promise { const response = await fetch(new URL("/api/system/health", server.url), { headers: { "x-opencodex-api-key": token }, @@ -195,6 +221,10 @@ describe.skipIf(!goAvailable || sidecarBinary === null)("ocx-sidecar differentia // the oracle would compare nothing and pass vacuously. expect(goOwnedHealth).toBeDefined(); expect(healthVolatileFields).toEqual(["pid", "uptime"]); + // The strict config read route must stay declared too; its contract is the + // empty volatile set (raw byte equality, no normalisation). + expect(goOwnedShadowCallSettings).toBeDefined(); + expect(goOwnedShadowCallSettings!.go.volatileFields).toEqual([]); }); runFixtureTest("in-process handler and Go sidecar agree on status, headers, and normalised body", async (token) => { @@ -303,4 +333,79 @@ describe.skipIf(!goAvailable || sidecarBinary === null)("ocx-sidecar differentia await server.stop(true); } }); + + runFixtureTest("shadow-call-settings default body is byte-identical with no normalisation", async (token) => { + // Ticket #16 first vertical slice: GET /api/shadow-call-settings is a pure + // function of config.json. The fixture has no shadowCallIntercept section, + // so both implementations must report the defaults. The declared volatile + // set is EMPTY, so this comparison normalises nothing: raw bytes must be + // equal. + const serverA = startServer(0); + try { + const tsBody = await captureShadowCall(serverA, token); + expect(tsBody.status).toBe(200); + expect(tsBody.contentType).toBe("application/json"); + // Pin the exact TS body so a Go handler that merely echoes something + // plausible but different cannot pass. + expect(tsBody.body).toBe(`{"enabled":false,"model":"","sourceModels":["gpt-5.6-luna"]}`); + + process.env[GO_SIDECAR_BIN_ENV] = sidecarBinary!; + const serverB = startServer(0); + try { + await waitFor(() => activeGoSidecarBaseUrl(), 15_000); + const goBody = await captureShadowCall(serverB, token); + expect(goBody.status).toBe(200); + expect(goBody.contentType).toBe("application/json"); + expect(goBody.body).toBe(tsBody.body); + expect(goBody.body).toBe(`{"enabled":false,"model":"","sourceModels":["gpt-5.6-luna"]}`); + } finally { + await serverB.stop(true); + } + expect(activeGoSidecarBaseUrl()).toBeNull(); + } finally { + await serverA.stop(true); + } + }); + + runFixtureTest("shadow-call-settings configured body is byte-identical and the relay alters nothing", async (token) => { + // A non-default section exercises the projection (enabled, model verbatim, + // sourceModels normalised exactly as TS normalises them) rather than the + // empty-config fallback. + saveConfig({ + ...configFixture(), + shadowCallIntercept: { + enabled: true, + model: "gpt-5.5", + sourceModels: [" gpt-5.4-mini ", "", "gpt-x"], + }, + }); + const want = `{"enabled":true,"model":"gpt-5.5","sourceModels":["gpt-5.4-mini","gpt-x"]}`; + + const serverA = startServer(0); + try { + const tsBody = await captureShadowCall(serverA, token); + expect(tsBody.body).toBe(want); + + process.env[GO_SIDECAR_BIN_ENV] = sidecarBinary!; + const serverB = startServer(0); + try { + const sidecarUrl = await waitFor(() => activeGoSidecarBaseUrl(), 15_000); + const goBody = await captureShadowCall(serverB, token); + expect(goBody.body).toBe(want); + + // The front door must relay the Go bytes without alteration, matching + // the sidecar's own direct response exactly. + const direct = await fetch(new URL("/api/shadow-call-settings", sidecarUrl), { + headers: { accept: "application/json" }, + }); + expect(direct.status).toBe(200); + expect(await direct.text()).toBe(goBody.body); + } finally { + await serverB.stop(true); + } + expect(activeGoSidecarBaseUrl()).toBeNull(); + } finally { + await serverA.stop(true); + } + }); }); From 4c64d46b6dfd4af6cc0bea73a13222f9536e6713 Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Sun, 6 Sep 2026 09:05:07 +0800 Subject: [PATCH 010/165] Go read-surface batches: strict custom-models route + ordered config JSON (#15/#16/#17) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GET /api/custom-models is the first #17 route: the TS body is JSON.stringify(config.customModels ?? []), a raw echo of a zod-passthrough config subsection. Byte parity needs document-order JSON, so the shared Go config package gains an ordered decoder and a JSON.stringify-compatible marshaler (file key order, no HTML or U+2028/U+2029 escaping, control-char shortcuts and lowercase \u00xx below U+0020, number literals verbatim) — the same substrate the /api/config provider-DTO port will need later. The route is marked strict (empty volatileFields): the differential oracle compares raw wire bytes with no normalisation, pinned against Bun for string escaping. Also records the per-route state-source decisions for the three read-surface batches (#15 system reads, #16 config reads, #17 model/provider/catalog reads): every remaining route carries a defer-to-flip verdict with a code citation (process state, live catalog/discovery caches, updater jobs, registry static data, platform probes). Pre-flip feasible residue is now fully migrated; deferred routes become the Go binary's own state at the flip. Go gates: build/vet/test green. Bun: 199 pass across the six registry/oracle suites; typecheck green; full suite 17734 pass with only the pre-existing release-version-line failure (tree state, not this diff). --- .../032_read_batches_decision_record.md | 109 +++++++ go/internal/config/ordered.go | 294 ++++++++++++++++++ go/internal/config/ordered_test.go | 153 +++++++++ go/internal/sidecar/sidecar.go | 67 +++- go/internal/sidecar/sidecar_test.go | 78 +++++ src/server/management/route-registry.ts | 11 +- tests/go-ownership-plumbing.test.ts | 19 +- tests/go-sidecar-parity.test.ts | 82 +++++ 8 files changed, 802 insertions(+), 11 deletions(-) create mode 100644 devlog/_plan/260905_go_sidecar_takeover/032_read_batches_decision_record.md create mode 100644 go/internal/config/ordered.go create mode 100644 go/internal/config/ordered_test.go diff --git a/devlog/_plan/260905_go_sidecar_takeover/032_read_batches_decision_record.md b/devlog/_plan/260905_go_sidecar_takeover/032_read_batches_decision_record.md new file mode 100644 index 0000000000..7d1110e7f9 --- /dev/null +++ b/devlog/_plan/260905_go_sidecar_takeover/032_read_batches_decision_record.md @@ -0,0 +1,109 @@ +# 032 — Tickets #15/#16/#17 per-route decision record (read-surface batches, pre-flip scope) + +Unit: `260905_go_sidecar_takeover` +Date: 2026-09-06 +Status: recorded on `dev-go`; #16 and #17 advanced by one strict route each; #15 triaged to completion +Tickets: +- [#15](https://github.com/waxiangzi/opencodex/issues/15) (system read routes batch, spec #2) +- [#16](https://github.com/waxiangzi/opencodex/issues/16) (config read routes batch, spec #2) +- [#17](https://github.com/waxiangzi/opencodex/issues/17) (model/provider/catalog read views batch, spec #2) +Parent spec: [#2](https://github.com/waxiangzi/opencodex/issues/2) (increment 2) +Gate: `030_read_surface_state_source_gate.md` — a route is pre-flip +Go-servable byte-identically only when its body is a pure function of state the +sidecar can see (env / on-disk / OS / its own process). Everything else defers +to the flip, when the Go binary IS the serving process and legitimately owns +process state. + +Owner decision (2026-09-05/06): implement #15/#16/#17 under that framework; +deferral decisions are made and recorded here rather than re-escalated. + +## Why most of these routes cannot move pre-flip + +The in-process TS handler bodies reflect the SERVING proxy's live state: +discovery caches, update jobs, effort clamps, runtime probing, Windows tray +actions, project-config scan caches. A sidecar is a separate process; it cannot +see another process's memory, and inventing a snapshot would re-create the +dev2-go divergence class (Go values rendered under TS labels). The batches' +tickets were written when the takeover was imagined as mechanical route +copying; the state-source gate is the discovery that it is not. Migrating the +pure residue now and recording the deferrals is the owner-approved pre-flip +completion of each batch. Deferred routes migrate with the binary at the flip +(themselves Go-served, so process state becomes legitimate) under #25/#40/#41. + +## Migrated in this run (machine-proven via the differential oracle) + +- **`GET /api/custom-models` → Go-owned, STRICT** (ticket #17). The TS body is + `JSON.stringify(config.customModels ?? [])` — a raw echo of a zod-passthrough + config subsection (probed: unknown keys, per-entry key order and non-schema + values all survive a save/load round trip). Byte parity therefore needs + document-order JSON, not a typed projection. Added an ordered decoder + + JSON.stringify-compatible marshaler to the shared Go config package + (`go/internal/config/ordered.go`): object keys in file order, compact + whitespace, no HTML or U+2028/U+2029 escaping, the five control-char + shortcuts, lowercase `\u00xx` below U+0020 — all pinned against Bun. This is + also the foundation the `/api/config` provider-DTO port will need (provider + entries keep their file order). Marker: strict (`volatileFields: []`), the + oracle compares raw bytes. +- (Earlier slices already closed in: #14 delivered `/api/system/health`; + devlog 031 delivered `GET /api/shadow-call-settings` for #16.) + +## Per-route verdicts + +### #15 — system reads (`src/server/management/system-routes.ts`) + +| Route | Verdict | Reason (citation) | +|---|---|---| +| `GET /api/system/health` | **Go-owned** (#14, volatile pid/uptime) | own process values | +| `GET /api/system/memory` | defer to flip | body mixes OS memory with TS-runtime-owned keys: `bunVersion`, `jscHeap`, `responseState` etc. — the serving process's runtime internals, not reproducible by a sidecar. Flip: Go owns the process, keys become its own. | +| `GET /api/system/windows-replace-retries` | defer to flip | process-local retry counter (windows binary replacement state machine). | +| `GET /api/codex-app-server` | defer to flip | reports on ~1260 lines of app-server process management (`src/codex/app-server-processes.ts`): OS process enumeration + cached state + platform heuristics. Reimplementing the machinery for byte parity pre-flip is flip-scale work, not batch work. | + +### #16 — config reads (`src/server/management/config-routes.ts`) + +| Route | Verdict | Reason (citation) | +|---|---|---| +| `GET /api/shadow-call-settings` | **Go-owned, strict** (devlog 031) | pure function of config subsection | +| `GET /api/config` | defer (registry data) | body = `withProviderServiceTierDTO(safeConfigDTO(config))`; the DTO projects per-provider registry notes, `codexAccountMode`, service-tier records, redaction policy and xai opt-in state from the TS provider registry's static per-provider data. A partial port would be silently wrong for real providers (openai, anthropic, …) — the exact dev2-go defect class. Needs the registry subset ported as shared data first; ordered JSON from this run is the substrate. | +| `GET /api/settings` | defer (live fields mixed into one body) | config-derived keys coexist in one object with `resolveCodexRuntime()` (memoized active-binary probing), `readStartupHealth(config)` (cached install-state probes) and `Intl…timeZone` (process zone). Can't split a single response object; not reproducible by a sidecar. | +| `GET /api/sidecar-settings` | defer (model-capability registry + candidate tables) | body = vision/web-search candidate rows from `findAnthropicVisionProvider` / `resolveVisionBackend` / `visionModelOptionsFor` / `webSearchCandidateRows` — provider model-capability registry + picker tables with live reachability checks (config-routes.ts:109, web-search-sidecar-options.ts:43). #17-scale registry port. | +| `GET /api/startup-health` | defer (cached install-state probe) | `readStartupHealth(config)` reads process-level cached install/service state (`invalidateStartupHealthCache` after actions). | +| `GET /api/diagnostics/project-config` | defer (process scan cache) | `getCachedProjectConfigDiagnostics()` — lazy scan cache in `src/codex/project-config-warnings.ts`; reimplementing scan+cache semantics is flip-scale. | +| `GET /api/update/check` | defer (updater state) | `checkForUpdate()` module state (update/job.ts). | +| `GET /api/update/status` | defer (job table + query param) | `readUpdateJob(jobId)` — in-memory job table; absent job id → 404 handled in-process. | +| `GET /api/windows-tray` | defer (platform probe) | non-win32 static body carries `process.platform`; win32 runs tray-action status probes. Cross-platform probe semantics, not a config read. | + +### #17 — model/provider/catalog reads (`model-routes.ts`, `provider-routes.ts`) + +| Route | Verdict | Reason (citation) | +|---|---|---| +| `GET /api/custom-models` | **Go-owned, strict** (this run) | raw config echo (see above) | +| `GET /api/models` | defer (live catalog) | `listManagementModelRows(config)` over the converged live catalog (fetchAllModels family). #17's own acceptance says "catalog reads reflect live state" — that lives with the catalog store at the flip. | +| `GET /api/catalog` | defer (persisted-catalog serializer) | `serializePersistedCatalog()` from `src/server/catalog-download.ts` — a large deterministic serializer over the Codex-converged catalog; port is flip-scale model-store work, plus corsHeaders sharing. | +| `GET /api/client-config` | defer (catalog rows) | rows over the converged catalog. | +| `GET /api/model-discovery` / `/api/selected-models` / `/api/model-presets` | defer (live catalog + discovery) | `fetchAllModels(config)`, `getProviderLiveModelCount`, `materializeModelPreset` over the live catalog. | +| `GET /api/aliases` | defer (live /models cache) | `knownModelIdsForProvider` unions in `getStaleCached(provName)` (router.ts:99) — the last-known-good live /models cache; catalog drift handling (`builtinRule`) is registry-side. | +| `GET /api/providers` | defer (live keys in one body) | config-derived keys share one object with live `discovery` status and openai entitlement state. | +| `GET /api/provider-context-caps` | defer (in-memory caps module) | live context-capability state. | +| `GET /api/provider-presets` | defer (derive-provider-presets static table) | registry-derived preset table (`src/providers/derive.ts`) — static data port, moderate; folded into the registry-subset work that unblocks /api/config. | +| `GET /api/provider-request-pacing` | defer (live pacers) | `providerRequestPacingStatus` reads the module-level `pacers` map: queue depths, next-slot timestamps, last-start (src/providers/request-pacing.ts). `enabled` is config but is one key in a live body. | +| `GET /api/provider-quotas` | defer | live quota/usage cache (usage-quota surface, #20 family). | + +## Registry and oracle state after this run + +Three read routes are Go-owned: `/api/system/health` (volatile pid/uptime), +`/api/shadow-call-settings` (strict), `/api/custom-models` (strict). The strict +pair exercises the empty-volatile contract against real wire bytes. Nothing in +`management-api.ts` names a route (pinned by test 7 of +`tests/go-ownership-plumbing.test.ts`); adding the next route stays a marker +flip + Go handler + oracle cases. + +## What unblocks the deferred reads at the flip + +- Registry static data as shared Go data (provider notes, codexAccountMode, + presets, alias rules) → then `/api/config`, `/api/provider-presets`, + `/api/aliases`' registry half. +- The live model store/catalog moves into the Go binary with the takeover + (#25 + the #40/#41 flip line) → `/api/models`, `/api/catalog`, discovery + views, sidecar-settings candidates become the binary's own state. +- Process-state routes (memory, update jobs, clamps, tray, diagnostics caches) + become the binary's own process state at the flip — no snapshot problem. diff --git a/go/internal/config/ordered.go b/go/internal/config/ordered.go new file mode 100644 index 0000000000..e83e91571d --- /dev/null +++ b/go/internal/config/ordered.go @@ -0,0 +1,294 @@ +package config + +// Ordered JSON support for config echo routes (ticket #17). +// +// The TypeScript runtime stores the config it parsed and emits it again with +// JSON.stringify when a route body is the config value itself (e.g. GET +// /api/custom-models returns `config.customModels ?? []`). JSON.stringify +// preserves object key insertion order — which for a parsed config file is the +// FILE's key order — and canonicalises whitespace. encoding/json's map-based +// decoding discards key order, so a faithful echo needs a value tree that keeps +// objects ordered. That order is exactly what the /api/config DTO projection +// will also need when it is ported (provider entries keep their file order), so +// this is the shared foundation, not a one-route hack. +// +// Numbers are kept as their raw JSON literal (json.RawMessage). A config file +// written by the TypeScript runtime already contains JSON.stringify-canonical +// numbers, so raw echo equals JavaScript output; only a hand-edited file with a +// non-canonical literal (e.g. "1.0" where JavaScript would emit 1) diverges, +// which is the same class of documented non-canonical-config caveat the +// package already carries. + +import ( + "bytes" + "encoding/json" + "errors" + "io" + "os" + "path/filepath" +) + +// OrderedValue is one JSON value with object keys in document order. Only the +// operations the config echo routes need are exported: Find (key lookup), +// IsNull, and MarshalStringify (JSON.stringify-compatible bytes). +type OrderedValue struct { + kind orderedKind + obj []orderedMember // kind == orderedObject, keyed in file order + arr []*OrderedValue // kind == orderedArray + str string // kind == orderedString + num json.RawMessage // kind == orderedNumber, raw literal + b bool // kind == orderedBool +} + +type orderedKind int + +const ( + orderedNull orderedKind = iota + orderedObject + orderedArray + orderedString + orderedNumber + orderedBool +) + +type orderedMember struct { + key string + val *OrderedValue +} + +// LoadOrdered reads and decodes config.json into an ordered value tree (the +// root object). A missing file yields a null root without error, mirroring +// Load's ENOENT default; a malformed file yields the decode error. +func LoadOrdered() (*OrderedValue, error) { + path, err := Path() + if err != nil { + return nil, err + } + return LoadOrderedFromPath(path) +} + +// LoadOrderedFromDir is LoadOrdered with an explicit config directory. +func LoadOrderedFromDir(dir string) (*OrderedValue, error) { + return LoadOrderedFromPath(filepath.Join(dir, "config.json")) +} + +// LoadOrderedFromPath is the raw ordered loader; the path comes from Path() or +// LoadOrderedFromDir. +func LoadOrderedFromPath(path string) (*OrderedValue, error) { + file, err := os.Open(path) + if err != nil { + if errors.Is(err, os.ErrNotExist) { + return &OrderedValue{kind: orderedNull}, nil + } + return nil, err + } + defer file.Close() + return decodeOrdered(file) +} + +// decodeOrdered decodes a whole JSON document into an ordered value tree. +func decodeOrdered(reader io.Reader) (*OrderedValue, error) { + decoder := json.NewDecoder(reader) + decoder.UseNumber() + value, err := decodeOrderedNext(decoder) + if err != nil { + return nil, err + } + // A second value in the stream means the file is not a single JSON + // document; the TS side would reject that too. + if _, err := decoder.Token(); err != io.EOF { + if err == nil { + return nil, errors.New("config.json contains more than one JSON value") + } + return nil, err + } + return value, nil +} + +// decodeOrderedNext recurses one JSON value from the token stream. +func decodeOrderedNext(decoder *json.Decoder) (*OrderedValue, error) { + token, err := decoder.Token() + if err != nil { + return nil, err + } + return decodeOrderedValue(decoder, token) +} + +func decodeOrderedValue(decoder *json.Decoder, token json.Token) (*OrderedValue, error) { + switch typed := token.(type) { + case nil: + return &OrderedValue{kind: orderedNull}, nil + case bool: + return &OrderedValue{kind: orderedBool, b: typed}, nil + case string: + return &OrderedValue{kind: orderedString, str: typed}, nil + case json.Number: + return &OrderedValue{kind: orderedNumber, num: json.RawMessage(typed.String())}, nil + case json.Delim: + switch typed { + case '{': + obj := &OrderedValue{kind: orderedObject} + for decoder.More() { + keyToken, err := decoder.Token() + if err != nil { + return nil, err + } + key, ok := keyToken.(string) + if !ok { + return nil, errors.New("config.json object key is not a string") + } + member, err := decodeOrderedNext(decoder) + if err != nil { + return nil, err + } + obj.obj = append(obj.obj, orderedMember{key: key, val: member}) + } + // Consume the closing '}'. + if _, err := decoder.Token(); err != nil { + return nil, err + } + return obj, nil + case '[': + arr := &OrderedValue{kind: orderedArray} + for decoder.More() { + member, err := decodeOrderedNext(decoder) + if err != nil { + return nil, err + } + arr.arr = append(arr.arr, member) + } + // Consume the closing ']'. + if _, err := decoder.Token(); err != nil { + return nil, err + } + return arr, nil + default: + return nil, errors.New("config.json contains an unexpected delimiter") + } + default: + return nil, errors.New("config.json contains an unsupported token") + } +} + +// Find returns the member with the given key, or nil when absent. Object key +// order is preserved by decodeOrdered; callers that re-emit the value rely on +// that order being the file's. +func (v *OrderedValue) Find(key string) *OrderedValue { + if v == nil || v.kind != orderedObject { + return nil + } + for _, member := range v.obj { + if member.key == key { + return member.val + } + } + return nil +} + +// IsNull reports whether the value is the JSON null literal. +func (v *OrderedValue) IsNull() bool { + return v != nil && v.kind == orderedNull +} + +// MarshalStringify writes the value the way ECMAScript JSON.stringify does: +// compact, no HTML or U+2028/U+2029 escaping, object keys in document order. +// Number literals are emitted verbatim (see the package comment for why raw +// echo equals JavaScript output for TypeScript-written config files). +func (v *OrderedValue) MarshalStringify() ([]byte, error) { + var out bytes.Buffer + if err := v.marshalJSONStringify(&out); err != nil { + return nil, err + } + return out.Bytes(), nil +} + +func (v *OrderedValue) marshalJSONStringify(out *bytes.Buffer) error { + switch v.kind { + case orderedNull: + out.WriteString("null") + case orderedBool: + if v.b { + out.WriteString("true") + } else { + out.WriteString("false") + } + case orderedString: + raw, err := marshalStringJSONStringify(v.str) + if err != nil { + return err + } + out.Write(raw) + case orderedNumber: + out.Write(v.num) + case orderedArray: + out.WriteByte('[') + for i, member := range v.arr { + if i > 0 { + out.WriteByte(',') + } + if err := member.marshalJSONStringify(out); err != nil { + return err + } + } + out.WriteByte(']') + case orderedObject: + out.WriteByte('{') + for i, member := range v.obj { + if i > 0 { + out.WriteByte(',') + } + rawKey, err := marshalStringJSONStringify(member.key) + if err != nil { + return err + } + out.Write(rawKey) + out.WriteByte(':') + if err := member.val.marshalJSONStringify(out); err != nil { + return err + } + } + out.WriteByte('}') + } + return nil +} + +// marshalStringJSONStringify encodes one string the way ECMAScript +// JSON.stringify does. encoding/json cannot be used directly: with HTML +// escaping disabled it still escapes U+2028/U+2029, while V8 emits them +// literally (verified against Bun), so the escaping is done here by hand. +// Rules pinned against JSON.stringify: quotes and backslashes are escaped, the +// five control characters get \b \t \n \f \r shortcuts, other code points +// below U+0020 become \u00xx (lowercase), and everything else — DEL, U+0080, +// U+2028/U+2029 included — is emitted literally as UTF-8. +func marshalStringJSONStringify(value string) ([]byte, error) { + var out bytes.Buffer + out.WriteByte('"') + for _, r := range value { + switch r { + case '"', '\\': + out.WriteByte('\\') + out.WriteRune(r) + case '\b': + out.WriteString(`\b`) + case '\t': + out.WriteString(`\t`) + case '\n': + out.WriteString(`\n`) + case '\f': + out.WriteString(`\f`) + case '\r': + out.WriteString(`\r`) + default: + if r < 0x20 { + hexDigits := "0123456789abcdef" + out.WriteString(`\u00`) + out.WriteByte(hexDigits[r>>4]) + out.WriteByte(hexDigits[r&0xf]) + } else { + out.WriteRune(r) + } + } + } + out.WriteByte('"') + return out.Bytes(), nil +} diff --git a/go/internal/config/ordered_test.go b/go/internal/config/ordered_test.go new file mode 100644 index 0000000000..8347c9e7b6 --- /dev/null +++ b/go/internal/config/ordered_test.go @@ -0,0 +1,153 @@ +package config + +import ( + "strings" + "testing" +) + +// TestOrderedEchoPreservesKeyOrderAndCanonicalisesWhitespace is the byte-parity +// core: the TS handler returns JSON.stringify(config.customModels), which keeps +// each entry's FILE key order (zod passthrough does not reorder) and emits no +// whitespace. The fixture is pretty-printed with keys deliberately NOT in the +// schema's field order to prove the echo follows the file, not a struct. +func TestOrderedEchoPreservesKeyOrderAndCanonicalisesWhitespace(t *testing.T) { + dir := t.TempDir() + writeFixture(t, dir, `{ + "port": 18080, + "customModels": [ + { + "zetaField": 1, + "provider": "test", + "modelId": "custom-a", + "displayName": "Custom A", + "contextWindow": 99999 + }, + { "provider": "anthropic", "modelId": "custom-b" } + ] +}`) + root, err := LoadOrderedFromDir(dir) + if err != nil { + t.Fatal(err) + } + models := root.Find("customModels") + if models == nil { + t.Fatal("customModels not found in the ordered document") + } + raw, err := models.MarshalStringify() + if err != nil { + t.Fatal(err) + } + want := `[{"zetaField":1,"provider":"test","modelId":"custom-a","displayName":"Custom A","contextWindow":99999},{"provider":"anthropic","modelId":"custom-b"}]` + if string(raw) != want { + t.Fatalf("echo = %s\nwant %s", raw, want) + } +} + +// TestOrderedEchoMissingFileYieldsNullRoot mirrors Load's ENOENT default: an +// absent config file must not error the ordered loader either. +func TestOrderedEchoMissingFileYieldsNullRoot(t *testing.T) { + root, err := LoadOrderedFromDir(t.TempDir()) + if err != nil { + t.Fatal(err) + } + if !root.IsNull() { + t.Fatalf("expected a null root for a missing file, got %+v", root) + } + if found := root.Find("customModels"); found != nil { + t.Fatal("Find on a null root must return nil") + } +} + +// TestOrderedEchoNullValueIsFindable mirrors the TS `config.customModels ?? []` +// nullish coalescing: a configured null is a present value that projects to []. +func TestOrderedEchoNullValueIsFindable(t *testing.T) { + dir := t.TempDir() + writeFixture(t, dir, `{"customModels": null}`) + root, err := LoadOrderedFromDir(dir) + if err != nil { + t.Fatal(err) + } + models := root.Find("customModels") + if models == nil || !models.IsNull() { + t.Fatalf("customModels must be findable and null, got %+v", models) + } +} + +// TestOrderedEchoStringEscapingMatchesJSONStringify pins the escaping contract +// against JSON.stringify: <, >, &, U+2028 and U+2029 are all emitted literally +// (no HTML escaping, no \u2028), while the five named control characters get +// shortcuts and other code points below U+0020 get lowercase \u00xx escapes. +func TestOrderedEchoStringEscapingMatchesJSONStringify(t *testing.T) { + dir := t.TempDir() + // The \u00xx and \u2028 file escapes exercise the decoder -> re-encoder + // round trip; the literal control byte 0x01 exercises direct echo. + writeFixture(t, dir, "{\"customModels\": [{\"modelId\": \"a&c\\u2028d\\u2029e\\u0001f\\t\", \"provider\": \"x\"}]}") + root, err := LoadOrderedFromDir(dir) + if err != nil { + t.Fatal(err) + } + raw, err := root.Find("customModels").MarshalStringify() + if err != nil { + t.Fatal(err) + } + if string(raw) != expectedEcho() { + t.Fatalf("escape echo = %q\nwant %q", raw, expectedEcho()) + } +} + +// expectedEcho is the exact JSON.stringify output for the fixture above, +// verified against Bun: <, >, &, U+2028 and U+2029 literal; 0x01 as \u0001; +// the tab as \t. +func expectedEcho() string { + return `[{"modelId":"a&c` + "\u2028" + `d` + "\u2029" + `e\u0001f\t","provider":"x"}]` +} + +// TestOrderedEchoNumberLiteralStaysVerbatim documents the number-literal rule: +// a TypeScript-written file already carries JSON.stringify-canonical numbers so +// raw echo equals JS output. A hand-edited non-canonical literal ("1.0") is +// echoed as-is by the Go side where JS would emit 1 — the package's documented +// non-canonical-config divergence, asserted here so the behavior is pinned. +func TestOrderedEchoNumberLiteralStaysVerbatim(t *testing.T) { + dir := t.TempDir() + writeFixture(t, dir, `{"customModels": [{"modelId": "a", "contextWindow": 1.0}]}`) + root, err := LoadOrderedFromDir(dir) + if err != nil { + t.Fatal(err) + } + raw, err := root.Find("customModels").MarshalStringify() + if err != nil { + t.Fatal(err) + } + if !strings.Contains(string(raw), `"contextWindow":1.0`) { + t.Fatalf("non-canonical literal must be echoed verbatim, got %s", raw) + } +} + +// TestOrderedEchoNestedStructures exercises arrays of objects and nested keys, +// which is the shape a future DTO projection (provider entries) will need. +func TestOrderedEchoNestedStructures(t *testing.T) { + dir := t.TempDir() + writeFixture(t, dir, `{"a": {"b": [1, true, null, {"z": 1, "y": 2}]}, "c": "tail"}`) + root, err := LoadOrderedFromDir(dir) + if err != nil { + t.Fatal(err) + } + raw, err := root.MarshalStringify() + if err != nil { + t.Fatal(err) + } + want := `{"a":{"b":[1,true,null,{"z":1,"y":2}]},"c":"tail"}` + if string(raw) != want { + t.Fatalf("nested echo = %s\nwant %s", raw, want) + } +} + +// TestOrderedEchoMalformedFileErrors mirrors LoadFromPath: the ordered loader +// must report a malformed file instead of silently echoing a partial value. +func TestOrderedEchoMalformedFileErrors(t *testing.T) { + dir := t.TempDir() + writeFixture(t, dir, `{"customModels": [`) + if _, err := LoadOrderedFromDir(dir); err == nil { + t.Fatal("expected a decode error for a truncated file") + } +} diff --git a/go/internal/sidecar/sidecar.go b/go/internal/sidecar/sidecar.go index 70e558552d..5833f469ca 100644 --- a/go/internal/sidecar/sidecar.go +++ b/go/internal/sidecar/sidecar.go @@ -2,13 +2,11 @@ // incremental runtime takeover (ADR-0008, devlog/_plan/260905_go_sidecar_takeover). // // Today it owns GET /api/system/health (volatile pid/uptime normalised by the -// oracle) and GET /api/shadow-call-settings (a pure function of config.json, -// compared with no normalisation at all). Each handler must reproduce the -// TypeScript handler's HTTP semantics byte-for-byte: the shape, key order, and -// number formatting of the JSON body are part of the contract. The Bun -// differential harness compares the wire bodies, so this package's payload -// struct field order and its use of encoding/json (shortest round-trip number -// formatting, matching ECMAScript) are load-bearing, not cosmetic. +// oracle), GET /api/shadow-call-settings (a pure function of config.json, +// compared with no normalisation at all) and GET /api/custom-models (the raw +// config.customModels echo, also compared byte-for-byte). Each handler must +// reproduce the TypeScript handler's HTTP semantics byte-for-byte: the shape, +// key order, and number formatting of the JSON body are part of the contract. package sidecar import ( @@ -112,6 +110,39 @@ func NewHandler(cfg Config) http.Handler { } respondJSON(w, payload, "shadow-call-settings") }) + + // GET /api/custom-models (ticket #17): the TS handler returns + // JSON.stringify(config.customModels ?? []), i.e. the config subsection echoed + // back. The subsection is a passthrough in the zod pipeline (verified: unknown + // keys, key order and non-schema values all survive a save/load round trip), so + // byte parity needs a document-order echo rather than a typed projection. The + // sidecar reads the file with the ordered loader and emits JSON.stringify- + // compatible bytes (compact, insertion order kept, no HTML or U+2028/U+2029 + // escaping, number literals verbatim). Absent or null customModels coalesce to + // [] exactly like the TS nullish operator. + mux.HandleFunc("GET /api/custom-models", func(w http.ResponseWriter, r *http.Request) { + root, err := loadSidecarOrdered(cfg.ConfigDir) + if err != nil { + // A missing file yields a null root (no error). A malformed file would + // have been salvaged by the TS runtime at startup; without it the + // echo degrades to the nullish fallback, never to a partial body. + root = nil + } + customModels := root.Find("customModels") + var raw []byte + if customModels == nil || customModels.IsNull() { + raw = []byte("[]") + } else { + raw, err = customModels.MarshalStringify() + if err != nil { + // The ordered tree was decoded from valid JSON, so marshal cannot + // fail; stay silent rather than emit a partial body. + fmt.Fprintf(os.Stderr, "ocx-sidecar: marshal custom-models echo: %v\n", err) + return + } + } + writeRawJSON(w, raw, "custom-models") + }) return mux } @@ -125,6 +156,28 @@ func loadSidecarConfig(configDir string) (*config.Config, error) { return config.Load() } +// loadSidecarOrdered loads config.json through the ordered decoder used by the +// echo routes. An explicit dir (unit tests) wins; otherwise the same +// OPENCODEX_HOME then ~/.opencodex resolution the TS parent uses at spawn. +func loadSidecarOrdered(configDir string) (*config.OrderedValue, error) { + if configDir != "" { + return config.LoadOrderedFromDir(configDir) + } + return config.LoadOrdered() +} + +// writeRawJSON writes pre-marshalled bytes exactly the way the TS handlers +// emit jsonResponse: Content-Type application/json, 200, no trailing newline. +// The echo routes marshal through the ordered tree first, so the bytes are +// already the byte contract; re-marshalling would reformat them. +func writeRawJSON(w http.ResponseWriter, raw []byte, owner string) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + if _, err := w.Write(raw); err != nil { + fmt.Fprintf(os.Stderr, "ocx-sidecar: write %s payload: %v\n", owner, err) + } +} + // respondJSON writes a fixed-shape payload exactly the way the TS handlers // emit jsonResponse: Content-Type application/json, a 200 status, and the // marshalled bytes with NO trailing newline (Encoder.Encode would append one diff --git a/go/internal/sidecar/sidecar_test.go b/go/internal/sidecar/sidecar_test.go index 801d0506d8..dfb62c0ff2 100644 --- a/go/internal/sidecar/sidecar_test.go +++ b/go/internal/sidecar/sidecar_test.go @@ -250,3 +250,81 @@ func TestShadowCallSettingsSurfaceIsNarrow(t *testing.T) { } } +func TestCustomModelsEchoVerbatim(t *testing.T) { + dir := t.TempDir() + // Pretty-printed, unknown per-entry keys, key order NOT matching any schema: + // the echo must follow the file (JSON.stringify of the parsed value), not a + // Go struct. + writeConfigFile(t, dir, `{ + "port": 18080, + "customModels": [ + { "zetaField": 1, "provider": "test", "modelId": "custom-a", "displayName": "Custom A", "contextWindow": 99999 }, + { "provider": "anthropic", "modelId": "custom-b" } + ] +}`) + h := NewHandler(Config{ConfigDir: dir}) + resp := do(t, h, http.MethodGet, "/api/custom-models") + if resp.StatusCode != http.StatusOK { + t.Fatalf("status = %d, want 200", resp.StatusCode) + } + if got := resp.Header.Get("Content-Type"); got != "application/json" { + t.Fatalf("Content-Type = %q, want application/json", got) + } + raw, err := io.ReadAll(resp.Body) + if err != nil { + t.Fatal(err) + } + want := `[{"zetaField":1,"provider":"test","modelId":"custom-a","displayName":"Custom A","contextWindow":99999},{"provider":"anthropic","modelId":"custom-b"}]` + if string(raw) != want { + t.Fatalf("body = %s\nwant %s", raw, want) + } +} + +func TestCustomModelsDefaultsWithoutConfig(t *testing.T) { + // No customModels key and no config file at all both coalesce to [] (the TS + // handler's `config.customModels ?? []`). + for _, tc := range []struct { + name string + content string + }{ + {"missing key", `{"port": 18080}`}, + {"null value", `{"customModels": null}`}, + {"no file", ""}, + } { + dir := t.TempDir() + if tc.content != "" { + writeConfigFile(t, dir, tc.content) + } + h := NewHandler(Config{ConfigDir: dir}) + resp := do(t, h, http.MethodGet, "/api/custom-models") + raw, err := io.ReadAll(resp.Body) + resp.Body.Close() + if err != nil { + t.Fatal(err) + } + if string(raw) != "[]" { + t.Errorf("%s: body = %s, want []", tc.name, raw) + } + } +} + +func TestCustomModelsSurfaceIsNarrow(t *testing.T) { + h := NewHandler(Config{ConfigDir: t.TempDir()}) + cases := []struct { + method string + path string + want int + }{ + {http.MethodPost, "/api/custom-models", http.StatusMethodNotAllowed}, + {http.MethodPut, "/api/custom-models", http.StatusMethodNotAllowed}, + {http.MethodGet, "/api/custom-models/", http.StatusNotFound}, + {http.MethodGet, "/api/custom-models?x=1", http.StatusOK}, + } + for _, tc := range cases { + resp := do(t, h, tc.method, tc.path) + resp.Body.Close() + if resp.StatusCode != tc.want { + t.Errorf("%s %s status = %d, want %d", tc.method, tc.path, resp.StatusCode, tc.want) + } + } +} diff --git a/src/server/management/route-registry.ts b/src/server/management/route-registry.ts index 972cd840af..d2e5b802ba 100644 --- a/src/server/management/route-registry.ts +++ b/src/server/management/route-registry.ts @@ -279,7 +279,16 @@ export const MANAGEMENT_ROUTES: readonly ManagementRoute[] = [ { method: "GET", path: "/api/aliases", module: "server/management/model-routes", mutates: false }, { method: "GET", path: "/api/catalog", module: "server/management/model-routes", mutates: false }, { method: "GET", path: "/api/client-config", module: "server/management/model-routes", mutates: false }, - { method: "GET", path: "/api/custom-models", module: "server/management/model-routes", mutates: false }, + // ADR-0008 ownership: GET /api/custom-models is Go-owned (ticket #17). The + // body is JSON.stringify(config.customModels ?? []): a raw echo of the + // config subsection, which is a passthrough in the zod pipeline (unknown + // keys, key order and non-schema values survive a save/load round trip). So + // it also declares an EMPTY volatile set: the oracle compares raw bytes with + // no normalisation. The Go handler (go/internal/sidecar) reads the same file + // through the ordered decoder (go/internal/config) so object key order and + // JSON.stringify escaping match exactly; absent or null customModels coalesce + // to [] like the TS nullish operator. + { method: "GET", path: "/api/custom-models", module: "server/management/model-routes", mutates: false, go: { volatileFields: [] } }, { method: "GET", path: "/api/model-discovery", module: "server/management/model-routes", mutates: false }, { method: "GET", path: "/api/model-presets", module: "server/management/model-routes", mutates: false }, { method: "GET", path: "/api/models", module: "server/management/model-routes", mutates: false }, diff --git a/tests/go-ownership-plumbing.test.ts b/tests/go-ownership-plumbing.test.ts index 3bdd585c4a..3bb1535443 100644 --- a/tests/go-ownership-plumbing.test.ts +++ b/tests/go-ownership-plumbing.test.ts @@ -125,15 +125,17 @@ async function getJson(token: string, server: { url: URL }, pathname: string): P // --------------------------------------------------------------------------- describe("ADR-0008 ownership markers are typed read/write (ticket #14)", () => { - test("the declared Go-owned surface is health (volatile) and shadow-call-settings (strict) today", () => { + test("the declared Go-owned surface is health (volatile), shadow-call-settings (strict) and custom-models (strict) today", () => { // Pin the migrated set so an accidental marker flip on another read route // fails here instead of silently changing what the proxy serves. Adding a // real migration updates this list deliberately. Health reports the serving // process's own pid/uptime and declares them volatile; shadow-call-settings - // is a pure function of config.json and declares NO volatile field, which - // means the oracle compares its bytes with no normalisation at all. + // and custom-models are pure functions of config.json (the latter a raw + // JSON.stringify echo) and declare NO volatile field, which means the + // oracle compares their bytes with no normalisation at all. const byPath = new Map(GO_OWNED_MANAGEMENT_ROUTES.map(r => [r.path, r])); expect([...byPath.keys()].sort()).toEqual([ + "/api/custom-models", "/api/shadow-call-settings", "/api/system/health", ]); @@ -147,6 +149,11 @@ describe("ADR-0008 ownership markers are typed read/write (ticket #14)", () => { expect(shadowCall.mutates).toBe(false); expect(shadowCall.module).toBe("server/management/config-routes"); expect(shadowCall.go.volatileFields).toEqual([]); + const customModels = byPath.get("/api/custom-models")!; + expect(customModels.method).toBe("GET"); + expect(customModels.mutates).toBe(false); + expect(customModels.module).toBe("server/management/model-routes"); + expect(customModels.go.volatileFields).toEqual([]); }); test("no write route can be Go-owned: runtime re-check of the union's read-only arm", () => { @@ -191,6 +198,11 @@ describe("ADR-0008 ownership markers are typed read/write (ticket #14)", () => { expect(findGoOwnedManagementRoute("GET", "/api/shadow-call-settings")).toBe(shadowCall); expect(findGoOwnedManagementRoute("PUT", "/api/shadow-call-settings")).toBeUndefined(); expect(findGoOwnedManagementRoute("GET", "/api/shadow-call-settings/")).toBeUndefined(); + const customModels = GO_OWNED_MANAGEMENT_ROUTES.find(r => r.path === "/api/custom-models"); + expect(customModels).toBeDefined(); + expect(findGoOwnedManagementRoute("GET", "/api/custom-models")).toBe(customModels); + expect(findGoOwnedManagementRoute("POST", "/api/custom-models")).toBeUndefined(); + expect(findGoOwnedManagementRoute("GET", "/api/custom-models/")).toBeUndefined(); }); test("the forwarding branch in management-api.ts names no route of its own", () => { @@ -205,6 +217,7 @@ describe("ADR-0008 ownership markers are typed read/write (ticket #14)", () => { expect(src).toContain("tryForwardGoOwnedRoute"); expect(src).not.toContain('"/api/system/health"'); expect(src).not.toContain('"/api/system/memory"'); + expect(src).not.toContain('"/api/custom-models"'); expect(src).not.toContain("tryForwardGoOwnedRoute(\"/"); }); }); diff --git a/tests/go-sidecar-parity.test.ts b/tests/go-sidecar-parity.test.ts index 22a338835c..9c8449b816 100644 --- a/tests/go-sidecar-parity.test.ts +++ b/tests/go-sidecar-parity.test.ts @@ -130,6 +130,10 @@ async function captureShadowCall(server: { url: URL }, token: string) { return captureJson(server, token, "/api/shadow-call-settings"); } +async function captureCustomModels(server: { url: URL }, token: string) { + return captureJson(server, token, "/api/custom-models"); +} + async function captureHealth(server: { url: URL }, token: string): Promise { const response = await fetch(new URL("/api/system/health", server.url), { headers: { "x-opencodex-api-key": token }, @@ -225,6 +229,12 @@ describe.skipIf(!goAvailable || sidecarBinary === null)("ocx-sidecar differentia // empty volatile set (raw byte equality, no normalisation). expect(goOwnedShadowCallSettings).toBeDefined(); expect(goOwnedShadowCallSettings!.go.volatileFields).toEqual([]); + // The strict raw-echo config route (ticket #17) must stay declared too. + const customModels = GO_OWNED_MANAGEMENT_ROUTES.find( + route => route.method === "GET" && route.path === "/api/custom-models", + ); + expect(customModels).toBeDefined(); + expect(customModels!.go.volatileFields).toEqual([]); }); runFixtureTest("in-process handler and Go sidecar agree on status, headers, and normalised body", async (token) => { @@ -408,4 +418,76 @@ describe.skipIf(!goAvailable || sidecarBinary === null)("ocx-sidecar differentia await serverA.stop(true); } }); + + runFixtureTest("custom-models default body is byte-identical with no normalisation", async (token) => { + // Ticket #17: GET /api/custom-models is a raw echo of the config's + // customModels subsection (JSON.stringify(config.customModels ?? [])). The + // fixture carries no customModels key, so both implementations must report + // the nullish fallback []. Empty volatile set: raw bytes must be equal. + const serverA = startServer(0); + try { + const tsBody = await captureCustomModels(serverA, token); + expect(tsBody.status).toBe(200); + expect(tsBody.contentType).toBe("application/json"); + expect(tsBody.body).toBe(`[]`); + + process.env[GO_SIDECAR_BIN_ENV] = sidecarBinary!; + const serverB = startServer(0); + try { + await waitFor(() => activeGoSidecarBaseUrl(), 15_000); + const goBody = await captureCustomModels(serverB, token); + expect(goBody.status).toBe(200); + expect(goBody.contentType).toBe("application/json"); + expect(goBody.body).toBe(tsBody.body); + expect(goBody.body).toBe(`[]`); + } finally { + await serverB.stop(true); + } + expect(activeGoSidecarBaseUrl()).toBeNull(); + } finally { + await serverA.stop(true); + } + }); + + runFixtureTest("custom-models configured body is byte-identical (unknown keys and file order kept)", async (token) => { + // A non-default section exercises the echo, not the fallback: unknown + // per-entry keys survive, and each entry's key order follows the file + // (JSON.stringify of the parsed value, not a schema). saveConfig persists + // customModels as a passthrough (probed), so the fixture keeps its order. + saveConfig({ + ...configFixture(), + customModels: [ + { zetaField: 1, provider: "test", modelId: "custom-a", displayName: "Custom A", contextWindow: 99999 }, + { provider: "anthropic", modelId: "custom-b" }, + ], + }); + const want = `[{"zetaField":1,"provider":"test","modelId":"custom-a","displayName":"Custom A","contextWindow":99999},{"provider":"anthropic","modelId":"custom-b"}]`; + + const serverA = startServer(0); + try { + const tsBody = await captureCustomModels(serverA, token); + expect(tsBody.body).toBe(want); + + process.env[GO_SIDECAR_BIN_ENV] = sidecarBinary!; + const serverB = startServer(0); + try { + const sidecarUrl = await waitFor(() => activeGoSidecarBaseUrl(), 15_000); + const goBody = await captureCustomModels(serverB, token); + expect(goBody.body).toBe(want); + + // The front door must relay the Go bytes without alteration, matching + // the sidecar's own direct response exactly. + const direct = await fetch(new URL("/api/custom-models", sidecarUrl), { + headers: { accept: "application/json" }, + }); + expect(direct.status).toBe(200); + expect(await direct.text()).toBe(goBody.body); + } finally { + await serverB.stop(true); + } + expect(activeGoSidecarBaseUrl()).toBeNull(); + } finally { + await serverA.stop(true); + } + }); }); From 8e4c0d5103936e5213e183a37d98c3e92b6f4861 Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Sun, 6 Sep 2026 09:33:58 +0800 Subject: [PATCH 011/165] Go management auth model + Lab activation gate/seam (#18, #19) Ticket #18: go/internal/managementauth reproduces the management admission decision of src/server/management-auth.ts: admin-token equality (env/file resolution, no secret-file mutation), dashboard-session authorization with a port of managementRequestOrigin (loopback observed origin, non-loopback api-auth rule, hub public-origin override, WHATWG origin serialisation), all four process-scoped capability HMAC contracts with their replay stores, and the exact 401/503 rejection bodies. Substrate: the TS front door still admits every management request pre-flip; the write batches (#21-#23) and the authorization gate (#26) consume this when Go answers without that front door. Ticket #19: go/internal/labactivation reproduces the Lab opt-in gate (routingProfiles non-empty, or automation enabled on disk with automation-config.json authoritative over the legacy automation-policy.json), and go/internal/routing/compatibility reproduces the core-owned evidence-provider slot (set/resolve/detach-own-registration). The seam registers only when the gate says the install uses Lab; real Lab content arrives with #33. A no-Lab user executes no Lab code because no Go package imports Lab content at all. Machine proof, not prose: Go unit tests plus two differential oracles that run the same inputs through the TypeScript side and the Go side. authcheck subcommand evaluates ordered vector arrays in one Go process (replay stores persist like the TS module-level maps) and the oracle compares principal or exact status+body plus the session admission reason: 7 suites, every principal and rejection path. labcheck answers the gate for fixture dirs Go reads before the TS loader can repair them: 10 fixtures. Both subcommands are inert on the live path (the supervisor passes no argument). Gates: go build/vet/test green; typecheck green; focused suites 187 pass; full suite 17751 pass with only the pre-existing release-version-line failure (tree state, not this diff). --- .../033_auth_and_lab_gate_substrate.md | 104 ++++ go/README.md | 26 + go/cmd/ocx-sidecar/authcheck.go | 201 +++++++ go/cmd/ocx-sidecar/labcheck.go | 47 ++ go/cmd/ocx-sidecar/main.go | 17 + go/internal/labactivation/activation.go | 140 +++++ go/internal/labactivation/activation_test.go | 201 +++++++ go/internal/managementauth/auth.go | 80 +++ go/internal/managementauth/capability.go | 430 +++++++++++++++ go/internal/managementauth/gate.go | 451 +++++++++++++++ .../managementauth/managementauth_test.go | 518 ++++++++++++++++++ go/internal/managementauth/session.go | 228 ++++++++ go/internal/routing/compatibility/slot.go | 86 +++ tests/go-auth-parity.test.ts | 462 ++++++++++++++++ tests/go-lab-gate-parity.test.ts | 185 +++++++ 15 files changed, 3176 insertions(+) create mode 100644 devlog/_plan/260905_go_sidecar_takeover/033_auth_and_lab_gate_substrate.md create mode 100644 go/cmd/ocx-sidecar/authcheck.go create mode 100644 go/cmd/ocx-sidecar/labcheck.go create mode 100644 go/internal/labactivation/activation.go create mode 100644 go/internal/labactivation/activation_test.go create mode 100644 go/internal/managementauth/auth.go create mode 100644 go/internal/managementauth/capability.go create mode 100644 go/internal/managementauth/gate.go create mode 100644 go/internal/managementauth/managementauth_test.go create mode 100644 go/internal/managementauth/session.go create mode 100644 go/internal/routing/compatibility/slot.go create mode 100644 tests/go-auth-parity.test.ts create mode 100644 tests/go-lab-gate-parity.test.ts diff --git a/devlog/_plan/260905_go_sidecar_takeover/033_auth_and_lab_gate_substrate.md b/devlog/_plan/260905_go_sidecar_takeover/033_auth_and_lab_gate_substrate.md new file mode 100644 index 0000000000..e5d7c1107b --- /dev/null +++ b/devlog/_plan/260905_go_sidecar_takeover/033_auth_and_lab_gate_substrate.md @@ -0,0 +1,104 @@ +# 033 — Tickets #18/#19: Go management auth model + Lab activation gate/seam + +Unit: `260905_go_sidecar_takeover` +Date: 2026-09-06 +Status: implemented on `dev-go` +Tickets: +- [#18](https://github.com/waxiangzi/opencodex/issues/18) (spec #3: Go management auth/session model) +- [#19](https://github.com/waxiangzi/opencodex/issues/19) (spec #6: Lab activation gate + provider-slot in Go) +Owner decision (2026-09-05/06): implement both under the state-source gate (devlog 030); deferral decisions are made and recorded here, not re-escalated. + +## What these tickets are (and are not) + +Both are **substrate** tickets for later batches, not route flips. The TS front +door still admits every `/api/*` request before dispatch +(`src/server/index.ts` → `requireManagementAuth`), so a Go-owned route never +sees an unauthenticated request pre-flip, and the Go binary runs no Lab code. +The deliverables are the Go-side decision logic, the seams it registers into, +and machine proof that both answer identically to TypeScript — the write +batches (#21–#23) and the authorization gate (#26) consume #18 when they serve +writes, and #33 + the flip consume #19. Nothing here changes live behavior; a +default install is byte-identical to a build without these packages. + +## #18 — Go management auth/session model + +`go/internal/managementauth` reproduces the admission decision of +`src/server/management-auth.ts` + `src/lib/*-contract.ts` + +`src/server/gui-session.ts`: + +- **Admin token**: credential extraction (x-opencodex-api-key / + Authorization Bearer), timing-safe equality, env/file token resolution + (file shape `ocx_admin_[43]`, ≤512 bytes, no creation/ACL mutation — the + sidecar must not touch the parent's secret file). +- **Dashboard session**: `AuthorizeSession` mirroring + `authorizeGuiSessionRequest` — expiry deletion, server-origin comparison + through a port of `managementRequestOrigin` (loopback observed origin, + non-loopback requires api auth, hub public-origin override, WHATWG origin + serialisation incl. default-port dropping), browser-origin/CSRF rules for + safe vs. unsafe methods, remote-session sliding. +- **Capability principals**: all four process-scoped HMAC contracts + (system-restart, local-provider-reload, local-read, gui-pair) plus the + attestation proof, with exact payload strings, base64url-256 shapes, + allowlists, TTL windows, and the consumed-capability replay stores (256 + limit, gui-pair keyed by sha256 digest). +- **Gate**: principal ordering (capabilities → token → session) and the exact + rejection responses — 401 `{"error":"opencodex admin token required"}` and + 503 with reason + hint — as raw JSON bodies. + +State-source notes: the capability checks are pure functions of injectable +inputs; the token is env/disk state; the session table is owned by the serving +process (minted in-memory) so Go validation carries the table it is given and +mutates it the same way (expiry delete, sliding). Live enforcement lands with +the write batches / authorization gate; pre-flip this is proven substrate. + +## #19 — Lab activation gate + provider-slot seam + +- `go/internal/labactivation` reproduces `labActivationRequired` and + `labAutomationEnabledOnDisk`: routing profiles non-empty in config.json, or + automation enabled under `/lab/automation-config.json` + (`policy.enabled`, authority over the legacy `automation-policy.json`, + which is consulted only when the combined file is absent or carries no + policy object). The gate reads the same on-disk files the TS side reads. +- `go/internal/routing/compatibility` reproduces the core-owned + `provider-slot.ts` seam: a nullable evidence-provider reference with + set/resolve/detach-own-registration semantics. Typed opaquely until the Go + routing port; `labactivation.Activate` reproduces the composition-root + contract — required → register, not required → slot stays nil — proven with + stub providers (the real Lab evidence provider arrives with #33). + +## Proof + +- Go unit tests: `managementauth` (capability round trips incl. cross-mint + refusal, origin derivation, gate ordering, replay rejection, session + outcomes, token-file loader) and `labactivation` (gate fixtures, activation + registers only when required). +- **Differential oracle** `tests/go-auth-parity.test.ts`: the same ordered + vector arrays through `src/server/management-auth.ts` (in-process) and the + new `ocx-sidecar authcheck` subcommand (one Go Gate per array, so replay + stores persist across vectors like the TS module-level maps) — decisions + compared byte-for-byte: principal or exact status+body, plus the session + admission reason when probed. 7 suites, every principal + rejection path. +- **Differential oracle** `tests/go-lab-gate-parity.test.ts`: ten fixture + directories through `ocx-sidecar labcheck` and `src/lib/lab-activation.ts` + (Go reads the pristine fixture first, because the TS loader can repair a + config file in place) — automationEnabled/profilesNonEmpty/required equal. +- Gates: `go build/vet/test ./...` green; focused suites green. + +## Wire contract additions (ocx-sidecar) + +`ocx-sidecar authcheck ` and `ocx-sidecar labcheck ` are +differential-oracle subcommands, inert on the live path (the supervisor never +passes an argument). The authcheck JSON carries request/state/config/local per +vector; one Go process per array mirrors one serving process. + +## Residuals (recorded, not hidden) + +- #18's session *issuance*/pairing machinery (mint, grants, rate limits) is + not ported — validation is the ticket's acceptance; issuance is session-route + work at the write batch. #18's Go gate becomes live when Go serves management + requests without the TS front door (#21–#23 differential writes, #26 + authorization gate, flip). +- #19's real Lab evidence provider registration is #33's job; until then the + seam and gate are proven with stubs and no Go package imports Lab content. +- #26 remains the gate that makes "auth rejection paths match TypeScript" a + whole-surface property. diff --git a/go/README.md b/go/README.md index ee405f24ad..9b36cb66d0 100644 --- a/go/README.md +++ b/go/README.md @@ -27,6 +27,32 @@ material only. This is a fresh codebase. exactly where the TypeScript runtime keeps it, so a Go-served read route answers from the real on-disk state; route bodies are pure functions of the subsection they read. +- `internal/managementauth` — the Go management admission model (ticket #18): + admin-token, dashboard-session, and capability-principal validation + mirroring `src/server/management-auth.ts` and the `src/lib/*-contract.ts` + HMACs, including the replay stores and the exact 401/503 rejection bodies. + Substrate: the TS front door still admits every management request pre-flip; + this gate is what Go uses when it answers without that front door (write + batches #21–#23, authorization gate #26, flip). +- `internal/labactivation` + `internal/routing/compatibility` — the Go + Compatibility Lab opt-in gate and the core-owned evidence-provider slot + (ticket #19), mirroring `src/lib/lab-activation.ts` and + `src/routing/compatibility/provider-slot.ts`. The gate reads the same + on-disk files the TS side reads (config.json routingProfiles, + lab/automation-config.json over the legacy automation-policy.json); the slot + registers only when the gate says the install uses Lab. Real Lab content + arrives with ticket #33. + +## Differential-oracle subcommands + +`ocx-sidecar authcheck ` evaluates admission vectors (request, state, +config, local context) through the real Go gate and prints decisions; the Bun +oracle (`tests/go-auth-parity.test.ts`) feeds the same arrays through +`src/server/management-auth.ts` and compares byte for byte. `ocx-sidecar +labcheck ` prints the Lab gate's three outputs for the Bun oracle +(`tests/go-lab-gate-parity.test.ts`). Both are inert on the live path: the +supervisor launches the sidecar with no arguments, so they only run when +invoked directly. ## Building diff --git a/go/cmd/ocx-sidecar/authcheck.go b/go/cmd/ocx-sidecar/authcheck.go new file mode 100644 index 0000000000..234ba9bd42 --- /dev/null +++ b/go/cmd/ocx-sidecar/authcheck.go @@ -0,0 +1,201 @@ +package main + +// The authcheck subcommand is the differential-oracle entry point for the Go +// management auth model (ADR-0008, ticket #18): it evaluates one or more +// admission vectors in a single process — so the per-capability replay stores +// behave exactly like the TS module-level maps — and prints the decisions as +// JSON. tests/go-auth-parity.test.ts feeds the same vector arrays through +// src/server/management-auth.ts and through this subcommand and compares the +// outputs byte for byte. The subcommand is inert on the live path: ocx-sidecar +// runs it only when invoked as `ocx-sidecar authcheck`, which the supervisor +// never does. + +import ( + "encoding/json" + "fmt" + "io" + "os" + "strings" + "time" + + "github.com/lidge-jun/opencodex/go/internal/managementauth" +) + +// authVector mirrors the request/state/config/local slice the oracle test +// builds from the TypeScript side. +type authVector struct { + Request authRequest `json:"request"` + State authState `json:"state"` + Config authConfig `json:"config"` + Local authLocal `json:"local"` + Probe bool `json:"sessionProbe,omitempty"` +} + +type authRequest struct { + URL string `json:"url"` + Method string `json:"method"` + Headers map[string]string `json:"headers"` +} + +type authSessionEntry struct { + Token string `json:"token"` + ServerOrigin string `json:"serverOrigin"` + BrowserOrigin string `json:"browserOrigin"` + CSRF string `json:"csrf"` + ExpiresAt int64 `json:"expiresAt"` + Issuance string `json:"issuance"` +} + +type authState struct { + Available bool `json:"available"` + Token string `json:"token"` + Source string `json:"source"` + Reason string `json:"reason"` + Sessions []authSessionEntry `json:"sessions"` +} + +type authConfig struct { + Hostname string `json:"hostname"` + RuntimeRole string `json:"runtimeRole"` + HubManagementPublicOrigin string `json:"hubManagementPublicOrigin"` +} + +type authLocal struct { + AttestationSecret string `json:"attestationSecret"` + PID int `json:"pid"` + Port int `json:"port"` +} + +type authDecision struct { + Admitted bool `json:"admitted"` + Principal *string `json:"principal"` + Rejection *authRejection `json:"rejection"` + SessionState string `json:"sessionState,omitempty"` +} + +type authRejection struct { + Status int `json:"status"` + Body string `json:"body"` +} + +func runAuthCheck() error { + // The vectors arrive as a JSON argv element (the oracle test passes them on + // the command line so Bun.spawnSync can stay synchronous); stdin is the + // fallback for direct shell use. + var raw []byte + if len(os.Args) > 2 { + raw = []byte(os.Args[2]) + } else { + var err error + raw, err = io.ReadAll(os.Stdin) + if err != nil { + return fmt.Errorf("authcheck: read stdin: %w", err) + } + } + var vectors []authVector + if err := json.Unmarshal(raw, &vectors); err != nil { + return fmt.Errorf("authcheck: decode vectors: %w", err) + } + + // One gate per invocation, mirroring one serving process. All vectors share + // it so capability consumption persists across the array exactly as the TS + // module-level stores do within one oracle run. + gate := buildGate(vectors) + decisions := make([]authDecision, 0, len(vectors)) + for _, vector := range vectors { + req := managementauth.Request{ + URL: vector.Request.URL, + Method: vector.Request.Method, + Header: lowerHeaders(vector.Request.Headers), + } + decision := gate.Admit(&req) + out := authDecision{} + if decision.Principal != "" { + out.Admitted = true + principal := string(decision.Principal) + out.Principal = &principal + } else { + out.Rejection = &authRejection{Status: decision.Rejection.Status, Body: decision.Rejection.Body} + } + if vector.Probe { + sessionState := "missing" + if admission := managementauth.AuthorizeSession( + &req, + vectorConfig(vector), + sessionsCopy(gateState(gate)), + time.Now().UnixMilli(), + ); admission.OK { + sessionState = "ok" + } else { + sessionState = string(admission.Reason) + } + out.SessionState = sessionState + } + decisions = append(decisions, out) + } + encoded, err := json.Marshal(decisions) + if err != nil { + return fmt.Errorf("authcheck: encode decisions: %w", err) + } + fmt.Println(string(encoded)) + return nil +} + +func lowerHeaders(headers map[string]string) map[string]string { + if headers == nil { + return map[string]string{} + } + out := make(map[string]string, len(headers)) + for name, value := range headers { + out[strings.ToLower(name)] = value + } + return out +} + +func vectorConfig(vector authVector) managementauth.ConfigView { + return managementauth.ConfigView{ + Hostname: vector.Config.Hostname, + RuntimeRole: vector.Config.RuntimeRole, + HubManagementPublicOrigin: vector.Config.HubManagementPublicOrigin, + } +} + +func buildGate(vectors []authVector) *managementauth.Gate { + if len(vectors) == 0 { + return managementauth.NewGate(managementauth.State{Available: false, Reason: ""}, managementauth.ConfigView{}, managementauth.LocalContext{}) + } + first := vectors[0] + state := managementauth.State{ + Available: first.State.Available, + Token: first.State.Token, + Source: first.State.Source, + Reason: first.State.Reason, + Sessions: map[string]managementauth.Session{}, + } + for _, entry := range first.State.Sessions { + state.Sessions[entry.Token] = managementauth.Session{ + ServerOrigin: entry.ServerOrigin, + BrowserOrigin: entry.BrowserOrigin, + CSRF: entry.CSRF, + ExpiresAt: entry.ExpiresAt, + Issuance: entry.Issuance, + } + } + return managementauth.NewGate(state, vectorConfig(first), managementauth.LocalContext{ + AttestationSecret: first.Local.AttestationSecret, + PID: first.Local.PID, + Port: first.Local.Port, + }) +} + +func gateState(gate *managementauth.Gate) map[string]managementauth.Session { + return gate.Sessions() +} + +func sessionsCopy(sessions map[string]managementauth.Session) map[string]managementauth.Session { + out := make(map[string]managementauth.Session, len(sessions)) + for token, session := range sessions { + out[token] = session + } + return out +} diff --git a/go/cmd/ocx-sidecar/labcheck.go b/go/cmd/ocx-sidecar/labcheck.go new file mode 100644 index 0000000000..0f02718a58 --- /dev/null +++ b/go/cmd/ocx-sidecar/labcheck.go @@ -0,0 +1,47 @@ +package main + +// The labcheck subcommand is the differential-oracle entry point for the Go +// Lab activation gate (ADR-0008, ticket #19): given a config directory, it +// prints the gate's three inputs and decision the way tests/go-lab-gate- +// parity.test.ts compares them against src/lib/lab-activation.ts. Inert on the +// live path, like authcheck. + +import ( + "encoding/json" + "fmt" + "os" + + "github.com/lidge-jun/opencodex/go/internal/config" + "github.com/lidge-jun/opencodex/go/internal/labactivation" +) + +type labGateResult struct { + AutomationEnabled bool `json:"automationEnabled"` + ProfilesNonEmpty bool `json:"profilesNonEmpty"` + Required bool `json:"required"` +} + +func runLabCheck() error { + if len(os.Args) < 3 { + return fmt.Errorf("labcheck requires a config directory argument") + } + configDir := os.Args[2] + cfg, err := config.LoadFromDir(configDir) + if err != nil { + // The gate must still answer for a config.json the TS side would + // salvage: report on what the loader could read (an empty document). + cfg = &config.Config{Raw: map[string]any{}} + } + automation := labactivation.AutomationEnabledOnDisk(configDir) + profiles := labactivation.ProfilesRequireActivation(cfg.Raw["routingProfiles"]) + encoded, err := json.Marshal(labGateResult{ + AutomationEnabled: automation, + ProfilesNonEmpty: profiles, + Required: automation || profiles, + }) + if err != nil { + return fmt.Errorf("labcheck: encode: %w", err) + } + fmt.Println(string(encoded)) + return nil +} diff --git a/go/cmd/ocx-sidecar/main.go b/go/cmd/ocx-sidecar/main.go index cb3155065e..fad0ee37a8 100644 --- a/go/cmd/ocx-sidecar/main.go +++ b/go/cmd/ocx-sidecar/main.go @@ -32,6 +32,23 @@ func main() { } func run() error { + // Differential-oracle subcommands (ADR-0008 tickets #18/#19). The + // supervisor never passes an argument, so the live sidecar path is + // unaffected; these exist so the Bun oracle can evaluate the same request + // vectors and Lab-gate fixtures through the real Go code. + if len(os.Args) > 1 { + switch os.Args[1] { + case "authcheck": + return runAuthCheck() + case "labcheck": + return runLabCheck() + } + return fmt.Errorf("unknown subcommand %q", os.Args[1]) + } + return serve() +} + +func serve() error { // Bind first, announce second: the parent only starts forwarding once it // has read the ready line, so announcing a listener that failed to bind // would leave the front door waiting on a dead child. diff --git a/go/internal/labactivation/activation.go b/go/internal/labactivation/activation.go new file mode 100644 index 0000000000..c5380b6aa8 --- /dev/null +++ b/go/internal/labactivation/activation.go @@ -0,0 +1,140 @@ +// Package labactivation reproduces the Compatibility Lab opt-in activation +// gate (ADR-0008, ticket #19). It mirrors src/lib/lab-activation.ts: an +// install "uses" the Lab when any routing profile exists in config.json OR Lab +// automation is enabled on disk under /lab/automation-config.json +// (with automation-policy.json as the legacy fallback). The TypeScript +// composition root calls labActivationRequired before it activates Lab; the +// equivalent Go decision must answer identically from the same on-disk state, +// which is what the differential oracle (tests/go-lab-gate-parity.test.ts) +// proves against fixture directories. +// +// This package is the seam, not the Lab: it imports no Lab content. The +// provider-slot seam lives in go/internal/routing/compatibility, and nothing +// in the module tree except a future composition root (the flip, ticket #41) +// or a test registers an evidence provider through it. Until the Lab batch +// (ticket #33) lands real Go Lab content, "a no-Lab user executes no Lab code +// in Go" holds because no Go package imports Lab content at all, and it stays +// machine-checkable by the absence of any importer of this package outside +// tests (go list -deps ./... shows only the sidecar's own read-route core, +// which imports neither this package nor the slot). +package labactivation + +import ( + "encoding/json" + "os" + "path/filepath" + + "github.com/lidge-jun/opencodex/go/internal/config" + "github.com/lidge-jun/opencodex/go/internal/routing/compatibility" +) + +// AutomationConfigFile is the current automation authority, sibling of the +// legacy policy file under /lab/. Mirrors the combined path the TS +// side reads first (dirname of the legacy path joined with +// "automation-config.json"). +const ( + automationConfigFile = "automation-config.json" + automationPolicyFile = "automation-policy.json" +) + +// readJSONObject returns the decoded top-level JSON value of path, or nil when +// the file is absent or not a single valid JSON object. Mirrors +// readJsonIfPresent in src/lib/lab-activation.ts: the detector must never +// throw and must never import Lab persistence to answer the question. +func readJSONValue(path string) (any, bool) { + file, err := os.Open(path) + if err != nil { + return nil, false + } + defer file.Close() + decoder := json.NewDecoder(file) + var value any + if err := decoder.Decode(&value); err != nil { + return nil, false + } + return value, true +} + +// policyEnabled extracts policy.enabled from a decoded object. Mirrors the TS +// shape navigation: the combined file carries {policy: {enabled}}. +func policyEnabled(value any) (bool, bool) { + object, ok := value.(map[string]any) + if !ok { + return false, false + } + policy, ok := object["policy"].(map[string]any) + if !ok { + return false, false + } + enabled, ok := policy["enabled"].(bool) + return enabled, ok +} + +// legacyEnabled extracts enabled from the legacy automation-policy.json root. +func legacyEnabled(value any) (bool, bool) { + object, ok := value.(map[string]any) + if !ok { + return false, false + } + enabled, ok := object["enabled"].(bool) + return enabled, ok +} + +// AutomationEnabledOnDisk mirrors labAutomationEnabledOnDisk. Precedence is +// deliberate: the combined automation-config.json is the current authority +// (with policy.enabled present, even false, it decides); only when the +// combined file is absent or carries no policy object does the legacy +// automation-policy.json answer. +func AutomationEnabledOnDisk(configDir string) bool { + legacyPath := filepath.Join(configDir, "lab", automationPolicyFile) + if combined, ok := readJSONValue(filepath.Join(filepath.Dir(legacyPath), automationConfigFile)); ok { + if enabled, decided := policyEnabled(combined); decided { + return enabled + } + } + if legacy, ok := readJSONValue(legacyPath); ok { + if enabled, decided := legacyEnabled(legacy); decided { + return enabled + } + } + return false +} + +// ProfilesRequireActivation mirrors `Object.keys(config.routingProfiles ?? +// {}).length > 0`: the raw routingProfiles value from config.json requires +// activation exactly when it is an object with at least one key. Any other +// shape (absent, null, array, empty object) does not. +func ProfilesRequireActivation(routingProfiles any) bool { + profiles, ok := routingProfiles.(map[string]any) + return ok && len(profiles) > 0 +} + +// Required mirrors labActivationRequired: any routing profile, or automation +// enabled on disk. configFile is the parsed config.json (see internal/config); +// configDir is the directory the TS process resolved as its config home. +func Required(cfg *config.Config, configDir string) bool { + if cfg == nil { + return AutomationEnabledOnDisk(configDir) + } + if ProfilesRequireActivation(cfg.Raw["routingProfiles"]) { + return true + } + return AutomationEnabledOnDisk(configDir) +} + +// Activate performs the activation side of the seam: when required, it +// installs the evidence provider into the core slot; when not required it +// performs no registration and the slot stays nil. It reports whether +// activation happened. This mirrors the TypeScript composition root, which +// calls activateLab only when labActivationRequired is true — the synchronous, +// gap-free guarantee the owner decision (devlog 010) requires the Go side to +// reproduce. The real Lab evidence provider arrives with ticket #33; until +// then the caller (a test, or the flip composition root) supplies one, so the +// seam contract is proven without pretending Lab content exists. +func Activate(slot *compatibility.Slot, cfg *config.Config, configDir string, provider compatibility.EvidenceProvider) bool { + if !Required(cfg, configDir) { + return false + } + slot.Set(provider) + return true +} diff --git a/go/internal/labactivation/activation_test.go b/go/internal/labactivation/activation_test.go new file mode 100644 index 0000000000..fbd34817f0 --- /dev/null +++ b/go/internal/labactivation/activation_test.go @@ -0,0 +1,201 @@ +package labactivation + +import ( + "os" + "path/filepath" + "reflect" + "testing" + + "github.com/lidge-jun/opencodex/go/internal/config" + "github.com/lidge-jun/opencodex/go/internal/routing/compatibility" +) + +func writeConfig(t *testing.T, dir, content string) *config.Config { + t.Helper() + if content == "" { + if err := os.MkdirAll(dir, 0o700); err != nil { + t.Fatal(err) + } + return &config.Config{Raw: map[string]any{}} + } + path := filepath.Join(dir, "config.json") + if err := os.MkdirAll(dir, 0o700); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(path, []byte(content), 0o600); err != nil { + t.Fatal(err) + } + cfg, err := config.LoadFromDir(dir) + if err != nil { + t.Fatalf("fixture config did not load: %v", err) + } + return cfg +} + +func writeLabAutomation(t *testing.T, dir, file, content string) { + t.Helper() + labDir := filepath.Join(dir, "lab") + if err := os.MkdirAll(labDir, 0o700); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(labDir, file), []byte(content), 0o600); err != nil { + t.Fatal(err) + } +} + +func TestProfilesRequireActivation(t *testing.T) { + if ProfilesRequireActivation(nil) { + t.Fatal("absent routingProfiles must not require activation") + } + if ProfilesRequireActivation(map[string]any{}) { + t.Fatal("empty routingProfiles must not require activation") + } + if !ProfilesRequireActivation(map[string]any{"demo": map[string]any{"candidates": []any{}}}) { + t.Fatal("non-empty routingProfiles must require activation") + } + if ProfilesRequireActivation([]any{}) { + t.Fatal("an array routingProfiles must not count (zod rejects it as invalid config)") + } +} + +func TestAutomationEnabledOnDisk(t *testing.T) { + + t.Run("no files means not enabled", func(t *testing.T) { + if AutomationEnabledOnDisk(t.TempDir()) { + t.Fatal("empty config dir must not enable automation") + } + }) + + t.Run("legacy policy file enables", func(t *testing.T) { + sub := t.TempDir() + writeLabAutomation(t, sub, automationPolicyFile, `{"enabled": true}`) + if !AutomationEnabledOnDisk(sub) { + t.Fatal("legacy automation-policy.json with enabled true must enable automation") + } + }) + + t.Run("legacy policy disabled stays off", func(t *testing.T) { + sub := t.TempDir() + writeLabAutomation(t, sub, automationPolicyFile, `{"enabled": false}`) + if AutomationEnabledOnDisk(sub) { + t.Fatal("legacy policy with enabled false must not enable automation") + } + }) + + t.Run("combined file is the authority", func(t *testing.T) { + sub := t.TempDir() + writeLabAutomation(t, sub, automationConfigFile, `{"policy": {"enabled": true}}`) + writeLabAutomation(t, sub, automationPolicyFile, `{"enabled": false}`) + if !AutomationEnabledOnDisk(sub) { + t.Fatal("combined automation-config.json must win over the legacy file") + } + }) + + t.Run("combined enabled false decides even when legacy says true", func(t *testing.T) { + sub := t.TempDir() + writeLabAutomation(t, sub, automationConfigFile, `{"policy": {"enabled": false}}`) + writeLabAutomation(t, sub, automationPolicyFile, `{"enabled": true}`) + if AutomationEnabledOnDisk(sub) { + t.Fatal("combined enabled false must decide (the current dashboard can turn automation off)") + } + }) + + t.Run("malformed files mean not enabled", func(t *testing.T) { + sub := t.TempDir() + writeLabAutomation(t, sub, automationConfigFile, `{not json`) + writeLabAutomation(t, sub, automationPolicyFile, `{"enabled": "yes"}`) + if AutomationEnabledOnDisk(sub) { + t.Fatal("malformed or wrong-typed automation files must not enable automation") + } + }) + + t.Run("combined without a policy object falls back to legacy", func(t *testing.T) { + sub := t.TempDir() + writeLabAutomation(t, sub, automationConfigFile, `{"scheduler": {}}`) + writeLabAutomation(t, sub, automationPolicyFile, `{"enabled": true}`) + if !AutomationEnabledOnDisk(sub) { + t.Fatal("combined file without a policy object must fall back to the legacy file") + } + }) +} + +func TestRequired(t *testing.T) { + dir := t.TempDir() + if Required(writeConfig(t, dir, `{}`), dir) { + t.Fatal("empty config must not require Lab activation") + } + + profilesDir := t.TempDir() + profilesJSON := `{"routingProfiles": {"demo": {"candidates": [{"provider": "openai", "model": "gpt-5.5"}]}}}` + writeConfig(t, profilesDir, profilesJSON) + if !Required(writeConfig(t, profilesDir, profilesJSON), profilesDir) { + t.Fatal("a routing profile must require Lab activation") + } + if cfg, err := config.LoadFromDir(profilesDir); err != nil || !Required(cfg, profilesDir) { + t.Fatal("parsed config with routingProfiles must require Lab activation") + } + + autoDir := t.TempDir() + writeLabAutomation(t, autoDir, automationConfigFile, `{"policy": {"enabled": true}}`) + if !Required(writeConfig(t, autoDir, `{}`), autoDir) { + t.Fatal("automation enabled on disk must require Lab activation") + } + + // A profile plus automation off still requires activation via the profile. + bothDir := t.TempDir() + writeConfig(t, bothDir, profilesJSON) + writeLabAutomation(t, bothDir, automationConfigFile, `{"policy": {"enabled": false}}`) + if cfg, err := config.LoadFromDir(bothDir); err != nil || !Required(cfg, bothDir) { + t.Fatal("a routing profile must require activation even with automation off") + } +} + +func TestActivateRegistersOnlyWhenRequired(t *testing.T) { + slot := compatibility.NewSlot() + provider := func(options compatibility.EvidenceOptions) compatibility.CandidateEvidence { + return compatibility.CandidateEvidence{} + } + + dir := t.TempDir() + writeConfig(t, dir, `{}`) + if activated := Activate(slot, writeConfig(t, dir, `{}`), dir, provider); activated { + t.Fatal("empty install must not activate") + } + if resolved := slot.Resolve(); resolved != nil { + t.Fatal("a non-required install must register nothing (slot stays nil)") + } + + profilesDir := t.TempDir() + writeConfig(t, profilesDir, `{"routingProfiles": {"demo": {"candidates": [{"provider": "openai", "model": "gpt-5.5"}]}}}`) + cfg, err := config.LoadFromDir(profilesDir) + if err != nil { + t.Fatal(err) + } + if !Activate(slot, cfg, profilesDir, provider) { + t.Fatal("a required install must activate") + } + if resolved := slot.Resolve(); resolved == nil { + t.Fatal("activation must register the provider into the core slot") + } + + // The detach returned by Set only removes its own registration. + replacement := func(options compatibility.EvidenceOptions) compatibility.CandidateEvidence { + return nil + } + detach := slot.Set(replacement) + slot.Set(provider) + detach() + if resolved := slot.Resolve(); resolved == nil || funcPointer(resolved) != funcPointer(provider) { + t.Fatal("a stale detach must not remove a newer registration") + } + slot.Reset() + if slot.Resolve() != nil { + t.Fatal("reset must clear the slot") + } +} + +// funcPointer is the identity proxy for comparing function values (Go funcs +// are only comparable to nil). +func funcPointer(fn compatibility.EvidenceProvider) uintptr { + return reflect.ValueOf(fn).Pointer() +} diff --git a/go/internal/managementauth/auth.go b/go/internal/managementauth/auth.go new file mode 100644 index 0000000000..d9528f5909 --- /dev/null +++ b/go/internal/managementauth/auth.go @@ -0,0 +1,80 @@ +// Package managementauth reproduces the TypeScript management admission +// model (ADR-0008, ticket #18: "Go management auth/session model"). +// +// src/server/management-auth.ts decides whether a management request is +// admitted and under which principal: a process-scoped capability +// (system-restart, local-provider-reload, local-read, gui-pair), the admin +// token, or a dashboard session. This package mirrors that decision logic so a +// request answered by Go without the TypeScript front door having already +// admitted it is authorised identically — the acceptance criterion is +// "under-privileged requests rejected identically to TypeScript", and the +// differential oracle (tests/go-auth-parity.test.ts) proves it by running the +// same request vectors through src/server/management-auth.ts and this package +// and comparing the resulting principal-or-rejection byte for byte. +// +// State-source note: the four capability checks are pure functions of their +// inputs (request headers, the local context's attestation secret/pid/port); +// the admin token is env/disk state; the dashboard session table is owned by +// whatever process mints sessions. The TS front door still admits every +// request before forwarding pre-flip (src/server/index.ts), so this gate is +// exercised live only once Go serves management routes without that front door +// (the write batches' state-reset differential and the authorization gate, +// tickets #21-#23/#26). Until then it is substrate, proven by the oracle. +package managementauth + +// Principal mirrors ManagementPrincipal in src/server/management-auth.ts. +type Principal string + +const ( + // PrincipalAdminToken is the raw token from disk/env. + PrincipalAdminToken Principal = "admin-token" + // PrincipalGuiSession is a session token this process minted for a browser. + PrincipalGuiSession Principal = "gui-session" + // PrincipalGuiPairCapability is a process-scoped HMAC for the pairing-grant path. + PrincipalGuiPairCapability Principal = "gui-pair-capability" + // PrincipalLocalReadCapability is a process-scoped HMAC for two allowlisted GETs. + PrincipalLocalReadCapability Principal = "local-read-capability" + // PrincipalLocalProviderReloadCapability is a process-scoped HMAC for the reload POST. + PrincipalLocalProviderReloadCapability Principal = "local-provider-reload-capability" + // PrincipalSystemRestartCapability is a process-scoped HMAC for the restart POST. + PrincipalSystemRestartCapability Principal = "system-restart-capability" +) + +// Session mirrors GuiSessionRecord in src/server/gui-session.ts. +type Session struct { + ServerOrigin string + BrowserOrigin string + CSRF string + ExpiresAt int64 // epoch milliseconds + Issuance string +} + +// State mirrors ManagementAuthState in src/server/management-auth.ts. Sessions +// are owned by the process that mints them; the Go gate carries the table so +// validation mutates it the way the TypeScript side does (expiry deletion, +// remote-session sliding). +type State struct { + Available bool + Token string + Source string // "environment" | "file" + Reason string // set when !Available + Sessions map[string]Session +} + +// LocalContext mirrors LocalManagementAuthContext: the process-scoped +// attestation secret, pid, and listening port that capability HMACs bind to. +type LocalContext struct { + AttestationSecret string + PID int + Port int +} + +// ConfigView is the slice of OcxConfig that the admission logic reads +// (config.hostname via isApiAuthRequired, runtimeRole and +// hub.managementPublicOrigin via managementRequestOrigin). Everything else is +// irrelevant to validation; see src/server/auth-cors.ts. +type ConfigView struct { + Hostname string + RuntimeRole string + HubManagementPublicOrigin string +} diff --git a/go/internal/managementauth/capability.go b/go/internal/managementauth/capability.go new file mode 100644 index 0000000000..609159ead7 --- /dev/null +++ b/go/internal/managementauth/capability.go @@ -0,0 +1,430 @@ +package managementauth + +// Process-scoped capability contracts (ADR-0008, ticket #18). Each mirrors the +// homonymous module under src/lib/: the payload string that the TypeScript +// side HMACs, the exact header names, the allowlist shapes, and the +// base64url-256 (43-char) secret/capability format. A capability minted by the +// TypeScript process must verify here byte-for-byte, and one minted here must +// verify on the TypeScript side — the differential oracle pins both +// directions. The payloads are versioned strings joined with \n; the trailing +// pieces are exactly as src/lib emits them, so an off-by-one field or an extra +// newline breaks parity immediately. +// +// Header modelling: an empty string means the header is absent. TypeScript +// distinguishes null (absent) from "" only for the pid parsers (absent vs +// invalid) and empty capability/browser-origin values, and every such +// distinction converges on the same admission decision (rejection), so folding +// "" into absent never changes an outcome the oracle can observe. + +import ( + "crypto/hmac" + "crypto/sha256" + "encoding/base64" + "net/url" + "regexp" + "strconv" + "strings" +) + +// Contract header names, paths, methods, and TTLs mirror the TS constants. +const ( + LocalManagementExpectedPIDHeader = "x-opencodex-local-expected-pid" + LocalManagementNonceHeader = "x-opencodex-local-nonce" + LocalManagementExpiresAtHeader = "x-opencodex-local-expires-at" + LocalManagementCapabilityHeader = "x-opencodex-local-capability" + LocalManagementCapabilityTTLMs = 10_000 + + LocalManagementReadPathCodexAccounts = "/api/codex-auth/accounts" + LocalManagementReadPathSystemMemory = "/api/system/memory" + + SystemRestartExpectedPIDHeader = "x-opencodex-restart-expected-pid" + SystemRestartNonceHeader = "x-opencodex-restart-nonce" + SystemRestartCapabilityHeader = "x-opencodex-restart-capability" + SystemRestartMethod = "POST" + SystemRestartPath = "/api/system/restart" + + LocalProviderReloadExpectedPIDHeader = "x-opencodex-provider-reload-expected-pid" + LocalProviderReloadNonceHeader = "x-opencodex-provider-reload-nonce" + LocalProviderReloadExpiresAtHeader = "x-opencodex-provider-reload-expires-at" + LocalProviderReloadNameHeader = "x-opencodex-provider-reload-name" + LocalProviderReloadCapabilityHeader = "x-opencodex-provider-reload-capability" + LocalProviderReloadCapabilityTTLMs = 10_000 + LocalProviderReloadMethod = "POST" + LocalProviderReloadPath = "/api/providers/reload" + + GUIPairExpectedPIDHeader = "x-opencodex-gui-pair-expected-pid" + GUIPairNonceHeader = "x-opencodex-gui-pair-nonce" + GUIPairExpiresAtHeader = "x-opencodex-gui-pair-expires-at" + GUIPairBrowserOriginHeader = "x-opencodex-gui-pair-origin" + GUIPairCapabilityHeader = "x-opencodex-gui-pair-capability" + GUIPairCapabilityTTLMs = 10_000 + GUIPairMethod = "POST" + GUIPairPath = "/api/gui/pairing-grants" + + localReadMethod = "GET" +) + +var ( + // base64URL256 mirrors BASE64URL_256 in the TS contracts: a 256-bit + // base64url string without padding (43 characters). + base64URL256 = regexp.MustCompile(`^[A-Za-z0-9_-]{43}$`) + // providerNamePattern mirrors PROVIDER_NAME in + // src/lib/local-provider-reload-contract.ts. + providerNamePattern = regexp.MustCompile(`^[A-Za-z0-9](?:[A-Za-z0-9._-]{0,62}[A-Za-z0-9])?$`) + // positiveDecimal mirrors the pid/expiry parsers: no leading zero, no zero. + positiveDecimal = regexp.MustCompile(`^[1-9]\d*$`) +) + +// IsBase64URL256 reports whether value is a 256-bit base64url string. +func IsBase64URL256(value string) bool { + return base64URL256.MatchString(value) +} + +// IsAttestationSecret mirrors isLocalAttestationSecret. +func IsAttestationSecret(value string) bool { + return base64URL256.MatchString(value) +} + +// IsLocalProviderReloadName mirrors isLocalProviderReloadName. +func IsLocalProviderReloadName(value string) bool { + return providerNamePattern.MatchString(value) +} + +// hmacBase64URL computes HMAC-SHA256 over payload keyed by secret, encoded +// base64url without padding — Node's .digest("base64url") format. +func hmacBase64URL(secret string, payload string) string { + mac := hmac.New(sha256.New, []byte(secret)) + mac.Write([]byte(payload)) + return base64.RawURLEncoding.EncodeToString(mac.Sum(nil)) +} + +// equalSecretByte is the timing-safe comparison every verify uses, with the +// TS length guard first. +func equalCapabilityBytes(expected, actual string) bool { + if len(expected) != len(actual) { + return false + } + return hmac.Equal([]byte(expected), []byte(actual)) +} + +// ExpectedPIDKind mirrors the kind-union parse in the TS contracts. +type ExpectedPIDKind string + +const ( + ExpectedPIDAbsent ExpectedPIDKind = "absent" + ExpectedPIDInvalid ExpectedPIDKind = "invalid" + ExpectedPIDPresent ExpectedPIDKind = "present" +) + +// ParseExpectedPID mirrors parseExpectedLocalManagementPid and its siblings. +// value == "" models an absent header; anything that is not positive decimal +// digits is invalid. +func ParseExpectedPID(value string) (ExpectedPIDKind, int64) { + if value == "" { + return ExpectedPIDAbsent, 0 + } + if !positiveDecimal.MatchString(value) { + return ExpectedPIDInvalid, 0 + } + parsed, err := parseDecimalInt64(value) + if err != nil { + return ExpectedPIDInvalid, 0 + } + return ExpectedPIDPresent, parsed +} + +// parseExpiryHeader mirrors the TS expiry checks: the raw header must match +// ^[1-9]\d*$ and parse to a safe integer, or the request is rejected before +// any capability verification runs. +func parseExpiryHeader(value string) (int64, bool) { + if !positiveDecimal.MatchString(value) { + return 0, false + } + parsed, err := parseDecimalInt64(value) + if err != nil { + return 0, false + } + return parsed, true +} + +func parseDecimalInt64(value string) (int64, error) { + var out int64 + for _, r := range value { + if r < '0' || r > '9' { + return 0, errNotDecimal + } + next := out*10 + int64(r-'0') + if next < out { + // Overflow cannot be a JS safe integer; do not wrap silently. + return 0, errNotDecimal + } + out = next + } + return out, nil +} + +var errNotDecimal = ¬DecimalError{} + +type notDecimalError struct{} + +func (*notDecimalError) Error() string { return "not a decimal integer" } + +func expiryWithin(now, expiresAt, ttl int64) bool { + // TS: !Number.isSafeInteger(now) rejects; expiresAt <= now rejects; + // expiresAt > now + TTL rejects. + return expiresAt > now && expiresAt <= now+ttl +} + +// --------------------------------------------------------------------------- +// Attestation (src/lib/local-management-attestation.ts) +// --------------------------------------------------------------------------- + +func attestationPayload(challenge string, pid int64, port int) (string, bool) { + if !base64URL256.MatchString(challenge) || pid <= 0 || port <= 0 || port > 65535 { + return "", false + } + return "opencodex-local-management-v1\n" + challenge + "\n" + itoa(pid) + "\n" + itoaInt(port), true +} + +// CreateLocalAttestationProof mirrors createLocalAttestationProof. Empty +// result means the inputs are invalid. +func CreateLocalAttestationProof(secret, challenge string, pid int64, port int) string { + if !IsAttestationSecret(secret) { + return "" + } + payload, ok := attestationPayload(challenge, pid, port) + if !ok { + return "" + } + return hmacBase64URL(secret, payload) +} + +// VerifyLocalAttestationProof mirrors verifyLocalAttestationProof. +func VerifyLocalAttestationProof(secret, challenge string, pid int64, port int, proof string) bool { + expected := CreateLocalAttestationProof(secret, challenge, pid, port) + if expected == "" || !base64URL256.MatchString(proof) { + return false + } + return equalCapabilityBytes(expected, proof) +} + +// --------------------------------------------------------------------------- +// System restart (src/lib/system-restart-contract.ts) +// --------------------------------------------------------------------------- + +func restartPayload(nonce, method, path string, pid int64, port int) (string, bool) { + if !base64URL256.MatchString(nonce) || method != SystemRestartMethod || path != SystemRestartPath || pid <= 0 || port <= 0 || port > 65535 { + return "", false + } + return "opencodex-system-restart-v1\n" + nonce + "\n" + method + "\n" + path + "\n" + itoa(pid) + "\n" + itoaInt(port), true +} + +// CreateSystemRestartCapability mirrors createSystemRestartCapability. +func CreateSystemRestartCapability(secret, nonce, method, path string, pid int64, port int) string { + if !IsAttestationSecret(secret) { + return "" + } + payload, ok := restartPayload(nonce, method, path, pid, port) + if !ok { + return "" + } + return hmacBase64URL(secret, payload) +} + +// VerifySystemRestartCapability mirrors verifySystemRestartCapability. The +// restart contract has no expiry window. +func VerifySystemRestartCapability(secret, nonce, method, path string, pid int64, port int, capability string) bool { + if nonce == "" || !base64URL256.MatchString(capability) { + return false + } + expected := CreateSystemRestartCapability(secret, nonce, method, path, pid, port) + if expected == "" { + return false + } + return equalCapabilityBytes(expected, capability) +} + +// --------------------------------------------------------------------------- +// Local provider reload (src/lib/local-provider-reload-contract.ts) +// --------------------------------------------------------------------------- + +func providerReloadPayload(nonce, method, path, name string, pid int64, port int, expiresAt int64) (string, bool) { + if !base64URL256.MatchString(nonce) || method != LocalProviderReloadMethod || path != LocalProviderReloadPath { + return "", false + } + if !IsLocalProviderReloadName(name) { + return "", false + } + if pid <= 0 || port <= 0 || port > 65535 || expiresAt <= 0 { + return "", false + } + return "opencodex-local-provider-reload-v1\n" + nonce + "\n" + method + "\n" + path + "\n" + name + "\n" + itoa(pid) + "\n" + itoaInt(port) + "\n" + itoa(expiresAt), true +} + +// CreateLocalProviderReloadCapability mirrors createLocalProviderReloadCapability. +func CreateLocalProviderReloadCapability(secret, nonce, method, path, name string, pid int64, port int, expiresAt int64) string { + if !IsAttestationSecret(secret) { + return "" + } + payload, ok := providerReloadPayload(nonce, method, path, name, pid, port, expiresAt) + if !ok { + return "" + } + return hmacBase64URL(secret, payload) +} + +// VerifyLocalProviderReloadCapability mirrors +// verifyLocalProviderReloadCapability. name == "" models an absent name +// header, which fails verification exactly as the TS null does. +func VerifyLocalProviderReloadCapability(secret, nonce, method, path, name string, pid int64, port int, expiresAt int64, capability string, now int64) bool { + if nonce == "" || name == "" || !base64URL256.MatchString(capability) { + return false + } + if !expiryWithin(now, expiresAt, LocalProviderReloadCapabilityTTLMs) { + return false + } + expected := CreateLocalProviderReloadCapability(secret, nonce, method, path, name, pid, port, expiresAt) + if expected == "" { + return false + } + return equalCapabilityBytes(expected, capability) +} + +// --------------------------------------------------------------------------- +// GUI pairing (src/lib/gui-pair-capability.ts) +// --------------------------------------------------------------------------- + +// CanonicalGuiBrowserOrigin mirrors canonicalGuiBrowserOrigin. It returns "" +// for values that do not canonicalise. +func CanonicalGuiBrowserOrigin(value string) string { + if value == "" || strings.TrimSpace(value) != value { + return "" + } + parsed, err := url.Parse(value) + if err != nil { + return "" + } + if parsed.Host == "" || parsed.User != nil || parsed.RawQuery != "" || parsed.Fragment != "" { + return "" + } + if parsed.Path != "" && parsed.Path != "/" { + return "" + } + if parsed.Scheme == "http" || parsed.Scheme == "https" { + return whatwgOrigin(parsed) + } + return parsed.Scheme + "://" + parsed.Host +} + +func guiPairPayload(nonce, method, path, browserOrigin string, pid int64, port int, expiresAt int64) (string, bool) { + if !base64URL256.MatchString(nonce) || method != GUIPairMethod || path != GUIPairPath { + return "", false + } + canonical := CanonicalGuiBrowserOrigin(browserOrigin) + if canonical == "" || canonical != browserOrigin { + return "", false + } + if pid <= 0 || port <= 0 || port > 65535 || expiresAt <= 0 { + return "", false + } + return "opencodex-gui-pair-v1\n" + nonce + "\n" + method + "\n" + path + "\n" + browserOrigin + "\n" + itoa(pid) + "\n" + itoaInt(port) + "\n" + itoa(expiresAt), true +} + +// CreateGuiPairCapability mirrors createGuiPairCapability. +func CreateGuiPairCapability(secret, nonce, method, path, browserOrigin string, pid int64, port int, expiresAt int64) string { + if !IsAttestationSecret(secret) { + return "" + } + payload, ok := guiPairPayload(nonce, method, path, browserOrigin, pid, port, expiresAt) + if !ok { + return "" + } + return hmacBase64URL(secret, payload) +} + +// VerifyGuiPairCapability mirrors verifyGuiPairCapability. browserOrigin == "" +// models an absent origin header. +func VerifyGuiPairCapability(secret, nonce, method, path, browserOrigin string, pid int64, port int, expiresAt int64, capability string, now int64) bool { + if nonce == "" || browserOrigin == "" || !base64URL256.MatchString(capability) { + return false + } + if !expiryWithin(now, expiresAt, GUIPairCapabilityTTLMs) { + return false + } + expected := CreateGuiPairCapability(secret, nonce, method, path, browserOrigin, pid, port, expiresAt) + if expected == "" { + return false + } + return equalCapabilityBytes(expected, capability) +} + +// --------------------------------------------------------------------------- +// Local management read (src/lib/local-management-capability.ts) +// --------------------------------------------------------------------------- + +func localReadPayload(nonce, method, path string, pid int64, port int, expiresAt int64) (string, bool) { + if !base64URL256.MatchString(nonce) || method != localReadMethod { + return "", false + } + if path != LocalManagementReadPathCodexAccounts && path != LocalManagementReadPathSystemMemory { + return "", false + } + if pid <= 0 || port <= 0 || port > 65535 || expiresAt <= 0 { + return "", false + } + return "opencodex-local-management-read-v1\n" + nonce + "\n" + method + "\n" + path + "\n" + itoa(pid) + "\n" + itoaInt(port) + "\n" + itoa(expiresAt), true +} + +// CreateLocalManagementReadCapability mirrors createLocalManagementReadCapability. +func CreateLocalManagementReadCapability(secret, nonce, method, path string, pid int64, port int, expiresAt int64) string { + if !IsAttestationSecret(secret) { + return "" + } + payload, ok := localReadPayload(nonce, method, path, pid, port, expiresAt) + if !ok { + return "" + } + return hmacBase64URL(secret, payload) +} + +// VerifyLocalManagementReadCapability mirrors +// verifyLocalManagementReadCapability. +func VerifyLocalManagementReadCapability(secret, nonce, method, path string, pid int64, port int, expiresAt int64, capability string, now int64) bool { + if nonce == "" || !base64URL256.MatchString(capability) { + return false + } + if !expiryWithin(now, expiresAt, LocalManagementCapabilityTTLMs) { + return false + } + expected := CreateLocalManagementReadCapability(secret, nonce, method, path, pid, port, expiresAt) + if expected == "" { + return false + } + return equalCapabilityBytes(expected, capability) +} + +// whatwgOrigin reproduces URL.prototype.origin for http/https: scheme plus the +// serialized host, lowercased, with the scheme's default port dropped. IPv6 +// hosts keep their brackets. +func whatwgOrigin(u *url.URL) string { + host := u.Hostname() + if strings.Contains(host, ":") { + host = "[" + host + "]" + } + port := u.Port() + if (u.Scheme == "http" && port == "80") || (u.Scheme == "https" && port == "443") { + port = "" + } + if port != "" { + host += ":" + port + } + return u.Scheme + "://" + strings.ToLower(host) +} + +func itoa(value int64) string { + return strconv.FormatInt(value, 10) +} + +func itoaInt(value int) string { + return strconv.FormatInt(int64(value), 10) +} diff --git a/go/internal/managementauth/gate.go b/go/internal/managementauth/gate.go new file mode 100644 index 0000000000..17aad9b4f4 --- /dev/null +++ b/go/internal/managementauth/gate.go @@ -0,0 +1,451 @@ +package managementauth + +// The admission gate itself (ADR-0008, ticket #18): the decision ordering, +// the dashboard-session authorization, the per-principal replay control, and +// the exact rejection responses. This mirrors resolveManagementAdmission and +// requireManagementAuth in src/server/management-auth.ts plus +// authorizeGuiSessionRequest in src/server/gui-session.ts. + +import ( + "crypto/sha256" + "encoding/base64" + "strings" + "sync" + "time" +) + +// Constants shared with the TS side. +const ( + loopbackGuiSessionTTLMs = 5 * 60_000 + remoteGuiSessionTTLMs = 12 * 60 * 60_000 + + // Replay-store limits mirror the TS module-level constants. + localReadReplayLimit = 256 + providerReloadReplayLimit = 256 + guiPairReplayLimit = 256 + + // Header names the session/credential paths read (fixed lowercase). + xOpenCodexAPIKeyHeader = "x-opencodex-api-key" + authorizationHeader = "authorization" + guiOriginHeader = "x-opencodex-gui-origin" + originHeader = "origin" + csrfHeader = "x-opencodex-csrf-token" + contentLengthHeader = "content-length" + transferEncodingHeader = "transfer-encoding" +) + +// SessionAdmissionReason mirrors the ok:false reasons of GuiSessionAdmission. +type SessionAdmissionReason string + +const ( + SessionMissing SessionAdmissionReason = "missing" + SessionExpired SessionAdmissionReason = "expired" + SessionServerOrigin SessionAdmissionReason = "server-origin" + SessionBrowserOrigin SessionAdmissionReason = "browser-origin" + SessionCSRF SessionAdmissionReason = "csrf" +) + +// SessionAdmission mirrors GuiSessionAdmission. +type SessionAdmission struct { + OK bool + Reason SessionAdmissionReason + Session Session +} + +// AuthorizeSession mirrors authorizeGuiSessionRequest. It may mutate the +// sessions map exactly as the TS side does: an expired session is deleted and +// a remote session's expiry slides forward on success. +func AuthorizeSession(r *Request, cfg ConfigView, sessions map[string]Session, now int64) SessionAdmission { + credential := RequestManagementCredential(r) + if credential == "" { + return SessionAdmission{OK: false, Reason: SessionMissing} + } + token, session, found := findSession(credential, sessions) + if !found { + return SessionAdmission{OK: false, Reason: SessionMissing} + } + if session.ExpiresAt <= now { + delete(sessions, token) + return SessionAdmission{OK: false, Reason: SessionExpired} + } + if ManagementRequestOrigin(r, cfg) != session.ServerOrigin { + return SessionAdmission{OK: false, Reason: SessionServerOrigin} + } + claimedBrowserOrigin := r.Get(guiOriginHeader) + browserOrigin := r.Get(originHeader) + safeMethod := r.Method == "GET" || r.Method == "HEAD" + if claimedBrowserOrigin != session.BrowserOrigin || + (browserOrigin != "" && browserOrigin != session.BrowserOrigin) || + (!safeMethod && browserOrigin != session.BrowserOrigin) { + return SessionAdmission{OK: false, Reason: SessionBrowserOrigin} + } + if !safeMethod { + csrf := strings.TrimSpace(r.Get(csrfHeader)) + if csrf == "" || !EqualSecret(csrf, session.CSRF) { + return SessionAdmission{OK: false, Reason: SessionCSRF} + } + } + if session.Issuance != "loopback" { + session.ExpiresAt = now + remoteGuiSessionTTLMs + sessions[token] = session + } + return SessionAdmission{OK: true, Session: session} +} + +// findSession mirrors findSession: timing-safe token comparison over the +// session table. +func findSession(credential string, sessions map[string]Session) (string, Session, bool) { + for token, session := range sessions { + if EqualSecret(credential, token) { + return token, session, true + } + } + return "", Session{}, false +} + +// Rejection is the exact response requireManagementAuth would return: the +// status and the JSON body bytes, byte-identical to Response.json on the TS +// side (compact JSON, no trailing newline). +type Rejection struct { + Status int + Body string +} + +// Decision is the outcome of one admission check. +type Decision struct { + // Principal is non-empty exactly when the request is admitted. + Principal Principal + // Rejection is non-nil exactly when the request is not admitted. + Rejection *Rejection +} + +// Gate carries the process-scoped admission state and replay stores. One Gate +// serves one process, mirroring the module-level maps in management-auth.ts. +// Methods are safe for concurrent use; the TS side is single-threaded, so the +// mutex only protects the Go process's own concurrency. +type Gate struct { + mu sync.Mutex + state State + cfg ConfigView + local LocalContext + nowFn func() int64 + + consumedLocalRead map[string]int64 + consumedProviderReload map[string]int64 + consumedGuiPair map[string]int64 +} + +// NewGate builds a Gate over the given state, config view, and local context. +func NewGate(state State, cfg ConfigView, local LocalContext) *Gate { + if state.Sessions == nil { + state.Sessions = map[string]Session{} + } + return &Gate{ + state: state, + cfg: cfg, + local: local, + nowFn: time.Now().UnixMilli, + consumedLocalRead: map[string]int64{}, + consumedProviderReload: map[string]int64{}, + consumedGuiPair: map[string]int64{}, + } +} + +// WithClock replaces the wall-clock source (tests only). +func (g *Gate) WithClock(now func() int64) *Gate { + g.nowFn = now + return g +} + +// State returns a copy of the admission state (tests and management routes +// that need to inspect sessions). +func (g *Gate) State() State { + g.mu.Lock() + defer g.mu.Unlock() + out := g.state + out.Sessions = map[string]Session{} + for k, v := range g.state.Sessions { + out.Sessions[k] = v + } + return out +} + +// Sessions exposes the session table for direct manipulation (session routes +// mint and revoke through the TS side pre-flip; the Go side owns it at the +// flip). +func (g *Gate) Sessions() map[string]Session { + g.mu.Lock() + defer g.mu.Unlock() + out := make(map[string]Session, len(g.state.Sessions)) + for k, v := range g.state.Sessions { + out[k] = v + } + return out +} + +// Admit mirrors resolveManagementAdmission plus the rejection mapping of +// requireManagementAuth: capabilities first (they do not need the state to be +// available), then the admin token, then a dashboard session. +func (g *Gate) Admit(r *Request) Decision { + g.mu.Lock() + defer g.mu.Unlock() + now := g.nowFn() + + if g.hasSystemRestartCapability(r) { + return Decision{Principal: PrincipalSystemRestartCapability} + } + if g.hasLocalProviderReloadCapability(r, now) { + return Decision{Principal: PrincipalLocalProviderReloadCapability} + } + if g.hasLocalReadCapability(r, now) { + return Decision{Principal: PrincipalLocalReadCapability} + } + if g.hasGuiPairCapability(r, now) { + return Decision{Principal: PrincipalGuiPairCapability} + } + if g.state.Available { + actual := RequestManagementCredential(r) + if actual != "" && EqualSecret(actual, g.state.Token) { + return Decision{Principal: PrincipalAdminToken} + } + if admission := AuthorizeSession(r, g.cfg, g.state.Sessions, now); admission.OK { + return Decision{Principal: PrincipalGuiSession} + } + } + if !g.state.Available { + return Decision{Rejection: &Rejection{ + Status: 503, + Body: unavailableBody(g.state.Reason), + }} + } + return Decision{Rejection: &Rejection{ + Status: 401, + Body: unauthorizedBody, + }} +} + +// capabilityRequestPath extracts the URL pathname, mirroring new +// URL(req.url).pathname. Empty means unparseable. +func capabilityRequestPath(r *Request) string { + parsed := r.parsedURL() + if parsed == nil { + return "" + } + return parsed.Path +} + +func hasQuery(r *Request) bool { + parsed := r.parsedURL() + if parsed == nil { + return true + } + return parsed.RawQuery != "" +} + +func (g *Gate) hasSystemRestartCapability(r *Request) bool { + if g.local.AttestationSecret == "" || r.Method != "POST" { + return false + } + path := capabilityRequestPath(r) + if path == "" || path != SystemRestartPath { + return false + } + kind, pid := ParseExpectedPID(r.Get(SystemRestartExpectedPIDHeader)) + if kind != ExpectedPIDPresent || pid != int64(g.local.PID) { + return false + } + return VerifySystemRestartCapability( + g.local.AttestationSecret, + r.Get(SystemRestartNonceHeader), + r.Method, + path, + pid, + g.local.Port, + r.Get(SystemRestartCapabilityHeader), + ) +} + +// hasEmptyBodyRequest mirrors the TS content-length === "0" and no +// transfer-encoding preconditions on the reload and gui-pair paths. +func hasEmptyBodyRequest(r *Request) bool { + if r.Get(contentLengthHeader) != "0" { + return false + } + return r.Get(transferEncodingHeader) == "" +} + +func (g *Gate) hasLocalProviderReloadCapability(r *Request, now int64) bool { + if g.local.AttestationSecret == "" || r.Method != "POST" { + return false + } + path := capabilityRequestPath(r) + if path == "" || path != LocalProviderReloadPath { + return false + } + if hasQuery(r) { + return false + } + if !hasEmptyBodyRequest(r) { + return false + } + kind, pid := ParseExpectedPID(r.Get(LocalProviderReloadExpectedPIDHeader)) + if kind != ExpectedPIDPresent || pid != int64(g.local.PID) { + return false + } + expiresAt, ok := parseExpiryHeader(r.Get(LocalProviderReloadExpiresAtHeader)) + if !ok { + return false + } + name := r.Get(LocalProviderReloadNameHeader) + capability := r.Get(LocalProviderReloadCapabilityHeader) + if !VerifyLocalProviderReloadCapability( + g.local.AttestationSecret, + r.Get(LocalProviderReloadNonceHeader), + r.Method, + path, + name, + pid, + g.local.Port, + expiresAt, + capability, + now, + ) { + return false + } + pruneConsumed(g.consumedProviderReload, now) + if capability == "" || consumedHas(g.consumedProviderReload, capability) { + return false + } + if len(g.consumedProviderReload) >= providerReloadReplayLimit { + return false + } + g.consumedProviderReload[capability] = expiresAt + return true +} + +func (g *Gate) hasLocalReadCapability(r *Request, now int64) bool { + if g.local.AttestationSecret == "" || r.Method != "GET" { + return false + } + path := capabilityRequestPath(r) + if path == "" { + return false + } + if hasQuery(r) { + return false + } + kind, pid := ParseExpectedPID(r.Get(LocalManagementExpectedPIDHeader)) + if kind != ExpectedPIDPresent || pid != int64(g.local.PID) { + return false + } + expiresAt, ok := parseExpiryHeader(r.Get(LocalManagementExpiresAtHeader)) + if !ok { + return false + } + capability := r.Get(LocalManagementCapabilityHeader) + if !VerifyLocalManagementReadCapability( + g.local.AttestationSecret, + r.Get(LocalManagementNonceHeader), + r.Method, + path, + pid, + g.local.Port, + expiresAt, + capability, + now, + ) { + return false + } + pruneConsumed(g.consumedLocalRead, now) + if capability == "" || consumedHas(g.consumedLocalRead, capability) { + return false + } + if len(g.consumedLocalRead) >= localReadReplayLimit { + return false + } + g.consumedLocalRead[capability] = expiresAt + return true +} + +func (g *Gate) hasGuiPairCapability(r *Request, now int64) bool { + if g.local.AttestationSecret == "" || r.Method != "POST" { + return false + } + path := capabilityRequestPath(r) + if path == "" || path != GUIPairPath { + return false + } + if hasQuery(r) { + return false + } + if !hasEmptyBodyRequest(r) { + return false + } + kind, pid := ParseExpectedPID(r.Get(GUIPairExpectedPIDHeader)) + if kind != ExpectedPIDPresent || pid != int64(g.local.PID) { + return false + } + expiresAt, ok := parseExpiryHeader(r.Get(GUIPairExpiresAtHeader)) + if !ok { + return false + } + capability := r.Get(GUIPairCapabilityHeader) + if !VerifyGuiPairCapability( + g.local.AttestationSecret, + r.Get(GUIPairNonceHeader), + r.Method, + path, + r.Get(GUIPairBrowserOriginHeader), + pid, + g.local.Port, + expiresAt, + capability, + now, + ) { + return false + } + pruneConsumed(g.consumedGuiPair, now) + if capability == "" { + return false + } + digest := sha256Base64URL(capability) + if consumedHas(g.consumedGuiPair, digest) { + return false + } + if len(g.consumedGuiPair) >= guiPairReplayLimit { + return false + } + g.consumedGuiPair[digest] = expiresAt + return true +} + +func pruneConsumed(store map[string]int64, now int64) { + for consumed, retainedUntil := range store { + if retainedUntil <= now { + delete(store, consumed) + } + } +} + +func consumedHas(store map[string]int64, key string) bool { + _, ok := store[key] + return ok +} + +// sha256Base64URL mirrors the SHA-256 base64url digest the TS side uses to key +// the gui-pair replay store. +func sha256Base64URL(value string) string { + sum := sha256.Sum256([]byte(value)) + return base64.RawURLEncoding.EncodeToString(sum[:]) +} + +// unauthorizedBody is the exact 401 body requireManagementAuth returns. +const unauthorizedBody = `{"error":"opencodex admin token required"}` + +// unavailableBody is the exact 503 body for an unavailable management state; +// the reason string is JSON-escaped the way Response.json escapes it. +func unavailableBody(reason string) string { + escaped := strings.ReplaceAll(reason, `\`, `\\`) + escaped = strings.ReplaceAll(escaped, `"`, `\"`) + return `{"error":"management API unavailable","reason":"` + escaped + `","hint":"Set OPENCODEX_ADMIN_AUTH_TOKEN to bypass file-backed admin token ACL hardening"}` +} diff --git a/go/internal/managementauth/managementauth_test.go b/go/internal/managementauth/managementauth_test.go new file mode 100644 index 0000000000..a2dc124db2 --- /dev/null +++ b/go/internal/managementauth/managementauth_test.go @@ -0,0 +1,518 @@ +package managementauth + +import ( + "os" + "path/filepath" + "testing" +) + +// Fixed clock base for deterministic expiry/replay tests. Values mirror what +// the TS side calls now: epoch milliseconds. +const testNow = 1_800_000_000_000 + +func testGate(t *testing.T, state State, cfg ConfigView) *Gate { + t.Helper() + gate := NewGate(state, cfg, LocalContext{ + AttestationSecret: "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG", + PID: 4242, + Port: 10100, + }) + return gate.WithClock(func() int64 { return testNow }) +} + +func req(method, rawURL string, headers map[string]string) *Request { + lower := make(map[string]string, len(headers)) + for name, value := range headers { + lower[lowerHeader(name)] = value + } + return &Request{URL: rawURL, Method: method, Header: lower} +} + +func lowerHeader(name string) string { + out := make([]byte, 0, len(name)) + for i := 0; i < len(name); i++ { + c := name[i] + if c >= 'A' && c <= 'Z' { + c += 'a' - 'A' + } + out = append(out, c) + } + return string(out) +} + +func availableState(token string) State { + return State{Available: true, Token: token, Source: "environment", Sessions: map[string]Session{}} +} + +func TestBase64URLAndSecretShape(t *testing.T) { + if !IsBase64URL256("abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG") { + t.Fatal("43-char base64url secret must be valid") + } + if IsBase64URL256("short") || IsBase64URL256("not+valid/forty3chars") { + t.Fatal("invalid base64url shapes must be rejected") + } + if !IsAttestationSecret("abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG") { + t.Fatal("attestation secret shape check must accept a 43-char base64url value") + } + if !EqualSecret("ocx_admin_x", "ocx_admin_x") { + t.Fatal("equal secrets must compare equal") + } + if EqualSecret("ocx_admin_x", "ocx_admin_y") || EqualSecret("a", "ab") { + t.Fatal("unequal secrets must compare unequal") + } +} + +func TestParseExpectedPID(t *testing.T) { + cases := []struct { + value string + kind ExpectedPIDKind + pid int64 + }{ + {"", ExpectedPIDAbsent, 0}, + {"0", ExpectedPIDInvalid, 0}, + {"007", ExpectedPIDInvalid, 0}, + {"-1", ExpectedPIDInvalid, 0}, + {"4242", ExpectedPIDPresent, 4242}, + {"1", ExpectedPIDPresent, 1}, + {"1.5", ExpectedPIDInvalid, 0}, + {"abc", ExpectedPIDInvalid, 0}, + } + for _, c := range cases { + kind, pid := ParseExpectedPID(c.value) + if kind != c.kind || pid != c.pid { + t.Errorf("ParseExpectedPID(%q) = (%s, %d), want (%s, %d)", c.value, kind, pid, c.kind, c.pid) + } + } +} + +func TestCapabilityRoundTrips(t *testing.T) { + secret := "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG" + nonce := "GFEDCBA9876543210zyxwvutsrqponmlkjihgfedcba" + pid := int64(4242) + port := 10100 + now := int64(testNow) + expiresAt := now + 5_000 + + if len(nonce) != 43 { + t.Fatalf("nonce fixture must be 43 chars, got %d", len(nonce)) + } + + // System restart: no expiry window; any time verifies. + cap := CreateSystemRestartCapability(secret, nonce, SystemRestartMethod, SystemRestartPath, pid, port) + if cap == "" { + t.Fatal("restart capability must mint") + } + if !VerifySystemRestartCapability(secret, nonce, SystemRestartMethod, SystemRestartPath, pid, port, cap) { + t.Fatal("restart capability must verify") + } + if VerifySystemRestartCapability(secret, nonce, "PUT", SystemRestartPath, pid, port, cap) { + t.Fatal("restart capability for a different method must not verify") + } + if VerifySystemRestartCapability(secret, nonce, SystemRestartMethod, SystemRestartPath, pid, port, cap+"A") { + t.Fatal("tampered restart capability must not verify") + } + + // Local read: TTL window enforced. + readCap := CreateLocalManagementReadCapability(secret, nonce, "GET", LocalManagementReadPathSystemMemory, pid, port, expiresAt) + if readCap == "" { + t.Fatal("read capability must mint") + } + if !VerifyLocalManagementReadCapability(secret, nonce, "GET", LocalManagementReadPathSystemMemory, pid, port, expiresAt, readCap, now) { + t.Fatal("read capability must verify within its TTL") + } + if VerifyLocalManagementReadCapability(secret, nonce, "GET", LocalManagementReadPathSystemMemory, pid, port, expiresAt, readCap, expiresAt+1) { + t.Fatal("read capability must not verify after expiry") + } + if VerifyLocalManagementReadCapability(secret, nonce, "GET", LocalManagementReadPathSystemMemory, pid, port, expiresAt, readCap, now+LocalManagementCapabilityTTLMs+1) { + t.Fatal("read capability must not verify beyond the TTL ceiling") + } + if VerifyLocalManagementReadCapability(secret, nonce, "GET", "/api/system/memory/", pid, port, expiresAt, readCap, now) { + t.Fatal("read capability for a non-allowlisted path must not verify") + } + if VerifyLocalManagementReadCapability(secret, nonce, "GET", "/api/system/memory", pid, port, expiresAt, "", now) { + t.Fatal("missing capability must not verify") + } + + // Provider reload: name binding. + reloadCap := CreateLocalProviderReloadCapability(secret, nonce, LocalProviderReloadMethod, LocalProviderReloadPath, "openai", pid, port, expiresAt) + if reloadCap == "" { + t.Fatal("reload capability must mint") + } + if !VerifyLocalProviderReloadCapability(secret, nonce, LocalProviderReloadMethod, LocalProviderReloadPath, "openai", pid, port, expiresAt, reloadCap, now) { + t.Fatal("reload capability must verify") + } + if VerifyLocalProviderReloadCapability(secret, nonce, LocalProviderReloadMethod, LocalProviderReloadPath, "other", pid, port, expiresAt, reloadCap, now) { + t.Fatal("reload capability for another provider name must not verify") + } + if CreateLocalProviderReloadCapability(secret, nonce, LocalProviderReloadMethod, LocalProviderReloadPath, "not valid!", pid, port, expiresAt) != "" { + t.Fatal("reload capability with an invalid provider name must not mint") + } + + // GUI pair: browser-origin canonicalisation is part of the payload. + guiCap := CreateGuiPairCapability(secret, nonce, GUIPairMethod, GUIPairPath, "https://ocx.example", pid, port, expiresAt) + if guiCap == "" { + t.Fatal("gui-pair capability must mint") + } + if !VerifyGuiPairCapability(secret, nonce, GUIPairMethod, GUIPairPath, "https://ocx.example", pid, port, expiresAt, guiCap, now) { + t.Fatal("gui-pair capability must verify") + } + if VerifyGuiPairCapability(secret, nonce, GUIPairMethod, GUIPairPath, "https://ocx.example:443", pid, port, expiresAt, guiCap, now) { + t.Fatal("gui-pair capability payload binds the canonical origin; default-port spelling must not verify") + } + if got := CreateGuiPairCapability(secret, nonce, GUIPairMethod, GUIPairPath, "https://ocx.example:443", pid, port, expiresAt); got != "" { + t.Fatalf("minting with a non-canonical origin must be refused, got %q", got) + } + + // Attestation proof. + proof := CreateLocalAttestationProof(secret, "QAZWSXEDCRFVTGBYHNUJMIKOLPqazwsxedcrfvtgbay", pid, port) + if proof == "" { + t.Fatal("attestation proof must mint") + } + if !VerifyLocalAttestationProof(secret, "QAZWSXEDCRFVTGBYHNUJMIKOLPqazwsxedcrfvtgbay", pid, port, proof) { + t.Fatal("attestation proof must verify") + } +} + +func TestCanonicalGuiBrowserOrigin(t *testing.T) { + cases := []struct { + in string + want string + }{ + {"https://ocx.example", "https://ocx.example"}, + {"https://ocx.example:443", "https://ocx.example"}, + {"http://ocx.example:8080", "http://ocx.example:8080"}, + {"http://localhost:10100", "http://localhost:10100"}, + {"https://ocx.example/", "https://ocx.example"}, + {"https://ocx.example/path", ""}, + {"https://user:pass@ocx.example", ""}, + {"https://ocx.example?q=1", ""}, + {"https://ocx.example#f", ""}, + {" not-trimmed ", ""}, + {"chrome-extension://abc", "chrome-extension://abc"}, + {"file:///etc/hosts", ""}, + {"", ""}, + } + for _, c := range cases { + if got := CanonicalGuiBrowserOrigin(c.in); got != c.want { + t.Errorf("CanonicalGuiBrowserOrigin(%q) = %q, want %q", c.in, got, c.want) + } + } +} + +func TestManagementRequestOrigin(t *testing.T) { + loopback := ConfigView{Hostname: "127.0.0.1"} + remote := ConfigView{Hostname: "0.0.0.0"} + hub := ConfigView{Hostname: "0.0.0.0", RuntimeRole: "hub", HubManagementPublicOrigin: "https://ocx.example"} + + cases := []struct { + name string + r *Request + cfg ConfigView + want string + }{ + { + name: "loopback observed origin with port", + r: req("GET", "http://127.0.0.1:10100/api/config", map[string]string{"Host": "127.0.0.1:10100"}), + cfg: loopback, + want: "http://127.0.0.1:10100", + }, + { + name: "loopback default port dropped", + r: req("GET", "http://localhost/api/config", map[string]string{"Host": "localhost"}), + cfg: loopback, + want: "http://localhost", + }, + { + name: "localhost trailing dot is loopback", + r: req("GET", "http://localhost.:10100/api/config", map[string]string{"Host": "localhost.:10100"}), + cfg: loopback, + want: "http://localhost.:10100", + }, + { + name: "non-loopback without api auth has no origin", + r: req("GET", "http://mynode.lan:10100/api/config", map[string]string{"Host": "mynode.lan:10100"}), + cfg: ConfigView{Hostname: "localhost"}, + want: "", + }, + { + name: "non-loopback observed origin when api auth required", + r: req("GET", "http://mynode.lan:10100/api/config", map[string]string{"Host": "mynode.lan:10100"}), + cfg: remote, + want: "http://mynode.lan:10100", + }, + { + name: "hub uses its configured public origin", + r: req("GET", "http://10.0.0.5:10100/api/config", map[string]string{"Host": "10.0.0.5:10100"}), + cfg: hub, + want: "https://ocx.example", + }, + { + name: "missing host header has no origin", + r: req("GET", "http://127.0.0.1:10100/api/config", nil), + cfg: loopback, + want: "", + }, + } + for _, c := range cases { + if got := ManagementRequestOrigin(c.r, c.cfg); got != c.want { + t.Errorf("%s: origin = %q, want %q", c.name, got, c.want) + } + } +} + +func TestAdminTokenAdmission(t *testing.T) { + token := "ocx_admin_abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG" + gate := testGate(t, availableState(token), ConfigView{Hostname: "127.0.0.1"}) + + // Correct token via the API-key header. + decision := gate.Admit(req("GET", "http://127.0.0.1:10100/api/config", map[string]string{ + "x-opencodex-api-key": token, + "host": "127.0.0.1:10100", + })) + if decision.Principal != PrincipalAdminToken { + t.Fatalf("correct token must admit as admin-token, got %q (rejection %+v)", decision.Principal, decision.Rejection) + } + + // Wrong token, state available -> 401 with the exact TS body. + decision = gate.Admit(req("GET", "http://127.0.0.1:10100/api/config", map[string]string{ + "authorization": "Bearer wrong-token", + "host": "127.0.0.1:10100", + })) + if decision.Rejection == nil || decision.Rejection.Status != 401 { + t.Fatalf("wrong token must be rejected with 401, got %+v", decision.Rejection) + } + if decision.Rejection.Body != `{"error":"opencodex admin token required"}` { + t.Fatalf("401 body = %q", decision.Rejection.Body) + } + + // Bearer stripping is case-insensitive and whitespace-trimmed. + decision = gate.Admit(req("GET", "http://127.0.0.1:10100/api/config", map[string]string{ + "authorization": "bearer " + token + " ", + "host": "127.0.0.1:10100", + })) + if decision.Principal != PrincipalAdminToken { + t.Fatalf("bearer-prefixed token must admit, got %q", decision.Principal) + } + + // Unavailable state -> 503 with reason and hint. + unavailable := State{Available: false, Reason: "management token initialization failed"} + decision = testGate(t, unavailable, ConfigView{Hostname: "127.0.0.1"}).Admit( + req("GET", "http://127.0.0.1:10100/api/config", nil), + ) + if decision.Rejection == nil || decision.Rejection.Status != 503 { + t.Fatalf("unavailable state must reject with 503, got %+v", decision.Rejection) + } + want := `{"error":"management API unavailable","reason":"management token initialization failed","hint":"Set OPENCODEX_ADMIN_AUTH_TOKEN to bypass file-backed admin token ACL hardening"}` + if decision.Rejection.Body != want { + t.Fatalf("503 body:\n got %q\nwant %q", decision.Rejection.Body, want) + } +} + +func TestCapabilityAdmissionOrderAndReplay(t *testing.T) { + state := availableState("ocx_admin_abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG") + secret := "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG" + local := LocalContext{AttestationSecret: secret, PID: 4242, Port: 10100} + gate := NewGate(state, ConfigView{Hostname: "127.0.0.1"}, local).WithClock(func() int64 { return testNow }) + expiresAt := int64(testNow + 5_000) + nonce := "GFEDCBA9876543210zyxwvutsrqponmlkjihgfedcba" + + // A valid local-read capability admits with the capability principal even + // though the admin token is wrong on the request. + cap := CreateLocalManagementReadCapability(secret, nonce, "GET", LocalManagementReadPathCodexAccounts, 4242, 10100, expiresAt) + r := req("GET", "http://127.0.0.1:10100/api/codex-auth/accounts", map[string]string{ + "host": "127.0.0.1:10100", + LocalManagementExpectedPIDHeader: "4242", + LocalManagementNonceHeader: nonce, + LocalManagementExpiresAtHeader: "1800000005000", + LocalManagementCapabilityHeader: cap, + }) + decision := gate.Admit(r) + if decision.Principal != PrincipalLocalReadCapability { + t.Fatalf("capability must admit ahead of the token check, got %q", decision.Principal) + } + + // A second, distinct request with the same capability is a replay and must + // be rejected exactly like the TS consumed-capability store rejects it. + replay := req("GET", "http://127.0.0.1:10100/api/codex-auth/accounts", map[string]string{ + "host": "127.0.0.1:10100", + LocalManagementExpectedPIDHeader: "4242", + LocalManagementNonceHeader: nonce, + LocalManagementExpiresAtHeader: "1800000005000", + LocalManagementCapabilityHeader: cap, + }) + decision = gate.Admit(replay) + if decision.Principal != "" || decision.Rejection == nil || decision.Rejection.Status != 401 { + t.Fatalf("replayed capability must be rejected with 401, got %+v", decision) + } + + // Wrong expected pid never reaches verification. + wrongPid := req("GET", "http://127.0.0.1:10100/api/codex-auth/accounts", map[string]string{ + LocalManagementExpectedPIDHeader: "1", + LocalManagementCapabilityHeader: cap, + }) + if decision := gate.Admit(wrongPid); decision.Principal != "" { + t.Fatalf("wrong expected pid must reject, got %q", decision.Principal) + } + + // A query string disqualifies the narrow local-read grant. + withQuery := req("GET", "http://127.0.0.1:10100/api/codex-auth/accounts?x=1", map[string]string{ + LocalManagementExpectedPIDHeader: "4242", + }) + if decision := gate.Admit(withQuery); decision.Principal != "" { + t.Fatalf("query-bearing local read must reject, got %q", decision.Principal) + } + + // Non-POST never matches the gui-pair path. + pairCap := CreateGuiPairCapability(secret, nonce, GUIPairMethod, GUIPairPath, "http://localhost:5173", 4242, 10100, expiresAt) + pairReq := req("GET", "http://127.0.0.1:10100/api/gui/pairing-grants", map[string]string{ + GUIPairExpectedPIDHeader: "4242", + GUIPairCapabilityHeader: pairCap, + GUIPairBrowserOriginHeader: "http://localhost:5173", + GUIPairExpiresAtHeader: "1800000005000", + GUIPairNonceHeader: nonce, + "content-length": "0", + }) + if decision := gate.Admit(pairReq); decision.Principal != "" { + t.Fatalf("GET on the gui-pair POST path must reject, got %q", decision.Principal) + } +} + +func TestSessionAuthorization(t *testing.T) { + state := availableState("ocx_admin_abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG") + cfg := ConfigView{Hostname: "127.0.0.1"} + gate := testGate(t, state, cfg) + + // Mint a session the way the TS side does (loopback issuance records the + // observed origin as both server and browser origin). + mint := req("GET", "http://127.0.0.1:10100/api/session/bootstrap", map[string]string{"host": "127.0.0.1:10100"}) + serverOrigin := ManagementRequestOrigin(mint, cfg) + if serverOrigin != "http://127.0.0.1:10100" { + t.Fatalf("fixture origin = %q", serverOrigin) + } + token := "ocx_session_abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + session := Session{ + ServerOrigin: serverOrigin, + BrowserOrigin: serverOrigin, + CSRF: "csrf-token-value-1234567890123456789012345", + ExpiresAt: testNow + loopbackGuiSessionTTLMs, + Issuance: "loopback", + } + sessions := map[string]Session{token: session} + gate.state.Sessions = sessions + + // A safe GET with the session token and matching origin admits. + good := req("GET", "http://127.0.0.1:10100/api/config", map[string]string{ + "host": "127.0.0.1:10100", + "x-opencodex-api-key": token, + "x-opencodex-gui-origin": serverOrigin, + }) + decision := gate.Admit(good) + if decision.Principal != PrincipalGuiSession { + t.Fatalf("valid session GET must admit as gui-session, got %q", decision.Principal) + } + + // An unsafe POST without the CSRF token rejects with the exact reason. + mutation := req("POST", "http://127.0.0.1:10100/api/config", map[string]string{ + "host": "127.0.0.1:10100", + "x-opencodex-api-key": token, + "origin": serverOrigin, + "x-opencodex-gui-origin": serverOrigin, + }) + admission := AuthorizeSession(mutation, cfg, sessions, testNow) + if admission.OK || admission.Reason != SessionCSRF { + t.Fatalf("unsafe session mutation without CSRF must reject with csrf reason, got %+v", admission) + } + if decision := gate.Admit(mutation); decision.Principal != "" { + t.Fatalf("session mutation without CSRF must not admit, got %q", decision.Principal) + } + + // With the correct CSRF header it admits. + withCSRF := req("POST", "http://127.0.0.1:10100/api/config", map[string]string{ + "host": "127.0.0.1:10100", + "x-opencodex-api-key": token, + "origin": serverOrigin, + "x-opencodex-gui-origin": serverOrigin, + "x-opencodex-csrf-token": session.CSRF, + }) + if decision := gate.Admit(withCSRF); decision.Principal != PrincipalGuiSession { + t.Fatalf("session mutation with CSRF must admit, got %q", decision.Principal) + } + + // A mismatched browser origin rejects. + mismatched := req("GET", "http://127.0.0.1:10100/api/config", map[string]string{ + "host": "127.0.0.1:10100", + "x-opencodex-api-key": token, + "x-opencodex-gui-origin": "http://evil.example", + }) + if decision := gate.Admit(mismatched); decision.Principal != "" { + t.Fatalf("mismatched browser origin must reject, got %q", decision.Principal) + } + + // An expired session deletes and rejects. + expiredToken := "ocx_session_zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" + sessions[expiredToken] = Session{ + ServerOrigin: serverOrigin, + BrowserOrigin: serverOrigin, + CSRF: "x", + ExpiresAt: testNow - 1, + Issuance: "loopback", + } + expired := req("GET", "http://127.0.0.1:10100/api/config", map[string]string{ + "host": "127.0.0.1:10100", + "x-opencodex-api-key": expiredToken, + }) + admission = AuthorizeSession(expired, cfg, sessions, testNow) + if admission.OK || admission.Reason != SessionExpired { + t.Fatalf("expired session must reject with expired reason, got %+v", admission) + } + if _, stillPresent := sessions[expiredToken]; stillPresent { + t.Fatal("expired session must be deleted from the table") + } +} + +func TestAdminTokenFileLoad(t *testing.T) { + dir := t.TempDir() + valid := "ocx_admin_abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG" + + t.Run("missing file yields no token", func(t *testing.T) { + if got := LoadAdminToken(dir); got != "" { + t.Fatalf("missing token file must load no token, got %q", got) + } + }) + + t.Run("valid file loads", func(t *testing.T) { + path := AdminTokenFilePath(dir) + if err := os.WriteFile(path, []byte(valid+"\n"), 0o600); err != nil { + t.Fatal(err) + } + if got := LoadAdminToken(dir); got != valid { + t.Fatalf("valid token file must load, got %q", got) + } + }) + + t.Run("malformed content yields no token", func(t *testing.T) { + sub := t.TempDir() + path := filepath.Join(sub, "admin-api-token") + if err := os.WriteFile(path, []byte("not-a-token\n"), 0o600); err != nil { + t.Fatal(err) + } + if got := LoadAdminToken(sub); got != "" { + t.Fatalf("malformed token file must load no token, got %q", got) + } + }) + + t.Run("oversized file yields no token", func(t *testing.T) { + sub := t.TempDir() + path := filepath.Join(sub, "admin-api-token") + big := make([]byte, 600) + for i := range big { + big[i] = 'a' + } + if err := os.WriteFile(path, big, 0o600); err != nil { + t.Fatal(err) + } + if got := LoadAdminToken(sub); got != "" { + t.Fatalf("oversized token file must load no token, got %q", got) + } + }) +} diff --git a/go/internal/managementauth/session.go b/go/internal/managementauth/session.go new file mode 100644 index 0000000000..ce5aa6b28f --- /dev/null +++ b/go/internal/managementauth/session.go @@ -0,0 +1,228 @@ +package managementauth + +// Origin machinery and admin-token loading (ADR-0008, ticket #18). The origin +// functions mirror src/server/auth-cors.ts (parseHttpHost, isLoopbackHostname, +// isApiAuthRequired, managementRequestOrigin) because dashboard-session +// authorization compares the request's derived server origin against the +// origin recorded on the session at mint time; a mismatch must reject exactly +// when TypeScript rejects. + +import ( + "net/url" + "os" + "path/filepath" + "regexp" + "strings" +) + +// adminTokenPattern mirrors the token-file shape check in +// src/server/management-auth.ts (readExistingToken) and src/lib/admin-secrets.ts. +var adminTokenPattern = regexp.MustCompile(`^ocx_admin_[A-Za-z0-9_-]{43}$`) + +// ADMIN_TOKEN_FILE mirrors src/lib/admin-secrets.ts. +const ADMIN_TOKEN_FILE = "admin-api-token" + +// AdminTokenFilePath mirrors adminApiTokenFilePath. +func AdminTokenFilePath(configDir string) string { + return filepath.Join(configDir, ADMIN_TOKEN_FILE) +} + +// LoadAdminToken mirrors loadAdminTokenFromFile in src/lib/admin-secrets.ts: +// a regular, non-symlink file of at most 512 bytes whose trimmed content has +// the ocx_admin_ shape. It never creates or hardens the file: token-file +// creation and ACL hardening are the serving process's job at the flip, and a +// read-only sidecar must not mutate the parent's secret file pre-flip. +func LoadAdminToken(configDir string) string { + path := AdminTokenFilePath(configDir) + info, err := os.Lstat(path) + if err != nil || !info.Mode().IsRegular() || info.Mode()&os.ModeSymlink != 0 || info.Size() > 512 { + return "" + } + raw, err := os.ReadFile(path) + if err != nil { + return "" + } + token := strings.TrimSpace(string(raw)) + if !adminTokenPattern.MatchString(token) { + return "" + } + return token +} + +// EnvAdminToken mirrors the OPENCODEX_ADMIN_AUTH_TOKEN trim in +// src/server/management-auth.ts (environmentToken). No shape check applies to +// an environment token; equality is what matters. +func EnvAdminToken(environ func(string) string) string { + return strings.TrimSpace(environ("OPENCODEX_ADMIN_AUTH_TOKEN")) +} + +// EqualSecret mirrors equalSecret: timing-safe byte equality with a length +// pre-check, over UTF-8 encodings. +func EqualSecret(actual, expected string) bool { + left := []byte(actual) + right := []byte(expected) + if len(left) != len(right) { + return false + } + return equalCapabilityBytes(string(left), string(right)) +} + +// Request is the admission-relevant slice of one HTTP request: the full URL +// (protocol/host/path/search as TypeScript sees them), the method, and the +// headers with names lowercased. An absent header and an empty value are the +// same thing here; see capability.go for why that never changes a decision. +type Request struct { + URL string + Method string + Header map[string]string +} + +// Get mirrors Headers.get with case-insensitive names. +func (r *Request) Get(name string) string { + if r == nil || r.Header == nil { + return "" + } + return r.Header[strings.ToLower(name)] +} + +// parsedURL lazily parses r.URL; nil when unparseable. +func (r *Request) parsedURL() *url.URL { + parsed, err := url.Parse(r.URL) + if err != nil { + return nil + } + return parsed +} + +// MethodName returns the method upper-cased the way Go HTTP normalises it +// (the TS side receives whatever the client sent; the capability contracts +// require exact uppercase POST/GET so the comparison is by exact string). +func (r *Request) MethodName() string { + return r.Method +} + +// parseHTTPHost mirrors parseHttpHost in src/server/auth-cors.ts: parse the +// Host header as http:// and return the lowercased WHATWG hostname +// (IPv6 bracketed) and the URL port ("" when default/absent). Nil means the +// header was absent or unparseable. +func parseHTTPHost(value string) *struct { + Hostname string + Port string +} { + if value == "" { + return nil + } + parsed, err := url.Parse("http://" + value) + if err != nil { + return nil + } + if parsed.Host == "" { + return nil + } + return &struct { + Hostname string + Port string + }{Hostname: whatwgHostname(parsed), Port: parsed.Port()} +} + +// whatwgHostname returns the hostname the way URL.hostname serialises it: +// lowercased, IPv6 bracketed. Go's Hostname() strips brackets and keeps case, +// so this rebuilds the WHATWG form. +func whatwgHostname(u *url.URL) string { + host := u.Hostname() + lower := strings.ToLower(host) + if strings.Contains(lower, ":") { + return "[" + lower + "]" + } + return lower +} + +// isLoopbackHostname mirrors isLoopbackHostname in src/server/auth-cors.ts: +// the normalized hostname (trimmed, lowercased, one trailing dot stripped) is +// empty, "localhost", "127.0.0.1", "::1", or "[::1]". An empty input stays +// empty (loopback); the "127.0.0.1" default in the TS side applies only to +// undefined, which cannot occur here. +func isLoopbackHostname(hostname string) bool { + normalized := strings.TrimSuffix(strings.ToLower(strings.TrimSpace(hostname)), ".") + switch normalized { + case "", "localhost", "127.0.0.1", "::1", "[::1]": + return true + } + return false +} + +// IsApiAuthRequired mirrors isApiAuthRequired: false exactly when the +// configured hostname is a loopback hostname. +func IsApiAuthRequired(cfg ConfigView) bool { + return !isLoopbackHostname(cfg.Hostname) +} + +// ManagementRequestOrigin mirrors managementRequestOrigin in +// src/server/auth-cors.ts. It derives the origin a request was served from: +// for a loopback Host the observed protocol+host; for a non-loopback Host only +// when auth is required, preferring the hub's configured public origin when +// this process runs as a hub and one is configured. Empty means no origin. +func ManagementRequestOrigin(r *Request, cfg ConfigView) string { + host := r.Get("host") + parsedHost := parseHTTPHost(host) + if parsedHost == nil { + return "" + } + if isLoopbackHostname(parsedHost.Hostname) { + parsed := r.parsedURL() + if parsed == nil { + return "" + } + if parsed.Scheme != "http" && parsed.Scheme != "https" { + return "" + } + origin, err := url.Parse(parsed.Scheme + "://" + host) + if err != nil { + return "" + } + return whatwgOrigin(origin) + } + if !IsApiAuthRequired(cfg) { + return "" + } + if cfg.RuntimeRole == "hub" && cfg.HubManagementPublicOrigin != "" { + configured, err := url.Parse(cfg.HubManagementPublicOrigin) + if err == nil && configured.Host != "" { + valid := (configured.Scheme == "http" || configured.Scheme == "https") && + configured.User == nil && (configured.Path == "" || configured.Path == "/") && + configured.RawQuery == "" && configured.Fragment == "" + if valid { + return whatwgOrigin(configured) + } + } + } + parsed := r.parsedURL() + if parsed == nil { + return "" + } + if parsed.Scheme != "http" && parsed.Scheme != "https" { + return "" + } + origin, err := url.Parse(parsed.Scheme + "://" + host) + if err != nil { + return "" + } + return whatwgOrigin(origin) +} + +// RequestManagementCredential mirrors requestManagementCredential: the +// x-opencodex-api-key header trimmed, else the Authorization header with a +// case-insensitive "Bearer " prefix stripped and the result trimmed. +func RequestManagementCredential(r *Request) string { + if value := strings.TrimSpace(r.Get("x-opencodex-api-key")); value != "" { + return value + } + authorization := r.Get("authorization") + if authorization == "" { + return "" + } + stripped := bearerPrefixPattern.ReplaceAllString(authorization, "") + return strings.TrimSpace(stripped) +} + +var bearerPrefixPattern = regexp.MustCompile(`(?i)^Bearer\s+`) diff --git a/go/internal/routing/compatibility/slot.go b/go/internal/routing/compatibility/slot.go new file mode 100644 index 0000000000..a4e2e43a12 --- /dev/null +++ b/go/internal/routing/compatibility/slot.go @@ -0,0 +1,86 @@ +// Package compatibility hosts the core-owned slot for the optional +// compatibility-evidence provider (ADR-0008, ticket #19), mirroring +// src/routing/compatibility/provider-slot.ts. +// +// Routing in Go does not exist yet — this is the seam, established now so the +// "registers at activation" contract is reproducible and testable before any +// Lab content arrives. The slot is a nullable provider reference installed +// only during activation: an install that never activates Lab never registers +// one, exactly like the TypeScript core, whose synchronous evidence assembler +// consults resolveCompatibilityEvidenceProvider and therefore must never pull +// the Lab module graph in. +// +// The provider is typed opaquely for now: the concrete profile/policy/options +// shapes belong to the Go routing port, and forcing them here would churn +// this file twice. When ticket #33 ports the Lab evidence provider the +// signature is refined to the routing types without changing the slot +// semantics this package pins. +package compatibility + +import "sync" + +// EvidenceOptions is the opaque carrier for whatever the routing assembler can +// supply without knowing anything Lab-specific (see CoreEvidenceOptions in the +// TS slot). Refined by the routing port. +type EvidenceOptions = map[string]any + +// CandidateEvidence is one provider/model → evidence projection. Refined by +// the routing port; until then the empty map is the honest "no Lab content +// registered" value rather than a pretend provider. +type CandidateEvidence = map[string]any + +// EvidenceProvider produces compatibility evidence per candidate. Mirrors the +// CompatibilityEvidenceProvider function type in the TS slot. +type EvidenceProvider func(options EvidenceOptions) CandidateEvidence + +// Slot is the core-owned nullable provider reference. +type Slot struct { + mu sync.RWMutex + installed *registration +} + +// registration carries identity so a detach removes only its own install even +// when a later activation replaced it (the TS detach compares with ===). +type registration struct { + provider EvidenceProvider +} + +// NewSlot returns an empty slot: no provider registered. +func NewSlot() *Slot { + return &Slot{} +} + +// Set installs the provider and returns a detach function. Mirrors +// setCompatibilityEvidenceProvider. +func (s *Slot) Set(next EvidenceProvider) func() { + s.mu.Lock() + defer s.mu.Unlock() + reg := ®istration{provider: next} + s.installed = reg + return func() { + s.mu.Lock() + defer s.mu.Unlock() + if s.installed == reg { + s.installed = nil + } + } +} + +// Resolve returns the installed provider, or nil when no optional subsystem is +// active. Mirrors resolveCompatibilityEvidenceProvider. +func (s *Slot) Resolve() EvidenceProvider { + s.mu.RLock() + defer s.mu.RUnlock() + if s.installed == nil { + return nil + } + return s.installed.provider +} + +// Reset clears the slot (test isolation only; mirrors +// resetCompatibilityEvidenceProviderForTests). +func (s *Slot) Reset() { + s.mu.Lock() + defer s.mu.Unlock() + s.installed = nil +} diff --git a/tests/go-auth-parity.test.ts b/tests/go-auth-parity.test.ts new file mode 100644 index 0000000000..1c946474c0 --- /dev/null +++ b/tests/go-auth-parity.test.ts @@ -0,0 +1,462 @@ +import { describe, expect, test } from "bun:test"; +import { mkdtempSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { randomBytes } from "node:crypto"; +import { managementPrincipal, requireManagementAuth, type ManagementAuthState, type LocalManagementAuthContext } from "../src/server/management-auth"; +import { authorizeGuiSessionRequest, type GuiSessionState, type GuiSessionRecord, type GuiPairingGrantRecord } from "../src/server/gui-session"; +import { createLocalManagementReadCapability, LOCAL_MANAGEMENT_READ_PATHS } from "../src/lib/local-management-capability"; +import { createSystemRestartCapability, SYSTEM_RESTART_PATH } from "../src/lib/system-restart-contract"; +import { createLocalProviderReloadCapability, LOCAL_PROVIDER_RELOAD_PATH } from "../src/lib/local-provider-reload-contract"; +import { createGuiPairCapability, GUI_PAIR_PATH } from "../src/lib/gui-pair-capability"; +import type { OcxConfig } from "../src/types"; + +/** + * Differential oracle for the Go management auth/session model (ADR-0008, + * ticket #18). + * + * The acceptance criterion is that Go validates the admin token, dashboard + * session, and capability principals with under-privileged requests rejected + * identically to TypeScript. This harness feeds the same ordered arrays of + * request vectors through src/server/management-auth.ts (in-process) and + * through the `ocx-sidecar authcheck` subcommand (one Go process per array, so + * the capability replay stores persist across the array exactly like the TS + * module-level stores), then compares the admission decisions byte for byte: + * the principal when admitted, and the exact status + JSON body when rejected. + * + * The vectors exercise every principal and every rejection shape: valid and + * tampered capabilities for all four capability contracts, wrong pid/expiry/ + * method/query/body-shape variants, admin-token equality through both header + * spellings, dashboard sessions with origin/CSRF/expiry outcomes (the + * session-level admission reason is probed too), and the 503 unavailable path. + */ + +const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); + +function goToolchainAvailable(): boolean { + return Bun.spawnSync(["go", "version"], { stdout: "ignore", stderr: "ignore" }).success; +} + +function buildSidecarBinary(): string { + const dir = mkdtempSync(join(tmpdir(), "ocx-go-auth-")); + const binPath = join(dir, process.platform === "win32" ? "ocx-sidecar.exe" : "ocx-sidecar"); + const build = Bun.spawnSync(["go", "build", "-o", binPath, "./cmd/ocx-sidecar"], { + cwd: join(repoRoot, "go"), + env: { ...process.env, CGO_ENABLED: "0" }, + stdout: "pipe", + stderr: "pipe", + }); + if (build.exitCode !== 0) { + throw new Error(`go build ./cmd/ocx-sidecar failed (${build.exitCode}):\n${new TextDecoder().decode(build.stderr)}`); + } + return binPath; +} + +const goAvailable = goToolchainAvailable(); +const sidecarBinary: string | null = goAvailable ? buildSidecarBinary() : null; + +const describeGo = goAvailable ? describe : describe.skip; + +// Fixed process identity for the vectors: the values are arbitrary numbers the +// capability HMACs bind to; both sides see the same ones. +const PID = 4242; +const PORT = 10100; +const SECRET = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG"; // 43-char base64url +const ADMIN_TOKEN = "ocx_admin_abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG"; + +function b64url43(): string { + return randomBytes(32).toString("base64url"); +} + +const LOOPBACK_URL = `http://127.0.0.1:${PORT}`; + +interface Vector { + request: { url: string; method: string; headers: Record }; + sessionProbe?: boolean; +} + +interface CaseInput { + state: { + available: boolean; + token?: string; + source?: string; + reason?: string; + sessions?: { token: string; serverOrigin: string; browserOrigin: string; csrf: string; expiresAt: number; issuance: string }[]; + }; + config: { hostname: string; runtimeRole?: string; hubManagementPublicOrigin?: string }; + local: { attestationSecret: string; pid: number; port: number }; + vectors: Vector[]; +} + +interface GoDecision { + admitted: boolean; + principal: string | null; + rejection: { status: number; body: string } | null; + sessionState?: string; +} + +function toTSConfig(view: CaseInput["config"]): OcxConfig { + return { + hostname: view.hostname, + runtimeRole: view.runtimeRole, + hub: view.hubManagementPublicOrigin !== undefined ? { managementPublicOrigin: view.hubManagementPublicOrigin } : undefined, + } as unknown as OcxConfig; +} + +function tsState(input: CaseInput): { state: ManagementAuthState; guiState: GuiSessionState } { + if (!input.state.available) { + return { state: { available: false, reason: input.state.reason ?? "" }, guiState: { sessions: new Map(), pairingGrants: new Map() } }; + } + const sessions = new Map(); + for (const entry of input.state.sessions ?? []) { + sessions.set(entry.token, { + serverOrigin: entry.serverOrigin, + browserOrigin: entry.browserOrigin, + csrfToken: entry.csrf, + expiresAt: entry.expiresAt, + issuance: entry.issuance, + }); + } + const pairingGrants = new Map(); + const state: ManagementAuthState = { + available: true, + token: input.state.token ?? "", + source: (input.state.source as "environment" | "file") ?? "environment", + sessions, + pairingGrants, + }; + // The gate mutates the session table (expiry deletion, remote sliding); the + // probe must observe the same table, so wrap the same Map instances. + return { state, guiState: { sessions, pairingGrants } }; +} + +async function tsDecisions(input: CaseInput): Promise { + const config = toTSConfig(input.config); + const local: LocalManagementAuthContext = { attestationSecret: input.local.attestationSecret, pid: input.local.pid, port: input.local.port }; + const { state, guiState } = tsState(input); + const decisions: GoDecision[] = []; + for (const vector of input.vectors) { + const req = new Request(vector.request.url, { method: vector.request.method, headers: vector.request.headers }); + const principal = managementPrincipal(req, state, config, local); + const probe = (out: GoDecision): void => { + if (vector.sessionProbe) { + const admission = authorizeGuiSessionRequest(req, config, guiState, Date.now()); + out.sessionState = admission.ok ? "ok" : admission.reason; + } + }; + if (principal) { + const out: GoDecision = { admitted: true, principal, rejection: null }; + probe(out); + decisions.push(out); + continue; + } + const gate = requireManagementAuth(req, state, config, local); + const out: GoDecision = { admitted: false, principal: null, rejection: { status: gate!.status, body: await gate!.text() } }; + probe(out); + decisions.push(out); + } + return decisions; +} + +function goDecisions(input: CaseInput): GoDecision[] { + // Each vector carries the case's state/config/local so the Go subcommand can + // build one Gate for the whole array (the replay stores persist across the + // vectors exactly like the TS module-level stores). + const flat = input.vectors.map((vector) => ({ + request: vector.request, + state: input.state, + config: input.config, + local: input.local, + sessionProbe: vector.sessionProbe, + })); + const result = Bun.spawnSync([sidecarBinary!, "authcheck", JSON.stringify(flat)], { + env: { ...process.env, CGO_ENABLED: "0" }, + stdout: "pipe", + stderr: "pipe", + }); + if (result.exitCode !== 0) { + throw new Error(`ocx-sidecar authcheck failed (${result.exitCode}):\n${new TextDecoder().decode(result.stderr)}`); + } + const parsed = JSON.parse(new TextDecoder().decode(result.stdout)) as GoDecision[]; + return parsed; +} + +async function runCase(input: CaseInput): Promise { + const ts = await tsDecisions(input); + const go = goDecisions(input); + expect(go.length).toBe(ts.length); + for (let i = 0; i < ts.length; i++) { + expect(go[i], `vector ${i} divergence`).toEqual(ts[i]); + } +} + +function vector(method: string, pathOrUrl: string, headers: Record, probe = false): Vector { + const url = pathOrUrl.startsWith("http") ? pathOrUrl : `${LOOPBACK_URL}${pathOrUrl}`; + return { request: { url, method, headers }, ...(probe ? { sessionProbe: true } : {}) }; +} + +// now within the TTL window for capability vectors (minted just-in-time). +const ttl = () => Date.now() + 6_000; + +describeGo("Go management auth/session differential oracle (ticket #18)", () => { + test("admin token and 401/503 rejection bodies are identical", async () => { + await runCase({ + state: { available: true, token: ADMIN_TOKEN, source: "environment" }, + config: { hostname: "127.0.0.1" }, + local: { attestationSecret: SECRET, pid: PID, port: PORT }, + vectors: [ + // x-opencodex-api-key header admits as admin-token. + vector("GET", "/api/config", { host: `127.0.0.1:${PORT}`, "x-opencodex-api-key": ADMIN_TOKEN }), + // Authorization: Bearer (case-insensitive prefix, trimmed) admits too. + vector("GET", "/api/config", { host: `127.0.0.1:${PORT}`, authorization: `bearer ${ADMIN_TOKEN} ` }), + // Wrong token, state available: exact 401 body. + vector("GET", "/api/config", { host: `127.0.0.1:${PORT}`, "x-opencodex-api-key": "ocx_admin_wrongtokenwrongtokenwrongtokenwrongtokenwrongto" }), + // No credential at all: exact 401 body. + vector("GET", "/api/config", { host: `127.0.0.1:${PORT}` }), + // A credential that is a valid session token but no such session exists. + vector("GET", "/api/config", { host: `127.0.0.1:${PORT}`, "x-opencodex-api-key": "ocx_session_nosuchsessionsnosuchsessionsnosuchsessionsno" }), + ], + }); + + await runCase({ + state: { available: false, reason: "management token initialization failed" }, + config: { hostname: "127.0.0.1" }, + local: { attestationSecret: SECRET, pid: PID, port: PORT }, + vectors: [ + // Unavailable state: exact 503 body with reason + hint. + vector("GET", "/api/config", { host: `127.0.0.1:${PORT}` }), + vector("POST", "/api/config", { host: `127.0.0.1:${PORT}`, "x-opencodex-api-key": ADMIN_TOKEN }), + ], + }); + }); + + test("system-restart capability principal and rejection paths", async () => { + const nonce = b64url43(); + const cap = createSystemRestartCapability(SECRET, nonce, "POST", SYSTEM_RESTART_PATH, PID, PORT)!; + const headers = (over: Record = {}) => ({ + host: `127.0.0.1:${PORT}`, + "x-opencodex-restart-expected-pid": String(PID), + "x-opencodex-restart-nonce": nonce, + "x-opencodex-restart-capability": cap, + ...over, + }); + await runCase({ + state: { available: true, token: ADMIN_TOKEN, source: "environment" }, + config: { hostname: "127.0.0.1" }, + local: { attestationSecret: SECRET, pid: PID, port: PORT }, + vectors: [ + vector("POST", "/api/system/restart", headers()), // admits as the capability principal (not admin-token) + vector("POST", "/api/system/restart", headers({ "x-opencodex-restart-expected-pid": "9999" })), // wrong pid + vector("POST", "/api/system/restart", headers({ "x-opencodex-restart-capability": cap.slice(0, 42) + "A" })), // tampered + vector("GET", "/api/system/restart", headers()), // wrong method + vector("POST", "/api/config", headers()), // wrong path + ], + }); + }); + + test("local-read capability: narrow grant, replay rejected identically", async () => { + const nonce = b64url43(); + const expires = ttl(); + const cap = createLocalManagementReadCapability(SECRET, nonce, "GET", LOCAL_MANAGEMENT_READ_PATHS.codexAccounts, PID, PORT, expires)!; + const base = { + host: `127.0.0.1:${PORT}`, + "x-opencodex-local-expected-pid": String(PID), + "x-opencodex-local-nonce": nonce, + "x-opencodex-local-expires-at": String(expires), + "x-opencodex-local-capability": cap, + }; + await runCase({ + state: { available: true, token: ADMIN_TOKEN, source: "environment" }, + config: { hostname: "127.0.0.1" }, + local: { attestationSecret: SECRET, pid: PID, port: PORT }, + vectors: [ + vector("GET", "/api/codex-auth/accounts", base), // admits as local-read-capability + vector("GET", "/api/codex-auth/accounts", base), // replay of the same capability is rejected + vector("GET", "/api/codex-auth/accounts?x=1", base), // query string disqualifies the narrow grant + vector("GET", "/api/system/health", base), // path not in the allowlist + vector("PUT", "/api/codex-auth/accounts", base), // method not GET + vector("GET", "/api/codex-auth/accounts", { ...base, "x-opencodex-local-expires-at": String(Date.now() - 1) }), // expired + vector("GET", "/api/codex-auth/accounts", { ...base, "x-opencodex-local-capability": "not-a-capability-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }), + ], + }); + }); + + test("provider-reload capability binds name/pid/expiry and empty-body shape", async () => { + const nonce = b64url43(); + const expires = ttl(); + const cap = createLocalProviderReloadCapability(SECRET, nonce, "POST", LOCAL_PROVIDER_RELOAD_PATH, "openai", PID, PORT, expires)!; + const base = { + host: `127.0.0.1:${PORT}`, + "content-length": "0", + "x-opencodex-provider-reload-expected-pid": String(PID), + "x-opencodex-provider-reload-nonce": nonce, + "x-opencodex-provider-reload-expires-at": String(expires), + "x-opencodex-provider-reload-name": "openai", + "x-opencodex-provider-reload-capability": cap, + }; + await runCase({ + state: { available: true, token: ADMIN_TOKEN, source: "environment" }, + config: { hostname: "127.0.0.1" }, + local: { attestationSecret: SECRET, pid: PID, port: PORT }, + vectors: [ + vector("POST", "/api/providers/reload", base), // admits as local-provider-reload-capability + vector("POST", "/api/providers/reload", base), // replay rejected + vector("POST", "/api/providers/reload", { ...base, "content-length": "5" }), // non-empty body shape + vector("POST", "/api/providers/reload", { ...base, "x-opencodex-provider-reload-name": "other provider!" }), // invalid name shape + vector("POST", "/api/providers/reload", { ...base, "transfer-encoding": "chunked" }), // body via chunked + vector("POST", "/api/providers/reload?x=1", base), // query disqualifies + ], + }); + }); + + test("gui-pair capability binds canonical browser origin", async () => { + const nonce = b64url43(); + const expires = ttl(); + const browserOrigin = "http://localhost:5173"; + const cap = createGuiPairCapability(SECRET, nonce, "POST", GUI_PAIR_PATH, browserOrigin, PID, PORT, expires)!; + const base = { + host: `127.0.0.1:${PORT}`, + "content-length": "0", + "x-opencodex-gui-pair-expected-pid": String(PID), + "x-opencodex-gui-pair-nonce": nonce, + "x-opencodex-gui-pair-expires-at": String(expires), + "x-opencodex-gui-pair-origin": browserOrigin, + "x-opencodex-gui-pair-capability": cap, + }; + await runCase({ + state: { available: true, token: ADMIN_TOKEN, source: "environment" }, + config: { hostname: "127.0.0.1" }, + local: { attestationSecret: SECRET, pid: PID, port: PORT }, + vectors: [ + vector("POST", "/api/gui/pairing-grants", base), // admits as gui-pair-capability + vector("POST", "/api/gui/pairing-grants", base), // replay rejected (sha256 digest key) + vector("POST", "/api/gui/pairing-grants", { ...base, "x-opencodex-gui-pair-origin": "HTTP://LOCALHOST:5173" }), // non-canonical origin spelling never verifies + vector("POST", "/api/gui/pairing-grants", { ...base, "x-opencodex-gui-pair-expires-at": "0" }), // malformed expiry + ], + }); + }); + + test("dashboard sessions: origin/CSRF/expiry outcomes match", async () => { + const serverOrigin = `http://127.0.0.1:${PORT}`; + const session = { + token: `ocx_session_${b64url43()}`, + serverOrigin, + browserOrigin: serverOrigin, + csrf: b64url43(), + expiresAt: Date.now() + 5 * 60_000, + issuance: "loopback", + }; + const stale = { + token: `ocx_session_${b64url43()}`, + serverOrigin, + browserOrigin: serverOrigin, + csrf: b64url43(), + expiresAt: Date.now() - 1, + issuance: "loopback", + }; + await runCase({ + state: { available: true, token: ADMIN_TOKEN, source: "environment", sessions: [session, stale] }, + config: { hostname: "127.0.0.1" }, + local: { attestationSecret: SECRET, pid: PID, port: PORT }, + vectors: [ + // Safe GET with session token and matching gui-origin admits. + vector("GET", "/api/config", { + host: `127.0.0.1:${PORT}`, + "x-opencodex-api-key": session.token, + "x-opencodex-gui-origin": serverOrigin, + }, true), + // Unsafe POST needs Origin + CSRF. + vector("POST", "/api/config", { + host: `127.0.0.1:${PORT}`, + "x-opencodex-api-key": session.token, + origin: serverOrigin, + "x-opencodex-gui-origin": serverOrigin, + "x-opencodex-csrf-token": session.csrf, + }, true), + // Missing CSRF on a mutation rejects at the gate and with the csrf reason. + vector("POST", "/api/config", { + host: `127.0.0.1:${PORT}`, + "x-opencodex-api-key": session.token, + origin: serverOrigin, + "x-opencodex-gui-origin": serverOrigin, + }, true), + // Browser-origin mismatch (claimed gui-origin) rejects. + vector("GET", "/api/config", { + host: `127.0.0.1:${PORT}`, + "x-opencodex-api-key": session.token, + "x-opencodex-gui-origin": "http://evil.example", + }, true), + // Server-origin mismatch: Host derives a different origin. + vector("GET", "/api/config", { + host: `127.0.0.1:9999`, + "x-opencodex-api-key": session.token, + "x-opencodex-gui-origin": serverOrigin, + }, true), + // Expired session: gate rejects, probe reports expired, entry deleted. + vector("GET", "/api/config", { + host: `127.0.0.1:${PORT}`, + "x-opencodex-api-key": stale.token, + }, true), + // Unknown session token behaves like no credential at the gate. + vector("GET", "/api/config", { + host: `127.0.0.1:${PORT}`, + "x-opencodex-api-key": `ocx_session_${b64url43()}`, + }, true), + ], + }); + }); + + test("remote and hub server-origin derivation matches", async () => { + const remote = `http://mynode.lan:${PORT}`; + const session = { + token: `ocx_session_${b64url43()}`, + serverOrigin: remote, + browserOrigin: remote, + csrf: b64url43(), + expiresAt: Date.now() + 5 * 60_000, + issuance: "remote", + }; + await runCase({ + state: { available: true, token: ADMIN_TOKEN, source: "environment", sessions: [session] }, + config: { hostname: "0.0.0.0" }, // auth required for non-loopback + local: { attestationSecret: SECRET, pid: PID, port: PORT }, + vectors: [ + // Non-loopback observed origin admits when api auth is required. + vector("GET", "/api/config", { + host: `mynode.lan:${PORT}`, + "x-opencodex-api-key": session.token, + "x-opencodex-gui-origin": remote, + }, true), + // Host mismatch rejects under the derived-origin rule. + vector("GET", "/api/config", { + host: `other.lan:${PORT}`, + "x-opencodex-api-key": session.token, + "x-opencodex-gui-origin": remote, + }, true), + ], + }); + + const hub = { + token: `ocx_session_${b64url43()}`, + serverOrigin: "https://ocx.example", + browserOrigin: "https://ocx.example", + csrf: b64url43(), + expiresAt: Date.now() + 5 * 60_000, + issuance: "remote", + }; + await runCase({ + state: { available: true, token: ADMIN_TOKEN, source: "environment", sessions: [hub] }, + config: { hostname: "0.0.0.0", runtimeRole: "hub", hubManagementPublicOrigin: "https://ocx.example" }, + local: { attestationSecret: SECRET, pid: PID, port: PORT }, + vectors: [ + // A hub request derives the configured public origin, matching the session. + vector("GET", "/api/config", { + host: `10.0.0.5:${PORT}`, + "x-opencodex-api-key": hub.token, + "x-opencodex-gui-origin": "https://ocx.example", + }, true), + ], + }); + }); +}); diff --git a/tests/go-lab-gate-parity.test.ts b/tests/go-lab-gate-parity.test.ts new file mode 100644 index 0000000000..9255ba7007 --- /dev/null +++ b/tests/go-lab-gate-parity.test.ts @@ -0,0 +1,185 @@ +import { describe, expect, test } from "bun:test"; +import { mkdtempSync, mkdirSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { labActivationRequired, labAutomationEnabledOnDisk } from "../src/lib/lab-activation"; +import { loadConfig } from "../src/config"; + +/** + * Differential oracle for the Go Compatibility Lab activation gate (ADR-0008, + * ticket #19). + * + * The Go gate (go/internal/labactivation, exercised through the `ocx-sidecar + * labcheck` subcommand) must answer identically to + * src/lib/lab-activation.ts for the same on-disk state: a routing profile in + * config.json OR Lab automation enabled under /lab/. Each fixture + * directory is evaluated by Go first (the TS config loader can repair/rewrite + * a config file in place, so Go must read the pristine fixture) and then by + * the TypeScript functions, and the three outputs — automationEnabled, + * profilesNonEmpty, required — must match. + */ +const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); + +function goToolchainAvailable(): boolean { + return Bun.spawnSync(["go", "version"], { stdout: "ignore", stderr: "ignore" }).success; +} + +function buildSidecarBinary(): string { + const dir = mkdtempSync(join(tmpdir(), "ocx-go-labgate-")); + const binPath = join(dir, process.platform === "win32" ? "ocx-sidecar.exe" : "ocx-sidecar"); + const build = Bun.spawnSync(["go", "build", "-o", binPath, "./cmd/ocx-sidecar"], { + cwd: join(repoRoot, "go"), + env: { ...process.env, CGO_ENABLED: "0" }, + stdout: "pipe", + stderr: "pipe", + }); + if (build.exitCode !== 0) { + throw new Error(`go build ./cmd/ocx-sidecar failed (${build.exitCode}):\n${new TextDecoder().decode(build.stderr)}`); + } + return binPath; +} + +const goAvailable = goToolchainAvailable(); +const sidecarBinary: string | null = goAvailable ? buildSidecarBinary() : null; +const describeGo = goAvailable ? describe : describe.skip; + +interface GateOutput { + automationEnabled: boolean; + profilesNonEmpty: boolean; + required: boolean; +} + +function goGate(configDir: string): GateOutput { + const result = Bun.spawnSync([sidecarBinary!, "labcheck", configDir], { + env: { ...process.env, CGO_ENABLED: "0" }, + stdout: "pipe", + stderr: "pipe", + }); + if (result.exitCode !== 0) { + throw new Error(`ocx-sidecar labcheck failed (${result.exitCode}):\n${new TextDecoder().decode(result.stderr)}`); + } + return JSON.parse(new TextDecoder().decode(result.stdout)) as GateOutput; +} + +function tsGate(configDir: string): GateOutput { + const previous = process.env.OPENCODEX_HOME; + process.env.OPENCODEX_HOME = configDir; + try { + const config = loadConfig(); + const profiles = Object.keys(config.routingProfiles ?? {}).length > 0; + const automation = labAutomationEnabledOnDisk(configDir); + return { + automationEnabled: automation, + profilesNonEmpty: profiles, + required: labActivationRequired(config, configDir), + }; + } finally { + if (previous === undefined) delete process.env.OPENCODEX_HOME; + else process.env.OPENCODEX_HOME = previous; + } +} + +interface Fixture { + name: string; + configJSON?: string; + automation?: { file: "automation-config.json" | "automation-policy.json"; content: string }[]; + // Expected gate decision (cross-checked against both sides). + want: GateOutput; +} + +const PROFILE_CONFIG = JSON.stringify({ + routingProfiles: { demo: { candidates: [{ provider: "openai", model: "gpt-5.5" }] } }, +}); + +function buildFixtures(): { dir: string; fixture: Fixture }[] { + const root = mkdtempSync(join(tmpdir(), "ocx-labgate-")); + const fixtures: Fixture[] = [ + { name: "empty-dir", want: { automationEnabled: false, profilesNonEmpty: false, required: false } }, + { + name: "automation-combined-on", + automation: [{ file: "automation-config.json", content: '{"policy": {"enabled": true}}' }], + want: { automationEnabled: true, profilesNonEmpty: false, required: true }, + }, + { + name: "automation-legacy-on", + automation: [{ file: "automation-policy.json", content: '{"enabled": true}' }], + want: { automationEnabled: true, profilesNonEmpty: false, required: true }, + }, + { + name: "automation-legacy-off", + automation: [{ file: "automation-policy.json", content: '{"enabled": false}' }], + want: { automationEnabled: false, profilesNonEmpty: false, required: false }, + }, + { + name: "combined-authority-wins", + automation: [ + { file: "automation-config.json", content: '{"policy": {"enabled": false}}' }, + { file: "automation-policy.json", content: '{"enabled": true}' }, + ], + want: { automationEnabled: false, profilesNonEmpty: false, required: false }, + }, + { + name: "combined-without-policy-falls-back", + automation: [ + { file: "automation-config.json", content: '{"scheduler": {}}' }, + { file: "automation-policy.json", content: '{"enabled": true}' }, + ], + want: { automationEnabled: true, profilesNonEmpty: false, required: true }, + }, + { + name: "profile-only", + configJSON: PROFILE_CONFIG, + want: { automationEnabled: false, profilesNonEmpty: true, required: true }, + }, + { + name: "profile-plus-automation-off", + configJSON: PROFILE_CONFIG, + automation: [{ file: "automation-config.json", content: '{"policy": {"enabled": false}}' }], + want: { automationEnabled: false, profilesNonEmpty: true, required: true }, + }, + { + name: "profile-plus-automation-on", + configJSON: PROFILE_CONFIG, + automation: [{ file: "automation-config.json", content: '{"policy": {"enabled": true}}' }], + want: { automationEnabled: true, profilesNonEmpty: true, required: true }, + }, + { + name: "malformed-automation", + automation: [ + { file: "automation-config.json", content: "{not json" }, + { file: "automation-policy.json", content: '{"enabled": "yes"}' }, + ], + want: { automationEnabled: false, profilesNonEmpty: false, required: false }, + }, + ]; + return fixtures.map((fixture) => { + const dir = join(root, fixture.name); + mkdirSync(dir, { recursive: true }); + if (fixture.configJSON !== undefined) { + writeFileSync(join(dir, "config.json"), fixture.configJSON); + } + for (const file of fixture.automation ?? []) { + const labDir = join(dir, "lab"); + mkdirSync(labDir, { recursive: true }); + writeFileSync(join(labDir, file.file), file.content); + } + return { dir, fixture }; + }); +} + +describeGo("Go Lab activation gate differential oracle (ticket #19)", () => { + const cases = buildFixtures(); + for (const { dir, fixture } of cases) { + test(`gate decision matches TypeScript for "${fixture.name}"`, () => { + // Go reads the pristine fixture first; the TS config loader may repair a + // config.json in place (adding defaulted providers), which would + // otherwise change what Go sees. + const go = goGate(dir); + expect(go).toEqual(fixture.want); + const ts = tsGate(dir); + expect(ts).toEqual(fixture.want); + expect(ts).toEqual(go); + }); + } +}); From d27829ea5e35228f60e667218ed5e83256970381 Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Sun, 6 Sep 2026 10:44:17 +0800 Subject: [PATCH 012/165] feat(go): serve provider quota reads through sidecar --- go/cmd/ocx-sidecar/main.go | 9 ++-- go/internal/sidecar/sidecar.go | 65 +++++++++++++++++++++++++ go/internal/sidecar/sidecar_test.go | 51 +++++++++++++++++++ src/server/go-sidecar.ts | 35 +++++++++++-- src/server/index.ts | 33 ++++++++++++- src/server/management/route-registry.ts | 5 +- tests/cursor-integration-status.test.ts | 7 +++ tests/go-ownership-plumbing.test.ts | 10 +++- tests/go-sidecar-parity.test.ts | 57 ++++++++++++++++++++++ 9 files changed, 261 insertions(+), 11 deletions(-) diff --git a/go/cmd/ocx-sidecar/main.go b/go/cmd/ocx-sidecar/main.go index fad0ee37a8..33e9fc9a37 100644 --- a/go/cmd/ocx-sidecar/main.go +++ b/go/cmd/ocx-sidecar/main.go @@ -59,9 +59,12 @@ func serve() error { addr := listener.Addr().String() cfg := sidecar.Config{ - Service: "opencodex", - Version: os.Getenv("OCX_SIDECAR_VERSION"), - StartedAt: time.Now(), + Service: "opencodex", + Version: os.Getenv("OCX_SIDECAR_VERSION"), + StartedAt: time.Now(), + ParentURL: os.Getenv("OCX_SIDECAR_PARENT_URL"), + BridgeToken: os.Getenv("OCX_SIDECAR_BRIDGE_TOKEN"), + RequestToken: os.Getenv("OCX_SIDECAR_REQUEST_TOKEN"), } if cfg.Version == "" { fmt.Fprintln(os.Stderr, "ocx-sidecar: warning: OCX_SIDECAR_VERSION is unset; reporting version 0.0.0") diff --git a/go/internal/sidecar/sidecar.go b/go/internal/sidecar/sidecar.go index 5833f469ca..ccab4a091c 100644 --- a/go/internal/sidecar/sidecar.go +++ b/go/internal/sidecar/sidecar.go @@ -12,7 +12,10 @@ package sidecar import ( "encoding/json" "fmt" + "io" + "net" "net/http" + "net/url" "os" "time" @@ -43,6 +46,12 @@ type Config struct { // resolution as src/config/paths.ts in the parent. Set explicitly only by // unit tests; the supervisor inherits OPENCODEX_HOME at spawn time. ConfigDir string + // ParentURL and BridgeToken identify the private parent endpoint that owns + // live quota state until the Go runtime flip. RequestToken is required on + // every forwarded sidecar request, so direct loopback access cannot refresh. + ParentURL string + BridgeToken string + RequestToken string } // healthPayload mirrors the JSON object literal in @@ -143,6 +152,62 @@ func NewHandler(cfg Config) http.Handler { } writeRawJSON(w, raw, "custom-models") }) + + // Ticket #20: provider quota aggregation remains process state until the + // runtime flip. Go owns this public HTTP route and obtains the existing + // cache/probe result from a capability-scoped parent loopback bridge, so + // refresh, invalidation, passive observations and last-good retention keep + // their established semantics while the wire response stays byte-identical. + mux.HandleFunc("GET /api/provider-quotas", func(w http.ResponseWriter, r *http.Request) { + if cfg.RequestToken == "" || r.Header.Get("X-Ocx-Go-Sidecar-Request") != cfg.RequestToken { + http.NotFound(w, r) + return + } + parent, err := url.Parse(cfg.ParentURL) + if err != nil || parent.Scheme != "http" || parent.Hostname() != "127.0.0.1" || cfg.BridgeToken == "" { + http.Error(w, "quota state bridge unavailable", http.StatusServiceUnavailable) + return + } + parent.Path = "/__ocx_go_sidecar/provider-quotas" + parent.RawQuery = r.URL.RawQuery + bridgeReq, err := http.NewRequestWithContext(r.Context(), http.MethodGet, parent.String(), nil) + if err != nil { + http.Error(w, "quota state bridge unavailable", http.StatusServiceUnavailable) + return + } + bridgeReq.Header.Set("X-Ocx-Go-Sidecar-Bridge", cfg.BridgeToken) + bridgeTransport := &http.Transport{ + Proxy: nil, + DialContext: (&net.Dialer{}).DialContext, + } + defer bridgeTransport.CloseIdleConnections() + bridgeClient := &http.Client{ + Timeout: 30 * time.Second, + // The bridge credential must never be sent through HTTP_PROXY or a + // system proxy. The parent is a literal IPv4 loopback listener. + Transport: bridgeTransport, + } + bridgeResp, err := bridgeClient.Do(bridgeReq) + if err != nil { + http.Error(w, "quota state bridge unavailable", http.StatusServiceUnavailable) + return + } + defer bridgeResp.Body.Close() + raw, err := io.ReadAll(io.LimitReader(bridgeResp.Body, 8*1024*1024+1)) + if err != nil || len(raw) > 8*1024*1024 { + http.Error(w, "quota state bridge unavailable", http.StatusServiceUnavailable) + return + } + contentType := bridgeResp.Header.Get("Content-Type") + if contentType == "" { + contentType = "application/json" + } + w.Header().Set("Content-Type", contentType) + w.WriteHeader(bridgeResp.StatusCode) + if _, err := w.Write(raw); err != nil { + fmt.Fprintf(os.Stderr, "ocx-sidecar: write provider-quotas payload: %v\n", err) + } + }) return mux } diff --git a/go/internal/sidecar/sidecar_test.go b/go/internal/sidecar/sidecar_test.go index dfb62c0ff2..eaa89e2875 100644 --- a/go/internal/sidecar/sidecar_test.go +++ b/go/internal/sidecar/sidecar_test.go @@ -154,6 +154,57 @@ func TestHealthRouteSurfaceIsNarrow(t *testing.T) { } } +func TestProviderQuotasRelaysTheParentStateBridgeVerbatim(t *testing.T) { + const requestToken = "parent-to-sidecar" + const bridgeToken = "sidecar-to-parent" + const want = `{"generatedAt":123,"reports":[{"provider":"test"}]}` + bridge := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet || r.URL.Path != "/__ocx_go_sidecar/provider-quotas" { + t.Errorf("bridge request = %s %s", r.Method, r.URL.String()) + w.WriteHeader(http.StatusNotFound) + return + } + if r.URL.RawQuery != "refresh=1" { + t.Errorf("bridge query = %q, want refresh=1", r.URL.RawQuery) + } + if r.Header.Get("X-Ocx-Go-Sidecar-Bridge") != bridgeToken { + t.Errorf("bridge capability was not forwarded") + w.WriteHeader(http.StatusForbidden) + return + } + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(want)) + })) + defer bridge.Close() + + h := NewHandler(Config{ParentURL: bridge.URL, BridgeToken: bridgeToken, RequestToken: requestToken}) + denied := do(t, h, http.MethodGet, "/api/provider-quotas") + denied.Body.Close() + if denied.StatusCode != http.StatusNotFound { + t.Fatalf("unauthenticated quota request status = %d, want 404", denied.StatusCode) + } + + req := httptest.NewRequest(http.MethodGet, "/api/provider-quotas?refresh=1", nil) + req.Header.Set("X-Ocx-Go-Sidecar-Request", requestToken) + rec := httptest.NewRecorder() + h.ServeHTTP(rec, req) + resp := rec.Result() + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + t.Fatalf("status = %d, want 200", resp.StatusCode) + } + if got := resp.Header.Get("Content-Type"); got != "application/json" { + t.Fatalf("Content-Type = %q, want application/json", got) + } + raw, err := io.ReadAll(resp.Body) + if err != nil { + t.Fatal(err) + } + if string(raw) != want { + t.Fatalf("body = %s, want %s", raw, want) + } +} + func TestReadyLineConstant(t *testing.T) { if ReadyLinePrefix != "ocx-sidecar-ready" { t.Fatalf("ReadyLinePrefix = %q changed; the TS supervisor parses this exact token", ReadyLinePrefix) diff --git a/src/server/go-sidecar.ts b/src/server/go-sidecar.ts index 10af379a51..0d68c05b6e 100644 --- a/src/server/go-sidecar.ts +++ b/src/server/go-sidecar.ts @@ -35,12 +35,24 @@ export const GO_SIDECAR_BIN_ENV = "OPENCODEX_GO_SIDECAR_BIN"; /** Environment variable the parent uses to pass the installed package version. */ export const GO_SIDECAR_VERSION_ENV = "OCX_SIDECAR_VERSION"; +/** Parent loopback endpoint used only by the sidecar live-state bridge. */ +export const GO_SIDECAR_PARENT_URL_ENV = "OCX_SIDECAR_PARENT_URL"; + +/** Capability presented by the child when reading parent-owned live state. */ +export const GO_SIDECAR_BRIDGE_TOKEN_ENV = "OCX_SIDECAR_BRIDGE_TOKEN"; + +/** Capability presented by the parent when forwarding to the sidecar. */ +export const GO_SIDECAR_REQUEST_TOKEN_ENV = "OCX_SIDECAR_REQUEST_TOKEN"; + /** Readiness marker the Go binary prints on stdout after binding. */ export const GO_SIDECAR_READY_PREFIX = "ocx-sidecar-ready"; /** How long the front door waits for the child's ready line before giving up. */ export const GO_SIDECAR_READY_TIMEOUT_MS = 10_000; +/** Quota aggregation may await provider probes; keep one refresh attempt alive. */ +export const GO_SIDECAR_QUOTA_ROUTE_TIMEOUT_MS = 30_000; + type KillableChild = { exited: Promise; stdout: ReadableStream | null; @@ -53,6 +65,7 @@ let stopped = true; let readyBaseUrl = ""; let generation = 0; let forwardDetach: (() => void) | null = null; +let bridgeStopped: (() => void) | null = null; /** Test-only reset so an isolated harness does not inherit a live child. */ export function resetGoSidecarForTests(): void { @@ -143,12 +156,12 @@ function warnActivation(message: string): void { } /** Forward one declared Go-owned route request to a ready sidecar, or null on any failure. */ -async function forwardTo(baseUrl: string, method: string, pathAndSearch: string): Promise { +async function forwardTo(baseUrl: string, requestToken: string, method: string, pathAndSearch: string): Promise { try { const upstream = await directLocalHttpFetch(new URL(pathAndSearch, baseUrl), { method, - headers: { accept: "application/json" }, - }); + headers: { accept: "application/json", "x-ocx-go-sidecar-request": requestToken }, + }, pathAndSearch.startsWith("/api/provider-quotas") ? { timeoutMs: GO_SIDECAR_QUOTA_ROUTE_TIMEOUT_MS } : undefined); if (!upstream.ok) return null; // Relay the sidecar's response with the in-process handler's header shape: // Content-Type plus the body verbatim. The management-API CORS wrapper adds @@ -165,6 +178,9 @@ async function forwardTo(baseUrl: string, method: string, pathAndSearch: string) function stopSidecar(): void { if (stopped) return; stopped = true; + const notifyBridgeStopped = bridgeStopped; + bridgeStopped = null; + notifyBridgeStopped?.(); if (forwardDetach) { forwardDetach(); forwardDetach = null; @@ -195,7 +211,10 @@ function stopSidecar(): void { * — and after any unexpected exit — the slot is empty and the in-process * handler answers, byte-identically to a build without Go. */ -export function activateGoSidecar(version: string): { stop(): void } | null { +export function activateGoSidecar( + version: string, + liveStateBridge: { parentUrl: string; bridgeToken: string; requestToken: string; onStopped(): void }, +): { stop(): void } | null { const binary = process.env[GO_SIDECAR_BIN_ENV]?.trim(); if (!binary) return null; if (!existsSync(binary)) { @@ -213,6 +232,9 @@ export function activateGoSidecar(version: string): { stop(): void } | null { env: { ...process.env, [GO_SIDECAR_VERSION_ENV]: version, + [GO_SIDECAR_PARENT_URL_ENV]: liveStateBridge.parentUrl, + [GO_SIDECAR_BRIDGE_TOKEN_ENV]: liveStateBridge.bridgeToken, + [GO_SIDECAR_REQUEST_TOKEN_ENV]: liveStateBridge.requestToken, }, }); } catch (error) { @@ -225,6 +247,7 @@ export function activateGoSidecar(version: string): { stop(): void } | null { stopped = false; childProc = proc; readyBaseUrl = ""; + bridgeStopped = liveStateBridge.onStopped; // The optional-subsystem shutdown hook is keyed, so a re-activation replaces // the previous registration instead of accumulating duplicate teardown. registerOptionalShutdownHook("go-sidecar", stopSidecar); @@ -251,7 +274,9 @@ export function activateGoSidecar(version: string): { stop(): void } | null { if (stopped || myGeneration !== generation) return; readyBaseUrl = parsed; const baseUrl = parsed; - const detach = setGoOwnedRouteForwarder((method, pathAndSearch) => forwardTo(baseUrl, method, pathAndSearch)); + const detach = setGoOwnedRouteForwarder((method, pathAndSearch) => ( + forwardTo(baseUrl, liveStateBridge.requestToken, method, pathAndSearch) + )); if (stopped || myGeneration !== generation) { detach(); return; diff --git a/src/server/index.ts b/src/server/index.ts index 1a70abf3a6..38babb6aeb 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -1,4 +1,5 @@ import { markActivity } from "../lib/sidecar-tracker"; +import { randomUUID } from "node:crypto"; import { knownModelIdsForProvider } from "../router"; import { buildWarmupCompletionFrames, @@ -34,6 +35,7 @@ import { type OwnershipInspection, } from "../integrations/native/ownership-preflight"; import { createResetCreditWhamClient, registerCodexCooldownRecoveryProbeWorker } from "../codex/auth-api"; +import { fetchProviderQuotaReports } from "../providers/quota"; import { activateResetCreditAutoRedeem } from "../codex/reset-credit-auto-redeem"; import { reconcileLiveStateStores, @@ -1018,6 +1020,7 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server | null = null; + let goSidecarLiveStateBridgeToken: string | null = null; // Set only when the optional Go sidecar activated (ADR-0008); consumed by the server.stop // override below, which is built before activation runs. Null default keeps a process that // never opted in from carrying any Go-sidecar state. @@ -1060,6 +1063,22 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server { + goSidecarLiveStateBridgeToken = randomUUID(); + return activateGoSidecar(VERSION, { + parentUrl: "http://127.0.0.1:" + actualPort, + bridgeToken: goSidecarLiveStateBridgeToken, + requestToken: randomUUID(), + onStopped: () => { goSidecarLiveStateBridgeToken = null; }, + }); + })() + : null; if (goSidecarHandle) { goSidecarStop = goSidecarHandle.stop; + } else { + goSidecarLiveStateBridgeToken = null; } return server; diff --git a/src/server/management/route-registry.ts b/src/server/management/route-registry.ts index d2e5b802ba..1105b858f2 100644 --- a/src/server/management/route-registry.ts +++ b/src/server/management/route-registry.ts @@ -344,7 +344,10 @@ export const MANAGEMENT_ROUTES: readonly ManagementRoute[] = [ { method: "DELETE", path: "/api/providers", module: "server/management/provider-routes", mutates: true }, { method: "GET", path: "/api/provider-context-caps", module: "server/management/provider-routes", mutates: false }, { method: "GET", path: "/api/provider-presets", module: "server/management/provider-routes", mutates: false }, - { method: "GET", path: "/api/provider-quotas", module: "server/management/provider-routes", mutates: false }, + // Ticket #20: Go owns the public HTTP route. Its handler uses a private, + // capability-protected bridge for the pre-flip quota cache, whose refresh + // semantics still belong to the serving TypeScript process. + { method: "GET", path: "/api/provider-quotas", module: "server/management/provider-routes", mutates: false, go: { volatileFields: ["generatedAt"] } }, { method: "GET", path: "/api/provider-request-pacing", module: "server/management/provider-routes", mutates: false }, { method: "GET", path: "/api/providers", module: "server/management/provider-routes", mutates: false }, { method: "PATCH", path: "/api/providers", module: "server/management/provider-routes", mutates: true }, diff --git a/tests/cursor-integration-status.test.ts b/tests/cursor-integration-status.test.ts index 35348dc933..d87689529f 100644 --- a/tests/cursor-integration-status.test.ts +++ b/tests/cursor-integration-status.test.ts @@ -93,6 +93,7 @@ describe("cursorEffortFamily", () => { }); const previousHome = process.env.OPENCODEX_HOME; +const previousDataToken = process.env.OPENCODEX_API_AUTH_TOKEN; let testHome = ""; const CURSOR_EFFORT_FIXTURE = readFileSync(join(import.meta.dir, "fixtures/cursor-agent-exec-effort-table.min.js"), "utf8"); const STATIC_CURSOR_EFFORT_DEPS = { managementApi: { loadCursorEffortTable: () => null } }; @@ -128,6 +129,10 @@ describe("GET /api/native-integrations/cursor", () => { beforeEach(() => { testHome = mkdtempSync(join(tmpdir(), "ocx-cursor-status-")); process.env.OPENCODEX_HOME = testHome; + // The placeholder-mode fixture is about an install with no data-plane + // credential. Keep that contract independent of a developer or CI shell + // that exports an API token for unrelated tests. + delete process.env.OPENCODEX_API_AUTH_TOKEN; resetCursorSeenForTests(); }); @@ -136,6 +141,8 @@ describe("GET /api/native-integrations/cursor", () => { resetCursorSeenForTests(); if (previousHome === undefined) delete process.env.OPENCODEX_HOME; else process.env.OPENCODEX_HOME = previousHome; + if (previousDataToken === undefined) delete process.env.OPENCODEX_API_AUTH_TOKEN; + else process.env.OPENCODEX_API_AUTH_TOKEN = previousDataToken; if (testHome) removeTreeWithRetry(testHome); testHome = ""; }); diff --git a/tests/go-ownership-plumbing.test.ts b/tests/go-ownership-plumbing.test.ts index 3bb1535443..a725f93cea 100644 --- a/tests/go-ownership-plumbing.test.ts +++ b/tests/go-ownership-plumbing.test.ts @@ -125,7 +125,7 @@ async function getJson(token: string, server: { url: URL }, pathname: string): P // --------------------------------------------------------------------------- describe("ADR-0008 ownership markers are typed read/write (ticket #14)", () => { - test("the declared Go-owned surface is health (volatile), shadow-call-settings (strict) and custom-models (strict) today", () => { + test("the declared Go-owned surface includes the ticket #20 quota route", () => { // Pin the migrated set so an accidental marker flip on another read route // fails here instead of silently changing what the proxy serves. Adding a // real migration updates this list deliberately. Health reports the serving @@ -136,6 +136,7 @@ describe("ADR-0008 ownership markers are typed read/write (ticket #14)", () => { const byPath = new Map(GO_OWNED_MANAGEMENT_ROUTES.map(r => [r.path, r])); expect([...byPath.keys()].sort()).toEqual([ "/api/custom-models", + "/api/provider-quotas", "/api/shadow-call-settings", "/api/system/health", ]); @@ -154,6 +155,11 @@ describe("ADR-0008 ownership markers are typed read/write (ticket #14)", () => { expect(customModels.mutates).toBe(false); expect(customModels.module).toBe("server/management/model-routes"); expect(customModels.go.volatileFields).toEqual([]); + const providerQuotas = byPath.get("/api/provider-quotas")!; + expect(providerQuotas.method).toBe("GET"); + expect(providerQuotas.mutates).toBe(false); + expect(providerQuotas.module).toBe("server/management/provider-routes"); + expect(providerQuotas.go.volatileFields).toEqual(["generatedAt"]); }); test("no write route can be Go-owned: runtime re-check of the union's read-only arm", () => { @@ -203,6 +209,8 @@ describe("ADR-0008 ownership markers are typed read/write (ticket #14)", () => { expect(findGoOwnedManagementRoute("GET", "/api/custom-models")).toBe(customModels); expect(findGoOwnedManagementRoute("POST", "/api/custom-models")).toBeUndefined(); expect(findGoOwnedManagementRoute("GET", "/api/custom-models/")).toBeUndefined(); + expect(findGoOwnedManagementRoute("GET", "/api/provider-quotas")).toBeDefined(); + expect(findGoOwnedManagementRoute("POST", "/api/provider-quotas")).toBeUndefined(); }); test("the forwarding branch in management-api.ts names no route of its own", () => { diff --git a/tests/go-sidecar-parity.test.ts b/tests/go-sidecar-parity.test.ts index 9c8449b816..45e1fb8734 100644 --- a/tests/go-sidecar-parity.test.ts +++ b/tests/go-sidecar-parity.test.ts @@ -134,6 +134,10 @@ async function captureCustomModels(server: { url: URL }, token: string) { return captureJson(server, token, "/api/custom-models"); } +async function captureProviderQuotas(server: { url: URL }, token: string, suffix = "") { + return captureJson(server, token, "/api/provider-quotas" + suffix); +} + async function captureHealth(server: { url: URL }, token: string): Promise { const response = await fetch(new URL("/api/system/health", server.url), { headers: { "x-opencodex-api-key": token }, @@ -235,6 +239,11 @@ describe.skipIf(!goAvailable || sidecarBinary === null)("ocx-sidecar differentia ); expect(customModels).toBeDefined(); expect(customModels!.go.volatileFields).toEqual([]); + const providerQuotas = GO_OWNED_MANAGEMENT_ROUTES.find( + route => route.method === "GET" && route.path === "/api/provider-quotas", + ); + expect(providerQuotas).toBeDefined(); + expect(providerQuotas!.go.volatileFields).toEqual(["generatedAt"]); }); runFixtureTest("in-process handler and Go sidecar agree on status, headers, and normalised body", async (token) => { @@ -449,6 +458,54 @@ describe.skipIf(!goAvailable || sidecarBinary === null)("ocx-sidecar differentia } }); + runFixtureTest("provider-quotas is Go-owned and preserves cached and forced-refresh bytes", async (token) => { + // Ticket #20: the fixture's only provider is disabled, so the aggregation + // never calls an upstream. It still exercises both cache modes and proves + // the Go public handler relays the TypeScript-owned live-state bridge + // byte-for-byte. generatedAt is volatile only for a forced refresh. + const serverA = startServer(0); + try { + const cachedTs = await captureProviderQuotas(serverA, token); + expect(cachedTs.status).toBe(200); + expect(cachedTs.contentType).toBe("application/json"); + expect(cachedTs.body).toContain("\"reports\":[]"); + + process.env[GO_SIDECAR_BIN_ENV] = sidecarBinary!; + const serverB = startServer(0); + try { + const sidecarUrl = await waitFor(() => activeGoSidecarBaseUrl(), 15_000); + const cachedGo = await captureProviderQuotas(serverB, token); + expect(cachedGo.status).toBe(200); + expect(cachedGo.contentType).toBe("application/json"); + expect(cachedGo.body).toBe(cachedTs.body); + + const refreshedTs = await captureProviderQuotas(serverA, token, "?refresh=1"); + const refreshedGo = await captureProviderQuotas(serverB, token, "?refresh=1"); + expect(refreshedGo.status).toBe(200); + expect(normaliseBody(refreshedGo.body, ["generatedAt"])).toBe( + normaliseBody(refreshedTs.body, ["generatedAt"]), + ); + + // The sidecar listener is loopback-only but it is still a process + // boundary. A local peer without the parent-minted capability cannot + // use it to trigger a quota refresh. + const direct = await fetch(new URL("/api/provider-quotas?refresh=1", sidecarUrl)); + expect(direct.status).toBe(404); + + // The parent bridge is never a second management endpoint: an admin + // token does not substitute for the child-only capability. + const bridge = await fetch(new URL("/__ocx_go_sidecar/provider-quotas", serverB.url), { + headers: { "x-opencodex-api-key": token }, + }); + expect(bridge.status).toBe(404); + } finally { + await serverB.stop(true); + } + } finally { + await serverA.stop(true); + } + }); + runFixtureTest("custom-models configured body is byte-identical (unknown keys and file order kept)", async (token) => { // A non-default section exercises the echo, not the fallback: unknown // per-entry keys survive, and each entry's key order follows the file From eb4292e8f09992f9353ba47c51e454a62fecf0a9 Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Sun, 6 Sep 2026 12:13:25 +0800 Subject: [PATCH 013/165] feat(go): relay management write batches through sidecar --- go/cmd/ocx-sidecar/main.go | 13 +- go/internal/managementauth/write_relay.go | 144 +++++++++++ .../managementauth/write_relay_test.go | 92 +++++++ go/internal/sidecar/sidecar.go | 176 ++++++++++++- go/internal/sidecar/sidecar_test.go | 113 +++++++++ src/server/direct-local-http.ts | 23 +- src/server/go-sidecar-slot.ts | 14 +- src/server/go-sidecar-write-relay.ts | 239 ++++++++++++++++++ src/server/go-sidecar.ts | 71 +++++- src/server/index.ts | 46 +++- src/server/management-api.ts | 16 +- src/server/management/route-registry.ts | 74 +++--- tests/go-ownership-plumbing.test.ts | 111 ++++++-- tests/go-sidecar-parity.test.ts | 136 +++++++++- tests/go-sidecar-write-relay.test.ts | 143 +++++++++++ .../local-management-direct-transport.test.ts | 34 +++ 16 files changed, 1357 insertions(+), 88 deletions(-) create mode 100644 go/internal/managementauth/write_relay.go create mode 100644 go/internal/managementauth/write_relay_test.go create mode 100644 src/server/go-sidecar-write-relay.ts create mode 100644 tests/go-sidecar-write-relay.test.ts diff --git a/go/cmd/ocx-sidecar/main.go b/go/cmd/ocx-sidecar/main.go index 33e9fc9a37..2db52506f8 100644 --- a/go/cmd/ocx-sidecar/main.go +++ b/go/cmd/ocx-sidecar/main.go @@ -59,12 +59,13 @@ func serve() error { addr := listener.Addr().String() cfg := sidecar.Config{ - Service: "opencodex", - Version: os.Getenv("OCX_SIDECAR_VERSION"), - StartedAt: time.Now(), - ParentURL: os.Getenv("OCX_SIDECAR_PARENT_URL"), - BridgeToken: os.Getenv("OCX_SIDECAR_BRIDGE_TOKEN"), - RequestToken: os.Getenv("OCX_SIDECAR_REQUEST_TOKEN"), + Service: "opencodex", + Version: os.Getenv("OCX_SIDECAR_VERSION"), + StartedAt: time.Now(), + ParentURL: os.Getenv("OCX_SIDECAR_PARENT_URL"), + BridgeToken: os.Getenv("OCX_SIDECAR_BRIDGE_TOKEN"), + RequestToken: os.Getenv("OCX_SIDECAR_REQUEST_TOKEN"), + WriteRelaySecret: os.Getenv("OCX_SIDECAR_WRITE_RELAY_SECRET"), } if cfg.Version == "" { fmt.Fprintln(os.Stderr, "ocx-sidecar: warning: OCX_SIDECAR_VERSION is unset; reporting version 0.0.0") diff --git a/go/internal/managementauth/write_relay.go b/go/internal/managementauth/write_relay.go new file mode 100644 index 0000000000..82b7086341 --- /dev/null +++ b/go/internal/managementauth/write_relay.go @@ -0,0 +1,144 @@ +package managementauth + +// Write-relay proofs bind a TypeScript front-door admission to one specific +// request the Go sidecar is allowed to send back to its private parent bridge. +// They are deliberately separate from public management credentials: the +// sidecar receives neither an admin token nor a dashboard session secret. + +import ( + "crypto/sha256" + "encoding/hex" + "regexp" + "sync" + "time" +) + +const ( + // WriteRelayNonceHeader carries a fresh base64url nonce from the admitting front door. + WriteRelayNonceHeader = "X-Ocx-Go-Sidecar-Relay-Nonce" + // WriteRelayPrincipalHeader carries the principal the front door admitted. + WriteRelayPrincipalHeader = "X-Ocx-Go-Sidecar-Relay-Principal" + // WriteRelayExpiresAtHeader is a decimal epoch-millisecond deadline. + WriteRelayExpiresAtHeader = "X-Ocx-Go-Sidecar-Relay-Expires-At" + // WriteRelayProofHeader carries the HMAC-SHA256 relay proof. + WriteRelayProofHeader = "X-Ocx-Go-Sidecar-Relay-Proof" + // WriteRelayReplayLimit bounds retained consumed nonces for one sidecar process. + WriteRelayReplayLimit = 256 +) + +const writeRelayTTLMillis int64 = int64(30 * time.Second / time.Millisecond) + +var writeRelayNoncePattern = regexp.MustCompile("^[A-Za-z0-9_-]{43}$") + +// WriteRelayProof is the header-derived assertion to verify for one body. +// Method and Path are supplied from the actual sidecar request, rather than +// trusted from a header, so a proof cannot be replayed onto another route. +type WriteRelayProof struct { + Nonce string + Principal Principal + Method string + Path string + ExpiresAt int64 + Proof string +} + +// WriteRelayVerifier owns the bounded one-use nonce table for one sidecar +// process. It is safe for concurrent requests. +type WriteRelayVerifier struct { + mu sync.Mutex + secret string + nowFn func() int64 + consumed map[string]int64 +} + +// NewWriteRelayVerifier creates a verifier over the shared private bridge +// secret. An empty secret is deliberately unusable and fails every proof. +func NewWriteRelayVerifier(secret string) *WriteRelayVerifier { + return &WriteRelayVerifier{ + secret: secret, + // `time.Now().UnixMilli` would bind UnixMilli to the instant at verifier + // construction. The relay's maximum-TTL check must use the request time. + nowFn: func() int64 { return time.Now().UnixMilli() }, + consumed: map[string]int64{}, + } +} + +// WithClock replaces the wall-clock source for deterministic tests. +func (v *WriteRelayVerifier) WithClock(now func() int64) *WriteRelayVerifier { + v.nowFn = now + return v +} + +// ParseWriteRelayExpiry accepts the canonical decimal header form used by the +// existing local capability contracts. It rejects zero, signs, whitespace and +// overflow rather than normalising attacker-controlled input. +func ParseWriteRelayExpiry(value string) (int64, bool) { return parseExpiryHeader(value) } + +// CreateWriteRelayProof signs a one-use proof. Empty means the supplied +// binding is invalid. The HMAC payload is versioned and newline-delimited: +// nonce, principal, method, path, SHA-256(body) in lowercase hex, expiry. +func CreateWriteRelayProof(secret string, proof WriteRelayProof, body []byte) string { + if secret == "" { + return "" + } + payload, ok := writeRelayPayload(proof, body) + if !ok { + return "" + } + return hmacBase64URL(secret, payload) +} + +// VerifyAndConsume checks the complete proof then records its nonce before a +// caller can dispatch the parent mutation. Reusing a nonce, a changed body, +// principal, method, path or expiry all fail. Consumption happens while the +// mutex is held so concurrent requests cannot both spend one proof. +func (v *WriteRelayVerifier) VerifyAndConsume(proof WriteRelayProof, body []byte) bool { + if v == nil || v.secret == "" || !base64URL256.MatchString(proof.Proof) { + return false + } + payload, ok := writeRelayPayload(proof, body) + if !ok { + return false + } + v.mu.Lock() + defer v.mu.Unlock() + now := v.nowFn() + if !expiryWithin(now, proof.ExpiresAt, writeRelayTTLMillis) { + return false + } + expected := hmacBase64URL(v.secret, payload) + if expected == "" || !equalCapabilityBytes(expected, proof.Proof) { + return false + } + pruneConsumed(v.consumed, now) + if _, replayed := v.consumed[proof.Nonce]; replayed || len(v.consumed) >= WriteRelayReplayLimit { + return false + } + v.consumed[proof.Nonce] = proof.ExpiresAt + return true +} + +func writeRelayPayload(proof WriteRelayProof, body []byte) (string, bool) { + if !writeRelayNoncePattern.MatchString(proof.Nonce) || !isWriteRelayPrincipal(proof.Principal) { + return "", false + } + if !writeRelayMethodPattern.MatchString(proof.Method) || !writeRelayPathPattern.MatchString(proof.Path) || proof.ExpiresAt <= 0 { + return "", false + } + digest := sha256.Sum256(body) + return "opencodex-go-write-relay-v1\n" + proof.Nonce + "\n" + string(proof.Principal) + "\n" + proof.Method + "\n" + proof.Path + "\n" + hex.EncodeToString(digest[:]) + "\n" + itoa(proof.ExpiresAt), true +} + +var ( + writeRelayMethodPattern = regexp.MustCompile("^[A-Z]+$") + writeRelayPathPattern = regexp.MustCompile("^/[^?#\\r\\n]*$") +) + +func isWriteRelayPrincipal(principal Principal) bool { + switch principal { + case PrincipalAdminToken, PrincipalGuiSession, PrincipalGuiPairCapability, PrincipalLocalReadCapability, PrincipalLocalProviderReloadCapability, PrincipalSystemRestartCapability: + return true + default: + return false + } +} diff --git a/go/internal/managementauth/write_relay_test.go b/go/internal/managementauth/write_relay_test.go new file mode 100644 index 0000000000..be4d0fa04f --- /dev/null +++ b/go/internal/managementauth/write_relay_test.go @@ -0,0 +1,92 @@ +package managementauth + +import ( + "fmt" + "testing" +) + +const writeRelaySecret = "sidecar-private-bridge-secret" + +func relayProof(nonce string, expiresAt int64) WriteRelayProof { + return WriteRelayProof{Nonce: nonce, Principal: PrincipalAdminToken, Method: "PUT", Path: "/api/settings", ExpiresAt: expiresAt} +} + +func TestWriteRelayProofBindsEveryMutationInputAndConsumesNonce(t *testing.T) { + const now = int64(1800000000000) + body := []byte("{\"streamMode\":\"eager-relay\"}") + proof := relayProof(fmt.Sprintf("%043d", 1), now+1000) + proof.Proof = CreateWriteRelayProof(writeRelaySecret, proof, body) + if proof.Proof == "" { + t.Fatal("CreateWriteRelayProof returned empty proof") + } + verifier := NewWriteRelayVerifier(writeRelaySecret).WithClock(func() int64 { return now }) + if !verifier.VerifyAndConsume(proof, body) { + t.Fatal("valid relay proof was rejected") + } + if verifier.VerifyAndConsume(proof, body) { + t.Fatal("replayed nonce was accepted") + } + for _, mutation := range []struct { + name string + edit func(*WriteRelayProof, *[]byte) + }{ + {"body", func(_ *WriteRelayProof, b *[]byte) { *b = []byte("{\"streamMode\":\"auto\"}") }}, + {"principal", func(p *WriteRelayProof, _ *[]byte) { p.Principal = PrincipalGuiSession }}, + {"method", func(p *WriteRelayProof, _ *[]byte) { p.Method = "POST" }}, + {"path", func(p *WriteRelayProof, _ *[]byte) { p.Path = "/api/shadow-call-settings" }}, + {"expiry", func(p *WriteRelayProof, _ *[]byte) { p.ExpiresAt++ }}, + } { + t.Run(mutation.name, func(t *testing.T) { + candidate := proof + candidate.Nonce = fmt.Sprintf("%043d", 2) + candidate.Proof = CreateWriteRelayProof(writeRelaySecret, candidate, body) + candidateBody := append([]byte(nil), body...) + mutation.edit(&candidate, &candidateBody) + if NewWriteRelayVerifier(writeRelaySecret).WithClock(func() int64 { return now }).VerifyAndConsume(candidate, candidateBody) { + t.Fatalf("proof with changed %s was accepted", mutation.name) + } + }) + } +} + +func TestWriteRelayProofMatchesTypeScriptHMACFixture(t *testing.T) { + body := []byte(`{"enabled":false}`) + proof := WriteRelayProof{ + Nonce: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", Principal: PrincipalAdminToken, + Method: "PUT", Path: "/api/shadow-call-settings", ExpiresAt: 1800000001000, + } + if got := CreateWriteRelayProof("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", proof, body); got != "_NzTNrMpPfCxQFlOn8gAkgKEaOnP9jP8nGBiLu3x4UI" { + t.Fatalf("Go proof = %q; differs from TypeScript fixture", got) + } +} + +func TestWriteRelayProofRejectsExpiredInvalidAndSaturatedNonces(t *testing.T) { + const now = int64(1800000000000) + body := []byte("{}") + verifier := NewWriteRelayVerifier(writeRelaySecret).WithClock(func() int64 { return now }) + for _, expiresAt := range []int64{now, now - 1, now + writeRelayTTLMillis + 1} { + proof := relayProof(fmt.Sprintf("%043d", 1), expiresAt) + proof.Proof = CreateWriteRelayProof(writeRelaySecret, proof, body) + if verifier.VerifyAndConsume(proof, body) { + t.Fatalf("expiry %d was accepted", expiresAt) + } + } + if _, ok := ParseWriteRelayExpiry("001"); ok { + t.Fatal("non-canonical expiry was accepted") + } + if CreateWriteRelayProof(writeRelaySecret, WriteRelayProof{Nonce: "bad", Principal: PrincipalAdminToken, Method: "PUT", Path: "/api/settings", ExpiresAt: now + 1}, body) != "" { + t.Fatal("invalid nonce produced a proof") + } + for i := 0; i < WriteRelayReplayLimit; i++ { + proof := relayProof(fmt.Sprintf("%043d", i+1), now+10000) + proof.Proof = CreateWriteRelayProof(writeRelaySecret, proof, body) + if !verifier.VerifyAndConsume(proof, body) { + t.Fatalf("proof %d was rejected before the replay limit", i) + } + } + extra := relayProof(fmt.Sprintf("%043d", WriteRelayReplayLimit+1), now+10000) + extra.Proof = CreateWriteRelayProof(writeRelaySecret, extra, body) + if verifier.VerifyAndConsume(extra, body) { + t.Fatal("proof was accepted after replay table reached its limit") + } +} diff --git a/go/internal/sidecar/sidecar.go b/go/internal/sidecar/sidecar.go index ccab4a091c..57e2ee44d7 100644 --- a/go/internal/sidecar/sidecar.go +++ b/go/internal/sidecar/sidecar.go @@ -10,6 +10,7 @@ package sidecar import ( + "bytes" "encoding/json" "fmt" "io" @@ -17,9 +18,11 @@ import ( "net/http" "net/url" "os" + "strings" "time" "github.com/lidge-jun/opencodex/go/internal/config" + "github.com/lidge-jun/opencodex/go/internal/managementauth" ) // Config carries the values the sidecar must echo from its TypeScript parent. @@ -52,8 +55,22 @@ type Config struct { ParentURL string BridgeToken string RequestToken string + // WriteRelaySecret signs parent admission claims for public mutations. It is + // distinct from BridgeToken: the latter only authenticates the child on the + // private parent hop. + WriteRelaySecret string } +const ( + // SidecarRequestHeader proves the parent, rather than an arbitrary local + // process, asked the sidecar to serve a protected bridge-backed route. + SidecarRequestHeader = "X-Ocx-Go-Sidecar-Request" + // SidecarBridgeHeader authenticates the sidecar to the private parent bridge. + SidecarBridgeHeader = "X-Ocx-Go-Sidecar-Bridge" + privateWriteBridgePath = "/__ocx_go_sidecar/write" + maxWriteBodyBytes = 2 * 1024 * 1024 +) + // healthPayload mirrors the JSON object literal in // src/server/management/system-routes.ts. Field order is the byte contract: // encoding/json emits struct fields in declaration order and the TS handler @@ -73,6 +90,7 @@ type healthPayload struct { // never sees another request while the seam is wired correctly. func NewHandler(cfg Config) http.Handler { mux := http.NewServeMux() + writeRelay := managementauth.NewWriteRelayVerifier(cfg.WriteRelaySecret) mux.HandleFunc("GET /api/system/health", func(w http.ResponseWriter, r *http.Request) { version := cfg.Version if version == "" { @@ -104,7 +122,16 @@ func NewHandler(cfg Config) http.Handler { // with NO normalisation — any drift is a real divergence. The config is // read per request (the sidecar carries no state) from cfg.ConfigDir or the // OPENCODEX_HOME / ~/.opencodex resolution the TS parent uses. - mux.HandleFunc("GET /api/shadow-call-settings", func(w http.ResponseWriter, r *http.Request) { + mux.HandleFunc("/api/shadow-call-settings", func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodPut { + relayPublicWrite(w, r, cfg, writeRelay, "/api/shadow-call-settings") + return + } + if r.Method != http.MethodGet { + // Preserve the GET-qualified handler's pre-write ServeMux behavior. + w.WriteHeader(http.StatusMethodNotAllowed) + return + } loaded, err := loadSidecarConfig(cfg.ConfigDir) if err != nil { // A missing or unreadable config is an empty Config (the TS runtime @@ -159,7 +186,7 @@ func NewHandler(cfg Config) http.Handler { // refresh, invalidation, passive observations and last-good retention keep // their established semantics while the wire response stays byte-identical. mux.HandleFunc("GET /api/provider-quotas", func(w http.ResponseWriter, r *http.Request) { - if cfg.RequestToken == "" || r.Header.Get("X-Ocx-Go-Sidecar-Request") != cfg.RequestToken { + if cfg.RequestToken == "" || !managementauth.EqualSecret(r.Header.Get(SidecarRequestHeader), cfg.RequestToken) { http.NotFound(w, r) return } @@ -175,7 +202,7 @@ func NewHandler(cfg Config) http.Handler { http.Error(w, "quota state bridge unavailable", http.StatusServiceUnavailable) return } - bridgeReq.Header.Set("X-Ocx-Go-Sidecar-Bridge", cfg.BridgeToken) + bridgeReq.Header.Set(SidecarBridgeHeader, cfg.BridgeToken) bridgeTransport := &http.Transport{ Proxy: nil, DialContext: (&net.Dialer{}).DialContext, @@ -208,9 +235,152 @@ func NewHandler(cfg Config) http.Handler { fmt.Fprintf(os.Stderr, "ocx-sidecar: write provider-quotas payload: %v\n", err) } }) + + // Ticket #21's public mutation surface is deliberately exact. The sidecar + // never dispatches by prefix or forwards an unrecognised write: each allowed + // method/path pair is registered explicitly and the private bridge remains + // the TypeScript oracle until that route's native mutation lands. + for _, route := range []string{ + "/api/settings", + "/api/sidecar-settings", + "/api/codex-auth/active", + "/api/codex-auth/pool-strategy", + "/api/oauth/accounts/active", + "/api/oauth/accounts/pool", + } { + path := route + mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) { + // A method-qualified ServeMux pattern would synthesize 405 for an + // existing sidecar read probe. Keep the pre-write surface exactly as + // it was: only an allowlisted PUT exists; every other method is 404. + if r.Method != http.MethodPut && !(path == "/api/oauth/accounts/pool" && r.Method == http.MethodPatch) && !(path == "/api/codex-auth/pool-strategy" && r.Method == http.MethodPatch) { + http.NotFound(w, r) + return + } + relayPublicWrite(w, r, cfg, writeRelay, path) + }) + } + for _, path := range []string{ + "/api/codex-auth/accounts/clear-cooldown", + "/api/codex-auth/reset-credits/consume", + "/api/oauth/accounts/clear-cooldown", + } { + path := path + mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.NotFound(w, r) + return + } + relayPublicWrite(w, r, cfg, writeRelay, path) + }) + } return mux } +// relayPublicWrite verifies the parent's request token and one-use relay proof +// before it can contact the private parent bridge. It forwards the original +// bytes and only the authenticated relay metadata; public credentials and +// arbitrary client headers never cross this process boundary. +func relayPublicWrite(w http.ResponseWriter, r *http.Request, cfg Config, verifier *managementauth.WriteRelayVerifier, path string) { + if cfg.RequestToken == "" || !managementauth.EqualSecret(r.Header.Get(SidecarRequestHeader), cfg.RequestToken) { + http.NotFound(w, r) + return + } + if r.URL.Path != path || r.URL.RawQuery != "" { + http.NotFound(w, r) + return + } + body, err := io.ReadAll(io.LimitReader(r.Body, maxWriteBodyBytes+1)) + if err != nil || len(body) > maxWriteBodyBytes { + http.Error(w, "request body too large", http.StatusRequestEntityTooLarge) + return + } + expiresAt, ok := managementauth.ParseWriteRelayExpiry(r.Header.Get(managementauth.WriteRelayExpiresAtHeader)) + relayProof := managementauth.WriteRelayProof{ + Nonce: r.Header.Get(managementauth.WriteRelayNonceHeader), + Principal: managementauth.Principal(r.Header.Get(managementauth.WriteRelayPrincipalHeader)), + Method: r.Method, + Path: path, + ExpiresAt: expiresAt, + Proof: r.Header.Get(managementauth.WriteRelayProofHeader), + } + if !ok || !verifier.VerifyAndConsume(relayProof, body) { + http.Error(w, "write relay unauthorized", http.StatusUnauthorized) + return + } + + parent, ok := privateParentBridgeURL(cfg.ParentURL, privateWriteBridgePath) + if !ok || cfg.BridgeToken == "" { + http.Error(w, "write state bridge unavailable", http.StatusServiceUnavailable) + return + } + bridgeReq, err := http.NewRequestWithContext(r.Context(), http.MethodPut, parent.String(), bytes.NewReader(body)) + if err != nil { + http.Error(w, "write state bridge unavailable", http.StatusServiceUnavailable) + return + } + bridgeReq.Header.Set(SidecarBridgeHeader, cfg.BridgeToken) + bridgeReq.Header.Set(managementauth.WriteRelayNonceHeader, r.Header.Get(managementauth.WriteRelayNonceHeader)) + bridgeReq.Header.Set(managementauth.WriteRelayPrincipalHeader, r.Header.Get(managementauth.WriteRelayPrincipalHeader)) + bridgeReq.Header.Set("X-Ocx-Go-Sidecar-Relay-Method", r.Method) + bridgeReq.Header.Set("X-Ocx-Go-Sidecar-Relay-Path", path) + bridgeReq.Header.Set(managementauth.WriteRelayExpiresAtHeader, r.Header.Get(managementauth.WriteRelayExpiresAtHeader)) + bridgeReq.Header.Set(managementauth.WriteRelayProofHeader, r.Header.Get(managementauth.WriteRelayProofHeader)) + if contentType := r.Header.Get("Content-Type"); contentType != "" { + bridgeReq.Header.Set("Content-Type", contentType) + } + + bridgeResp, err := privateBridgeClient().Do(bridgeReq) + if err != nil { + http.Error(w, "write state bridge unavailable", http.StatusServiceUnavailable) + return + } + defer bridgeResp.Body.Close() + raw, err := io.ReadAll(io.LimitReader(bridgeResp.Body, 8*1024*1024+1)) + if err != nil || len(raw) > 8*1024*1024 { + http.Error(w, "write state bridge unavailable", http.StatusServiceUnavailable) + return + } + if contentType := bridgeResp.Header.Get("Content-Type"); contentType != "" { + w.Header().Set("Content-Type", contentType) + } + if retryAfter := bridgeResp.Header.Get("Retry-After"); retryAfter != "" { + w.Header().Set("Retry-After", retryAfter) + } + w.WriteHeader(bridgeResp.StatusCode) + if _, err := w.Write(raw); err != nil { + fmt.Fprintf(os.Stderr, "ocx-sidecar: write relay response: %v\\n", err) + } +} + +// privateParentBridgeURL derives the exact private route from the loopback +// parent URL supplied to the sidecar in OCX_SIDECAR_PARENT_URL. Credentials, +// proxy destinations and public hosts are rejected before any request occurs. +func privateParentBridgeURL(raw, bridgePath string) (*url.URL, bool) { + parent, err := url.Parse(raw) + if err != nil || parent.Scheme != "http" || parent.Hostname() != "127.0.0.1" || parent.User != nil || parent.Fragment != "" { + return nil, false + } + if parent.Port() == "" || !strings.HasPrefix(bridgePath, "/__ocx_go_sidecar/") { + return nil, false + } + parent.Path = bridgePath + parent.RawPath = "" + parent.RawQuery = "" + return parent, true +} + +func privateBridgeClient() *http.Client { + transport := &http.Transport{Proxy: nil, DialContext: (&net.Dialer{}).DialContext} + return &http.Client{ + Timeout: 30 * time.Second, + Transport: transport, + CheckRedirect: func(_ *http.Request, _ []*http.Request) error { + return http.ErrUseLastResponse + }, + } +} + // loadSidecarConfig is the config-file loader used by the shadow-call route. // An explicit dir (unit tests) wins; otherwise the same OPENCODEX_HOME then // ~/.opencodex resolution the TS parent uses at spawn. diff --git a/go/internal/sidecar/sidecar_test.go b/go/internal/sidecar/sidecar_test.go index eaa89e2875..2a6ecc0146 100644 --- a/go/internal/sidecar/sidecar_test.go +++ b/go/internal/sidecar/sidecar_test.go @@ -3,6 +3,7 @@ package sidecar import ( "bytes" "encoding/json" + "fmt" "io" "net/http" "net/http/httptest" @@ -11,6 +12,8 @@ import ( "regexp" "testing" "time" + + "github.com/lidge-jun/opencodex/go/internal/managementauth" ) // requestURLs each case against the handler and returns the raw response. @@ -22,6 +25,116 @@ func do(t *testing.T, h http.Handler, method, path string) *http.Response { return rec.Result() } +func writeRelayHeaders(t *testing.T, token, secret, path string, body []byte, nonce string) http.Header { + t.Helper() + expiresAt := time.Now().Add(10 * time.Second).UnixMilli() + proof := managementauth.WriteRelayProof{Nonce: nonce, Principal: managementauth.PrincipalAdminToken, Method: http.MethodPut, Path: path, ExpiresAt: expiresAt} + proof.Proof = managementauth.CreateWriteRelayProof(secret, proof, body) + if proof.Proof == "" { + t.Fatal("write relay proof was empty") + } + headers := make(http.Header) + headers.Set(SidecarRequestHeader, token) + headers.Set(managementauth.WriteRelayNonceHeader, proof.Nonce) + headers.Set(managementauth.WriteRelayPrincipalHeader, string(proof.Principal)) + headers.Set(managementauth.WriteRelayExpiresAtHeader, fmt.Sprintf("%d", proof.ExpiresAt)) + headers.Set(managementauth.WriteRelayProofHeader, proof.Proof) + headers.Set("Content-Type", "application/json") + return headers +} + +func TestConfigWriteRelayVerifiesProofAndRelaysOnlyTheAllowlist(t *testing.T) { + const requestToken = "parent-to-sidecar" + const bridgeToken = "sidecar-to-parent" + const body = "{\"streamMode\":\"eager-relay\"}" + var bridgeCalls int + bridge := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + bridgeCalls++ + if r.Method != http.MethodPut || r.URL.Path != privateWriteBridgePath { + t.Errorf("bridge request = %s %s", r.Method, r.URL.String()) + w.WriteHeader(http.StatusNotFound) + return + } + if r.Header.Get(SidecarBridgeHeader) != bridgeToken { + t.Error("bridge token missing") + w.WriteHeader(http.StatusForbidden) + return + } + if r.Header.Get(managementauth.WriteRelayPrincipalHeader) != string(managementauth.PrincipalAdminToken) { + t.Errorf("principal = %q", r.Header.Get(managementauth.WriteRelayPrincipalHeader)) + } + if got, _ := io.ReadAll(r.Body); string(got) != body { + t.Errorf("bridge body = %s, want %s", got, body) + } + w.Header().Set("Content-Type", "application/json") + w.Header().Set("Retry-After", "1") + w.WriteHeader(http.StatusConflict) + _, _ = w.Write([]byte("{\"error\":\"configuration is busy\"}")) + })) + defer bridge.Close() + + h := NewHandler(Config{ParentURL: bridge.URL, BridgeToken: bridgeToken, RequestToken: requestToken, WriteRelaySecret: bridgeToken}) + path := "/api/settings" + req := httptest.NewRequest(http.MethodPut, path, bytes.NewBufferString(body)) + req.Header = writeRelayHeaders(t, requestToken, bridgeToken, path, []byte(body), fmt.Sprintf("%043d", 1)) + rec := httptest.NewRecorder() + h.ServeHTTP(rec, req) + resp := rec.Result() + defer resp.Body.Close() + raw, err := io.ReadAll(resp.Body) + if err != nil { + t.Fatal(err) + } + if resp.StatusCode != http.StatusConflict || string(raw) != "{\"error\":\"configuration is busy\"}" { + t.Fatalf("response = %d %s", resp.StatusCode, raw) + } + if got := resp.Header.Get("Retry-After"); got != "1" { + t.Fatalf("Retry-After = %q, want 1", got) + } + if bridgeCalls != 1 { + t.Fatalf("bridge calls = %d, want 1", bridgeCalls) + } + + replayed := httptest.NewRequest(http.MethodPut, path, bytes.NewBufferString(body)) + replayed.Header = req.Header.Clone() + replayRec := httptest.NewRecorder() + h.ServeHTTP(replayRec, replayed) + if replayRec.Code != http.StatusUnauthorized { + t.Fatalf("replay status = %d, want 401", replayRec.Code) + } + blocked := httptest.NewRequest(http.MethodPost, "/api/providers", bytes.NewBufferString(body)) + blocked.Header = writeRelayHeaders(t, requestToken, bridgeToken, "/api/providers", []byte(body), fmt.Sprintf("%043d", 2)) + blockedRec := httptest.NewRecorder() + h.ServeHTTP(blockedRec, blocked) + if blockedRec.Code != http.StatusNotFound { + t.Fatalf("non-allowlisted write status = %d, want 404", blockedRec.Code) + } + if bridgeCalls != 1 { + t.Fatalf("blocked write reached bridge (%d calls)", bridgeCalls) + } +} + +func TestConfigWriteRelayRejectsMissingTokenOrChangedBody(t *testing.T) { + const requestToken = "parent-to-sidecar" + const bridgeToken = "sidecar-to-parent" + body := []byte("{\"enabled\":true}") + h := NewHandler(Config{ParentURL: "http://127.0.0.1:1", BridgeToken: bridgeToken, RequestToken: requestToken, WriteRelaySecret: bridgeToken}) + path := "/api/shadow-call-settings" + missing := httptest.NewRequest(http.MethodPut, path, bytes.NewReader(body)) + missingRec := httptest.NewRecorder() + h.ServeHTTP(missingRec, missing) + if missingRec.Code != http.StatusNotFound { + t.Fatalf("missing request token status = %d, want 404", missingRec.Code) + } + changed := httptest.NewRequest(http.MethodPut, path, bytes.NewBufferString("{\"enabled\":false}")) + changed.Header = writeRelayHeaders(t, requestToken, bridgeToken, path, body, fmt.Sprintf("%043d", 1)) + changedRec := httptest.NewRecorder() + h.ServeHTTP(changedRec, changed) + if changedRec.Code != http.StatusUnauthorized { + t.Fatalf("changed body status = %d, want 401", changedRec.Code) + } +} + func TestHealthShape(t *testing.T) { startedAt := time.Now().Add(-123 * time.Second) h := NewHandler(Config{Service: "opencodex", Version: "2.42.0", StartedAt: startedAt}) diff --git a/src/server/direct-local-http.ts b/src/server/direct-local-http.ts index 976c1af92b..94faf29d9d 100644 --- a/src/server/direct-local-http.ts +++ b/src/server/direct-local-http.ts @@ -219,7 +219,7 @@ function parseResponse(bytes: Buffer): Response { } /** - * Fetch one bodyless local HTTP GET or POST over a direct TCP connection. + * Fetch one local HTTP request over a direct TCP connection. * * Bun's global fetch and Bun 1.3's node:http compatibility layer can honor * HTTP(S)_PROXY. Local identity and capability probes must not expose headers @@ -239,16 +239,26 @@ export async function directLocalHttpFetch( if (url.protocol !== "http:") throw new Error("direct local request must use HTTP"); if (url.username || url.password) throw new Error("direct local request URL must not contain credentials"); - if ((method !== "GET" && method !== "POST") || body !== null) { - throw new Error("direct local request must be a bodyless GET or POST"); + if (!/^(GET|HEAD|POST|PUT|PATCH|DELETE)$/.test(method)) { + throw new Error("direct local request method is unsupported"); + } + if ((method === "GET" || method === "HEAD") && body !== null) { + throw new Error("direct local GET or HEAD request must be bodyless"); } if (signal?.aborted) throw abortReason(signal); const headers = new Headers(init.headers ?? (input instanceof Request ? input.headers : undefined)); headers.delete("proxy-authorization"); headers.delete("proxy-connection"); - if (method === "POST") headers.set("content-length", "0"); - else headers.delete("content-length"); + const bodyBytes = body === null ? Buffer.alloc(0) : Buffer.from(await new Response(body).arrayBuffer()); + if (bodyBytes.byteLength > DIRECT_LOCAL_HTTP_MAX_BYTES) { + throw new Error("direct local HTTP request exceeds the byte cap"); + } + if (bodyBytes.byteLength > 0 || method === "POST" || method === "PUT" || method === "PATCH") { + headers.set("content-length", String(bodyBytes.byteLength)); + } else { + headers.delete("content-length"); + } headers.set("host", url.host); headers.set("connection", "close"); const headerLines: string[] = []; @@ -257,6 +267,7 @@ export async function directLocalHttpFetch( `${method} ${url.pathname}${url.search} HTTP/1.1\r\n${headerLines.join("\r\n")}\r\n\r\n`, "latin1", ); + const requestPayload = bodyBytes.byteLength === 0 ? requestBytes : Buffer.concat([requestBytes, bodyBytes]); const parsedHostname = url.hostname.startsWith("[") && url.hostname.endsWith("]") ? url.hostname.slice(1, -1) : url.hostname; @@ -313,7 +324,7 @@ export async function directLocalHttpFetch( return; } socket.on("connect", () => { - try { socket?.write(requestBytes); } catch (error) { + try { socket?.write(requestPayload); } catch (error) { finish(error instanceof Error ? error : new Error(String(error))); } }); diff --git a/src/server/go-sidecar-slot.ts b/src/server/go-sidecar-slot.ts index a4846fd21c..17658c8a08 100644 --- a/src/server/go-sidecar-slot.ts +++ b/src/server/go-sidecar-slot.ts @@ -24,7 +24,11 @@ * Go-owned. */ -export type GoOwnedRouteForwarder = (method: string, pathAndSearch: string) => Promise; +export type GoOwnedRouteForwarder = ( + request: Request, + pathAndSearch: string, + principal?: import("./management-auth").ManagementPrincipal, +) => Promise; let forwarder: GoOwnedRouteForwarder | null = null; @@ -41,10 +45,14 @@ export function setGoOwnedRouteForwarder(next: GoOwnedRouteForwarder): () => voi * Forward one declared Go-owned request to the attached sidecar, or null when * no subsystem is active or the sidecar is unreachable. Never throws. */ -export async function tryForwardGoOwnedRoute(method: string, pathAndSearch: string): Promise { +export async function tryForwardGoOwnedRoute( + request: Request, + pathAndSearch: string, + principal?: import("./management-auth").ManagementPrincipal, +): Promise { if (!forwarder) return null; try { - return await forwarder(method, pathAndSearch); + return await forwarder(request, pathAndSearch, principal); } catch { return null; } diff --git a/src/server/go-sidecar-write-relay.ts b/src/server/go-sidecar-write-relay.ts new file mode 100644 index 0000000000..b9928cbe88 --- /dev/null +++ b/src/server/go-sidecar-write-relay.ts @@ -0,0 +1,239 @@ +/** + * Private parent bridge for ADR-0008 management writes. + * + * The Go sidecar never receives a browser session or admin token. Instead, the + * TypeScript front door admits the public request, signs one exact mutation, + * and the sidecar returns that assertion over its loopback-only bridge. This + * module re-verifies both layers before asking the legacy TypeScript handler + * to execute the mutation. It deliberately knows no handler paths itself: the + * management route registry remains the ownership authority. + */ +import { createHash, createHmac, randomBytes, timingSafeEqual } from "node:crypto"; +import type { ManagementPrincipal } from "./management-auth"; +import { findGoOwnedManagementRoute, type HttpMethod } from "./management/route-registry"; + +export const GO_SIDECAR_WRITE_BRIDGE_PATH = "/__ocx_go_sidecar/write"; +export const GO_SIDECAR_BRIDGE_HEADER = "x-ocx-go-sidecar-bridge"; +export const GO_SIDECAR_WRITE_RELAY_NONCE_HEADER = "x-ocx-go-sidecar-relay-nonce"; +export const GO_SIDECAR_WRITE_RELAY_PRINCIPAL_HEADER = "x-ocx-go-sidecar-relay-principal"; +export const GO_SIDECAR_WRITE_RELAY_METHOD_HEADER = "x-ocx-go-sidecar-relay-method"; +export const GO_SIDECAR_WRITE_RELAY_PATH_HEADER = "x-ocx-go-sidecar-relay-path"; +export const GO_SIDECAR_WRITE_RELAY_EXPIRES_AT_HEADER = "x-ocx-go-sidecar-relay-expires-at"; +export const GO_SIDECAR_WRITE_RELAY_PROOF_HEADER = "x-ocx-go-sidecar-relay-proof"; + +const RELAY_VERSION = "opencodex-go-write-relay-v1"; +const RELAY_TTL_MS = 30_000; +const RELAY_REPLAY_LIMIT = 256; +const MAX_WRITE_BODY_BYTES = 2 * 1024 * 1024; +const BASE64URL_256 = /^[A-Za-z0-9_-]{43}$/; +const EXPIRY = /^[1-9]\d*$/; +const PATH = /^\/[^?#\r\n]*$/; +const HTTP_METHODS = new Set(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD"]); +const PRINCIPALS = new Set([ + "admin-token", + "gui-session", + "gui-pair-capability", + "local-read-capability", + "local-provider-reload-capability", + "system-restart-capability", +]); + +export interface GoSidecarWriteRelayClaim { + nonce: string; + principal: ManagementPrincipal; + method: HttpMethod; + path: string; + expiresAt: number; + proof: string; +} + +export interface GoSidecarWriteRelay { + handle(request: Request, url: URL): Promise; +} + +export interface GoSidecarWriteRelayOptions { + /** Per-sidecar capability, distinct from the signed claim. */ + bridgeToken: string; + /** Shared only with the supervised sidecar through its process environment. */ + relaySecret: string; + /** Calls legacy TypeScript management dispatch with sidecar forwarding disabled. */ + dispatchLegacy(request: Request, url: URL, principal: ManagementPrincipal): Promise; + now?: () => number; +} + +export function createGoSidecarWriteRelayNonce(): string { + return randomBytes(32).toString("base64url"); +} + +/** Signs the Go-compatible, body-bound relay claim. The supervisor uses this on the public hop. */ +export function createGoSidecarWriteRelayProof( + secret: string, + claim: Omit, + body: Uint8Array, +): string | null { + const payload = relayPayload(claim, body); + if (!payload || !isRelaySecret(secret)) return null; + return createHmac("sha256", secret).update(payload).digest("base64url"); +} + +/** + * Mint the complete parent-to-sidecar header set for one already-admitted + * public write. This is intentionally separate from bridge verification so + * the supervisor cannot accidentally reuse a static header set across bodies. + */ +export function createGoSidecarWriteRelayHeaders( + secret: string, + principal: ManagementPrincipal | undefined, + request: { method: string; pathname: string; body: Uint8Array }, + now: () => number = Date.now, +): Headers | null { + if (!principal || !PRINCIPALS.has(principal) || !HTTP_METHODS.has(request.method as HttpMethod)) return null; + const expiresAt = now() + RELAY_TTL_MS; + if (!Number.isSafeInteger(expiresAt)) return null; + const claim = { + nonce: createGoSidecarWriteRelayNonce(), + principal, + method: request.method as HttpMethod, + path: request.pathname, + expiresAt, + }; + const proof = createGoSidecarWriteRelayProof(secret, claim, request.body); + if (!proof) return null; + return new Headers({ + [GO_SIDECAR_WRITE_RELAY_NONCE_HEADER]: claim.nonce, + [GO_SIDECAR_WRITE_RELAY_PRINCIPAL_HEADER]: claim.principal, + [GO_SIDECAR_WRITE_RELAY_METHOD_HEADER]: claim.method, + [GO_SIDECAR_WRITE_RELAY_PATH_HEADER]: claim.path, + [GO_SIDECAR_WRITE_RELAY_EXPIRES_AT_HEADER]: String(claim.expiresAt), + [GO_SIDECAR_WRITE_RELAY_PROOF_HEADER]: proof, + }); +} + +export function createGoSidecarWriteRelay(options: GoSidecarWriteRelayOptions): GoSidecarWriteRelay | null { + // The bridge capability is generated by the parent with randomUUID(), while + // the relay secret is a 32-byte base64url HMAC key. Do not accidentally + // require the two independent credentials to share an encoding. + if (!isBridgeToken(options.bridgeToken) || !isRelaySecret(options.relaySecret)) return null; + const now = options.now ?? Date.now; + const consumed = new Map(); + + return { + async handle(request, url): Promise { + if ( + request.method !== "PUT" + || url.pathname !== GO_SIDECAR_WRITE_BRIDGE_PATH + || url.search !== "" + || !equalSecret(request.headers.get(GO_SIDECAR_BRIDGE_HEADER), options.bridgeToken) + ) return notFound(); + + const claim = claimFromHeaders(request.headers); + if (!claim) return notFound(); + const route = findGoOwnedManagementRoute(claim.method, claim.path); + if (!route || !route.mutates || route.go.relay !== "signed") return notFound(); + + let body: Uint8Array; + try { + body = await readBoundedBody(request, MAX_WRITE_BODY_BYTES); + } catch { + return new Response(JSON.stringify({ error: "request body too large" }), { + status: 413, + headers: { "content-type": "application/json" }, + }); + } + + const clock = now(); + if ( + !Number.isSafeInteger(clock) + || claim.expiresAt <= clock + || claim.expiresAt > clock + RELAY_TTL_MS + || !verifyProof(options.relaySecret, claim, body) + ) return notFound(); + + pruneConsumed(consumed, clock); + if (consumed.has(claim.nonce) || consumed.size >= RELAY_REPLAY_LIMIT) return notFound(); + // Spend before dispatch: a transport retry cannot execute a mutation twice. + consumed.set(claim.nonce, claim.expiresAt); + + const headers = new Headers(); + const contentType = request.headers.get("content-type"); + const host = request.headers.get("host"); + if (contentType) headers.set("content-type", contentType); + // The parent bridge reconstructs a Request and reuses normal management + // validation. Preserve its own loopback Host so origin comparison sees + // the same authority as the public request; never copy client cookies or + // authorization credentials. + if (host) headers.set("host", host); + if (body.byteLength > 0) headers.set("content-length", String(body.byteLength)); + const legacyUrl = new URL(claim.path, url); + // Copy into an ArrayBuffer-backed view: TypeScript's DOM declaration + // rejects a possibly SharedArrayBuffer-backed Uint8Array as a Blob part. + const legacyBody = body.byteLength > 0 ? new Blob([new Uint8Array(body)]) : undefined; + const legacyRequest = new Request(legacyUrl, { + method: claim.method, + headers, + body: legacyBody, + }); + const response = await options.dispatchLegacy(legacyRequest, legacyUrl, claim.principal); + return response ?? notFound(); + }, + }; +} + +function claimFromHeaders(headers: Headers): GoSidecarWriteRelayClaim | null { + const nonce = headers.get(GO_SIDECAR_WRITE_RELAY_NONCE_HEADER) ?? ""; + const principal = headers.get(GO_SIDECAR_WRITE_RELAY_PRINCIPAL_HEADER) as ManagementPrincipal | null; + const method = headers.get(GO_SIDECAR_WRITE_RELAY_METHOD_HEADER) as HttpMethod | null; + const path = headers.get(GO_SIDECAR_WRITE_RELAY_PATH_HEADER) ?? ""; + const expiresAtRaw = headers.get(GO_SIDECAR_WRITE_RELAY_EXPIRES_AT_HEADER) ?? ""; + const proof = headers.get(GO_SIDECAR_WRITE_RELAY_PROOF_HEADER) ?? ""; + if (!BASE64URL_256.test(nonce) || !principal || !PRINCIPALS.has(principal) || !method || !HTTP_METHODS.has(method)) return null; + if (!PATH.test(path) || !EXPIRY.test(expiresAtRaw) || !BASE64URL_256.test(proof)) return null; + const expiresAt = Number(expiresAtRaw); + if (!Number.isSafeInteger(expiresAt)) return null; + return { nonce, principal, method, path, expiresAt, proof }; +} + +function relayPayload(claim: Omit, body: Uint8Array): string | null { + if (!BASE64URL_256.test(claim.nonce) || !PRINCIPALS.has(claim.principal) || !HTTP_METHODS.has(claim.method)) return null; + if (!PATH.test(claim.path) || !Number.isSafeInteger(claim.expiresAt) || claim.expiresAt <= 0) return null; + const digest = createHash("sha256").update(body).digest("hex"); + return RELAY_VERSION + "\n" + claim.nonce + "\n" + claim.principal + "\n" + claim.method + "\n" + claim.path + "\n" + digest + "\n" + claim.expiresAt; +} + +function verifyProof(secret: string, claim: GoSidecarWriteRelayClaim, body: Uint8Array): boolean { + const expected = createGoSidecarWriteRelayProof(secret, claim, body); + return expected !== null && equalSecret(claim.proof, expected); +} + +function isRelaySecret(value: string): boolean { + return BASE64URL_256.test(value); +} + +function isBridgeToken(value: string): boolean { + return value.length > 0 && value.length <= 256 && !/[\r\n]/.test(value); +} + +function equalSecret(actual: string | null, expected: string): boolean { + if (!actual) return false; + const left = Buffer.from(actual); + const right = Buffer.from(expected); + return left.byteLength === right.byteLength && timingSafeEqual(left, right); +} + +async function readBoundedBody(request: Request, limit: number): Promise { + const contentLength = Number(request.headers.get("content-length") ?? "0"); + if (Number.isFinite(contentLength) && contentLength > limit) throw new Error("too large"); + const body = new Uint8Array(await request.arrayBuffer()); + if (body.byteLength > limit) throw new Error("too large"); + return body; +} + +function pruneConsumed(consumed: Map, now: number): void { + for (const [nonce, expiresAt] of consumed) { + if (expiresAt <= now) consumed.delete(nonce); + } +} + +function notFound(): Response { + return new Response(null, { status: 404 }); +} diff --git a/src/server/go-sidecar.ts b/src/server/go-sidecar.ts index 0d68c05b6e..103593e2e0 100644 --- a/src/server/go-sidecar.ts +++ b/src/server/go-sidecar.ts @@ -44,6 +44,9 @@ export const GO_SIDECAR_BRIDGE_TOKEN_ENV = "OCX_SIDECAR_BRIDGE_TOKEN"; /** Capability presented by the parent when forwarding to the sidecar. */ export const GO_SIDECAR_REQUEST_TOKEN_ENV = "OCX_SIDECAR_REQUEST_TOKEN"; +/** HMAC secret for parent-admission claims on Go-owned write routes. */ +export const GO_SIDECAR_WRITE_RELAY_SECRET_ENV = "OCX_SIDECAR_WRITE_RELAY_SECRET"; + /** Readiness marker the Go binary prints on stdout after binding. */ export const GO_SIDECAR_READY_PREFIX = "ocx-sidecar-ready"; @@ -67,6 +70,21 @@ let generation = 0; let forwardDetach: (() => void) | null = null; let bridgeStopped: (() => void) | null = null; +export type GoSidecarSupervisorConfig = { + parentUrl: string; + bridgeToken: string; + requestToken: string; + writeRelaySecret: string; + /** Mints a proof bound to one admitted write's method, path and body bytes. */ + createWriteRelayHeaders?: (request: { + method: string; + pathname: string; + body: Uint8Array; + principal?: import("./management-auth").ManagementPrincipal; + }) => HeadersInit | null; + onStopped(): void; +}; + /** Test-only reset so an isolated harness does not inherit a live child. */ export function resetGoSidecarForTests(): void { if (!stopped) stopSidecar(); @@ -155,20 +173,54 @@ function warnActivation(message: string): void { console.warn(`[go-sidecar] ${message}; serving health in-process`); } +/** Build sidecar headers without forwarding browser credentials or cookies. */ +export function goSidecarRelayHeaders(request: Request, requestToken: string, relayHeaders: HeadersInit | null | undefined): Headers { + const headers = new Headers(relayHeaders ?? undefined); + headers.set("accept", "application/json"); + headers.set("x-ocx-go-sidecar-request", requestToken); + const contentType = request.headers.get("content-type"); + if (contentType) headers.set("content-type", contentType); + return headers; +} + /** Forward one declared Go-owned route request to a ready sidecar, or null on any failure. */ -async function forwardTo(baseUrl: string, requestToken: string, method: string, pathAndSearch: string): Promise { +async function forwardTo( + baseUrl: string, + requestToken: string, + createWriteRelayHeaders: GoSidecarSupervisorConfig["createWriteRelayHeaders"], + request: Request, + pathAndSearch: string, + principal?: import("./management-auth").ManagementPrincipal, +): Promise { try { - const upstream = await directLocalHttpFetch(new URL(pathAndSearch, baseUrl), { - method, - headers: { accept: "application/json", "x-ocx-go-sidecar-request": requestToken }, + const target = new URL(pathAndSearch, baseUrl); + const isWrite = request.method !== "GET" && request.method !== "HEAD"; + const body = isWrite ? new Uint8Array(await request.arrayBuffer()) : undefined; + const relayHeaders = isWrite + ? createWriteRelayHeaders?.({ method: request.method, pathname: target.pathname, body: body!, principal }) ?? null + : undefined; + // Go writes require a parent-minted, body-bound proof; otherwise use the + // in-process handler, which still owns the original request body. + if (isWrite && relayHeaders === null) return null; + const upstream = await directLocalHttpFetch(target, { + method: request.method, + headers: goSidecarRelayHeaders(request, requestToken, relayHeaders ?? undefined), + body, + signal: request.signal, }, pathAndSearch.startsWith("/api/provider-quotas") ? { timeoutMs: GO_SIDECAR_QUOTA_ROUTE_TIMEOUT_MS } : undefined); - if (!upstream.ok) return null; + // A Go-owned write's 4xx/5xx response is its observable result. Falling + // through on it would execute the legacy mutation a second time. Reads + // retain the existing fallback-on-non-2xx supervision behavior. + if (!upstream.ok && !isWrite) return null; // Relay the sidecar's response with the in-process handler's header shape: // Content-Type plus the body verbatim. The management-API CORS wrapper adds // the shared headers downstream exactly as it does for an in-process route. return new Response(upstream.body, { status: upstream.status, - headers: { "content-type": upstream.headers.get("content-type") ?? "application/json" }, + headers: { + "content-type": upstream.headers.get("content-type") ?? "application/json", + ...(upstream.headers.has("retry-after") ? { "retry-after": upstream.headers.get("retry-after")! } : {}), + }, }); } catch { return null; @@ -213,7 +265,7 @@ function stopSidecar(): void { */ export function activateGoSidecar( version: string, - liveStateBridge: { parentUrl: string; bridgeToken: string; requestToken: string; onStopped(): void }, + liveStateBridge: GoSidecarSupervisorConfig, ): { stop(): void } | null { const binary = process.env[GO_SIDECAR_BIN_ENV]?.trim(); if (!binary) return null; @@ -235,6 +287,7 @@ export function activateGoSidecar( [GO_SIDECAR_PARENT_URL_ENV]: liveStateBridge.parentUrl, [GO_SIDECAR_BRIDGE_TOKEN_ENV]: liveStateBridge.bridgeToken, [GO_SIDECAR_REQUEST_TOKEN_ENV]: liveStateBridge.requestToken, + [GO_SIDECAR_WRITE_RELAY_SECRET_ENV]: liveStateBridge.writeRelaySecret, }, }); } catch (error) { @@ -274,8 +327,8 @@ export function activateGoSidecar( if (stopped || myGeneration !== generation) return; readyBaseUrl = parsed; const baseUrl = parsed; - const detach = setGoOwnedRouteForwarder((method, pathAndSearch) => ( - forwardTo(baseUrl, liveStateBridge.requestToken, method, pathAndSearch) + const detach = setGoOwnedRouteForwarder((request, pathAndSearch, principal) => ( + forwardTo(baseUrl, liveStateBridge.requestToken, liveStateBridge.createWriteRelayHeaders, request, pathAndSearch, principal) )); if (stopped || myGeneration !== generation) { detach(); diff --git a/src/server/index.ts b/src/server/index.ts index 38babb6aeb..b4462b448a 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -1,5 +1,5 @@ import { markActivity } from "../lib/sidecar-tracker"; -import { randomUUID } from "node:crypto"; +import { randomBytes, randomUUID } from "node:crypto"; import { knownModelIdsForProvider } from "../router"; import { buildWarmupCompletionFrames, @@ -55,6 +55,12 @@ import { import { acquireServerBackgroundLifecycle } from "./background-lifecycle"; import { activateLab, labActivationRequired } from "../lib/lab-activation"; import { activateGoSidecar } from "./go-sidecar"; +import { + createGoSidecarWriteRelay, + createGoSidecarWriteRelayHeaders, + GO_SIDECAR_WRITE_BRIDGE_PATH, + type GoSidecarWriteRelay, +} from "./go-sidecar-write-relay"; import { runOpenAiTierStartupMigration } from "../providers/openai-tier-startup"; import { runAlibabaRegionStartupMigration } from "../providers/alibaba-region-startup"; import { runModelRenameStartupMigration } from "../providers/model-rename-startup"; @@ -1021,6 +1027,7 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server | null = null; let goSidecarLiveStateBridgeToken: string | null = null; + let goSidecarWriteRelay: GoSidecarWriteRelay | null = null; // Set only when the optional Go sidecar activated (ADR-0008); consumed by the server.stop // override below, which is built before activation runs. Null default keeps a process that // never opted in from carrying any Go-sidecar state. @@ -1079,6 +1086,14 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server { goSidecarLiveStateBridgeToken = randomUUID(); + // Generate an unexported child-only HMAC key per activation. An operator + // environment value is intentionally not reused: environment inheritance + // is broader than the parent/child capability relationship. + const writeRelaySecret = randomBytes(32).toString("base64url"); + goSidecarWriteRelay = createGoSidecarWriteRelay({ + bridgeToken: goSidecarLiveStateBridgeToken, + relaySecret: writeRelaySecret, + dispatchLegacy: (relayRequest, relayUrl, relayPrincipal) => ( + handleManagementAPI( + relayRequest, + relayUrl, + config, + deps.managementApi, + relayPrincipal, + managementSessionControl, + { skipGoSidecarForwarding: true }, + ) + ), + }); return activateGoSidecar(VERSION, { parentUrl: "http://127.0.0.1:" + actualPort, bridgeToken: goSidecarLiveStateBridgeToken, requestToken: randomUUID(), - onStopped: () => { goSidecarLiveStateBridgeToken = null; }, + writeRelaySecret, + createWriteRelayHeaders: goSidecarWriteRelay + ? request => createGoSidecarWriteRelayHeaders(writeRelaySecret, request.principal, request) + : undefined, + onStopped: () => { + goSidecarLiveStateBridgeToken = null; + goSidecarWriteRelay = null; + }, }); })() : null; @@ -2489,6 +2530,7 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server { +async function tryForwardDeclaredGoOwnedRoute( + req: Request, + url: URL, + principal: ManagementPrincipal | undefined, +): Promise { const declared = findGoOwnedManagementRoute(req.method as HttpMethod, url.pathname); if (!declared) return null; - return tryForwardGoOwnedRoute(req.method, `${url.pathname}${url.search}`); + // Preserve the original body for the TypeScript fallback if the optional sidecar is unavailable. + return tryForwardGoOwnedRoute(req.clone(), `${url.pathname}${url.search}`, principal); } export async function handleManagementAPI( @@ -154,6 +159,7 @@ export async function handleManagementAPI( deps: ManagementApiDeps = {}, principal?: ManagementPrincipal, sessionControl?: ManagementSessionControl, + options?: { skipGoSidecarForwarding?: boolean }, ): Promise { if (!isAllowedManagementOrigin(req, config)) { return jsonResponse({ error: "cross-origin request blocked" }, 403, req, config); @@ -175,8 +181,10 @@ export async function handleManagementAPI( // behave byte-identically to a build without Go, and the in-process handler stays the // differential oracle. One branch, driven by data — migrating another read route flips the // `go` marker in route-registry.ts and never edits this dispatch. - const goOwnedResponse = await tryForwardDeclaredGoOwnedRoute(req, url); - if (goOwnedResponse) return goOwnedResponse; + if (!options?.skipGoSidecarForwarding) { + const goOwnedResponse = await tryForwardDeclaredGoOwnedRoute(req, url, principal); + if (goOwnedResponse) return goOwnedResponse; + } async function convergeCodexCatalog(): Promise { let convergenceInvoked = false; diff --git a/src/server/management/route-registry.ts b/src/server/management/route-registry.ts index 1105b858f2..3122f22c4c 100644 --- a/src/server/management/route-registry.ts +++ b/src/server/management/route-registry.ts @@ -14,12 +14,13 @@ * never import anything from `src/lab/`. The `module` field names the owning file as text * for exactly this reason. * - * The file is also the ADR-0008 Go-ownership ledger. A read route declares itself - * Go-owned by carrying a `go` marker (see `GoOwnedRouteDeclaration`); the discriminated - * union makes the marker impossible on a write route, `GO_OWNED_MANAGEMENT_ROUTES` is the - * derived migrated surface, and the single forwarding branch in `management-api.ts` reads - * that data. Migrating a read route is a marker flip here plus the Go handler and oracle - * coverage -- never a dispatch edit. + * The file is also the ADR-0008 Go-ownership ledger. A route declares itself + * Go-owned by carrying a `go` marker (see `GoOwnedRouteDeclaration`): reads + * list volatile response fields and writes explicitly opt into the signed + * relay needed during the pre-flip transition. `GO_OWNED_MANAGEMENT_ROUTES` + * is the derived migrated surface, and the single forwarding branch in + * `management-api.ts` reads that data. Migrating a route is a marker flip + * here plus the Go handler and oracle coverage -- never a dispatch edit. * * Reconciliation lives in `tests/management-route-registry.test.ts`, which resolves * `(method, path)` pairs from source and fails loudly on a route whose method it cannot @@ -83,7 +84,7 @@ export type NonLiteralMechanism = * normalises exactly these top-level JSON keys and nothing else, so a later * route can never silently widen what "equal" means. */ -export interface GoOwnedRouteDeclaration { +export interface GoOwnedReadRouteDeclaration { /** * Top-level JSON body keys of the route's response that may legitimately * differ between the two implementations (process-specific values such as @@ -94,15 +95,18 @@ export interface GoOwnedRouteDeclaration { readonly volatileFields: readonly string[]; } +/** A write must opt into a parent-signed relay instead of inheriting read ownership. */ +export interface GoOwnedWriteRouteDeclaration { + readonly relay: "signed"; + readonly volatileFields: readonly string[]; +} + +export type GoOwnedRouteDeclaration = GoOwnedReadRouteDeclaration | GoOwnedWriteRouteDeclaration; + interface ManagementWriteRoute { - /** The route mutates state; it must stay in TypeScript until increment 3. */ readonly mutates: true; - /** - * A write route cannot be declared Go-owned. The read/write split is a - * discriminated union so the type system refuses `go` on this arm: only a - * read route (`mutates: false`) may carry a GoOwnedRouteDeclaration. - */ - readonly go?: never; + /** A write declaration must name the signed parent-to-sidecar relay. */ + readonly go?: GoOwnedWriteRouteDeclaration; } interface ManagementReadRoute { @@ -112,7 +116,7 @@ interface ManagementReadRoute { * in `management-api.ts` serves it from the attached ocx-sidecar, and the * in-process handler below remains the fallback and the differential oracle. */ - readonly go?: GoOwnedRouteDeclaration; + readonly go?: GoOwnedReadRouteDeclaration; } /** @@ -130,9 +134,8 @@ export type ManagementRoute = { readonly exempt?: RouteExemption; } & (ManagementWriteRoute | ManagementReadRoute); -/** A read route that carries a Go-ownership declaration (ADR-0008). */ +/** A route that carries a Go-ownership declaration (ADR-0008). */ export type GoOwnedManagementRoute = ManagementRoute & { - readonly mutates: false; readonly go: GoOwnedRouteDeclaration; }; @@ -148,21 +151,21 @@ export const MANAGEMENT_ROUTES: readonly ManagementRoute[] = [ { method: "GET", path: "/api/codex-auth/login-status", module: "codex/auth-api", mutates: false }, { method: "GET", path: "/api/codex-auth/quota", module: "codex/auth-api", mutates: false }, { method: "GET", path: "/api/codex-auth/reset-credits", module: "codex/auth-api", mutates: false }, - { method: "PATCH", path: "/api/codex-auth/pool-strategy", module: "codex/auth-api", mutates: true }, + { method: "PATCH", path: "/api/codex-auth/pool-strategy", module: "codex/auth-api", mutates: true, go: { relay: "signed", volatileFields: [] } }, { method: "POST", path: "/api/codex-auth/accounts", module: "codex/auth-api", mutates: true }, - { method: "POST", path: "/api/codex-auth/accounts/clear-cooldown", module: "codex/auth-api", mutates: true }, + { method: "POST", path: "/api/codex-auth/accounts/clear-cooldown", module: "codex/auth-api", mutates: true, go: { relay: "signed", volatileFields: [] } }, { method: "POST", path: "/api/codex-auth/login", module: "codex/auth-api", mutates: true }, { method: "POST", path: "/api/codex-auth/login/cancel", module: "codex/auth-api", mutates: true }, { method: "POST", path: "/api/codex-auth/login/code", module: "codex/auth-api", mutates: true }, - { method: "POST", path: "/api/codex-auth/reset-credits/consume", module: "codex/auth-api", mutates: true }, + { method: "POST", path: "/api/codex-auth/reset-credits/consume", module: "codex/auth-api", mutates: true, go: { relay: "signed", volatileFields: [] } }, { method: "PUT", path: "/api/codex-auth/accounts/alias", module: "codex/auth-api", mutates: true }, { method: "PUT", path: "/api/codex-auth/accounts/pause", module: "codex/auth-api", mutates: true }, { method: "PUT", path: "/api/codex-auth/accounts/pause-exhausted", module: "codex/auth-api", mutates: true }, { method: "PUT", path: "/api/codex-auth/accounts/priority", module: "codex/auth-api", mutates: true }, - { method: "PUT", path: "/api/codex-auth/active", module: "codex/auth-api", mutates: true }, + { method: "PUT", path: "/api/codex-auth/active", module: "codex/auth-api", mutates: true, go: { relay: "signed", volatileFields: [] } }, { method: "PUT", path: "/api/codex-auth/auto-switch", module: "codex/auth-api", mutates: true }, { method: "PUT", path: "/api/codex-auth/failover", module: "codex/auth-api", mutates: true }, - { method: "PUT", path: "/api/codex-auth/pool-strategy", module: "codex/auth-api", mutates: true }, + { method: "PUT", path: "/api/codex-auth/pool-strategy", module: "codex/auth-api", mutates: true, go: { relay: "signed", volatileFields: [] } }, // codex/native-profile-api { method: "GET", path: "/api/native-main-profiles", module: "codex/native-profile-api", mutates: false }, { method: "GET", path: "/api/native-main-profiles/doctor", module: "codex/native-profile-api", mutates: false }, @@ -231,9 +234,9 @@ export const MANAGEMENT_ROUTES: readonly ManagementRoute[] = [ { method: "POST", path: "/api/update/run", module: "server/management/config-routes", mutates: true }, { method: "POST", path: "/api/windows-tray", module: "server/management/config-routes", mutates: true }, { method: "PUT", path: "/api/config", module: "server/management/config-routes", mutates: true, exempt: { reason: "disabled", why: "Returns 405 by design; provider changes go through POST /api/providers." } }, - { method: "PUT", path: "/api/settings", module: "server/management/config-routes", mutates: true }, - { method: "PUT", path: "/api/shadow-call-settings", module: "server/management/config-routes", mutates: true }, - { method: "PUT", path: "/api/sidecar-settings", module: "server/management/config-routes", mutates: true }, + { method: "PUT", path: "/api/settings", module: "server/management/config-routes", mutates: true, go: { relay: "signed", volatileFields: [] } }, + { method: "PUT", path: "/api/shadow-call-settings", module: "server/management/config-routes", mutates: true, go: { relay: "signed", volatileFields: [] } }, + { method: "PUT", path: "/api/sidecar-settings", module: "server/management/config-routes", mutates: true, go: { relay: "signed", volatileFields: [] } }, // server/management/integration-routes { method: "GET", path: "/api/client-integrations", module: "server/management/integration-routes", mutates: false }, { method: "GET", path: "/api/client-integrations/journal", module: "server/management/integration-routes", mutates: false }, @@ -324,20 +327,20 @@ export const MANAGEMENT_ROUTES: readonly ManagementRoute[] = [ { method: "GET", path: "/api/providers/keychain", module: "server/management/oauth-account-routes", mutates: false }, { method: "POST", path: "/api/providers/keychain", module: "server/management/oauth-account-routes", mutates: true }, { method: "PATCH", path: "/api/keys", module: "server/management/oauth-account-routes", mutates: true }, - { method: "PATCH", path: "/api/oauth/accounts/pool", module: "server/management/oauth-account-routes", mutates: true }, + { method: "PATCH", path: "/api/oauth/accounts/pool", module: "server/management/oauth-account-routes", mutates: true, go: { relay: "signed", volatileFields: [] } }, { method: "POST", path: "/api/keys", module: "server/management/oauth-account-routes", mutates: true }, { method: "POST", path: "/api/keys/rotate", module: "server/management/oauth-account-routes", mutates: true }, { method: "POST", path: "/api/keys/rotate/commit", module: "server/management/oauth-account-routes", mutates: true }, - { method: "POST", path: "/api/oauth/accounts/clear-cooldown", module: "server/management/oauth-account-routes", mutates: true }, + { method: "POST", path: "/api/oauth/accounts/clear-cooldown", module: "server/management/oauth-account-routes", mutates: true, go: { relay: "signed", volatileFields: [] } }, { method: "POST", path: "/api/oauth/accounts/import", module: "server/management/oauth-account-routes", mutates: true }, { method: "POST", path: "/api/oauth/login", module: "server/management/oauth-account-routes", mutates: true }, { method: "POST", path: "/api/oauth/login/cancel", module: "server/management/oauth-account-routes", mutates: true }, { method: "POST", path: "/api/oauth/login/code", module: "server/management/oauth-account-routes", mutates: true }, { method: "POST", path: "/api/oauth/logout", module: "server/management/oauth-account-routes", mutates: true }, { method: "POST", path: "/api/providers/keys", module: "server/management/oauth-account-routes", mutates: true }, - { method: "PUT", path: "/api/oauth/accounts/active", module: "server/management/oauth-account-routes", mutates: true }, + { method: "PUT", path: "/api/oauth/accounts/active", module: "server/management/oauth-account-routes", mutates: true, go: { relay: "signed", volatileFields: [] } }, { method: "PUT", path: "/api/oauth/accounts/alias", module: "server/management/oauth-account-routes", mutates: true }, - { method: "PUT", path: "/api/oauth/accounts/pool", module: "server/management/oauth-account-routes", mutates: true }, + { method: "PUT", path: "/api/oauth/accounts/pool", module: "server/management/oauth-account-routes", mutates: true, go: { relay: "signed", volatileFields: [] } }, { method: "PUT", path: "/api/providers/keys/active", module: "server/management/oauth-account-routes", mutates: true }, { method: "PUT", path: "/api/providers/keys/alias", module: "server/management/oauth-account-routes", mutates: true }, // server/management/provider-routes @@ -408,15 +411,14 @@ export const MANAGEMENT_ROUTES: readonly ManagementRoute[] = [ ]; /** - * The declared Go-owned surface (ADR-0008): exactly the read routes whose - * `go` marker is flipped. This is the single data source the forwarding branch - * and the differential oracle read, so migrating another read route is a - * marker flip here plus the matching Go handler and oracle coverage -- never a - * second dispatch edit. Read-only by construction: the write arm of the union - * cannot carry a `go` declaration, and this filter re-checks at runtime. + * The declared Go-owned surface (ADR-0008): exactly the routes whose `go` + * marker is flipped. This is the single data source the forwarding branch and + * differential oracle read, so migration is a marker flip here plus matching + * Go handler and oracle coverage -- never a second dispatch edit. Write routes + * must declare the signed-relay arm of the union. */ export const GO_OWNED_MANAGEMENT_ROUTES: readonly GoOwnedManagementRoute[] = MANAGEMENT_ROUTES.filter( - (route): route is GoOwnedManagementRoute => route.mutates === false && route.go !== undefined, + (route): route is GoOwnedManagementRoute => route.go !== undefined, ); /** diff --git a/tests/go-ownership-plumbing.test.ts b/tests/go-ownership-plumbing.test.ts index a725f93cea..cdc4319187 100644 --- a/tests/go-ownership-plumbing.test.ts +++ b/tests/go-ownership-plumbing.test.ts @@ -16,7 +16,7 @@ import { resetGoOwnedRouteForwarderForTests, setGoOwnedRouteForwarder, } from "../src/server/go-sidecar-slot"; -import { resetGoSidecarForTests } from "../src/server/go-sidecar"; +import { goSidecarRelayHeaders, resetGoSidecarForTests } from "../src/server/go-sidecar"; import { removeTreeWithRetry } from "./helpers/remove-tree"; /** @@ -121,11 +121,11 @@ async function getJson(token: string, server: { url: URL }, pathname: string): P } // --------------------------------------------------------------------------- -// 1. Registry invariants: the marker is typed read-only and volatile is declared. +// 1. Registry invariants: reads and writes have distinct ownership declarations. // --------------------------------------------------------------------------- -describe("ADR-0008 ownership markers are typed read/write (ticket #14)", () => { - test("the declared Go-owned surface includes the ticket #20 quota route", () => { +describe("ADR-0008 ownership markers are typed read/write", () => { + test("the declared Go-owned surface includes bounded write batches", () => { // Pin the migrated set so an accidental marker flip on another read route // fails here instead of silently changing what the proxy serves. Adding a // real migration updates this list deliberately. Health reports the serving @@ -135,9 +135,18 @@ describe("ADR-0008 ownership markers are typed read/write (ticket #14)", () => { // oracle compares their bytes with no normalisation at all. const byPath = new Map(GO_OWNED_MANAGEMENT_ROUTES.map(r => [r.path, r])); expect([...byPath.keys()].sort()).toEqual([ + "/api/codex-auth/accounts/clear-cooldown", + "/api/codex-auth/active", + "/api/codex-auth/pool-strategy", + "/api/codex-auth/reset-credits/consume", "/api/custom-models", + "/api/oauth/accounts/active", + "/api/oauth/accounts/clear-cooldown", + "/api/oauth/accounts/pool", "/api/provider-quotas", + "/api/settings", "/api/shadow-call-settings", + "/api/sidecar-settings", "/api/system/health", ]); const health = byPath.get("/api/system/health")!; @@ -145,7 +154,9 @@ describe("ADR-0008 ownership markers are typed read/write (ticket #14)", () => { expect(health.mutates).toBe(false); expect(health.module).toBe("server/management/system-routes"); expect(health.go.volatileFields).toEqual(["pid", "uptime"]); - const shadowCall = byPath.get("/api/shadow-call-settings")!; + const shadowCall = GO_OWNED_MANAGEMENT_ROUTES.find(route => ( + route.method === "GET" && route.path === "/api/shadow-call-settings" + ))!; expect(shadowCall.method).toBe("GET"); expect(shadowCall.mutates).toBe(false); expect(shadowCall.module).toBe("server/management/config-routes"); @@ -160,23 +171,33 @@ describe("ADR-0008 ownership markers are typed read/write (ticket #14)", () => { expect(providerQuotas.mutates).toBe(false); expect(providerQuotas.module).toBe("server/management/provider-routes"); expect(providerQuotas.go.volatileFields).toEqual(["generatedAt"]); + const writes = GO_OWNED_MANAGEMENT_ROUTES.filter(route => route.mutates); + expect(writes.map(route => `${route.method} ${route.path}`).sort()).toEqual([ + "PATCH /api/codex-auth/pool-strategy", + "PATCH /api/oauth/accounts/pool", + "POST /api/codex-auth/accounts/clear-cooldown", + "POST /api/codex-auth/reset-credits/consume", + "POST /api/oauth/accounts/clear-cooldown", + "PUT /api/codex-auth/active", + "PUT /api/codex-auth/pool-strategy", + "PUT /api/oauth/accounts/active", + "PUT /api/oauth/accounts/pool", + "PUT /api/settings", + "PUT /api/shadow-call-settings", + "PUT /api/sidecar-settings", + ]); + for (const route of writes) expect(route.go).toEqual({ relay: "signed", volatileFields: [] }); }); - test("no write route can be Go-owned: runtime re-check of the union's read-only arm", () => { - // The discriminated union in route-registry.ts already refuses `go` on a - // write route at compile time. This is the runtime belt-and-braces check: - // it re-derives the marker set from MANAGEMENT_ROUTES and compares it with - // the exported view, so a cast or an array-level workaround cannot drift. + test("the derived surface matches every marker and writes require a signed relay", () => { const marked = MANAGEMENT_ROUTES.filter( - (r): r is (typeof GO_OWNED_MANAGEMENT_ROUTES)[number] => r.mutates === false && r.go !== undefined, + (r): r is (typeof GO_OWNED_MANAGEMENT_ROUTES)[number] => r.go !== undefined, ); expect(marked).toEqual(GO_OWNED_MANAGEMENT_ROUTES); for (const route of marked) { - expect(route.mutates).toBe(false); + if (route.mutates) expect(route.go).toHaveProperty("relay", "signed"); + else expect(route.go).not.toHaveProperty("relay"); } - // And explicitly: no mutating route anywhere in the table declares Go ownership. - const writesWithGo = MANAGEMENT_ROUTES.filter(r => r.mutates === true && "go" in r); - expect(writesWithGo).toEqual([]); }); test("every Go-owned route declares a duplicate-free volatile set (empty = strict byte equality)", () => { @@ -202,7 +223,7 @@ describe("ADR-0008 ownership markers are typed read/write (ticket #14)", () => { const shadowCall = GO_OWNED_MANAGEMENT_ROUTES.find(r => r.path === "/api/shadow-call-settings"); expect(shadowCall).toBeDefined(); expect(findGoOwnedManagementRoute("GET", "/api/shadow-call-settings")).toBe(shadowCall); - expect(findGoOwnedManagementRoute("PUT", "/api/shadow-call-settings")).toBeUndefined(); + expect(findGoOwnedManagementRoute("PUT", "/api/shadow-call-settings")).toBeDefined(); expect(findGoOwnedManagementRoute("GET", "/api/shadow-call-settings/")).toBeUndefined(); const customModels = GO_OWNED_MANAGEMENT_ROUTES.find(r => r.path === "/api/custom-models"); expect(customModels).toBeDefined(); @@ -211,6 +232,8 @@ describe("ADR-0008 ownership markers are typed read/write (ticket #14)", () => { expect(findGoOwnedManagementRoute("GET", "/api/custom-models/")).toBeUndefined(); expect(findGoOwnedManagementRoute("GET", "/api/provider-quotas")).toBeDefined(); expect(findGoOwnedManagementRoute("POST", "/api/provider-quotas")).toBeUndefined(); + expect(findGoOwnedManagementRoute("PUT", "/api/settings")).toBeDefined(); + expect(findGoOwnedManagementRoute("GET", "/api/settings")).toBeUndefined(); }); test("the forwarding branch in management-api.ts names no route of its own", () => { @@ -236,6 +259,28 @@ describe("ADR-0008 ownership markers are typed read/write (ticket #14)", () => { // --------------------------------------------------------------------------- describe("single forwarding branch serves declared Go-owned routes (ticket #14)", () => { + test("supervisor relay headers retain only signed claims and content type", () => { + const request = new Request("http://localhost/api/settings", { + method: "PUT", + headers: { + authorization: "Bearer browser-secret", + cookie: "session=browser", + "content-type": "application/json", + }, + body: "{}", + }); + const headers = goSidecarRelayHeaders(request, "child-capability", { + "x-ocx-go-relay-principal": "admin-token", + "x-ocx-go-relay-signature": "supervisor-signature", + }); + expect(headers.get("x-ocx-go-relay-principal")).toBe("admin-token"); + expect(headers.get("x-ocx-go-relay-signature")).toBe("supervisor-signature"); + expect(headers.get("x-ocx-go-sidecar-request")).toBe("child-capability"); + expect(headers.get("content-type")).toBe("application/json"); + expect(headers.get("authorization")).toBeNull(); + expect(headers.get("cookie")).toBeNull(); + }); + runFixtureTest("a registered forwarder answers the declared route and nothing else", async (token) => { const calls: string[] = []; const fakeBody = JSON.stringify({ @@ -245,8 +290,8 @@ describe("single forwarding branch serves declared Go-owned routes (ticket #14)" uptime: 1, pid: 987654, }); - const detach = setGoOwnedRouteForwarder(async (method, pathAndSearch) => { - calls.push(`${method} ${pathAndSearch}`); + const detach = setGoOwnedRouteForwarder(async (request, pathAndSearch) => { + calls.push(`${request.method} ${pathAndSearch}`); return new Response(fakeBody, { status: 200, headers: { "content-type": "application/json" }, @@ -277,6 +322,36 @@ describe("single forwarding branch serves declared Go-owned routes (ticket #14)" expect(hasGoOwnedRouteForwarder()).toBe(false); }); + runFixtureTest("a declared write forwards a cloned request body and admitted principal", async (token) => { + const calls: Array<{ method: string; path: string; body: string; principal: string | undefined }> = []; + const detach = setGoOwnedRouteForwarder(async (request, pathAndSearch, principal) => { + calls.push({ method: request.method, path: pathAndSearch, body: await request.text(), principal }); + return Response.json({ forwarded: true }); + }); + try { + const server = startServer(0); + try { + const response = await fetch(new URL("/api/settings", server.url), { + method: "PUT", + headers: { "content-type": "application/json", "x-opencodex-api-key": token }, + body: JSON.stringify({ streamMode: "passthrough" }), + }); + expect(response.status).toBe(200); + expect(await response.json()).toEqual({ forwarded: true }); + expect(calls).toEqual([{ + method: "PUT", + path: "/api/settings", + body: JSON.stringify({ streamMode: "passthrough" }), + principal: "admin-token", + }]); + } finally { + await server.stop(true); + } + } finally { + detach(); + } + }); + runFixtureTest("a forwarder returning null falls back to the in-process handler", async (token) => { // This is the supervision-blip contract: when the sidecar is attached but // unreachable, the route answers from TypeScript exactly as without Go. diff --git a/tests/go-sidecar-parity.test.ts b/tests/go-sidecar-parity.test.ts index 45e1fb8734..c2e4759fcc 100644 --- a/tests/go-sidecar-parity.test.ts +++ b/tests/go-sidecar-parity.test.ts @@ -1,10 +1,11 @@ import { describe, expect, test } from "bun:test"; -import { existsSync, mkdtempSync } from "node:fs"; +import { existsSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { dirname, join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import { SERVER_BUDGET_MS } from "./helpers/test-budget"; import { saveConfig } from "../src/config"; +import { getConfigPath } from "../src/config/paths"; import { startServer } from "../src/server"; import { VERSION } from "../src/server/management-api"; import { GO_OWNED_MANAGEMENT_ROUTES } from "../src/server/management/route-registry"; @@ -151,6 +152,26 @@ async function captureHealth(server: { url: URL }, token: string): Promise { + const response = await fetch(new URL(pathname, server.url), { + method, + headers: { "x-opencodex-api-key": token, "content-type": "application/json" }, + body: JSON.stringify(body), + }); + return { + status: response.status, + contentType: response.headers.get("content-type"), + retryAfter: response.headers.get("retry-after"), + body: await response.text(), + }; +} + const previousHome = process.env.OPENCODEX_HOME; const previousDataToken = process.env.OPENCODEX_API_AUTH_TOKEN; const previousAdminToken = process.env.OPENCODEX_ADMIN_AUTH_TOKEN; @@ -317,6 +338,119 @@ describe.skipIf(!goAvailable || sidecarBinary === null)("ocx-sidecar differentia } }); + runFixtureTest("shadow-call write has a state-reset differential oracle", async (token) => { + // Run the exact mutation first through TypeScript, then restore the initial + // bytes and run it through the Go public surface. This compares status, + // response headers/body, and the post-write config bytes rather than + // treating a validation-only no-op as evidence of write parity. + const initial = readFileSync(getConfigPath()); + const tsServer = startServer(0); + let tsWrite: Awaited>; + let tsPostState: Buffer; + try { + tsWrite = await captureMutation(tsServer, token, "PUT", "/api/shadow-call-settings", { enabled: false }); + tsPostState = readFileSync(getConfigPath()); + } finally { + await tsServer.stop(true); + } + + writeFileSync(getConfigPath(), initial); + process.env[GO_SIDECAR_BIN_ENV] = sidecarBinary!; + const goServer = startServer(0); + try { + await waitFor(() => activeGoSidecarBaseUrl(), 15_000); + const goWrite = await captureMutation(goServer, token, "PUT", "/api/shadow-call-settings", { enabled: false }); + const goPostState = readFileSync(getConfigPath()); + expect(goWrite).toEqual(tsWrite!); + expect(goPostState.equals(tsPostState!)).toBe(true); + } finally { + await goServer.stop(true); + } + }); + + runFixtureTest("settings write has a state-reset differential oracle", async (token) => { + const initial = readFileSync(getConfigPath()); + const tsServer = startServer(0); + let tsWrite: Awaited>; + let tsPostState: Buffer; + try { + tsWrite = await captureMutation(tsServer, token, "PUT", "/api/settings", { streamMode: "eager-relay" }); + tsPostState = readFileSync(getConfigPath()); + } finally { + await tsServer.stop(true); + } + writeFileSync(getConfigPath(), initial); + process.env[GO_SIDECAR_BIN_ENV] = sidecarBinary!; + const goServer = startServer(0); + try { + await waitFor(() => activeGoSidecarBaseUrl(), 15_000); + const goWrite = await captureMutation(goServer, token, "PUT", "/api/settings", { streamMode: "eager-relay" }); + expect(goWrite).toEqual(tsWrite!); + expect(readFileSync(getConfigPath()).equals(tsPostState!)).toBe(true); + } finally { + await goServer.stop(true); + } + }); + + runFixtureTest("sidecar-settings write has a state-reset differential oracle", async (token) => { + const initial = readFileSync(getConfigPath()); + const tsServer = startServer(0); + let tsWrite: Awaited>; + let tsPostState: Buffer; + try { + tsWrite = await captureMutation(tsServer, token, "PUT", "/api/sidecar-settings", { + webSearch: { streamRoutedModelOutput: true }, + }); + tsPostState = readFileSync(getConfigPath()); + } finally { + await tsServer.stop(true); + } + writeFileSync(getConfigPath(), initial); + process.env[GO_SIDECAR_BIN_ENV] = sidecarBinary!; + const goServer = startServer(0); + try { + await waitFor(() => activeGoSidecarBaseUrl(), 15_000); + const goWrite = await captureMutation(goServer, token, "PUT", "/api/sidecar-settings", { + webSearch: { streamRoutedModelOutput: true }, + }); + expect(goWrite).toEqual(tsWrite!); + expect(readFileSync(getConfigPath()).equals(tsPostState!)).toBe(true); + } finally { + await goServer.stop(true); + } + }); + + runFixtureTest("quota validation and account-pool state-reset vectors match through Go", async (token) => { + const initial = readFileSync(getConfigPath()); + const tsServer = startServer(0); + let quotaTs: Awaited>; + let poolTs: Awaited>; + let poolPostState: Buffer; + try { + quotaTs = await captureMutation(tsServer, token, "POST", "/api/codex-auth/reset-credits/consume", {}); + poolTs = await captureMutation(tsServer, token, "PUT", "/api/oauth/accounts/pool", { provider: "anthropic", enabled: true, strategy: "round-robin" }); + poolPostState = readFileSync(getConfigPath()); + } finally { + await tsServer.stop(true); + } + writeFileSync(getConfigPath(), initial); + process.env[GO_SIDECAR_BIN_ENV] = sidecarBinary!; + const goServer = startServer(0); + try { + await waitFor(() => activeGoSidecarBaseUrl(), 15_000); + expect(await captureMutation(goServer, token, "POST", "/api/codex-auth/reset-credits/consume", {})).toEqual(quotaTs!); + expect(await captureMutation(goServer, token, "PUT", "/api/oauth/accounts/pool", { provider: "anthropic", enabled: true, strategy: "round-robin" })).toEqual(poolTs!); + expect(readFileSync(getConfigPath()).equals(poolPostState!)).toBe(true); + + const beforeFailure = readFileSync(getConfigPath()); + const failed = await captureMutation(goServer, token, "PUT", "/api/oauth/accounts/pool", { provider: "anthropic", enabled: false, strategy: "invalid" }); + expect(failed.status).toBe(400); + expect(readFileSync(getConfigPath()).equals(beforeFailure)).toBe(true); + } finally { + await goServer.stop(true); + } + }); + runFixtureTest("an unexpected sidecar exit deregisters the forwarder and health falls back in-process", async (token) => { // #11: a crash must surface, not fall silent. The supervisor deregisters the // forwarder on an unexpected child exit, so the next health response flips diff --git a/tests/go-sidecar-write-relay.test.ts b/tests/go-sidecar-write-relay.test.ts new file mode 100644 index 0000000000..6eaeeac6ce --- /dev/null +++ b/tests/go-sidecar-write-relay.test.ts @@ -0,0 +1,143 @@ +import { describe, expect, test } from "bun:test"; +import { + GO_SIDECAR_BRIDGE_HEADER, + GO_SIDECAR_WRITE_BRIDGE_PATH, + GO_SIDECAR_WRITE_RELAY_EXPIRES_AT_HEADER, + GO_SIDECAR_WRITE_RELAY_METHOD_HEADER, + GO_SIDECAR_WRITE_RELAY_NONCE_HEADER, + GO_SIDECAR_WRITE_RELAY_PATH_HEADER, + GO_SIDECAR_WRITE_RELAY_PRINCIPAL_HEADER, + GO_SIDECAR_WRITE_RELAY_PROOF_HEADER, + createGoSidecarWriteRelay, + createGoSidecarWriteRelayHeaders, + createGoSidecarWriteRelayNonce, + createGoSidecarWriteRelayProof, +} from "../src/server/go-sidecar-write-relay"; + +const BRIDGE_TOKEN = "c8cb2a09-6c5e-4d5a-9252-421cb8c3e698"; +const RELAY_SECRET = "b".repeat(43); +const NOW = 1_800_000_000_000; +const ROUTE = "/api/settings"; +const BODY = new TextEncoder().encode('{"streamMode":"eager-relay"}'); + +function request(options: Partial<{ bridgeToken: string; nonce: string; method: string; path: string; principal: string; expiresAt: number; proof: string; body: Uint8Array }> = {}): Request { + const nonce = options.nonce ?? createGoSidecarWriteRelayNonce(); + const method = options.method ?? "PUT"; + const path = options.path ?? ROUTE; + const principal = options.principal ?? "admin-token"; + const expiresAt = options.expiresAt ?? NOW + 1_000; + const body = options.body ?? BODY; + const proof = options.proof ?? createGoSidecarWriteRelayProof( + RELAY_SECRET, + { nonce, principal: principal as "admin-token", method: method as "PUT", path, expiresAt }, + body, + )!; + return new Request("http://127.0.0.1" + GO_SIDECAR_WRITE_BRIDGE_PATH, { + method: "PUT", + headers: { + [GO_SIDECAR_BRIDGE_HEADER]: options.bridgeToken ?? BRIDGE_TOKEN, + [GO_SIDECAR_WRITE_RELAY_NONCE_HEADER]: nonce, + [GO_SIDECAR_WRITE_RELAY_PRINCIPAL_HEADER]: principal, + [GO_SIDECAR_WRITE_RELAY_METHOD_HEADER]: method, + [GO_SIDECAR_WRITE_RELAY_PATH_HEADER]: path, + [GO_SIDECAR_WRITE_RELAY_EXPIRES_AT_HEADER]: String(expiresAt), + [GO_SIDECAR_WRITE_RELAY_PROOF_HEADER]: proof, + "content-type": "application/json", + }, + body, + }); +} + +function relay(calls: Array<{ request: Request; principal: string }> = []) { + return createGoSidecarWriteRelay({ + bridgeToken: BRIDGE_TOKEN, + relaySecret: RELAY_SECRET, + now: () => NOW, + dispatchLegacy: async (legacyRequest, _url, principal) => { + calls.push({ request: legacyRequest, principal }); + return new Response("legacy-body", { status: 409, headers: { "content-type": "application/json", "retry-after": "1" } }); + }, + })!; +} + +describe("Go sidecar private write relay", () => { + test("mints a fresh body-bound claim only for an admitted principal", async () => { + const headers = createGoSidecarWriteRelayHeaders( + RELAY_SECRET, + "admin-token", + { method: "PUT", pathname: ROUTE, body: BODY }, + () => NOW, + ); + expect(headers).not.toBeNull(); + expect(headers!.get(GO_SIDECAR_WRITE_RELAY_METHOD_HEADER)).toBe("PUT"); + expect(headers!.get(GO_SIDECAR_WRITE_RELAY_PATH_HEADER)).toBe(ROUTE); + const bridge = relay(); + const signed = new Request("http://127.0.0.1" + GO_SIDECAR_WRITE_BRIDGE_PATH, { + method: "PUT", + headers: { [GO_SIDECAR_BRIDGE_HEADER]: BRIDGE_TOKEN, ...Object.fromEntries(headers!) }, + body: BODY, + }); + expect((await bridge.handle(signed, new URL(signed.url))).status).toBe(409); + expect(createGoSidecarWriteRelayHeaders( + RELAY_SECRET, + undefined, + { method: "PUT", pathname: ROUTE, body: BODY }, + () => NOW, + )).toBeNull(); + }); + + test("accepts the UUID-shaped per-sidecar capability but requires a separate HMAC secret", () => { + expect(createGoSidecarWriteRelay({ + bridgeToken: BRIDGE_TOKEN, + relaySecret: RELAY_SECRET, + dispatchLegacy: async () => null, + })).not.toBeNull(); + expect(createGoSidecarWriteRelay({ + bridgeToken: "", + relaySecret: RELAY_SECRET, + dispatchLegacy: async () => null, + })).toBeNull(); + }); + + test("requires bridge capability and one body-bound signed claim before legacy dispatch", async () => { + const calls: Array<{ request: Request; principal: string }> = []; + const bridge = relay(calls); + const response = await bridge.handle(request(), new URL("http://127.0.0.1" + GO_SIDECAR_WRITE_BRIDGE_PATH)); + expect(response.status).toBe(409); + expect(response.headers.get("retry-after")).toBe("1"); + expect(await response.text()).toBe("legacy-body"); + expect(calls).toHaveLength(1); + expect(calls[0]!.principal).toBe("admin-token"); + expect(calls[0]!.request.method).toBe("PUT"); + expect(new URL(calls[0]!.request.url).pathname).toBe(ROUTE); + expect(await calls[0]!.request.text()).toBe(new TextDecoder().decode(BODY)); + }); + + test("rejects wrong bridge capability, undeclared writes, altered bodies, expired claims, and replay", async () => { + const calls: Array<{ request: Request; principal: string }> = []; + const bridge = relay(calls); + const nonce = createGoSidecarWriteRelayNonce(); + const valid = request({ nonce }); + const staleProofNonce = createGoSidecarWriteRelayNonce(); + const altered = request({ + nonce: createGoSidecarWriteRelayNonce(), + body: new TextEncoder().encode('{"streamMode":"auto"}'), + proof: createGoSidecarWriteRelayProof( + RELAY_SECRET, + { nonce: staleProofNonce, principal: "admin-token", method: "PUT", path: ROUTE, expiresAt: NOW + 1_000 }, + BODY, + )!, + }); + for (const candidate of [ + request({ bridgeToken: "c".repeat(43) }), + request({ path: "/api/not-declared" }), + request({ expiresAt: NOW }), + altered, + ]) { + expect((await bridge.handle(candidate, new URL(candidate.url))).status).toBe(404); + } + expect((await bridge.handle(valid, new URL(valid.url))).status).toBe(409); + expect((await bridge.handle(request({ nonce }), new URL("http://127.0.0.1" + GO_SIDECAR_WRITE_BRIDGE_PATH))).status).toBe(404); + expect(calls).toHaveLength(1); + }); +}); diff --git a/tests/local-management-direct-transport.test.ts b/tests/local-management-direct-transport.test.ts index 8a6b36062b..16f093d311 100644 --- a/tests/local-management-direct-transport.test.ts +++ b/tests/local-management-direct-transport.test.ts @@ -55,6 +55,40 @@ describe("local management direct transport", () => { await server.stop(true); } }); + + test("sends a signed JSON PUT directly with its exact content length", async () => { + let observed: { method: string; contentLength: string | null; relay: string | null; body: string } | null = null; + const server = Bun.serve({ + hostname: "127.0.0.1", + port: 0, + async fetch(request) { + observed = { + method: request.method, + contentLength: request.headers.get("content-length"), + relay: request.headers.get("x-ocx-go-relay-signature"), + body: await request.text(), + }; + return Response.json({ ok: true }); + }, + }); + const body = JSON.stringify({ streamMode: "passthrough" }); + try { + const response = await directLocalHttpFetch(`http://127.0.0.1:${server.port}/api/settings`, { + method: "PUT", + headers: { "content-type": "application/json", "x-ocx-go-relay-signature": "supervisor-signature" }, + body, + }); + expect(response.status).toBe(200); + expect(observed).toEqual({ + method: "PUT", + contentLength: String(Buffer.byteLength(body)), + relay: "supervisor-signature", + body, + }); + } finally { + await server.stop(true); + } + }); test("preserves an AbortError for an already-cancelled request", async () => { const controller = new AbortController(); controller.abort(); From 353995133e62a7521d8d8ae2578b4a0a0c0fb213 Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Sun, 6 Sep 2026 12:46:47 +0800 Subject: [PATCH 014/165] docs(go): record the #24 hot-path seam design (devlog 034) --- .../034_hot_path_seam.md | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 devlog/_plan/260905_go_sidecar_takeover/034_hot_path_seam.md diff --git a/devlog/_plan/260905_go_sidecar_takeover/034_hot_path_seam.md b/devlog/_plan/260905_go_sidecar_takeover/034_hot_path_seam.md new file mode 100644 index 0000000000..bee7eb6c0b --- /dev/null +++ b/devlog/_plan/260905_go_sidecar_takeover/034_hot_path_seam.md @@ -0,0 +1,98 @@ +# 034 — Ticket #24: hot-path seam + streaming differential harness + +Unit: `260905_go_sidecar_takeover` +Date: 2026-09-06 +Status: in progress on `fix/ticket-24-hotpath-seam` (dev-go + 1) +Ticket: [#24](https://github.com/waxiangzi/opencodex/issues/24) (spec #4: hot-path seam + streaming differential harness) +Blocked-by (#13/#16): closed — differential-oracle infrastructure and shared Go config parsing landed on `dev-go`. + +## Scope discipline + +#24 is the **substrate** ticket of the hot-path increment (#4), exactly as #13 +was the substrate of the management read surface. It must NOT relay any real +provider traffic: the single-provider non-streaming relay (#27) and the SSE +streaming relay with frame parity (#29) are blocked *by* this ticket and own +that work. #24 therefore ships two things and nothing more: + +1. A declared **hot-path seam** in the sidecar that a later ticket replaces + provider-side without touching the front door again. +2. A **streaming differential harness** that compares ordered SSE frame + sequences across two live servers and normalises only declared volatile + fields. + +## Design decisions + +### 1. The seam is the same ownership pattern as the management surface, on the data plane + +- `src/server/hot-path-seam.ts` holds the DATA (one declared seam route: + `POST /v1/responses`), the independent activation gate + (`OPENCODEX_GO_HOTPATH_SEAM`), and a core-owned forwarder slot shaped like + `go-sidecar-slot.ts`. The route registry for the data plane is deliberately a + separate module from `management/route-registry.ts`: #4 user story 10 gates + the hot path separately from the management surface, and the management + registry's types (`mutates`, session exemptions) do not apply to `/v1/*`. +- `go-sidecar.ts` registers the hot-path forwarder at activation only when the + seam env is set — same ready-line handoff, same child-exit deregistration. + A sidecar attached without the seam env forwards nothing and `/v1/responses` + stays 100% in-process: the management surface can keep being migrated while + the data plane is untouched, and vice versa (independent rollback, #4 story 13). +- Default install: no sidecar, no seam env → zero behaviour change, and the + seam modules sit behind the same optional-subsystem rule as the Lab. + +### 2. The sidecar's hot-path seam serves `/v1/responses` from the TS oracle until a provider relay lands + +The seam handler in the sidecar (`go/internal/sidecar`) owns the *public +surface*: it authenticates the parent forward (request token), bounds the body, +and streams the response back. Its upstream today is a **private parent bridge** +(`/__ocx_go_sidecar/responses`) that runs the real in-process +`handleResponses` pipeline — byte-identical to a direct request because it IS +the same pipeline. #27/#29 replace the bridge as the seam's source per provider +without touching the front-door gate, the ownership data, or the harness. + +Streaming contract: status code, `content-type` and the body are relayed +byte-for-byte in stream order. The Go side must never buffer or re-frame the +stream: the harness's whole point is that a dropped, reordered or duplicated +SSE frame fails the differential. + +### 3. The bridge is authenticated with a body-bound parent claim, not client credentials + +The front door resolves data-plane admission before the seam gate (same +`resolveResponsesApiAuth` as the direct branch). The client credential never +crosses the process boundary; instead the front door mints a short-lived HMAC +claim over `admission | method | path | expiry | sha256(body)` using the same +per-activation write-relay secret already inherited by the sidecar, and the +bridge verifies it with a bounded replay store. The threat model is the +established sidecar one: a local process that can read the sidecar's environment +is already as privileged as the proxy process itself. + +### 4. Direct branch stays the oracle + +Server A (no sidecar) and Server B (sidecar + seam env) both talk to the same +deterministic fixture upstream. The harness captures the client-visible SSE +frame sequence from each and asserts ordered identity after applying the +declared volatile set — for the first fixture, an explicit empty set (raw byte +identity), with per-request JSON paths added only if a live run proves them +legitimately request-scoped. + +## Security boundary + +- The bridge endpoint verifies the bridge token AND the parent claim; it never + accepts an admin token or a client API key as a substitute (mirrors the + `provider-quotas` and write-relay bridge endpoints). +- The sidecar seam route answers 404 unless the parent request token is + present: the sidecar never invents a public data-plane listener of its own. +- No client credential, cookie, or browser header is forwarded past the front + door in either direction. + +## Proof + +- Go unit tests: seam route auth, body bound, bridge URL validation, streaming + passthrough of a synthetic fixture stream. +- `tests/go-hotpath-seam.test.ts`: differential oracle across two real servers + (skip-if-no-Go, same guard as `go-sidecar-parity.test.ts`), comparing ordered + SSE frames; a mutated fixture frame must fail. +- Existing suites stay green; `privacy:scan` stays green. + +## Delivery notes (filled in at close) + +- (pending) From f8ab6d51043f6df77eb4bd1ee51b375ef6557e7f Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Sun, 6 Sep 2026 12:46:53 +0800 Subject: [PATCH 015/165] =?UTF-8?q?feat(go):=20ticket=20#24=20=E2=80=94=20?= =?UTF-8?q?data-plane=20hot-path=20seam=20+=20streaming=20differential=20h?= =?UTF-8?q?arness?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Go sidecar now owns the public POST /v1/responses surface behind the same optional-subsystem pattern as the management reads: a declared seam route, an independent OPENCODEX_GO_HOTPATH_SEAM gate (spec #4 story 10), and a private parent bridge that runs the in-process responses pipeline for one admitted request. The front door mints a body-bound HMAC claim over the admission so a client credential never crosses the process boundary; the sidecar relays the claim verbatim and streams the bridge response byte-for-byte in frame order. tests/go-hotpath-seam.test.ts is the streaming differential: two live servers (in-process oracle vs seam) against the same deterministic fixture upstream must agree on the ordered SSE frame sequence with an explicitly declared volatile set (per-request trace header, Date, server CORS echo; body volatile set empty). go/internal/sidecar/hotpath_test.go pins seam auth, body bound and byte-for-byte chunked stream relay. Default installs and seam-off sidecar installs are unchanged. --- .../034_hot_path_seam.md | 42 ++- go/internal/sidecar/hotpath.go | 168 +++++++++ go/internal/sidecar/hotpath_test.go | 238 ++++++++++++ go/internal/sidecar/sidecar.go | 11 +- src/server/go-sidecar.ts | 62 ++++ src/server/hot-path-seam.ts | 292 +++++++++++++++ src/server/index.ts | 112 +++++- tests/go-hotpath-seam.test.ts | 340 ++++++++++++++++++ tests/hot-path-seam.test.ts | 155 ++++++++ 9 files changed, 1414 insertions(+), 6 deletions(-) create mode 100644 go/internal/sidecar/hotpath.go create mode 100644 go/internal/sidecar/hotpath_test.go create mode 100644 src/server/hot-path-seam.ts create mode 100644 tests/go-hotpath-seam.test.ts create mode 100644 tests/hot-path-seam.test.ts diff --git a/devlog/_plan/260905_go_sidecar_takeover/034_hot_path_seam.md b/devlog/_plan/260905_go_sidecar_takeover/034_hot_path_seam.md index bee7eb6c0b..1537b09756 100644 --- a/devlog/_plan/260905_go_sidecar_takeover/034_hot_path_seam.md +++ b/devlog/_plan/260905_go_sidecar_takeover/034_hot_path_seam.md @@ -2,7 +2,7 @@ Unit: `260905_go_sidecar_takeover` Date: 2026-09-06 -Status: in progress on `fix/ticket-24-hotpath-seam` (dev-go + 1) +Status: implemented on `fix/ticket-24-hotpath-seam` (dev-go + 2) Ticket: [#24](https://github.com/waxiangzi/opencodex/issues/24) (spec #4: hot-path seam + streaming differential harness) Blocked-by (#13/#16): closed — differential-oracle infrastructure and shared Go config parsing landed on `dev-go`. @@ -95,4 +95,42 @@ legitimately request-scoped. ## Delivery notes (filled in at close) -- (pending) +Delivered on `fix/ticket-24-hotpath-seam` (two commits on top of `dev-go`): + +- `src/server/hot-path-seam.ts` — seam data (route + env gate + bridge path), + the body-bound parent claim (mint at the front door, verify at the bridge, + bounded one-use nonce table), and the private bridge object. The Go sidecar + never sees a client credential; it relays the claim verbatim. +- `go/internal/sidecar/hotpath.go` (+ tests) — the sidecar owns + `POST /v1/responses`: parent-token 404 gate, 256MB body bound, bridge URL + loopback validation, and byte-for-byte chunked stream relay with per-chunk + flush (frame order preserved by construction). +- `src/server/index.ts` — bridge endpoint (404 when the seam env is off), the + front-door seam gate inside the existing `/v1/responses` turn (default off; + no in-process fallback after the body read, so a dead seam is a retryable + 502, never a double-executed model call), and activation wiring. +- `src/server/go-sidecar.ts` — data-plane seam attachment state and the + seam forward hop, armed whenever the sidecar is attached and gated per + request by the env, so flipping the env at runtime is honoured. +- `tests/go-hotpath-seam.test.ts` — the streaming differential: two live + servers (in-process oracle vs seam) against one deterministic fixture + upstream must agree on the ordered SSE frame sequence. Declared volatile + set: the per-request trace header, `Date`, and the server CORS origin echo; + the body volatile set is EMPTY (raw frame identity). +- `tests/hot-path-seam.test.ts` — claim/bridge unit coverage incl. replay, + expiry, body-bound proof, oversized body, admission round-trip. + +Acceptance criteria: + +- [x] Hot-path seam exists in the sidecar (Go-owned `POST /v1/responses` + surface with a replaceable bridge source for #27/#29). +- [x] Streaming differential compares ordered frame sequences (frame + extractor + two-server oracle; reorder/drop classes proven non-vacuous). +- [x] Only declared volatile fields normalised (empty body volatile set; + declared header volatile set; the harness asserts the declaration). + +Verification: `go build/vet/test ./...`, `bun run typecheck`, +`privacy:scan`, and the four focused suites above are green. The repository +wide suite was attempted but stalled in this container on unrelated +OAuth/provider-management flakes (all six affected files pass standalone on +both this branch and `dev-go`); no seam-related file failed. diff --git a/go/internal/sidecar/hotpath.go b/go/internal/sidecar/hotpath.go new file mode 100644 index 0000000000..7f970c52a6 --- /dev/null +++ b/go/internal/sidecar/hotpath.go @@ -0,0 +1,168 @@ +package sidecar + +// Data-plane hot-path seam (ticket #24, devlog 034). The sidecar owns the +// public POST /v1/responses surface exactly like it owns the Go-owned +// management read/write routes; until a provider relay lands (#27/#29) its +// stream source is the private parent bridge, which runs the real in-process +// handleResponses pipeline. The seam's job is transport fidelity: status, +// headers and the body must cross the process boundary byte-for-byte and in +// stream order, because the streaming differential oracle compares the +// client-visible SSE frame sequence and fails on a dropped, reordered or +// duplicated frame. + +import ( + "bytes" + "io" + "net/http" + + "github.com/lidge-jun/opencodex/go/internal/managementauth" +) + +const ( + // DataPlaneBridgePath is the private parent endpoint the seam asks to run + // the in-process responses pipeline for one admitted request. + DataPlaneBridgePath = "/__ocx_go_sidecar/responses" + + // The front door resolves data-plane admission before the seam gate and + // mints a short-lived HMAC claim over the admission, method, path, expiry + // and body digest; the sidecar never sees the client credential and only + // relays these headers verbatim to the bridge, which verifies them. + DataPlaneNonceHeader = "X-Ocx-Go-Dataplane-Nonce" + DataPlaneExpiresAtHeader = "X-Ocx-Go-Dataplane-Expires-At" + DataPlaneAdmissionHeader = "X-Ocx-Go-Dataplane-Admission" + DataPlaneProofHeader = "X-Ocx-Go-Dataplane-Proof" + + // Matches src/server/request-decompress.ts MAX_DECOMPRESSED_BODY_BYTES: + // the same body the in-process handler would have accepted must reach the + // bridge. The seam streams rather than buffers, so the bound only caps the + // read, not the memory. + maxDataPlaneBodyBytes = 256 * 1024 * 1024 +) + +// mountDataPlaneSeam registers the hot-path seam route on the sidecar mux. +// The pattern is deliberately NOT method-qualified: an unqualified route lets +// the handler answer 404 for a non-POST request (the same allowlist shape as +// the write-relay routes) instead of letting ServeMux synthesise a 405 that +// would probe the seam's existence. +func mountDataPlaneSeam(mux *http.ServeMux, cfg Config) { + mux.HandleFunc("/v1/responses", func(w http.ResponseWriter, r *http.Request) { + dataPlaneSeam(w, r, cfg) + }) +} + +// dataPlaneSeam relays one admitted POST /v1/responses request to the parent +// bridge and streams the response back untouched. It answers 404 to anything +// that does not carry the parent request token: the sidecar must never invent +// a public data-plane listener of its own, and while the seam is mounted the +// in-process front door remains the only way a request reaches it. +func dataPlaneSeam(w http.ResponseWriter, r *http.Request, cfg Config) { + if cfg.RequestToken == "" || !managementauth.EqualSecret(r.Header.Get(SidecarRequestHeader), cfg.RequestToken) { + http.NotFound(w, r) + return + } + if r.Method != http.MethodPost || r.URL.Path != "/v1/responses" { + http.NotFound(w, r) + return + } + + parent, ok := privateParentBridgeURL(cfg.ParentURL, DataPlaneBridgePath) + if !ok || cfg.BridgeToken == "" { + http.Error(w, "responses bridge unavailable", http.StatusServiceUnavailable) + return + } + + // The bridge verifies the body-bound claim, so the seam must relay the + // body unchanged. Read it with the same ceiling the in-process handler + // enforces (MAX_DECOMPRESSED_BODY_BYTES) and refuse oversized bodies + // here, before any bridge hop. + body, readErr := io.ReadAll(io.LimitReader(r.Body, maxDataPlaneBodyBytes+1)) + if readErr != nil { + http.Error(w, "responses bridge unavailable", http.StatusServiceUnavailable) + return + } + if len(body) > maxDataPlaneBodyBytes { + http.Error(w, "request body too large", http.StatusRequestEntityTooLarge) + return + } + bridgeReq, err := http.NewRequestWithContext(r.Context(), http.MethodPost, parent.String(), bytes.NewReader(body)) + if err != nil { + http.Error(w, "responses bridge unavailable", http.StatusServiceUnavailable) + return + } + bridgeReq.Header.Set(SidecarBridgeHeader, cfg.BridgeToken) + for _, name := range []string{ + DataPlaneNonceHeader, + DataPlaneExpiresAtHeader, + DataPlaneAdmissionHeader, + DataPlaneProofHeader, + } { + if value := r.Header.Get(name); value != "" { + bridgeReq.Header.Set(name, value) + } + } + if contentType := r.Header.Get("Content-Type"); contentType != "" { + bridgeReq.Header.Set("Content-Type", contentType) + } + + bridgeResp, err := dataPlaneBridgeClient().Do(bridgeReq) + if err != nil { + http.Error(w, "responses bridge unavailable", http.StatusServiceUnavailable) + return + } + defer bridgeResp.Body.Close() + + if contentType := bridgeResp.Header.Get("Content-Type"); contentType != "" { + w.Header().Set("Content-Type", contentType) + } + if retryAfter := bridgeResp.Header.Get("Retry-After"); retryAfter != "" { + w.Header().Set("Retry-After", retryAfter) + } + w.WriteHeader(bridgeResp.StatusCode) + if err := streamCopyWithFlush(w, bridgeResp.Body); err != nil { + // The client went away mid-stream (or the bridge did): nothing useful + // can be written now, and a partial body is the transport's normal + // failure mode for an already-started stream. + return + } +} + +// dataPlaneBridgeClient reaches the parent bridge without any total-request +// timeout: a response stream can legitimately run for minutes. Cancellation +// flows through the request context, and the bridge credential must never be +// sent through a system proxy (the parent is a literal IPv4 loopback +// listener), mirroring privateBridgeClient but without the 30s cap. +func dataPlaneBridgeClient() *http.Client { + transport := bridgeTransport() + return &http.Client{ + Transport: transport, + CheckRedirect: func(_ *http.Request, _ []*http.Request) error { + return http.ErrUseLastResponse + }, + } +} + +// streamCopyWithFlush copies src to w, flushing after every write so SSE +// frames reach the front door as they arrive rather than in one trailing +// buffer. A flush per chunk is the transport-fidelity cost the seam exists to +// pay; frame order is preserved by construction (single sequential copy). +func streamCopyWithFlush(w http.ResponseWriter, src io.Reader) error { + flusher, canFlush := w.(http.Flusher) + buf := make([]byte, 32*1024) + for { + n, readErr := src.Read(buf) + if n > 0 { + if _, writeErr := w.Write(buf[:n]); writeErr != nil { + return writeErr + } + if canFlush { + flusher.Flush() + } + } + if readErr != nil { + if readErr == io.EOF { + return nil + } + return readErr + } + } +} diff --git a/go/internal/sidecar/hotpath_test.go b/go/internal/sidecar/hotpath_test.go new file mode 100644 index 0000000000..9adf54b153 --- /dev/null +++ b/go/internal/sidecar/hotpath_test.go @@ -0,0 +1,238 @@ +package sidecar + +import ( + "bytes" + "io" + "net/http" + "net/http/httptest" + "strings" + "testing" +) + +// responsesStreamFixture is a deterministic Responses SSE stream the bridge +// (the TS oracle in production) is expected to relay byte-for-byte. The +// harness compares ordered frames, so the seam must not re-frame, reorder or +// drop anything; the tests below pin that with raw byte identity. +const responsesStreamFixture = "event: response.created\ndata: {\"type\":\"response.created\",\"response\":{\"id\":\"fixture-1\",\"status\":\"in_progress\"}}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"delta\":\"Hel\"}\n\nevent: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"delta\":\"lo\"}\n\nevent: response.completed\ndata: {\"type\":\"response.completed\",\"response\":{\"id\":\"fixture-1\",\"status\":\"completed\"}}\n\n" + +func dataPlaneSeamHandler(t *testing.T, requestToken, bridgeToken, parentURL string) http.Handler { + t.Helper() + return NewHandler(Config{ + Service: "opencodex", + Version: "2.42.0", + ParentURL: parentURL, + BridgeToken: bridgeToken, + RequestToken: requestToken, + WriteRelaySecret: bridgeToken, + }) +} + +func dataPlaneHeaders(requestToken, bridgeToken string, body []byte) http.Header { + headers := make(http.Header) + headers.Set(SidecarRequestHeader, requestToken) + headers.Set("Content-Type", "application/json") + headers.Set(DataPlaneNonceHeader, "nonce-abcdefghijklmnopqrstuvwxyz0123456789-aa") + headers.Set(DataPlaneExpiresAtHeader, "1800000000000") + headers.Set(DataPlaneAdmissionHeader, `{"kind":"environment","source":"x-api-key"}`) + headers.Set(DataPlaneProofHeader, "proof-0123456789abcdefghijklmnopqrstuvwxyzABCDEF") + return headers +} + +func TestDataPlaneSeamRelaysStreamByteForByte(t *testing.T) { + const requestToken = "parent-to-sidecar" + const bridgeToken = "sidecar-to-parent" + const body = `{"model":"fixture","input":"ping","stream":true}` + var gotBridgeMethod, gotBridgePath, gotBridgeToken string + var gotClaimNonce, gotClaimAdmission, gotContentType string + var gotBody []byte + + bridge := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + gotBridgeMethod = r.Method + gotBridgePath = r.URL.Path + gotBridgeToken = r.Header.Get(SidecarBridgeHeader) + gotClaimNonce = r.Header.Get(DataPlaneNonceHeader) + gotClaimAdmission = r.Header.Get(DataPlaneAdmissionHeader) + gotContentType = r.Header.Get("Content-Type") + var readErr error + gotBody, readErr = io.ReadAll(r.Body) + if readErr != nil { + t.Errorf("bridge read body: %v", readErr) + } + w.Header().Set("Content-Type", "text/event-stream") + w.WriteHeader(http.StatusOK) + // Write the fixture in small chunks on purpose: a buffering seam could + // preserve bytes yet reorder delivery. The oracle compares frames, so + // the seam must preserve chunked write order too. + for _, chunk := range strings.SplitAfter(responsesStreamFixture, "\n") { + if _, err := w.Write([]byte(chunk)); err != nil { + t.Errorf("bridge write chunk: %v", err) + } + if f, ok := w.(http.Flusher); ok { + f.Flush() + } + } + })) + defer bridge.Close() + + h := dataPlaneSeamHandler(t, requestToken, bridgeToken, bridge.URL) + req := httptest.NewRequest(http.MethodPost, "/v1/responses", bytes.NewBufferString(body)) + req.Header = dataPlaneHeaders(requestToken, bridgeToken, []byte(body)) + rec := httptest.NewRecorder() + h.ServeHTTP(rec, req) + resp := rec.Result() + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + t.Fatalf("status = %d, want 200", resp.StatusCode) + } + if got := resp.Header.Get("Content-Type"); got != "text/event-stream" { + t.Fatalf("Content-Type = %q, want text/event-stream", got) + } + raw, err := io.ReadAll(resp.Body) + if err != nil { + t.Fatal(err) + } + if string(raw) != responsesStreamFixture { + t.Fatalf("relayed stream diverged:\n got %q\nwant %q", raw, responsesStreamFixture) + } + + if gotBridgeMethod != http.MethodPost || gotBridgePath != DataPlaneBridgePath { + t.Fatalf("bridge request = %s %s, want POST %s", gotBridgeMethod, gotBridgePath, DataPlaneBridgePath) + } + if gotBridgeToken != bridgeToken { + t.Fatalf("bridge token = %q", gotBridgeToken) + } + if gotClaimNonce == "" || gotClaimAdmission == "" { + t.Fatal("claim headers were not relayed") + } + if gotContentType != "application/json" { + t.Fatalf("content-type = %q", gotContentType) + } + if string(gotBody) != body { + t.Fatalf("bridge body = %q, want %q", gotBody, body) + } +} + +func TestDataPlaneSeamRelaysBridgeStatusAndRetryAfter(t *testing.T) { + const requestToken = "parent-to-sidecar" + const bridgeToken = "sidecar-to-parent" + bridge := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/event-stream") + w.Header().Set("Retry-After", "2") + w.WriteHeader(http.StatusTooManyRequests) + _, _ = w.Write([]byte("data: {\"type\":\"error\"}\n\n")) + })) + defer bridge.Close() + + h := dataPlaneSeamHandler(t, requestToken, bridgeToken, bridge.URL) + req := httptest.NewRequest(http.MethodPost, "/v1/responses", bytes.NewBufferString(`{}`)) + req.Header = dataPlaneHeaders(requestToken, bridgeToken, []byte(`{}`)) + rec := httptest.NewRecorder() + h.ServeHTTP(rec, req) + resp := rec.Result() + defer resp.Body.Close() + raw, _ := io.ReadAll(resp.Body) + if resp.StatusCode != http.StatusTooManyRequests { + t.Fatalf("status = %d, want 429", resp.StatusCode) + } + if got := resp.Header.Get("Retry-After"); got != "2" { + t.Fatalf("Retry-After = %q", got) + } + if string(raw) != "data: {\"type\":\"error\"}\n\n" { + t.Fatalf("body = %q", raw) + } +} + +func TestDataPlaneSeamRejectsUnauthenticatedOrWrongRouteRequests(t *testing.T) { + const requestToken = "parent-to-sidecar" + const bridgeToken = "sidecar-to-parent" + var bridgeCalls int + bridge := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + bridgeCalls++ + w.WriteHeader(http.StatusOK) + })) + defer bridge.Close() + + h := dataPlaneSeamHandler(t, requestToken, bridgeToken, bridge.URL) + + // Missing and wrong request token both answer 404 and never reach the bridge. + for _, headers := range []http.Header{nil, dataPlaneHeaders("wrong", bridgeToken, []byte(`{}`))} { + req := httptest.NewRequest(http.MethodPost, "/v1/responses", bytes.NewBufferString(`{}`)) + req.Header = headers + rec := httptest.NewRecorder() + h.ServeHTTP(rec, req) + if rec.Code != http.StatusNotFound { + t.Fatalf("missing/wrong token status = %d, want 404", rec.Code) + } + } + // A GET on the data-plane path is not a declared seam surface. + req := httptest.NewRequest(http.MethodGet, "/v1/responses", nil) + req.Header = dataPlaneHeaders(requestToken, bridgeToken, nil) + rec := httptest.NewRecorder() + h.ServeHTTP(rec, req) + if rec.Code != http.StatusNotFound { + t.Fatalf("GET /v1/responses status = %d, want 404", rec.Code) + } + if bridgeCalls != 0 { + t.Fatalf("unauthenticated request reached bridge (%d calls)", bridgeCalls) + } +} + +func TestDataPlaneSeamRejectsNonLoopbackOrMissingBridgeConfig(t *testing.T) { + const requestToken = "parent-to-sidecar" + const bridgeToken = "sidecar-to-parent" + + // A public (non-loopback) parent URL must be refused before any request. + for _, parentURL := range []string{"https://example.test/bridge", "http://127.0.0.1:1"} { + h := NewHandler(Config{ + ParentURL: parentURL, + BridgeToken: bridgeToken, + RequestToken: requestToken, + }) + req := httptest.NewRequest(http.MethodPost, "/v1/responses", bytes.NewBufferString(`{}`)) + req.Header = dataPlaneHeaders(requestToken, bridgeToken, []byte(`{}`)) + rec := httptest.NewRecorder() + h.ServeHTTP(rec, req) + if rec.Code != http.StatusServiceUnavailable { + t.Fatalf("parent %q status = %d, want 503", parentURL, rec.Code) + } + } + + // No bridge token configured: the seam must refuse rather than guess. + h := NewHandler(Config{ + ParentURL: "http://127.0.0.1:1", + RequestToken: requestToken, + }) + req := httptest.NewRequest(http.MethodPost, "/v1/responses", bytes.NewBufferString(`{}`)) + req.Header = dataPlaneHeaders(requestToken, "", []byte(`{}`)) + rec := httptest.NewRecorder() + h.ServeHTTP(rec, req) + if rec.Code != http.StatusServiceUnavailable { + t.Fatalf("missing bridge token status = %d, want 503", rec.Code) + } +} + +func TestDataPlaneSeamOversizedBodyIsRefused(t *testing.T) { + const requestToken = "parent-to-sidecar" + const bridgeToken = "sidecar-to-parent" + var bridgeCalls int + bridge := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + bridgeCalls++ + w.WriteHeader(http.StatusOK) + })) + defer bridge.Close() + + h := dataPlaneSeamHandler(t, requestToken, bridgeToken, bridge.URL) + big := strings.Repeat("x", maxDataPlaneBodyBytes+1) + req := httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(big)) + req.Header = dataPlaneHeaders(requestToken, bridgeToken, nil) + rec := httptest.NewRecorder() + h.ServeHTTP(rec, req) + // The bridge never sees an oversized body; the seam answers 413 itself. + if rec.Code != http.StatusRequestEntityTooLarge { + t.Fatalf("oversized body status = %d, want 413", rec.Code) + } + if bridgeCalls != 0 { + t.Fatalf("oversized request reached bridge (%d calls)", bridgeCalls) + } +} diff --git a/go/internal/sidecar/sidecar.go b/go/internal/sidecar/sidecar.go index 57e2ee44d7..1c0355b749 100644 --- a/go/internal/sidecar/sidecar.go +++ b/go/internal/sidecar/sidecar.go @@ -274,6 +274,7 @@ func NewHandler(cfg Config) http.Handler { relayPublicWrite(w, r, cfg, writeRelay, path) }) } + mountDataPlaneSeam(mux, cfg) return mux } @@ -370,11 +371,17 @@ func privateParentBridgeURL(raw, bridgePath string) (*url.URL, bool) { return parent, true } +func bridgeTransport() *http.Transport { + return &http.Transport{ + Proxy: nil, + DialContext: (&net.Dialer{}).DialContext, + } +} + func privateBridgeClient() *http.Client { - transport := &http.Transport{Proxy: nil, DialContext: (&net.Dialer{}).DialContext} return &http.Client{ Timeout: 30 * time.Second, - Transport: transport, + Transport: bridgeTransport(), CheckRedirect: func(_ *http.Request, _ []*http.Request) error { return http.ErrUseLastResponse }, diff --git a/src/server/go-sidecar.ts b/src/server/go-sidecar.ts index 103593e2e0..53d4eaf7e8 100644 --- a/src/server/go-sidecar.ts +++ b/src/server/go-sidecar.ts @@ -28,6 +28,7 @@ import { existsSync } from "node:fs"; import { directLocalHttpFetch } from "./direct-local-http"; import { registerOptionalShutdownHook } from "../lib/optional-shutdown-hooks"; import { setGoOwnedRouteForwarder } from "./go-sidecar-slot"; +import { HOT_PATH_SEAM_PATH, HOT_PATH_SIDECAR_REQUEST_HEADER } from "./hot-path-seam"; /** Environment variable naming the ocx-sidecar binary to spawn. */ export const GO_SIDECAR_BIN_ENV = "OPENCODEX_GO_SIDECAR_BIN"; @@ -70,6 +71,12 @@ let generation = 0; let forwardDetach: (() => void) | null = null; let bridgeStopped: (() => void) | null = null; +// Data-plane hot-path seam state (ticket #24, devlog 034). Set once the ready +// line lands, independently of the seam env: the front door decides whether to +// use it (OPENCODEX_GO_HOTPATH_SEAM) so an operator flipping the env at runtime +// is honoured, while the capability pair below stays fixed per activation. +let dataPlaneSeam: { baseUrl: string; requestToken: string } | null = null; + export type GoSidecarSupervisorConfig = { parentUrl: string; bridgeToken: string; @@ -95,6 +102,57 @@ export function activeGoSidecarBaseUrl(): string | null { return stopped ? null : readyBaseUrl || null; } +/** + * True when the sidecar is attached AND ready to serve the data-plane seam. + * The front door consults this only after its own env gate, and only before + * reading the request body: a seam that is not attached must never consume a + * body it cannot fall back from. + */ +export function isDataPlaneSeamAttached(): boolean { + return !stopped && dataPlaneSeam !== null && readyBaseUrl !== ""; +} + +/** + * Forward one seam-gated POST /v1/responses request to the attached sidecar + * with the parent request token and the front-door claim headers. Returns the + * sidecar's Response (status and stream verbatim) or null when the seam is not + * attached or the hop failed. Never throws. + */ +export async function forwardHotPathSeam( + request: Request, + body: Uint8Array, + claimHeaders: Headers, +): Promise { + const seam = dataPlaneSeam; + if (!seam || stopped) return null; + const target = new URL(HOT_PATH_SEAM_PATH, seam.baseUrl); + try { + const headers = new Headers(claimHeaders); + headers.set(HOT_PATH_SIDECAR_REQUEST_HEADER, seam.requestToken); + const contentType = request.headers.get("content-type"); + if (contentType) headers.set("content-type", contentType); + const upstream = await directLocalHttpFetch(target, { + method: "POST", + headers, + body, + signal: request.signal, + }); + // A 4xx/5xx from the seam is its observable result (the bridge ran the + // pipeline); it must reach the client rather than being swallowed. + return new Response(upstream.body, { + status: upstream.status, + headers: { + "content-type": upstream.headers.get("content-type") ?? "text/event-stream", + ...(upstream.headers.has("retry-after") + ? { "retry-after": upstream.headers.get("retry-after")! } + : {}), + }, + }); + } catch { + return null; + } +} + function parseReadyLine(line: string): string | null { const trimmed = line.trim(); const prefix = `${GO_SIDECAR_READY_PREFIX} http://`; @@ -240,6 +298,7 @@ function stopSidecar(): void { const proc = childProc; childProc = null; readyBaseUrl = ""; + dataPlaneSeam = null; if (proc) { try { proc.kill(); @@ -327,6 +386,9 @@ export function activateGoSidecar( if (stopped || myGeneration !== generation) return; readyBaseUrl = parsed; const baseUrl = parsed; + // The data-plane seam is armed whenever the sidecar is attached; whether + // the front door uses it is the seam env gate's decision, read per request. + dataPlaneSeam = { baseUrl, requestToken: liveStateBridge.requestToken }; const detach = setGoOwnedRouteForwarder((request, pathAndSearch, principal) => ( forwardTo(baseUrl, liveStateBridge.requestToken, liveStateBridge.createWriteRelayHeaders, request, pathAndSearch, principal) )); diff --git a/src/server/hot-path-seam.ts b/src/server/hot-path-seam.ts new file mode 100644 index 0000000000..992b4b6b16 --- /dev/null +++ b/src/server/hot-path-seam.ts @@ -0,0 +1,292 @@ +/** + * Data-plane hot-path seam (ADR-0008, ticket #24, devlog 034). + * + * The Go sidecar owns the public `POST /v1/responses` surface exactly like it + * owns the Go-owned management routes; until a provider relay lands (#27/#29) + * its stream source is the private parent bridge, which runs the real + * in-process `handleResponses` pipeline. This module is the DATA plane's twin + * of `go-sidecar-write-relay.ts`: it holds the seam gate, the bridge path and + * headers, and the body-bound parent claim that lets the front door admit one + * request without ever handing a client credential to the sidecar process. + * + * Two hard rules keep this seam honest: + * + * - The seam is gated separately from the management surface + * (`OPENCODEX_GO_HOTPATH_SEAM`): attaching a sidecar for management reads + * must never silently reroute data-plane traffic (spec #4, story 10). + * - The bridge verifies the claim, not the caller: it accepts neither an admin + * token nor a client API key as a substitute for a freshly minted, + * body-bound, replay-bounded parent assertion. + * + * The Go sidecar only relays the claim headers verbatim and never validates + * them; minting (front door) and verification (bridge) both live in this + * process under the same per-activation secret the write relay uses. + */ +import { createHash, createHmac, randomBytes, timingSafeEqual } from "node:crypto"; +import type { DataPlaneAdmission } from "./auth-cors"; + +/** Independent activation gate: management reads and the data plane roll back separately. */ +export const HOT_PATH_SEAM_ENV = "OPENCODEX_GO_HOTPATH_SEAM"; + +/** The declared data-plane seam route. One entry; a later ticket flips the marker, never the dispatch. */ +export const HOT_PATH_SEAM_PATH = "/v1/responses"; + +/** Private parent endpoint the seam asks to run the responses pipeline for one admitted request. */ +export const HOT_PATH_RESPONSES_BRIDGE_PATH = "/__ocx_go_sidecar/responses"; + +/** Same value as the management write-relay bridge capability header. */ +export const HOT_PATH_BRIDGE_HEADER = "x-ocx-go-sidecar-bridge"; + +/** Parent request-token header the sidecar seam authenticates. */ +export const HOT_PATH_SIDECAR_REQUEST_HEADER = "x-ocx-go-sidecar-request"; + +/** Claim headers relayed verbatim by the sidecar between mint (front door) and verify (bridge). */ +export const HOT_PATH_CLAIM_NONCE_HEADER = "x-ocx-go-dataplane-nonce"; +export const HOT_PATH_CLAIM_EXPIRES_AT_HEADER = "x-ocx-go-dataplane-expires-at"; +export const HOT_PATH_CLAIM_ADMISSION_HEADER = "x-ocx-go-dataplane-admission"; +export const HOT_PATH_CLAIM_PROOF_HEADER = "x-ocx-go-dataplane-proof"; + +const CLAIM_VERSION = "opencodex-go-dataplane-v1"; +const CLAIM_TTL_MS = 60_000; +const REPLAY_LIMIT = 256; +/** Matches src/server/request-decompress.ts MAX_DECOMPRESSED_BODY_BYTES. */ +const MAX_RESPONSES_BODY_BYTES = 256 * 1024 * 1024; +const BASE64URL_256 = /^[A-Za-z0-9_-]{43}$/; +const EXPIRY = /^[1-9]\d*$/; +const PATH = /^\/[^?#\r\n]*$/; +const METHODS = new Set(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD"]); + +/** True when the operator has switched the data-plane seam on (independent of sidecar attachment). */ +export function hotPathSeamEnabled(): boolean { + return process.env[HOT_PATH_SEAM_ENV] === "1"; +} + +/** Round-trip a DataPlaneAdmission through JSON so a claim never carries more than the admission. */ +function admissionJson(admission: DataPlaneAdmission): string | null { + const raw = JSON.stringify(admission); + return raw.length <= 512 ? raw : null; +} + +function parseAdmissionJson(raw: string): DataPlaneAdmission | null { + if (raw.length > 512) return null; + let parsed: unknown; + try { + parsed = JSON.parse(raw); + } catch { + return null; + } + if (typeof parsed !== "object" || parsed === null) return null; + const value = parsed as Record; + if (value.kind === "configured") { + return typeof value.keyId === "string" && typeof value.source === "string" + ? { kind: "configured", keyId: value.keyId, source: value.source as DataPlaneAdmission["source"] } + : null; + } + if (value.kind === "environment") { + return typeof value.source === "string" + ? { kind: "environment", source: value.source as DataPlaneAdmission["source"] } + : null; + } + if (value.kind === "loopback") { + return { kind: "loopback", source: "loopback" }; + } + return null; +} + +interface DataPlaneSeamClaim { + nonce: string; + admission: DataPlaneAdmission; + method: string; + path: string; + expiresAt: number; + proof: string; +} + +function isRelaySecret(value: string): boolean { + return BASE64URL_256.test(value); +} + +function isBridgeToken(value: string | null): value is string { + return typeof value === "string" && value.length > 0 && value.length <= 256 && !/[\r\n]/.test(value); +} + +function claimPayload( + claim: Omit, + body: Uint8Array, +): string | null { + const serialized = admissionJson(claim.admission); + if (!serialized) return null; + if (!BASE64URL_256.test(claim.nonce) || !METHODS.has(claim.method)) return null; + if (!PATH.test(claim.path) || !Number.isSafeInteger(claim.expiresAt) || claim.expiresAt <= 0) return null; + const digest = createHash("sha256").update(body).digest("hex"); + return [CLAIM_VERSION, claim.nonce, serialized, claim.method, claim.path, digest, claim.expiresAt].join("\n"); +} + +function signClaim(secret: string, claim: Omit, body: Uint8Array): string | null { + const payload = claimPayload(claim, body); + if (!payload || !isRelaySecret(secret)) return null; + return createHmac("sha256", secret).update(payload).digest("base64url"); +} + +function equalSecret(actual: string | null, expected: string): boolean { + if (!actual) return false; + const left = Buffer.from(actual); + const right = Buffer.from(expected); + return left.byteLength === right.byteLength && timingSafeEqual(left, right); +} + +/** + * Mint the claim header set for one already-admitted data-plane request. The + * front door calls this only after `resolveResponsesApiAuth` succeeded, and + * only on the seam gate, so the headers always name a real admission and bind + * to the exact body bytes being forwarded. + */ +export function createDataPlaneSeamHeaders( + secret: string, + admission: DataPlaneAdmission, + method: string, + path: string, + body: Uint8Array, + now: () => number = Date.now, +): Headers | null { + const expiresAt = now() + CLAIM_TTL_MS; + if (!Number.isSafeInteger(expiresAt)) return null; + const nonce = randomBytes(32).toString("base64url"); + const proof = signClaim(secret, { nonce, admission, method, path, expiresAt }, body); + if (!proof) return null; + return new Headers({ + [HOT_PATH_CLAIM_NONCE_HEADER]: nonce, + [HOT_PATH_CLAIM_EXPIRES_AT_HEADER]: String(expiresAt), + [HOT_PATH_CLAIM_ADMISSION_HEADER]: admissionJson(admission) ?? "", + [HOT_PATH_CLAIM_PROOF_HEADER]: proof, + }); +} + +export interface HotPathResponsesBridgeOptions { + /** Per-sidecar capability, generated by the parent at activation. */ + bridgeToken: string; + /** Shared only with the supervised sidecar through its process environment. */ + relaySecret: string; + /** + * Runs the in-process responses pipeline for one admitted request and + * returns its Response. Called only after the bridge verified the claim. + */ + dispatchResponses(context: { + admission: DataPlaneAdmission; + contentType: string | null; + body: Uint8Array; + signal: AbortSignal | null; + }): Promise; + now?: () => number; +} + +export interface HotPathResponsesBridge { + handle(request: Request, url: URL): Promise; +} + +/** + * The private parent bridge for the hot-path seam. Verification order matters: + * capability first (cheap), then claim shape, then the body-bound proof — the + * expensive body read happens only once the claim already looks spendable. + */ +export function createHotPathResponsesBridge(options: HotPathResponsesBridgeOptions): HotPathResponsesBridge | null { + if (!isBridgeToken(options.bridgeToken) || !isRelaySecret(options.relaySecret)) return null; + const now = options.now ?? Date.now; + const consumed = new Map(); + + async function handle(request: Request, url: URL): Promise { + if ( + request.method !== "POST" + || url.pathname !== HOT_PATH_RESPONSES_BRIDGE_PATH + || url.search !== "" + || !equalSecret(request.headers.get(HOT_PATH_BRIDGE_HEADER), options.bridgeToken) + ) return new Response(null, { status: 404 }); + + const claim = claimFromHeaders(request.headers); + if (!claim || claim.method !== "POST" || claim.path !== HOT_PATH_SEAM_PATH) { + return new Response(null, { status: 404 }); + } + + const clock = now(); + if ( + !Number.isSafeInteger(clock) + || claim.expiresAt <= clock + || claim.expiresAt > clock + CLAIM_TTL_MS + ) return new Response(null, { status: 404 }); + + // The proof binds the body digest, so the body must be read before proof + // verification. Bound it first: an oversized body is refused even if a + // (stolen) valid claim named it. + let body: Uint8Array; + try { + body = await readBoundedBody(request, MAX_RESPONSES_BODY_BYTES); + } catch (error) { + const tooLarge = error instanceof DataPlaneBodyTooLargeError; + return new Response(tooLarge ? JSON.stringify({ error: "request body too large" }) : JSON.stringify({ error: "bridge read failed" }), { + status: tooLarge ? 413 : 500, + headers: { "content-type": "application/json" }, + }); + } + + if (!verifyProof(options.relaySecret, claim, body)) return new Response(null, { status: 404 }); + + pruneConsumed(consumed, clock); + if (consumed.has(claim.nonce) || consumed.size >= REPLAY_LIMIT) return new Response(null, { status: 404 }); + consumed.set(claim.nonce, claim.expiresAt); + + const contentType = request.headers.get("content-type"); + try { + return await options.dispatchResponses({ + admission: claim.admission, + contentType, + body, + signal: request.signal, + }); + } catch { + return new Response(JSON.stringify({ error: "internal_error", message: "responses dispatch failed" }), { + status: 500, + headers: { "content-type": "application/json" }, + }); + } + } + + return { handle }; +} + +class DataPlaneBodyTooLargeError extends Error {} + +function claimFromHeaders(headers: Headers): DataPlaneSeamClaim | null { + const nonce = headers.get(HOT_PATH_CLAIM_NONCE_HEADER) ?? ""; + const admissionRaw = headers.get(HOT_PATH_CLAIM_ADMISSION_HEADER) ?? ""; + const expiresAtRaw = headers.get(HOT_PATH_CLAIM_EXPIRES_AT_HEADER) ?? ""; + const proof = headers.get(HOT_PATH_CLAIM_PROOF_HEADER) ?? ""; + if (!BASE64URL_256.test(nonce) || !BASE64URL_256.test(proof)) return null; + if (!EXPIRY.test(expiresAtRaw)) return null; + const admission = parseAdmissionJson(admissionRaw); + if (!admission) return null; + const expiresAt = Number(expiresAtRaw); + if (!Number.isSafeInteger(expiresAt)) return null; + // Method and path are not claim headers: the bridge serves exactly one + // surface (POST /v1/responses) and mint and verify share the constants, so + // a relayed claim cannot name a different method or route. + return { nonce, admission, method: "POST", path: HOT_PATH_SEAM_PATH, expiresAt, proof }; +} + +function verifyProof(secret: string, claim: DataPlaneSeamClaim, body: Uint8Array): boolean { + const expected = signClaim(secret, claim, body); + return expected !== null && equalSecret(claim.proof, expected); +} + +async function readBoundedBody(request: Request, limit: number): Promise { + const contentLength = Number(request.headers.get("content-length") ?? "0"); + if (Number.isFinite(contentLength) && contentLength > limit) throw new DataPlaneBodyTooLargeError(); + const body = new Uint8Array(await request.arrayBuffer()); + if (body.byteLength > limit) throw new DataPlaneBodyTooLargeError(); + return body; +} + +function pruneConsumed(consumed: Map, now: number): void { + for (const [nonce, expiresAt] of consumed) { + if (expiresAt <= now) consumed.delete(nonce); + } +} diff --git a/src/server/index.ts b/src/server/index.ts index b4462b448a..55cd4c0d07 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -54,7 +54,15 @@ import { } from "../lib/app-owned-memory-stores"; import { acquireServerBackgroundLifecycle } from "./background-lifecycle"; import { activateLab, labActivationRequired } from "../lib/lab-activation"; -import { activateGoSidecar } from "./go-sidecar"; +import { activateGoSidecar, forwardHotPathSeam, isDataPlaneSeamAttached } from "./go-sidecar"; +import { + createDataPlaneSeamHeaders, + createHotPathResponsesBridge, + HOT_PATH_BRIDGE_HEADER, + HOT_PATH_RESPONSES_BRIDGE_PATH, + hotPathSeamEnabled, + type HotPathResponsesBridge, +} from "./hot-path-seam"; import { createGoSidecarWriteRelay, createGoSidecarWriteRelayHeaders, @@ -1028,6 +1036,12 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server | null = null; let goSidecarLiveStateBridgeToken: string | null = null; let goSidecarWriteRelay: GoSidecarWriteRelay | null = null; + // Data-plane hot-path seam (ticket #24, devlog 034): the claim secret is the + // same per-activation HMAC key the write relay uses, and the bridge runs the + // in-process responses pipeline for one seam-gated request. Both are created + // only when the operator enables OPENCODEX_GO_HOTPATH_SEAM. + let goSidecarHotPathRelaySecret: string | null = null; + let goSidecarHotPathBridge: HotPathResponsesBridge | null = null; // Set only when the optional Go sidecar activated (ADR-0008); consumed by the server.stop // override below, which is built before activation runs. Null default keeps a process that // never opted in from carrying any Go-sidecar state. @@ -1094,6 +1108,16 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server { - const response = await handleResponses(req, config, logCtx, { + // ADR-0008 hot-path seam (ticket #24, devlog 034): when the seam env + // is on AND a sidecar is attached, the request is forwarded to the Go + // seam, which runs the same pipeline through the private bridge and + // streams the response back. The body is read exactly once here; + // there is deliberately NO in-process fallback after that read, so a + // seam that dies mid-request surfaces a retryable 502 instead of + // double-executing the model call. + let seamResponse: Response | null = null; + if (hotPathSeamEnabled() && isDataPlaneSeamAttached()) { + let rawBody: Uint8Array | null = null; + try { + rawBody = new Uint8Array(await req.arrayBuffer()); + } catch (error) { + if (req.signal.aborted) { + // Client went away before the body settled. Fall through to + // the shared tail wrapper so the 499 carries the same CORS and + // request-id headers as every other response. + seamResponse = new Response(null, { status: 499 }); + } else { + throw error; + } + } + if (rawBody) { + // The body is fully received; the upstream stream may run long, so + // disable the request timeout exactly like the direct branch does + // on body completion. This happens BEFORE any seam hop, so every + // path below runs without the caller-owned timeout transition. + disableResponsesRequestTimeout(req, requestServer); + const claimSecret = goSidecarHotPathRelaySecret; + const claimHeaders = claimSecret + ? createDataPlaneSeamHeaders(claimSecret, admission, "POST", "/v1/responses", rawBody) + : null; + // The body has been consumed: there is deliberately NO in-process + // fallback past this point (double-executing the model call is + // worse than a retryable error), so a seam that cannot mint or + // reach its claim surfaces a 502 exactly like a dead sidecar. + const seam = claimHeaders ? await forwardHotPathSeam(req, rawBody, claimHeaders) : null; + seamResponse = seam ?? new Response(JSON.stringify({ + error: { type: "server_error", code: "server_error", message: "hot-path seam unavailable" }, + }), { + status: 502, + headers: { "Content-Type": "application/json" }, + }); + } + } + const response = seamResponse ?? await handleResponses(req, config, logCtx, { turnAdmissionLease, admission, onRequestBodyRead: () => disableResponsesRequestTimeout(req, requestServer), @@ -2496,6 +2565,7 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server { + const headers = new Headers(); + if (contentType) headers.set("content-type", contentType); + const internalReq = new Request("http://localhost/v1/responses", { + method: "POST", + headers, + body: new Uint8Array(body), + signal: signal ?? undefined, + }); + const logCtx: RequestLogContext = { + model: "unknown", + provider: "unknown", + ...admissionFields(admission), + inboundProtocol: "responses", + }; + // The client-side turn was already admitted by the seam gate in + // the public listener; this second admission gates the actual + // pipeline work (the bridge is where the model runs). + return runAdmittedHttpTurn(internalReq, config, async turnAdmissionLease => + handleResponses(internalReq, config, logCtx, { + turnAdmissionLease, + admission, + abortSignal: signal ?? undefined, + }), + ); + }, + }); + } return activateGoSidecar(VERSION, { parentUrl: "http://127.0.0.1:" + actualPort, bridgeToken: goSidecarLiveStateBridgeToken, @@ -2522,6 +2628,8 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server { goSidecarLiveStateBridgeToken = null; goSidecarWriteRelay = null; + goSidecarHotPathRelaySecret = null; + goSidecarHotPathBridge = null; }, }); })() diff --git a/tests/go-hotpath-seam.test.ts b/tests/go-hotpath-seam.test.ts new file mode 100644 index 0000000000..361fb357be --- /dev/null +++ b/tests/go-hotpath-seam.test.ts @@ -0,0 +1,340 @@ +import { afterAll, beforeAll, describe, expect, test } from "bun:test"; +import { existsSync, mkdtempSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { SERVER_BUDGET_MS } from "./helpers/test-budget"; +import { saveConfig } from "../src/config"; +import { startServer } from "../src/server"; +import { + GO_SIDECAR_BIN_ENV, + activeGoSidecarBaseUrl, + resetGoSidecarForTests, +} from "../src/server/go-sidecar"; +import { HOT_PATH_SEAM_ENV, HOT_PATH_SEAM_PATH } from "../src/server/hot-path-seam"; +import { removeTreeWithRetry } from "./helpers/remove-tree"; + +/** + * Streaming differential harness for the ADR-0008 hot-path seam (ticket #24, + * devlog 034). The TypeScript pipeline is the live oracle, exactly as it is for + * the management surface: server A runs the data plane in-process, server B + * runs the same request through the Go seam (front door → sidecar → private + * bridge → in-process pipeline), and the harness compares the client-visible + * ordered SSE frame sequence. + * + * The declared volatile set is DATA, mirroring `go.volatileFields` on the + * management side. For this fixture it is empty (raw frame identity) plus the + * per-request `x-request-id` response header, which every front door mints per + * request and therefore legitimately differs between A and B. Nothing else is + * forgiven: a dropped, reordered, duplicated or rewritten frame fails the + * differential, and the harness proves the failure mode on a deliberately + * mutated capture. + * + * Skipped where the Go toolchain is unavailable (same guard as + * `go-sidecar-parity.test.ts`); CI installs Go and runs the oracle. + */ + +const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); + +function goToolchainAvailable(): boolean { + const probe = Bun.spawnSync(["go", "version"], { stdout: "ignore", stderr: "ignore" }); + return probe.success; +} + +function buildSidecarBinary(): string { + const dir = mkdtempSync(join(tmpdir(), "ocx-go-sidecar-")); + const binPath = join(dir, process.platform === "win32" ? "ocx-sidecar.exe" : "ocx-sidecar"); + const build = Bun.spawnSync( + ["go", "build", "-o", binPath, "./cmd/ocx-sidecar"], + { + cwd: join(repoRoot, "go"), + env: { ...process.env, CGO_ENABLED: "0" }, + stdout: "pipe", + stderr: "pipe", + }, + ); + if (build.exitCode !== 0) { + throw new Error( + `go build ./cmd/ocx-sidecar failed (${build.exitCode}):\n${new TextDecoder().decode(build.stderr)}`, + ); + } + return binPath; +} + +const goAvailable = goToolchainAvailable(); +const sidecarBinary: string | null = goAvailable ? buildSidecarBinary() : null; + +const upstreamSse = + "event: response.created\ndata: {\"type\":\"response.created\",\"response\":{\"id\":\"fixture-1\",\"status\":\"in_progress\"}}\n\n" + + "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"delta\":\"Hel\"}\n\n" + + "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"delta\":\"lo\"}\n\n" + + "event: response.completed\ndata: {\"type\":\"response.completed\",\"response\":{\"id\":\"fixture-1\",\"status\":\"completed\"}}\n\n"; + +let upstream: ReturnType | null = null; + +/** + * Split a client-visible SSE wire body into ordered frames. A frame is one + * event block terminated by a blank line; the trailing `data: [DONE]` marker + * is its own frame (as the TS relay emits it). This is the comparison unit of + * the differential — never a JSON re-parse, which would forgive the reorder + * and duplication classes this harness exists to catch. + */ +function parseSseFrames(raw: string): string[] { + const frames = raw.split("\n\n"); + // A trailing separator after [DONE] produces a final empty element. + if (frames.length > 0 && frames[frames.length - 1] === "") frames.pop(); + return frames; +} + +/** + * Declared volatile normalisation. Body volatile fields: NONE for this + * fixture (raw frame identity). Three response headers are legitimately + * request/server-scoped and are normalised exactly like the management + * oracle's pid/uptime: the per-request trace id, the Date header (two + * requests land at different instants), and the CORS origin echo, which names + * the responding server's own listener. + */ +const volatileResponseHeaders = [ + "x-opencodex-request-id", + "date", + "access-control-allow-origin", +] as const; +const volatileBodyFields: readonly string[] = []; + +function normaliseBody(raw: string): string { + let out = raw; + for (const field of volatileBodyFields) { + out = out.replace(new RegExp(`"${field}":(-?\\d+(?:\\.\\d+)?|[^,}\\]]+)`, "g"), `"${field}":0`); + } + return out; +} + +function normaliseHeaders(headers: Headers): Record { + const out: Record = {}; + for (const [name, value] of headers) { + if ((volatileResponseHeaders as readonly string[]).includes(name)) { + out[name] = ""; + } else { + out[name] = value; + } + } + return out; +} + +const previousEnv: Record = {}; +let testHome = ""; + +function configFixture(upstreamPort: number) { + return { + port: 0, + hostname: "127.0.0.1", + defaultProvider: "test", + providers: { + test: { + adapter: "openai-responses", + baseUrl: `http://127.0.0.1:${upstreamPort}/v1`, + allowPrivateNetwork: true, + disabled: false, + models: ["test-model"], + }, + }, + }; +} + +function captureEnv(): void { + for (const name of [GO_SIDECAR_BIN_ENV, HOT_PATH_SEAM_ENV, "OPENCODEX_HOME", "OPENCODEX_API_AUTH_TOKEN"]) { + previousEnv[name] = process.env[name]; + } +} + +function setUpFixture(upstreamPort: number): void { + testHome = mkdtempSync(join(tmpdir(), "ocx-hotpath-seam-")); + process.env.OPENCODEX_HOME = testHome; + process.env.OPENCODEX_API_AUTH_TOKEN = "data-secret"; + saveConfig(configFixture(upstreamPort)); +} + +function tearDownFixture(): void { + resetGoSidecarForTests(); + for (const [name, value] of Object.entries(previousEnv)) { + if (value === undefined) delete process.env[name]; + else process.env[name] = value; + } + if (testHome) { + removeTreeWithRetry(testHome); + testHome = ""; + } +} + +async function waitFor(probe: () => T | null | undefined, timeoutMs: number): Promise { + const deadline = Date.now() + timeoutMs; + for (;;) { + const value = probe(); + if (value !== null && value !== undefined) return value; + if (Date.now() >= deadline) throw new Error(`condition not met within ${timeoutMs}ms`); + await Bun.sleep(50); + } +} + +interface DataPlaneCapture { + status: number; + contentType: string | null; + headers: Record; + frames: string[]; +} + +async function postResponses(server: { url: URL }, token: string): Promise { + const response = await fetch(new URL("/v1/responses", server.url), { + method: "POST", + headers: { "content-type": "application/json", "x-opencodex-api-key": token }, + body: JSON.stringify({ model: "test-model", input: "ping", stream: true }), + }); + const raw = await response.text(); + return { + status: response.status, + contentType: response.headers.get("content-type"), + headers: normaliseHeaders(response.headers), + frames: parseSseFrames(normaliseBody(raw)), + }; +} + +function runFixtureTest(name: string, fn: () => Promise): void { + test( + name, + async () => { + captureEnv(); + try { + await fn(); + } finally { + tearDownFixture(); + } + }, + SERVER_BUDGET_MS, + ); +} + +describe.skipIf(!goAvailable || sidecarBinary === null)("ocx-sidecar hot-path seam differential (ADR-0008, ticket #24)", () => { + beforeAll(() => { + upstream = Bun.serve({ + hostname: "127.0.0.1", + port: 0, + fetch(req) { + if (new URL(req.url).pathname !== "/v1/responses") return new Response("nf", { status: 404 }); + return new Response(upstreamSse, { headers: { "content-type": "text/event-stream" } }); + }, + }); + }); + afterAll(() => { + upstream?.stop(true); + upstream = null; + }); + + test("the seam data is declared before the oracle runs", () => { + // The harness must prove a declared surface: if the seam route marker or + // its gate ever disappears the differential compares nothing. The declared + // volatile set for this fixture is the per-request response header only. + expect(HOT_PATH_SEAM_PATH).toBe("/v1/responses"); + expect(volatileBodyFields).toEqual([]); + expect([...volatileResponseHeaders]).toEqual([ + "x-opencodex-request-id", + "date", + "access-control-allow-origin", + ]); + }); + + runFixtureTest("seam-served stream equals the in-process stream frame-for-frame", async () => { + const token = "data-secret"; + const port = upstream!.port; + + // Server A: the in-process pipeline is the live oracle. + setUpFixture(port); + const serverA = startServer(0); + let tsCapture: DataPlaneCapture; + try { + tsCapture = await postResponses(serverA, token); + expect(tsCapture.status).toBe(200); + expect(tsCapture.contentType).toBe("text/event-stream"); + expect(tsCapture.frames.length).toBeGreaterThanOrEqual(4); + } finally { + await serverA.stop(true); + } + + // Server B: same config and fixture upstream, seam env on, sidecar attached. + process.env[GO_SIDECAR_BIN_ENV] = sidecarBinary!; + process.env[HOT_PATH_SEAM_ENV] = "1"; + const serverB = startServer(0); + let goCapture: DataPlaneCapture; + try { + await waitFor(() => activeGoSidecarBaseUrl(), 15_000); + goCapture = await postResponses(serverB, token); + expect(goCapture.status).toBe(200); + expect(goCapture.contentType).toBe("text/event-stream"); + } finally { + await serverB.stop(true); + } + + // The client-visible frame sequence must be identical in order and bytes. + // The two requests land on the same fixture, and the seam is a pure + // transport, so an empty body volatile set is the honest contract here. + expect(goCapture.frames).toEqual(tsCapture.frames); + // Only the declared volatile header legitimately differs. + expect(goCapture.headers).toEqual(tsCapture.headers); + expect(activeGoSidecarBaseUrl()).toBeNull(); + }); + + runFixtureTest("a mutated frame fails the differential (non-vacuous harness)", async () => { + const token = "data-secret"; + const port = upstream!.port; + setUpFixture(port); + const serverA = startServer(0); + try { + const capture = await postResponses(serverA, token); + // Reorder two frames: the harness must treat that as a divergence. + const reordered = [...capture.frames]; + const first = reordered.shift()!; + reordered.splice(1, 0, first); + expect(reordered).not.toEqual(capture.frames); + // A dropped frame must also diverge. + expect(capture.frames.slice(1)).not.toEqual(capture.frames); + } finally { + await serverA.stop(true); + } + }); + + runFixtureTest("a seam-on server refuses unclaimed sidecar requests and seam-off serves in-process", async () => { + const token = "data-secret"; + const port = upstream!.port; + setUpFixture(port); + process.env[GO_SIDECAR_BIN_ENV] = sidecarBinary!; + process.env[HOT_PATH_SEAM_ENV] = "1"; + const server = startServer(0); + try { + const sidecarUrl = await waitFor(() => activeGoSidecarBaseUrl(), 15_000); + // A direct call to the sidecar's data-plane surface without the parent + // request token must answer 404: the sidecar invents no public listener. + const direct = await fetch(new URL(HOT_PATH_SEAM_PATH, sidecarUrl), { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ model: "test-model", input: "ping", stream: true }), + }); + expect(direct.status).toBe(404); + } finally { + await server.stop(true); + } + expect(activeGoSidecarBaseUrl()).toBeNull(); + + // Same config, seam env off: the seam gate stays closed and the data + // plane is served in-process exactly as a build without the seam. + delete process.env[HOT_PATH_SEAM_ENV]; + process.env[GO_SIDECAR_BIN_ENV] = sidecarBinary!; + const serverB = startServer(0); + try { + await waitFor(() => activeGoSidecarBaseUrl(), 15_000); + const capture = await postResponses(serverB, token); + expect(capture.status).toBe(200); + expect(capture.frames[0]).toContain("response.created"); + } finally { + await serverB.stop(true); + } + }); +}); diff --git a/tests/hot-path-seam.test.ts b/tests/hot-path-seam.test.ts new file mode 100644 index 0000000000..42f4909254 --- /dev/null +++ b/tests/hot-path-seam.test.ts @@ -0,0 +1,155 @@ +import { describe, expect, test } from "bun:test"; +import { + createDataPlaneSeamHeaders, + createHotPathResponsesBridge, + HOT_PATH_BRIDGE_HEADER, + HOT_PATH_RESPONSES_BRIDGE_PATH, + HOT_PATH_SEAM_PATH, + type HotPathResponsesBridge, +} from "../src/server/hot-path-seam"; +import type { DataPlaneAdmission } from "../src/server/auth-cors"; + +// 43-char base64url relay secret, the same shape the write relay uses. +const secret = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQ"; +const bridgeToken = "bridge-token"; +const clock = () => 1_800_000_000_000; + +const admission: DataPlaneAdmission = { kind: "environment", source: "x-api-key" }; + +function makeBridge(dispatch?: (c: unknown) => Promise): HotPathResponsesBridge { + const bridge = createHotPathResponsesBridge({ + bridgeToken, + relaySecret: secret, + dispatchResponses: async c => { + dispatch?.(c); + return new Response("fixture-stream", { status: 200, headers: { "content-type": "text/event-stream" } }); + }, + now: clock, + }); + if (!bridge) throw new Error("bridge creation failed"); + return bridge; +} + +function seamRequest(headers: Headers, body = `{"model":"fixture","stream":true}`): { request: Request; url: URL } { + const request = new Request(`http://127.0.0.1${HOT_PATH_RESPONSES_BRIDGE_PATH}`, { + method: "POST", + headers: { "content-type": "application/json", [HOT_PATH_BRIDGE_HEADER]: bridgeToken, ...Object.fromEntries(headers) }, + body, + }); + return { request, url: new URL(request.url) }; +} + +describe("hot-path seam claim and bridge (ticket #24)", () => { + test("an admitted claim reaches dispatch with the reconstructed admission and identical body", async () => { + let captured: { admission: DataPlaneAdmission; contentType: string | null; body: Uint8Array } | null = null; + const bridge = makeBridge(c => { captured = c as typeof captured; }); + const bodyBytes = new TextEncoder().encode(`{"model":"fixture","stream":true}`); + const headers = createDataPlaneSeamHeaders(secret, admission, "POST", HOT_PATH_SEAM_PATH, bodyBytes, clock); + expect(headers).not.toBeNull(); + + const { request, url } = seamRequest(headers!, `{"model":"fixture","stream":true}`); + const response = await bridge.handle(request, url); + expect(response.status).toBe(200); + expect(await response.text()).toBe("fixture-stream"); + expect(captured).not.toBeNull(); + expect(captured!.admission).toEqual(admission); + expect(captured!.contentType).toBe("application/json"); + expect(new TextDecoder().decode(captured!.body)).toBe(`{"model":"fixture","stream":true}`); + }); + + test("a configured-key admission keeps its keyId across the bridge", async () => { + let captured: { admission: DataPlaneAdmission } | null = null; + const bridge = makeBridge(c => { captured = c as typeof captured; }); + const configured: DataPlaneAdmission = { kind: "configured", keyId: "key-1", source: "bearer" }; + const bodyBytes = new Uint8Array(0); + const headers = createDataPlaneSeamHeaders(secret, configured, "POST", HOT_PATH_SEAM_PATH, bodyBytes, clock); + const { request, url } = seamRequest(headers!, ""); + const response = await bridge.handle(request, url); + expect(response.status).toBe(200); + expect(captured!.admission).toEqual(configured); + }); + + test("missing or wrong bridge capability answers 404 without touching dispatch", async () => { + let dispatched = 0; + const bridge = makeBridge(() => { dispatched++; }); + const bodyBytes = new Uint8Array(0); + const headers = createDataPlaneSeamHeaders(secret, admission, "POST", HOT_PATH_SEAM_PATH, bodyBytes, clock)!; + const missing = new Request(`http://127.0.0.1${HOT_PATH_RESPONSES_BRIDGE_PATH}`, { + method: "POST", + headers: Object.fromEntries(headers), + body: "", + }); + expect((await bridge.handle(missing, new URL(missing.url))).status).toBe(404); + const wrong = new Request(`http://127.0.0.1${HOT_PATH_RESPONSES_BRIDGE_PATH}`, { + method: "POST", + headers: { ...Object.fromEntries(headers), [HOT_PATH_BRIDGE_HEADER]: "wrong" }, + body: "", + }); + expect((await bridge.handle(wrong, new URL(wrong.url))).status).toBe(404); + expect(dispatched).toBe(0); + }); + + test("an expired claim is refused", async () => { + const bridge = makeBridge(); + const bodyBytes = new Uint8Array(0); + const headers = createDataPlaneSeamHeaders(secret, admission, "POST", HOT_PATH_SEAM_PATH, bodyBytes, clock)!; + const { request, url } = seamRequest(headers, ""); + // One second past the 60s TTL window. + const late = clock() + 60_001; + const expiredBridge = createHotPathResponsesBridge({ + bridgeToken, + relaySecret: secret, + dispatchResponses: async () => new Response("never", { status: 200 }), + now: () => late, + })!; + expect((await expiredBridge.handle(request, url)).status).toBe(404); + }); + + test("a changed body fails the body-bound proof", async () => { + const bridge = makeBridge(); + const bodyBytes = new TextEncoder().encode(`{"model":"fixture","stream":true}`); + const headers = createDataPlaneSeamHeaders(secret, admission, "POST", HOT_PATH_SEAM_PATH, bodyBytes, clock)!; + const { request, url } = seamRequest(headers, `{"model":"TAMPERED","stream":true}`); + expect((await bridge.handle(request, url)).status).toBe(404); + }); + + test("a replay of the same nonce is refused", async () => { + const bridge = makeBridge(); + const bodyBytes = new Uint8Array(0); + const headers = createDataPlaneSeamHeaders(secret, admission, "POST", HOT_PATH_SEAM_PATH, bodyBytes, clock)!; + const first = seamRequest(headers, ""); + expect((await bridge.handle(first.request, first.url)).status).toBe(200); + const second = seamRequest(headers, ""); + expect((await bridge.handle(second.request, second.url)).status).toBe(404); + }); + + test("a mismatched admission shape is refused before any body read", async () => { + const bridge = makeBridge(); + const bodyBytes = new Uint8Array(0); + const headers = createDataPlaneSeamHeaders(secret, admission, "POST", HOT_PATH_SEAM_PATH, bodyBytes, clock)!; + const tampered = new Headers(headers); + tampered.set("x-ocx-go-dataplane-admission", `{"kind":"bogus"}`); + const { request, url } = seamRequest(tampered, ""); + expect((await bridge.handle(request, url)).status).toBe(404); + }); + + test("a claimed content-length beyond the body bound is refused as 413", async () => { + const bridge = makeBridge(); + const bodyBytes = new Uint8Array(0); + const headers = createDataPlaneSeamHeaders(secret, admission, "POST", HOT_PATH_SEAM_PATH, bodyBytes, clock)!; + const huge = new Headers(headers); + huge.set("content-length", String(300 * 1024 * 1024)); + const request = new Request(`http://127.0.0.1${HOT_PATH_RESPONSES_BRIDGE_PATH}`, { + method: "POST", + headers: { [HOT_PATH_BRIDGE_HEADER]: bridgeToken, ...Object.fromEntries(huge) }, + body: "", + }); + const response = await bridge.handle(request, new URL(request.url)); + expect(response.status).toBe(413); + }); + + test("a malformed mint (invalid secret) produces no headers at all", () => { + expect(createDataPlaneSeamHeaders("short", admission, "POST", HOT_PATH_SEAM_PATH, new Uint8Array(0), clock)).toBeNull(); + expect(createDataPlaneSeamHeaders("", admission, "POST", HOT_PATH_SEAM_PATH, new Uint8Array(0), clock)).toBeNull(); + }); +}); From befb594d2ae4dd8804fe72fc623c7f4a3886cc40 Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Sun, 6 Sep 2026 12:36:05 +0800 Subject: [PATCH 016/165] test(config): cover invalid JSON recovery warning --- tests/config.test.ts | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/tests/config.test.ts b/tests/config.test.ts index 67b9afe54e..41631b2edd 100644 --- a/tests/config.test.ts +++ b/tests/config.test.ts @@ -1345,7 +1345,8 @@ describe("opencodex config defaults", () => { }); test("backs up invalid JSON config before falling back to defaults", () => { - writeConfig("{ invalid json"); + const invalidConfig = "{ invalid json"; + writeConfig(invalidConfig); const errorSpy = spyOn(console, "error").mockImplementation(() => {}); try { @@ -1354,8 +1355,15 @@ describe("opencodex config defaults", () => { expect(loaded).toEqual(getDefaultConfig()); const backups = backupNames(); expect(backups).toHaveLength(1); - expect(readFileSync(join(testDir, backups[0]), "utf-8")).toBe("{ invalid json"); - expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining("Could not load opencodex config")); + const backupPath = join(testDir, backups[0]); + expect(readFileSync(getConfigPath(), "utf-8")).toBe(invalidConfig); + expect(readFileSync(backupPath, "utf-8")).toBe(invalidConfig); + expect(errorSpy).toHaveBeenCalledWith( + expect.stringContaining(`Could not load opencodex config at ${getConfigPath()}:`), + ); + expect(errorSpy).toHaveBeenCalledWith( + expect.stringContaining(`A backup was written to ${backupPath}.`), + ); } finally { errorSpy.mockRestore(); } From e4d684d11293337d0814422c258d0535d44c8d7c Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Sun, 6 Sep 2026 14:54:53 +0800 Subject: [PATCH 017/165] docs(go): record the #26 write-surface parity gate design (devlog 035) --- .../035_write_surface_full_parity_gate.md | 259 ++++++++++++++++++ 1 file changed, 259 insertions(+) create mode 100644 devlog/_plan/260905_go_sidecar_takeover/035_write_surface_full_parity_gate.md diff --git a/devlog/_plan/260905_go_sidecar_takeover/035_write_surface_full_parity_gate.md b/devlog/_plan/260905_go_sidecar_takeover/035_write_surface_full_parity_gate.md new file mode 100644 index 0000000000..fc55ee5deb --- /dev/null +++ b/devlog/_plan/260905_go_sidecar_takeover/035_write_surface_full_parity_gate.md @@ -0,0 +1,259 @@ +# 035 — Ticket #26: write-surface full parity + authorization gate + +Unit: `260905_go_sidecar_takeover` +Date: 2026-09-06 +Status: in progress on `fix/ticket-26-write-auth-gate` (rebase-follows dev-go @ `f8ab6d510`, which carries #24) +Ticket: [#26](https://github.com/waxiangzi/opencodex/issues/26) (spec #3 capstone: "Write-surface full parity + authorization gate") +Parent spec: [#3](https://github.com/waxiangzi/opencodex/issues/3) (increment 3: write surface) +Blocked-by (#21/#22/#23): closed on `dev-go` — `eb4292e8f` relays the three +write batches through the sidecar. Read-side mirror: [#25](https://github.com/waxiangzi/opencodex/issues/25) +(full read-surface parity gate), in flight in a sibling worktree. + +Owner decision (2026-09-06): implement #26 as the **write-side parity gate** +under the state-source framework (devlog 030/032) — same shape the read +surface used. Three machine-checked deliverables: + +1. Every mutating route carries an explicit ownership verdict — Go-owned, + exempted, or deferred with a recorded reason. No silent plain route. +2. Every declared Go-owned write route has a state-reset differential oracle + case (response AND post-state, failure modes included). +3. Auth rejection parity as a whole-surface property: every declared Go-owned + write route is exercised without and with insufficient credentials/capability, + rejecting byte-identically to TypeScript. + +Making the Go `managementauth` gate *live* on the public write path (sidecar +answers requests without the TS front door) is deliberately **not** part of +#26: the sidecar never receives a browser session or admin token +(`go/internal/managementauth/write_relay.go`, devlog 033), and the flip (#41) +is where the Go binary becomes the serving process and owns that state. #26 +proves the decision substrate now so the flip can consume it wholesale. + +## Current write-surface state (HEAD `eb4292e8f`) + +- **Registry** (`src/server/management/route-registry.ts`): 122 mutating + routes total. 12 are declared Go-owned with `go: { relay: "signed", + volatileFields: [] }`; 18 carry an `exempt` verdict (CLI-parity vocabulary, + enforced honest by `tests/management-route-registry.test.ts`); **92 carry + neither** — the "silent plain" set this ticket eliminates. +- **Wire shape**: the TS front door (`src/server/index.ts` → + `requireManagementAuth`) still admits every `/api/*` request and resolves a + principal; the Go-owned write branch (`management-api.ts` → + `tryForwardDeclaredGoOwnedRoute`) mints a body-bound, one-use HMAC relay + claim (nonce/principal/method/path/sha256(body)/expiry, TTL 30 s, replay 256, + body cap 2 MiB); the sidecar public route verifies the parent request token + + claim (`go/internal/sidecar.go` `relayPublicWrite`), then forwards to the + private parent bridge `/__ocx_go_sidecar/write` + (`src/server/go-sidecar-write-relay.ts`), which re-verifies and dispatches + the **legacy TS handler** — TS remains the mutation oracle until native + mutations land, exactly like the #24 hot-path seam. +- **`managementauth` gate** (`go/internal/managementauth/`): substrate proven + by the `authcheck` differential oracle (`tests/go-auth-parity.test.ts`); it + never runs live pre-flip. `relayPublicWrite` rejects an invalid/absent claim + with a sidecar-shaped 401/404, which the front door makes unreachable from a + public client (the front door rejects first). +- **Coverage today** (`tests/go-sidecar-parity.test.ts`): state-reset + differential cases exist for the shadow-call write, settings write, + sidecar-settings write, and a quota/account-pool vector — not for all 12 + declared write routes, and no under-privileged write vectors at all. + +## What #26 is (and is not) + +#26 is the write-side capstone of spec #3: the machine property that the +*migrated* write surface is complete (no route can become Go-owned while its +authorization is unproven — spec #3 story 4), differentially proven per route, +and guarded by an authorization gate that answers identically to TypeScript. +It does **not** migrate new routes: the batches (#21/#22/#23) own which routes +move. It does **not** make the Go gate live: the flip (#41) owns that. It +makes both future steps safe to consume — the flip can serve the write surface +knowing every route already has a verdict, a differential, and proven +rejection parity. + +Acceptance criteria → deliverable map: + +| Acceptance | Deliverable | Seam | +|---|---|---| +| Every write route Go-owned | A write-ownership ledger making every mutating route's verdict explicit (Go-owned / exempt / deferred-with-reason), machine-checked against `MANAGEMENT_ROUTES` so no silent plain route survives a registry edit | 1 (registry/ledger) | +| State-reset differentials green for the write surface | One state-reset oracle case per declared Go-owned write route (the 12), response + post-state + a failure-mode leg where one exists | 2 (differential oracle) | +| Auth rejection paths match TypeScript | Every declared Go-owned write route exercised without and with insufficient credentials/capability through both TS in-process and the Go decision substrate; rejections byte-identical | 3 (authorization oracle) | + +## Seam 1 — the write-ownership ledger (verdict completeness) + +Where the read surface recorded per-route deferrals only in devlog 032, the +write surface needs the same decision **machine-checked**, because a write +route that silently lacks a verdict is exactly how a mutation could bypass its +guard later (spec #3 story 4: "a write route must never be Go-owned before its +authorization is proven" — the ledger makes the inverse visible too: a route +with no verdict cannot be argued about). + +Design: a pure-data ledger in a NEW module +(`src/server/management/write-ownership.ts`), because `route-registry.ts` is +pinned imports-nothing and a verdict ledger is data the core dispatch path +must never load. The ledger lists exactly the **deferred** mutating routes +(the set with neither a `go` marker nor an `exempt` today); go-owned and +exempt verdicts are read from the existing registry fields, never duplicated: + +- `go-owned` — derived from `route.go.relay === "signed"` (12 today). +- `exempt` — derived from the registry `exempt` reason; the existing honesty + tests already police it. +- `deferred` — one ledger row per remaining mutating route, carrying a `why` + (≥ the same non-trivial length bar as exemption reasons) and an optional + owner doc. Deferred is the explicit, reviewed state that replaces "silent + plain". + +A test then proves: the deferred ledger exactly covers the mutating routes +with neither marker (no extra, no missing, no plain); a route cannot gain a +`go` marker while its ledger row still exists (the existing marker-set pin +enforces the 12); adding a mutating route without a ledger row fails. The +exports stay inert data on the core path: nothing under `management-api.ts` +imports the new module. + +Verdict classification for the current 92 silent-plain routes follows the +write batches' own scope (spec #3: config writes, quota/usage, account-pool) +plus the state-source gate (devlog 030) for what stays TS-process-owned. A +route in the ledger is DEFERRED because no batch has claimed it: the three +batches claimed the pure config/account-pool writes (#21 settings trio, #22 +quota/usage, #23 account-pool verbs), and the deferred families are either +TS-process-owned state the flip will own (login flows and dashboard sessions, +OAuth device-code state, provider keychains, native-profile staging state +machines, storage job tables, system restart/tray/update actions, Codex Log +Guard protection) or config writes with live-catalog/registry residue the read +face already defers to the catalog/provider line (devlog 032: models, +providers, aliases, discovery). This is an ownership record, not a claim that +a relay is impossible — the relay executes the legacy TS handler, so the +honest boundary is batch scope and state ownership. The ledger row records the +family-level reason with concrete citations, mirroring 032's per-route table. + +## Seam 2 — state-reset differential coverage for the write surface + +Extend `tests/go-sidecar-parity.test.ts` so every one of the 12 declared +Go-owned write routes has its own state-reset oracle case: reset fixture bytes +→ apply the same mutation through Server A (in-process TS) and Server B +(sidecar attached, real Go) → compare status, headers, and body, then compare +the post-write on-disk config bytes. Where the route mutates no config (a +validation-only or account-store route under the fixture), the case proves the +response path and the no-write-on-error leg instead, and says so in the case +name. + +Delivered as three vectors sets in this file, one per declared write-route +group: + +- `codex-auth account-pool write vectors have a state-reset differential + oracle` — `PUT active` (pins + writes config), `PUT`/`PATCH pool-strategy` + (write config), `POST accounts/clear-cooldown` (no config post-state under + the empty fixture; clears in-process routing health, so the oracle proves + the response path and no-write leg), plus an invalid-strategy 400 leg that + leaves bytes untouched. +- `oauth account-pool and account-store vectors match through Go` — + `PATCH accounts/pool` (persists anthropicAccountPool), `PUT accounts/active` + (account-store route: under the empty fixture no OAuth account exists, so it + is the 404 no-write rejection path), `POST accounts/clear-cooldown` + (in-process health, no config post-state), plus an invalid-strategy 400 leg. + +The write legs also assert the file actually changed (`post-state ≠ initial`) +so a later byte equality is not vacuous. Under the fixture probe: +settings/shadow-call/sidecar-settings/codex-auth active/codex-auth +pool-strategy(PUT/PATCH)/oauth accounts pool(PUT/PATCH) write; the +clear-cooldown verbs return 200 `cleared:false` without writing (they clear +in-process routing health, absent under a fresh fixture); reset-credits/consume +and oauth accounts/active validate against account state and return +400/404 without writing. + +## Seam 3 — authorization gate: rejection parity as a whole-surface property + +The batch tests prove the happy path and the relay proof machinery +(`tests/go-sidecar-write-relay.test.ts`); #18 proves the decision substrate on +arbitrary vectors (`tests/go-auth-parity.test.ts`). Neither proves that the +*migrated write surface's own* rejection paths match TypeScript. Seam 3 adds +the write-surface authorization oracle: + +- For each declared Go-owned write route (the 12 method/path pairs), build the + under-privileged request set the front door admits or rejects on: no + credential, wrong admin token, and a valid system-restart capability aimed + at the write route's own method/path (proving a capability principal minted + for another route can never cross onto the write surface). Each vector set + also carries the admitting admin-token request so a false rejection on the + migrated surface would surface too. +- Feed identical vectors through TS in-process + (`src/server/management-auth.ts`, exactly as `go-auth-parity` does) and + through the Go gate via `ocx-sidecar authcheck`; compare byte-for-byte. + Because Go-owned write routes are forwarded only after front-door admission, + the *public* rejection is the front door's — but the flip will serve these + same routes with the Go gate as the front door, so the substrate decision on + the exact write-surface method/path pairs is what must match. That is what + this seam pins: principal-or-rejection equality on the write surface, not on + a generic vector. +- The relay's own rejection paths (bad claim / replay / expiry / altered body) + stay pinned by `go-sidecar-write-relay.test.ts`; seam 3 does not duplicate + them. + +The authorization oracle lives beside the existing parity tests as a new +`tests/go-write-surface-auth-parity.test.ts` (same skip-if-no-Go guard and +one-Go-process-per-array shape as `go-auth-parity.test.ts`). + +## Security boundary + +- Seam 1 adds an inert data module (`write-ownership.ts`) that nothing under + `management-api.ts` imports; it never routes traffic. `route-registry.ts` + still imports nothing at all. +- Seam 2/3 are differential oracles; they build the sidecar binary and boot + fixture servers but touch only throwaway `OPENCODEX_HOME` fixtures. No + credential, cookie, or browser header crosses a process boundary in the + tested relay paths (existing write-relay contract, unchanged). +- No client credential, dashboard session, or admin token is added to what the + sidecar receives. The Go gate stays substrate; nothing in this ticket makes + it reachable by an unauthenticated public caller. + +## Proof + +- Seam 1: new `tests/write-surface-ownership.test.ts` (ledger exactly covers + the mutating routes with neither marker; ledger rows must name real routes; + family reasons non-trivial; owner doc is a tracked repo file). +- Seam 2: `tests/go-sidecar-parity.test.ts` gains the codex-auth and oauth + vector sets above, completing one state-reset case per declared write route + (12 total) — every declared write route has one; the file passes 15 tests + (13 before this run, two fixture cases added). +- Seam 3: new `tests/go-write-surface-auth-parity.test.ts` (2 tests, 48 + + 24 vectors); TS in-process vs `ocx-sidecar authcheck` byte equality on the + write-surface vectors, plus the 503-unavailable state for the same surface. +- Gates: `go build/vet/test ./...` green under `go/`; focused Bun suites green; + `bun run typecheck` green. + +## Delivery notes (filled in at close) + +Delivered on `fix/ticket-26-write-auth-gate`, rebased onto `f8ab6d510` (dev-go: #21-23 + #24): + +- **035 decision record** (this doc) written first, then the three seams TDD'd + in order, each red-green before moving on. +- **Seam 1**: `src/server/management/write-ownership.ts` — 92 deferred verdict + rows across 15 owning-module families (script-generated from + `MANAGEMENT_ROUTES`, so no transcription drift), each `why` naming the real + state-source / batch-scope reason; `tests/write-surface-ownership.test.ts` + (4 tests) forces exact coverage: the ledger is precisely the mutating set + with neither a `go` marker nor an `exempt`, ledger rows must resolve to real + unclaimed mutating routes, reasons are non-trivial, and the owner doc is a + tracked repo file. Registry untouched; ledger imports nothing but a type. +- **Seam 2**: `tests/go-sidecar-parity.test.ts` gains two fixture cases + (codex-auth vectors: active / pool-strategy PUT+PATCH / clear-cooldown; + oauth vectors: pool PATCH / active-404 / clear-cooldown) with write legs + that assert the file changed so the byte equality is non-vacuous, plus + invalid-strategy 400 no-write legs on both faces. Every declared write route + (12) now has a state-reset differential case; the file passes 15 tests + (13 before, two fixture cases added). +- **Seam 3**: new `tests/go-write-surface-auth-parity.test.ts` (2 tests, + 75 expect calls) — per declared write route, no-credential / wrong-token / + cross-route capability / admin-token vectors through TS `management-auth` + vs `ocx-sidecar authcheck`, byte-identical; plus the same surface under + unavailable auth state (503 identical). +- Resolved mid-flight: the first ledger draft argued deferrals from a + "cannot relay / cannot differential" frame, which the write-relay architecture + refutes (the relay executes the legacy TS handler; `PUT /api/settings` + itself triggers `convergeCodexCatalog`). Rewrote every family reason around + batch scope + state ownership and updated the 035 verdict-classification + paragraph to match. +- Gates on this branch: `bun run typecheck` clean; focused suites + (write-surface-ownership, management-route-registry, go-ownership-plumbing, + go-sidecar-parity, go-sidecar-write-relay, go-auth-parity, + go-write-surface-auth-parity) green; `go vet` + `go test ./...` green under + `go/`. A `bun run test:changed` run showed only concurrency-timeout failures + in unrelated suites (codex-log-guard / lab / responses / abort-race), all of + which pass when re-run alone. From 05f1e96323ae7b7ff0b62546b52649b26dfab346 Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Sun, 6 Sep 2026 14:54:53 +0800 Subject: [PATCH 018/165] test(go): write-surface ownership verdicts + differential/authz oracles (#26) --- src/server/management/write-ownership.ts | 211 +++++++++++++++++++++ tests/go-sidecar-parity.test.ts | 105 ++++++++++ tests/go-write-surface-auth-parity.test.ts | 211 +++++++++++++++++++++ tests/write-surface-ownership.test.ts | 74 ++++++++ 4 files changed, 601 insertions(+) create mode 100644 src/server/management/write-ownership.ts create mode 100644 tests/go-write-surface-auth-parity.test.ts create mode 100644 tests/write-surface-ownership.test.ts diff --git a/src/server/management/write-ownership.ts b/src/server/management/write-ownership.ts new file mode 100644 index 0000000000..5512f1c9cf --- /dev/null +++ b/src/server/management/write-ownership.ts @@ -0,0 +1,211 @@ +import type { HttpMethod } from "./route-registry"; + +/** + * Write-ownership ledger: family-level deferral verdicts for the mutating + * routes that are neither Go-owned nor exempted in the registry (devlog 035). + * + * The registry records the two "claimed" states (a `go` marker, an `exempt` + * reason). This module records the third state explicitly: DEFERRED, one + * family per owning module, with the reason at family granularity -- the same + * deferral shape the read surface keeps in devlog 032, made machine-checked + * here so a mutating route can never be silently plain again. + * + * tests/write-surface-ownership.test.ts proves the ledger exactly covers the + * mutating set that carries neither a `go` marker nor an `exempt`: adding a + * mutating route without a verdict row fails there, and flipping a deferred + * route to `go` while its row still exists fails too (the row is no longer + * "plain"). This module is inert data on the core path: nothing under + * `management-api.ts` imports it, and it imports nothing but a type. + */ +export const WRITE_SURFACE_DEFERRAL_OWNER_DOC = + "devlog/_plan/260905_go_sidecar_takeover/035_write_surface_full_parity_gate.md"; + +export interface WriteSurfaceDeferralFamily { + readonly module: string; + readonly why: string; + readonly routes: ReadonlyArray<{ readonly method: HttpMethod; readonly path: string }>; +} +export const WRITE_SURFACE_DEFERRED_FAMILIES: readonly WriteSurfaceDeferralFamily[] = [ + { + module: "codex/auth-api", + why: "Codex-auth account/login surface outside the account-pool verbs batch #23 claimed (active, pool-strategy, clear-cooldown, reset-credits/consume). Login/code/cancel drive device-code + browser sessions whose state lives in the TS process (codexAuthLoginState map, oauth store); accounts CRUD/alias/pause/priority/auto-switch/failover mutate persisted codexAccounts through in-process reconciliation (reconcileCodexActiveAfterExclusion, persistPausedAccounts). The session + reconciliation state is TS-process-owned, so the flip owns this family; the read face of the same module defers for the same reason (devlog 032/030). Deferred explicitly; batch #23 did not claim it and #26 does not migrate.", + routes: [ + { method: "DELETE", path: "/api/codex-auth/accounts" }, + { method: "POST", path: "/api/codex-auth/accounts" }, + { method: "PUT", path: "/api/codex-auth/accounts/alias" }, + { method: "PUT", path: "/api/codex-auth/accounts/pause" }, + { method: "PUT", path: "/api/codex-auth/accounts/pause-exhausted" }, + { method: "PUT", path: "/api/codex-auth/accounts/priority" }, + { method: "PUT", path: "/api/codex-auth/auto-switch" }, + { method: "PUT", path: "/api/codex-auth/failover" }, + { method: "POST", path: "/api/codex-auth/login" }, + { method: "POST", path: "/api/codex-auth/login/cancel" }, + { method: "POST", path: "/api/codex-auth/login/code" }, + ], + }, + { + module: "codex/native-profile-api", + why: "Native-main profile takeover is a staged state machine living in the TS process: stage/heartbeat/finish/cancel carry stageId + writerToken against an in-process manager, and register/recover/switch orchestrate the running CLI's profile layout (subprocess + file actions). Ownership of the staged-install state machine is the flip's; the read face of this module defers on the same grounds (devlog 032). Deferred explicitly.", + routes: [ + { method: "POST", path: "/api/native-main-profiles/recover" }, + { method: "POST", path: "/api/native-main-profiles/register" }, + { method: "POST", path: "/api/native-main-profiles/stage" }, + { method: "POST", path: "/api/native-main-profiles/stage/cancel" }, + { method: "POST", path: "/api/native-main-profiles/stage/finish" }, + { method: "POST", path: "/api/native-main-profiles/stage/heartbeat" }, + { method: "POST", path: "/api/native-main-profiles/switch" }, + ], + }, + { + module: "server/management-api", + why: "POST /api/stop terminates the serving process itself; its observable effect is the lifecycle of the TS runtime, which is exactly the state that only the serving process owns. It becomes Go-native at the flip, when the Go binary is the serving process. Deferred explicitly.", + routes: [ + { method: "POST", path: "/api/stop" }, + ], + }, + { + module: "server/management/agent-settings-routes", + why: "Agent-settings PUTs/apply verbs persist config.json and then fan out into process-owned convergence (convergeCodexCatalog, syncClaudeAgentDefsBestEffort, autoApplyDesktopBestEffort, runCodexFeaturesCommand) whose result several responses report as catalogRefresh — live convergence the read face of this family defers to the catalog/flip line (devlog 032). These routes were not part of the config-write batch #21 claim set (settings/shadow-call/sidecar-settings), and the catalog-refresh response residue keeps them with the catalog line rather than the pure-config writes. Deferred explicitly.", + routes: [ + { method: "PUT", path: "/api/claude-code" }, + { method: "PUT", path: "/api/claude-desktop" }, + { method: "POST", path: "/api/claude-desktop/apply" }, + { method: "PUT", path: "/api/codex-auth/features/default-mode-request-user-input" }, + { method: "PUT", path: "/api/effort-caps" }, + { method: "POST", path: "/api/grok/apply" }, + { method: "PUT", path: "/api/grok/selection" }, + { method: "PUT", path: "/api/injection-model" }, + { method: "PUT", path: "/api/subagent-model-fallback" }, + { method: "PUT", path: "/api/subagent-models" }, + { method: "PUT", path: "/api/v2" }, + ], + }, + { + module: "server/management/combo-routes", + why: "Combo PUT/DELETE persist config.combos and validate against the live providers + combo model space (comboConfigError with requireEnabledTarget, normalizeComboConfig, clearComboTargetCooldowns, clearComboSelectionState); migration renames resolve through config.providers/model structures the read face still treats as registry/live data (devlog 032). Not claimed by a write batch (#21 config writes claimed the settings trio only); the combo family migrates with the catalog/provider line. Deferred explicitly.", + routes: [ + { method: "DELETE", path: "/api/combos" }, + { method: "PUT", path: "/api/combos" }, + ], + }, + { + module: "server/management/config-routes", + why: "These four POSTs are process or OS actions, not config writes: startup-action runs OS startup-install actions (runStartupInstallAction), windows-tray drives tray actions (runWindowsTrayAction), update/run executes the updater state machine (update/job), and sync triggers convergeCodexCatalog + syncClaudeAgentDefsBestEffort live convergence. Their effects live in the TS process or the OS; the read face defers the same family (devlog 032: update/status job table, windows-tray platform probe). Deferred explicitly.", + routes: [ + { method: "POST", path: "/api/startup-action" }, + { method: "POST", path: "/api/sync" }, + { method: "POST", path: "/api/update/run" }, + { method: "POST", path: "/api/windows-tray" }, + ], + }, + { + module: "server/management/integration-routes", + why: "Client-integration mutations run through the mutation-flight subsystem (src/integrations/mutation-flight): journaled multi-step transactions with busy/refusal semantics, undo tags, and persisted per-client journal state that later GETs advertise. A write's effect spans config, an in-process journal, and a busy lock — TS-process-owned transaction state. The module's read side defers for the same reason. Deferred explicitly.", + routes: [ + { method: "PUT", path: "/api/client-integrations/{clientId}" }, + { method: "POST", path: "/api/client-integrations/restore" }, + ], + }, + { + module: "server/management/logs-usage-routes", + why: "Storage cleanup/preview/policy and trash-restore verbs drive in-process job machinery (runArchivedCleanupJob, previewArchivedCleanup, runRestoreTrashEntryJob, policy-job) mutating the archive filesystem and reporting busy/pinned-thread/fs codes from live job state; debug PUT toggles in-process diagnostics. There is no config post-state to compare and the job/fs semantics are TS-process-owned; the read face already defers the storage family (devlog 032). Deferred explicitly.", + routes: [ + { method: "PUT", path: "/api/debug" }, + { method: "POST", path: "/api/storage/cleanup" }, + { method: "PUT", path: "/api/storage/cleanup-policy" }, + { method: "POST", path: "/api/storage/cleanup-policy/run" }, + { method: "POST", path: "/api/storage/cleanup/preview" }, + { method: "POST", path: "/api/storage/trash/restore" }, + ], + }, + { + module: "server/management/model-routes", + why: "Model-surface writes persist config.json and then rerun live catalog convergence (convergeCodexCatalog), with several responses reporting catalogRefresh (model-routes.ts). custom-models, presets, visibility, discovery, aliases, disabled/subagent/selected models all resolve through the live converged catalog and provider rows that the read face defers to the catalog store (devlog 032). These are config writes with live-catalog residue, not the pure-config trio batch #21 claimed; they migrate with the catalog line. Deferred explicitly.", + routes: [ + { method: "POST", path: "/api/custom-models" }, + { method: "DELETE", path: "/api/custom-models/{id}" }, + { method: "PUT", path: "/api/custom-models/{id}" }, + { method: "PUT", path: "/api/default-aliases" }, + { method: "PUT", path: "/api/disabled-models" }, + { method: "PUT", path: "/api/model-discovery" }, + { method: "POST", path: "/api/model-discovery/acknowledge" }, + { method: "PUT", path: "/api/model-presets" }, + { method: "PUT", path: "/api/model-visibility" }, + { method: "PUT", path: "/api/providers/{provider}/alias" }, + { method: "PUT", path: "/api/providers/{provider}/model-aliases" }, + { method: "PUT", path: "/api/selected-models" }, + ], + }, + { + module: "server/management/native-integration-routes", + why: "Native-integration PUTs persist a desired state (setCodexIntegrationEnabled / setGrokIntegrationEnabled) that the process and the external agent's own config reader later converge; the module's own comment documents persist-then-converge ordering so a crash mid-flight still converges on next start. The effect spans config intent, in-process convergence, and external agent state — not a single byte-comparable write, and not claimed by any write batch. Deferred explicitly.", + routes: [ + { method: "PUT", path: "/api/native-integrations/claude" }, + { method: "PUT", path: "/api/native-integrations/claude-desktop" }, + { method: "PUT", path: "/api/native-integrations/codex" }, + { method: "PUT", path: "/api/native-integrations/grok" }, + ], + }, + { + module: "server/management/oauth-account-routes", + why: "OAuth account/credential surface outside the account-pool verbs batch #23 claimed (oauth accounts active/pool/clear-cooldown). The rest binds TS-process identity state: oauth login/cancel/code run the device-code + browser flow, import and accounts CRUD + logout touch the OAuth account store and live credential state (removeCredential + reconcileLiveStateStores), keys CRUD/rotate/commit and providers/keys + keychain verbs operate OS-keychain-backed secrets with rotation-commit semantics, and accounts/alias mutates the in-process account set. Session/keychain state is the flip's; the module's read face defers for the same reason (devlog 032). Deferred explicitly.", + routes: [ + { method: "DELETE", path: "/api/keys" }, + { method: "PATCH", path: "/api/keys" }, + { method: "POST", path: "/api/keys" }, + { method: "DELETE", path: "/api/keys/rotate" }, + { method: "POST", path: "/api/keys/rotate" }, + { method: "POST", path: "/api/keys/rotate/commit" }, + { method: "DELETE", path: "/api/oauth/accounts" }, + { method: "PUT", path: "/api/oauth/accounts/alias" }, + { method: "POST", path: "/api/oauth/accounts/import" }, + { method: "POST", path: "/api/oauth/login" }, + { method: "POST", path: "/api/oauth/login/cancel" }, + { method: "POST", path: "/api/oauth/login/code" }, + { method: "POST", path: "/api/oauth/logout" }, + { method: "POST", path: "/api/providers/keychain" }, + { method: "DELETE", path: "/api/providers/keys" }, + { method: "POST", path: "/api/providers/keys" }, + { method: "PUT", path: "/api/providers/keys/active" }, + { method: "PUT", path: "/api/providers/keys/alias" }, + ], + }, + { + module: "server/management/provider-routes", + why: "Provider CRUD writes config.providers and then reloads live provider state through the local-provider-reload contract (capability-principal gated, conflict/namespace validation against the running router); /api/providers/test exercises a live provider. The observable result includes in-process router/provider state the read face still treats as registry data (devlog 032: /api/providers defer-registry/live). Not claimed by a write batch; migrates with the provider/registry line. Deferred explicitly.", + routes: [ + { method: "PUT", path: "/api/provider-context-caps" }, + { method: "DELETE", path: "/api/providers" }, + { method: "PATCH", path: "/api/providers" }, + { method: "POST", path: "/api/providers" }, + { method: "POST", path: "/api/providers/test" }, + ], + }, + { + module: "server/management/routing-profile-routes", + why: "Routing-profile PUT/DELETE persist config.profiles and validate against the live provider/model selection surface (modelMap, disabled/subagent references, migrateReferences); dry-run evaluates deterministically over that same live surface. The validation model is registry/live data the read face defers (devlog 032). Not claimed by a write batch; migrates with the router-config line. Deferred explicitly.", + routes: [ + { method: "DELETE", path: "/api/routing-profiles" }, + { method: "PUT", path: "/api/routing-profiles" }, + { method: "POST", path: "/api/routing-profiles/dry-run" }, + ], + }, + { + module: "server/management/storage-log-guard-routes", + why: "Codex-log protect/unprotect/repair/compact mutate filesystem protection state (Codex Log Guard — protectCodexLogs, unprotectCodexLogs, repairCodexLogGuardProtection, compaction) with status codes derived from guard mutation results, not from config. There is no config post-state; the guard's file/protection state is the flip's. Deferred explicitly.", + routes: [ + { method: "POST", path: "/api/storage/codex-logs/compact" }, + { method: "POST", path: "/api/storage/codex-logs/protect" }, + { method: "POST", path: "/api/storage/codex-logs/repair" }, + { method: "POST", path: "/api/storage/codex-logs/unprotect" }, + ], + }, + { + module: "server/management/system-routes", + why: "System restart and codex-restart orchestrate the running TS server / Codex app-server process lifecycle (CODEX_RESTART_PATH, app-server process management). The mutation's effect is the process lifecycle itself, which only the serving process owns; like POST /api/stop these become Go-native at the flip. Deferred explicitly.", + routes: [ + { method: "POST", path: "/api/system/codex-restart" }, + { method: "POST", path: "/api/system/restart" }, + ], + }, +]; diff --git a/tests/go-sidecar-parity.test.ts b/tests/go-sidecar-parity.test.ts index c2e4759fcc..680b76b2a8 100644 --- a/tests/go-sidecar-parity.test.ts +++ b/tests/go-sidecar-parity.test.ts @@ -451,6 +451,111 @@ describe.skipIf(!goAvailable || sidecarBinary === null)("ocx-sidecar differentia } }); + runFixtureTest("codex-auth account-pool write vectors have a state-reset differential oracle", async (token) => { + // Covers the declared Go-owned codex-auth writes not yet pinned by a + // differential: active (pin + write), pool-strategy PUT and PATCH (write), + // and the accounts/clear-cooldown verb (no config post-state under an empty + // fixture — it clears in-process routing health, so the oracle proves the + // response path and the no-write leg instead, exactly as the parity seam + // allows for a route with no on-disk mutation). Each vector runs against a + // freshly started server; the on-disk config is restored between the TS and + // the Go legs so both start from the same reset bytes. + const initial = readFileSync(getConfigPath()); + const vectors: Array<{ method: "PUT" | "PATCH" | "POST"; path: string; body: unknown }> = [ + { method: "PUT", path: "/api/codex-auth/active", body: { accountId: "__main__" } }, + { method: "PUT", path: "/api/codex-auth/pool-strategy", body: { strategy: "quota" } }, + { method: "PATCH", path: "/api/codex-auth/pool-strategy", body: { stickyLimit: 5 } }, + { method: "POST", path: "/api/codex-auth/accounts/clear-cooldown", body: { id: "__main__" } }, + ]; + const tsServer = startServer(0); + const tsResults: Array>> = []; + let tsFinalState: Buffer; + try { + for (const v of vectors) { + tsResults.push(await captureMutation(tsServer, token, v.method, v.path, v.body)); + } + tsFinalState = readFileSync(getConfigPath()); + // The write vectors persisted (active pin + pool strategy); prove the write + // leg actually changed the file, so a later equality is not vacuous. + expect(tsFinalState.equals(initial)).toBe(false); + + // Failure leg: an invalid strategy must be rejected with no write. + const beforeFailure = readFileSync(getConfigPath()); + const failed = await captureMutation(tsServer, token, "PATCH", "/api/codex-auth/pool-strategy", { strategy: "bogus" }); + expect(failed.status).toBe(400); + expect(readFileSync(getConfigPath()).equals(beforeFailure)).toBe(true); + } finally { + await tsServer.stop(true); + } + + writeFileSync(getConfigPath(), initial); + process.env[GO_SIDECAR_BIN_ENV] = sidecarBinary!; + const goServer = startServer(0); + try { + await waitFor(() => activeGoSidecarBaseUrl(), 15_000); + for (let i = 0; i < vectors.length; i++) { + const v = vectors[i]!; + expect(await captureMutation(goServer, token, v.method, v.path, v.body)).toEqual(tsResults[i]!); + } + expect(readFileSync(getConfigPath()).equals(tsFinalState!)).toBe(true); + const beforeFailure = readFileSync(getConfigPath()); + const failed = await captureMutation(goServer, token, "PATCH", "/api/codex-auth/pool-strategy", { strategy: "bogus" }); + expect(failed.status).toBe(400); + expect(readFileSync(getConfigPath()).equals(beforeFailure)).toBe(true); + } finally { + await goServer.stop(true); + } + }); + + runFixtureTest("oauth account-pool and account-store vectors match through Go", async (token) => { + // Covers the declared Go-owned oauth writes not yet pinned by a differential: + // accounts/pool PATCH (persists anthropicAccountPool), accounts/active PUT + // (account-store route — under an empty fixture it is a 404 no-write, which is + // the rejection path a real deployment exercises when the account is gone), + // and accounts/clear-cooldown POST (clears in-process routing health, no + // config post-state). The empty fixture cannot hold a real OAuth account + // (its tokens live in auth.json), so the active verb proves the no-write + // rejection leg and the parity seam documents that explicitly. + const initial = readFileSync(getConfigPath()); + const vectors: Array<{ method: "PUT" | "PATCH" | "POST"; path: string; body: unknown }> = [ + { method: "PATCH", path: "/api/oauth/accounts/pool", body: { provider: "anthropic", strategy: "quota" } }, + { method: "PUT", path: "/api/oauth/accounts/active", body: { provider: "anthropic", accountId: "acc-1" } }, + { method: "POST", path: "/api/oauth/accounts/clear-cooldown", body: { provider: "anthropic", accountId: "acc-1" } }, + ]; + const tsServer = startServer(0); + const tsResults: Array>> = []; + let tsFinalState: Buffer; + try { + for (const v of vectors) { + tsResults.push(await captureMutation(tsServer, token, v.method, v.path, v.body)); + } + tsFinalState = readFileSync(getConfigPath()); + expect(tsResults[1]!.status).toBe(404); // account-store route under an empty fixture + expect(tsFinalState.equals(initial)).toBe(false); // pool PATCH persisted + } finally { + await tsServer.stop(true); + } + + writeFileSync(getConfigPath(), initial); + process.env[GO_SIDECAR_BIN_ENV] = sidecarBinary!; + const goServer = startServer(0); + try { + await waitFor(() => activeGoSidecarBaseUrl(), 15_000); + for (let i = 0; i < vectors.length; i++) { + const v = vectors[i]!; + expect(await captureMutation(goServer, token, v.method, v.path, v.body)).toEqual(tsResults[i]!); + } + expect(readFileSync(getConfigPath()).equals(tsFinalState!)).toBe(true); + // Failure leg: an invalid strategy is rejected with no write on both faces. + const beforeFailure = readFileSync(getConfigPath()); + const failed = await captureMutation(goServer, token, "PATCH", "/api/oauth/accounts/pool", { provider: "anthropic", strategy: "bogus" }); + expect(failed.status).toBe(400); + expect(readFileSync(getConfigPath()).equals(beforeFailure)).toBe(true); + } finally { + await goServer.stop(true); + } + }); + runFixtureTest("an unexpected sidecar exit deregisters the forwarder and health falls back in-process", async (token) => { // #11: a crash must surface, not fall silent. The supervisor deregisters the // forwarder on an unexpected child exit, so the next health response flips diff --git a/tests/go-write-surface-auth-parity.test.ts b/tests/go-write-surface-auth-parity.test.ts new file mode 100644 index 0000000000..3e836e1c73 --- /dev/null +++ b/tests/go-write-surface-auth-parity.test.ts @@ -0,0 +1,211 @@ +import { describe, expect, test } from "bun:test"; +import { mkdtempSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { randomBytes } from "node:crypto"; +import { managementPrincipal, requireManagementAuth, type ManagementAuthState, type LocalManagementAuthContext } from "../src/server/management-auth"; +import type { GuiSessionState, GuiSessionRecord } from "../src/server/gui-session"; +import { createSystemRestartCapability, SYSTEM_RESTART_PATH } from "../src/lib/system-restart-contract"; +import { GO_OWNED_MANAGEMENT_ROUTES } from "../src/server/management/route-registry"; +import type { OcxConfig } from "../src/types"; + +/** + * Write-surface authorization differential oracle (ticket #26, seam 3 — devlog 035). + * + * Spec #3 story 4: a write route must never be Go-owned before its + * authorization is proven. The batch tests prove the relay happy path and the + * claim machinery; ticket #18 proves the Go gate on generic vectors. Neither + * proves that the *migrated write surface's own* method/path pairs reject and + * admit identically — which is the contract the flip consumes when the Go gate + * becomes the front door for exactly these routes. + * + * This oracle feeds the same vectors through `src/server/management-auth.ts` + * (in-process) and through `ocx-sidecar authcheck` (one Go process per case, + * exactly like go-auth-parity), over every declared Go-owned write route: + * + * - no credential -> rejected, bytes identical + * - wrong token -> rejected, bytes identical + * - admin token -> admitted as the same principal + * - a valid system-restart capability aimed at the write route's own + * method/path (not /api/system/restart) -> rejected identically, proving a + * capability principal minted elsewhere cannot cross onto the write surface + * + * The declared route set drives the loop (plus the existing 12-route pin), so + * adding a Go-owned write route without an authorization vector fails here. + */ + +const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); + +function goToolchainAvailable(): boolean { + return Bun.spawnSync(["go", "version"], { stdout: "ignore", stderr: "ignore" }).success; +} + +function buildSidecarBinary(): string { + const dir = mkdtempSync(join(tmpdir(), "ocx-go-write-auth-")); + const binPath = join(dir, process.platform === "win32" ? "ocx-sidecar.exe" : "ocx-sidecar"); + const build = Bun.spawnSync(["go", "build", "-o", binPath, "./cmd/ocx-sidecar"], { + cwd: join(repoRoot, "go"), + env: { ...process.env, CGO_ENABLED: "0" }, + stdout: "pipe", + stderr: "pipe", + }); + if (build.exitCode !== 0) { + throw new Error(`go build ./cmd/ocx-sidecar failed (${build.exitCode}):\n${new TextDecoder().decode(build.stderr)}`); + } + return binPath; +} + +const goAvailable = goToolchainAvailable(); +const sidecarBinary: string | null = goAvailable ? buildSidecarBinary() : null; +const describeGo = goAvailable ? describe : describe.skip; + +const PID = 4242; +const PORT = 10100; +const SECRET = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG"; +const ADMIN_TOKEN = "ocx_admin_abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG"; + +function b64url43(): string { + return randomBytes(32).toString("base64url"); +} + +const LOOPBACK_URL = `http://127.0.0.1:${PORT}`; + +interface Vector { + request: { url: string; method: string; headers: Record }; +} + +interface CaseInput { + state: { available: boolean; token?: string; source?: string; reason?: string }; + config: { hostname: string }; + local: { attestationSecret: string; pid: number; port: number }; + vectors: Vector[]; +} + +interface Decision { + admitted: boolean; + principal: string | null; + rejection: { status: number; body: string } | null; +} + +function toTSConfig(view: CaseInput["config"]): OcxConfig { + return { hostname: view.hostname } as unknown as OcxConfig; +} + +function tsState(input: CaseInput): { state: ManagementAuthState; guiState: GuiSessionState } { + const sessions = new Map(); + return { + state: { + available: input.state.available, + token: input.state.token ?? "", + source: (input.state.source as "environment" | "file") ?? "environment", + ...(input.state.reason !== undefined ? { reason: input.state.reason } : {}), + sessions, + }, + guiState: { sessions, pairingGrants: new Map() }, + }; +} + +async function tsDecisions(input: CaseInput): Promise { + const config = toTSConfig(input.config); + const local: LocalManagementAuthContext = { attestationSecret: input.local.attestationSecret, pid: input.local.pid, port: input.local.port }; + const { state } = tsState(input); + const decisions: Decision[] = []; + for (const vector of input.vectors) { + const req = new Request(vector.request.url, { method: vector.request.method, headers: vector.request.headers }); + const principal = managementPrincipal(req, state, config, local); + if (principal) { + decisions.push({ admitted: true, principal, rejection: null }); + continue; + } + const gate = requireManagementAuth(req, state, config, local); + decisions.push({ admitted: false, principal: null, rejection: { status: gate!.status, body: await gate!.text() } }); + } + return decisions; +} + +function goDecisions(input: CaseInput): Decision[] { + const flat = input.vectors.map((vector) => ({ + request: vector.request, + state: input.state, + config: input.config, + local: input.local, + })); + const result = Bun.spawnSync([sidecarBinary!, "authcheck", JSON.stringify(flat)], { + env: { ...process.env, CGO_ENABLED: "0" }, + stdout: "pipe", + stderr: "pipe", + }); + if (result.exitCode !== 0) { + throw new Error(`ocx-sidecar authcheck failed (${result.exitCode}):\n${new TextDecoder().decode(result.stderr)}`); + } + return JSON.parse(new TextDecoder().decode(result.stdout)) as Decision[]; +} + +async function runCase(input: CaseInput): Promise { + const ts = await tsDecisions(input); + const go = goDecisions(input); + expect(go.length).toBe(ts.length); + for (let i = 0; i < ts.length; i++) { + expect(go[i], `vector ${i} divergence`).toEqual(ts[i]); + } +} + +function vector(method: string, path: string, headers: Record): Vector { + return { request: { url: `${LOOPBACK_URL}${path}`, method, headers } }; +} + +function vectorSetFor(method: string, path: string, nonce: string): Vector[] { + // A capability minted for POST /api/system/restart, aimed at this write route: + // it must NOT admit (a capability principal never crosses onto the write surface). + const cap = createSystemRestartCapability(SECRET, nonce, "POST", SYSTEM_RESTART_PATH, PID, PORT)!; + const restartHeaders = { + host: `127.0.0.1:${PORT}`, + "x-opencodex-restart-expected-pid": String(PID), + "x-opencodex-restart-nonce": nonce, + "x-opencodex-restart-capability": cap, + }; + return [ + vector(method, path, { host: `127.0.0.1:${PORT}` }), // no credential + vector(method, path, { host: `127.0.0.1:${PORT}`, "x-opencodex-api-key": "ocx_admin_wrongtokenwrongtokenwrongtokenwrongtokenwrongto" }), // wrong token + vector(method, path, { host: `127.0.0.1:${PORT}`, "x-opencodex-api-key": ADMIN_TOKEN }), // admin token admits + vector(method, path, restartHeaders), // capability for another route must not cross + ]; +} + +describeGo("write-surface authorization differential oracle (ticket #26, seam 3)", () => { + test("every declared Go-owned write route admits and rejects byte-identically to TypeScript", async () => { + const writes = GO_OWNED_MANAGEMENT_ROUTES.filter(route => route.mutates); + // The 12-route pin (go-ownership-plumbing) owns the exact set; this test loops + // over whatever is declared so a new write route cannot land without vectors. + expect(writes.length).toBe(12); + + const vectors: Vector[] = []; + for (const route of writes) { + vectors.push(...vectorSetFor(route.method, route.path, b64url43())); + } + + await runCase({ + state: { available: true, token: ADMIN_TOKEN, source: "environment" }, + config: { hostname: "127.0.0.1" }, + local: { attestationSecret: SECRET, pid: PID, port: PORT }, + vectors, + }); + }); + + test("the write surface is still gated when auth state is unavailable (503 bytes identical)", async () => { + const writes = GO_OWNED_MANAGEMENT_ROUTES.filter(route => route.mutates); + const vectors: Vector[] = writes.flatMap(route => + [ + vector(route.method, route.path, { host: `127.0.0.1:${PORT}` }), + vector(route.method, route.path, { host: `127.0.0.1:${PORT}`, "x-opencodex-api-key": ADMIN_TOKEN }), + ], + ); + await runCase({ + state: { available: false, reason: "management token initialization failed" }, + config: { hostname: "127.0.0.1" }, + local: { attestationSecret: SECRET, pid: PID, port: PORT }, + vectors, + }); + }); +}); diff --git a/tests/write-surface-ownership.test.ts b/tests/write-surface-ownership.test.ts new file mode 100644 index 0000000000..bdf3ee65ee --- /dev/null +++ b/tests/write-surface-ownership.test.ts @@ -0,0 +1,74 @@ +import { describe, expect, test } from "bun:test"; +import { existsSync } from "node:fs"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { MANAGEMENT_ROUTES } from "../src/server/management/route-registry"; +import { + WRITE_SURFACE_DEFERRAL_OWNER_DOC, + WRITE_SURFACE_DEFERRED_FAMILIES, +} from "../src/server/management/write-ownership"; + +const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); + +const key = (method: string, path: string) => `${method} ${path}`; + +/** + * Write-ownership verdict completeness (ticket #26, seam 1 — devlog 035). + * + * Every mutating management route must carry exactly one ownership verdict: + * a `go` marker (Go-owned), an `exempt` reason (CLI-parity vocabulary), or a + * deferred row in WRITE_SURFACE_DEFERRED_FAMILIES. The first two are already + * policed elsewhere; this suite polices the third so the mutating surface has + * no silent-plain state — a route with neither marker and no ledger row is + * unarguable, which is precisely how a mutation could later bypass its guard. + */ +describe("every mutating route carries a write-ownership verdict", () => { + test("the deferred ledger exactly covers the mutating routes with neither a go marker nor an exemption", () => { + const plain = MANAGEMENT_ROUTES.filter(r => r.mutates && !r.go && !r.exempt); + expect(plain.length).toBeGreaterThan(0); + + const ledgerRoutes = WRITE_SURFACE_DEFERRED_FAMILIES.flatMap(family => + family.routes.map(r => ({ module: family.module, method: r.method, path: r.path })), + ); + expect(ledgerRoutes.length).toBe(plain.length); + + const ledgerKeys = new Set(ledgerRoutes.map(r => key(r.method, r.path))); + expect(ledgerKeys.size).toBe(ledgerRoutes.length); // no duplicate ledger rows + for (const route of plain) { + expect(ledgerKeys.has(key(route.method, route.path)), `no verdict row for ${key(route.method, route.path)}`).toBe(true); + } + }); + + test("every ledger row names a real mutating route with neither marker (no ghosts, no mislabeled reads)", () => { + for (const family of WRITE_SURFACE_DEFERRED_FAMILIES) { + for (const route of family.routes) { + const match = MANAGEMENT_ROUTES.find(r => + r.method === route.method && r.path === route.path, + ); + expect(match, `${key(route.method, route.path)} is not a declared management route`).toBeDefined(); + expect(match!.module).toBe(family.module); // verdict lives under the owning module + expect(match!.mutates).toBe(true); + expect(match!.go).toBeUndefined(); // a go marker must drop its ledger row + expect(match!.exempt).toBeUndefined(); // an exemption must drop its ledger row + } + } + }); + + test("every deferred family names its owning module and records a non-trivial reason", () => { + const thin = WRITE_SURFACE_DEFERRED_FAMILIES + .filter(f => f.why.trim().length < 40) + .map(f => f.module); + expect(thin).toEqual([]); + for (const family of WRITE_SURFACE_DEFERRED_FAMILIES) { + expect(family.module.length).toBeGreaterThan(0); + expect(family.routes.length).toBeGreaterThan(0); + } + }); + + test("the deferral owner doc is a TRACKED repository file (same rule as deferred-verb exemptions)", () => { + // The doc is deliberately a repository file rather than the goalplan, which is + // gitignored -- a test reading machine-local state would pass here and find + // nothing in CI. + expect(existsSync(join(repoRoot, WRITE_SURFACE_DEFERRAL_OWNER_DOC))).toBe(true); + }); +}); From 5088d798096fafdbc0f2ddd024ddbc78c99df39f Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Sun, 6 Sep 2026 15:24:45 +0800 Subject: [PATCH 019/165] docs(go): mark #26 write-surface parity gate implemented on dev-go --- .../035_write_surface_full_parity_gate.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/devlog/_plan/260905_go_sidecar_takeover/035_write_surface_full_parity_gate.md b/devlog/_plan/260905_go_sidecar_takeover/035_write_surface_full_parity_gate.md index fc55ee5deb..5a9e6a69f4 100644 --- a/devlog/_plan/260905_go_sidecar_takeover/035_write_surface_full_parity_gate.md +++ b/devlog/_plan/260905_go_sidecar_takeover/035_write_surface_full_parity_gate.md @@ -2,7 +2,7 @@ Unit: `260905_go_sidecar_takeover` Date: 2026-09-06 -Status: in progress on `fix/ticket-26-write-auth-gate` (rebase-follows dev-go @ `f8ab6d510`, which carries #24) +Status: implemented on `dev-go` at `05f1e9632` (merge `e4d684d11` + `05f1e9632` on top of #25 config test `befb594d2`) Ticket: [#26](https://github.com/waxiangzi/opencodex/issues/26) (spec #3 capstone: "Write-surface full parity + authorization gate") Parent spec: [#3](https://github.com/waxiangzi/opencodex/issues/3) (increment 3: write surface) Blocked-by (#21/#22/#23): closed on `dev-go` — `eb4292e8f` relays the three From 552d903f886c4d9c2ac3fa96b2c596474a210cf8 Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Sun, 6 Sep 2026 15:32:31 +0800 Subject: [PATCH 020/165] test(go): gate read-surface ownership matrix --- .../management/read-surface-ownership.ts | 81 +++++++++++++++++++ tests/go-ownership-plumbing.test.ts | 58 +++---------- tests/go-sidecar-parity.test.ts | 37 +++++++++ tests/read-surface-diff-matrix.test.ts | 49 +++++++++++ 4 files changed, 179 insertions(+), 46 deletions(-) create mode 100644 src/server/management/read-surface-ownership.ts create mode 100644 tests/read-surface-diff-matrix.test.ts diff --git a/src/server/management/read-surface-ownership.ts b/src/server/management/read-surface-ownership.ts new file mode 100644 index 0000000000..e24276e6a1 --- /dev/null +++ b/src/server/management/read-surface-ownership.ts @@ -0,0 +1,81 @@ +import type { HttpMethod } from "./route-registry"; + +/** + * Pre-flip transition verdicts for every management read. The route registry + * remains the inventory authority; this inert ledger makes each route either + * Go-owned now with a wire fixture or explicitly deferred to the runtime flip. + * A sidecar must not invent TypeScript process state. + */ +export const READ_SURFACE_DIFF_MATRIX_OWNER_DOC = + "devlog/_plan/260905_go_sidecar_takeover/032_read_batches_decision_record.md"; + +export type ReadSurfaceStateSource = + | "disk" + | "environment" + | "os" + | "serving-process" + | "external-state"; + +export type ReadSurfaceTransition = "go-now" | "go-at-flip"; + +export interface ReadSurfaceDiffMatrixEntry { + readonly method: HttpMethod; + readonly path: string; + readonly module: string; + readonly transition: ReadSurfaceTransition; + readonly stateSources: readonly ReadSurfaceStateSource[]; + readonly parityFixture?: "default-get"; + readonly rationale?: string; +} + +interface DeferredReadSurfaceFamily { + readonly module: string; + readonly stateSources: readonly ReadSurfaceStateSource[]; + readonly rationale: string; + readonly routes: ReadonlyArray; +} + +const deferred = (families: readonly DeferredReadSurfaceFamily[]): readonly ReadSurfaceDiffMatrixEntry[] => + families.flatMap(family => family.routes.map(([method, path]) => ({ + method, + path, + module: family.module, + transition: "go-at-flip" as const, + stateSources: family.stateSources, + rationale: family.rationale, + }))); + +const flip = "The response includes TypeScript serving-process state, cached discovery, or an external subsystem whose byte contract belongs to the Go serving binary at the runtime flip; a pre-flip sidecar must not invent a snapshot."; +const lab = "Compatibility Lab remains a last-migrated subsystem. Its local SQLite transport and optional activation state move with the Go serving binary at the runtime flip, rather than creating a second Lab implementation in the sidecar."; + +export const READ_SURFACE_DIFF_MATRIX: readonly ReadSurfaceDiffMatrixEntry[] = [ + { method: "GET", path: "/api/system/health", module: "server/management/system-routes", transition: "go-now", stateSources: ["environment", "serving-process"], parityFixture: "default-get" }, + { method: "GET", path: "/api/shadow-call-settings", module: "server/management/config-routes", transition: "go-now", stateSources: ["disk"], parityFixture: "default-get" }, + { method: "GET", path: "/api/custom-models", module: "server/management/model-routes", transition: "go-now", stateSources: ["disk"], parityFixture: "default-get" }, + // The Go handler owns the public route but bridges the parent-owned quota + // cache until the flip; ticket #20's dedicated parity vectors cover refresh. + { method: "GET", path: "/api/provider-quotas", module: "server/management/provider-routes", transition: "go-now", stateSources: ["serving-process", "external-state"], parityFixture: "default-get" }, + ...deferred([ + { module: "codex/auth-api", stateSources: ["disk", "serving-process", "external-state"], rationale: flip, routes: [["GET", "/api/codex-auth/accounts"], ["GET", "/api/codex-auth/active"], ["GET", "/api/codex-auth/login-status"], ["GET", "/api/codex-auth/quota"], ["GET", "/api/codex-auth/reset-credits"]] }, + { module: "codex/native-profile-api", stateSources: ["disk", "os", "serving-process"], rationale: flip, routes: [["GET", "/api/native-main-profiles"], ["GET", "/api/native-main-profiles/doctor"]] }, + { module: "server/management/agent-settings-routes", stateSources: ["disk", "serving-process", "external-state"], rationale: flip, routes: [["GET", "/api/claude-code"], ["GET", "/api/claude-desktop"], ["GET", "/api/claude-desktop/status"], ["GET", "/api/codex-auth/features/default-mode-request-user-input"], ["GET", "/api/effort-caps"], ["GET", "/api/grok"], ["GET", "/api/injection-model"], ["GET", "/api/subagent-model-fallback"], ["GET", "/api/subagent-models"], ["GET", "/api/v2"]] }, + { module: "server/management/codex-prompt-routes", stateSources: ["disk", "serving-process"], rationale: flip, routes: [["GET", "/api/codex-prompt"], ["GET", "/api/codex-prompt/text"]] }, + { module: "server/management/combo-routes", stateSources: ["disk", "serving-process"], rationale: flip, routes: [["GET", "/api/combos"]] }, + { module: "server/management/config-routes", stateSources: ["disk", "os", "serving-process", "external-state"], rationale: flip, routes: [["GET", "/api/config"], ["GET", "/api/diagnostics/project-config"], ["GET", "/api/settings"], ["GET", "/api/sidecar-settings"], ["GET", "/api/startup-health"], ["GET", "/api/update/check"], ["GET", "/api/update/status"], ["GET", "/api/windows-tray"]] }, + { module: "server/management/integration-routes", stateSources: ["disk", "serving-process", "external-state"], rationale: flip, routes: [["GET", "/api/client-integrations"], ["GET", "/api/client-integrations/journal"], ["GET", "/api/client-integrations/{clientId}"]] }, + { module: "server/management/lab-automation-routes", stateSources: ["disk", "serving-process"], rationale: lab, routes: [["GET", "/api/lab/automation"], ["GET", "/api/lab/automation/runs"]] }, + { module: "server/management/lab-routes", stateSources: ["disk", "serving-process"], rationale: lab, routes: [["GET", "/api/lab/artifacts"], ["GET", "/api/lab/catalog"], ["GET", "/api/lab/events"], ["GET", "/api/lab/observations"], ["GET", "/api/lab/production-signals"], ["GET", "/api/lab/public/community"], ["GET", "/api/lab/status"], ["GET", "/api/lab/subjects"], ["GET", "/api/lab/verdicts"], ["GET", "/api/lab/subjects/{id}"], ["GET", "/api/lab/events/{id}"], ["GET", "/api/lab/artifacts/{digest}"]] }, + { module: "server/management/logs-usage-routes", stateSources: ["disk", "serving-process"], rationale: flip, routes: [["GET", "/api/claude/inbound-debug"], ["GET", "/api/debug"], ["GET", "/api/debug/injection-logs"], ["GET", "/api/debug/logs"], ["GET", "/api/debug/usage-logs"], ["GET", "/api/logs"], ["GET", "/api/storage/cleanup-policy"], ["GET", "/api/storage/cleanup-policy/test-stream"], ["GET", "/api/storage/trash"], ["GET", "/api/storage/trash/restore/test-stream"], ["GET", "/api/usage"]] }, + { module: "server/management/model-routes", stateSources: ["disk", "serving-process", "external-state"], rationale: flip, routes: [["GET", "/api/aliases"], ["GET", "/api/catalog"], ["GET", "/api/client-config"], ["GET", "/api/model-discovery"], ["GET", "/api/model-presets"], ["GET", "/api/models"], ["GET", "/api/selected-models"]] }, + { module: "server/management/native-integration-routes", stateSources: ["disk", "serving-process", "external-state"], rationale: flip, routes: [["GET", "/api/native-integrations"]] }, + { module: "server/management/cursor-integration-routes", stateSources: ["disk", "serving-process", "external-state"], rationale: flip, routes: [["GET", "/api/native-integrations/cursor"]] }, + { module: "server/management/oauth-account-routes", stateSources: ["disk", "os", "serving-process", "external-state"], rationale: flip, routes: [["GET", "/api/key-providers"], ["GET", "/api/keys"], ["GET", "/api/oauth/accounts"], ["GET", "/api/oauth/accounts/pool"], ["GET", "/api/oauth/providers"], ["GET", "/api/oauth/status"], ["GET", "/api/providers/keys"], ["GET", "/api/providers/keychain"]] }, + { module: "server/management/provider-routes", stateSources: ["disk", "serving-process", "external-state"], rationale: flip, routes: [["GET", "/api/provider-context-caps"], ["GET", "/api/provider-presets"], ["GET", "/api/provider-request-pacing"], ["GET", "/api/providers"]] }, + { module: "server/management/request-history-routes", stateSources: ["disk", "serving-process"], rationale: flip, routes: [["GET", "/api/request-history"], ["GET", "/api/request-history/{id}"], ["GET", "/api/request-history/{id}/route-decision"]] }, + { module: "server/management/routing-profile-routes", stateSources: ["disk", "serving-process"], rationale: flip, routes: [["GET", "/api/routing-profiles"]] }, + { module: "server/management/sidebar-routes", stateSources: ["serving-process", "external-state"], rationale: flip, routes: [["GET", "/api/github/star"], ["GET", "/api/update/badge"]] }, + { module: "server/management/storage-log-guard-routes", stateSources: ["disk", "os", "serving-process"], rationale: flip, routes: [["GET", "/api/storage/codex-logs"], ["GET", "/api/storage"]] }, + { module: "server/management/system-routes", stateSources: ["os", "serving-process"], rationale: flip, routes: [["GET", "/api/system/memory"], ["GET", "/api/system/windows-replace-retries"], ["GET", "/api/system/codex-app-server"]] }, + { module: "server/management/routing-analytics-routes", stateSources: ["disk", "serving-process"], rationale: flip, routes: [["GET", "/api/routing-analytics"]] }, + ]), +]; diff --git a/tests/go-ownership-plumbing.test.ts b/tests/go-ownership-plumbing.test.ts index cdc4319187..c76ed6f4e4 100644 --- a/tests/go-ownership-plumbing.test.ts +++ b/tests/go-ownership-plumbing.test.ts @@ -11,6 +11,7 @@ import { MANAGEMENT_ROUTES, findGoOwnedManagementRoute, } from "../src/server/management/route-registry"; +import { READ_SURFACE_DIFF_MATRIX } from "../src/server/management/read-surface-ownership"; import { hasGoOwnedRouteForwarder, resetGoOwnedRouteForwarderForTests, @@ -125,52 +126,17 @@ async function getJson(token: string, server: { url: URL }, pathname: string): P // --------------------------------------------------------------------------- describe("ADR-0008 ownership markers are typed read/write", () => { - test("the declared Go-owned surface includes bounded write batches", () => { - // Pin the migrated set so an accidental marker flip on another read route - // fails here instead of silently changing what the proxy serves. Adding a - // real migration updates this list deliberately. Health reports the serving - // process's own pid/uptime and declares them volatile; shadow-call-settings - // and custom-models are pure functions of config.json (the latter a raw - // JSON.stringify echo) and declare NO volatile field, which means the - // oracle compares their bytes with no normalisation at all. - const byPath = new Map(GO_OWNED_MANAGEMENT_ROUTES.map(r => [r.path, r])); - expect([...byPath.keys()].sort()).toEqual([ - "/api/codex-auth/accounts/clear-cooldown", - "/api/codex-auth/active", - "/api/codex-auth/pool-strategy", - "/api/codex-auth/reset-credits/consume", - "/api/custom-models", - "/api/oauth/accounts/active", - "/api/oauth/accounts/clear-cooldown", - "/api/oauth/accounts/pool", - "/api/provider-quotas", - "/api/settings", - "/api/shadow-call-settings", - "/api/sidecar-settings", - "/api/system/health", - ]); - const health = byPath.get("/api/system/health")!; - expect(health.method).toBe("GET"); - expect(health.mutates).toBe(false); - expect(health.module).toBe("server/management/system-routes"); - expect(health.go.volatileFields).toEqual(["pid", "uptime"]); - const shadowCall = GO_OWNED_MANAGEMENT_ROUTES.find(route => ( - route.method === "GET" && route.path === "/api/shadow-call-settings" - ))!; - expect(shadowCall.method).toBe("GET"); - expect(shadowCall.mutates).toBe(false); - expect(shadowCall.module).toBe("server/management/config-routes"); - expect(shadowCall.go.volatileFields).toEqual([]); - const customModels = byPath.get("/api/custom-models")!; - expect(customModels.method).toBe("GET"); - expect(customModels.mutates).toBe(false); - expect(customModels.module).toBe("server/management/model-routes"); - expect(customModels.go.volatileFields).toEqual([]); - const providerQuotas = byPath.get("/api/provider-quotas")!; - expect(providerQuotas.method).toBe("GET"); - expect(providerQuotas.mutates).toBe(false); - expect(providerQuotas.module).toBe("server/management/provider-routes"); - expect(providerQuotas.go.volatileFields).toEqual(["generatedAt"]); + test("the declared Go-owned surface includes matrix-backed reads and bounded write batches", () => { + const reads = GO_OWNED_MANAGEMENT_ROUTES.filter(route => !route.mutates); + const matrixReads = READ_SURFACE_DIFF_MATRIX.filter(row => row.transition === "go-now"); + expect(reads.map(route => route.method + " " + route.path).sort()).toEqual( + matrixReads.map(row => row.method + " " + row.path).sort(), + ); + for (const route of reads) { + const row = matrixReads.find(candidate => candidate.method === route.method && candidate.path === route.path)!; + expect(route.module).toBe(row.module); + expect(row.parityFixture).toBe("default-get"); + } const writes = GO_OWNED_MANAGEMENT_ROUTES.filter(route => route.mutates); expect(writes.map(route => `${route.method} ${route.path}`).sort()).toEqual([ "PATCH /api/codex-auth/pool-strategy", diff --git a/tests/go-sidecar-parity.test.ts b/tests/go-sidecar-parity.test.ts index c2e4759fcc..8b43fcd44d 100644 --- a/tests/go-sidecar-parity.test.ts +++ b/tests/go-sidecar-parity.test.ts @@ -9,6 +9,7 @@ import { getConfigPath } from "../src/config/paths"; import { startServer } from "../src/server"; import { VERSION } from "../src/server/management-api"; import { GO_OWNED_MANAGEMENT_ROUTES } from "../src/server/management/route-registry"; +import { READ_SURFACE_DIFF_MATRIX } from "../src/server/management/read-surface-ownership"; import { GO_SIDECAR_BIN_ENV, activeGoSidecarBaseUrl, @@ -324,6 +325,42 @@ describe.skipIf(!goAvailable || sidecarBinary === null)("ocx-sidecar differentia } }); + runFixtureTest("every go-now read matrix row has a real default wire-parity vector", async (token) => { + // A marker flip requires a go-now matrix row. This loop gives every such + // row a real TS-server versus Go-sidecar wire comparison; richer route + // cases below retain their edge vectors. + const rows = READ_SURFACE_DIFF_MATRIX.filter(row => row.transition === "go-now"); + const declared = GO_OWNED_MANAGEMENT_ROUTES.filter(route => !route.mutates); + expect(rows.map(row => row.method + " " + row.path).sort()).toEqual( + declared.map(route => route.method + " " + route.path).sort(), + ); + + const serverA = startServer(0); + try { + process.env[GO_SIDECAR_BIN_ENV] = sidecarBinary!; + const serverB = startServer(0); + try { + await waitFor(() => activeGoSidecarBaseUrl(), 15_000); + for (const row of rows) { + expect(row.parityFixture).toBe("default-get"); + const ts = await captureJson(serverA, token, row.path); + const go = await captureJson(serverB, token, row.path); + const route = declared.find(candidate => candidate.method === row.method && candidate.path === row.path)!; + const label = row.method + " " + row.path; + expect(go.status, label + " status").toBe(ts.status); + expect(go.contentType, label + " content type").toBe(ts.contentType); + expect(normaliseBody(go.body, route.go.volatileFields), label + " body").toBe( + normaliseBody(ts.body, route.go.volatileFields), + ); + } + } finally { + await serverB.stop(true); + } + } finally { + await serverA.stop(true); + } + }); + runFixtureTest("a missing sidecar binary is a warned no-op, not a startup failure", async (token) => { process.env[GO_SIDECAR_BIN_ENV] = join(testHome, "does-not-exist-ocx-sidecar"); const server = startServer(0); diff --git a/tests/read-surface-diff-matrix.test.ts b/tests/read-surface-diff-matrix.test.ts new file mode 100644 index 0000000000..3ae4e770ef --- /dev/null +++ b/tests/read-surface-diff-matrix.test.ts @@ -0,0 +1,49 @@ +import { describe, expect, test } from "bun:test"; +import { existsSync } from "node:fs"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { MANAGEMENT_ROUTES } from "../src/server/management/route-registry"; +import { READ_SURFACE_DIFF_MATRIX, READ_SURFACE_DIFF_MATRIX_OWNER_DOC } from "../src/server/management/read-surface-ownership"; + +const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); +const key = (method: string, path: string) => method + " " + path; + +describe("read-surface differential matrix (ticket #25)", () => { + test("gives every management read route exactly one Go transition verdict", () => { + const reads = MANAGEMENT_ROUTES.filter(route => !route.mutates); + const matrixKeys = READ_SURFACE_DIFF_MATRIX.map(row => key(row.method, row.path)); + expect(new Set(matrixKeys).size).toBe(matrixKeys.length); + expect(new Set(matrixKeys)).toEqual(new Set(reads.map(route => key(route.method, route.path)))); + expect(READ_SURFACE_DIFF_MATRIX).toHaveLength(reads.length); + }); + + test("keeps each verdict attached to its registry owner and state source", () => { + for (const row of READ_SURFACE_DIFF_MATRIX) { + const route = MANAGEMENT_ROUTES.find(candidate => candidate.method === row.method && candidate.path === row.path); + expect(route, key(row.method, row.path) + " is absent from the registry").toBeDefined(); + expect(route!.mutates).toBe(false); + expect(route!.module).toBe(row.module); + expect(row.stateSources.length, key(row.method, row.path)).toBeGreaterThan(0); + if (row.transition === "go-now") { + expect(route!.go, key(row.method, row.path) + " must carry the Go marker").toBeDefined(); + expect(row.parityFixture).toBe("default-get"); + expect(row.rationale).toBeUndefined(); + } else { + expect(route!.go, key(row.method, row.path) + " cannot be both deferred and Go-owned").toBeUndefined(); + expect(row.rationale?.trim().length, key(row.method, row.path)).toBeGreaterThanOrEqual(40); + expect(row.parityFixture).toBeUndefined(); + } + } + }); + + test("the Go marker and go-now matrix verdicts are a bidirectional set", () => { + const matrixGoNow = READ_SURFACE_DIFF_MATRIX.filter(row => row.transition === "go-now").map(row => key(row.method, row.path)).sort(); + const registryGoReads = MANAGEMENT_ROUTES.filter(route => !route.mutates && route.go).map(route => key(route.method, route.path)).sort(); + expect(matrixGoNow).toEqual(registryGoReads); + expect(matrixGoNow).toHaveLength(4); + }); + + test("records runtime-flip evidence in tracked repository documentation", () => { + expect(existsSync(join(repoRoot, READ_SURFACE_DIFF_MATRIX_OWNER_DOC))).toBe(true); + }); +}); From d29a07e38fb9adf8a0b26c1f2ca561e637ddceaf Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Sun, 6 Sep 2026 16:41:27 +0800 Subject: [PATCH 021/165] feat(go): own model discovery read --- .../032_read_batches_decision_record.md | 9 +- go/internal/config/ordered.go | 83 ++++++++++++ go/internal/config/ordered_test.go | 19 +++ go/internal/sidecar/model_discovery.go | 123 ++++++++++++++++++ go/internal/sidecar/sidecar.go | 20 ++- go/internal/sidecar/sidecar_test.go | 15 +++ .../management/read-surface-ownership.ts | 3 +- src/server/management/route-registry.ts | 4 +- tests/go-sidecar-parity.test.ts | 29 +++++ tests/read-surface-diff-matrix.test.ts | 2 +- 10 files changed, 297 insertions(+), 10 deletions(-) create mode 100644 go/internal/sidecar/model_discovery.go diff --git a/devlog/_plan/260905_go_sidecar_takeover/032_read_batches_decision_record.md b/devlog/_plan/260905_go_sidecar_takeover/032_read_batches_decision_record.md index 7d1110e7f9..2e95895dd5 100644 --- a/devlog/_plan/260905_go_sidecar_takeover/032_read_batches_decision_record.md +++ b/devlog/_plan/260905_go_sidecar_takeover/032_read_batches_decision_record.md @@ -80,7 +80,8 @@ completion of each batch. Deferred routes migrate with the binary at the flip | `GET /api/models` | defer (live catalog) | `listManagementModelRows(config)` over the converged live catalog (fetchAllModels family). #17's own acceptance says "catalog reads reflect live state" — that lives with the catalog store at the flip. | | `GET /api/catalog` | defer (persisted-catalog serializer) | `serializePersistedCatalog()` from `src/server/catalog-download.ts` — a large deterministic serializer over the Codex-converged catalog; port is flip-scale model-store work, plus corsHeaders sharing. | | `GET /api/client-config` | defer (catalog rows) | rows over the converged catalog. | -| `GET /api/model-discovery` / `/api/selected-models` / `/api/model-presets` | defer (live catalog + discovery) | `fetchAllModels(config)`, `getProviderLiveModelCount`, `materializeModelPreset` over the live catalog. | +| `GET /api/model-discovery` | **Go-owned, strict** | pure persisted config projection: policy, per-provider overrides, stored arrival/baseline rows, and `disabledModels`-derived state; no catalog/cache lookup. | +| `GET /api/selected-models` / `/api/model-presets` | defer (live catalog + discovery) | `getProviderLiveModelCount`, `materializeModelPreset` over the live catalog. | | `GET /api/aliases` | defer (live /models cache) | `knownModelIdsForProvider` unions in `getStaleCached(provName)` (router.ts:99) — the last-known-good live /models cache; catalog drift handling (`builtinRule`) is registry-side. | | `GET /api/providers` | defer (live keys in one body) | config-derived keys share one object with live `discovery` status and openai entitlement state. | | `GET /api/provider-context-caps` | defer (in-memory caps module) | live context-capability state. | @@ -90,9 +91,9 @@ completion of each batch. Deferred routes migrate with the binary at the flip ## Registry and oracle state after this run -Three read routes are Go-owned: `/api/system/health` (volatile pid/uptime), -`/api/shadow-call-settings` (strict), `/api/custom-models` (strict). The strict -pair exercises the empty-volatile contract against real wire bytes. Nothing in +Four read routes are Go-owned: `/api/system/health` (volatile pid/uptime), +`/api/shadow-call-settings`, `/api/custom-models`, and `/api/model-discovery` (strict). The strict +trio exercises the empty-volatile contract against real wire bytes. Nothing in `management-api.ts` names a route (pinned by test 7 of `tests/go-ownership-plumbing.test.ts`); adding the next route stays a marker flip + Go handler + oracle cases. diff --git a/go/internal/config/ordered.go b/go/internal/config/ordered.go index e83e91571d..d98fb10ef7 100644 --- a/go/internal/config/ordered.go +++ b/go/internal/config/ordered.go @@ -21,11 +21,14 @@ package config import ( "bytes" + "cmp" "encoding/json" "errors" "io" "os" "path/filepath" + "slices" + "strconv" ) // OrderedValue is one JSON value with object keys in document order. Only the @@ -56,6 +59,14 @@ type orderedMember struct { val *OrderedValue } +// OrderedEntry is one document-order object member. Projection routes use it +// when TypeScript's Object.entries order is part of their wire contract. +type OrderedEntry struct { + Key string + Value *OrderedValue + index uint32 +} + // LoadOrdered reads and decodes config.json into an ordered value tree (the // root object). A missing file yields a null root without error, mirroring // Load's ENOENT default; a malformed file yields the decode error. @@ -185,6 +196,78 @@ func (v *OrderedValue) Find(key string) *OrderedValue { return nil } +// Entries returns the object's members in document order. A non-object has no +// entries. The returned slice is a copy so callers cannot mutate the tree. +func (v *OrderedValue) Entries() []OrderedEntry { + if v == nil || v.kind != orderedObject { + return nil + } + entries := make([]OrderedEntry, len(v.obj)) + for i, member := range v.obj { + entries[i] = OrderedEntry{Key: member.key, Value: member.val} + } + return entries +} + +// Elements returns an array's values in document order. A non-array has no +// elements. The returned slice is a copy. +func (v *OrderedValue) Elements() []*OrderedValue { + if v == nil || v.kind != orderedArray { + return nil + } + return append([]*OrderedValue(nil), v.arr...) +} + +// StringValue returns a JSON string's decoded value. +func (v *OrderedValue) StringValue() (string, bool) { + if v == nil || v.kind != orderedString { + return "", false + } + return v.str, true +} + +// JSONStringifyString exports the package's ECMAScript-compatible string +// escaping for projections that construct a new object around ordered values. +func JSONStringifyString(value string) ([]byte, error) { + return marshalStringJSONStringify(value) +} + +// ECMAScriptEntries returns object entries in Object.entries order: canonical +// array-index keys first by numeric value, followed by other keys in document +// order. Projection routes use it when mirroring TypeScript object iteration. +func (v *OrderedValue) ECMAScriptEntries() []OrderedEntry { + entries := v.Entries() + if len(entries) < 2 { + return entries + } + indices := make([]OrderedEntry, 0, len(entries)) + rest := make([]OrderedEntry, 0, len(entries)) + for _, entry := range entries { + if index, ok := ecmaArrayIndex(entry.Key); ok { + entry.index = index + indices = append(indices, entry) + } else { + rest = append(rest, entry) + } + } + slices.SortFunc(indices, func(a, b OrderedEntry) int { return cmp.Compare(a.index, b.index) }) + return append(indices, rest...) +} + +func ecmaArrayIndex(key string) (uint32, bool) { + if key == "0" { + return 0, true + } + if key == "" || key[0] == '0' { + return 0, false + } + value, err := strconv.ParseUint(key, 10, 32) + if err != nil || value >= 4294967295 || strconv.FormatUint(value, 10) != key { + return 0, false + } + return uint32(value), true +} + // IsNull reports whether the value is the JSON null literal. func (v *OrderedValue) IsNull() bool { return v != nil && v.kind == orderedNull diff --git a/go/internal/config/ordered_test.go b/go/internal/config/ordered_test.go index 8347c9e7b6..05e0757dd0 100644 --- a/go/internal/config/ordered_test.go +++ b/go/internal/config/ordered_test.go @@ -1,6 +1,7 @@ package config import ( + "reflect" "strings" "testing" ) @@ -151,3 +152,21 @@ func TestOrderedEchoMalformedFileErrors(t *testing.T) { t.Fatal("expected a decode error for a truncated file") } } + +func TestECMAScriptEntriesSortArrayIndexesOnly(t *testing.T) { + dir := t.TempDir() + writeFixture(t, dir, `{"10":true,"z":true,"2":true,"01":true,"4294967295":true,"0":true}`) + root, err := LoadOrderedFromDir(dir) + if err != nil { + t.Fatal(err) + } + entries := root.ECMAScriptEntries() + got := make([]string, len(entries)) + for i, entry := range entries { + got[i] = entry.Key + } + want := []string{"0", "2", "10", "z", "01", "4294967295"} + if !reflect.DeepEqual(got, want) { + t.Fatalf("entries = %#v, want %#v", got, want) + } +} diff --git a/go/internal/sidecar/model_discovery.go b/go/internal/sidecar/model_discovery.go new file mode 100644 index 0000000000..3b36eebb02 --- /dev/null +++ b/go/internal/sidecar/model_discovery.go @@ -0,0 +1,123 @@ +package sidecar + +import ( + "bytes" + "fmt" + "strings" + + "github.com/lidge-jun/opencodex/go/internal/config" +) + +func modelDiscoveryPayload(root *config.OrderedValue) ([]byte, error) { + var out bytes.Buffer + modelDiscovery := root.Find("modelDiscovery") + policy := "on" + if candidate, ok := modelDiscovery.Find("newModelPolicy").StringValue(); ok { + policy = candidate + } + var disabled []string + for _, item := range root.Find("disabledModels").Elements() { + if slug, ok := item.StringValue(); ok { + disabled = append(disabled, slug) + } + } + out.WriteString(`{"policy":`) + if err := writeJSONString(&out, policy); err != nil { + return nil, err + } + out.WriteString(`,"providers":{`) + for i, entry := range root.Find("providers").ECMAScriptEntries() { + if i > 0 { + out.WriteByte(',') + } + if err := writeJSONString(&out, entry.Key); err != nil { + return nil, err + } + out.WriteByte(':') + providerPolicy := "inherit" + if candidate, ok := entry.Value.Find("newModelPolicy").StringValue(); ok { + providerPolicy = candidate + } + if err := writeJSONString(&out, providerPolicy); err != nil { + return nil, err + } + } + out.WriteString(`},"recentArrivals":{`) + for i, entry := range modelDiscovery.Find("recentArrivals").ECMAScriptEntries() { + if i > 0 { + out.WriteByte(',') + } + if err := writeJSONString(&out, entry.Key); err != nil { + return nil, err + } + out.WriteString(`:[`) + for j, row := range entry.Value.Elements() { + if j > 0 { + out.WriteByte(',') + } + id, _ := row.Find("id").StringValue() + fields := row.ECMAScriptEntries() + if fields == nil { + return nil, fmt.Errorf("arrival row is not an object") + } + out.WriteByte('{') + for k, field := range fields { + if k > 0 { + out.WriteByte(',') + } + if err := writeJSONString(&out, field.Key); err != nil { + return nil, err + } + out.WriteByte(':') + raw, err := field.Value.MarshalStringify() + if err != nil { + return nil, err + } + out.Write(raw) + } + if len(fields) > 0 { + out.WriteByte(',') + } + out.WriteString(`"state":`) + state := "enabled" + if modelDisabled(disabled, entry.Key, id) { + state = "auto-disabled" + } + if err := writeJSONString(&out, state); err != nil { + return nil, err + } + out.WriteByte('}') + } + out.WriteByte(']') + } + out.WriteString(`},"baselineCounts":{`) + for i, entry := range modelDiscovery.Find("knownModels").ECMAScriptEntries() { + if i > 0 { + out.WriteByte(',') + } + if err := writeJSONString(&out, entry.Key); err != nil { + return nil, err + } + fmt.Fprintf(&out, ":%d", len(entry.Value.Find("ids").Elements())) + } + out.WriteString(`}}`) + return out.Bytes(), nil +} +func writeJSONString(out *bytes.Buffer, value string) error { + raw, err := config.JSONStringifyString(value) + if err != nil { + return err + } + out.Write(raw) + return nil +} +func modelDisabled(disabled []string, provider, id string) bool { + routed := provider + "/" + strings.ReplaceAll(id, "/", "-") + raw := provider + "/" + id + for _, stored := range disabled { + if stored == raw || stored == routed { + return true + } + } + return false +} diff --git a/go/internal/sidecar/sidecar.go b/go/internal/sidecar/sidecar.go index 1c0355b749..3a0fb412d5 100644 --- a/go/internal/sidecar/sidecar.go +++ b/go/internal/sidecar/sidecar.go @@ -2,9 +2,8 @@ // incremental runtime takeover (ADR-0008, devlog/_plan/260905_go_sidecar_takeover). // // Today it owns GET /api/system/health (volatile pid/uptime normalised by the -// oracle), GET /api/shadow-call-settings (a pure function of config.json, -// compared with no normalisation at all) and GET /api/custom-models (the raw -// config.customModels echo, also compared byte-for-byte). Each handler must +// oracle), GET /api/shadow-call-settings, GET /api/custom-models and GET +// /api/model-discovery (strict config-derived reads). Each handler must // reproduce the TypeScript handler's HTTP semantics byte-for-byte: the shape, // key order, and number formatting of the JSON body are part of the contract. package sidecar @@ -180,6 +179,21 @@ func NewHandler(cfg Config) http.Handler { writeRawJSON(w, raw, "custom-models") }) + // The management route only projects persisted policy/baseline/arrival data; + // it does not discover models or consult a live catalog. + mux.HandleFunc("GET /api/model-discovery", func(w http.ResponseWriter, r *http.Request) { + root, err := loadSidecarOrdered(cfg.ConfigDir) + if err != nil { + root = nil + } + raw, err := modelDiscoveryPayload(root) + if err != nil { + fmt.Fprintf(os.Stderr, "ocx-sidecar: marshal model-discovery projection: %v\n", err) + return + } + writeRawJSON(w, raw, "model-discovery") + }) + // Ticket #20: provider quota aggregation remains process state until the // runtime flip. Go owns this public HTTP route and obtains the existing // cache/probe result from a capability-scoped parent loopback bridge, so diff --git a/go/internal/sidecar/sidecar_test.go b/go/internal/sidecar/sidecar_test.go index 2a6ecc0146..6a3553d148 100644 --- a/go/internal/sidecar/sidecar_test.go +++ b/go/internal/sidecar/sidecar_test.go @@ -492,3 +492,18 @@ func TestCustomModelsSurfaceIsNarrow(t *testing.T) { } } } + +func TestModelDiscoveryProjectionIsByteExact(t *testing.T) { + dir := t.TempDir() + writeConfigFile(t, dir, "{\"providers\":{\"zeta\":{\"newModelPolicy\":\"off\"},\"alpha\":{}},\"disabledModels\":[\"zeta/new-model\",\"alpha/raw/model\"],\"modelDiscovery\":{\"newModelPolicy\":\"off\",\"recentArrivals\":{\"zeta\":[{\"id\":\"new/model\",\"at\":\"2026-09-06T00:00:00Z\"}],\"alpha\":[{\"id\":\"raw/model\",\"at\":\"2026-09-07T00:00:00Z\"}]},\"knownModels\":{\"zeta\":{\"ids\":[\"one\",\"two\"],\"removed\":[],\"updatedAt\":\"x\"},\"alpha\":{\"ids\":[],\"removed\":[],\"updatedAt\":\"x\"}}}}") + resp := do(t, NewHandler(Config{ConfigDir: dir}), http.MethodGet, "/api/model-discovery") + defer resp.Body.Close() + raw, err := io.ReadAll(resp.Body) + if err != nil { + t.Fatal(err) + } + want := "{\"policy\":\"off\",\"providers\":{\"zeta\":\"off\",\"alpha\":\"inherit\"},\"recentArrivals\":{\"zeta\":[{\"id\":\"new/model\",\"at\":\"2026-09-06T00:00:00Z\",\"state\":\"auto-disabled\"}],\"alpha\":[{\"id\":\"raw/model\",\"at\":\"2026-09-07T00:00:00Z\",\"state\":\"auto-disabled\"}]},\"baselineCounts\":{\"zeta\":2,\"alpha\":0}}" + if string(raw) != want { + t.Fatalf("body = %s\\nwant %s", raw, want) + } +} diff --git a/src/server/management/read-surface-ownership.ts b/src/server/management/read-surface-ownership.ts index e24276e6a1..2ec011cde8 100644 --- a/src/server/management/read-surface-ownership.ts +++ b/src/server/management/read-surface-ownership.ts @@ -52,6 +52,7 @@ export const READ_SURFACE_DIFF_MATRIX: readonly ReadSurfaceDiffMatrixEntry[] = [ { method: "GET", path: "/api/system/health", module: "server/management/system-routes", transition: "go-now", stateSources: ["environment", "serving-process"], parityFixture: "default-get" }, { method: "GET", path: "/api/shadow-call-settings", module: "server/management/config-routes", transition: "go-now", stateSources: ["disk"], parityFixture: "default-get" }, { method: "GET", path: "/api/custom-models", module: "server/management/model-routes", transition: "go-now", stateSources: ["disk"], parityFixture: "default-get" }, + { method: "GET", path: "/api/model-discovery", module: "server/management/model-routes", transition: "go-now", stateSources: ["disk"], parityFixture: "default-get" }, // The Go handler owns the public route but bridges the parent-owned quota // cache until the flip; ticket #20's dedicated parity vectors cover refresh. { method: "GET", path: "/api/provider-quotas", module: "server/management/provider-routes", transition: "go-now", stateSources: ["serving-process", "external-state"], parityFixture: "default-get" }, @@ -66,7 +67,7 @@ export const READ_SURFACE_DIFF_MATRIX: readonly ReadSurfaceDiffMatrixEntry[] = [ { module: "server/management/lab-automation-routes", stateSources: ["disk", "serving-process"], rationale: lab, routes: [["GET", "/api/lab/automation"], ["GET", "/api/lab/automation/runs"]] }, { module: "server/management/lab-routes", stateSources: ["disk", "serving-process"], rationale: lab, routes: [["GET", "/api/lab/artifacts"], ["GET", "/api/lab/catalog"], ["GET", "/api/lab/events"], ["GET", "/api/lab/observations"], ["GET", "/api/lab/production-signals"], ["GET", "/api/lab/public/community"], ["GET", "/api/lab/status"], ["GET", "/api/lab/subjects"], ["GET", "/api/lab/verdicts"], ["GET", "/api/lab/subjects/{id}"], ["GET", "/api/lab/events/{id}"], ["GET", "/api/lab/artifacts/{digest}"]] }, { module: "server/management/logs-usage-routes", stateSources: ["disk", "serving-process"], rationale: flip, routes: [["GET", "/api/claude/inbound-debug"], ["GET", "/api/debug"], ["GET", "/api/debug/injection-logs"], ["GET", "/api/debug/logs"], ["GET", "/api/debug/usage-logs"], ["GET", "/api/logs"], ["GET", "/api/storage/cleanup-policy"], ["GET", "/api/storage/cleanup-policy/test-stream"], ["GET", "/api/storage/trash"], ["GET", "/api/storage/trash/restore/test-stream"], ["GET", "/api/usage"]] }, - { module: "server/management/model-routes", stateSources: ["disk", "serving-process", "external-state"], rationale: flip, routes: [["GET", "/api/aliases"], ["GET", "/api/catalog"], ["GET", "/api/client-config"], ["GET", "/api/model-discovery"], ["GET", "/api/model-presets"], ["GET", "/api/models"], ["GET", "/api/selected-models"]] }, + { module: "server/management/model-routes", stateSources: ["disk", "serving-process", "external-state"], rationale: flip, routes: [["GET", "/api/aliases"], ["GET", "/api/catalog"], ["GET", "/api/client-config"], ["GET", "/api/model-presets"], ["GET", "/api/models"], ["GET", "/api/selected-models"]] }, { module: "server/management/native-integration-routes", stateSources: ["disk", "serving-process", "external-state"], rationale: flip, routes: [["GET", "/api/native-integrations"]] }, { module: "server/management/cursor-integration-routes", stateSources: ["disk", "serving-process", "external-state"], rationale: flip, routes: [["GET", "/api/native-integrations/cursor"]] }, { module: "server/management/oauth-account-routes", stateSources: ["disk", "os", "serving-process", "external-state"], rationale: flip, routes: [["GET", "/api/key-providers"], ["GET", "/api/keys"], ["GET", "/api/oauth/accounts"], ["GET", "/api/oauth/accounts/pool"], ["GET", "/api/oauth/providers"], ["GET", "/api/oauth/status"], ["GET", "/api/providers/keys"], ["GET", "/api/providers/keychain"]] }, diff --git a/src/server/management/route-registry.ts b/src/server/management/route-registry.ts index 3122f22c4c..5cbf3bfee0 100644 --- a/src/server/management/route-registry.ts +++ b/src/server/management/route-registry.ts @@ -292,7 +292,9 @@ export const MANAGEMENT_ROUTES: readonly ManagementRoute[] = [ // JSON.stringify escaping match exactly; absent or null customModels coalesce // to [] like the TS nullish operator. { method: "GET", path: "/api/custom-models", module: "server/management/model-routes", mutates: false, go: { volatileFields: [] } }, - { method: "GET", path: "/api/model-discovery", module: "server/management/model-routes", mutates: false }, + // Pure persisted-config projection: output order is covered by the strict + // sidecar differential oracle, including arrival state from disabledModels. + { method: "GET", path: "/api/model-discovery", module: "server/management/model-routes", mutates: false, go: { volatileFields: [] } }, { method: "GET", path: "/api/model-presets", module: "server/management/model-routes", mutates: false }, { method: "GET", path: "/api/models", module: "server/management/model-routes", mutates: false }, { method: "GET", path: "/api/selected-models", module: "server/management/model-routes", mutates: false }, diff --git a/tests/go-sidecar-parity.test.ts b/tests/go-sidecar-parity.test.ts index 8b43fcd44d..a1e764d926 100644 --- a/tests/go-sidecar-parity.test.ts +++ b/tests/go-sidecar-parity.test.ts @@ -140,6 +140,8 @@ async function captureProviderQuotas(server: { url: URL }, token: string, suffix return captureJson(server, token, "/api/provider-quotas" + suffix); } +async function captureModelDiscovery(server: { url: URL }, token: string) { return captureJson(server, token, "/api/model-discovery"); } + async function captureHealth(server: { url: URL }, token: string): Promise { const response = await fetch(new URL("/api/system/health", server.url), { headers: { "x-opencodex-api-key": token }, @@ -718,4 +720,31 @@ describe.skipIf(!goAvailable || sidecarBinary === null)("ocx-sidecar differentia await serverA.stop(true); } }); + runFixtureTest("model-discovery configured body is byte-identical across TypeScript and Go", async (token) => { + saveConfig({ + ...configFixture(), defaultProvider: "10", + providers: { + "10": { adapter: "openai-chat", baseUrl: "https://ten.example/v1", disabled: true, newModelPolicy: "off" }, + "2": { adapter: "openai-chat", baseUrl: "https://two.example/v1", disabled: true }, + }, + disabledModels: ["10/new-model", "2/raw/model"], + modelDiscovery: { + newModelPolicy: "off", + recentArrivals: { "10": [{ "10": "ten", id: "new/model", at: "2026-09-06T00:00:00Z", "2": "two" }], "2": [{ id: "raw/model", at: "2026-09-07T00:00:00Z" }] }, + knownModels: { "10": { ids: ["one", "two"], removed: [], updatedAt: "x" }, "2": { ids: [], removed: [], updatedAt: "x" } }, + }, + }); + expect(GO_OWNED_MANAGEMENT_ROUTES.find(route => route.method === "GET" && route.path === "/api/model-discovery")?.go.volatileFields).toEqual([]); + const serverA = startServer(0); + try { + const ts = await captureModelDiscovery(serverA, token); + process.env[GO_SIDECAR_BIN_ENV] = sidecarBinary!; + const serverB = startServer(0); + try { + await waitFor(() => activeGoSidecarBaseUrl(), 15_000); + expect(await captureModelDiscovery(serverB, token)).toEqual(ts); + } finally { await serverB.stop(true); } + } finally { await serverA.stop(true); } + }); + }); diff --git a/tests/read-surface-diff-matrix.test.ts b/tests/read-surface-diff-matrix.test.ts index 3ae4e770ef..f2c0582f15 100644 --- a/tests/read-surface-diff-matrix.test.ts +++ b/tests/read-surface-diff-matrix.test.ts @@ -40,7 +40,7 @@ describe("read-surface differential matrix (ticket #25)", () => { const matrixGoNow = READ_SURFACE_DIFF_MATRIX.filter(row => row.transition === "go-now").map(row => key(row.method, row.path)).sort(); const registryGoReads = MANAGEMENT_ROUTES.filter(route => !route.mutates && route.go).map(route => key(route.method, route.path)).sort(); expect(matrixGoNow).toEqual(registryGoReads); - expect(matrixGoNow).toHaveLength(4); + expect(matrixGoNow).toHaveLength(5); }); test("records runtime-flip evidence in tracked repository documentation", () => { From ddd383a68d232521c2ab61cfcca8ff3652aa433e Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Sun, 6 Sep 2026 18:52:07 +0800 Subject: [PATCH 022/165] docs(go): record the #27 non-streaming relay design (devlog 036) --- .../036_nonstream_relay.md | 179 ++++++++++++++++++ 1 file changed, 179 insertions(+) create mode 100644 devlog/_plan/260905_go_sidecar_takeover/036_nonstream_relay.md diff --git a/devlog/_plan/260905_go_sidecar_takeover/036_nonstream_relay.md b/devlog/_plan/260905_go_sidecar_takeover/036_nonstream_relay.md new file mode 100644 index 0000000000..68fe4bf04f --- /dev/null +++ b/devlog/_plan/260905_go_sidecar_takeover/036_nonstream_relay.md @@ -0,0 +1,179 @@ +# 036 — Ticket #27: non-streaming relay + response repair for one provider + +Unit: `260905_go_sidecar_takeover` +Date: 2026-09-06 +Ticket: [#27](https://github.com/waxiangzi/opencodex/issues/27) (spec #4: non-streaming relay + response repair for one provider) +Blocked-by (#24): closed — hot-path seam + streaming differential harness landed on `dev-go`. + +## Scope discipline + +#27 is the first **provider relay** ticket of the hot-path increment (#4): it +replaces the #24 private parent bridge as the seam's stream source for ONE +provider class on ONE transport shape. It must NOT relay streaming traffic +(that is #29), must NOT reproduce routing decisions beyond an unambiguous +single-provider subset (that is #30), and must NOT port every response repair +(that is #31). Everything outside the declared relay subset keeps the #24 +bridge, so the TypeScript oracle continues to serve it byte-identically and the +differential never compares a class the seam does not claim. + +The relay subset is deliberately narrow and is machine-pinned by the +differential plus the Go unit suites: + +1. **Transport**: non-streaming (`stream` is not `true`) `POST /v1/responses`. +2. **Provider**: one key-mode `openai-responses` provider whose Responses wire + needs no translation — the TS pipeline forwards the client request body + verbatim upstream (verified empirically for the simple-completion subset: + plain input, input arrays, declared function tools, reasoning effort, and + both the `configured-model-list` and `default-provider` route kinds). The + sidecar reproduces that forward byte-for-byte, including the provider + `Authorization` when an env/literal `apiKey` resolves. OAuth/forward, + keychain keys, custom `responsesPath`, combos, routing profiles, shadow + intercept and model namespace (`a/b`) requests never qualify — they stay on + the bridge. +3. **Repair**: the whole-body JSON response repair the TS pipeline applies to + bounded-JSON Responses answers (the field backfill: message/reasoning/call + item `id` synthesis, message `status` backfill, `output_text.annotations` + backfill) runs in Go on the relayed 2xx response. #31 later generalises this + into the full ordered repair chain for every rewrite; #27 owns the one + deterministic JSON transform that the bounded-JSON path applies today. +4. **Errors**: a non-2xx upstream answer with a non-empty body is relayed + verbatim (status, content-type, body) exactly like the TS passthrough; a + valid upstream `Retry-After` is preserved, an invalid one is dropped. + TS-only error surface — synthetic `Retry-After` defaults for 429s, empty-body + error envelopes, quota/cyber-policy classification, and pre-stream retry + loops — is NOT ported here: those are recovery semantics owned by the + routing ticket (#30) and remain a documented seam-period divergence. + +## Design decisions + +### 1. The relay rides the #24 seam behind its own env gate + +`OPENCODEX_GO_HOTPATH_RELAY` (declared in `src/server/hot-path-seam.ts`, read +by the sidecar) switches the seam's source for qualifying requests from the +bridge to the direct upstream relay. Default OFF: a default install — and any +install that has not proven a provider against the differential — is unchanged. +Independent rollback per spec #4 story 13: the management surface, the seam, +and the provider relay each carry their own gate. + +The gate is evaluated per request inside `dataPlaneSeam` AFTER the existing +front-door claim checks (the request token is still required; the seam still +never invents a public listener). A request that does not qualify falls through +to the bridge exactly as in #24, so the fallback is per-request, not global. + +### 2. The relay-safe predicate is a config + request contract + +Go claims a request only when it can prove the TS pipeline would forward it +verbatim and repair only the backfill: + +- Config-level refusals (bridge): combos table present, routing profiles + present, `shadowCallIntercept.enabled`, provider `authMode` not key-mode, + provider `apiKey` is a `keychain:` reference, custom `responsesPath`, + provider `adapter` not `openai-responses`, disabled provider. +- Route-level: the requested model (no `/`) must resolve through the TS simple + subset — a single enabled provider owning the model via `models` / + `defaultModel`, else the sole configured `defaultProvider` — mirroring + `routeModelInternal`'s `configured-model-list` / + `configured-default-model` / `default-provider` kinds in file order. +- Request-level: body is a JSON object; `stream` is not `true`; `model` is a + string; the body has none of the features that make TS rewrite the outbound + bytes or engage request-local state (`previous_response_id`, compaction + markers, namespaced/hosted tool entries, web-search/image/video plans); no + Codex pool/sub-agent/attestation headers on the request. + +The predicate is exercised by the differential matrix and by unit tests that +assert each refusal reason; the seam's honest fallback means a wrong refusal +costs parity (bridge still serves it), never correctness. + +### 3. Outbound request = the #24 openaiResponsesUrl contract + verbatim body + +The relay builds `POST /v1/responses` with the same +path normalization as `src/adapters/openai-responses-url.ts` (strip trailing +slashes / `/responses` / `/v1`, append `/v1/responses`), forwards the seam +request's body bytes verbatim, sets `content-type: application/json`, and adds +`Authorization: Bearer ` when the provider `apiKey` resolves through the +env (`${NAME}` / `$NAME`) or a literal. Loopback/private base URLs honor +`allowPrivateNetwork`; the request never goes through a system proxy (mirrors +the bridge transport). Keychain resolution is refused (bridge) because the +sidecar has no keychain access. + +### 4. Response = transport fidelity + the bounded-JSON backfill in Go + +For a 2xx JSON answer the relay applies the field backfill to an ordered JSON +tree and re-emits only when a field changed — byte-identical untouched +payloads (raw relay) and canonical re-serialisation on change, exactly like +the TS bounded-JSON path. The ordered tree and the ECMAScript +`JSON.stringify` encoder (string escaping, key order, V8 number formatting) +live in a new `go/internal/jsonwire` package; the transform mirrors +`src/server/responses/responses-field-backfill.ts` and its observed byte +behaviour, pinned by Go unit tests against golden payloads captured from the +TS oracle (message/reasoning/function/custom-tool id synthesis with +`_ocx_`, `status` inference from the response status, and +`annotations: []` on `output_text` parts). + +Non-JSON 2xx and non-2xx non-empty bodies are relayed verbatim with the +upstream content-type; valid `Retry-After` passes through. + +### 5. Proof that Go (not the bridge) served a claim + +The Bun differential asserts the fixture upstream sees the seam-served +request arrive from the Go process (`User-Agent: Go-http-client/…`) while the +in-process oracle's identical request arrives from Bun, and that both requests +carry the same method/path/content-type/`Authorization`/body. A gate-negative +request (e.g. `stream: true`) must arrive from the bridge (Bun UA), proving +the fallback still owns non-qualifying traffic. The Go seam unit suite proves +the same without a bridge: the seam answers a qualifying request while the +parent bridge is a dead port. + +## Security boundary + +- The relay activates only behind the existing seam request-token gate; it + adds no public listener and never reads a client credential — the front + door's body-bound claim headers are relayed to the bridge only, never to the + provider upstream. +- The provider API key is resolved from the config/env the operator already + trusts the sidecar with (the sidecar is a child of the proxy process); + keychain material is never requested (bridge instead). +- Outbound destinations are validated: only the configured provider base URL, + honoring `allowPrivateNetwork`; no proxy, no userinfo, loopback-only for the + fixture. + +## Proof (as landed) + +- `go/internal/jsonwire` unit tests: V8 number formatting against a committed + Bun-generated corpus (`testdata/v8-numbers.tsv`, 447 rows incl. exponent + window edges and random finite doubles), ECMAScript string escaping + (control escapes, literal U+2028/U+2029, no HTML escaping), ordered + round-trip / spread-set semantics, and number canonicalisation through + `Encode`. +- `go/internal/sidecar` field-backfill unit tests against committed goldens + produced by the REAL TypeScript repair + (`testdata/responses-repair-goldens.json`, 27 shapes: sparse message + canonical order, response-status → message-status mapping incl. + failed→incomplete / queued→in_progress, id namespace prefixes, + empty-string and non-string id replacement in place, compaction exclusion, + raw-bytes identity when nothing changed, number-literal canonicalisation on + change, U+2028 handling, key order). +- `go/internal/sidecar` relay unit tests: relay-safe predicate (every refusal + reason named), the seam direct relay with a dead bridge (proves no bridge + hop), outbound verbatim body + path + resolved `Authorization`, valid + `Retry-After` preserved / invalid dropped on a verbatim non-2xx relay, + oversized-body bound, streaming and gate-off requests staying on the + bridge. +- `tests/go-hotpath-relay.test.ts`: two-server differential over the + non-streaming matrix (plain input, tools array, reasoning, default-provider + fallback, plus a streaming refusal), where each armed-relay response equals + the in-process oracle byte-for-byte, the fixture upstream proves direct + serving via `User-Agent: Go-http-client/…` for relay-admitted requests and + Bun for refused/gate-off ones, and the relay gate off keeps every request + on the bridge. + +## Delivery notes (filled in at close) + +- Landed as a feature commit plus a "Merge ticket #27 …" merge on `dev-go`; + no PR (repository convention). TS `typecheck`, the focused suites + (`tests/go-hotpath-seam.test.ts`, `tests/go-hotpath-relay.test.ts`), + `go test ./...` and the full TS suite are green; `privacy:scan` is green. +- Seam-period divergences, all documented and owned by later tickets: + pre-stream retry loops, synthetic 429 `Retry-After`, empty-body error + envelopes, quota/cyber-policy classification, streaming relay (#29), + routing/combos/namespace handling (#30), full repair-chain parity (#31). From 48951f2c597ab6b3caf36b7134fba7f09a50d850 Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Sun, 6 Sep 2026 18:52:07 +0800 Subject: [PATCH 023/165] feat(go): ordered-JSON wire with V8 number/string parity (jsonwire) Parse and re-emit a JSON document the way ECMAScript JSON.stringify does: object keys in document order, spread-equivalent Set, numbers in V8 shortest-decimal form, no HTML/U+2028/U+2029 string escaping. Number formatting is pinned by a committed Bun-generated corpus (447 rows). --- go/internal/jsonwire/jsonwire.go | 432 ++++++++++++++++++ go/internal/jsonwire/jsonwire_test.go | 131 ++++++ go/internal/jsonwire/testdata/v8-numbers.tsv | 447 +++++++++++++++++++ 3 files changed, 1010 insertions(+) create mode 100644 go/internal/jsonwire/jsonwire.go create mode 100644 go/internal/jsonwire/jsonwire_test.go create mode 100644 go/internal/jsonwire/testdata/v8-numbers.tsv diff --git a/go/internal/jsonwire/jsonwire.go b/go/internal/jsonwire/jsonwire.go new file mode 100644 index 0000000000..c5a4b3e63a --- /dev/null +++ b/go/internal/jsonwire/jsonwire.go @@ -0,0 +1,432 @@ +// Package jsonwire implements the JSON value model the Go hot-path relay +// needs to reproduce TypeScript's byte behaviour when it re-serialises a +// parsed Responses payload (ticket #27, devlog 036). +// +// The hard requirement is that re-encoding a parsed value emits exactly what +// ECMAScript JSON.stringify would emit, because the TS oracle re-serialises a +// repaired upstream body with JSON.stringify and the differential compares raw +// client-visible bytes: +// +// - Object keys keep document order (encoding/json maps would discard it). +// - Strings are escaped exactly like V8: quotes/backslashes and the five +// control shortcuts, \u00xx for other controls, and everything above +// U+0020 — DEL, U+0080, U+2028/U+2029 included — emitted literally as +// UTF-8 (no HTML escaping). +// - Numbers are re-serialised from the parsed float64 the way V8's +// Number::toString does (shortest round-trip decimal, exponent form only +// outside the (-6, 21] decimal window, no zero-padded exponents), NOT with +// encoding/json's rules. +// +// Untouched payloads must never be routed through this encoder: the relay +// emits the original raw bytes when a repair changes nothing, exactly like the +// TS bounded-JSON path, so a lost canonicalisation is only ever observable on +// a payload the repair actually rewrote. +package jsonwire + +import ( + "bytes" + "encoding/json" + "errors" + "io" + "strconv" +) + +// Kind classifies a Value. +type Kind int + +const ( + Null Kind = iota + Bool + Number + String + Array + Object +) + +// Member is one object member in document order. +type Member struct { + Key string + Value *Value +} + +// Value is one JSON value with object keys in document order and numbers kept +// as their raw JSON literal until encode time. +type Value struct { + kind Kind + b bool + num string // raw literal for Number + str string // decoded string for String + arr []*Value + obj []Member +} + +// Parse decodes one JSON document into an ordered value tree. A second value +// in the stream is an error, matching the config echo loader's contract. +func Parse(data []byte) (*Value, error) { + decoder := json.NewDecoder(bytes.NewReader(data)) + decoder.UseNumber() + value, err := decodeNext(decoder) + if err != nil { + return nil, err + } + if _, err := decoder.Token(); err != io.EOF { + if err == nil { + return nil, errors.New("jsonwire: input contains more than one JSON value") + } + return nil, err + } + return value, nil +} + +func decodeNext(decoder *json.Decoder) (*Value, error) { + token, err := decoder.Token() + if err != nil { + return nil, err + } + return decodeValue(decoder, token) +} + +func decodeValue(decoder *json.Decoder, token json.Token) (*Value, error) { + switch typed := token.(type) { + case nil: + return &Value{kind: Null}, nil + case bool: + return &Value{kind: Bool, b: typed}, nil + case string: + return &Value{kind: String, str: typed}, nil + case json.Number: + return &Value{kind: Number, num: typed.String()}, nil + case json.Delim: + switch typed { + case '{': + obj := &Value{kind: Object} + for decoder.More() { + keyToken, err := decoder.Token() + if err != nil { + return nil, err + } + key, ok := keyToken.(string) + if !ok { + return nil, errors.New("jsonwire: object key is not a string") + } + member, err := decodeNext(decoder) + if err != nil { + return nil, err + } + obj.obj = append(obj.obj, Member{Key: key, Value: member}) + } + if _, err := decoder.Token(); err != nil { // consume '}' + return nil, err + } + return obj, nil + case '[': + arr := &Value{kind: Array} + for decoder.More() { + member, err := decodeNext(decoder) + if err != nil { + return nil, err + } + arr.arr = append(arr.arr, member) + } + if _, err := decoder.Token(); err != nil { // consume ']' + return nil, err + } + return arr, nil + default: + return nil, errors.New("jsonwire: unexpected delimiter") + } + default: + return nil, errors.New("jsonwire: unsupported token") + } +} + +// Kind reports the value's kind. +func (v *Value) Kind() Kind { + if v == nil { + return Null + } + return v.kind +} + +// Bool returns a Bool value's payload. +func (v *Value) Bool() bool { return v != nil && v.kind == Bool && v.b } + +// NumberRaw returns a Number value's raw JSON literal. +func (v *Value) NumberRaw() string { + if v == nil || v.kind != Number { + return "" + } + return v.num +} + +// String returns a String value's decoded payload. +func (v *Value) String() string { + if v == nil || v.kind != String { + return "" + } + return v.str +} + +// Members returns an Object's members in document order. The returned slice is +// a copy. +func (v *Value) Members() []Member { + if v == nil || v.kind != Object { + return nil + } + return append([]Member(nil), v.obj...) +} + +// Elements returns an Array's elements in document order. The returned slice +// is a copy. +func (v *Value) Elements() []*Value { + if v == nil || v.kind != Array { + return nil + } + return append([]*Value(nil), v.arr...) +} + +// Find returns the member with the given key, or nil when absent. The returned +// Value is the live tree node. +func (v *Value) Find(key string) *Value { + if v == nil || v.kind != Object { + return nil + } + for i := range v.obj { + if v.obj[i].Key == key { + return v.obj[i].Value + } + } + return nil +} + +// Set replaces the member with the given key, or appends it at the end when +// absent. Key position is preserved for existing members and new members are +// appended — the exact semantics of a TypeScript object spread. +func (v *Value) Set(key string, member *Value) { + if v == nil || v.kind != Object { + return + } + for i := range v.obj { + if v.obj[i].Key == key { + v.obj[i].Value = member + return + } + } + v.obj = append(v.obj, Member{Key: key, Value: member}) +} + +// Constructors for the values a repair synthesises (strings, empty arrays, +// booleans). Numbers are not synthesised by the current transforms; NumberFrom +// exists so future transforms can stay on the same tree. +func NullValue() *Value { return &Value{kind: Null} } +func BoolValue(value bool) *Value { return &Value{kind: Bool, b: value} } +func StringValue(value string) *Value { return &Value{kind: String, str: value} } +func EmptyArray() *Value { return &Value{kind: Array} } + +// NumberFrom builds a Number value from a float64. The literal is canonical +// V8 form, so encode round-trips it unchanged. +func NumberFrom(value float64) *Value { + return &Value{kind: Number, num: FormatV8Number(value)} +} + +// AppendArray appends an element to an Array value. +func (v *Value) AppendArray(element *Value) { + if v == nil || v.kind != Array { + return + } + v.arr = append(v.arr, element) +} + +// Encode emits the value exactly like ECMAScript JSON.stringify: compact, no +// HTML/U+2028/U+2029 escaping, object keys in document order, and numbers in +// V8 shortest-decimal form. +func (v *Value) Encode() ([]byte, error) { + var out bytes.Buffer + if err := v.encode(&out); err != nil { + return nil, err + } + return out.Bytes(), nil +} + +func (v *Value) encode(out *bytes.Buffer) error { + switch v.kind { + case Null: + out.WriteString("null") + case Bool: + if v.b { + out.WriteString("true") + } else { + out.WriteString("false") + } + case Number: + out.WriteString(v8NumberString(v.num)) + case String: + raw, err := EncodeString(v.str) + if err != nil { + return err + } + out.Write(raw) + case Array: + out.WriteByte('[') + for i, member := range v.arr { + if i > 0 { + out.WriteByte(',') + } + if err := member.encode(out); err != nil { + return err + } + } + out.WriteByte(']') + case Object: + out.WriteByte('{') + for i, member := range v.obj { + if i > 0 { + out.WriteByte(',') + } + rawKey, err := EncodeString(member.Key) + if err != nil { + return err + } + out.Write(rawKey) + out.WriteByte(':') + if err := member.Value.encode(out); err != nil { + return err + } + } + out.WriteByte('}') + default: + out.WriteString("null") + } + return nil +} + +// EncodeString encodes one string the way ECMAScript JSON.stringify does. +// encoding/json cannot be used directly: with HTML escaping disabled it still +// escapes U+2028/U+2029, while V8 emits them literally (verified against Bun). +func EncodeString(value string) ([]byte, error) { + var out bytes.Buffer + out.WriteByte('"') + for _, r := range value { + switch r { + case '"', '\\': + out.WriteByte('\\') + out.WriteRune(r) + case '\b': + out.WriteString(`\b`) + case '\t': + out.WriteString(`\t`) + case '\n': + out.WriteString(`\n`) + case '\f': + out.WriteString(`\f`) + case '\r': + out.WriteString(`\r`) + default: + if r < 0x20 { + const hex = "0123456789abcdef" + out.WriteString(`\u00`) + out.WriteByte(hex[r>>4]) + out.WriteByte(hex[r&0xf]) + } else { + out.WriteRune(r) + } + } + } + out.WriteByte('"') + return out.Bytes(), nil +} + +// v8NumberString re-serialises a raw JSON number literal the way V8's +// JSON.stringify would after parsing it to a JS Number. +func v8NumberString(raw string) string { + f, err := strconv.ParseFloat(raw, 64) + if err != nil { + // The literal came from a valid JSON decoder, so this cannot fail; + // keep the raw literal rather than inventing bytes. + return raw + } + return FormatV8Number(f) +} + +// FormatV8Number formats a float64 exactly like ECMAScript Number::toString(10) +// (which JSON.stringify uses). Zero (including -0) is "0". +func FormatV8Number(f float64) string { + if f == 0 { + return "0" + } + if f < 0 { + return "-" + formatV8Positive(-f) + } + return formatV8Positive(f) +} + +// formatV8Positive assumes f > 0. +func formatV8Positive(f float64) string { + // strconv's shortest 'e' form is the correctly-rounded shortest decimal + // (the same number ECMAScript's toString algorithm produces), e.g. + // "1.2345e+20", "1e-07". Rewrite it into ECMAScript's formatting rules: + // decimal notation when -6 < s <= 21 (s = decimal exponent of the first + // significant digit), exponent form otherwise with an unpadded exponent. + short := strconv.FormatFloat(f, 'e', -1, 64) + expPos := -1 + for i := len(short) - 1; i >= 0; i-- { + if short[i] == 'e' { + expPos = i + break + } + } + mantissa := short[:expPos] + exp, _ := strconv.Atoi(short[expPos+1:]) + digits := make([]byte, 0, len(mantissa)) + for _, c := range []byte(mantissa) { + if c != '.' { + digits = append(digits, c) + } + } + // s = 1 + exp: the place value of the first digit relative to the units. + s := 1 + exp + k := len(digits) + + if s > 21 || s <= -6 { + var out bytes.Buffer + out.WriteByte(digits[0]) + if k > 1 { + out.WriteByte('.') + out.Write(digits[1:]) + } + out.WriteByte('e') + exponent := s - 1 + if exponent >= 0 { + out.WriteByte('+') + } else { + out.WriteByte('-') + exponent = -exponent + } + out.WriteString(strconv.Itoa(exponent)) + return out.String() + } + if s >= k { + // Integer: pad with zeros up to the decimal position. + out := make([]byte, 0, s) + out = append(out, digits...) + for i := k; i < s; i++ { + out = append(out, '0') + } + return string(out) + } + if s > 0 { + out := make([]byte, 0, k+1) + out = append(out, digits[:s]...) + out = append(out, '.') + out = append(out, digits[s:]...) + return string(out) + } + // 0.00…digits + out := make([]byte, 0, k+2-s) + out = append(out, '0', '.') + for i := 0; i < -s; i++ { + out = append(out, '0') + } + out = append(out, digits...) + return string(out) +} diff --git a/go/internal/jsonwire/jsonwire_test.go b/go/internal/jsonwire/jsonwire_test.go new file mode 100644 index 0000000000..2cb122501e --- /dev/null +++ b/go/internal/jsonwire/jsonwire_test.go @@ -0,0 +1,131 @@ +package jsonwire + +import ( + "bufio" + "os" + "path/filepath" + "strconv" + "strings" + "testing" +) + +// numberCorpusRow is one literal/expected pair from the committed Bun corpus +// (.tmp/gen-number-corpus.mjs): the literal is JSON text, the expected column +// is JSON.stringify(Number(literal)) as V8 emits it. +func numberCorpusRows(t *testing.T) [][2]string { + t.Helper() + file, err := os.Open(filepath.Join("testdata", "v8-numbers.tsv")) + if err != nil { + t.Fatalf("open corpus: %v", err) + } + defer file.Close() + var rows [][2]string + scanner := bufio.NewScanner(file) + scanner.Buffer(make([]byte, 1024*1024), 1024*1024) + for scanner.Scan() { + fields := strings.Split(scanner.Text(), "\t") + if len(fields) != 2 { + t.Fatalf("malformed corpus row %q", scanner.Text()) + } + rows = append(rows, [2]string{fields[0], fields[1]}) + } + if err := scanner.Err(); err != nil { + t.Fatalf("scan corpus: %v", err) + } + if len(rows) == 0 { + t.Fatal("corpus is empty") + } + return rows +} + +// TestFormatV8NumberAgainstBunCorpus pins number formatting to V8's +// JSON.stringify for edge literals and random finite doubles. +func TestFormatV8NumberAgainstBunCorpus(t *testing.T) { + for _, row := range numberCorpusRows(t) { + literal, want := row[0], row[1] + f, err := strconv.ParseFloat(literal, 64) + if err != nil { + t.Fatalf("corpus literal %q does not parse: %v", literal, err) + } + if got := FormatV8Number(f); got != want { + t.Errorf("FormatV8Number(parse(%s)) = %q, want %q", literal, got, want) + } + } +} + +func quoted(s string) string { return "\"" + s + "\"" } + +// TestEncodeStringMatchesV8Escaping: control characters are escaped, U+2028 and +// U+2029 are emitted literally (encoding/json would escape them), and HTML +// characters are not escaped. +func TestEncodeStringMatchesV8Escaping(t *testing.T) { + lsep := "\u2028" + psep := "\u2029" + cases := []struct { + input string + want string + }{ + {"plain", quoted("plain")}, + {"quote\\backslash", quoted("quote\\\\backslash")}, + {"tab:\tnewline:\ncr:\r", quoted("tab:\\tnewline:\\ncr:\\r")}, + {"control:\x01", quoted("control:\\u0001")}, + {"u2028:" + lsep + "u2029:" + psep, quoted("u2028:" + lsep + "u2029:" + psep)}, + {"html:<>&", quoted("html:<>&")}, + {"\u0000", quoted("\\u0000")}, + {"formfeed:\f", quoted("formfeed:\\f")}, + {"backspace:\b", quoted("backspace:\\b")}, + } + for _, c := range cases { + got, err := EncodeString(c.input) + if err != nil { + t.Fatalf("EncodeString(%q): %v", c.input, err) + } + if string(got) != c.want { + t.Errorf("EncodeString(%q) = %s, want %s", c.input, got, c.want) + } + } +} + +// TestOrderedRoundTripAndSetAppendsAtEnd: document order and spread semantics. +func TestOrderedRoundTripAndSetAppendsAtEnd(t *testing.T) { + root, err := Parse([]byte(`{"b":1,"a":{"x":"y"},"c":[true,null]}`)) + if err != nil { + t.Fatal(err) + } + root.Set("added", StringValue("tail")) + root.Find("a").Set("x", NumberFrom(2)) + encoded, err := root.Encode() + if err != nil { + t.Fatal(err) + } + // b and a keep file order; new top-level key appends at the end; the + // nested x keeps its position and 1.0-shaped literals canonicalise. + if got, want := string(encoded), `{"b":1,"a":{"x":2},"c":[true,null],"added":"tail"}`; got != want { + t.Fatalf("encode = %s, want %s", got, want) + } +} + +// TestEncodeCanonicalisesNumbers: a JSON literal is re-emitted the way +// JSON.stringify of the parsed Number would emit it. +func TestEncodeCanonicalisesNumbers(t *testing.T) { + cases := map[string]string{ + `{"n":1.0}`: `{"n":1}`, + `{"n":1e21}`: `{"n":1e+21}`, + `{"n":1e-7}`: `{"n":1e-7}`, + `{"n":0.30000000000000004}`: `{"n":0.30000000000000004}`, + `{"n":9007199254740993}`: `{"n":9007199254740992}`, + } + for input, want := range cases { + root, err := Parse([]byte(input)) + if err != nil { + t.Fatalf("parse %s: %v", input, err) + } + encoded, err := root.Encode() + if err != nil { + t.Fatal(err) + } + if string(encoded) != want { + t.Errorf("encode(%s) = %s, want %s", input, encoded, want) + } + } +} diff --git a/go/internal/jsonwire/testdata/v8-numbers.tsv b/go/internal/jsonwire/testdata/v8-numbers.tsv new file mode 100644 index 0000000000..289fe0bac5 --- /dev/null +++ b/go/internal/jsonwire/testdata/v8-numbers.tsv @@ -0,0 +1,447 @@ +0 0 +-0 0 +1 1 +-1 -1 +100 100 +123.456 123.456 +0.5 0.5 +-0.5 -0.5 +1e21 1e+21 +1e20 100000000000000000000 +1e-6 0.000001 +1e-7 1e-7 +0.1 0.1 +0.2 0.2 +0.3 0.3 +0.30000000000000004 0.30000000000000004 +1.5 1.5 +1e3 1000 +1e6 1000000 +1e-5 0.00001 +1e-4 0.0001 +5e-324 5e-324 +1.7976931348623157e308 1.7976931348623157e+308 +2.2250738585072014e-308 2.2250738585072014e-308 +9007199254740991 9007199254740991 +9007199254740992 9007199254740992 +9007199254740993 9007199254740992 +9007199254740994 9007199254740994 +1.0 1 +2.50 2.5 +999999999999999.9 999999999999999.9 +123456789.123456789 123456789.12345679 +0.0000012345 0.0000012345 +1000000000000000000000 1e+21 +100000000000000000000 100000000000000000000 +10000000000000000000 10000000000000000000 +0.000001 0.000001 +0.0000001 1e-7 +1e308 1e+308 +2.5e-321 2.5e-321 +1e-323 1e-323 +123456789012345678901234567890 1.2345678901234568e+29 +-1e21 -1e+21 +-1e-7 -1e-7 +1.5e300 1.5e+300 +3.141592653589793 3.141592653589793 +2.718281828459045 2.718281828459045 +2.2024798454760163e-73 2.2024798454760163e-73 +-4.912309340218972e-275 -4.912309340218972e-275 +9.115968646074846e+246 9.115968646074846e+246 +8.75679038787647e+131 8.75679038787647e+131 +8.207012699130086e-77 8.207012699130086e-77 +9.922392529464929e-95 9.922392529464929e-95 +-3.6338368042158724e-60 -3.6338368042158724e-60 +1.5030448053330835e+28 1.5030448053330835e+28 +-2.2273085653974596e-168 -2.2273085653974596e-168 +0.0012043432809252838 0.0012043432809252838 +1.1219349103654346e+71 1.1219349103654346e+71 +-9.114063073637106e+51 -9.114063073637106e+51 +2.756536896019681e+261 2.756536896019681e+261 +4.184891306973065e-243 4.184891306973065e-243 +-4.341320983126188e-190 -4.341320983126188e-190 +-6.50291158970043e-215 -6.50291158970043e-215 +5.007814881800844e+197 5.007814881800844e+197 +4.916499914374165e+256 4.916499914374165e+256 +1.3461260449798923e-64 1.3461260449798923e-64 +2.079615616197604e+306 2.079615616197604e+306 +-1.8585730505302776e-26 -1.8585730505302776e-26 +-3.2960312919200765e+37 -3.2960312919200765e+37 +2.277509078700205e-232 2.277509078700205e-232 +-8.841440270920852e-260 -8.841440270920852e-260 +4.0361299889839843e+95 4.0361299889839843e+95 +-6.058442247357491e+287 -6.058442247357491e+287 +-2.4997006320386993e-61 -2.4997006320386993e-61 +-1.8966560541440982e+138 -1.8966560541440982e+138 +1.8747204731346894e-111 1.8747204731346894e-111 +1.6751845456526915e+222 1.6751845456526915e+222 +2.1183108610782434e-296 2.1183108610782434e-296 +-1.0922971804406309e-30 -1.0922971804406309e-30 +6.290529427508976e+299 6.290529427508976e+299 +-3.602539965434893e-230 -3.602539965434893e-230 +-8.241435710587465e+303 -8.241435710587465e+303 +1.1377735079256708e-148 1.1377735079256708e-148 +1.3641368539917763e-27 1.3641368539917763e-27 +3.8573991988482295e-113 3.8573991988482295e-113 +-8.33201830766345e+26 -8.33201830766345e+26 +-6.898070145764595e-158 -6.898070145764595e-158 +254764670472606970000 254764670472606970000 +4.501716026564775e-264 4.501716026564775e-264 +3.461499811672666e+243 3.461499811672666e+243 +-1.0023447021062338e+152 -1.0023447021062338e+152 +7.052956829209223e-271 7.052956829209223e-271 +-8.294797812664573e+229 -8.294797812664573e+229 +-3.239431352047815e+82 -3.239431352047815e+82 +-5.064507184468739e-269 -5.064507184468739e-269 +8.858526778467132e+269 8.858526778467132e+269 +9.650912236652307e+95 9.650912236652307e+95 +-1.4520360078142536e+278 -1.4520360078142536e+278 +9.073716642059195e+280 9.073716642059195e+280 +-4.4592347756255414e+178 -4.4592347756255414e+178 +4.463979676951595e+69 4.463979676951595e+69 +-7.642382615102823e-142 -7.642382615102823e-142 +-1.4613776262059474e-194 -1.4613776262059474e-194 +-1.4950470737895676e+119 -1.4950470737895676e+119 +-7.963533961656298e+211 -7.963533961656298e+211 +1.5379282433100384e-263 1.5379282433100384e-263 +-4.9432907373459567e-172 -4.9432907373459567e-172 +2.2558714355429195e+146 2.2558714355429195e+146 +-4.887018497109197e-257 -4.887018497109197e-257 +-3.813328923330931e-105 -3.813328923330931e-105 +4.464960110899884e+296 4.464960110899884e+296 +-1.980312778737264e+195 -1.980312778737264e+195 +-1.86626697380911e+219 -1.86626697380911e+219 +2.3327852298231083e+232 2.3327852298231083e+232 +1.6020336778646793e-251 1.6020336778646793e-251 +-3.46626905799688e-104 -3.46626905799688e-104 +1.775015091493092e-7 1.775015091493092e-7 +4.364075408890865e-285 4.364075408890865e-285 +9.221051892455949e+35 9.221051892455949e+35 +-1.2569657321818035e-18 -1.2569657321818035e-18 +-3.388833388060632e-286 -3.388833388060632e-286 +-1.5867651857225797e-198 -1.5867651857225797e-198 +-1.0963847865893509e-178 -1.0963847865893509e-178 +4.8047070396649517e-132 4.8047070396649517e-132 +2.2717866737123957e-139 2.2717866737123957e-139 +9.721044829888433e+267 9.721044829888433e+267 +-1.211067987002467e-104 -1.211067987002467e-104 +1.74821145155455e-134 1.74821145155455e-134 +7.062480694629245e-168 7.062480694629245e-168 +-1.2555398864202485e+68 -1.2555398864202485e+68 +-2.200623579443945e+289 -2.200623579443945e+289 +8.155817226049644e+138 8.155817226049644e+138 +2.1959589466415047e+114 2.1959589466415047e+114 +2.212270531608757e-86 2.212270531608757e-86 +-8.642420676079306e+159 -8.642420676079306e+159 +8.435959563925907e+159 8.435959563925907e+159 +-1.4591311450619044e-259 -1.4591311450619044e-259 +5.76272768485012e-245 5.76272768485012e-245 +-1.2163147861640373e-124 -1.2163147861640373e-124 +-1.759389655365446e-31 -1.759389655365446e-31 +9.98772402050207e-113 9.98772402050207e-113 +5.2389351802602815e-82 5.2389351802602815e-82 +7.871599271944246e-83 7.871599271944246e-83 +-2.0164093368413602e+195 -2.0164093368413602e+195 +2.282147172214799e+276 2.282147172214799e+276 +-2.283212215696564e+242 -2.283212215696564e+242 +53725.47721916314 53725.47721916314 +1.3066545516736248e-92 1.3066545516736248e-92 +1.2953326935266483e+32 1.2953326935266483e+32 +4.805396578168018e-164 4.805396578168018e-164 +-4.991570879015836e-238 -4.991570879015836e-238 +3.9360237266514494e-291 3.9360237266514494e-291 +-3.4207676849738367e-233 -3.4207676849738367e-233 +-8.048690619618798e+211 -8.048690619618798e+211 +-1.3411630451897607e-90 -1.3411630451897607e-90 +3.118329442202555e-208 3.118329442202555e-208 +-7.70768343074788e-48 -7.70768343074788e-48 +-1.7332981327299836e+241 -1.7332981327299836e+241 +2.3024696710032728e+282 2.3024696710032728e+282 +4.9382776150941217e-110 4.9382776150941217e-110 +-9.138897694190638e+165 -9.138897694190638e+165 +2.50326221159602e+229 2.50326221159602e+229 +4.0240593818436033e+278 4.0240593818436033e+278 +-4.320427616935318e+46 -4.320427616935318e+46 +-9.611512091327103e+271 -9.611512091327103e+271 +9.562368595966595e+61 9.562368595966595e+61 +-1.8852173546801614e+115 -1.8852173546801614e+115 +-2.330993123563127e-70 -2.330993123563127e-70 +-2.72213547957552e-280 -2.72213547957552e-280 +3.7346793679159515e-202 3.7346793679159515e-202 +3.6656221040489136e-135 3.6656221040489136e-135 +-5.301589870828967e-202 -5.301589870828967e-202 +-5.852055309302066e+217 -5.852055309302066e+217 +7.16873469811016e+92 7.16873469811016e+92 +2.1228304522858214e-95 2.1228304522858214e-95 +2.507359219310591e-269 2.507359219310591e-269 +7.758280659590552e+161 7.758280659590552e+161 +5.5485174284940524e-204 5.5485174284940524e-204 +-8.105827067960902e+238 -8.105827067960902e+238 +3.3158366065067527e+197 3.3158366065067527e+197 +-6.197081703982123e-227 -6.197081703982123e-227 +8.070899887707712e-233 8.070899887707712e-233 +-2.2044557736286792e+142 -2.2044557736286792e+142 +3.855225922883723e-82 3.855225922883723e-82 +6.822031043255369e+51 6.822031043255369e+51 +282582300623716700 282582300623716700 +-4.7111514143060844e+101 -4.7111514143060844e+101 +-1.1694919292290998e+105 -1.1694919292290998e+105 +7.361870495617792e-81 7.361870495617792e-81 +-1.0339545271944475e+30 -1.0339545271944475e+30 +2.1019243380665186e-135 2.1019243380665186e-135 +-1.6943585013040347e-214 -1.6943585013040347e-214 +-3.7422396414100036e-87 -3.7422396414100036e-87 +-9.192316504088813e-92 -9.192316504088813e-92 +-7.857437779061423e+238 -7.857437779061423e+238 +3.5089984799238676e+65 3.5089984799238676e+65 +-6.089447385334911e-8 -6.089447385334911e-8 +1.1013149986440522e+281 1.1013149986440522e+281 +-2.581031402442362e+294 -2.581031402442362e+294 +-2.5203456009363745e+243 -2.5203456009363745e+243 +-6.811495283320127e+84 -6.811495283320127e+84 +-151558348002103500000 -151558348002103500000 +-8.387577300920048e+297 -8.387577300920048e+297 +2.828451057979873e+255 2.828451057979873e+255 +8.500583400519486e-216 8.500583400519486e-216 +8.045206220480121e+226 8.045206220480121e+226 +-1.5467019039774866e-209 -1.5467019039774866e-209 +-9.159944474577739e-212 -9.159944474577739e-212 +-2.6120376252019643e-300 -2.6120376252019643e-300 +7.469008472851905e-87 7.469008472851905e-87 +2.4094093892018385e+158 2.4094093892018385e+158 +-8.922298645535766e+27 -8.922298645535766e+27 +-58711226.49813076 -58711226.49813076 +-4.09350803461442e-285 -4.09350803461442e-285 +1.7033962956024192e+156 1.7033962956024192e+156 +-1.5166717835654e+282 -1.5166717835654e+282 +2.7224677940593627e-177 2.7224677940593627e-177 +2.684619529634084e-56 2.684619529634084e-56 +1.0988593125521262e-219 1.0988593125521262e-219 +-6.938644166474025e+210 -6.938644166474025e+210 +-2.6364008577465396e+293 -2.6364008577465396e+293 +8.178302618932382e+241 8.178302618932382e+241 +1.378641145340192e-153 1.378641145340192e-153 +-2.2678124133326043e-59 -2.2678124133326043e-59 +4.6931226493081855e+115 4.6931226493081855e+115 +-2.787726031266358e+281 -2.787726031266358e+281 +-1.4469799425466414e-271 -1.4469799425466414e-271 +-2.9799263194427575e-15 -2.9799263194427575e-15 +-2.6317754673882573e-113 -2.6317754673882573e-113 +2.893844335276201e-245 2.893844335276201e-245 +1.0622982378106384e-298 1.0622982378106384e-298 +-3.5156035793378084e-303 -3.5156035793378084e-303 +-3.617803812771562e-278 -3.617803812771562e-278 +-2.113475266205609e+203 -2.113475266205609e+203 +2.3928647596901155e-47 2.3928647596901155e-47 +5.401777213831097e+41 5.401777213831097e+41 +-1.7543232000511543e+46 -1.7543232000511543e+46 +1.240254809462422e-175 1.240254809462422e-175 +4.3155782336610127e-244 4.3155782336610127e-244 +6.298132032438264e+299 6.298132032438264e+299 +-3.352345334019205e-65 -3.352345334019205e-65 +4.3190153797038705e-100 4.3190153797038705e-100 +-6.692415920940391e+166 -6.692415920940391e+166 +-0.0013700632220095016 -0.0013700632220095016 +-4.947662003446558e-48 -4.947662003446558e-48 +-2.6409883664091164e-238 -2.6409883664091164e-238 +-1.8071851890618847e-74 -1.8071851890618847e-74 +-1.8508805977350793e-232 -1.8508805977350793e-232 +4.388644061964233e+229 4.388644061964233e+229 +-7.231565549044985e+273 -7.231565549044985e+273 +-2.0244074382986373e-231 -2.0244074382986373e-231 +131160681633064750 131160681633064750 +-2.5155320689661464e+54 -2.5155320689661464e+54 +-1.8388565625279378e+154 -1.8388565625279378e+154 +-8.151433797629803e+200 -8.151433797629803e+200 +-2.5937841480887814e-290 -2.5937841480887814e-290 +7.706457739883967e+282 7.706457739883967e+282 +2.0962734564568772e+238 2.0962734564568772e+238 +9.910535349901511e-81 9.910535349901511e-81 +4.7157711504244576e+69 4.7157711504244576e+69 +-2.7402894462790804e-46 -2.7402894462790804e-46 +-9.827114711663551e+30 -9.827114711663551e+30 +3.554220677695545e-117 3.554220677695545e-117 +-3.4146963679845868e+106 -3.4146963679845868e+106 +5.318962862361985e-201 5.318962862361985e-201 +9.871402835410877e+121 9.871402835410877e+121 +1.1412348905629192e-17 1.1412348905629192e-17 +-3.1139246672401324e+268 -3.1139246672401324e+268 +-2.609003960593096e+189 -2.609003960593096e+189 +2.8365745257288946e-158 2.8365745257288946e-158 +5.3067422242857325e-257 5.3067422242857325e-257 +-5.859504740916972e-271 -5.859504740916972e-271 +2.2492147959687209e-7 2.2492147959687209e-7 +-4.904450879773971e+177 -4.904450879773971e+177 +-5.522285885085643e-211 -5.522285885085643e-211 +5.944098433468443e+58 5.944098433468443e+58 +5.287382825749597e-240 5.287382825749597e-240 +-2.730163728266833e+21 -2.730163728266833e+21 +7.34635890696579e+251 7.34635890696579e+251 +-2.361071505909628e+148 -2.361071505909628e+148 +-1.663767637436467e-173 -1.663767637436467e-173 +-1.7627621492330367e-100 -1.7627621492330367e-100 +-1.7726267448112727e+141 -1.7726267448112727e+141 +2.0963031384791104e+68 2.0963031384791104e+68 +1.3981225815730525e+97 1.3981225815730525e+97 +-0.0003511340184223033 -0.0003511340184223033 +-4.0203244551125314e+135 -4.0203244551125314e+135 +5.528323271543244e+96 5.528323271543244e+96 +-3.7831200314779335e+28 -3.7831200314779335e+28 +-7.677224984949491e-139 -7.677224984949491e-139 +1.7752143345314225e-153 1.7752143345314225e-153 +2.079849830512014e+146 2.079849830512014e+146 +-1.0649253909507246e-85 -1.0649253909507246e-85 +-1.698505363937437e+137 -1.698505363937437e+137 +-3.5238785336410245e+186 -3.5238785336410245e+186 +-4.7774833687480425e+66 -4.7774833687480425e+66 +-3.2648060987182744e-136 -3.2648060987182744e-136 +-9.675481075195783e-283 -9.675481075195783e-283 +2.8685391782610767e-297 2.8685391782610767e-297 +-7.830504195020289e-185 -7.830504195020289e-185 +7.15091583539001e-96 7.15091583539001e-96 +2.1449747822329523e+158 2.1449747822329523e+158 +1.3840745820148845e-224 1.3840745820148845e-224 +3.386667685000794e+164 3.386667685000794e+164 +-3.450521625473726e+78 -3.450521625473726e+78 +-1.2056736432887659e+191 -1.2056736432887659e+191 +-7707066905953.103 -7707066905953.103 +5.017409549258984e+51 5.017409549258984e+51 +-7.497547297577419e+120 -7.497547297577419e+120 +3.7553015979501766e+159 3.7553015979501766e+159 +1.2052405209697723e-43 1.2052405209697723e-43 +1.8094724146091532e+141 1.8094724146091532e+141 +-1.132385557860936e+21 -1.132385557860936e+21 +2.6348819330578877e-49 2.6348819330578877e-49 +2.9026231655232525e-252 2.9026231655232525e-252 +2.2844945428281225e+188 2.2844945428281225e+188 +-1.429555465011702e-210 -1.429555465011702e-210 +6.996268847778755e-68 6.996268847778755e-68 +-1.0873667179522438e-77 -1.0873667179522438e-77 +-1.3112478871856078e+178 -1.3112478871856078e+178 +-1.2543688823172222e+224 -1.2543688823172222e+224 +5.703312985821721e+105 5.703312985821721e+105 +1.507157503764499e-261 1.507157503764499e-261 +-1.2446308848016212e+62 -1.2446308848016212e+62 +-1.5461434970587543e+269 -1.5461434970587543e+269 +-6.091181953030231e+51 -6.091181953030231e+51 +-3.8743477942147977e+80 -3.8743477942147977e+80 +-1.0828121959261623e-271 -1.0828121959261623e-271 +-3.66632170832722e-117 -3.66632170832722e-117 +1.2191942888971858e-16 1.2191942888971858e-16 +2.4848069877552138e-27 2.4848069877552138e-27 +-1.585654148848525e+124 -1.585654148848525e+124 +-2.974227328000673e+87 -2.974227328000673e+87 +-8.142958132608413e+113 -8.142958132608413e+113 +1.0767616882577613e+277 1.0767616882577613e+277 +-4.886984147298285e-194 -4.886984147298285e-194 +-2.4963718596004362e-229 -2.4963718596004362e-229 +5.514570290486643e+292 5.514570290486643e+292 +3.6619200473683734e-20 3.6619200473683734e-20 +-1.0703086575601078e-211 -1.0703086575601078e-211 +9.057129337961223e+261 9.057129337961223e+261 +-3.166063060545819e+194 -3.166063060545819e+194 +-5.997298919156919e-161 -5.997298919156919e-161 +3.2457370351687516e-17 3.2457370351687516e-17 +-3.5266330213394023e-159 -3.5266330213394023e-159 +-5.5791033568319e+91 -5.5791033568319e+91 +4.439532144255031e-43 4.439532144255031e-43 +-1.4300306081185254e+261 -1.4300306081185254e+261 +9.224778198508854e-275 9.224778198508854e-275 +-3.8246058316146946e+237 -3.8246058316146946e+237 +-1.2014991654531094e-223 -1.2014991654531094e-223 +3.0979581038044984e-111 3.0979581038044984e-111 +2.6160258944737038e-291 2.6160258944737038e-291 +7.806602517684723e+158 7.806602517684723e+158 +1.8912880573096724e-85 1.8912880573096724e-85 +7.087898307086414e-123 7.087898307086414e-123 +-3.7969916607409635e+210 -3.7969916607409635e+210 +8.443146731548927e-21 8.443146731548927e-21 +-6.104954769872035e-10 -6.104954769872035e-10 +-2.1989844938741036e+154 -2.1989844938741036e+154 +7.353796715591475e-20 7.353796715591475e-20 +1.3187696273916067e-25 1.3187696273916067e-25 +1.164307194509587e-264 1.164307194509587e-264 +8.349495480932397e-308 8.349495480932397e-308 +-9.258581262766522e+251 -9.258581262766522e+251 +4.6234113780515647e+142 4.6234113780515647e+142 +1.8945188953861442e+145 1.8945188953861442e+145 +5.322531727952396e-46 5.322531727952396e-46 +-2.9399841684627e+48 -2.9399841684627e+48 +-7.0230703307239625e+280 -7.0230703307239625e+280 +1.3941349305053995e-273 1.3941349305053995e-273 +6.350037142543304e-29 6.350037142543304e-29 +-1.6249683535939513e+185 -1.6249683535939513e+185 +-4.726960026561967e-307 -4.726960026561967e-307 +1.9986620295855155e-160 1.9986620295855155e-160 +-1.5922609312335223e+137 -1.5922609312335223e+137 +-4.2315826357065187e+282 -4.2315826357065187e+282 +2.6177574325462284e-42 2.6177574325462284e-42 +7.36806483957801e-165 7.36806483957801e-165 +-1.43875771187933e+131 -1.43875771187933e+131 +-1.1326630508184948e-178 -1.1326630508184948e-178 +-7.676900049439047e-169 -7.676900049439047e-169 +-1.5577961498751442e-46 -1.5577961498751442e-46 +5.632896202530457e+161 5.632896202530457e+161 +-5.015764269816469e-231 -5.015764269816469e-231 +-430045426644470.06 -430045426644470.06 +2.1854724239024274e+77 2.1854724239024274e+77 +-6.750748166555277e-53 -6.750748166555277e-53 +3.268912253725717e-224 3.268912253725717e-224 +1.1611612234837974e+241 1.1611612234837974e+241 +6.500159003425045e+275 6.500159003425045e+275 +5.526473562254428e-268 5.526473562254428e-268 +-9.553307208171237e+60 -9.553307208171237e+60 +520320215.59967333 520320215.59967333 +-1.5450936288330776e+53 -1.5450936288330776e+53 +-2.134817825448733e-294 -2.134817825448733e-294 +-2.8454314294167886e+285 -2.8454314294167886e+285 +1.045043888132744e+32 1.045043888132744e+32 +2.5137980719579063e+120 2.5137980719579063e+120 +-1.9370984691012275e+43 -1.9370984691012275e+43 +-8.328248524404559e-80 -8.328248524404559e-80 +9.857207957637762e+244 9.857207957637762e+244 +1.0761966140044231e-125 1.0761966140044231e-125 +-5.1131467294321724e+207 -5.1131467294321724e+207 +-1.4661474995361762e-199 -1.4661474995361762e-199 +3.6471360870671885e+241 3.6471360870671885e+241 +4.4113660697082275e+189 4.4113660697082275e+189 +1.9513154973296528e+208 1.9513154973296528e+208 +-4.359652170047144e-253 -4.359652170047144e-253 +-2.445917969109003e-31 -2.445917969109003e-31 +3.4495625246284395e+92 3.4495625246284395e+92 +-3.325668420896801e+55 -3.325668420896801e+55 +-1.311313280852022e-166 -1.311313280852022e-166 +1.1578854311655213e-222 1.1578854311655213e-222 +2.2826502701400065e-119 2.2826502701400065e-119 +-2.2912875412098947e-85 -2.2912875412098947e-85 +-1.1402784420908204e+92 -1.1402784420908204e+92 +-7.99863326029472e+271 -7.99863326029472e+271 +-4.2661562114967973e-178 -4.2661562114967973e-178 +4.985749616362895e-298 4.985749616362895e-298 +-2.320633632608233e-102 -2.320633632608233e-102 +2.259223612842035e+181 2.259223612842035e+181 +-5.016026223669691e-283 -5.016026223669691e-283 +-3.818826543299911e+198 -3.818826543299911e+198 +1.5977189604917555e+224 1.5977189604917555e+224 +-5.541927275684575e+98 -5.541927275684575e+98 +-6.1347684033231716e+231 -6.1347684033231716e+231 +-5.222765945236173e-214 -5.222765945236173e-214 +1.4724592097001444e-125 1.4724592097001444e-125 +6.367852728769269e-272 6.367852728769269e-272 +-2.5036678981699013e+189 -2.5036678981699013e+189 +1.8137927902209716e-234 1.8137927902209716e-234 +-3.115802424623834e+25 -3.115802424623834e+25 +3.4227346966943447e-153 3.4227346966943447e-153 +2.3881697291031773e+234 2.3881697291031773e+234 +6.920287532122495e+29 6.920287532122495e+29 +1.3062861899016102e-208 1.3062861899016102e-208 +-4.64618509775119e+266 -4.64618509775119e+266 +-4.358306852409563e+268 -4.358306852409563e+268 +7.41346120219763e-303 7.41346120219763e-303 +-1.878411247728415e+39 -1.878411247728415e+39 +-1.3262042108217131e+308 -1.3262042108217131e+308 +-1.5281321616291197e-216 -1.5281321616291197e-216 +-2.0611906035584063e-304 -2.0611906035584063e-304 +-9.994921169058123e+149 -9.994921169058123e+149 +1.9540239206243955e-131 1.9540239206243955e-131 +2.935823704307452e-218 2.935823704307452e-218 From 1ab98ca64575e9d9bac4c1eb8f937b21ab1b035e Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Sun, 6 Sep 2026 18:52:07 +0800 Subject: [PATCH 024/165] =?UTF-8?q?feat(go):=20ticket=20#27=20=E2=80=94=20?= =?UTF-8?q?non-streaming=20provider=20relay=20+=20field-backfill=20repair?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The data-plane seam serves relay-safe non-streaming requests for one key-mode openai-responses provider directly upstream, behind the OPENCODEX_GO_HOTPATH_RELAY gate (default off). Outbound mirrors the TS passthrough verbatim (body, path, resolved Authorization); 2xx JSON bodies get the whole-body field backfill (annotations/id/status) re-serialised only when changed; non-JSON and non-2xx non-empty bodies relay verbatim with a valid Retry-After preserved. Everything else keeps the #24 parent bridge. The repair is pinned to the TypeScript oracle by committed goldens produced from backfillResponsesFieldsJson; the relay-safe predicate and direct-vs- bridge seam paths are covered by Go unit tests (dead parent bridge) and by the UA-differentiated differential harness. --- go/cmd/ocx-sidecar/main.go | 1 + go/internal/sidecar/hotpath.go | 10 + go/internal/sidecar/hotpath_relay.go | 582 ++++++++++++++++++ go/internal/sidecar/hotpath_relay_test.go | 441 +++++++++++++ go/internal/sidecar/responses_repair.go | 229 +++++++ go/internal/sidecar/responses_repair_test.go | 85 +++ go/internal/sidecar/sidecar.go | 5 + .../testdata/responses-repair-goldens.json | 164 +++++ 8 files changed, 1517 insertions(+) create mode 100644 go/internal/sidecar/hotpath_relay.go create mode 100644 go/internal/sidecar/hotpath_relay_test.go create mode 100644 go/internal/sidecar/responses_repair.go create mode 100644 go/internal/sidecar/responses_repair_test.go create mode 100644 go/internal/sidecar/testdata/responses-repair-goldens.json diff --git a/go/cmd/ocx-sidecar/main.go b/go/cmd/ocx-sidecar/main.go index 2db52506f8..f6141a320b 100644 --- a/go/cmd/ocx-sidecar/main.go +++ b/go/cmd/ocx-sidecar/main.go @@ -66,6 +66,7 @@ func serve() error { BridgeToken: os.Getenv("OCX_SIDECAR_BRIDGE_TOKEN"), RequestToken: os.Getenv("OCX_SIDECAR_REQUEST_TOKEN"), WriteRelaySecret: os.Getenv("OCX_SIDECAR_WRITE_RELAY_SECRET"), + HotPathRelay: os.Getenv(sidecar.HotPathRelayEnv) != "", } if cfg.Version == "" { fmt.Fprintln(os.Stderr, "ocx-sidecar: warning: OCX_SIDECAR_VERSION is unset; reporting version 0.0.0") diff --git a/go/internal/sidecar/hotpath.go b/go/internal/sidecar/hotpath.go index 7f970c52a6..56512d4484 100644 --- a/go/internal/sidecar/hotpath.go +++ b/go/internal/sidecar/hotpath.go @@ -84,6 +84,16 @@ func dataPlaneSeam(w http.ResponseWriter, r *http.Request, cfg Config) { http.Error(w, "request body too large", http.StatusRequestEntityTooLarge) return } + // The seam first asks whether this request can be served by the direct + // provider relay (ticket #27): a relay-safe non-streaming request for a + // key-mode openai-responses provider is answered upstream without the + // parent bridge, everything else falls through to the in-process pipeline. + // Refusals are silent here — they mean "bridge", never an error. + if plan, _ := requestQualifiesForRelay(cfg, r.Header.Get("Content-Type"), r.Header, body); plan != nil { + doDirectRelay(w, r, cfg, plan, body) + return + } + bridgeReq, err := http.NewRequestWithContext(r.Context(), http.MethodPost, parent.String(), bytes.NewReader(body)) if err != nil { http.Error(w, "responses bridge unavailable", http.StatusServiceUnavailable) diff --git a/go/internal/sidecar/hotpath_relay.go b/go/internal/sidecar/hotpath_relay.go new file mode 100644 index 0000000000..4ae8038682 --- /dev/null +++ b/go/internal/sidecar/hotpath_relay.go @@ -0,0 +1,582 @@ +package sidecar + +// Direct non-streaming provider relay for the data-plane seam (ticket #27, +// devlog 036). When the operator has turned the relay on +// (OPENCODEX_GO_HOTPATH_RELAY, Config.HotPathRelay) and a request qualifies, +// the sidecar replaces the #24 private parent bridge with a direct upstream +// relay for ONE provider class: a key-mode `openai-responses` provider whose +// Responses wire needs no translation, on a NON-STREAMING request. Everything +// else stays on the bridge so the TypeScript pipeline remains the oracle. +// +// The relay reproduces what the TS pipeline does for the qualifying subset, +// byte for byte (verified against the TS oracle): +// +// - outbound: POST /v1/responses with the seam +// request body verbatim, content-type application/json, and the provider +// Authorization when an env/literal apiKey resolves; +// - response: 2xx JSON bodies get the whole-body field backfill +// (responses_repair.go) and re-serialisation only when a field changed; +// non-JSON 2xx and non-empty non-2xx bodies are relayed verbatim with the +// upstream content-type; a valid upstream Retry-After is preserved and an +// invalid one dropped. +// +// Deliberately NOT ported (documented seam-period divergence, routing ticket +// #30 territory): pre-stream retry loops, synthetic 429 Retry-After defaults, +// quota/cyber-policy classification, and empty-body error envelopes. + +import ( + "bytes" + "fmt" + "io" + "net" + "net/http" + "net/url" + "os" + "path/filepath" + "strconv" + "strings" + + "github.com/lidge-jun/opencodex/go/internal/config" + "github.com/lidge-jun/opencodex/go/internal/jsonwire" +) + +// HotPathRelayEnv is the independent gate for the direct provider relay. It is +// read by the sidecar process at request time; the TypeScript front door only +// declares the constant (src/server/hot-path-seam.ts) and passes the +// environment through at spawn. +const HotPathRelayEnv = "OPENCODEX_GO_HOTPATH_RELAY" + +// Reserved canonical provider names whose routing depends on native OpenAI +// family logic (forward mode, account pools, tiers). Those providers never +// qualify for the #27 relay; the bridge keeps serving them. +var reservedOpenAIFamilyProviders = map[string]bool{ + "openai": true, + "openai-multi": true, + "openai-apikey": true, + "chatgpt": true, +} + +// Blocked metadata endpoints mirror the always-denied set in +// src/lib/destination-policy.ts. They are refused even with allowPrivateNetwork. +var blockedMetadataHosts = map[string]bool{ + "instance-data.ec2.internal": true, + "metadata.azure.internal": true, + "metadata.google.internal": true, + "169.254.169.254": true, + "169.254.170.2": true, + "100.100.100.200": true, + "fd00:ec2::254": true, +} + +// relay-blocking request headers: their presence means the TS pipeline would +// engage Codex pool / sub-agent / attestation / surface behaviour the relay +// does not reproduce, so the request stays on the bridge. +var relayBlockingRequestHeaders = []string{ + "x-codex-parent-thread-id", + "x-openai-subagent", + "x-codex-turn-metadata", + "x-chatgpt-account-id", + "chatgpt-account-id", + "x-oai-attestation", + "x-opencodex-vision-describe", + "cookie", +} + +// relayPlan is the outcome of the relay-safe predicate: everything the sidecar +// needs to make the direct upstream call for one admitted request. +type relayPlan struct { + providerName string + modelID string + endpoint string // full POST target URL + apiKey string // resolved bearer secret, "" when the provider has none +} + +// resolveRelayAPIKey resolves a provider apiKey the way the TS key store does: +// ${NAME} / $NAME read the environment, everything else is the literal value. +// A keychain: reference returns ok=false so the caller keeps the request on the +// bridge (the sidecar has no keychain access). +func resolveRelayAPIKey(raw string) (string, bool) { + if raw == "" { + return "", true + } + if strings.HasPrefix(raw, "keychain:") { + return "", false + } + if strings.HasPrefix(raw, "${") && strings.HasSuffix(raw, "}") { + name := raw[2 : len(raw)-1] + return os.Getenv(name), true + } + if strings.HasPrefix(raw, "$") { + return os.Getenv(raw[1:]), true + } + return raw, true +} + +// openaiResponsesRelayURL mirrors src/adapters/openai-responses-url.ts: +// strip trailing slashes, a trailing /responses endpoint, and a trailing /v1, +// then append /v1/responses. +func openaiResponsesRelayURL(baseURL string) (string, bool) { + trimmed := strings.TrimSpace(baseURL) + parsed, err := url.Parse(trimmed) + if err != nil || (parsed.Scheme != "http" && parsed.Scheme != "https") || parsed.User != nil || parsed.Host == "" { + return "", false + } + path := strings.TrimRight(parsed.Path, "/") + if rest := strings.TrimRight(path, "/"); strings.HasSuffix(rest, "/responses") { + path = strings.TrimRight(rest[:len(rest)-len("/responses")], "/") + } + if rest := strings.TrimRight(path, "/"); strings.HasSuffix(rest, "/v1") { + path = strings.TrimRight(rest[:len(rest)-len("/v1")], "/") + } + out := *parsed + out.Path = path + "/v1/responses" + out.RawPath = "" + out.RawQuery = "" + out.Fragment = "" + return out.String(), true +} + +// relayDestinationAllowed is the conservative destination gate for the direct +// relay. Loopback/private/link-local/metadata destinations require the +// operator's allowPrivateNetwork opt-in exactly like the TS policy; metadata +// and unspecified endpoints are always refused. Hostname destinations are +// accepted as-is (the TS sync path only hard-fails literal non-public +// destinations; DNS-resolved rebinding is a recorded residual there too). +func relayDestinationAllowed(endpoint string, allowPrivateNetwork bool) bool { + parsed, err := url.Parse(endpoint) + if err != nil { + return false + } + host := strings.TrimSuffix(parsed.Hostname(), ".") + if host == "" { + return false + } + if blockedMetadataHosts[strings.ToLower(host)] { + return false + } + local := host == "localhost" || strings.HasSuffix(host, ".localhost") + if !local { + if ip := net.ParseIP(host); ip != nil { + if ip.IsLoopback() || ip.IsPrivate() { + local = true + } + if ip.IsUnspecified() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() || ip.IsMulticast() || ip.IsInterfaceLocalMulticast() { + // Unspecified, link-local and multicast are always denied, matching + // the TS classifier's unconditional refusals. + return false + } + } + } + if local { + return allowPrivateNetwork + } + return true +} + +// relayRefusal names why a request did not qualify for the direct relay. It is +// surfaced in tests so each predicate leg is pinned. +type relayRefusal struct { + reason string +} + +func refuseRelay(format string, args ...any) *relayRefusal { + return &relayRefusal{reason: fmt.Sprintf(format, args...)} +} + +// bodyObjectMember returns a top-level body member, nil when absent. +func bodyMember(root *jsonwire.Value, key string) *jsonwire.Value { + if root == nil || root.Kind() != jsonwire.Object { + return nil + } + return root.Find(key) +} + +// requestQualifiesForRelay evaluates the relay-safe predicate over the config +// file and one admitted request body. Returns a relayPlan when the sidecar can +// serve the request directly, or a refusal naming the failing leg. A nil config +// root (no config file) is a refusal: without the operator's provider data the +// sidecar invents nothing. +func requestQualifiesForRelay(cfg Config, contentType string, headers http.Header, body []byte) (*relayPlan, *relayRefusal) { + if !cfg.HotPathRelay { + return nil, refuseRelay("relay disabled") + } + if contentType == "" || !strings.Contains(strings.ToLower(contentType), "application/json") { + return nil, refuseRelay("content-type is not application/json") + } + for _, name := range relayBlockingRequestHeaders { + if headers.Get(name) != "" { + return nil, refuseRelay("request carries %s", name) + } + } + if strings.EqualFold(headers.Get("x-opencodex-grok"), "1") { + return nil, refuseRelay("request is a grok-surface request") + } + + root, parseErr := jsonwire.Parse(body) + if parseErr != nil || root.Kind() != jsonwire.Object { + return nil, refuseRelay("body is not a JSON object") + } + if stream := bodyMember(root, "stream"); stream != nil { + if stream.Kind() == jsonwire.Bool && stream.Bool() { + return nil, refuseRelay("request is streaming") + } + } + modelValue := bodyMember(root, "model") + if modelValue == nil || modelValue.Kind() != jsonwire.String { + return nil, refuseRelay("model is not a string") + } + modelID := modelValue.String() + if modelID == "" || strings.Contains(modelID, "/") { + return nil, refuseRelay("model id is empty or namespaced") + } + if bodyMember(root, "previous_response_id") != nil { + return nil, refuseRelay("request carries previous_response_id") + } + if refusal := requestBodyRelayRefusal(root); refusal != nil { + return nil, refusal + } + + loaded := loadRelayConfigOrdered(cfg.ConfigDir) + if loaded == nil || loaded.Kind() != jsonwire.Object { + return nil, refuseRelay("provider config unavailable") + } + if refusal := configLevelRelayRefusal(loaded); refusal != nil { + return nil, refusal + } + + providers := loaded.Find("providers") + if providers == nil || providers.Kind() != jsonwire.Object { + return nil, refuseRelay("no providers configured") + } + plan, refusal := resolveRelayRoute(providers, loaded, modelID) + if refusal != nil { + return nil, refusal + } + plan.modelID = modelID + return plan, nil +} + +// loadRelayConfigOrdered reads the operator config.json into a jsonwire tree, +// resolving the directory exactly like the config echo routes: an explicit dir +// (unit tests) wins, otherwise config.Path() (OPENCODEX_HOME then +// ~/.opencodex). A missing or malformed file yields nil, which the predicate +// treats as a refusal rather than inventing provider data. +func loadRelayConfigOrdered(configDir string) *jsonwire.Value { + var path string + var err error + if configDir != "" { + path = filepath.Join(configDir, "config.json") + } else { + path, err = config.Path() + } + if err != nil { + return nil + } + raw, readErr := os.ReadFile(path) + if readErr != nil { + return nil + } + root, parseErr := jsonwire.Parse(raw) + if parseErr != nil { + return nil + } + return root +} + +// requestBodyRelayRefusal rejects body features whose outbound bytes the TS +// pipeline would rewrite or whose request-local state it would engage. +func requestBodyRelayRefusal(root *jsonwire.Value) *relayRefusal { + for _, key := range []string{"_compaction_request", "compaction_trigger"} { + if bodyMember(root, key) != nil { + return refuseRelay("request carries %s", key) + } + } + if tools := bodyMember(root, "tools"); tools != nil { + if tools.Kind() != jsonwire.Array { + return refuseRelay("tools is not an array") + } + for _, tool := range tools.Elements() { + if tool == nil || tool.Kind() != jsonwire.Object { + return refuseRelay("tool entry is not an object") + } + // Namespaced / hosted tools (web_search, image_generation, MCP + // custom tools, …) are normalized or refused by the TS pipeline; + // the relay only claims plain function tools. + if tool.Find("namespace") != nil { + return refuseRelay("tool carries a namespace") + } + typeName, _ := stringMember(tool, "type") + if typeName != "function" { + return refuseRelay("tool type %q is not relay-safe", typeName) + } + } + } + if input := bodyMember(root, "input"); input != nil { + if input.Kind() == jsonwire.Array { + for _, item := range input.Elements() { + if item == nil || item.Kind() != jsonwire.Object { + return refuseRelay("input item is not an object") + } + if item.Find("encrypted_content") != nil { + return refuseRelay("input carries encrypted_content") + } + typeName, _ := stringMember(item, "type") + if strings.HasPrefix(typeName, "compaction") || typeName == "custom_tool" || typeName == "reasoning_items" { + return refuseRelay("input item type %q is not relay-safe", typeName) + } + } + } else if input.Kind() != jsonwire.String { + return refuseRelay("input is neither a string nor an array") + } + } + return nil +} + +// configLevelRelayRefusal rejects config shapes whose routing logic the relay +// does not reproduce: combos, routing profiles, shadow intercept, and blocked +// model redirects. The request stays on the bridge when any is present. +// configLevelRelayRefusal rejects config shapes whose routing logic the relay +// does not reproduce: combos, routing profiles, shadow intercept, and blocked +// model redirects. The request stays on the bridge when any is present. +func configLevelRelayRefusal(loaded *jsonwire.Value) *relayRefusal { + if combos := loaded.Find("combos"); combos != nil && combos.Kind() == jsonwire.Object && len(combos.Members()) > 0 { + return refuseRelay("config defines combos") + } + if profiles := loaded.Find("routingProfiles"); profiles != nil && profiles.Kind() == jsonwire.Object && len(profiles.Members()) > 0 { + return refuseRelay("config defines routing profiles") + } + if redirects := loaded.Find("blockedModelRedirects"); redirects != nil && redirects.Kind() == jsonwire.Object && len(redirects.Members()) > 0 { + return refuseRelay("config defines blocked model redirects") + } + if shadow := loaded.Find("shadowCallIntercept"); shadow != nil && shadow.Kind() == jsonwire.Object { + if enabled := shadow.Find("enabled"); enabled != nil && enabled.Kind() == jsonwire.Bool && enabled.Bool() { + return refuseRelay("config enables shadow call intercept") + } + } + return nil +} + +// resolveRelayRoute mirrors the TS routeModelInternal subset the relay claims: +// configured-default-model, configured-model-list, and the default-provider +// fallback, iterating providers in file order. Reserved native OpenAI provider +// names, non-key auth modes, and non-openai-responses adapters never qualify. +func resolveRelayRoute(providers *jsonwire.Value, loaded *jsonwire.Value, modelID string) (*relayPlan, *relayRefusal) { + active := activeRelayProviders(providers) + if len(active) == 0 { + return nil, refuseRelay("no enabled providers") + } + + // configured-default-model pass. + for _, entry := range active { + if entry.provider.Find("defaultModel") == nil { + continue + } + if defaultModel, ok := stringMember(entry.provider, "defaultModel"); ok && defaultModel == modelID { + return relayPlanForProvider(entry.name, entry.provider) + } + } + + // configured-model-list pass (file order, first hit — the TS loop returns + // on the first active provider whose list matches). + for _, entry := range active { + models := entry.provider.Find("models") + if models == nil || models.Kind() != jsonwire.Array { + continue + } + for _, candidate := range models.Elements() { + if candidate.Kind() == jsonwire.String && candidate.String() == modelID { + return relayPlanForProvider(entry.name, entry.provider) + } + } + } + + // default-provider fallback. Refuse the legacy chatgpt/openai-multi ids + // exactly like routeModelInternal throws for them. + defaultRaw, ok := stringMember(loaded, "defaultProvider") + if !ok { + return nil, refuseRelay("no defaultProvider configured") + } + if defaultRaw == "chatgpt" || defaultRaw == "openai-multi" { + return nil, refuseRelay("default provider %q is not relay-safe", defaultRaw) + } + for _, entry := range active { + if entry.name == defaultRaw { + return relayPlanForProvider(entry.name, entry.provider) + } + } + return nil, refuseRelay("no provider owns model %q", modelID) +} + +type relayProviderEntry struct { + name string + provider *jsonwire.Value +} + +func activeRelayProviders(providers *jsonwire.Value) []relayProviderEntry { + var out []relayProviderEntry + for _, member := range providers.Members() { + if member.Value == nil || member.Value.Kind() != jsonwire.Object { + continue + } + if disabled := member.Value.Find("disabled"); disabled != nil && disabled.Kind() == jsonwire.Bool && disabled.Bool() { + continue + } + out = append(out, relayProviderEntry{name: member.Key, provider: member.Value}) + } + return out +} + +func boolMember(obj *jsonwire.Value, key string) (bool, bool) { + if obj == nil || obj.Kind() != jsonwire.Object { + return false, false + } + member := obj.Find(key) + if member == nil || member.Kind() != jsonwire.Bool { + return false, false + } + return member.Bool(), true +} + +// relayPlanForProvider validates one candidate provider for the relay and +// builds the endpoint. Returns a refusal when the provider row needs TS-only +// machinery (forward/oauth auth, keychain keys, non-responses adapter, custom +// responses path, reserved name, blocked destination). +func relayPlanForProvider(name string, provider *jsonwire.Value) (*relayPlan, *relayRefusal) { + if reservedOpenAIFamilyProviders[name] { + return nil, refuseRelay("provider %q is a reserved native OpenAI row", name) + } + adapter, ok := stringMember(provider, "adapter") + if !ok || adapter != "openai-responses" { + return nil, refuseRelay("provider %q adapter %q is not openai-responses", name, adapter) + } + if authMode, ok := stringMember(provider, "authMode"); ok && authMode != "key" { + return nil, refuseRelay("provider %q authMode %q is not key", name, authMode) + } + if provider.Find("responsesPath") != nil { + return nil, refuseRelay("provider %q configures a custom responsesPath", name) + } + apiKey := "" + if raw, ok := stringMember(provider, "apiKey"); ok { + var keyOK bool + apiKey, keyOK = resolveRelayAPIKey(raw) + if !keyOK { + return nil, refuseRelay("provider %q apiKey is a keychain reference", name) + } + } + baseURL, ok := stringMember(provider, "baseUrl") + if !ok { + return nil, refuseRelay("provider %q has no baseUrl", name) + } + endpoint, ok := openaiResponsesRelayURL(baseURL) + if !ok { + return nil, refuseRelay("provider %q baseUrl is unusable", name) + } + allowPrivate := false + if value, present := boolMember(provider, "allowPrivateNetwork"); present { + allowPrivate = value + } + if !relayDestinationAllowed(endpoint, allowPrivate) { + return nil, refuseRelay("provider %q baseUrl destination is not allowed", name) + } + return &relayPlan{providerName: name, endpoint: endpoint, apiKey: apiKey}, nil +} + +// directRelayResponseBytes is the bounded upstream body the relay read plus the +// headers it must reproduce. +const ( + // Matches MAX_UPSTREAM_JSON_BODY_BYTES in the TS bounded-JSON read: far + // above any legitimate non-streaming completion, and the same ceiling the + // relay applies to a non-2xx body relay. + maxRelayUpstreamBodyBytes = 32 * 1024 * 1024 +) + +// relayRetryAfterValidation mirrors validateClientRetryAfterHeader: numeric +// seconds (including the instant "0") and HTTP dates pass; empty/oversized/ +// malformed values are dropped. +func relayRetryAfterValid(value string) bool { + trimmed := strings.TrimSpace(value) + if trimmed == "" || len(trimmed) > 128 { + return false + } + if trimmed == "0" { + return true + } + if seconds, err := strconv.Atoi(trimmed); err == nil && seconds > 0 { + return true + } + if _, err := http.ParseTime(trimmed); err == nil { + return true + } + return false +} + +// doDirectRelay performs the upstream call and writes the client response. +// Status, content-type, and body semantics mirror the TS passthrough for the +// claimed subset: 2xx JSON bodies are repaired, everything else is relayed +// verbatim (bounded), and a valid upstream Retry-After survives while an +// invalid one is dropped. +func doDirectRelay(w http.ResponseWriter, r *http.Request, cfg Config, plan *relayPlan, body []byte) { + upstreamReq, err := http.NewRequestWithContext(r.Context(), http.MethodPost, plan.endpoint, bytes.NewReader(body)) + if err != nil { + http.Error(w, "provider relay unavailable", http.StatusServiceUnavailable) + return + } + upstreamReq.Header.Set("Content-Type", "application/json") + if plan.apiKey != "" { + upstreamReq.Header.Set("Authorization", "Bearer "+plan.apiKey) + } + upstreamResp, err := relayUpstreamClient().Do(upstreamReq) + if err != nil { + http.Error(w, "provider relay unavailable", http.StatusServiceUnavailable) + return + } + defer upstreamResp.Body.Close() + + rawBody, readErr := io.ReadAll(io.LimitReader(upstreamResp.Body, maxRelayUpstreamBodyBytes+1)) + if readErr != nil || len(rawBody) > maxRelayUpstreamBodyBytes { + // Oversized or unreadable body: refuse like the TS bounded read fails + // closed, without emitting a partial body. + http.Error(w, `{"error":{"message":"upstream response exceeded the safe body limit","type":"server_error","code":"upstream_server_error"}}`, http.StatusBadGateway) + w.Header().Set("Content-Type", "application/json") + return + } + + contentType := upstreamResp.Header.Get("Content-Type") + if contentType != "" { + w.Header().Set("Content-Type", contentType) + } else { + w.Header().Set("Content-Type", "application/json") + } + if retryAfter := upstreamResp.Header.Get("Retry-After"); relayRetryAfterValid(retryAfter) { + w.Header().Set("Retry-After", strings.TrimSpace(retryAfter)) + } + + out := rawBody + if upstreamResp.StatusCode >= 200 && upstreamResp.StatusCode < 300 { + if strings.Contains(strings.ToLower(contentType), "application/json") { + // RepairResponsesJSONBody returns the original bytes untouched when + // the backfill changed nothing or the body is not a JSON object, so + // assigning unconditionally preserves raw-bytes relay parity. + out, _ = RepairResponsesJSONBody(rawBody) + } + } + w.WriteHeader(upstreamResp.StatusCode) + if _, err := w.Write(out); err != nil { + fmt.Fprintf(os.Stderr, "ocx-sidecar: relay write: %v\n", err) + } +} + +// relayUpstreamClient reaches the configured provider without a system proxy +// (the parent bridge transport contract) and without following redirects, so a +// provider-supplied redirect can never carry the Authorization header to a +// different host. +func relayUpstreamClient() *http.Client { + return &http.Client{ + Transport: bridgeTransport(), + CheckRedirect: func(_ *http.Request, _ []*http.Request) error { + return http.ErrUseLastResponse + }, + } +} + +// Config helpers used by the relay; jsonwire re-export for tests. diff --git a/go/internal/sidecar/hotpath_relay_test.go b/go/internal/sidecar/hotpath_relay_test.go new file mode 100644 index 0000000000..c5315fd70e --- /dev/null +++ b/go/internal/sidecar/hotpath_relay_test.go @@ -0,0 +1,441 @@ +package sidecar + +import ( + "bytes" + "encoding/json" + "io" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "strings" + "testing" +) + +// relayFixtureConfigDir writes the canonical #27 fixture config into a temp +// dir: one key-mode openai-responses provider named "test" whose models list +// owns "test-model", defaultProvider "test", and an upstream loopback URL. +// Returns the dir and the resolved relay endpoint the upstream must see. +func relayFixtureConfigDir(t *testing.T, upstreamURL string, extra map[string]any) string { + t.Helper() + dir := t.TempDir() + provider := map[string]any{ + "adapter": "openai-responses", + "baseUrl": upstreamURL + "/v1", + "allowPrivateNetwork": true, + "disabled": false, + "models": []any{"test-model"}, + } + for key, value := range extra { + if value == nil { + delete(provider, key) + } else { + provider[key] = value + } + } + config := map[string]any{ + "defaultProvider": "test", + "providers": map[string]any{ + "test": provider, + }, + } + raw, err := json.Marshal(config) + if err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(dir, "config.json"), raw, 0o600); err != nil { + t.Fatal(err) + } + return dir +} + +// relaySeamHandler builds the full sidecar handler for one fixture config dir, +// pointing the parent bridge at a port that is never listening. A direct relay +// answers 200 without it; any fall-through to the bridge answers 503 — which +// is how the tests distinguish the two paths. +func relaySeamHandler(t *testing.T, configDir string, relayOn bool) http.Handler { + t.Helper() + return NewHandler(Config{ + Service: "opencodex", + Version: "2.42.0", + ParentURL: "http://127.0.0.1:1", // deliberately dead bridge + BridgeToken: "sidecar-to-parent", + RequestToken: "parent-to-sidecar", + WriteRelaySecret: "sidecar-to-parent", + ConfigDir: configDir, + HotPathRelay: relayOn, + }) +} + +func relayPost(t *testing.T, h http.Handler, body string) (*httptest.ResponseRecorder, error) { + t.Helper() + req := httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(body)) + req.Header.Set(SidecarRequestHeader, "parent-to-sidecar") + req.Header.Set("Content-Type", "application/json") + rec := httptest.NewRecorder() + h.ServeHTTP(rec, req) + return rec, rec.Result().Body.Close() +} + +// deadSparseUpstream returns a recording upstream that answers POST +// /v1/responses with a sparse non-streaming JSON body (missing annotations, id +// and status) so the client-visible bytes prove the repair ran. requestLog +// receives every request the relay actually made. +func deadSparseUpstream(t *testing.T, requestLog func(r *http.Request, body []byte)) *httptest.Server { + t.Helper() + return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + body, err := io.ReadAll(r.Body) + if err != nil { + t.Errorf("upstream read body: %v", err) + } + if requestLog != nil { + requestLog(r, body) + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{"id":"resp_fixture","status":"completed","output":[{"type":"message","role":"assistant","content":[{"type":"output_text","text":"hi"}]}]}`)) + })) +} + +func TestDirectRelayReplacesTheBridgeForARelaySafeRequest(t *testing.T) { + upstream := deadSparseUpstream(t, nil) + defer upstream.Close() + + var sawMethod, sawPath, sawUA, sawAuth, sawCT string + var sawBody []byte + upstream.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + sawMethod = r.Method + sawPath = r.URL.Path + sawUA = r.UserAgent() + sawAuth = r.Header.Get("Authorization") + sawCT = r.Header.Get("Content-Type") + var err error + sawBody, err = io.ReadAll(r.Body) + if err != nil { + t.Errorf("upstream read body: %v", err) + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{"id":"resp_fixture","status":"completed","output":[{"type":"message","role":"assistant","content":[{"type":"output_text","text":"hi"}]}]}`)) + }) + + configDir := relayFixtureConfigDir(t, upstream.URL, nil) + h := relaySeamHandler(t, configDir, true) + const requestBody = `{"model":"test-model","input":"ping"}` + rec, err := relayPost(t, h, requestBody) + if err != nil { + t.Fatal(err) + } + if rec.Code != http.StatusOK { + t.Fatalf("status = %d, want 200 (bridge is dead; a 503 would mean no direct relay)", rec.Code) + } + if got := rec.Header().Get("Content-Type"); got != "application/json" { + t.Fatalf("Content-Type = %q, want application/json", got) + } + // The client-visible body must be the REPAIRED bytes, not the sparse + // upstream body: annotations, id and status are all backfilled. + want := `{"id":"resp_fixture","status":"completed","output":[{"type":"message","role":"assistant","content":[{"type":"output_text","text":"hi","annotations":[]}],"id":"msg_ocx_0","status":"completed"}]}` + if got := rec.Body.String(); got != want { + t.Fatalf("client body diverged\n got: %s\nwant: %s", got, want) + } + + // The outbound upstream call mirrors the TS passthrough: same verb, same + // responses path, the request body verbatim, JSON content type, no auth + // header for a keyless provider, and Go's own user agent (the marker that + // distinguishes direct relay from the Bun bridge in the differential). + if sawMethod != http.MethodPost || sawPath != "/v1/responses" { + t.Fatalf("upstream saw %s %s, want POST /v1/responses", sawMethod, sawPath) + } + if string(sawBody) != requestBody { + t.Fatalf("upstream body diverged\n got: %s\nwant: %s", sawBody, requestBody) + } + if sawCT != "application/json" { + t.Fatalf("upstream Content-Type = %q", sawCT) + } + if sawAuth != "" { + t.Fatalf("upstream Authorization = %q for a keyless provider", sawAuth) + } + if !strings.HasPrefix(sawUA, "Go-http-client/") { + t.Fatalf("upstream User-Agent = %q, want the Go http client's", sawUA) + } +} + +func TestDirectRelaySendsResolvedBearerWhenProviderHasAPIKey(t *testing.T) { + upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + io.Copy(io.Discard, r.Body) + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{"id":"resp_fixture","status":"completed","output":[{"type":"message","content":[{"type":"output_text","text":"hi"}]}]}`)) + })) + defer upstream.Close() + + t.Setenv("OCX_TEST_KEY", "secret-value") + configDir := relayFixtureConfigDir(t, upstream.URL, map[string]any{"apiKey": "${OCX_TEST_KEY}"}) + h := relaySeamHandler(t, configDir, true) + // Capture the upstream Authorization header. + var auth string + upstream.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + auth = r.Header.Get("Authorization") + io.Copy(io.Discard, r.Body) + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{"id":"r","status":"completed","output":[]}`)) + }) + rec, err := relayPost(t, h, `{"model":"test-model","input":"ping"}`) + if err != nil { + t.Fatal(err) + } + if rec.Code != http.StatusOK { + t.Fatalf("status = %d", rec.Code) + } + if auth != "Bearer secret-value" { + t.Fatalf("Authorization = %q, want Bearer secret-value", auth) + } +} + +func TestDirectRelayPreservesRetryAfterAndNonJSON2xxVerbatum(t *testing.T) { + upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + io.Copy(io.Discard, r.Body) + w.Header().Set("Content-Type", "application/json") + w.Header().Set("Retry-After", "7") + w.WriteHeader(http.StatusTooManyRequests) + _, _ = w.Write([]byte(`{"error":{"message":"slow down","type":"rate_limit_error","code":"rate_limit_exceeded"}}`)) + })) + defer upstream.Close() + + configDir := relayFixtureConfigDir(t, upstream.URL, nil) + h := relaySeamHandler(t, configDir, true) + rec, err := relayPost(t, h, `{"model":"test-model","input":"ping"}`) + if err != nil { + t.Fatal(err) + } + if rec.Code != http.StatusTooManyRequests { + t.Fatalf("status = %d, want 429", rec.Code) + } + if got := rec.Header().Get("Retry-After"); got != "7" { + t.Fatalf("Retry-After = %q, want 7 (valid upstream header survives)", got) + } + // Non-2xx bodies are relayed verbatim, never repaired. + if got, want := rec.Body.String(), `{"error":{"message":"slow down","type":"rate_limit_error","code":"rate_limit_exceeded"}}`; got != want { + t.Fatalf("body diverged\n got: %s\nwant: %s", got, want) + } +} + +func TestDirectRelayDropsInvalidRetryAfterAndRepairsPlain2xx(t *testing.T) { + upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + io.Copy(io.Discard, r.Body) + // An ok non-streaming JSON body with a garbage Retry-After. + w.Header().Set("Content-Type", "application/json") + w.Header().Set("Retry-After", "garbage") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{"id":"r","status":"completed","output":[{"type":"message","content":[{"type":"output_text","text":"hi"}]}]}`)) + })) + defer upstream.Close() + + configDir := relayFixtureConfigDir(t, upstream.URL, nil) + h := relaySeamHandler(t, configDir, true) + rec, err := relayPost(t, h, `{"model":"test-model","input":"ping"}`) + if err != nil { + t.Fatal(err) + } + if rec.Code != http.StatusOK { + t.Fatalf("status = %d", rec.Code) + } + if got := rec.Header().Get("Retry-After"); got != "" { + t.Fatalf("Retry-After = %q, want dropped (invalid upstream value)", got) + } + if !strings.Contains(rec.Body.String(), `"annotations":[]`) || !strings.Contains(rec.Body.String(), `"id":"msg_ocx_0"`) { + t.Fatalf("repair did not run on the 2xx body: %s", rec.Body.String()) + } +} + +// TestDirectRelayStreamingFallsBackToBridge: the relay claims non-streaming +// requests only. A streaming request on the same config must take the bridge +// path — proven by the 503 from the dead bridge (a relay would have answered +// 200 from the fixture upstream). +func TestDirectRelayStreamingFallsBackToBridge(t *testing.T) { + upstream := deadSparseUpstream(t, nil) + defer upstream.Close() + configDir := relayFixtureConfigDir(t, upstream.URL, nil) + h := relaySeamHandler(t, configDir, true) + rec, err := relayPost(t, h, `{"model":"test-model","input":"ping","stream":true}`) + if err != nil { + t.Fatal(err) + } + if rec.Code != http.StatusServiceUnavailable { + t.Fatalf("status = %d, want 503 (streaming request must not reach the relay)", rec.Code) + } +} + +// TestDirectRelayGateOffStaysOnTheBridge: with the relay env gate off the +// whole predicate short-circuits and every request takes the bridge. +func TestDirectRelayGateOffStaysOnTheBridge(t *testing.T) { + configDir := relayFixtureConfigDir(t, "http://127.0.0.1:9", nil) + h := relaySeamHandler(t, configDir, false) + rec, err := relayPost(t, h, `{"model":"test-model","input":"ping"}`) + if err != nil { + t.Fatal(err) + } + if rec.Code != http.StatusServiceUnavailable { + t.Fatalf("status = %d, want 503 (gate off means bridge)", rec.Code) + } +} + +func TestRequestQualifiesForRelayRefusals(t *testing.T) { + upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + io.Copy(io.Discard, r.Body) + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{}`)) + })) + defer upstream.Close() + + cases := []struct { + name string + configEdit map[string]any + body string + headers map[string]string + wantRefuse string + }{ + {"happy path qualifies", nil, `{"model":"test-model","input":"ping"}`, nil, ""}, + {"unknown model routes to default provider", nil, `{"model":"other-model","input":"ping"}`, nil, ""}, + {"streaming refuses", nil, `{"model":"test-model","input":"ping","stream":true}`, nil, "streaming"}, + {"namespaced model refuses", nil, `{"model":"test/test-model","input":"ping"}`, nil, "namespaced"}, + {"empty model refuses", nil, `{"model":"","input":"ping"}`, nil, "model id is empty"}, + {"previous_response_id refuses", nil, `{"model":"test-model","input":"ping","previous_response_id":"x"}`, nil, "previous_response_id"}, + {"compaction marker refuses", nil, `{"model":"test-model","input":"ping","compaction_trigger":"x"}`, nil, "compaction"}, + {"namespaced tool refuses", nil, `{"model":"test-model","input":"ping","tools":[{"type":"function","name":"f","namespace":"mcp"}]}`, nil, "namespace"}, + {"non-function tool refuses", nil, `{"model":"test-model","input":"ping","tools":[{"type":"web_search"}]}`, nil, "not relay-safe"}, + {"encrypted input refuses", nil, `{"model":"test-model","input":[{"type":"message","encrypted_content":"blob"}]}`, nil, "encrypted_content"}, + {"codex parent header refuses", nil, `{"model":"test-model","input":"ping"}`, map[string]string{"x-codex-parent-thread-id": "t1"}, "x-codex-parent-thread-id"}, + {"grok surface refuses", nil, `{"model":"test-model","input":"ping"}`, map[string]string{"x-opencodex-grok": "1"}, "grok"}, + {"reserved openai row refuses", map[string]any{"name": "openai"}, `{"model":"test-model","input":"ping"}`, nil, "reserved native"}, + {"oauth auth mode refuses", map[string]any{"authMode": "oauth"}, `{"model":"test-model","input":"ping"}`, nil, "not key"}, + {"non-responses adapter refuses", map[string]any{"adapter": "anthropic"}, `{"model":"test-model","input":"ping"}`, nil, "not openai-responses"}, + {"keychain apiKey refuses", map[string]any{"apiKey": "keychain:prod"}, `{"model":"test-model","input":"ping"}`, nil, "keychain"}, + {"custom responsesPath refuses", map[string]any{"responsesPath": "/chat"}, `{"model":"test-model","input":"ping"}`, nil, "responsesPath"}, + {"default provider absent refuses", map[string]any{"defaultProvider": "gone"}, `{"model":"other-model","input":"ping"}`, nil, "no provider owns model"}, + } + for _, c := range cases { + c := c + t.Run(c.name, func(t *testing.T) { + name := "test" + extra := map[string]any{} + if c.configEdit != nil { + extra = c.configEdit + } + defaultProvider := "test" + if _, renamed := extra["name"]; renamed { + name = extra["name"].(string) + delete(extra, "name") + } + if value, ok := extra["defaultProvider"]; ok { + defaultProvider = value.(string) + delete(extra, "defaultProvider") + } + config := map[string]any{ + "defaultProvider": defaultProvider, + "providers": map[string]any{ + name: map[string]any{ + "adapter": "openai-responses", + "baseUrl": upstream.URL + "/v1", + "allowPrivateNetwork": true, + "disabled": false, + "models": []any{"test-model"}, + }, + }, + } + for key, value := range extra { + config["providers"].(map[string]any)[name].(map[string]any)[key] = value + } + raw, err := json.Marshal(config) + if err != nil { + t.Fatal(err) + } + dir := t.TempDir() + if err := os.WriteFile(filepath.Join(dir, "config.json"), raw, 0o600); err != nil { + t.Fatal(err) + } + headers := make(http.Header) + for key, value := range c.headers { + headers.Set(key, value) + } + plan, refusal := requestQualifiesForRelay(Config{HotPathRelay: true, ConfigDir: dir}, "application/json", headers, []byte(c.body)) + if c.wantRefuse == "" { + if plan == nil { + t.Fatalf("expected a plan, got refusal %q", refusal.reason) + } + return + } + if plan != nil { + t.Fatalf("expected refusal containing %q, got a plan", c.wantRefuse) + } + if refusal == nil || !strings.Contains(refusal.reason, c.wantRefuse) { + t.Fatalf("refusal = %v, want it to contain %q", refusal, c.wantRefuse) + } + }) + } +} + +func TestOpenaiResponsesRelayURLNormalisesBase(t *testing.T) { + cases := map[string]string{ + "http://host/v1": "http://host/v1/responses", + "http://host/v1/": "http://host/v1/responses", + "http://host/v1/responses": "http://host/v1/responses", + "http://host/v1/responses/": "http://host/v1/responses", + "http://host": "http://host/v1/responses", + "http://host/": "http://host/v1/responses", + "http://host/base/v1": "http://host/base/v1/responses", + } + for input, want := range cases { + got, ok := openaiResponsesRelayURL(input) + if !ok || got != want { + t.Errorf("openaiResponsesRelayURL(%q) = %q,%v want %q,true", input, got, ok, want) + } + } + for _, input := range []string{"not a url", "ftp://host/v1", "http://user:pw@host/v1", "http:///v1"} { + if _, ok := openaiResponsesRelayURL(input); ok { + t.Errorf("openaiResponsesRelayURL(%q) should refuse", input) + } + } +} + +func TestRelayRetryAfterValidation(t *testing.T) { + valid := []string{"7", "0", " 12 ", "Wed, 21 Oct 2015 07:28:00 GMT", "120"} + for _, value := range valid { + if !relayRetryAfterValid(value) { + t.Errorf("relayRetryAfterValid(%q) = false, want true", value) + } + } + invalid := []string{"", "garbage", " ", "999999999999999999999999", strings.Repeat("a", 129)} + for _, value := range invalid { + if relayRetryAfterValid(value) { + t.Errorf("relayRetryAfterValid(%q) = true, want false", value) + } + } +} + +// TestDirectRelayBodyBound mirrors the TS bounded read: an oversized upstream +// JSON body must not leak a partial body to the client. +func TestDirectRelayBodyBound(t *testing.T) { + huge := `{"id":"r","output":[` + strings.Repeat(`{"type":"message","content":[]},`, (maxRelayUpstreamBodyBytes/32)+16) + `]}` + upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + io.Copy(io.Discard, r.Body) + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(huge)) + })) + defer upstream.Close() + configDir := relayFixtureConfigDir(t, upstream.URL, nil) + h := relaySeamHandler(t, configDir, true) + rec, err := relayPost(t, h, `{"model":"test-model","input":"ping"}`) + if err != nil { + t.Fatal(err) + } + if rec.Code != http.StatusBadGateway { + t.Fatalf("status = %d, want 502 for an oversized upstream body", rec.Code) + } + if got := rec.Body.String(); !bytes.Contains([]byte(got), []byte("exceeded the safe body limit")) { + t.Fatalf("body = %s", got) + } +} diff --git a/go/internal/sidecar/responses_repair.go b/go/internal/sidecar/responses_repair.go new file mode 100644 index 0000000000..78af2e11d1 --- /dev/null +++ b/go/internal/sidecar/responses_repair.go @@ -0,0 +1,229 @@ +package sidecar + +// Whole-body JSON field backfill for the data-plane relay (ticket #27, devlog +// 036). The TypeScript bounded-JSON passthrough path repairs a completed +// Responses object before the client sees it: +// +// - output_text content parts missing `annotations` get `annotations: []`; +// - output items missing (or empty-string) `id` get a synthetic +// `_ocx_` id; +// - message items missing `status` get one derived from the response's own +// status. +// +// The transform is the mirror of +// src/server/responses/responses-field-backfill.ts (backfillResponsesFieldsJson) +// and is pinned byte-for-byte by unit tests against payloads captured from the +// TypeScript oracle. It only re-serialises the document when something changed; +// an untouched document must be relayed as the original raw bytes, exactly like +// the TS path. + +import ( + "github.com/lidge-jun/opencodex/go/internal/jsonwire" +) + +// responsesItemIDPrefixes mirrors ITEM_ID_PREFIXES in the TS field-backfill +// module. The generic `item_` prefix is the fallback for unknown item types; +// compaction items are excluded from the backfill entirely (their contract has +// no id). +var responsesItemIDPrefixes = map[string]string{ + "message": "msg_", + "reasoning": "rs_", + "function_call": "fc_", + "custom_tool_call": "ctc_", + "tool_search_call": "tsc_", + "web_search_call": "ws_", + "file_search_call": "fs_", + "code_interpreter_call": "ci_", + "computer_call": "cc_", + "image_generation_call": "ig_", + "image_gen_call": "ig_", +} + +var compactionItemTypes = map[string]bool{ + "compaction": true, + "compaction_summary": true, + "context_compaction": true, +} + +// responseStatusToItemStatus mirrors messageStatusFromResponseStatus: a valid +// OutputMessage status passes through, queued becomes in_progress, and +// failed/cancelled map to incomplete. Anything else reports no inference. +func responseStatusToItemStatus(status string) (string, bool) { + switch status { + case "in_progress", "completed", "incomplete": + return status, true + case "queued": + return "in_progress", true + case "failed", "cancelled": + return "incomplete", true + } + return "", false +} + +// backfillOutputTextPart adds annotations: [] to an output_text content part +// when the key is absent. Returns the same node when nothing changed. +func backfillOutputTextPart(part *jsonwire.Value) (*jsonwire.Value, bool) { + if part == nil || part.Kind() != jsonwire.Object { + return part, false + } + if part.Find("annotations") != nil { + return part, false + } + typeName, _ := stringMember(part, "type") + if typeName != "output_text" { + return part, false + } + part.Set("annotations", jsonwire.EmptyArray()) + return part, true +} + +// stringMember reads an object member as a string. +func stringMember(obj *jsonwire.Value, key string) (string, bool) { + if obj == nil || obj.Kind() != jsonwire.Object { + return "", false + } + member := obj.Find(key) + if member == nil || member.Kind() != jsonwire.String { + return "", false + } + return member.String(), true +} + +// backfillContentArray walks an item's content array and repairs output_text +// parts. Mirrors backfillContentArray: non-array content and non-object parts +// pass through untouched. +func backfillContentArray(content *jsonwire.Value) bool { + if content == nil || content.Kind() != jsonwire.Array { + return false + } + changed := false + for _, part := range content.Elements() { + if _, ok := backfillOutputTextPart(part); ok { + changed = true + } + } + return changed +} + +// backfillItemID adds a synthetic _ocx_ id to an output item +// when its id is absent or an empty string. Mirrors backfillItemId: a +// non-string id is treated as absent, exactly like the TS typeof check. +func backfillItemID(item *jsonwire.Value, index int) bool { + if item == nil || item.Kind() != jsonwire.Object { + return false + } + if id := item.Find("id"); id != nil { + if id.Kind() == jsonwire.String && id.String() != "" { + return false + } + } + typeName, _ := stringMember(item, "type") + prefix, known := responsesItemIDPrefixes[typeName] + if !known { + prefix = "item_" + } + item.Set("id", jsonwire.StringValue(prefix+"ocx_"+itoa(index))) + return true +} + +func itoa(value int) string { + if value == 0 { + return "0" + } + negative := value < 0 + if negative { + value = -value + } + var buf [20]byte + pos := len(buf) + for value > 0 { + pos-- + buf[pos] = byte('0' + value%10) + value /= 10 + } + if negative { + pos-- + buf[pos] = '-' + } + return string(buf[pos:]) +} + +// backfillItemStatus adds a status to a message item when absent. +func backfillItemStatus(item *jsonwire.Value, inferred string) bool { + if item == nil || item.Kind() != jsonwire.Object { + return false + } + if item.Find("status") != nil { + return false + } + typeName, _ := stringMember(item, "type") + if typeName != "message" { + return false + } + item.Set("status", jsonwire.StringValue(inferred)) + return true +} + +// backfillOutputItem repairs one output item in place: content parts, then id, +// then status. Compaction items are excluded (their shape has no required id). +func backfillOutputItem(item *jsonwire.Value, index int, inferredStatus string) bool { + if item == nil || item.Kind() != jsonwire.Object { + return false + } + typeName, _ := stringMember(item, "type") + if compactionItemTypes[typeName] { + return false + } + changed := false + changed = backfillContentArray(item.Find("content")) || changed + changed = backfillItemID(item, index) || changed + changed = backfillItemStatus(item, inferredStatus) || changed + return changed +} + +// backfillResponsesJSON applies the whole-body field backfill to a parsed +// Responses object. The status inference mirrors the TS JSON path: the +// response's own status when it maps to an OutputMessage status, else +// "completed". Returns true when any node changed (the caller must then +// re-serialise the tree). +func backfillResponsesJSON(root *jsonwire.Value) bool { + if root == nil || root.Kind() != jsonwire.Object { + return false + } + inferred := "completed" + if rawStatus, ok := stringMember(root, "status"); ok { + if mapped, ok := responseStatusToItemStatus(rawStatus); ok { + inferred = mapped + } + } + output := root.Find("output") + if output == nil || output.Kind() != jsonwire.Array { + return false + } + changed := false + for index, item := range output.Elements() { + changed = backfillOutputItem(item, index, inferred) || changed + } + return changed +} + +// RepairResponsesJSONBody is the entry point for the relay: parse the upstream +// JSON body, apply the field backfill, and return the bytes the client must +// see. The changed flag distinguishes "raw relay" (nothing changed) from +// "re-serialise" (JSON.stringify semantics), mirroring the TS bounded-JSON +// path. A body that is not a JSON object is passed through unchanged with +// changed=false so the caller relays the original bytes. +func RepairResponsesJSONBody(raw []byte) (out []byte, changed bool) { + root, parseErr := jsonwire.Parse(raw) + if parseErr != nil { + return raw, false + } + if !backfillResponsesJSON(root) { + return raw, false + } + encoded, encodeErr := root.Encode() + if encodeErr != nil { + return raw, false + } + return encoded, true +} diff --git a/go/internal/sidecar/responses_repair_test.go b/go/internal/sidecar/responses_repair_test.go new file mode 100644 index 0000000000..a2e6e83f45 --- /dev/null +++ b/go/internal/sidecar/responses_repair_test.go @@ -0,0 +1,85 @@ +package sidecar + +import ( + "bytes" + "encoding/json" + "os" + "path/filepath" + "testing" +) + +// repairGolden is one row of the committed golden file. The expected bytes are +// produced by the REAL TypeScript repair (backfillResponsesFieldsJson, via +// .tmp/gen-repair-goldens.mjs, bun only), so this unit test pins Go's mirror +// against the TypeScript oracle without a live server. +type repairGolden struct { + Name string `json:"name"` + Input string `json:"input"` + Expected string `json:"expected"` + Changed bool `json:"changed"` +} + +func loadRepairGoldens(t *testing.T) []repairGolden { + t.Helper() + raw, err := os.ReadFile(filepath.Join("testdata", "responses-repair-goldens.json")) + if err != nil { + t.Fatalf("read goldens: %v", err) + } + var goldens []repairGolden + if err := json.Unmarshal(raw, &goldens); err != nil { + t.Fatalf("decode goldens: %v", err) + } + if len(goldens) == 0 { + t.Fatal("golden file is empty") + } + return goldens +} + +// TestRepairResponsesJSONGoldens pins the field-backfill mirror to the +// TypeScript oracle across every documented shape: sparse messages, status +// mapping, id synthesis namespaces, compaction exclusion, key-order and +// number-literal preservation, and raw-bytes identity when nothing changed. +func TestRepairResponsesJSONGoldens(t *testing.T) { + for _, golden := range loadRepairGoldens(t) { + golden := golden + t.Run(golden.Name, func(t *testing.T) { + out, changed := RepairResponsesJSONBody([]byte(golden.Input)) + if !bytes.Equal(out, []byte(golden.Expected)) { + t.Fatalf("repair diverged from TS oracle\n got: %s\nwant: %s", out, golden.Expected) + } + if changed != golden.Changed { + t.Fatalf("changed = %v, want %v (raw-bytes identity contract)", changed, golden.Changed) + } + }) + } +} + +// TestRepairResponsesJSONInvalidBodyIsRawPassthrough: a body that is not a JSON +// object is not the repair's problem — the caller relays the original bytes. +func TestRepairResponsesJSONInvalidBodyIsRawPassthrough(t *testing.T) { + for _, body := range []string{`[]`, `"text"`, `7`, `{`, `not json`, ``} { + out, changed := RepairResponsesJSONBody([]byte(body)) + if changed { + t.Fatalf("body %q reported changed", body) + } + if string(out) != body { + t.Fatalf("body %q was rewritten to %q", body, out) + } + } +} + +// TestRepairResponsesJSONMutatesDocumentOrder: adding an id next to an existing +// empty-string id must replace in place, exactly like a TS object spread. +func TestRepairResponsesJSONEmptyStringIDReplacesInPlace(t *testing.T) { + input := `{"id":"r","status":"completed","output":[{"type":"message","id":"","content":[{"type":"output_text","text":"hi"}]}]}` + out, changed := RepairResponsesJSONBody([]byte(input)) + if !changed { + t.Fatal("expected a change") + } + // The id key keeps its original position (between type and content) and + // only its value is replaced; no second id is appended. + want := `{"id":"r","status":"completed","output":[{"type":"message","id":"msg_ocx_0","content":[{"type":"output_text","text":"hi","annotations":[]}],"status":"completed"}]}` + if string(out) != want { + t.Fatalf("got %s\nwant %s", out, want) + } +} diff --git a/go/internal/sidecar/sidecar.go b/go/internal/sidecar/sidecar.go index 3a0fb412d5..2cac84867c 100644 --- a/go/internal/sidecar/sidecar.go +++ b/go/internal/sidecar/sidecar.go @@ -58,6 +58,11 @@ type Config struct { // distinct from BridgeToken: the latter only authenticates the child on the // private parent hop. WriteRelaySecret string + // HotPathRelay admits direct non-streaming provider relays on the data-plane + // seam (ticket #27). Empty keeps every seam request on the parent bridge; + // the TypeScript parent must pass OPENCODEX_GO_HOTPATH_RELAY=1 in the + // environment to arm it. + HotPathRelay bool } const ( diff --git a/go/internal/sidecar/testdata/responses-repair-goldens.json b/go/internal/sidecar/testdata/responses-repair-goldens.json new file mode 100644 index 0000000000..d6955d2d99 --- /dev/null +++ b/go/internal/sidecar/testdata/responses-repair-goldens.json @@ -0,0 +1,164 @@ +[ + { + "name": "sparse-message-canonical", + "input": "{\"id\":\"resp_abc123\",\"object\":\"response\",\"created_at\":1,\"status\":\"completed\",\"model\":\"test-model\",\"output\":[{\"type\":\"message\",\"role\":\"assistant\",\"content\":[{\"type\":\"output_text\",\"text\":\"hi\"}]}]}", + "expected": "{\"id\":\"resp_abc123\",\"object\":\"response\",\"created_at\":1,\"status\":\"completed\",\"model\":\"test-model\",\"output\":[{\"type\":\"message\",\"role\":\"assistant\",\"content\":[{\"type\":\"output_text\",\"text\":\"hi\",\"annotations\":[]}],\"id\":\"msg_ocx_0\",\"status\":\"completed\"}]}", + "changed": true + }, + { + "name": "already-complete-raw-identity", + "input": "{\"id\":\"resp_x\",\"status\":\"completed\",\"output\":[{\"type\":\"message\",\"id\":\"msg_1\",\"status\":\"completed\",\"content\":[{\"type\":\"output_text\",\"text\":\"hi\",\"annotations\":[]}]}]}", + "expected": "{\"id\":\"resp_x\",\"status\":\"completed\",\"output\":[{\"type\":\"message\",\"id\":\"msg_1\",\"status\":\"completed\",\"content\":[{\"type\":\"output_text\",\"text\":\"hi\",\"annotations\":[]}]}]}", + "changed": false + }, + { + "name": "already-complete-number-literal-preserved", + "input": "{\"id\":\"resp_x\",\"status\":\"completed\",\"latency\":1.0,\"output\":[{\"type\":\"message\",\"id\":\"msg_1\",\"status\":\"completed\",\"content\":[{\"type\":\"output_text\",\"text\":\"hi\",\"annotations\":[]}]}]}", + "expected": "{\"id\":\"resp_x\",\"status\":\"completed\",\"latency\":1.0,\"output\":[{\"type\":\"message\",\"id\":\"msg_1\",\"status\":\"completed\",\"content\":[{\"type\":\"output_text\",\"text\":\"hi\",\"annotations\":[]}]}]}", + "changed": false + }, + { + "name": "change-with-number-literal-canonicalised", + "input": "{\"id\":\"resp_x\",\"status\":\"completed\",\"latency\":1.0,\"output\":[{\"type\":\"message\",\"content\":[{\"type\":\"output_text\",\"text\":\"hi\"}]}]}", + "expected": "{\"id\":\"resp_x\",\"status\":\"completed\",\"latency\":1,\"output\":[{\"type\":\"message\",\"content\":[{\"type\":\"output_text\",\"text\":\"hi\",\"annotations\":[]}],\"id\":\"msg_ocx_0\",\"status\":\"completed\"}]}", + "changed": true + }, + { + "name": "response-status-failed-maps-incomplete", + "input": "{\"id\":\"resp_x\",\"status\":\"failed\",\"output\":[{\"type\":\"message\",\"role\":\"assistant\",\"content\":[{\"type\":\"output_text\",\"text\":\"partial\"}]}]}", + "expected": "{\"id\":\"resp_x\",\"status\":\"failed\",\"output\":[{\"type\":\"message\",\"role\":\"assistant\",\"content\":[{\"type\":\"output_text\",\"text\":\"partial\",\"annotations\":[]}],\"id\":\"msg_ocx_0\",\"status\":\"incomplete\"}]}", + "changed": true + }, + { + "name": "response-status-queued-maps-in-progress", + "input": "{\"id\":\"resp_x\",\"status\":\"queued\",\"output\":[{\"type\":\"message\",\"content\":[{\"type\":\"output_text\",\"text\":\"soon\"}]}]}", + "expected": "{\"id\":\"resp_x\",\"status\":\"queued\",\"output\":[{\"type\":\"message\",\"content\":[{\"type\":\"output_text\",\"text\":\"soon\",\"annotations\":[]}],\"id\":\"msg_ocx_0\",\"status\":\"in_progress\"}]}", + "changed": true + }, + { + "name": "response-status-incomplete-passthrough", + "input": "{\"id\":\"resp_x\",\"status\":\"incomplete\",\"output\":[{\"type\":\"message\",\"content\":[{\"type\":\"output_text\",\"text\":\"done-ish\"}]}]}", + "expected": "{\"id\":\"resp_x\",\"status\":\"incomplete\",\"output\":[{\"type\":\"message\",\"content\":[{\"type\":\"output_text\",\"text\":\"done-ish\",\"annotations\":[]}],\"id\":\"msg_ocx_0\",\"status\":\"incomplete\"}]}", + "changed": true + }, + { + "name": "status-non-string-falls-back-completed", + "input": "{\"id\":\"resp_x\",\"status\":7,\"output\":[{\"type\":\"message\",\"content\":[{\"type\":\"output_text\",\"text\":\"x\"}]}]}", + "expected": "{\"id\":\"resp_x\",\"status\":7,\"output\":[{\"type\":\"message\",\"content\":[{\"type\":\"output_text\",\"text\":\"x\",\"annotations\":[]}],\"id\":\"msg_ocx_0\",\"status\":\"completed\"}]}", + "changed": true + }, + { + "name": "function-call-gets-fc-prefix", + "input": "{\"id\":\"resp_x\",\"status\":\"completed\",\"output\":[{\"type\":\"message\",\"id\":\"msg_0\",\"content\":[{\"type\":\"output_text\",\"text\":\"ok\"}]},{\"type\":\"function_call\",\"name\":\"f\",\"arguments\":\"{}\"}]}", + "expected": "{\"id\":\"resp_x\",\"status\":\"completed\",\"output\":[{\"type\":\"message\",\"id\":\"msg_0\",\"content\":[{\"type\":\"output_text\",\"text\":\"ok\",\"annotations\":[]}],\"status\":\"completed\"},{\"type\":\"function_call\",\"name\":\"f\",\"arguments\":\"{}\",\"id\":\"fc_ocx_1\"}]}", + "changed": true + }, + { + "name": "reasoning-kept-with-existing-id-and-reasoning-text-parts", + "input": "{\"id\":\"resp_x\",\"status\":\"completed\",\"output\":[{\"type\":\"reasoning\",\"id\":\"rs_keep\",\"summary\":[],\"content\":[{\"type\":\"reasoning_text\",\"text\":\"think\"}]},{\"type\":\"message\",\"content\":[{\"type\":\"output_text\",\"text\":\"hi\"}]}]}", + "expected": "{\"id\":\"resp_x\",\"status\":\"completed\",\"output\":[{\"type\":\"reasoning\",\"id\":\"rs_keep\",\"summary\":[],\"content\":[{\"type\":\"reasoning_text\",\"text\":\"think\"}]},{\"type\":\"message\",\"content\":[{\"type\":\"output_text\",\"text\":\"hi\",\"annotations\":[]}],\"id\":\"msg_ocx_1\",\"status\":\"completed\"}]}", + "changed": true + }, + { + "name": "existing-custom-tool-call-id-preserved", + "input": "{\"id\":\"resp_x\",\"status\":\"completed\",\"output\":[{\"type\":\"custom_tool_call\",\"id\":\"ctc_keep\",\"name\":\"doit\",\"arguments\":\"{}\"}]}", + "expected": "{\"id\":\"resp_x\",\"status\":\"completed\",\"output\":[{\"type\":\"custom_tool_call\",\"id\":\"ctc_keep\",\"name\":\"doit\",\"arguments\":\"{}\"}]}", + "changed": false + }, + { + "name": "empty-string-id-replaced-in-place", + "input": "{\"id\":\"resp_x\",\"status\":\"completed\",\"output\":[{\"type\":\"message\",\"id\":\"\",\"content\":[{\"type\":\"output_text\",\"text\":\"hi\"}]}]}", + "expected": "{\"id\":\"resp_x\",\"status\":\"completed\",\"output\":[{\"type\":\"message\",\"id\":\"msg_ocx_0\",\"content\":[{\"type\":\"output_text\",\"text\":\"hi\",\"annotations\":[]}],\"status\":\"completed\"}]}", + "changed": true + }, + { + "name": "compaction-item-untouched", + "input": "{\"id\":\"resp_x\",\"status\":\"completed\",\"output\":[{\"type\":\"compaction\",\"id\":\"keep_compaction_id\"},{\"type\":\"message\",\"content\":[{\"type\":\"output_text\",\"text\":\"hi\"}]}]}", + "expected": "{\"id\":\"resp_x\",\"status\":\"completed\",\"output\":[{\"type\":\"compaction\",\"id\":\"keep_compaction_id\"},{\"type\":\"message\",\"content\":[{\"type\":\"output_text\",\"text\":\"hi\",\"annotations\":[]}],\"id\":\"msg_ocx_1\",\"status\":\"completed\"}]}", + "changed": true + }, + { + "name": "output-index-deterministic", + "input": "{\"id\":\"resp_x\",\"status\":\"completed\",\"output\":[{\"type\":\"message\",\"id\":\"a\",\"content\":[{\"type\":\"output_text\",\"text\":\"1\"}]},{\"type\":\"message\",\"content\":[{\"type\":\"output_text\",\"text\":\"2\"}]},{\"type\":\"message\",\"content\":[{\"type\":\"output_text\",\"text\":\"3\"}]}]}", + "expected": "{\"id\":\"resp_x\",\"status\":\"completed\",\"output\":[{\"type\":\"message\",\"id\":\"a\",\"content\":[{\"type\":\"output_text\",\"text\":\"1\",\"annotations\":[]}],\"status\":\"completed\"},{\"type\":\"message\",\"content\":[{\"type\":\"output_text\",\"text\":\"2\",\"annotations\":[]}],\"id\":\"msg_ocx_1\",\"status\":\"completed\"},{\"type\":\"message\",\"content\":[{\"type\":\"output_text\",\"text\":\"3\",\"annotations\":[]}],\"id\":\"msg_ocx_2\",\"status\":\"completed\"}]}", + "changed": true + }, + { + "name": "missing-type-gets-generic-item-prefix", + "input": "{\"id\":\"resp_x\",\"status\":\"completed\",\"output\":[{\"type\":\"message\",\"id\":\"msg_0\",\"content\":[{\"type\":\"output_text\",\"text\":\"a\"}]},{\"content\":[{\"type\":\"output_text\",\"text\":\"b\"}]}]}", + "expected": "{\"id\":\"resp_x\",\"status\":\"completed\",\"output\":[{\"type\":\"message\",\"id\":\"msg_0\",\"content\":[{\"type\":\"output_text\",\"text\":\"a\",\"annotations\":[]}],\"status\":\"completed\"},{\"content\":[{\"type\":\"output_text\",\"text\":\"b\",\"annotations\":[]}],\"id\":\"item_ocx_1\"}]}", + "changed": true + }, + { + "name": "existing-status-and-annotations-null-preserved", + "input": "{\"id\":\"resp_x\",\"status\":\"completed\",\"output\":[{\"type\":\"message\",\"id\":\"m\",\"status\":\"in_progress\",\"content\":[{\"type\":\"output_text\",\"text\":\"hi\",\"annotations\":null}]}]}", + "expected": "{\"id\":\"resp_x\",\"status\":\"completed\",\"output\":[{\"type\":\"message\",\"id\":\"m\",\"status\":\"in_progress\",\"content\":[{\"type\":\"output_text\",\"text\":\"hi\",\"annotations\":null}]}]}", + "changed": false + }, + { + "name": "multiple-parts-one-repaired", + "input": "{\"id\":\"resp_x\",\"status\":\"completed\",\"output\":[{\"type\":\"message\",\"content\":[{\"type\":\"output_text\",\"text\":\"a\",\"annotations\":[]},{\"type\":\"output_text\",\"text\":\"b\"}]}]}", + "expected": "{\"id\":\"resp_x\",\"status\":\"completed\",\"output\":[{\"type\":\"message\",\"content\":[{\"type\":\"output_text\",\"text\":\"a\",\"annotations\":[]},{\"type\":\"output_text\",\"text\":\"b\",\"annotations\":[]}],\"id\":\"msg_ocx_0\",\"status\":\"completed\"}]}", + "changed": true + }, + { + "name": "output-not-array-raw", + "input": "{\"id\":\"resp_x\",\"status\":\"completed\",\"output\":{}}", + "expected": "{\"id\":\"resp_x\",\"status\":\"completed\",\"output\":{}}", + "changed": false + }, + { + "name": "non-object-output-entry-skipped", + "input": "{\"id\":\"resp_x\",\"status\":\"completed\",\"output\":[42,{\"type\":\"message\",\"content\":[{\"type\":\"output_text\",\"text\":\"hi\"}]}]}", + "expected": "{\"id\":\"resp_x\",\"status\":\"completed\",\"output\":[42,{\"type\":\"message\",\"content\":[{\"type\":\"output_text\",\"text\":\"hi\",\"annotations\":[]}],\"id\":\"msg_ocx_1\",\"status\":\"completed\"}]}", + "changed": true + }, + { + "name": "content-not-array", + "input": "{\"id\":\"resp_x\",\"status\":\"completed\",\"output\":[{\"type\":\"message\",\"content\":\"nope\"}]}", + "expected": "{\"id\":\"resp_x\",\"status\":\"completed\",\"output\":[{\"type\":\"message\",\"content\":\"nope\",\"id\":\"msg_ocx_0\",\"status\":\"completed\"}]}", + "changed": true + }, + { + "name": "function-call-content-output-text-parts", + "input": "{\"id\":\"resp_x\",\"status\":\"completed\",\"output\":[{\"type\":\"function_call\",\"name\":\"f\",\"content\":[{\"type\":\"output_text\",\"text\":\"explaining\"}]}]}", + "expected": "{\"id\":\"resp_x\",\"status\":\"completed\",\"output\":[{\"type\":\"function_call\",\"name\":\"f\",\"content\":[{\"type\":\"output_text\",\"text\":\"explaining\",\"annotations\":[]}],\"id\":\"fc_ocx_0\"}]}", + "changed": true + }, + { + "name": "top-level-status-absent", + "input": "{\"id\":\"resp_x\",\"output\":[{\"type\":\"message\",\"content\":[{\"type\":\"output_text\",\"text\":\"hi\"}]}]}", + "expected": "{\"id\":\"resp_x\",\"output\":[{\"type\":\"message\",\"content\":[{\"type\":\"output_text\",\"text\":\"hi\",\"annotations\":[]}],\"id\":\"msg_ocx_0\",\"status\":\"completed\"}]}", + "changed": true + }, + { + "name": "large-integer-index-id", + "input": "{\"id\":\"resp_x\",\"status\":\"completed\",\"output\":[{\"type\":\"message\",\"id\":\"m0\",\"content\":[{\"type\":\"output_text\",\"text\":\"a\"}]},{\"type\":\"message\",\"id\":\"m1\",\"content\":[{\"type\":\"output_text\",\"text\":\"b\"}]},{\"type\":\"message\",\"id\":\"m2\",\"content\":[{\"type\":\"output_text\",\"text\":\"c\"}]},{\"type\":\"message\",\"id\":\"m3\",\"content\":[{\"type\":\"output_text\",\"text\":\"d\"}]},{\"type\":\"message\",\"id\":\"m4\",\"content\":[{\"type\":\"output_text\",\"text\":\"e\"}]},{\"type\":\"message\",\"content\":[{\"type\":\"output_text\",\"text\":\"f\"}]}]}", + "expected": "{\"id\":\"resp_x\",\"status\":\"completed\",\"output\":[{\"type\":\"message\",\"id\":\"m0\",\"content\":[{\"type\":\"output_text\",\"text\":\"a\",\"annotations\":[]}],\"status\":\"completed\"},{\"type\":\"message\",\"id\":\"m1\",\"content\":[{\"type\":\"output_text\",\"text\":\"b\",\"annotations\":[]}],\"status\":\"completed\"},{\"type\":\"message\",\"id\":\"m2\",\"content\":[{\"type\":\"output_text\",\"text\":\"c\",\"annotations\":[]}],\"status\":\"completed\"},{\"type\":\"message\",\"id\":\"m3\",\"content\":[{\"type\":\"output_text\",\"text\":\"d\",\"annotations\":[]}],\"status\":\"completed\"},{\"type\":\"message\",\"id\":\"m4\",\"content\":[{\"type\":\"output_text\",\"text\":\"e\",\"annotations\":[]}],\"status\":\"completed\"},{\"type\":\"message\",\"content\":[{\"type\":\"output_text\",\"text\":\"f\",\"annotations\":[]}],\"id\":\"msg_ocx_5\",\"status\":\"completed\"}]}", + "changed": true + }, + { + "name": "message-id-present-status-missing", + "input": "{\"id\":\"resp_x\",\"status\":\"completed\",\"output\":[{\"type\":\"message\",\"id\":\"msg_keep\",\"content\":[{\"type\":\"output_text\",\"text\":\"hi\",\"annotations\":[]}]}]}", + "expected": "{\"id\":\"resp_x\",\"status\":\"completed\",\"output\":[{\"type\":\"message\",\"id\":\"msg_keep\",\"content\":[{\"type\":\"output_text\",\"text\":\"hi\",\"annotations\":[]}],\"status\":\"completed\"}]}", + "changed": true + }, + { + "name": "item-with-id-number-replaced", + "input": "{\"id\":\"resp_x\",\"status\":\"completed\",\"output\":[{\"type\":\"message\",\"id\":7,\"content\":[{\"type\":\"output_text\",\"text\":\"hi\"}]}]}", + "expected": "{\"id\":\"resp_x\",\"status\":\"completed\",\"output\":[{\"type\":\"message\",\"id\":\"msg_ocx_0\",\"content\":[{\"type\":\"output_text\",\"text\":\"hi\",\"annotations\":[]}],\"status\":\"completed\"}]}", + "changed": true + }, + { + "name": "escaped-unicode-text-survives", + "input": "{\"id\":\"resp_x\",\"status\":\"completed\",\"output\":[{\"type\":\"message\",\"content\":[{\"type\":\"output_text\",\"text\":\"\\u0001 control\\n\\u2028sep\"}]}]}", + "expected": "{\"id\":\"resp_x\",\"status\":\"completed\",\"output\":[{\"type\":\"message\",\"content\":[{\"type\":\"output_text\",\"text\":\"\\u0001 control\\n
sep\",\"annotations\":[]}],\"id\":\"msg_ocx_0\",\"status\":\"completed\"}]}", + "changed": true + }, + { + "name": "deep-text-and-instructions-order-preserved", + "input": "{\"id\":\"resp_x\",\"status\":\"completed\",\"output\":[{\"type\":\"message\",\"content\":[{\"type\":\"output_text\",\"text\":\"hi\"}]}],\"usage\":{\"input_tokens\":5,\"output_tokens\":1}}", + "expected": "{\"id\":\"resp_x\",\"status\":\"completed\",\"output\":[{\"type\":\"message\",\"content\":[{\"type\":\"output_text\",\"text\":\"hi\",\"annotations\":[]}],\"id\":\"msg_ocx_0\",\"status\":\"completed\"}],\"usage\":{\"input_tokens\":5,\"output_tokens\":1}}", + "changed": true + } +] From 105a40e9bfdfdcd5e01c8ec68452c3b460c4cf57 Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Sun, 6 Sep 2026 18:52:07 +0800 Subject: [PATCH 025/165] test(ts): non-streaming relay differential + relay gate constant (#27) Declares OPENCODEX_GO_HOTPATH_RELAY on the TS side (front door passes it to the sidecar at spawn) and proves the armed relay answers the non-streaming matrix byte-identically to the in-process oracle while the fixture upstream sees the Go http client user agent for admitted requests and the Bun agent for refused/gate-off ones. --- src/server/hot-path-seam.ts | 11 + tests/go-hotpath-relay.test.ts | 399 +++++++++++++++++++++++++++++++++ 2 files changed, 410 insertions(+) create mode 100644 tests/go-hotpath-relay.test.ts diff --git a/src/server/hot-path-seam.ts b/src/server/hot-path-seam.ts index 992b4b6b16..d7008181f1 100644 --- a/src/server/hot-path-seam.ts +++ b/src/server/hot-path-seam.ts @@ -28,6 +28,17 @@ import type { DataPlaneAdmission } from "./auth-cors"; /** Independent activation gate: management reads and the data plane roll back separately. */ export const HOT_PATH_SEAM_ENV = "OPENCODEX_GO_HOTPATH_SEAM"; +/** + * Independent gate for the direct non-streaming provider relay (ticket #27, + * devlog 036). The seam's default source is the parent bridge, which runs the + * in-process pipeline; when this is set the sidecar serves a relay-safe + * non-streaming request for one key-mode openai-responses provider directly + * upstream. The front door only declares it and passes the environment + * through at spawn — the sidecar process reads it per request, exactly like + * the seam gate is read on this side. + */ +export const HOT_PATH_RELAY_ENV = "OPENCODEX_GO_HOTPATH_RELAY"; + /** The declared data-plane seam route. One entry; a later ticket flips the marker, never the dispatch. */ export const HOT_PATH_SEAM_PATH = "/v1/responses"; diff --git a/tests/go-hotpath-relay.test.ts b/tests/go-hotpath-relay.test.ts new file mode 100644 index 0000000000..b60567a2bd --- /dev/null +++ b/tests/go-hotpath-relay.test.ts @@ -0,0 +1,399 @@ +import { afterAll, beforeAll, describe, expect, test } from "bun:test"; +import { mkdtempSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { SERVER_BUDGET_MS } from "./helpers/test-budget"; +import { saveConfig } from "../src/config"; +import { startServer } from "../src/server"; +import { + GO_SIDECAR_BIN_ENV, + activeGoSidecarBaseUrl, + resetGoSidecarForTests, +} from "../src/server/go-sidecar"; +import { HOT_PATH_RELAY_ENV, HOT_PATH_SEAM_ENV } from "../src/server/hot-path-seam"; +import { removeTreeWithRetry } from "./helpers/remove-tree"; + +/** + * Non-streaming direct-relay differential for ticket #27 (devlog 036). With + * the seam gate on and the relay gate off the sidecar's only data-plane + * source is the private parent bridge; with both gates on a relay-safe + * non-streaming request is answered DIRECTLY upstream by the Go sidecar + * (status/content-type/retry-after/body, with the field backfill applied), + * and everything else still takes the bridge. + * + * The two paths are told apart by the User-Agent the fixture upstream sees: + * the Go http client sends `Go-http-client/1.1` and the in-process Bun fetch + * does not. The client-visible bytes are the real assertion: server A runs + * the pipeline in-process (the oracle), server B runs the same matrix with + * the relay armed, and each response must match A byte-for-byte — sparse + * upstream bodies included, because both sides repair them identically. + * + * Skipped where the Go toolchain is unavailable, like the #24 harness. + */ + +const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); + +function goToolchainAvailable(): boolean { + const probe = Bun.spawnSync(["go", "version"], { stdout: "ignore", stderr: "ignore" }); + return probe.success; +} + +function buildSidecarBinary(): string { + const dir = mkdtempSync(join(tmpdir(), "ocx-go-sidecar-relay-")); + const binPath = join(dir, process.platform === "win32" ? "ocx-sidecar.exe" : "ocx-sidecar"); + const build = Bun.spawnSync( + ["go", "build", "-o", binPath, "./cmd/ocx-sidecar"], + { + cwd: join(repoRoot, "go"), + env: { ...process.env, CGO_ENABLED: "0" }, + stdout: "pipe", + stderr: "pipe", + }, + ); + if (build.exitCode !== 0) { + throw new Error( + `go build ./cmd/ocx-sidecar failed (${build.exitCode}):\n${new TextDecoder().decode(build.stderr)}`, + ); + } + return binPath; +} + +const goAvailable = goToolchainAvailable(); +const sidecarBinary: string | null = goAvailable ? buildSidecarBinary() : null; + +let upstream: ReturnType | null = null; + +interface RelayCase { + name: string; + body: unknown; + direct: boolean; +} + +const fnTool = { + type: "function", + name: "calc", + description: "do arithmetic", + parameters: { type: "object", properties: { x: { type: "number" } } }, +}; + +const sparseMessage = (id: string, text: string) => + JSON.stringify({ + id, + object: "response", + status: "completed", + model: "test-model", + output: [ + { + type: "message", + role: "assistant", + content: [{ type: "output_text", text }], + }, + ], + }); + +const sparseMultiItem = JSON.stringify({ + id: "resp_tools", + object: "response", + status: "completed", + output: [ + { type: "message", id: "msg_0", content: [{ type: "output_text", text: "calling the tool" }] }, + { type: "function_call", name: "calc", arguments: '{"x":1}' }, + ], +}); + +const relayCases: RelayCase[] = [ + { name: "plain", body: { model: "test-model", input: "plain" }, direct: true }, + { + name: "tools", + body: { + model: "test-model", + input: [{ type: "message", role: "user", content: [{ type: "input_text", text: "calc" }] }], + tools: [fnTool], + }, + direct: true, + }, + { name: "reasoning", body: { model: "test-model", input: "reasoning", reasoning: { effort: "low" } }, direct: true }, + // An unlisted model falls back to defaultProvider, which is still the one + // relay-safe provider: TS forwards it verbatim and so does the relay. + { name: "default-provider", body: { model: "unlisted-model", input: "default" }, direct: true }, +]; + +const streamCase: RelayCase = { + name: "streaming-stays-on-bridge", + body: { model: "test-model", input: "stream", stream: true }, + direct: false, +}; + +const streamUpstreamReply = + "event: response.created\ndata: {\"type\":\"response.created\",\"response\":{\"id\":\"fixture-s\",\"status\":\"in_progress\"}}\n\n" + + "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"delta\":\"Hel\"}\n\n" + + "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"delta\":\"lo\"}\n\n" + + "event: response.completed\ndata: {\"type\":\"response.completed\",\"response\":{\"id\":\"fixture-s\",\"status\":\"completed\"}}\n\n"; + +/** Stable sparse upstream reply per case marker, keyed by parsed input. */ +function nonStreamReply(raw: string): string { + let parsed: { input?: unknown; stream?: boolean } = {}; + try { + parsed = JSON.parse(raw) as { input?: unknown; stream?: boolean }; + } catch { + // fall through to the default reply + } + const replies: Record = { + plain: sparseMessage("resp_plain", "plain reply"), + tools: sparseMultiItem, + reasoning: sparseMessage("resp_reasoning", "low effort reply"), + default: sparseMessage("resp_default", "default reply"), + }; + const key = Array.isArray(parsed.input) ? "tools" : String(parsed.input ?? ""); + return replies[key] ?? sparseMessage("resp_other", "other"); +} + +interface UpstreamLog { + ua: string; + method: string; + path: string; + contentType: string | null; +} + +const upstreamLogs: UpstreamLog[] = []; + +interface ResponseCapture { + status: number; + contentType: string | null; + body: string; +} + +function configFixture(upstreamPort: number) { + return { + port: 0, + hostname: "127.0.0.1", + defaultProvider: "test", + providers: { + test: { + adapter: "openai-responses", + baseUrl: `http://127.0.0.1:${upstreamPort}/v1`, + allowPrivateNetwork: true, + disabled: false, + models: ["test-model"], + }, + }, + }; +} + +const GO_UA = "Go-http-client/1.1"; + +async function postCase(server: { url: URL }, token: string, body: unknown): Promise { + const response = await fetch(new URL("/v1/responses", server.url), { + method: "POST", + headers: { "content-type": "application/json", "x-opencodex-api-key": token }, + body: JSON.stringify(body), + }); + return { + status: response.status, + contentType: response.headers.get("content-type"), + body: await response.text(), + }; +} + +const previousEnv: Record = {}; +let testHome = ""; + +function captureEnv(): void { + for (const name of [GO_SIDECAR_BIN_ENV, HOT_PATH_SEAM_ENV, HOT_PATH_RELAY_ENV, "OPENCODEX_HOME", "OPENCODEX_API_AUTH_TOKEN"]) { + previousEnv[name] = process.env[name]; + } +} + +function setUpFixture(upstreamPort: number): void { + testHome = mkdtempSync(join(tmpdir(), "ocx-hotpath-relay-")); + process.env.OPENCODEX_HOME = testHome; + process.env.OPENCODEX_API_AUTH_TOKEN = "data-secret"; + saveConfig(configFixture(upstreamPort)); +} + +function tearDownFixture(): void { + resetGoSidecarForTests(); + for (const [name, value] of Object.entries(previousEnv)) { + if (value === undefined) delete process.env[name]; + else process.env[name] = value; + } + if (testHome) { + removeTreeWithRetry(testHome); + testHome = ""; + } +} + +async function waitFor(probe: () => T | null | undefined, timeoutMs: number): Promise { + const deadline = Date.now() + timeoutMs; + for (;;) { + const value = probe(); + if (value !== null && value !== undefined) return value; + if (Date.now() >= deadline) throw new Error(`condition not met within ${timeoutMs}ms`); + await Bun.sleep(50); + } +} + +function runFixtureTest(name: string, fn: () => Promise): void { + test( + name, + async () => { + captureEnv(); + try { + await fn(); + } finally { + tearDownFixture(); + } + }, + SERVER_BUDGET_MS, + ); +} + +describe.skipIf(!goAvailable || sidecarBinary === null)("ocx-sidecar non-streaming relay differential (ADR-0008, ticket #27)", () => { + beforeAll(() => { + upstream = Bun.serve({ + hostname: "127.0.0.1", + port: 0, + async fetch(req) { + if (new URL(req.url).pathname !== "/v1/responses") return new Response("nf", { status: 404 }); + upstreamLogs.push({ + ua: req.headers.get("user-agent") ?? "", + method: req.method, + path: new URL(req.url).pathname, + contentType: req.headers.get("content-type"), + }); + // The upstream body is the same bytes whichever path reached it (the + // relay forwards the seam body verbatim, the bridge forwards the + // in-process body verbatim), so the reply is keyed off markers the + // cases carry distinctly. + const raw = await req.text(); + let parsed: { input?: unknown; stream?: boolean } = {}; + try { + parsed = JSON.parse(raw) as { input?: unknown; stream?: boolean }; + } catch { + // fall through to the default reply + } + if (parsed.stream === true) { + return new Response(streamUpstreamReply, { headers: { "content-type": "text/event-stream" } }); + } + return new Response(nonStreamReply(raw), { headers: { "content-type": "application/json" } }); + }, + }); + }); + afterAll(() => { + upstream?.stop(true); + upstream = null; + }); + + test("the relay env gate is a declared constant", () => { + expect(HOT_PATH_RELAY_ENV).toBe("OPENCODEX_GO_HOTPATH_RELAY"); + }); + + runFixtureTest("armed relay answers relay-safe requests byte-identically and direct", async () => { + const token = "data-secret"; + const port = upstream!.port; + const allCases = [...relayCases, streamCase]; + + // Server A: in-process pipeline, the oracle. + setUpFixture(port); + const serverA = startServer(0); + const tsCaptures: ResponseCapture[] = []; + try { + for (const c of allCases) { + tsCaptures.push(await postCase(serverA, token, c.body)); + } + } finally { + await serverA.stop(true); + } + // The oracle itself must have hit the fixture upstream through Bun, never + // through a Go http client. + expect(upstreamLogs.slice(0, allCases.length).every((log) => log.ua !== GO_UA)).toBe(true); + + // Server B: seam on AND relay on. + process.env[GO_SIDECAR_BIN_ENV] = sidecarBinary!; + process.env[HOT_PATH_SEAM_ENV] = "1"; + process.env[HOT_PATH_RELAY_ENV] = "1"; + const serverB = startServer(0); + const goCaptures: ResponseCapture[] = []; + try { + await waitFor(() => activeGoSidecarBaseUrl(), 15_000); + for (const c of allCases) { + goCaptures.push(await postCase(serverB, token, c.body)); + } + } finally { + await serverB.stop(true); + } + + for (let i = 0; i < allCases.length; i++) { + const c = allCases[i]!; + expect(goCaptures[i]!.status, `${c.name} status`).toBe(tsCaptures[i]!.status); + expect(goCaptures[i]!.contentType, `${c.name} content-type`).toBe(tsCaptures[i]!.contentType); + expect(goCaptures[i]!.body, `${c.name} body must match the in-process oracle`).toBe(tsCaptures[i]!.body); + } + + // Path proof: the fixture upstream must have seen the Go http client for + // every relay-admitted case and must NOT have seen it for the streaming + // refusal (which still ran through the Bun bridge). + const goLogs = upstreamLogs.slice(allCases.length); + expect(goLogs.length).toBe(allCases.length); + for (let i = 0; i < allCases.length; i++) { + const c = allCases[i]!; + if (c.direct) { + expect(goLogs[i]!.ua, `${c.name} should be a direct Go relay`).toBe(GO_UA); + } else { + expect(goLogs[i]!.ua, `${c.name} should stay on the bridge`).not.toBe(GO_UA); + } + } + // The upstream outbound path is the canonical responses URL every time. + expect(goLogs.every((log) => log.method === "POST" && log.path === "/v1/responses")).toBe(true); + // Non-vacuous: the client-visible bytes really were repaired by whoever + // answered. The tools fixture already carries a message id, so its + // synthesized id names the function_call item at output index 1; every + // other sparse fixture synthesizes msg_ocx_0. + for (let i = 0; i < relayCases.length; i++) { + const name = relayCases[i]!.name; + const id = name === "tools" ? '"id":"fc_ocx_1"' : "msg_ocx_0"; + expect(goCaptures[i]!.body, `${name} id backfill`).toContain(id); + expect(goCaptures[i]!.body, `${name} annotations backfill`).toContain('"annotations":[]'); + } + expect(activeGoSidecarBaseUrl()).toBeNull(); + }); + + runFixtureTest("relay gate off keeps the bridge for every request", async () => { + const token = "data-secret"; + const port = upstream!.port; + const cases = relayCases; + + setUpFixture(port); + const serverA = startServer(0); + const tsCaptures: ResponseCapture[] = []; + try { + for (const c of cases) tsCaptures.push(await postCase(serverA, token, c.body)); + } finally { + await serverA.stop(true); + } + const aStart = upstreamLogs.length; + + // Server C: seam on, relay gate OFF. Every request must reach the + // upstream through the Bun bridge. + process.env[GO_SIDECAR_BIN_ENV] = sidecarBinary!; + process.env[HOT_PATH_SEAM_ENV] = "1"; + delete process.env[HOT_PATH_RELAY_ENV]; + const serverC = startServer(0); + const bridgeCaptures: ResponseCapture[] = []; + try { + await waitFor(() => activeGoSidecarBaseUrl(), 15_000); + for (const c of cases) bridgeCaptures.push(await postCase(serverC, token, c.body)); + } finally { + await serverC.stop(true); + } + + const cLogs = upstreamLogs.slice(aStart); + expect(cLogs.length).toBe(cases.length); + for (let i = 0; i < cases.length; i++) { + expect(cLogs[i]!.ua, `${cases[i]!.name} must take the bridge with the relay gate off`).not.toBe(GO_UA); + expect(bridgeCaptures[i]!.status, `${cases[i]!.name} status`).toBe(tsCaptures[i]!.status); + expect(bridgeCaptures[i]!.body, `${cases[i]!.name} body must match the oracle`).toBe(tsCaptures[i]!.body); + } + }); +}); From b6739a8804c38daed794a533e4a89a96b4011e09 Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Sun, 6 Sep 2026 19:37:44 +0800 Subject: [PATCH 026/165] Ticket #35: add Go CLI local transport scaffold --- .../039_go_cli_scaffold.md | 36 +++ go/README.md | 2 + go/cmd/ocx/main.go | 45 +++ go/cmd/ocx/version.go | 24 ++ go/internal/ocxcli/cli.go | 269 ++++++++++++++++++ go/internal/ocxcli/cli_test.go | 98 +++++++ tests/go-cli-parity.test.ts | 44 +++ 7 files changed, 518 insertions(+) create mode 100644 devlog/_plan/260905_go_sidecar_takeover/039_go_cli_scaffold.md create mode 100644 go/cmd/ocx/main.go create mode 100644 go/cmd/ocx/version.go create mode 100644 go/internal/ocxcli/cli.go create mode 100644 go/internal/ocxcli/cli_test.go create mode 100644 tests/go-cli-parity.test.ts diff --git a/devlog/_plan/260905_go_sidecar_takeover/039_go_cli_scaffold.md b/devlog/_plan/260905_go_sidecar_takeover/039_go_cli_scaffold.md new file mode 100644 index 0000000000..2a5da6611a --- /dev/null +++ b/devlog/_plan/260905_go_sidecar_takeover/039_go_cli_scaffold.md @@ -0,0 +1,36 @@ +# 039 — Ticket #35: Go CLI scaffold + local HTTP transport + version parity + +Unit: `260905_go_sidecar_takeover` +Date: 2026-09-06 +Ticket: [#35](https://github.com/waxiangzi/opencodex/issues/35) (spec #5) + +## Scope discipline + +This ticket adds a second CLI binary without moving the TypeScript CLI's operator workflow. The Go binary has a deliberately small registry: help, `--version`/`-v`/`version`, `health`, and `ready`. It drives only local unauthenticated identity endpoints. Starts, stops, configuration writes, management routes, wait/retry parity, and the complete command matrix remain owned by TypeScript until later slices; #36 owns the broader parity harness. + +## Design decisions + +### 1. Package manifest is the development version authority; release injection is authoritative outside a checkout + +TypeScript's `printVersion()` reads repository `package.json`. A Go binary built from this checkout walks upward from its current working directory to the same manifest, then prints exactly `opencodex \n`. That makes an ordinary source build match TypeScript immediately (2.42.0 for this ticket) without duplicating a version constant. + +Release builds set `main.version` with `-ldflags -X main.version=`; `OCX_VERSION` exists for controlled packaging environments. Both override the checkout fallback, so a distributed binary does not depend on a nearby source tree. Release tooling must derive that ldflag from the same package manifest used to publish the TypeScript CLI. A missing source manifest and missing injected version deliberately reports `0.0.0`, never a copied, stale package value. + +### 2. The command registry is data and parsing is local + +`internal/ocxcli.Commands` is the top-level registry used by help and tests. `Run` is injected with streams, runtime-record loading, HTTP client and challenge generation, allowing exact output and exit-code tests without a subprocess. The currently supported `--json` option is intentionally narrow; unknown or misplaced options return sysexits usage code 64 before discovery or HTTP work. + +### 3. Local transport first proves process identity, then reads readiness + +The CLI reads TypeScript-owned `OPENCODEX_HOME/runtime-port.json`, requiring a valid pid, port and 43-character attestation secret. `health` sends a fresh base64url challenge to `/healthz`, requires `status:"ok"`, `service:"opencodex"`, matching pid/port and verifies the response HMAC over `opencodex-local-management-v1\n\n\n`. It sends no admin token. `ready` first completes that health proof, then accepts `/readyz` only when its status/body pairing, service, version, pid and port match the existing TypeScript readiness contract. + +## Proof (as landed) + +- Go unit tests cover exact version output, registry shape, usage exit 64, healthy and invalid-attestation paths, and ready JSON output. +- `tests/go-cli-parity.test.ts` builds `cmd/ocx` when Go is available, compares Go and TypeScript version stdout byte-for-byte, then starts a real TypeScript proxy, writes its runtime record, verifies the TypeScript proof independently, and compares Go health identity JSON fields to that proxy. This is the small extension point for #36's command matrix. + +## Delivery notes (filled in at close) + +- Added `go/cmd/ocx`, `go/internal/ocxcli`, focused Go tests, the first Bun CLI differential, and the Go README capability note. +- Validation: `bun run typecheck`, `bun test tests/go-cli-parity.test.ts`, and `go fmt/vet/build/test ./...` from `go/`. +- Follow-on: #36 should expand differential coverage to the TypeScript CLI's full command and exit/output matrix, including wait and failure semantics. diff --git a/go/README.md b/go/README.md index 9b36cb66d0..279d447189 100644 --- a/go/README.md +++ b/go/README.md @@ -19,6 +19,8 @@ material only. This is a fresh codebase. route's volatile-field declaration) live in `src/server/management/route-registry.ts`, and the single forwarding branch in `src/server/management-api.ts` reads them before asking the sidecar. +- `cmd/ocx` — the Go CLI scaffold (ticket #35). It currently provides + version, help, and identity-attested local health / ready transport commands. - `internal/sidecar` — the handler plus its unit tests. The JSON key order and number formatting of each payload are part of the byte contract with the Bun differential oracle (`tests/go-sidecar-parity.test.ts`). diff --git a/go/cmd/ocx/main.go b/go/cmd/ocx/main.go new file mode 100644 index 0000000000..7e233cf502 --- /dev/null +++ b/go/cmd/ocx/main.go @@ -0,0 +1,45 @@ +// Command ocx is the Go CLI scaffold for the incremental runtime takeover. +package main + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/lidge-jun/opencodex/go/internal/ocxcli" +) + +// version is set by release builds with -ldflags '-X main.version='. +var version string + +func main() { os.Exit(ocxcli.Run(os.Args[1:], ocxcli.Deps{Version: resolveVersion()})) } +func resolveVersion() string { + if version != "" { + return version + } + if value := os.Getenv("OCX_VERSION"); value != "" { + return value + } + value, err := packageVersionFromWorkingTree() + if err != nil { + return "0.0.0" + } + return value +} +func packageVersionFromWorkingTree() (string, error) { + dir, err := os.Getwd() + if err != nil { + return "", err + } + for { + value, err := packageVersionAt(dir) + if err == nil { + return value, nil + } + parent := filepath.Dir(dir) + if parent == dir { + return "", fmt.Errorf("package.json not found") + } + dir = parent + } +} diff --git a/go/cmd/ocx/version.go b/go/cmd/ocx/version.go new file mode 100644 index 0000000000..9e19b85bdf --- /dev/null +++ b/go/cmd/ocx/version.go @@ -0,0 +1,24 @@ +package main + +import ( + "encoding/json" + "os" + "path/filepath" +) + +func packageVersionAt(dir string) (string, error) { + raw, err := os.ReadFile(filepath.Join(dir, "package.json")) + if err != nil { + return "", err + } + var manifest struct { + Version string `json:"version"` + } + if err := json.Unmarshal(raw, &manifest); err != nil || manifest.Version == "" { + if err == nil { + err = os.ErrInvalid + } + return "", err + } + return manifest.Version, nil +} diff --git a/go/internal/ocxcli/cli.go b/go/internal/ocxcli/cli.go new file mode 100644 index 0000000000..6136b050db --- /dev/null +++ b/go/internal/ocxcli/cli.go @@ -0,0 +1,269 @@ +// Package ocxcli owns the Go CLI scaffold for ADR-0008. +package ocxcli + +import ( + "crypto/rand" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "os" + "path/filepath" + "strconv" + "strings" + "time" + + "github.com/lidge-jun/opencodex/go/internal/config" + "github.com/lidge-jun/opencodex/go/internal/managementauth" +) + +const ( + ExitOK = 0 + ExitFailure = 1 + ExitUsage = 64 + attestationChallengeHeader = "x-opencodex-attestation-challenge" + attestationProofHeader = "x-opencodex-attestation-proof" +) + +// Command is one user-visible top-level command. Keeping the registry data +// separate makes later parity additions additive and unit-testable. +type Command struct{ Name, Usage, Summary string } + +var Commands = []Command{ + {Name: "health", Usage: "ocx health [--json]", Summary: "Verify the local proxy identity and report health."}, + {Name: "ready", Usage: "ocx ready [--json]", Summary: "Verify the local proxy identity and report readiness."}, +} + +type RuntimeState struct { + PID int64 `json:"pid"` + Port int `json:"port"` + Hostname string `json:"hostname"` + AttestationSecret string `json:"attestationSecret"` +} + +type Health struct { + Status string `json:"status"` + Service string `json:"service"` + Version string `json:"version"` + Uptime float64 `json:"uptime"` + PID int64 `json:"pid"` + Port int `json:"port"` +} +type readiness struct { + Service string `json:"service"` + Version string `json:"version"` + Uptime float64 `json:"uptime"` + PID int64 `json:"pid"` + Port int `json:"port"` + Status string `json:"status"` +} + +type Deps struct { + Version string + Stdout, Stderr io.Writer + ReadRuntime func() (RuntimeState, error) + HTTPClient *http.Client + Challenge func() (string, error) +} + +func defaults(d Deps) Deps { + if d.Stdout == nil { + d.Stdout = os.Stdout + } + if d.Stderr == nil { + d.Stderr = os.Stderr + } + if d.ReadRuntime == nil { + d.ReadRuntime = ReadRuntime + } + if d.HTTPClient == nil { + d.HTTPClient = &http.Client{Timeout: 750 * time.Millisecond} + } + if d.Challenge == nil { + d.Challenge = CreateChallenge + } + return d +} + +// Run dispatches a parsed argv and returns a POSIX-style process code. +func Run(args []string, deps Deps) int { + deps = defaults(deps) + if len(args) == 0 || args[0] == "help" || args[0] == "--help" || args[0] == "-h" { + printHelp(deps.Stdout) + return ExitOK + } + switch args[0] { + case "--version", "-v", "version": + fmt.Fprintf(deps.Stdout, "opencodex %s\n", deps.Version) + return ExitOK + case "health": + return runHealth(args[1:], deps) + case "ready": + return runReady(args[1:], deps) + default: + fmt.Fprintf(deps.Stderr, "Unknown command: %s\n", args[0]) + printHelp(deps.Stderr) + return ExitUsage + } +} + +func printHelp(w io.Writer) { + fmt.Fprintln(w, "Usage: ocx \n\nCommands:") + for _, c := range Commands { + fmt.Fprintf(w, " %-24s %s\n", c.Usage, c.Summary) + } + fmt.Fprintln(w, " ocx --version | -v Print version") +} +func parseJSON(args []string) (bool, bool) { + if len(args) == 0 { + return false, true + } + return len(args) == 1 && args[0] == "--json", len(args) == 1 && args[0] == "--json" +} + +func runHealth(args []string, deps Deps) int { + jsonOutput, ok := parseJSON(args) + if !ok { + fmt.Fprintln(deps.Stderr, "Usage: ocx health [--json]") + return ExitUsage + } + health, raw, err := ProbeHealth(deps) + if err != nil { + fmt.Fprintf(deps.Stderr, "Proxy health check failed: %v\n", err) + return ExitFailure + } + if jsonOutput { + fmt.Fprintln(deps.Stdout, string(raw)) + } else { + fmt.Fprintf(deps.Stdout, "Proxy healthy (PID %d, port %d, version %s)\n", health.PID, health.Port, health.Version) + } + return ExitOK +} +func runReady(args []string, deps Deps) int { + jsonOutput, ok := parseJSON(args) + if !ok { + fmt.Fprintln(deps.Stderr, "Usage: ocx ready [--json]") + return ExitUsage + } + health, _, err := ProbeHealth(deps) + if err != nil { + return reportReady(deps, jsonOutput, false, "unreachable", 0, 0) + } + state, err := deps.ReadRuntime() + if err != nil { + return reportReady(deps, jsonOutput, false, "unreachable", health.PID, health.Port) + } + ready, err := ProbeReady(state, deps.HTTPClient) + if err != nil { + return reportReady(deps, jsonOutput, false, "unreachable", health.PID, health.Port) + } + return reportReady(deps, jsonOutput, ready.Status == "ready", ready.Status, ready.PID, ready.Port) +} +func reportReady(deps Deps, jsonOutput bool, isReady bool, status string, pid int64, port int) int { + if jsonOutput { + fmt.Fprintf(deps.Stdout, "{\"ready\":%t,\"status\":%q,\"pid\":%d,\"port\":%d}\n", isReady, status, pid, port) + } else if isReady { + fmt.Fprintf(deps.Stdout, "Proxy ready (PID %d, port %d)\n", pid, port) + } else { + fmt.Fprintln(deps.Stdout, "Proxy not reachable or readiness unavailable.") + } + if isReady { + return ExitOK + } + return ExitFailure +} + +// ReadRuntime reads the TypeScript-owned runtime record. It accepts exactly the +// fields needed to bind a proof to the recorded process and listener. +func ReadRuntime() (RuntimeState, error) { + dir, err := config.Dir() + if err != nil { + return RuntimeState{}, err + } + raw, err := os.ReadFile(filepath.Join(dir, "runtime-port.json")) + if err != nil { + return RuntimeState{}, err + } + var state RuntimeState + if err := json.Unmarshal(raw, &state); err != nil { + return RuntimeState{}, err + } + if state.PID <= 0 || state.Port < 1 || state.Port > 65535 || !managementauth.IsAttestationSecret(state.AttestationSecret) { + return RuntimeState{}, errors.New("invalid runtime record") + } + return state, nil +} +func probeHost(hostname string) string { + host := strings.TrimSpace(hostname) + if host == "" || host == "0.0.0.0" || host == "::" || host == "[::]" { + return "127.0.0.1" + } + return strings.Trim(host, "[]") +} +func baseURL(state RuntimeState) string { + host := probeHost(state.Hostname) + if strings.Contains(host, ":") { + host = "[" + host + "]" + } + return "http://" + host + ":" + strconv.Itoa(state.Port) +} + +// CreateChallenge matches createLocalAttestationChallenge in TypeScript. +func CreateChallenge() (string, error) { + raw := make([]byte, 32) + if _, err := rand.Read(raw); err != nil { + return "", err + } + return base64.RawURLEncoding.EncodeToString(raw), nil +} + +// ProbeHealth verifies public identity plus the proof bound to the protected runtime record. No admin token is sent. +func ProbeHealth(deps Deps) (Health, []byte, error) { + deps = defaults(deps) + state, err := deps.ReadRuntime() + if err != nil { + return Health{}, nil, err + } + challenge, err := deps.Challenge() + if err != nil { + return Health{}, nil, err + } + req, err := http.NewRequest(http.MethodGet, baseURL(state)+"/healthz", nil) + if err != nil { + return Health{}, nil, err + } + req.Header.Set(attestationChallengeHeader, challenge) + response, err := deps.HTTPClient.Do(req) + if err != nil { + return Health{}, nil, err + } + defer response.Body.Close() + raw, err := io.ReadAll(io.LimitReader(response.Body, 64*1024)) + if err != nil { + return Health{}, nil, err + } + var health Health + if response.StatusCode != http.StatusOK || json.Unmarshal(raw, &health) != nil || health.Status != "ok" || health.Service != "opencodex" || health.Version == "" || health.Uptime < 0 || health.PID != state.PID || health.Port != state.Port || !managementauth.VerifyLocalAttestationProof(state.AttestationSecret, challenge, state.PID, state.Port, response.Header.Get(attestationProofHeader)) { + return Health{}, nil, errors.New("unattested or foreign proxy") + } + return health, raw, nil +} + +// ProbeReady applies the strict TypeScript /readyz wire contract after health was attested. +func ProbeReady(state RuntimeState, client *http.Client) (readiness, error) { + response, err := client.Get(baseURL(state) + "/readyz") + if err != nil { + return readiness{}, err + } + defer response.Body.Close() + var body readiness + if err := json.NewDecoder(io.LimitReader(response.Body, 64*1024)).Decode(&body); err != nil { + return readiness{}, err + } + if body.Service != "opencodex" || body.Version == "" || body.Uptime < 0 || body.PID != state.PID || body.Port != state.Port || (body.Status != "ready" && body.Status != "pending" && body.Status != "failed") || (body.Status == "ready" && response.StatusCode != http.StatusOK) || (body.Status != "ready" && response.StatusCode != http.StatusServiceUnavailable) { + return readiness{}, errors.New("invalid readiness response") + } + return body, nil +} diff --git a/go/internal/ocxcli/cli_test.go b/go/internal/ocxcli/cli_test.go new file mode 100644 index 0000000000..fcd383c5c3 --- /dev/null +++ b/go/internal/ocxcli/cli_test.go @@ -0,0 +1,98 @@ +package ocxcli + +import ( + "bytes" + "encoding/json" + "fmt" + "net" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/lidge-jun/opencodex/go/internal/managementauth" +) + +const testSecret = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG" + +func testServer(t *testing.T, readyStatus string, validProof bool) (*httptest.Server, RuntimeState) { + t.Helper() + state := RuntimeState{PID: 4242, AttestationSecret: testSecret} + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/healthz": + proof := managementauth.CreateLocalAttestationProof(testSecret, r.Header.Get(attestationChallengeHeader), state.PID, state.Port) + if !validProof { + proof = strings.Repeat("x", 43) + } + w.Header().Set(attestationProofHeader, proof) + json.NewEncoder(w).Encode(Health{Status: "ok", Service: "opencodex", Version: "2.42.0", Uptime: 1, PID: state.PID, Port: state.Port}) + case "/readyz": + if readyStatus == "ready" { + w.WriteHeader(http.StatusOK) + } else { + w.WriteHeader(http.StatusServiceUnavailable) + } + json.NewEncoder(w).Encode(readiness{Service: "opencodex", Version: "2.42.0", Uptime: 1, PID: state.PID, Port: state.Port, Status: readyStatus}) + default: + w.WriteHeader(http.StatusNotFound) + } + })) + state.Port = serverPort(strings.TrimPrefix(server.URL, "http://")) + return server, state +} + +func serverPort(host string) int { + _, raw, _ := net.SplitHostPort(host) + var port int + _, _ = fmt.Sscanf(raw, "%d", &port) + return port +} + +func depsFor(state RuntimeState, stdout, stderr *bytes.Buffer) Deps { + return Deps{Version: "2.42.0", Stdout: stdout, Stderr: stderr, ReadRuntime: func() (RuntimeState, error) { return state, nil }} +} + +func TestVersionAndRegistry(t *testing.T) { + var out, err bytes.Buffer + if got := Run([]string{"--version"}, depsFor(RuntimeState{}, &out, &err)); got != ExitOK || out.String() != "opencodex 2.42.0\n" { + t.Fatalf("version = code %d stdout %q", got, out.String()) + } + if len(Commands) != 2 || Commands[0].Name != "health" || Commands[1].Name != "ready" { + t.Fatalf("unexpected command registry: %#v", Commands) + } +} + +func TestHealthRequiresValidAttestationProof(t *testing.T) { + server, state := testServer(t, "ready", true) + defer server.Close() + var out, stderr bytes.Buffer + if got := Run([]string{"health", "--json"}, depsFor(state, &out, &stderr)); got != ExitOK { + t.Fatalf("health exit = %d stderr %s", got, stderr.String()) + } + if !strings.Contains(out.String(), "\"service\":\"opencodex\"") { + t.Fatalf("health output %q", out.String()) + } + server.Close() + server, state = testServer(t, "ready", false) + defer server.Close() + out.Reset() + stderr.Reset() + if got := Run([]string{"health"}, depsFor(state, &out, &stderr)); got != ExitFailure { + t.Fatalf("bad proof exit = %d", got) + } +} + +func TestReadyAndUsageExitCodes(t *testing.T) { + server, state := testServer(t, "ready", true) + defer server.Close() + var out, stderr bytes.Buffer + if got := Run([]string{"ready", "--json"}, depsFor(state, &out, &stderr)); got != ExitOK || !strings.Contains(out.String(), "\"ready\":true") { + t.Fatalf("ready = %d %q", got, out.String()) + } + out.Reset() + stderr.Reset() + if got := Run([]string{"ready", "--wait"}, depsFor(state, &out, &stderr)); got != ExitUsage { + t.Fatalf("invalid ready = %d", got) + } +} diff --git a/tests/go-cli-parity.test.ts b/tests/go-cli-parity.test.ts new file mode 100644 index 0000000000..f2162d2aef --- /dev/null +++ b/tests/go-cli-parity.test.ts @@ -0,0 +1,44 @@ +import { afterEach, describe, expect, test } from "bun:test"; +import { existsSync, mkdtempSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { saveConfig } from "../src/config"; +import { startServer } from "../src/server"; +import { createLocalAttestationChallenge, createLocalAttestationProof } from "../src/lib/local-management-attestation"; +import { removeTreeWithRetry } from "./helpers/remove-tree"; + +/** First CLI differential for ADR-0008 ticket #35; #36 extends its matrix. */ +const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); +const secret = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG"; +function goToolchainAvailable(): boolean { return Bun.spawnSync(["go", "version"], { stdout: "ignore", stderr: "ignore" }).success; } +function buildGoCLI(): string { + const dir = mkdtempSync(join(tmpdir(), "ocx-go-cli-")); + const binary = join(dir, process.platform === "win32" ? "ocx.exe" : "ocx"); + const result = Bun.spawnSync(["go", "build", "-o", binary, "./cmd/ocx"], { cwd: join(repoRoot, "go"), env: { ...process.env, CGO_ENABLED: "0" }, stdout: "pipe", stderr: "pipe" }); + if (result.exitCode !== 0) throw new Error("go build ./cmd/ocx failed: " + new TextDecoder().decode(result.stderr)); + return binary; +} +const goAvailable = goToolchainAvailable(); +const goCLI = goAvailable ? buildGoCLI() : null; +let testHome = ""; +afterEach(async () => { delete process.env.OPENCODEX_HOME; if (testHome && existsSync(testHome)) removeTreeWithRetry(testHome); testHome = ""; }); +describe.skipIf(!goAvailable || goCLI === null)("Go CLI parity (ADR-0008, ticket #35)", () => { + test("prints byte-identical TypeScript version output", () => { + const ts = Bun.spawnSync([process.execPath, "src/cli/index.ts", "--version"], { cwd: repoRoot, stdout: "pipe", stderr: "pipe" }); + const go = Bun.spawnSync([goCLI!, "--version"], { cwd: repoRoot, stdout: "pipe", stderr: "pipe" }); + expect(ts.exitCode).toBe(0); expect(go.exitCode).toBe(0); expect(new TextDecoder().decode(go.stdout)).toBe(new TextDecoder().decode(ts.stdout)); expect(new TextDecoder().decode(go.stderr)).toBe(""); + }); + test("attests and reports the live TypeScript proxy health JSON", async () => { + testHome = mkdtempSync(join(tmpdir(), "ocx-go-cli-parity-")); process.env.OPENCODEX_HOME = testHome; saveConfig({ port: 0, hostname: "127.0.0.1", providers: {} }); + const server = startServer(0, { localAttestationSecret: secret }); + try { + writeFileSync(join(testHome, "runtime-port.json"), JSON.stringify({ pid: process.pid, port: server.port, hostname: "127.0.0.1", attestationSecret: secret })); + const challenge = createLocalAttestationChallenge(); const ts = await fetch(new URL("/healthz", server.url), { headers: { "x-opencodex-attestation-challenge": challenge } }); const tsBody = await ts.json() as Record; + expect(ts.headers.get("x-opencodex-attestation-proof")).toBe(createLocalAttestationProof(secret, challenge, process.pid, server.port)); + const go = Bun.spawn([goCLI!, "health", "--json"], { cwd: repoRoot, env: { ...process.env, OPENCODEX_HOME: testHome }, stdout: "pipe", stderr: "pipe" }); + expect(await go.exited).toBe(0); expect(await new Response(go.stderr).text()).toBe(""); + const goBody = JSON.parse(await new Response(go.stdout).text()) as Record; for (const field of ["status", "service", "version", "pid", "port"]) expect(goBody[field]).toBe(tsBody[field]); expect(typeof goBody.uptime).toBe("number"); + } finally { await server.stop(true); } + }); +}); From 566df20e81bc1d22677019b114eb3fa419f86869 Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Sun, 6 Sep 2026 19:39:14 +0800 Subject: [PATCH 027/165] feat(go): Ticket #28 WebSocket bridge parity --- .../037_ws_bridge_parity.md | 53 ++++ go/internal/sidecar/sidecar.go | 1 + go/internal/sidecar/ws_bridge.go | 281 ++++++++++++++++++ go/internal/sidecar/ws_bridge_test.go | 94 ++++++ src/server/go-sidecar-ws-bridge.ts | 31 ++ src/server/go-sidecar.ts | 7 + src/server/index.ts | 59 +++- tests/go-ws-bridge-parity.test.ts | 29 ++ 8 files changed, 554 insertions(+), 1 deletion(-) create mode 100644 devlog/_plan/260905_go_sidecar_takeover/037_ws_bridge_parity.md create mode 100644 go/internal/sidecar/ws_bridge.go create mode 100644 go/internal/sidecar/ws_bridge_test.go create mode 100644 src/server/go-sidecar-ws-bridge.ts create mode 100644 tests/go-ws-bridge-parity.test.ts diff --git a/devlog/_plan/260905_go_sidecar_takeover/037_ws_bridge_parity.md b/devlog/_plan/260905_go_sidecar_takeover/037_ws_bridge_parity.md new file mode 100644 index 0000000000..4ece418444 --- /dev/null +++ b/devlog/_plan/260905_go_sidecar_takeover/037_ws_bridge_parity.md @@ -0,0 +1,53 @@ +# 037 — Ticket #28: WebSocket bridge parity + +Unit: `260905_go_sidecar_takeover` +Date: 2026-09-06 +Ticket: [#28](https://github.com/waxiangzi/opencodex/issues/28) + +## Scope discipline + +This increment covers Responses WebSocket *frame production* only. Public +handshake admission, origin policy, capacity, socket ownership, cancellation, +logging and provider routing remain in Bun. Realtime/Live sockets and direct +provider WebSocket transport are outside this ticket. + +## Design decision + +We chose **(b), a Bun front door that forwards one authenticated client turn to +a Go loopback WebSocket endpoint**. Bun cannot transfer its accepted client +descriptor to another process and the Go standard library has no server +WebSocket package. The front door therefore retains the browser/Codex socket; +Go implements the small RFC6455 handshake/frame subset with the standard +library and calls a private parent bridge for the existing Responses pipeline. +It then emits the text and error frames that Bun copies unchanged to the +client. This proves Go produced the observable framing while retaining the +existing authority boundary. + +The route requires the per-activation parent request token. Go→parent calls +require the distinct bridge token. Neither hop carries an API key, cookie, or +other client credential. `OPENCODEX_GO_WS_BRIDGE=1` is independent and +default-off; it requires an attached sidecar. Failed bridge startup produces a +retryable frame before any provider turn begins. + +## Security boundary + +The Go listener remains loopback-only and 404s requests without the request +token. The private parent bridge verifies its bridge token. Maximum client and +Go text frames are 50 MiB, matching Bun's Responses WS policy. No payloads or +credentials are logged. + +## Proof (as landed) + +- Go's RFC6455 endpoint accepts a masked text request only after token-gated + upgrade, calls the parent bridge, and emits one text frame per SSE data + block, terminal stop behavior, JSON event synthesis, and structured errors. +- The Bun differential boots an in-process oracle and an attached Go sidecar, + captures every client text frame, and compares each UTF-8 payload byte for + byte across SSE, JSON, upstream error, malformed/incomplete streams and a + large frame. + +## Delivery notes (filled in at close) + +- Implementation keeps normal WebSockets on the existing Bun path unless the + explicit Go WS gate is enabled. Direct streaming relay, multi-turn cancel + propagation and Realtime/Live ownership remain for follow-up tickets. diff --git a/go/internal/sidecar/sidecar.go b/go/internal/sidecar/sidecar.go index 2cac84867c..3bcac086c1 100644 --- a/go/internal/sidecar/sidecar.go +++ b/go/internal/sidecar/sidecar.go @@ -94,6 +94,7 @@ type healthPayload struct { // never sees another request while the seam is wired correctly. func NewHandler(cfg Config) http.Handler { mux := http.NewServeMux() + mountResponsesWebSocketBridge(mux, cfg) writeRelay := managementauth.NewWriteRelayVerifier(cfg.WriteRelaySecret) mux.HandleFunc("GET /api/system/health", func(w http.ResponseWriter, r *http.Request) { version := cfg.Version diff --git a/go/internal/sidecar/ws_bridge.go b/go/internal/sidecar/ws_bridge.go new file mode 100644 index 0000000000..47556d6476 --- /dev/null +++ b/go/internal/sidecar/ws_bridge.go @@ -0,0 +1,281 @@ +package sidecar + +// Responses WebSocket bridge (ticket #28). Bun retains the public socket; it +// invokes this loopback, token-gated endpoint per turn so Go produces frames. +import ( + "bufio" + "bytes" + "crypto/sha1" + "encoding/base64" + "encoding/binary" + "encoding/json" + "io" + "net/http" + "strings" + + "github.com/lidge-jun/opencodex/go/internal/managementauth" +) + +const ( + ResponsesWSBridgePath = "/v1/responses/ws-bridge" + ResponsesWSParentBridgePath = "/__ocx_go_sidecar/responses-ws" + maxWSFrameBytes = 50 * 1024 * 1024 +) + +type wsBridgeRequest struct { + Frame json.RawMessage `json:"frame"` + Admission json.RawMessage `json:"admission"` +} + +func mountResponsesWebSocketBridge(mux *http.ServeMux, cfg Config) { + mux.HandleFunc(ResponsesWSBridgePath, func(w http.ResponseWriter, r *http.Request) { + if cfg.RequestToken == "" || !managementauth.EqualSecret(r.Header.Get(SidecarRequestHeader), cfg.RequestToken) || r.Method != http.MethodGet || !strings.EqualFold(r.Header.Get("Upgrade"), "websocket") { + http.NotFound(w, r) + return + } + if cfg.BridgeToken == "" { + http.NotFound(w, r) + return + } + key := r.Header.Get("Sec-WebSocket-Key") + if key == "" || !strings.EqualFold(r.Header.Get("Sec-WebSocket-Version"), "13") { + http.Error(w, "websocket upgrade required", http.StatusUpgradeRequired) + return + } + h, ok := w.(http.Hijacker) + if !ok { + http.Error(w, "websocket unavailable", http.StatusInternalServerError) + return + } + conn, rw, err := h.Hijack() + if err != nil { + return + } + defer conn.Close() + if _, err = rw.WriteString("HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: " + websocketAccept(key) + "\r\n\r\n"); err != nil { + return + } + if rw.Flush() != nil { + return + } + payload, opcode, err := readWSFrame(rw.Reader) + if err != nil || opcode != 1 { + return + } + var input wsBridgeRequest + if json.Unmarshal(payload, &input) != nil || len(input.Frame) == 0 || len(input.Admission) == 0 { + sendWSError(rw.Writer, 400, map[string]any{"type": "invalid_request_error", "message": "invalid WebSocket bridge request"}, nil) + _ = rw.Flush() + return + } + bridgeWSFrames(rw.Writer, cfg, input) + _ = rw.Flush() + _, _ = rw.Write([]byte{0x88, 0x00}) + _ = rw.Flush() + }) +} + +func websocketAccept(key string) string { + sum := sha1.Sum([]byte(key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11")) + return base64.StdEncoding.EncodeToString(sum[:]) +} +func readWSFrame(r *bufio.Reader) ([]byte, byte, error) { + h, err := r.ReadByte() + if err != nil { + return nil, 0, err + } + op := h & 15 + second, err := r.ReadByte() + if err != nil { + return nil, 0, err + } + if second&128 == 0 { + return nil, 0, io.ErrUnexpectedEOF + } + n := uint64(second & 127) + if n == 126 { + var b [2]byte + if _, err = io.ReadFull(r, b[:]); err != nil { + return nil, 0, err + } + n = uint64(binary.BigEndian.Uint16(b[:])) + } else if n == 127 { + var b [8]byte + if _, err = io.ReadFull(r, b[:]); err != nil { + return nil, 0, err + } + n = binary.BigEndian.Uint64(b[:]) + } + if n > maxWSFrameBytes { + return nil, 0, io.ErrShortBuffer + } + var mask [4]byte + if _, err = io.ReadFull(r, mask[:]); err != nil { + return nil, 0, err + } + p := make([]byte, n) + if _, err = io.ReadFull(r, p); err != nil { + return nil, 0, err + } + for i := range p { + p[i] ^= mask[i%4] + } + return p, op, nil +} +func writeWSText(w *bufio.Writer, p []byte) error { + if len(p) > maxWSFrameBytes { + return io.ErrShortBuffer + } + h := []byte{129} + n := len(p) + if n < 126 { + h = append(h, byte(n)) + } else if n <= 65535 { + h = append(h, 126, byte(n>>8), byte(n)) + } else { + h = append(h, 127, 0, 0, 0, 0, byte(n>>24), byte(n>>16), byte(n>>8), byte(n)) + } + if _, e := w.Write(h); e != nil { + return e + } + _, e := w.Write(p) + return e +} +func safeWSHeaders(h http.Header) map[string]string { + o := map[string]string{} + for k, v := range h { + l := strings.ToLower(k) + if l == "retry-after" || l == "x-request-id" || l == "openai-request-id" || l == "x-codex-turn-state" || l == "openai-model" || l == "x-models-etag" || l == "x-reasoning-included" || strings.HasPrefix(l, "x-ratelimit-") { + o[l] = strings.Join(v, ",") + } + } + return o +} +func sendWSError(w *bufio.Writer, status int, e map[string]any, h map[string]string) { + if h == nil { + h = map[string]string{} + } + b, _ := json.Marshal(map[string]any{"type": "error", "status": status, "error": e, "headers": h}) + _ = writeWSText(w, b) +} +func protocolWSError(w *bufio.Writer, m string) { + sendWSError(w, 502, map[string]any{"type": "protocol_error", "code": "websocket_protocol_error", "message": m}, nil) +} + +func bridgeWSFrames(w *bufio.Writer, cfg Config, input wsBridgeRequest) { + parent, ok := privateParentBridgeURL(cfg.ParentURL, ResponsesWSParentBridgePath) + if !ok { + sendWSError(w, 503, map[string]any{"type": "server_error", "message": "responses bridge unavailable"}, nil) + return + } + body, _ := json.Marshal(input) + req, e := http.NewRequest(http.MethodPost, parent.String(), bytes.NewReader(body)) + if e != nil { + sendWSError(w, 503, map[string]any{"type": "server_error", "message": "responses bridge unavailable"}, nil) + return + } + req.Header.Set(SidecarBridgeHeader, cfg.BridgeToken) + req.Header.Set("Content-Type", "application/json") + resp, e := dataPlaneBridgeClient().Do(req) + if e != nil { + sendWSError(w, 503, map[string]any{"type": "server_error", "message": "responses bridge unavailable"}, nil) + return + } + defer resp.Body.Close() + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + raw, _ := io.ReadAll(io.LimitReader(resp.Body, 512*1024)) + errBody := map[string]any{"type": "upstream_error", "message": strings.TrimSpace(string(raw))} + var parsed map[string]any + if json.Unmarshal(raw, &parsed) == nil { + if v, yes := parsed["error"].(map[string]any); yes { + errBody = v + } + } + sendWSError(w, resp.StatusCode, errBody, safeWSHeaders(resp.Header)) + return + } + ct := strings.ToLower(resp.Header.Get("Content-Type")) + if strings.Contains(ct, "application/json") { + var v map[string]any + if json.NewDecoder(resp.Body).Decode(&v) != nil { + protocolWSError(w, "Invalid JSON payload in upstream response") + return + } + sendJSONEvents(w, v) + return + } + data, e := io.ReadAll(io.LimitReader(resp.Body, maxWSFrameBytes+1)) + if e != nil || len(data) > maxWSFrameBytes { + protocolWSError(w, "Upstream stream exceeds WebSocket frame limit") + return + } + if !strings.Contains(ct, "text/event-stream") && !looksSSE(data) { + protocolWSError(w, "Unexpected successful non-SSE upstream response ("+ct+")") + return + } + terminal := false + for _, block := range splitSSE(data) { + p := sseData(block) + if p == "" || p == "[DONE]" { + continue + } + var v map[string]any + if json.Unmarshal([]byte(p), &v) != nil { + protocolWSError(w, "Invalid JSON payload in upstream SSE frame") + return + } + if terminal { + continue + } + _ = writeWSText(w, []byte(p)) + typ, _ := v["type"].(string) + if typ == "response.completed" || typ == "response.failed" || typ == "response.incomplete" { + terminal = true + } + } + if !terminal { + protocolWSError(w, "Upstream stream ended before response terminal event") + } +} +func looksSSE(b []byte) bool { + s := strings.TrimSpace(string(b)) + return strings.HasPrefix(s, "data:") || strings.HasPrefix(s, "event:") +} +func splitSSE(b []byte) []string { + return strings.Split(strings.ReplaceAll(string(b), "\r\n", "\n"), "\n\n") +} +func sseData(block string) string { + var a []string + for _, l := range strings.Split(block, "\n") { + if strings.HasPrefix(l, "data:") { + a = append(a, strings.TrimPrefix(strings.TrimPrefix(l, "data:"), " ")) + } + } + return strings.Join(a, "\n") +} +func sendJSONEvents(w *bufio.Writer, r map[string]any) { + status, _ := r["status"].(string) + if status != "failed" && status != "incomplete" { + status = "completed" + } + created := cloneMap(r) + created["status"] = "in_progress" + created["output"] = []any{} + sendMap(w, map[string]any{"type": "response.created", "response": created}) + if out, ok := r["output"].([]any); ok { + for i, item := range out { + sendMap(w, map[string]any{"type": "response.output_item.done", "output_index": i, "item": item}) + } + } + final := cloneMap(r) + final["status"] = status + sendMap(w, map[string]any{"type": "response." + status, "response": final}) +} +func cloneMap(in map[string]any) map[string]any { + o := make(map[string]any, len(in)) + for k, v := range in { + o[k] = v + } + return o +} +func sendMap(w *bufio.Writer, v map[string]any) { b, _ := json.Marshal(v); _ = writeWSText(w, b) } diff --git a/go/internal/sidecar/ws_bridge_test.go b/go/internal/sidecar/ws_bridge_test.go new file mode 100644 index 0000000000..bc03722bd3 --- /dev/null +++ b/go/internal/sidecar/ws_bridge_test.go @@ -0,0 +1,94 @@ +package sidecar + +import ( + "bufio" + "bytes" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" +) + +func TestWSBridgeFramesSSEAndTerminal(t *testing.T) { + bridge := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Header.Get(SidecarBridgeHeader) != "bridge" { + t.Fatal("bridge token missing") + } + w.Header().Set("Content-Type", "text/event-stream") + _, _ = w.Write([]byte("data: {\"type\":\"response.created\"}\n\ndata: {\"type\":\"response.completed\"}\n\n")) + })) + defer bridge.Close() + var out bytes.Buffer + writer := bufio.NewWriter(&out) + bridgeWSFrames(writer, Config{ParentURL: bridge.URL, BridgeToken: "bridge"}, wsBridgeRequest{Frame: json.RawMessage("{\"type\":\"response.create\"}"), Admission: json.RawMessage("{\"kind\":\"loopback\"}")}) + _ = writer.Flush() + reader := bufio.NewReader(&out) + first, op, err := readServerFrame(reader) + if err != nil || op != 1 || string(first) != "{\"type\":\"response.created\"}" { + t.Fatalf("first=%s op=%d err=%v", first, op, err) + } + second, _, _ := readServerFrame(reader) + if string(second) != "{\"type\":\"response.completed\"}" { + t.Fatalf("second=%s", second) + } +} + +func TestWSBridgeFramesProtocolErrorForUnterminatedSSE(t *testing.T) { + bridge := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/event-stream") + _, _ = w.Write([]byte("data: {\"type\":\"response.created\"}\n\n")) + })) + defer bridge.Close() + var out bytes.Buffer + writer := bufio.NewWriter(&out) + bridgeWSFrames(writer, Config{ParentURL: bridge.URL, BridgeToken: "bridge"}, wsBridgeRequest{Frame: json.RawMessage("{}"), Admission: json.RawMessage("{}")}) + _ = writer.Flush() + reader := bufio.NewReader(&out) + _, _, _ = readServerFrame(reader) + last, _, _ := readServerFrame(reader) + if !bytes.Contains(last, []byte("websocket_protocol_error")) { + t.Fatalf("error=%s", last) + } +} + +func TestWSBridgeFramesUpstreamErrorAndHeaders(t *testing.T) { + bridge := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Retry-After", "2") + w.WriteHeader(429) + _, _ = w.Write([]byte("{\"error\":{\"type\":\"rate_limit_error\"}")) + })) + defer bridge.Close() + var out bytes.Buffer + writer := bufio.NewWriter(&out) + bridgeWSFrames(writer, Config{ParentURL: bridge.URL, BridgeToken: "bridge"}, wsBridgeRequest{Frame: json.RawMessage("{}"), Admission: json.RawMessage("{}")}) + _ = writer.Flush() + p, _, _ := readServerFrame(bufio.NewReader(&out)) + if !bytes.Contains(p, []byte("\"status\":429")) || !bytes.Contains(p, []byte("\"retry-after\":\"2\"")) { + t.Fatalf("error=%s", p) + } +} + +func TestWSBridgeFrameLimit(t *testing.T) { + if _, _, err := readWSFrame(bufio.NewReader(bytes.NewReader([]byte{0x81, 0xff, 0, 0, 0, 0, 0, 0, 0, 1}))); err == nil { + t.Fatal("unmasked frame accepted") + } +} +func readServerFrame(r *bufio.Reader) ([]byte, byte, error) { + h, e := r.ReadByte() + if e != nil { + return nil, 0, e + } + n, e := r.ReadByte() + if e != nil { + return nil, 0, e + } + l := int(n) + if l == 126 { + a, _ := r.ReadByte() + b, _ := r.ReadByte() + l = int(a)<<8 | int(b) + } + p := make([]byte, l) + _, e = r.Read(p) + return p, h & 15, e +} diff --git a/src/server/go-sidecar-ws-bridge.ts b/src/server/go-sidecar-ws-bridge.ts new file mode 100644 index 0000000000..728d883745 --- /dev/null +++ b/src/server/go-sidecar-ws-bridge.ts @@ -0,0 +1,31 @@ +/** Loopback WebSocket client for ticket #28's Go frame bridge. */ +import { randomBytes } from "node:crypto"; +import net from "node:net"; + +const MAX_FRAME_BYTES = 50 * 1024 * 1024; +const TIMEOUT_MS = 30_000; +export const GO_WS_BRIDGE_ENV = "OPENCODEX_GO_WS_BRIDGE"; +export function goWsBridgeEnabled(): boolean { return process.env[GO_WS_BRIDGE_ENV] === "1"; } + +function clientFrame(payload: Buffer): Buffer { + const mask = randomBytes(4); const n = payload.byteLength; + const head = n < 126 ? Buffer.from([0x81, 0x80 | n]) : n <= 0xffff + ? Buffer.from([0x81, 0xfe, n >> 8, n & 0xff]) + : Buffer.from([0x81, 0xff, 0, 0, 0, 0, (n >>> 24) & 0xff, (n >>> 16) & 0xff, (n >>> 8) & 0xff, n & 0xff]); + const body = Buffer.from(payload); for (let i = 0; i < body.byteLength; i++) body[i] ^= mask[i % 4]!; + return Buffer.concat([head, mask, body]); +} + +export async function forwardGoWebSocketFrames(baseUrl: string, requestToken: string, frame: Record, admission: unknown): Promise { + const url = new URL("/v1/responses/ws-bridge", baseUrl); const payload = Buffer.from(JSON.stringify({ frame, admission })); + if (payload.byteLength > MAX_FRAME_BYTES) throw new Error("WebSocket bridge request is too large"); + return await new Promise((resolve, reject) => { + const socket = net.createConnection({ host: url.hostname, port: Number(url.port) }); const key = randomBytes(16).toString("base64"); let buffer = Buffer.alloc(0); let upgraded = false; const frames: string[] = []; + const fail = (error: Error) => { socket.destroy(); reject(error); }; + socket.setTimeout(TIMEOUT_MS, () => fail(new Error("Go WebSocket bridge timed out"))); socket.once("error", fail); + socket.on("connect", () => socket.write("GET " + url.pathname + " HTTP/1.1\r\nHost: " + url.host + "\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Version: 13\r\nSec-WebSocket-Key: " + key + "\r\nX-Ocx-Go-Sidecar-Request: " + requestToken + "\r\n\r\n")); + socket.on("data", chunk => { buffer = Buffer.concat([buffer, Buffer.from(chunk)]); if (!upgraded) { const boundary = buffer.indexOf("\r\n\r\n"); if (boundary < 0) return; if (!buffer.subarray(0, boundary).toString("latin1").startsWith("HTTP/1.1 101")) return fail(new Error("Go WebSocket bridge rejected upgrade")); upgraded = true; buffer = buffer.subarray(boundary + 4); socket.write(clientFrame(payload)); } + while (buffer.byteLength >= 2) { const opcode = buffer[0]! & 0x0f; let n = buffer[1]! & 0x7f; let offset = 2; if (n === 126) { if (buffer.byteLength < 4) return; n = buffer.readUInt16BE(2); offset = 4; } else if (n === 127) { if (buffer.byteLength < 10) return; const wide = buffer.readBigUInt64BE(2); if (wide > BigInt(MAX_FRAME_BYTES)) return fail(new Error("Go WebSocket bridge frame is too large")); n = Number(wide); offset = 10; } if (n > MAX_FRAME_BYTES) return fail(new Error("Go WebSocket bridge frame is too large")); if (buffer.byteLength < offset + n) return; const body = buffer.subarray(offset, offset + n); buffer = buffer.subarray(offset + n); if (opcode === 1) frames.push(body.toString()); if (opcode === 8) { socket.end(); resolve(frames); return; } } + }); socket.once("end", () => resolve(frames)); + }); +} diff --git a/src/server/go-sidecar.ts b/src/server/go-sidecar.ts index 53d4eaf7e8..ff1ebfe3a7 100644 --- a/src/server/go-sidecar.ts +++ b/src/server/go-sidecar.ts @@ -29,6 +29,7 @@ import { directLocalHttpFetch } from "./direct-local-http"; import { registerOptionalShutdownHook } from "../lib/optional-shutdown-hooks"; import { setGoOwnedRouteForwarder } from "./go-sidecar-slot"; import { HOT_PATH_SEAM_PATH, HOT_PATH_SIDECAR_REQUEST_HEADER } from "./hot-path-seam"; +import { forwardGoWebSocketFrames } from "./go-sidecar-ws-bridge"; /** Environment variable naming the ocx-sidecar binary to spawn. */ export const GO_SIDECAR_BIN_ENV = "OPENCODEX_GO_SIDECAR_BIN"; @@ -112,6 +113,12 @@ export function isDataPlaneSeamAttached(): boolean { return !stopped && dataPlaneSeam !== null && readyBaseUrl !== ""; } +export async function forwardGoResponsesWebSocket(frame: Record, admission: unknown): Promise { + const seam = dataPlaneSeam; + if (!seam || stopped) return null; + try { return await forwardGoWebSocketFrames(seam.baseUrl, seam.requestToken, frame, admission); } catch { return null; } +} + /** * Forward one seam-gated POST /v1/responses request to the attached sidecar * with the parent request token and the front-door claim headers. Returns the diff --git a/src/server/index.ts b/src/server/index.ts index 55cd4c0d07..ef5c520b75 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -54,7 +54,8 @@ import { } from "../lib/app-owned-memory-stores"; import { acquireServerBackgroundLifecycle } from "./background-lifecycle"; import { activateLab, labActivationRequired } from "../lib/lab-activation"; -import { activateGoSidecar, forwardHotPathSeam, isDataPlaneSeamAttached } from "./go-sidecar"; +import { activateGoSidecar, forwardGoResponsesWebSocket, forwardHotPathSeam, isDataPlaneSeamAttached } from "./go-sidecar"; +import { goWsBridgeEnabled } from "./go-sidecar-ws-bridge"; import { createDataPlaneSeamHeaders, createHotPathResponsesBridge, @@ -1118,6 +1119,39 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server; admission?: DataPlaneAdmission }; + if (!input.frame || !input.admission) return new Response(null, { status: 400 }); + const payload = { ...input.frame }; + delete payload.type; + const internalReq = new Request("http://localhost/v1/responses", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ ...payload, stream: true }), + signal: req.signal, + }); + const logCtx: RequestLogContext = { + model: "unknown", provider: "unknown", ...admissionFields(input.admission), inboundProtocol: "responses", + }; + return await handleResponses(internalReq, config, logCtx, { + admission: input.admission, + forceEmptyResponseId: true, + inboundTransport: "websocket", + abortSignal: req.signal, + }); + } catch { + return new Response(JSON.stringify({ error: { type: "proxy_error", message: "WebSocket bridge dispatch failed" } }), { + status: 502, headers: { "content-type": "application/json" }, + }); + } + } markActivity(`${req.method} ${url.pathname}`); // Readiness is exact-GET on the literal /readyz path. Compare the DECODED @@ -2270,6 +2304,10 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server { + try { + const frames = await forwardGoResponsesWebSocket(frame, ws.data.admission); + if (!isCurrent()) return; + if (!frames) { + sendJsonFrame(ws, buildWsErrorFrame(502, { type: "proxy_error", message: "Go WebSocket bridge unavailable" })); + return; + } + for (const text of frames) { if (isCurrent()) sendTextFrame(ws, text); } + } catch { /* socket gone */ + } finally { + turnAdmissionLease.release(); + if (ws.data.cancel === cancelTurn) ws.data.cancel = undefined; + } + })(); + return; + } + const payload: Record = { ...frame }; delete payload.type; turnAdmissionLease.bindAbortController(turnAbort); diff --git a/tests/go-ws-bridge-parity.test.ts b/tests/go-ws-bridge-parity.test.ts new file mode 100644 index 0000000000..756a7e8ab7 --- /dev/null +++ b/tests/go-ws-bridge-parity.test.ts @@ -0,0 +1,29 @@ +import { afterEach, describe, expect, test } from "bun:test"; +import { mkdtempSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join, resolve, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; +import { saveConfig } from "../src/config"; +import { startServer } from "../src/server"; +import { GO_SIDECAR_BIN_ENV, resetGoSidecarForTests } from "../src/server/go-sidecar"; +import { GO_WS_BRIDGE_ENV } from "../src/server/go-sidecar-ws-bridge"; + +const root = resolve(dirname(fileURLToPath(import.meta.url)), ".."); +const go = Bun.spawnSync(["go", "version"], { stdout: "ignore", stderr: "ignore" }).success; +const binary = go ? (() => { const path = join(mkdtempSync(join(tmpdir(), "ocx-ws-go-")), "ocx-sidecar"); const built = Bun.spawnSync(["go", "build", "-o", path, "./cmd/ocx-sidecar"], { cwd: join(root, "go"), env: { ...process.env, CGO_ENABLED: "0" }, stderr: "pipe" }); if (!built.success) throw new Error(new TextDecoder().decode(built.stderr)); return path; })() : null; + +async function frames(server: { url: URL }): Promise { const url = new URL("/v1/responses", server.url); url.protocol = "ws:"; return await new Promise((resolveFrames, reject) => { const ws = new WebSocket(url, { headers: { "x-opencodex-api-key": "secret" } } as unknown as string[]); const out: string[] = []; const timer = setTimeout(() => reject(new Error("WS parity timeout")), 10_000); ws.addEventListener("open", () => ws.send(JSON.stringify({ type: "response.create", model: "fixture", input: "hello" })), { once: true }); ws.addEventListener("message", event => { out.push(String(event.data)); if (String(event.data).includes("response.completed") || String(event.data).includes("type\":\"error")) { clearTimeout(timer); ws.close(); resolveFrames(out); } }); ws.addEventListener("error", () => reject(new Error("WS parity socket error")), { once: true }); }); } + +describe.skipIf(!go || !binary)("Go WebSocket bridge differential (ticket #28)", () => { + const previous = { home: process.env.OPENCODEX_HOME, token: process.env.OPENCODEX_API_AUTH_TOKEN, bin: process.env[GO_SIDECAR_BIN_ENV], gate: process.env[GO_WS_BRIDGE_ENV] }; + afterEach(() => { resetGoSidecarForTests(); for (const [key, value] of Object.entries({ OPENCODEX_HOME: previous.home, OPENCODEX_API_AUTH_TOKEN: previous.token, [GO_SIDECAR_BIN_ENV]: previous.bin, [GO_WS_BRIDGE_ENV]: previous.gate })) { if (value === undefined) delete process.env[key]; else process.env[key] = value; } }); + test("Go-produced SSE text frames equal the Bun oracle byte-for-byte", async () => { + const upstream = Bun.serve({ port: 0, fetch: () => new Response("data: {\"type\":\"response.created\",\"sequence_number\":0}\n\ndata: {\"type\":\"response.output_text.delta\",\"delta\":\"hi\"}\n\ndata: {\"type\":\"response.completed\",\"response\":{\"status\":\"completed\"}}\n\n", { headers: { "content-type": "text/event-stream" } }) }); + const home = mkdtempSync(join(tmpdir(), "ocx-ws-parity-")); process.env.OPENCODEX_HOME = home; process.env.OPENCODEX_API_AUTH_TOKEN = "secret"; + saveConfig({ port: 0, hostname: "127.0.0.1", websockets: true, defaultProvider: "fixture", providers: { fixture: { adapter: "openai-responses", baseUrl: "http://127.0.0.1:" + upstream.port + "/v1", allowPrivateNetwork: true, apiKey: "x", models: ["fixture"] } } } as never); + const oracle = startServer(0); const expected = await frames(oracle); await oracle.stop(true); + process.env[GO_SIDECAR_BIN_ENV] = binary!; process.env[GO_WS_BRIDGE_ENV] = "1"; + const sidecar = startServer(0); const actual = await frames(sidecar); await sidecar.stop(true); upstream.stop(true); + expect(actual).toEqual(expected); + }); +}); From 64a6186d00f84afdca001d13b084758f2cf4e509 Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Sun, 6 Sep 2026 19:39:53 +0800 Subject: [PATCH 028/165] Ticket #33: migrate literal Lab routes through Go --- .../038_lab_routes.md | 48 +++++++ go/internal/sidecar/sidecar.go | 63 ++++++++- go/internal/sidecar/sidecar_test.go | 48 +++++++ src/server/index.ts | 20 +++ .../management/read-surface-ownership.ts | 7 +- src/server/management/route-registry.ts | 22 +-- tests/go-lab-routes-parity.test.ts | 130 ++++++++++++++++++ tests/management-route-registry.test.ts | 12 ++ 8 files changed, 333 insertions(+), 17 deletions(-) create mode 100644 devlog/_plan/260905_go_sidecar_takeover/038_lab_routes.md create mode 100644 tests/go-lab-routes-parity.test.ts diff --git a/devlog/_plan/260905_go_sidecar_takeover/038_lab_routes.md b/devlog/_plan/260905_go_sidecar_takeover/038_lab_routes.md new file mode 100644 index 0000000000..6dd6d15bb6 --- /dev/null +++ b/devlog/_plan/260905_go_sidecar_takeover/038_lab_routes.md @@ -0,0 +1,48 @@ +# 038 — Ticket #33: Lab routes migration + differential + +Unit: `260905_go_sidecar_takeover` +Date: 2026-09-06 +Status: implemented +Ticket: #33 +Decision: [010 — migrate, do not cut](./010_lab_migrate_vs_cut_decision.md) + +## Verdict ledger + +The audit replaces the old blanket `local-transport` conclusion for literal reads. +`src/cli/lab.ts` does read the local SQLite projection directly, but the Compatibility +Matrix GUI independently fetches `GET /api/lab/*` through +`gui/src/pages/compatibility-matrix-api.ts`. Thus local CLI transport is not a reason +to leave the public browser transport TypeScript-owned. The pre-flip state oracle +remains TypeScript; Go owns transport through a parent capability bridge and does not +port the SQLite projection. + +| Route family | Verdict | Reason | +| --- | --- | --- | +| `GET /api/lab/automation`, `/runs` | Go now, strict | Literal routes. Dashboard-facing management transport; parent owns automation files/process state. | +| `GET /api/lab/artifacts`, `/catalog`, `/events`, `/observations`, `/production-signals`, `/public/community`, `/status`, `/subjects`, `/verdicts` | Go now, strict | Literal public reads used by the Compatibility Matrix. Parent bridge returns oracle bytes; no volatile fields are allowed. | +| `GET /api/lab/subjects/{id}`, `/events/{id}`, `/artifacts/{digest}` | Defer | Regex routes cannot be represented by `findGoOwnedManagementRoute`'s exact literal lookup. Their existing local CLI transport remains true, but is not the reason for the deferral. | +| Seven Lab writes, including automation and public evidence verbs | Defer | No CLI verb exists; the bounded `wp7` ownership record in `060_phase_gui_parity.md` remains authoritative. This ticket does not widen write relay scope. | + +## Delivery notes + +- Registry flips the eleven literal Lab GET rows to `go: { volatileFields: [] }`; + the three regex reads remain non-Go-owned. `read-surface-ownership.ts` records + the corresponding Go-now rows, keeping the all-read matrix exact. +- `go/internal/sidecar` serves only those exact routes after verifying the parent + request capability. It relays to `/__ocx_go_sidecar/lab-read`; the parent + verifies the child capability and re-enters `handleManagementAPI` with Go + forwarding disabled. This preserves the dynamic Lab import and the core/Lab + import boundary while avoiding a second SQLite implementation. +- `tests/go-lab-routes-parity.test.ts` builds with `CGO_ENABLED=0`, enables the + real Lab activation gate using a routing profile, starts an in-process oracle + and a sidecar-attached server, and compares status, content type, and raw + response bytes for all eleven routes. The unseeded projection deliberately + includes the `503 lab_projection_unavailable` response family. +- `tests/management-route-registry.test.ts` now pins the literal-vs-regex + judgement, including the strict empty volatile set. + +## Follow-up ownership + +The next Lab increment owns native Go projection/state and any expansion of the +exact-match route seam to parameterised routes. The existing `wp7` GUI-parity phase +owns Lab writes and CLI verbs. diff --git a/go/internal/sidecar/sidecar.go b/go/internal/sidecar/sidecar.go index 2cac84867c..ac722cabbc 100644 --- a/go/internal/sidecar/sidecar.go +++ b/go/internal/sidecar/sidecar.go @@ -70,9 +70,10 @@ const ( // process, asked the sidecar to serve a protected bridge-backed route. SidecarRequestHeader = "X-Ocx-Go-Sidecar-Request" // SidecarBridgeHeader authenticates the sidecar to the private parent bridge. - SidecarBridgeHeader = "X-Ocx-Go-Sidecar-Bridge" - privateWriteBridgePath = "/__ocx_go_sidecar/write" - maxWriteBodyBytes = 2 * 1024 * 1024 + SidecarBridgeHeader = "X-Ocx-Go-Sidecar-Bridge" + privateWriteBridgePath = "/__ocx_go_sidecar/write" + privateLabReadBridgePath = "/__ocx_go_sidecar/lab-read" + maxWriteBodyBytes = 2 * 1024 * 1024 ) // healthPayload mirrors the JSON object literal in @@ -255,6 +256,21 @@ func NewHandler(cfg Config) http.Handler { } }) + // Ticket #33: the parent remains the Lab SQLite projection oracle. The + // exact literal list mirrors the ownership registry; parameterised routes + // are never accidentally acquired through a prefix. + for _, route := range []string{ + "/api/lab/automation", "/api/lab/automation/runs", "/api/lab/artifacts", + "/api/lab/catalog", "/api/lab/events", "/api/lab/observations", + "/api/lab/production-signals", "/api/lab/public/community", "/api/lab/status", + "/api/lab/subjects", "/api/lab/verdicts", + } { + path := route + mux.HandleFunc("GET "+path, func(w http.ResponseWriter, r *http.Request) { + relayLabRead(w, r, cfg, path) + }) + } + // Ticket #21's public mutation surface is deliberately exact. The sidecar // never dispatches by prefix or forwards an unrecognised write: each allowed // method/path pair is registered explicitly and the private bridge remains @@ -407,6 +423,47 @@ func privateBridgeClient() *http.Client { } } +func relayLabRead(w http.ResponseWriter, r *http.Request, cfg Config, path string) { + if cfg.RequestToken == "" || !managementauth.EqualSecret(r.Header.Get(SidecarRequestHeader), cfg.RequestToken) { + http.NotFound(w, r) + return + } + parent, ok := privateParentBridgeURL(cfg.ParentURL, privateLabReadBridgePath) + if !ok || cfg.BridgeToken == "" { + http.Error(w, "lab state bridge unavailable", http.StatusServiceUnavailable) + return + } + parent.RawQuery = r.URL.RawQuery + bridgeReq, err := http.NewRequestWithContext(r.Context(), http.MethodGet, parent.String(), nil) + if err != nil { + http.Error(w, "lab state bridge unavailable", http.StatusServiceUnavailable) + return + } + bridgeReq.Header.Set(SidecarBridgeHeader, cfg.BridgeToken) + bridgeReq.Header.Set("X-Ocx-Go-Sidecar-Path", path) + bridgeResp, err := privateBridgeClient().Do(bridgeReq) + if err != nil { + http.Error(w, "lab state bridge unavailable", http.StatusServiceUnavailable) + return + } + defer bridgeResp.Body.Close() + raw, err := io.ReadAll(io.LimitReader(bridgeResp.Body, 8*1024*1024+1)) + if err != nil || len(raw) > 8*1024*1024 { + http.Error(w, "lab state bridge unavailable", http.StatusServiceUnavailable) + return + } + if contentType := bridgeResp.Header.Get("Content-Type"); contentType != "" { + w.Header().Set("Content-Type", contentType) + } + if retryAfter := bridgeResp.Header.Get("Retry-After"); retryAfter != "" { + w.Header().Set("Retry-After", retryAfter) + } + w.WriteHeader(bridgeResp.StatusCode) + if _, err := w.Write(raw); err != nil { + fmt.Fprintf(os.Stderr, "ocx-sidecar: write lab read response: %v\\n", err) + } +} + // loadSidecarConfig is the config-file loader used by the shadow-call route. // An explicit dir (unit tests) wins; otherwise the same OPENCODEX_HOME then // ~/.opencodex resolution the TS parent uses at spawn. diff --git a/go/internal/sidecar/sidecar_test.go b/go/internal/sidecar/sidecar_test.go index 6a3553d148..370b87ed74 100644 --- a/go/internal/sidecar/sidecar_test.go +++ b/go/internal/sidecar/sidecar_test.go @@ -135,6 +135,54 @@ func TestConfigWriteRelayRejectsMissingTokenOrChangedBody(t *testing.T) { } } +func TestLabReadBridgeIsExactAndCapabilityProtected(t *testing.T) { + const requestToken = "parent-to-sidecar" + const bridgeToken = "sidecar-to-parent" + var calls int + bridge := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + calls++ + if r.Method != http.MethodGet || r.URL.Path != privateLabReadBridgePath { + t.Fatalf("bridge request = %s %s", r.Method, r.URL.String()) + } + if r.Header.Get(SidecarBridgeHeader) != bridgeToken { + t.Fatal("bridge token missing") + } + if r.Header.Get("X-Ocx-Go-Sidecar-Path") != "/api/lab/verdicts" { + t.Fatalf("path header = %q", r.Header.Get("X-Ocx-Go-Sidecar-Path")) + } + if r.URL.RawQuery != "limit=1" { + t.Fatalf("query = %q", r.URL.RawQuery) + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusServiceUnavailable) + _, _ = w.Write([]byte(`{"error":{"code":"lab_projection_unavailable"}}`)) + })) + defer bridge.Close() + h := NewHandler(Config{ParentURL: bridge.URL, BridgeToken: bridgeToken, RequestToken: requestToken}) + missing := do(t, h, http.MethodGet, "/api/lab/verdicts?limit=1") + if missing.StatusCode != http.StatusNotFound { + t.Fatalf("missing token status = %d, want 404", missing.StatusCode) + } + req := httptest.NewRequest(http.MethodGet, "/api/lab/verdicts?limit=1", nil) + req.Header.Set(SidecarRequestHeader, requestToken) + rec := httptest.NewRecorder() + h.ServeHTTP(rec, req) + want := `{"error":{"code":"lab_projection_unavailable"}}` + if rec.Code != http.StatusServiceUnavailable || rec.Body.String() != want { + t.Fatalf("relay = %d %s", rec.Code, rec.Body.String()) + } + if calls != 1 { + t.Fatalf("bridge calls = %d, want 1", calls) + } + unknown := httptest.NewRequest(http.MethodGet, "/api/lab/verdicts/extra", nil) + unknown.Header.Set(SidecarRequestHeader, requestToken) + unknownRec := httptest.NewRecorder() + h.ServeHTTP(unknownRec, unknown) + if unknownRec.Code != http.StatusNotFound || calls != 1 { + t.Fatalf("nonliteral route status/calls = %d/%d", unknownRec.Code, calls) + } +} + func TestHealthShape(t *testing.T) { startedAt := time.Now().Add(-123 * time.Second) h := NewHandler(Config{Service: "opencodex", Version: "2.42.0", StartedAt: startedAt}) diff --git a/src/server/index.ts b/src/server/index.ts index 55cd4c0d07..a39b030654 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -55,6 +55,7 @@ import { import { acquireServerBackgroundLifecycle } from "./background-lifecycle"; import { activateLab, labActivationRequired } from "../lib/lab-activation"; import { activateGoSidecar, forwardHotPathSeam, isDataPlaneSeamAttached } from "./go-sidecar"; +import { findGoOwnedManagementRoute } from "./management/route-registry"; import { createDataPlaneSeamHeaders, createHotPathResponsesBridge, @@ -1100,6 +1101,25 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server ({ method: "GET" as const, path, module: "server/management/lab-automation-routes", transition: "go-now" as const, stateSources: ["disk", "serving-process"] as const, parityFixture: "default-get" as const })), + ...["/api/lab/artifacts", "/api/lab/catalog", "/api/lab/events", "/api/lab/observations", "/api/lab/production-signals", "/api/lab/public/community", "/api/lab/status", "/api/lab/subjects", "/api/lab/verdicts"].map(path => ({ method: "GET" as const, path, module: "server/management/lab-routes", transition: "go-now" as const, stateSources: ["disk", "serving-process"] as const, parityFixture: "default-get" as const })), ...deferred([ { module: "codex/auth-api", stateSources: ["disk", "serving-process", "external-state"], rationale: flip, routes: [["GET", "/api/codex-auth/accounts"], ["GET", "/api/codex-auth/active"], ["GET", "/api/codex-auth/login-status"], ["GET", "/api/codex-auth/quota"], ["GET", "/api/codex-auth/reset-credits"]] }, { module: "codex/native-profile-api", stateSources: ["disk", "os", "serving-process"], rationale: flip, routes: [["GET", "/api/native-main-profiles"], ["GET", "/api/native-main-profiles/doctor"]] }, @@ -64,8 +66,7 @@ export const READ_SURFACE_DIFF_MATRIX: readonly ReadSurfaceDiffMatrixEntry[] = [ { module: "server/management/combo-routes", stateSources: ["disk", "serving-process"], rationale: flip, routes: [["GET", "/api/combos"]] }, { module: "server/management/config-routes", stateSources: ["disk", "os", "serving-process", "external-state"], rationale: flip, routes: [["GET", "/api/config"], ["GET", "/api/diagnostics/project-config"], ["GET", "/api/settings"], ["GET", "/api/sidecar-settings"], ["GET", "/api/startup-health"], ["GET", "/api/update/check"], ["GET", "/api/update/status"], ["GET", "/api/windows-tray"]] }, { module: "server/management/integration-routes", stateSources: ["disk", "serving-process", "external-state"], rationale: flip, routes: [["GET", "/api/client-integrations"], ["GET", "/api/client-integrations/journal"], ["GET", "/api/client-integrations/{clientId}"]] }, - { module: "server/management/lab-automation-routes", stateSources: ["disk", "serving-process"], rationale: lab, routes: [["GET", "/api/lab/automation"], ["GET", "/api/lab/automation/runs"]] }, - { module: "server/management/lab-routes", stateSources: ["disk", "serving-process"], rationale: lab, routes: [["GET", "/api/lab/artifacts"], ["GET", "/api/lab/catalog"], ["GET", "/api/lab/events"], ["GET", "/api/lab/observations"], ["GET", "/api/lab/production-signals"], ["GET", "/api/lab/public/community"], ["GET", "/api/lab/status"], ["GET", "/api/lab/subjects"], ["GET", "/api/lab/verdicts"], ["GET", "/api/lab/subjects/{id}"], ["GET", "/api/lab/events/{id}"], ["GET", "/api/lab/artifacts/{digest}"]] }, + { module: "server/management/lab-routes", stateSources: ["disk", "serving-process"], rationale: lab, routes: [["GET", "/api/lab/subjects/{id}"], ["GET", "/api/lab/events/{id}"], ["GET", "/api/lab/artifacts/{digest}"]] }, { module: "server/management/logs-usage-routes", stateSources: ["disk", "serving-process"], rationale: flip, routes: [["GET", "/api/claude/inbound-debug"], ["GET", "/api/debug"], ["GET", "/api/debug/injection-logs"], ["GET", "/api/debug/logs"], ["GET", "/api/debug/usage-logs"], ["GET", "/api/logs"], ["GET", "/api/storage/cleanup-policy"], ["GET", "/api/storage/cleanup-policy/test-stream"], ["GET", "/api/storage/trash"], ["GET", "/api/storage/trash/restore/test-stream"], ["GET", "/api/usage"]] }, { module: "server/management/model-routes", stateSources: ["disk", "serving-process", "external-state"], rationale: flip, routes: [["GET", "/api/aliases"], ["GET", "/api/catalog"], ["GET", "/api/client-config"], ["GET", "/api/model-presets"], ["GET", "/api/models"], ["GET", "/api/selected-models"]] }, { module: "server/management/native-integration-routes", stateSources: ["disk", "serving-process", "external-state"], rationale: flip, routes: [["GET", "/api/native-integrations"]] }, diff --git a/src/server/management/route-registry.ts b/src/server/management/route-registry.ts index 5cbf3bfee0..ab90fc1f0b 100644 --- a/src/server/management/route-registry.ts +++ b/src/server/management/route-registry.ts @@ -242,20 +242,20 @@ export const MANAGEMENT_ROUTES: readonly ManagementRoute[] = [ { method: "GET", path: "/api/client-integrations/journal", module: "server/management/integration-routes", mutates: false }, { method: "POST", path: "/api/client-integrations/restore", module: "server/management/integration-routes", mutates: true }, // server/management/lab-automation-routes - { method: "GET", path: "/api/lab/automation", module: "server/management/lab-automation-routes", mutates: false, exempt: { reason: "local-transport", why: "ocx lab reads the same rows from the local SQLite projection; src/cli/lab.ts imports ../lab/query directly and never fetches /api/lab." } }, - { method: "GET", path: "/api/lab/automation/runs", module: "server/management/lab-automation-routes", mutates: false, exempt: { reason: "local-transport", why: "ocx lab reads the same rows from the local SQLite projection; src/cli/lab.ts imports ../lab/query directly and never fetches /api/lab." } }, + { method: "GET", path: "/api/lab/automation", module: "server/management/lab-automation-routes", mutates: false, go: { volatileFields: [] } }, + { method: "GET", path: "/api/lab/automation/runs", module: "server/management/lab-automation-routes", mutates: false, go: { volatileFields: [] } }, { method: "POST", path: "/api/lab/automation/run", module: "server/management/lab-automation-routes", mutates: true, exempt: { reason: "deferred-verb", why: "Lab automation run has no CLI verb yet. A local SQLite read cannot drive it, so local-transport does not apply.", owner: "wp7", ownerDoc: "devlog/_plan/260828_ocx_agentic_control/060_phase_gui_parity.md" } }, { method: "PUT", path: "/api/lab/automation", module: "server/management/lab-automation-routes", mutates: true, exempt: { reason: "deferred-verb", why: "Lab automation config update has no CLI verb yet. A local SQLite read cannot drive it, so local-transport does not apply.", owner: "wp7", ownerDoc: "devlog/_plan/260828_ocx_agentic_control/060_phase_gui_parity.md" } }, // server/management/lab-routes - { method: "GET", path: "/api/lab/artifacts", module: "server/management/lab-routes", mutates: false, exempt: { reason: "local-transport", why: "ocx lab reads the same rows from the local SQLite projection; src/cli/lab.ts imports ../lab/query directly and never fetches /api/lab." } }, - { method: "GET", path: "/api/lab/catalog", module: "server/management/lab-routes", mutates: false, exempt: { reason: "local-transport", why: "ocx lab reads the same rows from the local SQLite projection; src/cli/lab.ts imports ../lab/query directly and never fetches /api/lab." } }, - { method: "GET", path: "/api/lab/events", module: "server/management/lab-routes", mutates: false, exempt: { reason: "local-transport", why: "ocx lab reads the same rows from the local SQLite projection; src/cli/lab.ts imports ../lab/query directly and never fetches /api/lab." } }, - { method: "GET", path: "/api/lab/observations", module: "server/management/lab-routes", mutates: false, exempt: { reason: "local-transport", why: "ocx lab reads the same rows from the local SQLite projection; src/cli/lab.ts imports ../lab/query directly and never fetches /api/lab." } }, - { method: "GET", path: "/api/lab/production-signals", module: "server/management/lab-routes", mutates: false, exempt: { reason: "local-transport", why: "ocx lab reads the same rows from the local SQLite projection; src/cli/lab.ts imports ../lab/query directly and never fetches /api/lab." } }, - { method: "GET", path: "/api/lab/public/community", module: "server/management/lab-routes", mutates: false, exempt: { reason: "local-transport", why: "ocx lab reads the same rows from the local SQLite projection; src/cli/lab.ts imports ../lab/query directly and never fetches /api/lab." } }, - { method: "GET", path: "/api/lab/status", module: "server/management/lab-routes", mutates: false, exempt: { reason: "local-transport", why: "ocx lab reads the same rows from the local SQLite projection; src/cli/lab.ts imports ../lab/query directly and never fetches /api/lab." } }, - { method: "GET", path: "/api/lab/subjects", module: "server/management/lab-routes", mutates: false, exempt: { reason: "local-transport", why: "ocx lab reads the same rows from the local SQLite projection; src/cli/lab.ts imports ../lab/query directly and never fetches /api/lab." } }, - { method: "GET", path: "/api/lab/verdicts", module: "server/management/lab-routes", mutates: false, exempt: { reason: "local-transport", why: "ocx lab reads the same rows from the local SQLite projection; src/cli/lab.ts imports ../lab/query directly and never fetches /api/lab." } }, + { method: "GET", path: "/api/lab/artifacts", module: "server/management/lab-routes", mutates: false, go: { volatileFields: [] } }, + { method: "GET", path: "/api/lab/catalog", module: "server/management/lab-routes", mutates: false, go: { volatileFields: [] } }, + { method: "GET", path: "/api/lab/events", module: "server/management/lab-routes", mutates: false, go: { volatileFields: [] } }, + { method: "GET", path: "/api/lab/observations", module: "server/management/lab-routes", mutates: false, go: { volatileFields: [] } }, + { method: "GET", path: "/api/lab/production-signals", module: "server/management/lab-routes", mutates: false, go: { volatileFields: [] } }, + { method: "GET", path: "/api/lab/public/community", module: "server/management/lab-routes", mutates: false, go: { volatileFields: [] } }, + { method: "GET", path: "/api/lab/status", module: "server/management/lab-routes", mutates: false, go: { volatileFields: [] } }, + { method: "GET", path: "/api/lab/subjects", module: "server/management/lab-routes", mutates: false, go: { volatileFields: [] } }, + { method: "GET", path: "/api/lab/verdicts", module: "server/management/lab-routes", mutates: false, go: { volatileFields: [] } }, { method: "POST", path: "/api/lab/public/community/import", module: "server/management/lab-routes", mutates: true, exempt: { reason: "deferred-verb", why: "Community evidence import has no CLI verb yet. A local SQLite read cannot drive it, so local-transport does not apply.", owner: "wp7", ownerDoc: "devlog/_plan/260828_ocx_agentic_control/060_phase_gui_parity.md" } }, { method: "POST", path: "/api/lab/public/export", module: "server/management/lab-routes", mutates: true, exempt: { reason: "deferred-verb", why: "Public evidence export has no CLI verb yet. A local SQLite read cannot drive it, so local-transport does not apply.", owner: "wp7", ownerDoc: "devlog/_plan/260828_ocx_agentic_control/060_phase_gui_parity.md" } }, { method: "POST", path: "/api/lab/public/preview", module: "server/management/lab-routes", mutates: true, exempt: { reason: "deferred-verb", why: "Public evidence preview has no CLI verb yet. A local SQLite read cannot drive it, so local-transport does not apply.", owner: "wp7", ownerDoc: "devlog/_plan/260828_ocx_agentic_control/060_phase_gui_parity.md" } }, diff --git a/tests/go-lab-routes-parity.test.ts b/tests/go-lab-routes-parity.test.ts new file mode 100644 index 0000000000..ad530e866b --- /dev/null +++ b/tests/go-lab-routes-parity.test.ts @@ -0,0 +1,130 @@ +import { afterEach, describe, expect, test } from "bun:test"; +import { existsSync, mkdtempSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { saveConfig } from "../src/config"; +import { startServer } from "../src/server"; +import { GO_OWNED_MANAGEMENT_ROUTES } from "../src/server/management/route-registry"; +import { GO_SIDECAR_BIN_ENV, activeGoSidecarBaseUrl, resetGoSidecarForTests } from "../src/server/go-sidecar"; +import { removeTreeWithRetry } from "./helpers/remove-tree"; +import { SERVER_BUDGET_MS } from "./helpers/test-budget"; + +/** + * Ticket #33 differential oracle. Lab activation is deliberately enabled in + * this fixture: its route handlers are dynamically loaded and the production + * SQLite projection remains the TypeScript parent oracle. Server B therefore + * proves the real Go public hop plus private bridge, rather than comparing two + * direct calls to the dynamic handler. Bodies are compared as raw bytes; all + * migrated Lab reads declare an empty volatile set. + */ +const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); +const previous = { + bin: process.env[GO_SIDECAR_BIN_ENV], + home: process.env.OPENCODEX_HOME, + data: process.env.OPENCODEX_API_AUTH_TOKEN, + admin: process.env.OPENCODEX_ADMIN_AUTH_TOKEN, +}; + +function goAvailable(): boolean { + return Bun.spawnSync(["go", "version"], { stdout: "ignore", stderr: "ignore" }).success; +} + +function buildSidecar(): string { + const dir = mkdtempSync(join(tmpdir(), "ocx-go-lab-routes-")); + const bin = join(dir, process.platform === "win32" ? "ocx-sidecar.exe" : "ocx-sidecar"); + const result = Bun.spawnSync(["go", "build", "-o", bin, "./cmd/ocx-sidecar"], { + cwd: join(repoRoot, "go"), env: { ...process.env, CGO_ENABLED: "0" }, stdout: "pipe", stderr: "pipe", + }); + if (result.exitCode !== 0) throw new Error(new TextDecoder().decode(result.stderr)); + return bin; +} + +const binary = goAvailable() ? buildSidecar() : null; +const literalLabReads = GO_OWNED_MANAGEMENT_ROUTES.filter(route => !route.mutates && route.path.startsWith("/api/lab/")); +const vectors = [ + "/api/lab/automation", + "/api/lab/automation/runs?limit=1", + "/api/lab/artifacts?limit=1", + "/api/lab/catalog?layer=protocol_conformance", + "/api/lab/events?limit=1", + "/api/lab/observations?limit=1", + "/api/lab/production-signals", + "/api/lab/public/community", + "/api/lab/status", + "/api/lab/subjects?limit=1", + "/api/lab/verdicts?limit=1", +] as const; + +async function waitForSidecar(): Promise { + const deadline = Date.now() + 15_000; + while (!activeGoSidecarBaseUrl()) { + if (Date.now() >= deadline) throw new Error("sidecar did not attach"); + await Bun.sleep(25); + } +} + +async function capture(server: { url: URL }, path: string): Promise<{ status: number; contentType: string | null; body: string }> { + const response = await fetch(new URL(path, server.url), { headers: { "x-opencodex-api-key": "admin-secret" } }); + return { status: response.status, contentType: response.headers.get("content-type"), body: await response.text() }; +} + +afterEach(() => { + resetGoSidecarForTests(); + for (const [key, value] of Object.entries({ + [GO_SIDECAR_BIN_ENV]: previous.bin, + OPENCODEX_HOME: previous.home, + OPENCODEX_API_AUTH_TOKEN: previous.data, + OPENCODEX_ADMIN_AUTH_TOKEN: previous.admin, + })) { + if (value === undefined) delete process.env[key]; else process.env[key] = value; + } +}); + +describe.skipIf(binary === null)("Go Lab route differential oracle (ticket #33)", () => { + test("every literal Lab read is Go-owned with a strict byte contract", () => { + expect(existsSync(binary!)).toBe(true); + expect(literalLabReads.map(route => route.path).sort()).toEqual([ + "/api/lab/artifacts", "/api/lab/automation", "/api/lab/automation/runs", "/api/lab/catalog", + "/api/lab/events", "/api/lab/observations", "/api/lab/production-signals", "/api/lab/public/community", + "/api/lab/status", "/api/lab/subjects", "/api/lab/verdicts", + ]); + expect(literalLabReads.every(route => route.go.volatileFields.length === 0)).toBe(true); + }); + + test("activated Lab read responses match TypeScript oracle byte-for-byte through Go", async () => { + const home = mkdtempSync(join(tmpdir(), "ocx-go-lab-routes-home-")); + process.env.OPENCODEX_HOME = home; + process.env.OPENCODEX_API_AUTH_TOKEN = "data-secret"; + process.env.OPENCODEX_ADMIN_AUTH_TOKEN = "admin-secret"; + // A valid non-empty profile is the production activation gate. The route + // tests intentionally use an unseeded projection too, covering its 503 + // contract without bypassing activation. + saveConfig({ + port: 0, + hostname: "0.0.0.0", + defaultProvider: "test", + providers: { test: { adapter: "openai-chat", baseUrl: "https://example.test/v1", models: ["gpt-test"] } }, + routingProfiles: { lab: { candidates: [{ provider: "test", model: "gpt-test" }] } }, + }); + const oracle = startServer(0); + try { + process.env[GO_SIDECAR_BIN_ENV] = binary!; + const go = startServer(0); + try { + await waitForSidecar(); + for (const path of vectors) { + const [ts, actual] = await Promise.all([capture(oracle, path), capture(go, path)]); + expect(actual.status, path + " status").toBe(ts.status); + expect(actual.contentType, path + " content-type").toBe(ts.contentType); + expect(actual.body, path + " raw body").toBe(ts.body); + } + } finally { + await go.stop(true); + } + } finally { + await oracle.stop(true); + removeTreeWithRetry(home); + } + }, SERVER_BUDGET_MS); +}); diff --git a/tests/management-route-registry.test.ts b/tests/management-route-registry.test.ts index d5ccab6efe..6d24314a54 100644 --- a/tests/management-route-registry.test.ts +++ b/tests/management-route-registry.test.ts @@ -212,6 +212,18 @@ describe("route exemptions stay honest", () => { .map(r => key(r.method, r.path)); expect(wrong).toEqual([]); }); + + test("literal Lab reads are Go-owned; parameterised reads stay exact-match deferred", () => { + const labReads = MANAGEMENT_ROUTES.filter(route => route.path.startsWith("/api/lab/") && !route.mutates); + const literal = labReads.filter(route => !route.mechanism); + const parameterised = labReads.filter(route => route.mechanism === "regex"); + expect(literal).toHaveLength(11); + expect(literal.every(route => route.go?.volatileFields.length === 0)).toBe(true); + expect(parameterised.map(route => route.path).sort()).toEqual([ + "/api/lab/artifacts/{digest}", "/api/lab/events/{id}", "/api/lab/subjects/{id}", + ]); + expect(parameterised.every(route => route.exempt?.reason === "local-transport")).toBe(true); + }); }); describe("the registry is inert data", () => { From e4f4f84c55c9d67a7562ca1d680e6ff9ebd7d460 Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Sun, 6 Sep 2026 20:36:59 +0800 Subject: [PATCH 029/165] =?UTF-8?q?feat(go):=20ticket=20#30=20=E2=80=94=20?= =?UTF-8?q?hot-path=20routing=20decisions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../037_hotpath_routing.md | 9 + go/cmd/ocx-sidecar/main.go | 2 + go/cmd/ocx-sidecar/routingcheck.go | 28 +++ go/internal/routing/hotpath/hotpath.go | 162 ++++++++++++++++++ go/internal/routing/hotpath/hotpath_test.go | 16 ++ tests/go-hotpath-routing-parity.test.ts | 38 ++++ 6 files changed, 255 insertions(+) create mode 100644 devlog/_fin/260906_go_hotpath_routing/037_hotpath_routing.md create mode 100644 go/cmd/ocx-sidecar/routingcheck.go create mode 100644 go/internal/routing/hotpath/hotpath.go create mode 100644 go/internal/routing/hotpath/hotpath_test.go create mode 100644 tests/go-hotpath-routing-parity.test.ts diff --git a/devlog/_fin/260906_go_hotpath_routing/037_hotpath_routing.md b/devlog/_fin/260906_go_hotpath_routing/037_hotpath_routing.md new file mode 100644 index 0000000000..4798b68f23 --- /dev/null +++ b/devlog/_fin/260906_go_hotpath_routing/037_hotpath_routing.md @@ -0,0 +1,9 @@ +# 037 — Go hot-path routing decisions (ticket #30) + +The Go increment adds a pure state-snapshot decision package for quota account selection, hard/soft cooldown admission, and API-key-pool 429 failover. The routingcheck sidecar subcommand exposes that engine to the Bun differential harness; it is not a live sidecar route. + +The differential vectors prove both issue acceptance points for identical supplied state: quota/account selection agrees, cooldown admission returns the same candidate or earliest unavailable time, and key failover selects the same next eligible key and Retry-After cooldown. + +The engine is intentionally not wired into hotpath_relay.go. Ticket #27's direct relay performs one upstream request and returns its first 429. TypeScript performs key rotation later in src/server/responses/core.ts through rotateProviderTransportOn429 after handleResponses owns the route and config persistence. A trial direct Go retry returned 200 where the TypeScript oracle returned 429, so it was reverted. + +A later flip needs a body-bound parent bridge claim carrying the selected route, volatile quota/cooldown snapshot, and retry/persistence authority, or a Go-owned equivalent store. Until then the TS front door remains the owner of credentials, live account state, and observable retry execution; the Go engine is the checked decision kernel for that bridge. diff --git a/go/cmd/ocx-sidecar/main.go b/go/cmd/ocx-sidecar/main.go index f6141a320b..bb2bfa0d7b 100644 --- a/go/cmd/ocx-sidecar/main.go +++ b/go/cmd/ocx-sidecar/main.go @@ -42,6 +42,8 @@ func run() error { return runAuthCheck() case "labcheck": return runLabCheck() + case "routingcheck": + return runRoutingCheck() } return fmt.Errorf("unknown subcommand %q", os.Args[1]) } diff --git a/go/cmd/ocx-sidecar/routingcheck.go b/go/cmd/ocx-sidecar/routingcheck.go new file mode 100644 index 0000000000..23ecd9eedf --- /dev/null +++ b/go/cmd/ocx-sidecar/routingcheck.go @@ -0,0 +1,28 @@ +package main + +import ( + "encoding/json" + "fmt" + "github.com/lidge-jun/opencodex/go/internal/routing/hotpath" + "os" +) + +func runRoutingCheck() error { + if len(os.Args) != 3 { + return fmt.Errorf("routingcheck requires one JSON array argument") + } + var inputs []hotpath.Input + if err := json.Unmarshal([]byte(os.Args[2]), &inputs); err != nil { + return fmt.Errorf("routingcheck: decode: %w", err) + } + out := make([]hotpath.Decision, len(inputs)) + for i, input := range inputs { + out[i] = hotpath.Decide(input) + } + raw, err := json.Marshal(out) + if err != nil { + return fmt.Errorf("routingcheck: encode: %w", err) + } + _, err = fmt.Println(string(raw)) + return err +} diff --git a/go/internal/routing/hotpath/hotpath.go b/go/internal/routing/hotpath/hotpath.go new file mode 100644 index 0000000000..54565059b9 --- /dev/null +++ b/go/internal/routing/hotpath/hotpath.go @@ -0,0 +1,162 @@ +package hotpath + +import ( + "net/http" + "sort" + "strconv" + "strings" +) + +const DefaultKeyCooldownMS int64 = 60000 +const MaxKeyCooldownMS int64 = 600000 + +type Account struct { + ID string `json:"id"` + Paused bool `json:"paused,omitempty"` + Usable bool `json:"usable"` + UsagePercent *float64 `json:"usagePercent,omitempty"` + CooldownUntilMS int64 `json:"cooldownUntilMs,omitempty"` + SoftAvoidUntilMS int64 `json:"softAvoidUntilMs,omitempty"` +} +type Key struct { + ID string `json:"id"` + CooldownUntilMS int64 `json:"cooldownUntilMs,omitempty"` +} +type Input struct { + NowMS int64 `json:"nowMs"` + Strategy string `json:"strategy,omitempty"` + ActiveAccountID string `json:"activeAccountId,omitempty"` + AutoSwitchThreshold *float64 `json:"autoSwitchThreshold,omitempty"` + Accounts []Account `json:"accounts,omitempty"` + Keys []Key `json:"keys,omitempty"` + FailedKeyID string `json:"failedKeyId,omitempty"` + Status int `json:"status,omitempty"` + RetryAfter string `json:"retryAfter,omitempty"` +} +type Decision struct { + AccountID string `json:"accountId,omitempty"` + CooldownUntilMS int64 `json:"cooldownUntilMs,omitempty"` + KeyID string `json:"keyId,omitempty"` +} + +func Decide(in Input) Decision { + d := SelectAccount(in.Accounts, in.ActiveAccountID, in.AutoSwitchThreshold, in.Strategy, in.NowMS) + if in.FailedKeyID != "" && in.Status == http.StatusTooManyRequests { + d.KeyID, d.CooldownUntilMS = FailoverKey(in.Keys, in.FailedKeyID, in.RetryAfter, in.NowMS) + } + return d +} +func SelectAccount(as []Account, active string, threshold *float64, strategy string, now int64) Decision { + es := []Account{} + var earliest int64 + for _, a := range as { + if a.ID == "" || a.Paused || !a.Usable { + continue + } + if a.CooldownUntilMS > now || a.SoftAvoidUntilMS > now { + u := a.CooldownUntilMS + if u <= now || (a.SoftAvoidUntilMS > now && a.SoftAvoidUntilMS < u) { + u = a.SoftAvoidUntilMS + } + if u > now && (earliest == 0 || u < earliest) { + earliest = u + } + continue + } + es = append(es, a) + } + if len(es) == 0 { + return Decision{CooldownUntilMS: earliest} + } + limit := 80.0 + if threshold != nil { + limit = *threshold + } + if strategy == "round-robin" { + return Decision{AccountID: es[0].ID} + } + if strategy == "fill-first" { + return Decision{AccountID: fill(es, active, limit).ID} + } + for _, a := range es { + if a.ID == active && (a.UsagePercent == nil || limit <= 0 || *a.UsagePercent < limit) { + return Decision{AccountID: a.ID} + } + } + best := es[0] + for _, a := range es[1:] { + if usage(a) < usage(best) { + best = a + } + } + return Decision{AccountID: best.ID} +} +func fill(es []Account, active string, limit float64) Account { + xs := append([]Account(nil), es...) + sort.SliceStable(xs, func(i, j int) bool { return xs[i].ID < xs[j].ID }) + for _, a := range xs { + if a.ID == active && (a.UsagePercent == nil || limit <= 0 || *a.UsagePercent < limit) { + return a + } + } + for _, a := range xs { + if a.UsagePercent == nil || limit <= 0 || *a.UsagePercent < limit { + return a + } + } + return xs[0] +} +func usage(a Account) float64 { + if a.UsagePercent == nil { + return 101 + } + if *a.UsagePercent < 0 { + return 0 + } + if *a.UsagePercent > 100 { + return 100 + } + return *a.UsagePercent +} +func FailoverKey(keys []Key, failed, retry string, now int64) (string, int64) { + until := now + retryMS(retry, now) + start := -1 + for i := range keys { + if keys[i].ID == failed { + keys[i].CooldownUntilMS = until + start = i + break + } + } + for n := 1; n <= len(keys); n++ { + k := keys[(start+n+len(keys))%len(keys)] + if k.ID != "" && k.CooldownUntilMS <= now { + return k.ID, until + } + } + return "", until +} +func retryMS(v string, now int64) int64 { + s := strings.TrimSpace(v) + if n, e := strconv.ParseFloat(s, 64); e == nil && n >= 0 { + d := int64(n*1000 + .999999) + if d < 1 { + d = 1 + } + if d > MaxKeyCooldownMS { + return MaxKeyCooldownMS + } + return d + } + if t, e := http.ParseTime(s); e == nil { + d := t.UnixMilli() - now + if d < 1 { + d = 1 + } + if d > MaxKeyCooldownMS { + return MaxKeyCooldownMS + } + return d + } + return DefaultKeyCooldownMS +} diff --git a/go/internal/routing/hotpath/hotpath_test.go b/go/internal/routing/hotpath/hotpath_test.go new file mode 100644 index 0000000000..c11eb29b4e --- /dev/null +++ b/go/internal/routing/hotpath/hotpath_test.go @@ -0,0 +1,16 @@ +package hotpath + +import "testing" + +func num(v float64) *float64 { return &v } +func TestDecideQuotaCooldownAndFailover(t *testing.T) { + now := int64(1000) + d := Decide(Input{NowMS: now, ActiveAccountID: "a", Accounts: []Account{{ID: "a", Usable: true, UsagePercent: num(90)}, {ID: "b", Usable: true, UsagePercent: num(10)}, {ID: "c", Usable: true, UsagePercent: num(1), CooldownUntilMS: now + 5}}}) + if d.AccountID != "b" { + t.Fatal(d) + } + d = Decide(Input{NowMS: now, Keys: []Key{{ID: "a"}, {ID: "b"}}, FailedKeyID: "a", Status: 429, RetryAfter: "7"}) + if d.KeyID != "b" || d.CooldownUntilMS != 8000 { + t.Fatal(d) + } +} diff --git a/tests/go-hotpath-routing-parity.test.ts b/tests/go-hotpath-routing-parity.test.ts new file mode 100644 index 0000000000..2ab5cc4445 --- /dev/null +++ b/tests/go-hotpath-routing-parity.test.ts @@ -0,0 +1,38 @@ +import { describe, expect, test } from "bun:test"; +import { mkdtempSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const root = resolve(dirname(fileURLToPath(import.meta.url)), ".."); +const hasGo = Bun.spawnSync(["go", "version"], { stdout: "ignore", stderr: "ignore" }).success; +const binary = hasGo ? (() => { + const path = join(mkdtempSync(join(tmpdir(), "ocx-go-routing-")), "ocx-sidecar"); + const result = Bun.spawnSync(["go", "build", "-o", path, "./cmd/ocx-sidecar"], { cwd: join(root, "go"), env: { ...process.env, CGO_ENABLED: "0" }, stdout: "pipe", stderr: "pipe" }); + if (result.exitCode !== 0) throw new Error(new TextDecoder().decode(result.stderr)); + return path; +})() : null; + +function go(vectors: Record[]) { + const result = Bun.spawnSync([binary!, "routingcheck", JSON.stringify(vectors)], { stdout: "pipe", stderr: "pipe" }); + if (result.exitCode !== 0) throw new Error(new TextDecoder().decode(result.stderr)); + return JSON.parse(new TextDecoder().decode(result.stdout)); +} + +describe.skipIf(!hasGo || !binary)("Go hot-path routing differential (ticket #30)", () => { + test("quota/account, cooldown admission, and key failover decisions match state vectors", () => { + // Values are worked from the TS routing rules in codex/routing.ts and + // providers/key-failover.ts, then compared to the Go executable oracle. + expect(go([ + { nowMs: 1000, activeAccountId: "a", accounts: [{ id: "a", usable: true, usagePercent: 90 }, { id: "b", usable: true, usagePercent: 10 }, { id: "c", usable: true, usagePercent: 1, cooldownUntilMs: 2000 }] }, + { nowMs: 1000, strategy: "fill-first", activeAccountId: "b", accounts: [{ id: "b", usable: true, usagePercent: 90 }, { id: "a", usable: true, usagePercent: 10 }] }, + { nowMs: 1000, accounts: [{ id: "a", usable: true, cooldownUntilMs: 5000 }, { id: "b", usable: true, softAvoidUntilMs: 3000 }] }, + { nowMs: 1000, keys: [{ id: "a" }, { id: "b" }, { id: "c", cooldownUntilMs: 9000 }], failedKeyId: "a", status: 429, retryAfter: "7" }, + ])).toEqual([ + { accountId: "b" }, + { accountId: "a" }, + { cooldownUntilMs: 3000 }, + { cooldownUntilMs: 8000, keyId: "b" }, + ]); + }); +}); From e5b5ee3259f6b9ec281c0e9f2987281d54bed4b3 Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Sun, 6 Sep 2026 20:38:35 +0800 Subject: [PATCH 030/165] docs(go): relocate #30 design record to the takeover plan series (040) --- .../260905_go_sidecar_takeover/040_hotpath_routing.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename devlog/{_fin/260906_go_hotpath_routing/037_hotpath_routing.md => _plan/260905_go_sidecar_takeover/040_hotpath_routing.md} (100%) diff --git a/devlog/_fin/260906_go_hotpath_routing/037_hotpath_routing.md b/devlog/_plan/260905_go_sidecar_takeover/040_hotpath_routing.md similarity index 100% rename from devlog/_fin/260906_go_hotpath_routing/037_hotpath_routing.md rename to devlog/_plan/260905_go_sidecar_takeover/040_hotpath_routing.md From d021a1cd35b97187421c9731d4c72c83df711b67 Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Sun, 6 Sep 2026 20:52:29 +0800 Subject: [PATCH 031/165] test(go): make ticket #30 routing oracle differential --- .../040_hotpath_routing.md | 12 ++-- go/internal/routing/hotpath/hotpath.go | 48 +++++++------- go/internal/routing/hotpath/hotpath_test.go | 8 +++ tests/go-hotpath-routing-parity.test.ts | 63 +++++++++++-------- 4 files changed, 78 insertions(+), 53 deletions(-) diff --git a/devlog/_plan/260905_go_sidecar_takeover/040_hotpath_routing.md b/devlog/_plan/260905_go_sidecar_takeover/040_hotpath_routing.md index 4798b68f23..7084c07409 100644 --- a/devlog/_plan/260905_go_sidecar_takeover/040_hotpath_routing.md +++ b/devlog/_plan/260905_go_sidecar_takeover/040_hotpath_routing.md @@ -1,9 +1,11 @@ -# 037 — Go hot-path routing decisions (ticket #30) +# 040 — Go hot-path routing decisions (ticket #30) -The Go increment adds a pure state-snapshot decision package for quota account selection, hard/soft cooldown admission, and API-key-pool 429 failover. The routingcheck sidecar subcommand exposes that engine to the Bun differential harness; it is not a live sidecar route. +The new Go routing package is a pure state-snapshot kernel. The `routingcheck` sidecar subcommand runs it only for Bun differential tests; it is not a live sidecar route and the relay remains unchanged. -The differential vectors prove both issue acceptance points for identical supplied state: quota/account selection agrees, cooldown admission returns the same candidate or earliest unavailable time, and key failover selects the same next eligible key and Retry-After cooldown. +The differential imports TypeScript as the oracle. It creates an isolated home, writes account credentials with `saveCodexAccountCredential`, seeds quota with `updateAccountQuota`, and clears quota, health, thread, rotation, and key-cooldown state around each vector. It calls `resolveCodexAccountForThread` for quota selection and a real 429 cooldown created by `recordCodexUpstreamOutcome`; key vectors call `rotateKeyOn429` and `getKeyCooldownUntil`. Each TypeScript decision is compared with JSON from the Go executable. -The engine is intentionally not wired into hotpath_relay.go. Ticket #27's direct relay performs one upstream request and returns its first 429. TypeScript performs key rotation later in src/server/responses/core.ts through rotateProviderTransportOn429 after handleResponses owns the route and config persistence. A trial direct Go retry returned 200 where the TypeScript oracle returned 429, so it was reverted. +Covered matrix: quota strategy active threshold rotation to lowest known usage; hard quota cooldown exclusion where another account is eligible; and key-pool 429 ring selection plus numeric Retry-After cooldown. The Go parser rejects non-TypeScript numeric forms such as `1e3`, `+5`, and `0x10`. -A later flip needs a body-bound parent bridge claim carrying the selected route, volatile quota/cooldown snapshot, and retry/persistence authority, or a Go-owned equivalent store. Until then the TS front door remains the owner of credentials, live account state, and observable retry execution; the Go engine is the checked decision kernel for that bridge. +Not yet covered or claimed: round-robin smooth-weight/sticky-success state, fill-first runtime cursor, unknown and plan-window quota scoring, all-unavailable sentinel behavior, priorities, affinity, reauth, scoped cooldowns, and soft avoid. A later parent-authorized state bridge must carry these state snapshots and successors before those paths can flip. + +The engine is intentionally not wired into the direct relay. The relay returns its first upstream 429, while TypeScript executes `rotateProviderTransportOn429` later in `handleResponses`, after owning route and config persistence. A Go retry returned 200 where the TypeScript oracle returned 429, so that wiring was reverted. diff --git a/go/internal/routing/hotpath/hotpath.go b/go/internal/routing/hotpath/hotpath.go index 54565059b9..d3112f2334 100644 --- a/go/internal/routing/hotpath/hotpath.go +++ b/go/internal/routing/hotpath/hotpath.go @@ -2,7 +2,6 @@ package hotpath import ( "net/http" - "sort" "strconv" "strings" ) @@ -72,11 +71,12 @@ func SelectAccount(as []Account, active string, threshold *float64, strategy str if threshold != nil { limit = *threshold } - if strategy == "round-robin" { - return Decision{AccountID: es[0].ID} - } - if strategy == "fill-first" { - return Decision{AccountID: fill(es, active, limit).ID} + // Rotation strategies are not claimed by this seam yet: their TS source of + // truth includes mutable smooth-weight and sticky-success state. Returning + // no account is deliberate; callers must retain TS ownership until that + // state is carried in a parent-authorized snapshot. + if strategy == "round-robin" || strategy == "fill-first" { + return Decision{} } for _, a := range es { if a.ID == active && (a.UsagePercent == nil || limit <= 0 || *a.UsagePercent < limit) { @@ -91,21 +91,6 @@ func SelectAccount(as []Account, active string, threshold *float64, strategy str } return Decision{AccountID: best.ID} } -func fill(es []Account, active string, limit float64) Account { - xs := append([]Account(nil), es...) - sort.SliceStable(xs, func(i, j int) bool { return xs[i].ID < xs[j].ID }) - for _, a := range xs { - if a.ID == active && (a.UsagePercent == nil || limit <= 0 || *a.UsagePercent < limit) { - return a - } - } - for _, a := range xs { - if a.UsagePercent == nil || limit <= 0 || *a.UsagePercent < limit { - return a - } - } - return xs[0] -} func usage(a Account) float64 { if a.UsagePercent == nil { return 101 @@ -138,7 +123,8 @@ func FailoverKey(keys []Key, failed, retry string, now int64) (string, int64) { } func retryMS(v string, now int64) int64 { s := strings.TrimSpace(v) - if n, e := strconv.ParseFloat(s, 64); e == nil && n >= 0 { + if numericRetryAfter(s) { + n, _ := strconv.ParseFloat(s, 64) d := int64(n*1000 + .999999) if d < 1 { d = 1 @@ -160,3 +146,21 @@ func retryMS(v string, now int64) int64 { } return DefaultKeyCooldownMS } + +func numericRetryAfter(value string) bool { + if value == "" { + return false + } + dot := false + for index, r := range value { + if r >= '0' && r <= '9' { + continue + } + if r == '.' && !dot && index > 0 && index < len(value)-1 { + dot = true + continue + } + return false + } + return true +} diff --git a/go/internal/routing/hotpath/hotpath_test.go b/go/internal/routing/hotpath/hotpath_test.go index c11eb29b4e..9226fdac46 100644 --- a/go/internal/routing/hotpath/hotpath_test.go +++ b/go/internal/routing/hotpath/hotpath_test.go @@ -14,3 +14,11 @@ func TestDecideQuotaCooldownAndFailover(t *testing.T) { t.Fatal(d) } } + +func TestRetryAfterRejectsNonTypeScriptNumericForms(t *testing.T) { + for _, value := range []string{"1e3", "+5", "0x10"} { + if got := retryMS(value, 1_000); got != DefaultKeyCooldownMS { + t.Fatalf("%q = %d, want default", value, got) + } + } +} diff --git a/tests/go-hotpath-routing-parity.test.ts b/tests/go-hotpath-routing-parity.test.ts index 2ab5cc4445..9b7458ccc7 100644 --- a/tests/go-hotpath-routing-parity.test.ts +++ b/tests/go-hotpath-routing-parity.test.ts @@ -1,38 +1,49 @@ -import { describe, expect, test } from "bun:test"; +import { afterEach, beforeEach, describe, expect, test } from "bun:test"; import { mkdtempSync } from "node:fs"; import { tmpdir } from "node:os"; import { dirname, join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; +import { saveCodexAccountCredential } from "../src/codex/account-store"; +import { clearAccountQuota, updateAccountQuota } from "../src/codex/auth-api"; +import { clearPoolRotationState } from "../src/codex/pool-rotation"; +import { clearCodexUpstreamHealth, clearThreadAccountMap, recordCodexUpstreamOutcome, resolveCodexAccountForThread } from "../src/codex/routing"; +import { clearKeyCooldowns, getKeyCooldownUntil, rotateKeyOn429 } from "../src/providers/key-failover"; +import type { OcxConfig } from "../src/types"; const root = resolve(dirname(fileURLToPath(import.meta.url)), ".."); const hasGo = Bun.spawnSync(["go", "version"], { stdout: "ignore", stderr: "ignore" }).success; -const binary = hasGo ? (() => { - const path = join(mkdtempSync(join(tmpdir(), "ocx-go-routing-")), "ocx-sidecar"); - const result = Bun.spawnSync(["go", "build", "-o", path, "./cmd/ocx-sidecar"], { cwd: join(root, "go"), env: { ...process.env, CGO_ENABLED: "0" }, stdout: "pipe", stderr: "pipe" }); - if (result.exitCode !== 0) throw new Error(new TextDecoder().decode(result.stderr)); - return path; -})() : null; +const binary = hasGo ? (() => { const p = join(mkdtempSync(join(tmpdir(), "ocx-go-routing-")), "ocx-sidecar"); const r = Bun.spawnSync(["go", "build", "-o", p, "./cmd/ocx-sidecar"], { cwd: join(root, "go"), env: { ...process.env, CGO_ENABLED: "0" }, stdout: "pipe", stderr: "pipe" }); if (r.exitCode !== 0) throw new Error(new TextDecoder().decode(r.stderr)); return p; })() : null; +let home = ""; -function go(vectors: Record[]) { - const result = Bun.spawnSync([binary!, "routingcheck", JSON.stringify(vectors)], { stdout: "pipe", stderr: "pipe" }); - if (result.exitCode !== 0) throw new Error(new TextDecoder().decode(result.stderr)); - return JSON.parse(new TextDecoder().decode(result.stdout)); -} +function config(strategy = "quota", active = "a", threshold = 80): OcxConfig { return { providers: {}, accountPoolStrategy: strategy as OcxConfig["accountPoolStrategy"], activeCodexAccountId: active, autoSwitchThreshold: threshold, codexAccounts: ["a", "b", "c"].map(id => ({ id, email: id + "@example.test", isMain: false })) } as OcxConfig; } +function credential(id: string) { saveCodexAccountCredential(id, { accessToken: "access-" + id, refreshToken: "refresh-" + id, expiresAt: Date.now() + 60_000, chatgptAccountId: "acct-" + id }); } +function go(vector: unknown) { const r = Bun.spawnSync([binary!, "routingcheck", JSON.stringify([vector])], { stdout: "pipe", stderr: "pipe" }); if (r.exitCode !== 0) throw new Error(new TextDecoder().decode(r.stderr)); return JSON.parse(new TextDecoder().decode(r.stdout))[0]; } + +beforeEach(() => { home = mkdtempSync(join(tmpdir(), "ocx-go-routing-state-")); process.env.OPENCODEX_HOME = home; process.env.CODEX_HOME = home; clearAccountQuota(); clearCodexUpstreamHealth(); clearThreadAccountMap(); clearPoolRotationState(); clearKeyCooldowns(); ["a", "b", "c"].forEach(credential); }); +afterEach(() => { delete process.env.OPENCODEX_HOME; delete process.env.CODEX_HOME; clearAccountQuota(); clearCodexUpstreamHealth(); clearThreadAccountMap(); clearPoolRotationState(); clearKeyCooldowns(); }); describe.skipIf(!hasGo || !binary)("Go hot-path routing differential (ticket #30)", () => { - test("quota/account, cooldown admission, and key failover decisions match state vectors", () => { - // Values are worked from the TS routing rules in codex/routing.ts and - // providers/key-failover.ts, then compared to the Go executable oracle. - expect(go([ - { nowMs: 1000, activeAccountId: "a", accounts: [{ id: "a", usable: true, usagePercent: 90 }, { id: "b", usable: true, usagePercent: 10 }, { id: "c", usable: true, usagePercent: 1, cooldownUntilMs: 2000 }] }, - { nowMs: 1000, strategy: "fill-first", activeAccountId: "b", accounts: [{ id: "b", usable: true, usagePercent: 90 }, { id: "a", usable: true, usagePercent: 10 }] }, - { nowMs: 1000, accounts: [{ id: "a", usable: true, cooldownUntilMs: 5000 }, { id: "b", usable: true, softAvoidUntilMs: 3000 }] }, - { nowMs: 1000, keys: [{ id: "a" }, { id: "b" }, { id: "c", cooldownUntilMs: 9000 }], failedKeyId: "a", status: 429, retryAfter: "7" }, - ])).toEqual([ - { accountId: "b" }, - { accountId: "a" }, - { cooldownUntilMs: 3000 }, - { cooldownUntilMs: 8000, keyId: "b" }, - ]); + test("quota account selection uses resolveCodexAccountForThread as its oracle", () => { + const cfg = config(); updateAccountQuota("a", 90); updateAccountQuota("b", 10); updateAccountQuota("c", 30); + const ts = resolveCodexAccountForThread(null, cfg); + const decision = go({ nowMs: Date.now(), activeAccountId: "a", autoSwitchThreshold: 80, accounts: [{ id: "a", usable: true, usagePercent: 90 }, { id: "b", usable: true, usagePercent: 10 }, { id: "c", usable: true, usagePercent: 30 }] }); + expect(decision).toEqual({ accountId: ts }); + }); + + test("cooldown admission excludes the same account as resolveCodexAccountForThread", () => { + const now = Date.now(); const cfg = config(); updateAccountQuota("a", 10); updateAccountQuota("b", 20); updateAccountQuota("c", 30); + recordCodexUpstreamOutcome(cfg, "a", 429, { now, retryAfter: "60" }); + const ts = resolveCodexAccountForThread(null, cfg); + const decision = go({ nowMs: now, activeAccountId: "a", accounts: [{ id: "a", usable: true, usagePercent: 10, cooldownUntilMs: now + 60_000 }, { id: "b", usable: true, usagePercent: 20 }, { id: "c", usable: true, usagePercent: 30 }] }); + expect(decision).toEqual({ accountId: ts }); + }); + + test("key failover uses rotateKeyOn429 as its oracle", () => { + const now = 1_000_000; const keys = [{ id: "k1", key: "one", addedAt: 1 }, { id: "k2", key: "two", addedAt: 2 }, { id: "k3", key: "three", addedAt: 3 }]; + const cfg = { defaultProvider: "p", providers: { p: { adapter: "openai-chat", baseUrl: "https://api.example.test/v1", apiKey: "one", apiKeyPool: keys } } } as unknown as OcxConfig; + const rotated = rotateKeyOn429(cfg, "p", "7", now, "one"); + const ts = { keyId: rotated?.apiKey === "two" ? "k2" : rotated?.apiKey === "three" ? "k3" : undefined, cooldownUntilMs: getKeyCooldownUntil("p", "k1", now) ?? undefined }; + const decision = go({ nowMs: now, keys: [{ id: "k1" }, { id: "k2" }, { id: "k3" }], failedKeyId: "k1", status: 429, retryAfter: "7" }); + expect(decision).toEqual(ts); }); }); From 7da513198bd6d7cd74f08a7b397e8ec98be91722 Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Sun, 6 Sep 2026 20:55:41 +0800 Subject: [PATCH 032/165] test(go): add CLI parity harness --- .github/workflows/ci.yml | 4 +- devlog/_fin/036_cli_parity_harness.md | 24 ++++ go/internal/ocxcli/cli.go | 151 +++++++++++++++++++------- go/internal/ocxcli/cli_test.go | 4 +- go/internal/ocxcli/help.go | 5 + tests/go-cli-parity.test.ts | 90 +++++++++++---- 6 files changed, 216 insertions(+), 62 deletions(-) create mode 100644 devlog/_fin/036_cli_parity_harness.md create mode 100644 go/internal/ocxcli/help.go diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cee9d50b49..bc4fd16edb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -450,8 +450,8 @@ jobs: GOOS="$os" GOARCH="$arch" CGO_ENABLED=0 go build -o "/tmp/ocx-sidecar-$os-$arch" ./cmd/ocx-sidecar done - - name: Differential oracle - run: bun test --timeout 60000 tests/go-sidecar-parity.test.ts + - name: Differential oracles + run: bun test --timeout 60000 tests/go-sidecar-parity.test.ts tests/go-cli-parity.test.ts # Everything that is not the suite: type safety, privacy, lint, build, smoke. # One runner, once per push. Splitting these across the shards would repeat a diff --git a/devlog/_fin/036_cli_parity_harness.md b/devlog/_fin/036_cli_parity_harness.md new file mode 100644 index 0000000000..b420bd47d7 --- /dev/null +++ b/devlog/_fin/036_cli_parity_harness.md @@ -0,0 +1,24 @@ +# Issue #36 — CLI parity harness + +Date: 2026-09-06 +Status: DONE + +The ADR-0008 Go migration now has a subprocess differential harness in +`tests/go-cli-parity.test.ts`. It builds `go/cmd/ocx` with `CGO_ENABLED=0`, +runs the TypeScript and Go CLIs with the same argv and isolated +`OPENCODEX_HOME`, and compares stdout, stderr, and exit status byte-for-byte +for Go-owned version aliases and ready usage failures. The table also names +`status` and `config show` as TS-only until their Go implementations exist, so +the matrix records the incomplete surface rather than treating it as parity. + +The harness is part of the Go CI job's differential-oracle step alongside the +existing sidecar oracle. It skips only when a local developer has no Go +toolchain; CI installs Go before executing it. + +The Go CLI was aligned with the TypeScript contract for full help, command help, +unknown-command output/exit status, health output, and ready parser status +messages. `skills/ocx` is generated solely from the TypeScript capability +registry and has no Go-specific surface, so no generated skill file changed. + +Verification: `go test ./...`; `bun test tests/go-cli-parity.test.ts`; +`bun run typecheck`; `bun run test`. diff --git a/go/internal/ocxcli/cli.go b/go/internal/ocxcli/cli.go index 6136b050db..35015a44b8 100644 --- a/go/internal/ocxcli/cli.go +++ b/go/internal/ocxcli/cli.go @@ -90,10 +90,20 @@ func defaults(d Deps) Deps { // Run dispatches a parsed argv and returns a POSIX-style process code. func Run(args []string, deps Deps) int { deps = defaults(deps) - if len(args) == 0 || args[0] == "help" || args[0] == "--help" || args[0] == "-h" { + if len(args) == 0 || args[0] == "--help" || args[0] == "-h" { printHelp(deps.Stdout) return ExitOK } + if args[0] == "help" { + if len(args) > 1 { + return printSubcommandHelp(args[1], deps) + } + printHelp(deps.Stdout) + return ExitOK + } + if hasHelpFlag(args[1:]) { + return printSubcommandHelp(args[0], deps) + } switch args[0] { case "--version", "-v", "version": fmt.Fprintf(deps.Stdout, "opencodex %s\n", deps.Version) @@ -104,68 +114,130 @@ func Run(args []string, deps Deps) int { return runReady(args[1:], deps) default: fmt.Fprintf(deps.Stderr, "Unknown command: %s\n", args[0]) - printHelp(deps.Stderr) - return ExitUsage + printHelp(deps.Stdout) + return ExitFailure } } - -func printHelp(w io.Writer) { - fmt.Fprintln(w, "Usage: ocx \n\nCommands:") - for _, c := range Commands { - fmt.Fprintf(w, " %-24s %s\n", c.Usage, c.Summary) - } - fmt.Fprintln(w, " ocx --version | -v Print version") +func printHelp(w io.Writer) { fmt.Fprint(w, fullUsage) } +func hasHelpFlag(args []string) bool { + for _, arg := range args { + if arg == "--help" || arg == "-h" || arg == "help" { + return true + } + } + return false } -func parseJSON(args []string) (bool, bool) { - if len(args) == 0 { - return false, true +func printSubcommandHelp(name string, deps Deps) int { + switch name { + case "health": + fmt.Fprint(deps.Stdout, "Usage: ocx health [--json]\n\nCheck proxy health. Exits 0 if healthy, 1 otherwise.\n\nUse --json for structured output: {ok, pid, port}.\n") + case "ready": + fmt.Fprint(deps.Stdout, "Usage: ocx ready [--json] [--wait [--timeout ]]\n\nCheck post-sync readiness. Exits 0 only when ready.\n\nExact unauthenticated GET /readyz returns HTTP 200 when ready, or 503 with Retry-After: 1 for pending or failed.\nIts sanitized HTTP identity is {service, version, uptime, pid, port, status}; /healthz is separate liveness, not readiness.\nDefault is a single identity-checked /readyz probe; old proxies without /readyz fail closed as unreachable.\n--wait polls until ready or timeout, but exits immediately on terminal failed (default 45s, max 300s).\n--timeout requires --wait and accepts a positive integer (1..300).\n--json emits {ready, status, pid, port}; status is one of ready|pending|failed|unreachable.\nInvalid or unknown arguments exit 64. Not-ready, pending, failed, timeout, and unreachable exit 1.\n") + default: + fmt.Fprintf(deps.Stderr, "Unknown command: %s\n", name) + printHelp(deps.Stdout) + return ExitFailure } - return len(args) == 1 && args[0] == "--json", len(args) == 1 && args[0] == "--json" + return ExitOK } - func runHealth(args []string, deps Deps) int { - jsonOutput, ok := parseJSON(args) - if !ok { - fmt.Fprintln(deps.Stderr, "Usage: ocx health [--json]") - return ExitUsage + jsonOutput := false + for _, arg := range args { + if arg == "--json" { + jsonOutput = true + } } - health, raw, err := ProbeHealth(deps) + health, _, err := ProbeHealth(deps) if err != nil { - fmt.Fprintf(deps.Stderr, "Proxy health check failed: %v\n", err) + if jsonOutput { + fmt.Fprintln(deps.Stdout, "{\"ok\":false,\"pid\":null,\"port\":null}") + } else { + fmt.Fprintln(deps.Stdout, "Proxy not healthy") + } return ExitFailure } if jsonOutput { - fmt.Fprintln(deps.Stdout, string(raw)) + fmt.Fprintf(deps.Stdout, "{\"ok\":true,\"pid\":%d,\"port\":%d}\n", health.PID, health.Port) } else { - fmt.Fprintf(deps.Stdout, "Proxy healthy (PID %d, port %d, version %s)\n", health.PID, health.Port, health.Version) + fmt.Fprintf(deps.Stdout, "Proxy healthy (PID %d, port %d)\n", health.PID, health.Port) } return ExitOK } + +type readyArgs struct { + json, wait, hasTimeout bool + timeout time.Duration +} + +func parseReady(args []string) (readyArgs, bool) { + parsed := readyArgs{timeout: 45 * time.Second} + for i := 0; i < len(args); i++ { + switch args[i] { + case "--json": + parsed.json = true + case "--wait": + parsed.wait = true + case "--timeout": + if i+1 >= len(args) { + return readyArgs{}, false + } + seconds, err := strconv.Atoi(args[i+1]) + if err != nil || seconds < 1 || seconds > 300 { + return readyArgs{}, false + } + parsed.timeout = time.Duration(seconds) * time.Second + parsed.hasTimeout = true + i++ + default: + return readyArgs{}, false + } + } + if parsed.hasTimeout && !parsed.wait { + return readyArgs{}, false + } + return parsed, true +} func runReady(args []string, deps Deps) int { - jsonOutput, ok := parseJSON(args) + parsed, ok := parseReady(args) if !ok { - fmt.Fprintln(deps.Stderr, "Usage: ocx ready [--json]") + fmt.Fprintln(deps.Stderr, "Usage: ocx ready [--json] [--wait [--timeout ]]") + fmt.Fprintln(deps.Stderr, " --timeout requires --wait; must be a positive integer (1..300).") + fmt.Fprintln(deps.Stderr, " Default wait timeout is 45 seconds.") return ExitUsage } - health, _, err := ProbeHealth(deps) - if err != nil { - return reportReady(deps, jsonOutput, false, "unreachable", 0, 0) - } - state, err := deps.ReadRuntime() - if err != nil { - return reportReady(deps, jsonOutput, false, "unreachable", health.PID, health.Port) + deadline := time.Now().Add(parsed.timeout) + for { + _, _, err := ProbeHealth(deps) + if err != nil { + return reportReady(deps, parsed.json, false, "unreachable", 0, 0) + } + state, err := deps.ReadRuntime() + if err != nil { + return reportReady(deps, parsed.json, false, "unreachable", 0, 0) + } + ready, err := ProbeReady(state, deps.HTTPClient) + if err != nil { + return reportReady(deps, parsed.json, false, "unreachable", 0, 0) + } + if ready.Status != "pending" || !parsed.wait || time.Now().Add(500*time.Millisecond).After(deadline) { + return reportReady(deps, parsed.json, ready.Status == "ready", ready.Status, ready.PID, ready.Port) + } + time.Sleep(500 * time.Millisecond) } - ready, err := ProbeReady(state, deps.HTTPClient) - if err != nil { - return reportReady(deps, jsonOutput, false, "unreachable", health.PID, health.Port) - } - return reportReady(deps, jsonOutput, ready.Status == "ready", ready.Status, ready.PID, ready.Port) } func reportReady(deps Deps, jsonOutput bool, isReady bool, status string, pid int64, port int) int { if jsonOutput { - fmt.Fprintf(deps.Stdout, "{\"ready\":%t,\"status\":%q,\"pid\":%d,\"port\":%d}\n", isReady, status, pid, port) + if status == "unreachable" { + fmt.Fprintln(deps.Stdout, "{\"ready\":false,\"status\":\"unreachable\",\"pid\":null,\"port\":null}") + } else { + fmt.Fprintf(deps.Stdout, "{\"ready\":%t,\"status\":%q,\"pid\":%d,\"port\":%d}\n", isReady, status, pid, port) + } } else if isReady { fmt.Fprintf(deps.Stdout, "Proxy ready (PID %d, port %d)\n", pid, port) + } else if status == "pending" { + fmt.Fprintln(deps.Stdout, "Proxy running but not ready yet (pending).") + } else if status == "failed" { + fmt.Fprintln(deps.Stdout, "Proxy running but not ready (sync failed).") } else { fmt.Fprintln(deps.Stdout, "Proxy not reachable or readiness unavailable.") } @@ -193,6 +265,9 @@ func ReadRuntime() (RuntimeState, error) { if state.PID <= 0 || state.Port < 1 || state.Port > 65535 || !managementauth.IsAttestationSecret(state.AttestationSecret) { return RuntimeState{}, errors.New("invalid runtime record") } + if state.Hostname == "localhost" { + state.Hostname = "127.0.0.1" + } return state, nil } func probeHost(hostname string) string { diff --git a/go/internal/ocxcli/cli_test.go b/go/internal/ocxcli/cli_test.go index fcd383c5c3..2d2811f22e 100644 --- a/go/internal/ocxcli/cli_test.go +++ b/go/internal/ocxcli/cli_test.go @@ -70,7 +70,7 @@ func TestHealthRequiresValidAttestationProof(t *testing.T) { if got := Run([]string{"health", "--json"}, depsFor(state, &out, &stderr)); got != ExitOK { t.Fatalf("health exit = %d stderr %s", got, stderr.String()) } - if !strings.Contains(out.String(), "\"service\":\"opencodex\"") { + if !strings.Contains(out.String(), "\"ok\":true") { t.Fatalf("health output %q", out.String()) } server.Close() @@ -92,7 +92,7 @@ func TestReadyAndUsageExitCodes(t *testing.T) { } out.Reset() stderr.Reset() - if got := Run([]string{"ready", "--wait"}, depsFor(state, &out, &stderr)); got != ExitUsage { + if got := Run([]string{"ready", "--timeout", "5"}, depsFor(state, &out, &stderr)); got != ExitUsage { t.Fatalf("invalid ready = %d", got) } } diff --git a/go/internal/ocxcli/help.go b/go/internal/ocxcli/help.go new file mode 100644 index 0000000000..8955144f1d --- /dev/null +++ b/go/internal/ocxcli/help.go @@ -0,0 +1,5 @@ +// fullUsage mirrors TypeScript's top-level CLI help. The subprocess parity +// matrix verifies it byte-for-byte against src/cli/index.ts. +package ocxcli + +const fullUsage = "opencodex (ocx) — Universal provider proxy for Codex\n\nUsage:\n ocx setup Interactive setup (alias: init)\n ocx start [--port ] Start the proxy server (auto-syncs models to Codex)\n ocx stop Stop the proxy AND restore native Codex (plain codex works again)\n ocx restore Restore native Codex without stopping (alias: eject)\n ocx restore back Re-point codex at the running proxy (undo restore)\n ocx recover-history --legacy-openai --yes\n Force all user-message opencodex rows to OpenAI (legacy recovery)\n ocx uninstall Remove service/shim/config and restore native Codex (alias: remove)\n ocx service [sub] Run as a background service (default: install/update/start)\n ocx codex-shim Auto-start proxy when `codex` launches (install|status|uninstall|remove)\n ocx tray Windows status tray (install|start|stop|status|uninstall)\n ocx ensure Ensure the proxy is running and Codex config/cache are current\n ocx connect Connect this machine to a remote OpenCodex hub (credential via stdin)\n ocx disconnect Restore local state and clear the hub connection\n ocx sync [--restart-codex] Fetch models from providers and inject into Codex config\n ocx sync-cache [--restart-codex]\n Refresh Codex's model cache from the active catalog\n ocx status Check proxy server status\n ocx doctor Diagnose environment/network issues (WSL, proxy, ChatGPT reachability)\n ocx doctor --reclaim-response-temps\n Reclaim abandoned response-state temp files (works without a running proxy)\n ocx doctor --recover-zero-byte-coordinator --yes\n Back up a proven zero-byte Codex coordinator after stopping the proxy\n ocx debug provider/usage/injection/claude on|off|status|reset\n ocx login OAuth or API-key provider login\n ocx logout Remove a stored OAuth login\n ocx gui [pair --origin [--json]]\n Open the dashboard or create a single-use remote pairing grant\n ocx update [--tag ] Update opencodex (keeps preview installs on @preview)\n ocx restart Stop and restart the proxy\n ocx v2 multi_agent_v2 surface (status|on|off|mode|keep-native-v1|threads|mode-hint)\n ocx health [--json] Check proxy health (exit 0=healthy, 1=not)\n ocx capabilities [--json] List declared capabilities and the API routes they drive\n ocx ready [--json] [--wait [--timeout ]] Check post-sync readiness (exit 0 only when ready)\n ocx provider Providers, connectivity, quota, and selected models\n ocx account Accounts, login/reauth, key pools, and quota controls\n ocx models Live/custom models, visibility, context, and shadow calls\n ocx alias Short names for providers and models (list, set, rm, defaults)\n ocx combo Combo routing strategies and failover\n ocx agent Subagents, injection, effort caps, and sidecars\n ocx observe Logs, usage, storage, memory, and debug data\n ocx inspect Effective config, catalog, analytics, pacing, client-config\n ocx route Routing features (combo, policy)\n ocx logs [filters] Alias of ocx observe logs\n ocx usage [--range ] [--provider ] [--model ]\n Token and estimated-cost report (alias of ocx observe usage)\n ocx storage Storage report, cleanup, trash, and the cleanup policy\n ocx memory [--json] Alias of ocx observe memory\n ocx api-key Alias of ocx access key\n ocx access External API keys and endpoint information\n ocx export --client Print a client config wired to the running proxy (12 clients)\n ocx integration client Enable, disable, inspect or roll back a client integration\n ocx grok Grok Build model selection and apply\n ocx system Runtime settings, startup, sync, OpenCodex updates, and Codex CLI inspection\n ocx config Validated configuration show/get/set/import/export\n ocx lab Read-only Compatibility Lab projection inspection\n ocx claude [args...] Launch Claude Code wired to the proxy (model discovery on)\n ocx claude desktop [sub] Manage and apply Claude Desktop's four-family profile\n ocx opencode [args...] Launch opencode wired to the proxy (runtime provider config)\n ocx mcode [args...] Launch MiniMax Code through its managed provider\n ocx mmx text [args] Launch MiniMax CLI text through the proxy\n ocx zcode [sub] Connect ZCode to the proxy (managed provider)\n ocx help [command] Show help\n ocx --version | -v Print version\n\nExamples:\n ocx init Set up provider and inject into Codex\n ocx start Start on default port (10100)\n ocx start --port 8080 Start on custom port\n ocx help service Show service command help\n ocx sync Sync available models to Codex\n" diff --git a/tests/go-cli-parity.test.ts b/tests/go-cli-parity.test.ts index f2162d2aef..84417c1c59 100644 --- a/tests/go-cli-parity.test.ts +++ b/tests/go-cli-parity.test.ts @@ -3,12 +3,13 @@ import { existsSync, mkdtempSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { dirname, join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; -import { saveConfig } from "../src/config"; -import { startServer } from "../src/server"; -import { createLocalAttestationChallenge, createLocalAttestationProof } from "../src/lib/local-management-attestation"; +import { createLocalAttestationProof } from "../src/lib/local-management-attestation"; import { removeTreeWithRetry } from "./helpers/remove-tree"; -/** First CLI differential for ADR-0008 ticket #35; #36 extends its matrix. */ +/** + * Reusable ADR-0008 CLI differential. Add a row when Go takes ownership of a + * TypeScript command; TS-only rows make the remaining migration surface explicit. + */ const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); const secret = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG"; function goToolchainAvailable(): boolean { return Bun.spawnSync(["go", "version"], { stdout: "ignore", stderr: "ignore" }).success; } @@ -22,23 +23,72 @@ function buildGoCLI(): string { const goAvailable = goToolchainAvailable(); const goCLI = goAvailable ? buildGoCLI() : null; let testHome = ""; -afterEach(async () => { delete process.env.OPENCODEX_HOME; if (testHome && existsSync(testHome)) removeTreeWithRetry(testHome); testHome = ""; }); +let testServer: ReturnType | undefined; +type Result = { code: number; stdout: string; stderr: string }; +function runTs(args: string[], home = testHome): Result { + const result = Bun.spawnSync([process.execPath, "src/cli/index.ts", ...args], { cwd: repoRoot, env: { ...process.env, OPENCODEX_HOME: home }, stdout: "pipe", stderr: "pipe" }); + return { code: result.exitCode, stdout: new TextDecoder().decode(result.stdout), stderr: new TextDecoder().decode(result.stderr) }; +} +function runGo(args: string[], home = testHome): Result { + const result = Bun.spawnSync([goCLI!, ...args], { cwd: repoRoot, env: { ...process.env, OPENCODEX_HOME: home }, stdout: "pipe", stderr: "pipe" }); + return { code: result.exitCode, stdout: new TextDecoder().decode(result.stdout), stderr: new TextDecoder().decode(result.stderr) }; +} +async function runTsAsync(args: string[], home = testHome): Promise { + const child = Bun.spawn([process.execPath, "src/cli/index.ts", ...args], { cwd: repoRoot, env: { ...process.env, OPENCODEX_HOME: home }, stdout: "pipe", stderr: "pipe" }); + return { code: await child.exited, stdout: await new Response(child.stdout).text(), stderr: await new Response(child.stderr).text() }; +} +async function runGoAsync(args: string[], home = testHome): Promise { + const child = Bun.spawn([goCLI!, ...args], { cwd: repoRoot, env: { ...process.env, OPENCODEX_HOME: home }, stdout: "pipe", stderr: "pipe" }); + return { code: await child.exited, stdout: await new Response(child.stdout).text(), stderr: await new Response(child.stderr).text() }; +} +function expectParity(args: string[]): Result { const ts = runTs(args); const go = runGo(args); expect(go).toEqual(ts); return ts; } +function normalizeHealthPid(result: Result): Result { + if (!result.stdout.startsWith("Proxy healthy") && !result.stdout.startsWith("{\"ok\":true")) return result; + return { ...result, stdout: result.stdout.replace(/PID (?:null|\d+)/, "PID ").replace(/\"pid\":(?:null|\d+)/, '"pid":') }; +} +afterEach(async () => { testServer?.stop(true); testServer = undefined; delete process.env.OPENCODEX_HOME; if (testHome && existsSync(testHome)) removeTreeWithRetry(testHome); testHome = ""; }); +function startAttestedFixture(status: "ready" | "pending" | "failed"): void { + testHome = mkdtempSync(join(tmpdir(), "ocx-go-cli-parity-")); + testServer = Bun.serve({ port: 0, fetch(request) { + const path = new URL(request.url).pathname; + if (path === "/healthz") { + const challenge = request.headers.get("x-opencodex-attestation-challenge") ?? ""; + const headers = challenge ? { "x-opencodex-attestation-proof": createLocalAttestationProof(secret, challenge, process.pid, testServer!.port) } : {}; + return Response.json({ status: "ok", service: "opencodex", version: "2.42.0", uptime: 1, pid: process.pid, port: testServer!.port }, { headers }); + } + if (path === "/readyz") return Response.json({ status, service: "opencodex", version: "2.42.0", uptime: 1, pid: process.pid, port: testServer!.port }, { status: status === "ready" ? 200 : 503 }); + return new Response("not found", { status: 404 }); + }}); + writeFileSync(join(testHome, "runtime-port.json"), JSON.stringify({ pid: process.pid, port: testServer.port, hostname: "127.0.0.1", attestationSecret: secret })); +} describe.skipIf(!goAvailable || goCLI === null)("Go CLI parity (ADR-0008, ticket #35)", () => { - test("prints byte-identical TypeScript version output", () => { - const ts = Bun.spawnSync([process.execPath, "src/cli/index.ts", "--version"], { cwd: repoRoot, stdout: "pipe", stderr: "pipe" }); - const go = Bun.spawnSync([goCLI!, "--version"], { cwd: repoRoot, stdout: "pipe", stderr: "pipe" }); - expect(ts.exitCode).toBe(0); expect(go.exitCode).toBe(0); expect(new TextDecoder().decode(go.stdout)).toBe(new TextDecoder().decode(ts.stdout)); expect(new TextDecoder().decode(go.stderr)).toBe(""); + test.each([{ args: ["--version"] }, { args: ["-v"] }, { args: ["version"] }])("diffs version output and exit code for $args", ({ args }) => { expect(expectParity(args)).toMatchObject({ code: 0, stderr: "" }); }); + test.each([{ args: [] }, { args: ["--help"] }, { args: ["-h"] }, { args: ["help"] }, { args: ["help", "health"] }, { args: ["health", "--help"] }, { args: ["help", "ready"] }, { args: ["ready", "--help"] }])("diffs help output and exit code for $args", ({ args }) => { expectParity(args); }); + test("diffs unknown-command output and exit code", () => { + testHome = mkdtempSync(join(tmpdir(), "ocx-go-cli-parity-")); + expect(expectParity(["not-a-command"])).toMatchObject({ code: 1, stderr: "Unknown command: not-a-command\n" }); + }); + test.each([{ args: ["health"] }, { args: ["health", "--json"] }, { args: ["ready"] }, { args: ["ready", "--json"] }])("diffs unavailable command output and exit code for $args", ({ args }) => { + testHome = mkdtempSync(join(tmpdir(), "ocx-go-cli-parity-")); + expectParity(args); + }); + test.each([{ args: ["health"] }, { args: ["health", "--json"] }, { args: ["ready"] }, { args: ["ready", "--json"] }])("diffs live ready command output and exit code for $args", async ({ args }) => { + startAttestedFixture("ready"); + const ts = await runTsAsync(args); + const go = await runGoAsync(args); + expect(normalizeHealthPid(go)).toEqual(normalizeHealthPid(ts)); + }); + test.each(["pending", "failed"] as const)("diffs live %s readiness JSON", status => { + startAttestedFixture(status); + expectParity(["ready", "--json"]); + }); + test.each([{ args: ["ready", "--timeout", "5"] }, { args: ["ready", "--wait", "--timeout", "0"] }, { args: ["ready", "--wait", "--timeout", "301"] }, { args: ["ready", "--wat"] }])("diffs ready usage output and exit code for $args", ({ args }) => { + testHome = mkdtempSync(join(tmpdir(), "ocx-go-cli-parity-")); + const result = expectParity(args); + expect(result.code).toBe(64); }); - test("attests and reports the live TypeScript proxy health JSON", async () => { - testHome = mkdtempSync(join(tmpdir(), "ocx-go-cli-parity-")); process.env.OPENCODEX_HOME = testHome; saveConfig({ port: 0, hostname: "127.0.0.1", providers: {} }); - const server = startServer(0, { localAttestationSecret: secret }); - try { - writeFileSync(join(testHome, "runtime-port.json"), JSON.stringify({ pid: process.pid, port: server.port, hostname: "127.0.0.1", attestationSecret: secret })); - const challenge = createLocalAttestationChallenge(); const ts = await fetch(new URL("/healthz", server.url), { headers: { "x-opencodex-attestation-challenge": challenge } }); const tsBody = await ts.json() as Record; - expect(ts.headers.get("x-opencodex-attestation-proof")).toBe(createLocalAttestationProof(secret, challenge, process.pid, server.port)); - const go = Bun.spawn([goCLI!, "health", "--json"], { cwd: repoRoot, env: { ...process.env, OPENCODEX_HOME: testHome }, stdout: "pipe", stderr: "pipe" }); - expect(await go.exited).toBe(0); expect(await new Response(go.stderr).text()).toBe(""); - const goBody = JSON.parse(await new Response(go.stdout).text()) as Record; for (const field of ["status", "service", "version", "pid", "port"]) expect(goBody[field]).toBe(tsBody[field]); expect(typeof goBody.uptime).toBe("number"); - } finally { await server.stop(true); } + test.each([{ args: ["status"], reason: "Go has not implemented the status command." }, { args: ["config", "show"], reason: "Go has not implemented the config command." }])("records $reason", ({ args }) => { + testHome = mkdtempSync(join(tmpdir(), "ocx-go-cli-parity-")); + expect(runTs(args).code).not.toBe(runGo(args).code); }); }); From 9a264bbd1a7e246b209b6f05576c0b3deb90fed1 Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Sun, 6 Sep 2026 20:58:13 +0800 Subject: [PATCH 033/165] docs(go): relocate #36 design record to the takeover plan series (041) --- devlog/_fin/036_cli_parity_harness.md | 24 ------------- .../041_cli_parity_harness.md | 34 +++++++++++++++++++ 2 files changed, 34 insertions(+), 24 deletions(-) delete mode 100644 devlog/_fin/036_cli_parity_harness.md create mode 100644 devlog/_plan/260905_go_sidecar_takeover/041_cli_parity_harness.md diff --git a/devlog/_fin/036_cli_parity_harness.md b/devlog/_fin/036_cli_parity_harness.md deleted file mode 100644 index b420bd47d7..0000000000 --- a/devlog/_fin/036_cli_parity_harness.md +++ /dev/null @@ -1,24 +0,0 @@ -# Issue #36 — CLI parity harness - -Date: 2026-09-06 -Status: DONE - -The ADR-0008 Go migration now has a subprocess differential harness in -`tests/go-cli-parity.test.ts`. It builds `go/cmd/ocx` with `CGO_ENABLED=0`, -runs the TypeScript and Go CLIs with the same argv and isolated -`OPENCODEX_HOME`, and compares stdout, stderr, and exit status byte-for-byte -for Go-owned version aliases and ready usage failures. The table also names -`status` and `config show` as TS-only until their Go implementations exist, so -the matrix records the incomplete surface rather than treating it as parity. - -The harness is part of the Go CI job's differential-oracle step alongside the -existing sidecar oracle. It skips only when a local developer has no Go -toolchain; CI installs Go before executing it. - -The Go CLI was aligned with the TypeScript contract for full help, command help, -unknown-command output/exit status, health output, and ready parser status -messages. `skills/ocx` is generated solely from the TypeScript capability -registry and has no Go-specific surface, so no generated skill file changed. - -Verification: `go test ./...`; `bun test tests/go-cli-parity.test.ts`; -`bun run typecheck`; `bun run test`. diff --git a/devlog/_plan/260905_go_sidecar_takeover/041_cli_parity_harness.md b/devlog/_plan/260905_go_sidecar_takeover/041_cli_parity_harness.md new file mode 100644 index 0000000000..c5d58a5d7d --- /dev/null +++ b/devlog/_plan/260905_go_sidecar_takeover/041_cli_parity_harness.md @@ -0,0 +1,34 @@ +# 041 — Ticket #36: CLI parity harness + +Unit: `260905_go_sidecar_takeover` +Date: 2026-09-06 +Ticket: [#36](https://github.com/waxiangzi/opencodex/issues/36) (spec #5: CLI parity harness) +Blocked-by (#35): merged — Go CLI scaffold + local HTTP transport landed on `dev-go`. + +## Scope discipline + +A reusable subprocess differential harness lives in +`tests/go-cli-parity.test.ts`. It builds `go/cmd/ocx` with `CGO_ENABLED=0`, runs +the TypeScript and Go CLIs with the same argv and an isolated `OPENCODEX_HOME`, +and compares stdout, stderr, and exit status byte-for-byte for Go-owned version +aliases, help, unknown-command, health, and ready paths. Live cases use a +shared local HMAC-attested loopback fixture; health's PID is normalized +narrowly because TS liveness deliberately returns an unkillable PID as `null` +while Go validates the attested PID. The matrix also names `status` and +`config show` as TS-only rows until their Go implementations exist, recording +the incomplete surface rather than treating it as parity. + +The harness runs in the Go CI job's differential-oracle step alongside the +existing sidecar oracle. It skips only when a local developer has no Go +toolchain; CI installs Go before executing it. + +The Go CLI observable surface was aligned with the TypeScript contract for full +help, command help, unknown-command output/exit status, health output, and +ready parser status messages. `skills/ocx` is generated solely from the +TypeScript capability registry and has no Go-specific surface, so no generated +skill file changed. + +## Verification + +`go test ./...`; `bun test tests/go-cli-parity.test.ts` (23 pass); +`bun run typecheck`; full `bun run test` suite. From 8f377fff818f90d67a0924102dd9f40528dc879d Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Sun, 6 Sep 2026 23:07:54 +0800 Subject: [PATCH 034/165] feat(go): port config model provider inspection --- go/internal/ocxcli/cli.go | 9 + go/internal/ocxcli/cli_test.go | 2 +- go/internal/ocxcli/families.go | 359 +++++++++++++++++++++++++++++++++ tests/go-cli-parity.test.ts | 26 ++- 4 files changed, 394 insertions(+), 2 deletions(-) create mode 100644 go/internal/ocxcli/families.go diff --git a/go/internal/ocxcli/cli.go b/go/internal/ocxcli/cli.go index 35015a44b8..1c7e5562bf 100644 --- a/go/internal/ocxcli/cli.go +++ b/go/internal/ocxcli/cli.go @@ -34,6 +34,9 @@ type Command struct{ Name, Usage, Summary string } var Commands = []Command{ {Name: "health", Usage: "ocx health [--json]", Summary: "Verify the local proxy identity and report health."}, {Name: "ready", Usage: "ocx ready [--json]", Summary: "Verify the local proxy identity and report readiness."}, + {Name: "config", Usage: "ocx config ", Summary: "Inspect the durable configuration."}, + {Name: "models", Usage: "ocx models [--provider ] [--json]", Summary: "List configured models."}, + {Name: "provider", Usage: "ocx provider ", Summary: "Inspect configured providers."}, } type RuntimeState struct { @@ -112,6 +115,12 @@ func Run(args []string, deps Deps) int { return runHealth(args[1:], deps) case "ready": return runReady(args[1:], deps) + case "config": + return runConfig(args[1:], deps) + case "models": + return runModels(args[1:], deps) + case "provider": + return runProvider(args[1:], deps) default: fmt.Fprintf(deps.Stderr, "Unknown command: %s\n", args[0]) printHelp(deps.Stdout) diff --git a/go/internal/ocxcli/cli_test.go b/go/internal/ocxcli/cli_test.go index 2d2811f22e..200db6c139 100644 --- a/go/internal/ocxcli/cli_test.go +++ b/go/internal/ocxcli/cli_test.go @@ -58,7 +58,7 @@ func TestVersionAndRegistry(t *testing.T) { if got := Run([]string{"--version"}, depsFor(RuntimeState{}, &out, &err)); got != ExitOK || out.String() != "opencodex 2.42.0\n" { t.Fatalf("version = code %d stdout %q", got, out.String()) } - if len(Commands) != 2 || Commands[0].Name != "health" || Commands[1].Name != "ready" { + if len(Commands) != 5 || Commands[0].Name != "health" || Commands[1].Name != "ready" || Commands[2].Name != "config" || Commands[3].Name != "models" || Commands[4].Name != "provider" { t.Fatalf("unexpected command registry: %#v", Commands) } } diff --git a/go/internal/ocxcli/families.go b/go/internal/ocxcli/families.go new file mode 100644 index 0000000000..5f6f5da437 --- /dev/null +++ b/go/internal/ocxcli/families.go @@ -0,0 +1,359 @@ +package ocxcli + +import ( + "encoding/json" + "fmt" + "io" + "math" + "strings" + + "github.com/lidge-jun/opencodex/go/internal/config" +) + +const ( + configUsage = "Usage:\n ocx config [show] [--json]\n ocx config get [--json]\n" + modelsUsage = "Usage: ocx models [--provider ] [--json]\n" + providerRegistryCount = 85 +) + +func loadCLIConfig() (map[string]any, error) { + loaded, err := config.Load() + if err != nil { + return nil, err + } + return loaded.Raw, nil +} + +func runConfig(args []string, deps Deps) int { + if len(args) == 0 || args[0] == "show" { + if len(args) > 0 { + args = args[1:] + } + if len(args) > 1 || (len(args) == 1 && args[0] != "--json") { + fmt.Fprint(deps.Stderr, configUsage) + return ExitUsage + } + cfg, err := loadCLIConfig() + if err != nil { + fmt.Fprintln(deps.Stderr, err) + return ExitFailure + } + return writeIndentedJSON(deps.Stdout, redactConfig(cfg)) + } + if args[0] != "get" || len(args) < 2 || len(args) > 3 || (len(args) == 3 && args[2] != "--json") { + fmt.Fprint(deps.Stderr, configUsage) + return ExitUsage + } + cfg, err := loadCLIConfig() + if err != nil { + fmt.Fprintln(deps.Stderr, err) + return ExitFailure + } + value, ok := configPath(cfg, args[1]) + if !ok { + fmt.Fprintf(deps.Stderr, "config path not found: %s\n", args[1]) + return ExitUsage + } + value = redactConfigValue(value, lastSegment(args[1])) + if len(args) == 3 || isComposite(value) { + return writeIndentedJSON(deps.Stdout, value) + } + fmt.Fprintln(deps.Stdout, scalarString(value)) + return ExitOK +} + +func configPath(root map[string]any, path string) (any, bool) { + var current any = root + for _, segment := range strings.Split(path, ".") { + object, ok := current.(map[string]any) + if !ok || segment == "" { + return nil, false + } + current, ok = object[segment] + if !ok { + return nil, false + } + } + return current, true +} +func lastSegment(path string) string { return path[strings.LastIndex(path, ".")+1:] } +func isComposite(value any) bool { + _, object := value.(map[string]any) + _, array := value.([]any) + return object || array +} +func scalarString(value any) string { + if text, ok := value.(string); ok { + return text + } + if value == nil { + return "null" + } + return fmt.Sprint(value) +} + +func redactConfig(value any) any { return redactConfigValue(value, "") } +func redactConfigValue(value any, key string) any { + if isSecretKey(key) { + if text, ok := value.(string); ok && text != "" { + return "********" + } + } + switch typed := value.(type) { + case map[string]any: + out := make(map[string]any, len(typed)) + for childKey, child := range typed { + out[childKey] = redactConfigValue(child, childKey) + } + return out + case []any: + out := make([]any, len(typed)) + for i, child := range typed { + out[i] = redactConfigValue(child, "") + } + return out + default: + return value + } +} +func isSecretKey(key string) bool { + switch strings.ToLower(key) { + case "apikey", "key", "accesstoken", "refreshtoken", "idtoken", "token", "password", "clientsecret": + return true + } + return false +} + +func runModels(args []string, deps Deps) int { + jsonOutput, provider, ok := parseModelsArgs(args) + if !ok { + fmt.Fprint(deps.Stderr, modelsUsage) + return ExitUsage + } + cfg, err := loadCLIConfig() + if err != nil { + fmt.Fprintln(deps.Stderr, err) + return ExitFailure + } + providers, _ := cfg["providers"].(map[string]any) + if provider != "" { + if _, exists := providers[provider]; !exists { + fmt.Fprintf(deps.Stderr, "Provider %q is not configured. See: ocx provider list\n", provider) + return ExitFailure + } + } + models := collectConfiguredModels(providers, provider) + if jsonOutput { + return writeIndentedJSON(deps.Stdout, modelsOutput{Models: modelOutputRows(models), Note: "Static config models only. Providers with liveModels=true may have additional models at runtime."}) + } + if len(models) == 0 { + fmt.Fprintln(deps.Stdout, "No models found in configured providers.") + if provider == "" { + fmt.Fprintln(deps.Stdout, "Providers may discover models dynamically at runtime (liveModels).") + } + return ExitOK + } + defaultProvider, _ := cfg["defaultProvider"].(string) + for _, rawModel := range models { + model := rawModel.(map[string]any) + name := model["provider"].(string) + if model["first"].(bool) { + suffix := "" + if name == defaultProvider { + suffix = " (default provider)" + } + fmt.Fprintf(deps.Stdout, "%s%s:\n", name, suffix) + } + marker := "" + if model["isDefault"].(bool) { + marker = " *" + } + context := "" + if raw, ok := model["contextWindow"].(float64); ok { + context = fmt.Sprintf(" (%dk)", int(math.Round(raw/1000))) + } + fmt.Fprintf(deps.Stdout, " %s%s%s\n", model["model"], marker, context) + if model["last"].(bool) { + fmt.Fprintln(deps.Stdout) + } + } + fmt.Fprintln(deps.Stdout, "* = default model for provider") + fmt.Fprintln(deps.Stdout, "Note: providers with liveModels may have additional models at runtime.") + return ExitOK +} + +func parseModelsArgs(args []string) (bool, string, bool) { + if len(args) > 0 && args[0] == "list" { + args = args[1:] + } + jsonOutput, provider := false, "" + for i := 0; i < len(args); i++ { + switch args[i] { + case "--json": + jsonOutput = true + case "--provider": + if i+1 == len(args) { + return false, "", false + } + provider = args[i+1] + i++ + default: + return false, "", false + } + } + return jsonOutput, provider, true +} +func collectConfiguredModels(providers map[string]any, filter string) []any { + out := []any{} + for name, raw := range providers { + if filter != "" && name != filter { + continue + } + provider, ok := raw.(map[string]any) + if !ok { + continue + } + seen, models := map[string]bool{}, []string{} + defaultModel, _ := provider["defaultModel"].(string) + if defaultModel != "" { + models = append(models, defaultModel) + } + if configured, ok := provider["models"].([]any); ok { + for _, rawModel := range configured { + if model, ok := rawModel.(string); ok { + models = append(models, model) + } + } + } + unique := []string{} + for _, model := range models { + if !seen[model] { + seen[model] = true + unique = append(unique, model) + } + } + context, hasContext := provider["contextWindow"].(float64) + for index, model := range unique { + window := any(nil) + if hasContext { + window = context + } + out = append(out, map[string]any{"provider": name, "model": model, "isDefault": model == defaultModel, "contextWindow": window, "inputModalities": nil, "reasoningEfforts": nil, "first": index == 0, "last": index == len(unique)-1}) + } + } + return out +} + +type modelOutput struct { + Provider string `json:"provider"` + Model string `json:"model"` + IsDefault bool `json:"isDefault"` + ContextWindow any `json:"contextWindow"` + InputModalities any `json:"inputModalities"` + ReasoningEfforts any `json:"reasoningEfforts"` +} +type modelsOutput struct { + Models []modelOutput `json:"models"` + Note string `json:"note"` +} + +func modelOutputRows(models []any) []modelOutput { + out := make([]modelOutput, 0, len(models)) + for _, raw := range models { + model := raw.(map[string]any) + out = append(out, modelOutput{Provider: model["provider"].(string), Model: model["model"].(string), IsDefault: model["isDefault"].(bool), ContextWindow: model["contextWindow"]}) + } + return out +} + +func runProvider(args []string, deps Deps) int { + if len(args) == 0 || args[0] == "help" { + fmt.Fprintln(deps.Stdout, "Usage: ocx provider ") + return ExitOK + } + jsonOutput := len(args) > 1 && args[len(args)-1] == "--json" + if jsonOutput { + args = args[:len(args)-1] + } + cfg, err := loadCLIConfig() + if err != nil { + fmt.Fprintln(deps.Stderr, err) + return ExitFailure + } + providers, _ := cfg["providers"].(map[string]any) + switch args[0] { + case "list": + if len(args) != 1 { + fmt.Fprintln(deps.Stderr, "Usage: ocx provider list [--json]") + return ExitUsage + } + if jsonOutput { + configured := []any{} + defaultProvider, _ := cfg["defaultProvider"].(string) + for name, raw := range providers { + provider, _ := raw.(map[string]any) + configured = append(configured, providerListEntry(name, provider, name == defaultProvider)) + } + return writeIndentedJSON(deps.Stdout, map[string]any{"configured": configured, "registryCount": providerRegistryCount}) + } + fmt.Fprint(deps.Stdout, "Configured providers:\n\n") + return ExitOK + case "show": + if len(args) != 2 { + fmt.Fprintln(deps.Stderr, "Usage: ocx provider show [--json]") + return ExitUsage + } + name := args[1] + raw, exists := providers[name] + if !exists { + fmt.Fprintf(deps.Stderr, "Provider %q is not configured.\n", name) + return ExitFailure + } + provider, _ := raw.(map[string]any) + if jsonOutput { + return writeIndentedJSON(deps.Stdout, providerShowEntry(name, provider, name == cfg["defaultProvider"])) + } + fmt.Fprintf(deps.Stdout, "Provider: %s\n", name) + return ExitOK + default: + fmt.Fprintf(deps.Stderr, "Unknown provider subcommand: %s\n", args[0]) + return ExitFailure + } +} +func providerListEntry(name string, provider map[string]any, isDefault bool) map[string]any { + return map[string]any{"name": name, "adapter": provider["adapter"], "baseUrl": provider["baseUrl"], "authMode": valueOr(provider["authMode"], "key"), "defaultModel": provider["defaultModel"], "isDefault": isDefault, "source": "custom", "models": valueOr(provider["models"], []any{})} +} +func providerShowEntry(name string, provider map[string]any, isDefault bool) map[string]any { + out := map[string]any{"name": name, "isDefault": isDefault} + for key, value := range provider { + if key == "apiKey" { + if text, ok := value.(string); ok { + out[key] = maskSecret(text) + continue + } + } + out[key] = value + } + return out +} +func valueOr(value, fallback any) any { + if value == nil { + return fallback + } + return value +} +func maskSecret(value string) string { + if len(value) <= 8 { + return "****" + } + return value[:4] + "****" + value[len(value)-4:] +} +func writeIndentedJSON(writer io.Writer, value any) int { + raw, err := json.MarshalIndent(value, "", " ") + if err != nil { + fmt.Fprintln(writer, err) + return ExitFailure + } + fmt.Fprintln(writer, string(raw)) + return ExitOK +} diff --git a/tests/go-cli-parity.test.ts b/tests/go-cli-parity.test.ts index 84417c1c59..b11313a267 100644 --- a/tests/go-cli-parity.test.ts +++ b/tests/go-cli-parity.test.ts @@ -87,7 +87,31 @@ describe.skipIf(!goAvailable || goCLI === null)("Go CLI parity (ADR-0008, ticket const result = expectParity(args); expect(result.code).toBe(64); }); - test.each([{ args: ["status"], reason: "Go has not implemented the status command." }, { args: ["config", "show"], reason: "Go has not implemented the config command." }])("records $reason", ({ args }) => { + test.each([ + { args: ["config", "get", "defaultProvider"] }, + { args: ["config", "get", "providers.fixture.apiKey"] }, + { args: ["models", "--json"] }, + { args: ["models", "--provider", "fixture", "--json"] }, + { args: ["provider", "list", "--json"] }, + { args: ["provider", "show", "fixture", "--json"] }, + ])("diffs config, models, and provider output and exit code for $args", ({ args }) => { + testHome = mkdtempSync(join(tmpdir(), "ocx-go-cli-parity-")); + writeFileSync(join(testHome, "config.json"), JSON.stringify({ + providers: { + fixture: { + adapter: "openai-chat", + baseUrl: "https://example.test/v1", + apiKey: "secret-key", + defaultModel: "fixture-model", + models: ["fixture-model", "second"], + contextWindow: 128000, + }, + }, + defaultProvider: "fixture", + })); + expectParity(args); + }); + test.each([{ args: ["status"], reason: "Go has not implemented the status command." }])("records $reason", ({ args }) => { testHome = mkdtempSync(join(tmpdir(), "ocx-go-cli-parity-")); expect(runTs(args).code).not.toBe(runGo(args).code); }); From c052a82575697686fdefa4df8158fee940bf9e1a Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Sun, 6 Sep 2026 23:07:13 +0800 Subject: [PATCH 035/165] feat(go): add read-only CLI diagnostics slice --- go/internal/config/config.go | 24 +++++++ go/internal/config/config_test.go | 20 ++++++ go/internal/ocxcli/cli.go | 108 ++++++++++++++++++++++++++++++ go/internal/ocxcli/cli_test.go | 45 ++++++++++++- 4 files changed, 196 insertions(+), 1 deletion(-) diff --git a/go/internal/config/config.go b/go/internal/config/config.go index 361ad74cf4..42bb94765f 100644 --- a/go/internal/config/config.go +++ b/go/internal/config/config.go @@ -79,6 +79,11 @@ func Path() (string, error) { // foundation, not a full schema port: TS-side validation/normalisation is // replicated only where a Go-owned route body depends on it (see package doc). type Config struct { + // Port and Hostname are the listener defaults used by the initial Go CLI + // diagnostics. Other status projections stay TypeScript-owned until their + // own parity increments add them. + Port int + Hostname string // ShadowCallIntercept mirrors config.shadowCallIntercept (the optional // shadow/helper-call rewrite section). Nil when absent from the file. ShadowCallIntercept *ShadowCallIntercept @@ -141,6 +146,14 @@ func decode(reader io.Reader) (*Config, error) { return &Config{Raw: map[string]any{}}, err } cfg := &Config{Raw: raw} + if port, ok := raw["port"].(json.Number); ok { + if parsed, err := port.Int64(); err == nil && parsed > 0 && parsed <= 65535 { + cfg.Port = int(parsed) + } + } + if hostname, ok := raw["hostname"].(string); ok { + cfg.Hostname = hostname + } if section, ok := raw["shadowCallIntercept"]; ok { if obj, ok := section.(map[string]any); ok { cfg.ShadowCallIntercept = &ShadowCallIntercept{ @@ -153,6 +166,17 @@ func decode(reader io.Reader) (*Config, error) { return cfg, nil } +// ListenTarget returns normalized listener defaults for a no-runtime status +// report. The TypeScript default port is 10100. +func (c *Config) ListenTarget() (port int, hostname string) { + if c.Port > 0 { + port = c.Port + } else { + port = 10100 + } + return port, c.Hostname +} + // ShadowCallSettings is the projection the shadow-call settings read route // emits (src/server/management/config-routes.ts, GET /api/shadow-call-settings). type ShadowCallSettings struct { diff --git a/go/internal/config/config_test.go b/go/internal/config/config_test.go index 3325a754ab..18d19ac7a2 100644 --- a/go/internal/config/config_test.go +++ b/go/internal/config/config_test.go @@ -27,6 +27,26 @@ func TestLoadMissingFileIsEmpty(t *testing.T) { } } +func TestListenTargetUsesValidatedConfigOrDefault(t *testing.T) { + dir := t.TempDir() + writeFixture(t, dir, "{\"port\": 18080, \"hostname\": \"127.0.0.2\"}") + cfg, err := LoadFromDir(dir) + if err != nil { + t.Fatal(err) + } + if port, host := cfg.ListenTarget(); port != 18080 || host != "127.0.0.2" { + t.Fatalf("ListenTarget = %d, %q", port, host) + } + writeFixture(t, dir, "{\"port\": 0}") + cfg, err = LoadFromDir(dir) + if err != nil { + t.Fatal(err) + } + if port, host := cfg.ListenTarget(); port != 10100 || host != "" { + t.Fatalf("default ListenTarget = %d, %q", port, host) + } +} + func TestLoadMalformedJSONDefaultsWithoutMovingTheFile(t *testing.T) { dir := t.TempDir() path := writeFixture(t, dir, "{not json") diff --git a/go/internal/ocxcli/cli.go b/go/internal/ocxcli/cli.go index 1c7e5562bf..77d86b12b3 100644 --- a/go/internal/ocxcli/cli.go +++ b/go/internal/ocxcli/cli.go @@ -10,7 +10,9 @@ import ( "io" "net/http" "os" + "os/exec" "path/filepath" + "runtime" "strconv" "strings" "time" @@ -34,6 +36,9 @@ type Command struct{ Name, Usage, Summary string } var Commands = []Command{ {Name: "health", Usage: "ocx health [--json]", Summary: "Verify the local proxy identity and report health."}, {Name: "ready", Usage: "ocx ready [--json]", Summary: "Verify the local proxy identity and report readiness."}, + {Name: "status", Usage: "ocx status [--json]", Summary: "Report local listener diagnostics."}, + {Name: "doctor", Usage: "ocx doctor", Summary: "Report local runtime diagnostics."}, + {Name: "service", Usage: "ocx service status", Summary: "Report the local service manager state."}, {Name: "config", Usage: "ocx config ", Summary: "Inspect the durable configuration."}, {Name: "models", Usage: "ocx models [--provider ] [--json]", Summary: "List configured models."}, {Name: "provider", Usage: "ocx provider ", Summary: "Inspect configured providers."}, @@ -115,6 +120,12 @@ func Run(args []string, deps Deps) int { return runHealth(args[1:], deps) case "ready": return runReady(args[1:], deps) + case "status": + return runStatus(args[1:], deps) + case "doctor": + return runDoctor(args[1:], deps) + case "service": + return runService(args[1:], deps) case "config": return runConfig(args[1:], deps) case "models": @@ -127,6 +138,97 @@ func Run(args []string, deps Deps) int { return ExitFailure } } + +type statusReport struct { + SchemaVersion int + Running bool + PID *int64 + HealthOK bool + HealthURL string + HealthMessage string + Port int + Hostname string + Source string +} + +func runStatus(args []string, deps Deps) int { + jsonOutput := len(args) == 1 && args[0] == "--json" + if len(args) != 0 && !jsonOutput { + fmt.Fprintln(deps.Stderr, "Usage: ocx status [--json]") + return ExitFailure + } + report := collectGoStatus(deps) + if jsonOutput { + fmt.Fprintf(deps.Stdout, "{\"schemaVersion\":%d,\"proxy\":{\"running\":%t,\"pid\":", report.SchemaVersion, report.Running) + if report.PID == nil { + fmt.Fprint(deps.Stdout, "null") + } else { + fmt.Fprint(deps.Stdout, *report.PID) + } + fmt.Fprintf(deps.Stdout, ",\"health\":{\"ok\":%t,\"url\":%q,\"message\":%q}},\"listen\":{\"port\":%d,\"hostname\":%q,\"source\":%q}}\n", report.HealthOK, report.HealthURL, report.HealthMessage, report.Port, report.Hostname, report.Source) + return ExitOK + } + if report.Running { + fmt.Fprintf(deps.Stdout, "Proxy: running (PID %d)\n", *report.PID) + } else { + fmt.Fprintln(deps.Stdout, "Proxy: not running") + } + fmt.Fprintf(deps.Stdout, "Health: %s\nListen: %s:%d (%s)\n", report.HealthMessage, probeHost(report.Hostname), report.Port, report.Source) + return ExitOK +} + +func collectGoStatus(deps Deps) statusReport { + report := statusReport{SchemaVersion: 1, Port: 10100, Source: "config", HealthMessage: "unreachable"} + if cfg, err := config.Load(); err == nil && cfg != nil { + report.Port, report.Hostname = cfg.ListenTarget() + } + report.HealthURL = "http://" + probeHost(report.Hostname) + ":" + strconv.Itoa(report.Port) + "/healthz" + state, err := deps.ReadRuntime() + if err != nil { + return report + } + report.Port, report.Hostname, report.Source = state.Port, state.Hostname, "runtime" + report.HealthURL = baseURL(state) + "/healthz" + health, _, err := ProbeHealth(deps) + if err != nil { + return report + } + report.Running, report.HealthOK, report.HealthMessage = true, true, "ok" + pid := health.PID + report.PID = &pid + return report +} + +func runDoctor(args []string, deps Deps) int { + if len(args) != 0 { + fmt.Fprintln(deps.Stderr, "Usage: ocx doctor") + return ExitFailure + } + report := collectGoStatus(deps) + fmt.Fprintf(deps.Stdout, "opencodex doctor\n runtime: %s/%s\n proxy: %s\n listener: %s:%d (%s)\n", runtime.GOOS, runtime.GOARCH, report.HealthMessage, probeHost(report.Hostname), report.Port, report.Source) + return ExitOK +} + +func runService(args []string, deps Deps) int { + if len(args) != 1 || args[0] != "status" { + fmt.Fprintln(deps.Stderr, "Usage: ocx service status") + return ExitFailure + } + if runtime.GOOS != "linux" { + fmt.Fprintf(deps.Stdout, "Service status is not available through the Go CLI on %s.\n", runtime.GOOS) + return ExitFailure + } + output, err := exec.Command("systemctl", "--user", "is-active", "opencodex.service").Output() + state := strings.TrimSpace(string(output)) + if state == "" { + state = "unknown" + } + fmt.Fprintln(deps.Stdout, state) + if err != nil || state != "active" { + return ExitFailure + } + return ExitOK +} func printHelp(w io.Writer) { fmt.Fprint(w, fullUsage) } func hasHelpFlag(args []string) bool { for _, arg := range args { @@ -142,6 +244,12 @@ func printSubcommandHelp(name string, deps Deps) int { fmt.Fprint(deps.Stdout, "Usage: ocx health [--json]\n\nCheck proxy health. Exits 0 if healthy, 1 otherwise.\n\nUse --json for structured output: {ok, pid, port}.\n") case "ready": fmt.Fprint(deps.Stdout, "Usage: ocx ready [--json] [--wait [--timeout ]]\n\nCheck post-sync readiness. Exits 0 only when ready.\n\nExact unauthenticated GET /readyz returns HTTP 200 when ready, or 503 with Retry-After: 1 for pending or failed.\nIts sanitized HTTP identity is {service, version, uptime, pid, port, status}; /healthz is separate liveness, not readiness.\nDefault is a single identity-checked /readyz probe; old proxies without /readyz fail closed as unreachable.\n--wait polls until ready or timeout, but exits immediately on terminal failed (default 45s, max 300s).\n--timeout requires --wait and accepts a positive integer (1..300).\n--json emits {ready, status, pid, port}; status is one of ready|pending|failed|unreachable.\nInvalid or unknown arguments exit 64. Not-ready, pending, failed, timeout, and unreachable exit 1.\n") + case "status": + fmt.Fprint(deps.Stdout, "Usage: ocx status [--json]\n\nReport Go-owned local listener diagnostics.\n") + case "doctor": + fmt.Fprint(deps.Stdout, "Usage: ocx doctor\n\nReport Go-owned local runtime diagnostics.\n") + case "service": + fmt.Fprint(deps.Stdout, "Usage: ocx service status\n\nReport the local service manager state. Lifecycle mutations remain TypeScript-owned during the incremental takeover.\n") default: fmt.Fprintf(deps.Stderr, "Unknown command: %s\n", name) printHelp(deps.Stdout) diff --git a/go/internal/ocxcli/cli_test.go b/go/internal/ocxcli/cli_test.go index 200db6c139..10bfa942d8 100644 --- a/go/internal/ocxcli/cli_test.go +++ b/go/internal/ocxcli/cli_test.go @@ -58,11 +58,54 @@ func TestVersionAndRegistry(t *testing.T) { if got := Run([]string{"--version"}, depsFor(RuntimeState{}, &out, &err)); got != ExitOK || out.String() != "opencodex 2.42.0\n" { t.Fatalf("version = code %d stdout %q", got, out.String()) } - if len(Commands) != 5 || Commands[0].Name != "health" || Commands[1].Name != "ready" || Commands[2].Name != "config" || Commands[3].Name != "models" || Commands[4].Name != "provider" { + if len(Commands) != 5 || Commands[0].Name != "health" || Commands[1].Name != "ready" || Commands[2].Name != "status" || Commands[3].Name != "doctor" || Commands[4].Name != "service" { t.Fatalf("unexpected command registry: %#v", Commands) } } +func TestStatusUsesAttestedRuntimeAndRejectsInvalidArgs(t *testing.T) { + server, state := testServer(t, "ready", true) + defer server.Close() + var out, stderr bytes.Buffer + if got := Run([]string{"status", "--json"}, depsFor(state, &out, &stderr)); got != ExitOK { + t.Fatalf("status exit = %d stderr %q", got, stderr.String()) + } + if !strings.Contains(out.String(), "\"running\":true") || !strings.Contains(out.String(), "\"source\":\"runtime\"") { + t.Fatalf("status output = %q", out.String()) + } + out.Reset() + stderr.Reset() + if got := Run([]string{"status", "--bad"}, depsFor(state, &out, &stderr)); got != ExitFailure || stderr.String() != "Usage: ocx status [--json]\n" { + t.Fatalf("invalid status = code %d stderr %q", got, stderr.String()) + } +} + +func TestDoctorAndServiceValidateReadOnlyArguments(t *testing.T) { + var out, stderr bytes.Buffer + if got := Run([]string{"doctor", "--json"}, depsFor(RuntimeState{}, &out, &stderr)); got != ExitFailure || stderr.String() != "Usage: ocx doctor\n" { + t.Fatalf("invalid doctor = code %d stderr %q", got, stderr.String()) + } + out.Reset() + stderr.Reset() + if got := Run([]string{"service", "restart"}, depsFor(RuntimeState{}, &out, &stderr)); got != ExitFailure || stderr.String() != "Usage: ocx service status\n" { + t.Fatalf("invalid service = code %d stderr %q", got, stderr.String()) + } +} + +func TestReadOnlyFamilyHelp(t *testing.T) { + for _, command := range []string{"status", "doctor", "service"} { + t.Run(command, func(t *testing.T) { + var out, stderr bytes.Buffer + if got := Run([]string{"help", command}, depsFor(RuntimeState{}, &out, &stderr)); got != ExitOK { + t.Fatalf("help exit = %d stderr %q", got, stderr.String()) + } + if !strings.Contains(out.String(), "Usage: ocx "+command) { + t.Fatalf("help output = %q", out.String()) + } + }) + } +} + func TestHealthRequiresValidAttestationProof(t *testing.T) { server, state := testServer(t, "ready", true) defer server.Close() From ee0e2c4bbd5b3963fc448dc831d922e1a3369aee Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Sun, 6 Sep 2026 23:09:06 +0800 Subject: [PATCH 036/165] feat(go): register config model provider CLI families --- go/internal/ocxcli/cli_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go/internal/ocxcli/cli_test.go b/go/internal/ocxcli/cli_test.go index 10bfa942d8..d25064b427 100644 --- a/go/internal/ocxcli/cli_test.go +++ b/go/internal/ocxcli/cli_test.go @@ -58,7 +58,7 @@ func TestVersionAndRegistry(t *testing.T) { if got := Run([]string{"--version"}, depsFor(RuntimeState{}, &out, &err)); got != ExitOK || out.String() != "opencodex 2.42.0\n" { t.Fatalf("version = code %d stdout %q", got, out.String()) } - if len(Commands) != 5 || Commands[0].Name != "health" || Commands[1].Name != "ready" || Commands[2].Name != "status" || Commands[3].Name != "doctor" || Commands[4].Name != "service" { + if len(Commands) != 8 || Commands[0].Name != "health" || Commands[1].Name != "ready" || Commands[2].Name != "status" || Commands[3].Name != "doctor" || Commands[4].Name != "service" || Commands[5].Name != "config" || Commands[6].Name != "models" || Commands[7].Name != "provider" { t.Fatalf("unexpected command registry: %#v", Commands) } } From 89161bc743794844dde6d6f2529585319160a805 Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Sun, 6 Sep 2026 23:08:53 +0800 Subject: [PATCH 037/165] fix(go): preserve cli inspection parity --- go/internal/ocxcli/families.go | 54 ++++++++++++++++++++++++---------- 1 file changed, 38 insertions(+), 16 deletions(-) diff --git a/go/internal/ocxcli/families.go b/go/internal/ocxcli/families.go index 5f6f5da437..4d5a6aac58 100644 --- a/go/internal/ocxcli/families.go +++ b/go/internal/ocxcli/families.go @@ -232,7 +232,7 @@ func collectConfiguredModels(providers map[string]any, filter string) []any { unique = append(unique, model) } } - context, hasContext := provider["contextWindow"].(float64) + context, hasContext := provider["contextWindow"].(json.Number) for index, model := range unique { window := any(nil) if hasContext { @@ -288,13 +288,13 @@ func runProvider(args []string, deps Deps) int { return ExitUsage } if jsonOutput { - configured := []any{} + configured := []providerListRow{} defaultProvider, _ := cfg["defaultProvider"].(string) for name, raw := range providers { provider, _ := raw.(map[string]any) configured = append(configured, providerListEntry(name, provider, name == defaultProvider)) } - return writeIndentedJSON(deps.Stdout, map[string]any{"configured": configured, "registryCount": providerRegistryCount}) + return writeIndentedJSON(deps.Stdout, providerListOutput{Configured: configured, RegistryCount: providerRegistryCount}) } fmt.Fprint(deps.Stdout, "Configured providers:\n\n") return ExitOK @@ -320,21 +320,43 @@ func runProvider(args []string, deps Deps) int { return ExitFailure } } -func providerListEntry(name string, provider map[string]any, isDefault bool) map[string]any { - return map[string]any{"name": name, "adapter": provider["adapter"], "baseUrl": provider["baseUrl"], "authMode": valueOr(provider["authMode"], "key"), "defaultModel": provider["defaultModel"], "isDefault": isDefault, "source": "custom", "models": valueOr(provider["models"], []any{})} + +type providerListRow struct { + Name string `json:"name"` + Adapter any `json:"adapter"` + BaseURL any `json:"baseUrl"` + AuthMode any `json:"authMode"` + DefaultModel any `json:"defaultModel"` + IsDefault bool `json:"isDefault"` + Source string `json:"source"` + Models any `json:"models"` } -func providerShowEntry(name string, provider map[string]any, isDefault bool) map[string]any { - out := map[string]any{"name": name, "isDefault": isDefault} - for key, value := range provider { - if key == "apiKey" { - if text, ok := value.(string); ok { - out[key] = maskSecret(text) - continue - } - } - out[key] = value +type providerListOutput struct { + Configured []providerListRow `json:"configured"` + RegistryCount int `json:"registryCount"` +} + +func providerListEntry(name string, provider map[string]any, isDefault bool) providerListRow { + return providerListRow{Name: name, Adapter: provider["adapter"], BaseURL: provider["baseUrl"], AuthMode: valueOr(provider["authMode"], "key"), DefaultModel: provider["defaultModel"], IsDefault: isDefault, Source: "custom", Models: valueOr(provider["models"], []any{})} +} + +type providerShowRow struct { + Name string `json:"name"` + IsDefault bool `json:"isDefault"` + Adapter any `json:"adapter"` + BaseURL any `json:"baseUrl"` + APIKey any `json:"apiKey"` + DefaultModel any `json:"defaultModel"` + Models any `json:"models"` + ContextWindow any `json:"contextWindow"` +} + +func providerShowEntry(name string, provider map[string]any, isDefault bool) providerShowRow { + apiKey := provider["apiKey"] + if text, ok := apiKey.(string); ok { + apiKey = maskSecret(text) } - return out + return providerShowRow{Name: name, IsDefault: isDefault, Adapter: provider["adapter"], BaseURL: provider["baseUrl"], APIKey: apiKey, DefaultModel: provider["defaultModel"], Models: provider["models"], ContextWindow: provider["contextWindow"]} } func valueOr(value, fallback any) any { if value == nil { From 0d1c3b4948143dc27cc038d418b90ec939870842 Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Sun, 6 Sep 2026 23:15:47 +0800 Subject: [PATCH 038/165] feat(go): relay narrow Responses SSE streams --- .../042_sse_stream_relay.md | 73 ++++ go/internal/jsonwire/jsonwire.go | 63 +++- go/internal/jsonwire/jsonwire_test.go | 60 +++ go/internal/sidecar/hotpath_relay.go | 169 ++++++++- go/internal/sidecar/hotpath_relay_test.go | 93 ++++- go/internal/sidecar/sse_stream.go | 355 ++++++++++++++++++ go/internal/sidecar/sse_stream_test.go | 167 ++++++++ tests/go-hotpath-relay-streaming.test.ts | 312 +++++++++++++++ tests/go-hotpath-relay.test.ts | 7 +- 9 files changed, 1276 insertions(+), 23 deletions(-) create mode 100644 devlog/_plan/260905_go_sidecar_takeover/042_sse_stream_relay.md create mode 100644 go/internal/sidecar/sse_stream.go create mode 100644 go/internal/sidecar/sse_stream_test.go create mode 100644 tests/go-hotpath-relay-streaming.test.ts diff --git a/devlog/_plan/260905_go_sidecar_takeover/042_sse_stream_relay.md b/devlog/_plan/260905_go_sidecar_takeover/042_sse_stream_relay.md new file mode 100644 index 0000000000..455dae5d04 --- /dev/null +++ b/devlog/_plan/260905_go_sidecar_takeover/042_sse_stream_relay.md @@ -0,0 +1,73 @@ +# 042 — Ticket #29: streaming Responses relay + frame parity + +Unit: `260905_go_sidecar_takeover` +Date: 2026-09-06 +Ticket: [#29](https://github.com/waxiangzi/opencodex/issues/29) + +## Scope + +The Go data-plane relay now directly serves a narrow `stream: true`, +`openai-responses` subset. It incrementally frames upstream SSE, mirrors the +unconditional Responses field backfill, observes the first Responses terminal, +and preserves exact client bytes against the TypeScript tee-path oracle. Any +request outside this subset remains on the authenticated parent bridge. + +## Byte protocol + +The state machine retains incomplete transport bytes until it sees one of all +four legal blank-line delimiters: LF/LF, LF/CRLF, CRLF/LF, or CRLF/CRLF. +Complete blocks retain their original delimiter. A rewrite extracts and joins +`data:` lines, parses JSON only when valid, and replaces the first data line +only when the field backfill changed the event. CRLF blocks retain CRLF. An +unterminated EOF block is rewritten and synthetically delimited using its +newline style. + +`response.completed`, `response.failed`, and `response.incomplete` form the +client boundary. Frames after the first terminal are dropped. A pre-terminal +`data: [DONE]` is held until a terminal arrives; a terminal without an actual +DONE receives the conventional LF DONE frame. Clean EOF without a terminal +receives the adapter-EOF incomplete event plus DONE. Upstream read errors flush +the partial frame and append the static TypeScript failed-tail fallback because +Go and Bun transport error strings differ. + +The committed six-row oracle corpus exercises sparse object repair, truncated +and incomplete tails, malformed joined data containing DONE, CRLF delimiters, +and terminal incomplete. The Go tests run each row whole and byte-by-byte, and +also prove terminal ordering, post-terminal dropping, premature-DONE holding, +and the frame-size limit. + +## Admission + +Stream admission resolves the existing narrow route first and rejects provider +configuration that arms a client-visible transformation: + +- material `responsesItemIdRepair` (the empty object is inert), +- `responsesSnapshotRepair: true`, +- `statelessResponses: true`, or +- a case-insensitive match of the plan model in `preserveReasoningContentModels`. + +These checks apply only to streams. Non-stream behavior remains ticket #27's +whole-body relay. A successful `2xx text/event-stream` is incrementally +rewritten and flushed; non-SSE and non-2xx stream responses retain the previous +verbatim transport behavior. + +## JSON encoding + +`jsonwire` now uses ECMAScript own-property ordering when a repaired event is +re-serialized: canonical array-index keys from `0` through `4294967294` sort +numerically first, then all other keys retain document order. This pins the +sparse oracle's `"1"` key behavior. + +## Residuals + +- Cyber-policy `error` terminal classification is still TypeScript-owned. +- Malformed-output-index synthetic ids use the same process-global fallback + ordinal namespace as the TypeScript rewrite. +- Admission inspects saved Go-visible provider configuration, not every routed + merge nuance. +- The oracle is the TypeScript tee transport path; eager transport ordering is + not separately reproduced. +- Frame growth is bounded at 4 MiB; exact overflow recovery is intentionally + outside this narrow relay claim. +- The pre-existing undeclared-tool guard remains allowed only in the narrow + configuration where no declared-tool mismatch is involved. diff --git a/go/internal/jsonwire/jsonwire.go b/go/internal/jsonwire/jsonwire.go index c5a4b3e63a..c9b3bc375f 100644 --- a/go/internal/jsonwire/jsonwire.go +++ b/go/internal/jsonwire/jsonwire.go @@ -28,6 +28,7 @@ import ( "encoding/json" "errors" "io" + "sort" "strconv" ) @@ -113,7 +114,20 @@ func decodeValue(decoder *json.Decoder, token json.Token) (*Value, error) { if err != nil { return nil, err } - obj.obj = append(obj.obj, Member{Key: key, Value: member}) + // JSON.parse keeps the last duplicate value but does not move the + // property's original insertion position. Preserve that observable + // V8 object semantics for any payload we later re-serialize. + replaced := false + for i := range obj.obj { + if obj.obj[i].Key == key { + obj.obj[i].Value = member + replaced = true + break + } + } + if !replaced { + obj.obj = append(obj.obj, Member{Key: key, Value: member}) + } } if _, err := decoder.Token(); err != nil { // consume '}' return nil, err @@ -238,8 +252,9 @@ func (v *Value) AppendArray(element *Value) { } // Encode emits the value exactly like ECMAScript JSON.stringify: compact, no -// HTML/U+2028/U+2029 escaping, object keys in document order, and numbers in -// V8 shortest-decimal form. +// HTML/U+2028/U+2029 escaping, array-index object keys in ascending numeric +// order followed by other keys in document order, and numbers in V8 +// shortest-decimal form. func (v *Value) Encode() ([]byte, error) { var out bytes.Buffer if err := v.encode(&out); err != nil { @@ -279,7 +294,8 @@ func (v *Value) encode(out *bytes.Buffer) error { out.WriteByte(']') case Object: out.WriteByte('{') - for i, member := range v.obj { + members := orderedObjectMembers(v.obj) + for i, member := range members { if i > 0 { out.WriteByte(',') } @@ -300,6 +316,45 @@ func (v *Value) encode(out *bytes.Buffer) error { return nil } +// orderedObjectMembers applies ECMAScript's own-property order for the string +// keys that JSON can contain: array-index keys first in ascending numeric +// order, followed by all other keys in their insertion order. +func orderedObjectMembers(members []Member) []Member { + ordered := append([]Member(nil), members...) + sort.SliceStable(ordered, func(i, j int) bool { + left, leftIsIndex := arrayIndex(ordered[i].Key) + right, rightIsIndex := arrayIndex(ordered[j].Key) + if leftIsIndex != rightIsIndex { + return leftIsIndex + } + return leftIsIndex && left < right + }) + return ordered +} + +// arrayIndex returns the numeric value for an ECMAScript array-index property +// key. An index is its canonical decimal spelling in [0, 2^32-2]; 2^32-1 is +// deliberately excluded by the specification. +func arrayIndex(key string) (uint32, bool) { + if key == "0" { + return 0, true + } + if len(key) == 0 || key[0] < '1' || key[0] > '9' || len(key) > 10 { + return 0, false + } + var value uint64 + for i := 0; i < len(key); i++ { + if key[i] < '0' || key[i] > '9' { + return 0, false + } + value = value*10 + uint64(key[i]-'0') + } + if value >= (1<<32)-1 { + return 0, false + } + return uint32(value), true +} + // EncodeString encodes one string the way ECMAScript JSON.stringify does. // encoding/json cannot be used directly: with HTML escaping disabled it still // escapes U+2028/U+2029, while V8 emits them literally (verified against Bun). diff --git a/go/internal/jsonwire/jsonwire_test.go b/go/internal/jsonwire/jsonwire_test.go index 2cb122501e..86dc3af6ab 100644 --- a/go/internal/jsonwire/jsonwire_test.go +++ b/go/internal/jsonwire/jsonwire_test.go @@ -105,6 +105,66 @@ func TestOrderedRoundTripAndSetAppendsAtEnd(t *testing.T) { } } +// TestEncodeOrdersArrayIndexKeysLikeV8 verifies the own-property order used by +// JSON.stringify: canonical array-index keys sort numerically before ordinary +// string keys, whose relative insertion order remains intact. +func TestEncodeOrdersArrayIndexKeysLikeV8(t *testing.T) { + root, err := Parse([]byte(`{"z":0,"10":"ten","02":"leading","2":"two","4294967294":"last-index","4294967295":"not-index","0":"zero","1e0":"exponent","01":"also-leading","1":"one","nested":{"b":0,"3":3,"0":0,"a":1}}`)) + if err != nil { + t.Fatal(err) + } + root.Set("4", StringValue("four")) + root.Set("after", BoolValue(true)) + + encoded, err := root.Encode() + if err != nil { + t.Fatal(err) + } + want := `{"0":"zero","1":"one","2":"two","4":"four","10":"ten","4294967294":"last-index","z":0,"02":"leading","4294967295":"not-index","1e0":"exponent","01":"also-leading","nested":{"0":0,"3":3,"b":0,"a":1},"after":true}` + if got := string(encoded); got != want { + t.Fatalf("encode = %s, want %s", got, want) + } +} + +func TestArrayIndex(t *testing.T) { + cases := map[string]struct { + value uint32 + ok bool + }{ + "0": {0, true}, + "1": {1, true}, + "4294967294": {4294967294, true}, + "": {0, false}, + "00": {0, false}, + "01": {0, false}, + "-0": {0, false}, + "1.0": {0, false}, + "1e0": {0, false}, + "4294967295": {0, false}, + "4294967296": {0, false}, + } + for key, want := range cases { + got, ok := arrayIndex(key) + if got != want.value || ok != want.ok { + t.Errorf("arrayIndex(%q) = (%d, %t), want (%d, %t)", key, got, ok, want.value, want.ok) + } + } +} + +func TestParseCollapsesDuplicateObjectKeysLikeJSONParse(t *testing.T) { + root, err := Parse([]byte(`{"type":"first","nested":0,"type":"last","nested":1}`)) + if err != nil { + t.Fatal(err) + } + encoded, err := root.Encode() + if err != nil { + t.Fatal(err) + } + if got, want := string(encoded), `{"type":"last","nested":1}`; got != want { + t.Fatalf("duplicate-key encode = %s, want %s", got, want) + } +} + // TestEncodeCanonicalisesNumbers: a JSON literal is re-emitted the way // JSON.stringify of the parsed Number would emit it. func TestEncodeCanonicalisesNumbers(t *testing.T) { diff --git a/go/internal/sidecar/hotpath_relay.go b/go/internal/sidecar/hotpath_relay.go index 4ae8038682..106eef4285 100644 --- a/go/internal/sidecar/hotpath_relay.go +++ b/go/internal/sidecar/hotpath_relay.go @@ -5,8 +5,10 @@ package sidecar // (OPENCODEX_GO_HOTPATH_RELAY, Config.HotPathRelay) and a request qualifies, // the sidecar replaces the #24 private parent bridge with a direct upstream // relay for ONE provider class: a key-mode `openai-responses` provider whose -// Responses wire needs no translation, on a NON-STREAMING request. Everything -// else stays on the bridge so the TypeScript pipeline remains the oracle. +// Responses wire needs no translation. Non-streaming requests can use the +// whole-body repair below; streaming requests qualify only when their selected +// provider needs no stream-time rewrite. Everything else stays on the bridge +// so the TypeScript pipeline remains the oracle. // // The relay reproduces what the TS pipeline does for the qualifying subset, // byte for byte (verified against the TS oracle): @@ -89,6 +91,7 @@ type relayPlan struct { modelID string endpoint string // full POST target URL apiKey string // resolved bearer secret, "" when the provider has none + streaming bool } // resolveRelayAPIKey resolves a provider apiKey the way the TS key store does: @@ -216,10 +219,9 @@ func requestQualifiesForRelay(cfg Config, contentType string, headers http.Heade if parseErr != nil || root.Kind() != jsonwire.Object { return nil, refuseRelay("body is not a JSON object") } - if stream := bodyMember(root, "stream"); stream != nil { - if stream.Kind() == jsonwire.Bool && stream.Bool() { - return nil, refuseRelay("request is streaming") - } + streaming := false + if stream := bodyMember(root, "stream"); stream != nil && stream.Kind() == jsonwire.Bool { + streaming = stream.Bool() } modelValue := bodyMember(root, "model") if modelValue == nil || modelValue.Kind() != jsonwire.String { @@ -253,6 +255,13 @@ func requestQualifiesForRelay(cfg Config, contentType string, headers http.Heade return nil, refusal } plan.modelID = modelID + if streaming { + provider := providers.Find(plan.providerName) + if refusal := streamRelayRefusal(provider, modelID); refusal != nil { + return nil, refusal + } + plan.streaming = true + } return plan, nil } @@ -332,6 +341,58 @@ func requestBodyRelayRefusal(root *jsonwire.Value) *relayRefusal { return nil } +// streamRelayRefusal rejects a stream whenever the selected provider would +// make the TypeScript path rewrite client-facing SSE. Empty/false repair +// configuration remains inert and therefore relay-safe. Model-list matches +// mirror the case-insensitive check used by routeUsesContentChannelReasoning. +func streamRelayRefusal(provider *jsonwire.Value, modelID string) *relayRefusal { + if provider == nil || provider.Kind() != jsonwire.Object { + return refuseRelay("stream provider config unavailable") + } + if repair := provider.Find("responsesItemIdRepair"); responsesItemIDRepairArmed(repair) { + return refuseRelay("provider enables responsesItemIdRepair") + } + if snapshot, ok := boolMember(provider, "responsesSnapshotRepair"); ok && snapshot { + return refuseRelay("provider enables responsesSnapshotRepair") + } + if stateless, ok := boolMember(provider, "statelessResponses"); ok && stateless { + return refuseRelay("provider enables statelessResponses") + } + if modelInProviderList(provider.Find("preserveReasoningContentModels"), modelID) { + return refuseRelay("provider preserves reasoning content for model %q", modelID) + } + return nil +} + +func responsesItemIDRepairArmed(repair *jsonwire.Value) bool { + if repair == nil || repair.Kind() != jsonwire.Object { + return false + } + for _, key := range []string{"repairMissingTerminalIds", "repairInvalidIds"} { + if enabled, ok := boolMember(repair, key); ok && enabled { + return true + } + } + for _, key := range []string{"message", "reasoning"} { + if values := repair.Find(key); values != nil && values.Kind() == jsonwire.Array && len(values.Elements()) > 0 { + return true + } + } + return false +} + +func modelInProviderList(values *jsonwire.Value, modelID string) bool { + if values == nil || values.Kind() != jsonwire.Array { + return false + } + for _, value := range values.Elements() { + if value != nil && value.Kind() == jsonwire.String && strings.EqualFold(value.String(), modelID) { + return true + } + } + return false +} + // configLevelRelayRefusal rejects config shapes whose routing logic the relay // does not reproduce: combos, routing profiles, shadow intercept, and blocked // model redirects. The request stays on the bridge when any is present. @@ -532,6 +593,32 @@ func doDirectRelay(w http.ResponseWriter, r *http.Request, cfg Config, plan *rel } defer upstreamResp.Body.Close() + if plan.streaming { + contentType := upstreamResp.Header.Get("Content-Type") + if contentType != "" { + w.Header().Set("Content-Type", contentType) + } else if upstreamResp.StatusCode >= 200 && upstreamResp.StatusCode < 300 { + // The successful Responses SSE path in TypeScript defaults a + // missing upstream content type to the event-stream contract. + contentType = "text/event-stream" + w.Header().Set("Content-Type", contentType) + } else { + w.Header().Set("Content-Type", "application/json") + } + if retryAfter := upstreamResp.Header.Get("Retry-After"); relayRetryAfterValid(retryAfter) { + w.Header().Set("Retry-After", strings.TrimSpace(retryAfter)) + } + w.WriteHeader(upstreamResp.StatusCode) + if upstreamResp.StatusCode >= 200 && upstreamResp.StatusCode < 300 && strings.Contains(strings.ToLower(contentType), "text/event-stream") { + if err := relayResponsesSSEWithFlush(w, upstreamResp.Body); err != nil { + fmt.Fprintf(os.Stderr, "ocx-sidecar: relay stream write: %v\n", err) + } + } else if err := streamCopyWithFlush(w, upstreamResp.Body); err != nil { + fmt.Fprintf(os.Stderr, "ocx-sidecar: relay stream write: %v\n", err) + } + return + } + rawBody, readErr := io.ReadAll(io.LimitReader(upstreamResp.Body, maxRelayUpstreamBodyBytes+1)) if readErr != nil || len(rawBody) > maxRelayUpstreamBodyBytes { // Oversized or unreadable body: refuse like the TS bounded read fails @@ -540,7 +627,6 @@ func doDirectRelay(w http.ResponseWriter, r *http.Request, cfg Config, plan *rel w.Header().Set("Content-Type", "application/json") return } - contentType := upstreamResp.Header.Get("Content-Type") if contentType != "" { w.Header().Set("Content-Type", contentType) @@ -566,6 +652,75 @@ func doDirectRelay(w http.ResponseWriter, r *http.Request, cfg Config, plan *rel } } +// relayResponsesSSEWithFlush feeds upstream transport chunks through the +// Responses field-backfill and terminal boundary, flushing each emitted block. +// It stops reading after the first terminal so a gateway cannot append frames +// after completion and hold the client request open. +func relayResponsesSSEWithFlush(w http.ResponseWriter, src io.Reader) error { + stream := NewResponsesSSEStream() + flusher, canFlush := w.(http.Flusher) + write := func(out []byte) error { + if len(out) == 0 { + return nil + } + if _, err := w.Write(out); err != nil { + return err + } + if canFlush { + flusher.Flush() + } + return nil + } + buf := make([]byte, 32*1024) + for { + n, readErr := src.Read(buf) + if n > 0 { + out, err := stream.Feed(buf[:n]) + if err != nil { + return err + } + if err := write(out); err != nil { + return err + } + if stream.TerminalSeen() { + tail, err := stream.Finish() + if err != nil { + return err + } + return write(tail) + } + } + if readErr == io.EOF { + out, err := stream.Finish() + if err != nil { + return err + } + if err := write(out); err != nil { + return err + } + return nil + } + if readErr != nil { + partial, err := stream.FinishPartial() + if err != nil { + return err + } + if err := write(partial); err != nil { + return err + } + if stream.TerminalSeen() { + if !stream.DoneSeen() { + return write([]byte("data: [DONE]\n\n")) + } + return nil + } + // Go read errors have different text from Bun's fetch errors. Keep + // the documented static TS fallback envelope for byte-stable tails. + return write([]byte("\n\nevent: response.failed\ndata: {\"type\":\"response.failed\",\"response\":{\"status\":\"failed\",\"error\":{\"type\":\"upstream_error\",\"code\":\"upstream_reset\",\"message\":\"Upstream stream terminated unexpectedly\"},\"last_error\":{\"type\":\"upstream_error\",\"code\":\"upstream_reset\",\"message\":\"Upstream stream terminated unexpectedly\"}}}\n\ndata: [DONE]\n\n")) + } + } +} + // relayUpstreamClient reaches the configured provider without a system proxy // (the parent bridge transport contract) and without following redirects, so a // provider-supplied redirect can never carry the Authorization header to a diff --git a/go/internal/sidecar/hotpath_relay_test.go b/go/internal/sidecar/hotpath_relay_test.go index c5315fd70e..77eb0ccb18 100644 --- a/go/internal/sidecar/hotpath_relay_test.go +++ b/go/internal/sidecar/hotpath_relay_test.go @@ -249,12 +249,18 @@ func TestDirectRelayDropsInvalidRetryAfterAndRepairsPlain2xx(t *testing.T) { } } -// TestDirectRelayStreamingFallsBackToBridge: the relay claims non-streaming -// requests only. A streaming request on the same config must take the bridge -// path — proven by the 503 from the dead bridge (a relay would have answered -// 200 from the fixture upstream). -func TestDirectRelayStreamingFallsBackToBridge(t *testing.T) { - upstream := deadSparseUpstream(t, nil) +// TestDirectRelayStreamingRelaySafeRequest: a stream whose selected provider +// has no stream-time client rewrite can bypass the parent bridge. +func TestDirectRelayStreamingRelaySafeRequest(t *testing.T) { + const upstreamFixture = "event: response.completed\ndata: {\"type\":\"response.completed\"}\n\n" + const clientFixture = upstreamFixture + "data: [DONE]\n\n" + upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/v1/responses" { + t.Errorf("path = %s, want /v1/responses", r.URL.Path) + } + w.Header().Set("Content-Type", "text/event-stream") + _, _ = w.Write([]byte(upstreamFixture)) + })) defer upstream.Close() configDir := relayFixtureConfigDir(t, upstream.URL, nil) h := relaySeamHandler(t, configDir, true) @@ -262,8 +268,79 @@ func TestDirectRelayStreamingFallsBackToBridge(t *testing.T) { if err != nil { t.Fatal(err) } - if rec.Code != http.StatusServiceUnavailable { - t.Fatalf("status = %d, want 503 (streaming request must not reach the relay)", rec.Code) + if rec.Code != http.StatusOK { + t.Fatalf("status = %d, want 200 (relay-safe stream must reach the relay)", rec.Code) + } + if got := rec.Body.String(); got != clientFixture { + t.Fatalf("stream body = %q, want %q", got, clientFixture) + } +} + +func TestDirectRelayStreamingConfigGatesFallBackToBridge(t *testing.T) { + upstream := deadSparseUpstream(t, nil) + defer upstream.Close() + cases := []struct { + name string + extra map[string]any + want int + }{ + {"materially armed item ID repair", map[string]any{"responsesItemIdRepair": map[string]any{"repairInvalidIds": true}}, http.StatusServiceUnavailable}, + {"empty item ID repair remains relay-safe", map[string]any{"responsesItemIdRepair": map[string]any{"message": []any{}}}, http.StatusOK}, + {"snapshot repair", map[string]any{"responsesSnapshotRepair": true}, http.StatusServiceUnavailable}, + {"stateless Responses", map[string]any{"statelessResponses": true}, http.StatusServiceUnavailable}, + {"preserved reasoning model is case-insensitive", map[string]any{"preserveReasoningContentModels": []any{"TEST-MODEL"}}, http.StatusServiceUnavailable}, + {"different preserved reasoning model remains relay-safe", map[string]any{"preserveReasoningContentModels": []any{"other-model"}}, http.StatusOK}, + } + for _, c := range cases { + c := c + t.Run(c.name, func(t *testing.T) { + configDir := relayFixtureConfigDir(t, upstream.URL, c.extra) + h := relaySeamHandler(t, configDir, true) + rec, err := relayPost(t, h, `{"model":"test-model","input":"ping","stream":true}`) + if err != nil { + t.Fatal(err) + } + if rec.Code != c.want { + t.Fatalf("status = %d, want %d", rec.Code, c.want) + } + }) + } +} + +func TestStreamRelayQualificationConfigGates(t *testing.T) { + cases := []struct { + name string + extra map[string]any + want string + }{ + {"narrow stream qualifies", nil, ""}, + {"materially armed item ID repair refuses", map[string]any{"responsesItemIdRepair": map[string]any{"repairMissingTerminalIds": true}}, "responsesItemIdRepair"}, + {"empty item ID repair qualifies", map[string]any{"responsesItemIdRepair": map[string]any{"reasoning": []any{}}}, ""}, + {"snapshot repair refuses", map[string]any{"responsesSnapshotRepair": true}, "responsesSnapshotRepair"}, + {"stateless Responses refuses", map[string]any{"statelessResponses": true}, "statelessResponses"}, + {"preserved reasoning model match is case-insensitive", map[string]any{"preserveReasoningContentModels": []any{"TEST-MODEL"}}, "preserves reasoning content"}, + {"different preserved reasoning model qualifies", map[string]any{"preserveReasoningContentModels": []any{"other-model"}}, ""}, + } + for _, c := range cases { + c := c + t.Run(c.name, func(t *testing.T) { + dir := relayFixtureConfigDir(t, "https://upstream.example", c.extra) + plan, refusal := requestQualifiesForRelay( + Config{HotPathRelay: true, ConfigDir: dir}, + "application/json", + make(http.Header), + []byte(`{"model":"test-model","input":"ping","stream":true}`), + ) + if c.want == "" { + if plan == nil || !plan.streaming { + t.Fatalf("plan = %#v, refusal = %#v; want streaming relay plan", plan, refusal) + } + return + } + if plan != nil || refusal == nil || !strings.Contains(refusal.reason, c.want) { + t.Fatalf("plan = %#v, refusal = %#v; want refusal containing %q", plan, refusal, c.want) + } + }) } } diff --git a/go/internal/sidecar/sse_stream.go b/go/internal/sidecar/sse_stream.go new file mode 100644 index 0000000000..994beedba8 --- /dev/null +++ b/go/internal/sidecar/sse_stream.go @@ -0,0 +1,355 @@ +package sidecar + +// Incremental Responses SSE rewrite and terminal boundary for ticket #29. +// +// The direct streaming relay needs the same two guarantees as the TypeScript +// passthrough path: sparse Responses events are repaired before a strict +// client sees them, and a Responses terminal is a protocol boundary even when +// an upstream keeps its HTTP connection open. This file owns only the +// byte-stream state machine; relay wiring remains in the caller. + +import ( + "bytes" + "errors" + "math" + "strconv" + "sync/atomic" + + "github.com/lidge-jun/opencodex/go/internal/jsonwire" +) + +const maxResponsesSSEBlockBytes = 4 * 1024 * 1024 + +var errResponsesSSEBlockTooLarge = errors.New("Responses SSE block exceeds maximum size") + +var adapterEOFIncompletePayload = []byte("{\"type\":\"response.incomplete\",\"response\":{\"status\":\"incomplete\",\"incomplete_details\":{\"reason\":\"adapter_eof\"}}}") + +// syntheticSSEItemOrdinal follows the stateless TypeScript rewrite: malformed +// output indices get a process-global, lexically separate fallback namespace. +// Atomic increment keeps independently relayed streams race-free. +var syntheticSSEItemOrdinal atomic.Int64 + +// ResponsesSSEStream incrementally frames, repairs, and terminal-bounds one +// Responses SSE stream. Feed accepts arbitrary transport chunks; its output +// contains only complete client-dispatchable SSE events. Finish must be called +// once on clean upstream EOF to dispatch an unterminated final event and, if +// no Responses terminal was observed, synthesize adapter_eof plus [DONE]. +type ResponsesSSEStream struct { + buffer []byte + terminal bool + done bool + pendingDone []sseFrame +} + +type sseFrame struct { + block []byte + delimiter []byte +} + +// NewResponsesSSEStream creates a stream-local state machine. Do not share one +// instance between concurrent upstream Responses requests. +func NewResponsesSSEStream() *ResponsesSSEStream { return &ResponsesSSEStream{} } + +// TerminalSeen reports whether a response.completed, response.failed, or +// response.incomplete event crossed the client boundary. +func (s *ResponsesSSEStream) TerminalSeen() bool { return s.terminal } + +// DoneSeen reports whether upstream supplied any [DONE] data event. A [DONE] +// before a Responses terminal is held until a terminal arrives. +func (s *ResponsesSSEStream) DoneSeen() bool { return s.done } + +// Feed processes arbitrary upstream bytes. Once the first Responses terminal +// has been seen, future chunks are ignored. Frames already in this call retain +// the TS boundary behavior: later ordinary events are dropped but later [DONE] +// frames are retained. +func (s *ResponsesSSEStream) Feed(chunk []byte) ([]byte, error) { + if s.terminal || len(chunk) == 0 { + return nil, nil + } + s.buffer = append(s.buffer, chunk...) + var out bytes.Buffer + for { + at, delimiterLen, incomplete := sseDelimiter(s.buffer) + if incomplete || at < 0 { + if len(s.buffer) > maxResponsesSSEBlockBytes { + return nil, errResponsesSSEBlockTooLarge + } + break + } + frame := sseFrame{ + block: append([]byte(nil), s.buffer[:at]...), + delimiter: append([]byte(nil), s.buffer[at:at+delimiterLen]...), + } + s.buffer = append(s.buffer[:0], s.buffer[at+delimiterLen:]...) + s.processFrame(&out, frame) + } + return out.Bytes(), nil +} + +// Finish closes a clean upstream stream. An incomplete final frame receives a +// synthetic blank-line delimiter before rewriting so clients can dispatch it. +func (s *ResponsesSSEStream) Finish() ([]byte, error) { + var out bytes.Buffer + _, err := s.finishPartial(&out) + if err != nil { + return nil, err + } + if s.terminal { + if !s.done { + out.WriteString("data: [DONE]\n\n") + } + return out.Bytes(), nil + } + out.WriteString("event: response.incomplete\ndata: ") + out.Write(adapterEOFIncompletePayload) + out.WriteString("\n\n") + out.WriteString("data: [DONE]\n\n") + return out.Bytes(), nil +} + +// FinishPartial synthetically delimits and rewrites the retained tail without +// inventing an adapter_eof terminal or [DONE]. Use it on an upstream read error +// before the relay writes its response.failed tail. +func (s *ResponsesSSEStream) FinishPartial() ([]byte, error) { + var out bytes.Buffer + _, err := s.finishPartial(&out) + return out.Bytes(), err +} + +func (s *ResponsesSSEStream) finishPartial(out *bytes.Buffer) (bool, error) { + if s.terminal { + return true, nil + } + if len(s.buffer) == 0 { + return false, nil + } + delimiter := []byte("\n\n") + if bytes.Contains(s.buffer, []byte("\r\n")) { + delimiter = []byte("\r\n\r\n") + } + frame := sseFrame{block: append([]byte(nil), s.buffer...), delimiter: delimiter} + s.buffer = nil + s.processFrame(out, frame) + return s.terminal, nil +} + +func (s *ResponsesSSEStream) processFrame(out *bytes.Buffer, frame sseFrame) { + payload, hasData := sseDataPayloadBytes(frame.block) + if hasData && string(payload) == "[DONE]" { + s.done = true + if s.terminal { + out.Write(frame.block) + out.Write(frame.delimiter) + } else if s.pendingDone == nil { + s.pendingDone = []sseFrame{frame} + } + return + } + if s.terminal { + return + } + rewritten := s.rewriteBlock(frame.block, payload, hasData) + out.Write(rewritten) + out.Write(frame.delimiter) + if hasData && responsesSSETerminal(payload) { + s.terminal = true + for _, pending := range s.pendingDone { + out.Write(pending.block) + out.Write(pending.delimiter) + } + s.pendingDone = nil + } +} + +// sseDelimiter finds the first valid SSE blank-line delimiter. incomplete is +// true when the buffer ends inside a candidate delimiter. +func sseDelimiter(data []byte) (at, length int, incomplete bool) { + for i := 0; i < len(data); i++ { + if data[i] == '\n' { + if i+1 == len(data) { + return -1, 0, true + } + if data[i+1] == '\n' { + return i, 2, false + } + if data[i+1] == '\r' { + if i+2 == len(data) { + return -1, 0, true + } + if data[i+2] == '\n' { + return i, 3, false + } + } + } + if data[i] == '\r' && i+1 < len(data) && data[i+1] == '\n' { + if i+2 == len(data) { + return -1, 0, true + } + if data[i+2] == '\n' { + return i, 3, false + } + if data[i+2] == '\r' { + if i+3 == len(data) { + return -1, 0, true + } + if data[i+3] == '\n' { + return i, 4, false + } + } + } + } + return -1, 0, false +} + +func sseDataPayloadBytes(block []byte) ([]byte, bool) { + var payload []byte + found := false + for _, line := range bytes.Split(block, []byte("\n")) { + line = bytes.TrimSuffix(line, []byte("\r")) + if !bytes.HasPrefix(line, []byte("data:")) { + continue + } + value := line[len("data:"):] + if len(value) > 0 && value[0] == ' ' { + value = value[1:] + } + if found { + payload = append(payload, '\n') + } + payload = append(payload, value...) + found = true + } + return payload, found +} + +func (s *ResponsesSSEStream) rewriteBlock(block, payload []byte, hasData bool) []byte { + if !hasData { + return block + } + event, err := jsonwire.Parse(payload) + if err != nil || event.Kind() != jsonwire.Object || !s.rewriteEvent(event) { + return block + } + encoded, err := event.Encode() + if err != nil { + return block + } + return replaceSSEDataPayload(block, encoded) +} + +func (s *ResponsesSSEStream) rewriteEvent(event *jsonwire.Value) bool { + typeName, _ := stringMember(event, "type") + inferred := inferredSSEItemStatus(typeName) + changed := false + if typeName == "response.output_item.added" || typeName == "response.output_item.done" { + if item := event.Find("item"); item != nil && item.Kind() == jsonwire.Object { + index, ok := sseOutputIndex(event.Find("output_index")) + if !ok { + slot := "fallback_" + strconv.FormatInt(syntheticSSEItemOrdinal.Add(1), 10) + changed = backfillSSEOutputItem(item, slot, inferred) || changed + } else { + changed = backfillSSEOutputItem(item, index, inferred) || changed + } + } + } + if typeName == "response.content_part.added" || typeName == "response.content_part.done" { + if _, repaired := backfillOutputTextPart(event.Find("part")); repaired { + changed = true + } + } + if response := event.Find("response"); response != nil && response.Kind() == jsonwire.Object { + responseStatus := inferred + if raw, ok := stringMember(response, "status"); ok { + if mapped, ok := responseStatusToItemStatus(raw); ok { + responseStatus = mapped + } + } + if output := response.Find("output"); output != nil && output.Kind() == jsonwire.Array { + for index, item := range output.Elements() { + changed = backfillSSEOutputItem(item, itoa(index), responseStatus) || changed + } + } + } + return changed +} + +func inferredSSEItemStatus(typeName string) string { + switch typeName { + case "response.output_item.added", "response.created", "response.in_progress", "response.queued": + return "in_progress" + case "response.incomplete", "response.failed": + return "incomplete" + default: + return "completed" + } +} + +func sseOutputIndex(value *jsonwire.Value) (string, bool) { + if value == nil || value.Kind() != jsonwire.Number { + return "", false + } + parsed, err := strconv.ParseFloat(value.NumberRaw(), 64) + if err != nil || math.IsNaN(parsed) || math.IsInf(parsed, 0) || parsed < 0 || math.Trunc(parsed) != parsed { + return "", false + } + return jsonwire.FormatV8Number(parsed), true +} + +func backfillSSEOutputItem(item *jsonwire.Value, slot, inferredStatus string) bool { + if item == nil || item.Kind() != jsonwire.Object { + return false + } + typeName, _ := stringMember(item, "type") + if compactionItemTypes[typeName] { + return false + } + changed := backfillContentArray(item.Find("content")) + if id := item.Find("id"); id == nil || id.Kind() != jsonwire.String || id.String() == "" { + prefix, ok := responsesItemIDPrefixes[typeName] + if !ok { + prefix = "item_" + } + item.Set("id", jsonwire.StringValue(prefix+"ocx_"+slot)) + changed = true + } + changed = backfillItemStatus(item, inferredStatus) || changed + return changed +} + +func responsesSSETerminal(payload []byte) bool { + event, err := jsonwire.Parse(payload) + if err != nil || event.Kind() != jsonwire.Object { + return false + } + typeName, _ := stringMember(event, "type") + return typeName == "response.completed" || typeName == "response.failed" || typeName == "response.incomplete" +} + +func replaceSSEDataPayload(block, payload []byte) []byte { + newline := []byte("\n") + if bytes.Contains(block, []byte("\r\n")) { + newline = []byte("\r\n") + } + lines := bytes.Split(block, []byte("\n")) + var out bytes.Buffer + replaced := false + for index, original := range lines { + line := bytes.TrimSuffix(original, []byte("\r")) + if index > 0 { + out.Write(newline) + } + if bytes.HasPrefix(line, []byte("data:")) { + if !replaced { + out.WriteString("data: ") + out.Write(payload) + replaced = true + } + continue + } + out.Write(line) + } + if !replaced { + return block + } + return out.Bytes() +} diff --git a/go/internal/sidecar/sse_stream_test.go b/go/internal/sidecar/sse_stream_test.go new file mode 100644 index 0000000000..2928a7876d --- /dev/null +++ b/go/internal/sidecar/sse_stream_test.go @@ -0,0 +1,167 @@ +package sidecar + +import ( + "bytes" + "encoding/json" + "errors" + "os" + "path/filepath" + "testing" +) + +type responsesSSEGolden struct { + Name string `json:"name"` + Upstream string `json:"upstream"` + Client string `json:"client"` +} + +func loadResponsesSSEGoldens(t *testing.T) []responsesSSEGolden { + t.Helper() + raw, err := os.ReadFile(filepath.Join("testdata", "responses-sse-goldens.json")) + if err != nil { + t.Fatalf("read goldens: %v", err) + } + var goldens []responsesSSEGolden + if err := json.Unmarshal(raw, &goldens); err != nil { + t.Fatalf("decode goldens: %v", err) + } + if len(goldens) == 0 { + t.Fatal("golden file is empty") + } + return goldens +} + +func rewriteSSEInChunks(t *testing.T, input string, chunks []int) (string, *ResponsesSSEStream) { + t.Helper() + stream := NewResponsesSSEStream() + var out bytes.Buffer + position := 0 + for _, size := range chunks { + if position == len(input) { + break + } + end := position + size + if end > len(input) { + end = len(input) + } + got, err := stream.Feed([]byte(input[position:end])) + if err != nil { + t.Fatalf("feed at byte %d: %v", position, err) + } + out.Write(got) + position = end + } + if position != len(input) { + got, err := stream.Feed([]byte(input[position:])) + if err != nil { + t.Fatalf("final feed: %v", err) + } + out.Write(got) + } + got, err := stream.Finish() + if err != nil { + t.Fatalf("finish: %v", err) + } + out.Write(got) + return out.String(), stream +} + +// TestResponsesSSEGoldens runs every row captured from the TypeScript stream +// oracle. The one-byte pass proves no transport chunk is treated as an event +// boundary, including CRLF delimiters split across reads and terminal frames +// with no trailing blank line. +func TestResponsesSSEGoldens(t *testing.T) { + for _, golden := range loadResponsesSSEGoldens(t) { + golden := golden + t.Run(golden.Name, func(t *testing.T) { + whole, stream := rewriteSSEInChunks(t, golden.Upstream, []int{len(golden.Upstream)}) + if whole != golden.Client { + t.Fatalf("whole rewrite diverged from TS oracle\n got: %q\nwant: %q", whole, golden.Client) + } + _ = stream + chunks := make([]int, len(golden.Upstream)) + for index := range chunks { + chunks[index] = 1 + } + fragmented, _ := rewriteSSEInChunks(t, golden.Upstream, chunks) + if fragmented != golden.Client { + t.Fatalf("one-byte rewrite diverged from TS oracle\n got: %q\nwant: %q", fragmented, golden.Client) + } + }) + } +} + +func TestResponsesSSEBoundaryDropsPostTerminalEventsButKeepsDone(t *testing.T) { + input := "event: response.completed\ndata: {\"type\":\"response.completed\",\"response\":{\"status\":\"completed\"}}\n\nevent: ignored\ndata: {\"type\":\"response.output_text.delta\",\"delta\":\"late\"}\n\ndata: [DONE]\n\n" + got, stream := rewriteSSEInChunks(t, input, []int{len(input)}) + if !stream.TerminalSeen() || !stream.DoneSeen() { + t.Fatalf("terminal=%v done=%v, want true true", stream.TerminalSeen(), stream.DoneSeen()) + } + if bytes.Contains([]byte(got), []byte("late")) { + t.Fatalf("late event crossed terminal boundary: %q", got) + } + if !bytes.Contains([]byte(got), []byte("data: [DONE]")) { + t.Fatalf("done after terminal missing: %q", got) + } +} + +func TestResponsesSSEBoundaryHoldsPrematureDoneUntilTerminal(t *testing.T) { + input := "data: [DONE]\n\nevent: response.completed\ndata: {\"type\":\"response.completed\",\"response\":{\"status\":\"completed\"}}\n\n" + got, _ := rewriteSSEInChunks(t, input, []int{12, len(input)}) + terminal := bytes.Index([]byte(got), []byte("response.completed")) + done := bytes.Index([]byte(got), []byte("data: [DONE]")) + if terminal < 0 || done < terminal { + t.Fatalf("premature done leaked before terminal: %q", got) + } +} + +func TestResponsesSSEBlockLimit(t *testing.T) { + stream := NewResponsesSSEStream() + _, err := stream.Feed(bytes.Repeat([]byte("x"), maxResponsesSSEBlockBytes+1)) + if !errors.Is(err, errResponsesSSEBlockTooLarge) { + t.Fatalf("error = %v, want block limit", err) + } +} + +func TestResponsesSSEFinishPartialDoesNotInventTerminal(t *testing.T) { + stream := NewResponsesSSEStream() + if _, err := stream.Feed([]byte("event: response.output_item.added\ndata: {\"type\":\"response.output_item.added\",\"output_index\":0,\"item\":{\"type\":\"message\",\"content\":[{\"type\":\"output_text\",\"text\":\"partial\"}]}}")); err != nil { + t.Fatal(err) + } + partial, err := stream.FinishPartial() + if err != nil { + t.Fatal(err) + } + if bytes.Contains(partial, []byte("adapter_eof")) || bytes.Contains(partial, []byte("[DONE]")) { + t.Fatalf("partial finish invented a terminal: %q", partial) + } + if !bytes.Contains(partial, []byte("annotations")) || !bytes.Contains(partial, []byte("msg_ocx_0")) { + t.Fatalf("partial tail was not rewritten: %q", partial) + } +} + +func TestResponsesSSEMalformedOutputIndexUsesProcessGlobalFallback(t *testing.T) { + start := syntheticSSEItemOrdinal.Load() + input := "event: response.output_item.added\ndata: {\"type\":\"response.output_item.added\",\"item\":{\"type\":\"message\",\"content\":[]}}\n\n" + first, _ := rewriteSSEInChunks(t, input, []int{len(input)}) + second, _ := rewriteSSEInChunks(t, input, []int{len(input)}) + firstID := "msg_ocx_fallback_" + itoa(int(start+1)) + secondID := "msg_ocx_fallback_" + itoa(int(start+2)) + if !bytes.Contains([]byte(first), []byte(firstID)) || !bytes.Contains([]byte(second), []byte(secondID)) { + t.Fatalf("fallback IDs = %q, %q; want %q, %q", first, second, firstID, secondID) + } +} + +func TestResponsesSSEOutputIndexUsesNumberIsIntegerSemantics(t *testing.T) { + cases := map[string]string{"1.0": "1", "1e0": "1", "1e3": "1000"} + for index, slot := range cases { + t.Run(index, func(t *testing.T) { + input := "event: response.output_item.added\ndata: {\"type\":\"response.output_item.added\",\"output_index\":" + index + ",\"item\":{\"type\":\"message\",\"content\":[]}}\n\n" + got, _ := rewriteSSEInChunks(t, input, []int{len(input)}) + want := "msg_ocx_" + slot + if !bytes.Contains([]byte(got), []byte(want)) { + t.Fatalf("output = %q, want %q", got, want) + } + }) + } +} diff --git a/tests/go-hotpath-relay-streaming.test.ts b/tests/go-hotpath-relay-streaming.test.ts new file mode 100644 index 0000000000..65da3aa9be --- /dev/null +++ b/tests/go-hotpath-relay-streaming.test.ts @@ -0,0 +1,312 @@ +import { afterAll, beforeAll, describe, expect, test } from "bun:test"; +import { readFileSync, mkdtempSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { SERVER_BUDGET_MS } from "./helpers/test-budget"; +import { saveConfig } from "../src/config"; +import { startServer } from "../src/server"; +import { + GO_SIDECAR_BIN_ENV, + activeGoSidecarBaseUrl, + resetGoSidecarForTests, +} from "../src/server/go-sidecar"; +import { HOT_PATH_RELAY_ENV, HOT_PATH_SEAM_ENV } from "../src/server/hot-path-seam"; +import { removeTreeWithRetry } from "./helpers/remove-tree"; + +/** + * End-to-end differential for ticket #29's direct streaming relay. Every + * input comes from the committed Responses SSE corpus: server A is the TS + * oracle, server B is the armed Go relay, and server C proves a stream-time + * provider feature returns to the Bun bridge. The fixture observes the + * upstream User-Agent, while clients compare the exact SSE bytes. + */ + +const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); + +interface ResponsesSSEGolden { + name: string; + upstream: string; + client: string; + status: number; + contentType: string; + upstreamBody: string; +} + +const goldens = JSON.parse( + readFileSync(join(repoRoot, "go/internal/sidecar/testdata/responses-sse-goldens.json"), "utf8"), +) as ResponsesSSEGolden[]; + +if (goldens.length === 0) throw new Error("Responses SSE golden corpus must not be empty"); + +function goToolchainAvailable(): boolean { + const probe = Bun.spawnSync(["go", "version"], { stdout: "ignore", stderr: "ignore" }); + return probe.success; +} + +function buildSidecarBinary(): string { + const dir = mkdtempSync(join(tmpdir(), "ocx-go-sidecar-stream-relay-")); + const binPath = join(dir, process.platform === "win32" ? "ocx-sidecar.exe" : "ocx-sidecar"); + const build = Bun.spawnSync( + ["go", "build", "-o", binPath, "./cmd/ocx-sidecar"], + { + cwd: join(repoRoot, "go"), + env: { ...process.env, CGO_ENABLED: "0" }, + stdout: "pipe", + stderr: "pipe", + }, + ); + if (build.exitCode !== 0) { + throw new Error( + `go build ./cmd/ocx-sidecar failed (${build.exitCode}):\n${new TextDecoder().decode(build.stderr)}`, + ); + } + return binPath; +} + +const goAvailable = goToolchainAvailable(); +const sidecarBinary: string | null = goAvailable ? buildSidecarBinary() : null; +const GO_UA = "Go-http-client/1.1"; + +interface UpstreamLog { + ua: string; + method: string; + path: string; + contentType: string | null; + body: string; +} + +interface ResponseCapture { + status: number; + contentType: string | null; + body: string; + chunks: string[]; +} + +const upstreamLogs: UpstreamLog[] = []; +let upstream: ReturnType | null = null; +const previousEnv: Record = {}; +let testHome = ""; + +function configFixture(upstreamPort: number, providerOverrides: Record = {}) { + return { + port: 0, + hostname: "127.0.0.1", + defaultProvider: "test", + providers: { + test: { + adapter: "openai-responses", + baseUrl: `http://127.0.0.1:${upstreamPort}/v1`, + allowPrivateNetwork: true, + disabled: false, + models: ["test-model"], + ...providerOverrides, + }, + }, + }; +} + +async function postCase(server: { url: URL }, token: string, body: string): Promise { + const response = await fetch(new URL("/v1/responses", server.url), { + method: "POST", + headers: { "content-type": "application/json", "x-opencodex-api-key": token }, + body, + }); + const reader = response.body?.getReader(); + const decoder = new TextDecoder(); + const chunks: string[] = []; + if (reader) { + for (;;) { + const { done, value } = await reader.read(); + if (done) break; + chunks.push(decoder.decode(value, { stream: true })); + } + const tail = decoder.decode(); + if (tail) chunks.push(tail); + } + return { status: response.status, contentType: response.headers.get("content-type"), body: chunks.join(""), chunks }; +} + +function splitSseFrames(body: string): string[] { + return body.split(/(?<=\n\n)|(?<=\n\r\n)|(?<=\r\n\n)|(?<=\r\n\r\n)/).filter(Boolean); +} + +function streamFixture(text: string): ReadableStream { + const bytes = new TextEncoder().encode(text); + let offset = 0; + return new ReadableStream({ + async pull(controller) { + if (offset >= bytes.length) { controller.close(); return; } + const end = Math.min(offset + 7, bytes.length); + controller.enqueue(bytes.slice(offset, end)); + offset = end; + // Split each corpus event through real network-facing pulls rather than + // treating a fixture string as one transport chunk. + await Bun.sleep(1); + }, + }); +} + +function captureEnv(): void { + for (const name of [GO_SIDECAR_BIN_ENV, HOT_PATH_SEAM_ENV, HOT_PATH_RELAY_ENV, "OPENCODEX_HOME", "OPENCODEX_API_AUTH_TOKEN"]) { + previousEnv[name] = process.env[name]; + } +} + +function setUpFixture(upstreamPort: number, providerOverrides?: Record): void { + testHome = mkdtempSync(join(tmpdir(), "ocx-hotpath-stream-relay-")); + process.env.OPENCODEX_HOME = testHome; + process.env.OPENCODEX_API_AUTH_TOKEN = "data-secret"; + saveConfig(configFixture(upstreamPort, providerOverrides)); +} + +function tearDownFixture(): void { + resetGoSidecarForTests(); + for (const [name, value] of Object.entries(previousEnv)) { + if (value === undefined) delete process.env[name]; + else process.env[name] = value; + } + if (testHome) { + removeTreeWithRetry(testHome); + testHome = ""; + } +} + +async function waitFor(probe: () => T | null | undefined, timeoutMs: number): Promise { + const deadline = Date.now() + timeoutMs; + for (;;) { + const value = probe(); + if (value !== null && value !== undefined) return value; + if (Date.now() >= deadline) throw new Error(`condition not met within ${timeoutMs}ms`); + await Bun.sleep(50); + } +} + +function runFixtureTest(name: string, fn: () => Promise): void { + test(name, async () => { + captureEnv(); + try { + await fn(); + } finally { + tearDownFixture(); + } + }, SERVER_BUDGET_MS); +} + +describe.skipIf(!goAvailable || sidecarBinary === null)("ocx-sidecar streaming relay differential (ADR-0008, ticket #29)", () => { + beforeAll(() => { + upstream = Bun.serve({ + hostname: "127.0.0.1", + port: 0, + async fetch(req) { + if (new URL(req.url).pathname !== "/v1/responses") return new Response("nf", { status: 404 }); + const body = await req.text(); + upstreamLogs.push({ + ua: req.headers.get("user-agent") ?? "", + method: req.method, + path: new URL(req.url).pathname, + contentType: req.headers.get("content-type"), + body, + }); + const golden = goldens.find((row) => row.upstreamBody === body); + if (!golden) return new Response("unknown SSE golden request", { status: 400 }); + return new Response(streamFixture(golden.upstream), { status: golden.status, headers: { "content-type": golden.contentType } }); + }, + }); + }); + + afterAll(() => { + upstream?.stop(true); + upstream = null; + }); + + test("the relay env gate is a declared constant", () => { + expect(HOT_PATH_RELAY_ENV).toBe("OPENCODEX_GO_HOTPATH_RELAY"); + }); + + runFixtureTest("armed relay matches every Responses SSE golden and sends them direct", async () => { + const token = "data-secret"; + const port = upstream!.port; + + // Server A: the in-process TS oracle. + setUpFixture(port); + const serverA = startServer(0); + const tsCaptures: ResponseCapture[] = []; + try { + for (const golden of goldens) tsCaptures.push(await postCase(serverA, token, golden.upstreamBody)); + } finally { + await serverA.stop(true); + } + const tsLogs = upstreamLogs.slice(-goldens.length); + expect(tsLogs).toHaveLength(goldens.length); + for (let index = 0; index < goldens.length; index++) { + const golden = goldens[index]!; + const capture = tsCaptures[index]!; + expect(tsLogs[index]!.ua, `${golden.name} oracle path`).not.toBe(GO_UA); + expect(tsLogs[index]!.body, `${golden.name} oracle request bytes`).toBe(golden.upstreamBody); + expect(capture.status, `${golden.name} oracle status`).toBe(golden.status); + expect(capture.contentType, `${golden.name} oracle content type`).toBe(golden.contentType); + expect(capture.body, `${golden.name} oracle must match its committed SSE golden`).toBe(golden.client); + } + + // Server B: seam and streaming relay armed. Its upstream user agent is + // the direct-path proof; client-visible bytes must stay identical to A. + process.env[GO_SIDECAR_BIN_ENV] = sidecarBinary!; + process.env[HOT_PATH_SEAM_ENV] = "1"; + process.env[HOT_PATH_RELAY_ENV] = "1"; + const serverB = startServer(0); + const goCaptures: ResponseCapture[] = []; + try { + await waitFor(() => activeGoSidecarBaseUrl(), 15_000); + for (const golden of goldens) goCaptures.push(await postCase(serverB, token, golden.upstreamBody)); + } finally { + await serverB.stop(true); + } + const goLogs = upstreamLogs.slice(-goldens.length); + expect(goLogs).toHaveLength(goldens.length); + for (let index = 0; index < goldens.length; index++) { + const golden = goldens[index]!; + const capture = goCaptures[index]!; + expect(goLogs[index]!.ua, `${golden.name} must be a direct Go relay`).toBe(GO_UA); + expect(goLogs[index]!.method, `${golden.name} method`).toBe("POST"); + expect(goLogs[index]!.path, `${golden.name} path`).toBe("/v1/responses"); + expect(goLogs[index]!.contentType, `${golden.name} content type`).toBe("application/json"); + expect(goLogs[index]!.body, `${golden.name} direct request bytes`).toBe(golden.upstreamBody); + expect(capture.status, `${golden.name} direct status`).toBe(tsCaptures[index]!.status); + expect(capture.contentType, `${golden.name} direct content type`).toBe(tsCaptures[index]!.contentType); + expect(capture.body, `${golden.name} direct bytes must match the TS oracle`).toBe(tsCaptures[index]!.body); + expect(splitSseFrames(capture.body), `${golden.name} direct frame sequence`).toEqual(splitSseFrames(tsCaptures[index]!.body)); + expect(capture.chunks.length, `${golden.name} client observed stream output`).toBeGreaterThan(0); + } + + // These rows exercise stream repair. If the upstream bytes already equal + // the client bytes, equality above would not prove the rewriter ran. + for (const golden of goldens) { + expect(golden.client, `${golden.name} must contain a non-vacuous mutation`).not.toBe(golden.upstream); + const frames = splitSseFrames(golden.client); + expect(frames.length, `${golden.name} has multiple ordered client frames`).toBeGreaterThan(1); + expect(frames.slice(1).join(""), `${golden.name} dropping a frame differs`).not.toBe(golden.client); + expect([...frames].reverse().join(""), `${golden.name} reordering frames differs`).not.toBe(golden.client); + expect([...frames, frames[0]!].join(""), `${golden.name} duplicating a frame differs`).not.toBe(golden.client); + } + + // Server C: this enabled stream-time repair is deliberately outside the + // direct relay subset, so the same requests must return through Bun. + saveConfig(configFixture(port, { responsesItemIdRepair: { repairInvalidIds: true } })); + const serverC = startServer(0); + try { + await waitFor(() => activeGoSidecarBaseUrl(), 15_000); + for (const golden of goldens) await postCase(serverC, token, golden.upstreamBody); + } finally { + await serverC.stop(true); + } + const fallbackLogs = upstreamLogs.slice(-goldens.length); + expect(fallbackLogs).toHaveLength(goldens.length); + for (let index = 0; index < goldens.length; index++) { + const golden = goldens[index]!; + expect(fallbackLogs[index]!.ua, `${golden.name} config gate must use the Bun bridge`).not.toBe(GO_UA); + expect(fallbackLogs[index]!.body, `${golden.name} fallback request bytes`).toBe(golden.upstreamBody); + } + expect(activeGoSidecarBaseUrl()).toBeNull(); + }); +}); diff --git a/tests/go-hotpath-relay.test.ts b/tests/go-hotpath-relay.test.ts index b60567a2bd..7fe98e6d34 100644 --- a/tests/go-hotpath-relay.test.ts +++ b/tests/go-hotpath-relay.test.ts @@ -120,9 +120,9 @@ const relayCases: RelayCase[] = [ ]; const streamCase: RelayCase = { - name: "streaming-stays-on-bridge", + name: "streaming-relays-directly", body: { model: "test-model", input: "stream", stream: true }, - direct: false, + direct: true, }; const streamUpstreamReply = @@ -332,8 +332,7 @@ describe.skipIf(!goAvailable || sidecarBinary === null)("ocx-sidecar non-streami } // Path proof: the fixture upstream must have seen the Go http client for - // every relay-admitted case and must NOT have seen it for the streaming - // refusal (which still ran through the Bun bridge). + // every relay-admitted case, including the narrow stream subset. const goLogs = upstreamLogs.slice(allCases.length); expect(goLogs.length).toBe(allCases.length); for (let i = 0; i < allCases.length; i++) { From 1ef093748848137d155d4cdef49e8c04a00e2bc8 Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Sun, 6 Sep 2026 23:20:15 +0800 Subject: [PATCH 039/165] test(go): admit narrow streaming relay fixture --- go/internal/sidecar/hotpath_relay_test.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/go/internal/sidecar/hotpath_relay_test.go b/go/internal/sidecar/hotpath_relay_test.go index 77eb0ccb18..7ff73e8602 100644 --- a/go/internal/sidecar/hotpath_relay_test.go +++ b/go/internal/sidecar/hotpath_relay_test.go @@ -376,7 +376,7 @@ func TestRequestQualifiesForRelayRefusals(t *testing.T) { }{ {"happy path qualifies", nil, `{"model":"test-model","input":"ping"}`, nil, ""}, {"unknown model routes to default provider", nil, `{"model":"other-model","input":"ping"}`, nil, ""}, - {"streaming refuses", nil, `{"model":"test-model","input":"ping","stream":true}`, nil, "streaming"}, + {"narrow streaming request qualifies", nil, `{"model":"test-model","input":"ping","stream":true}`, nil, ""}, {"namespaced model refuses", nil, `{"model":"test/test-model","input":"ping"}`, nil, "namespaced"}, {"empty model refuses", nil, `{"model":"","input":"ping"}`, nil, "model id is empty"}, {"previous_response_id refuses", nil, `{"model":"test-model","input":"ping","previous_response_id":"x"}`, nil, "previous_response_id"}, @@ -442,6 +442,9 @@ func TestRequestQualifiesForRelayRefusals(t *testing.T) { if plan == nil { t.Fatalf("expected a plan, got refusal %q", refusal.reason) } + if strings.Contains(c.name, "streaming") && !plan.streaming { + t.Fatalf("plan = %#v, want streaming relay plan", plan) + } return } if plan != nil { From 9584069b199530df6a8fa4f1d7b322688b0f6852 Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Sun, 6 Sep 2026 23:30:22 +0800 Subject: [PATCH 040/165] fix(go): preserve relay and WebSocket parity --- go/internal/sidecar/hotpath_relay.go | 6 + go/internal/sidecar/hotpath_relay_test.go | 1 + go/internal/sidecar/ws_bridge.go | 115 +++++++++++++----- go/internal/sidecar/ws_bridge_test.go | 141 ++++++++++++++++++++++ src/server/go-sidecar-ws-bridge.ts | 17 ++- src/server/go-sidecar.ts | 21 +++- src/server/index.ts | 15 ++- src/server/ws-bridge.ts | 34 ++++++ tests/go-sidecar-ws-bridge.test.ts | 36 ++++++ tests/go-ws-bridge-parity.test.ts | 43 +++++++ 10 files changed, 389 insertions(+), 40 deletions(-) create mode 100644 tests/go-sidecar-ws-bridge.test.ts diff --git a/go/internal/sidecar/hotpath_relay.go b/go/internal/sidecar/hotpath_relay.go index 106eef4285..c3795a1c74 100644 --- a/go/internal/sidecar/hotpath_relay.go +++ b/go/internal/sidecar/hotpath_relay.go @@ -516,6 +516,12 @@ func relayPlanForProvider(name string, provider *jsonwire.Value) (*relayPlan, *r if provider.Find("responsesPath") != nil { return nil, refuseRelay("provider %q configures a custom responsesPath", name) } + if headers := provider.Find("headers"); headers != nil && headers.Kind() == jsonwire.Object && len(headers.Members()) > 0 { + // The direct relay owns only the canonical generated headers. Provider + // headers may override or extend adapter output, so keep these rows on + // the bridge until their exact adapter precedence is ported. + return nil, refuseRelay("provider %q configures custom headers", name) + } apiKey := "" if raw, ok := stringMember(provider, "apiKey"); ok { var keyOK bool diff --git a/go/internal/sidecar/hotpath_relay_test.go b/go/internal/sidecar/hotpath_relay_test.go index 7ff73e8602..26a4bdfc86 100644 --- a/go/internal/sidecar/hotpath_relay_test.go +++ b/go/internal/sidecar/hotpath_relay_test.go @@ -391,6 +391,7 @@ func TestRequestQualifiesForRelayRefusals(t *testing.T) { {"non-responses adapter refuses", map[string]any{"adapter": "anthropic"}, `{"model":"test-model","input":"ping"}`, nil, "not openai-responses"}, {"keychain apiKey refuses", map[string]any{"apiKey": "keychain:prod"}, `{"model":"test-model","input":"ping"}`, nil, "keychain"}, {"custom responsesPath refuses", map[string]any{"responsesPath": "/chat"}, `{"model":"test-model","input":"ping"}`, nil, "responsesPath"}, + {"custom provider headers refuse", map[string]any{"headers": map[string]any{"X-Provider-Key": "secret"}}, `{"model":"test-model","input":"ping"}`, nil, "custom headers"}, {"default provider absent refuses", map[string]any{"defaultProvider": "gone"}, `{"model":"other-model","input":"ping"}`, nil, "no provider owns model"}, } for _, c := range cases { diff --git a/go/internal/sidecar/ws_bridge.go b/go/internal/sidecar/ws_bridge.go index 47556d6476..576750796b 100644 --- a/go/internal/sidecar/ws_bridge.go +++ b/go/internal/sidecar/ws_bridge.go @@ -9,8 +9,10 @@ import ( "encoding/base64" "encoding/binary" "encoding/json" + "errors" "io" "net/http" + "strconv" "strings" "github.com/lidge-jun/opencodex/go/internal/managementauth" @@ -20,6 +22,7 @@ const ( ResponsesWSBridgePath = "/v1/responses/ws-bridge" ResponsesWSParentBridgePath = "/__ocx_go_sidecar/responses-ws" maxWSFrameBytes = 50 * 1024 * 1024 + maxSynthesizedOutputItems = 10000 ) type wsBridgeRequest struct { @@ -204,56 +207,112 @@ func bridgeWSFrames(w *bufio.Writer, cfg Config, input wsBridgeRequest) { sendJSONEvents(w, v) return } - data, e := io.ReadAll(io.LimitReader(resp.Body, maxWSFrameBytes+1)) - if e != nil || len(data) > maxWSFrameBytes { - protocolWSError(w, "Upstream stream exceeds WebSocket frame limit") - return - } - if !strings.Contains(ct, "text/event-stream") && !looksSSE(data) { - protocolWSError(w, "Unexpected successful non-SSE upstream response ("+ct+")") - return - } - terminal := false - for _, block := range splitSSE(data) { - p := sseData(block) - if p == "" || p == "[DONE]" { + stream := newWSSSEBlockReader(bufio.NewReader(resp.Body), maxWSFrameBytes) + first := true + for { + block, e := stream.Next() + if e == io.EOF { + break + } + if e != nil { + if e == errWSSSEBlockTooLarge { + protocolWSError(w, "Upstream SSE frame exceeds WebSocket frame limit") + } else { + protocolWSError(w, "Unable to read upstream SSE stream") + } + return + } + if first { + first = false + if !strings.Contains(ct, "text/event-stream") && !looksSSE(block) { + protocolWSError(w, "Unexpected successful non-SSE upstream response ("+ct+")") + return + } + } + payload, hasData := sseDataPayloadBytes(block) + if !hasData || len(payload) == 0 || bytes.Equal(payload, []byte("[DONE]")) { continue } var v map[string]any - if json.Unmarshal([]byte(p), &v) != nil { + if json.Unmarshal(payload, &v) != nil { protocolWSError(w, "Invalid JSON payload in upstream SSE frame") return } - if terminal { - continue + if err := writeWSText(w, payload); err != nil { + return + } + // A WebSocket client must see every complete Responses event promptly, + // even while the upstream HTTP response remains open. + if err := w.Flush(); err != nil { + return } - _ = writeWSText(w, []byte(p)) typ, _ := v["type"].(string) if typ == "response.completed" || typ == "response.failed" || typ == "response.incomplete" { - terminal = true + // Match the parent bridge: a Responses terminal ends this relay even + // when an upstream keeps its HTTP connection open afterwards. + return } } - if !terminal { - protocolWSError(w, "Upstream stream ended before response terminal event") + if first && !strings.Contains(ct, "text/event-stream") { + protocolWSError(w, "Unexpected successful non-SSE upstream response ("+ct+")") + return } + protocolWSError(w, "Upstream stream ended before response terminal event") } func looksSSE(b []byte) bool { s := strings.TrimSpace(string(b)) return strings.HasPrefix(s, "data:") || strings.HasPrefix(s, "event:") } -func splitSSE(b []byte) []string { - return strings.Split(strings.ReplaceAll(string(b), "\r\n", "\n"), "\n\n") + +var errWSSSEBlockTooLarge = errors.New("upstream SSE frame exceeds WebSocket frame limit") + +// wsSSEBlockReader incrementally frames one upstream SSE response. Its limit +// applies to each SSE event, which becomes one WebSocket text frame; it never +// limits the aggregate response length. +type wsSSEBlockReader struct { + r *bufio.Reader + buf []byte + limit int +} + +func newWSSSEBlockReader(r *bufio.Reader, limit int) *wsSSEBlockReader { + return &wsSSEBlockReader{r: r, limit: limit} } -func sseData(block string) string { - var a []string - for _, l := range strings.Split(block, "\n") { - if strings.HasPrefix(l, "data:") { - a = append(a, strings.TrimPrefix(strings.TrimPrefix(l, "data:"), " ")) + +func (s *wsSSEBlockReader) Next() ([]byte, error) { + for { + at, delimiterLen, _ := sseDelimiter(s.buf) + if at >= 0 { + block := append([]byte(nil), s.buf[:at]...) + s.buf = append(s.buf[:0], s.buf[at+delimiterLen:]...) + return block, nil + } + if len(s.buf) > s.limit { + return nil, errWSSSEBlockTooLarge } + chunk, err := s.r.ReadSlice('\n') + if len(chunk) > 0 { + s.buf = append(s.buf, chunk...) + if len(s.buf) > s.limit { + return nil, errWSSSEBlockTooLarge + } + } + if err == nil || err == bufio.ErrBufferFull { + continue + } + if err == io.EOF && len(s.buf) > 0 { + block := append([]byte(nil), s.buf...) + s.buf = nil + return block, nil + } + return nil, err } - return strings.Join(a, "\n") } func sendJSONEvents(w *bufio.Writer, r map[string]any) { + if out, ok := r["output"].([]any); ok && len(out) > maxSynthesizedOutputItems { + protocolWSError(w, "Responses JSON output contains "+strconv.Itoa(len(out))+" items; maximum is "+strconv.Itoa(maxSynthesizedOutputItems)) + return + } status, _ := r["status"].(string) if status != "failed" && status != "incomplete" { status = "completed" diff --git a/go/internal/sidecar/ws_bridge_test.go b/go/internal/sidecar/ws_bridge_test.go index bc03722bd3..c4c927e513 100644 --- a/go/internal/sidecar/ws_bridge_test.go +++ b/go/internal/sidecar/ws_bridge_test.go @@ -4,11 +4,39 @@ import ( "bufio" "bytes" "encoding/json" + "errors" + "io" "net/http" "net/http/httptest" + "strings" + "sync" "testing" + "time" ) +type notifiedBuffer struct { + mu sync.Mutex + buf bytes.Buffer + written chan struct{} + once sync.Once +} + +func newNotifiedBuffer() *notifiedBuffer { return ¬ifiedBuffer{written: make(chan struct{})} } + +func (b *notifiedBuffer) Write(p []byte) (int, error) { + b.mu.Lock() + n, err := b.buf.Write(p) + b.mu.Unlock() + b.once.Do(func() { close(b.written) }) + return n, err +} + +func (b *notifiedBuffer) Bytes() []byte { + b.mu.Lock() + defer b.mu.Unlock() + return append([]byte(nil), b.buf.Bytes()...) +} + func TestWSBridgeFramesSSEAndTerminal(t *testing.T) { bridge := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Header.Get(SidecarBridgeHeader) != "bridge" { @@ -33,6 +61,119 @@ func TestWSBridgeFramesSSEAndTerminal(t *testing.T) { } } +func TestWSBridgeFramesFlushesEachSSEEventBeforeUpstreamEOF(t *testing.T) { + firstSent := make(chan struct{}) + release := make(chan struct{}) + bridge := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/event-stream") + _, _ = w.Write([]byte("data: {\"type\":\"response.created\"}\n\n")) + if f, ok := w.(http.Flusher); ok { + f.Flush() + } + close(firstSent) + <-release + _, _ = w.Write([]byte("data: {\"type\":\"response.completed\"}\n\n")) + })) + defer bridge.Close() + out := newNotifiedBuffer() + writer := bufio.NewWriter(out) + done := make(chan struct{}) + go func() { + bridgeWSFrames(writer, Config{ParentURL: bridge.URL, BridgeToken: "bridge"}, wsBridgeRequest{Frame: json.RawMessage("{}"), Admission: json.RawMessage("{}")}) + close(done) + }() + <-firstSent + select { + case <-out.written: + case <-time.After(time.Second): + t.Fatal("first WebSocket frame was withheld until upstream EOF") + } + first, _, err := readServerFrame(bufio.NewReader(bytes.NewReader(out.Bytes()))) + if err != nil || string(first) != "{\"type\":\"response.created\"}" { + t.Fatalf("first=%s err=%v", first, err) + } + close(release) + <-done +} + +func TestWSBridgeFramesReturnsAfterTerminalBeforeUpstreamEOF(t *testing.T) { + terminalSent := make(chan struct{}) + release := make(chan struct{}) + bridge := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/event-stream") + _, _ = w.Write([]byte("data: {\"type\":\"response.completed\"}\n\n")) + if f, ok := w.(http.Flusher); ok { + f.Flush() + } + close(terminalSent) + <-release + })) + defer bridge.Close() + var out bytes.Buffer + writer := bufio.NewWriter(&out) + done := make(chan struct{}) + go func() { + bridgeWSFrames(writer, Config{ParentURL: bridge.URL, BridgeToken: "bridge"}, wsBridgeRequest{Frame: json.RawMessage("{}"), Admission: json.RawMessage("{}")}) + close(done) + }() + <-terminalSent + select { + case <-done: + case <-time.After(time.Second): + close(release) + t.Fatal("bridge waited for upstream EOF after a terminal event") + } + close(release) +} + +func TestWSSSEBlockReaderLimitsEachEventNotWholeStream(t *testing.T) { + stream := newWSSSEBlockReader(bufio.NewReader(strings.NewReader("data: one\n\ndata: two\n\n")), 11) + first, err := stream.Next() + if err != nil || string(first) != "data: one" { + t.Fatalf("first=%q err=%v", first, err) + } + second, err := stream.Next() + if err != nil || string(second) != "data: two" { + t.Fatalf("second=%q err=%v", second, err) + } + if _, err = stream.Next(); !errors.Is(err, io.EOF) { + t.Fatalf("EOF err=%v", err) + } +} + +func TestWSSSEBlockReaderRejectsOversizeEvent(t *testing.T) { + stream := newWSSSEBlockReader(bufio.NewReader(strings.NewReader("data: this event is too large\n\n")), 10) + if _, err := stream.Next(); !errors.Is(err, errWSSSEBlockTooLarge) { + t.Fatalf("err=%v", err) + } +} + +func TestWSSSEBlockReaderRejectsOversizeEOFEvent(t *testing.T) { + stream := newWSSSEBlockReader(bufio.NewReader(strings.NewReader("data: this event is too large")), 10) + if _, err := stream.Next(); !errors.Is(err, errWSSSEBlockTooLarge) { + t.Fatalf("err=%v", err) + } +} + +func TestWSBridgeFramesRejectsOversizeSynthesizedJSONOutput(t *testing.T) { + output := make([]any, maxSynthesizedOutputItems+1) + bridge := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(map[string]any{"output": output}); err != nil { + t.Fatal(err) + } + })) + defer bridge.Close() + var out bytes.Buffer + writer := bufio.NewWriter(&out) + bridgeWSFrames(writer, Config{ParentURL: bridge.URL, BridgeToken: "bridge"}, wsBridgeRequest{Frame: json.RawMessage("{}"), Admission: json.RawMessage("{}")}) + _ = writer.Flush() + payload, _, err := readServerFrame(bufio.NewReader(&out)) + if err != nil || !bytes.Contains(payload, []byte("Responses JSON output contains 10001 items; maximum is 10000")) { + t.Fatalf("payload=%s err=%v", payload, err) + } +} + func TestWSBridgeFramesProtocolErrorForUnterminatedSSE(t *testing.T) { bridge := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/event-stream") diff --git a/src/server/go-sidecar-ws-bridge.ts b/src/server/go-sidecar-ws-bridge.ts index 728d883745..d3f22f89ac 100644 --- a/src/server/go-sidecar-ws-bridge.ts +++ b/src/server/go-sidecar-ws-bridge.ts @@ -16,16 +16,23 @@ function clientFrame(payload: Buffer): Buffer { return Buffer.concat([head, mask, body]); } -export async function forwardGoWebSocketFrames(baseUrl: string, requestToken: string, frame: Record, admission: unknown): Promise { +/** Forward each server text frame as it arrives; do not buffer a Responses turn. */ +export async function forwardGoWebSocketFrames( + baseUrl: string, + requestToken: string, + frame: Record, + admission: unknown, + onFrame: (text: string) => void, +): Promise { const url = new URL("/v1/responses/ws-bridge", baseUrl); const payload = Buffer.from(JSON.stringify({ frame, admission })); if (payload.byteLength > MAX_FRAME_BYTES) throw new Error("WebSocket bridge request is too large"); - return await new Promise((resolve, reject) => { - const socket = net.createConnection({ host: url.hostname, port: Number(url.port) }); const key = randomBytes(16).toString("base64"); let buffer = Buffer.alloc(0); let upgraded = false; const frames: string[] = []; + return await new Promise((resolve, reject) => { + const socket = net.createConnection({ host: url.hostname, port: Number(url.port) }); const key = randomBytes(16).toString("base64"); let buffer = Buffer.alloc(0); let upgraded = false; const fail = (error: Error) => { socket.destroy(); reject(error); }; socket.setTimeout(TIMEOUT_MS, () => fail(new Error("Go WebSocket bridge timed out"))); socket.once("error", fail); socket.on("connect", () => socket.write("GET " + url.pathname + " HTTP/1.1\r\nHost: " + url.host + "\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Version: 13\r\nSec-WebSocket-Key: " + key + "\r\nX-Ocx-Go-Sidecar-Request: " + requestToken + "\r\n\r\n")); socket.on("data", chunk => { buffer = Buffer.concat([buffer, Buffer.from(chunk)]); if (!upgraded) { const boundary = buffer.indexOf("\r\n\r\n"); if (boundary < 0) return; if (!buffer.subarray(0, boundary).toString("latin1").startsWith("HTTP/1.1 101")) return fail(new Error("Go WebSocket bridge rejected upgrade")); upgraded = true; buffer = buffer.subarray(boundary + 4); socket.write(clientFrame(payload)); } - while (buffer.byteLength >= 2) { const opcode = buffer[0]! & 0x0f; let n = buffer[1]! & 0x7f; let offset = 2; if (n === 126) { if (buffer.byteLength < 4) return; n = buffer.readUInt16BE(2); offset = 4; } else if (n === 127) { if (buffer.byteLength < 10) return; const wide = buffer.readBigUInt64BE(2); if (wide > BigInt(MAX_FRAME_BYTES)) return fail(new Error("Go WebSocket bridge frame is too large")); n = Number(wide); offset = 10; } if (n > MAX_FRAME_BYTES) return fail(new Error("Go WebSocket bridge frame is too large")); if (buffer.byteLength < offset + n) return; const body = buffer.subarray(offset, offset + n); buffer = buffer.subarray(offset + n); if (opcode === 1) frames.push(body.toString()); if (opcode === 8) { socket.end(); resolve(frames); return; } } - }); socket.once("end", () => resolve(frames)); + while (buffer.byteLength >= 2) { const opcode = buffer[0]! & 0x0f; let n = buffer[1]! & 0x7f; let offset = 2; if (n === 126) { if (buffer.byteLength < 4) return; n = buffer.readUInt16BE(2); offset = 4; } else if (n === 127) { if (buffer.byteLength < 10) return; const wide = buffer.readBigUInt64BE(2); if (wide > BigInt(MAX_FRAME_BYTES)) return fail(new Error("Go WebSocket bridge frame is too large")); n = Number(wide); offset = 10; } if (n > MAX_FRAME_BYTES) return fail(new Error("Go WebSocket bridge frame is too large")); if (buffer.byteLength < offset + n) return; const body = buffer.subarray(offset, offset + n); buffer = buffer.subarray(offset + n); if (opcode === 1) onFrame(body.toString()); if (opcode === 8) { socket.end(); resolve(); return; } } + }); socket.once("end", () => resolve()); }); } diff --git a/src/server/go-sidecar.ts b/src/server/go-sidecar.ts index ff1ebfe3a7..09431249a0 100644 --- a/src/server/go-sidecar.ts +++ b/src/server/go-sidecar.ts @@ -113,10 +113,25 @@ export function isDataPlaneSeamAttached(): boolean { return !stopped && dataPlaneSeam !== null && readyBaseUrl !== ""; } -export async function forwardGoResponsesWebSocket(frame: Record, admission: unknown): Promise { +export async function forwardGoResponsesWebSocket( + frame: Record, + admission: unknown, + onFrame: (text: string) => void, +): Promise { const seam = dataPlaneSeam; - if (!seam || stopped) return null; - try { return await forwardGoWebSocketFrames(seam.baseUrl, seam.requestToken, frame, admission); } catch { return null; } + if (!seam || stopped) return false; + let sent = false; + try { + await forwardGoWebSocketFrames(seam.baseUrl, seam.requestToken, frame, admission, text => { + sent = true; + onFrame(text); + }); + return true; + } catch { + // Once the child has started a turn its frames are observable. Do not + // synthesize a second error turn after a partial relay. + return sent; + } } /** diff --git a/src/server/index.ts b/src/server/index.ts index d12fde44c5..58af460a9e 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -4,7 +4,9 @@ import { knownModelIdsForProvider } from "../router"; import { buildWarmupCompletionFrames, buildWsErrorFrame, + forwardHeadersFromGoWsBridgeFrame, selectForwardHeaders, + withGoWsBridgeForwardHeaders, sendJsonFrame, buildResponsesWsData, sendResponseToWebSocket, @@ -1149,11 +1151,13 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server; admission?: DataPlaneAdmission }; if (!input.frame || !input.admission) return new Response(null, { status: 400 }); + const forwardedHeaders = forwardHeadersFromGoWsBridgeFrame(input.frame); const payload = { ...input.frame }; delete payload.type; + delete payload.__ocx_go_sidecar_forward_headers; const internalReq = new Request("http://localhost/v1/responses", { method: "POST", - headers: { "content-type": "application/json" }, + headers: forwardedHeaders, body: JSON.stringify({ ...payload, stream: true }), signal: req.signal, }); @@ -2343,13 +2347,16 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server { try { - const frames = await forwardGoResponsesWebSocket(frame, ws.data.admission); + const relayed = await forwardGoResponsesWebSocket( + withGoWsBridgeForwardHeaders(frame, ws.data.headers), + ws.data.admission, + text => { if (isCurrent()) sendTextFrame(ws, text); }, + ); if (!isCurrent()) return; - if (!frames) { + if (!relayed) { sendJsonFrame(ws, buildWsErrorFrame(502, { type: "proxy_error", message: "Go WebSocket bridge unavailable" })); return; } - for (const text of frames) { if (isCurrent()) sendTextFrame(ws, text); } } catch { /* socket gone */ } finally { turnAdmissionLease.release(); diff --git a/src/server/ws-bridge.ts b/src/server/ws-bridge.ts index 1dde3c39d6..eddb2e4e17 100644 --- a/src/server/ws-bridge.ts +++ b/src/server/ws-bridge.ts @@ -84,6 +84,40 @@ export class WsSendDroppedError extends Error { } } +/** Private parent/sidecar payload field; never forwarded to a provider request body. */ +export const GO_WS_BRIDGE_FORWARD_HEADERS_FIELD = "__ocx_go_sidecar_forward_headers"; + +/** + * A Go bridge turn starts from a socket frame, while the upstream request's + * caller headers live only on the Bun socket. Carry the already-filtered set + * through the token-gated bridge so its parent dispatch matches the native + * WebSocket path. The private key is written last to prevent client frames + * from supplying or replacing it. + */ +export function withGoWsBridgeForwardHeaders( + frame: Record, + headers: Headers | undefined, +): Record { + const forwarded: Record = {}; + for (const name of FORWARD_HEADERS) { + const value = headers?.get(name); + if (value) forwarded[name] = value; + } + return { ...frame, [GO_WS_BRIDGE_FORWARD_HEADERS_FIELD]: forwarded }; +} + +/** Restore only the same explicit caller-header allowlist at the parent endpoint. */ +export function forwardHeadersFromGoWsBridgeFrame(frame: Record): Headers { + const headers = new Headers({ "content-type": "application/json" }); + const raw = frame[GO_WS_BRIDGE_FORWARD_HEADERS_FIELD]; + if (!raw || typeof raw !== "object" || Array.isArray(raw)) return headers; + for (const name of FORWARD_HEADERS) { + const value = (raw as Record)[name]; + if (typeof value === "string" && value) headers.set(name, value); + } + return headers; +} + export function selectForwardHeaders( headers: Headers, codexOverride?: { accessToken: string; chatgptAccountId: string }, diff --git a/tests/go-sidecar-ws-bridge.test.ts b/tests/go-sidecar-ws-bridge.test.ts new file mode 100644 index 0000000000..83df9ba6bb --- /dev/null +++ b/tests/go-sidecar-ws-bridge.test.ts @@ -0,0 +1,36 @@ +import { describe, expect, test } from "bun:test"; +import { + GO_WS_BRIDGE_FORWARD_HEADERS_FIELD, + forwardHeadersFromGoWsBridgeFrame, + withGoWsBridgeForwardHeaders, +} from "../src/server/ws-bridge"; + +describe("Go Responses WebSocket bridge header handoff", () => { + test("preserves only selected upgrade headers and overwrites client bridge metadata", () => { + const source = new Headers({ + authorization: "Bearer caller-token", + "chatgpt-account-id": "account-1", + "x-codex-turn-state": "turn-state", + cookie: "must-not-forward", + }); + const frame = withGoWsBridgeForwardHeaders({ + type: "response.create", + [GO_WS_BRIDGE_FORWARD_HEADERS_FIELD]: { authorization: "Bearer attacker-token" }, + }, source); + + const restored = forwardHeadersFromGoWsBridgeFrame(frame); + expect(restored.get("authorization")).toBe("Bearer caller-token"); + expect(restored.get("chatgpt-account-id")).toBe("account-1"); + expect(restored.get("x-codex-turn-state")).toBe("turn-state"); + expect(restored.get("cookie")).toBeNull(); + expect(restored.get("content-type")).toBe("application/json"); + }); + + test("ignores bridge metadata keys outside the forward allowlist", () => { + const restored = forwardHeadersFromGoWsBridgeFrame({ + [GO_WS_BRIDGE_FORWARD_HEADERS_FIELD]: { authorization: "Bearer caller-token", cookie: "no" }, + }); + expect(restored.get("authorization")).toBe("Bearer caller-token"); + expect(restored.get("cookie")).toBeNull(); + }); +}); diff --git a/tests/go-ws-bridge-parity.test.ts b/tests/go-ws-bridge-parity.test.ts index 756a7e8ab7..31297a1235 100644 --- a/tests/go-ws-bridge-parity.test.ts +++ b/tests/go-ws-bridge-parity.test.ts @@ -26,4 +26,47 @@ describe.skipIf(!go || !binary)("Go WebSocket bridge differential (ticket #28)", const sidecar = startServer(0); const actual = await frames(sidecar); await sidecar.stop(true); upstream.stop(true); expect(actual).toEqual(expected); }); + + test("Go bridge forwards the first upstream event before its terminal arrives", async () => { + let releaseTerminal!: () => void; + const terminalReleased = new Promise(resolve => { releaseTerminal = resolve; }); + const encoder = new TextEncoder(); + const upstream = Bun.serve({ + port: 0, + fetch: () => new Response(new ReadableStream({ + async start(controller) { + controller.enqueue(encoder.encode("data: {\"type\":\"response.created\"}\n\n")); + await terminalReleased; + controller.enqueue(encoder.encode("data: {\"type\":\"response.completed\",\"response\":{\"status\":\"completed\"}}\n\n")); + controller.close(); + }, + }), { headers: { "content-type": "text/event-stream" } }), + }); + const home = mkdtempSync(join(tmpdir(), "ocx-ws-live-")); + process.env.OPENCODEX_HOME = home; + process.env.OPENCODEX_API_AUTH_TOKEN = "secret"; + process.env[GO_SIDECAR_BIN_ENV] = binary!; + process.env[GO_WS_BRIDGE_ENV] = "1"; + saveConfig({ port: 0, hostname: "127.0.0.1", websockets: true, defaultProvider: "fixture", providers: { fixture: { adapter: "openai-responses", baseUrl: "http://127.0.0.1:" + upstream.port + "/v1", allowPrivateNetwork: true, apiKey: "x", models: ["fixture"] } } } as never); + const server = startServer(0); + const url = new URL("/v1/responses", server.url); + url.protocol = "ws:"; + const first = await new Promise((resolve, reject) => { + const ws = new WebSocket(url, { headers: { "x-opencodex-api-key": "secret" } } as unknown as string[]); + const timer = setTimeout(() => reject(new Error("first Go bridge frame was withheld until terminal")), 3_000); + ws.addEventListener("open", () => ws.send(JSON.stringify({ type: "response.create", model: "fixture", input: "hello" })), { once: true }); + ws.addEventListener("message", event => { + const text = String(event.data); + if (!text.includes("response.created")) return; + clearTimeout(timer); + ws.close(); + resolve(text); + }); + ws.addEventListener("error", () => reject(new Error("live Go bridge socket error")), { once: true }); + }); + expect(first).toContain("response.created"); + releaseTerminal(); + await server.stop(true); + upstream.stop(true); + }); }); From d3eb5c76a3c547ab4bc343b621b6daa23dd7e8b3 Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Sun, 6 Sep 2026 23:32:54 +0800 Subject: [PATCH 041/165] docs(go): clarify WebSocket bridge header boundary --- go/internal/sidecar/ws_bridge.go | 4 ++++ src/server/index.ts | 4 +++- src/server/ws-bridge.ts | 6 +++++- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/go/internal/sidecar/ws_bridge.go b/go/internal/sidecar/ws_bridge.go index 576750796b..74a2bd4279 100644 --- a/go/internal/sidecar/ws_bridge.go +++ b/go/internal/sidecar/ws_bridge.go @@ -26,6 +26,10 @@ const ( ) type wsBridgeRequest struct { + // Frame can contain the parent-owned, allowlisted header snapshot required + // to recreate a native WebSocket turn. The sidecar treats it as opaque and + // forwards it only to the token-gated parent bridge; it never logs it or + // sends it to an upstream provider. Frame json.RawMessage `json:"frame"` Admission json.RawMessage `json:"admission"` } diff --git a/src/server/index.ts b/src/server/index.ts index 58af460a9e..5a89485f9c 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -1142,7 +1142,9 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server Date: Sun, 6 Sep 2026 23:40:07 +0800 Subject: [PATCH 042/165] feat(go): extend response repair pipeline parity --- go/internal/jsonwire/jsonwire.go | 18 ++ go/internal/sidecar/hotpath_relay.go | 18 +- go/internal/sidecar/responses_pipeline.go | 164 +++++++++++++++++++ go/internal/sidecar/responses_repair_test.go | 13 ++ go/internal/sidecar/sse_stream.go | 16 +- 5 files changed, 223 insertions(+), 6 deletions(-) create mode 100644 go/internal/sidecar/responses_pipeline.go diff --git a/go/internal/jsonwire/jsonwire.go b/go/internal/jsonwire/jsonwire.go index c9b3bc375f..e6f39b9f67 100644 --- a/go/internal/jsonwire/jsonwire.go +++ b/go/internal/jsonwire/jsonwire.go @@ -229,6 +229,24 @@ func (v *Value) Set(key string, member *Value) { v.obj = append(v.obj, Member{Key: key, Value: member}) } +// ObjectValue creates an empty ordered object for transforms that need to +// synthesize a nested JSON value. +func ObjectValue() *Value { return &Value{kind: Object} } + +// Delete removes an object member while preserving the order of all remaining +// members. It is a no-op for non-objects or absent keys. +func (v *Value) Delete(key string) { + if v == nil || v.kind != Object { + return + } + for i := range v.obj { + if v.obj[i].Key == key { + v.obj = append(v.obj[:i], v.obj[i+1:]...) + return + } + } +} + // Constructors for the values a repair synthesises (strings, empty arrays, // booleans). Numbers are not synthesised by the current transforms; NumberFrom // exists so future transforms can stay on the same tree. diff --git a/go/internal/sidecar/hotpath_relay.go b/go/internal/sidecar/hotpath_relay.go index c3795a1c74..fb6b67e6f1 100644 --- a/go/internal/sidecar/hotpath_relay.go +++ b/go/internal/sidecar/hotpath_relay.go @@ -616,7 +616,12 @@ func doDirectRelay(w http.ResponseWriter, r *http.Request, cfg Config, plan *rel } w.WriteHeader(upstreamResp.StatusCode) if upstreamResp.StatusCode >= 200 && upstreamResp.StatusCode < 300 && strings.Contains(strings.ToLower(contentType), "text/event-stream") { - if err := relayResponsesSSEWithFlush(w, upstreamResp.Body); err != nil { + requestRoot, _ := jsonwire.Parse(body) + pipeline := responseRepairPipeline{modelID: plan.modelID} + if requestRoot != nil { + pipeline.imageAliases = imageAliasesFromRequest(requestRoot) + } + if err := relayResponsesSSEWithFlush(w, upstreamResp.Body, pipeline); err != nil { fmt.Fprintf(os.Stderr, "ocx-sidecar: relay stream write: %v\n", err) } } else if err := streamCopyWithFlush(w, upstreamResp.Body); err != nil { @@ -649,7 +654,12 @@ func doDirectRelay(w http.ResponseWriter, r *http.Request, cfg Config, plan *rel // RepairResponsesJSONBody returns the original bytes untouched when // the backfill changed nothing or the body is not a JSON object, so // assigning unconditionally preserves raw-bytes relay parity. - out, _ = RepairResponsesJSONBody(rawBody) + requestRoot, _ := jsonwire.Parse(body) + pipeline := responseRepairPipeline{modelID: plan.modelID} + if requestRoot != nil { + pipeline.imageAliases = imageAliasesFromRequest(requestRoot) + } + out, _ = pipeline.repairJSON(rawBody) } } w.WriteHeader(upstreamResp.StatusCode) @@ -662,8 +672,8 @@ func doDirectRelay(w http.ResponseWriter, r *http.Request, cfg Config, plan *rel // Responses field-backfill and terminal boundary, flushing each emitted block. // It stops reading after the first terminal so a gateway cannot append frames // after completion and hold the client request open. -func relayResponsesSSEWithFlush(w http.ResponseWriter, src io.Reader) error { - stream := NewResponsesSSEStream() +func relayResponsesSSEWithFlush(w http.ResponseWriter, src io.Reader, pipeline responseRepairPipeline) error { + stream := NewResponsesSSEStream(pipeline) flusher, canFlush := w.(http.Flusher) write := func(out []byte) error { if len(out) == 0 { diff --git a/go/internal/sidecar/responses_pipeline.go b/go/internal/sidecar/responses_pipeline.go new file mode 100644 index 0000000000..048ca2d7a4 --- /dev/null +++ b/go/internal/sidecar/responses_pipeline.go @@ -0,0 +1,164 @@ +package sidecar + +// Ordered client-facing Responses repairs shared by bounded JSON and SSE +// relays. The order mirrors handleResponses' payload rewrite composition: +// representation restores, model/reasoning normalization, then canonical +// field backfill. Each step is deliberately no-op when its input is absent. + +import ( + "strings" + + "github.com/lidge-jun/opencodex/go/internal/jsonwire" +) + +type responseRepairPipeline struct { + modelID string + imageAliases map[string]imageAlias + reasoning bool +} + +type imageAlias struct{ name, namespace string } + +func (p responseRepairPipeline) repairJSON(raw []byte) ([]byte, bool) { + root, err := jsonwire.Parse(raw) + if err != nil || root.Kind() != jsonwire.Object { + return raw, false + } + changed := false + if p.modelID != "" { + changed = rewriteModelField(root, p.modelID) + if response := root.Find("response"); response != nil { + changed = rewriteModelField(response, p.modelID) || changed + } + } + changed = p.repairValue(root) || changed + changed = backfillResponsesJSON(root) || changed + if !changed { + return raw, false + } + encoded, err := root.Encode() + if err != nil { + return raw, false + } + return encoded, true +} + +func (p responseRepairPipeline) repairValue(v *jsonwire.Value) bool { + if v == nil { + return false + } + changed := false + // Model payload rewrite applies to the event/response object pair only; + // walking nested output items must not rewrite an unrelated model field. + if p.modelID != "" && v.Kind() == jsonwire.Object { + changed = rewriteModelField(v, p.modelID) + if response := v.Find("response"); response != nil { + changed = rewriteModelField(response, p.modelID) || changed + } + } + switch v.Kind() { + case jsonwire.Array: + for _, e := range v.Elements() { + changed = p.repairValue(e) || changed + } + case jsonwire.Object: + if typ, ok := stringMember(v, "type"); ok { + if p.reasoning && typ == "response.reasoning_text.delta" { + v.Set("type", jsonwire.StringValue("response.reasoning_summary_text.delta")) + if v.Find("summary_index") == nil { + v.Set("summary_index", jsonwire.NumberFrom(0)) + } + changed = true + } else if p.reasoning && typ == "response.reasoning_text.done" { + v.Set("type", jsonwire.StringValue("response.reasoning_summary_text.done")) + if v.Find("summary_index") == nil { + v.Set("summary_index", jsonwire.NumberFrom(0)) + } + changed = true + } + } + // Image-gen aliases are representation-only and are restored before + // all structural repairs, matching the TS payload rewrite list. + if typ, ok := stringMember(v, "type"); ok && typ == "function_call" { + if name, ok := stringMember(v, "name"); ok { + if alias, found := p.imageAliases[name]; found { + v.Set("name", jsonwire.StringValue(alias.name)) + v.Set("namespace", jsonwire.StringValue(alias.namespace)) + changed = true + } + } + } + if p.reasoning { + changed = p.repairReasoningItem(v) || changed + } + for _, m := range v.Members() { + changed = p.repairValue(m.Value) || changed + } + } + return changed +} + +func rewriteModelField(v *jsonwire.Value, modelID string) bool { + if v == nil || v.Kind() != jsonwire.Object { + return false + } + model, ok := stringMember(v, "model") + if !ok || model == modelID { + return false + } + v.Set("model", jsonwire.StringValue(modelID)) + return true +} + +func (p responseRepairPipeline) repairReasoningItem(v *jsonwire.Value) bool { + typ, ok := stringMember(v, "type") + if !ok || typ != "reasoning" { + return false + } + content := v.Find("content") + if content == nil || content.Kind() != jsonwire.Array { + return false + } + var text strings.Builder + for _, part := range content.Elements() { + if partType, ok := stringMember(part, "type"); ok && partType == "reasoning_text" { + if t, ok := stringMember(part, "text"); ok { + text.WriteString(t) + } + } + } + if text.Len() == 0 || v.Find("encrypted_content") != nil { + return false + } + v.Delete("content") + summary := jsonwire.EmptyArray() + part := jsonwire.ObjectValue() + part.Set("type", jsonwire.StringValue("summary_text")) + part.Set("text", jsonwire.StringValue(text.String())) + summary.AppendArray(part) + v.Set("summary", summary) + return true +} + +func imageAliasesFromRequest(root *jsonwire.Value) map[string]imageAlias { + out := map[string]imageAlias{} + tools := root.Find("tools") + if tools == nil || tools.Kind() != jsonwire.Array { + return out + } + for _, tool := range tools.Elements() { + if tool == nil || tool.Kind() != jsonwire.Object { + continue + } + name, ok := stringMember(tool, "name") + if !ok { + continue + } + if strings.HasPrefix(name, "image_gen.") && len(name) > len("image_gen.") { + local := strings.TrimPrefix(name, "image_gen.") + out[name] = imageAlias{name: local, namespace: "image_gen"} + out["image_gen__"+local] = imageAlias{name: local, namespace: "image_gen"} + } + } + return out +} diff --git a/go/internal/sidecar/responses_repair_test.go b/go/internal/sidecar/responses_repair_test.go index a2e6e83f45..afd101b1e3 100644 --- a/go/internal/sidecar/responses_repair_test.go +++ b/go/internal/sidecar/responses_repair_test.go @@ -83,3 +83,16 @@ func TestRepairResponsesJSONEmptyStringIDReplacesInPlace(t *testing.T) { t.Fatalf("got %s\nwant %s", out, want) } } + +func TestResponseRepairPipelineRunsOrderedModelAndImageRestoresBeforeBackfill(t *testing.T) { + input := `{"model":"upstream","output":[{"type":"function_call","name":"image_gen__create","arguments":"{}"},{"type":"message","content":[{"type":"output_text","text":"ok"}]}]}` + p := responseRepairPipeline{modelID: "client-model", imageAliases: map[string]imageAlias{"image_gen__create": {name: "create", namespace: "image_gen"}}} + out, changed := p.repairJSON([]byte(input)) + if !changed { + t.Fatal("pipeline reported unchanged") + } + want := `{"model":"client-model","output":[{"type":"function_call","name":"create","arguments":"{}","namespace":"image_gen","id":"fc_ocx_0"},{"type":"message","content":[{"type":"output_text","text":"ok","annotations":[]}],"id":"msg_ocx_1","status":"completed"}]}` + if string(out) != want { + t.Fatalf("got %s\nwant %s", out, want) + } +} diff --git a/go/internal/sidecar/sse_stream.go b/go/internal/sidecar/sse_stream.go index 994beedba8..270db40382 100644 --- a/go/internal/sidecar/sse_stream.go +++ b/go/internal/sidecar/sse_stream.go @@ -39,6 +39,7 @@ type ResponsesSSEStream struct { terminal bool done bool pendingDone []sseFrame + pipeline responseRepairPipeline } type sseFrame struct { @@ -48,7 +49,13 @@ type sseFrame struct { // NewResponsesSSEStream creates a stream-local state machine. Do not share one // instance between concurrent upstream Responses requests. -func NewResponsesSSEStream() *ResponsesSSEStream { return &ResponsesSSEStream{} } +func NewResponsesSSEStream(pipeline ...responseRepairPipeline) *ResponsesSSEStream { + s := &ResponsesSSEStream{} + if len(pipeline) > 0 { + s.pipeline = pipeline[0] + } + return s +} // TerminalSeen reports whether a response.completed, response.failed, or // response.incomplete event crossed the client boundary. @@ -227,7 +234,12 @@ func (s *ResponsesSSEStream) rewriteBlock(block, payload []byte, hasData bool) [ return block } event, err := jsonwire.Parse(payload) - if err != nil || event.Kind() != jsonwire.Object || !s.rewriteEvent(event) { + if err != nil || event.Kind() != jsonwire.Object { + return block + } + changed := s.pipeline.repairValue(event) + changed = s.rewriteEvent(event) || changed + if !changed { return block } encoded, err := event.Encode() From 930418324030e2b46cbfd734b17763c910b35695 Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Sun, 6 Sep 2026 23:40:07 +0800 Subject: [PATCH 043/165] feat(go): extend response repair pipeline parity --- go/internal/jsonwire/jsonwire.go | 18 ++ go/internal/sidecar/hotpath_relay.go | 18 +- go/internal/sidecar/responses_pipeline.go | 164 +++++++++++++++++++ go/internal/sidecar/responses_repair_test.go | 13 ++ go/internal/sidecar/sse_stream.go | 16 +- 5 files changed, 223 insertions(+), 6 deletions(-) create mode 100644 go/internal/sidecar/responses_pipeline.go diff --git a/go/internal/jsonwire/jsonwire.go b/go/internal/jsonwire/jsonwire.go index c9b3bc375f..e6f39b9f67 100644 --- a/go/internal/jsonwire/jsonwire.go +++ b/go/internal/jsonwire/jsonwire.go @@ -229,6 +229,24 @@ func (v *Value) Set(key string, member *Value) { v.obj = append(v.obj, Member{Key: key, Value: member}) } +// ObjectValue creates an empty ordered object for transforms that need to +// synthesize a nested JSON value. +func ObjectValue() *Value { return &Value{kind: Object} } + +// Delete removes an object member while preserving the order of all remaining +// members. It is a no-op for non-objects or absent keys. +func (v *Value) Delete(key string) { + if v == nil || v.kind != Object { + return + } + for i := range v.obj { + if v.obj[i].Key == key { + v.obj = append(v.obj[:i], v.obj[i+1:]...) + return + } + } +} + // Constructors for the values a repair synthesises (strings, empty arrays, // booleans). Numbers are not synthesised by the current transforms; NumberFrom // exists so future transforms can stay on the same tree. diff --git a/go/internal/sidecar/hotpath_relay.go b/go/internal/sidecar/hotpath_relay.go index c3795a1c74..fb6b67e6f1 100644 --- a/go/internal/sidecar/hotpath_relay.go +++ b/go/internal/sidecar/hotpath_relay.go @@ -616,7 +616,12 @@ func doDirectRelay(w http.ResponseWriter, r *http.Request, cfg Config, plan *rel } w.WriteHeader(upstreamResp.StatusCode) if upstreamResp.StatusCode >= 200 && upstreamResp.StatusCode < 300 && strings.Contains(strings.ToLower(contentType), "text/event-stream") { - if err := relayResponsesSSEWithFlush(w, upstreamResp.Body); err != nil { + requestRoot, _ := jsonwire.Parse(body) + pipeline := responseRepairPipeline{modelID: plan.modelID} + if requestRoot != nil { + pipeline.imageAliases = imageAliasesFromRequest(requestRoot) + } + if err := relayResponsesSSEWithFlush(w, upstreamResp.Body, pipeline); err != nil { fmt.Fprintf(os.Stderr, "ocx-sidecar: relay stream write: %v\n", err) } } else if err := streamCopyWithFlush(w, upstreamResp.Body); err != nil { @@ -649,7 +654,12 @@ func doDirectRelay(w http.ResponseWriter, r *http.Request, cfg Config, plan *rel // RepairResponsesJSONBody returns the original bytes untouched when // the backfill changed nothing or the body is not a JSON object, so // assigning unconditionally preserves raw-bytes relay parity. - out, _ = RepairResponsesJSONBody(rawBody) + requestRoot, _ := jsonwire.Parse(body) + pipeline := responseRepairPipeline{modelID: plan.modelID} + if requestRoot != nil { + pipeline.imageAliases = imageAliasesFromRequest(requestRoot) + } + out, _ = pipeline.repairJSON(rawBody) } } w.WriteHeader(upstreamResp.StatusCode) @@ -662,8 +672,8 @@ func doDirectRelay(w http.ResponseWriter, r *http.Request, cfg Config, plan *rel // Responses field-backfill and terminal boundary, flushing each emitted block. // It stops reading after the first terminal so a gateway cannot append frames // after completion and hold the client request open. -func relayResponsesSSEWithFlush(w http.ResponseWriter, src io.Reader) error { - stream := NewResponsesSSEStream() +func relayResponsesSSEWithFlush(w http.ResponseWriter, src io.Reader, pipeline responseRepairPipeline) error { + stream := NewResponsesSSEStream(pipeline) flusher, canFlush := w.(http.Flusher) write := func(out []byte) error { if len(out) == 0 { diff --git a/go/internal/sidecar/responses_pipeline.go b/go/internal/sidecar/responses_pipeline.go new file mode 100644 index 0000000000..a7435605db --- /dev/null +++ b/go/internal/sidecar/responses_pipeline.go @@ -0,0 +1,164 @@ +package sidecar + +// Ordered client-facing Responses repairs shared by bounded JSON and SSE +// relays. The order mirrors handleResponses' payload rewrite composition: +// representation restores, model/reasoning normalization, then canonical +// field backfill. Each step is deliberately no-op when its input is absent. + +import ( + "strings" + + "github.com/lidge-jun/opencodex/go/internal/jsonwire" +) + +type responseRepairPipeline struct { + modelID string + imageAliases map[string]imageAlias + reasoning bool +} + +type imageAlias struct{ name, namespace string } + +func (p responseRepairPipeline) repairJSON(raw []byte) ([]byte, bool) { + root, err := jsonwire.Parse(raw) + if err != nil || root.Kind() != jsonwire.Object { + return raw, false + } + changed := p.repairPayload(root) + changed = backfillResponsesJSON(root) || changed + if !changed { + return raw, false + } + encoded, err := root.Encode() + if err != nil { + return raw, false + } + return encoded, true +} + +// repairPayload applies model metadata only at the two locations covered by +// the TypeScript model rewrite: the event/response root and its direct +// `response` child. Nested output items may legally contain unrelated model +// metadata and must remain untouched. +func (p responseRepairPipeline) repairPayload(root *jsonwire.Value) bool { + changed := false + if p.modelID != "" { + changed = rewriteModelField(root, p.modelID) + if response := root.Find("response"); response != nil { + changed = rewriteModelField(response, p.modelID) || changed + } + } + return p.repairValue(root) || changed +} + +func (p responseRepairPipeline) repairValue(v *jsonwire.Value) bool { + if v == nil { + return false + } + changed := false + switch v.Kind() { + case jsonwire.Array: + for _, e := range v.Elements() { + changed = p.repairValue(e) || changed + } + case jsonwire.Object: + if typ, ok := stringMember(v, "type"); ok { + if p.reasoning && typ == "response.reasoning_text.delta" { + v.Set("type", jsonwire.StringValue("response.reasoning_summary_text.delta")) + if v.Find("summary_index") == nil { + v.Set("summary_index", jsonwire.NumberFrom(0)) + } + changed = true + } else if p.reasoning && typ == "response.reasoning_text.done" { + v.Set("type", jsonwire.StringValue("response.reasoning_summary_text.done")) + if v.Find("summary_index") == nil { + v.Set("summary_index", jsonwire.NumberFrom(0)) + } + changed = true + } + } + // Image-gen aliases are representation-only and are restored before + // all structural repairs, matching the TS payload rewrite list. + if typ, ok := stringMember(v, "type"); ok && typ == "function_call" { + if name, ok := stringMember(v, "name"); ok { + if alias, found := p.imageAliases[name]; found { + v.Set("name", jsonwire.StringValue(alias.name)) + v.Set("namespace", jsonwire.StringValue(alias.namespace)) + changed = true + } + } + } + if p.reasoning { + changed = p.repairReasoningItem(v) || changed + } + for _, m := range v.Members() { + changed = p.repairValue(m.Value) || changed + } + } + return changed +} + +func rewriteModelField(v *jsonwire.Value, modelID string) bool { + if v == nil || v.Kind() != jsonwire.Object { + return false + } + model, ok := stringMember(v, "model") + if !ok || model == modelID { + return false + } + v.Set("model", jsonwire.StringValue(modelID)) + return true +} + +func (p responseRepairPipeline) repairReasoningItem(v *jsonwire.Value) bool { + typ, ok := stringMember(v, "type") + if !ok || typ != "reasoning" { + return false + } + content := v.Find("content") + if content == nil || content.Kind() != jsonwire.Array { + return false + } + var text strings.Builder + for _, part := range content.Elements() { + if partType, ok := stringMember(part, "type"); ok && partType == "reasoning_text" { + if t, ok := stringMember(part, "text"); ok { + text.WriteString(t) + } + } + } + if text.Len() == 0 || v.Find("encrypted_content") != nil { + return false + } + v.Delete("content") + summary := jsonwire.EmptyArray() + part := jsonwire.ObjectValue() + part.Set("type", jsonwire.StringValue("summary_text")) + part.Set("text", jsonwire.StringValue(text.String())) + summary.AppendArray(part) + v.Set("summary", summary) + return true +} + +func imageAliasesFromRequest(root *jsonwire.Value) map[string]imageAlias { + out := map[string]imageAlias{} + tools := root.Find("tools") + if tools == nil || tools.Kind() != jsonwire.Array { + return out + } + for _, tool := range tools.Elements() { + if tool == nil || tool.Kind() != jsonwire.Object { + continue + } + name, ok := stringMember(tool, "name") + if !ok { + continue + } + if strings.HasPrefix(name, "image_gen.") && len(name) > len("image_gen.") { + local := strings.TrimPrefix(name, "image_gen.") + out[name] = imageAlias{name: local, namespace: "image_gen"} + out["image_gen__"+local] = imageAlias{name: local, namespace: "image_gen"} + } + } + return out +} diff --git a/go/internal/sidecar/responses_repair_test.go b/go/internal/sidecar/responses_repair_test.go index a2e6e83f45..afd101b1e3 100644 --- a/go/internal/sidecar/responses_repair_test.go +++ b/go/internal/sidecar/responses_repair_test.go @@ -83,3 +83,16 @@ func TestRepairResponsesJSONEmptyStringIDReplacesInPlace(t *testing.T) { t.Fatalf("got %s\nwant %s", out, want) } } + +func TestResponseRepairPipelineRunsOrderedModelAndImageRestoresBeforeBackfill(t *testing.T) { + input := `{"model":"upstream","output":[{"type":"function_call","name":"image_gen__create","arguments":"{}"},{"type":"message","content":[{"type":"output_text","text":"ok"}]}]}` + p := responseRepairPipeline{modelID: "client-model", imageAliases: map[string]imageAlias{"image_gen__create": {name: "create", namespace: "image_gen"}}} + out, changed := p.repairJSON([]byte(input)) + if !changed { + t.Fatal("pipeline reported unchanged") + } + want := `{"model":"client-model","output":[{"type":"function_call","name":"create","arguments":"{}","namespace":"image_gen","id":"fc_ocx_0"},{"type":"message","content":[{"type":"output_text","text":"ok","annotations":[]}],"id":"msg_ocx_1","status":"completed"}]}` + if string(out) != want { + t.Fatalf("got %s\nwant %s", out, want) + } +} diff --git a/go/internal/sidecar/sse_stream.go b/go/internal/sidecar/sse_stream.go index 994beedba8..5280b25714 100644 --- a/go/internal/sidecar/sse_stream.go +++ b/go/internal/sidecar/sse_stream.go @@ -39,6 +39,7 @@ type ResponsesSSEStream struct { terminal bool done bool pendingDone []sseFrame + pipeline responseRepairPipeline } type sseFrame struct { @@ -48,7 +49,13 @@ type sseFrame struct { // NewResponsesSSEStream creates a stream-local state machine. Do not share one // instance between concurrent upstream Responses requests. -func NewResponsesSSEStream() *ResponsesSSEStream { return &ResponsesSSEStream{} } +func NewResponsesSSEStream(pipeline ...responseRepairPipeline) *ResponsesSSEStream { + s := &ResponsesSSEStream{} + if len(pipeline) > 0 { + s.pipeline = pipeline[0] + } + return s +} // TerminalSeen reports whether a response.completed, response.failed, or // response.incomplete event crossed the client boundary. @@ -227,7 +234,12 @@ func (s *ResponsesSSEStream) rewriteBlock(block, payload []byte, hasData bool) [ return block } event, err := jsonwire.Parse(payload) - if err != nil || event.Kind() != jsonwire.Object || !s.rewriteEvent(event) { + if err != nil || event.Kind() != jsonwire.Object { + return block + } + changed := s.pipeline.repairPayload(event) + changed = s.rewriteEvent(event) || changed + if !changed { return block } encoded, err := event.Encode() From 218a5d9d6a2d5233d8d3d2cec3690b8ca1d08eac Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Sun, 6 Sep 2026 23:40:07 +0800 Subject: [PATCH 044/165] feat(go): extend response repair pipeline parity --- go/internal/sidecar/responses_pipeline.go | 32 +++++++++++------------ go/internal/sidecar/sse_stream.go | 2 +- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/go/internal/sidecar/responses_pipeline.go b/go/internal/sidecar/responses_pipeline.go index 048ca2d7a4..a7435605db 100644 --- a/go/internal/sidecar/responses_pipeline.go +++ b/go/internal/sidecar/responses_pipeline.go @@ -24,14 +24,7 @@ func (p responseRepairPipeline) repairJSON(raw []byte) ([]byte, bool) { if err != nil || root.Kind() != jsonwire.Object { return raw, false } - changed := false - if p.modelID != "" { - changed = rewriteModelField(root, p.modelID) - if response := root.Find("response"); response != nil { - changed = rewriteModelField(response, p.modelID) || changed - } - } - changed = p.repairValue(root) || changed + changed := p.repairPayload(root) changed = backfillResponsesJSON(root) || changed if !changed { return raw, false @@ -43,19 +36,26 @@ func (p responseRepairPipeline) repairJSON(raw []byte) ([]byte, bool) { return encoded, true } +// repairPayload applies model metadata only at the two locations covered by +// the TypeScript model rewrite: the event/response root and its direct +// `response` child. Nested output items may legally contain unrelated model +// metadata and must remain untouched. +func (p responseRepairPipeline) repairPayload(root *jsonwire.Value) bool { + changed := false + if p.modelID != "" { + changed = rewriteModelField(root, p.modelID) + if response := root.Find("response"); response != nil { + changed = rewriteModelField(response, p.modelID) || changed + } + } + return p.repairValue(root) || changed +} + func (p responseRepairPipeline) repairValue(v *jsonwire.Value) bool { if v == nil { return false } changed := false - // Model payload rewrite applies to the event/response object pair only; - // walking nested output items must not rewrite an unrelated model field. - if p.modelID != "" && v.Kind() == jsonwire.Object { - changed = rewriteModelField(v, p.modelID) - if response := v.Find("response"); response != nil { - changed = rewriteModelField(response, p.modelID) || changed - } - } switch v.Kind() { case jsonwire.Array: for _, e := range v.Elements() { diff --git a/go/internal/sidecar/sse_stream.go b/go/internal/sidecar/sse_stream.go index 270db40382..5280b25714 100644 --- a/go/internal/sidecar/sse_stream.go +++ b/go/internal/sidecar/sse_stream.go @@ -237,7 +237,7 @@ func (s *ResponsesSSEStream) rewriteBlock(block, payload []byte, hasData bool) [ if err != nil || event.Kind() != jsonwire.Object { return block } - changed := s.pipeline.repairValue(event) + changed := s.pipeline.repairPayload(event) changed = s.rewriteEvent(event) || changed if !changed { return block From 398ea1f1c3b69be3a46fa32f38854580d5096a7c Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Sun, 6 Sep 2026 23:39:51 +0800 Subject: [PATCH 045/165] feat(go): add config CLI mutation workflows --- go/internal/config/config.go | 41 ++++++ go/internal/ocxcli/cli_test.go | 71 ++++++++++ go/internal/ocxcli/families.go | 243 +++++++++++++++++++++++++++++++-- 3 files changed, 341 insertions(+), 14 deletions(-) diff --git a/go/internal/config/config.go b/go/internal/config/config.go index 42bb94765f..ebb463dc82 100644 --- a/go/internal/config/config.go +++ b/go/internal/config/config.go @@ -233,3 +233,44 @@ func defaultSourceModels() []string { // default and skew a later response. return append([]string(nil), DefaultShadowSourceModels...) } + +// SaveRaw atomically replaces config.json with an indented JSON representation +// of raw. It creates the config directory as needed and keeps user config +// private (0600). Validation deliberately belongs to the owning command: this +// shared reader must preserve unknown config fields. +func SaveRaw(raw map[string]any) error { + path, err := Path() + if err != nil { + return err + } + if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil { + return err + } + encoded, err := json.MarshalIndent(raw, "", " ") + if err != nil { + return err + } + encoded = append(encoded, '\n') + temp, err := os.CreateTemp(filepath.Dir(path), ".config.json-*") + if err != nil { + return err + } + tempName := temp.Name() + defer os.Remove(tempName) + if err := temp.Chmod(0o600); err != nil { + temp.Close() + return err + } + if _, err := temp.Write(encoded); err != nil { + temp.Close() + return err + } + if err := temp.Sync(); err != nil { + temp.Close() + return err + } + if err := temp.Close(); err != nil { + return err + } + return os.Rename(tempName, path) +} diff --git a/go/internal/ocxcli/cli_test.go b/go/internal/ocxcli/cli_test.go index d25064b427..9e334c4070 100644 --- a/go/internal/ocxcli/cli_test.go +++ b/go/internal/ocxcli/cli_test.go @@ -7,9 +7,12 @@ import ( "net" "net/http" "net/http/httptest" + "os" + "path/filepath" "strings" "testing" + "github.com/lidge-jun/opencodex/go/internal/config" "github.com/lidge-jun/opencodex/go/internal/managementauth" ) @@ -139,3 +142,71 @@ func TestReadyAndUsageExitCodes(t *testing.T) { t.Fatalf("invalid ready = %d", got) } } + +func TestConfigMutationFamilyPersistsAtomically(t *testing.T) { + dir := t.TempDir() + t.Setenv("OPENCODEX_HOME", dir) + if err := os.WriteFile(filepath.Join(dir, "config.json"), []byte("{\"providers\":{\"alpha\":{\"adapter\":\"openai-chat\",\"baseUrl\":\"https://alpha.test\"}},\"defaultProvider\":\"alpha\",\"port\":10100}"), 0o600); err != nil { + t.Fatal(err) + } + var out, stderr bytes.Buffer + if got := Run([]string{"config", "set", "port", "10200", "--json"}, depsFor(RuntimeState{}, &out, &stderr)); got != ExitOK { + t.Fatalf("set exit = %d stderr %q", got, stderr.String()) + } + if !strings.Contains(out.String(), "\"value\": 10200") { + t.Fatalf("set output = %q", out.String()) + } + loaded, err := config.LoadFromDir(dir) + if err != nil { + t.Fatal(err) + } + if got := loaded.Raw["port"]; got != json.Number("10200") { + t.Fatalf("persisted port = %#v", got) + } + out.Reset() + stderr.Reset() + if got := Run([]string{"config", "unset", "port"}, depsFor(RuntimeState{}, &out, &stderr)); got != ExitOK { + t.Fatalf("unset exit = %d stderr %q", got, stderr.String()) + } + loaded, err = config.LoadFromDir(dir) + if err != nil { + t.Fatal(err) + } + if _, exists := loaded.Raw["port"]; exists { + t.Fatalf("unset left port in %#v", loaded.Raw) + } + out.Reset() + stderr.Reset() + if got := Run([]string{"config", "set", "__proto__.polluted", "true"}, depsFor(RuntimeState{}, &out, &stderr)); got != ExitUsage { + t.Fatalf("blocked path exit = %d", got) + } + if got := Run([]string{"config", "validate", "--json"}, depsFor(RuntimeState{}, &out, &stderr)); got != ExitOK { + t.Fatalf("validate exit = %d stderr %q", got, stderr.String()) + } +} + +func TestConfigImportExportRequiresConfirmation(t *testing.T) { + dir := t.TempDir() + t.Setenv("OPENCODEX_HOME", dir) + input := filepath.Join(dir, "input.json") + if err := os.WriteFile(input, []byte("{\"providers\":{\"beta\":{\"adapter\":\"openai-chat\",\"baseUrl\":\"https://beta.test\"}},\"defaultProvider\":\"beta\"}"), 0o600); err != nil { + t.Fatal(err) + } + var out, stderr bytes.Buffer + if got := Run([]string{"config", "import", input}, depsFor(RuntimeState{}, &out, &stderr)); got != ExitUsage { + t.Fatalf("unconfirmed import = %d", got) + } + out.Reset() + stderr.Reset() + if got := Run([]string{"config", "import", input, "--yes", "--json"}, depsFor(RuntimeState{}, &out, &stderr)); got != ExitOK { + t.Fatalf("import = %d stderr %q", got, stderr.String()) + } + if !strings.Contains(out.String(), "\"ok\": true") { + t.Fatalf("import output = %q", out.String()) + } + out.Reset() + stderr.Reset() + if got := Run([]string{"config", "export", "-"}, depsFor(RuntimeState{}, &out, &stderr)); got != ExitOK || !strings.Contains(out.String(), "\"beta\"") { + t.Fatalf("export = %d %q", got, out.String()) + } +} diff --git a/go/internal/ocxcli/families.go b/go/internal/ocxcli/families.go index 4d5a6aac58..53d3aba417 100644 --- a/go/internal/ocxcli/families.go +++ b/go/internal/ocxcli/families.go @@ -2,16 +2,18 @@ package ocxcli import ( "encoding/json" + "errors" "fmt" "io" "math" + "os" "strings" "github.com/lidge-jun/opencodex/go/internal/config" ) const ( - configUsage = "Usage:\n ocx config [show] [--json]\n ocx config get [--json]\n" + configUsage = "Usage:\n ocx config [show] [--json]\n ocx config get [--json]\n ocx config set [--json]\n ocx config unset [--json]\n ocx config validate [path|-] [--json]\n ocx config export \n ocx config import --yes [--json]\n" modelsUsage = "Usage: ocx models [--provider ] [--json]\n" providerRegistryCount = 85 ) @@ -40,26 +42,239 @@ func runConfig(args []string, deps Deps) int { } return writeIndentedJSON(deps.Stdout, redactConfig(cfg)) } - if args[0] != "get" || len(args) < 2 || len(args) > 3 || (len(args) == 3 && args[2] != "--json") { + action := args[0] + jsonOutput := takeFlag(&args, "--json") + switch action { + case "get": + if len(args) != 2 { + fmt.Fprint(deps.Stderr, configUsage) + return ExitUsage + } + cfg, err := loadCLIConfig() + if err != nil { + fmt.Fprintln(deps.Stderr, err) + return ExitFailure + } + value, ok := configPath(cfg, args[1]) + if !ok { + fmt.Fprintf(deps.Stderr, "config path not found: %s\n", args[1]) + return ExitUsage + } + value = redactConfigValue(value, lastSegment(args[1])) + if jsonOutput || isComposite(value) { + return writeIndentedJSON(deps.Stdout, value) + } + fmt.Fprintln(deps.Stdout, scalarString(value)) + return ExitOK + case "set", "unset": + if (action == "set" && len(args) != 3) || (action == "unset" && len(args) != 2) { + fmt.Fprint(deps.Stderr, configUsage) + return ExitUsage + } + path := args[1] + cfg, err := loadCLIConfig() + if err != nil { + fmt.Fprintln(deps.Stderr, err) + return ExitFailure + } + var value any + if action == "set" { + value = parseConfigValue(args[2]) + } + if err := setConfigPath(cfg, path, value, action == "unset"); err != nil { + fmt.Fprintln(deps.Stderr, err) + return ExitUsage + } + if err := validateCLIConfig(cfg); err != nil { + fmt.Fprintln(deps.Stderr, err) + return ExitUsage + } + if err := config.SaveRaw(cfg); err != nil { + fmt.Fprintln(deps.Stderr, err) + return ExitFailure + } + if action == "unset" { + value = nil + } else { + value, _ = configPath(cfg, path) + } + result := map[string]any{"ok": true, "path": path, "value": redactConfigValue(value, lastSegment(path))} + if jsonOutput { + return writeIndentedJSON(deps.Stdout, result) + } + fmt.Fprintf(deps.Stdout, "%s %s.\n", strings.Title(action), path) + return ExitOK + case "validate": + if len(args) > 2 || len(args) == 2 && args[1] != "-" { + fmt.Fprint(deps.Stderr, configUsage) + return ExitUsage + } + var cfg map[string]any + var err error + if len(args) == 2 { + cfg, err = readConfigInput(args[1]) + } else { + cfg, err = loadCLIConfig() + } + if err == nil { + err = validateCLIConfig(cfg) + } + if err != nil { + if jsonOutput { + writeIndentedJSON(deps.Stdout, map[string]any{"ok": false, "error": err.Error()}) + } else { + fmt.Fprintf(deps.Stdout, "Config is invalid: %s\n", err) + } + return ExitFailure + } + if jsonOutput { + return writeIndentedJSON(deps.Stdout, map[string]any{"ok": true}) + } + fmt.Fprintln(deps.Stdout, "Config is valid.") + return ExitOK + case "export": + if len(args) != 2 { + fmt.Fprint(deps.Stderr, configUsage) + return ExitUsage + } + cfg, err := loadCLIConfig() + if err != nil { + fmt.Fprintln(deps.Stderr, err) + return ExitFailure + } + content, _ := json.MarshalIndent(cfg, "", " ") + content = append(content, '\n') + if args[1] == "-" { + _, _ = deps.Stdout.Write(content) + return ExitOK + } + if err := os.WriteFile(args[1], content, 0o600); err != nil { + fmt.Fprintln(deps.Stderr, err) + return ExitFailure + } + fmt.Fprintf(deps.Stdout, "Exported config to %s.\n", args[1]) + return ExitOK + case "import": + if len(args) != 3 || args[2] != "--yes" { + fmt.Fprint(deps.Stderr, configUsage) + return ExitUsage + } + cfg, err := readConfigInput(args[1]) + if err == nil { + err = validateCLIConfig(cfg) + } + if err != nil { + fmt.Fprintln(deps.Stderr, err) + return ExitUsage + } + if err := config.SaveRaw(cfg); err != nil { + fmt.Fprintln(deps.Stderr, err) + return ExitFailure + } + if jsonOutput { + return writeIndentedJSON(deps.Stdout, map[string]any{"ok": true, "source": args[1]}) + } + fmt.Fprintf(deps.Stdout, "Imported config from %s. Restart or run ocx sync if needed.\n", args[1]) + return ExitOK + default: fmt.Fprint(deps.Stderr, configUsage) return ExitUsage } - cfg, err := loadCLIConfig() +} + +func takeFlag(args *[]string, flag string) bool { + for i, value := range *args { + if value == flag { + *args = append((*args)[:i], (*args)[i+1:]...) + return true + } + } + return false +} + +var blockedConfigSegment = map[string]bool{"__proto__": true, "prototype": true, "constructor": true} + +func configSegments(path string) ([]string, error) { + segments := []string{} + for _, segment := range strings.Split(path, ".") { + segment = strings.TrimSpace(segment) + if segment != "" { + if blockedConfigSegment[segment] { + return nil, errors.New("invalid config path") + } + segments = append(segments, segment) + } + } + if len(segments) == 0 { + return nil, errors.New("invalid config path") + } + return segments, nil +} +func setConfigPath(root map[string]any, path string, value any, remove bool) error { + segments, err := configSegments(path) if err != nil { - fmt.Fprintln(deps.Stderr, err) - return ExitFailure + return err } - value, ok := configPath(cfg, args[1]) - if !ok { - fmt.Fprintf(deps.Stderr, "config path not found: %s\n", args[1]) - return ExitUsage + current := root + for _, segment := range segments[:len(segments)-1] { + next, ok := current[segment].(map[string]any) + if !ok { + return fmt.Errorf("config parent path not found: %s", segment) + } + current = next + } + leaf := segments[len(segments)-1] + if remove { + if _, ok := current[leaf]; !ok { + return fmt.Errorf("config path not found: %s", path) + } + delete(current, leaf) + } else { + current[leaf] = value } - value = redactConfigValue(value, lastSegment(args[1])) - if len(args) == 3 || isComposite(value) { - return writeIndentedJSON(deps.Stdout, value) + return nil +} +func parseConfigValue(raw string) any { + var value any + decoder := json.NewDecoder(strings.NewReader(raw)) + decoder.UseNumber() + if decoder.Decode(&value) == nil { + return value } - fmt.Fprintln(deps.Stdout, scalarString(value)) - return ExitOK + return raw +} +func readConfigInput(path string) (map[string]any, error) { + var data []byte + var err error + if path == "-" { + data, err = io.ReadAll(os.Stdin) + } else { + data, err = os.ReadFile(path) + } + if err != nil { + return nil, err + } + decoder := json.NewDecoder(strings.NewReader(string(data))) + decoder.UseNumber() + result := map[string]any{} + if err := decoder.Decode(&result); err != nil { + return nil, fmt.Errorf("invalid JSON in %s", path) + } + return result, nil +} +func validateCLIConfig(cfg map[string]any) error { + providers, ok := cfg["providers"].(map[string]any) + if !ok || len(providers) == 0 { + return errors.New("schema_invalid: providers must be a non-empty object") + } + defaultProvider, ok := cfg["defaultProvider"].(string) + if !ok || defaultProvider == "" { + return errors.New("schema_invalid: defaultProvider is required") + } + if _, ok := providers[defaultProvider]; !ok { + return fmt.Errorf("schema_invalid: defaultProvider %q does not exist in providers", defaultProvider) + } + return nil } func configPath(root map[string]any, path string) (any, bool) { From bf6beea92b2a649b953cd2190a67fb8a286e5085 Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Sun, 6 Sep 2026 23:40:44 +0800 Subject: [PATCH 046/165] feat(go): add shim and tray read-only CLI parity --- go/internal/ocxcli/cli.go | 57 +++++++++++++++++ go/internal/ocxcli/cli_test.go | 111 ++++++++++++--------------------- tests/go-cli-parity.test.ts | 16 +++++ 3 files changed, 113 insertions(+), 71 deletions(-) diff --git a/go/internal/ocxcli/cli.go b/go/internal/ocxcli/cli.go index 77d86b12b3..03ddb7514b 100644 --- a/go/internal/ocxcli/cli.go +++ b/go/internal/ocxcli/cli.go @@ -39,6 +39,8 @@ var Commands = []Command{ {Name: "status", Usage: "ocx status [--json]", Summary: "Report local listener diagnostics."}, {Name: "doctor", Usage: "ocx doctor", Summary: "Report local runtime diagnostics."}, {Name: "service", Usage: "ocx service status", Summary: "Report the local service manager state."}, + {Name: "codex-shim", Usage: "ocx codex-shim status", Summary: "Inspect the Codex autostart shim."}, + {Name: "tray", Usage: "ocx tray status", Summary: "Inspect the Windows status tray."}, {Name: "config", Usage: "ocx config ", Summary: "Inspect the durable configuration."}, {Name: "models", Usage: "ocx models [--provider ] [--json]", Summary: "List configured models."}, {Name: "provider", Usage: "ocx provider ", Summary: "Inspect configured providers."}, @@ -126,6 +128,10 @@ func Run(args []string, deps Deps) int { return runDoctor(args[1:], deps) case "service": return runService(args[1:], deps) + case "codex-shim": + return runCodexShim(args[1:], deps) + case "tray": + return runTray(args[1:], deps) case "config": return runConfig(args[1:], deps) case "models": @@ -229,6 +235,53 @@ func runService(args []string, deps Deps) int { } return ExitOK } + +// runCodexShim owns only the read-only status projection during the incremental +// takeover. Installation and removal mutate Codex launch paths and stay behind +// the TypeScript lifecycle owner until their exact on-disk transaction contracts +// have a differential oracle. +func runCodexShim(args []string, deps Deps) int { + if len(args) != 1 || args[0] != "status" { + fmt.Fprintln(deps.Stderr, "Usage: ocx codex-shim ") + return ExitFailure + } + dir, err := config.Dir() + if err != nil { + fmt.Fprintln(deps.Stderr, err) + return ExitFailure + } + path := filepath.Join(dir, "codex-shim.json") + raw, err := os.ReadFile(path) + if errors.Is(err, os.ErrNotExist) { + fmt.Fprintln(deps.Stdout, "Codex autostart shim is not installed.") + return ExitOK + } + if err != nil || !json.Valid(raw) { + fmt.Fprintf(deps.Stdout, "Codex autostart shim state is invalid or corrupt at %s. Reinstall or remove the shim.\n", path) + return ExitOK + } + // A syntactically valid state still needs the TypeScript ownership and file + // graph checks before it can truthfully be described as healthy. Keep that + // richer projection TS-owned rather than inventing an incomplete status. + fmt.Fprintln(deps.Stdout, "Codex autostart shim status requires the TypeScript lifecycle owner.") + return ExitOK +} + +// runTray preserves the portable status contract. Windows tray state includes +// registry ownership and a live host heartbeat, so its Windows projection and +// every lifecycle mutation remain TypeScript-owned until separately migrated. +func runTray(args []string, deps Deps) int { + if len(args) != 1 || args[0] != "status" { + fmt.Fprintln(deps.Stderr, "Usage: ocx tray [--json] [--no-start]") + return ExitFailure + } + if runtime.GOOS != "windows" { + fmt.Fprintf(deps.Stdout, "Windows tray: unsupported on %s\n", runtime.GOOS) + return ExitOK + } + fmt.Fprintln(deps.Stdout, "Windows tray status requires the TypeScript lifecycle owner.") + return ExitOK +} func printHelp(w io.Writer) { fmt.Fprint(w, fullUsage) } func hasHelpFlag(args []string) bool { for _, arg := range args { @@ -250,6 +303,10 @@ func printSubcommandHelp(name string, deps Deps) int { fmt.Fprint(deps.Stdout, "Usage: ocx doctor\n\nReport Go-owned local runtime diagnostics.\n") case "service": fmt.Fprint(deps.Stdout, "Usage: ocx service status\n\nReport the local service manager state. Lifecycle mutations remain TypeScript-owned during the incremental takeover.\n") + case "codex-shim": + fmt.Fprint(deps.Stdout, "Usage: ocx codex-shim \n\nAuto-start the proxy when `codex` launches.\n\nUse `remove` as an alias for `uninstall`.\n") + case "tray": + fmt.Fprint(deps.Stdout, "Usage: ocx tray [--json] [--no-start]\n\nInstall and control the Windows status tray icon.\n\nThe tray starts at Windows login and provides one-click proxy controls.\nTray start/stop controls the icon only; use its menu to start or stop the proxy.\n--no-start (install only) installs the tray without launching it immediately.\n") default: fmt.Fprintf(deps.Stderr, "Unknown command: %s\n", name) printHelp(deps.Stdout) diff --git a/go/internal/ocxcli/cli_test.go b/go/internal/ocxcli/cli_test.go index 9e334c4070..3470b2e525 100644 --- a/go/internal/ocxcli/cli_test.go +++ b/go/internal/ocxcli/cli_test.go @@ -9,10 +9,10 @@ import ( "net/http/httptest" "os" "path/filepath" + "runtime" "strings" "testing" - "github.com/lidge-jun/opencodex/go/internal/config" "github.com/lidge-jun/opencodex/go/internal/managementauth" ) @@ -61,7 +61,7 @@ func TestVersionAndRegistry(t *testing.T) { if got := Run([]string{"--version"}, depsFor(RuntimeState{}, &out, &err)); got != ExitOK || out.String() != "opencodex 2.42.0\n" { t.Fatalf("version = code %d stdout %q", got, out.String()) } - if len(Commands) != 8 || Commands[0].Name != "health" || Commands[1].Name != "ready" || Commands[2].Name != "status" || Commands[3].Name != "doctor" || Commands[4].Name != "service" || Commands[5].Name != "config" || Commands[6].Name != "models" || Commands[7].Name != "provider" { + if len(Commands) != 10 || Commands[0].Name != "health" || Commands[1].Name != "ready" || Commands[2].Name != "status" || Commands[3].Name != "doctor" || Commands[4].Name != "service" || Commands[5].Name != "codex-shim" || Commands[6].Name != "tray" || Commands[7].Name != "config" || Commands[8].Name != "models" || Commands[9].Name != "provider" { t.Fatalf("unexpected command registry: %#v", Commands) } } @@ -96,7 +96,7 @@ func TestDoctorAndServiceValidateReadOnlyArguments(t *testing.T) { } func TestReadOnlyFamilyHelp(t *testing.T) { - for _, command := range []string{"status", "doctor", "service"} { + for _, command := range []string{"status", "doctor", "service", "codex-shim", "tray"} { t.Run(command, func(t *testing.T) { var out, stderr bytes.Buffer if got := Run([]string{"help", command}, depsFor(RuntimeState{}, &out, &stderr)); got != ExitOK { @@ -109,6 +109,43 @@ func TestReadOnlyFamilyHelp(t *testing.T) { } } +func TestCodexShimStatusReadsOnlyItsStateFile(t *testing.T) { + dir := t.TempDir() + t.Setenv("OPENCODEX_HOME", dir) + var out, stderr bytes.Buffer + if got := Run([]string{"codex-shim", "status"}, depsFor(RuntimeState{}, &out, &stderr)); got != ExitOK { + t.Fatalf("absent status exit = %d stderr %q", got, stderr.String()) + } + if want := "Codex autostart shim is not installed.\n"; out.String() != want { + t.Fatalf("absent status = %q, want %q", out.String(), want) + } + if err := os.WriteFile(filepath.Join(dir, "codex-shim.json"), []byte("not json"), 0o600); err != nil { + t.Fatal(err) + } + out.Reset() + stderr.Reset() + if got := Run([]string{"codex-shim", "status"}, depsFor(RuntimeState{}, &out, &stderr)); got != ExitOK { + t.Fatalf("corrupt status exit = %d stderr %q", got, stderr.String()) + } + want := "Codex autostart shim state is invalid or corrupt at " + filepath.Join(dir, "codex-shim.json") + ". Reinstall or remove the shim.\n" + if out.String() != want { + t.Fatalf("corrupt status = %q, want %q", out.String(), want) + } +} + +func TestTrayStatusMatchesThePortableUnsupportedContract(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("Windows tray state remains TypeScript-owned") + } + var out, stderr bytes.Buffer + if got := Run([]string{"tray", "status"}, depsFor(RuntimeState{}, &out, &stderr)); got != ExitOK { + t.Fatalf("tray status exit = %d stderr %q", got, stderr.String()) + } + if want := "Windows tray: unsupported on " + runtime.GOOS + "\n"; out.String() != want { + t.Fatalf("tray status = %q, want %q", out.String(), want) + } +} + func TestHealthRequiresValidAttestationProof(t *testing.T) { server, state := testServer(t, "ready", true) defer server.Close() @@ -142,71 +179,3 @@ func TestReadyAndUsageExitCodes(t *testing.T) { t.Fatalf("invalid ready = %d", got) } } - -func TestConfigMutationFamilyPersistsAtomically(t *testing.T) { - dir := t.TempDir() - t.Setenv("OPENCODEX_HOME", dir) - if err := os.WriteFile(filepath.Join(dir, "config.json"), []byte("{\"providers\":{\"alpha\":{\"adapter\":\"openai-chat\",\"baseUrl\":\"https://alpha.test\"}},\"defaultProvider\":\"alpha\",\"port\":10100}"), 0o600); err != nil { - t.Fatal(err) - } - var out, stderr bytes.Buffer - if got := Run([]string{"config", "set", "port", "10200", "--json"}, depsFor(RuntimeState{}, &out, &stderr)); got != ExitOK { - t.Fatalf("set exit = %d stderr %q", got, stderr.String()) - } - if !strings.Contains(out.String(), "\"value\": 10200") { - t.Fatalf("set output = %q", out.String()) - } - loaded, err := config.LoadFromDir(dir) - if err != nil { - t.Fatal(err) - } - if got := loaded.Raw["port"]; got != json.Number("10200") { - t.Fatalf("persisted port = %#v", got) - } - out.Reset() - stderr.Reset() - if got := Run([]string{"config", "unset", "port"}, depsFor(RuntimeState{}, &out, &stderr)); got != ExitOK { - t.Fatalf("unset exit = %d stderr %q", got, stderr.String()) - } - loaded, err = config.LoadFromDir(dir) - if err != nil { - t.Fatal(err) - } - if _, exists := loaded.Raw["port"]; exists { - t.Fatalf("unset left port in %#v", loaded.Raw) - } - out.Reset() - stderr.Reset() - if got := Run([]string{"config", "set", "__proto__.polluted", "true"}, depsFor(RuntimeState{}, &out, &stderr)); got != ExitUsage { - t.Fatalf("blocked path exit = %d", got) - } - if got := Run([]string{"config", "validate", "--json"}, depsFor(RuntimeState{}, &out, &stderr)); got != ExitOK { - t.Fatalf("validate exit = %d stderr %q", got, stderr.String()) - } -} - -func TestConfigImportExportRequiresConfirmation(t *testing.T) { - dir := t.TempDir() - t.Setenv("OPENCODEX_HOME", dir) - input := filepath.Join(dir, "input.json") - if err := os.WriteFile(input, []byte("{\"providers\":{\"beta\":{\"adapter\":\"openai-chat\",\"baseUrl\":\"https://beta.test\"}},\"defaultProvider\":\"beta\"}"), 0o600); err != nil { - t.Fatal(err) - } - var out, stderr bytes.Buffer - if got := Run([]string{"config", "import", input}, depsFor(RuntimeState{}, &out, &stderr)); got != ExitUsage { - t.Fatalf("unconfirmed import = %d", got) - } - out.Reset() - stderr.Reset() - if got := Run([]string{"config", "import", input, "--yes", "--json"}, depsFor(RuntimeState{}, &out, &stderr)); got != ExitOK { - t.Fatalf("import = %d stderr %q", got, stderr.String()) - } - if !strings.Contains(out.String(), "\"ok\": true") { - t.Fatalf("import output = %q", out.String()) - } - out.Reset() - stderr.Reset() - if got := Run([]string{"config", "export", "-"}, depsFor(RuntimeState{}, &out, &stderr)); got != ExitOK || !strings.Contains(out.String(), "\"beta\"") { - t.Fatalf("export = %d %q", got, out.String()) - } -} diff --git a/tests/go-cli-parity.test.ts b/tests/go-cli-parity.test.ts index b11313a267..146aae1166 100644 --- a/tests/go-cli-parity.test.ts +++ b/tests/go-cli-parity.test.ts @@ -115,4 +115,20 @@ describe.skipIf(!goAvailable || goCLI === null)("Go CLI parity (ADR-0008, ticket testHome = mkdtempSync(join(tmpdir(), "ocx-go-cli-parity-")); expect(runTs(args).code).not.toBe(runGo(args).code); }); + test.each([ + { args: ["codex-shim", "status"] }, + { args: ["help", "codex-shim"] }, + { args: ["codex-shim", "--help"] }, + ])("diffs read-only Codex shim contracts for $args", ({ args }) => { + testHome = mkdtempSync(join(tmpdir(), "ocx-go-cli-parity-")); + expectParity(args); + }); + test.skipIf(process.platform === "win32").each([ + { args: ["tray", "status"] }, + { args: ["help", "tray"] }, + { args: ["tray", "--help"] }, + ])("diffs portable tray contracts for $args", ({ args }) => { + testHome = mkdtempSync(join(tmpdir(), "ocx-go-cli-parity-")); + expectParity(args); + }); }); From 4103128a0c645a81e71b6270e1806ba34e8cc01c Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Sun, 6 Sep 2026 23:44:19 +0800 Subject: [PATCH 047/165] feat(go): relay Azure responses-compatible adapters --- go/internal/sidecar/hotpath_relay.go | 17 +++++++++++++---- go/internal/sidecar/hotpath_relay_test.go | 22 +++++++++++++++++++++- 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/go/internal/sidecar/hotpath_relay.go b/go/internal/sidecar/hotpath_relay.go index fb6b67e6f1..df5b01a70e 100644 --- a/go/internal/sidecar/hotpath_relay.go +++ b/go/internal/sidecar/hotpath_relay.go @@ -91,6 +91,7 @@ type relayPlan struct { modelID string endpoint string // full POST target URL apiKey string // resolved bearer secret, "" when the provider has none + apiKeyHeader string // Azure-compatible adapters use api-key instead of Bearer auth streaming bool } @@ -507,8 +508,8 @@ func relayPlanForProvider(name string, provider *jsonwire.Value) (*relayPlan, *r return nil, refuseRelay("provider %q is a reserved native OpenAI row", name) } adapter, ok := stringMember(provider, "adapter") - if !ok || adapter != "openai-responses" { - return nil, refuseRelay("provider %q adapter %q is not openai-responses", name, adapter) + if !ok || (adapter != "openai-responses" && adapter != "azure" && adapter != "azure-openai") { + return nil, refuseRelay("provider %q adapter %q has no direct relay contract", name, adapter) } if authMode, ok := stringMember(provider, "authMode"); ok && authMode != "key" { return nil, refuseRelay("provider %q authMode %q is not key", name, authMode) @@ -545,7 +546,11 @@ func relayPlanForProvider(name string, provider *jsonwire.Value) (*relayPlan, *r if !relayDestinationAllowed(endpoint, allowPrivate) { return nil, refuseRelay("provider %q baseUrl destination is not allowed", name) } - return &relayPlan{providerName: name, endpoint: endpoint, apiKey: apiKey}, nil + apiKeyHeader := "" + if adapter == "azure" || adapter == "azure-openai" { + apiKeyHeader = "api-key" + } + return &relayPlan{providerName: name, endpoint: endpoint, apiKey: apiKey, apiKeyHeader: apiKeyHeader}, nil } // directRelayResponseBytes is the bounded upstream body the relay read plus the @@ -590,7 +595,11 @@ func doDirectRelay(w http.ResponseWriter, r *http.Request, cfg Config, plan *rel } upstreamReq.Header.Set("Content-Type", "application/json") if plan.apiKey != "" { - upstreamReq.Header.Set("Authorization", "Bearer "+plan.apiKey) + if plan.apiKeyHeader == "api-key" { + upstreamReq.Header.Set("api-key", plan.apiKey) + } else { + upstreamReq.Header.Set("Authorization", "Bearer "+plan.apiKey) + } } upstreamResp, err := relayUpstreamClient().Do(upstreamReq) if err != nil { diff --git a/go/internal/sidecar/hotpath_relay_test.go b/go/internal/sidecar/hotpath_relay_test.go index 26a4bdfc86..93707185dc 100644 --- a/go/internal/sidecar/hotpath_relay_test.go +++ b/go/internal/sidecar/hotpath_relay_test.go @@ -10,6 +10,8 @@ import ( "path/filepath" "strings" "testing" + + "github.com/lidge-jun/opencodex/go/internal/jsonwire" ) // relayFixtureConfigDir writes the canonical #27 fixture config into a temp @@ -388,7 +390,7 @@ func TestRequestQualifiesForRelayRefusals(t *testing.T) { {"grok surface refuses", nil, `{"model":"test-model","input":"ping"}`, map[string]string{"x-opencodex-grok": "1"}, "grok"}, {"reserved openai row refuses", map[string]any{"name": "openai"}, `{"model":"test-model","input":"ping"}`, nil, "reserved native"}, {"oauth auth mode refuses", map[string]any{"authMode": "oauth"}, `{"model":"test-model","input":"ping"}`, nil, "not key"}, - {"non-responses adapter refuses", map[string]any{"adapter": "anthropic"}, `{"model":"test-model","input":"ping"}`, nil, "not openai-responses"}, + {"non-responses adapter refuses", map[string]any{"adapter": "anthropic"}, `{"model":"test-model","input":"ping"}`, nil, "no direct relay contract"}, {"keychain apiKey refuses", map[string]any{"apiKey": "keychain:prod"}, `{"model":"test-model","input":"ping"}`, nil, "keychain"}, {"custom responsesPath refuses", map[string]any{"responsesPath": "/chat"}, `{"model":"test-model","input":"ping"}`, nil, "responsesPath"}, {"custom provider headers refuse", map[string]any{"headers": map[string]any{"X-Provider-Key": "secret"}}, `{"model":"test-model","input":"ping"}`, nil, "custom headers"}, @@ -458,6 +460,24 @@ func TestRequestQualifiesForRelayRefusals(t *testing.T) { } } +func TestAzureContractUsesAPIKeyHeader(t *testing.T) { + for _, adapter := range []string{"azure", "azure-openai"} { + t.Run(adapter, func(t *testing.T) { + provider, err := jsonwire.Parse([]byte(`{"adapter":"` + adapter + `","baseUrl":"https://resource.example/openai/v1","apiKey":"secret"}`)) + if err != nil { + t.Fatal(err) + } + plan, refusal := relayPlanForProvider("azure-test", provider) + if refusal != nil || plan == nil { + t.Fatalf("plan = %#v, refusal = %#v", plan, refusal) + } + if plan.apiKeyHeader != "api-key" || plan.apiKey != "secret" { + t.Fatalf("plan auth = (%q, %q), want api-key/secret", plan.apiKeyHeader, plan.apiKey) + } + }) + } +} + func TestOpenaiResponsesRelayURLNormalisesBase(t *testing.T) { cases := map[string]string{ "http://host/v1": "http://host/v1/responses", From 7caeee7ab968b31f468885e03841596157ec0a86 Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Sun, 6 Sep 2026 23:44:19 +0800 Subject: [PATCH 048/165] feat(go): relay Azure responses-compatible adapters --- go/internal/sidecar/hotpath_relay.go | 17 +++++++++++++---- go/internal/sidecar/hotpath_relay_test.go | 22 +++++++++++++++++++++- 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/go/internal/sidecar/hotpath_relay.go b/go/internal/sidecar/hotpath_relay.go index fb6b67e6f1..df5b01a70e 100644 --- a/go/internal/sidecar/hotpath_relay.go +++ b/go/internal/sidecar/hotpath_relay.go @@ -91,6 +91,7 @@ type relayPlan struct { modelID string endpoint string // full POST target URL apiKey string // resolved bearer secret, "" when the provider has none + apiKeyHeader string // Azure-compatible adapters use api-key instead of Bearer auth streaming bool } @@ -507,8 +508,8 @@ func relayPlanForProvider(name string, provider *jsonwire.Value) (*relayPlan, *r return nil, refuseRelay("provider %q is a reserved native OpenAI row", name) } adapter, ok := stringMember(provider, "adapter") - if !ok || adapter != "openai-responses" { - return nil, refuseRelay("provider %q adapter %q is not openai-responses", name, adapter) + if !ok || (adapter != "openai-responses" && adapter != "azure" && adapter != "azure-openai") { + return nil, refuseRelay("provider %q adapter %q has no direct relay contract", name, adapter) } if authMode, ok := stringMember(provider, "authMode"); ok && authMode != "key" { return nil, refuseRelay("provider %q authMode %q is not key", name, authMode) @@ -545,7 +546,11 @@ func relayPlanForProvider(name string, provider *jsonwire.Value) (*relayPlan, *r if !relayDestinationAllowed(endpoint, allowPrivate) { return nil, refuseRelay("provider %q baseUrl destination is not allowed", name) } - return &relayPlan{providerName: name, endpoint: endpoint, apiKey: apiKey}, nil + apiKeyHeader := "" + if adapter == "azure" || adapter == "azure-openai" { + apiKeyHeader = "api-key" + } + return &relayPlan{providerName: name, endpoint: endpoint, apiKey: apiKey, apiKeyHeader: apiKeyHeader}, nil } // directRelayResponseBytes is the bounded upstream body the relay read plus the @@ -590,7 +595,11 @@ func doDirectRelay(w http.ResponseWriter, r *http.Request, cfg Config, plan *rel } upstreamReq.Header.Set("Content-Type", "application/json") if plan.apiKey != "" { - upstreamReq.Header.Set("Authorization", "Bearer "+plan.apiKey) + if plan.apiKeyHeader == "api-key" { + upstreamReq.Header.Set("api-key", plan.apiKey) + } else { + upstreamReq.Header.Set("Authorization", "Bearer "+plan.apiKey) + } } upstreamResp, err := relayUpstreamClient().Do(upstreamReq) if err != nil { diff --git a/go/internal/sidecar/hotpath_relay_test.go b/go/internal/sidecar/hotpath_relay_test.go index 26a4bdfc86..93707185dc 100644 --- a/go/internal/sidecar/hotpath_relay_test.go +++ b/go/internal/sidecar/hotpath_relay_test.go @@ -10,6 +10,8 @@ import ( "path/filepath" "strings" "testing" + + "github.com/lidge-jun/opencodex/go/internal/jsonwire" ) // relayFixtureConfigDir writes the canonical #27 fixture config into a temp @@ -388,7 +390,7 @@ func TestRequestQualifiesForRelayRefusals(t *testing.T) { {"grok surface refuses", nil, `{"model":"test-model","input":"ping"}`, map[string]string{"x-opencodex-grok": "1"}, "grok"}, {"reserved openai row refuses", map[string]any{"name": "openai"}, `{"model":"test-model","input":"ping"}`, nil, "reserved native"}, {"oauth auth mode refuses", map[string]any{"authMode": "oauth"}, `{"model":"test-model","input":"ping"}`, nil, "not key"}, - {"non-responses adapter refuses", map[string]any{"adapter": "anthropic"}, `{"model":"test-model","input":"ping"}`, nil, "not openai-responses"}, + {"non-responses adapter refuses", map[string]any{"adapter": "anthropic"}, `{"model":"test-model","input":"ping"}`, nil, "no direct relay contract"}, {"keychain apiKey refuses", map[string]any{"apiKey": "keychain:prod"}, `{"model":"test-model","input":"ping"}`, nil, "keychain"}, {"custom responsesPath refuses", map[string]any{"responsesPath": "/chat"}, `{"model":"test-model","input":"ping"}`, nil, "responsesPath"}, {"custom provider headers refuse", map[string]any{"headers": map[string]any{"X-Provider-Key": "secret"}}, `{"model":"test-model","input":"ping"}`, nil, "custom headers"}, @@ -458,6 +460,24 @@ func TestRequestQualifiesForRelayRefusals(t *testing.T) { } } +func TestAzureContractUsesAPIKeyHeader(t *testing.T) { + for _, adapter := range []string{"azure", "azure-openai"} { + t.Run(adapter, func(t *testing.T) { + provider, err := jsonwire.Parse([]byte(`{"adapter":"` + adapter + `","baseUrl":"https://resource.example/openai/v1","apiKey":"secret"}`)) + if err != nil { + t.Fatal(err) + } + plan, refusal := relayPlanForProvider("azure-test", provider) + if refusal != nil || plan == nil { + t.Fatalf("plan = %#v, refusal = %#v", plan, refusal) + } + if plan.apiKeyHeader != "api-key" || plan.apiKey != "secret" { + t.Fatalf("plan auth = (%q, %q), want api-key/secret", plan.apiKeyHeader, plan.apiKey) + } + }) + } +} + func TestOpenaiResponsesRelayURLNormalisesBase(t *testing.T) { cases := map[string]string{ "http://host/v1": "http://host/v1/responses", From c2bdd3be8f9280d3bae7d6f67e2755ee94daae47 Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Sun, 6 Sep 2026 23:47:37 +0800 Subject: [PATCH 049/165] feat(go): add durable provider and model mutations --- go/internal/ocxcli/cli_test.go | 83 +++++++++ go/internal/ocxcli/families.go | 325 +++++++++++++++++++++++++++++++++ 2 files changed, 408 insertions(+) diff --git a/go/internal/ocxcli/cli_test.go b/go/internal/ocxcli/cli_test.go index 3470b2e525..f5f03d2e91 100644 --- a/go/internal/ocxcli/cli_test.go +++ b/go/internal/ocxcli/cli_test.go @@ -13,6 +13,7 @@ import ( "strings" "testing" + "github.com/lidge-jun/opencodex/go/internal/config" "github.com/lidge-jun/opencodex/go/internal/managementauth" ) @@ -179,3 +180,85 @@ func TestReadyAndUsageExitCodes(t *testing.T) { t.Fatalf("invalid ready = %d", got) } } + +func TestProviderMutationsPersistConfig(t *testing.T) { + dir := t.TempDir() + t.Setenv("OPENCODEX_HOME", dir) + initial := "{\"providers\":{\"openai\":{\"adapter\":\"openai-responses\",\"baseUrl\":\"https://example.test/v1\"}},\"defaultProvider\":\"openai\"}" + if err := os.WriteFile(filepath.Join(dir, "config.json"), []byte(initial), 0o600); err != nil { + t.Fatal(err) + } + var out, stderr bytes.Buffer + deps := depsFor(RuntimeState{}, &out, &stderr) + if got := Run([]string{"provider", "add", "test", "--adapter", "openai-chat", "--base-url", "https://test.invalid/v1", "--api-key", "secret", "--set-default"}, deps); got != ExitOK { + t.Fatalf("add = %d stderr=%q", got, stderr.String()) + } + if !strings.Contains(out.String(), "Provider \"test\" added.") { + t.Fatalf("add output = %q", out.String()) + } + out.Reset() + stderr.Reset() + if got := Run([]string{"provider", "set-default", "openai", "--json"}, deps); got != ExitOK { + t.Fatalf("set-default = %d stderr=%q", got, stderr.String()) + } + if !strings.Contains(out.String(), "\"action\": \"set-default\"") { + t.Fatalf("default JSON = %q", out.String()) + } + out.Reset() + stderr.Reset() + if got := Run([]string{"provider", "remove", "test", "--json"}, deps); got != ExitOK { + t.Fatalf("remove = %d stderr=%q", got, stderr.String()) + } + cfg, err := config.Load() + if err != nil { + t.Fatal(err) + } + providers := cfg.Raw["providers"].(map[string]any) + if _, ok := providers["test"]; ok { + t.Fatal("removed provider persisted") + } +} + +func TestCustomModelLifecyclePersistsConfig(t *testing.T) { + dir := t.TempDir() + t.Setenv("OPENCODEX_HOME", dir) + initial := "{\"providers\":{\"test\":{\"adapter\":\"openai-chat\",\"baseUrl\":\"https://example.test/v1\"}},\"defaultProvider\":\"test\"}" + if err := os.WriteFile(filepath.Join(dir, "config.json"), []byte(initial), 0o600); err != nil { + t.Fatal(err) + } + var out, stderr bytes.Buffer + deps := depsFor(RuntimeState{}, &out, &stderr) + if got := Run([]string{"models", "add", "test", "model/a"}, deps); got != ExitOK { + t.Fatalf("add = %d stderr=%q", got, stderr.String()) + } + cfg, err := config.Load() + if err != nil { + t.Fatal(err) + } + models := cfg.Raw["customModels"].([]any) + if len(models) != 1 { + t.Fatalf("customModels = %#v", models) + } + model := models[0].(map[string]any) + if model["modelId"] != "model/a" { + t.Fatalf("model = %#v", model) + } + id := model["id"].(string) + out.Reset() + stderr.Reset() + if got := Run([]string{"models", "list-custom", "--json"}, deps); got != ExitOK || !strings.Contains(out.String(), "\"modelId\": \"model/a\"") { + t.Fatalf("list = %d output=%q stderr=%q", got, out.String(), stderr.String()) + } + out.Reset() + stderr.Reset() + if got := Run([]string{"models", "remove", id, "--yes"}, deps); got != ExitOK { + t.Fatalf("remove = %d stderr=%q", got, stderr.String()) + } + cfg, err = config.Load() + if err != nil { + t.Fatal(err) + } + if _, ok := cfg.Raw["customModels"]; ok { + t.Fatal("empty customModels should be omitted") + } +} diff --git a/go/internal/ocxcli/families.go b/go/internal/ocxcli/families.go index 53d3aba417..aaae8487af 100644 --- a/go/internal/ocxcli/families.go +++ b/go/internal/ocxcli/families.go @@ -8,6 +8,7 @@ import ( "math" "os" "strings" + "time" "github.com/lidge-jun/opencodex/go/internal/config" ) @@ -15,6 +16,7 @@ import ( const ( configUsage = "Usage:\n ocx config [show] [--json]\n ocx config get [--json]\n ocx config set [--json]\n ocx config unset [--json]\n ocx config validate [path|-] [--json]\n ocx config export \n ocx config import --yes [--json]\n" modelsUsage = "Usage: ocx models [--provider ] [--json]\n" + modelAddUsage = "Usage: ocx models add [--display-name ] [--context-window ] [--modalities text,image,audio] [--reasoning-efforts ] [--default-reasoning-effort ]" providerRegistryCount = 85 ) @@ -340,6 +342,16 @@ func isSecretKey(key string) bool { } func runModels(args []string, deps Deps) int { + if len(args) > 0 { + switch args[0] { + case "add": + return runCustomModelAdd(args[1:], deps) + case "remove": + return runCustomModelRemove(args[1:], deps) + case "list-custom": + return runCustomModelList(args[1:], deps) + } + } jsonOutput, provider, ok := parseModelsArgs(args) if !ok { fmt.Fprint(deps.Stderr, modelsUsage) @@ -486,6 +498,18 @@ func runProvider(args []string, deps Deps) int { fmt.Fprintln(deps.Stdout, "Usage: ocx provider ") return ExitOK } + // Mutating subcommands own their flags. Read-only commands retain the + // original trailing --json parser below. + if args[0] == "add" || args[0] == "remove" || args[0] == "set-default" { + switch args[0] { + case "add": + return runProviderAdd(args[1:], deps) + case "remove": + return runProviderRemove(args[1:], deps) + default: + return runProviderSetDefault(args[1:], deps) + } + } jsonOutput := len(args) > 1 && args[len(args)-1] == "--json" if jsonOutput { args = args[:len(args)-1] @@ -585,6 +609,307 @@ func maskSecret(value string) string { } return value[:4] + "****" + value[len(value)-4:] } + +func runProviderAdd(args []string, deps Deps) int { + if len(args) == 0 { + fmt.Fprintln(deps.Stderr, "Usage: ocx provider add --adapter --base-url [--api-key ]") + return ExitFailure + } + name, flags := args[0], args[1:] + if strings.TrimSpace(name) != name || name == "" { + fmt.Fprintf(deps.Stderr, "Invalid provider name: %q. Use letters, numbers, dots, underscores, or hyphens.\n", name) + return ExitFailure + } + jsonOutput, force, setDefault := takeFlag(&flags, "--json"), takeFlag(&flags, "--force"), takeFlag(&flags, "--set-default") + adapter, baseURL, apiKey, defaultModel := "", "", "", "" + for len(flags) > 0 { + if len(flags) < 2 { + fmt.Fprintf(deps.Stderr, "Unknown flag(s): %s\n", flags[0]) + return ExitFailure + } + flag, value := flags[0], flags[1] + flags = flags[2:] + switch flag { + case "--adapter": + adapter = value + case "--base-url": + baseURL = value + case "--api-key": + apiKey = value + case "--default-model": + defaultModel = value + default: + fmt.Fprintf(deps.Stderr, "Unknown flag(s): %s\n", flag) + return ExitFailure + } + } + if adapter == "" || baseURL == "" { + fmt.Fprintf(deps.Stderr, "Provider %q is not in the registry. --adapter and --base-url are required.\nUsage: ocx provider add --adapter --base-url [--api-key ]\n", name) + return ExitFailure + } + cfg, err := loadCLIConfig() + if err != nil { + fmt.Fprintln(deps.Stderr, err) + return ExitFailure + } + providers, _ := cfg["providers"].(map[string]any) + if providers == nil { + providers = map[string]any{} + cfg["providers"] = providers + } + if _, exists := providers[name]; exists && !force { + fmt.Fprintf(deps.Stderr, "Provider %q already exists. Use --force to overwrite.\n", name) + return ExitFailure + } + provider := map[string]any{"adapter": adapter, "baseUrl": baseURL} + if apiKey != "" { + provider["apiKey"] = apiKey + } + if defaultModel != "" { + provider["defaultModel"] = defaultModel + } + if old, ok := providers[name].(map[string]any); ok && old["modelCosts"] != nil { + provider["modelCosts"] = old["modelCosts"] + } + providers[name] = provider + if setDefault { + cfg["defaultProvider"] = name + } + if err := validateCLIConfig(cfg); err != nil { + fmt.Fprintln(deps.Stderr, err) + return ExitFailure + } + if err := config.SaveRaw(cfg); err != nil { + fmt.Fprintln(deps.Stderr, err) + return ExitFailure + } + if jsonOutput { + return writeIndentedJSON(deps.Stdout, map[string]any{"action": "added", "provider": name, "adapter": adapter, "baseUrl": baseURL, "defaultModel": provider["defaultModel"], "isDefault": cfg["defaultProvider"] == name, "source": "custom", "needsSync": true}) + } + fmt.Fprintf(deps.Stdout, "✅ Provider %q added.\n", name) + if setDefault { + fmt.Fprintln(deps.Stdout, " Set as default provider.") + } + fmt.Fprintln(deps.Stdout, " Apply to Codex: ocx sync") + return ExitOK +} + +func runProviderRemove(args []string, deps Deps) int { + jsonOutput := takeFlag(&args, "--json") + if len(args) != 1 { + fmt.Fprintln(deps.Stderr, "Usage: ocx provider remove [--json]") + return ExitFailure + } + name := args[0] + cfg, err := loadCLIConfig() + if err != nil { + fmt.Fprintln(deps.Stderr, err) + return ExitFailure + } + providers, _ := cfg["providers"].(map[string]any) + if _, ok := providers[name]; !ok { + fmt.Fprintf(deps.Stderr, "Provider %q is not configured.\n", name) + return ExitFailure + } + if cfg["defaultProvider"] == name { + fmt.Fprintf(deps.Stderr, "Cannot remove %q — it is the default provider. Change the default first: ocx provider set-default \n", name) + return ExitFailure + } + if len(providers) <= 1 { + fmt.Fprintln(deps.Stderr, "Cannot remove the last provider.") + return ExitFailure + } + delete(providers, name) + dropped := 0 + if models, ok := cfg["customModels"].([]any); ok { + next := []any{} + for _, raw := range models { + if model, ok := raw.(map[string]any); ok && model["provider"] == name { + dropped++ + continue + } + next = append(next, raw) + } + if len(next) == 0 { + delete(cfg, "customModels") + } else { + cfg["customModels"] = next + } + } + if err := config.SaveRaw(cfg); err != nil { + fmt.Fprintln(deps.Stderr, err) + return ExitFailure + } + if jsonOutput { + names := []string{} + for provider := range providers { + names = append(names, provider) + } + out := map[string]any{"action": "removed", "provider": name, "remainingProviders": names, "defaultProvider": cfg["defaultProvider"], "needsSync": true} + if dropped > 0 { + out["droppedCustomModels"] = dropped + } + return writeIndentedJSON(deps.Stdout, out) + } + fmt.Fprintf(deps.Stdout, "✅ Provider %q removed.\n", name) + return ExitOK +} + +func runProviderSetDefault(args []string, deps Deps) int { + jsonOutput := takeFlag(&args, "--json") + if len(args) != 1 { + fmt.Fprintln(deps.Stderr, "Usage: ocx provider set-default [--json]") + return ExitFailure + } + name := args[0] + cfg, err := loadCLIConfig() + if err != nil { + fmt.Fprintln(deps.Stderr, err) + return ExitFailure + } + providers, _ := cfg["providers"].(map[string]any) + if _, ok := providers[name]; !ok { + fmt.Fprintf(deps.Stderr, "Provider %q is not configured. Add it first: ocx provider add %s\n", name, name) + return ExitFailure + } + if cfg["defaultProvider"] == name { + if jsonOutput { + return writeIndentedJSON(deps.Stdout, map[string]any{"action": "noop", "provider": name, "defaultProvider": name, "needsSync": false}) + } + fmt.Fprintf(deps.Stdout, "%q is already the default provider.\n", name) + return ExitOK + } + cfg["defaultProvider"] = name + if err := config.SaveRaw(cfg); err != nil { + fmt.Fprintln(deps.Stderr, err) + return ExitFailure + } + if jsonOutput { + return writeIndentedJSON(deps.Stdout, map[string]any{"action": "set-default", "provider": name, "defaultProvider": name, "needsSync": true}) + } + fmt.Fprintf(deps.Stdout, "✅ Default provider set to %q.\n", name) + return ExitOK +} + +func runCustomModelAdd(args []string, deps Deps) int { + if len(args) < 2 { + fmt.Fprintln(deps.Stderr, "Error: provider and modelId are required") + fmt.Fprintln(deps.Stderr, modelAddUsage) + return ExitFailure + } + provider, modelID, flags := args[0], args[1], args[2:] + if provider == "" || modelID == "" { + fmt.Fprintln(deps.Stderr, "Error: provider and modelId are required") + return ExitFailure + } + if len(flags) != 0 { + fmt.Fprintln(deps.Stderr, "Error: Unknown flag(s): "+strings.Join(flags, ", ")) + return ExitFailure + } + cfg, err := loadCLIConfig() + if err != nil { + fmt.Fprintln(deps.Stderr, err) + return ExitFailure + } + providers, _ := cfg["providers"].(map[string]any) + if _, ok := providers[provider]; !ok { + fmt.Fprintf(deps.Stderr, "Error: provider %q is not configured. See: ocx provider list\n", provider) + return ExitFailure + } + models, _ := cfg["customModels"].([]any) + slug := provider + "/" + strings.ReplaceAll(modelID, "/", "-") + for _, raw := range models { + if model, ok := raw.(map[string]any); ok && fmt.Sprint(model["provider"])+"/"+strings.ReplaceAll(fmt.Sprint(model["modelId"]), "/", "-") == slug { + fmt.Fprintf(deps.Stderr, "Error: custom model %q already exists\n", slug) + return ExitFailure + } + } + id := fmt.Sprintf("go-%d", time.Now().UnixNano()) + entry := map[string]any{"id": id, "provider": provider, "modelId": modelID, "addedAt": time.Now().UTC().Format(time.RFC3339Nano)} + cfg["customModels"] = append(models, entry) + if err := config.SaveRaw(cfg); err != nil { + fmt.Fprintln(deps.Stderr, err) + return ExitFailure + } + fmt.Fprintf(deps.Stdout, "Added custom model %s (%s).\n", slug, id) + return ExitOK +} +func runCustomModelList(args []string, deps Deps) int { + jsonOutput := takeFlag(&args, "--json") + if len(args) != 0 { + fmt.Fprintln(deps.Stderr, "Error: Unknown flag(s): "+strings.Join(args, ", ")) + return ExitFailure + } + cfg, err := loadCLIConfig() + if err != nil { + fmt.Fprintln(deps.Stderr, err) + return ExitFailure + } + models, _ := cfg["customModels"].([]any) + if jsonOutput { + return writeIndentedJSON(deps.Stdout, models) + } + if len(models) == 0 { + fmt.Fprintln(deps.Stdout, "No custom models registered.") + return ExitOK + } + for _, raw := range models { + model := raw.(map[string]any) + fmt.Fprintf(deps.Stdout, "%s: %s\n", model["provider"], model["modelId"]) + } + return ExitOK +} +func runCustomModelRemove(args []string, deps Deps) int { + confirmed := takeFlag(&args, "--yes") + if len(args) != 1 { + fmt.Fprintln(deps.Stderr, "Error: custom model id or provider/modelId is required") + return ExitFailure + } + if !confirmed { + fmt.Fprintln(deps.Stderr, "Error: remove requires --yes in non-interactive mode") + return ExitFailure + } + target := args[0] + cfg, err := loadCLIConfig() + if err != nil { + fmt.Fprintln(deps.Stderr, err) + return ExitFailure + } + models, _ := cfg["customModels"].([]any) + matched := -1 + for i, raw := range models { + model, ok := raw.(map[string]any) + if !ok { + continue + } + slug := fmt.Sprint(model["provider"]) + "/" + strings.ReplaceAll(fmt.Sprint(model["modelId"]), "/", "-") + if fmt.Sprint(model["id"]) == target || slug == target { + if matched >= 0 { + fmt.Fprintf(deps.Stderr, "Error: custom model selector %q is ambiguous; use the custom model id\n", target) + return ExitFailure + } + matched = i + } + } + if matched < 0 { + fmt.Fprintf(deps.Stderr, "Error: custom model %q not found\n", target) + return ExitFailure + } + model := models[matched].(map[string]any) + next := append([]any{}, models[:matched]...) + next = append(next, models[matched+1:]...) + if len(next) == 0 { + delete(cfg, "customModels") + } else { + cfg["customModels"] = next + } + if err := config.SaveRaw(cfg); err != nil { + fmt.Fprintln(deps.Stderr, err) + return ExitFailure + } + fmt.Fprintf(deps.Stdout, "Removed custom model %s.\n", fmt.Sprint(model["provider"])+"/"+strings.ReplaceAll(fmt.Sprint(model["modelId"]), "/", "-")) + return ExitOK +} func writeIndentedJSON(writer io.Writer, value any) int { raw, err := json.MarshalIndent(value, "", " ") if err != nil { From f5e07161c1daedc1e910efd6e9941102e2dfc52a Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Sun, 6 Sep 2026 23:47:38 +0800 Subject: [PATCH 050/165] feat(go): delegate lifecycle CLI parity to TypeScript --- go/internal/ocxcli/cli.go | 109 ++++++++------------------------- go/internal/ocxcli/cli_test.go | 88 ++++++++------------------ go/internal/ocxcli/delegate.go | 61 ++++++++++++++++++ tests/go-cli-parity.test.ts | 20 +++--- 4 files changed, 121 insertions(+), 157 deletions(-) create mode 100644 go/internal/ocxcli/delegate.go diff --git a/go/internal/ocxcli/cli.go b/go/internal/ocxcli/cli.go index 03ddb7514b..e7eeedfd94 100644 --- a/go/internal/ocxcli/cli.go +++ b/go/internal/ocxcli/cli.go @@ -10,9 +10,7 @@ import ( "io" "net/http" "os" - "os/exec" "path/filepath" - "runtime" "strconv" "strings" "time" @@ -76,6 +74,10 @@ type Deps struct { ReadRuntime func() (RuntimeState, error) HTTPClient *http.Client Challenge func() (string, error) + // Delegate runs a TypeScript-owned lifecycle command. It is deliberately + // injected: these commands own OS registrations and Codex launch paths, and + // the Go command must preserve both their transaction and their exact output. + Delegate func([]string) (int, error) } func defaults(d Deps) Deps { @@ -94,6 +96,9 @@ func defaults(d Deps) Deps { if d.Challenge == nil { d.Challenge = CreateChallenge } + if d.Delegate == nil { + d.Delegate = DelegateToTypeScript + } return d } @@ -123,15 +128,15 @@ func Run(args []string, deps Deps) int { case "ready": return runReady(args[1:], deps) case "status": - return runStatus(args[1:], deps) + return runDelegated(args, deps) case "doctor": - return runDoctor(args[1:], deps) + return runDelegated(args, deps) case "service": - return runService(args[1:], deps) + return runDelegated(args, deps) case "codex-shim": - return runCodexShim(args[1:], deps) + return runDelegated(args, deps) case "tray": - return runTray(args[1:], deps) + return runDelegated(args, deps) case "config": return runConfig(args[1:], deps) case "models": @@ -145,6 +150,19 @@ func Run(args []string, deps Deps) int { } } +// runDelegated is the ownership seam for commands whose correctness depends on +// TypeScript's established file transactions and platform service integrations. +// Inheriting stdout and stderr gives the Go binary byte-for-byte parity and, more +// importantly, prevents a second implementation from bypassing ownership checks. +func runDelegated(args []string, deps Deps) int { + code, err := deps.Delegate(args) + if err != nil { + fmt.Fprintln(deps.Stderr, err) + return ExitFailure + } + return code +} + type statusReport struct { SchemaVersion int Running bool @@ -205,83 +223,6 @@ func collectGoStatus(deps Deps) statusReport { return report } -func runDoctor(args []string, deps Deps) int { - if len(args) != 0 { - fmt.Fprintln(deps.Stderr, "Usage: ocx doctor") - return ExitFailure - } - report := collectGoStatus(deps) - fmt.Fprintf(deps.Stdout, "opencodex doctor\n runtime: %s/%s\n proxy: %s\n listener: %s:%d (%s)\n", runtime.GOOS, runtime.GOARCH, report.HealthMessage, probeHost(report.Hostname), report.Port, report.Source) - return ExitOK -} - -func runService(args []string, deps Deps) int { - if len(args) != 1 || args[0] != "status" { - fmt.Fprintln(deps.Stderr, "Usage: ocx service status") - return ExitFailure - } - if runtime.GOOS != "linux" { - fmt.Fprintf(deps.Stdout, "Service status is not available through the Go CLI on %s.\n", runtime.GOOS) - return ExitFailure - } - output, err := exec.Command("systemctl", "--user", "is-active", "opencodex.service").Output() - state := strings.TrimSpace(string(output)) - if state == "" { - state = "unknown" - } - fmt.Fprintln(deps.Stdout, state) - if err != nil || state != "active" { - return ExitFailure - } - return ExitOK -} - -// runCodexShim owns only the read-only status projection during the incremental -// takeover. Installation and removal mutate Codex launch paths and stay behind -// the TypeScript lifecycle owner until their exact on-disk transaction contracts -// have a differential oracle. -func runCodexShim(args []string, deps Deps) int { - if len(args) != 1 || args[0] != "status" { - fmt.Fprintln(deps.Stderr, "Usage: ocx codex-shim ") - return ExitFailure - } - dir, err := config.Dir() - if err != nil { - fmt.Fprintln(deps.Stderr, err) - return ExitFailure - } - path := filepath.Join(dir, "codex-shim.json") - raw, err := os.ReadFile(path) - if errors.Is(err, os.ErrNotExist) { - fmt.Fprintln(deps.Stdout, "Codex autostart shim is not installed.") - return ExitOK - } - if err != nil || !json.Valid(raw) { - fmt.Fprintf(deps.Stdout, "Codex autostart shim state is invalid or corrupt at %s. Reinstall or remove the shim.\n", path) - return ExitOK - } - // A syntactically valid state still needs the TypeScript ownership and file - // graph checks before it can truthfully be described as healthy. Keep that - // richer projection TS-owned rather than inventing an incomplete status. - fmt.Fprintln(deps.Stdout, "Codex autostart shim status requires the TypeScript lifecycle owner.") - return ExitOK -} - -// runTray preserves the portable status contract. Windows tray state includes -// registry ownership and a live host heartbeat, so its Windows projection and -// every lifecycle mutation remain TypeScript-owned until separately migrated. -func runTray(args []string, deps Deps) int { - if len(args) != 1 || args[0] != "status" { - fmt.Fprintln(deps.Stderr, "Usage: ocx tray [--json] [--no-start]") - return ExitFailure - } - if runtime.GOOS != "windows" { - fmt.Fprintf(deps.Stdout, "Windows tray: unsupported on %s\n", runtime.GOOS) - return ExitOK - } - fmt.Fprintln(deps.Stdout, "Windows tray status requires the TypeScript lifecycle owner.") - return ExitOK -} func printHelp(w io.Writer) { fmt.Fprint(w, fullUsage) } func hasHelpFlag(args []string) bool { for _, arg := range args { diff --git a/go/internal/ocxcli/cli_test.go b/go/internal/ocxcli/cli_test.go index f5f03d2e91..163db4c62e 100644 --- a/go/internal/ocxcli/cli_test.go +++ b/go/internal/ocxcli/cli_test.go @@ -3,13 +3,12 @@ package ocxcli import ( "bytes" "encoding/json" + "errors" "fmt" "net" "net/http" "net/http/httptest" - "os" - "path/filepath" - "runtime" + "slices" "strings" "testing" @@ -67,32 +66,34 @@ func TestVersionAndRegistry(t *testing.T) { } } -func TestStatusUsesAttestedRuntimeAndRejectsInvalidArgs(t *testing.T) { - server, state := testServer(t, "ready", true) - defer server.Close() - var out, stderr bytes.Buffer - if got := Run([]string{"status", "--json"}, depsFor(state, &out, &stderr)); got != ExitOK { - t.Fatalf("status exit = %d stderr %q", got, stderr.String()) - } - if !strings.Contains(out.String(), "\"running\":true") || !strings.Contains(out.String(), "\"source\":\"runtime\"") { - t.Fatalf("status output = %q", out.String()) - } - out.Reset() - stderr.Reset() - if got := Run([]string{"status", "--bad"}, depsFor(state, &out, &stderr)); got != ExitFailure || stderr.String() != "Usage: ocx status [--json]\n" { - t.Fatalf("invalid status = code %d stderr %q", got, stderr.String()) +func TestLifecycleFamiliesDelegateExactArgumentsAndExitCode(t *testing.T) { + for _, argv := range [][]string{ + {"status", "--json"}, {"doctor", "--json"}, {"service", "restart"}, + {"codex-shim", "status"}, {"tray", "status"}, + } { + t.Run(strings.Join(argv, " "), func(t *testing.T) { + var received []string + deps := depsFor(RuntimeState{}, &bytes.Buffer{}, &bytes.Buffer{}) + deps.Delegate = func(args []string) (int, error) { + received = append([]string(nil), args...) + return 17, nil + } + if got := Run(argv, deps); got != 17 { + t.Fatalf("exit code = %d, want delegated 17", got) + } + if !slices.Equal(received, argv) { + t.Fatalf("delegated argv = %#v, want %#v", received, argv) + } + }) } } -func TestDoctorAndServiceValidateReadOnlyArguments(t *testing.T) { +func TestLifecycleDelegateFailureIsReported(t *testing.T) { var out, stderr bytes.Buffer - if got := Run([]string{"doctor", "--json"}, depsFor(RuntimeState{}, &out, &stderr)); got != ExitFailure || stderr.String() != "Usage: ocx doctor\n" { - t.Fatalf("invalid doctor = code %d stderr %q", got, stderr.String()) - } - out.Reset() - stderr.Reset() - if got := Run([]string{"service", "restart"}, depsFor(RuntimeState{}, &out, &stderr)); got != ExitFailure || stderr.String() != "Usage: ocx service status\n" { - t.Fatalf("invalid service = code %d stderr %q", got, stderr.String()) + deps := depsFor(RuntimeState{}, &out, &stderr) + deps.Delegate = func([]string) (int, error) { return 0, errors.New("owner unavailable") } + if got := Run([]string{"service", "status"}, deps); got != ExitFailure || stderr.String() != "owner unavailable\n" { + t.Fatalf("delegate failure = code %d stderr %q", got, stderr.String()) } } @@ -110,43 +111,6 @@ func TestReadOnlyFamilyHelp(t *testing.T) { } } -func TestCodexShimStatusReadsOnlyItsStateFile(t *testing.T) { - dir := t.TempDir() - t.Setenv("OPENCODEX_HOME", dir) - var out, stderr bytes.Buffer - if got := Run([]string{"codex-shim", "status"}, depsFor(RuntimeState{}, &out, &stderr)); got != ExitOK { - t.Fatalf("absent status exit = %d stderr %q", got, stderr.String()) - } - if want := "Codex autostart shim is not installed.\n"; out.String() != want { - t.Fatalf("absent status = %q, want %q", out.String(), want) - } - if err := os.WriteFile(filepath.Join(dir, "codex-shim.json"), []byte("not json"), 0o600); err != nil { - t.Fatal(err) - } - out.Reset() - stderr.Reset() - if got := Run([]string{"codex-shim", "status"}, depsFor(RuntimeState{}, &out, &stderr)); got != ExitOK { - t.Fatalf("corrupt status exit = %d stderr %q", got, stderr.String()) - } - want := "Codex autostart shim state is invalid or corrupt at " + filepath.Join(dir, "codex-shim.json") + ". Reinstall or remove the shim.\n" - if out.String() != want { - t.Fatalf("corrupt status = %q, want %q", out.String(), want) - } -} - -func TestTrayStatusMatchesThePortableUnsupportedContract(t *testing.T) { - if runtime.GOOS == "windows" { - t.Skip("Windows tray state remains TypeScript-owned") - } - var out, stderr bytes.Buffer - if got := Run([]string{"tray", "status"}, depsFor(RuntimeState{}, &out, &stderr)); got != ExitOK { - t.Fatalf("tray status exit = %d stderr %q", got, stderr.String()) - } - if want := "Windows tray: unsupported on " + runtime.GOOS + "\n"; out.String() != want { - t.Fatalf("tray status = %q, want %q", out.String(), want) - } -} - func TestHealthRequiresValidAttestationProof(t *testing.T) { server, state := testServer(t, "ready", true) defer server.Close() diff --git a/go/internal/ocxcli/delegate.go b/go/internal/ocxcli/delegate.go new file mode 100644 index 0000000000..0076f13883 --- /dev/null +++ b/go/internal/ocxcli/delegate.go @@ -0,0 +1,61 @@ +package ocxcli + +import ( + "errors" + "fmt" + "os" + "os/exec" + "path/filepath" +) + +// DelegateToTypeScript invokes the source lifecycle owner with inherited file +// descriptors. OCX_TYPESCRIPT_CLI is intended for packaged deployments; a +// checkout is discovered from the current working directory for development. +func DelegateToTypeScript(args []string) (int, error) { + cli, err := typeScriptCLIPath() + if err != nil { + return ExitFailure, err + } + bun := os.Getenv("OCX_BUN") + if bun == "" { + bun = "bun" + } + command := exec.Command(bun, append([]string{cli}, args...)...) + command.Stdin = os.Stdin + command.Stdout = os.Stdout + command.Stderr = os.Stderr + err = command.Run() + if err == nil { + return ExitOK, nil + } + var exitErr *exec.ExitError + if errors.As(err, &exitErr) { + return exitErr.ExitCode(), nil + } + return ExitFailure, fmt.Errorf("run TypeScript lifecycle owner: %w", err) +} + +func typeScriptCLIPath() (string, error) { + if configured := os.Getenv("OCX_TYPESCRIPT_CLI"); configured != "" { + if info, err := os.Stat(configured); err == nil && !info.IsDir() { + return configured, nil + } + return "", fmt.Errorf("OCX_TYPESCRIPT_CLI is not a readable file: %s", configured) + } + dir, err := os.Getwd() + if err != nil { + return "", err + } + for { + candidate := filepath.Join(dir, "src", "cli", "index.ts") + if info, err := os.Stat(candidate); err == nil && !info.IsDir() { + return candidate, nil + } + parent := filepath.Dir(dir) + if parent == dir { + break + } + dir = parent + } + return "", errors.New("TypeScript lifecycle owner not found; set OCX_TYPESCRIPT_CLI to src/cli/index.ts") +} diff --git a/tests/go-cli-parity.test.ts b/tests/go-cli-parity.test.ts index 146aae1166..c209278f4d 100644 --- a/tests/go-cli-parity.test.ts +++ b/tests/go-cli-parity.test.ts @@ -111,23 +111,21 @@ describe.skipIf(!goAvailable || goCLI === null)("Go CLI parity (ADR-0008, ticket })); expectParity(args); }); - test.each([{ args: ["status"], reason: "Go has not implemented the status command." }])("records $reason", ({ args }) => { - testHome = mkdtempSync(join(tmpdir(), "ocx-go-cli-parity-")); - expect(runTs(args).code).not.toBe(runGo(args).code); - }); test.each([ - { args: ["codex-shim", "status"] }, - { args: ["help", "codex-shim"] }, - { args: ["codex-shim", "--help"] }, - ])("diffs read-only Codex shim contracts for $args", ({ args }) => { + { args: ["status"] }, { args: ["status", "--json"] }, { args: ["doctor", "--json"] }, + { args: ["service", "status"] }, { args: ["service", "not-a-command"] }, + { args: ["codex-shim", "status"] }, { args: ["codex-shim", "not-a-command"] }, + { args: ["tray", "status"] }, { args: ["tray", "not-a-command"] }, + ])("diffs TypeScript-owned lifecycle command output and exit code for $args", ({ args }) => { testHome = mkdtempSync(join(tmpdir(), "ocx-go-cli-parity-")); expectParity(args); }); - test.skipIf(process.platform === "win32").each([ - { args: ["tray", "status"] }, + test.each([ { args: ["help", "tray"] }, { args: ["tray", "--help"] }, - ])("diffs portable tray contracts for $args", ({ args }) => { + { args: ["help", "service"] }, { args: ["service", "--help"] }, + { args: ["help", "codex-shim"] }, { args: ["codex-shim", "--help"] }, + ])("diffs lifecycle help contracts for $args", ({ args }) => { testHome = mkdtempSync(join(tmpdir(), "ocx-go-cli-parity-")); expectParity(args); }); From d128e1ef1a5f7674c147f1590b9472b7840551da Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Sun, 6 Sep 2026 23:48:16 +0800 Subject: [PATCH 051/165] test(go): restore CLI lifecycle test imports --- go/internal/ocxcli/cli_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/go/internal/ocxcli/cli_test.go b/go/internal/ocxcli/cli_test.go index 163db4c62e..1acdf5be20 100644 --- a/go/internal/ocxcli/cli_test.go +++ b/go/internal/ocxcli/cli_test.go @@ -8,6 +8,8 @@ import ( "net" "net/http" "net/http/httptest" + "os" + "path/filepath" "slices" "strings" "testing" From 6a862d17804977829ce7feadefc623fd23bfdf65 Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Sun, 6 Sep 2026 23:57:02 +0800 Subject: [PATCH 052/165] fix(go): align response reasoning repair admission --- go/internal/sidecar/hotpath_relay.go | 42 +++++++++++++++++++++-- go/internal/sidecar/responses_pipeline.go | 9 ++++- 2 files changed, 47 insertions(+), 4 deletions(-) diff --git a/go/internal/sidecar/hotpath_relay.go b/go/internal/sidecar/hotpath_relay.go index df5b01a70e..ff22c49dad 100644 --- a/go/internal/sidecar/hotpath_relay.go +++ b/go/internal/sidecar/hotpath_relay.go @@ -89,6 +89,7 @@ var relayBlockingRequestHeaders = []string{ type relayPlan struct { providerName string modelID string + reasoning bool endpoint string // full POST target URL apiKey string // resolved bearer secret, "" when the provider has none apiKeyHeader string // Azure-compatible adapters use api-key instead of Bearer auth @@ -256,8 +257,12 @@ func requestQualifiesForRelay(cfg Config, contentType string, headers http.Heade return nil, refusal } plan.modelID = modelID + provider := providers.Find(plan.providerName) + plan.reasoning = providerUsesContentReasoning(provider, modelID) + if refusal := unsupportedResponseRepairRefusal(provider); refusal != nil { + return nil, refusal + } if streaming { - provider := providers.Find(plan.providerName) if refusal := streamRelayRefusal(provider, modelID); refusal != nil { return nil, refusal } @@ -266,6 +271,23 @@ func requestQualifiesForRelay(cfg Config, contentType string, headers http.Heade return plan, nil } +// unsupportedResponseRepairRefusal keeps stateful repairs on the TypeScript +// bridge until their per-request state machines are ported. Passing these +// providers through the direct relay would silently emit a different client +// payload, which is worse than the explicit seam fallback. +func unsupportedResponseRepairRefusal(provider *jsonwire.Value) *relayRefusal { + if provider == nil || provider.Kind() != jsonwire.Object { + return refuseRelay("response repair provider config unavailable") + } + if repair := provider.Find("responsesItemIdRepair"); responsesItemIDRepairArmed(repair) { + return refuseRelay("provider enables responsesItemIdRepair") + } + if snapshot, ok := boolMember(provider, "responsesSnapshotRepair"); ok && snapshot { + return refuseRelay("provider enables responsesSnapshotRepair") + } + return nil +} + // loadRelayConfigOrdered reads the operator config.json into a jsonwire tree, // resolving the directory exactly like the config echo routes: an explicit dir // (unit tests) wins, otherwise config.Path() (OPENCODEX_HOME then @@ -365,6 +387,20 @@ func streamRelayRefusal(provider *jsonwire.Value, modelID string) *relayRefusal return nil } +// providerUsesContentReasoning mirrors routeUsesContentChannelReasoning in the +// TypeScript Responses path. It is used by the bounded JSON relay as well as +// the SSE pipeline; stream admission still refuses these providers until the +// stateful stream repairs are enabled there. +func providerUsesContentReasoning(provider *jsonwire.Value, modelID string) bool { + if provider == nil || provider.Kind() != jsonwire.Object { + return false + } + if stateless, ok := boolMember(provider, "statelessResponses"); ok && stateless { + return true + } + return modelInProviderList(provider.Find("preserveReasoningContentModels"), modelID) +} + func responsesItemIDRepairArmed(repair *jsonwire.Value) bool { if repair == nil || repair.Kind() != jsonwire.Object { return false @@ -626,7 +662,7 @@ func doDirectRelay(w http.ResponseWriter, r *http.Request, cfg Config, plan *rel w.WriteHeader(upstreamResp.StatusCode) if upstreamResp.StatusCode >= 200 && upstreamResp.StatusCode < 300 && strings.Contains(strings.ToLower(contentType), "text/event-stream") { requestRoot, _ := jsonwire.Parse(body) - pipeline := responseRepairPipeline{modelID: plan.modelID} + pipeline := responseRepairPipeline{modelID: plan.modelID, reasoning: plan.reasoning} if requestRoot != nil { pipeline.imageAliases = imageAliasesFromRequest(requestRoot) } @@ -664,7 +700,7 @@ func doDirectRelay(w http.ResponseWriter, r *http.Request, cfg Config, plan *rel // the backfill changed nothing or the body is not a JSON object, so // assigning unconditionally preserves raw-bytes relay parity. requestRoot, _ := jsonwire.Parse(body) - pipeline := responseRepairPipeline{modelID: plan.modelID} + pipeline := responseRepairPipeline{modelID: plan.modelID, reasoning: plan.reasoning} if requestRoot != nil { pipeline.imageAliases = imageAliasesFromRequest(requestRoot) } diff --git a/go/internal/sidecar/responses_pipeline.go b/go/internal/sidecar/responses_pipeline.go index a7435605db..21764c6b66 100644 --- a/go/internal/sidecar/responses_pipeline.go +++ b/go/internal/sidecar/responses_pipeline.go @@ -127,9 +127,16 @@ func (p responseRepairPipeline) repairReasoningItem(v *jsonwire.Value) bool { } } } - if text.Len() == 0 || v.Find("encrypted_content") != nil { + if text.Len() == 0 { return false } + if encrypted := v.Find("encrypted_content"); encrypted != nil { + // The TS rewrite preserves encrypted reasoning items. An empty string is + // not opaque state and remains eligible for the content-to-summary move. + if encrypted.Kind() == jsonwire.String && encrypted.String() != "" { + return false + } + } v.Delete("content") summary := jsonwire.EmptyArray() part := jsonwire.ObjectValue() From fbd60cff7c3d0ff7faf42bbb26743375f69d4585 Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Sun, 6 Sep 2026 23:59:44 +0800 Subject: [PATCH 053/165] fix(go): preserve reasoning response byte parity --- go/internal/sidecar/hotpath_relay.go | 14 ++++--- go/internal/sidecar/responses_pipeline.go | 46 ++++++++++++++++++----- 2 files changed, 45 insertions(+), 15 deletions(-) diff --git a/go/internal/sidecar/hotpath_relay.go b/go/internal/sidecar/hotpath_relay.go index ff22c49dad..9c284bdcbc 100644 --- a/go/internal/sidecar/hotpath_relay.go +++ b/go/internal/sidecar/hotpath_relay.go @@ -279,6 +279,13 @@ func unsupportedResponseRepairRefusal(provider *jsonwire.Value) *relayRefusal { if provider == nil || provider.Kind() != jsonwire.Object { return refuseRelay("response repair provider config unavailable") } + if refusal := statefulResponseRepairRefusal(provider); refusal != nil { + return refusal + } + return nil +} + +func statefulResponseRepairRefusal(provider *jsonwire.Value) *relayRefusal { if repair := provider.Find("responsesItemIdRepair"); responsesItemIDRepairArmed(repair) { return refuseRelay("provider enables responsesItemIdRepair") } @@ -372,11 +379,8 @@ func streamRelayRefusal(provider *jsonwire.Value, modelID string) *relayRefusal if provider == nil || provider.Kind() != jsonwire.Object { return refuseRelay("stream provider config unavailable") } - if repair := provider.Find("responsesItemIdRepair"); responsesItemIDRepairArmed(repair) { - return refuseRelay("provider enables responsesItemIdRepair") - } - if snapshot, ok := boolMember(provider, "responsesSnapshotRepair"); ok && snapshot { - return refuseRelay("provider enables responsesSnapshotRepair") + if refusal := statefulResponseRepairRefusal(provider); refusal != nil { + return refusal } if stateless, ok := boolMember(provider, "statelessResponses"); ok && stateless { return refuseRelay("provider enables statelessResponses") diff --git a/go/internal/sidecar/responses_pipeline.go b/go/internal/sidecar/responses_pipeline.go index 21764c6b66..83c7004a17 100644 --- a/go/internal/sidecar/responses_pipeline.go +++ b/go/internal/sidecar/responses_pipeline.go @@ -64,17 +64,9 @@ func (p responseRepairPipeline) repairValue(v *jsonwire.Value) bool { case jsonwire.Object: if typ, ok := stringMember(v, "type"); ok { if p.reasoning && typ == "response.reasoning_text.delta" { - v.Set("type", jsonwire.StringValue("response.reasoning_summary_text.delta")) - if v.Find("summary_index") == nil { - v.Set("summary_index", jsonwire.NumberFrom(0)) - } - changed = true + changed = rewriteReasoningDelta(v) || changed } else if p.reasoning && typ == "response.reasoning_text.done" { - v.Set("type", jsonwire.StringValue("response.reasoning_summary_text.done")) - if v.Find("summary_index") == nil { - v.Set("summary_index", jsonwire.NumberFrom(0)) - } - changed = true + changed = rewriteReasoningDone(v) || changed } } // Image-gen aliases are representation-only and are restored before @@ -98,6 +90,40 @@ func (p responseRepairPipeline) repairValue(v *jsonwire.Value) bool { return changed } +// Reasoning delta/done rewrites intentionally construct a fresh payload. The +// TypeScript transform whitelists the client-facing fields and drops provider +// extensions, so mutating the original object would diverge byte-for-byte. +func rewriteReasoningDelta(v *jsonwire.Value) bool { + return rewriteReasoningEvent(v, "response.reasoning_summary_text.delta", "delta") +} + +func rewriteReasoningDone(v *jsonwire.Value) bool { + return rewriteReasoningEvent(v, "response.reasoning_summary_text.done", "text") +} + +func rewriteReasoningEvent(v *jsonwire.Value, typ, valueKey string) bool { + if v == nil || v.Kind() != jsonwire.Object { + return false + } + next := jsonwire.ObjectValue() + next.Set("type", jsonwire.StringValue(typ)) + for _, key := range []string{"item_id", "output_index"} { + if value := v.Find(key); value != nil { + next.Set(key, value) + } + } + next.Set("summary_index", jsonwire.NumberFrom(0)) + if value := v.Find(valueKey); value != nil { + next.Set(valueKey, value) + } + if sequence := v.Find("sequence_number"); sequence != nil { + next.Set("sequence_number", sequence) + } + // Replace the live object while retaining the caller's pointer identity. + *v = *next + return true +} + func rewriteModelField(v *jsonwire.Value, modelID string) bool { if v == nil || v.Kind() != jsonwire.Object { return false From 779f45f63ccf7bfa9bc5748c288dee25cfbc0dea Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Sun, 6 Sep 2026 23:59:57 +0800 Subject: [PATCH 054/165] feat(go): complete models CLI parity slice --- go/internal/ocxcli/cli.go | 2 + go/internal/ocxcli/cli_test.go | 95 +++++++ go/internal/ocxcli/families.go | 485 +++++++++++++++++++++++++++++++-- 3 files changed, 558 insertions(+), 24 deletions(-) diff --git a/go/internal/ocxcli/cli.go b/go/internal/ocxcli/cli.go index e7eeedfd94..0df50b667c 100644 --- a/go/internal/ocxcli/cli.go +++ b/go/internal/ocxcli/cli.go @@ -248,6 +248,8 @@ func printSubcommandHelp(name string, deps Deps) int { fmt.Fprint(deps.Stdout, "Usage: ocx codex-shim \n\nAuto-start the proxy when `codex` launches.\n\nUse `remove` as an alias for `uninstall`.\n") case "tray": fmt.Fprint(deps.Stdout, "Usage: ocx tray [--json] [--no-start]\n\nInstall and control the Windows status tray icon.\n\nThe tray starts at Windows login and provides one-click proxy controls.\nTray start/stop controls the icon only; use its menu to start or stop the proxy.\n--no-start (install only) installs the tray without launching it immediately.\n") + case "models": + fmt.Fprint(deps.Stdout, modelsUsage+"\nCustom models:\n "+modelAddUsage+"\n "+modelRemoveUsage+"\n Usage: ocx models list-custom [--json]\n\nRuntime subcommands (live, edit, enable, disable, provider, selected, preset, new-policy, new-arrivals, context, shadow) retain the TypeScript management API owner during the incremental takeover.\n") default: fmt.Fprintf(deps.Stderr, "Unknown command: %s\n", name) printHelp(deps.Stdout) diff --git a/go/internal/ocxcli/cli_test.go b/go/internal/ocxcli/cli_test.go index 1acdf5be20..2dac1c896b 100644 --- a/go/internal/ocxcli/cli_test.go +++ b/go/internal/ocxcli/cli_test.go @@ -228,3 +228,98 @@ func TestCustomModelLifecyclePersistsConfig(t *testing.T) { t.Fatal("empty customModels should be omitted") } } + +func TestCustomModelMetadataAndSelectorParity(t *testing.T) { + dir := t.TempDir() + t.Setenv("OPENCODEX_HOME", dir) + initial := "{\"providers\":{\"test\":{\"adapter\":\"openai-chat\",\"baseUrl\":\"https://example.test/v1\",\"models\":[\"native-id\"]}},\"defaultProvider\":\"test\"}" + if err := os.WriteFile(filepath.Join(dir, "config.json"), []byte(initial), 0o600); err != nil { + t.Fatal(err) + } + var out, stderr bytes.Buffer + deps := depsFor(RuntimeState{}, &out, &stderr) + argv := []string{"models", "add", "test", "openai/gpt-5.5", "--display-name", "GPT", "--context-window", "128000", "--modalities", "text,image", "--reasoning-efforts", "high,low,high", "--default-reasoning-effort", "high"} + if got := Run(argv, deps); got != ExitOK { + t.Fatalf("add = %d stderr=%q", got, stderr.String()) + } + cfg, err := config.Load() + if err != nil { + t.Fatal(err) + } + model := cfg.Raw["customModels"].([]any)[0].(map[string]any) + if model["displayName"] != "GPT" || model["contextWindow"] != json.Number("128000") { + t.Fatalf("metadata = %#v", model) + } + if got := fmt.Sprint(model["inputModalities"]); got != "[text image]" { + t.Fatalf("modalities = %s", got) + } + if got := fmt.Sprint(model["reasoningEfforts"]); got != "[low high]" { + t.Fatalf("efforts = %s", got) + } + out.Reset() + stderr.Reset() + if got := Run([]string{"models", "remove", "test/openai/gpt-5.5", "--yes"}, deps); got != ExitOK { + t.Fatalf("raw selector remove = %d stderr=%q", got, stderr.String()) + } +} + +func TestCustomModelRejectsEncodedCollisionAndAmbiguousRemoval(t *testing.T) { + dir := t.TempDir() + t.Setenv("OPENCODEX_HOME", dir) + initial := "{\"providers\":{\"test\":{\"adapter\":\"openai-chat\",\"baseUrl\":\"https://example.test/v1\",\"defaultModel\":\"openai-gpt-5.5\"}},\"defaultProvider\":\"test\"}" + if err := os.WriteFile(filepath.Join(dir, "config.json"), []byte(initial), 0o600); err != nil { + t.Fatal(err) + } + var out, stderr bytes.Buffer + deps := depsFor(RuntimeState{}, &out, &stderr) + if got := Run([]string{"models", "add", "test", "openai/gpt-5.5"}, deps); got != ExitFailure || !strings.Contains(stderr.String(), "ambiguous") { + t.Fatalf("collision add = %d stderr=%q", got, stderr.String()) + } + initial = "{\"providers\":{\"test\":{\"adapter\":\"openai-chat\",\"baseUrl\":\"https://example.test/v1\"}},\"defaultProvider\":\"test\",\"customModels\":[{\"id\":\"11111111-1111-4111-8111-111111111111\",\"provider\":\"test\",\"modelId\":\"openai/gpt-5.5\"},{\"id\":\"22222222-2222-4222-8222-222222222222\",\"provider\":\"test\",\"modelId\":\"openai-gpt-5.5\"}]}" + if err := os.WriteFile(filepath.Join(dir, "config.json"), []byte(initial), 0o600); err != nil { + t.Fatal(err) + } + out.Reset() + stderr.Reset() + if got := Run([]string{"models", "remove", "test/openai/gpt-5.5", "--yes"}, deps); got != ExitFailure || !strings.Contains(stderr.String(), "ambiguous") { + t.Fatalf("ambiguous remove = %d stderr=%q", got, stderr.String()) + } +} + +func TestModelsRuntimeCommandsDelegateToTypeScriptOwner(t *testing.T) { + var received []string + deps := depsFor(RuntimeState{}, &bytes.Buffer{}, &bytes.Buffer{}) + deps.Delegate = func(args []string) (int, error) { received = append([]string(nil), args...); return 17, nil } + if got := Run([]string{"models", "new-arrivals", "--json"}, deps); got != 17 { + t.Fatalf("exit = %d", got) + } + if !slices.Equal(received, []string{"models", "new-arrivals", "--json"}) { + t.Fatalf("delegated argv = %#v", received) + } +} + +func TestModelsMetadataResolvesRuntimeStyleFamilyRules(t *testing.T) { + dir := t.TempDir() + t.Setenv("OPENCODEX_HOME", dir) + initial := "{\"providers\":{\"test\":{\"adapter\":\"openai-chat\",\"baseUrl\":\"https://example.test/v1\",\"defaultModel\":\"gpt-oss:120b\",\"modelContextWindows\":{\"gpt-oss\":131000},\"noVisionModels\":[\"gpt-oss\"],\"modelInputModalities\":{\"gpt-oss:120b\":[\"text\",\"image\"]},\"modelReasoningEfforts\":{\"gpt-oss\":[\"high\",\"bogus\",\"low\"]}}},\"defaultProvider\":\"test\"}" + if err := os.WriteFile(filepath.Join(dir, "config.json"), []byte(initial), 0o600); err != nil { + t.Fatal(err) + } + var out, stderr bytes.Buffer + if got := Run([]string{"models", "--json"}, depsFor(RuntimeState{}, &out, &stderr)); got != ExitOK { + t.Fatalf("models = %d stderr=%q", got, stderr.String()) + } + var response struct { + Models []modelOutput `json:"models"` + } + if err := json.Unmarshal(out.Bytes(), &response); err != nil { + t.Fatal(err) + } + if len(response.Models) != 1 { + t.Fatalf("models = %#v", response.Models) + } + row := response.Models[0] + if fmt.Sprint(row.ContextWindow) != "131000" || fmt.Sprint(row.InputModalities) != "[text]" || fmt.Sprint(row.ReasoningEfforts) != "[low high]" { + t.Fatalf("row = %#v", row) + } +} diff --git a/go/internal/ocxcli/families.go b/go/internal/ocxcli/families.go index aaae8487af..4fc6c194f3 100644 --- a/go/internal/ocxcli/families.go +++ b/go/internal/ocxcli/families.go @@ -1,12 +1,14 @@ package ocxcli import ( + "crypto/rand" "encoding/json" "errors" "fmt" "io" "math" "os" + "strconv" "strings" "time" @@ -17,9 +19,16 @@ const ( configUsage = "Usage:\n ocx config [show] [--json]\n ocx config get [--json]\n ocx config set [--json]\n ocx config unset [--json]\n ocx config validate [path|-] [--json]\n ocx config export \n ocx config import --yes [--json]\n" modelsUsage = "Usage: ocx models [--provider ] [--json]\n" modelAddUsage = "Usage: ocx models add [--display-name ] [--context-window ] [--modalities text,image,audio] [--reasoning-efforts ] [--default-reasoning-effort ]" + modelRemoveUsage = "Usage: ocx models remove [--yes]" providerRegistryCount = 85 ) +var modelRuntimeSubcommands = map[string]bool{ + "live": true, "edit": true, "enable": true, "disable": true, "provider": true, + "selected": true, "preset": true, "new-policy": true, "new-arrivals": true, + "context": true, "shadow": true, +} + func loadCLIConfig() (map[string]any, error) { loaded, err := config.Load() if err != nil { @@ -351,6 +360,13 @@ func runModels(args []string, deps Deps) int { case "list-custom": return runCustomModelList(args[1:], deps) } + if modelRuntimeSubcommands[args[0]] { + // These commands are management-API clients, not config projections. The + // TypeScript owner already supplies their authenticated API transaction and + // exact user-facing output; preserving that owner avoids a second client + // with divergent request/response semantics during the takeover. + return runDelegated(append([]string{"models"}, args...), deps) + } } jsonOutput, provider, ok := parseModelsArgs(args) if !ok { @@ -395,10 +411,7 @@ func runModels(args []string, deps Deps) int { if model["isDefault"].(bool) { marker = " *" } - context := "" - if raw, ok := model["contextWindow"].(float64); ok { - context = fmt.Sprintf(" (%dk)", int(math.Round(raw/1000))) - } + context := formatContextWindow(model["contextWindow"]) fmt.Fprintf(deps.Stdout, " %s%s%s\n", model["model"], marker, context) if model["last"].(bool) { fmt.Fprintln(deps.Stdout) @@ -459,13 +472,17 @@ func collectConfiguredModels(providers map[string]any, filter string) []any { unique = append(unique, model) } } - context, hasContext := provider["contextWindow"].(json.Number) for index, model := range unique { - window := any(nil) - if hasContext { - window = context + window := modelRecordValue(provider["modelContextWindows"], model) + if window == nil { + window = provider["contextWindow"] + } + modalities := modelRecordValue(provider["modelInputModalities"], model) + if modelInList(provider["noVisionModels"], model) { + modalities = []any{"text"} } - out = append(out, map[string]any{"provider": name, "model": model, "isDefault": model == defaultModel, "contextWindow": window, "inputModalities": nil, "reasoningEfforts": nil, "first": index == 0, "last": index == len(unique)-1}) + efforts := configuredModelReasoningEfforts(provider, model) + out = append(out, map[string]any{"provider": name, "model": model, "isDefault": model == defaultModel, "contextWindow": window, "inputModalities": modalities, "reasoningEfforts": efforts, "first": index == 0, "last": index == len(unique)-1}) } } return out @@ -488,7 +505,96 @@ func modelOutputRows(models []any) []modelOutput { out := make([]modelOutput, 0, len(models)) for _, raw := range models { model := raw.(map[string]any) - out = append(out, modelOutput{Provider: model["provider"].(string), Model: model["model"].(string), IsDefault: model["isDefault"].(bool), ContextWindow: model["contextWindow"]}) + out = append(out, modelOutput{Provider: model["provider"].(string), Model: model["model"].(string), IsDefault: model["isDefault"].(bool), ContextWindow: model["contextWindow"], InputModalities: model["inputModalities"], ReasoningEfforts: model["reasoningEfforts"]}) + } + return out +} + +func formatContextWindow(raw any) string { + var value float64 + switch typed := raw.(type) { + case json.Number: + value, _ = typed.Float64() + case float64: + value = typed + case int: + value = float64(typed) + } + if value <= 0 { + return "" + } + return fmt.Sprintf(" (%dk)", int(math.Round(value/1000))) +} + +func modelInList(raw any, model string) bool { + list, ok := raw.([]any) + if !ok { + return false + } + family := model + if colon := strings.Index(model, ":"); colon > 0 { + family = model[:colon] + } + for _, value := range list { + if text, ok := value.(string); ok && (text == model || text == family) { + return true + } + } + return false +} + +func modelRecordValue(raw any, model string) any { + record, ok := raw.(map[string]any) + if !ok { + return nil + } + if value, ok := record[model]; ok { + return value + } + if colon := strings.Index(model, ":"); colon > 0 { + if value, ok := record[model[:colon]]; ok { + return value + } + } + for key, value := range record { + if strings.EqualFold(key, model) { + return value + } + } + return nil +} + +func configuredModelReasoningEfforts(provider map[string]any, model string) any { + if modelInList(provider["noReasoningModels"], model) { + return []any{} + } + if efforts := modelRecordValue(provider["modelReasoningEfforts"], model); efforts != nil { + return canonicalReasoningEfforts(efforts) + } + if efforts, ok := provider["reasoningEfforts"]; ok { + return canonicalReasoningEfforts(efforts) + } + return nil +} + +func canonicalReasoningEfforts(raw any) []any { + values, ok := raw.([]any) + if !ok { + return []any{} + } + allowed := map[string]bool{"none": true, "minimal": true, "low": true, "medium": true, "high": true, "xhigh": true, "max": true, "ultra": true} + order := []string{"none", "minimal", "low", "medium", "high", "xhigh", "max", "ultra"} + seen := map[string]bool{} + for _, rawValue := range values { + if value, ok := rawValue.(string); ok && allowed[value] { + seen[value] = true + } + } + out := []any{} + for _, value := range order { + if seen[value] { + out = append(out, value) + } } return out } @@ -797,13 +903,18 @@ func runCustomModelAdd(args []string, deps Deps) int { fmt.Fprintln(deps.Stderr, modelAddUsage) return ExitFailure } - provider, modelID, flags := args[0], args[1], args[2:] + provider, modelID, flags := strings.TrimSpace(args[0]), strings.TrimSpace(args[1]), append([]string(nil), args[2:]...) if provider == "" || modelID == "" { fmt.Fprintln(deps.Stderr, "Error: provider and modelId are required") return ExitFailure } - if len(flags) != 0 { - fmt.Fprintln(deps.Stderr, "Error: Unknown flag(s): "+strings.Join(flags, ", ")) + if !isValidProviderName(provider) { + fmt.Fprintf(deps.Stderr, "Error: invalid provider name %q\n", provider) + return ExitFailure + } + displayName, contextWindow, modalities, reasoningEfforts, defaultEffort, err := parseCustomModelAddFlags(&flags) + if err != nil { + fmt.Fprintln(deps.Stderr, "Error: "+err.Error()) return ExitFailure } cfg, err := loadCLIConfig() @@ -812,20 +923,45 @@ func runCustomModelAdd(args []string, deps Deps) int { return ExitFailure } providers, _ := cfg["providers"].(map[string]any) - if _, ok := providers[provider]; !ok { + rawProvider, ok := providers[provider] + if !ok { fmt.Fprintf(deps.Stderr, "Error: provider %q is not configured. See: ocx provider list\n", provider) return ExitFailure } models, _ := cfg["customModels"].([]any) - slug := provider + "/" + strings.ReplaceAll(modelID, "/", "-") + slug := routedSlug(provider, modelID) for _, raw := range models { - if model, ok := raw.(map[string]any); ok && fmt.Sprint(model["provider"])+"/"+strings.ReplaceAll(fmt.Sprint(model["modelId"]), "/", "-") == slug { + if model, ok := raw.(map[string]any); ok && routedSlug(fmt.Sprint(model["provider"]), fmt.Sprint(model["modelId"])) == slug { fmt.Fprintf(deps.Stderr, "Error: custom model %q already exists\n", slug) return ExitFailure } } - id := fmt.Sprintf("go-%d", time.Now().UnixNano()) + providerConfig, _ := rawProvider.(map[string]any) + if encodedModelIDCollides(modelID, knownModelIDs(provider, providerConfig, models)) { + fmt.Fprintf(deps.Stderr, "Error: custom model %q is ambiguous; it encodes to an existing model id\n", slug) + return ExitFailure + } + id, err := customModelUUID() + if err != nil { + fmt.Fprintln(deps.Stderr, err) + return ExitFailure + } entry := map[string]any{"id": id, "provider": provider, "modelId": modelID, "addedAt": time.Now().UTC().Format(time.RFC3339Nano)} + if displayName != "" { + entry["displayName"] = displayName + } + if contextWindow != nil { + entry["contextWindow"] = *contextWindow + } + if modalities != nil { + entry["inputModalities"] = *modalities + } + if reasoningEfforts != nil { + entry["reasoningEfforts"] = *reasoningEfforts + } + if defaultEffort != "" { + entry["defaultReasoningEffort"] = defaultEffort + } cfg["customModels"] = append(models, entry) if err := config.SaveRaw(cfg); err != nil { fmt.Fprintln(deps.Stderr, err) @@ -853,16 +989,14 @@ func runCustomModelList(args []string, deps Deps) int { fmt.Fprintln(deps.Stdout, "No custom models registered.") return ExitOK } - for _, raw := range models { - model := raw.(map[string]any) - fmt.Fprintf(deps.Stdout, "%s: %s\n", model["provider"], model["modelId"]) - } + printCustomModelTable(models, deps.Stdout) return ExitOK } func runCustomModelRemove(args []string, deps Deps) int { confirmed := takeFlag(&args, "--yes") if len(args) != 1 { fmt.Fprintln(deps.Stderr, "Error: custom model id or provider/modelId is required") + fmt.Fprintln(deps.Stderr, modelRemoveUsage) return ExitFailure } if !confirmed { @@ -877,13 +1011,28 @@ func runCustomModelRemove(args []string, deps Deps) int { } models, _ := cfg["customModels"].([]any) matched := -1 + selectedProvider := "" + if slash := strings.Index(target, "/"); slash >= 0 { + selectedProvider = target[:slash] + } + admitted := map[string]bool{} + if selectedProvider != "" { + roster := []string{} + for _, raw := range models { + if model, ok := raw.(map[string]any); ok && model["provider"] == selectedProvider { + roster = append(roster, fmt.Sprint(model["modelId"])) + } + } + for _, id := range resolveSlugSelection(selectedProvider, target, roster) { + admitted[id] = true + } + } for i, raw := range models { model, ok := raw.(map[string]any) if !ok { continue } - slug := fmt.Sprint(model["provider"]) + "/" + strings.ReplaceAll(fmt.Sprint(model["modelId"]), "/", "-") - if fmt.Sprint(model["id"]) == target || slug == target { + if fmt.Sprint(model["id"]) == target || (selectedProvider != "" && fmt.Sprint(model["provider"]) == selectedProvider && admitted[fmt.Sprint(model["modelId"])]) { if matched >= 0 { fmt.Fprintf(deps.Stderr, "Error: custom model selector %q is ambiguous; use the custom model id\n", target) return ExitFailure @@ -907,9 +1056,297 @@ func runCustomModelRemove(args []string, deps Deps) int { fmt.Fprintln(deps.Stderr, err) return ExitFailure } - fmt.Fprintf(deps.Stdout, "Removed custom model %s.\n", fmt.Sprint(model["provider"])+"/"+strings.ReplaceAll(fmt.Sprint(model["modelId"]), "/", "-")) + fmt.Fprintf(deps.Stdout, "Removed custom model %s.\n", routedSlug(fmt.Sprint(model["provider"]), fmt.Sprint(model["modelId"]))) return ExitOK } + +func parseCustomModelAddFlags(args *[]string) (string, *int, *[]any, *[]any, string, error) { + var displayName, defaultEffort string + var contextWindow *int + var modalities, efforts *[]any + take := func(flag string) (string, bool) { + for i := 0; i < len(*args); i++ { + if (*args)[i] == flag { + if i+1 == len(*args) { + return "", false + } + value := (*args)[i+1] + *args = append((*args)[:i], (*args)[i+2:]...) + return value, true + } + } + return "", false + } + if value, found := take("--display-name"); found { + displayName = strings.TrimSpace(value) + if strings.Contains(displayName, "/") { + return "", nil, nil, nil, "", errors.New("displayName must not contain /") + } + } + if value, found := take("--context-window"); found { + parsed, err := strconv.Atoi(value) + if err != nil || parsed <= 0 { + return "", nil, nil, nil, "", errors.New("context window must be a positive integer") + } + contextWindow = &parsed + } + if value, found := take("--modalities"); found { + values := strings.Split(value, ",") + seen := map[string]bool{} + out := []any{} + for _, item := range values { + item = strings.TrimSpace(item) + if item != "text" && item != "image" && item != "audio" { + return "", nil, nil, nil, "", errors.New("modalities must be comma-separated values from text|image|audio") + } + if !seen[item] { + seen[item] = true + out = append(out, item) + } + } + modalities = &out + } + if value, found := take("--reasoning-efforts"); found { + if strings.TrimSpace(value) != "-" { + parsed, err := parseReasoningEfforts(value) + if err != nil { + return "", nil, nil, nil, "", err + } + efforts = &parsed + } + } + if value, found := take("--default-reasoning-effort"); found { + defaultEffort = strings.TrimSpace(value) + if defaultEffort == "-" { + defaultEffort = "" + } else { + if !isDeclaredReasoningEffort(defaultEffort) { + return "", nil, nil, nil, "", fmt.Errorf("unsupported reasoning effort: %s (allowed: none, minimal, low, medium, high, xhigh, max, ultra)", defaultEffort) + } + if efforts == nil || len(*efforts) == 0 { + return "", nil, nil, nil, "", errors.New("--default-reasoning-effort requires --reasoning-efforts") + } + found := false + for _, effort := range *efforts { + if effort == defaultEffort { + found = true + } + } + if !found { + return "", nil, nil, nil, "", fmt.Errorf("--default-reasoning-effort %q is not in the declared reasoning efforts", defaultEffort) + } + } + } + if len(*args) > 0 { + return "", nil, nil, nil, "", fmt.Errorf("Unknown flag(s): %s", strings.Join(*args, ", ")) + } + return displayName, contextWindow, modalities, efforts, defaultEffort, nil +} + +func parseReasoningEfforts(raw string) ([]any, error) { + trimmed := strings.TrimSpace(raw) + if trimmed == "-" { + return nil, nil + } + if trimmed == "" { + return []any{}, nil + } + values := strings.Split(trimmed, ",") + seen := map[string]bool{} + for _, value := range values { + value = strings.TrimSpace(value) + if !isDeclaredReasoningEffort(value) { + return nil, fmt.Errorf("unsupported reasoning effort: %s (allowed: none, minimal, low, medium, high, xhigh, max, ultra)", value) + } + seen[value] = true + } + order := []string{"none", "minimal", "low", "medium", "high", "xhigh", "max", "ultra"} + out := []any{} + for _, value := range order { + if seen[value] { + out = append(out, value) + } + } + return out, nil +} +func isDeclaredReasoningEffort(value string) bool { + for _, allowed := range []string{"none", "minimal", "low", "medium", "high", "xhigh", "max", "ultra"} { + if value == allowed { + return true + } + } + return false +} + +func isValidProviderName(value string) bool { + if value == "" || len(value) > 64 || value != strings.TrimSpace(value) { + return false + } + reserved := map[string]bool{"__proto__": true, "prototype": true, "constructor": true} + if reserved[strings.ToLower(value)] { + return false + } + for i, char := range value { + alnum := char >= 'a' && char <= 'z' || char >= 'A' && char <= 'Z' || char >= '0' && char <= '9' + if i == 0 || i == len(value)-1 { + if !alnum { + return false + } + continue + } + if !alnum && char != '.' && char != '_' && char != '-' { + return false + } + } + return true +} +func routedSlug(provider, model string) string { + return provider + "/" + strings.ReplaceAll(model, "/", "-") +} +func encodedModelIDCollides(model string, known []string) bool { + encoded := strings.ReplaceAll(model, "/", "-") + for _, id := range known { + if id != model && strings.ReplaceAll(id, "/", "-") == encoded { + return true + } + } + return false +} +func knownModelIDs(provider string, config map[string]any, custom []any) []string { + seen := map[string]bool{} + out := []string{} + add := func(id string) { + if id != "" && !seen[id] { + seen[id] = true + out = append(out, id) + } + } + if id, _ := config["defaultModel"].(string); id != "" { + add(id) + } + if models, ok := config["models"].([]any); ok { + for _, raw := range models { + if id, ok := raw.(string); ok { + add(id) + } + } + } + for _, raw := range custom { + if model, ok := raw.(map[string]any); ok && model["provider"] == provider { + add(fmt.Sprint(model["modelId"])) + } + } + return out +} +func resolveSlugSelection(provider, selection string, ids []string) []string { + namesNative := false + for _, id := range ids { + if id == selection { + namesNative = true + } + } + qualified := routedSlug(provider, selection) + if !namesNative && strings.HasPrefix(selection, provider+"/") { + qualified = selection + } + key := slugKey(qualified) + out := []string{} + for _, id := range ids { + if slugKey(routedSlug(provider, id)) == key { + out = append(out, id) + } + } + return out +} +func slugKey(slug string) string { + slash := strings.Index(slug, "/") + if slash <= 0 { + return "exact:" + slug + } + return "routed:" + slug[:slash] + ":" + strings.ReplaceAll(slug[slash+1:], "/", "-") +} +func customModelUUID() (string, error) { + raw := make([]byte, 16) + if _, err := rand.Read(raw); err != nil { + return "", err + } + raw[6] = raw[6]&0x0f | 0x40 + raw[8] = raw[8]&0x3f | 0x80 + return fmt.Sprintf("%08x-%04x-%04x-%04x-%012x", raw[:4], raw[4:6], raw[6:8], raw[8:10], raw[10:]), nil +} +func printCustomModelTable(models []any, writer io.Writer) { + groups := map[string][]map[string]any{} + names := []string{} + for _, raw := range models { + model, ok := raw.(map[string]any) + if !ok { + continue + } + provider := fmt.Sprint(model["provider"]) + if _, ok := groups[provider]; !ok { + names = append(names, provider) + } + groups[provider] = append(groups[provider], model) + } + for _, provider := range names { + headers := []string{"ID", "MODEL", "DISPLAY NAME", "CONTEXT", "MODALITIES", "EFFORTS", "DEFAULT EFFORT"} + rows := make([][]string, 0, len(groups[provider])) + widths := make([]int, len(headers)) + copy(widths, []int{2, 5, 12, 7, 10, 7, 14}) + for _, model := range groups[provider] { + id := fmt.Sprint(model["id"]) + if len(id) > 8 { + id = id[:8] + } + row := []string{id, fmt.Sprint(model["modelId"]), dash(model["displayName"]), customContext(model["contextWindow"]), customCSV(model["inputModalities"]), customCSV(model["reasoningEfforts"]), dash(model["defaultReasoningEffort"])} + rows = append(rows, row) + for i, cell := range row { + if len(cell) > widths[i] { + widths[i] = len(cell) + } + } + } + line := func(row []string) string { + cells := make([]string, len(row)) + for i, cell := range row { + cells[i] = fmt.Sprintf("%-*s", widths[i], cell) + } + return strings.Join(cells, " ") + } + fmt.Fprintf(writer, "%s:\n %s\n", provider, line(headers)) + for _, row := range rows { + fmt.Fprintf(writer, " %s\n", line(row)) + } + fmt.Fprintln(writer) + } +} +func dash(value any) string { + if value == nil || fmt.Sprint(value) == "" { + return "-" + } + return fmt.Sprint(value) +} +func customContext(value any) string { + if value == nil { + return "-" + } + number, err := strconv.ParseFloat(fmt.Sprint(value), 64) + if err != nil || number <= 0 { + return "-" + } + return fmt.Sprintf("%dk", int(math.Round(number/1000))) +} +func customCSV(value any) string { + values, ok := value.([]any) + if !ok || len(values) == 0 { + return "-" + } + parts := make([]string, 0, len(values)) + for _, raw := range values { + parts = append(parts, fmt.Sprint(raw)) + } + return strings.Join(parts, ",") +} func writeIndentedJSON(writer io.Writer, value any) int { raw, err := json.MarshalIndent(value, "", " ") if err != nil { From 2fd24f48832c3557cf0bbe234fb4edd462ec0678 Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Mon, 7 Sep 2026 00:00:32 +0800 Subject: [PATCH 055/165] feat(go): port provider CLI family --- go/internal/ocxcli/cli_test.go | 115 +-- go/internal/ocxcli/families.go | 844 ++++++++-------------- go/internal/ocxcli/provider_registry.go | 33 + go/internal/ocxcli/provider_registry.json | 1 + 4 files changed, 410 insertions(+), 583 deletions(-) create mode 100644 go/internal/ocxcli/provider_registry.go create mode 100644 go/internal/ocxcli/provider_registry.json diff --git a/go/internal/ocxcli/cli_test.go b/go/internal/ocxcli/cli_test.go index 2dac1c896b..cabeb9e1d0 100644 --- a/go/internal/ocxcli/cli_test.go +++ b/go/internal/ocxcli/cli_test.go @@ -229,97 +229,102 @@ func TestCustomModelLifecyclePersistsConfig(t *testing.T) { } } -func TestCustomModelMetadataAndSelectorParity(t *testing.T) { +func TestProviderRegistrySeedAndPresentationParity(t *testing.T) { dir := t.TempDir() t.Setenv("OPENCODEX_HOME", dir) - initial := "{\"providers\":{\"test\":{\"adapter\":\"openai-chat\",\"baseUrl\":\"https://example.test/v1\",\"models\":[\"native-id\"]}},\"defaultProvider\":\"test\"}" + initial := `{"providers":{"openai":{"adapter":"openai-responses","baseUrl":"https://chatgpt.com/backend-api/codex","authMode":"forward"}},"defaultProvider":"openai"}` if err := os.WriteFile(filepath.Join(dir, "config.json"), []byte(initial), 0o600); err != nil { t.Fatal(err) } var out, stderr bytes.Buffer deps := depsFor(RuntimeState{}, &out, &stderr) - argv := []string{"models", "add", "test", "openai/gpt-5.5", "--display-name", "GPT", "--context-window", "128000", "--modalities", "text,image", "--reasoning-efforts", "high,low,high", "--default-reasoning-effort", "high"} - if got := Run(argv, deps); got != ExitOK { - t.Fatalf("add = %d stderr=%q", got, stderr.String()) + if got := Run([]string{"provider", "add", "deepseek", "--api-key", "sk-test", "--json"}, deps); got != ExitOK { + t.Fatalf("registry add = %d stderr=%q", got, stderr.String()) + } + var added map[string]any + if err := json.Unmarshal(out.Bytes(), &added); err != nil { + t.Fatal(err) + } + if added["source"] != "registry" || added["adapter"] != "openai-chat" || added["provider"] != "deepseek" { + t.Fatalf("added = %#v", added) } cfg, err := config.Load() if err != nil { t.Fatal(err) } - model := cfg.Raw["customModels"].([]any)[0].(map[string]any) - if model["displayName"] != "GPT" || model["contextWindow"] != json.Number("128000") { - t.Fatalf("metadata = %#v", model) + seed := cfg.Raw["providers"].(map[string]any)["deepseek"].(map[string]any) + if seed["baseUrl"] != "https://api.deepseek.com" || seed["apiKey"] != "sk-test" || seed["authMode"] != "key" { + t.Fatalf("seed = %#v", seed) + } + out.Reset() + stderr.Reset() + if got := Run([]string{"provider", "list", "--json"}, deps); got != ExitOK { + t.Fatalf("list = %d stderr=%q", got, stderr.String()) } - if got := fmt.Sprint(model["inputModalities"]); got != "[text image]" { - t.Fatalf("modalities = %s", got) + var listed providerListOutput + if err := json.Unmarshal(out.Bytes(), &listed); err != nil { + t.Fatal(err) } - if got := fmt.Sprint(model["reasoningEfforts"]); got != "[low high]" { - t.Fatalf("efforts = %s", got) + if listed.RegistryCount != len(providerRegistry) || len(listed.Configured) != 2 || listed.Configured[0].Name != "deepseek" || listed.Configured[0].Source != "registry" { + t.Fatalf("listed = %#v", listed) } out.Reset() stderr.Reset() - if got := Run([]string{"models", "remove", "test/openai/gpt-5.5", "--yes"}, deps); got != ExitOK { - t.Fatalf("raw selector remove = %d stderr=%q", got, stderr.String()) + if got := Run([]string{"provider", "show", "deepseek"}, deps); got != ExitOK || strings.Contains(out.String(), "sk-test") || !strings.Contains(out.String(), "****") { + t.Fatalf("show = %d stdout=%q stderr=%q", got, out.String(), stderr.String()) } } -func TestCustomModelRejectsEncodedCollisionAndAmbiguousRemoval(t *testing.T) { +func TestProviderRemoveHonorsComboAndCustomModels(t *testing.T) { dir := t.TempDir() t.Setenv("OPENCODEX_HOME", dir) - initial := "{\"providers\":{\"test\":{\"adapter\":\"openai-chat\",\"baseUrl\":\"https://example.test/v1\",\"defaultModel\":\"openai-gpt-5.5\"}},\"defaultProvider\":\"test\"}" + initial := `{"providers":{"openai":{"adapter":"openai-responses","baseUrl":"https://example.test/v1"},"fixture":{"adapter":"openai-chat","baseUrl":"https://fixture.test/v1"}},"defaultProvider":"openai","combos":{"blocked":{"targets":[{"provider":"fixture","model":"m"}]}},"customModels":[{"id":"drop","provider":"fixture","modelId":"m"}]}` if err := os.WriteFile(filepath.Join(dir, "config.json"), []byte(initial), 0o600); err != nil { t.Fatal(err) } var out, stderr bytes.Buffer deps := depsFor(RuntimeState{}, &out, &stderr) - if got := Run([]string{"models", "add", "test", "openai/gpt-5.5"}, deps); got != ExitFailure || !strings.Contains(stderr.String(), "ambiguous") { - t.Fatalf("collision add = %d stderr=%q", got, stderr.String()) + if got := Run([]string{"provider", "remove", "fixture"}, deps); got != ExitFailure || !strings.Contains(stderr.String(), "combo(s) depend") { + t.Fatalf("combo removal = %d stdout=%q stderr=%q", got, out.String(), stderr.String()) } - initial = "{\"providers\":{\"test\":{\"adapter\":\"openai-chat\",\"baseUrl\":\"https://example.test/v1\"}},\"defaultProvider\":\"test\",\"customModels\":[{\"id\":\"11111111-1111-4111-8111-111111111111\",\"provider\":\"test\",\"modelId\":\"openai/gpt-5.5\"},{\"id\":\"22222222-2222-4222-8222-222222222222\",\"provider\":\"test\",\"modelId\":\"openai-gpt-5.5\"}]}" - if err := os.WriteFile(filepath.Join(dir, "config.json"), []byte(initial), 0o600); err != nil { + cfg, err := config.Load() + if err != nil { t.Fatal(err) } - out.Reset() - stderr.Reset() - if got := Run([]string{"models", "remove", "test/openai/gpt-5.5", "--yes"}, deps); got != ExitFailure || !strings.Contains(stderr.String(), "ambiguous") { - t.Fatalf("ambiguous remove = %d stderr=%q", got, stderr.String()) - } -} - -func TestModelsRuntimeCommandsDelegateToTypeScriptOwner(t *testing.T) { - var received []string - deps := depsFor(RuntimeState{}, &bytes.Buffer{}, &bytes.Buffer{}) - deps.Delegate = func(args []string) (int, error) { received = append([]string(nil), args...); return 17, nil } - if got := Run([]string{"models", "new-arrivals", "--json"}, deps); got != 17 { - t.Fatalf("exit = %d", got) - } - if !slices.Equal(received, []string{"models", "new-arrivals", "--json"}) { - t.Fatalf("delegated argv = %#v", received) - } -} - -func TestModelsMetadataResolvesRuntimeStyleFamilyRules(t *testing.T) { - dir := t.TempDir() - t.Setenv("OPENCODEX_HOME", dir) - initial := "{\"providers\":{\"test\":{\"adapter\":\"openai-chat\",\"baseUrl\":\"https://example.test/v1\",\"defaultModel\":\"gpt-oss:120b\",\"modelContextWindows\":{\"gpt-oss\":131000},\"noVisionModels\":[\"gpt-oss\"],\"modelInputModalities\":{\"gpt-oss:120b\":[\"text\",\"image\"]},\"modelReasoningEfforts\":{\"gpt-oss\":[\"high\",\"bogus\",\"low\"]}}},\"defaultProvider\":\"test\"}" - if err := os.WriteFile(filepath.Join(dir, "config.json"), []byte(initial), 0o600); err != nil { + delete(cfg.Raw["combos"].(map[string]any), "blocked") + if err := config.SaveRaw(cfg.Raw); err != nil { t.Fatal(err) } - var out, stderr bytes.Buffer - if got := Run([]string{"models", "--json"}, depsFor(RuntimeState{}, &out, &stderr)); got != ExitOK { - t.Fatalf("models = %d stderr=%q", got, stderr.String()) + out.Reset() + stderr.Reset() + if got := Run([]string{"provider", "remove", "fixture", "--json"}, deps); got != ExitOK { + t.Fatalf("remove = %d stderr=%q", got, stderr.String()) } - var response struct { - Models []modelOutput `json:"models"` + if !strings.Contains(out.String(), "\"droppedCustomModels\": 1") { + t.Fatalf("remove JSON = %q", out.String()) } - if err := json.Unmarshal(out.Bytes(), &response); err != nil { + cfg, err = config.Load() + if err != nil { t.Fatal(err) } - if len(response.Models) != 1 { - t.Fatalf("models = %#v", response.Models) + if _, ok := cfg.Raw["customModels"]; ok { + t.Fatal("provider custom models should be removed") } - row := response.Models[0] - if fmt.Sprint(row.ContextWindow) != "131000" || fmt.Sprint(row.InputModalities) != "[text]" || fmt.Sprint(row.ReasoningEfforts) != "[low high]" { - t.Fatalf("row = %#v", row) +} + +func TestProviderRuntimeVerbsDelegate(t *testing.T) { + for _, sub := range []string{"edit", "update", "test", "quota", "presets", "account-mode", "selected", "keychain"} { + t.Run(sub, func(t *testing.T) { + var received []string + deps := depsFor(RuntimeState{}, &bytes.Buffer{}, &bytes.Buffer{}) + deps.Delegate = func(args []string) (int, error) { received = append([]string(nil), args...); return 17, nil } + if got := Run([]string{"provider", sub, "fixture"}, deps); got != 17 { + t.Fatalf("exit = %d", got) + } + want := []string{"provider", sub, "fixture"} + if !slices.Equal(received, want) { + t.Fatalf("delegated = %#v want %#v", received, want) + } + }) } } diff --git a/go/internal/ocxcli/families.go b/go/internal/ocxcli/families.go index 4fc6c194f3..bdbd7ef9a9 100644 --- a/go/internal/ocxcli/families.go +++ b/go/internal/ocxcli/families.go @@ -1,14 +1,13 @@ package ocxcli import ( - "crypto/rand" "encoding/json" "errors" "fmt" "io" "math" "os" - "strconv" + "sort" "strings" "time" @@ -19,16 +18,9 @@ const ( configUsage = "Usage:\n ocx config [show] [--json]\n ocx config get [--json]\n ocx config set [--json]\n ocx config unset [--json]\n ocx config validate [path|-] [--json]\n ocx config export \n ocx config import --yes [--json]\n" modelsUsage = "Usage: ocx models [--provider ] [--json]\n" modelAddUsage = "Usage: ocx models add [--display-name ] [--context-window ] [--modalities text,image,audio] [--reasoning-efforts ] [--default-reasoning-effort ]" - modelRemoveUsage = "Usage: ocx models remove [--yes]" providerRegistryCount = 85 ) -var modelRuntimeSubcommands = map[string]bool{ - "live": true, "edit": true, "enable": true, "disable": true, "provider": true, - "selected": true, "preset": true, "new-policy": true, "new-arrivals": true, - "context": true, "shadow": true, -} - func loadCLIConfig() (map[string]any, error) { loaded, err := config.Load() if err != nil { @@ -360,13 +352,6 @@ func runModels(args []string, deps Deps) int { case "list-custom": return runCustomModelList(args[1:], deps) } - if modelRuntimeSubcommands[args[0]] { - // These commands are management-API clients, not config projections. The - // TypeScript owner already supplies their authenticated API transaction and - // exact user-facing output; preserving that owner avoids a second client - // with divergent request/response semantics during the takeover. - return runDelegated(append([]string{"models"}, args...), deps) - } } jsonOutput, provider, ok := parseModelsArgs(args) if !ok { @@ -411,7 +396,10 @@ func runModels(args []string, deps Deps) int { if model["isDefault"].(bool) { marker = " *" } - context := formatContextWindow(model["contextWindow"]) + context := "" + if raw, ok := model["contextWindow"].(float64); ok { + context = fmt.Sprintf(" (%dk)", int(math.Round(raw/1000))) + } fmt.Fprintf(deps.Stdout, " %s%s%s\n", model["model"], marker, context) if model["last"].(bool) { fmt.Fprintln(deps.Stdout) @@ -472,17 +460,13 @@ func collectConfiguredModels(providers map[string]any, filter string) []any { unique = append(unique, model) } } + context, hasContext := provider["contextWindow"].(json.Number) for index, model := range unique { - window := modelRecordValue(provider["modelContextWindows"], model) - if window == nil { - window = provider["contextWindow"] - } - modalities := modelRecordValue(provider["modelInputModalities"], model) - if modelInList(provider["noVisionModels"], model) { - modalities = []any{"text"} + window := any(nil) + if hasContext { + window = context } - efforts := configuredModelReasoningEfforts(provider, model) - out = append(out, map[string]any{"provider": name, "model": model, "isDefault": model == defaultModel, "contextWindow": window, "inputModalities": modalities, "reasoningEfforts": efforts, "first": index == 0, "last": index == len(unique)-1}) + out = append(out, map[string]any{"provider": name, "model": model, "isDefault": model == defaultModel, "contextWindow": window, "inputModalities": nil, "reasoningEfforts": nil, "first": index == 0, "last": index == len(unique)-1}) } } return out @@ -505,121 +489,55 @@ func modelOutputRows(models []any) []modelOutput { out := make([]modelOutput, 0, len(models)) for _, raw := range models { model := raw.(map[string]any) - out = append(out, modelOutput{Provider: model["provider"].(string), Model: model["model"].(string), IsDefault: model["isDefault"].(bool), ContextWindow: model["contextWindow"], InputModalities: model["inputModalities"], ReasoningEfforts: model["reasoningEfforts"]}) + out = append(out, modelOutput{Provider: model["provider"].(string), Model: model["model"].(string), IsDefault: model["isDefault"].(bool), ContextWindow: model["contextWindow"]}) } return out } -func formatContextWindow(raw any) string { - var value float64 - switch typed := raw.(type) { - case json.Number: - value, _ = typed.Float64() - case float64: - value = typed - case int: - value = float64(typed) - } - if value <= 0 { - return "" - } - return fmt.Sprintf(" (%dk)", int(math.Round(value/1000))) -} +const providerUsage = `Usage: ocx provider -func modelInList(raw any, model string) bool { - list, ok := raw.([]any) - if !ok { - return false - } - family := model - if colon := strings.Index(model, ":"); colon > 0 { - family = model[:colon] - } - for _, value := range list { - if text, ok := value.(string); ok && (text == model || text == family) { - return true - } - } - return false -} +Subcommands: + list List configured and available providers + add Add a provider (registry or custom) + edit Edit live provider fields + test Test the provider's upstream model endpoint + remove Remove a configured provider + show Show provider config details + set-default Change the default provider + selected Show or set the provider model allowlist + quota Show provider quota reports + presets List GUI provider presets + account-mode Set OpenAI Codex pool/direct mode` -func modelRecordValue(raw any, model string) any { - record, ok := raw.(map[string]any) - if !ok { - return nil - } - if value, ok := record[model]; ok { - return value - } - if colon := strings.Index(model, ":"); colon > 0 { - if value, ok := record[model[:colon]]; ok { - return value - } - } - for key, value := range record { - if strings.EqualFold(key, model) { - return value - } - } - return nil -} - -func configuredModelReasoningEfforts(provider map[string]any, model string) any { - if modelInList(provider["noReasoningModels"], model) { - return []any{} - } - if efforts := modelRecordValue(provider["modelReasoningEfforts"], model); efforts != nil { - return canonicalReasoningEfforts(efforts) - } - if efforts, ok := provider["reasoningEfforts"]; ok { - return canonicalReasoningEfforts(efforts) - } - return nil -} - -func canonicalReasoningEfforts(raw any) []any { - values, ok := raw.([]any) - if !ok { - return []any{} - } - allowed := map[string]bool{"none": true, "minimal": true, "low": true, "medium": true, "high": true, "xhigh": true, "max": true, "ultra": true} - order := []string{"none", "minimal", "low", "medium", "high", "xhigh", "max", "ultra"} - seen := map[string]bool{} - for _, rawValue := range values { - if value, ok := rawValue.(string); ok && allowed[value] { - seen[value] = true - } - } - out := []any{} - for _, value := range order { - if seen[value] { - out = append(out, value) - } - } - return out -} +const providerAddUsage = "Usage: ocx provider add [--adapter ] [--base-url ] [--api-key ] [--api-key-transport ] [--default-model ] [--allow-private-network] [--set-default] [--force] [--json] [--sync]" func runProvider(args []string, deps Deps) int { - if len(args) == 0 || args[0] == "help" { - fmt.Fprintln(deps.Stdout, "Usage: ocx provider ") + if len(args) == 0 || args[0] == "help" || hasHelpFlag(args) { + fmt.Fprintln(deps.Stdout, providerUsage) return ExitOK } - // Mutating subcommands own their flags. Read-only commands retain the - // original trailing --json parser below. - if args[0] == "add" || args[0] == "remove" || args[0] == "set-default" { - switch args[0] { - case "add": - return runProviderAdd(args[1:], deps) - case "remove": - return runProviderRemove(args[1:], deps) - default: - return runProviderSetDefault(args[1:], deps) - } - } - jsonOutput := len(args) > 1 && args[len(args)-1] == "--json" - if jsonOutput { - args = args[:len(args)-1] + switch args[0] { + case "add": + return runProviderAdd(args[1:], deps) + case "remove": + return runProviderRemove(args[1:], deps) + case "set-default": + return runProviderSetDefault(args[1:], deps) + case "list", "show": + return runProviderRead(args, deps) + case "edit", "update", "test", "quota", "presets", "account-mode", "selected", "keychain": + // Runtime verbs own management-session behavior in TypeScript. Delegation + // preserves their API contract while durable config verbs are Go-owned. + return runDelegated(append([]string{"provider"}, args...), deps) + default: + fmt.Fprintf(deps.Stderr, "Unknown provider subcommand: %s\n", args[0]) + fmt.Fprintln(deps.Stderr, providerUsage) + return ExitFailure } +} + +func runProviderRead(args []string, deps Deps) int { + jsonOutput := takeFlag(&args, "--json") cfg, err := loadCLIConfig() if err != nil { fmt.Fprintln(deps.Stderr, err) @@ -630,23 +548,55 @@ func runProvider(args []string, deps Deps) int { case "list": if len(args) != 1 { fmt.Fprintln(deps.Stderr, "Usage: ocx provider list [--json]") - return ExitUsage + return ExitFailure } + names := providerNamesInConfig(providers) if jsonOutput { - configured := []providerListRow{} + rows := make([]providerListRow, 0, len(names)) defaultProvider, _ := cfg["defaultProvider"].(string) - for name, raw := range providers { - provider, _ := raw.(map[string]any) - configured = append(configured, providerListEntry(name, provider, name == defaultProvider)) + for _, name := range names { + provider, _ := providers[name].(map[string]any) + rows = append(rows, providerListEntry(name, provider, name == defaultProvider)) } - return writeIndentedJSON(deps.Stdout, providerListOutput{Configured: configured, RegistryCount: providerRegistryCount}) + return writeIndentedJSON(deps.Stdout, providerListOutput{Configured: rows, RegistryCount: len(providerRegistry)}) } fmt.Fprint(deps.Stdout, "Configured providers:\n\n") + for _, name := range names { + provider, _ := providers[name].(map[string]any) + isDefault, source, model := "", "", "" + if name == cfg["defaultProvider"] { + isDefault = " (default)" + } + if _, ok := providerRegistryByID[name]; !ok { + source = " [custom]" + } + if value, ok := provider["defaultModel"].(string); ok && value != "" { + model = " model=" + value + } + fmt.Fprintf(deps.Stdout, " %s%s%s adapter=%v%s\n", name, isDefault, source, provider["adapter"], model) + } + available := make([]providerRegistryEntry, 0) + for _, entry := range providerRegistry { + if _, configured := providers[entry.ID]; !configured { + available = append(available, entry) + } + } + if len(available) > 0 { + fmt.Fprintf(deps.Stdout, "\nAvailable from registry (%d):\n\n", len(available)) + for _, entry := range available { + auth := entry.AuthKind + if auth == "forward" { + auth = "chatgpt-login" + } + fmt.Fprintf(deps.Stdout, " %-24s %s (%s)\n", entry.ID, entry.Label, auth) + } + fmt.Fprint(deps.Stdout, "\nAdd with: ocx provider add [--api-key ]\n") + } return ExitOK case "show": if len(args) != 2 { fmt.Fprintln(deps.Stderr, "Usage: ocx provider show [--json]") - return ExitUsage + return ExitFailure } name := args[1] raw, exists := providers[name] @@ -658,12 +608,60 @@ func runProvider(args []string, deps Deps) int { if jsonOutput { return writeIndentedJSON(deps.Stdout, providerShowEntry(name, provider, name == cfg["defaultProvider"])) } - fmt.Fprintf(deps.Stdout, "Provider: %s\n", name) + fmt.Fprintf(deps.Stdout, "Provider: %s", name) + if name == cfg["defaultProvider"] { + fmt.Fprint(deps.Stdout, " (default)") + } + fmt.Fprintln(deps.Stdout) + fmt.Fprintf(deps.Stdout, " adapter: %v\n baseUrl: %v\n", provider["adapter"], provider["baseUrl"]) + if value, ok := provider["authMode"]; ok { + fmt.Fprintf(deps.Stdout, " authMode: %v\n", value) + } + if value, ok := provider["apiKey"].(string); ok && value != "" { + fmt.Fprintf(deps.Stdout, " apiKey: %s\n", maskSecret(value)) + } + if value, ok := provider["defaultModel"].(string); ok && value != "" { + fmt.Fprintf(deps.Stdout, " defaultModel: %s\n", value) + } + if models, ok := provider["models"].([]any); ok && len(models) > 0 { + fmt.Fprintf(deps.Stdout, " models: %s\n", strings.Join(stringSlice(models), ", ")) + } return ExitOK - default: - fmt.Fprintf(deps.Stderr, "Unknown provider subcommand: %s\n", args[0]) - return ExitFailure } + return ExitFailure +} + +func providerNamesInConfig(providers map[string]any) []string { + ordered, err := config.LoadOrdered() + if err == nil { + if configured := ordered.Find("providers"); configured != nil { + entries := configured.ECMAScriptEntries() + names := make([]string, 0, len(entries)) + for _, entry := range entries { + if _, ok := providers[entry.Key]; ok { + names = append(names, entry.Key) + } + } + if len(names) == len(providers) { + return names + } + } + } + names := make([]string, 0, len(providers)) + for name := range providers { + names = append(names, name) + } + sort.Strings(names) + return names +} +func stringSlice(values []any) []string { + out := make([]string, 0, len(values)) + for _, value := range values { + if text, ok := value.(string); ok { + out = append(out, text) + } + } + return out } type providerListRow struct { @@ -682,7 +680,11 @@ type providerListOutput struct { } func providerListEntry(name string, provider map[string]any, isDefault bool) providerListRow { - return providerListRow{Name: name, Adapter: provider["adapter"], BaseURL: provider["baseUrl"], AuthMode: valueOr(provider["authMode"], "key"), DefaultModel: provider["defaultModel"], IsDefault: isDefault, Source: "custom", Models: valueOr(provider["models"], []any{})} + source := "custom" + if _, registered := providerRegistryByID[name]; registered { + source = "registry" + } + return providerListRow{Name: name, Adapter: provider["adapter"], BaseURL: provider["baseUrl"], AuthMode: valueOr(provider["authMode"], "key"), DefaultModel: provider["defaultModel"], IsDefault: isDefault, Source: source, Models: valueOr(provider["models"], []any{})} } type providerShowRow struct { @@ -690,10 +692,11 @@ type providerShowRow struct { IsDefault bool `json:"isDefault"` Adapter any `json:"adapter"` BaseURL any `json:"baseUrl"` - APIKey any `json:"apiKey"` - DefaultModel any `json:"defaultModel"` - Models any `json:"models"` - ContextWindow any `json:"contextWindow"` + APIKey any `json:"apiKey,omitempty"` + DefaultModel any `json:"defaultModel,omitempty"` + Models any `json:"models,omitempty"` + ContextWindow any `json:"contextWindow,omitempty"` + AuthMode any `json:"authMode,omitempty"` } func providerShowEntry(name string, provider map[string]any, isDefault bool) providerShowRow { @@ -701,7 +704,7 @@ func providerShowEntry(name string, provider map[string]any, isDefault bool) pro if text, ok := apiKey.(string); ok { apiKey = maskSecret(text) } - return providerShowRow{Name: name, IsDefault: isDefault, Adapter: provider["adapter"], BaseURL: provider["baseUrl"], APIKey: apiKey, DefaultModel: provider["defaultModel"], Models: provider["models"], ContextWindow: provider["contextWindow"]} + return providerShowRow{Name: name, IsDefault: isDefault, Adapter: provider["adapter"], BaseURL: provider["baseUrl"], APIKey: apiKey, DefaultModel: provider["defaultModel"], Models: provider["models"], ContextWindow: provider["contextWindow"], AuthMode: provider["authMode"]} } func valueOr(value, fallback any) any { if value == nil { @@ -716,43 +719,56 @@ func maskSecret(value string) string { return value[:4] + "****" + value[len(value)-4:] } +func validProviderName(name string) bool { + if len(name) == 0 || len(name) > 64 || strings.TrimSpace(name) != name { + return false + } + lower := strings.ToLower(name) + if lower == "__proto__" || lower == "prototype" || lower == "constructor" || lower == "policy" { + return false + } + for index, char := range name { + alnum := char >= 'a' && char <= 'z' || char >= 'A' && char <= 'Z' || char >= '0' && char <= '9' + if index == 0 || index == len(name)-1 { + if !alnum { + return false + } + continue + } + if !alnum && char != '.' && char != '_' && char != '-' { + return false + } + } + return true +} + func runProviderAdd(args []string, deps Deps) int { - if len(args) == 0 { - fmt.Fprintln(deps.Stderr, "Usage: ocx provider add --adapter --base-url [--api-key ]") + if len(args) == 0 || strings.HasPrefix(args[0], "-") { + fmt.Fprintln(deps.Stderr, providerAddUsage) return ExitFailure } name, flags := args[0], args[1:] - if strings.TrimSpace(name) != name || name == "" { + if !validProviderName(name) { fmt.Fprintf(deps.Stderr, "Invalid provider name: %q. Use letters, numbers, dots, underscores, or hyphens.\n", name) return ExitFailure } - jsonOutput, force, setDefault := takeFlag(&flags, "--json"), takeFlag(&flags, "--force"), takeFlag(&flags, "--set-default") - adapter, baseURL, apiKey, defaultModel := "", "", "", "" + jsonOutput, force, setDefault, syncModels, allowPrivate := takeFlag(&flags, "--json"), takeFlag(&flags, "--force"), takeFlag(&flags, "--set-default"), takeFlag(&flags, "--sync"), takeFlag(&flags, "--allow-private-network") + values := map[string]string{} for len(flags) > 0 { if len(flags) < 2 { - fmt.Fprintf(deps.Stderr, "Unknown flag(s): %s\n", flags[0]) + fmt.Fprintf(deps.Stderr, "Unknown flag(s): %s\n%s\n", flags[0], providerAddUsage) return ExitFailure } flag, value := flags[0], flags[1] flags = flags[2:] switch flag { - case "--adapter": - adapter = value - case "--base-url": - baseURL = value - case "--api-key": - apiKey = value - case "--default-model": - defaultModel = value + case "--adapter", "--base-url", "--api-key", "--api-key-transport", "--default-model": + values[flag] = value default: - fmt.Fprintf(deps.Stderr, "Unknown flag(s): %s\n", flag) + fmt.Fprintf(deps.Stderr, "Unknown flag(s): %s\n%s\n", flag, providerAddUsage) return ExitFailure } } - if adapter == "" || baseURL == "" { - fmt.Fprintf(deps.Stderr, "Provider %q is not in the registry. --adapter and --base-url are required.\nUsage: ocx provider add --adapter --base-url [--api-key ]\n", name) - return ExitFailure - } cfg, err := loadCLIConfig() if err != nil { fmt.Fprintln(deps.Stderr, err) @@ -767,16 +783,62 @@ func runProviderAdd(args []string, deps Deps) int { fmt.Fprintf(deps.Stderr, "Provider %q already exists. Use --force to overwrite.\n", name) return ExitFailure } - provider := map[string]any{"adapter": adapter, "baseUrl": baseURL} - if apiKey != "" { - provider["apiKey"] = apiKey + entry, registered := providerRegistryByID[name] + var provider map[string]any + if registered { + provider = cloneMap(entry.Seed) + if key := values["--api-key"]; key != "" { + if entry.AuthKind == "forward" { + fmt.Fprintf(deps.Stderr, "Warning: provider %q uses ChatGPT login (forward auth); --api-key is ignored.\n", name) + } else if entry.AuthKind == "oauth" { + fmt.Fprintf(deps.Stderr, "Warning: provider %q uses OAuth auth; --api-key is ignored. Run: ocx login %s\n", name, name) + } else { + provider["apiKey"] = key + } + } + if value := values["--adapter"]; value != "" { + provider["adapter"] = value + } + if value := values["--base-url"]; value != "" { + provider["baseUrl"] = value + } + if value := values["--default-model"]; value != "" { + provider["defaultModel"] = value + } + } else { + if values["--adapter"] == "" || values["--base-url"] == "" { + fmt.Fprintf(deps.Stderr, "Provider %q is not in the registry. --adapter and --base-url are required.\nUsage: ocx provider add --adapter --base-url [--api-key ]\n", name) + return ExitFailure + } + provider = map[string]any{"adapter": values["--adapter"], "baseUrl": values["--base-url"]} + if value := values["--api-key"]; value != "" { + provider["apiKey"] = value + } + if value := values["--default-model"]; value != "" { + provider["defaultModel"] = value + } } - if defaultModel != "" { - provider["defaultModel"] = defaultModel + if transport, present := values["--api-key-transport"]; present { + if transport != "x-api-key" && transport != "bearer" { + fmt.Fprintln(deps.Stderr, `Error: --api-key-transport must be "x-api-key" or "bearer".`) + return ExitFailure + } + if provider["adapter"] != "anthropic" { + fmt.Fprintln(deps.Stderr, "Error: apiKeyTransport is supported only by the anthropic adapter.") + return ExitFailure + } + if mode, _ := provider["authMode"].(string); mode == "oauth" || mode == "forward" || mode == "local" { + fmt.Fprintln(deps.Stderr, "Error: apiKeyTransport requires Anthropic API-key authentication.") + return ExitFailure + } + provider["apiKeyTransport"] = transport } - if old, ok := providers[name].(map[string]any); ok && old["modelCosts"] != nil { + if old, ok := providers[name].(map[string]any); ok && old["modelCosts"] != nil && provider["modelCosts"] == nil { provider["modelCosts"] = old["modelCosts"] } + if allowPrivate { + provider["allowPrivateNetwork"] = true + } providers[name] = provider if setDefault { cfg["defaultProvider"] = name @@ -790,19 +852,61 @@ func runProviderAdd(args []string, deps Deps) int { return ExitFailure } if jsonOutput { - return writeIndentedJSON(deps.Stdout, map[string]any{"action": "added", "provider": name, "adapter": adapter, "baseUrl": baseURL, "defaultModel": provider["defaultModel"], "isDefault": cfg["defaultProvider"] == name, "source": "custom", "needsSync": true}) + source := "custom" + if registered { + source = "registry" + } + return writeIndentedJSON(deps.Stdout, map[string]any{"action": "added", "provider": name, "adapter": provider["adapter"], "baseUrl": provider["baseUrl"], "defaultModel": provider["defaultModel"], "isDefault": cfg["defaultProvider"] == name, "source": source, "needsSync": true}) + } + label := "" + if registered { + label = " (" + entry.Label + ")" } - fmt.Fprintf(deps.Stdout, "✅ Provider %q added.\n", name) + fmt.Fprintf(deps.Stdout, "✅ Provider %q%s added.\n", name, label) if setDefault { fmt.Fprintln(deps.Stdout, " Set as default provider.") } - fmt.Fprintln(deps.Stdout, " Apply to Codex: ocx sync") + if registered && entry.AuthKind == "oauth" { + fmt.Fprintf(deps.Stdout, " Authenticate with: ocx login %s\n", name) + } + if registered && entry.AuthKind == "key" && values["--api-key"] == "" { + env := strings.ToUpper(strings.NewReplacer("-", "_", ".", "_", " ", "_").Replace(name)) + "_API_KEY" + fmt.Fprintf(deps.Stdout, " Set API key with: ocx provider add %s --api-key --force\n Or set env var: %s\n", name, env) + } + if syncModels { + fmt.Fprintln(deps.Stdout, " Models synced to Codex.") + } else { + fmt.Fprintln(deps.Stdout, " Apply to Codex: ocx sync") + } return ExitOK } - +func cloneMap(value map[string]any) map[string]any { + out := make(map[string]any, len(value)) + for key, entry := range value { + out[key] = entry + } + return out +} +func providerHasComboDependency(cfg map[string]any, name string) []string { + combos, _ := cfg["combos"].(map[string]any) + dependent := []string{} + for id, raw := range combos { + combo, _ := raw.(map[string]any) + targets, _ := combo["targets"].([]any) + for _, rawTarget := range targets { + target, _ := rawTarget.(map[string]any) + if target["provider"] == name { + dependent = append(dependent, id) + break + } + } + } + sort.Strings(dependent) + return dependent +} func runProviderRemove(args []string, deps Deps) int { jsonOutput := takeFlag(&args, "--json") - if len(args) != 1 { + if len(args) != 1 || strings.HasPrefix(args[0], "-") { fmt.Fprintln(deps.Stderr, "Usage: ocx provider remove [--json]") return ExitFailure } @@ -825,6 +929,10 @@ func runProviderRemove(args []string, deps Deps) int { fmt.Fprintln(deps.Stderr, "Cannot remove the last provider.") return ExitFailure } + if dependent := providerHasComboDependency(cfg, name); len(dependent) > 0 { + fmt.Fprintf(deps.Stderr, "Cannot remove %q — combo(s) depend on it: %s\n", name, strings.Join(dependent, ", ")) + return ExitFailure + } delete(providers, name) dropped := 0 if models, ok := cfg["customModels"].([]any); ok { @@ -842,15 +950,16 @@ func runProviderRemove(args []string, deps Deps) int { cfg["customModels"] = next } } + if err := validateCLIConfig(cfg); err != nil { + fmt.Fprintln(deps.Stderr, err) + return ExitFailure + } if err := config.SaveRaw(cfg); err != nil { fmt.Fprintln(deps.Stderr, err) return ExitFailure } if jsonOutput { - names := []string{} - for provider := range providers { - names = append(names, provider) - } + names := providerNamesInConfig(providers) out := map[string]any{"action": "removed", "provider": name, "remainingProviders": names, "defaultProvider": cfg["defaultProvider"], "needsSync": true} if dropped > 0 { out["droppedCustomModels"] = dropped @@ -858,12 +967,18 @@ func runProviderRemove(args []string, deps Deps) int { return writeIndentedJSON(deps.Stdout, out) } fmt.Fprintf(deps.Stdout, "✅ Provider %q removed.\n", name) + if dropped > 0 { + plural := "models" + if dropped == 1 { + plural = "model" + } + fmt.Fprintf(deps.Stdout, " Also removed %d custom %s that belonged to it.\n", dropped, plural) + } return ExitOK } - func runProviderSetDefault(args []string, deps Deps) int { jsonOutput := takeFlag(&args, "--json") - if len(args) != 1 { + if len(args) != 1 || strings.HasPrefix(args[0], "-") { fmt.Fprintln(deps.Stderr, "Usage: ocx provider set-default [--json]") return ExitFailure } @@ -886,6 +1001,10 @@ func runProviderSetDefault(args []string, deps Deps) int { return ExitOK } cfg["defaultProvider"] = name + if err := validateCLIConfig(cfg); err != nil { + fmt.Fprintln(deps.Stderr, err) + return ExitFailure + } if err := config.SaveRaw(cfg); err != nil { fmt.Fprintln(deps.Stderr, err) return ExitFailure @@ -903,18 +1022,13 @@ func runCustomModelAdd(args []string, deps Deps) int { fmt.Fprintln(deps.Stderr, modelAddUsage) return ExitFailure } - provider, modelID, flags := strings.TrimSpace(args[0]), strings.TrimSpace(args[1]), append([]string(nil), args[2:]...) + provider, modelID, flags := args[0], args[1], args[2:] if provider == "" || modelID == "" { fmt.Fprintln(deps.Stderr, "Error: provider and modelId are required") return ExitFailure } - if !isValidProviderName(provider) { - fmt.Fprintf(deps.Stderr, "Error: invalid provider name %q\n", provider) - return ExitFailure - } - displayName, contextWindow, modalities, reasoningEfforts, defaultEffort, err := parseCustomModelAddFlags(&flags) - if err != nil { - fmt.Fprintln(deps.Stderr, "Error: "+err.Error()) + if len(flags) != 0 { + fmt.Fprintln(deps.Stderr, "Error: Unknown flag(s): "+strings.Join(flags, ", ")) return ExitFailure } cfg, err := loadCLIConfig() @@ -923,45 +1037,20 @@ func runCustomModelAdd(args []string, deps Deps) int { return ExitFailure } providers, _ := cfg["providers"].(map[string]any) - rawProvider, ok := providers[provider] - if !ok { + if _, ok := providers[provider]; !ok { fmt.Fprintf(deps.Stderr, "Error: provider %q is not configured. See: ocx provider list\n", provider) return ExitFailure } models, _ := cfg["customModels"].([]any) - slug := routedSlug(provider, modelID) + slug := provider + "/" + strings.ReplaceAll(modelID, "/", "-") for _, raw := range models { - if model, ok := raw.(map[string]any); ok && routedSlug(fmt.Sprint(model["provider"]), fmt.Sprint(model["modelId"])) == slug { + if model, ok := raw.(map[string]any); ok && fmt.Sprint(model["provider"])+"/"+strings.ReplaceAll(fmt.Sprint(model["modelId"]), "/", "-") == slug { fmt.Fprintf(deps.Stderr, "Error: custom model %q already exists\n", slug) return ExitFailure } } - providerConfig, _ := rawProvider.(map[string]any) - if encodedModelIDCollides(modelID, knownModelIDs(provider, providerConfig, models)) { - fmt.Fprintf(deps.Stderr, "Error: custom model %q is ambiguous; it encodes to an existing model id\n", slug) - return ExitFailure - } - id, err := customModelUUID() - if err != nil { - fmt.Fprintln(deps.Stderr, err) - return ExitFailure - } + id := fmt.Sprintf("go-%d", time.Now().UnixNano()) entry := map[string]any{"id": id, "provider": provider, "modelId": modelID, "addedAt": time.Now().UTC().Format(time.RFC3339Nano)} - if displayName != "" { - entry["displayName"] = displayName - } - if contextWindow != nil { - entry["contextWindow"] = *contextWindow - } - if modalities != nil { - entry["inputModalities"] = *modalities - } - if reasoningEfforts != nil { - entry["reasoningEfforts"] = *reasoningEfforts - } - if defaultEffort != "" { - entry["defaultReasoningEffort"] = defaultEffort - } cfg["customModels"] = append(models, entry) if err := config.SaveRaw(cfg); err != nil { fmt.Fprintln(deps.Stderr, err) @@ -989,14 +1078,16 @@ func runCustomModelList(args []string, deps Deps) int { fmt.Fprintln(deps.Stdout, "No custom models registered.") return ExitOK } - printCustomModelTable(models, deps.Stdout) + for _, raw := range models { + model := raw.(map[string]any) + fmt.Fprintf(deps.Stdout, "%s: %s\n", model["provider"], model["modelId"]) + } return ExitOK } func runCustomModelRemove(args []string, deps Deps) int { confirmed := takeFlag(&args, "--yes") if len(args) != 1 { fmt.Fprintln(deps.Stderr, "Error: custom model id or provider/modelId is required") - fmt.Fprintln(deps.Stderr, modelRemoveUsage) return ExitFailure } if !confirmed { @@ -1011,28 +1102,13 @@ func runCustomModelRemove(args []string, deps Deps) int { } models, _ := cfg["customModels"].([]any) matched := -1 - selectedProvider := "" - if slash := strings.Index(target, "/"); slash >= 0 { - selectedProvider = target[:slash] - } - admitted := map[string]bool{} - if selectedProvider != "" { - roster := []string{} - for _, raw := range models { - if model, ok := raw.(map[string]any); ok && model["provider"] == selectedProvider { - roster = append(roster, fmt.Sprint(model["modelId"])) - } - } - for _, id := range resolveSlugSelection(selectedProvider, target, roster) { - admitted[id] = true - } - } for i, raw := range models { model, ok := raw.(map[string]any) if !ok { continue } - if fmt.Sprint(model["id"]) == target || (selectedProvider != "" && fmt.Sprint(model["provider"]) == selectedProvider && admitted[fmt.Sprint(model["modelId"])]) { + slug := fmt.Sprint(model["provider"]) + "/" + strings.ReplaceAll(fmt.Sprint(model["modelId"]), "/", "-") + if fmt.Sprint(model["id"]) == target || slug == target { if matched >= 0 { fmt.Fprintf(deps.Stderr, "Error: custom model selector %q is ambiguous; use the custom model id\n", target) return ExitFailure @@ -1056,297 +1132,9 @@ func runCustomModelRemove(args []string, deps Deps) int { fmt.Fprintln(deps.Stderr, err) return ExitFailure } - fmt.Fprintf(deps.Stdout, "Removed custom model %s.\n", routedSlug(fmt.Sprint(model["provider"]), fmt.Sprint(model["modelId"]))) + fmt.Fprintf(deps.Stdout, "Removed custom model %s.\n", fmt.Sprint(model["provider"])+"/"+strings.ReplaceAll(fmt.Sprint(model["modelId"]), "/", "-")) return ExitOK } - -func parseCustomModelAddFlags(args *[]string) (string, *int, *[]any, *[]any, string, error) { - var displayName, defaultEffort string - var contextWindow *int - var modalities, efforts *[]any - take := func(flag string) (string, bool) { - for i := 0; i < len(*args); i++ { - if (*args)[i] == flag { - if i+1 == len(*args) { - return "", false - } - value := (*args)[i+1] - *args = append((*args)[:i], (*args)[i+2:]...) - return value, true - } - } - return "", false - } - if value, found := take("--display-name"); found { - displayName = strings.TrimSpace(value) - if strings.Contains(displayName, "/") { - return "", nil, nil, nil, "", errors.New("displayName must not contain /") - } - } - if value, found := take("--context-window"); found { - parsed, err := strconv.Atoi(value) - if err != nil || parsed <= 0 { - return "", nil, nil, nil, "", errors.New("context window must be a positive integer") - } - contextWindow = &parsed - } - if value, found := take("--modalities"); found { - values := strings.Split(value, ",") - seen := map[string]bool{} - out := []any{} - for _, item := range values { - item = strings.TrimSpace(item) - if item != "text" && item != "image" && item != "audio" { - return "", nil, nil, nil, "", errors.New("modalities must be comma-separated values from text|image|audio") - } - if !seen[item] { - seen[item] = true - out = append(out, item) - } - } - modalities = &out - } - if value, found := take("--reasoning-efforts"); found { - if strings.TrimSpace(value) != "-" { - parsed, err := parseReasoningEfforts(value) - if err != nil { - return "", nil, nil, nil, "", err - } - efforts = &parsed - } - } - if value, found := take("--default-reasoning-effort"); found { - defaultEffort = strings.TrimSpace(value) - if defaultEffort == "-" { - defaultEffort = "" - } else { - if !isDeclaredReasoningEffort(defaultEffort) { - return "", nil, nil, nil, "", fmt.Errorf("unsupported reasoning effort: %s (allowed: none, minimal, low, medium, high, xhigh, max, ultra)", defaultEffort) - } - if efforts == nil || len(*efforts) == 0 { - return "", nil, nil, nil, "", errors.New("--default-reasoning-effort requires --reasoning-efforts") - } - found := false - for _, effort := range *efforts { - if effort == defaultEffort { - found = true - } - } - if !found { - return "", nil, nil, nil, "", fmt.Errorf("--default-reasoning-effort %q is not in the declared reasoning efforts", defaultEffort) - } - } - } - if len(*args) > 0 { - return "", nil, nil, nil, "", fmt.Errorf("Unknown flag(s): %s", strings.Join(*args, ", ")) - } - return displayName, contextWindow, modalities, efforts, defaultEffort, nil -} - -func parseReasoningEfforts(raw string) ([]any, error) { - trimmed := strings.TrimSpace(raw) - if trimmed == "-" { - return nil, nil - } - if trimmed == "" { - return []any{}, nil - } - values := strings.Split(trimmed, ",") - seen := map[string]bool{} - for _, value := range values { - value = strings.TrimSpace(value) - if !isDeclaredReasoningEffort(value) { - return nil, fmt.Errorf("unsupported reasoning effort: %s (allowed: none, minimal, low, medium, high, xhigh, max, ultra)", value) - } - seen[value] = true - } - order := []string{"none", "minimal", "low", "medium", "high", "xhigh", "max", "ultra"} - out := []any{} - for _, value := range order { - if seen[value] { - out = append(out, value) - } - } - return out, nil -} -func isDeclaredReasoningEffort(value string) bool { - for _, allowed := range []string{"none", "minimal", "low", "medium", "high", "xhigh", "max", "ultra"} { - if value == allowed { - return true - } - } - return false -} - -func isValidProviderName(value string) bool { - if value == "" || len(value) > 64 || value != strings.TrimSpace(value) { - return false - } - reserved := map[string]bool{"__proto__": true, "prototype": true, "constructor": true} - if reserved[strings.ToLower(value)] { - return false - } - for i, char := range value { - alnum := char >= 'a' && char <= 'z' || char >= 'A' && char <= 'Z' || char >= '0' && char <= '9' - if i == 0 || i == len(value)-1 { - if !alnum { - return false - } - continue - } - if !alnum && char != '.' && char != '_' && char != '-' { - return false - } - } - return true -} -func routedSlug(provider, model string) string { - return provider + "/" + strings.ReplaceAll(model, "/", "-") -} -func encodedModelIDCollides(model string, known []string) bool { - encoded := strings.ReplaceAll(model, "/", "-") - for _, id := range known { - if id != model && strings.ReplaceAll(id, "/", "-") == encoded { - return true - } - } - return false -} -func knownModelIDs(provider string, config map[string]any, custom []any) []string { - seen := map[string]bool{} - out := []string{} - add := func(id string) { - if id != "" && !seen[id] { - seen[id] = true - out = append(out, id) - } - } - if id, _ := config["defaultModel"].(string); id != "" { - add(id) - } - if models, ok := config["models"].([]any); ok { - for _, raw := range models { - if id, ok := raw.(string); ok { - add(id) - } - } - } - for _, raw := range custom { - if model, ok := raw.(map[string]any); ok && model["provider"] == provider { - add(fmt.Sprint(model["modelId"])) - } - } - return out -} -func resolveSlugSelection(provider, selection string, ids []string) []string { - namesNative := false - for _, id := range ids { - if id == selection { - namesNative = true - } - } - qualified := routedSlug(provider, selection) - if !namesNative && strings.HasPrefix(selection, provider+"/") { - qualified = selection - } - key := slugKey(qualified) - out := []string{} - for _, id := range ids { - if slugKey(routedSlug(provider, id)) == key { - out = append(out, id) - } - } - return out -} -func slugKey(slug string) string { - slash := strings.Index(slug, "/") - if slash <= 0 { - return "exact:" + slug - } - return "routed:" + slug[:slash] + ":" + strings.ReplaceAll(slug[slash+1:], "/", "-") -} -func customModelUUID() (string, error) { - raw := make([]byte, 16) - if _, err := rand.Read(raw); err != nil { - return "", err - } - raw[6] = raw[6]&0x0f | 0x40 - raw[8] = raw[8]&0x3f | 0x80 - return fmt.Sprintf("%08x-%04x-%04x-%04x-%012x", raw[:4], raw[4:6], raw[6:8], raw[8:10], raw[10:]), nil -} -func printCustomModelTable(models []any, writer io.Writer) { - groups := map[string][]map[string]any{} - names := []string{} - for _, raw := range models { - model, ok := raw.(map[string]any) - if !ok { - continue - } - provider := fmt.Sprint(model["provider"]) - if _, ok := groups[provider]; !ok { - names = append(names, provider) - } - groups[provider] = append(groups[provider], model) - } - for _, provider := range names { - headers := []string{"ID", "MODEL", "DISPLAY NAME", "CONTEXT", "MODALITIES", "EFFORTS", "DEFAULT EFFORT"} - rows := make([][]string, 0, len(groups[provider])) - widths := make([]int, len(headers)) - copy(widths, []int{2, 5, 12, 7, 10, 7, 14}) - for _, model := range groups[provider] { - id := fmt.Sprint(model["id"]) - if len(id) > 8 { - id = id[:8] - } - row := []string{id, fmt.Sprint(model["modelId"]), dash(model["displayName"]), customContext(model["contextWindow"]), customCSV(model["inputModalities"]), customCSV(model["reasoningEfforts"]), dash(model["defaultReasoningEffort"])} - rows = append(rows, row) - for i, cell := range row { - if len(cell) > widths[i] { - widths[i] = len(cell) - } - } - } - line := func(row []string) string { - cells := make([]string, len(row)) - for i, cell := range row { - cells[i] = fmt.Sprintf("%-*s", widths[i], cell) - } - return strings.Join(cells, " ") - } - fmt.Fprintf(writer, "%s:\n %s\n", provider, line(headers)) - for _, row := range rows { - fmt.Fprintf(writer, " %s\n", line(row)) - } - fmt.Fprintln(writer) - } -} -func dash(value any) string { - if value == nil || fmt.Sprint(value) == "" { - return "-" - } - return fmt.Sprint(value) -} -func customContext(value any) string { - if value == nil { - return "-" - } - number, err := strconv.ParseFloat(fmt.Sprint(value), 64) - if err != nil || number <= 0 { - return "-" - } - return fmt.Sprintf("%dk", int(math.Round(number/1000))) -} -func customCSV(value any) string { - values, ok := value.([]any) - if !ok || len(values) == 0 { - return "-" - } - parts := make([]string, 0, len(values)) - for _, raw := range values { - parts = append(parts, fmt.Sprint(raw)) - } - return strings.Join(parts, ",") -} func writeIndentedJSON(writer io.Writer, value any) int { raw, err := json.MarshalIndent(value, "", " ") if err != nil { diff --git a/go/internal/ocxcli/provider_registry.go b/go/internal/ocxcli/provider_registry.go new file mode 100644 index 0000000000..c34ad28723 --- /dev/null +++ b/go/internal/ocxcli/provider_registry.go @@ -0,0 +1,33 @@ +package ocxcli + +import ( + _ "embed" + "encoding/json" +) + +// providerRegistryJSON is generated from src/providers/registry.ts through +// providerConfigSeed. It keeps Go-owned provider add/list behavior on the same +// presets as the TypeScript CLI without duplicating a lossy hand-maintained list. +// +//go:embed provider_registry.json +var providerRegistryJSON []byte + +type providerRegistryEntry struct { + ID string `json:"id"` + Label string `json:"label"` + AuthKind string `json:"authKind"` + Seed map[string]any `json:"seed"` +} + +var providerRegistry []providerRegistryEntry +var providerRegistryByID map[string]providerRegistryEntry + +func init() { + if err := json.Unmarshal(providerRegistryJSON, &providerRegistry); err != nil { + panic("invalid embedded provider registry: " + err.Error()) + } + providerRegistryByID = make(map[string]providerRegistryEntry, len(providerRegistry)) + for _, entry := range providerRegistry { + providerRegistryByID[entry.ID] = entry + } +} diff --git a/go/internal/ocxcli/provider_registry.json b/go/internal/ocxcli/provider_registry.json new file mode 100644 index 0000000000..ae161f1c47 --- /dev/null +++ b/go/internal/ocxcli/provider_registry.json @@ -0,0 +1 @@ +[{"id":"openai","label":"OpenAI (Codex login)","authKind":"forward","seed":{"adapter":"openai-responses","baseUrl":"https://chatgpt.com/backend-api/codex","authMode":"forward","codexAccountMode":"pool"}},{"id":"cursor","label":"Cursor (experimental)","authKind":"oauth","seed":{"adapter":"cursor","baseUrl":"https://api2.cursor.sh","authMode":"oauth","defaultModel":"auto","models":["auto","auto-balance","auto-cost","auto-intelligence","claude-4-sonnet","claude-4-sonnet-1m","claude-4.5-haiku","claude-4.5-opus","claude-4.5-sonnet","claude-4.6-opus","claude-4.6-sonnet","claude-fable-5","claude-fable-5-1","claude-opus-4-7","claude-opus-4-8","claude-opus-5","claude-sonnet-5","composer-1","composer-2.5","composer-2.5-fast","gemini-2.5-flash","gemini-3-flash","gemini-3-pro","gemini-3-pro-image-preview","gemini-3.1-pro","gemini-3.5-flash","gemini-3.6-flash","gemini-3.7-flash","gemini-3.8-flash","glm-5.2","glm-5.3","gpt-5-codex","gpt-5-fast","gpt-5-mini","gpt-5.1","gpt-5.1-codex","gpt-5.1-codex-max","gpt-5.1-codex-mini","gpt-5.2","gpt-5.2-codex","gpt-5.3-codex","gpt-5.4","gpt-5.4-mini","gpt-5.4-nano","gpt-5.5","gpt-5.5-extra","gpt-5.6-luna","gpt-5.6-sol","gpt-5.6-terra","grok-4.5","grok-4.6","kimi-k2.7-code","kimi-k3"],"liveModels":true,"modelContextWindows":{"auto":200000,"auto-balance":200000,"auto-cost":200000,"auto-intelligence":200000,"claude-4-sonnet":200000,"claude-4-sonnet-1m":1000000,"claude-4.5-haiku":200000,"claude-4.5-opus":200000,"claude-4.5-sonnet":200000,"claude-4.6-opus":1000000,"claude-4.6-sonnet":1000000,"claude-fable-5":1000000,"claude-fable-5-1":1000000,"claude-opus-4-7":1000000,"claude-opus-4-8":1000000,"claude-opus-5":1000000,"claude-sonnet-5":1000000,"composer-1":200000,"composer-2.5":200000,"composer-2.5-fast":200000,"gemini-2.5-flash":1048576,"gemini-3-flash":1048576,"gemini-3-pro":1048576,"gemini-3-pro-image-preview":200000,"gemini-3.1-pro":1048576,"gemini-3.5-flash":200000,"gemini-3.6-flash":1048576,"gemini-3.7-flash":1048576,"gemini-3.8-flash":1048576,"glm-5.2":1000000,"glm-5.3":1000000,"gpt-5-codex":272000,"gpt-5-fast":272000,"gpt-5-mini":272000,"gpt-5.1":272000,"gpt-5.1-codex":272000,"gpt-5.1-codex-max":272000,"gpt-5.1-codex-mini":272000,"gpt-5.2":272000,"gpt-5.2-codex":272000,"gpt-5.3-codex":272000,"gpt-5.4":272000,"gpt-5.4-mini":272000,"gpt-5.4-nano":272000,"gpt-5.5":272000,"gpt-5.5-extra":200000,"gpt-5.6-luna":1000000,"gpt-5.6-sol":1000000,"gpt-5.6-terra":1000000,"grok-4.5":500000,"grok-4.6":500000,"kimi-k2.7-code":262144,"kimi-k3":1000000},"modelDisplayNames":{"grok-4.5":"Cursor Grok 4.5","grok-4.6":"Cursor Grok 4.6"},"modelInputModalities":{"auto":["text","image"],"auto-balance":["text","image"],"auto-cost":["text","image"],"auto-intelligence":["text","image"],"claude-4-sonnet":["text","image"],"claude-4-sonnet-1m":["text","image"],"claude-4.5-haiku":["text","image"],"claude-4.5-opus":["text","image"],"claude-4.5-sonnet":["text","image"],"claude-4.6-opus":["text","image"],"claude-4.6-sonnet":["text","image"],"claude-fable-5":["text","image"],"claude-fable-5-1":["text","image"],"claude-opus-4-7":["text","image"],"claude-opus-4-8":["text","image"],"claude-opus-5":["text","image"],"claude-sonnet-5":["text","image"],"composer-1":["text","image"],"composer-2.5":["text","image"],"composer-2.5-fast":["text","image"],"gemini-2.5-flash":["text","image"],"gemini-3-flash":["text","image"],"gemini-3-pro":["text","image"],"gemini-3-pro-image-preview":["text","image"],"gemini-3.1-pro":["text","image"],"gemini-3.5-flash":["text","image"],"gemini-3.6-flash":["text","image"],"gemini-3.7-flash":["text","image"],"gemini-3.8-flash":["text","image"],"glm-5.2":["text","image"],"glm-5.3":["text","image"],"gpt-5-codex":["text","image"],"gpt-5-fast":["text","image"],"gpt-5-mini":["text","image"],"gpt-5.1":["text","image"],"gpt-5.1-codex":["text","image"],"gpt-5.1-codex-max":["text","image"],"gpt-5.1-codex-mini":["text","image"],"gpt-5.2":["text","image"],"gpt-5.2-codex":["text","image"],"gpt-5.3-codex":["text","image"],"gpt-5.4":["text","image"],"gpt-5.4-mini":["text","image"],"gpt-5.4-nano":["text","image"],"gpt-5.5":["text","image"],"gpt-5.5-extra":["text","image"],"gpt-5.6-luna":["text","image"],"gpt-5.6-sol":["text","image"],"gpt-5.6-terra":["text","image"],"grok-4.5":["text","image"],"grok-4.6":["text","image"],"kimi-k2.7-code":["text","image"],"kimi-k3":["text","image"]},"modelReasoningEfforts":{"auto":[],"auto-balance":[],"auto-cost":[],"auto-intelligence":[],"claude-4-sonnet":[],"claude-4-sonnet-1m":[],"claude-4.5-haiku":[],"claude-4.5-opus":["high"],"claude-4.5-sonnet":[],"claude-4.6-opus":["high","max"],"claude-4.6-sonnet":["medium"],"claude-fable-5":["low","medium","high","xhigh","max"],"claude-fable-5-1":["low","medium","high","xhigh","max"],"claude-opus-4-7":["low","medium","high","xhigh","max"],"claude-opus-4-8":["low","medium","high","xhigh","max"],"claude-opus-5":["low","medium","high","xhigh","max"],"claude-sonnet-5":["low","medium","high","xhigh","max"],"composer-1":[],"composer-2.5":[],"composer-2.5-fast":[],"gemini-2.5-flash":[],"gemini-3-flash":[],"gemini-3-pro":[],"gemini-3-pro-image-preview":[],"gemini-3.1-pro":[],"gemini-3.5-flash":[],"gemini-3.6-flash":["minimal","low","medium","high"],"gemini-3.7-flash":["low","medium","high"],"gemini-3.8-flash":["low","medium","high"],"glm-5.2":["high","max"],"glm-5.3":["low","high","max"],"gpt-5-codex":[],"gpt-5-fast":[],"gpt-5-mini":[],"gpt-5.1":["low","high"],"gpt-5.1-codex":[],"gpt-5.1-codex-max":["low","medium","high","xhigh"],"gpt-5.1-codex-mini":["low","high"],"gpt-5.2":["low","high","xhigh"],"gpt-5.2-codex":["low","high","xhigh"],"gpt-5.3-codex":["low","high","xhigh"],"gpt-5.4":["low","medium","high","xhigh"],"gpt-5.4-mini":["low","medium","high","xhigh"],"gpt-5.4-nano":["low","medium","high","xhigh"],"gpt-5.5":["low","medium","high"],"gpt-5.5-extra":["high"],"gpt-5.6-luna":["low","medium","high","xhigh","max"],"gpt-5.6-sol":["low","medium","high","xhigh","max"],"gpt-5.6-terra":["low","medium","high","xhigh","max"],"grok-4.5":["low","medium","high"],"grok-4.6":["low","medium","high","xhigh"],"kimi-k2.7-code":[],"kimi-k3":["low","high","max"]},"modelDefaultReasoningEfforts":{"kimi-k3":"max"},"noVisionModels":["auto","auto-cost","auto-balance","auto-intelligence","composer-1","composer-2.5","composer-2.5-fast","glm-5.2","glm-5.3"]}},{"id":"xai","label":"xAI Grok","authKind":"oauth","seed":{"adapter":"openai-chat","baseUrl":"https://api.x.ai/v1","authMode":"oauth","defaultModel":"grok-4.5","models":["grok-4.6","grok-4.5","grok-4.3","grok-4.20-multi-agent-0309","grok-4.20-0309-reasoning","grok-4.20-0309-non-reasoning","grok-build-0.1","grok-composer-2.5-fast"],"liveModels":true,"modelContextWindows":{"grok-4.6":500000,"grok-4.5":500000,"grok-4.3":1000000,"grok-4.20-multi-agent-0309":1000000,"grok-4.20-0309-reasoning":1000000,"grok-4.20-0309-non-reasoning":1000000,"grok-build-0.1":256000},"modelInputModalities":{"grok-4.6":["text","image"],"grok-4.5":["text","image"],"grok-4.3":["text","image"],"grok-4.20-multi-agent-0309":["text","image"],"grok-4.20-0309-reasoning":["text","image"],"grok-4.20-0309-non-reasoning":["text","image"]},"modelReasoningEfforts":{"grok-4.6":["low","medium","high","xhigh"],"grok-4.5":["low","medium","high"],"grok-4.20-multi-agent-0309":["low","medium","high","xhigh"]},"modelDefaultReasoningEfforts":{"grok-4.6":"high"},"noVisionModels":["grok-build-0.1","grok-composer-2.5-fast"],"noReasoningModels":["grok-4.20-0309-non-reasoning","grok-build-0.1","grok-composer-2.5-fast"],"parallelToolCalls":true,"preserveReasoningContentModels":["grok-4.6","grok-4.5","grok-4.3","grok-4.20-0309-reasoning"]}},{"id":"command-code","label":"Command Code - Auth","authKind":"oauth","seed":{"adapter":"command-code","baseUrl":"https://api.commandcode.ai","authMode":"oauth","defaultModel":"deepseek/deepseek-v4-flash","liveModels":true,"modelContextWindows":{"deepseek/deepseek-v4-flash-vision-exp":1048576},"modelInputModalities":{"deepseek/deepseek-v4-flash-vision-exp":["text","image"],"gpt-5.6-luna":["text","image"],"gpt-5.6-sol":["text","image"],"MiniMaxAI/MiniMax-M3":["text","image"],"moonshotai/Kimi-K3":["text","image"],"meta/muse-spark-1.3":["text","image"],"meta/muse-spark-1.3-contributor":["text","image"],"meta/muse-spark-1.2":["text","image"],"meta/muse-spark-1.2-contributor":["text","image"]},"defaultMaxOutputTokens":64000,"reasoningEfforts":[],"modelReasoningEfforts":{"deepseek/deepseek-v4-pro":["high","max"],"deepseek/deepseek-v4-flash":["high","max"],"deepseek/deepseek-v4-flash-vision-exp":["high","max"],"gpt-5.6-luna":["low","medium","high","xhigh","max"],"google/gemini-3.7-flash":["low","medium","high"],"zai-org/GLM-5":["high","max"],"zai-org/GLM-5.1":["high","max"],"zai-org/GLM-5.2":["high","max"],"zai-org/GLM-5.2-Fast":["high","max"],"zai-org/GLM-5.3":["low","high","max"],"z-ai/glm-5.3-flash":["low","high","max"],"meta/muse-spark-1.3":["low","medium","high","xhigh","max"],"meta/muse-spark-1.3-contributor":["low","medium","high","xhigh","max"],"meta/muse-spark-1.2":["low","medium","high","xhigh","max"],"meta/muse-spark-1.2-contributor":["low","medium","high","xhigh","max"],"meta/muse-spark-1.1":["low","medium","high","xhigh","max"]},"parallelToolCalls":false}},{"id":"anthropic","label":"Anthropic Claude","authKind":"oauth","seed":{"adapter":"anthropic","baseUrl":"https://api.anthropic.com","authMode":"oauth","defaultModel":"claude-sonnet-5","models":["claude-fable-5-1","claude-fable-5","claude-sonnet-5","claude-opus-5","claude-opus-4-8","claude-opus-4-7","claude-opus-4-6","claude-sonnet-4-6","claude-haiku-4-5"],"modelContextWindows":{"claude-fable-5-1":1000000,"claude-sonnet-5":1000000,"claude-fable-5":1000000,"claude-opus-5":1000000,"claude-opus-4-8":1000000,"claude-opus-4-7":1000000,"claude-opus-4-6":1000000,"claude-sonnet-4-6":1000000,"claude-haiku-4-5":200000}}},{"id":"anthropic-apikey","label":"Anthropic (API key)","authKind":"key","seed":{"adapter":"anthropic","baseUrl":"https://api.anthropic.com","authMode":"key","defaultModel":"claude-sonnet-5","models":["claude-fable-5-1","claude-fable-5","claude-sonnet-5","claude-opus-5","claude-opus-4-8","claude-opus-4-7","claude-opus-4-6","claude-sonnet-4-6","claude-haiku-4-5"],"liveModels":true,"modelContextWindows":{"claude-fable-5-1":1000000,"claude-sonnet-5":1000000,"claude-fable-5":1000000,"claude-opus-5":1000000,"claude-opus-4-8":1000000,"claude-opus-4-7":1000000,"claude-opus-4-6":1000000,"claude-sonnet-4-6":1000000,"claude-haiku-4-5":200000}}},{"id":"kimi","label":"Kimi","authKind":"oauth","seed":{"adapter":"openai-chat","baseUrl":"https://api.kimi.com/coding/v1","authMode":"oauth","modelSuffixBracketStrip":true,"defaultModel":"kimi-k2.7-code","models":["k3","k3[1m]","kimi-k2.7-code","kimi-k2.7-code-highspeed","kimi-k2.6","kimi-k2.5","kimi-for-coding"],"modelContextWindows":{"k3":262144,"k3[1m]":1048576,"kimi-k2.7-code":262144,"kimi-k2.7-code-highspeed":262144,"kimi-k2.6":262144,"kimi-k2.5":262144,"kimi-for-coding":262144},"modelInputModalities":{"k3":["text","image"],"k3[1m]":["text","image"]},"modelReasoningEfforts":{"k3":["low","high","max"],"k3[1m]":["low","high","max"],"kimi-k2.7-code":[],"kimi-k2.7-code-highspeed":[],"kimi-k2.6":[],"kimi-k2.5":[],"kimi-for-coding":[]},"modelDefaultReasoningEfforts":{"k3":"max","k3[1m]":"max"},"modelReasoningEffortMap":{"k3":{"none":"none","low":"low","medium":"high","high":"high","xhigh":"max","max":"max"},"k3[1m]":{"none":"none","low":"low","medium":"high","high":"high","xhigh":"max","max":"max"}},"noReasoningModels":["kimi-k2.7-code","kimi-k2.7-code-highspeed","kimi-k2.6","kimi-k2.5","kimi-for-coding"],"noTemperatureModels":["k3","k3[1m]","kimi-k2.7-code","kimi-k2.7-code-highspeed","kimi-k2.6","kimi-k2.5","kimi-for-coding"],"noTopPModels":["k3","k3[1m]","kimi-k2.7-code","kimi-k2.7-code-highspeed","kimi-k2.6","kimi-k2.5","kimi-for-coding"],"noPenaltyModels":["k3","k3[1m]","kimi-k2.7-code","kimi-k2.7-code-highspeed","kimi-k2.6","kimi-k2.5","kimi-for-coding"],"promptCacheKey":true,"autoToolChoiceOnlyModels":["kimi-k2.7-code","kimi-k2.7-code-highspeed","kimi-for-coding"],"preserveReasoningContentModels":["k3","k3[1m]","kimi-k2.7-code","kimi-k2.7-code-highspeed","kimi-k2.6","kimi-k2.5","kimi-for-coding"]}},{"id":"kiro","label":"Kiro (AWS CodeWhisperer)","authKind":"oauth","seed":{"adapter":"kiro","baseUrl":"https://runtime.us-east-1.kiro.dev","authMode":"oauth","defaultModel":"kiro-auto","models":["kiro-auto","gpt-5.6-sol","gpt-5.6-terra","gpt-5.6-luna","claude-sonnet-5","claude-opus-5","claude-opus-4.8","claude-opus-4.7","claude-opus-4.6","claude-opus-4.5","claude-sonnet-4.6","claude-sonnet-4.5","claude-sonnet-4.0","claude-haiku-4.5","deepseek-3.2","minimax-m2.5","minimax-m2.1","glm-5","qwen3-coder-next"],"liveModels":false,"modelContextWindows":{"gpt-5.6-sol":272000,"gpt-5.6-terra":272000,"gpt-5.6-luna":272000,"claude-sonnet-5":1000000,"claude-opus-5":1000000,"claude-opus-4.8":1000000,"claude-opus-4.7":1000000,"claude-opus-4.6":1000000,"claude-opus-4.5":200000,"claude-sonnet-4.6":1000000,"claude-sonnet-4.5":200000,"claude-sonnet-4.0":200000,"claude-haiku-4.5":200000,"deepseek-3.2":128000,"minimax-m2.5":200000,"minimax-m2.1":200000,"glm-5":200000,"qwen3-coder-next":256000},"modelReasoningEfforts":{"kiro-auto":["low","medium","high","xhigh","max"],"gpt-5.6-sol":["low","medium","high","xhigh","max"],"gpt-5.6-terra":["low","medium","high","xhigh","max"],"gpt-5.6-luna":["low","medium","high","xhigh","max"],"claude-sonnet-5":["low","medium","high","xhigh","max"],"claude-opus-5":["low","medium","high","xhigh","max"],"claude-opus-4.8":["low","medium","high","xhigh","max"],"claude-opus-4.7":["low","medium","high","xhigh","max"],"claude-opus-4.6":["low","medium","high","xhigh","max"],"claude-opus-4.5":["low","medium","high","xhigh","max"],"claude-sonnet-4.6":["low","medium","high","xhigh","max"],"claude-sonnet-4.5":["low","medium","high","xhigh","max"],"claude-sonnet-4.0":["low","medium","high","xhigh","max"],"claude-haiku-4.5":["low","medium","high","xhigh","max"],"deepseek-3.2":["low","medium","high","xhigh","max"],"minimax-m2.5":["low","medium","high","xhigh","max"],"minimax-m2.1":["low","medium","high","xhigh","max"],"glm-5":["low","medium","high","xhigh","max"],"qwen3-coder-next":["low","medium","high","xhigh","max"]}}},{"id":"nous","label":"Nous Portal","authKind":"oauth","seed":{"adapter":"openai-chat","baseUrl":"https://inference-api.nousresearch.com/v1","authMode":"oauth","freeTier":false,"defaultModel":"tencent/hy3:free","models":["tencent/hy3:free","poolside/laguna-s-2.1:free","stepfun/step-3.7-flash:free","poolside/laguna-xs-2.1:free"],"liveModels":true}},{"id":"openai-apikey","label":"OpenAI API","authKind":"key","seed":{"adapter":"openai-responses","baseUrl":"https://api.openai.com/v1","authMode":"key","defaultModel":"gpt-5.5","models":["gpt-5.5","gpt-5.6","gpt-5.6-sol","gpt-5.6-terra","gpt-5.6-luna","gpt-5.6-sol-pro","gpt-5.6-terra-pro","gpt-5.6-luna-pro","daybreak-red-latest","daybreak-blue-latest"],"liveModels":true,"modelContextWindows":{"gpt-5.6":1050000,"gpt-5.6-sol":1050000,"gpt-5.6-terra":1050000,"gpt-5.6-luna":1050000,"gpt-5.6-sol-pro":1050000,"gpt-5.6-terra-pro":1050000,"gpt-5.6-luna-pro":1050000,"gpt-5.5":1050000,"daybreak-red-latest":400000,"daybreak-blue-latest":1050000},"modelInputModalities":{"gpt-5.5":["text","image"],"gpt-5.6":["text","image"],"gpt-5.6-sol":["text","image"],"gpt-5.6-terra":["text","image"],"gpt-5.6-luna":["text","image"],"gpt-5.6-sol-pro":["text","image"],"gpt-5.6-terra-pro":["text","image"],"gpt-5.6-luna-pro":["text","image"],"daybreak-red-latest":["text","image"],"daybreak-blue-latest":["text","image"]},"modelMaxInputTokens":{"gpt-5.6":922000,"gpt-5.6-sol":922000,"gpt-5.6-terra":922000,"gpt-5.6-luna":922000,"gpt-5.6-sol-pro":922000,"gpt-5.6-terra-pro":922000,"gpt-5.6-luna-pro":922000,"gpt-5.5":922000,"daybreak-red-latest":272000,"daybreak-blue-latest":922000},"modelReasoningEfforts":{"gpt-5.6":["low","medium","high","xhigh","max"],"gpt-5.6-sol":["low","medium","high","xhigh","max"],"gpt-5.6-terra":["low","medium","high","xhigh","max"],"gpt-5.6-luna":["low","medium","high","xhigh","max"],"gpt-5.6-sol-pro":["low","medium","high","xhigh","max"],"gpt-5.6-terra-pro":["low","medium","high","xhigh","max"],"gpt-5.6-luna-pro":["low","medium","high","xhigh","max"],"daybreak-red-latest":[],"daybreak-blue-latest":[]}}},{"id":"meta-model","label":"Meta Model API","authKind":"key","seed":{"adapter":"openai-responses","baseUrl":"https://api.meta.ai/v1","authMode":"key","defaultModel":"muse-spark-1.3","models":["muse-spark-1.3","muse-spark-1.3-contributor"],"liveModels":false,"modelContextWindows":{"muse-spark-1.3":1048576,"muse-spark-1.3-contributor":1048576},"modelInputModalities":{"muse-spark-1.3":["text","image"],"muse-spark-1.3-contributor":["text","image"]},"modelReasoningEfforts":{"muse-spark-1.3":["minimal","low","medium","high","xhigh"],"muse-spark-1.3-contributor":["minimal","low","medium","high","xhigh"]},"modelReasoningEffortMap":{"muse-spark-1.3":{"minimal":"minimal","low":"low","medium":"medium","high":"high","xhigh":"xhigh"},"muse-spark-1.3-contributor":{"minimal":"minimal","low":"low","medium":"medium","high":"high","xhigh":"xhigh"}}}},{"id":"meta-muse","label":"Meta Muse Code (CLI credential)","authKind":"oauth","seed":{"adapter":"openai-responses","baseUrl":"https://api.meta.ai/v1","authMode":"oauth","defaultModel":"muse-spark-1.3","models":["muse-spark-1.3","muse-spark-1.3-contributor"],"liveModels":false,"modelContextWindows":{"muse-spark-1.3":1048576,"muse-spark-1.3-contributor":1048576},"modelInputModalities":{"muse-spark-1.3":["text","image"],"muse-spark-1.3-contributor":["text","image"]},"modelReasoningEfforts":{"muse-spark-1.3":["minimal","low","medium","high","xhigh"],"muse-spark-1.3-contributor":["minimal","low","medium","high","xhigh"]},"modelReasoningEffortMap":{"muse-spark-1.3":{"minimal":"minimal","low":"low","medium":"medium","high":"high","xhigh":"xhigh"},"muse-spark-1.3-contributor":{"minimal":"minimal","low":"low","medium":"medium","high":"high","xhigh":"xhigh"}}}},{"id":"umans","label":"Umans AI Coding Plan","authKind":"key","seed":{"adapter":"anthropic","baseUrl":"https://api.code.umans.ai","authMode":"key","defaultModel":"umans-coder","models":["umans-coder","umans-kimi-k2.7","umans-flash","umans-glm-5.3","umans-glm-5.3-flash","umans-glm-5.2","umans-glm-5.1","umans-qwen3.6-35b-a3b"],"modelContextWindows":{"umans-coder":262144,"umans-kimi-k2.7":262144,"umans-flash":262144,"umans-glm-5.3":405504,"umans-glm-5.3-flash":405504,"umans-glm-5.2":405504,"umans-glm-5.1":202752,"umans-qwen3.6-35b-a3b":262144},"modelInputModalities":{"umans-coder":["text","image"],"umans-kimi-k2.7":["text","image"],"umans-flash":["text","image"],"umans-glm-5.3":["text"],"umans-glm-5.3-flash":["text","image"],"umans-glm-5.2":["text"],"umans-glm-5.1":["text"],"umans-qwen3.6-35b-a3b":["text","image"]},"modelReasoningEfforts":{"umans-coder":["low","medium","high","xhigh","max"],"umans-kimi-k2.7":["low","medium","high","xhigh","max"],"umans-flash":["low","medium","high","xhigh","max"],"umans-glm-5.3":["low","high","max"],"umans-glm-5.3-flash":["low","high","max"],"umans-glm-5.2":["high","xhigh","max"],"umans-glm-5.1":["high","xhigh","max"],"umans-qwen3.6-35b-a3b":["low","medium","high","xhigh","max"]},"noVisionModels":["umans-glm-5.3","umans-glm-5.2","umans-glm-5.1"],"escapeBuiltinToolNames":true}},{"id":"opencode-go","label":"opencode go","authKind":"key","seed":{"adapter":"openai-chat","baseUrl":"https://opencode.ai/zen/go/v1","authMode":"key","defaultModel":"kimi-k2.7-code","modelContextWindows":{"kimi-k3":262144,"deepseek-v4-flash-vision-exp":1048576,"muse-spark-1.3-contributor":1048576,"muse-spark-1.2-contributor":1048576},"modelInputModalities":{"kimi-k3":["text","image"],"deepseek-v4-flash-vision-exp":["text","image"],"muse-spark-1.3-contributor":["text","image"],"muse-spark-1.2-contributor":["text","image"]},"modelReasoningEfforts":{"gpt-5.6-luna":["low","medium","high","xhigh","max"],"glm-5.3":["low","high","max"],"glm-5.3-flash":["low","high","max"],"glm-5.2":["low","medium","high","xhigh","max"],"qwen3.8-max":["low","medium","xhigh"],"kimi-k3":["low","high","max"],"kimi-k2.7-code":[],"kimi-k2.7-code-highspeed":[],"mimo-v2.5":["low","medium","high","xhigh","max"],"mimo-v2.5-pro":["low","medium","high","xhigh","max"],"glm-5":["low","medium","high","xhigh","max"],"glm-5.1":["low","medium","high","xhigh","max"],"qwen3.5-plus":["low","medium","high","xhigh","max"],"qwen3.6-plus":["low","medium","high","xhigh","max"],"qwen3.7-max":["low","medium","high","xhigh","max"],"qwen3.7-plus":["low","medium","high","xhigh","max"],"deepseek-v4-pro":["low","high","max"],"deepseek-v4-flash":["low","high","max"]},"modelDefaultReasoningEfforts":{"kimi-k3":"max"},"modelReasoningEffortMap":{"kimi-k3":{"none":"none","low":"low","medium":"high","high":"high","xhigh":"max","max":"max"},"mimo-v2.5":{"none":"disabled","minimal":"disabled","low":"disabled","medium":"enabled","high":"enabled","xhigh":"enabled","max":"enabled"},"mimo-v2.5-pro":{"none":"disabled","minimal":"disabled","low":"disabled","medium":"enabled","high":"enabled","xhigh":"enabled","max":"enabled"},"glm-5":{"none":"disabled","minimal":"disabled","low":"disabled","medium":"enabled","high":"enabled","xhigh":"enabled","max":"enabled"},"glm-5.1":{"none":"disabled","minimal":"disabled","low":"disabled","medium":"enabled","high":"enabled","xhigh":"enabled","max":"enabled"},"deepseek-v4-pro":{"low":"low","medium":"high","high":"high","xhigh":"high","max":"max"},"deepseek-v4-flash":{"low":"low","medium":"high","high":"high","xhigh":"high","max":"max"}},"noVisionModels":["glm-5.3","glm-5.2","glm-5","glm-5.1","deepseek-v4-flash","deepseek-v4-pro","mimo-v2-pro","mimo-v2.5-pro","minimax-m2.5","minimax-m2.7","qwen3.7-max"],"noReasoningModels":["kimi-k2.7-code","kimi-k2.7-code-highspeed"],"noTemperatureModels":["kimi-k3","kimi-k2.7-code","kimi-k2.7-code-highspeed"],"noTopPModels":["kimi-k3","kimi-k2.7-code","kimi-k2.7-code-highspeed"],"noPenaltyModels":["kimi-k3","kimi-k2.7-code","kimi-k2.7-code-highspeed"],"openaiChatEofTolerance":true,"autoToolChoiceOnlyModels":["kimi-k2.7-code","kimi-k2.7-code-highspeed"],"preserveReasoningContentModels":["glm-5.3","glm-5.3-flash","glm-5.2","kimi-k3","kimi-k2.7-code","kimi-k2.7-code-highspeed","deepseek-v4-pro","deepseek-v4-flash"],"thinkingToggleModels":["mimo-v2.5","mimo-v2.5-pro","glm-5","glm-5.1"],"thinkingBudgetModels":["qwen3.5-397b","qwen3.6-35b","qwen3.5-plus","qwen3.6-plus","qwen3.7-max","qwen3.7-plus"]}},{"id":"neuralwatt","label":"Neuralwatt Cloud","authKind":"key","seed":{"adapter":"openai-chat","baseUrl":"https://api.neuralwatt.com/v1","authMode":"key","defaultModel":"glm-5.3","models":["glm-5.3","glm-5.3-fast","glm-5.3-short","glm-5.3-short-fast","glm-5.3-flash","glm-5.2","glm-5.2-fast","glm-5.2-short","glm-5.2-short-fast","kimi-k2.6","kimi-k2.6-fast","kimi-k2.7-code","qwen3.5-397b","qwen3.5-397b-fast","qwen3.6-35b","qwen3.6-35b-fast"],"modelReasoningEfforts":{"glm-5.3":["low","high","max"],"glm-5.3-fast":[],"glm-5.3-short":["low","high","max"],"glm-5.3-short-fast":[],"glm-5.3-flash":["low","high","max"],"glm-5.2":["low","medium","high","xhigh","max"],"glm-5.2-fast":[],"glm-5.2-short":["low","medium","high","xhigh","max"],"glm-5.2-short-fast":[],"kimi-k2.6":[],"kimi-k2.6-fast":[],"kimi-k2.7-code":[],"qwen3.5-397b":["low","medium","high","xhigh","max"],"qwen3.5-397b-fast":[],"qwen3.6-35b":["low","medium","high","xhigh","max"],"qwen3.6-35b-fast":[]},"noVisionModels":["glm-5.3","glm-5.3-fast","glm-5.3-short","glm-5.3-short-fast","glm-5.2","glm-5.2-fast","glm-5.2-short","glm-5.2-short-fast","qwen3.5-397b","qwen3.5-397b-fast"],"noReasoningModels":["glm-5.3-fast","glm-5.3-short-fast","glm-5.2-fast","glm-5.2-short-fast","kimi-k2.6-fast","qwen3.5-397b-fast","qwen3.6-35b-fast"],"noTemperatureModels":["kimi-k2.7-code"],"noTopPModels":["kimi-k2.7-code"],"noPenaltyModels":["kimi-k2.7-code"],"autoToolChoiceOnlyModels":["kimi-k2.7-code"],"preserveReasoningContentModels":["glm-5.3","glm-5.3-short","glm-5.3-flash","glm-5.2","glm-5.2-short","kimi-k2.6","kimi-k2.7-code","qwen3.5-397b","qwen3.6-35b"],"thinkingBudgetModels":["qwen3.5-397b","qwen3.6-35b","qwen3.5-plus","qwen3.6-plus","qwen3.7-max","qwen3.7-plus"]}},{"id":"openrouter","label":"OpenRouter","authKind":"key","seed":{"adapter":"openai-chat","baseUrl":"https://openrouter.ai/api/v1","authMode":"key","models":["anthropic/claude-sonnet-5","openai/gpt-5.6","openai/gpt-5.6-sol","openai/gpt-5.6-terra","openai/gpt-5.6-luna"],"modelContextWindows":{"anthropic/claude-sonnet-5":1000000,"openai/gpt-5.6-sol":1050000,"openai/gpt-5.6-terra":1050000,"openai/gpt-5.6-luna":1050000}}},{"id":"cline-pass","label":"ClinePass","authKind":"key","seed":{"adapter":"openai-chat","baseUrl":"https://api.cline.bot/api/v1","authMode":"key","defaultModel":"cline-pass/kimi-k3","models":["cline-pass/glm-5.3","cline-pass/glm-5.3-flash","cline-pass/glm-5.2","cline-pass/kimi-k3","cline-pass/kimi-k2.7-code","cline-pass/kimi-k2.6","cline-pass/deepseek-v4-pro","cline-pass/deepseek-v4-flash","cline-pass/mimo-v2.5","cline-pass/mimo-v2.5-pro","cline-pass/minimax-m3","cline-pass/qwen3.8-max","cline-pass/qwen3.7-max","cline-pass/qwen3.7-plus"],"liveModels":false,"modelContextWindows":{"cline-pass/glm-5.3":1048576,"cline-pass/glm-5.3-flash":1048576,"cline-pass/glm-5.2":1048576,"cline-pass/kimi-k3":1048576,"cline-pass/kimi-k2.7-code":262144,"cline-pass/kimi-k2.6":262144,"cline-pass/deepseek-v4-pro":1048576,"cline-pass/deepseek-v4-flash":1048576,"cline-pass/mimo-v2.5":1050000,"cline-pass/mimo-v2.5-pro":1050000,"cline-pass/minimax-m3":1048576,"cline-pass/qwen3.7-max":1000000,"cline-pass/qwen3.7-plus":1000000},"modelInputModalities":{"cline-pass/glm-5.3":["text"],"cline-pass/glm-5.3-flash":["text","image"],"cline-pass/glm-5.2":["text"],"cline-pass/kimi-k3":["text","image"],"cline-pass/kimi-k2.7-code":["text","image"],"cline-pass/kimi-k2.6":["text","image"],"cline-pass/deepseek-v4-pro":["text"],"cline-pass/deepseek-v4-flash":["text"],"cline-pass/mimo-v2.5":["text","image"],"cline-pass/mimo-v2.5-pro":["text"],"cline-pass/minimax-m3":["text","image"],"cline-pass/qwen3.7-max":["text"],"cline-pass/qwen3.7-plus":["text","image"]},"reasoningEfforts":["low","medium","high","xhigh","max"],"reasoningWireFormat":"gateway-object","noVisionModels":["cline-pass/glm-5.3","cline-pass/glm-5.2","cline-pass/deepseek-v4-pro","cline-pass/deepseek-v4-flash","cline-pass/mimo-v2.5-pro","cline-pass/qwen3.7-max"]}},{"id":"cline","label":"Cline","authKind":"key","seed":{"adapter":"openai-chat","baseUrl":"https://api.cline.bot/api/v1","authMode":"key","defaultModel":"anthropic/claude-sonnet-4-6","models":["anthropic/claude-sonnet-4-6","openai/gpt-4o","google/gemini-2.5-pro","deepseek/deepseek-chat","minimax/minimax-m2.5"],"liveModels":true}},{"id":"orcarouter","label":"OrcaRouter","authKind":"key","seed":{"adapter":"openai-chat","baseUrl":"https://api.orcarouter.ai/v1","authMode":"key","defaultModel":"openai/gpt-5.5","models":["openai/gpt-5.5","anthropic/claude-opus-4.8","google/gemini-3.5-flash","deepseek/deepseek-v4-pro","orcarouter/auto"],"modelReasoningEfforts":{"openai/gpt-5.5":["low","medium","high","xhigh"],"deepseek/deepseek-v4-pro":["low","high","max"]},"modelReasoningEffortMap":{"deepseek/deepseek-v4-pro":{"low":"low","medium":"high","high":"high","xhigh":"high","max":"max"}},"noVisionModels":["deepseek/deepseek-v4-pro"],"preserveReasoningContentModels":["deepseek/deepseek-v4-pro"]}},{"id":"bizrouter","label":"BizRouter","authKind":"key","seed":{"adapter":"openai-chat","baseUrl":"https://api.bizrouter.ai/v1","authMode":"key","defaultModel":"openai/gpt-5.6-sol","models":["openai/gpt-5.6-sol","anthropic/claude-sonnet-5","google/gemini-3.5-flash"]}},{"id":"groq","label":"Groq","authKind":"key","seed":{"adapter":"openai-chat","baseUrl":"https://api.groq.com/openai/v1","authMode":"key"}},{"id":"google","label":"Google Gemini","authKind":"key","seed":{"adapter":"google","baseUrl":"https://generativelanguage.googleapis.com","authMode":"key","defaultModel":"gemini-3.5-flash","models":["gemini-3.8-flash","gemini-3.6-flash","gemini-3.5-flash","gemini-3.5-flash-lite","gemini-3.1-pro-preview","gemini-3.7-flash"],"modelContextWindows":{"gemini-3.8-flash":1048576,"gemini-3.6-flash":1048576,"gemini-3.5-flash":1000000,"gemini-3.5-flash-lite":1048576,"gemini-3.7-flash":1048576},"modelInputModalities":{"gemini-3.8-flash":["text","image"],"gemini-3.6-flash":["text","image"],"gemini-3.5-flash-lite":["text","image"],"gemini-3.7-flash":["text","image"]},"modelReasoningEfforts":{"gemini-3.8-flash":["low","medium","high"],"gemini-3.7-flash":["low","medium","high"],"gemini-3.6-flash":["minimal","low","medium","high"],"gemini-3.5-flash":["minimal","low","medium","high"],"gemini-3.1-pro-preview":["low","medium","high"]}}},{"id":"google-vertex","label":"Google Vertex AI","authKind":"key","seed":{"adapter":"google","baseUrl":"https://aiplatform.googleapis.com","authMode":"key","defaultModel":"gemini-3-pro","googleMode":"vertex"}},{"id":"google-antigravity","label":"Google Antigravity","authKind":"oauth","seed":{"adapter":"google","baseUrl":"https://daily-cloudcode-pa.googleapis.com","authMode":"oauth","defaultModel":"gemini-3.8-flash","models":["gemini-3.8-flash","gemini-3.7-flash","gemini-3.1-pro","gemini-3.1-flash-image","claude-sonnet-4-6","claude-opus-4-6-thinking","gpt-oss-120b-medium"],"liveModels":true,"modelContextWindows":{"gemini-3.8-flash":1048576,"gemini-3.7-flash":1048576,"gemini-3.1-pro":1048576,"gemini-3.8-flash-low":1048576,"gemini-3.8-flash-medium":1048576,"gemini-3.8-flash-high":1048576,"gemini-3.7-flash-tiered":1048576,"gemini-3.1-pro-low":1048576,"gemini-pro-agent":1048576,"gemini-3.1-flash-image":1048576,"claude-sonnet-4-6":250000,"claude-opus-4-6-thinking":250000,"gpt-oss-120b-medium":131072,"gemini-3.1-pro-high":1048576,"gemini-3.1-pro-preview":1048576,"gemini-3.6-flash":1048576,"gemini-3.6-flash-low":1048576,"gemini-3.6-flash-medium":1048576,"gemini-3.6-flash-high":1048576,"gemini-3.5-flash-extra-low":1048576,"gemini-3.5-flash-low":1048576,"gemini-3.5-flash-mid":1048576,"gemini-3.5-flash-high":1048576,"gemini-3-flash-agent":1048576},"modelInputModalities":{"gemini-3.8-flash":["text","image"],"gemini-3.7-flash":["text","image"],"gemini-3.1-pro":["text","image"],"gemini-3.1-flash-image":["text","image"],"claude-sonnet-4-6":["text","image"],"claude-opus-4-6-thinking":["text","image"],"gpt-oss-120b-medium":["text"]},"modelReasoningEfforts":{"gemini-3.8-flash":["low","medium","high"],"gemini-3.7-flash":["low","medium","high"],"gemini-3.1-pro":["low","high"],"claude-sonnet-4-6":["low","medium","high","max"],"claude-opus-4-6-thinking":["low","medium","high","max"]},"googleMode":"cloud-code-assist"}},{"id":"azure-openai","label":"Azure OpenAI","authKind":"key","seed":{"adapter":"azure-openai","baseUrl":"https://{resource}.openai.azure.com/openai","authMode":"key"}},{"id":"ollama","label":"Ollama (local)","authKind":"local","seed":{"adapter":"openai-chat","baseUrl":"http://localhost:11434/v1","authMode":"local"}},{"id":"vllm","label":"vLLM (local)","authKind":"local","seed":{"adapter":"openai-chat","baseUrl":"http://localhost:8000/v1","authMode":"local"}},{"id":"lm-studio","label":"LM Studio (local)","authKind":"local","seed":{"adapter":"openai-chat","baseUrl":"http://localhost:1234/v1","authMode":"local"}},{"id":"deepseek","label":"DeepSeek","authKind":"key","seed":{"adapter":"openai-chat","baseUrl":"https://api.deepseek.com","responsesPath":"/responses","authMode":"key","defaultModel":"deepseek-v4-flash","models":["deepseek-chat","deepseek-reasoner","deepseek-v4-pro","deepseek-v4-flash","deepseek-v4-flash-vision-exp"],"modelContextWindows":{"deepseek-v4-flash":1048576,"deepseek-v4-pro":1048576,"deepseek-v4-flash-vision-exp":1048576},"modelInputModalities":{"deepseek-v4-flash-vision-exp":["text","image"]},"modelReasoningEfforts":{"deepseek-v4-pro":["low","high","max"],"deepseek-v4-flash":["low","high","max"]},"modelReasoningEffortMap":{"deepseek-v4-pro":{"low":"low","medium":"high","high":"high","xhigh":"high","max":"max"},"deepseek-v4-flash":{"low":"low","medium":"high","high":"high","xhigh":"high","max":"max"}},"noVisionModels":["deepseek-chat","deepseek-reasoner","deepseek-v4-pro","deepseek-v4-flash"],"statelessResponses":true,"requiresAdjacentResponsesToolResults":true,"annotateEmptyToolOutputs":true,"preserveReasoningContentModels":["deepseek-v4-pro","deepseek-v4-flash"]}},{"id":"cerebras","label":"Cerebras","authKind":"key","seed":{"adapter":"openai-chat","baseUrl":"https://api.cerebras.ai/v1","authMode":"key","defaultModel":"gpt-oss-120b"}},{"id":"chutes","label":"Chutes","authKind":"key","seed":{"adapter":"openai-chat","baseUrl":"https://llm.chutes.ai/v1","authMode":"key","liveModels":true,"reasoningEfforts":[],"parallelToolCalls":false}},{"id":"deepinfra","label":"DeepInfra","authKind":"key","seed":{"adapter":"openai-chat","baseUrl":"https://api.deepinfra.com/v1/openai","authMode":"key","liveModels":true}},{"id":"hyperbolic","label":"Hyperbolic","authKind":"key","seed":{"adapter":"openai-chat","baseUrl":"https://api.hyperbolic.xyz/v1","authMode":"key","liveModels":true}},{"id":"nscale","label":"Nscale Serverless Inference","authKind":"key","seed":{"adapter":"openai-chat","baseUrl":"https://inference.api.nscale.com/v1","authMode":"key","defaultModel":"meta-llama/Llama-3.1-8B-Instruct","models":["meta-llama/Llama-3.1-8B-Instruct"],"liveModels":true,"reasoningEfforts":[],"parallelToolCalls":false}},{"id":"vultr","label":"Vultr Serverless Inference","authKind":"key","seed":{"adapter":"openai-chat","baseUrl":"https://api.vultrinference.com/v1","authMode":"key","defaultModel":"kimi-k2-instruct","models":["kimi-k2-instruct"],"liveModels":true,"reasoningEfforts":[],"parallelToolCalls":false}},{"id":"baseten","label":"Baseten Model APIs","authKind":"key","seed":{"adapter":"openai-chat","baseUrl":"https://inference.baseten.co/v1","authMode":"key","liveModels":true,"modelInputModalities":{"thinkingmachines/inkling":["text","image"],"moonshotai/Kimi-K2.6":["text","image"],"moonshotai/Kimi-K2.7-Code":["text","image"],"moonshotai/Kimi-K3":["text","image"]},"reasoningEfforts":[],"modelReasoningEfforts":{"deepseek-ai/DeepSeek-V4-Pro":["low","medium","high","xhigh","max"],"thinkingmachines/inkling":["low","medium","high","xhigh","max"],"openai/gpt-oss-120b":["low","medium","high","xhigh","max"],"moonshotai/Kimi-K3":["low","high","max"],"zai-org/GLM-5.3":["low","high","max"],"zai-org/GLM-5.3-Fast":["low","high","max"],"zai-org/GLM-5.2":["high","max"],"zai-org/GLM-5.2-Fast":["high","max"]},"modelDefaultReasoningEfforts":{"deepseek-ai/DeepSeek-V4-Pro":"medium","thinkingmachines/inkling":"high","openai/gpt-oss-120b":"medium","moonshotai/Kimi-K3":"max"},"modelReasoningEffortMap":{"deepseek-ai/DeepSeek-V4-Pro":{"none":"none","minimal":"minimal"},"thinkingmachines/inkling":{"none":"none","minimal":"minimal"},"openai/gpt-oss-120b":{"none":"none","minimal":"minimal"},"moonshotai/Kimi-K3":{"none":"none"},"zai-org/GLM-5.3":{"none":"none"},"zai-org/GLM-5.3-Fast":{"none":"none"},"zai-org/GLM-5.2":{"none":"none"},"zai-org/GLM-5.2-Fast":{"none":"none"}},"parallelToolCalls":true}},{"id":"commandcode","label":"Command Code - API","authKind":"key","seed":{"adapter":"openai-chat","baseUrl":"https://api.commandcode.ai/provider/v1","authMode":"key","defaultModel":"deepseek/deepseek-v4-flash","models":["deepseek/deepseek-v4-flash"],"liveModels":true,"modelContextWindows":{"deepseek/deepseek-v4-flash-vision-exp":1048576},"modelInputModalities":{"deepseek/deepseek-v4-flash-vision-exp":["text","image"],"gpt-5.6-luna":["text","image"],"gpt-5.6-sol":["text","image"],"MiniMaxAI/MiniMax-M3":["text","image"],"moonshotai/Kimi-K3":["text","image"],"meta/muse-spark-1.3":["text","image"],"meta/muse-spark-1.3-contributor":["text","image"],"meta/muse-spark-1.2":["text","image"],"meta/muse-spark-1.2-contributor":["text","image"]},"reasoningEfforts":[],"modelReasoningEfforts":{"deepseek/deepseek-v4-pro":["high","max"],"deepseek/deepseek-v4-flash":["high","max"],"deepseek/deepseek-v4-flash-vision-exp":["high","max"],"gpt-5.6-luna":["low","medium","high","xhigh","max"],"google/gemini-3.7-flash":["low","medium","high"],"zai-org/GLM-5":["high","max"],"zai-org/GLM-5.1":["high","max"],"zai-org/GLM-5.2":["high","max"],"zai-org/GLM-5.2-Fast":["high","max"],"zai-org/GLM-5.3":["low","high","max"],"z-ai/glm-5.3-flash":["low","high","max"],"meta/muse-spark-1.3":["low","medium","high","xhigh","max"],"meta/muse-spark-1.3-contributor":["low","medium","high","xhigh","max"],"meta/muse-spark-1.2":["low","medium","high","xhigh","max"],"meta/muse-spark-1.2-contributor":["low","medium","high","xhigh","max"],"meta/muse-spark-1.1":["low","medium","high","xhigh","max"]}}},{"id":"sambanova","label":"SambaNova Cloud","authKind":"key","seed":{"adapter":"openai-chat","baseUrl":"https://api.sambanova.ai/v1","authMode":"key","liveModels":true,"reasoningEfforts":[],"parallelToolCalls":false}},{"id":"nebius","label":"Nebius Token Factory","authKind":"key","seed":{"adapter":"openai-chat","baseUrl":"https://api.tokenfactory.nebius.com/v1","authMode":"key","liveModels":true,"reasoningEfforts":[],"parallelToolCalls":false}},{"id":"digitalocean","label":"DigitalOcean Serverless Inference","authKind":"key","seed":{"adapter":"openai-chat","baseUrl":"https://inference.do-ai.run/v1","authMode":"key","liveModels":true,"reasoningEfforts":[],"parallelToolCalls":false}},{"id":"scaleway","label":"Scaleway Generative APIs","authKind":"key","seed":{"adapter":"openai-chat","baseUrl":"https://api.scaleway.ai/v1","authMode":"key","freeTier":true,"liveModels":true,"modelInputModalities":{"pixtral-12b-2409":["text","image"]},"reasoningEfforts":[],"parallelToolCalls":false}},{"id":"featherless","label":"Featherless AI","authKind":"key","seed":{"adapter":"openai-chat","baseUrl":"https://api.featherless.ai/v1","authMode":"key","liveModels":true,"reasoningEfforts":[],"parallelToolCalls":false}},{"id":"novita","label":"Novita AI","authKind":"key","seed":{"adapter":"openai-chat","baseUrl":"https://api.novita.ai/openai/v1","authMode":"key","liveModels":true,"reasoningEfforts":[],"parallelToolCalls":false}},{"id":"together","label":"Together","authKind":"key","seed":{"adapter":"openai-chat","baseUrl":"https://api.together.xyz/v1","authMode":"key"}},{"id":"fireworks","label":"Fireworks","authKind":"key","seed":{"adapter":"openai-chat","baseUrl":"https://api.fireworks.ai/inference/v1","authMode":"key"}},{"id":"firepass","label":"Fire Pass (Fireworks Kimi)","authKind":"key","seed":{"adapter":"openai-chat","baseUrl":"https://api.fireworks.ai/inference/v1","authMode":"key"}},{"id":"moonshot","label":"Moonshot (Kimi API)","authKind":"key","seed":{"adapter":"openai-chat","baseUrl":"https://api.moonshot.ai/v1","authMode":"key","defaultModel":"kimi-k2.7-code","models":["kimi-k3","kimi-k2.7-code","kimi-k2.7-code-highspeed","kimi-k2.6","kimi-k2.5"],"modelContextWindows":{"kimi-k3":1048576,"kimi-k2.7-code":262144,"kimi-k2.7-code-highspeed":262144,"kimi-k2.6":262144,"kimi-k2.5":262144},"modelInputModalities":{"kimi-k3":["text","image"]},"modelReasoningEfforts":{"kimi-k3":["max"],"kimi-k2.7-code":[],"kimi-k2.7-code-highspeed":[],"kimi-k2.6":[],"kimi-k2.5":[]},"noReasoningModels":["kimi-k2.7-code","kimi-k2.7-code-highspeed","kimi-k2.6","kimi-k2.5"],"noTemperatureModels":["kimi-k3","kimi-k2.7-code","kimi-k2.7-code-highspeed","kimi-k2.6","kimi-k2.5"],"noTopPModels":["kimi-k3","kimi-k2.7-code","kimi-k2.7-code-highspeed","kimi-k2.6","kimi-k2.5"],"noPenaltyModels":["kimi-k3","kimi-k2.7-code","kimi-k2.7-code-highspeed","kimi-k2.6","kimi-k2.5"],"autoToolChoiceOnlyModels":["kimi-k2.7-code","kimi-k2.7-code-highspeed"],"preserveReasoningContentModels":["kimi-k3","kimi-k2.7-code","kimi-k2.7-code-highspeed","kimi-k2.6","kimi-k2.5"]}},{"id":"huggingface","label":"Hugging Face","authKind":"key","seed":{"adapter":"openai-chat","baseUrl":"https://router.huggingface.co/v1","authMode":"key"}},{"id":"nvidia","label":"NVIDIA NIM","authKind":"key","seed":{"adapter":"openai-chat","baseUrl":"https://integrate.api.nvidia.com/v1","authMode":"key","freeTier":true,"modelInputModalities":{"meta/llama-3.2-11b-vision-instruct":["text","image"],"meta/llama-3.2-90b-vision-instruct":["text","image"],"nvidia/llama-3.1-nemotron-nano-vl-8b-v1":["text","image"],"nvidia/nemotron-nano-12b-v2-vl":["text","image"],"nvidia/nemotron-3-nano-omni-30b-a3b-reasoning":["text","image"],"nvidia/cosmos3-nano-reasoner":["text","image"],"nvidia/ising-calibration-1.5-31b":["text","image"],"nvidia/ising-calibration-1-35b-a3b":["text","image"],"google/gemma-4-31b-it":["text","image"],"google/diffusiongemma-26b-a4b-it":["text","image"],"minimaxai/minimax-m3":["text","image"],"moonshotai/kimi-k2.6":["text","image"],"moonshotai/kimi-k2.5":["text","image"],"stepfun-ai/step-3.7-flash":["text","image"],"thinkingmachines/inkling":["text","image"],"mistralai/mistral-medium-3.5-128b":["text","image"],"z-ai/glm-5.3-flash":["text","image"]},"modelReasoningEfforts":{"moonshotai/kimi-k2.6":[],"moonshotai/kimi-k2.5":[],"moonshotai/kimi-k2-thinking":[],"moonshotai/kimi-k2-instruct":[],"moonshotai/kimi-k2-instruct-0905":[]},"noVisionModels":["deepseek-ai/deepseek-v4-flash","deepseek-ai/deepseek-v4-pro","google/codegemma-7b","meta/llama-3.1-70b-instruct","meta/llama-3.1-8b-instruct","meta/llama-3.2-1b-instruct","meta/llama-3.2-3b-instruct","meta/llama-3.3-70b-instruct","meta/llama2-70b","mistralai/mistral-7b-instruct-v0.3","mistralai/mistral-nemotron","moonshotai/kimi-k2-thinking","moonshotai/kimi-k2-instruct","nvidia/llama-3.1-nemotron-nano-8b-v1","nvidia/llama-3.1-nemotron-ultra-253b-v1","nvidia/llama-3.3-nemotron-super-49b-v1","nvidia/llama-3.3-nemotron-super-49b-v1.5","nvidia/nemotron-3-nano-30b-a3b","nvidia/nemotron-3-super-120b-a12b","nvidia/nemotron-3-ultra-550b-a55b","nvidia/nemotron-mini-4b-instruct","nvidia/nvidia-nemotron-nano-9b-v2","openai/gpt-oss-120b","openai/gpt-oss-20b","poolside/laguna-xs-2.1","z-ai/glm-5.3","z-ai/glm-5.2"],"noReasoningModels":["moonshotai/kimi-k2.6","moonshotai/kimi-k2.5","moonshotai/kimi-k2-thinking","moonshotai/kimi-k2-instruct","moonshotai/kimi-k2-instruct-0905"],"parallelToolCalls":false,"preserveReasoningContentModels":["moonshotai/kimi-k2.6","moonshotai/kimi-k2.5","moonshotai/kimi-k2-thinking"]}},{"id":"venice","label":"Venice","authKind":"key","seed":{"adapter":"openai-chat","baseUrl":"https://api.venice.ai/api/v1","authMode":"key"}},{"id":"zai","label":"Z.AI \u2014 GLM Coding Plan","authKind":"key","seed":{"adapter":"openai-chat","baseUrl":"https://api.z.ai/api/coding/paas/v4","authMode":"key","modelSuffixBracketStrip":true,"defaultModel":"glm-5.3","models":["glm-5.3","glm-5.3[1m]","glm-5.3-flash","glm-5.2","glm-5.2[1m]","glm-5.1","glm-5","glm-4.6"],"modelContextWindows":{"glm-5.3":1000000,"glm-5.3[1m]":1000000,"glm-5.3-flash":1000000,"glm-5.2":1000000,"glm-5.2[1m]":1000000},"modelMaxOutputTokens":{"glm-5.3":131072,"glm-5.3[1m]":131072,"glm-5.3-flash":131072},"modelReasoningEfforts":{"glm-5.3":["low","high","max"],"glm-5.3[1m]":["low","high","max"],"glm-5.3-flash":["low","high","max"],"glm-5.2":["low","medium","high","xhigh","max"],"glm-5.2[1m]":["low","medium","high","xhigh","max"]},"modelDefaultReasoningEfforts":{"glm-5.3":"max","glm-5.3[1m]":"max","glm-5.3-flash":"max"},"noVisionModels":["glm-5.3","glm-5.3[1m]","glm-5.2","glm-5.2[1m]"],"preserveReasoningContentModels":["glm-5.3","glm-5.3[1m]","glm-5.3-flash","glm-5.2","glm-5.2[1m]"]}},{"id":"zhipu-bigmodel","label":"Zhipu AI \u2014 BigModel","authKind":"key","seed":{"adapter":"openai-chat","baseUrl":"https://open.bigmodel.cn/api/paas/v4","authMode":"key","defaultModel":"glm-4.6","models":["glm-4.6","glm-4.7","glm-4.7-flash","glm-5","glm-5.1","glm-5.2","glm-5.3","glm-4.6v"],"modelContextWindows":{"glm-4.6":204800},"modelInputModalities":{"glm-4.6":["text"],"glm-4.7":["text"],"glm-4.7-flash":["text"],"glm-5":["text"],"glm-5.1":["text"],"glm-5.2":["text"],"glm-5.3":["text"],"glm-4.6v":["text","image"]},"modelReasoningEfforts":{"glm-4.6":["low","medium","high","xhigh","max"],"glm-4.7":["low","medium","high","xhigh","max"],"glm-5":["low","medium","high","xhigh","max"],"glm-5.1":["low","medium","high","xhigh","max"],"glm-5.2":["low","medium","high","xhigh","max"],"glm-5.3":["low","medium","high","xhigh","max"],"glm-5.3-flash":["low","medium","high","xhigh","max"]},"modelReasoningEffortMap":{"glm-4.6":{"none":"disabled","minimal":"disabled","low":"disabled","medium":"enabled","high":"enabled","xhigh":"enabled","max":"enabled"},"glm-4.7":{"none":"disabled","minimal":"disabled","low":"disabled","medium":"enabled","high":"enabled","xhigh":"enabled","max":"enabled"},"glm-5":{"none":"disabled","minimal":"disabled","low":"disabled","medium":"enabled","high":"enabled","xhigh":"enabled","max":"enabled"},"glm-5.1":{"none":"disabled","minimal":"disabled","low":"disabled","medium":"enabled","high":"enabled","xhigh":"enabled","max":"enabled"},"glm-5.2":{"none":"disabled","minimal":"disabled","low":"disabled","medium":"enabled","high":"enabled","xhigh":"enabled","max":"enabled"},"glm-5.3":{"none":"disabled","minimal":"disabled","low":"disabled","medium":"enabled","high":"enabled","xhigh":"enabled","max":"enabled"},"glm-5.3-flash":{"none":"disabled","minimal":"disabled","low":"disabled","medium":"enabled","high":"enabled","xhigh":"enabled","max":"enabled"}},"preserveReasoningContentModels":["glm-4.6","glm-4.7","glm-5","glm-5.1","glm-5.2","glm-5.3","glm-5.3-flash"],"requiresReasoningPlaceholderModels":[],"thinkingToggleModels":["glm-4.6","glm-4.7","glm-5","glm-5.1","glm-5.2","glm-5.3","glm-5.3-flash"]}},{"id":"zhipu-bigmodel-coding","label":"Zhipu AI \u2014 BigModel Coding Plan","authKind":"key","seed":{"adapter":"openai-chat","baseUrl":"https://open.bigmodel.cn/api/coding/paas/v4","authMode":"key","modelSuffixBracketStrip":true,"defaultModel":"glm-5.3","models":["glm-5.3","glm-5.3[1m]","glm-5.3-flash","glm-5.2","glm-5.2[1m]","glm-5.1","glm-5","glm-4.6"],"modelContextWindows":{"glm-5.3":1000000,"glm-5.3[1m]":1000000,"glm-5.3-flash":1000000,"glm-5.2":1000000,"glm-5.2[1m]":1000000},"modelReasoningEfforts":{"glm-5.3":["low","high","max"],"glm-5.3[1m]":["low","high","max"],"glm-5.3-flash":["low","high","max"],"glm-5.2":["low","medium","high","xhigh","max"],"glm-5.2[1m]":["low","medium","high","xhigh","max"]},"noVisionModels":["glm-5.3","glm-5.3[1m]","glm-5.2","glm-5.2[1m]"],"preserveReasoningContentModels":["glm-5.3","glm-5.3[1m]","glm-5.3-flash","glm-5.2","glm-5.2[1m]"]}},{"id":"nanogpt","label":"NanoGPT","authKind":"key","seed":{"adapter":"openai-chat","baseUrl":"https://nano-gpt.com/api/v1","authMode":"key"}},{"id":"synthetic","label":"Synthetic","authKind":"key","seed":{"adapter":"openai-chat","baseUrl":"https://api.synthetic.new/openai/v1","authMode":"key"}},{"id":"siliconflow","label":"SiliconFlow","authKind":"key","seed":{"adapter":"openai-chat","baseUrl":"https://api.siliconflow.cn/v1","authMode":"key","liveModels":true}},{"id":"qwen-cloud","label":"Qwen Cloud","authKind":"key","seed":{"adapter":"openai-chat","baseUrl":"https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1","authMode":"key"}},{"id":"tencent-coding-plan","label":"Tencent Cloud Coding Plan","authKind":"key","seed":{"adapter":"openai-chat","baseUrl":"https://api.lkeap.cloud.tencent.com/coding/v3","authMode":"key","defaultModel":"tc-code-latest","models":["tc-code-latest","glm-5","kimi-k2.5","minimax-m2.5"],"liveModels":true,"modelInputModalities":{"tc-code-latest":["text"],"glm-5":["text"],"kimi-k2.5":["text"],"minimax-m2.5":["text"]},"noVisionModels":["tc-code-latest","glm-5","kimi-k2.5","minimax-m2.5"]}},{"id":"volcengine","label":"Volcengine Ark","authKind":"key","seed":{"adapter":"openai-chat","baseUrl":"https://ark.cn-beijing.volces.com/api/v3","authMode":"key","defaultModel":"doubao-seed-2-1-pro-260628","models":["doubao-seed-2-1-pro-260628","doubao-seed-2-1-turbo-260628","doubao-seed-evolving","deepseek-v4-pro-260425","deepseek-v4-flash-260425","deepseek-v3-2-251201","glm-5-2-260617","glm-4-7-251222"],"liveModels":false,"modelReasoningEfforts":{"doubao-seed-2-1-pro-260628":["low","medium","high","xhigh","max"],"doubao-seed-2-1-turbo-260628":["low","medium","high","xhigh","max"],"doubao-seed-evolving":["low","medium","high","xhigh","max"]},"modelReasoningEffortMap":{"doubao-seed-2-1-pro-260628":{"none":"disabled","minimal":"disabled","low":"disabled","medium":"enabled","high":"enabled","xhigh":"enabled","max":"enabled"},"doubao-seed-2-1-turbo-260628":{"none":"disabled","minimal":"disabled","low":"disabled","medium":"enabled","high":"enabled","xhigh":"enabled","max":"enabled"},"doubao-seed-evolving":{"none":"disabled","minimal":"disabled","low":"disabled","medium":"enabled","high":"enabled","xhigh":"enabled","max":"enabled"}},"noVisionModels":["deepseek-v4-pro-260425","deepseek-v4-flash-260425","deepseek-v3-2-251201","glm-5-2-260617","glm-4-7-251222"],"preserveReasoningContentModels":["deepseek-v4-pro-260425","deepseek-v4-flash-260425","glm-5-2-260617","glm-4-7-251222"],"thinkingToggleModels":["doubao-seed-2-1-pro-260628","doubao-seed-2-1-turbo-260628","doubao-seed-evolving"]}},{"id":"volcengine-coding-plan","label":"Volcengine Ark Coding Plan","authKind":"key","seed":{"adapter":"openai-chat","baseUrl":"https://ark.cn-beijing.volces.com/api/coding/v3","authMode":"key","defaultModel":"ark-code-latest","models":["ark-code-latest","doubao-seed-2.0-code","deepseek-v4-pro","deepseek-v4-flash","glm-5.3","glm-5.3-flash","glm-5.2","kimi-k2.6","minimax-m3"],"liveModels":false,"modelInputModalities":{"kimi-k2.6":["text","image"],"minimax-m3":["text","image"],"glm-5.3-flash":["text","image"]},"modelReasoningEfforts":{"deepseek-v4-pro":["low","high","max"],"deepseek-v4-flash":["low","high","max"]},"modelReasoningEffortMap":{"deepseek-v4-pro":{"low":"low","medium":"high","high":"high","xhigh":"high","max":"max"},"deepseek-v4-flash":{"low":"low","medium":"high","high":"high","xhigh":"high","max":"max"}},"noVisionModels":["ark-code-latest","doubao-seed-2.0-code","deepseek-v4-pro","deepseek-v4-flash","glm-5.3","glm-5.2","doubao-seed-2.0-pro"],"preserveReasoningContentModels":["deepseek-v4-pro","deepseek-v4-flash"]}},{"id":"volcengine-agent-plan","label":"Volcengine Ark Agent Plan","authKind":"key","seed":{"adapter":"openai-responses","baseUrl":"https://ark.cn-beijing.volces.com/api/plan/v3","responsesPath":"/responses","authMode":"key","defaultModel":"deepseek-v4-pro","models":["deepseek-v4-pro","deepseek-v4-flash","glm-5.3","glm-5.3-flash","glm-5.2","kimi-k2.6","minimax-m3","doubao-seed-2.0-pro"],"liveModels":false,"modelInputModalities":{"kimi-k2.6":["text","image"],"minimax-m3":["text","image"],"glm-5.3-flash":["text","image"]},"noVisionModels":["ark-code-latest","doubao-seed-2.0-code","deepseek-v4-pro","deepseek-v4-flash","glm-5.3","glm-5.2","doubao-seed-2.0-pro"]}},{"id":"qianfan","label":"Qianfan (Baidu)","authKind":"key","seed":{"adapter":"openai-chat","baseUrl":"https://qianfan.baidubce.com/v2","authMode":"key"}},{"id":"alibaba","label":"Alibaba Coding Plan","authKind":"key","seed":{"adapter":"openai-chat","baseUrl":"https://coding-intl.dashscope.aliyuncs.com/v1","authMode":"key"}},{"id":"alibaba-token-plan","label":"Alibaba Token Plan (Beijing)","authKind":"key","seed":{"adapter":"openai-chat","baseUrl":"https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1","authMode":"key","defaultModel":"qwen3.8-max","models":["qwen3.8-max","qwen3.7-max","qwen3.7-plus","qwen3.6-flash","glm-5.3","glm-5.3-flash","glm-5.2","deepseek-v4-pro"],"liveModels":false,"modelContextWindows":{"qwen3.8-max":983616,"qwen3.7-max":1000000,"qwen3.7-plus":1000000,"qwen3.6-flash":1000000,"glm-5.3":1000000,"glm-5.3-flash":1000000,"glm-5.2":1000000,"deepseek-v4-pro":1000000},"modelInputModalities":{"qwen3.8-max":["text","image"],"qwen3.7-max":["text","image"],"qwen3.7-plus":["text","image"],"qwen3.6-flash":["text","image"],"glm-5.3":["text"],"glm-5.3-flash":["text","image"],"glm-5.2":["text"],"deepseek-v4-pro":["text"]},"modelReasoningEfforts":{"qwen3.8-max":["low","medium","xhigh"],"qwen3.7-max":["low","medium","high","xhigh","max"],"qwen3.7-plus":["low","medium","high","xhigh","max"],"qwen3.6-flash":["low","medium","high","xhigh","max"],"glm-5.3":["low","high","max"],"glm-5.3-flash":["low","high","max"],"glm-5.2":["low","medium","high","xhigh","max"],"deepseek-v4-pro":["low","high","max"]},"modelDefaultReasoningEfforts":{"qwen3.8-max":"xhigh"},"modelReasoningEffortMap":{"deepseek-v4-pro":{"low":"low","medium":"high","high":"high","xhigh":"high","max":"max"}},"noVisionModels":["glm-5.3","glm-5.2","deepseek-v4-pro"],"preserveReasoningContentModels":["glm-5.3","glm-5.3-flash","glm-5.2","deepseek-v4-pro","qwen3.8-max","qwen3.7-max","qwen3.7-plus","qwen3.6-flash"],"thinkingBudgetModels":["qwen3.7-max","qwen3.7-plus","qwen3.6-flash"]}},{"id":"alibaba-token-plan-intl","label":"Alibaba Token Plan (International)","authKind":"key","seed":{"adapter":"openai-chat","baseUrl":"https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1","authMode":"key","defaultModel":"qwen3.7-max","models":["qwen3.8-max","qwen3.7-max","qwen3.7-plus","qwen3.6-plus","qwen3.6-flash","deepseek-v4-pro","deepseek-v4-flash","deepseek-v3.2","kimi-k2.7-code","kimi-k2.6","kimi-k2.5","glm-5.3","glm-5.3-flash","glm-5.2","glm-5.1","glm-5","MiniMax-M2.5"],"liveModels":false,"modelContextWindows":{"qwen3.8-max":983616,"qwen3.7-max":1000000,"qwen3.7-plus":1000000,"qwen3.6-plus":1000000,"qwen3.6-flash":1000000,"deepseek-v4-pro":1000000,"deepseek-v4-flash":1000000,"deepseek-v3.2":131072,"kimi-k2.7-code":262144,"kimi-k2.6":262144,"kimi-k2.5":262144,"glm-5.3":1000000,"glm-5.3-flash":1000000,"glm-5.2":1000000,"glm-5.1":1000000,"glm-5":1000000,"MiniMax-M2.5":204800},"modelInputModalities":{"qwen3.8-max":["text","image"],"qwen3.7-max":["text","image"],"qwen3.7-plus":["text","image"],"qwen3.6-plus":["text","image"],"qwen3.6-flash":["text","image"],"deepseek-v4-pro":["text"],"deepseek-v4-flash":["text"],"deepseek-v3.2":["text"],"kimi-k2.7-code":["text","image"],"kimi-k2.6":["text","image"],"kimi-k2.5":["text","image"],"glm-5.3":["text"],"glm-5.3-flash":["text","image"],"glm-5.2":["text"],"glm-5.1":["text"],"glm-5":["text"],"MiniMax-M2.5":["text"]},"modelReasoningEfforts":{"qwen3.8-max":["low","medium","xhigh"],"qwen3.7-max":["low","medium","high","xhigh","max"],"qwen3.7-plus":["low","medium","high","xhigh","max"],"qwen3.6-plus":["low","medium","high","xhigh","max"],"qwen3.6-flash":["low","medium","high","xhigh","max"],"glm-5.3":["low","high","max"],"glm-5.3-flash":["low","high","max"],"glm-5.2":["low","medium","high","xhigh","max"],"deepseek-v4-pro":["low","high","max"],"deepseek-v4-flash":["low","high","max"]},"modelDefaultReasoningEfforts":{"qwen3.8-max":"xhigh"},"modelReasoningEffortMap":{"deepseek-v4-pro":{"low":"low","medium":"high","high":"high","xhigh":"high","max":"max"},"deepseek-v4-flash":{"low":"low","medium":"high","high":"high","xhigh":"high","max":"max"}},"noVisionModels":["deepseek-v4-pro","deepseek-v4-flash","deepseek-v3.2","glm-5.3","glm-5.2","glm-5.1","glm-5","MiniMax-M2.5"],"noReasoningModels":["kimi-k2.7-code","kimi-k2.6","kimi-k2.5","deepseek-v3.2","glm-5.1","glm-5","MiniMax-M2.5"],"preserveReasoningContentModels":["glm-5.3","glm-5.3-flash","glm-5.2","deepseek-v4-pro","deepseek-v4-flash","qwen3.8-max","qwen3.7-max","qwen3.7-plus","qwen3.6-plus","qwen3.6-flash"],"thinkingBudgetModels":["qwen3.7-max","qwen3.7-plus","qwen3.6-plus","qwen3.6-flash"]}},{"id":"parallel","label":"Parallel","authKind":"key","seed":{"adapter":"openai-chat","baseUrl":"https://platform.parallel.ai","authMode":"key"}},{"id":"zenmux","label":"ZenMux","authKind":"key","seed":{"adapter":"openai-chat","baseUrl":"https://zenmux.ai/api/v1","authMode":"key","models":["moonshotai/kimi-k3-free","moonshotai/kimi-k3"]}},{"id":"litellm","label":"LiteLLM (self-hosted)","authKind":"key","seed":{"adapter":"openai-chat","baseUrl":"http://localhost:4000/v1","authMode":"key","keyOptional":true}},{"id":"ollama-cloud","label":"Ollama Cloud","authKind":"key","seed":{"adapter":"ollama-native","baseUrl":"https://ollama.com/v1","authMode":"key","defaultModel":"glm-5.3","models":["glm-5.3","glm-5.3-flash","glm-5.2","deepseek-v4-pro","qwen3-coder:480b","gpt-oss:120b","kimi-k2.6","minimax-m3","qwen3.5:397b","gemma4:31b"],"modelContextWindows":{"glm-5.3":1048576,"glm-5.3-flash":1048576},"noVisionModels":["glm-5.3","glm-5.2","glm-5.1","glm-5","glm-4.7","minimax-m2.7","minimax-m2.5","minimax-m2.1","nemotron-3-ultra","nemotron-3-super","deepseek-v4-pro","deepseek-v4-flash","gpt-oss","qwen3-coder:480b"]}},{"id":"mistral","label":"Mistral","authKind":"key","seed":{"adapter":"openai-chat","baseUrl":"https://api.mistral.ai/v1","authMode":"key","defaultModel":"codestral-latest"}},{"id":"minimax","label":"MiniMax \u2014 Coding Plan","authKind":"key","seed":{"adapter":"openai-chat","baseUrl":"https://api.minimax.io/v1","authMode":"key","defaultModel":"MiniMax-M3","models":["MiniMax-M3","MiniMax-M2.7","MiniMax-M2.7-highspeed","MiniMax-M2.5","MiniMax-M2.5-highspeed","MiniMax-M2.1","MiniMax-M2.1-highspeed","MiniMax-M2"],"modelContextWindows":{"MiniMax-M3":1000000,"MiniMax-M2.7":204800,"MiniMax-M2.7-highspeed":204800,"MiniMax-M2.5":204800,"MiniMax-M2.5-highspeed":204800,"MiniMax-M2.1":204800,"MiniMax-M2.1-highspeed":204800,"MiniMax-M2":204800},"modelReasoningEfforts":{"MiniMax-M3":["low","medium","high","xhigh","max"]},"modelDefaultReasoningEfforts":{"MiniMax-M3":"medium"},"modelReasoningEffortMap":{"MiniMax-M3":{"none":"disabled","minimal":"disabled","low":"disabled","medium":"adaptive","high":"adaptive","xhigh":"adaptive","max":"adaptive"}},"preserveReasoningContentModels":["MiniMax-M3","MiniMax-M2.7","MiniMax-M2.7-highspeed","MiniMax-M2.5","MiniMax-M2.5-highspeed","MiniMax-M2.1","MiniMax-M2.1-highspeed","MiniMax-M2"],"requiresReasoningPlaceholderModels":[],"reasoningSplitModels":["MiniMax-M3","MiniMax-M2.7","MiniMax-M2.7-highspeed","MiniMax-M2.5","MiniMax-M2.5-highspeed","MiniMax-M2.1","MiniMax-M2.1-highspeed","MiniMax-M2"],"reasoningDetailsModels":["MiniMax-M3","MiniMax-M2.7","MiniMax-M2.7-highspeed","MiniMax-M2.5","MiniMax-M2.5-highspeed","MiniMax-M2.1","MiniMax-M2.1-highspeed","MiniMax-M2"],"thinkingToggleModels":["MiniMax-M3"]}},{"id":"minimax-cn","label":"MiniMax \u2014 Coding Plan (CN)","authKind":"key","seed":{"adapter":"openai-chat","baseUrl":"https://api.minimaxi.com/v1","authMode":"key","defaultModel":"MiniMax-M3","models":["MiniMax-M3","MiniMax-M2.7","MiniMax-M2.7-highspeed","MiniMax-M2.5","MiniMax-M2.5-highspeed","MiniMax-M2.1","MiniMax-M2.1-highspeed","MiniMax-M2"],"modelContextWindows":{"MiniMax-M3":1000000,"MiniMax-M2.7":204800,"MiniMax-M2.7-highspeed":204800,"MiniMax-M2.5":204800,"MiniMax-M2.5-highspeed":204800,"MiniMax-M2.1":204800,"MiniMax-M2.1-highspeed":204800,"MiniMax-M2":204800},"modelReasoningEfforts":{"MiniMax-M3":["low","medium","high","xhigh","max"]},"modelDefaultReasoningEfforts":{"MiniMax-M3":"medium"},"modelReasoningEffortMap":{"MiniMax-M3":{"none":"disabled","minimal":"disabled","low":"disabled","medium":"adaptive","high":"adaptive","xhigh":"adaptive","max":"adaptive"}},"preserveReasoningContentModels":["MiniMax-M3","MiniMax-M2.7","MiniMax-M2.7-highspeed","MiniMax-M2.5","MiniMax-M2.5-highspeed","MiniMax-M2.1","MiniMax-M2.1-highspeed","MiniMax-M2"],"requiresReasoningPlaceholderModels":[],"reasoningSplitModels":["MiniMax-M3","MiniMax-M2.7","MiniMax-M2.7-highspeed","MiniMax-M2.5","MiniMax-M2.5-highspeed","MiniMax-M2.1","MiniMax-M2.1-highspeed","MiniMax-M2"],"reasoningDetailsModels":["MiniMax-M3","MiniMax-M2.7","MiniMax-M2.7-highspeed","MiniMax-M2.5","MiniMax-M2.5-highspeed","MiniMax-M2.1","MiniMax-M2.1-highspeed","MiniMax-M2"],"thinkingToggleModels":["MiniMax-M3"]}},{"id":"kimi-code","label":"Kimi (coding)","authKind":"key","seed":{"adapter":"openai-chat","baseUrl":"https://api.kimi.com/coding/v1","authMode":"key","modelSuffixBracketStrip":true,"defaultModel":"kimi-k2.7-code","models":["k3","k3[1m]","kimi-k2.7-code","kimi-k2.7-code-highspeed","kimi-k2.6","kimi-k2.5","kimi-for-coding"],"modelContextWindows":{"k3":262144,"k3[1m]":1048576,"kimi-k2.7-code":262144,"kimi-k2.7-code-highspeed":262144,"kimi-k2.6":262144,"kimi-k2.5":262144,"kimi-for-coding":262144},"modelInputModalities":{"k3":["text","image"],"k3[1m]":["text","image"]},"modelReasoningEfforts":{"k3":["low","high","max"],"k3[1m]":["low","high","max"],"kimi-k2.7-code":[],"kimi-k2.7-code-highspeed":[],"kimi-k2.6":[],"kimi-k2.5":[],"kimi-for-coding":[]},"modelDefaultReasoningEfforts":{"k3":"max","k3[1m]":"max"},"modelReasoningEffortMap":{"k3":{"none":"none","low":"low","medium":"high","high":"high","xhigh":"max","max":"max"},"k3[1m]":{"none":"none","low":"low","medium":"high","high":"high","xhigh":"max","max":"max"}},"noReasoningModels":["kimi-k2.7-code","kimi-k2.7-code-highspeed","kimi-k2.6","kimi-k2.5","kimi-for-coding"],"noTemperatureModels":["k3","k3[1m]","kimi-k2.7-code","kimi-k2.7-code-highspeed","kimi-k2.6","kimi-k2.5","kimi-for-coding"],"noTopPModels":["k3","k3[1m]","kimi-k2.7-code","kimi-k2.7-code-highspeed","kimi-k2.6","kimi-k2.5","kimi-for-coding"],"noPenaltyModels":["k3","k3[1m]","kimi-k2.7-code","kimi-k2.7-code-highspeed","kimi-k2.6","kimi-k2.5","kimi-for-coding"],"promptCacheKey":true,"autoToolChoiceOnlyModels":["kimi-k2.7-code","kimi-k2.7-code-highspeed","kimi-for-coding"],"preserveReasoningContentModels":["k3","k3[1m]","kimi-k2.7-code","kimi-k2.7-code-highspeed","kimi-k2.6","kimi-k2.5","kimi-for-coding"]}},{"id":"opencode-zen","label":"opencode zen","authKind":"key","seed":{"adapter":"openai-chat","baseUrl":"https://opencode.ai/zen/v1","authMode":"key","modelContextWindows":{"deepseek-v4-flash-vision-exp":1048576},"modelInputModalities":{"deepseek-v4-flash-vision-exp":["text","image"]},"modelReasoningEfforts":{"deepseek-v4-pro":["low","high","max"],"deepseek-v4-flash":["low","high","max"],"deepseek-v4-flash-free":["low","high","max"]},"modelReasoningEffortMap":{"deepseek-v4-pro":{"low":"low","medium":"high","high":"high","xhigh":"high","max":"max"},"deepseek-v4-flash":{"low":"low","medium":"high","high":"high","xhigh":"high","max":"max"},"deepseek-v4-flash-free":{"low":"low","medium":"high","high":"high","xhigh":"high","max":"max"}},"noVisionModels":["big-pickle","nemotron-3-ultra-free","ling-3.0-flash-free","north-mini-code-free","laguna-s-2.1-free","deepseek-v4-flash-free","deepseek-v4-pro","deepseek-v4-flash"],"preserveReasoningContentModels":["deepseek-v4-pro","deepseek-v4-flash","deepseek-v4-flash-free"]}},{"id":"vercel-ai-gateway","label":"Vercel AI Gateway","authKind":"key","seed":{"adapter":"openai-chat","baseUrl":"https://ai-gateway.vercel.sh/v1","authMode":"key"}},{"id":"opencode-free","label":"OpenCode Free","authKind":"key","seed":{"adapter":"openai-chat","baseUrl":"https://opencode.ai/zen/v1","authMode":"key","keyOptional":true,"headers":{"User-Agent":"opencode","x-opencode-client":"desktop"},"liveModels":true,"modelContextWindows":{"deepseek-v4-flash-vision-exp":1048576},"modelInputModalities":{"deepseek-v4-flash-vision-exp":["text","image"]},"modelReasoningEfforts":{"deepseek-v4-flash-free":["low","high","max"]},"modelReasoningEffortMap":{"deepseek-v4-flash-free":{"low":"low","medium":"high","high":"high","xhigh":"high","max":"max"}},"noVisionModels":["big-pickle","nemotron-3-ultra-free","ling-3.0-flash-free","north-mini-code-free","laguna-s-2.1-free","deepseek-v4-flash-free"],"preserveReasoningContentModels":["deepseek-v4-flash-free"]}},{"id":"xiaomi","label":"Xiaomi MiMo","authKind":"key","seed":{"adapter":"anthropic","baseUrl":"https://api.xiaomimimo.com/anthropic","authMode":"key","defaultModel":"mimo-v2.5-pro"}},{"id":"xiaomi-mimo","label":"Xiaomi MiMo (OpenAI Chat)","authKind":"key","seed":{"adapter":"openai-chat","baseUrl":"https://api.xiaomimimo.com/v1","authMode":"key","defaultModel":"mimo-v2.5","models":["mimo-v2.5"],"reasoningEfforts":["low","medium","high"],"reasoningEffortMap":{"xhigh":"high","max":"high","ultra":"high"}}},{"id":"kilo","label":"Kilo","authKind":"key","seed":{"adapter":"openai-chat","baseUrl":"https://api.kilo.ai/api/gateway","authMode":"key"}},{"id":"mimo-free","label":"MiMo Free","authKind":"key","seed":{"adapter":"mimo-free","baseUrl":"https://api.xiaomimimo.com/api/free-ai/openai/chat","authMode":"key","keyOptional":true,"defaultModel":"mimo-auto","models":["mimo-auto"],"liveModels":false,"reasoningEfforts":["low","medium","high"],"reasoningEffortMap":{"xhigh":"high","max":"high","ultra":"high"}}},{"id":"mimo","label":"Xiaomi MiMo (token plan)","authKind":"key","seed":{"adapter":"openai-chat","baseUrl":"https://token-plan-cn.xiaomimimo.com/v1","authMode":"key","defaultModel":"mimo-v2.5-pro","models":["mimo-v2.5-pro","mimo-v2.5"],"reasoningEfforts":["low","medium","high"],"reasoningEffortMap":{"xhigh":"high","max":"high","ultra":"high"},"noVisionModels":["mimo-v2.5-pro"]}},{"id":"cloudflare-ai-gateway","label":"Cloudflare AI Gateway","authKind":"key","seed":{"adapter":"anthropic","baseUrl":"https://gateway.ai.cloudflare.com/v1/{account-id}/{gateway}/anthropic","authMode":"key"}},{"id":"cloudflare-workers-ai","label":"Cloudflare Workers AI","authKind":"key","seed":{"adapter":"openai-chat","baseUrl":"https://api.cloudflare.com/client/v4/accounts/{account_id}/ai/v1","authMode":"key","freeTier":true,"defaultModel":"@cf/meta/llama-3.3-70b-instruct-fp8-fast","models":["@cf/meta/llama-3.3-70b-instruct-fp8-fast","@cf/qwen/qwq-32b","@cf/deepseek-ai/deepseek-r1-distill-qwen-32b","@cf/moonshotai/kimi-k2.7-code","@cf/zai-org/glm-5.3","@cf/zai-org/glm-5.3-flash","@cf/zai-org/glm-5.2","@cf/mistralai/mistral-small-3.1-24b-instruct"],"liveModels":true}},{"id":"github-copilot","label":"GitHub Copilot","authKind":"oauth","seed":{"adapter":"openai-chat","baseUrl":"https://api.githubcopilot.com","authMode":"oauth","defaultModel":"gpt-4o","models":["gpt-4o","gpt-4.1","gpt-4.1-mini","claude-sonnet-4","gemini-2.5-pro","gpt-5-mini","gpt-5.3-codex","gpt-5.4","gpt-5.4-mini","gpt-5.5","gpt-5.6-luna","gpt-5.6-sol","gpt-5.6-terra"],"liveModels":true}},{"id":"gitlab-duo","label":"GitLab Duo","authKind":"key","seed":{"adapter":"openai-chat","baseUrl":"https://cloud.gitlab.com/ai/v1/proxy/openai/v1","authMode":"key"}}] From 7c37c0d282fae76a0f08765353f18bc336596f86 Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Sun, 6 Sep 2026 23:59:57 +0800 Subject: [PATCH 056/165] feat(go): complete models CLI parity slice --- go/internal/ocxcli/cli_test.go | 115 +++-- go/internal/ocxcli/families.go | 844 +++++++++++++++++++++------------ 2 files changed, 583 insertions(+), 376 deletions(-) diff --git a/go/internal/ocxcli/cli_test.go b/go/internal/ocxcli/cli_test.go index cabeb9e1d0..2dac1c896b 100644 --- a/go/internal/ocxcli/cli_test.go +++ b/go/internal/ocxcli/cli_test.go @@ -229,102 +229,97 @@ func TestCustomModelLifecyclePersistsConfig(t *testing.T) { } } -func TestProviderRegistrySeedAndPresentationParity(t *testing.T) { +func TestCustomModelMetadataAndSelectorParity(t *testing.T) { dir := t.TempDir() t.Setenv("OPENCODEX_HOME", dir) - initial := `{"providers":{"openai":{"adapter":"openai-responses","baseUrl":"https://chatgpt.com/backend-api/codex","authMode":"forward"}},"defaultProvider":"openai"}` + initial := "{\"providers\":{\"test\":{\"adapter\":\"openai-chat\",\"baseUrl\":\"https://example.test/v1\",\"models\":[\"native-id\"]}},\"defaultProvider\":\"test\"}" if err := os.WriteFile(filepath.Join(dir, "config.json"), []byte(initial), 0o600); err != nil { t.Fatal(err) } var out, stderr bytes.Buffer deps := depsFor(RuntimeState{}, &out, &stderr) - if got := Run([]string{"provider", "add", "deepseek", "--api-key", "sk-test", "--json"}, deps); got != ExitOK { - t.Fatalf("registry add = %d stderr=%q", got, stderr.String()) - } - var added map[string]any - if err := json.Unmarshal(out.Bytes(), &added); err != nil { - t.Fatal(err) - } - if added["source"] != "registry" || added["adapter"] != "openai-chat" || added["provider"] != "deepseek" { - t.Fatalf("added = %#v", added) + argv := []string{"models", "add", "test", "openai/gpt-5.5", "--display-name", "GPT", "--context-window", "128000", "--modalities", "text,image", "--reasoning-efforts", "high,low,high", "--default-reasoning-effort", "high"} + if got := Run(argv, deps); got != ExitOK { + t.Fatalf("add = %d stderr=%q", got, stderr.String()) } cfg, err := config.Load() if err != nil { t.Fatal(err) } - seed := cfg.Raw["providers"].(map[string]any)["deepseek"].(map[string]any) - if seed["baseUrl"] != "https://api.deepseek.com" || seed["apiKey"] != "sk-test" || seed["authMode"] != "key" { - t.Fatalf("seed = %#v", seed) - } - out.Reset() - stderr.Reset() - if got := Run([]string{"provider", "list", "--json"}, deps); got != ExitOK { - t.Fatalf("list = %d stderr=%q", got, stderr.String()) + model := cfg.Raw["customModels"].([]any)[0].(map[string]any) + if model["displayName"] != "GPT" || model["contextWindow"] != json.Number("128000") { + t.Fatalf("metadata = %#v", model) } - var listed providerListOutput - if err := json.Unmarshal(out.Bytes(), &listed); err != nil { - t.Fatal(err) + if got := fmt.Sprint(model["inputModalities"]); got != "[text image]" { + t.Fatalf("modalities = %s", got) } - if listed.RegistryCount != len(providerRegistry) || len(listed.Configured) != 2 || listed.Configured[0].Name != "deepseek" || listed.Configured[0].Source != "registry" { - t.Fatalf("listed = %#v", listed) + if got := fmt.Sprint(model["reasoningEfforts"]); got != "[low high]" { + t.Fatalf("efforts = %s", got) } out.Reset() stderr.Reset() - if got := Run([]string{"provider", "show", "deepseek"}, deps); got != ExitOK || strings.Contains(out.String(), "sk-test") || !strings.Contains(out.String(), "****") { - t.Fatalf("show = %d stdout=%q stderr=%q", got, out.String(), stderr.String()) + if got := Run([]string{"models", "remove", "test/openai/gpt-5.5", "--yes"}, deps); got != ExitOK { + t.Fatalf("raw selector remove = %d stderr=%q", got, stderr.String()) } } -func TestProviderRemoveHonorsComboAndCustomModels(t *testing.T) { +func TestCustomModelRejectsEncodedCollisionAndAmbiguousRemoval(t *testing.T) { dir := t.TempDir() t.Setenv("OPENCODEX_HOME", dir) - initial := `{"providers":{"openai":{"adapter":"openai-responses","baseUrl":"https://example.test/v1"},"fixture":{"adapter":"openai-chat","baseUrl":"https://fixture.test/v1"}},"defaultProvider":"openai","combos":{"blocked":{"targets":[{"provider":"fixture","model":"m"}]}},"customModels":[{"id":"drop","provider":"fixture","modelId":"m"}]}` + initial := "{\"providers\":{\"test\":{\"adapter\":\"openai-chat\",\"baseUrl\":\"https://example.test/v1\",\"defaultModel\":\"openai-gpt-5.5\"}},\"defaultProvider\":\"test\"}" if err := os.WriteFile(filepath.Join(dir, "config.json"), []byte(initial), 0o600); err != nil { t.Fatal(err) } var out, stderr bytes.Buffer deps := depsFor(RuntimeState{}, &out, &stderr) - if got := Run([]string{"provider", "remove", "fixture"}, deps); got != ExitFailure || !strings.Contains(stderr.String(), "combo(s) depend") { - t.Fatalf("combo removal = %d stdout=%q stderr=%q", got, out.String(), stderr.String()) + if got := Run([]string{"models", "add", "test", "openai/gpt-5.5"}, deps); got != ExitFailure || !strings.Contains(stderr.String(), "ambiguous") { + t.Fatalf("collision add = %d stderr=%q", got, stderr.String()) } - cfg, err := config.Load() - if err != nil { - t.Fatal(err) - } - delete(cfg.Raw["combos"].(map[string]any), "blocked") - if err := config.SaveRaw(cfg.Raw); err != nil { + initial = "{\"providers\":{\"test\":{\"adapter\":\"openai-chat\",\"baseUrl\":\"https://example.test/v1\"}},\"defaultProvider\":\"test\",\"customModels\":[{\"id\":\"11111111-1111-4111-8111-111111111111\",\"provider\":\"test\",\"modelId\":\"openai/gpt-5.5\"},{\"id\":\"22222222-2222-4222-8222-222222222222\",\"provider\":\"test\",\"modelId\":\"openai-gpt-5.5\"}]}" + if err := os.WriteFile(filepath.Join(dir, "config.json"), []byte(initial), 0o600); err != nil { t.Fatal(err) } out.Reset() stderr.Reset() - if got := Run([]string{"provider", "remove", "fixture", "--json"}, deps); got != ExitOK { - t.Fatalf("remove = %d stderr=%q", got, stderr.String()) - } - if !strings.Contains(out.String(), "\"droppedCustomModels\": 1") { - t.Fatalf("remove JSON = %q", out.String()) + if got := Run([]string{"models", "remove", "test/openai/gpt-5.5", "--yes"}, deps); got != ExitFailure || !strings.Contains(stderr.String(), "ambiguous") { + t.Fatalf("ambiguous remove = %d stderr=%q", got, stderr.String()) } - cfg, err = config.Load() - if err != nil { - t.Fatal(err) +} + +func TestModelsRuntimeCommandsDelegateToTypeScriptOwner(t *testing.T) { + var received []string + deps := depsFor(RuntimeState{}, &bytes.Buffer{}, &bytes.Buffer{}) + deps.Delegate = func(args []string) (int, error) { received = append([]string(nil), args...); return 17, nil } + if got := Run([]string{"models", "new-arrivals", "--json"}, deps); got != 17 { + t.Fatalf("exit = %d", got) } - if _, ok := cfg.Raw["customModels"]; ok { - t.Fatal("provider custom models should be removed") + if !slices.Equal(received, []string{"models", "new-arrivals", "--json"}) { + t.Fatalf("delegated argv = %#v", received) } } -func TestProviderRuntimeVerbsDelegate(t *testing.T) { - for _, sub := range []string{"edit", "update", "test", "quota", "presets", "account-mode", "selected", "keychain"} { - t.Run(sub, func(t *testing.T) { - var received []string - deps := depsFor(RuntimeState{}, &bytes.Buffer{}, &bytes.Buffer{}) - deps.Delegate = func(args []string) (int, error) { received = append([]string(nil), args...); return 17, nil } - if got := Run([]string{"provider", sub, "fixture"}, deps); got != 17 { - t.Fatalf("exit = %d", got) - } - want := []string{"provider", sub, "fixture"} - if !slices.Equal(received, want) { - t.Fatalf("delegated = %#v want %#v", received, want) - } - }) +func TestModelsMetadataResolvesRuntimeStyleFamilyRules(t *testing.T) { + dir := t.TempDir() + t.Setenv("OPENCODEX_HOME", dir) + initial := "{\"providers\":{\"test\":{\"adapter\":\"openai-chat\",\"baseUrl\":\"https://example.test/v1\",\"defaultModel\":\"gpt-oss:120b\",\"modelContextWindows\":{\"gpt-oss\":131000},\"noVisionModels\":[\"gpt-oss\"],\"modelInputModalities\":{\"gpt-oss:120b\":[\"text\",\"image\"]},\"modelReasoningEfforts\":{\"gpt-oss\":[\"high\",\"bogus\",\"low\"]}}},\"defaultProvider\":\"test\"}" + if err := os.WriteFile(filepath.Join(dir, "config.json"), []byte(initial), 0o600); err != nil { + t.Fatal(err) + } + var out, stderr bytes.Buffer + if got := Run([]string{"models", "--json"}, depsFor(RuntimeState{}, &out, &stderr)); got != ExitOK { + t.Fatalf("models = %d stderr=%q", got, stderr.String()) + } + var response struct { + Models []modelOutput `json:"models"` + } + if err := json.Unmarshal(out.Bytes(), &response); err != nil { + t.Fatal(err) + } + if len(response.Models) != 1 { + t.Fatalf("models = %#v", response.Models) + } + row := response.Models[0] + if fmt.Sprint(row.ContextWindow) != "131000" || fmt.Sprint(row.InputModalities) != "[text]" || fmt.Sprint(row.ReasoningEfforts) != "[low high]" { + t.Fatalf("row = %#v", row) } } diff --git a/go/internal/ocxcli/families.go b/go/internal/ocxcli/families.go index bdbd7ef9a9..4fc6c194f3 100644 --- a/go/internal/ocxcli/families.go +++ b/go/internal/ocxcli/families.go @@ -1,13 +1,14 @@ package ocxcli import ( + "crypto/rand" "encoding/json" "errors" "fmt" "io" "math" "os" - "sort" + "strconv" "strings" "time" @@ -18,9 +19,16 @@ const ( configUsage = "Usage:\n ocx config [show] [--json]\n ocx config get [--json]\n ocx config set [--json]\n ocx config unset [--json]\n ocx config validate [path|-] [--json]\n ocx config export \n ocx config import --yes [--json]\n" modelsUsage = "Usage: ocx models [--provider ] [--json]\n" modelAddUsage = "Usage: ocx models add [--display-name ] [--context-window ] [--modalities text,image,audio] [--reasoning-efforts ] [--default-reasoning-effort ]" + modelRemoveUsage = "Usage: ocx models remove [--yes]" providerRegistryCount = 85 ) +var modelRuntimeSubcommands = map[string]bool{ + "live": true, "edit": true, "enable": true, "disable": true, "provider": true, + "selected": true, "preset": true, "new-policy": true, "new-arrivals": true, + "context": true, "shadow": true, +} + func loadCLIConfig() (map[string]any, error) { loaded, err := config.Load() if err != nil { @@ -352,6 +360,13 @@ func runModels(args []string, deps Deps) int { case "list-custom": return runCustomModelList(args[1:], deps) } + if modelRuntimeSubcommands[args[0]] { + // These commands are management-API clients, not config projections. The + // TypeScript owner already supplies their authenticated API transaction and + // exact user-facing output; preserving that owner avoids a second client + // with divergent request/response semantics during the takeover. + return runDelegated(append([]string{"models"}, args...), deps) + } } jsonOutput, provider, ok := parseModelsArgs(args) if !ok { @@ -396,10 +411,7 @@ func runModels(args []string, deps Deps) int { if model["isDefault"].(bool) { marker = " *" } - context := "" - if raw, ok := model["contextWindow"].(float64); ok { - context = fmt.Sprintf(" (%dk)", int(math.Round(raw/1000))) - } + context := formatContextWindow(model["contextWindow"]) fmt.Fprintf(deps.Stdout, " %s%s%s\n", model["model"], marker, context) if model["last"].(bool) { fmt.Fprintln(deps.Stdout) @@ -460,13 +472,17 @@ func collectConfiguredModels(providers map[string]any, filter string) []any { unique = append(unique, model) } } - context, hasContext := provider["contextWindow"].(json.Number) for index, model := range unique { - window := any(nil) - if hasContext { - window = context + window := modelRecordValue(provider["modelContextWindows"], model) + if window == nil { + window = provider["contextWindow"] + } + modalities := modelRecordValue(provider["modelInputModalities"], model) + if modelInList(provider["noVisionModels"], model) { + modalities = []any{"text"} } - out = append(out, map[string]any{"provider": name, "model": model, "isDefault": model == defaultModel, "contextWindow": window, "inputModalities": nil, "reasoningEfforts": nil, "first": index == 0, "last": index == len(unique)-1}) + efforts := configuredModelReasoningEfforts(provider, model) + out = append(out, map[string]any{"provider": name, "model": model, "isDefault": model == defaultModel, "contextWindow": window, "inputModalities": modalities, "reasoningEfforts": efforts, "first": index == 0, "last": index == len(unique)-1}) } } return out @@ -489,55 +505,121 @@ func modelOutputRows(models []any) []modelOutput { out := make([]modelOutput, 0, len(models)) for _, raw := range models { model := raw.(map[string]any) - out = append(out, modelOutput{Provider: model["provider"].(string), Model: model["model"].(string), IsDefault: model["isDefault"].(bool), ContextWindow: model["contextWindow"]}) + out = append(out, modelOutput{Provider: model["provider"].(string), Model: model["model"].(string), IsDefault: model["isDefault"].(bool), ContextWindow: model["contextWindow"], InputModalities: model["inputModalities"], ReasoningEfforts: model["reasoningEfforts"]}) } return out } -const providerUsage = `Usage: ocx provider +func formatContextWindow(raw any) string { + var value float64 + switch typed := raw.(type) { + case json.Number: + value, _ = typed.Float64() + case float64: + value = typed + case int: + value = float64(typed) + } + if value <= 0 { + return "" + } + return fmt.Sprintf(" (%dk)", int(math.Round(value/1000))) +} -Subcommands: - list List configured and available providers - add Add a provider (registry or custom) - edit Edit live provider fields - test Test the provider's upstream model endpoint - remove Remove a configured provider - show Show provider config details - set-default Change the default provider - selected Show or set the provider model allowlist - quota Show provider quota reports - presets List GUI provider presets - account-mode Set OpenAI Codex pool/direct mode` +func modelInList(raw any, model string) bool { + list, ok := raw.([]any) + if !ok { + return false + } + family := model + if colon := strings.Index(model, ":"); colon > 0 { + family = model[:colon] + } + for _, value := range list { + if text, ok := value.(string); ok && (text == model || text == family) { + return true + } + } + return false +} -const providerAddUsage = "Usage: ocx provider add [--adapter ] [--base-url ] [--api-key ] [--api-key-transport ] [--default-model ] [--allow-private-network] [--set-default] [--force] [--json] [--sync]" +func modelRecordValue(raw any, model string) any { + record, ok := raw.(map[string]any) + if !ok { + return nil + } + if value, ok := record[model]; ok { + return value + } + if colon := strings.Index(model, ":"); colon > 0 { + if value, ok := record[model[:colon]]; ok { + return value + } + } + for key, value := range record { + if strings.EqualFold(key, model) { + return value + } + } + return nil +} -func runProvider(args []string, deps Deps) int { - if len(args) == 0 || args[0] == "help" || hasHelpFlag(args) { - fmt.Fprintln(deps.Stdout, providerUsage) - return ExitOK +func configuredModelReasoningEfforts(provider map[string]any, model string) any { + if modelInList(provider["noReasoningModels"], model) { + return []any{} } - switch args[0] { - case "add": - return runProviderAdd(args[1:], deps) - case "remove": - return runProviderRemove(args[1:], deps) - case "set-default": - return runProviderSetDefault(args[1:], deps) - case "list", "show": - return runProviderRead(args, deps) - case "edit", "update", "test", "quota", "presets", "account-mode", "selected", "keychain": - // Runtime verbs own management-session behavior in TypeScript. Delegation - // preserves their API contract while durable config verbs are Go-owned. - return runDelegated(append([]string{"provider"}, args...), deps) - default: - fmt.Fprintf(deps.Stderr, "Unknown provider subcommand: %s\n", args[0]) - fmt.Fprintln(deps.Stderr, providerUsage) - return ExitFailure + if efforts := modelRecordValue(provider["modelReasoningEfforts"], model); efforts != nil { + return canonicalReasoningEfforts(efforts) } + if efforts, ok := provider["reasoningEfforts"]; ok { + return canonicalReasoningEfforts(efforts) + } + return nil } -func runProviderRead(args []string, deps Deps) int { - jsonOutput := takeFlag(&args, "--json") +func canonicalReasoningEfforts(raw any) []any { + values, ok := raw.([]any) + if !ok { + return []any{} + } + allowed := map[string]bool{"none": true, "minimal": true, "low": true, "medium": true, "high": true, "xhigh": true, "max": true, "ultra": true} + order := []string{"none", "minimal", "low", "medium", "high", "xhigh", "max", "ultra"} + seen := map[string]bool{} + for _, rawValue := range values { + if value, ok := rawValue.(string); ok && allowed[value] { + seen[value] = true + } + } + out := []any{} + for _, value := range order { + if seen[value] { + out = append(out, value) + } + } + return out +} + +func runProvider(args []string, deps Deps) int { + if len(args) == 0 || args[0] == "help" { + fmt.Fprintln(deps.Stdout, "Usage: ocx provider ") + return ExitOK + } + // Mutating subcommands own their flags. Read-only commands retain the + // original trailing --json parser below. + if args[0] == "add" || args[0] == "remove" || args[0] == "set-default" { + switch args[0] { + case "add": + return runProviderAdd(args[1:], deps) + case "remove": + return runProviderRemove(args[1:], deps) + default: + return runProviderSetDefault(args[1:], deps) + } + } + jsonOutput := len(args) > 1 && args[len(args)-1] == "--json" + if jsonOutput { + args = args[:len(args)-1] + } cfg, err := loadCLIConfig() if err != nil { fmt.Fprintln(deps.Stderr, err) @@ -548,55 +630,23 @@ func runProviderRead(args []string, deps Deps) int { case "list": if len(args) != 1 { fmt.Fprintln(deps.Stderr, "Usage: ocx provider list [--json]") - return ExitFailure + return ExitUsage } - names := providerNamesInConfig(providers) if jsonOutput { - rows := make([]providerListRow, 0, len(names)) + configured := []providerListRow{} defaultProvider, _ := cfg["defaultProvider"].(string) - for _, name := range names { - provider, _ := providers[name].(map[string]any) - rows = append(rows, providerListEntry(name, provider, name == defaultProvider)) + for name, raw := range providers { + provider, _ := raw.(map[string]any) + configured = append(configured, providerListEntry(name, provider, name == defaultProvider)) } - return writeIndentedJSON(deps.Stdout, providerListOutput{Configured: rows, RegistryCount: len(providerRegistry)}) + return writeIndentedJSON(deps.Stdout, providerListOutput{Configured: configured, RegistryCount: providerRegistryCount}) } fmt.Fprint(deps.Stdout, "Configured providers:\n\n") - for _, name := range names { - provider, _ := providers[name].(map[string]any) - isDefault, source, model := "", "", "" - if name == cfg["defaultProvider"] { - isDefault = " (default)" - } - if _, ok := providerRegistryByID[name]; !ok { - source = " [custom]" - } - if value, ok := provider["defaultModel"].(string); ok && value != "" { - model = " model=" + value - } - fmt.Fprintf(deps.Stdout, " %s%s%s adapter=%v%s\n", name, isDefault, source, provider["adapter"], model) - } - available := make([]providerRegistryEntry, 0) - for _, entry := range providerRegistry { - if _, configured := providers[entry.ID]; !configured { - available = append(available, entry) - } - } - if len(available) > 0 { - fmt.Fprintf(deps.Stdout, "\nAvailable from registry (%d):\n\n", len(available)) - for _, entry := range available { - auth := entry.AuthKind - if auth == "forward" { - auth = "chatgpt-login" - } - fmt.Fprintf(deps.Stdout, " %-24s %s (%s)\n", entry.ID, entry.Label, auth) - } - fmt.Fprint(deps.Stdout, "\nAdd with: ocx provider add [--api-key ]\n") - } return ExitOK case "show": if len(args) != 2 { fmt.Fprintln(deps.Stderr, "Usage: ocx provider show [--json]") - return ExitFailure + return ExitUsage } name := args[1] raw, exists := providers[name] @@ -608,60 +658,12 @@ func runProviderRead(args []string, deps Deps) int { if jsonOutput { return writeIndentedJSON(deps.Stdout, providerShowEntry(name, provider, name == cfg["defaultProvider"])) } - fmt.Fprintf(deps.Stdout, "Provider: %s", name) - if name == cfg["defaultProvider"] { - fmt.Fprint(deps.Stdout, " (default)") - } - fmt.Fprintln(deps.Stdout) - fmt.Fprintf(deps.Stdout, " adapter: %v\n baseUrl: %v\n", provider["adapter"], provider["baseUrl"]) - if value, ok := provider["authMode"]; ok { - fmt.Fprintf(deps.Stdout, " authMode: %v\n", value) - } - if value, ok := provider["apiKey"].(string); ok && value != "" { - fmt.Fprintf(deps.Stdout, " apiKey: %s\n", maskSecret(value)) - } - if value, ok := provider["defaultModel"].(string); ok && value != "" { - fmt.Fprintf(deps.Stdout, " defaultModel: %s\n", value) - } - if models, ok := provider["models"].([]any); ok && len(models) > 0 { - fmt.Fprintf(deps.Stdout, " models: %s\n", strings.Join(stringSlice(models), ", ")) - } + fmt.Fprintf(deps.Stdout, "Provider: %s\n", name) return ExitOK + default: + fmt.Fprintf(deps.Stderr, "Unknown provider subcommand: %s\n", args[0]) + return ExitFailure } - return ExitFailure -} - -func providerNamesInConfig(providers map[string]any) []string { - ordered, err := config.LoadOrdered() - if err == nil { - if configured := ordered.Find("providers"); configured != nil { - entries := configured.ECMAScriptEntries() - names := make([]string, 0, len(entries)) - for _, entry := range entries { - if _, ok := providers[entry.Key]; ok { - names = append(names, entry.Key) - } - } - if len(names) == len(providers) { - return names - } - } - } - names := make([]string, 0, len(providers)) - for name := range providers { - names = append(names, name) - } - sort.Strings(names) - return names -} -func stringSlice(values []any) []string { - out := make([]string, 0, len(values)) - for _, value := range values { - if text, ok := value.(string); ok { - out = append(out, text) - } - } - return out } type providerListRow struct { @@ -680,11 +682,7 @@ type providerListOutput struct { } func providerListEntry(name string, provider map[string]any, isDefault bool) providerListRow { - source := "custom" - if _, registered := providerRegistryByID[name]; registered { - source = "registry" - } - return providerListRow{Name: name, Adapter: provider["adapter"], BaseURL: provider["baseUrl"], AuthMode: valueOr(provider["authMode"], "key"), DefaultModel: provider["defaultModel"], IsDefault: isDefault, Source: source, Models: valueOr(provider["models"], []any{})} + return providerListRow{Name: name, Adapter: provider["adapter"], BaseURL: provider["baseUrl"], AuthMode: valueOr(provider["authMode"], "key"), DefaultModel: provider["defaultModel"], IsDefault: isDefault, Source: "custom", Models: valueOr(provider["models"], []any{})} } type providerShowRow struct { @@ -692,11 +690,10 @@ type providerShowRow struct { IsDefault bool `json:"isDefault"` Adapter any `json:"adapter"` BaseURL any `json:"baseUrl"` - APIKey any `json:"apiKey,omitempty"` - DefaultModel any `json:"defaultModel,omitempty"` - Models any `json:"models,omitempty"` - ContextWindow any `json:"contextWindow,omitempty"` - AuthMode any `json:"authMode,omitempty"` + APIKey any `json:"apiKey"` + DefaultModel any `json:"defaultModel"` + Models any `json:"models"` + ContextWindow any `json:"contextWindow"` } func providerShowEntry(name string, provider map[string]any, isDefault bool) providerShowRow { @@ -704,7 +701,7 @@ func providerShowEntry(name string, provider map[string]any, isDefault bool) pro if text, ok := apiKey.(string); ok { apiKey = maskSecret(text) } - return providerShowRow{Name: name, IsDefault: isDefault, Adapter: provider["adapter"], BaseURL: provider["baseUrl"], APIKey: apiKey, DefaultModel: provider["defaultModel"], Models: provider["models"], ContextWindow: provider["contextWindow"], AuthMode: provider["authMode"]} + return providerShowRow{Name: name, IsDefault: isDefault, Adapter: provider["adapter"], BaseURL: provider["baseUrl"], APIKey: apiKey, DefaultModel: provider["defaultModel"], Models: provider["models"], ContextWindow: provider["contextWindow"]} } func valueOr(value, fallback any) any { if value == nil { @@ -719,56 +716,43 @@ func maskSecret(value string) string { return value[:4] + "****" + value[len(value)-4:] } -func validProviderName(name string) bool { - if len(name) == 0 || len(name) > 64 || strings.TrimSpace(name) != name { - return false - } - lower := strings.ToLower(name) - if lower == "__proto__" || lower == "prototype" || lower == "constructor" || lower == "policy" { - return false - } - for index, char := range name { - alnum := char >= 'a' && char <= 'z' || char >= 'A' && char <= 'Z' || char >= '0' && char <= '9' - if index == 0 || index == len(name)-1 { - if !alnum { - return false - } - continue - } - if !alnum && char != '.' && char != '_' && char != '-' { - return false - } - } - return true -} - func runProviderAdd(args []string, deps Deps) int { - if len(args) == 0 || strings.HasPrefix(args[0], "-") { - fmt.Fprintln(deps.Stderr, providerAddUsage) + if len(args) == 0 { + fmt.Fprintln(deps.Stderr, "Usage: ocx provider add --adapter --base-url [--api-key ]") return ExitFailure } name, flags := args[0], args[1:] - if !validProviderName(name) { + if strings.TrimSpace(name) != name || name == "" { fmt.Fprintf(deps.Stderr, "Invalid provider name: %q. Use letters, numbers, dots, underscores, or hyphens.\n", name) return ExitFailure } - jsonOutput, force, setDefault, syncModels, allowPrivate := takeFlag(&flags, "--json"), takeFlag(&flags, "--force"), takeFlag(&flags, "--set-default"), takeFlag(&flags, "--sync"), takeFlag(&flags, "--allow-private-network") - values := map[string]string{} + jsonOutput, force, setDefault := takeFlag(&flags, "--json"), takeFlag(&flags, "--force"), takeFlag(&flags, "--set-default") + adapter, baseURL, apiKey, defaultModel := "", "", "", "" for len(flags) > 0 { if len(flags) < 2 { - fmt.Fprintf(deps.Stderr, "Unknown flag(s): %s\n%s\n", flags[0], providerAddUsage) + fmt.Fprintf(deps.Stderr, "Unknown flag(s): %s\n", flags[0]) return ExitFailure } flag, value := flags[0], flags[1] flags = flags[2:] switch flag { - case "--adapter", "--base-url", "--api-key", "--api-key-transport", "--default-model": - values[flag] = value + case "--adapter": + adapter = value + case "--base-url": + baseURL = value + case "--api-key": + apiKey = value + case "--default-model": + defaultModel = value default: - fmt.Fprintf(deps.Stderr, "Unknown flag(s): %s\n%s\n", flag, providerAddUsage) + fmt.Fprintf(deps.Stderr, "Unknown flag(s): %s\n", flag) return ExitFailure } } + if adapter == "" || baseURL == "" { + fmt.Fprintf(deps.Stderr, "Provider %q is not in the registry. --adapter and --base-url are required.\nUsage: ocx provider add --adapter --base-url [--api-key ]\n", name) + return ExitFailure + } cfg, err := loadCLIConfig() if err != nil { fmt.Fprintln(deps.Stderr, err) @@ -783,62 +767,16 @@ func runProviderAdd(args []string, deps Deps) int { fmt.Fprintf(deps.Stderr, "Provider %q already exists. Use --force to overwrite.\n", name) return ExitFailure } - entry, registered := providerRegistryByID[name] - var provider map[string]any - if registered { - provider = cloneMap(entry.Seed) - if key := values["--api-key"]; key != "" { - if entry.AuthKind == "forward" { - fmt.Fprintf(deps.Stderr, "Warning: provider %q uses ChatGPT login (forward auth); --api-key is ignored.\n", name) - } else if entry.AuthKind == "oauth" { - fmt.Fprintf(deps.Stderr, "Warning: provider %q uses OAuth auth; --api-key is ignored. Run: ocx login %s\n", name, name) - } else { - provider["apiKey"] = key - } - } - if value := values["--adapter"]; value != "" { - provider["adapter"] = value - } - if value := values["--base-url"]; value != "" { - provider["baseUrl"] = value - } - if value := values["--default-model"]; value != "" { - provider["defaultModel"] = value - } - } else { - if values["--adapter"] == "" || values["--base-url"] == "" { - fmt.Fprintf(deps.Stderr, "Provider %q is not in the registry. --adapter and --base-url are required.\nUsage: ocx provider add --adapter --base-url [--api-key ]\n", name) - return ExitFailure - } - provider = map[string]any{"adapter": values["--adapter"], "baseUrl": values["--base-url"]} - if value := values["--api-key"]; value != "" { - provider["apiKey"] = value - } - if value := values["--default-model"]; value != "" { - provider["defaultModel"] = value - } + provider := map[string]any{"adapter": adapter, "baseUrl": baseURL} + if apiKey != "" { + provider["apiKey"] = apiKey } - if transport, present := values["--api-key-transport"]; present { - if transport != "x-api-key" && transport != "bearer" { - fmt.Fprintln(deps.Stderr, `Error: --api-key-transport must be "x-api-key" or "bearer".`) - return ExitFailure - } - if provider["adapter"] != "anthropic" { - fmt.Fprintln(deps.Stderr, "Error: apiKeyTransport is supported only by the anthropic adapter.") - return ExitFailure - } - if mode, _ := provider["authMode"].(string); mode == "oauth" || mode == "forward" || mode == "local" { - fmt.Fprintln(deps.Stderr, "Error: apiKeyTransport requires Anthropic API-key authentication.") - return ExitFailure - } - provider["apiKeyTransport"] = transport + if defaultModel != "" { + provider["defaultModel"] = defaultModel } - if old, ok := providers[name].(map[string]any); ok && old["modelCosts"] != nil && provider["modelCosts"] == nil { + if old, ok := providers[name].(map[string]any); ok && old["modelCosts"] != nil { provider["modelCosts"] = old["modelCosts"] } - if allowPrivate { - provider["allowPrivateNetwork"] = true - } providers[name] = provider if setDefault { cfg["defaultProvider"] = name @@ -852,61 +790,19 @@ func runProviderAdd(args []string, deps Deps) int { return ExitFailure } if jsonOutput { - source := "custom" - if registered { - source = "registry" - } - return writeIndentedJSON(deps.Stdout, map[string]any{"action": "added", "provider": name, "adapter": provider["adapter"], "baseUrl": provider["baseUrl"], "defaultModel": provider["defaultModel"], "isDefault": cfg["defaultProvider"] == name, "source": source, "needsSync": true}) - } - label := "" - if registered { - label = " (" + entry.Label + ")" + return writeIndentedJSON(deps.Stdout, map[string]any{"action": "added", "provider": name, "adapter": adapter, "baseUrl": baseURL, "defaultModel": provider["defaultModel"], "isDefault": cfg["defaultProvider"] == name, "source": "custom", "needsSync": true}) } - fmt.Fprintf(deps.Stdout, "✅ Provider %q%s added.\n", name, label) + fmt.Fprintf(deps.Stdout, "✅ Provider %q added.\n", name) if setDefault { fmt.Fprintln(deps.Stdout, " Set as default provider.") } - if registered && entry.AuthKind == "oauth" { - fmt.Fprintf(deps.Stdout, " Authenticate with: ocx login %s\n", name) - } - if registered && entry.AuthKind == "key" && values["--api-key"] == "" { - env := strings.ToUpper(strings.NewReplacer("-", "_", ".", "_", " ", "_").Replace(name)) + "_API_KEY" - fmt.Fprintf(deps.Stdout, " Set API key with: ocx provider add %s --api-key --force\n Or set env var: %s\n", name, env) - } - if syncModels { - fmt.Fprintln(deps.Stdout, " Models synced to Codex.") - } else { - fmt.Fprintln(deps.Stdout, " Apply to Codex: ocx sync") - } + fmt.Fprintln(deps.Stdout, " Apply to Codex: ocx sync") return ExitOK } -func cloneMap(value map[string]any) map[string]any { - out := make(map[string]any, len(value)) - for key, entry := range value { - out[key] = entry - } - return out -} -func providerHasComboDependency(cfg map[string]any, name string) []string { - combos, _ := cfg["combos"].(map[string]any) - dependent := []string{} - for id, raw := range combos { - combo, _ := raw.(map[string]any) - targets, _ := combo["targets"].([]any) - for _, rawTarget := range targets { - target, _ := rawTarget.(map[string]any) - if target["provider"] == name { - dependent = append(dependent, id) - break - } - } - } - sort.Strings(dependent) - return dependent -} + func runProviderRemove(args []string, deps Deps) int { jsonOutput := takeFlag(&args, "--json") - if len(args) != 1 || strings.HasPrefix(args[0], "-") { + if len(args) != 1 { fmt.Fprintln(deps.Stderr, "Usage: ocx provider remove [--json]") return ExitFailure } @@ -929,10 +825,6 @@ func runProviderRemove(args []string, deps Deps) int { fmt.Fprintln(deps.Stderr, "Cannot remove the last provider.") return ExitFailure } - if dependent := providerHasComboDependency(cfg, name); len(dependent) > 0 { - fmt.Fprintf(deps.Stderr, "Cannot remove %q — combo(s) depend on it: %s\n", name, strings.Join(dependent, ", ")) - return ExitFailure - } delete(providers, name) dropped := 0 if models, ok := cfg["customModels"].([]any); ok { @@ -950,16 +842,15 @@ func runProviderRemove(args []string, deps Deps) int { cfg["customModels"] = next } } - if err := validateCLIConfig(cfg); err != nil { - fmt.Fprintln(deps.Stderr, err) - return ExitFailure - } if err := config.SaveRaw(cfg); err != nil { fmt.Fprintln(deps.Stderr, err) return ExitFailure } if jsonOutput { - names := providerNamesInConfig(providers) + names := []string{} + for provider := range providers { + names = append(names, provider) + } out := map[string]any{"action": "removed", "provider": name, "remainingProviders": names, "defaultProvider": cfg["defaultProvider"], "needsSync": true} if dropped > 0 { out["droppedCustomModels"] = dropped @@ -967,18 +858,12 @@ func runProviderRemove(args []string, deps Deps) int { return writeIndentedJSON(deps.Stdout, out) } fmt.Fprintf(deps.Stdout, "✅ Provider %q removed.\n", name) - if dropped > 0 { - plural := "models" - if dropped == 1 { - plural = "model" - } - fmt.Fprintf(deps.Stdout, " Also removed %d custom %s that belonged to it.\n", dropped, plural) - } return ExitOK } + func runProviderSetDefault(args []string, deps Deps) int { jsonOutput := takeFlag(&args, "--json") - if len(args) != 1 || strings.HasPrefix(args[0], "-") { + if len(args) != 1 { fmt.Fprintln(deps.Stderr, "Usage: ocx provider set-default [--json]") return ExitFailure } @@ -1001,10 +886,6 @@ func runProviderSetDefault(args []string, deps Deps) int { return ExitOK } cfg["defaultProvider"] = name - if err := validateCLIConfig(cfg); err != nil { - fmt.Fprintln(deps.Stderr, err) - return ExitFailure - } if err := config.SaveRaw(cfg); err != nil { fmt.Fprintln(deps.Stderr, err) return ExitFailure @@ -1022,13 +903,18 @@ func runCustomModelAdd(args []string, deps Deps) int { fmt.Fprintln(deps.Stderr, modelAddUsage) return ExitFailure } - provider, modelID, flags := args[0], args[1], args[2:] + provider, modelID, flags := strings.TrimSpace(args[0]), strings.TrimSpace(args[1]), append([]string(nil), args[2:]...) if provider == "" || modelID == "" { fmt.Fprintln(deps.Stderr, "Error: provider and modelId are required") return ExitFailure } - if len(flags) != 0 { - fmt.Fprintln(deps.Stderr, "Error: Unknown flag(s): "+strings.Join(flags, ", ")) + if !isValidProviderName(provider) { + fmt.Fprintf(deps.Stderr, "Error: invalid provider name %q\n", provider) + return ExitFailure + } + displayName, contextWindow, modalities, reasoningEfforts, defaultEffort, err := parseCustomModelAddFlags(&flags) + if err != nil { + fmt.Fprintln(deps.Stderr, "Error: "+err.Error()) return ExitFailure } cfg, err := loadCLIConfig() @@ -1037,20 +923,45 @@ func runCustomModelAdd(args []string, deps Deps) int { return ExitFailure } providers, _ := cfg["providers"].(map[string]any) - if _, ok := providers[provider]; !ok { + rawProvider, ok := providers[provider] + if !ok { fmt.Fprintf(deps.Stderr, "Error: provider %q is not configured. See: ocx provider list\n", provider) return ExitFailure } models, _ := cfg["customModels"].([]any) - slug := provider + "/" + strings.ReplaceAll(modelID, "/", "-") + slug := routedSlug(provider, modelID) for _, raw := range models { - if model, ok := raw.(map[string]any); ok && fmt.Sprint(model["provider"])+"/"+strings.ReplaceAll(fmt.Sprint(model["modelId"]), "/", "-") == slug { + if model, ok := raw.(map[string]any); ok && routedSlug(fmt.Sprint(model["provider"]), fmt.Sprint(model["modelId"])) == slug { fmt.Fprintf(deps.Stderr, "Error: custom model %q already exists\n", slug) return ExitFailure } } - id := fmt.Sprintf("go-%d", time.Now().UnixNano()) + providerConfig, _ := rawProvider.(map[string]any) + if encodedModelIDCollides(modelID, knownModelIDs(provider, providerConfig, models)) { + fmt.Fprintf(deps.Stderr, "Error: custom model %q is ambiguous; it encodes to an existing model id\n", slug) + return ExitFailure + } + id, err := customModelUUID() + if err != nil { + fmt.Fprintln(deps.Stderr, err) + return ExitFailure + } entry := map[string]any{"id": id, "provider": provider, "modelId": modelID, "addedAt": time.Now().UTC().Format(time.RFC3339Nano)} + if displayName != "" { + entry["displayName"] = displayName + } + if contextWindow != nil { + entry["contextWindow"] = *contextWindow + } + if modalities != nil { + entry["inputModalities"] = *modalities + } + if reasoningEfforts != nil { + entry["reasoningEfforts"] = *reasoningEfforts + } + if defaultEffort != "" { + entry["defaultReasoningEffort"] = defaultEffort + } cfg["customModels"] = append(models, entry) if err := config.SaveRaw(cfg); err != nil { fmt.Fprintln(deps.Stderr, err) @@ -1078,16 +989,14 @@ func runCustomModelList(args []string, deps Deps) int { fmt.Fprintln(deps.Stdout, "No custom models registered.") return ExitOK } - for _, raw := range models { - model := raw.(map[string]any) - fmt.Fprintf(deps.Stdout, "%s: %s\n", model["provider"], model["modelId"]) - } + printCustomModelTable(models, deps.Stdout) return ExitOK } func runCustomModelRemove(args []string, deps Deps) int { confirmed := takeFlag(&args, "--yes") if len(args) != 1 { fmt.Fprintln(deps.Stderr, "Error: custom model id or provider/modelId is required") + fmt.Fprintln(deps.Stderr, modelRemoveUsage) return ExitFailure } if !confirmed { @@ -1102,13 +1011,28 @@ func runCustomModelRemove(args []string, deps Deps) int { } models, _ := cfg["customModels"].([]any) matched := -1 + selectedProvider := "" + if slash := strings.Index(target, "/"); slash >= 0 { + selectedProvider = target[:slash] + } + admitted := map[string]bool{} + if selectedProvider != "" { + roster := []string{} + for _, raw := range models { + if model, ok := raw.(map[string]any); ok && model["provider"] == selectedProvider { + roster = append(roster, fmt.Sprint(model["modelId"])) + } + } + for _, id := range resolveSlugSelection(selectedProvider, target, roster) { + admitted[id] = true + } + } for i, raw := range models { model, ok := raw.(map[string]any) if !ok { continue } - slug := fmt.Sprint(model["provider"]) + "/" + strings.ReplaceAll(fmt.Sprint(model["modelId"]), "/", "-") - if fmt.Sprint(model["id"]) == target || slug == target { + if fmt.Sprint(model["id"]) == target || (selectedProvider != "" && fmt.Sprint(model["provider"]) == selectedProvider && admitted[fmt.Sprint(model["modelId"])]) { if matched >= 0 { fmt.Fprintf(deps.Stderr, "Error: custom model selector %q is ambiguous; use the custom model id\n", target) return ExitFailure @@ -1132,9 +1056,297 @@ func runCustomModelRemove(args []string, deps Deps) int { fmt.Fprintln(deps.Stderr, err) return ExitFailure } - fmt.Fprintf(deps.Stdout, "Removed custom model %s.\n", fmt.Sprint(model["provider"])+"/"+strings.ReplaceAll(fmt.Sprint(model["modelId"]), "/", "-")) + fmt.Fprintf(deps.Stdout, "Removed custom model %s.\n", routedSlug(fmt.Sprint(model["provider"]), fmt.Sprint(model["modelId"]))) return ExitOK } + +func parseCustomModelAddFlags(args *[]string) (string, *int, *[]any, *[]any, string, error) { + var displayName, defaultEffort string + var contextWindow *int + var modalities, efforts *[]any + take := func(flag string) (string, bool) { + for i := 0; i < len(*args); i++ { + if (*args)[i] == flag { + if i+1 == len(*args) { + return "", false + } + value := (*args)[i+1] + *args = append((*args)[:i], (*args)[i+2:]...) + return value, true + } + } + return "", false + } + if value, found := take("--display-name"); found { + displayName = strings.TrimSpace(value) + if strings.Contains(displayName, "/") { + return "", nil, nil, nil, "", errors.New("displayName must not contain /") + } + } + if value, found := take("--context-window"); found { + parsed, err := strconv.Atoi(value) + if err != nil || parsed <= 0 { + return "", nil, nil, nil, "", errors.New("context window must be a positive integer") + } + contextWindow = &parsed + } + if value, found := take("--modalities"); found { + values := strings.Split(value, ",") + seen := map[string]bool{} + out := []any{} + for _, item := range values { + item = strings.TrimSpace(item) + if item != "text" && item != "image" && item != "audio" { + return "", nil, nil, nil, "", errors.New("modalities must be comma-separated values from text|image|audio") + } + if !seen[item] { + seen[item] = true + out = append(out, item) + } + } + modalities = &out + } + if value, found := take("--reasoning-efforts"); found { + if strings.TrimSpace(value) != "-" { + parsed, err := parseReasoningEfforts(value) + if err != nil { + return "", nil, nil, nil, "", err + } + efforts = &parsed + } + } + if value, found := take("--default-reasoning-effort"); found { + defaultEffort = strings.TrimSpace(value) + if defaultEffort == "-" { + defaultEffort = "" + } else { + if !isDeclaredReasoningEffort(defaultEffort) { + return "", nil, nil, nil, "", fmt.Errorf("unsupported reasoning effort: %s (allowed: none, minimal, low, medium, high, xhigh, max, ultra)", defaultEffort) + } + if efforts == nil || len(*efforts) == 0 { + return "", nil, nil, nil, "", errors.New("--default-reasoning-effort requires --reasoning-efforts") + } + found := false + for _, effort := range *efforts { + if effort == defaultEffort { + found = true + } + } + if !found { + return "", nil, nil, nil, "", fmt.Errorf("--default-reasoning-effort %q is not in the declared reasoning efforts", defaultEffort) + } + } + } + if len(*args) > 0 { + return "", nil, nil, nil, "", fmt.Errorf("Unknown flag(s): %s", strings.Join(*args, ", ")) + } + return displayName, contextWindow, modalities, efforts, defaultEffort, nil +} + +func parseReasoningEfforts(raw string) ([]any, error) { + trimmed := strings.TrimSpace(raw) + if trimmed == "-" { + return nil, nil + } + if trimmed == "" { + return []any{}, nil + } + values := strings.Split(trimmed, ",") + seen := map[string]bool{} + for _, value := range values { + value = strings.TrimSpace(value) + if !isDeclaredReasoningEffort(value) { + return nil, fmt.Errorf("unsupported reasoning effort: %s (allowed: none, minimal, low, medium, high, xhigh, max, ultra)", value) + } + seen[value] = true + } + order := []string{"none", "minimal", "low", "medium", "high", "xhigh", "max", "ultra"} + out := []any{} + for _, value := range order { + if seen[value] { + out = append(out, value) + } + } + return out, nil +} +func isDeclaredReasoningEffort(value string) bool { + for _, allowed := range []string{"none", "minimal", "low", "medium", "high", "xhigh", "max", "ultra"} { + if value == allowed { + return true + } + } + return false +} + +func isValidProviderName(value string) bool { + if value == "" || len(value) > 64 || value != strings.TrimSpace(value) { + return false + } + reserved := map[string]bool{"__proto__": true, "prototype": true, "constructor": true} + if reserved[strings.ToLower(value)] { + return false + } + for i, char := range value { + alnum := char >= 'a' && char <= 'z' || char >= 'A' && char <= 'Z' || char >= '0' && char <= '9' + if i == 0 || i == len(value)-1 { + if !alnum { + return false + } + continue + } + if !alnum && char != '.' && char != '_' && char != '-' { + return false + } + } + return true +} +func routedSlug(provider, model string) string { + return provider + "/" + strings.ReplaceAll(model, "/", "-") +} +func encodedModelIDCollides(model string, known []string) bool { + encoded := strings.ReplaceAll(model, "/", "-") + for _, id := range known { + if id != model && strings.ReplaceAll(id, "/", "-") == encoded { + return true + } + } + return false +} +func knownModelIDs(provider string, config map[string]any, custom []any) []string { + seen := map[string]bool{} + out := []string{} + add := func(id string) { + if id != "" && !seen[id] { + seen[id] = true + out = append(out, id) + } + } + if id, _ := config["defaultModel"].(string); id != "" { + add(id) + } + if models, ok := config["models"].([]any); ok { + for _, raw := range models { + if id, ok := raw.(string); ok { + add(id) + } + } + } + for _, raw := range custom { + if model, ok := raw.(map[string]any); ok && model["provider"] == provider { + add(fmt.Sprint(model["modelId"])) + } + } + return out +} +func resolveSlugSelection(provider, selection string, ids []string) []string { + namesNative := false + for _, id := range ids { + if id == selection { + namesNative = true + } + } + qualified := routedSlug(provider, selection) + if !namesNative && strings.HasPrefix(selection, provider+"/") { + qualified = selection + } + key := slugKey(qualified) + out := []string{} + for _, id := range ids { + if slugKey(routedSlug(provider, id)) == key { + out = append(out, id) + } + } + return out +} +func slugKey(slug string) string { + slash := strings.Index(slug, "/") + if slash <= 0 { + return "exact:" + slug + } + return "routed:" + slug[:slash] + ":" + strings.ReplaceAll(slug[slash+1:], "/", "-") +} +func customModelUUID() (string, error) { + raw := make([]byte, 16) + if _, err := rand.Read(raw); err != nil { + return "", err + } + raw[6] = raw[6]&0x0f | 0x40 + raw[8] = raw[8]&0x3f | 0x80 + return fmt.Sprintf("%08x-%04x-%04x-%04x-%012x", raw[:4], raw[4:6], raw[6:8], raw[8:10], raw[10:]), nil +} +func printCustomModelTable(models []any, writer io.Writer) { + groups := map[string][]map[string]any{} + names := []string{} + for _, raw := range models { + model, ok := raw.(map[string]any) + if !ok { + continue + } + provider := fmt.Sprint(model["provider"]) + if _, ok := groups[provider]; !ok { + names = append(names, provider) + } + groups[provider] = append(groups[provider], model) + } + for _, provider := range names { + headers := []string{"ID", "MODEL", "DISPLAY NAME", "CONTEXT", "MODALITIES", "EFFORTS", "DEFAULT EFFORT"} + rows := make([][]string, 0, len(groups[provider])) + widths := make([]int, len(headers)) + copy(widths, []int{2, 5, 12, 7, 10, 7, 14}) + for _, model := range groups[provider] { + id := fmt.Sprint(model["id"]) + if len(id) > 8 { + id = id[:8] + } + row := []string{id, fmt.Sprint(model["modelId"]), dash(model["displayName"]), customContext(model["contextWindow"]), customCSV(model["inputModalities"]), customCSV(model["reasoningEfforts"]), dash(model["defaultReasoningEffort"])} + rows = append(rows, row) + for i, cell := range row { + if len(cell) > widths[i] { + widths[i] = len(cell) + } + } + } + line := func(row []string) string { + cells := make([]string, len(row)) + for i, cell := range row { + cells[i] = fmt.Sprintf("%-*s", widths[i], cell) + } + return strings.Join(cells, " ") + } + fmt.Fprintf(writer, "%s:\n %s\n", provider, line(headers)) + for _, row := range rows { + fmt.Fprintf(writer, " %s\n", line(row)) + } + fmt.Fprintln(writer) + } +} +func dash(value any) string { + if value == nil || fmt.Sprint(value) == "" { + return "-" + } + return fmt.Sprint(value) +} +func customContext(value any) string { + if value == nil { + return "-" + } + number, err := strconv.ParseFloat(fmt.Sprint(value), 64) + if err != nil || number <= 0 { + return "-" + } + return fmt.Sprintf("%dk", int(math.Round(number/1000))) +} +func customCSV(value any) string { + values, ok := value.([]any) + if !ok || len(values) == 0 { + return "-" + } + parts := make([]string, 0, len(values)) + for _, raw := range values { + parts = append(parts, fmt.Sprint(raw)) + } + return strings.Join(parts, ",") +} func writeIndentedJSON(writer io.Writer, value any) int { raw, err := json.MarshalIndent(value, "", " ") if err != nil { From b176d06a0a34b32c296a9e107a7d967924382ede Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Mon, 7 Sep 2026 00:02:19 +0800 Subject: [PATCH 057/165] fix(go): delegate config family for exact parity --- go/internal/ocxcli/cli_test.go | 18 ++++++++++- tests/go-cli-parity.test.ts | 58 ++++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 1 deletion(-) diff --git a/go/internal/ocxcli/cli_test.go b/go/internal/ocxcli/cli_test.go index 2dac1c896b..b185eabb16 100644 --- a/go/internal/ocxcli/cli_test.go +++ b/go/internal/ocxcli/cli_test.go @@ -68,10 +68,11 @@ func TestVersionAndRegistry(t *testing.T) { } } -func TestLifecycleFamiliesDelegateExactArgumentsAndExitCode(t *testing.T) { +func TestTypeScriptOwnedFamiliesDelegateExactArgumentsAndExitCode(t *testing.T) { for _, argv := range [][]string{ {"status", "--json"}, {"doctor", "--json"}, {"service", "restart"}, {"codex-shim", "status"}, {"tray", "status"}, + {"config", "set", "port", "10101", "--json"}, } { t.Run(strings.Join(argv, " "), func(t *testing.T) { var received []string @@ -90,6 +91,21 @@ func TestLifecycleFamiliesDelegateExactArgumentsAndExitCode(t *testing.T) { } } +func TestConfigHelpDelegatesToTheConfigOwner(t *testing.T) { + var received []string + deps := depsFor(RuntimeState{}, &bytes.Buffer{}, &bytes.Buffer{}) + deps.Delegate = func(args []string) (int, error) { + received = append([]string(nil), args...) + return ExitOK, nil + } + if got := Run([]string{"help", "config"}, deps); got != ExitOK { + t.Fatalf("help config exit = %d", got) + } + if want := []string{"config", "--help"}; !slices.Equal(received, want) { + t.Fatalf("delegated argv = %#v, want %#v", received, want) + } +} + func TestLifecycleDelegateFailureIsReported(t *testing.T) { var out, stderr bytes.Buffer deps := depsFor(RuntimeState{}, &out, &stderr) diff --git a/tests/go-cli-parity.test.ts b/tests/go-cli-parity.test.ts index c209278f4d..a89612f78a 100644 --- a/tests/go-cli-parity.test.ts +++ b/tests/go-cli-parity.test.ts @@ -111,6 +111,64 @@ describe.skipIf(!goAvailable || goCLI === null)("Go CLI parity (ADR-0008, ticket })); expectParity(args); }); + test("diffs every config mutation, validation, and export path through the shared owner", () => { + const home = mkdtempSync(join(tmpdir(), "ocx-go-config-parity-")); + const configPath = join(home, "config.json"); + const exportPath = join(home, "export.json"); + const importPath = join(home, "import.json"); + const initial = { + port: 10100, + providers: { fixture: { adapter: "openai-chat", baseUrl: "https://example.test/v1", apiKey: "secret-key" } }, + defaultProvider: "fixture", + autoSwitchThreshold: 50, + }; + const reset = () => writeFileSync(configPath, JSON.stringify(initial)); + const parity = (args: string[]) => { + reset(); + const ts = runTs(args, home); + reset(); + const go = runGo(args, home); + expect(go).toEqual(ts); + }; + try { + writeFileSync(importPath, JSON.stringify({ ...initial, port: 10102 })); + parity(["config", "show", "--source"]); + parity(["config", "set", "autoSwitchThreshold", "70", "--json"]); + parity(["config", "set", "port", "-1", "--json"]); + parity(["config", "unset", "autoSwitchThreshold", "--json"]); + parity(["config", "validate", "--json"]); + parity(["config", "validate", importPath, "--json"]); + parity(["config", "export", "-"]); + parity(["config", "export", exportPath]); + parity(["config", "import", importPath, "--yes", "--json"]); + parity(["config", "import", importPath, "--json"]); + } finally { + removeTreeWithRetry(home); + } + }); + test("keeps a rejected config mutation atomic in both CLI entry points", async () => { + const home = mkdtempSync(join(tmpdir(), "ocx-go-config-atomic-")); + const configPath = join(home, "config.json"); + const initial = JSON.stringify({ + port: 10100, + providers: { fixture: { adapter: "openai-chat", baseUrl: "https://example.test/v1" } }, + defaultProvider: "fixture", + appOwnedMemoryBudgetMb: 128, + }); + try { + writeFileSync(configPath, initial); + const ts = runTs(["config", "set", "appOwnedMemoryBudgetMb", "63", "--json"], home); + const afterTS = await Bun.file(configPath).text(); + writeFileSync(configPath, initial); + const go = runGo(["config", "set", "appOwnedMemoryBudgetMb", "63", "--json"], home); + const afterGo = await Bun.file(configPath).text(); + expect(go).toEqual(ts); + expect(afterTS).toBe(initial); + expect(afterGo).toBe(initial); + } finally { + removeTreeWithRetry(home); + } + }); test.each([ { args: ["status"] }, { args: ["status", "--json"] }, { args: ["doctor", "--json"] }, { args: ["service", "status"] }, { args: ["service", "not-a-command"] }, From 643ef4d40eec94ce50260410b452db6f2a947951 Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Mon, 7 Sep 2026 00:03:03 +0800 Subject: [PATCH 058/165] fix(go): route config family through parity owner --- go/internal/ocxcli/cli.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/go/internal/ocxcli/cli.go b/go/internal/ocxcli/cli.go index 0df50b667c..4d8842d4c8 100644 --- a/go/internal/ocxcli/cli.go +++ b/go/internal/ocxcli/cli.go @@ -138,7 +138,7 @@ func Run(args []string, deps Deps) int { case "tray": return runDelegated(args, deps) case "config": - return runConfig(args[1:], deps) + return runDelegated(args, deps) case "models": return runModels(args[1:], deps) case "provider": @@ -248,6 +248,8 @@ func printSubcommandHelp(name string, deps Deps) int { fmt.Fprint(deps.Stdout, "Usage: ocx codex-shim \n\nAuto-start the proxy when `codex` launches.\n\nUse `remove` as an alias for `uninstall`.\n") case "tray": fmt.Fprint(deps.Stdout, "Usage: ocx tray [--json] [--no-start]\n\nInstall and control the Windows status tray icon.\n\nThe tray starts at Windows login and provides one-click proxy controls.\nTray start/stop controls the icon only; use its menu to start or stop the proxy.\n--no-start (install only) installs the tray without launching it immediately.\n") + case "config": + return runDelegated([]string{"config", "--help"}, deps) case "models": fmt.Fprint(deps.Stdout, modelsUsage+"\nCustom models:\n "+modelAddUsage+"\n "+modelRemoveUsage+"\n Usage: ocx models list-custom [--json]\n\nRuntime subcommands (live, edit, enable, disable, provider, selected, preset, new-policy, new-arrivals, context, shadow) retain the TypeScript management API owner during the incremental takeover.\n") default: From 5f1283e62a46b08ce9d3317fa67ad14dc723851e Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Mon, 7 Sep 2026 00:04:18 +0800 Subject: [PATCH 059/165] test(go): pin reasoning rewrite field parity --- go/internal/sidecar/responses_repair_test.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/go/internal/sidecar/responses_repair_test.go b/go/internal/sidecar/responses_repair_test.go index afd101b1e3..53f2100099 100644 --- a/go/internal/sidecar/responses_repair_test.go +++ b/go/internal/sidecar/responses_repair_test.go @@ -96,3 +96,16 @@ func TestResponseRepairPipelineRunsOrderedModelAndImageRestoresBeforeBackfill(t t.Fatalf("got %s\nwant %s", out, want) } } + +func TestResponseRepairPipelineReasoningEventUsesClientFieldWhitelist(t *testing.T) { + input := `{"type":"response.reasoning_text.delta","item_id":"rs_1","output_index":2,"delta":"think","sequence_number":7,"provider_extra":"drop-me"}` + p := responseRepairPipeline{reasoning: true} + out, changed := p.repairJSON([]byte(input)) + if !changed { + t.Fatal("pipeline reported unchanged") + } + want := `{"type":"response.reasoning_summary_text.delta","item_id":"rs_1","output_index":2,"summary_index":0,"delta":"think","sequence_number":7}` + if string(out) != want { + t.Fatalf("got %s\nwant %s", out, want) + } +} From b035b15bda4f29cb0e5a68432b960f7c459ce70f Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Mon, 7 Sep 2026 00:10:25 +0800 Subject: [PATCH 060/165] fix(go): complete status doctor service parity --- go/internal/ocxcli/cli.go | 66 ++-------------------------------- go/internal/ocxcli/cli_test.go | 22 +++++++++++- 2 files changed, 24 insertions(+), 64 deletions(-) diff --git a/go/internal/ocxcli/cli.go b/go/internal/ocxcli/cli.go index 4d8842d4c8..ffb33bbebf 100644 --- a/go/internal/ocxcli/cli.go +++ b/go/internal/ocxcli/cli.go @@ -163,66 +163,6 @@ func runDelegated(args []string, deps Deps) int { return code } -type statusReport struct { - SchemaVersion int - Running bool - PID *int64 - HealthOK bool - HealthURL string - HealthMessage string - Port int - Hostname string - Source string -} - -func runStatus(args []string, deps Deps) int { - jsonOutput := len(args) == 1 && args[0] == "--json" - if len(args) != 0 && !jsonOutput { - fmt.Fprintln(deps.Stderr, "Usage: ocx status [--json]") - return ExitFailure - } - report := collectGoStatus(deps) - if jsonOutput { - fmt.Fprintf(deps.Stdout, "{\"schemaVersion\":%d,\"proxy\":{\"running\":%t,\"pid\":", report.SchemaVersion, report.Running) - if report.PID == nil { - fmt.Fprint(deps.Stdout, "null") - } else { - fmt.Fprint(deps.Stdout, *report.PID) - } - fmt.Fprintf(deps.Stdout, ",\"health\":{\"ok\":%t,\"url\":%q,\"message\":%q}},\"listen\":{\"port\":%d,\"hostname\":%q,\"source\":%q}}\n", report.HealthOK, report.HealthURL, report.HealthMessage, report.Port, report.Hostname, report.Source) - return ExitOK - } - if report.Running { - fmt.Fprintf(deps.Stdout, "Proxy: running (PID %d)\n", *report.PID) - } else { - fmt.Fprintln(deps.Stdout, "Proxy: not running") - } - fmt.Fprintf(deps.Stdout, "Health: %s\nListen: %s:%d (%s)\n", report.HealthMessage, probeHost(report.Hostname), report.Port, report.Source) - return ExitOK -} - -func collectGoStatus(deps Deps) statusReport { - report := statusReport{SchemaVersion: 1, Port: 10100, Source: "config", HealthMessage: "unreachable"} - if cfg, err := config.Load(); err == nil && cfg != nil { - report.Port, report.Hostname = cfg.ListenTarget() - } - report.HealthURL = "http://" + probeHost(report.Hostname) + ":" + strconv.Itoa(report.Port) + "/healthz" - state, err := deps.ReadRuntime() - if err != nil { - return report - } - report.Port, report.Hostname, report.Source = state.Port, state.Hostname, "runtime" - report.HealthURL = baseURL(state) + "/healthz" - health, _, err := ProbeHealth(deps) - if err != nil { - return report - } - report.Running, report.HealthOK, report.HealthMessage = true, true, "ok" - pid := health.PID - report.PID = &pid - return report -} - func printHelp(w io.Writer) { fmt.Fprint(w, fullUsage) } func hasHelpFlag(args []string) bool { for _, arg := range args { @@ -239,11 +179,11 @@ func printSubcommandHelp(name string, deps Deps) int { case "ready": fmt.Fprint(deps.Stdout, "Usage: ocx ready [--json] [--wait [--timeout ]]\n\nCheck post-sync readiness. Exits 0 only when ready.\n\nExact unauthenticated GET /readyz returns HTTP 200 when ready, or 503 with Retry-After: 1 for pending or failed.\nIts sanitized HTTP identity is {service, version, uptime, pid, port, status}; /healthz is separate liveness, not readiness.\nDefault is a single identity-checked /readyz probe; old proxies without /readyz fail closed as unreachable.\n--wait polls until ready or timeout, but exits immediately on terminal failed (default 45s, max 300s).\n--timeout requires --wait and accepts a positive integer (1..300).\n--json emits {ready, status, pid, port}; status is one of ready|pending|failed|unreachable.\nInvalid or unknown arguments exit 64. Not-ready, pending, failed, timeout, and unreachable exit 1.\n") case "status": - fmt.Fprint(deps.Stdout, "Usage: ocx status [--json]\n\nReport Go-owned local listener diagnostics.\n") + return runDelegated([]string{"status", "--help"}, deps) case "doctor": - fmt.Fprint(deps.Stdout, "Usage: ocx doctor\n\nReport Go-owned local runtime diagnostics.\n") + return runDelegated([]string{"doctor", "--help"}, deps) case "service": - fmt.Fprint(deps.Stdout, "Usage: ocx service status\n\nReport the local service manager state. Lifecycle mutations remain TypeScript-owned during the incremental takeover.\n") + return runDelegated([]string{"service", "--help"}, deps) case "codex-shim": fmt.Fprint(deps.Stdout, "Usage: ocx codex-shim \n\nAuto-start the proxy when `codex` launches.\n\nUse `remove` as an alias for `uninstall`.\n") case "tray": diff --git a/go/internal/ocxcli/cli_test.go b/go/internal/ocxcli/cli_test.go index b185eabb16..e98fe485c9 100644 --- a/go/internal/ocxcli/cli_test.go +++ b/go/internal/ocxcli/cli_test.go @@ -116,7 +116,7 @@ func TestLifecycleDelegateFailureIsReported(t *testing.T) { } func TestReadOnlyFamilyHelp(t *testing.T) { - for _, command := range []string{"status", "doctor", "service", "codex-shim", "tray"} { + for _, command := range []string{"codex-shim", "tray"} { t.Run(command, func(t *testing.T) { var out, stderr bytes.Buffer if got := Run([]string{"help", command}, depsFor(RuntimeState{}, &out, &stderr)); got != ExitOK { @@ -129,6 +129,26 @@ func TestReadOnlyFamilyHelp(t *testing.T) { } } +func TestDelegatedFamilyHelpUsesOwnerOutput(t *testing.T) { + for _, command := range []string{"status", "doctor", "service"} { + t.Run(command, func(t *testing.T) { + var received []string + deps := depsFor(RuntimeState{}, &bytes.Buffer{}, &bytes.Buffer{}) + deps.Delegate = func(args []string) (int, error) { + received = append([]string(nil), args...) + return ExitOK, nil + } + if got := Run([]string{"help", command}, deps); got != ExitOK { + t.Fatalf("help exit = %d", got) + } + want := []string{command, "--help"} + if !slices.Equal(received, want) { + t.Fatalf("delegated argv = %#v, want %#v", received, want) + } + }) + } +} + func TestHealthRequiresValidAttestationProof(t *testing.T) { server, state := testServer(t, "ready", true) defer server.Close() From 1f25b6a3ea82aa24441cf402dbd1da7d7cdfa6fc Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Mon, 7 Sep 2026 00:24:28 +0800 Subject: [PATCH 061/165] fix(go): pin relay model fallthrough identity\n\nUnlisted model ids now resolve to the provider defaultModel (or first\ncatalog entry) before the direct relay, matching the TypeScript route\nfallthrough that the Responses model metadata rewrite exposes. --- go/internal/sidecar/hotpath_relay.go | 28 ++++++++++++++++++++ go/internal/sidecar/hotpath_relay_test.go | 32 +++++++++++++++++++++++ 2 files changed, 60 insertions(+) diff --git a/go/internal/sidecar/hotpath_relay.go b/go/internal/sidecar/hotpath_relay.go index 9c284bdcbc..545c411d8d 100644 --- a/go/internal/sidecar/hotpath_relay.go +++ b/go/internal/sidecar/hotpath_relay.go @@ -258,6 +258,7 @@ func requestQualifiesForRelay(cfg Config, contentType string, headers http.Heade } plan.modelID = modelID provider := providers.Find(plan.providerName) + plan.modelID = selectedRelayModel(provider, modelID) plan.reasoning = providerUsesContentReasoning(provider, modelID) if refusal := unsupportedResponseRepairRefusal(provider); refusal != nil { return nil, refusal @@ -271,6 +272,33 @@ func requestQualifiesForRelay(cfg Config, contentType string, headers http.Heade return plan, nil } +// selectedRelayModel mirrors the model identity exposed by the TS route when +// the request falls through to a provider's default model. A configured model +// keeps the requested id; an unlisted id uses defaultModel or the first model +// in the provider catalog, which is the value returned by Responses model +// metadata rewrite. +func selectedRelayModel(provider *jsonwire.Value, requested string) string { + if provider == nil || provider.Kind() != jsonwire.Object { + return requested + } + if defaultModel, ok := stringMember(provider, "defaultModel"); ok && defaultModel != "" { + if defaultModel == requested || !modelInProviderList(provider.Find("models"), requested) { + return defaultModel + } + } + if modelInProviderList(provider.Find("models"), requested) { + return requested + } + if models := provider.Find("models"); models != nil && models.Kind() == jsonwire.Array { + for _, model := range models.Elements() { + if model != nil && model.Kind() == jsonwire.String && model.String() != "" { + return model.String() + } + } + } + return requested +} + // unsupportedResponseRepairRefusal keeps stateful repairs on the TypeScript // bridge until their per-request state machines are ported. Passing these // providers through the direct relay would silently emit a different client diff --git a/go/internal/sidecar/hotpath_relay_test.go b/go/internal/sidecar/hotpath_relay_test.go index 93707185dc..0801dcd360 100644 --- a/go/internal/sidecar/hotpath_relay_test.go +++ b/go/internal/sidecar/hotpath_relay_test.go @@ -460,6 +460,38 @@ func TestRequestQualifiesForRelayRefusals(t *testing.T) { } } +func TestSelectedRelayModelMatchesRouteFallthrough(t *testing.T) { + cases := []struct { + name string + provider string + requested string + want string + }{ + {"listed model keeps requested id", `{"models":["test-model"]}`, "test-model", "test-model"}, + {"unlisted model uses defaultModel", `{"models":["test-model"],"defaultModel":"fallback-model"}`, "other-model", "fallback-model"}, + {"requesting defaultModel keeps it", `{"models":["test-model"],"defaultModel":"fallback-model"}`, "fallback-model", "fallback-model"}, + {"unlisted model without defaultModel uses first catalog entry", `{"models":["first-model","second-model"]}`, "other-model", "first-model"}, + {"unlisted model without catalog keeps requested id", `{"defaultModel":""}`, "other-model", "other-model"}, + {"missing provider keeps requested id", `null`, "other-model", "other-model"}, + } + for _, c := range cases { + c := c + t.Run(c.name, func(t *testing.T) { + var provider *jsonwire.Value + if c.provider != "null" { + parsed, err := jsonwire.Parse([]byte(c.provider)) + if err != nil { + t.Fatal(err) + } + provider = parsed + } + if got := selectedRelayModel(provider, c.requested); got != c.want { + t.Fatalf("selectedRelayModel(%s, %q) = %q, want %q", c.provider, c.requested, got, c.want) + } + }) + } +} + func TestAzureContractUsesAPIKeyHeader(t *testing.T) { for _, adapter := range []string{"azure", "azure-openai"} { t.Run(adapter, func(t *testing.T) { From 144555163789ddd6a5eb71885eb0ba0d6cc2101f Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Mon, 7 Sep 2026 00:30:21 +0800 Subject: [PATCH 062/165] feat(go): reconcile cli surface ownership map in ci --- go/internal/ocxcli/cli.go | 160 +++++++++++++++++++++++++-------- go/internal/ocxcli/cli_test.go | 86 ++++++++++++++++-- go/internal/ocxcli/families.go | 10 +-- 3 files changed, 206 insertions(+), 50 deletions(-) diff --git a/go/internal/ocxcli/cli.go b/go/internal/ocxcli/cli.go index ffb33bbebf..8892f0ef54 100644 --- a/go/internal/ocxcli/cli.go +++ b/go/internal/ocxcli/cli.go @@ -27,21 +27,112 @@ const ( attestationProofHeader = "x-opencodex-attestation-proof" ) -// Command is one user-visible top-level command. Keeping the registry data -// separate makes later parity additions additive and unit-testable. -type Command struct{ Name, Usage, Summary string } +// Ownership identifies the runtime that implements a documented command. +// TypeScriptOwned is an explicit migration seam, not a claim that Go owns the +// command merely because the Go binary forwards it. +type Ownership string + +const ( + GoOwned Ownership = "go-owned" + TypeScriptOwned Ownership = "typescript-owned" +) + +// Command is one user-visible top-level command. Commands is the Go CLI's +// machine-readable surface map: fullUsage, dispatch, and CI tests are all +// reconciled against it. Aliases resolve to the same owner as their command. +type Command struct { + Name, Usage, Summary string + Aliases []string + Owner Ownership +} var Commands = []Command{ - {Name: "health", Usage: "ocx health [--json]", Summary: "Verify the local proxy identity and report health."}, - {Name: "ready", Usage: "ocx ready [--json]", Summary: "Verify the local proxy identity and report readiness."}, - {Name: "status", Usage: "ocx status [--json]", Summary: "Report local listener diagnostics."}, - {Name: "doctor", Usage: "ocx doctor", Summary: "Report local runtime diagnostics."}, - {Name: "service", Usage: "ocx service status", Summary: "Report the local service manager state."}, - {Name: "codex-shim", Usage: "ocx codex-shim status", Summary: "Inspect the Codex autostart shim."}, - {Name: "tray", Usage: "ocx tray status", Summary: "Inspect the Windows status tray."}, - {Name: "config", Usage: "ocx config ", Summary: "Inspect the durable configuration."}, - {Name: "models", Usage: "ocx models [--provider ] [--json]", Summary: "List configured models."}, - {Name: "provider", Usage: "ocx provider ", Summary: "Inspect configured providers."}, + {Name: "setup", Aliases: []string{"init"}, Usage: "ocx setup", Summary: "Interactive setup.", Owner: TypeScriptOwned}, + {Name: "start", Usage: "ocx start [--port ]", Summary: "Start the proxy.", Owner: TypeScriptOwned}, + {Name: "stop", Usage: "ocx stop", Summary: "Stop the proxy.", Owner: TypeScriptOwned}, + {Name: "restore", Aliases: []string{"eject"}, Usage: "ocx restore [back]", Summary: "Restore native Codex configuration.", Owner: TypeScriptOwned}, + {Name: "recover-history", Usage: "ocx recover-history --legacy-openai --yes", Summary: "Recover legacy history.", Owner: TypeScriptOwned}, + {Name: "uninstall", Aliases: []string{"remove"}, Usage: "ocx uninstall", Summary: "Remove OpenCodex integration.", Owner: TypeScriptOwned}, + {Name: "service", Usage: "ocx service [sub]", Summary: "Run as a background service.", Owner: TypeScriptOwned}, + {Name: "codex-shim", Usage: "ocx codex-shim ", Summary: "Manage the Codex autostart shim.", Owner: TypeScriptOwned}, + {Name: "tray", Usage: "ocx tray ", Summary: "Manage the Windows status tray.", Owner: TypeScriptOwned}, + {Name: "ensure", Usage: "ocx ensure", Summary: "Ensure the proxy is running.", Owner: TypeScriptOwned}, + {Name: "connect", Usage: "ocx connect ", Summary: "Connect to a remote hub.", Owner: TypeScriptOwned}, + {Name: "disconnect", Usage: "ocx disconnect", Summary: "Disconnect from a remote hub.", Owner: TypeScriptOwned}, + {Name: "sync", Usage: "ocx sync [--restart-codex]", Summary: "Sync provider models.", Owner: TypeScriptOwned}, + {Name: "sync-cache", Usage: "ocx sync-cache [--restart-codex]", Summary: "Refresh the model cache.", Owner: TypeScriptOwned}, + {Name: "status", Usage: "ocx status", Summary: "Check proxy status.", Owner: TypeScriptOwned}, + {Name: "doctor", Usage: "ocx doctor", Summary: "Diagnose the environment.", Owner: TypeScriptOwned}, + {Name: "debug", Usage: "ocx debug ", Summary: "Manage debug settings.", Owner: TypeScriptOwned}, + {Name: "login", Usage: "ocx login ", Summary: "Log in to a provider.", Owner: TypeScriptOwned}, + {Name: "logout", Usage: "ocx logout ", Summary: "Log out from a provider.", Owner: TypeScriptOwned}, + {Name: "gui", Usage: "ocx gui", Summary: "Open the dashboard.", Owner: TypeScriptOwned}, + {Name: "update", Usage: "ocx update [--tag ]", Summary: "Update OpenCodex.", Owner: TypeScriptOwned}, + {Name: "restart", Usage: "ocx restart", Summary: "Restart the proxy.", Owner: TypeScriptOwned}, + {Name: "v2", Usage: "ocx v2 ", Summary: "Manage the v2 surface.", Owner: TypeScriptOwned}, + {Name: "health", Usage: "ocx health [--json]", Summary: "Verify the local proxy identity and report health.", Owner: GoOwned}, + {Name: "capabilities", Usage: "ocx capabilities [--json]", Summary: "List declared capabilities.", Owner: TypeScriptOwned}, + {Name: "ready", Usage: "ocx ready [--json] [--wait [--timeout ]]", Summary: "Verify readiness.", Owner: GoOwned}, + {Name: "provider", Usage: "ocx provider ", Summary: "Inspect configured providers.", Owner: GoOwned}, + {Name: "account", Usage: "ocx account ", Summary: "Manage accounts.", Owner: TypeScriptOwned}, + {Name: "models", Usage: "ocx models [--provider ] [--json]", Summary: "List configured models.", Owner: GoOwned}, + {Name: "alias", Usage: "ocx alias ", Summary: "Manage aliases.", Owner: TypeScriptOwned}, + {Name: "combo", Usage: "ocx combo ", Summary: "Manage combo routing.", Owner: TypeScriptOwned}, + {Name: "agent", Usage: "ocx agent ", Summary: "Manage agents.", Owner: TypeScriptOwned}, + {Name: "observe", Usage: "ocx observe ", Summary: "Inspect runtime observations.", Owner: TypeScriptOwned}, + {Name: "inspect", Usage: "ocx inspect ", Summary: "Inspect effective state.", Owner: TypeScriptOwned}, + {Name: "route", Usage: "ocx route ", Summary: "Manage routing.", Owner: TypeScriptOwned}, + {Name: "logs", Usage: "ocx logs [filters]", Summary: "Read logs.", Owner: TypeScriptOwned}, + {Name: "usage", Usage: "ocx usage", Summary: "Report usage.", Owner: TypeScriptOwned}, + {Name: "storage", Usage: "ocx storage ", Summary: "Manage storage.", Owner: TypeScriptOwned}, + {Name: "memory", Usage: "ocx memory [--json]", Summary: "Inspect memory.", Owner: TypeScriptOwned}, + {Name: "api-key", Usage: "ocx api-key ", Summary: "Manage API keys.", Owner: TypeScriptOwned}, + {Name: "access", Usage: "ocx access ", Summary: "Manage external access.", Owner: TypeScriptOwned}, + {Name: "export", Usage: "ocx export --client ", Summary: "Export client configuration.", Owner: TypeScriptOwned}, + {Name: "integration", Usage: "ocx integration client ", Summary: "Manage integrations.", Owner: TypeScriptOwned}, + {Name: "grok", Usage: "ocx grok ", Summary: "Manage Grok Build.", Owner: TypeScriptOwned}, + {Name: "system", Usage: "ocx system ", Summary: "Manage runtime settings.", Owner: TypeScriptOwned}, + {Name: "config", Usage: "ocx config ", Summary: "Manage configuration.", Owner: TypeScriptOwned}, + {Name: "lab", Usage: "ocx lab ", Summary: "Inspect Compatibility Lab.", Owner: TypeScriptOwned}, + {Name: "claude", Usage: "ocx claude [args...]", Summary: "Launch Claude Code.", Owner: TypeScriptOwned}, + {Name: "opencode", Usage: "ocx opencode [args...]", Summary: "Launch opencode.", Owner: TypeScriptOwned}, + {Name: "mcode", Usage: "ocx mcode [args...]", Summary: "Launch MiniMax Code.", Owner: TypeScriptOwned}, + {Name: "mmx", Usage: "ocx mmx text [args]", Summary: "Launch MiniMax CLI.", Owner: TypeScriptOwned}, + {Name: "zcode", Usage: "ocx zcode [sub]", Summary: "Connect ZCode.", Owner: TypeScriptOwned}, +} + +// commandForName resolves canonical command names and aliases from Commands. +func commandForName(name string) (Command, bool) { + for _, command := range Commands { + if command.Name == name { + return command, true + } + for _, alias := range command.Aliases { + if alias == name { + return command, true + } + } + } + return Command{}, false +} + +// OwnershipFor reports the owner of argv's command surface. It is intentionally +// data-driven so a command cannot become native by accident while it still +// routes through DelegateToTypeScript. +func OwnershipFor(args []string) (Ownership, bool) { + if len(args) == 0 { + return GoOwned, true // Root help is emitted by this binary. + } + command, ok := commandForName(args[0]) + if !ok { + return "", false + } + if command.Name == "models" && len(args) > 1 { + if owner, ok := modelRuntimeSubcommands[args[1]]; ok { + return owner, true + } + } + return command.Owner, true } type RuntimeState struct { @@ -123,29 +214,29 @@ func Run(args []string, deps Deps) int { case "--version", "-v", "version": fmt.Fprintf(deps.Stdout, "opencodex %s\n", deps.Version) return ExitOK + } + owner, known := OwnershipFor(args) + if !known { + fmt.Fprintf(deps.Stderr, "Unknown command: %s\n", args[0]) + printHelp(deps.Stdout) + return ExitFailure + } + if owner == TypeScriptOwned { + return runDelegated(args, deps) + } + switch args[0] { case "health": return runHealth(args[1:], deps) case "ready": return runReady(args[1:], deps) - case "status": - return runDelegated(args, deps) - case "doctor": - return runDelegated(args, deps) - case "service": - return runDelegated(args, deps) - case "codex-shim": - return runDelegated(args, deps) - case "tray": - return runDelegated(args, deps) - case "config": - return runDelegated(args, deps) case "models": return runModels(args[1:], deps) case "provider": return runProvider(args[1:], deps) default: - fmt.Fprintf(deps.Stderr, "Unknown command: %s\n", args[0]) - printHelp(deps.Stdout) + // The ownership registry above and this switch must be reconciled by + // TestOwnershipMapMatchesDispatch; this is defensive for future edits. + fmt.Fprintf(deps.Stderr, "Unimplemented Go-owned command: %s\n", args[0]) return ExitFailure } } @@ -173,23 +264,14 @@ func hasHelpFlag(args []string) bool { return false } func printSubcommandHelp(name string, deps Deps) int { + if owner, known := OwnershipFor([]string{name}); known && owner == TypeScriptOwned { + return runDelegated([]string{name, "--help"}, deps) + } switch name { case "health": fmt.Fprint(deps.Stdout, "Usage: ocx health [--json]\n\nCheck proxy health. Exits 0 if healthy, 1 otherwise.\n\nUse --json for structured output: {ok, pid, port}.\n") case "ready": fmt.Fprint(deps.Stdout, "Usage: ocx ready [--json] [--wait [--timeout ]]\n\nCheck post-sync readiness. Exits 0 only when ready.\n\nExact unauthenticated GET /readyz returns HTTP 200 when ready, or 503 with Retry-After: 1 for pending or failed.\nIts sanitized HTTP identity is {service, version, uptime, pid, port, status}; /healthz is separate liveness, not readiness.\nDefault is a single identity-checked /readyz probe; old proxies without /readyz fail closed as unreachable.\n--wait polls until ready or timeout, but exits immediately on terminal failed (default 45s, max 300s).\n--timeout requires --wait and accepts a positive integer (1..300).\n--json emits {ready, status, pid, port}; status is one of ready|pending|failed|unreachable.\nInvalid or unknown arguments exit 64. Not-ready, pending, failed, timeout, and unreachable exit 1.\n") - case "status": - return runDelegated([]string{"status", "--help"}, deps) - case "doctor": - return runDelegated([]string{"doctor", "--help"}, deps) - case "service": - return runDelegated([]string{"service", "--help"}, deps) - case "codex-shim": - fmt.Fprint(deps.Stdout, "Usage: ocx codex-shim \n\nAuto-start the proxy when `codex` launches.\n\nUse `remove` as an alias for `uninstall`.\n") - case "tray": - fmt.Fprint(deps.Stdout, "Usage: ocx tray [--json] [--no-start]\n\nInstall and control the Windows status tray icon.\n\nThe tray starts at Windows login and provides one-click proxy controls.\nTray start/stop controls the icon only; use its menu to start or stop the proxy.\n--no-start (install only) installs the tray without launching it immediately.\n") - case "config": - return runDelegated([]string{"config", "--help"}, deps) case "models": fmt.Fprint(deps.Stdout, modelsUsage+"\nCustom models:\n "+modelAddUsage+"\n "+modelRemoveUsage+"\n Usage: ocx models list-custom [--json]\n\nRuntime subcommands (live, edit, enable, disable, provider, selected, preset, new-policy, new-arrivals, context, shadow) retain the TypeScript management API owner during the incremental takeover.\n") default: diff --git a/go/internal/ocxcli/cli_test.go b/go/internal/ocxcli/cli_test.go index e98fe485c9..0b59d28a8d 100644 --- a/go/internal/ocxcli/cli_test.go +++ b/go/internal/ocxcli/cli_test.go @@ -63,8 +63,76 @@ func TestVersionAndRegistry(t *testing.T) { if got := Run([]string{"--version"}, depsFor(RuntimeState{}, &out, &err)); got != ExitOK || out.String() != "opencodex 2.42.0\n" { t.Fatalf("version = code %d stdout %q", got, out.String()) } - if len(Commands) != 10 || Commands[0].Name != "health" || Commands[1].Name != "ready" || Commands[2].Name != "status" || Commands[3].Name != "doctor" || Commands[4].Name != "service" || Commands[5].Name != "codex-shim" || Commands[6].Name != "tray" || Commands[7].Name != "config" || Commands[8].Name != "models" || Commands[9].Name != "provider" { - t.Fatalf("unexpected command registry: %#v", Commands) + if len(Commands) < 50 { + t.Fatalf("incomplete command registry: %#v", Commands) + } +} + +func TestOwnershipMapMatchesDispatch(t *testing.T) { + for _, command := range Commands { + for _, name := range append([]string{command.Name}, command.Aliases...) { + t.Run(name, func(t *testing.T) { + var delegated []string + deps := depsFor(RuntimeState{}, &bytes.Buffer{}, &bytes.Buffer{}) + deps.Delegate = func(args []string) (int, error) { + delegated = append([]string(nil), args...) + return 17, nil + } + owner, known := OwnershipFor([]string{name}) + if !known || owner != command.Owner { + t.Fatalf("OwnershipFor(%q) = %q, %t; want %q, true", name, owner, known, command.Owner) + } + got := Run([]string{name}, deps) + if command.Owner == TypeScriptOwned { + if got != 17 || !slices.Equal(delegated, []string{name}) { + t.Fatalf("typescript-owned %q did not delegate: code=%d argv=%#v", name, got, delegated) + } + } else if len(delegated) != 0 { + t.Fatalf("go-owned %q delegated argv=%#v", name, delegated) + } + }) + } + } +} + +func TestModelRuntimeOwnershipDelegates(t *testing.T) { + for subcommand, owner := range modelRuntimeSubcommands { + if owner != TypeScriptOwned { + t.Fatalf("models %s owner = %q, want typescript-owned", subcommand, owner) + } + if got, known := OwnershipFor([]string{"models", subcommand}); !known || got != TypeScriptOwned { + t.Fatalf("OwnershipFor(models %s) = %q, %t", subcommand, got, known) + } + } +} + +func TestHelpSurfaceMatchesCommandRegistry(t *testing.T) { + documented := map[string]bool{} + for _, line := range strings.Split(fullUsage, "\n") { + if !strings.HasPrefix(line, " ocx ") { + continue + } + fields := strings.Fields(strings.TrimPrefix(strings.TrimSpace(line), "ocx ")) + if len(fields) > 0 { + documented[fields[0]] = true + } + } + for name := range documented { + if name == "help" || name == "--version" { + continue // Dispatch-head entries never select an implementation owner. + } + if _, ok := commandForName(name); !ok { + t.Fatalf("help documents %q but ownership registry has no command", name) + } + } + for _, command := range Commands { + found := documented[command.Name] + for _, alias := range command.Aliases { + found = found || documented[alias] + } + if !found { + t.Fatalf("ownership registry command %q is missing from top-level help", command.Name) + } } } @@ -115,15 +183,21 @@ func TestLifecycleDelegateFailureIsReported(t *testing.T) { } } -func TestReadOnlyFamilyHelp(t *testing.T) { +func TestTypeScriptOwnedFamilyHelpDelegates(t *testing.T) { for _, command := range []string{"codex-shim", "tray"} { t.Run(command, func(t *testing.T) { var out, stderr bytes.Buffer - if got := Run([]string{"help", command}, depsFor(RuntimeState{}, &out, &stderr)); got != ExitOK { + var received []string + deps := depsFor(RuntimeState{}, &out, &stderr) + deps.Delegate = func(args []string) (int, error) { + received = append([]string(nil), args...) + return ExitOK, nil + } + if got := Run([]string{"help", command}, deps); got != ExitOK { t.Fatalf("help exit = %d stderr %q", got, stderr.String()) } - if !strings.Contains(out.String(), "Usage: ocx "+command) { - t.Fatalf("help output = %q", out.String()) + if want := []string{command, "--help"}; !slices.Equal(received, want) { + t.Fatalf("delegated argv = %#v, want %#v", received, want) } }) } diff --git a/go/internal/ocxcli/families.go b/go/internal/ocxcli/families.go index 4fc6c194f3..43cb5e0f31 100644 --- a/go/internal/ocxcli/families.go +++ b/go/internal/ocxcli/families.go @@ -23,10 +23,10 @@ const ( providerRegistryCount = 85 ) -var modelRuntimeSubcommands = map[string]bool{ - "live": true, "edit": true, "enable": true, "disable": true, "provider": true, - "selected": true, "preset": true, "new-policy": true, "new-arrivals": true, - "context": true, "shadow": true, +var modelRuntimeSubcommands = map[string]Ownership{ + "live": TypeScriptOwned, "edit": TypeScriptOwned, "enable": TypeScriptOwned, "disable": TypeScriptOwned, "provider": TypeScriptOwned, + "selected": TypeScriptOwned, "preset": TypeScriptOwned, "new-policy": TypeScriptOwned, "new-arrivals": TypeScriptOwned, + "context": TypeScriptOwned, "shadow": TypeScriptOwned, } func loadCLIConfig() (map[string]any, error) { @@ -360,7 +360,7 @@ func runModels(args []string, deps Deps) int { case "list-custom": return runCustomModelList(args[1:], deps) } - if modelRuntimeSubcommands[args[0]] { + if modelRuntimeSubcommands[args[0]] == TypeScriptOwned { // These commands are management-API clients, not config projections. The // TypeScript owner already supplies their authenticated API transaction and // exact user-facing output; preserving that owner avoids a second client From 36a0c2cfb3d5a1a63257612d395dea915ed6a5e3 Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Mon, 7 Sep 2026 00:34:37 +0800 Subject: [PATCH 063/165] ci(go): add cross-compile build matrix scaffold --- .github/workflows/go-release-artifacts.yml | 86 ++++++++++++++++++++++ scripts/build-go-release-artifact.sh | 40 ++++++++++ 2 files changed, 126 insertions(+) create mode 100644 .github/workflows/go-release-artifacts.yml create mode 100755 scripts/build-go-release-artifact.sh diff --git a/.github/workflows/go-release-artifacts.yml b/.github/workflows/go-release-artifacts.yml new file mode 100644 index 0000000000..0a20ae2e79 --- /dev/null +++ b/.github/workflows/go-release-artifacts.yml @@ -0,0 +1,86 @@ +name: Go release artifact scaffold + +on: + # This workflow deliberately stays outside the release path until #40 proves + # that the Go artifact embeds every runtime asset required by a clean host. + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: go-release-artifacts-${{ github.ref }} + cancel-in-progress: true + +jobs: + verify-go-runtime: + name: verify Go runtime + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Checkout + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + with: + persist-credentials: false + + - name: Setup Go + uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5 + with: + go-version-file: go/go.mod + cache: false + + - name: Build, vet, and test Go runtime + run: | + set -euo pipefail + cd go + CGO_ENABLED=0 go build -buildvcs=false ./... + go vet ./... + go test ./... + + - name: Smoke test Linux release candidate + run: | + set -euo pipefail + scripts/build-go-release-artifact.sh linux/amd64 .tmp/go-release/linux-amd64 + .tmp/go-release/linux-amd64/ocx-linux-amd64 --version + + build-release-artifact: + name: build ${{ matrix.target }} + needs: verify-go-runtime + runs-on: ubuntu-latest + timeout-minutes: 10 + strategy: + fail-fast: false + matrix: + include: + - target: linux/amd64 + artifact: ocx-linux-amd64 + - target: linux/arm64 + artifact: ocx-linux-arm64 + - target: darwin/amd64 + artifact: ocx-darwin-amd64 + - target: darwin/arm64 + artifact: ocx-darwin-arm64 + - target: windows/amd64 + artifact: ocx-windows-amd64 + steps: + - name: Checkout + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + with: + persist-credentials: false + + - name: Setup Go + uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5 + with: + go-version-file: go/go.mod + cache: false + + - name: Cross-compile static ocx candidate + run: scripts/build-go-release-artifact.sh '${{ matrix.target }}' dist + + - name: Upload candidate + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: ${{ matrix.artifact }} + path: dist/ + if-no-files-found: error + retention-days: 7 diff --git a/scripts/build-go-release-artifact.sh b/scripts/build-go-release-artifact.sh new file mode 100755 index 0000000000..6ac0712db7 --- /dev/null +++ b/scripts/build-go-release-artifact.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +# Build one static Go ocx release candidate. This is a staging-only helper for +# #42: release.ts remains the release authority until #40 makes this artifact +# the complete single-binary distribution. +set -euo pipefail + +usage() { + cat >&2 <<'USAGE' +Usage: scripts/build-go-release-artifact.sh / + +Supported targets: linux/amd64, linux/arm64, darwin/amd64, darwin/arm64, windows/amd64 +USAGE + exit 64 +} + +[ "$#" -eq 2 ] || usage +target="$1" +output_dir="$2" +repo_root="$(cd "$(dirname "$0")/.." && pwd)" +case "$output_dir" in + /*) ;; + *) output_dir="$repo_root/$output_dir" ;; +esac +case "$target" in + linux/amd64|linux/arm64|darwin/amd64|darwin/arm64|windows/amd64) ;; + *) echo "unsupported Go release target: $target" >&2; usage ;; +esac + +goos="${target%/*}" +goarch="${target#*/}" +filename="ocx-${goos}-${goarch}" +if [ "$goos" = windows ]; then + filename="${filename}.exe" +fi + +mkdir -p "$output_dir" +cd "$repo_root/go" +GOOS="$goos" GOARCH="$goarch" CGO_ENABLED=0 \ + go build -buildvcs=false -trimpath -o "$output_dir/$filename" ./cmd/ocx +printf '%s\n' "$output_dir/$filename" From 13368973d9f3ef719153a5c16de8037018c01337 Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Mon, 7 Sep 2026 00:42:59 +0800 Subject: [PATCH 064/165] feat(go): port config schema normalizer and ordered persistence --- go/internal/configschema/lock_unix.go | 24 ++ go/internal/configschema/lock_windows.go | 25 ++ go/internal/configschema/persistence.go | 78 ++++ go/internal/configschema/persistence_test.go | 63 +++ go/internal/configschema/schema.go | 394 +++++++++++++++++++ go/internal/configschema/schema_test.go | 68 ++++ 6 files changed, 652 insertions(+) create mode 100644 go/internal/configschema/lock_unix.go create mode 100644 go/internal/configschema/lock_windows.go create mode 100644 go/internal/configschema/persistence.go create mode 100644 go/internal/configschema/persistence_test.go create mode 100644 go/internal/configschema/schema.go create mode 100644 go/internal/configschema/schema_test.go diff --git a/go/internal/configschema/lock_unix.go b/go/internal/configschema/lock_unix.go new file mode 100644 index 0000000000..e9f420c035 --- /dev/null +++ b/go/internal/configschema/lock_unix.go @@ -0,0 +1,24 @@ +//go:build unix + +package configschema + +import ( + "errors" + "os" + "syscall" +) + +func tryLockPath(path string) (release func(), acquired bool, err error) { + f, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR, 0o600) + if err != nil { + return nil, false, err + } + if err := syscall.Flock(int(f.Fd()), syscall.LOCK_EX|syscall.LOCK_NB); err != nil { + _ = f.Close() + if errors.Is(err, syscall.EWOULDBLOCK) || errors.Is(err, syscall.EAGAIN) { + return nil, false, nil + } + return nil, false, err + } + return func() { _ = syscall.Flock(int(f.Fd()), syscall.LOCK_UN); _ = f.Close() }, true, nil +} diff --git a/go/internal/configschema/lock_windows.go b/go/internal/configschema/lock_windows.go new file mode 100644 index 0000000000..c6c6332f51 --- /dev/null +++ b/go/internal/configschema/lock_windows.go @@ -0,0 +1,25 @@ +//go:build windows + +package configschema + +import ( + "os" + "sync" +) + +// This keeps cross-compiled builds dependency-free. Native Windows LockFileEx +// wiring belongs to the native config command increment. +var windowsLocks sync.Map + +func tryLockPath(path string) (release func(), acquired bool, err error) { + f, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR, 0o600) + if err != nil { + return nil, false, err + } + _ = f.Close() + _, loaded := windowsLocks.LoadOrStore(path, struct{}{}) + if loaded { + return nil, false, nil + } + return func() { windowsLocks.Delete(path) }, true, nil +} diff --git a/go/internal/configschema/persistence.go b/go/internal/configschema/persistence.go new file mode 100644 index 0000000000..c6147bbcf1 --- /dev/null +++ b/go/internal/configschema/persistence.go @@ -0,0 +1,78 @@ +package configschema + +import ( + "context" + "os" + "path/filepath" + "time" +) + +// WithPathLock serializes cooperative Go config writers using an OS advisory +// lock held on a stable sidecar beside config.json. Keeping the sidecar stable +// is essential: deleting it after release would let two writers lock different +// inodes. A crashed process releases its OS lock automatically. +// +// TypeScript currently coordinates config mutations through BEGIN IMMEDIATE on +// config-mutation.sqlite. This preliminary package deliberately does not claim +// to join that transaction: wiring a SQLite driver and the generation protocol +// belongs with the native config command that will consume this package. +func WithPathLock(ctx context.Context, path string, fn func() error) error { + if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil { + return err + } + lock := path + ".lock" + for { + release, acquired, err := tryLockPath(lock) + if err != nil { + return err + } + if acquired { + defer release() + return fn() + } + select { + case <-ctx.Done(): + return ctx.Err() + case <-time.After(5 * time.Millisecond): + } + } +} + +// WriteAtomicLocked is the persistence transaction used by a future Go-native +// config dispatcher: serialize in canonical order, fsync a 0600 temp file, and +// publish it through rename while holding the shared lock. +func WriteAtomicLocked(ctx context.Context, path string, config *Normalized) error { + return WithPathLock(ctx, path, func() error { + data, err := config.IndentedJSON() + if err != nil { + return err + } + data = append(data, '\n') + dir := filepath.Dir(path) + tmp, err := os.CreateTemp(dir, ".config.json-*") + if err != nil { + return err + } + name := tmp.Name() + defer os.Remove(name) + if err := tmp.Chmod(0o600); err != nil { + tmp.Close() + return err + } + if _, err := tmp.Write(data); err != nil { + tmp.Close() + return err + } + if err := tmp.Sync(); err != nil { + tmp.Close() + return err + } + if err := tmp.Close(); err != nil { + return err + } + if err := os.Rename(name, path); err != nil { + return err + } + return os.Chmod(path, 0o600) + }) +} diff --git a/go/internal/configschema/persistence_test.go b/go/internal/configschema/persistence_test.go new file mode 100644 index 0000000000..04084d0d0d --- /dev/null +++ b/go/internal/configschema/persistence_test.go @@ -0,0 +1,63 @@ +package configschema + +import ( + "context" + "errors" + "os" + "path/filepath" + "testing" + "time" +) + +func TestWriteAtomicLockedPersistsOrderedJSONWithPrivateMode(t *testing.T) { + path := filepath.Join(t.TempDir(), "nested", "config.json") + normalized, err := NormalizeJSON([]byte(`{"providers":{}}`)) + if err != nil { + t.Fatal(err) + } + if err := WriteAtomicLocked(context.Background(), path, normalized); err != nil { + t.Fatal(err) + } + got, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + if string(got)[:15] != "{\n \"port\": 101" { + t.Fatalf("unexpected persisted content: %s", got) + } + info, err := os.Stat(path) + if err != nil { + t.Fatal(err) + } + if info.Mode().Perm() != 0o600 { + t.Fatalf("mode = %o, want 0600", info.Mode().Perm()) + } + lock, err := os.Stat(path + ".lock") + if err != nil { + t.Fatalf("stable lock sidecar missing: %v", err) + } + if lock.Mode().Perm() != 0o600 { + t.Fatalf("lock mode = %o, want 0600", lock.Mode().Perm()) + } +} + +func TestWithPathLockHonorsContextWhileAnotherWriterOwnsLock(t *testing.T) { + path := filepath.Join(t.TempDir(), "config.json") + release := make(chan struct{}) + entered := make(chan struct{}) + done := make(chan error, 1) + go func() { + done <- WithPathLock(context.Background(), path, func() error { close(entered); <-release; return nil }) + }() + <-entered + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Millisecond) + defer cancel() + err := WithPathLock(ctx, path, func() error { return nil }) + if !errors.Is(err, context.DeadlineExceeded) { + t.Fatalf("error = %v, want deadline exceeded", err) + } + close(release) + if err := <-done; err != nil { + t.Fatal(err) + } +} diff --git a/go/internal/configschema/schema.go b/go/internal/configschema/schema.go new file mode 100644 index 0000000000..52205b1cd4 --- /dev/null +++ b/go/internal/configschema/schema.go @@ -0,0 +1,394 @@ +// Package configschema ports the config.json boundary shared by the TypeScript +// config command. It intentionally keeps JSON object order: config show and +// config export expose the Zod schema's default-injection order. +package configschema + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "math" + "strconv" + "strings" +) + +const ( + defaultPort = int64(10100) + defaultUsageMaxReadBytes = int64(64 * 1024 * 1024) + defaultAppOwnedMemoryBudgetMB = int64(256) + maxAppOwnedMemoryBudgetMB = int64(4096) +) + +// Normalized is a config document whose object order has been projected onto +// TypeScript's configSchema order. Unknown fields remain present after known +// schema fields, matching Zod's passthrough object result. +type Normalized struct{ root *value } + +func NormalizeJSON(raw []byte) (*Normalized, error) { + v, err := parse(raw) + if err != nil { + return nil, err + } + if v.kind != objectKind { + return nil, errors.New("config must be a JSON object") + } + return &Normalized{root: normalizeLoad(v)}, nil +} + +// ValidateCandidateJSON implements the strict write boundary. Loading can +// degrade selected optional fields; writes never silently accept an invalid +// config candidate. Error wording follows Zod 4 as used in src/config.ts. +func ValidateCandidateJSON(raw []byte) (*Normalized, error) { + v, err := parse(raw) + if err != nil { + return nil, fmt.Errorf("invalid JSON: %w", err) + } + if v.kind != objectKind { + return nil, errors.New("schema_invalid: Invalid input: expected object, received array") + } + if err := validateTop(v); err != nil { + return nil, err + } + return &Normalized{root: normalizeLoad(v)}, nil +} + +func (n *Normalized) CompactJSON() ([]byte, error) { + if n == nil || n.root == nil { + return nil, errors.New("nil normalized config") + } + return n.root.compact(), nil +} + +func (n *Normalized) IndentedJSON() ([]byte, error) { + compact, err := n.CompactJSON() + if err != nil { + return nil, err + } + var out bytes.Buffer + if err := json.Indent(&out, compact, "", " "); err != nil { + return nil, err + } + return out.Bytes(), nil +} + +type valueKind uint8 + +const ( + nullKind valueKind = iota + boolKind + numberKind + stringKind + arrayKind + objectKind +) + +type member struct { + key string + value *value +} +type value struct { + kind valueKind + b bool + number json.Number + text string + array []*value + object []member +} + +func parse(raw []byte) (*value, error) { + d := json.NewDecoder(bytes.NewReader(raw)) + d.UseNumber() + v, err := decodeValue(d) + if err != nil { + return nil, err + } + if _, err := d.Token(); err != io.EOF { + if err == nil { + return nil, errors.New("multiple JSON values") + } + return nil, err + } + return v, nil +} +func decodeValue(d *json.Decoder) (*value, error) { + t, err := d.Token() + if err != nil { + return nil, err + } + return decodeToken(d, t) +} +func decodeToken(d *json.Decoder, token json.Token) (*value, error) { + switch x := token.(type) { + case nil: + return &value{kind: nullKind}, nil + case bool: + return &value{kind: boolKind, b: x}, nil + case string: + return &value{kind: stringKind, text: x}, nil + case json.Number: + return &value{kind: numberKind, number: x}, nil + case json.Delim: + switch x { + case '{': + v := &value{kind: objectKind} + for d.More() { + key, err := d.Token() + if err != nil { + return nil, err + } + s, ok := key.(string) + if !ok { + return nil, errors.New("object key is not a string") + } + child, err := decodeValue(d) + if err != nil { + return nil, err + } + v.set(s, child) + } + _, err := d.Token() + return v, err + case '[': + v := &value{kind: arrayKind} + for d.More() { + child, err := decodeValue(d) + if err != nil { + return nil, err + } + v.array = append(v.array, child) + } + _, err := d.Token() + return v, err + } + } + return nil, errors.New("unsupported JSON token") +} +func (v *value) find(key string) *value { + if v == nil || v.kind != objectKind { + return nil + } + for _, m := range v.object { + if m.key == key { + return m.value + } + } + return nil +} +func (v *value) set(key string, x *value) { + for i := range v.object { + if v.object[i].key == key { + v.object[i].value = x + return + } + } + v.object = append(v.object, member{key, x}) +} +func (v *value) has(key string) bool { return v.find(key) != nil } +func number(n int64) *value { + return &value{kind: numberKind, number: json.Number(strconv.FormatInt(n, 10))} +} +func stringValue(s string) *value { return &value{kind: stringKind, text: s} } + +var schemaOrder = []string{"port", "runtimeRole", "hub", "remoteGui", "client", "managementUsageMaxReadBytes", "upstreamHostCircuitThreshold", "maxUpstreamBodyBytes", "appOwnedMemoryBudgetMb", "hostname", "unauthenticatedLoopbackListener", "providers", "defaultProvider", "defaultModelAliases", "cursorEffortRows", "configRebaseProvenance", "emptyCompletionRetry", "oauthOpenBrowser", "openaiProviderTierVersion", "googleAntigravityStaticCatalogVersion", "clientIntegrations", "providerContextCaps", "contextCapValue", "multiAgentGuidanceEnabled", "agentTaskRecovery", "injectionModel", "injectionEffort", "syncCodexSubagentDefaults", "subagentModelFallbackByModel", "codexShimAutoRestore", "codexDesktopAuthless", "pausedCodexAccountIds", "codexAccountNamespaces", "codexAccountPriorities", "activeCodexAccountPinned", "codexAccountPickerEnabled", "showCodexSparkQuota", "resetCreditAutoRedeem", "grokExcludedModels", "streamMode", "blockedModelRedirects", "experimentalRealtimeWsBaseUrl", "apiKeys"} + +func normalizeLoad(in *value) *value { + out := &value{kind: objectKind} + known := map[string]bool{} + for _, key := range schemaOrder { + known[key] = true + x := in.find(key) + switch key { + case "port": + if x == nil || !validIntRange(x, 0, 65535) { + out.set(key, number(defaultPort)) + } else { + out.set(key, x) + } + case "managementUsageMaxReadBytes": + if x == nil || !validIntRange(x, 1, math.MaxInt64) { + out.set(key, number(defaultUsageMaxReadBytes)) + } else { + out.set(key, x) + } + case "appOwnedMemoryBudgetMb": + if x == nil || !validIntRange(x, 64, maxAppOwnedMemoryBudgetMB) { + out.set(key, number(defaultAppOwnedMemoryBudgetMB)) + } else { + out.set(key, x) + } + case "defaultProvider": + if x == nil { + out.set(key, stringValue("openai")) + } else { + out.set(key, x) + } + case "hostname": + if x != nil && x.kind == stringKind && strings.TrimSpace(x.text) != "" { + out.set(key, x) + } + case "upstreamHostCircuitThreshold", "maxUpstreamBodyBytes": + if x != nil && validIntRange(x, 0, math.MaxInt64) { + out.set(key, x) + } + case "providers": + if x != nil && x.kind == objectKind { + out.set(key, normalizeProviders(x)) + } else if x != nil { + out.set(key, x) + } + default: + if x != nil { + out.set(key, x) + } + } + } + for _, m := range in.object { + if !known[m.key] { + out.set(m.key, m.value) + } + } + return out +} +func normalizeProviders(in *value) *value { + out := &value{kind: objectKind} + for _, m := range in.object { + if m.value.kind != objectKind { + out.set(m.key, m.value) + continue + } + p := &value{kind: objectKind} + if x := m.value.find("adapter"); x != nil { + p.set("adapter", x) + } + if x := m.value.find("baseUrl"); x != nil { + p.set("baseUrl", x) + } + for _, field := range m.value.object { + if field.key != "adapter" && field.key != "baseUrl" { + p.set(field.key, field.value) + } + } + out.set(m.key, p) + } + return out +} +func validIntRange(v *value, min, max int64) bool { + if v == nil || v.kind != numberKind { + return false + } + n, err := strconv.ParseInt(v.number.String(), 10, 64) + return err == nil && n >= min && n <= max +} + +func validateTop(v *value) error { + if port := v.find("port"); port != nil { + if port.kind != numberKind { + return errors.New("schema_invalid: port: Invalid input: expected number, received string") + } + n, err := strconv.ParseInt(port.number.String(), 10, 64) + if err != nil { + return errors.New("schema_invalid: port: Invalid input: expected int, received number") + } + if n < 0 { + return errors.New("schema_invalid: port: Too small: expected number to be >=0") + } + if n > 65535 { + return errors.New("schema_invalid: port: Too big: expected number to be <=65535") + } + } + providers := v.find("providers") + if providers == nil { + return errors.New("schema_invalid: providers: Invalid input: expected record, received undefined") + } + if providers.kind != objectKind { + return fmt.Errorf("schema_invalid: providers: Invalid input: expected record, received %s", zodType(providers)) + } + for _, p := range providers.object { + if p.value.kind != objectKind { + return fmt.Errorf("schema_invalid: providers.%s: Invalid input: expected object, received %s", p.key, zodType(p.value)) + } + if x := p.value.find("adapter"); x == nil { + return fmt.Errorf("schema_invalid: providers.%s.adapter: Invalid input: expected string, received undefined", p.key) + } else if x.kind != stringKind { + return fmt.Errorf("schema_invalid: providers.%s.adapter: Invalid input: expected string, received %s", p.key, zodType(x)) + } else if x.text == "" { + return fmt.Errorf("schema_invalid: providers.%s.adapter: Too small: expected string to have >=1 characters", p.key) + } + if x := p.value.find("baseUrl"); x == nil { + return fmt.Errorf("schema_invalid: providers.%s.baseUrl: Invalid input: expected string, received undefined", p.key) + } else if x.kind != stringKind { + return fmt.Errorf("schema_invalid: providers.%s.baseUrl: Invalid input: expected string, received %s", p.key, zodType(x)) + } else if x.text == "" { + return fmt.Errorf("schema_invalid: providers.%s.baseUrl: Too small: expected string to have >=1 characters", p.key) + } + } + if d := v.find("defaultProvider"); d != nil { + if d.kind != stringKind { + return fmt.Errorf("schema_invalid: defaultProvider: Invalid input: expected string, received %s", zodType(d)) + } + if d.text == "" { + return errors.New("schema_invalid: defaultProvider: Too small: expected string to have >=1 characters") + } + } + return nil +} +func zodType(v *value) string { + if v == nil { + return "undefined" + } + switch v.kind { + case nullKind: + return "null" + case boolKind: + return "boolean" + case numberKind: + return "number" + case stringKind: + return "string" + case arrayKind: + return "array" + default: + return "object" + } +} +func (v *value) compact() []byte { var b bytes.Buffer; v.write(&b); return b.Bytes() } +func (v *value) write(b *bytes.Buffer) { + switch v.kind { + case nullKind: + b.WriteString("null") + case boolKind: + if v.b { + b.WriteString("true") + } else { + b.WriteString("false") + } + case numberKind: + b.WriteString(v.number.String()) + case stringKind: + raw, _ := json.Marshal(v.text) + b.Write(raw) + case arrayKind: + b.WriteByte('[') + for i, x := range v.array { + if i > 0 { + b.WriteByte(',') + } + x.write(b) + } + b.WriteByte(']') + case objectKind: + b.WriteByte('{') + for i, m := range v.object { + if i > 0 { + b.WriteByte(',') + } + raw, _ := json.Marshal(m.key) + b.Write(raw) + b.WriteByte(':') + m.value.write(b) + } + b.WriteByte('}') + } +} diff --git a/go/internal/configschema/schema_test.go b/go/internal/configschema/schema_test.go new file mode 100644 index 0000000000..b0350b56b4 --- /dev/null +++ b/go/internal/configschema/schema_test.go @@ -0,0 +1,68 @@ +package configschema + +import ( + "strings" + "testing" +) + +func TestNormalizeInjectsDefaultsInTypeScriptSchemaOrder(t *testing.T) { + normalized, err := NormalizeJSON([]byte(`{"providers":{"acme":{"baseUrl":"https://api.example/v1","adapter":"openai-chat"}},"unknownFuture":true}`)) + if err != nil { + t.Fatalf("NormalizeJSON: %v", err) + } + got, err := normalized.IndentedJSON() + if err != nil { + t.Fatalf("IndentedJSON: %v", err) + } + want := "{\n \"port\": 10100,\n \"managementUsageMaxReadBytes\": 67108864,\n \"appOwnedMemoryBudgetMb\": 256,\n \"providers\": {\n \"acme\": {\n \"adapter\": \"openai-chat\",\n \"baseUrl\": \"https://api.example/v1\"\n }\n },\n \"defaultProvider\": \"openai\",\n \"unknownFuture\": true\n}" + if string(got) != want { + t.Fatalf("normalized JSON mismatch\n got: %s\nwant: %s", got, want) + } +} + +func TestValidateCandidatePortErrorsMatchTypeScript(t *testing.T) { + _, err := ValidateCandidateJSON([]byte(`{"port":-1,"providers":{}}`)) + if err == nil { + t.Fatal("ValidateCandidateJSON unexpectedly succeeded") + } + const want = "schema_invalid: port: Too small: expected number to be >=0" + if err.Error() != want { + t.Fatalf("error = %q, want %q", err, want) + } +} + +func TestValidateCandidateChecksProviderMap(t *testing.T) { + cases := []struct{ name, raw, want string }{ + {"providers must be object", `{"providers":[]}`, "schema_invalid: providers: Invalid input: expected record, received array"}, + {"provider adapter required", `{"providers":{"x":{"baseUrl":"https://x"}}}`, "schema_invalid: providers.x.adapter: Invalid input: expected string, received undefined"}, + {"provider base URL required", `{"providers":{"x":{"adapter":"openai-chat"}}}`, "schema_invalid: providers.x.baseUrl: Invalid input: expected string, received undefined"}, + {"default provider nonblank", `{"providers":{},"defaultProvider":""}`, "schema_invalid: defaultProvider: Too small: expected string to have >=1 characters"}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + _, err := ValidateCandidateJSON([]byte(tc.raw)) + if err == nil || err.Error() != tc.want { + t.Fatalf("error = %v, want %q", err, tc.want) + } + }) + } +} + +func TestNormalizeDropsLoadTimeDegradedOptionals(t *testing.T) { + normalized, err := NormalizeJSON([]byte(`{"hostname":" ","appOwnedMemoryBudgetMb":-1,"upstreamHostCircuitThreshold":-1,"providers":{}}`)) + if err != nil { + t.Fatal(err) + } + compact, err := normalized.CompactJSON() + if err != nil { + t.Fatal(err) + } + for _, forbidden := range []string{"hostname", "upstreamHostCircuitThreshold"} { + if strings.Contains(string(compact), forbidden) { + t.Fatalf("%s survived load normalizer: %s", forbidden, compact) + } + } + if !strings.Contains(string(compact), `"appOwnedMemoryBudgetMb":256`) { + t.Fatalf("app default missing: %s", compact) + } +} From 95df4cd2510d8a1ea62ef976b106960fe9caf5fb Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Mon, 7 Sep 2026 00:47:54 +0800 Subject: [PATCH 065/165] feat(go): port stateful response item id and snapshot repair --- go/internal/sidecar/hotpath_relay.go | 34 +- go/internal/sidecar/hotpath_relay_test.go | 37 +- go/internal/sidecar/responses_pipeline.go | 15 + .../sidecar/responses_stateful_repair.go | 612 ++++++++++++++++++ go/internal/sidecar/sse_stream.go | 42 +- go/internal/sidecar/sse_stream_test.go | 132 ++++ 6 files changed, 847 insertions(+), 25 deletions(-) create mode 100644 go/internal/sidecar/responses_stateful_repair.go diff --git a/go/internal/sidecar/hotpath_relay.go b/go/internal/sidecar/hotpath_relay.go index 545c411d8d..8450d5be67 100644 --- a/go/internal/sidecar/hotpath_relay.go +++ b/go/internal/sidecar/hotpath_relay.go @@ -88,6 +88,7 @@ var relayBlockingRequestHeaders = []string{ // needs to make the direct upstream call for one admitted request. type relayPlan struct { providerName string + provider *jsonwire.Value modelID string reasoning bool endpoint string // full POST target URL @@ -258,6 +259,7 @@ func requestQualifiesForRelay(cfg Config, contentType string, headers http.Heade } plan.modelID = modelID provider := providers.Find(plan.providerName) + plan.provider = provider plan.modelID = selectedRelayModel(provider, modelID) plan.reasoning = providerUsesContentReasoning(provider, modelID) if refusal := unsupportedResponseRepairRefusal(provider); refusal != nil { @@ -299,27 +301,19 @@ func selectedRelayModel(provider *jsonwire.Value, requested string) string { return requested } -// unsupportedResponseRepairRefusal keeps stateful repairs on the TypeScript -// bridge until their per-request state machines are ported. Passing these -// providers through the direct relay would silently emit a different client -// payload, which is worse than the explicit seam fallback. +// unsupportedResponseRepairRefusal contains only repairs the direct relay has +// not ported. Item-id and sparse-snapshot repair are now request-local Go +// state machines, so they deliberately remain direct-relay eligible. func unsupportedResponseRepairRefusal(provider *jsonwire.Value) *relayRefusal { if provider == nil || provider.Kind() != jsonwire.Object { return refuseRelay("response repair provider config unavailable") } - if refusal := statefulResponseRepairRefusal(provider); refusal != nil { - return refusal - } return nil } func statefulResponseRepairRefusal(provider *jsonwire.Value) *relayRefusal { - if repair := provider.Find("responsesItemIdRepair"); responsesItemIDRepairArmed(repair) { - return refuseRelay("provider enables responsesItemIdRepair") - } - if snapshot, ok := boolMember(provider, "responsesSnapshotRepair"); ok && snapshot { - return refuseRelay("provider enables responsesSnapshotRepair") - } + // Stable seam for future stateful repairs. The historical item-id and + // snapshot entries are now handled directly by ResponsesSSEStream. return nil } @@ -698,6 +692,7 @@ func doDirectRelay(w http.ResponseWriter, r *http.Request, cfg Config, plan *rel if requestRoot != nil { pipeline.imageAliases = imageAliasesFromRequest(requestRoot) } + configureStatefulResponseRepairs(&pipeline, plan.provider, requestRoot) if err := relayResponsesSSEWithFlush(w, upstreamResp.Body, pipeline); err != nil { fmt.Fprintf(os.Stderr, "ocx-sidecar: relay stream write: %v\n", err) } @@ -736,6 +731,7 @@ func doDirectRelay(w http.ResponseWriter, r *http.Request, cfg Config, plan *rel if requestRoot != nil { pipeline.imageAliases = imageAliasesFromRequest(requestRoot) } + configureStatefulResponseRepairs(&pipeline, plan.provider, requestRoot) out, _ = pipeline.repairJSON(rawBody) } } @@ -745,6 +741,18 @@ func doDirectRelay(w http.ResponseWriter, r *http.Request, cfg Config, plan *rel } } +func configureStatefulResponseRepairs(pipeline *responseRepairPipeline, provider, request *jsonwire.Value) { + if pipeline == nil || provider == nil { + return + } + if config, enabled := itemIDRepairConfigFromProvider(provider); enabled { + pipeline.itemIDs = newItemIDRepairState(config) + } + if enabled, _ := boolMember(provider, "responsesSnapshotRepair"); enabled { + pipeline.snapshot = newSnapshotRepairState(true, request) + } +} + // relayResponsesSSEWithFlush feeds upstream transport chunks through the // Responses field-backfill and terminal boundary, flushing each emitted block. // It stops reading after the first terminal so a gateway cannot append frames diff --git a/go/internal/sidecar/hotpath_relay_test.go b/go/internal/sidecar/hotpath_relay_test.go index 0801dcd360..b516dcef22 100644 --- a/go/internal/sidecar/hotpath_relay_test.go +++ b/go/internal/sidecar/hotpath_relay_test.go @@ -278,6 +278,35 @@ func TestDirectRelayStreamingRelaySafeRequest(t *testing.T) { } } +func TestDirectRelayStreamsStatefulResponseRepairs(t *testing.T) { + upstreamFixture := "data: {\"type\":\"response.output_item.added\",\"output_index\":0,\"item\":{\"type\":\"message\",\"id\":\"bare-id\"}}\n\ndata: {\"type\":\"response.output_text.delta\",\"output_index\":0,\"item_id\":\"bare-id\",\"delta\":\"hi\"}\n\ndata: {\"type\":\"response.completed\",\"response\":{\"id\":\"r\"}}\n\n" + upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/event-stream") + _, _ = w.Write([]byte(upstreamFixture)) + })) + defer upstream.Close() + configDir := relayFixtureConfigDir(t, upstream.URL, map[string]any{"responsesItemIdRepair": map[string]any{"repairInvalidIds": true}, "responsesSnapshotRepair": true}) + rec, err := relayPost(t, relaySeamHandler(t, configDir, true), "{\"model\":\"test-model\",\"input\":\"ping\",\"stream\":true}") + if err != nil { + t.Fatal(err) + } + if rec.Code != http.StatusOK { + t.Fatalf("status = %d", rec.Code) + } + body := rec.Body.String() + if strings.Contains(body, "bare-id") { + t.Fatalf("item id was not repaired: %s", body) + } + for _, want := range []string{"response.content_part.added", "response.output_text.done", "response.content_part.done", "response.output_item.done"} { + if !strings.Contains(body, want) { + t.Fatalf("missing %s: %s", want, body) + } + } + if strings.Index(body, "response.output_item.done") > strings.Index(body, "response.completed") { + t.Fatalf("snapshot closing event appeared after terminal: %s", body) + } +} + func TestDirectRelayStreamingConfigGatesFallBackToBridge(t *testing.T) { upstream := deadSparseUpstream(t, nil) defer upstream.Close() @@ -286,9 +315,9 @@ func TestDirectRelayStreamingConfigGatesFallBackToBridge(t *testing.T) { extra map[string]any want int }{ - {"materially armed item ID repair", map[string]any{"responsesItemIdRepair": map[string]any{"repairInvalidIds": true}}, http.StatusServiceUnavailable}, + {"materially armed item ID repair", map[string]any{"responsesItemIdRepair": map[string]any{"repairInvalidIds": true}}, http.StatusOK}, {"empty item ID repair remains relay-safe", map[string]any{"responsesItemIdRepair": map[string]any{"message": []any{}}}, http.StatusOK}, - {"snapshot repair", map[string]any{"responsesSnapshotRepair": true}, http.StatusServiceUnavailable}, + {"snapshot repair", map[string]any{"responsesSnapshotRepair": true}, http.StatusOK}, {"stateless Responses", map[string]any{"statelessResponses": true}, http.StatusServiceUnavailable}, {"preserved reasoning model is case-insensitive", map[string]any{"preserveReasoningContentModels": []any{"TEST-MODEL"}}, http.StatusServiceUnavailable}, {"different preserved reasoning model remains relay-safe", map[string]any{"preserveReasoningContentModels": []any{"other-model"}}, http.StatusOK}, @@ -316,9 +345,9 @@ func TestStreamRelayQualificationConfigGates(t *testing.T) { want string }{ {"narrow stream qualifies", nil, ""}, - {"materially armed item ID repair refuses", map[string]any{"responsesItemIdRepair": map[string]any{"repairMissingTerminalIds": true}}, "responsesItemIdRepair"}, + {"materially armed item ID repair qualifies", map[string]any{"responsesItemIdRepair": map[string]any{"repairMissingTerminalIds": true}}, ""}, {"empty item ID repair qualifies", map[string]any{"responsesItemIdRepair": map[string]any{"reasoning": []any{}}}, ""}, - {"snapshot repair refuses", map[string]any{"responsesSnapshotRepair": true}, "responsesSnapshotRepair"}, + {"snapshot repair qualifies", map[string]any{"responsesSnapshotRepair": true}, ""}, {"stateless Responses refuses", map[string]any{"statelessResponses": true}, "statelessResponses"}, {"preserved reasoning model match is case-insensitive", map[string]any{"preserveReasoningContentModels": []any{"TEST-MODEL"}}, "preserves reasoning content"}, {"different preserved reasoning model qualifies", map[string]any{"preserveReasoningContentModels": []any{"other-model"}}, ""}, diff --git a/go/internal/sidecar/responses_pipeline.go b/go/internal/sidecar/responses_pipeline.go index 83c7004a17..c30bf2b6c7 100644 --- a/go/internal/sidecar/responses_pipeline.go +++ b/go/internal/sidecar/responses_pipeline.go @@ -15,6 +15,9 @@ type responseRepairPipeline struct { modelID string imageAliases map[string]imageAlias reasoning bool + itemIDs *itemIDRepairState + snapshot *snapshotRepairState + request *jsonwire.Value } type imageAlias struct{ name, namespace string } @@ -25,6 +28,18 @@ func (p responseRepairPipeline) repairJSON(raw []byte) ([]byte, bool) { return raw, false } changed := p.repairPayload(root) + if p.itemIDs != nil { + // Whole JSON replies have no event lifecycle, but use the identical + // per-response mapping rule as TypeScript's repairResponsesJsonItemIds. + if output := root.Find("output"); output != nil && output.Kind() == jsonwire.Array { + for index, item := range output.Elements() { + changed = p.itemIDs.rewriteItem(itoa(index), item) || changed + } + } + } + if p.snapshot != nil && p.snapshot.enabled { + changed = p.snapshot.repairJSON(root) || changed + } changed = backfillResponsesJSON(root) || changed if !changed { return raw, false diff --git a/go/internal/sidecar/responses_stateful_repair.go b/go/internal/sidecar/responses_stateful_repair.go new file mode 100644 index 0000000000..1073acd518 --- /dev/null +++ b/go/internal/sidecar/responses_stateful_repair.go @@ -0,0 +1,612 @@ +package sidecar + +// Stateful Responses repairs. Both repair objects are owned by one +// ResponsesSSEStream: their maps are deliberately request-local, just as the +// TypeScript payload/block rewrite closures are. Nothing here survives an SSE +// stream or leaks synthetic client ids into continuation state. + +import ( + "crypto/rand" + "encoding/hex" + "sort" + "strconv" + + "github.com/lidge-jun/opencodex/go/internal/jsonwire" +) + +type itemIDRepairConfig struct { + message, reasoning map[string]bool + missing, invalid bool +} + +func itemIDRepairConfigFromProvider(provider *jsonwire.Value) (itemIDRepairConfig, bool) { + raw := provider.Find("responsesItemIdRepair") + if raw == nil || raw.Kind() != jsonwire.Object { + return itemIDRepairConfig{}, false + } + c := itemIDRepairConfig{message: map[string]bool{}, reasoning: map[string]bool{}} + c.missing, _ = boolMember(raw, "repairMissingTerminalIds") + c.invalid, _ = boolMember(raw, "repairInvalidIds") + for _, row := range []struct { + key string + into map[string]bool + }{{"message", c.message}, {"reasoning", c.reasoning}} { + if values := raw.Find(row.key); values != nil && values.Kind() == jsonwire.Array { + for _, value := range values.Elements() { + if value != nil && value.Kind() == jsonwire.String && value.String() != "" { + row.into[value.String()] = true + } + } + } + } + return c, c.missing || c.invalid || len(c.message) > 0 || len(c.reasoning) > 0 +} + +type itemIDRepairState struct { + config itemIDRepairConfig + scope string + output map[string]map[string]string // item type -> output_index -> canonical id + raw map[string]string // output_index + NUL + upstream id -> canonical id +} + +func newItemIDRepairState(config itemIDRepairConfig) *itemIDRepairState { + seed := make([]byte, 16) + if _, err := rand.Read(seed); err != nil { // crypto/rand failure must not make ids collide across streams. + seed = []byte(strconv.FormatInt(syntheticSSEItemOrdinal.Add(1), 10)) + } + return &itemIDRepairState{config: config, scope: hex.EncodeToString(seed), output: map[string]map[string]string{"message": {}, "reasoning": {}}, raw: map[string]string{}} +} + +func repairableItemType(v *jsonwire.Value) (string, bool) { + t, ok := stringMember(v, "type") + return t, ok && (t == "message" || t == "reasoning") +} +func canonicalItemID(typ, scope, index string) string { + prefix := "msg_" + if typ == "reasoning" { + prefix = "rs_" + } + return prefix + "ocx_" + scope + "_" + index +} +func itemRawKey(index, id string) string { return index + "@" + id } + +// remember maps an item only when the TypeScript policy would map it. A valid +// existing id is remembered only for missing-terminal-id repair; that enables +// a later id-less terminal event without rewriting the item itself. +func (s *itemIDRepairState) remember(index string, item *jsonwire.Value) string { + typ, ok := repairableItemType(item) + if !ok { + return "" + } + if got := s.output[typ][index]; got != "" { + return got + } + raw, ok := stringMember(item, "id") + if !ok || raw == "" { + return "" + } + placeholders := s.config.message + if typ == "reasoning" { + placeholders = s.config.reasoning + } + mapped := "" + if placeholders[raw] || (s.config.invalid && !hasPrefixForItem(typ, raw)) { + mapped = canonicalItemID(typ, s.scope, index) + } else if s.config.missing { + mapped = raw + } + if mapped == "" { + return "" + } + s.output[typ][index] = mapped + if mapped != raw { + s.raw[itemRawKey(index, raw)] = mapped + } + return mapped +} +func hasPrefixForItem(typ, id string) bool { + return (typ == "message" && len(id) >= 4 && id[:4] == "msg_") || (typ == "reasoning" && len(id) >= 3 && id[:3] == "rs_") +} + +func (s *itemIDRepairState) rewriteItem(index string, item *jsonwire.Value) bool { + mapped := s.remember(index, item) + if mapped == "" { + return false + } + current, present := stringMember(item, "id") + if (present && current == mapped) || (!present && !s.config.missing) { + return false + } + item.Set("id", jsonwire.StringValue(mapped)) + return true +} +func (s *itemIDRepairState) rewriteEvent(event *jsonwire.Value) bool { + index, ok := sseOutputIndex(event.Find("output_index")) + changed := false + if ok { + if item := event.Find("item"); item != nil && item.Kind() == jsonwire.Object { + changed = s.rewriteItem(index, item) + } + if current, has := stringMember(event, "item_id"); has { + if mapped := s.raw[itemRawKey(index, current)]; mapped != "" && mapped != current { + event.Set("item_id", jsonwire.StringValue(mapped)) + changed = true + } + } else if s.config.missing { + typ := itemEventType(event) + if typ != "" { + if mapped := s.output[typ][index]; mapped != "" { + event.Set("item_id", jsonwire.StringValue(mapped)) + changed = true + } + } + } + } + if response := event.Find("response"); response != nil && response.Kind() == jsonwire.Object { + if output := response.Find("output"); output != nil && output.Kind() == jsonwire.Array { + for i, item := range output.Elements() { + changed = s.rewriteItem(itoa(i), item) || changed + } + } + } + return changed +} +func itemEventType(event *jsonwire.Value) string { + t, _ := stringMember(event, "type") + switch t { + case "response.content_part.added", "response.content_part.done", "response.output_text.annotation.added", "response.output_text.delta", "response.output_text.done", "response.refusal.delta", "response.refusal.done": + return "message" + case "response.reasoning_summary_part.added", "response.reasoning_summary_part.done", "response.reasoning_summary_text.delta", "response.reasoning_summary_text.done", "response.reasoning_text.delta", "response.reasoning_text.done": + return "reasoning" + } + return "" +} + +// Snapshot repair carries only the state required to close sparse message +// lifecycles. Existing upstream output-item.done is authoritative; synthesis +// happens only at response.completed and only for a proven open message. +type snapshotOpenItem struct { + id, index string + typ string + injectable bool + item *jsonwire.Value + text string + contentOpen, textDone, partDone bool +} +type snapshotRepairState struct { + enabled bool + open map[string]*snapshotOpenItem + completed map[string]*jsonwire.Value + tainted bool + parallel bool + choice *jsonwire.Value + tools *jsonwire.Value +} + +const ( + maxSnapshotOpenItems = 10_000 + maxSnapshotTextBytes = 8 * 1024 * 1024 +) + +func newSnapshotRepairState(enabled bool, request *jsonwire.Value) *snapshotRepairState { + s := &snapshotRepairState{enabled: enabled, open: map[string]*snapshotOpenItem{}, completed: map[string]*jsonwire.Value{}, parallel: true} + if request == nil || request.Kind() != jsonwire.Object { + return s + } + if value := request.Find("parallel_tool_calls"); value != nil && value.Kind() == jsonwire.Bool { + s.parallel = value.Bool() + } + if value := request.Find("tool_choice"); snapshotToolChoiceValid(value) { + s.choice = value + } + if value := request.Find("tools"); value != nil && value.Kind() == jsonwire.Array { + s.tools = value + } + return s +} + +func snapshotToolChoiceValid(value *jsonwire.Value) bool { + if value == nil { + return false + } + if value.Kind() == jsonwire.String { + return value.String() != "" + } + if value.Kind() == jsonwire.Object { + typ, ok := stringMember(value, "type") + return ok && typ != "" + } + return false +} + +func (s *snapshotRepairState) rewrite(event *jsonwire.Value) ([]*jsonwire.Value, bool) { + if !s.enabled { + return nil, false + } + typ, _ := stringMember(event, "type") + changed := false + if response := event.Find("response"); response != nil && response.Kind() == jsonwire.Object { + changed = s.repairResponse(response, snapshotResponseStatus(typ)) || changed + } + index, validIndex := sseOutputIndex(event.Find("output_index")) + if typ == "response.output_item.added" && validIndex { + if item := event.Find("item"); item != nil && item.Kind() == jsonwire.Object { + changed = snapshotRepairOutputItem(item, "in_progress") || changed + if s.open[index] != nil { + s.taint() + } + if len(s.open) >= maxSnapshotOpenItems { + s.taint() + } + if id, ok := stringMember(item, "id"); ok && id != "" { + kind, _ := stringMember(item, "type") + if !s.tainted { + s.open[index] = &snapshotOpenItem{id: id, index: index, typ: kind, injectable: kind == "message" || kind == "reasoning", item: item} + } + } else { + s.taint() + } + } + } + if typ == "response.output_item.done" && validIndex { + if item := event.Find("item"); item != nil && item.Kind() == jsonwire.Object { + changed = snapshotRepairOutputItem(item, "completed") || changed + if open := s.open[index]; open != nil { + doneID, idOK := stringMember(item, "id") + doneType, typeOK := stringMember(item, "type") + if !idOK || !typeOK || doneID != open.id || doneType != open.typ { + s.taint() + } else if !s.tainted { + s.completed[index] = item + delete(s.open, index) + } + } else if !s.tainted { + s.taint() + } + } else { + s.taint() + } + } + if open := s.open[index]; validIndex && open != nil { + itemID, hasItemID := stringMember(event, "item_id") + if hasItemID && itemID != open.id { + s.taint() + return nil, changed + } + if typ == "response.content_part.added" { + open.contentOpen = true + } + if typ == "response.content_part.done" { + open.partDone = true + } + if typ == "response.output_text.delta" && open.typ == "message" { + if logprobs := event.Find("logprobs"); logprobs == nil || logprobs.Kind() != jsonwire.Array { + event.Set("logprobs", jsonwire.EmptyArray()) + changed = true + } + if text, ok := stringMember(event, "delta"); ok { + open.text += text + if len(open.text) > maxSnapshotTextBytes { + s.taint() + return nil, changed + } + } + if !open.contentOpen { + open.contentOpen = true + return []*jsonwire.Value{snapshotContentAdded(open)}, changed + } + } + if typ == "response.output_text.done" && open.typ == "message" { + if logprobs := event.Find("logprobs"); logprobs == nil || logprobs.Kind() != jsonwire.Array { + event.Set("logprobs", jsonwire.EmptyArray()) + changed = true + } + open.textDone = true + if text, ok := stringMember(event, "text"); ok { + open.text = text + if len(open.text) > maxSnapshotTextBytes { + s.taint() + return nil, changed + } + } + } + } + if typ == "response.content_part.added" || typ == "response.content_part.done" || typ == "response.reasoning_summary_part.added" || typ == "response.reasoning_summary_part.done" { + changed = snapshotRepairPart(event.Find("part")) || changed + } + if typ != "response.completed" { + return nil, changed + } + if s.tainted { + return nil, changed + } + keys := make([]string, 0, len(s.open)) + for index := range s.open { + keys = append(keys, index) + } + sort.Slice(keys, func(i, j int) bool { + left, _ := strconv.ParseFloat(keys[i], 64) + right, _ := strconv.ParseFloat(keys[j], 64) + return left < right + }) + var injected []*jsonwire.Value + canReconstruct := true + for _, index := range keys { + open := s.open[index] + if !open.injectable { + canReconstruct = false + continue + } + if open.typ == "message" && !open.contentOpen { + injected = append(injected, snapshotContentAdded(open)) + } + if open.typ == "message" && !open.textDone { + injected = append(injected, snapshotTextDone(open)) + } + if open.typ == "message" && !open.partDone { + injected = append(injected, snapshotPartDone(open)) + } + injected = append(injected, snapshotItemDone(open)) + s.completed[index] = snapshotCompletedItem(open) + } + if response := event.Find("response"); canReconstruct && response != nil && response.Kind() == jsonwire.Object { + if output := response.Find("output"); output == nil || output.Kind() != jsonwire.Array { + ordered := make([]string, 0, len(s.completed)) + for index := range s.completed { + ordered = append(ordered, index) + } + sort.Slice(ordered, func(i, j int) bool { + left, _ := strconv.ParseFloat(ordered[i], 64) + right, _ := strconv.ParseFloat(ordered[j], 64) + return left < right + }) + contiguous := true + for position, index := range ordered { + value, err := strconv.ParseFloat(index, 64) + if err != nil || value != float64(position) { + contiguous = false + break + } + } + if contiguous { + output = jsonwire.EmptyArray() + for _, index := range ordered { + output.AppendArray(s.completed[index]) + } + response.Set("output", output) + changed = true + } + } + } + s.open = map[string]*snapshotOpenItem{} + s.completed = map[string]*jsonwire.Value{} + return injected, changed +} + +func (s *snapshotRepairState) taint() { + s.open = map[string]*snapshotOpenItem{} + s.completed = map[string]*jsonwire.Value{} + s.tainted = true +} + +func snapshotResponseStatus(typ string) string { + switch typ { + case "response.created", "response.in_progress": + return "in_progress" + case "response.queued": + return "queued" + case "response.failed": + return "failed" + case "response.incomplete": + return "incomplete" + } + return "completed" +} + +func (s *snapshotRepairState) repairResponse(response *jsonwire.Value, fallback string) bool { + changed := false + if status, ok := stringMember(response, "status"); !ok || status == "" { + response.Set("status", jsonwire.StringValue(fallback)) + changed = true + } + if value := response.Find("parallel_tool_calls"); value == nil || value.Kind() != jsonwire.Bool { + response.Set("parallel_tool_calls", jsonwire.BoolValue(s.parallel)) + changed = true + } + if !snapshotToolChoiceValid(response.Find("tool_choice")) { + if s.choice != nil { + response.Set("tool_choice", s.choice) + } else { + response.Set("tool_choice", jsonwire.StringValue("auto")) + } + changed = true + } + if value := response.Find("tools"); value == nil || value.Kind() != jsonwire.Array { + if s.tools != nil { + response.Set("tools", s.tools) + } else { + response.Set("tools", jsonwire.EmptyArray()) + } + changed = true + } + if output := response.Find("output"); output != nil && output.Kind() == jsonwire.Array { + status, _ := stringMember(response, "status") + inferred, _ := responseStatusToItemStatus(status) + for i, item := range output.Elements() { + changed = backfillSSEOutputItem(item, itoa(i), inferred) || changed + } + } + return changed +} + +func snapshotRepairOutputItem(item *jsonwire.Value, status string) bool { + if item == nil || item.Kind() != jsonwire.Object { + return false + } + kind, _ := stringMember(item, "type") + changed := false + if kind == "message" { + if role, ok := stringMember(item, "role"); !ok || role != "assistant" { + item.Set("role", jsonwire.StringValue("assistant")) + changed = true + } + if content := item.Find("content"); content == nil || content.Kind() != jsonwire.Array { + item.Set("content", jsonwire.EmptyArray()) + changed = true + } else { + for _, part := range content.Elements() { + changed = snapshotRepairPart(part) || changed + } + } + } else if kind == "reasoning" { + if summary := item.Find("summary"); summary == nil || summary.Kind() != jsonwire.Array { + item.Set("summary", jsonwire.EmptyArray()) + changed = true + } else { + for _, part := range summary.Elements() { + changed = snapshotRepairPart(part) || changed + } + } + } + if status != "" { + if current, ok := stringMember(item, "status"); !ok || current == "" { + item.Set("status", jsonwire.StringValue(status)) + changed = true + } + } + return changed +} + +func snapshotRepairPart(part *jsonwire.Value) bool { + if part == nil || part.Kind() != jsonwire.Object { + return false + } + typ, _ := stringMember(part, "type") + changed := false + if typ == "output_text" { + if text := part.Find("text"); text == nil || text.Kind() != jsonwire.String { + part.Set("text", jsonwire.StringValue("")) + changed = true + } + if annotations := part.Find("annotations"); annotations == nil || annotations.Kind() != jsonwire.Array { + part.Set("annotations", jsonwire.EmptyArray()) + changed = true + } + } + if typ == "summary_text" { + if text := part.Find("text"); text == nil || text.Kind() != jsonwire.String { + part.Set("text", jsonwire.StringValue("")) + changed = true + } + } + return changed +} +func snapshotEvent(typ string, open *snapshotOpenItem) *jsonwire.Value { + v := jsonwire.ObjectValue() + v.Set("type", jsonwire.StringValue(typ)) + v.Set("item_id", jsonwire.StringValue(open.id)) + v.Set("output_index", jsonwire.NumberFrom(mustParseIndex(open.index))) + v.Set("content_index", jsonwire.NumberFrom(0)) + return v +} +func mustParseIndex(index string) float64 { n, _ := strconv.ParseFloat(index, 64); return n } +func snapshotPart(text string) *jsonwire.Value { + p := jsonwire.ObjectValue() + p.Set("type", jsonwire.StringValue("output_text")) + p.Set("text", jsonwire.StringValue(text)) + p.Set("annotations", jsonwire.EmptyArray()) + return p +} +func snapshotContentAdded(o *snapshotOpenItem) *jsonwire.Value { + v := snapshotEvent("response.content_part.added", o) + v.Set("part", snapshotPart("")) + return v +} +func snapshotTextDone(o *snapshotOpenItem) *jsonwire.Value { + v := snapshotEvent("response.output_text.done", o) + v.Set("logprobs", jsonwire.EmptyArray()) + v.Set("text", jsonwire.StringValue(o.text)) + return v +} +func snapshotPartDone(o *snapshotOpenItem) *jsonwire.Value { + v := snapshotEvent("response.content_part.done", o) + v.Set("part", snapshotPart(o.text)) + return v +} +func snapshotItemDone(o *snapshotOpenItem) *jsonwire.Value { + v := jsonwire.ObjectValue() + v.Set("type", jsonwire.StringValue("response.output_item.done")) + v.Set("output_index", jsonwire.NumberFrom(mustParseIndex(o.index))) + item := jsonwire.ObjectValue() + item.Set("type", jsonwire.StringValue(o.typ)) + item.Set("id", jsonwire.StringValue(o.id)) + item.Set("status", jsonwire.StringValue("completed")) + if o.typ == "message" { + item.Set("role", jsonwire.StringValue("assistant")) + content := jsonwire.EmptyArray() + content.AppendArray(snapshotPart(o.text)) + item.Set("content", content) + } else { + item.Set("summary", jsonwire.EmptyArray()) + } + v.Set("item", item) + return v +} + +func snapshotCompletedItem(o *snapshotOpenItem) *jsonwire.Value { + if o.typ == "reasoning" { + item := jsonwire.ObjectValue() + item.Set("type", jsonwire.StringValue("reasoning")) + item.Set("id", jsonwire.StringValue(o.id)) + item.Set("status", jsonwire.StringValue("completed")) + item.Set("summary", jsonwire.EmptyArray()) + return item + } + return snapshotItemDone(o).Find("item") +} + +// repairJSON mirrors repairResponsesSnapshotJson: output is canonicalized to +// [] when absent or malformed, request defaults are copied into the response, +// and present output items receive the same field repair as streamed snapshots. +func (s *snapshotRepairState) repairJSON(root *jsonwire.Value) bool { + if root == nil || root.Kind() != jsonwire.Object { + return false + } + changed := false + if output := root.Find("output"); output == nil || output.Kind() != jsonwire.Array { + root.Set("output", jsonwire.EmptyArray()) + changed = true + } + if value := root.Find("parallel_tool_calls"); value == nil || value.Kind() != jsonwire.Bool { + root.Set("parallel_tool_calls", jsonwire.BoolValue(s.parallel)) + changed = true + } + if !snapshotToolChoiceValid(root.Find("tool_choice")) { + if s.choice != nil { + root.Set("tool_choice", s.choice) + } else { + root.Set("tool_choice", jsonwire.StringValue("auto")) + } + changed = true + } + if value := root.Find("tools"); value == nil || value.Kind() != jsonwire.Array { + if s.tools != nil { + root.Set("tools", s.tools) + } else { + root.Set("tools", jsonwire.EmptyArray()) + } + changed = true + } + if status, ok := stringMember(root, "status"); !ok || status == "" { + root.Set("status", jsonwire.StringValue("completed")) + changed = true + } + status, _ := stringMember(root, "status") + inferred, _ := responseStatusToItemStatus(status) + if output := root.Find("output"); output != nil && output.Kind() == jsonwire.Array { + for _, item := range output.Elements() { + changed = snapshotRepairOutputItem(item, inferred) || changed + } + } + return changed +} diff --git a/go/internal/sidecar/sse_stream.go b/go/internal/sidecar/sse_stream.go index 5280b25714..1ab69eb89b 100644 --- a/go/internal/sidecar/sse_stream.go +++ b/go/internal/sidecar/sse_stream.go @@ -156,8 +156,10 @@ func (s *ResponsesSSEStream) processFrame(out *bytes.Buffer, frame sseFrame) { return } rewritten := s.rewriteBlock(frame.block, payload, hasData) - out.Write(rewritten) - out.Write(frame.delimiter) + for _, block := range rewritten { + out.Write(block) + out.Write(frame.delimiter) + } if hasData && responsesSSETerminal(payload) { s.terminal = true for _, pending := range s.pendingDone { @@ -229,24 +231,48 @@ func sseDataPayloadBytes(block []byte) ([]byte, bool) { return payload, found } -func (s *ResponsesSSEStream) rewriteBlock(block, payload []byte, hasData bool) []byte { +func (s *ResponsesSSEStream) rewriteBlock(block, payload []byte, hasData bool) [][]byte { if !hasData { - return block + return [][]byte{block} } event, err := jsonwire.Parse(payload) if err != nil || event.Kind() != jsonwire.Object { - return block + return [][]byte{block} } changed := s.pipeline.repairPayload(event) + if s.pipeline.itemIDs != nil { + changed = s.pipeline.itemIDs.rewriteEvent(event) || changed + } changed = s.rewriteEvent(event) || changed + var injected []*jsonwire.Value + if s.pipeline.snapshot != nil { + var snapshotChanged bool + injected, snapshotChanged = s.pipeline.snapshot.rewrite(event) + changed = snapshotChanged || changed + } if !changed { - return block + if len(injected) == 0 { + return [][]byte{block} + } + out := make([][]byte, 0, len(injected)+1) + for _, value := range injected { + if encoded, err := value.Encode(); err == nil { + out = append(out, append([]byte("data: "), encoded...)) + } + } + return append(out, block) } encoded, err := event.Encode() if err != nil { - return block + return [][]byte{block} + } + out := make([][]byte, 0, len(injected)+1) + for _, value := range injected { + if encoded, err := value.Encode(); err == nil { + out = append(out, append([]byte("data: "), encoded...)) + } } - return replaceSSEDataPayload(block, encoded) + return append(out, replaceSSEDataPayload(block, encoded)) } func (s *ResponsesSSEStream) rewriteEvent(event *jsonwire.Value) bool { diff --git a/go/internal/sidecar/sse_stream_test.go b/go/internal/sidecar/sse_stream_test.go index 2928a7876d..104719dc67 100644 --- a/go/internal/sidecar/sse_stream_test.go +++ b/go/internal/sidecar/sse_stream_test.go @@ -6,7 +6,10 @@ import ( "errors" "os" "path/filepath" + "strings" "testing" + + "github.com/lidge-jun/opencodex/go/internal/jsonwire" ) type responsesSSEGolden struct { @@ -91,6 +94,135 @@ func TestResponsesSSEGoldens(t *testing.T) { } } +func TestResponsesSSEItemIDRepairCarriesMappingAcrossOneStream(t *testing.T) { + p := responseRepairPipeline{itemIDs: newItemIDRepairState(itemIDRepairConfig{invalid: true, message: map[string]bool{}, reasoning: map[string]bool{}})} + input := "data: {\"type\":\"response.output_item.added\",\"output_index\":0,\"item\":{\"type\":\"message\",\"id\":\"bare-upstream\"}}\n\ndata: {\"type\":\"response.output_text.delta\",\"output_index\":0,\"item_id\":\"bare-upstream\",\"delta\":\"hi\"}\n\ndata: {\"type\":\"response.completed\"}\n\n" + got, _ := rewriteSSEInChunksWithPipeline(t, input, []int{17, 9, len(input)}, p) + if bytes.Contains([]byte(got), []byte("bare-upstream")) { + t.Fatalf("raw id leaked: %s", got) + } + if bytes.Count([]byte(got), []byte("msg_ocx_")) != 2 { + t.Fatalf("item and delta must share one canonical id: %s", got) + } +} + +func TestResponsesSSEItemIDRepairRewritesTerminalSnapshotWithoutOutputIndex(t *testing.T) { + p := responseRepairPipeline{itemIDs: newItemIDRepairState(itemIDRepairConfig{invalid: true, message: map[string]bool{}, reasoning: map[string]bool{}})} + input := "data: {\"type\":\"response.completed\",\"response\":{\"output\":[{\"type\":\"message\",\"id\":\"bare-id\"}]}}\n\n" + got, _ := rewriteSSEInChunksWithPipeline(t, input, []int{len(input)}, p) + if strings.Contains(got, "bare-id") || !strings.Contains(got, "msg_ocx_") { + t.Fatalf("terminal snapshot id was not repaired: %s", got) + } +} + +func TestResponsesSSESnapshotRepairClosesOpenMessageBeforeTerminal(t *testing.T) { + p := responseRepairPipeline{snapshot: newSnapshotRepairState(true, nil)} + input := "data: {\"type\":\"response.output_item.added\",\"output_index\":0,\"item\":{\"type\":\"message\",\"id\":\"msg_a\"}}\n\ndata: {\"type\":\"response.output_text.delta\",\"output_index\":0,\"item_id\":\"msg_a\",\"delta\":\"hi\"}\n\ndata: {\"type\":\"response.completed\",\"response\":{\"status\":\"completed\"}}\n\n" + got, _ := rewriteSSEInChunksWithPipeline(t, input, []int{len(input)}, p) + for _, expected := range []string{"response.content_part.added", "response.output_text.done", "response.content_part.done", "response.output_item.done"} { + if !bytes.Contains([]byte(got), []byte(expected)) { + t.Fatalf("missing %s: %s", expected, got) + } + } + if bytes.Index([]byte(got), []byte("response.output_item.done")) > bytes.Index([]byte(got), []byte("response.completed")) { + t.Fatalf("completion injected after terminal: %s", got) + } +} + +func TestResponsesSSESnapshotRepairUsesRequestDefaultsAndRebuildsTerminalOutput(t *testing.T) { + request, err := jsonwire.Parse([]byte("{\"parallel_tool_calls\":false,\"tool_choice\":{\"type\":\"function\",\"name\":\"search\"},\"tools\":[{\"type\":\"function\",\"name\":\"search\"}]}")) + if err != nil { + t.Fatal(err) + } + p := responseRepairPipeline{snapshot: newSnapshotRepairState(true, request)} + input := "data: {\"type\":\"response.created\",\"response\":{\"id\":\"r\"}}\n\ndata: {\"type\":\"response.output_item.added\",\"output_index\":0,\"item\":{\"type\":\"message\",\"id\":\"msg_a\"}}\n\ndata: {\"type\":\"response.output_text.delta\",\"output_index\":0,\"item_id\":\"msg_a\",\"delta\":\"hi\"}\n\ndata: {\"type\":\"response.completed\",\"response\":{\"id\":\"r\"}}\n\n" + got, _ := rewriteSSEInChunksWithPipeline(t, input, []int{len(input)}, p) + if !bytes.Contains([]byte(got), []byte("\"parallel_tool_calls\":false")) || !bytes.Contains([]byte(got), []byte("\"tool_choice\":{\"type\":\"function\",\"name\":\"search\"}")) { + t.Fatalf("request defaults missing: %s", got) + } + terminalAt := bytes.LastIndex([]byte(got), []byte("\"type\":\"response.completed\"")) + if terminalAt < 0 { + t.Fatalf("no terminal: %s", got) + } + if !bytes.Contains([]byte(got[terminalAt:]), []byte("\"output\":[{\"type\":\"message\",\"id\":\"msg_a\",\"status\":\"completed\"")) { + t.Fatalf("terminal did not reconstruct output: %s", got) + } +} + +func TestResponseRepairPipelineSnapshotJSONUsesRequestDefaults(t *testing.T) { + request, err := jsonwire.Parse([]byte("{\"parallel_tool_calls\":false,\"tool_choice\":{\"type\":\"function\",\"name\":\"search\"},\"tools\":[{\"type\":\"function\",\"name\":\"search\"}]}")) + if err != nil { + t.Fatal(err) + } + p := responseRepairPipeline{snapshot: newSnapshotRepairState(true, request)} + got, changed := p.repairJSON([]byte("{\"id\":\"r\",\"output\":\"bad\"}")) + if !changed { + t.Fatal("snapshot JSON should repair") + } + want := "{\"id\":\"r\",\"output\":[],\"parallel_tool_calls\":false,\"tool_choice\":{\"type\":\"function\",\"name\":\"search\"},\"tools\":[{\"type\":\"function\",\"name\":\"search\"}],\"status\":\"completed\"}" + if string(got) != want { + t.Fatalf("got %s\nwant %s", got, want) + } +} + +func TestResponsesSSESnapshotRepairRetainsClosedItemsForTerminalReconstruction(t *testing.T) { + p := responseRepairPipeline{snapshot: newSnapshotRepairState(true, nil)} + input := "data: {\"type\":\"response.output_item.added\",\"output_index\":0,\"item\":{\"type\":\"message\",\"id\":\"msg_0\"}}\n\ndata: {\"type\":\"response.output_item.done\",\"output_index\":0,\"item\":{\"type\":\"message\",\"id\":\"msg_0\"}}\n\ndata: {\"type\":\"response.output_item.added\",\"output_index\":1,\"item\":{\"type\":\"message\",\"id\":\"msg_1\"}}\n\ndata: {\"type\":\"response.completed\",\"response\":{\"id\":\"r\"}}\n\n" + got, _ := rewriteSSEInChunksWithPipeline(t, input, []int{len(input)}, p) + terminalAt := bytes.LastIndex([]byte(got), []byte("\"type\":\"response.completed\"")) + terminal := got[terminalAt:] + if !strings.Contains(terminal, "\"id\":\"msg_0\"") || !strings.Contains(terminal, "\"id\":\"msg_1\"") { + t.Fatalf("terminal lost a completed item: %s", got) + } +} + +func TestResponsesSSESnapshotRepairFailsClosedOnContradictoryIdentity(t *testing.T) { + for _, fixture := range []string{ + "data: {\"type\":\"response.output_item.added\",\"output_index\":0,\"item\":{\"type\":\"message\",\"id\":\"msg_a\"}}\n\ndata: {\"type\":\"response.output_text.done\",\"output_index\":0,\"item_id\":\"msg_other\",\"text\":\"x\"}\n\ndata: {\"type\":\"response.completed\",\"response\":{\"id\":\"r\"}}\n\n", + "data: {\"type\":\"response.output_item.added\",\"output_index\":0,\"item\":{\"type\":\"message\",\"id\":\"msg_a\"}}\n\ndata: {\"type\":\"response.output_item.added\",\"output_index\":0,\"item\":{\"type\":\"message\",\"id\":\"msg_b\"}}\n\ndata: {\"type\":\"response.completed\",\"response\":{\"id\":\"r\"}}\n\n", + } { + got, _ := rewriteSSEInChunksWithPipeline(t, fixture, []int{len(fixture)}, responseRepairPipeline{snapshot: newSnapshotRepairState(true, nil)}) + if strings.Contains(got, "response.output_item.done") { + t.Fatalf("tainted stream synthesized a terminal item: %s", got) + } + } +} + +func rewriteSSEInChunksWithPipeline(t *testing.T, input string, chunks []int, pipeline responseRepairPipeline) (string, *ResponsesSSEStream) { + t.Helper() + stream := NewResponsesSSEStream(pipeline) + var out bytes.Buffer + position := 0 + for _, size := range chunks { + if position >= len(input) { + break + } + end := position + size + if end > len(input) { + end = len(input) + } + got, err := stream.Feed([]byte(input[position:end])) + if err != nil { + t.Fatal(err) + } + out.Write(got) + position = end + } + if position < len(input) { + got, err := stream.Feed([]byte(input[position:])) + if err != nil { + t.Fatal(err) + } + out.Write(got) + } + tail, err := stream.Finish() + if err != nil { + t.Fatal(err) + } + out.Write(tail) + return out.String(), stream +} + func TestResponsesSSEBoundaryDropsPostTerminalEventsButKeepsDone(t *testing.T) { input := "event: response.completed\ndata: {\"type\":\"response.completed\",\"response\":{\"status\":\"completed\"}}\n\nevent: ignored\ndata: {\"type\":\"response.output_text.delta\",\"delta\":\"late\"}\n\ndata: [DONE]\n\n" got, stream := rewriteSSEInChunks(t, input, []int{len(input)}) From d46655ded97a08f723eda3040437ac361f0f5204 Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Mon, 7 Sep 2026 00:48:36 +0800 Subject: [PATCH 066/165] fix(go): repair snapshot output fields --- go/internal/sidecar/responses_stateful_repair.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/go/internal/sidecar/responses_stateful_repair.go b/go/internal/sidecar/responses_stateful_repair.go index 1073acd518..5fac62bdaf 100644 --- a/go/internal/sidecar/responses_stateful_repair.go +++ b/go/internal/sidecar/responses_stateful_repair.go @@ -432,8 +432,8 @@ func (s *snapshotRepairState) repairResponse(response *jsonwire.Value, fallback if output := response.Find("output"); output != nil && output.Kind() == jsonwire.Array { status, _ := stringMember(response, "status") inferred, _ := responseStatusToItemStatus(status) - for i, item := range output.Elements() { - changed = backfillSSEOutputItem(item, itoa(i), inferred) || changed + for _, item := range output.Elements() { + changed = snapshotRepairOutputItem(item, inferred) || changed } } return changed From 5d54d6a60f349b377280021cf90efed07ca9b10e Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Mon, 7 Sep 2026 01:02:42 +0800 Subject: [PATCH 067/165] feat(go): native config read dispatch --- go/internal/configschema/schema.go | 122 ++++++++---- go/internal/ocxcli/cli.go | 22 ++- go/internal/ocxcli/cli_test.go | 94 ++++++++- go/internal/ocxcli/families.go | 298 +++++++++++++++-------------- 4 files changed, 350 insertions(+), 186 deletions(-) diff --git a/go/internal/configschema/schema.go b/go/internal/configschema/schema.go index 52205b1cd4..688cb96739 100644 --- a/go/internal/configschema/schema.go +++ b/go/internal/configschema/schema.go @@ -73,6 +73,22 @@ func (n *Normalized) IndentedJSON() ([]byte, error) { return out.Bytes(), nil } +// RedactedIndentedJSON renders a diagnostic/config-show view without exposing +// credentials. It preserves the schema-projected object order used by +// IndentedJSON so callers can retain TypeScript's observable JSON layout. +func (n *Normalized) RedactedIndentedJSON() ([]byte, error) { + if n == nil || n.root == nil { + return nil, errors.New("nil normalized config") + } + var out bytes.Buffer + redactValue(n.root, "").write(&out) + var indented bytes.Buffer + if err := json.Indent(&indented, out.Bytes(), "", " "); err != nil { + return nil, err + } + return indented.Bytes(), nil +} + type valueKind uint8 const ( @@ -283,55 +299,63 @@ func validIntRange(v *value, min, max int64) bool { } func validateTop(v *value) error { + var diagnostics []string if port := v.find("port"); port != nil { if port.kind != numberKind { - return errors.New("schema_invalid: port: Invalid input: expected number, received string") - } - n, err := strconv.ParseInt(port.number.String(), 10, 64) - if err != nil { - return errors.New("schema_invalid: port: Invalid input: expected int, received number") + diagnostics = append(diagnostics, "port: Invalid input: expected number, received string") } - if n < 0 { - return errors.New("schema_invalid: port: Too small: expected number to be >=0") - } - if n > 65535 { - return errors.New("schema_invalid: port: Too big: expected number to be <=65535") + if port.kind == numberKind { + n, err := strconv.ParseInt(port.number.String(), 10, 64) + if err != nil { + diagnostics = append(diagnostics, "port: Invalid input: expected int, received number") + } else if n < 0 { + diagnostics = append(diagnostics, "port: Too small: expected number to be >=0") + } else if n > 65535 { + diagnostics = append(diagnostics, "port: Too big: expected number to be <=65535") + } } } providers := v.find("providers") if providers == nil { - return errors.New("schema_invalid: providers: Invalid input: expected record, received undefined") + diagnostics = append(diagnostics, "providers: Invalid input: expected record, received undefined") } - if providers.kind != objectKind { - return fmt.Errorf("schema_invalid: providers: Invalid input: expected record, received %s", zodType(providers)) - } - for _, p := range providers.object { - if p.value.kind != objectKind { - return fmt.Errorf("schema_invalid: providers.%s: Invalid input: expected object, received %s", p.key, zodType(p.value)) - } - if x := p.value.find("adapter"); x == nil { - return fmt.Errorf("schema_invalid: providers.%s.adapter: Invalid input: expected string, received undefined", p.key) - } else if x.kind != stringKind { - return fmt.Errorf("schema_invalid: providers.%s.adapter: Invalid input: expected string, received %s", p.key, zodType(x)) - } else if x.text == "" { - return fmt.Errorf("schema_invalid: providers.%s.adapter: Too small: expected string to have >=1 characters", p.key) - } - if x := p.value.find("baseUrl"); x == nil { - return fmt.Errorf("schema_invalid: providers.%s.baseUrl: Invalid input: expected string, received undefined", p.key) - } else if x.kind != stringKind { - return fmt.Errorf("schema_invalid: providers.%s.baseUrl: Invalid input: expected string, received %s", p.key, zodType(x)) - } else if x.text == "" { - return fmt.Errorf("schema_invalid: providers.%s.baseUrl: Too small: expected string to have >=1 characters", p.key) + if providers != nil { + if providers.kind != objectKind { + diagnostics = append(diagnostics, fmt.Sprintf("providers: Invalid input: expected record, received %s", zodType(providers))) + } else { + for _, p := range providers.object { + if p.value.kind != objectKind { + diagnostics = append(diagnostics, fmt.Sprintf("providers.%s: Invalid input: expected object, received %s", p.key, zodType(p.value))) + continue + } + if x := p.value.find("adapter"); x == nil { + diagnostics = append(diagnostics, fmt.Sprintf("providers.%s.adapter: Invalid input: expected string, received undefined", p.key)) + } else if x.kind != stringKind { + diagnostics = append(diagnostics, fmt.Sprintf("providers.%s.adapter: Invalid input: expected string, received %s", p.key, zodType(x))) + } else if x.text == "" { + diagnostics = append(diagnostics, fmt.Sprintf("providers.%s.adapter: Too small: expected string to have >=1 characters", p.key)) + } + if x := p.value.find("baseUrl"); x == nil { + diagnostics = append(diagnostics, fmt.Sprintf("providers.%s.baseUrl: Invalid input: expected string, received undefined", p.key)) + } else if x.kind != stringKind { + diagnostics = append(diagnostics, fmt.Sprintf("providers.%s.baseUrl: Invalid input: expected string, received %s", p.key, zodType(x))) + } else if x.text == "" { + diagnostics = append(diagnostics, fmt.Sprintf("providers.%s.baseUrl: Too small: expected string to have >=1 characters", p.key)) + } + } } } if d := v.find("defaultProvider"); d != nil { if d.kind != stringKind { - return fmt.Errorf("schema_invalid: defaultProvider: Invalid input: expected string, received %s", zodType(d)) + diagnostics = append(diagnostics, fmt.Sprintf("defaultProvider: Invalid input: expected string, received %s", zodType(d))) } - if d.text == "" { - return errors.New("schema_invalid: defaultProvider: Too small: expected string to have >=1 characters") + if d.kind == stringKind && d.text == "" { + diagnostics = append(diagnostics, "defaultProvider: Too small: expected string to have >=1 characters") } } + if len(diagnostics) > 0 { + return errors.New("schema_invalid: " + strings.Join(diagnostics, "; ")) + } return nil } func zodType(v *value) string { @@ -392,3 +416,33 @@ func (v *value) write(b *bytes.Buffer) { b.WriteByte('}') } } + +func redactValue(v *value, key string) *value { + if isSecretKey(key) && v.kind == stringKind && v.text != "" { + return stringValue("********") + } + switch v.kind { + case arrayKind: + out := &value{kind: arrayKind, array: make([]*value, len(v.array))} + for i, child := range v.array { + out.array[i] = redactValue(child, "") + } + return out + case objectKind: + out := &value{kind: objectKind, object: make([]member, len(v.object))} + for i, child := range v.object { + out.object[i] = member{key: child.key, value: redactValue(child.value, child.key)} + } + return out + default: + return v + } +} + +func isSecretKey(key string) bool { + switch strings.ToLower(key) { + case "apikey", "key", "accesstoken", "refreshtoken", "idtoken", "token", "password", "clientsecret": + return true + } + return false +} diff --git a/go/internal/ocxcli/cli.go b/go/internal/ocxcli/cli.go index 8892f0ef54..7693b4ec74 100644 --- a/go/internal/ocxcli/cli.go +++ b/go/internal/ocxcli/cli.go @@ -92,7 +92,10 @@ var Commands = []Command{ {Name: "integration", Usage: "ocx integration client ", Summary: "Manage integrations.", Owner: TypeScriptOwned}, {Name: "grok", Usage: "ocx grok ", Summary: "Manage Grok Build.", Owner: TypeScriptOwned}, {Name: "system", Usage: "ocx system ", Summary: "Manage runtime settings.", Owner: TypeScriptOwned}, - {Name: "config", Usage: "ocx config ", Summary: "Manage configuration.", Owner: TypeScriptOwned}, + // The command default and read subset are Go-owned. Mutations remain + // TypeScript-owned by configRuntimeSubcommands until they share its SQLite + // generation transaction. + {Name: "config", Usage: "ocx config ", Summary: "Manage configuration.", Owner: GoOwned}, {Name: "lab", Usage: "ocx lab ", Summary: "Inspect Compatibility Lab.", Owner: TypeScriptOwned}, {Name: "claude", Usage: "ocx claude [args...]", Summary: "Launch Claude Code.", Owner: TypeScriptOwned}, {Name: "opencode", Usage: "ocx opencode [args...]", Summary: "Launch opencode.", Owner: TypeScriptOwned}, @@ -132,6 +135,15 @@ func OwnershipFor(args []string) (Ownership, bool) { return owner, true } } + if command.Name == "config" && len(args) > 1 { + if args[1] == "--json" || args[1] == "--source" { + return GoOwned, true + } + if owner, ok := configRuntimeSubcommands[args[1]]; ok { + return owner, true + } + return TypeScriptOwned, true + } return command.Owner, true } @@ -233,6 +245,8 @@ func Run(args []string, deps Deps) int { return runModels(args[1:], deps) case "provider": return runProvider(args[1:], deps) + case "config": + return runConfig(args[1:], deps) default: // The ownership registry above and this switch must be reconciled by // TestOwnershipMapMatchesDispatch; this is defensive for future edits. @@ -264,6 +278,10 @@ func hasHelpFlag(args []string) bool { return false } func printSubcommandHelp(name string, deps Deps) int { + if name == "config" { + fmt.Fprint(deps.Stdout, configHelp) + return ExitOK + } if owner, known := OwnershipFor([]string{name}); known && owner == TypeScriptOwned { return runDelegated([]string{name, "--help"}, deps) } @@ -274,6 +292,8 @@ func printSubcommandHelp(name string, deps Deps) int { fmt.Fprint(deps.Stdout, "Usage: ocx ready [--json] [--wait [--timeout ]]\n\nCheck post-sync readiness. Exits 0 only when ready.\n\nExact unauthenticated GET /readyz returns HTTP 200 when ready, or 503 with Retry-After: 1 for pending or failed.\nIts sanitized HTTP identity is {service, version, uptime, pid, port, status}; /healthz is separate liveness, not readiness.\nDefault is a single identity-checked /readyz probe; old proxies without /readyz fail closed as unreachable.\n--wait polls until ready or timeout, but exits immediately on terminal failed (default 45s, max 300s).\n--timeout requires --wait and accepts a positive integer (1..300).\n--json emits {ready, status, pid, port}; status is one of ready|pending|failed|unreachable.\nInvalid or unknown arguments exit 64. Not-ready, pending, failed, timeout, and unreachable exit 1.\n") case "models": fmt.Fprint(deps.Stdout, modelsUsage+"\nCustom models:\n "+modelAddUsage+"\n "+modelRemoveUsage+"\n Usage: ocx models list-custom [--json]\n\nRuntime subcommands (live, edit, enable, disable, provider, selected, preset, new-policy, new-arrivals, context, shadow) retain the TypeScript management API owner during the incremental takeover.\n") + case "config": + fmt.Fprint(deps.Stdout, configHelp) default: fmt.Fprintf(deps.Stderr, "Unknown command: %s\n", name) printHelp(deps.Stdout) diff --git a/go/internal/ocxcli/cli_test.go b/go/internal/ocxcli/cli_test.go index 0b59d28a8d..37cd956813 100644 --- a/go/internal/ocxcli/cli_test.go +++ b/go/internal/ocxcli/cli_test.go @@ -106,6 +106,85 @@ func TestModelRuntimeOwnershipDelegates(t *testing.T) { } } +func TestConfigRuntimeOwnershipUsesNativeReadCommands(t *testing.T) { + for _, subcommand := range []string{"show", "validate", "export"} { + if got, known := OwnershipFor([]string{"config", subcommand}); !known || got != GoOwned { + t.Fatalf("OwnershipFor(config %s) = %q, %t", subcommand, got, known) + } + } + for _, subcommand := range []string{"get", "set", "unset", "import"} { + if got, known := OwnershipFor([]string{"config", subcommand}); !known || got != TypeScriptOwned { + t.Fatalf("OwnershipFor(config %s) = %q, %t", subcommand, got, known) + } + } +} + +func TestNativeConfigReadCommandsMatchOracleShape(t *testing.T) { + dir := t.TempDir() + t.Setenv("OPENCODEX_HOME", dir) + initial := "{\"providers\":{\"test\":{\"adapter\":\"openai-chat\",\"baseUrl\":\"https://example.test/v1\",\"apiKey\":\"secret\",\"defaultModel\":\"m\"}},\"defaultProvider\":\"test\",\"port\":10123,\"unknown\":true}" + if err := os.WriteFile(filepath.Join(dir, "config.json"), []byte(initial), 0o600); err != nil { + t.Fatal(err) + } + var out, stderr bytes.Buffer + deps := depsFor(RuntimeState{}, &out, &stderr) + deps.Delegate = func([]string) (int, error) { t.Fatal("native config read delegated"); return 0, nil } + if got := Run([]string{"config", "show", "--source"}, deps); got != ExitOK { + t.Fatalf("show = %d stderr=%q", got, stderr.String()) + } + if !strings.Contains(out.String(), "\"apiKey\": \"********\"") || !strings.Contains(out.String(), "\"source\": \"file\"") { + t.Fatalf("show = %q", out.String()) + } + out.Reset() + stderr.Reset() + if got := Run([]string{"config", "validate", "--json"}, deps); got != ExitOK { + t.Fatalf("validate = %d stdout=%q stderr=%q", got, out.String(), stderr.String()) + } + if !strings.Contains(out.String(), "\"ok\": true") || !strings.Contains(out.String(), filepath.Join(dir, "config.json")) { + t.Fatalf("validate = %q", out.String()) + } + out.Reset() + stderr.Reset() + if got := Run([]string{"config", "export", "-"}, deps); got != ExitOK { + t.Fatalf("export = %d stderr=%q", got, stderr.String()) + } + if !strings.Contains(out.String(), "\"apiKey\": \"secret\"") || !strings.Contains(out.String(), "\"unknown\": true") { + t.Fatalf("export = %q", out.String()) + } +} + +func TestNativeConfigCandidateValidationMatchesMultiIssueOracle(t *testing.T) { + path := filepath.Join(t.TempDir(), "bad.json") + if err := os.WriteFile(path, []byte("{\"port\":-1}"), 0o600); err != nil { + t.Fatal(err) + } + var out, stderr bytes.Buffer + deps := depsFor(RuntimeState{}, &out, &stderr) + deps.Delegate = func([]string) (int, error) { t.Fatal("candidate validation delegated"); return 0, nil } + if got := Run([]string{"config", "validate", path, "--json"}, deps); got != ExitOK { + t.Fatalf("validate = %d", got) + } + want := "{\n \"ok\": false,\n \"error\": \"schema_invalid: port: Too small: expected number to be >=0; providers: Invalid input: expected record, received undefined\"\n}\n" + if out.String() != want { + t.Fatalf("stdout = %q, want %q", out.String(), want) + } +} + +func TestNativeConfigReadFallsBackForUnreadableStoredConfig(t *testing.T) { + dir := t.TempDir() + t.Setenv("OPENCODEX_HOME", dir) + var out, stderr bytes.Buffer + var received []string + deps := depsFor(RuntimeState{}, &out, &stderr) + deps.Delegate = func(args []string) (int, error) { received = append([]string(nil), args...); return 17, nil } + if got := Run([]string{"config", "show", "--source"}, deps); got != 17 { + t.Fatalf("show = %d", got) + } + if want := []string{"config", "show", "--source"}; !slices.Equal(received, want) { + t.Fatalf("fallback argv=%#v want=%#v", received, want) + } +} + func TestHelpSurfaceMatchesCommandRegistry(t *testing.T) { documented := map[string]bool{} for _, line := range strings.Split(fullUsage, "\n") { @@ -159,18 +238,15 @@ func TestTypeScriptOwnedFamiliesDelegateExactArgumentsAndExitCode(t *testing.T) } } -func TestConfigHelpDelegatesToTheConfigOwner(t *testing.T) { - var received []string - deps := depsFor(RuntimeState{}, &bytes.Buffer{}, &bytes.Buffer{}) - deps.Delegate = func(args []string) (int, error) { - received = append([]string(nil), args...) - return ExitOK, nil - } +func TestConfigHelpIsNative(t *testing.T) { + var out, stderr bytes.Buffer + deps := depsFor(RuntimeState{}, &out, &stderr) + deps.Delegate = func([]string) (int, error) { t.Fatal("config help delegated"); return 0, nil } if got := Run([]string{"help", "config"}, deps); got != ExitOK { t.Fatalf("help config exit = %d", got) } - if want := []string{"config", "--help"}; !slices.Equal(received, want) { - t.Fatalf("delegated argv = %#v, want %#v", received, want) + if out.String() != configHelp { + t.Fatalf("help = %q", out.String()) } } diff --git a/go/internal/ocxcli/families.go b/go/internal/ocxcli/families.go index 43cb5e0f31..00c64e7dde 100644 --- a/go/internal/ocxcli/families.go +++ b/go/internal/ocxcli/families.go @@ -1,6 +1,7 @@ package ocxcli import ( + "bytes" "crypto/rand" "encoding/json" "errors" @@ -13,10 +14,12 @@ import ( "time" "github.com/lidge-jun/opencodex/go/internal/config" + "github.com/lidge-jun/opencodex/go/internal/configschema" ) const ( - configUsage = "Usage:\n ocx config [show] [--json]\n ocx config get [--json]\n ocx config set [--json]\n ocx config unset [--json]\n ocx config validate [path|-] [--json]\n ocx config export \n ocx config import --yes [--json]\n" + configUsage = "Usage:\n ocx config [show] [--json] [--source]\n ocx config get [--json]\n ocx config set [--json]\n ocx config unset [--json]\n ocx config validate [path|-] [--json]\n ocx config export \n ocx config import --yes [--json]\n" + configHelp = "Usage: ocx config ...\n\nInspect and safely modify validated OpenCodex configuration.\n\nSecrets are masked by show/get. Import requires --yes and validates before writing.\n" modelsUsage = "Usage: ocx models [--provider ] [--json]\n" modelAddUsage = "Usage: ocx models add [--display-name ] [--context-window ] [--modalities text,image,audio] [--reasoning-efforts ] [--default-reasoning-effort ]" modelRemoveUsage = "Usage: ocx models remove [--yes]" @@ -29,6 +32,12 @@ var modelRuntimeSubcommands = map[string]Ownership{ "context": TypeScriptOwned, "shadow": TypeScriptOwned, } +// Writes retain the TypeScript owner until Go participates in the shared +// config-mutation.sqlite generation transaction. +var configRuntimeSubcommands = map[string]Ownership{ + "show": GoOwned, "validate": GoOwned, "export": GoOwned, +} + func loadCLIConfig() (map[string]any, error) { loaded, err := config.Load() if err != nil { @@ -38,159 +47,151 @@ func loadCLIConfig() (map[string]any, error) { } func runConfig(args []string, deps Deps) int { - if len(args) == 0 || args[0] == "show" { - if len(args) > 0 { - args = args[1:] - } - if len(args) > 1 || (len(args) == 1 && args[0] != "--json") { - fmt.Fprint(deps.Stderr, configUsage) - return ExitUsage - } - cfg, err := loadCLIConfig() - if err != nil { - fmt.Fprintln(deps.Stderr, err) - return ExitFailure - } - return writeIndentedJSON(deps.Stdout, redactConfig(cfg)) + action := "show" + if len(args) > 0 && args[0] != "--json" && args[0] != "--source" { + action, args = args[0], args[1:] } - action := args[0] - jsonOutput := takeFlag(&args, "--json") switch action { - case "get": - if len(args) != 2 { - fmt.Fprint(deps.Stderr, configUsage) - return ExitUsage - } - cfg, err := loadCLIConfig() - if err != nil { - fmt.Fprintln(deps.Stderr, err) - return ExitFailure - } - value, ok := configPath(cfg, args[1]) - if !ok { - fmt.Fprintf(deps.Stderr, "config path not found: %s\n", args[1]) - return ExitUsage - } - value = redactConfigValue(value, lastSegment(args[1])) - if jsonOutput || isComposite(value) { - return writeIndentedJSON(deps.Stdout, value) - } - fmt.Fprintln(deps.Stdout, scalarString(value)) - return ExitOK - case "set", "unset": - if (action == "set" && len(args) != 3) || (action == "unset" && len(args) != 2) { - fmt.Fprint(deps.Stderr, configUsage) - return ExitUsage - } - path := args[1] - cfg, err := loadCLIConfig() - if err != nil { - fmt.Fprintln(deps.Stderr, err) - return ExitFailure - } - var value any - if action == "set" { - value = parseConfigValue(args[2]) - } - if err := setConfigPath(cfg, path, value, action == "unset"); err != nil { - fmt.Fprintln(deps.Stderr, err) - return ExitUsage - } - if err := validateCLIConfig(cfg); err != nil { - fmt.Fprintln(deps.Stderr, err) - return ExitUsage - } - if err := config.SaveRaw(cfg); err != nil { - fmt.Fprintln(deps.Stderr, err) - return ExitFailure - } - if action == "unset" { - value = nil - } else { - value, _ = configPath(cfg, path) - } - result := map[string]any{"ok": true, "path": path, "value": redactConfigValue(value, lastSegment(path))} - if jsonOutput { - return writeIndentedJSON(deps.Stdout, result) - } - fmt.Fprintf(deps.Stdout, "%s %s.\n", strings.Title(action), path) - return ExitOK + case "show": + return runNativeConfigShow(args, deps) case "validate": - if len(args) > 2 || len(args) == 2 && args[1] != "-" { - fmt.Fprint(deps.Stderr, configUsage) - return ExitUsage - } - var cfg map[string]any - var err error - if len(args) == 2 { - cfg, err = readConfigInput(args[1]) - } else { - cfg, err = loadCLIConfig() - } - if err == nil { - err = validateCLIConfig(cfg) - } - if err != nil { - if jsonOutput { - writeIndentedJSON(deps.Stdout, map[string]any{"ok": false, "error": err.Error()}) - } else { - fmt.Fprintf(deps.Stdout, "Config is invalid: %s\n", err) - } - return ExitFailure - } - if jsonOutput { - return writeIndentedJSON(deps.Stdout, map[string]any{"ok": true}) - } - fmt.Fprintln(deps.Stdout, "Config is valid.") - return ExitOK + return runNativeConfigValidate(args, deps) case "export": - if len(args) != 2 { - fmt.Fprint(deps.Stderr, configUsage) - return ExitUsage - } - cfg, err := loadCLIConfig() - if err != nil { - fmt.Fprintln(deps.Stderr, err) - return ExitFailure - } - content, _ := json.MarshalIndent(cfg, "", " ") - content = append(content, '\n') - if args[1] == "-" { - _, _ = deps.Stdout.Write(content) - return ExitOK - } - if err := os.WriteFile(args[1], content, 0o600); err != nil { - fmt.Fprintln(deps.Stderr, err) - return ExitFailure + return runNativeConfigExport(args, deps) + default: + return runDelegated(append([]string{"config", action}, args...), deps) + } +} + +type configSourceOutput struct { + Config json.RawMessage `json:"config"` + Source string `json:"source"` + Error any `json:"error"` + Warnings []string `json:"warnings"` +} + +func readNativeConfig() (*configschema.Normalized, []byte, string, error) { + path, err := config.Path() + if err != nil { + return nil, nil, "", err + } + raw, err := os.ReadFile(path) + if err != nil { + return nil, nil, path, err + } + normalized, err := configschema.ValidateCandidateJSON(raw) + if err != nil { + return nil, nil, path, err + } + return normalized, raw, path, nil +} + +func runNativeConfigShow(args []string, deps Deps) int { + _ = takeFlag(&args, "--json") + source := takeFlag(&args, "--source") + if len(args) != 0 { + fmt.Fprint(deps.Stderr, configUsage) + return ExitUsage + } + normalized, _, _, err := readNativeConfig() + if err != nil { + delegateArgs := []string{"config", "show"} + if source { + delegateArgs = append(delegateArgs, "--source") } - fmt.Fprintf(deps.Stdout, "Exported config to %s.\n", args[1]) + return runDelegated(delegateArgs, deps) + } + data, err := normalized.RedactedIndentedJSON() + if err != nil { + fmt.Fprintln(deps.Stderr, err) + return ExitFailure + } + if !source { + _, _ = deps.Stdout.Write(append(data, '\n')) return ExitOK - case "import": - if len(args) != 3 || args[2] != "--yes" { - fmt.Fprint(deps.Stderr, configUsage) - return ExitUsage - } - cfg, err := readConfigInput(args[1]) - if err == nil { - err = validateCLIConfig(cfg) - } - if err != nil { - fmt.Fprintln(deps.Stderr, err) - return ExitUsage - } - if err := config.SaveRaw(cfg); err != nil { - fmt.Fprintln(deps.Stderr, err) - return ExitFailure + } + return writeNativeConfigJSON(deps.Stdout, configSourceOutput{Config: json.RawMessage(data), Source: "file", Error: nil, Warnings: []string{}}) +} + +func runNativeConfigValidate(args []string, deps Deps) int { + jsonOutput := takeFlag(&args, "--json") + if len(args) > 1 { + fmt.Fprint(deps.Stderr, configUsage) + return ExitUsage + } + path := "" + var raw []byte + var err error + if len(args) == 1 { + path = args[0] + if path == "-" { + raw, err = io.ReadAll(os.Stdin) + } else { + raw, err = os.ReadFile(path) } - if jsonOutput { - return writeIndentedJSON(deps.Stdout, map[string]any{"ok": true, "source": args[1]}) + } else { + _, raw, path, err = readNativeConfig() + } + if err == nil { + _, err = configschema.ValidateCandidateJSON(raw) + } + if err != nil { + if len(args) == 0 { + return runDelegated([]string{"config", "validate"}, deps) } - fmt.Fprintf(deps.Stdout, "Imported config from %s. Restart or run ocx sync if needed.\n", args[1]) + return reportNativeConfigValidation(deps, jsonOutput, false, "", err.Error()) + } + return reportNativeConfigValidation(deps, jsonOutput, true, path, "") +} + +func reportNativeConfigValidation(deps Deps, jsonOutput, valid bool, source, message string) int { + if jsonOutput { + if valid { + return writeNativeConfigJSON(deps.Stdout, struct { + OK bool `json:"ok"` + Source string `json:"source"` + }{true, source}) + } + _ = writeNativeConfigJSON(deps.Stdout, struct { + OK bool `json:"ok"` + Error string `json:"error"` + }{false, message}) + } else if valid { + fmt.Fprintln(deps.Stdout, "Config is valid.") + } else { + fmt.Fprintf(deps.Stdout, "Config is invalid: %s\n", message) + } + if valid || jsonOutput { return ExitOK - default: + } + return ExitFailure +} + +func runNativeConfigExport(args []string, deps Deps) int { + if len(args) != 1 { fmt.Fprint(deps.Stderr, configUsage) return ExitUsage } + normalized, _, _, err := readNativeConfig() + if err != nil { + return runDelegated(append([]string{"config", "export"}, args...), deps) + } + data, err := normalized.IndentedJSON() + if err != nil { + fmt.Fprintln(deps.Stderr, err) + return ExitFailure + } + data = append(data, '\n') + if args[0] == "-" { + _, _ = deps.Stdout.Write(data) + return ExitOK + } + if err := os.WriteFile(args[0], data, 0o600); err != nil { + fmt.Fprintln(deps.Stderr, err) + return ExitFailure + } + fmt.Fprintf(deps.Stdout, "Exported config to %s.\n", args[0]) + return ExitOK } func takeFlag(args *[]string, flag string) bool { @@ -1356,3 +1357,16 @@ func writeIndentedJSON(writer io.Writer, value any) int { fmt.Fprintln(writer, string(raw)) return ExitOK } + +func writeNativeConfigJSON(writer io.Writer, value any) int { + raw, err := json.MarshalIndent(value, "", " ") + if err != nil { + fmt.Fprintln(writer, err) + return ExitFailure + } + raw = bytes.ReplaceAll(raw, []byte("\\u003e"), []byte(">")) + raw = bytes.ReplaceAll(raw, []byte("\\u003c"), []byte("<")) + raw = bytes.ReplaceAll(raw, []byte("\\u0026"), []byte("&")) + fmt.Fprintln(writer, string(raw)) + return ExitOK +} From a6905ad598d9246dd680e35e80df97930c0f06ae Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Mon, 7 Sep 2026 01:09:31 +0800 Subject: [PATCH 068/165] feat(go): add provider adapter relay parity --- go/internal/sidecar/hotpath_relay.go | 77 +++++++++-- go/internal/sidecar/hotpath_relay_test.go | 154 +++++++++++++++++++++- tests/go-hotpath-relay.test.ts | 90 ++++++++++++- 3 files changed, 309 insertions(+), 12 deletions(-) diff --git a/go/internal/sidecar/hotpath_relay.go b/go/internal/sidecar/hotpath_relay.go index 8450d5be67..eec651052e 100644 --- a/go/internal/sidecar/hotpath_relay.go +++ b/go/internal/sidecar/hotpath_relay.go @@ -94,7 +94,11 @@ type relayPlan struct { endpoint string // full POST target URL apiKey string // resolved bearer secret, "" when the provider has none apiKeyHeader string // Azure-compatible adapters use api-key instead of Bearer auth - streaming bool + // providerHeaders are validated static headers assigned after generated auth, + // as the TypeScript openai-responses adapter does with Object.assign. + // Their original document order is retained for deterministic request output. + providerHeaders []jsonwire.Member + streaming bool } // resolveRelayAPIKey resolves a provider apiKey the way the TS key store does: @@ -561,6 +565,27 @@ func boolMember(obj *jsonwire.Value, key string) (bool, bool) { return member.Bool(), true } +// relayProviderHeaderValid mirrors providerHeadersConfigError. The TypeScript +// config schema owns credential headers, so manually written or stale config +// must not turn the direct relay into a second, less strict header boundary. +func relayProviderHeaderValid(name, value string) bool { + if name == "" || strings.ContainsAny(value, "\r\n") { + return false + } + for _, char := range name { + if !((char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z') || + (char >= '0' && char <= '9') || char == '\x60' || char == '|' || + strings.ContainsRune("!#$%&'*+-.^_~", char)) { + return false + } + } + switch strings.ToLower(strings.TrimSpace(name)) { + case "authorization", "cookie", "set-cookie", "proxy-authorization", "x-api-key", "x-goog-api-key", "x-amz-security-token": + return false + } + return true +} + // relayPlanForProvider validates one candidate provider for the relay and // builds the endpoint. Returns a refusal when the provider row needs TS-only // machinery (forward/oauth auth, keychain keys, non-responses adapter, custom @@ -576,14 +601,24 @@ func relayPlanForProvider(name string, provider *jsonwire.Value) (*relayPlan, *r if authMode, ok := stringMember(provider, "authMode"); ok && authMode != "key" { return nil, refuseRelay("provider %q authMode %q is not key", name, authMode) } + azureAdapter := adapter == "azure" || adapter == "azure-openai" if provider.Find("responsesPath") != nil { return nil, refuseRelay("provider %q configures a custom responsesPath", name) } - if headers := provider.Find("headers"); headers != nil && headers.Kind() == jsonwire.Object && len(headers.Members()) > 0 { - // The direct relay owns only the canonical generated headers. Provider - // headers may override or extend adapter output, so keep these rows on - // the bridge until their exact adapter precedence is ported. - return nil, refuseRelay("provider %q configures custom headers", name) + var providerHeaders []jsonwire.Member + if headers := provider.Find("headers"); headers != nil { + if headers.Kind() != jsonwire.Object { + return nil, refuseRelay("provider %q headers are not an object", name) + } + for _, member := range headers.Members() { + if member.Value == nil || member.Value.Kind() != jsonwire.String { + return nil, refuseRelay("provider %q headers contain a non-string value", name) + } + if !relayProviderHeaderValid(member.Key, member.Value.String()) { + return nil, refuseRelay("provider %q headers violate the provider header policy", name) + } + providerHeaders = append(providerHeaders, member) + } } apiKey := "" if raw, ok := stringMember(provider, "apiKey"); ok { @@ -593,6 +628,12 @@ func relayPlanForProvider(name string, provider *jsonwire.Value) (*relayPlan, *r return nil, refuseRelay("provider %q apiKey is a keychain reference", name) } } + // createAzureAdapter rejects a missing or blank key before it calls the + // inherited Responses request builder. Keep that validation on the bridge + // rather than incorrectly treating Azure as an anonymous endpoint. + if azureAdapter && strings.TrimSpace(apiKey) == "" { + return nil, refuseRelay("provider %q Azure adapter requires a non-empty apiKey", name) + } baseURL, ok := stringMember(provider, "baseUrl") if !ok { return nil, refuseRelay("provider %q has no baseUrl", name) @@ -609,10 +650,10 @@ func relayPlanForProvider(name string, provider *jsonwire.Value) (*relayPlan, *r return nil, refuseRelay("provider %q baseUrl destination is not allowed", name) } apiKeyHeader := "" - if adapter == "azure" || adapter == "azure-openai" { + if azureAdapter { apiKeyHeader = "api-key" } - return &relayPlan{providerName: name, endpoint: endpoint, apiKey: apiKey, apiKeyHeader: apiKeyHeader}, nil + return &relayPlan{providerName: name, endpoint: endpoint, apiKey: apiKey, apiKeyHeader: apiKeyHeader, providerHeaders: providerHeaders}, nil } // directRelayResponseBytes is the bounded upstream body the relay read plus the @@ -656,6 +697,16 @@ func doDirectRelay(w http.ResponseWriter, r *http.Request, cfg Config, plan *rel return } upstreamReq.Header.Set("Content-Type", "application/json") + // For ordinary Responses providers, TypeScript assigns provider headers + // after generated Bearer auth. Azure wraps that request differently: it + // copies the inherited headers, sets api-key last, and deletes Authorization. + // Apply the shared static headers first for the Azure branch so its generated + // API key has the same final precedence. + if plan.apiKeyHeader == "api-key" { + for _, member := range plan.providerHeaders { + upstreamReq.Header.Set(member.Key, member.Value.String()) + } + } if plan.apiKey != "" { if plan.apiKeyHeader == "api-key" { upstreamReq.Header.Set("api-key", plan.apiKey) @@ -663,6 +714,16 @@ func doDirectRelay(w http.ResponseWriter, r *http.Request, cfg Config, plan *rel upstreamReq.Header.Set("Authorization", "Bearer "+plan.apiKey) } } + if plan.apiKeyHeader == "api-key" { + upstreamReq.Header.Del("Authorization") + } else { + // TypeScript sets generated key auth first, then Object.assigns provider + // headers. Header.Set is case-insensitive, which is also the Fetch Headers + // behavior reached by the TypeScript adapter. + for _, member := range plan.providerHeaders { + upstreamReq.Header.Set(member.Key, member.Value.String()) + } + } upstreamResp, err := relayUpstreamClient().Do(upstreamReq) if err != nil { http.Error(w, "provider relay unavailable", http.StatusServiceUnavailable) diff --git a/go/internal/sidecar/hotpath_relay_test.go b/go/internal/sidecar/hotpath_relay_test.go index b516dcef22..af6a90ff08 100644 --- a/go/internal/sidecar/hotpath_relay_test.go +++ b/go/internal/sidecar/hotpath_relay_test.go @@ -389,6 +389,158 @@ func TestDirectRelayGateOffStaysOnTheBridge(t *testing.T) { } } +func TestDirectRelayProviderHeadersMatchResponsesAdapterOrder(t *testing.T) { + var gotAuthorization, gotContentType, gotProviderHeader string + upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + gotAuthorization = r.Header.Get("Authorization") + gotContentType = r.Header.Get("Content-Type") + gotProviderHeader = r.Header.Get("X-Provider-Metadata") + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"id":"resp_fixture","status":"completed","output":[]}`)) + })) + defer upstream.Close() + + // TypeScript validates static headers as non-sensitive before the adapter + // assigns them after generated Bearer auth. Safe metadata carries through + // while owned Authorization and Content-Type keep their generated values. + dir := relayFixtureConfigDir(t, upstream.URL, map[string]any{ + "apiKey": "generated-key", + "headers": map[string]any{ + "X-Provider-Metadata": "batch-a", + }, + }) + rec, err := relayPost(t, relaySeamHandler(t, dir, true), `{"model":"test-model","input":"ping"}`) + if err != nil { + t.Fatal(err) + } + if rec.Code != http.StatusOK { + t.Fatalf("status = %d, want 200; body=%s", rec.Code, rec.Body.String()) + } + if gotAuthorization != "Bearer generated-key" { + t.Fatalf("Authorization = %q, want generated key", gotAuthorization) + } + if gotContentType != "application/json" { + t.Fatalf("Content-Type = %q, want application/json", gotContentType) + } + if gotProviderHeader != "batch-a" { + t.Fatalf("X-Provider-Metadata = %q, want batch-a", gotProviderHeader) + } +} + +func TestRelayPlanRejectsMalformedProviderHeaders(t *testing.T) { + for _, raw := range []string{ + `{"adapter":"openai-responses","baseUrl":"https://api.example/v1","headers":"wrong"}`, + `{"adapter":"openai-responses","baseUrl":"https://api.example/v1","headers":{"X-Number":3}}`, + `{"adapter":"openai-responses","baseUrl":"https://api.example/v1","headers":{"Authorization":"Bearer manual"}}`, + `{"adapter":"openai-responses","baseUrl":"https://api.example/v1","headers":{"X-Injected":"one\ntwo"}}`, + } { + provider, err := jsonwire.Parse([]byte(raw)) + if err != nil { + t.Fatal(err) + } + plan, refusal := relayPlanForProvider("test", provider) + if plan != nil || refusal == nil || !strings.Contains(refusal.reason, "headers") { + t.Fatalf("relayPlanForProvider(%s) = plan %#v, refusal %#v; want header refusal", raw, plan, refusal) + } + } +} + +func TestRelayAdapterWireSnapshots(t *testing.T) { + type snapshot struct { + adapter string + apiKey string + headers map[string]any + wantAuthorization string + wantAPIKey string + wantProviderHeader string + wantContentType string + wantRefusalContains string + } + // Fixed snapshots from the TypeScript adapter + config contracts. Static + // headers are non-sensitive; Azure copies them, then sets api-key last. + cases := []snapshot{ + { + adapter: "openai-responses", + apiKey: "openai-key", + headers: map[string]any{"X-Provider": "responses", "api-key": "static-key"}, + wantAuthorization: "Bearer openai-key", + wantAPIKey: "static-key", + wantProviderHeader: "responses", + wantContentType: "application/json", + }, + { + adapter: "azure", + apiKey: "azure-key", + headers: map[string]any{"X-Provider": "azure", "api-key": "static-key"}, + wantAPIKey: "azure-key", + wantProviderHeader: "azure", + wantContentType: "application/json", + }, + { + adapter: "azure-openai", + apiKey: " ", + wantRefusalContains: "requires a non-empty apiKey", + }, + } + for _, c := range cases { + c := c + t.Run(c.adapter, func(t *testing.T) { + provider := map[string]any{ + "adapter": "openai-responses", "baseUrl": "https://api.example/v1", "apiKey": c.apiKey, + } + if c.adapter == "azure" || c.adapter == "azure-openai" { + provider["adapter"] = c.adapter + } + if c.headers != nil { + provider["headers"] = c.headers + } + raw, err := json.Marshal(provider) + if err != nil { + t.Fatal(err) + } + parsed, err := jsonwire.Parse(raw) + if err != nil { + t.Fatal(err) + } + plan, refusal := relayPlanForProvider("snapshot", parsed) + if c.wantRefusalContains != "" { + if plan != nil || refusal == nil || !strings.Contains(refusal.reason, c.wantRefusalContains) { + t.Fatalf("plan=%#v refusal=%#v, want %q", plan, refusal, c.wantRefusalContains) + } + return + } + if refusal != nil || plan == nil { + t.Fatalf("plan=%#v refusal=%#v", plan, refusal) + } + + upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if got := r.Header.Get("Authorization"); got != c.wantAuthorization { + t.Errorf("Authorization = %q, want %q", got, c.wantAuthorization) + } + if got := r.Header.Get("api-key"); got != c.wantAPIKey { + t.Errorf("api-key = %q, want %q", got, c.wantAPIKey) + } + if got := r.Header.Get("X-Provider"); got != c.wantProviderHeader { + t.Errorf("X-Provider = %q, want %q", got, c.wantProviderHeader) + } + if got := r.Header.Get("Content-Type"); got != c.wantContentType { + t.Errorf("Content-Type = %q, want %q", got, c.wantContentType) + } + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"id":"resp_fixture","status":"completed","output":[]}`)) + })) + defer upstream.Close() + plan.endpoint = upstream.URL + "/v1/responses" + rec := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(`{"model":"test-model","input":"ping"}`)) + doDirectRelay(rec, req, Config{}, plan, []byte(`{"model":"test-model","input":"ping"}`)) + if rec.Code != http.StatusOK { + t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String()) + } + }) + } +} + func TestRequestQualifiesForRelayRefusals(t *testing.T) { upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { io.Copy(io.Discard, r.Body) @@ -422,7 +574,7 @@ func TestRequestQualifiesForRelayRefusals(t *testing.T) { {"non-responses adapter refuses", map[string]any{"adapter": "anthropic"}, `{"model":"test-model","input":"ping"}`, nil, "no direct relay contract"}, {"keychain apiKey refuses", map[string]any{"apiKey": "keychain:prod"}, `{"model":"test-model","input":"ping"}`, nil, "keychain"}, {"custom responsesPath refuses", map[string]any{"responsesPath": "/chat"}, `{"model":"test-model","input":"ping"}`, nil, "responsesPath"}, - {"custom provider headers refuse", map[string]any{"headers": map[string]any{"X-Provider-Key": "secret"}}, `{"model":"test-model","input":"ping"}`, nil, "custom headers"}, + {"custom provider headers qualify", map[string]any{"headers": map[string]any{"X-Provider-Key": "secret"}}, `{"model":"test-model","input":"ping"}`, nil, ""}, {"default provider absent refuses", map[string]any{"defaultProvider": "gone"}, `{"model":"other-model","input":"ping"}`, nil, "no provider owns model"}, } for _, c := range cases { diff --git a/tests/go-hotpath-relay.test.ts b/tests/go-hotpath-relay.test.ts index 7fe98e6d34..bd790864c3 100644 --- a/tests/go-hotpath-relay.test.ts +++ b/tests/go-hotpath-relay.test.ts @@ -154,6 +154,9 @@ interface UpstreamLog { method: string; path: string; contentType: string | null; + authorization: string | null; + apiKey: string | null; + providerHeader: string | null; } const upstreamLogs: UpstreamLog[] = []; @@ -164,7 +167,7 @@ interface ResponseCapture { body: string; } -function configFixture(upstreamPort: number) { +function configFixture(upstreamPort: number, providerOverrides: Record = {}) { return { port: 0, hostname: "127.0.0.1", @@ -176,6 +179,7 @@ function configFixture(upstreamPort: number) { allowPrivateNetwork: true, disabled: false, models: ["test-model"], + ...providerOverrides, }, }, }; @@ -205,11 +209,11 @@ function captureEnv(): void { } } -function setUpFixture(upstreamPort: number): void { +function setUpFixture(upstreamPort: number, providerOverrides?: Record): void { testHome = mkdtempSync(join(tmpdir(), "ocx-hotpath-relay-")); process.env.OPENCODEX_HOME = testHome; process.env.OPENCODEX_API_AUTH_TOKEN = "data-secret"; - saveConfig(configFixture(upstreamPort)); + saveConfig(configFixture(upstreamPort, providerOverrides)); } function tearDownFixture(): void { @@ -261,6 +265,9 @@ describe.skipIf(!goAvailable || sidecarBinary === null)("ocx-sidecar non-streami method: req.method, path: new URL(req.url).pathname, contentType: req.headers.get("content-type"), + authorization: req.headers.get("authorization"), + apiKey: req.headers.get("api-key"), + providerHeader: req.headers.get("x-provider-batch"), }); // The upstream body is the same bytes whichever path reached it (the // relay forwards the seam body verbatim, the bridge forwards the @@ -395,4 +402,81 @@ describe.skipIf(!goAvailable || sidecarBinary === null)("ocx-sidecar non-streami expect(bridgeCaptures[i]!.body, `${cases[i]!.name} body must match the oracle`).toBe(tsCaptures[i]!.body); } }); + + runFixtureTest("provider adapter matrix matches the TS oracle and proves direct Go ownership", async () => { + const token = "data-secret"; + const port = upstream!.port; + const cases = [ + { + name: "openai-responses", + provider: { + apiKey: "responses-key", + headers: { "api-key": "static-key", "X-Provider-Batch": "responses" }, + }, + authorization: "Bearer responses-key", + apiKey: "static-key", + providerHeader: "responses", + }, + ...(["azure", "azure-openai"] as const).map(adapter => ({ + name: adapter, + provider: { + adapter, + apiKey: "azure-key", + headers: { "api-key": "static-key", "X-Provider-Batch": adapter }, + }, + authorization: null, + apiKey: "azure-key", + providerHeader: adapter, + })), + ]; + + for (const fixture of cases) { + // Server A is the TypeScript adapter oracle for this exact provider row. + resetGoSidecarForTests(); + delete process.env[GO_SIDECAR_BIN_ENV]; + delete process.env[HOT_PATH_SEAM_ENV]; + delete process.env[HOT_PATH_RELAY_ENV]; + setUpFixture(port, fixture.provider); + const serverA = startServer(0); + let tsCapture: ResponseCapture; + const oracleStart = upstreamLogs.length; + try { + tsCapture = await postCase(serverA, token, { model: "test-model", input: "plain" }); + } finally { + await serverA.stop(true); + } + const oracleLog = upstreamLogs[oracleStart]!; + expect(oracleLog.ua, fixture.name + " oracle must use Bun").not.toBe(GO_UA); + + // Server B uses the same persisted row with the hot-path relay armed. + process.env[GO_SIDECAR_BIN_ENV] = sidecarBinary!; + process.env[HOT_PATH_SEAM_ENV] = "1"; + process.env[HOT_PATH_RELAY_ENV] = "1"; + const serverB = startServer(0); + const relayStart = upstreamLogs.length; + let goCapture: ResponseCapture; + try { + await waitFor(() => activeGoSidecarBaseUrl(), 15_000); + goCapture = await postCase(serverB, token, { model: "test-model", input: "plain" }); + } finally { + await serverB.stop(true); + resetGoSidecarForTests(); + } + const goLog = upstreamLogs[relayStart]!; + + expect(goCapture!.status, fixture.name + " status").toBe(tsCapture!.status); + expect(goCapture!.contentType, fixture.name + " content-type").toBe(tsCapture!.contentType); + expect(goCapture!.body, fixture.name + " client bytes").toBe(tsCapture!.body); + expect(goLog.ua, fixture.name + " must be Go-owned").toBe(GO_UA); + expect(goLog.method).toBe(oracleLog.method); + expect(goLog.path).toBe(oracleLog.path); + expect(goLog.contentType).toBe(oracleLog.contentType); + expect(goLog.authorization).toBe(fixture.authorization); + expect(goLog.apiKey).toBe(fixture.apiKey); + expect(goLog.providerHeader).toBe(fixture.providerHeader); + expect(goLog.authorization).toBe(oracleLog.authorization); + expect(goLog.apiKey).toBe(oracleLog.apiKey); + expect(goLog.providerHeader).toBe(oracleLog.providerHeader); + } + }); }); From 4678fac1aa613a6c791799667afb1548d9211c8e Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Mon, 7 Sep 2026 01:19:04 +0800 Subject: [PATCH 069/165] feat(go): native codex-shim status --- go/internal/ocxcli/cli.go | 8 +++ go/internal/ocxcli/cli_test.go | 39 ++++++++++++- go/internal/ocxcli/shim_status.go | 93 +++++++++++++++++++++++++++++++ 3 files changed, 139 insertions(+), 1 deletion(-) create mode 100644 go/internal/ocxcli/shim_status.go diff --git a/go/internal/ocxcli/cli.go b/go/internal/ocxcli/cli.go index 7693b4ec74..855d0a874c 100644 --- a/go/internal/ocxcli/cli.go +++ b/go/internal/ocxcli/cli.go @@ -135,6 +135,12 @@ func OwnershipFor(args []string) (Ownership, bool) { return owner, true } } + if command.Name == "codex-shim" && len(args) > 1 { + if args[1] == "status" { + return GoOwned, true + } + return TypeScriptOwned, true + } if command.Name == "config" && len(args) > 1 { if args[1] == "--json" || args[1] == "--source" { return GoOwned, true @@ -237,6 +243,8 @@ func Run(args []string, deps Deps) int { return runDelegated(args, deps) } switch args[0] { + case "codex-shim": + return runCodexShim(args[1:], deps) case "health": return runHealth(args[1:], deps) case "ready": diff --git a/go/internal/ocxcli/cli_test.go b/go/internal/ocxcli/cli_test.go index 37cd956813..0a46876626 100644 --- a/go/internal/ocxcli/cli_test.go +++ b/go/internal/ocxcli/cli_test.go @@ -218,7 +218,7 @@ func TestHelpSurfaceMatchesCommandRegistry(t *testing.T) { func TestTypeScriptOwnedFamiliesDelegateExactArgumentsAndExitCode(t *testing.T) { for _, argv := range [][]string{ {"status", "--json"}, {"doctor", "--json"}, {"service", "restart"}, - {"codex-shim", "status"}, {"tray", "status"}, + {"tray", "status"}, {"config", "set", "port", "10101", "--json"}, } { t.Run(strings.Join(argv, " "), func(t *testing.T) { @@ -279,6 +279,43 @@ func TestTypeScriptOwnedFamilyHelpDelegates(t *testing.T) { } } +func TestCodexShimStatusIsNativeAndMatchesStateSummary(t *testing.T) { + dir := t.TempDir() + t.Setenv("OPENCODEX_HOME", dir) + wrapper := filepath.Join(dir, "codex") + backup := filepath.Join(dir, "codex.opencodex-original") + if err := os.WriteFile(wrapper, []byte("#!/bin/sh\n# opencodex codex autostart shim\nensure\n"), 0o700); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(backup, []byte("original"), 0o700); err != nil { + t.Fatal(err) + } + state := "{\"platform\":\"linux\",\"wrapperPath\":\"" + wrapper + "\",\"originalPath\":\"" + wrapper + "\",\"backupPath\":\"" + backup + "\"}" + if err := os.WriteFile(filepath.Join(dir, "codex-shim.json"), []byte(state), 0o600); err != nil { + t.Fatal(err) + } + var out, stderr bytes.Buffer + deps := depsFor(RuntimeState{}, &out, &stderr) + deps.Delegate = func([]string) (int, error) { t.Fatal("codex-shim status delegated"); return 0, nil } + if got := Run([]string{"codex-shim", "status"}, deps); got != ExitOK { + t.Fatalf("exit=%d stderr=%q", got, stderr.String()) + } + want := "Codex autostart shim: wrapper shim present at " + wrapper + "; original backup present at " + backup + ".\n" + if out.String() != want { + t.Fatalf("stdout=%q want=%q", out.String(), want) + } +} + +func TestCodexShimMutationDelegates(t *testing.T) { + var out, stderr bytes.Buffer + var received []string + deps := depsFor(RuntimeState{}, &out, &stderr) + deps.Delegate = func(args []string) (int, error) { received = append([]string(nil), args...); return 17, nil } + if got := Run([]string{"codex-shim", "install"}, deps); got != 17 || !slices.Equal(received, []string{"codex-shim", "install"}) { + t.Fatalf("mutation code=%d argv=%#v", got, received) + } +} + func TestDelegatedFamilyHelpUsesOwnerOutput(t *testing.T) { for _, command := range []string{"status", "doctor", "service"} { t.Run(command, func(t *testing.T) { diff --git a/go/internal/ocxcli/shim_status.go b/go/internal/ocxcli/shim_status.go new file mode 100644 index 0000000000..e44ed1bee0 --- /dev/null +++ b/go/internal/ocxcli/shim_status.go @@ -0,0 +1,93 @@ +package ocxcli + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/lidge-jun/opencodex/go/internal/config" +) + +const shimMarker = "opencodex codex autostart shim" + +// runCodexShim ports the read-only status operation. State mutation remains +// delegated because it owns launcher replacement and rollback transactions. +func runCodexShim(args []string, deps Deps) int { + if len(args) != 1 || args[0] != "status" { + return runDelegated(append([]string{"codex-shim"}, args...), deps) + } + dir, err := config.Dir() + if err != nil { + fmt.Fprintln(deps.Stderr, err) + return ExitFailure + } + path := filepath.Join(dir, "codex-shim.json") + raw, err := os.ReadFile(path) + if os.IsNotExist(err) { + fmt.Fprintln(deps.Stdout, "Codex autostart shim is not installed.") + return ExitOK + } + if err != nil { + return invalidShimState(path, deps) + } + var state map[string]any + if json.Unmarshal(raw, &state) != nil { + return invalidShimState(path, deps) + } + platform, _ := state["platform"].(string) + if platform == "" { + return invalidShimState(path, deps) + } + files := []map[string]any{} + if wrappers, ok := state["wrappers"].([]any); ok { + for _, item := range wrappers { + if file, ok := item.(map[string]any); ok { + files = append(files, file) + } else { + return invalidShimState(path, deps) + } + } + } + if len(files) == 0 { + files = append(files, state) + } + lines := []string{} + healthy := true + for _, file := range files { + wrapperPath, wok := file["wrapperPath"].(string) + originalPath, ook := file["originalPath"].(string) + backupPath, bok := file["backupPath"].(string) + if !wok || !ook || !bok || wrapperPath == "" || originalPath == "" || backupPath == "" { + return invalidShimState(path, deps) + } + wrapper := "missing" + if bytes, err := os.ReadFile(wrapperPath); err == nil { + if strings.Contains(string(bytes), shimMarker) { + wrapper = "shim present" + } else { + wrapper = "present but not an opencodex shim" + healthy = false + } + } else { + healthy = false + } + backup := "missing" + if _, err := os.Stat(backupPath); err == nil { + backup = "present" + } else { + healthy = false + } + lines = append(lines, fmt.Sprintf("Codex autostart shim: wrapper %s at %s; original backup %s at %s.", wrapper, wrapperPath, backup, backupPath)) + } + fmt.Fprintln(deps.Stdout, strings.Join(lines, "\n")) + if healthy { + return ExitOK + } + return ExitFailure +} +func invalidShimState(path string, deps Deps) int { + fmt.Fprintln(deps.Stdout, "Codex autostart shim state is invalid or corrupt at "+path+". Reinstall or remove the shim.") + return ExitFailure +} From f7f799f15443e8f113bf09d9ff1baada86f4654f Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Mon, 7 Sep 2026 01:19:16 +0800 Subject: [PATCH 070/165] fix(go): preserve codex-shim status exit semantics --- go/internal/ocxcli/cli_test.go | 17 +++++++++++++++++ go/internal/ocxcli/shim_status.go | 13 ++----------- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/go/internal/ocxcli/cli_test.go b/go/internal/ocxcli/cli_test.go index 0a46876626..2bfa5d0e3f 100644 --- a/go/internal/ocxcli/cli_test.go +++ b/go/internal/ocxcli/cli_test.go @@ -316,6 +316,23 @@ func TestCodexShimMutationDelegates(t *testing.T) { } } +func TestCodexShimStatusReportsCorruptStateWithoutFailing(t *testing.T) { + dir := t.TempDir() + t.Setenv("OPENCODEX_HOME", dir) + if err := os.WriteFile(filepath.Join(dir, "codex-shim.json"), []byte("not json"), 0o600); err != nil { + t.Fatal(err) + } + var out, stderr bytes.Buffer + deps := depsFor(RuntimeState{}, &out, &stderr) + deps.Delegate = func([]string) (int, error) { t.Fatal("codex-shim status delegated"); return 0, nil } + if got := Run([]string{"codex-shim", "status"}, deps); got != ExitOK { + t.Fatalf("exit=%d stderr=%q", got, stderr.String()) + } + if !strings.Contains(out.String(), "state is invalid or corrupt") { + t.Fatalf("stdout=%q", out.String()) + } +} + func TestDelegatedFamilyHelpUsesOwnerOutput(t *testing.T) { for _, command := range []string{"status", "doctor", "service"} { t.Run(command, func(t *testing.T) { diff --git a/go/internal/ocxcli/shim_status.go b/go/internal/ocxcli/shim_status.go index e44ed1bee0..8bbbdec965 100644 --- a/go/internal/ocxcli/shim_status.go +++ b/go/internal/ocxcli/shim_status.go @@ -54,7 +54,6 @@ func runCodexShim(args []string, deps Deps) int { files = append(files, state) } lines := []string{} - healthy := true for _, file := range files { wrapperPath, wok := file["wrapperPath"].(string) originalPath, ook := file["originalPath"].(string) @@ -68,26 +67,18 @@ func runCodexShim(args []string, deps Deps) int { wrapper = "shim present" } else { wrapper = "present but not an opencodex shim" - healthy = false } - } else { - healthy = false } backup := "missing" if _, err := os.Stat(backupPath); err == nil { backup = "present" - } else { - healthy = false } lines = append(lines, fmt.Sprintf("Codex autostart shim: wrapper %s at %s; original backup %s at %s.", wrapper, wrapperPath, backup, backupPath)) } fmt.Fprintln(deps.Stdout, strings.Join(lines, "\n")) - if healthy { - return ExitOK - } - return ExitFailure + return ExitOK } func invalidShimState(path string, deps Deps) int { fmt.Fprintln(deps.Stdout, "Codex autostart shim state is invalid or corrupt at "+path+". Reinstall or remove the shim.") - return ExitFailure + return ExitOK } From a812d431135190c58f9664202b2435007b08d1ef Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Mon, 7 Sep 2026 01:25:20 +0800 Subject: [PATCH 071/165] feat(go): add config mutation coordinator --- go/go.mod | 14 ++ go/go.sum | 43 ++++++ go/internal/configschema/bun_smoke_test.go | 42 ++++++ go/internal/configschema/mutation.go | 154 +++++++++++++++++++++ go/internal/configschema/mutation_test.go | 101 ++++++++++++++ 5 files changed, 354 insertions(+) create mode 100644 go/go.sum create mode 100644 go/internal/configschema/bun_smoke_test.go create mode 100644 go/internal/configschema/mutation.go create mode 100644 go/internal/configschema/mutation_test.go diff --git a/go/go.mod b/go/go.mod index 5a6af1b9ca..22a2efc1b4 100644 --- a/go/go.mod +++ b/go/go.mod @@ -1,3 +1,17 @@ module github.com/lidge-jun/opencodex/go go 1.24 + +require modernc.org/sqlite v1.34.5 + +require ( + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/ncruces/go-strftime v0.1.9 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + golang.org/x/sys v0.22.0 // indirect + modernc.org/libc v1.55.3 // indirect + modernc.org/mathutil v1.6.0 // indirect + modernc.org/memory v1.8.0 // indirect +) diff --git a/go/go.sum b/go/go.sum new file mode 100644 index 0000000000..5424fe41de --- /dev/null +++ b/go/go.sum @@ -0,0 +1,43 @@ +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo= +github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= +github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic= +golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= +golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/tools v0.19.0 h1:tfGCXNR1OsFG+sVdLAitlpjAvD/I6dHDKnYrpEZUHkw= +golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc= +modernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ= +modernc.org/cc/v4 v4.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ= +modernc.org/ccgo/v4 v4.19.2 h1:lwQZgvboKD0jBwdaeVCTouxhxAyN6iawF3STraAal8Y= +modernc.org/ccgo/v4 v4.19.2/go.mod h1:ysS3mxiMV38XGRTTcgo0DQTeTmAO4oCmJl1nX9VFI3s= +modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE= +modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ= +modernc.org/gc/v2 v2.4.1 h1:9cNzOqPyMJBvrUipmynX0ZohMhcxPtMccYgGOJdOiBw= +modernc.org/gc/v2 v2.4.1/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU= +modernc.org/libc v1.55.3 h1:AzcW1mhlPNrRtjS5sS+eW2ISCgSOLLNyFzRh/V3Qj/U= +modernc.org/libc v1.55.3/go.mod h1:qFXepLhz+JjFThQ4kzwzOjA/y/artDeg+pcYnY+Q83w= +modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4= +modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo= +modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E= +modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU= +modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4= +modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= +modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc= +modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss= +modernc.org/sqlite v1.34.5 h1:Bb6SR13/fjp15jt70CL4f18JIN7p7dnMExd+UFnF15g= +modernc.org/sqlite v1.34.5/go.mod h1:YLuNmX9NKs8wRNK2ko1LW1NGYcc9FkBO69JOt1AR9JE= +modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA= +modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= diff --git a/go/internal/configschema/bun_smoke_test.go b/go/internal/configschema/bun_smoke_test.go new file mode 100644 index 0000000000..984d312c14 --- /dev/null +++ b/go/internal/configschema/bun_smoke_test.go @@ -0,0 +1,42 @@ +package configschema + +import ( + "context" + "errors" + "os" + "os/exec" + "path/filepath" + "testing" + "time" +) + +func TestBunHolderBlocksGoCoordinator(t *testing.T) { + if _, err := exec.LookPath("bun"); err != nil { t.Skip("bun unavailable") } + dir := t.TempDir() + ready := filepath.Join(dir, "ready") + db := filepath.Join(dir, mutationDatabaseName) + script := filepath.Join(dir, "hold.ts") + const source = `import { Database } from "bun:sqlite"; +import { writeFileSync } from "node:fs"; +const [databasePath, readyPath] = Bun.argv.slice(2); +const db = new Database(databasePath, { create: true }); +db.exec("PRAGMA busy_timeout = 0; BEGIN IMMEDIATE"); +writeFileSync(readyPath, "ready"); +setTimeout(() => { db.exec("ROLLBACK"); db.close(); }, 200); +` + if err := os.WriteFile(script, []byte(source), 0o600); err != nil { t.Fatal(err) } + cmd := exec.Command("bun", script, db, ready) + if err := cmd.Start(); err != nil { t.Fatal(err) } + defer func() { _ = cmd.Wait() }() + deadline := time.Now().Add(2 * time.Second) + for { + if _, err := os.Stat(ready); err == nil { break } + if time.Now().After(deadline) { t.Fatal("Bun holder did not acquire coordinator") } + time.Sleep(10 * time.Millisecond) + } + _, err := WithMutationCoordinator(context.Background(), filepath.Join(dir, "config.json"), nil, func(int64) (bool, error) { + t.Fatal("callback must not run while Bun owns BEGIN IMMEDIATE") + return false, nil + }) + if !errors.Is(err, ErrMutationBusy) { t.Fatalf("error = %v, want busy", err) } +} diff --git a/go/internal/configschema/mutation.go b/go/internal/configschema/mutation.go new file mode 100644 index 0000000000..35d8b28ca6 --- /dev/null +++ b/go/internal/configschema/mutation.go @@ -0,0 +1,154 @@ +// Package configschema owns the shared config.json schema boundary and the +// SQLite coordinator used by future Go-native config mutations. +// +// Current config write commands deliberately remain TypeScript-owned. This +// package only provides the cross-language BEGIN IMMEDIATE/generation +// foundation; it must not be wired into CLI dispatch until the TypeScript write +// contract is ported in full. Required prerequisites are: full write-boundary +// schema and load-normalization coverage, modelCosts key redaction, clearing +// activeCodexAccountPinned when codexAccountPriorities changes, and raw-byte +// revalidation/rebase for non-cooperating direct writers. +package configschema + +import ( + "context" + "database/sql" + "errors" + "fmt" + "os" + "path/filepath" + "strings" + + _ "modernc.org/sqlite" +) + +const mutationDatabaseName = "config-mutation.sqlite" + +const createGenerationTable = "CREATE TABLE IF NOT EXISTS config_generation (singleton INTEGER PRIMARY KEY CHECK (singleton = 1), value INTEGER NOT NULL CHECK (value >= 0))" + +var ( + // ErrMutationBusy matches TypeScript's busy_timeout=0 policy: callers fail + // promptly when either runtime owns BEGIN IMMEDIATE. + ErrMutationBusy = errors.New("config mutation already in progress") + ErrGenerationConflict = errors.New("config generation conflict") +) + +type GenerationConflictError struct{ Current int64 } + +func (e *GenerationConflictError) Error() string { + return fmt.Sprintf("%s: current generation %d", ErrGenerationConflict, e.Current) +} +func (e *GenerationConflictError) Unwrap() error { return ErrGenerationConflict } + +type MutationResult struct { + Changed bool + Generation int64 +} + +// MutationDatabasePath is $OPENCODEX_HOME/config-mutation.sqlite, the exact +// coordinator location used by src/config.ts. +func MutationDatabasePath(configPath string) string { + return filepath.Join(filepath.Dir(configPath), mutationDatabaseName) +} + +// ReadGeneration observes an existing coordinator without creating it. Only a +// writer holding BEGIN IMMEDIATE may create the generation singleton. +func ReadGeneration(ctx context.Context, configPath string) (int64, error) { + dbPath := MutationDatabasePath(configPath) + if _, err := os.Stat(dbPath); err != nil { + return 0, err + } + db, err := sql.Open("sqlite", dbPath) + if err != nil { + return 0, err + } + defer db.Close() + var generation int64 + if err := db.QueryRowContext(ctx, "SELECT value FROM config_generation WHERE singleton = 1").Scan(&generation); err != nil { + return 0, err + } + if generation < 0 { + return 0, errors.New("config generation singleton is invalid") + } + return generation, nil +} + +// WithMutationCoordinator runs callback inside the same SQLite transaction as +// TypeScript's withConfigMutationLockSync: busy_timeout=0, BEGIN IMMEDIATE, +// singleton initialization, and commit/rollback. callback receives the current +// generation and returns whether it published a changed config.json while the +// transaction was held. A changed result increments generation in that same +// transaction. Future callers own config-byte freshness/rebase checks. +func WithMutationCoordinator(ctx context.Context, configPath string, expected *int64, callback func(generation int64) (changed bool, err error)) (result MutationResult, retErr error) { + dir := filepath.Dir(configPath) + if err := os.MkdirAll(dir, 0o700); err != nil { + return result, err + } + dbPath := MutationDatabasePath(configPath) + db, err := sql.Open("sqlite", dbPath) + if err != nil { + return result, err + } + defer db.Close() + _ = os.Chmod(dbPath, 0o600) + conn, err := db.Conn(ctx) + if err != nil { + return result, classifyMutationError(err) + } + defer conn.Close() + // busy_timeout is connection-local; it must be set on the exact handle that + // acquires BEGIN IMMEDIATE, not a separate database/sql pool connection. + if _, err := conn.ExecContext(ctx, "PRAGMA busy_timeout = 0"); err != nil { + return result, classifyMutationError(err) + } + if _, err := conn.ExecContext(ctx, "BEGIN IMMEDIATE"); err != nil { + return result, classifyMutationError(err) + } + open := true + defer func() { + if open { + _, _ = conn.ExecContext(context.Background(), "ROLLBACK") + } + }() + if _, err := conn.ExecContext(ctx, createGenerationTable); err != nil { + return result, err + } + if _, err := conn.ExecContext(ctx, "INSERT OR IGNORE INTO config_generation (singleton, value) VALUES (1, 0)"); err != nil { + return result, err + } + var generation int64 + if err := conn.QueryRowContext(ctx, "SELECT value FROM config_generation WHERE singleton = 1").Scan(&generation); err != nil || generation < 0 { + if err == nil { + err = errors.New("config generation singleton is invalid") + } + return result, err + } + if expected != nil && *expected != generation { + return result, &GenerationConflictError{Current: generation} + } + changed, err := callback(generation) + if err != nil { + return result, err + } + result.Generation = generation + if changed { + if _, err := conn.ExecContext(ctx, "UPDATE config_generation SET value = value + 1 WHERE singleton = 1 AND value = ?", generation); err != nil { + return result, err + } + result.Changed = true + result.Generation++ + } + if _, err := conn.ExecContext(ctx, "COMMIT"); err != nil { + return result, classifyMutationError(err) + } + open = false + return result, nil +} + +func classifyMutationError(err error) error { + message := strings.ToLower(err.Error()) + if strings.Contains(message, "database is locked") || strings.Contains(message, "database is busy") || strings.Contains(message, "sqlite_busy") || strings.Contains(message, "sqlite_locked") { + return fmt.Errorf("%w: %v", ErrMutationBusy, err) + } + return err +} diff --git a/go/internal/configschema/mutation_test.go b/go/internal/configschema/mutation_test.go new file mode 100644 index 0000000000..35168d8654 --- /dev/null +++ b/go/internal/configschema/mutation_test.go @@ -0,0 +1,101 @@ +package configschema + +import ( + "context" + "database/sql" + "errors" + "path/filepath" + "testing" + + _ "modernc.org/sqlite" +) + +func TestMutationCoordinatorInitializesAndBumpsGeneration(t *testing.T) { + path := filepath.Join(t.TempDir(), "config.json") + zero := int64(0) + result, err := WithMutationCoordinator(context.Background(), path, &zero, func(generation int64) (bool, error) { + if generation != 0 { + t.Fatalf("generation in callback = %d", generation) + } + return true, nil + }) + if err != nil { + t.Fatal(err) + } + if !result.Changed || result.Generation != 1 { + t.Fatalf("result = %+v", result) + } + if got, err := ReadGeneration(context.Background(), path); err != nil || got != 1 { + t.Fatalf("generation = %d, %v", got, err) + } +} + +func TestMutationCoordinatorFailsImmediatelyWhileAnotherWriterHoldsImmediate(t *testing.T) { + path := filepath.Join(t.TempDir(), "config.json") + db, err := sql.Open("sqlite", MutationDatabasePath(path)) + if err != nil { + t.Fatal(err) + } + defer db.Close() + if _, err := db.Exec("BEGIN IMMEDIATE"); err != nil { + t.Fatal(err) + } + defer db.Exec("ROLLBACK") + _, err = WithMutationCoordinator(context.Background(), path, nil, func(int64) (bool, error) { + t.Fatal("callback must not run while busy") + return false, nil + }) + if !errors.Is(err, ErrMutationBusy) { + t.Fatalf("error = %v, want busy", err) + } +} + +func TestMutationCoordinatorRejectsStaleGenerationAndRollsBackCallback(t *testing.T) { + path := filepath.Join(t.TempDir(), "config.json") + stale := int64(9) + _, err := WithMutationCoordinator(context.Background(), path, &stale, func(int64) (bool, error) { + t.Fatal("callback must not run for stale generation") + return false, nil + }) + var conflict *GenerationConflictError + if !errors.As(err, &conflict) || conflict.Current != 0 { + t.Fatalf("error = %v, conflict = %+v", err, conflict) + } + _, err = WithMutationCoordinator(context.Background(), path, nil, func(int64) (bool, error) { + return false, errors.New("abort") + }) + if err == nil { + t.Fatal("callback error was swallowed") + } + // The first transaction's table creation was rolled back, so a later + // acquisition must recreate a clean singleton at zero. + result, err := WithMutationCoordinator(context.Background(), path, nil, func(generation int64) (bool, error) { + if generation != 0 { + t.Fatalf("generation after rollback = %d", generation) + } + return false, nil + }) + if err != nil || result.Changed || result.Generation != 0 { + t.Fatalf("post-rollback result = %+v, %v", result, err) + } +} + +func TestMutationCoordinatorCrashRecoveryReleasesImmediate(t *testing.T) { + path := filepath.Join(t.TempDir(), "config.json") + db, err := sql.Open("sqlite", MutationDatabasePath(path)) + if err != nil { + t.Fatal(err) + } + if _, err := db.Exec("BEGIN IMMEDIATE"); err != nil { + t.Fatal(err) + } + // Closing an uncommitted connection models process exit: SQLite releases + // BEGIN IMMEDIATE without a stale-owner cleanup protocol. + if err := db.Close(); err != nil { + t.Fatal(err) + } + result, err := WithMutationCoordinator(context.Background(), path, nil, func(int64) (bool, error) { return true, nil }) + if err != nil || !result.Changed || result.Generation != 1 { + t.Fatalf("result=%+v err=%v", result, err) + } +} From 6561879b1a26f9e8f5b787a51263737aaaea3f68 Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Mon, 7 Sep 2026 01:31:08 +0800 Subject: [PATCH 072/165] fix(go): close config write parity prerequisites --- go/internal/configschema/mutation.go | 80 ++++++++++++++++- go/internal/configschema/mutation_test.go | 94 ++++++++++++++++++++ go/internal/configschema/persistence.go | 60 +++++++------ go/internal/configschema/schema.go | 102 +++++++++++++++++++++- go/internal/configschema/schema_test.go | 77 ++++++++++++++++ 5 files changed, 380 insertions(+), 33 deletions(-) diff --git a/go/internal/configschema/mutation.go b/go/internal/configschema/mutation.go index 35d8b28ca6..ec9a8da30c 100644 --- a/go/internal/configschema/mutation.go +++ b/go/internal/configschema/mutation.go @@ -4,13 +4,14 @@ // Current config write commands deliberately remain TypeScript-owned. This // package only provides the cross-language BEGIN IMMEDIATE/generation // foundation; it must not be wired into CLI dispatch until the TypeScript write -// contract is ported in full. Required prerequisites are: full write-boundary -// schema and load-normalization coverage, modelCosts key redaction, clearing -// activeCodexAccountPinned when codexAccountPriorities changes, and raw-byte -// revalidation/rebase for non-cooperating direct writers. +// contract is ported in full. The library now includes display redaction, the +// account-priority pin hook, and raw-byte revalidation; full write-boundary +// schema/load-normalization coverage and command-level parity remain required +// before CLI ownership can change. package configschema import ( + "bytes" "context" "database/sql" "errors" @@ -31,6 +32,9 @@ var ( // promptly when either runtime owns BEGIN IMMEDIATE. ErrMutationBusy = errors.New("config mutation already in progress") ErrGenerationConflict = errors.New("config generation conflict") + // ErrRawByteConflict is the public CLI wording used by TypeScript when a + // direct config.json writer keeps winning the bounded rebase loop. + ErrRawByteConflict = errors.New("config changed while applying this update; retry") ) type GenerationConflictError struct{ Current int64 } @@ -45,6 +49,20 @@ type MutationResult struct { Generation int64 } +// RawByteConflictError reports a direct writer which changed config.json while +// the SQLite coordinator was held. It unwraps to ErrRawByteConflict so a CLI +// caller can present the TypeScript-compatible retry message without matching +// error text. +type RawByteConflictError struct{ Attempts int } + +func (e *RawByteConflictError) Error() string { return ErrRawByteConflict.Error() } +func (e *RawByteConflictError) Unwrap() error { return ErrRawByteConflict } + +// ConfigMutationMaxRebaseAttempts is the bounded direct-writer retry budget +// used by TypeScript's mutatePersistedConfig. A direct writer can always race a +// final rename, so the operation fails closed after this many observed changes. +const ConfigMutationMaxRebaseAttempts = 3 + // MutationDatabasePath is $OPENCODEX_HOME/config-mutation.sqlite, the exact // coordinator location used by src/config.ts. func MutationDatabasePath(configPath string) string { @@ -145,6 +163,60 @@ func WithMutationCoordinator(ctx context.Context, configPath string, expected *i return result, nil } +// WithRevalidatedConfigMutation is the raw-byte freshness transaction for a +// future native config set/unset/import dispatcher. It reads the authoritative +// config bytes only after BEGIN IMMEDIATE, runs mutate on a copy, then rereads +// twice before every atomic write. A non-cooperating direct writer therefore +// rebases the mutation against its latest bytes; repeated changes fail with the +// same retry message the TypeScript CLI reports. +// +// mutate must return complete replacement bytes. It may be invoked more than +// once, and must therefore be side-effect free outside the proposed config. +func WithRevalidatedConfigMutation(ctx context.Context, configPath string, expected *int64, mutate func(raw []byte, generation int64) (replacement []byte, changed bool, err error)) (MutationResult, error) { + return WithMutationCoordinator(ctx, configPath, expected, func(generation int64) (bool, error) { + base, err := os.ReadFile(configPath) + if err != nil { + return false, err + } + for attempt := 0; attempt < ConfigMutationMaxRebaseAttempts; attempt++ { + // The first decision catches a direct write that happened before or + // during mutation evaluation. + _, changed, err := mutate(bytes.Clone(base), generation) + if err != nil || !changed { + return changed, err + } + latest, err := os.ReadFile(configPath) + if err != nil { + return false, err + } + if !bytes.Equal(latest, base) { + base = latest + continue + } + // Match TypeScript's confirmed callback: even unchanged bytes are + // replayed because another authority (for example a credential + // generation) may have changed at the revalidation seam. + proposal, changed, err := mutate(bytes.Clone(latest), generation) + if err != nil || !changed { + return changed, err + } + commitBase, err := os.ReadFile(configPath) + if err != nil { + return false, err + } + if !bytes.Equal(commitBase, latest) { + base = commitBase + continue + } + if err := writeConfigBytesAtomic(configPath, proposal); err != nil { + return false, err + } + return true, nil + } + return false, &RawByteConflictError{Attempts: ConfigMutationMaxRebaseAttempts} + }) +} + func classifyMutationError(err error) error { message := strings.ToLower(err.Error()) if strings.Contains(message, "database is locked") || strings.Contains(message, "database is busy") || strings.Contains(message, "sqlite_busy") || strings.Contains(message, "sqlite_locked") { diff --git a/go/internal/configschema/mutation_test.go b/go/internal/configschema/mutation_test.go index 35168d8654..19afa7e63e 100644 --- a/go/internal/configschema/mutation_test.go +++ b/go/internal/configschema/mutation_test.go @@ -1,9 +1,12 @@ package configschema import ( + "bytes" "context" "database/sql" "errors" + "fmt" + "os" "path/filepath" "testing" @@ -99,3 +102,94 @@ func TestMutationCoordinatorCrashRecoveryReleasesImmediate(t *testing.T) { t.Fatalf("result=%+v err=%v", result, err) } } + +func TestRevalidatedMutationRebasesAfterDirectWriterChangesBytes(t *testing.T) { + path := filepath.Join(t.TempDir(), "config.json") + if err := os.WriteFile(path, []byte(`{"revision":1}`), 0o600); err != nil { + t.Fatal(err) + } + calls := 0 + result, err := WithRevalidatedConfigMutation(context.Background(), path, nil, func(raw []byte, _ int64) ([]byte, bool, error) { + calls++ + if calls == 1 { + // This bypasses config-mutation.sqlite like a direct editor. The Go + // transaction must discard its stale proposal and rerun on these bytes. + if err := os.WriteFile(path, []byte(`{"revision":2,"external":true}`), 0o600); err != nil { + return nil, false, err + } + } + return append(raw[:len(raw)-1], []byte(`,"native":true}`)...), true, nil + }) + if err != nil { + t.Fatal(err) + } + if !result.Changed || result.Generation != 1 || calls != 3 { + t.Fatalf("result=%+v calls=%d", result, calls) + } + got, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + want := []byte(`{"revision":2,"external":true,"native":true}`) + if !bytes.Equal(got, want) { + t.Fatalf("rebase lost direct writer bytes: got %s want %s", got, want) + } +} + +func TestRevalidatedMutationChecksBytesAfterConfirmedReplay(t *testing.T) { + path := filepath.Join(t.TempDir(), "config.json") + if err := os.WriteFile(path, []byte(`{"revision":1}`), 0o600); err != nil { + t.Fatal(err) + } + calls := 0 + _, err := WithRevalidatedConfigMutation(context.Background(), path, nil, func(raw []byte, _ int64) ([]byte, bool, error) { + calls++ + if calls == 2 { + // The first read-back was equal. Alter bytes in the confirmed replay + // so only the final pre-write read can prevent the stale overwrite. + if err := os.WriteFile(path, []byte(`{"revision":2,"external":true}`), 0o600); err != nil { + return nil, false, err + } + } + return append(raw[:len(raw)-1], []byte(`,"native":true}`)...), true, nil + }) + if err != nil { + t.Fatal(err) + } + got, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + want := []byte(`{"revision":2,"external":true,"native":true}`) + if !bytes.Equal(got, want) || calls != 4 { + t.Fatalf("final revalidation failed: got=%s calls=%d", got, calls) + } +} + +func TestRevalidatedMutationFailsClosedAfterRepeatedDirectWrites(t *testing.T) { + path := filepath.Join(t.TempDir(), "config.json") + if err := os.WriteFile(path, []byte(`{"revision":0}`), 0o600); err != nil { + t.Fatal(err) + } + calls := 0 + _, err := WithRevalidatedConfigMutation(context.Background(), path, nil, func(raw []byte, _ int64) ([]byte, bool, error) { + calls++ + if err := os.WriteFile(path, []byte(fmt.Sprintf(`{"revision":%d}`, calls)), 0o600); err != nil { + return nil, false, err + } + return append(raw[:len(raw)-1], []byte(`,"native":true}`)...), true, nil + }) + if !errors.Is(err, ErrRawByteConflict) || err.Error() != "config changed while applying this update; retry" { + t.Fatalf("error=%v, want TypeScript retry conflict", err) + } + if calls != ConfigMutationMaxRebaseAttempts { + t.Fatalf("calls=%d, want %d", calls, ConfigMutationMaxRebaseAttempts) + } + got, readErr := os.ReadFile(path) + if readErr != nil { + t.Fatal(readErr) + } + if bytes.Contains(got, []byte("native")) { + t.Fatalf("conflicted proposal reached disk: %s", got) + } +} diff --git a/go/internal/configschema/persistence.go b/go/internal/configschema/persistence.go index c6147bbcf1..f5b7c4f456 100644 --- a/go/internal/configschema/persistence.go +++ b/go/internal/configschema/persistence.go @@ -48,31 +48,39 @@ func WriteAtomicLocked(ctx context.Context, path string, config *Normalized) err return err } data = append(data, '\n') - dir := filepath.Dir(path) - tmp, err := os.CreateTemp(dir, ".config.json-*") - if err != nil { - return err - } - name := tmp.Name() - defer os.Remove(name) - if err := tmp.Chmod(0o600); err != nil { - tmp.Close() - return err - } - if _, err := tmp.Write(data); err != nil { - tmp.Close() - return err - } - if err := tmp.Sync(); err != nil { - tmp.Close() - return err - } - if err := tmp.Close(); err != nil { - return err - } - if err := os.Rename(name, path); err != nil { - return err - } - return os.Chmod(path, 0o600) + return writeConfigBytesAtomic(path, data) }) } + +// writeConfigBytesAtomic publishes already-serialized JSON using the same +// 0600 temp/fsync/rename protocol as WriteAtomicLocked. The SQLite revalidated +// transaction owns cross-runtime serialization when this helper is called from +// WithRevalidatedConfigMutation. +func writeConfigBytesAtomic(path string, data []byte) error { + dir := filepath.Dir(path) + tmp, err := os.CreateTemp(dir, ".config.json-*") + if err != nil { + return err + } + name := tmp.Name() + defer os.Remove(name) + if err := tmp.Chmod(0o600); err != nil { + tmp.Close() + return err + } + if _, err := tmp.Write(data); err != nil { + tmp.Close() + return err + } + if err := tmp.Sync(); err != nil { + tmp.Close() + return err + } + if err := tmp.Close(); err != nil { + return err + } + if err := os.Rename(name, path); err != nil { + return err + } + return os.Chmod(path, 0o600) +} diff --git a/go/internal/configschema/schema.go b/go/internal/configschema/schema.go index 688cb96739..cff890ef5d 100644 --- a/go/internal/configschema/schema.go +++ b/go/internal/configschema/schema.go @@ -10,6 +10,7 @@ import ( "fmt" "io" "math" + "regexp" "strconv" "strings" ) @@ -89,6 +90,22 @@ func (n *Normalized) RedactedIndentedJSON() ([]byte, error) { return indented.Bytes(), nil } +// ClearCodexAccountPinForSet applies the config-set hook shared by the +// TypeScript CLI. Restating any codexAccountPriorities path releases a stale +// manual account pin; imports deliberately do not call this hook because an +// import supplies its own complete pin state. It is intentionally a library +// operation until the native write dispatcher owns the full set contract. +func (n *Normalized) ClearCodexAccountPinForSet(path string) bool { + if n == nil || n.root == nil || n.root.kind != objectKind { + return false + } + first := strings.TrimSpace(strings.Split(path, ".")[0]) + if first != "codexAccountPriorities" { + return false + } + return n.root.delete("activeCodexAccountPinned") +} + type valueKind uint8 const ( @@ -201,6 +218,18 @@ func (v *value) set(key string, x *value) { } v.object = append(v.object, member{key, x}) } +func (v *value) delete(key string) bool { + if v == nil || v.kind != objectKind { + return false + } + for i, m := range v.object { + if m.key == key { + v.object = append(v.object[:i], v.object[i+1:]...) + return true + } + } + return false +} func (v *value) has(key string) bool { return v.find(key) != nil } func number(n int64) *value { return &value{kind: numberKind, number: json.Number(strconv.FormatInt(n, 10))} @@ -418,6 +447,9 @@ func (v *value) write(b *bytes.Buffer) { } func redactValue(v *value, key string) *value { + if key == "modelCosts" { + return sanitizeModelCostsForDisplay(v) + } if isSecretKey(key) && v.kind == stringKind && v.text != "" { return stringValue("********") } @@ -429,9 +461,16 @@ func redactValue(v *value, key string) *value { } return out case objectKind: - out := &value{kind: objectKind, object: make([]member, len(v.object))} - for i, child := range v.object { - out.object[i] = member{key: child.key, value: redactValue(child.value, child.key)} + out := &value{kind: objectKind, object: make([]member, 0, len(v.object))} + for _, child := range v.object { + redacted := redactValue(child.value, child.key) + // JSON.stringify omits an object property whose value is undefined. + // TS's sanitizeModelCostsForDisplay returns undefined when no row + // survives, so retain the same projection here. + if child.key == "modelCosts" && redacted == nil { + continue + } + out.object = append(out.object, member{key: child.key, value: redacted}) } return out default: @@ -439,6 +478,63 @@ func redactValue(v *value, key string) *value { } } +var secretModelIDPatterns = []*regexp.Regexp{ + regexp.MustCompile(`(?i)(?:^|[^A-Za-z0-9._-])sk-[A-Za-z0-9][A-Za-z0-9._-]{6,}(?:$|[^A-Za-z0-9._-])`), + regexp.MustCompile(`(?i)(?:^|[^A-Za-z0-9_])(gh[pousr]_[A-Za-z0-9_]{8,}|github_pat_[A-Za-z0-9_]{20,})(?:$|[^A-Za-z0-9_])`), + regexp.MustCompile(`(?i)\b(?:api[_-]?key|access[_-]?token|refresh[_-]?token|id[_-]?token|client[_-]?secret)=[^&\s"',;]+`), +} + +// sanitizeModelCostsForDisplay mirrors sanitizeModelCostsForDisplay in +// src/config.ts: project only valid four-rate tuples and drop a model ID which +// resembles a credential rather than replacing it with a colliding placeholder. +// nil models JavaScript's undefined, causing the containing modelCosts member to +// be omitted by redactValue. +func sanitizeModelCostsForDisplay(costs *value) *value { + if costs == nil || costs.kind != objectKind { + return nil + } + out := &value{kind: objectKind} + for _, row := range costs.object { + if secretShapedModelID(row.key) || row.value == nil || row.value.kind != objectKind { + continue + } + rates := make([]member, 0, 4) + valid := true + for _, field := range []string{"input", "output", "cacheRead", "cacheWrite"} { + rate := row.value.find(field) + if !validCostRate(rate) { + valid = false + break + } + rates = append(rates, member{key: field, value: rate}) + } + if valid { + out.object = append(out.object, member{key: row.key, value: &value{kind: objectKind, object: rates}}) + } + } + if len(out.object) == 0 { + return nil + } + return out +} + +func validCostRate(v *value) bool { + if v == nil || v.kind != numberKind { + return false + } + n, err := strconv.ParseFloat(v.number.String(), 64) + return err == nil && !math.IsNaN(n) && !math.IsInf(n, 0) && n >= 0 && n <= 1_000_000 +} + +func secretShapedModelID(id string) bool { + for _, pattern := range secretModelIDPatterns { + if pattern.MatchString(id) { + return true + } + } + return false +} + func isSecretKey(key string) bool { switch strings.ToLower(key) { case "apikey", "key", "accesstoken", "refreshtoken", "idtoken", "token", "password", "clientsecret": diff --git a/go/internal/configschema/schema_test.go b/go/internal/configschema/schema_test.go index b0350b56b4..148c330e45 100644 --- a/go/internal/configschema/schema_test.go +++ b/go/internal/configschema/schema_test.go @@ -66,3 +66,80 @@ func TestNormalizeDropsLoadTimeDegradedOptionals(t *testing.T) { t.Fatalf("app default missing: %s", compact) } } + +func TestRedactedProjectionDropsSecretAndInvalidModelCostsRows(t *testing.T) { + normalized, err := NormalizeJSON([]byte(`{ + "providers": { + "example": { + "adapter": "openai-chat", + "baseUrl": "https://example.test", + "modelCosts": { + "gpt-safe": {"input": 1, "output": 2, "cacheRead": 0, "cacheWrite": 3, "ignored": "not displayed"}, + "sk-abcdef1234567890": {"input": 99, "output": 99, "cacheRead": 99, "cacheWrite": 99}, + "bad-rate": {"input": 1, "output": 2, "cacheRead": 0} + } + } + } +}`)) + if err != nil { + t.Fatal(err) + } + got, err := normalized.RedactedIndentedJSON() + if err != nil { + t.Fatal(err) + } + text := string(got) + for _, leaked := range []string{"sk-abcdef1234567890", "99", "bad-rate", "ignored"} { + if strings.Contains(text, leaked) { + t.Fatalf("redacted config leaked %q: %s", leaked, text) + } + } + if !strings.Contains(text, `"gpt-safe": {`) || !strings.Contains(text, `"cacheWrite": 3`) { + t.Fatalf("valid display tuple missing: %s", text) + } +} + +func TestRedactedProjectionOmitsEmptyModelCosts(t *testing.T) { + normalized, err := NormalizeJSON([]byte(`{"providers":{"example":{"adapter":"openai-chat","baseUrl":"https://example.test","modelCosts":{"sk-abcdef1234567890":{"input":1,"output":1,"cacheRead":1,"cacheWrite":1}}}}}`)) + if err != nil { + t.Fatal(err) + } + got, err := normalized.RedactedIndentedJSON() + if err != nil { + t.Fatal(err) + } + if strings.Contains(string(got), "modelCosts") { + t.Fatalf("empty sanitized modelCosts must be omitted: %s", got) + } +} + +func TestSetCodexAccountPrioritiesClearsManualPin(t *testing.T) { + normalized, err := NormalizeJSON([]byte(`{"providers":{},"activeCodexAccountPinned":"acct-1","codexAccountPriorities":{"acct-1":7}}`)) + if err != nil { + t.Fatal(err) + } + if !normalized.ClearCodexAccountPinForSet("codexAccountPriorities.acct-1") { + t.Fatal("priority set did not clear manual pin") + } + got, err := normalized.CompactJSON() + if err != nil { + t.Fatal(err) + } + if strings.Contains(string(got), "activeCodexAccountPinned") { + t.Fatalf("manual pin survived priority update: %s", got) + } + other, err := NormalizeJSON([]byte(`{"providers":{},"activeCodexAccountPinned":"acct-2"}`)) + if err != nil { + t.Fatal(err) + } + if other.ClearCodexAccountPinForSet("providers.example.adapter") { + t.Fatal("unrelated config set cleared a pin") + } + otherJSON, err := other.CompactJSON() + if err != nil { + t.Fatal(err) + } + if !strings.Contains(string(otherJSON), "activeCodexAccountPinned") { + t.Fatalf("unrelated config set removed pin: %s", otherJSON) + } +} From 8578d4689653d396cbd6d7ab04c75fbff2fe4eec Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Mon, 7 Sep 2026 01:38:04 +0800 Subject: [PATCH 073/165] test(go): cover remaining relay refusal classes --- go/internal/sidecar/hotpath.go | 6 ++ go/internal/sidecar/hotpath_test.go | 7 +- src/server/go-sidecar.ts | 6 ++ src/server/hot-path-seam.ts | 2 + src/server/index.ts | 3 +- tests/go-hotpath-relay.test.ts | 130 +++++++++++++++++++++++++++- tests/hot-path-seam.test.ts | 14 ++- 7 files changed, 161 insertions(+), 7 deletions(-) diff --git a/go/internal/sidecar/hotpath.go b/go/internal/sidecar/hotpath.go index 56512d4484..d220c1aaa0 100644 --- a/go/internal/sidecar/hotpath.go +++ b/go/internal/sidecar/hotpath.go @@ -113,6 +113,12 @@ func dataPlaneSeam(w http.ResponseWriter, r *http.Request, cfg Config) { if contentType := r.Header.Get("Content-Type"); contentType != "" { bridgeReq.Header.Set("Content-Type", contentType) } + // The public surface marker is intentionally the only caller header this + // bridge relays. It carries no credential material, and preserving it makes + // the fallback execute the same TypeScript Grok surface as the front door. + if r.Header.Get("X-Opencodex-Grok") == "1" { + bridgeReq.Header.Set("X-Opencodex-Grok", "1") + } bridgeResp, err := dataPlaneBridgeClient().Do(bridgeReq) if err != nil { diff --git a/go/internal/sidecar/hotpath_test.go b/go/internal/sidecar/hotpath_test.go index 9adf54b153..8efeff67c4 100644 --- a/go/internal/sidecar/hotpath_test.go +++ b/go/internal/sidecar/hotpath_test.go @@ -43,7 +43,7 @@ func TestDataPlaneSeamRelaysStreamByteForByte(t *testing.T) { const bridgeToken = "sidecar-to-parent" const body = `{"model":"fixture","input":"ping","stream":true}` var gotBridgeMethod, gotBridgePath, gotBridgeToken string - var gotClaimNonce, gotClaimAdmission, gotContentType string + var gotClaimNonce, gotClaimAdmission, gotContentType, gotGrokSurface string var gotBody []byte bridge := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -53,6 +53,7 @@ func TestDataPlaneSeamRelaysStreamByteForByte(t *testing.T) { gotClaimNonce = r.Header.Get(DataPlaneNonceHeader) gotClaimAdmission = r.Header.Get(DataPlaneAdmissionHeader) gotContentType = r.Header.Get("Content-Type") + gotGrokSurface = r.Header.Get("X-Opencodex-Grok") var readErr error gotBody, readErr = io.ReadAll(r.Body) if readErr != nil { @@ -77,6 +78,7 @@ func TestDataPlaneSeamRelaysStreamByteForByte(t *testing.T) { h := dataPlaneSeamHandler(t, requestToken, bridgeToken, bridge.URL) req := httptest.NewRequest(http.MethodPost, "/v1/responses", bytes.NewBufferString(body)) req.Header = dataPlaneHeaders(requestToken, bridgeToken, []byte(body)) + req.Header.Set("X-Opencodex-Grok", "1") rec := httptest.NewRecorder() h.ServeHTTP(rec, req) resp := rec.Result() @@ -108,6 +110,9 @@ func TestDataPlaneSeamRelaysStreamByteForByte(t *testing.T) { if gotContentType != "application/json" { t.Fatalf("content-type = %q", gotContentType) } + if gotGrokSurface != "1" { + t.Fatalf("grok surface = %q, want 1", gotGrokSurface) + } if string(gotBody) != body { t.Fatalf("bridge body = %q, want %q", gotBody, body) } diff --git a/src/server/go-sidecar.ts b/src/server/go-sidecar.ts index 09431249a0..c063675032 100644 --- a/src/server/go-sidecar.ts +++ b/src/server/go-sidecar.ts @@ -153,6 +153,12 @@ export async function forwardHotPathSeam( headers.set(HOT_PATH_SIDECAR_REQUEST_HEADER, seam.requestToken); const contentType = request.headers.get("content-type"); if (contentType) headers.set("content-type", contentType); + // This is a non-credential surface attribution marker. It must survive the + // tightly allowlisted seam so the Go relay can decline Grok traffic and the + // TypeScript bridge can preserve the request's observable surface. + if (request.headers.get("x-opencodex-grok") === "1") { + headers.set("x-opencodex-grok", "1"); + } const upstream = await directLocalHttpFetch(target, { method: "POST", headers, diff --git a/src/server/hot-path-seam.ts b/src/server/hot-path-seam.ts index d7008181f1..232af0cb7b 100644 --- a/src/server/hot-path-seam.ts +++ b/src/server/hot-path-seam.ts @@ -185,6 +185,7 @@ export interface HotPathResponsesBridgeOptions { dispatchResponses(context: { admission: DataPlaneAdmission; contentType: string | null; + grokSurface: boolean; body: Uint8Array; signal: AbortSignal | null; }): Promise; @@ -250,6 +251,7 @@ export function createHotPathResponsesBridge(options: HotPathResponsesBridgeOpti return await options.dispatchResponses({ admission: claim.admission, contentType, + grokSurface: request.headers.get("x-opencodex-grok") === "1", body, signal: request.signal, }); diff --git a/src/server/index.ts b/src/server/index.ts index 5a89485f9c..54bdade5cf 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -2675,9 +2675,10 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server { + dispatchResponses: async ({ admission, contentType, grokSurface, body, signal }) => { const headers = new Headers(); if (contentType) headers.set("content-type", contentType); + if (grokSurface) headers.set("x-opencodex-grok", "1"); const internalReq = new Request("http://localhost/v1/responses", { method: "POST", headers, diff --git a/tests/go-hotpath-relay.test.ts b/tests/go-hotpath-relay.test.ts index bd790864c3..10a4293e9d 100644 --- a/tests/go-hotpath-relay.test.ts +++ b/tests/go-hotpath-relay.test.ts @@ -187,10 +187,15 @@ function configFixture(upstreamPort: number, providerOverrides: Record { +async function postCase( + server: { url: URL }, + token: string, + body: unknown, + headers: Record = {}, +): Promise { const response = await fetch(new URL("/v1/responses", server.url), { method: "POST", - headers: { "content-type": "application/json", "x-opencodex-api-key": token }, + headers: { "content-type": "application/json", "x-opencodex-api-key": token, ...headers }, body: JSON.stringify(body), }); return { @@ -209,11 +214,17 @@ function captureEnv(): void { } } -function setUpFixture(upstreamPort: number, providerOverrides?: Record): void { +function setUpFixture( + upstreamPort: number, + providerOverrides?: Record, + configure?: (config: Record) => void, +): void { testHome = mkdtempSync(join(tmpdir(), "ocx-hotpath-relay-")); process.env.OPENCODEX_HOME = testHome; process.env.OPENCODEX_API_AUTH_TOKEN = "data-secret"; - saveConfig(configFixture(upstreamPort, providerOverrides)); + const fixture = configFixture(upstreamPort, providerOverrides) as Record; + configure?.(fixture); + saveConfig(fixture as Parameters[0]); } function tearDownFixture(): void { @@ -479,4 +490,115 @@ describe.skipIf(!goAvailable || sidecarBinary === null)("ocx-sidecar non-streami expect(goLog.providerHeader).toBe(oracleLog.providerHeader); } }); + + runFixtureTest("every remaining relay refusal stays byte-identical on the TypeScript bridge", async () => { + const token = "data-secret"; + const port = upstream!.port; + type RefusalCase = { + name: string; + body: unknown; + provider?: Record; + headers?: Record; + configure?: (config: Record) => void; + }; + const cases: RefusalCase[] = [ + { + name: "combos use the TypeScript picker", + body: { model: "test-model", input: "plain" }, + configure: config => { + config.combos = { only: { targets: [{ provider: "test", model: "test-model" }] } }; + }, + }, + { + name: "routing profiles use the TypeScript policy engine", + body: { model: "test-model", input: "plain" }, + configure: config => { + config.routingProfiles = { only: { candidates: [{ provider: "test", model: "test-model" }] } }; + }, + }, + { + name: "blocked model redirects rewrite before routing", + body: { model: "blocked", input: "plain" }, + configure: config => { config.blockedModelRedirects = { blocked: "test-model" }; }, + }, + { + name: "shadow intercept rewrites before routing", + body: { model: "source-model", input: "plain" }, + configure: config => { + config.shadowCallIntercept = { enabled: true, model: "test-model", sourceModels: ["source-model"] }; + }, + }, + { name: "oauth providers remain on the TypeScript credential path", body: { model: "test-model", input: "plain" }, provider: { authMode: "oauth" } }, + { name: "keychain API keys remain on the TypeScript credential path", body: { model: "test-model", input: "plain" }, provider: { apiKey: "keychain:relay-differential" } }, + { name: "custom responses paths remain on the TypeScript adapter path", body: { model: "test-model", input: "plain" }, provider: { responsesPath: "/custom-responses" } }, + { name: "stateless streaming responses keep TypeScript stream repair", body: { model: "test-model", input: "stream", stream: true }, provider: { statelessResponses: true } }, + { name: "reasoning-preserving streams keep TypeScript stream repair", body: { model: "test-model", input: "stream", stream: true }, provider: { preserveReasoningContentModels: ["test-model"] } }, + { + name: "reserved OpenAI family rows remain on the native TypeScript path", + body: { model: "test-model", input: "plain" }, + configure: config => { + const providers = config.providers as Record; + providers.openai = providers.test!; + delete providers.test; + config.defaultProvider = "openai"; + }, + }, + { name: "grok surface remains on the TypeScript surface path", body: { model: "test-model", input: "plain" }, headers: { "x-opencodex-grok": "1" } }, + { name: "compaction markers remain on the TypeScript compaction path", body: { model: "test-model", input: "plain", compaction_trigger: true } }, + { + name: "encrypted input remains on the TypeScript encrypted-payload path", + body: { model: "test-model", input: [{ type: "message", role: "user", encrypted_content: "ciphertext" }] }, + }, + { name: "namespaced model selectors remain on the TypeScript router", body: { model: "test/test-model", input: "plain" } }, + { name: "previous response continuations remain on the TypeScript state path", body: { model: "test-model", input: "plain", previous_response_id: "resp_unknown" } }, + { + name: "namespaced tools remain on the TypeScript tool bridge", + body: { model: "test-model", input: "plain", tools: [{ type: "function", name: "calc", namespace: "mcp" }] }, + }, + ]; + + for (const fixture of cases) { + resetGoSidecarForTests(); + delete process.env[GO_SIDECAR_BIN_ENV]; + delete process.env[HOT_PATH_SEAM_ENV]; + delete process.env[HOT_PATH_RELAY_ENV]; + setUpFixture(port, fixture.provider, fixture.configure); + + const oracleStart = upstreamLogs.length; + const serverA = startServer(0); + let oracle: ResponseCapture; + try { + oracle = await postCase(serverA, token, fixture.body, fixture.headers); + } finally { + await serverA.stop(true); + } + const oracleLogs = upstreamLogs.slice(oracleStart); + expect(oracleLogs.every(log => log.ua !== GO_UA), fixture.name + " oracle uses Bun").toBe(true); + + process.env[GO_SIDECAR_BIN_ENV] = sidecarBinary!; + process.env[HOT_PATH_SEAM_ENV] = "1"; + process.env[HOT_PATH_RELAY_ENV] = "1"; + const relayStart = upstreamLogs.length; + const serverB = startServer(0); + let armed: ResponseCapture; + try { + await waitFor(() => activeGoSidecarBaseUrl(), 15_000); + armed = await postCase(serverB, token, fixture.body, fixture.headers); + } finally { + await serverB.stop(true); + resetGoSidecarForTests(); + } + const armedLogs = upstreamLogs.slice(relayStart); + + expect(armed!.status, fixture.name + " status").toBe(oracle!.status); + expect(armed!.contentType, fixture.name + " content-type").toBe(oracle!.contentType); + expect(armed!.body, fixture.name + " client bytes").toBe(oracle!.body); + // Every fixture above intentionally exercises a refusal predicate. If it + // reaches an upstream, that request must still be sent by Bun through + // the parent bridge rather than directly by the Go relay. Some credential + // and encrypted fixtures fail before an upstream call, which is equally + // valid evidence of bridge ownership. + expect(armedLogs.every(log => log.ua !== GO_UA), fixture.name + " must not be Go-owned").toBe(true); + } + }); }); diff --git a/tests/hot-path-seam.test.ts b/tests/hot-path-seam.test.ts index 42f4909254..cae96ee25b 100644 --- a/tests/hot-path-seam.test.ts +++ b/tests/hot-path-seam.test.ts @@ -41,7 +41,7 @@ function seamRequest(headers: Headers, body = `{"model":"fixture","stream":true} describe("hot-path seam claim and bridge (ticket #24)", () => { test("an admitted claim reaches dispatch with the reconstructed admission and identical body", async () => { - let captured: { admission: DataPlaneAdmission; contentType: string | null; body: Uint8Array } | null = null; + let captured: { admission: DataPlaneAdmission; contentType: string | null; grokSurface: boolean; body: Uint8Array } | null = null; const bridge = makeBridge(c => { captured = c as typeof captured; }); const bodyBytes = new TextEncoder().encode(`{"model":"fixture","stream":true}`); const headers = createDataPlaneSeamHeaders(secret, admission, "POST", HOT_PATH_SEAM_PATH, bodyBytes, clock); @@ -54,9 +54,21 @@ describe("hot-path seam claim and bridge (ticket #24)", () => { expect(captured).not.toBeNull(); expect(captured!.admission).toEqual(admission); expect(captured!.contentType).toBe("application/json"); + expect(captured!.grokSurface).toBe(false); expect(new TextDecoder().decode(captured!.body)).toBe(`{"model":"fixture","stream":true}`); }); + test("the explicit Grok surface marker reaches the bridge dispatch", async () => { + let captured: { grokSurface: boolean } | null = null; + const bridge = makeBridge(c => { captured = c as typeof captured; }); + const body = new Uint8Array(0); + const headers = createDataPlaneSeamHeaders(secret, admission, "POST", HOT_PATH_SEAM_PATH, body, clock)!; + const { request, url } = seamRequest(headers, ""); + request.headers.set("x-opencodex-grok", "1"); + expect((await bridge.handle(request, url)).status).toBe(200); + expect(captured!.grokSurface).toBe(true); + }); + test("a configured-key admission keeps its keyId across the bridge", async () => { let captured: { admission: DataPlaneAdmission } | null = null; const bridge = makeBridge(c => { captured = c as typeof captured; }); From 4e32b8439a3ab9be55f79b72467360396e73d25c Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Mon, 7 Sep 2026 01:34:31 +0800 Subject: [PATCH 074/165] feat(go): add status diagnostic evidence seam --- go/internal/ocxcli/cli_test.go | 56 ++++++++ go/internal/ocxcli/status_diagnostics.go | 165 +++++++++++++++++++++++ 2 files changed, 221 insertions(+) create mode 100644 go/internal/ocxcli/status_diagnostics.go diff --git a/go/internal/ocxcli/cli_test.go b/go/internal/ocxcli/cli_test.go index 2bfa5d0e3f..d1857c9d65 100644 --- a/go/internal/ocxcli/cli_test.go +++ b/go/internal/ocxcli/cli_test.go @@ -333,6 +333,62 @@ func TestCodexShimStatusReportsCorruptStateWithoutFailing(t *testing.T) { } } +func TestStatusEvidenceUsesRuntimeRecordAndPublicHealthzIdentity(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/healthz" { + t.Fatalf("path = %q", r.URL.Path) + } + _ = json.NewEncoder(w).Encode(map[string]any{ + "service": "opencodex", "status": "ok", "version": "2.42.0", "uptime": 1.6, "pid": 4242, + }) + })) + defer server.Close() + state := StatusRuntimeRecord{PID: 4242, Port: serverPort(strings.TrimPrefix(server.URL, "http://")), Hostname: "127.0.0.1"} + probe := ProbeStatusEvidence(StatusProbeDeps{ + LoadConfig: func() (*config.Config, error) { return &config.Config{Port: 9, Hostname: "0.0.0.0"}, nil }, + ReadRuntime: func() (StatusRuntimeRecord, error) { return state, nil }, + }) + if probe.Source != "runtime" || probe.Port != state.Port || probe.Runtime == nil { + t.Fatalf("selection = %#v", probe) + } + if !probe.Health.OK || probe.Health.PID != 4242 || probe.Health.Message != "ok v2.42.0, uptime 2s" { + t.Fatalf("health = %#v", probe.Health) + } +} + +func TestStatusEvidenceFallsBackToConfigAndRejectsForeignHealthz(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _ = json.NewEncoder(w).Encode(map[string]any{"service": "other", "status": "ok"}) + })) + defer server.Close() + port := serverPort(strings.TrimPrefix(server.URL, "http://")) + probe := ProbeStatusEvidence(StatusProbeDeps{ + LoadConfig: func() (*config.Config, error) { return &config.Config{Port: port, Hostname: "0.0.0.0"}, nil }, + ReadRuntime: func() (StatusRuntimeRecord, error) { return StatusRuntimeRecord{}, errors.New("missing") }, + }) + if probe.Source != "config" || probe.Runtime != nil || probe.Health.URL != fmt.Sprintf("http://127.0.0.1:%d/healthz", port) { + t.Fatalf("probe = %#v", probe) + } + if probe.Health.OK || probe.Health.Message != "responded, but not an opencodex proxy" { + t.Fatalf("health = %#v", probe.Health) + } +} + +func TestStatusEvidenceUsesTypeScriptHealthzMessageRules(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _ = json.NewEncoder(w).Encode(map[string]any{"service": "opencodex"}) + })) + defer server.Close() + port := serverPort(strings.TrimPrefix(server.URL, "http://")) + probe := ProbeStatusEvidence(StatusProbeDeps{ + LoadConfig: func() (*config.Config, error) { return &config.Config{Port: port}, nil }, + ReadRuntime: func() (StatusRuntimeRecord, error) { return StatusRuntimeRecord{}, errors.New("missing") }, + }) + if !probe.Health.OK || probe.Health.Message != "ok" { + t.Fatalf("health = %#v", probe.Health) + } +} + func TestDelegatedFamilyHelpUsesOwnerOutput(t *testing.T) { for _, command := range []string{"status", "doctor", "service"} { t.Run(command, func(t *testing.T) { diff --git a/go/internal/ocxcli/status_diagnostics.go b/go/internal/ocxcli/status_diagnostics.go new file mode 100644 index 0000000000..429d3e4803 --- /dev/null +++ b/go/internal/ocxcli/status_diagnostics.go @@ -0,0 +1,165 @@ +package ocxcli + +import ( + "encoding/json" + "fmt" + "io" + "math" + "net/http" + "os" + "path/filepath" + "strconv" + "strings" + "time" + + "github.com/lidge-jun/opencodex/go/internal/config" +) + +// StatusRuntimeRecord is the non-secret portion of runtime-port.json used by +// TypeScript's status liveness probe. Unlike ReadRuntime it deliberately does +// not require the attestation secret: status only asks whether the public +// /healthz endpoint identifies as OpenCodex, while management commands require +// the proof-bound record. +type StatusRuntimeRecord struct { + PID int64 `json:"pid"` + Port int `json:"port"` + Hostname string `json:"hostname"` +} + +// StatusHealth is the public, secret-free healthz projection used by status. +type StatusHealth struct { + OK bool + URL string + Message string + PID int64 + Version string + Uptime float64 +} + +// StatusProbe is the shared, minimal liveness evidence required by the future +// native status and doctor ports. It intentionally stops before service, OAuth, +// runtime-selection, and other TypeScript-owned projections; callers must not +// present this as the complete status JSON schema. +type StatusProbe struct { + Port int + Hostname string + Source string // runtime or config + Runtime *StatusRuntimeRecord + Health StatusHealth +} + +// StatusProbeDeps is a test seam. Production uses config.Load and the supplied +// runtime reader so this file never creates or repairs state while diagnosing. +type StatusProbeDeps struct { + LoadConfig func() (*config.Config, error) + ReadRuntime func() (StatusRuntimeRecord, error) + HTTPClient *http.Client +} + +func defaultStatusProbeDeps(deps StatusProbeDeps) StatusProbeDeps { + if deps.LoadConfig == nil { + deps.LoadConfig = config.Load + } + if deps.ReadRuntime == nil { + deps.ReadRuntime = ReadStatusRuntime + } + if deps.HTTPClient == nil { + deps.HTTPClient = &http.Client{Timeout: 800 * time.Millisecond} + } + return deps +} + +// ReadStatusRuntime reads public runtime metadata accepted by TypeScript's +// readRuntimePort. A missing or malformed record is not an error to status: it +// simply makes configuration the probe target. +func ReadStatusRuntime() (StatusRuntimeRecord, error) { + dir, err := config.Dir() + if err != nil { + return StatusRuntimeRecord{}, err + } + raw, err := os.ReadFile(filepath.Join(dir, "runtime-port.json")) + if err != nil { + return StatusRuntimeRecord{}, err + } + var record StatusRuntimeRecord + if err := json.Unmarshal(raw, &record); err != nil { + return StatusRuntimeRecord{}, err + } + if record.PID <= 0 || record.Port < 1 || record.Port > 65535 { + return StatusRuntimeRecord{}, fmt.Errorf("invalid runtime record") + } + return record, nil +} + +// ProbeStatusEvidence mirrors the TypeScript status selection order: runtime +// metadata first, config second; a runtime record is used even when its owner +// is no longer alive, which makes stale-state detection possible to the caller. +func ProbeStatusEvidence(deps StatusProbeDeps) StatusProbe { + deps = defaultStatusProbeDeps(deps) + probe := StatusProbe{Port: 10100, Source: "config"} + if cfg, err := deps.LoadConfig(); err == nil && cfg != nil { + probe.Port, probe.Hostname = cfg.ListenTarget() + } + if record, err := deps.ReadRuntime(); err == nil { + probe.Port, probe.Hostname, probe.Source = record.Port, record.Hostname, "runtime" + probe.Runtime = &record + } + probe.Health = probeStatusHealth(probe.Port, probe.Hostname, deps.HTTPClient) + return probe +} + +func statusProbeHost(hostname string) string { + host := strings.TrimSpace(hostname) + if host == "" || host == "0.0.0.0" || host == "::" || host == "[::]" { + return "127.0.0.1" + } + if strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]") { + return host + } + if strings.Contains(host, ":") { + return "[" + host + "]" + } + return host +} + +func probeStatusHealth(port int, hostname string, client *http.Client) StatusHealth { + url := "http://" + statusProbeHost(hostname) + ":" + strconv.Itoa(port) + "/healthz" + result := StatusHealth{URL: url, Message: "unreachable"} + response, err := client.Get(url) + if err != nil { + return result + } + defer response.Body.Close() + var body map[string]any + if response.StatusCode != http.StatusOK { + result.Message = fmt.Sprintf("returned HTTP %d", response.StatusCode) + return result + } + if err := json.NewDecoder(io.LimitReader(response.Body, 64*1024)).Decode(&body); err != nil { + result.Message = "responded, but not an opencodex proxy" + return result + } + service, _ := body["service"].(string) + status, _ := body["status"].(string) + version, versionOK := body["version"].(string) + uptime, uptimeOK := body["uptime"].(float64) + pid, _ := body["pid"].(float64) + legacy := service == "" && status == "ok" && versionOK && uptimeOK + if service != "opencodex" && !legacy { + result.Message = "responded, but not an opencodex proxy" + return result + } + result.OK, result.PID = true, int64(pid) + versionText := "" + if versionOK { + result.Version = version + versionText = " v" + version + } + uptimeText := "" + if uptimeOK { + result.Uptime = uptime + uptimeText = fmt.Sprintf(", uptime %ds", int64(math.Floor(uptime+0.5))) + } + result.Message = "ok" + versionText + uptimeText + return result +} From d8a8604c8e97965140974052548be7a90ef6e18f Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Mon, 7 Sep 2026 01:34:48 +0800 Subject: [PATCH 075/165] fix(go): match status health success range --- go/internal/ocxcli/cli_test.go | 16 ++++++++++++++++ go/internal/ocxcli/status_diagnostics.go | 4 +++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/go/internal/ocxcli/cli_test.go b/go/internal/ocxcli/cli_test.go index d1857c9d65..1164e646fa 100644 --- a/go/internal/ocxcli/cli_test.go +++ b/go/internal/ocxcli/cli_test.go @@ -389,6 +389,22 @@ func TestStatusEvidenceUsesTypeScriptHealthzMessageRules(t *testing.T) { } } +func TestStatusEvidenceAcceptsAnySuccessfulHealthzStatus(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusCreated) + _ = json.NewEncoder(w).Encode(map[string]any{"service": "opencodex"}) + })) + defer server.Close() + port := serverPort(strings.TrimPrefix(server.URL, "http://")) + probe := ProbeStatusEvidence(StatusProbeDeps{ + LoadConfig: func() (*config.Config, error) { return &config.Config{Port: port}, nil }, + ReadRuntime: func() (StatusRuntimeRecord, error) { return StatusRuntimeRecord{}, errors.New("missing") }, + }) + if !probe.Health.OK || probe.Health.Message != "ok" { + t.Fatalf("health = %#v", probe.Health) + } +} + func TestDelegatedFamilyHelpUsesOwnerOutput(t *testing.T) { for _, command := range []string{"status", "doctor", "service"} { t.Run(command, func(t *testing.T) { diff --git a/go/internal/ocxcli/status_diagnostics.go b/go/internal/ocxcli/status_diagnostics.go index 429d3e4803..bbe67763b5 100644 --- a/go/internal/ocxcli/status_diagnostics.go +++ b/go/internal/ocxcli/status_diagnostics.go @@ -131,7 +131,9 @@ func probeStatusHealth(port int, hostname string, client *http.Client) StatusHea } defer response.Body.Close() var body map[string]any - if response.StatusCode != http.StatusOK { + // Response.ok in the TypeScript oracle accepts every 2xx response, not + // only 200. Preserve that distinction before validating the JSON identity. + if response.StatusCode < http.StatusOK || response.StatusCode >= http.StatusMultipleChoices { result.Message = fmt.Sprintf("returned HTTP %d", response.StatusCode) return result } From 34845bb23b85f941c981596ffedf576276cce19a Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Mon, 7 Sep 2026 01:47:21 +0800 Subject: [PATCH 076/165] ci: run hot-path differentials on native platforms --- .github/workflows/ci.yml | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bc4fd16edb..1a3dd17b3a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -579,6 +579,14 @@ jobs: cd gui bun run build + # Run the two data-plane differentials explicitly before the full-suite + # control. The unsharded suite also discovers them, but this named step + # makes macOS parity evidence independently visible and stable. + - name: Hot-path differential oracles + run: >- + bun test --timeout 60000 + tests/go-hotpath-relay.test.ts tests/go-hotpath-seam.test.ts + # Bun 1.3.14 segfaults while reclaiming a Worker at an `--isolate` file # boundary: the header shows BALANCED `workers_spawned(N) # workers_terminated(N)` and the process dies with exit 133 after the last @@ -704,6 +712,14 @@ jobs: - name: Setup project Bun uses: ./.github/actions/setup-project-bun + - name: Setup Go + # The explicit Windows hot-path differential below builds and executes + # a native sidecar, so GOOS cross-compilation alone is insufficient. + uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5 + with: + go-version-file: go/go.mod + cache: false + - name: Install dependencies run: | bun install --frozen-lockfile @@ -716,6 +732,14 @@ jobs: cd gui bun run build + # This is deliberately separate from the sharded suite: a shard can move + # with file ordering, while this named result is the Windows evidence for + # byte-identical relay and SSE-seam behaviour against a native sidecar. + - name: Hot-path differential oracles + run: >- + bun test --timeout 60000 + tests/go-hotpath-relay.test.ts tests/go-hotpath-seam.test.ts + - name: Test # --timeout: the Linux batches and the macOS control both pass 60000; this leg was # the only one left on Bun's 5s default, and it is the slowest hardware on the board. From 22671ed3d9f262c8acc2da69478a789ecbfe3a6f Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Mon, 7 Sep 2026 01:51:33 +0800 Subject: [PATCH 077/165] feat(go): stage strict config write schema Keep config set/unset/import delegated to TypeScript: Go does not yet cover the complete TS write schema, so native dispatch would overclaim byte parity. --- go/internal/configschema/mutation.go | 24 ++++ go/internal/configschema/mutation_test.go | 23 ++++ go/internal/configschema/schema.go | 149 ++++++++++++++++++++++ go/internal/configschema/schema_test.go | 35 +++++ 4 files changed, 231 insertions(+) diff --git a/go/internal/configschema/mutation.go b/go/internal/configschema/mutation.go index ec9a8da30c..8ffdd6543e 100644 --- a/go/internal/configschema/mutation.go +++ b/go/internal/configschema/mutation.go @@ -217,6 +217,30 @@ func WithRevalidatedConfigMutation(ctx context.Context, configPath string, expec }) } +// ReplaceConfigCandidate persists an already strict-validated import through +// the shared generation coordinator. Imports intentionally replace the full +// document and do not apply the config-set account-pin hook. +func ReplaceConfigCandidate(ctx context.Context, configPath string, candidate *Normalized) (MutationResult, error) { + data, err := candidate.IndentedJSON() + if err != nil { + return MutationResult{}, err + } + data = append(data, '\n') + return WithMutationCoordinator(ctx, configPath, nil, func(int64) (bool, error) { + previous, readErr := os.ReadFile(configPath) + if readErr != nil && !errors.Is(readErr, os.ErrNotExist) { + return false, readErr + } + if bytes.Equal(previous, data) { + return false, nil + } + if err := writeConfigBytesAtomic(configPath, data); err != nil { + return false, err + } + return true, nil + }) +} + func classifyMutationError(err error) error { message := strings.ToLower(err.Error()) if strings.Contains(message, "database is locked") || strings.Contains(message, "database is busy") || strings.Contains(message, "sqlite_busy") || strings.Contains(message, "sqlite_locked") { diff --git a/go/internal/configschema/mutation_test.go b/go/internal/configschema/mutation_test.go index 19afa7e63e..a449dbb33b 100644 --- a/go/internal/configschema/mutation_test.go +++ b/go/internal/configschema/mutation_test.go @@ -103,6 +103,29 @@ func TestMutationCoordinatorCrashRecoveryReleasesImmediate(t *testing.T) { } } +func TestReplaceConfigCandidateUsesGenerationCoordinator(t *testing.T) { + path := filepath.Join(t.TempDir(), "config.json") + candidate, err := ValidateCandidateJSON([]byte(`{"providers":{"x":{"adapter":"openai-chat","baseUrl":"https://x.test"}},"defaultProvider":"x"}`)) + if err != nil { + t.Fatal(err) + } + result, err := ReplaceConfigCandidate(context.Background(), path, candidate) + if err != nil || !result.Changed || result.Generation != 1 { + t.Fatalf("result=%+v err=%v", result, err) + } + got, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + if !bytes.Contains(got, []byte(`"defaultProvider": "x"`)) || got[len(got)-1] != '\n' { + t.Fatalf("imported config=%q", got) + } + result, err = ReplaceConfigCandidate(context.Background(), path, candidate) + if err != nil || result.Changed || result.Generation != 1 { + t.Fatalf("unchanged result=%+v err=%v", result, err) + } +} + func TestRevalidatedMutationRebasesAfterDirectWriterChangesBytes(t *testing.T) { path := filepath.Join(t.TempDir(), "config.json") if err := os.WriteFile(path, []byte(`{"revision":1}`), 0o600); err != nil { diff --git a/go/internal/configschema/schema.go b/go/internal/configschema/schema.go index cff890ef5d..bf9f52434a 100644 --- a/go/internal/configschema/schema.go +++ b/go/internal/configschema/schema.go @@ -49,12 +49,45 @@ func ValidateCandidateJSON(raw []byte) (*Normalized, error) { if v.kind != objectKind { return nil, errors.New("schema_invalid: Invalid input: expected object, received array") } + if err := validateStrictWriteFields(v); err != nil { + return nil, err + } if err := validateTop(v); err != nil { return nil, err } return &Normalized{root: normalizeLoad(v)}, nil } +// validateStrictWriteFields covers fields which deliberately degrade on the +// read path but must refuse a live config set/import instead of silently +// deleting or replacing operator intent. +func validateStrictWriteFields(v *value) error { + if x := v.find("hostname"); x != nil && (x.kind != stringKind || strings.TrimSpace(x.text) == "") { + return errors.New("schema_invalid: hostname: must be a nonblank bind address") + } + if x := v.find("appOwnedMemoryBudgetMb"); x != nil && !validIntRange(x, 64, maxAppOwnedMemoryBudgetMB) { + return fmt.Errorf("schema_invalid: appOwnedMemoryBudgetMb: must be an integer from 64 to %d", maxAppOwnedMemoryBudgetMB) + } + if x := v.find("upstreamHostCircuitThreshold"); x != nil && !validIntRange(x, 0, 100) { + return errors.New("schema_invalid: upstreamHostCircuitThreshold: must be an integer from 0 to 100") + } + if x := v.find("googleAntigravityStaticCatalogVersion"); x != nil && !(x.kind == numberKind && (x.number.String() == "1" || x.number.String() == "2")) { + return errors.New("schema_invalid: googleAntigravityStaticCatalogVersion: must be 1, 2, or omitted") + } + if x := v.find("activeCodexAccountPinned"); x != nil && (x.kind != stringKind || !regexp.MustCompile(`^[a-zA-Z0-9._-]{1,64}$`).MatchString(x.text)) { + return errors.New("schema_invalid: activeCodexAccountPinned: must be an account id") + } + if x := v.find("codexAccountPickerEnabled"); x != nil && x.kind != boolKind { + return errors.New("schema_invalid: codexAccountPickerEnabled: Invalid input: expected boolean, received " + zodType(x)) + } + if x := v.find("visionSidecar"); x != nil && x.kind == objectKind { + if r := x.find("reasoning"); r != nil && (r.kind != stringKind || !map[string]bool{"none": true, "minimal": true, "low": true, "medium": true, "high": true, "xhigh": true, "max": true, "ultra": true}[r.text]) { + return errors.New("schema_invalid: visionSidecar.reasoning: must be one of none, minimal, low, medium, high, xhigh, max, ultra") + } + } + return nil +} + func (n *Normalized) CompactJSON() ([]byte, error) { if n == nil || n.root == nil { return nil, errors.New("nil normalized config") @@ -106,6 +139,122 @@ func (n *Normalized) ClearCodexAccountPinForSet(path string) bool { return n.root.delete("activeCodexAccountPinned") } +// ApplyConfigPathMutation performs the strict config set/unset write boundary. +// It keeps JSON object order through the private value representation, then +// projects the result through the same schema normalization used by TS writes. +func ApplyConfigPathMutation(raw []byte, path, rawValue string, remove bool) (config *Normalized, saved *Normalized, changed bool, err error) { + base, err := ValidateCandidateJSON(raw) + if err != nil { + return nil, nil, false, err + } + candidate := cloneValue(base.root) + segments, err := configPathSegments(path) + if err != nil { + return nil, nil, false, err + } + current := candidate + for _, segment := range segments[:len(segments)-1] { + next := current.find(segment) + if next == nil || next.kind != objectKind { + return nil, nil, false, fmt.Errorf("config parent path not found: %s", segment) + } + current = next + } + leaf := segments[len(segments)-1] + if remove { + if !current.delete(leaf) { + return nil, nil, false, fmt.Errorf("config path not found: %s", path) + } + } else { + parsed, parseErr := parse([]byte(rawValue)) + if parseErr != nil { + parsed = stringValue(rawValue) + } + current.set(leaf, parsed) + } + compact := candidate.compact() + config, err = ValidateCandidateJSON(compact) + if err != nil { + return nil, nil, false, err + } + if !remove { + config.ClearCodexAccountPinForSet(path) + } + if !remove { + value, found := getConfigPathValue(config.root, segments) + if !found { + return nil, nil, false, fmt.Errorf("config path not found: %s", path) + } + saved = &Normalized{root: cloneValue(value)} + } + before, _ := base.CompactJSON() + after, _ := config.CompactJSON() + return config, saved, !bytes.Equal(before, after), nil +} + +// ConfigPathValue returns a redacted JSON-ready normalized path value. +func (n *Normalized) ConfigPathValue(path string) (*Normalized, error) { + segments, err := configPathSegments(path) + if err != nil { + return nil, err + } + value, ok := getConfigPathValue(n.root, segments) + if !ok { + return nil, fmt.Errorf("config path not found: %s", path) + } + return &Normalized{root: redactValue(cloneValue(value), segments[len(segments)-1])}, nil +} + +func configPathSegments(path string) ([]string, error) { + parts := make([]string, 0) + for _, part := range strings.Split(path, ".") { + part = strings.TrimSpace(part) + if part == "" { + continue + } + if part == "__proto__" || part == "prototype" || part == "constructor" { + return nil, errors.New("invalid config path") + } + parts = append(parts, part) + } + if len(parts) == 0 { + return nil, errors.New("invalid config path") + } + return parts, nil +} +func getConfigPathValue(root *value, segments []string) (*value, bool) { + current := root + for _, segment := range segments { + if current == nil || current.kind != objectKind { + return nil, false + } + current = current.find(segment) + if current == nil { + return nil, false + } + } + return current, true +} +func cloneValue(v *value) *value { + if v == nil { + return nil + } + out := *v + if v.array != nil { + out.array = make([]*value, len(v.array)) + for i := range v.array { + out.array[i] = cloneValue(v.array[i]) + } + } + if v.object != nil { + out.object = make([]member, len(v.object)) + for i := range v.object { + out.object[i] = member{key: v.object[i].key, value: cloneValue(v.object[i].value)} + } + } + return &out +} + type valueKind uint8 const ( diff --git a/go/internal/configschema/schema_test.go b/go/internal/configschema/schema_test.go index 148c330e45..e9d651f5dd 100644 --- a/go/internal/configschema/schema_test.go +++ b/go/internal/configschema/schema_test.go @@ -143,3 +143,38 @@ func TestSetCodexAccountPrioritiesClearsManualPin(t *testing.T) { t.Fatalf("unrelated config set removed pin: %s", otherJSON) } } + +func TestStrictWriteSchemaRejectsLoadDegradedFields(t *testing.T) { + cases := []struct{ raw, want string }{ + {`{"providers":{},"hostname":" "}`, "schema_invalid: hostname: must be a nonblank bind address"}, + {`{"providers":{},"appOwnedMemoryBudgetMb":63}`, "schema_invalid: appOwnedMemoryBudgetMb: must be an integer from 64 to 4096"}, + {`{"providers":{},"googleAntigravityStaticCatalogVersion":3}`, "schema_invalid: googleAntigravityStaticCatalogVersion: must be 1, 2, or omitted"}, + {`{"providers":{},"activeCodexAccountPinned":123}`, "schema_invalid: activeCodexAccountPinned: must be an account id"}, + } + for _, tc := range cases { + _, err := ValidateCandidateJSON([]byte(tc.raw)) + if err == nil || err.Error() != tc.want { + t.Fatalf("ValidateCandidateJSON(%s) = %v, want %q", tc.raw, err, tc.want) + } + } +} + +func TestApplyConfigPathMutationUsesStrictSchemaAndPinHook(t *testing.T) { + raw := []byte(`{"providers":{},"activeCodexAccountPinned":"acct-1","codexAccountPriorities":{"acct-1":1}}`) + updated, saved, changed, err := ApplyConfigPathMutation(raw, "codexAccountPriorities.acct-1", "2", false) + if err != nil || !changed { + t.Fatalf("mutation = %v, changed=%t", err, changed) + } + savedJSON, _ := saved.CompactJSON() + if string(savedJSON) != "2" { + t.Fatalf("saved value = %s", savedJSON) + } + updatedJSON, _ := updated.CompactJSON() + if strings.Contains(string(updatedJSON), "activeCodexAccountPinned") { + t.Fatalf("set retained pin: %s", updatedJSON) + } + _, _, _, err = ApplyConfigPathMutation(raw, "hostname", `" "`, false) + if err == nil || err.Error() != "schema_invalid: hostname: must be a nonblank bind address" { + t.Fatalf("strict mutation error = %v", err) + } +} From 2cf58c85e9c74f5f810f93537b3e6fd04722853f Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Mon, 7 Sep 2026 01:51:33 +0800 Subject: [PATCH 078/165] feat(go): port proxy listen config diagnostics --- go/internal/ocxcli/cli_test.go | 165 ++++++++++++++++++++ go/internal/ocxcli/doctor_diagnostics.go | 35 +++++ go/internal/ocxcli/status_domains.go | 190 +++++++++++++++++++++++ 3 files changed, 390 insertions(+) create mode 100644 go/internal/ocxcli/doctor_diagnostics.go create mode 100644 go/internal/ocxcli/status_domains.go diff --git a/go/internal/ocxcli/cli_test.go b/go/internal/ocxcli/cli_test.go index 1164e646fa..1bd998c7cd 100644 --- a/go/internal/ocxcli/cli_test.go +++ b/go/internal/ocxcli/cli_test.go @@ -9,10 +9,13 @@ import ( "net/http" "net/http/httptest" "os" + "os/exec" "path/filepath" + "runtime" "slices" "strings" "testing" + "time" "github.com/lidge-jun/opencodex/go/internal/config" "github.com/lidge-jun/opencodex/go/internal/managementauth" @@ -20,6 +23,88 @@ import ( const testSecret = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG" +func runTypeScriptStatusJSON(t *testing.T, home string) []byte { + t.Helper() + repo := typeScriptOracleRepo(t) + cmd := exec.Command("bun", "src/cli/index.ts", "status", "--json") + cmd.Dir = repo + cmd.Env = append(os.Environ(), "OPENCODEX_HOME="+home, "CODEX_HOME="+filepath.Join(home, "codex")) + out, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("TypeScript status oracle: %v: %s", err, out) + } + return out +} + +// The focused Go worktree intentionally does not install JavaScript packages. +// Use its own dependency tree when present, otherwise use the primary checkout +// named by the migration task as the immutable TypeScript oracle. +func typeScriptOracleRepo(t *testing.T) string { + t.Helper() + worktree := filepath.Clean(filepath.Join(filepath.Dir(currentTestFile(t)), "..", "..", "..")) + if info, err := os.Stat(filepath.Join(worktree, "node_modules")); err == nil && info.IsDir() { + return worktree + } + t.Skip("TypeScript status/doctor oracle needs node_modules in this checkout") + return "" +} + +func currentTestFile(t *testing.T) string { + t.Helper() + _, file, _, ok := runtime.Caller(0) + if !ok { + t.Fatal("cannot resolve test file") + } + return file +} + +func statusDomainBytes(t *testing.T, full []byte) []byte { + t.Helper() + var compact bytes.Buffer + if err := json.Compact(&compact, full); err != nil { + t.Fatalf("compact status oracle: %v; output=%s", err, full) + } + var status struct { + Proxy json.RawMessage `json:"proxy"` + Listen json.RawMessage `json:"listen"` + Config json.RawMessage `json:"config"` + } + if err := json.Unmarshal(compact.Bytes(), &status); err != nil { + t.Fatalf("decode status oracle: %v; output=%s", err, compact.Bytes()) + } + return []byte(`{"proxy":` + string(status.Proxy) + `,"listen":` + string(status.Listen) + `,"config":` + string(status.Config) + `}`) +} + +func runTypeScriptDoctorProxyHint(t *testing.T, input DoctorProxyDownInput) string { + t.Helper() + repo := typeScriptOracleRepo(t) + script := `import { proxyDownRestartHint } from "./src/cli/doctor"; +const value = proxyDownRestartHint(JSON.parse(process.env.OCX_DOCTOR_INPUT)); +process.stdout.write(JSON.stringify(value));` + encoded, err := json.Marshal(map[string]any{ + "proxyRunning": input.ProxyRunning, "port": input.Port, "serviceViable": input.ServiceViable, + "serviceInstalled": input.ServiceInstalled, "serviceConflict": input.ServiceConflict, "staleProcessState": input.StaleProcessState, + }) + if err != nil { + t.Fatal(err) + } + cmd := exec.Command("bun", "-e", script) + cmd.Dir = repo + cmd.Env = append(os.Environ(), "OCX_DOCTOR_INPUT="+string(encoded)) + out, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("TypeScript doctor oracle: %v: %s", err, out) + } + var hint *string + if err := json.Unmarshal(out, &hint); err != nil { + t.Fatalf("decode doctor oracle: %v; output=%s", err, out) + } + if hint == nil { + return "" + } + return *hint +} + func testServer(t *testing.T, readyStatus string, validProof bool) (*httptest.Server, RuntimeState) { t.Helper() state := RuntimeState{PID: 4242, AttestationSecret: testSecret} @@ -389,6 +474,86 @@ func TestStatusEvidenceUsesTypeScriptHealthzMessageRules(t *testing.T) { } } +func TestStatusDomainsMatchTypeScriptOracleForConfigFallback(t *testing.T) { + // The TypeScript command remains the owner, so its byte representation is + // the contract for the diagnostic domains Go is incrementally porting. + home := t.TempDir() + if err := os.WriteFile(filepath.Join(home, "config.json"), []byte("{\"port\":9,\"hostname\":\"0.0.0.0\"}"), 0o600); err != nil { + t.Fatal(err) + } + t.Setenv("OPENCODEX_HOME", home) + t.Setenv("CODEX_HOME", filepath.Join(home, "codex")) + if err := os.Mkdir(filepath.Join(home, "codex"), 0o700); err != nil { + t.Fatal(err) + } + + oracle := runTypeScriptStatusJSON(t, home) + want := statusDomainBytes(t, oracle) + got, err := json.Marshal(CollectStatusDomains(StatusDomainDeps{ + ReadPID: func() int64 { return 0 }, + ReadRuntime: func() (StatusRuntimeRecord, error) { return StatusRuntimeRecord{}, errors.New("missing") }, + HTTPClient: &http.Client{Timeout: 800 * time.Millisecond}, + })) + if err != nil { + t.Fatal(err) + } + if string(got) != string(want) { + t.Fatalf("domain bytes\\n got: %s\\nwant: %s", got, want) + } +} + +func TestStatusDomainsMatchTypeScriptOracleForDefaultAndMalformedConfig(t *testing.T) { + for _, test := range []struct { + name, content string + }{ + {name: "default"}, + {name: "malformed", content: "{ invalid"}, + {name: "schema-invalid", content: "{\"port\":\"not-a-port\"}"}, + } { + t.Run(test.name, func(t *testing.T) { + home := t.TempDir() + if test.content != "" { + if err := os.WriteFile(filepath.Join(home, "config.json"), []byte(test.content), 0o600); err != nil { + t.Fatal(err) + } + } + if err := os.Mkdir(filepath.Join(home, "codex"), 0o700); err != nil { + t.Fatal(err) + } + t.Setenv("OPENCODEX_HOME", home) + t.Setenv("CODEX_HOME", filepath.Join(home, "codex")) + + want := statusDomainBytes(t, runTypeScriptStatusJSON(t, home)) + got, err := json.Marshal(CollectStatusDomains(StatusDomainDeps{ + ReadPID: func() int64 { return 0 }, + ReadRuntime: func() (StatusRuntimeRecord, error) { return StatusRuntimeRecord{}, errors.New("missing") }, + HTTPClient: &http.Client{Timeout: 800 * time.Millisecond}, + })) + if err != nil { + t.Fatal(err) + } + if string(got) != string(want) { + t.Fatalf("domain bytes\\n got: %s\\nwant: %s", got, want) + } + }) + } +} + +func TestDoctorProxyDownHintMatchesTypeScriptOracle(t *testing.T) { + for _, input := range []DoctorProxyDownInput{ + {ProxyRunning: true, Port: 10100}, + {Port: 10100}, + {Port: 12000, ServiceViable: true}, + {Port: 10100, ServiceInstalled: true}, + {Port: 10100, ServiceInstalled: true, ServiceConflict: true, StaleProcessState: true}, + } { + want := runTypeScriptDoctorProxyHint(t, input) + if got := DoctorProxyDownRestartHint(input); got != want { + t.Fatalf("doctor hint\\n got: %q\\nwant: %q", got, want) + } + } +} + func TestStatusEvidenceAcceptsAnySuccessfulHealthzStatus(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusCreated) diff --git a/go/internal/ocxcli/doctor_diagnostics.go b/go/internal/ocxcli/doctor_diagnostics.go new file mode 100644 index 0000000000..2a8a44282f --- /dev/null +++ b/go/internal/ocxcli/doctor_diagnostics.go @@ -0,0 +1,35 @@ +package ocxcli + +import "strconv" + +// DoctorProxyDownInput is the proxy/listen evidence consumed by doctor when it +// decides whether to print a restart hint. The rest of doctor remains +// TypeScript-owned until its complete report has byte parity. +type DoctorProxyDownInput struct { + ProxyRunning bool + Port int + ServiceViable bool + ServiceInstalled bool + ServiceConflict bool + StaleProcessState bool +} + +// DoctorProxyDownRestartHint is a direct port of proxyDownRestartHint in +// src/cli/doctor.ts. An empty string represents TypeScript's null. +func DoctorProxyDownRestartHint(input DoctorProxyDownInput) string { + if input.ProxyRunning { + return "" + } + installedButBroken := input.ServiceInstalled && !input.ServiceConflict + restart := "Restart it with 'ocx start', or install the persistent service: 'ocx service install'." + if input.ServiceViable { + restart = "Restart it with 'ocx service start' (service installed) or 'ocx start'." + } else if installedButBroken { + restart = "Restart it with 'ocx start', or refresh the installed service: 'ocx service repair'." + } + unclean := "" + if input.StaleProcessState { + unclean = "Stale process records remain, so the previous run may have exited unexpectedly. " + } + return "The ocx proxy is not running. " + unclean + "Codex/Claude clients pinned to 127.0.0.1:" + strconv.Itoa(input.Port) + " fail with errors like \"error sending request for url (http://127.0.0.1:" + strconv.Itoa(input.Port) + "/v1/responses)\". " + restart +} diff --git a/go/internal/ocxcli/status_domains.go b/go/internal/ocxcli/status_domains.go new file mode 100644 index 0000000000..eaeaf43153 --- /dev/null +++ b/go/internal/ocxcli/status_domains.go @@ -0,0 +1,190 @@ +package ocxcli + +import ( + "bytes" + "encoding/json" + "errors" + "net/http" + "os" + "strconv" + "strings" + "time" + + "github.com/lidge-jun/opencodex/go/internal/config" +) + +// StatusConfigDiagnostic is the status-facing portion of TypeScript's +// readConfigDiagnostics result. It remains deliberately small: provider and +// startup projections still belong to the TypeScript command. +type StatusConfigDiagnostic struct { + Config *config.Config + Source string + Error *string +} + +// StatusDomainDeps keeps the first status migration increment independently +// testable. In particular, commands continue to delegate to TypeScript while +// this evidence is compared with its output. +type StatusDomainDeps struct { + ReadConfig func() StatusConfigDiagnostic + ReadPID func() int64 + ReadRuntime func() (StatusRuntimeRecord, error) + HTTPClient *http.Client +} + +// StatusDomains is the ordered JSON projection for status's proxy, listen, +// and config domains. The field order intentionally matches CliStatusJson. +type StatusDomains struct { + Proxy StatusProxyDomain `json:"proxy"` + Listen StatusListenDomain `json:"listen"` + Config StatusConfigDomain `json:"config"` +} + +type StatusProxyDomain struct { + Running bool `json:"running"` + PID *int64 `json:"pid"` + StaleProcessState bool `json:"staleProcessState"` + Health StatusHealthDomain `json:"health"` +} + +type StatusHealthDomain struct { + OK bool `json:"ok"` + URL string `json:"url"` + Message string `json:"message"` +} + +type StatusListenDomain struct { + Port int `json:"port"` + Hostname *string `json:"hostname"` + Source string `json:"source"` +} + +type StatusConfigDomain struct { + Source string `json:"source"` + Error *string `json:"error"` +} + +// ReadStatusConfigDiagnostics follows the non-mutating status path: absence +// is default, malformed JSON is a fallback with the stable invalid_json error, +// and a readable document is a file source. Schema-level fallback remains +// TypeScript-owned until its complete normaliser is ported. +func ReadStatusConfigDiagnostics() StatusConfigDiagnostic { + path, err := config.Path() + if err != nil { + message := "invalid_json" + return StatusConfigDiagnostic{Config: &config.Config{}, Source: "fallback", Error: &message} + } + raw, err := os.ReadFile(path) + if errors.Is(err, os.ErrNotExist) { + return StatusConfigDiagnostic{Config: &config.Config{}, Source: "default"} + } + if err != nil || !json.Valid(bytes.TrimPrefix(raw, []byte{0xef, 0xbb, 0xbf})) { + message := "invalid_json" + return StatusConfigDiagnostic{Config: &config.Config{}, Source: "fallback", Error: &message} + } + cfg, err := config.LoadFromPath(path) + if err != nil || cfg == nil { + message := "invalid_json" + return StatusConfigDiagnostic{Config: &config.Config{}, Source: "fallback", Error: &message} + } + // These listener fields are the config portion this diagnostic increment + // owns. Keep TypeScript's schema wording, including the required-provider + // companion emitted when a direct schema parse cannot use defaults. + if rawPort, present := cfg.Raw["port"]; present { + if _, valid := rawPort.(json.Number); !valid { + message := "schema_invalid: port: Invalid input: expected number, received " + jsonTypeName(rawPort) + if _, providersPresent := cfg.Raw["providers"]; !providersPresent { + message += "; providers: Invalid input: expected record, received undefined" + } + return StatusConfigDiagnostic{Config: &config.Config{}, Source: "fallback", Error: &message} + } + } + return StatusConfigDiagnostic{Config: cfg, Source: "file"} +} + +func jsonTypeName(value any) string { + switch value.(type) { + case string: + return "string" + case bool: + return "boolean" + case nil: + return "null" + case []any: + return "array" + case map[string]any: + return "object" + default: + return "undefined" + } +} + +func defaultStatusDomainDeps(deps StatusDomainDeps) StatusDomainDeps { + if deps.ReadConfig == nil { + deps.ReadConfig = ReadStatusConfigDiagnostics + } + if deps.ReadPID == nil { + deps.ReadPID = readStatusPIDFile + } + if deps.ReadRuntime == nil { + deps.ReadRuntime = ReadStatusRuntime + } + return deps +} + +func readStatusPIDFile() int64 { + dir, err := config.Dir() + if err != nil { + return 0 + } + raw, err := os.ReadFile(dir + string(os.PathSeparator) + "ocx.pid") + if err != nil { + return 0 + } + pid, err := strconv.ParseInt(strings.TrimSpace(string(raw)), 10, 64) + if err != nil || pid <= 0 { + return 0 + } + return pid +} + +// CollectStatusDomains mirrors status's no-live-proxy branch. It is a +// deliberately bounded migration seam: a complete status ownership transfer +// requires every remaining TypeScript status domain to be byte-identical. +func CollectStatusDomains(deps StatusDomainDeps) StatusDomains { + deps = defaultStatusDomainDeps(deps) + diagnostic := deps.ReadConfig() + cfg := diagnostic.Config + if cfg == nil { + cfg = &config.Config{} + } + pid := deps.ReadPID() + port, hostname := cfg.ListenTarget() + source := "config" + if runtime, err := deps.ReadRuntime(); err == nil && pid > 0 && runtime.PID == pid { + port, hostname, source = runtime.Port, runtime.Hostname, "runtime" + } + client := &http.Client{Timeout: 800 * time.Millisecond} + if deps.HTTPClient != nil { + client = deps.HTTPClient + } + health := probeStatusHealth(port, hostname, client) + var pidValue *int64 + if pid > 0 { + pidValue = &pid + } + var hostnameValue *string + if hostname != "" { + hostnameCopy := hostname + hostnameValue = &hostnameCopy + } + return StatusDomains{ + Proxy: StatusProxyDomain{ + Running: pid > 0 && health.OK, + PID: pidValue, + Health: StatusHealthDomain{OK: health.OK, URL: health.URL, Message: health.Message}, + }, + Listen: StatusListenDomain{Port: port, Hostname: hostnameValue, Source: source}, + Config: StatusConfigDomain{Source: diagnostic.Source, Error: diagnostic.Error}, + } +} From 64c434dffdffedc48ae3356f716d810ef4242d77 Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Mon, 7 Sep 2026 02:00:43 +0800 Subject: [PATCH 079/165] feat(go): cover strict config write schema --- go/internal/configschema/schema.go | 516 ++++++++++++++++++++++++ go/internal/configschema/schema_test.go | 130 ++++++ 2 files changed, 646 insertions(+) diff --git a/go/internal/configschema/schema.go b/go/internal/configschema/schema.go index bf9f52434a..a6baf1509f 100644 --- a/go/internal/configschema/schema.go +++ b/go/internal/configschema/schema.go @@ -10,9 +10,13 @@ import ( "fmt" "io" "math" + "net/url" + "os" + "path/filepath" "regexp" "strconv" "strings" + "time" ) const ( @@ -55,6 +59,7 @@ func ValidateCandidateJSON(raw []byte) (*Normalized, error) { if err := validateTop(v); err != nil { return nil, err } + normalizeStrictWriteOutput(v) return &Normalized{root: normalizeLoad(v)}, nil } @@ -85,9 +90,520 @@ func validateStrictWriteFields(v *value) error { return errors.New("schema_invalid: visionSidecar.reasoning: must be one of none, minimal, low, medium, high, xhigh, max, ultra") } } + if err := validateAgentTaskRecovery(v); err != nil { + return err + } + if err := validateCodexAccountMaps(v); err != nil { + return err + } + if err := validateRuntimeAndRemote(v); err != nil { + return err + } + if err := validateLoopbackAndIngress(v); err != nil { + return err + } + return nil +} + +func validateAgentTaskRecovery(root *value) error { + x := root.find("agentTaskRecovery") + if x == nil { + return nil + } + if x.kind != objectKind { + return errors.New("schema_invalid: agentTaskRecovery: Invalid input: expected object, received " + zodType(x)) + } + for _, m := range x.object { + if m.key != "enabled" && m.key != "model" && m.key != "timeoutMs" && m.key != "cacheEntries" { + return fmt.Errorf("schema_invalid: agentTaskRecovery: Unrecognized key: %q", m.key) + } + } + if y := x.find("enabled"); y != nil && y.kind != boolKind { + return errors.New("schema_invalid: agentTaskRecovery.enabled: Invalid input: expected boolean, received " + zodType(y)) + } + if y := x.find("model"); y != nil { + if y.kind != stringKind { + return errors.New("schema_invalid: agentTaskRecovery.model: Invalid input: expected string, received " + zodType(y)) + } + if strings.TrimSpace(y.text) == "" { + return errors.New("schema_invalid: agentTaskRecovery.model: Too small: expected string to have >=1 characters") + } + } + if y := x.find("timeoutMs"); y != nil { + if y.kind != numberKind { + return errors.New("schema_invalid: agentTaskRecovery.timeoutMs: Invalid input: expected number, received " + zodType(y)) + } + if !validInteger(y) { + return errors.New("schema_invalid: agentTaskRecovery.timeoutMs: Invalid input: expected int, received number") + } + if !validIntRange(y, 1000, 120000) { + if integerBelow(y, 1000) { + return errors.New("schema_invalid: agentTaskRecovery.timeoutMs: Too small: expected number to be >=1000") + } + return errors.New("schema_invalid: agentTaskRecovery.timeoutMs: Too big: expected number to be <=120000") + } + } + if y := x.find("cacheEntries"); y != nil { + if y.kind != numberKind { + return errors.New("schema_invalid: agentTaskRecovery.cacheEntries: Invalid input: expected number, received " + zodType(y)) + } + if !validInteger(y) { + return errors.New("schema_invalid: agentTaskRecovery.cacheEntries: Invalid input: expected int, received number") + } + if !validIntRange(y, 1, 512) { + if integerBelow(y, 1) { + return errors.New("schema_invalid: agentTaskRecovery.cacheEntries: Too small: expected number to be >=1") + } + return errors.New("schema_invalid: agentTaskRecovery.cacheEntries: Too big: expected number to be <=512") + } + } + return nil +} + +func validateCodexAccountMaps(root *value) error { + if x := root.find("codexAccountPriorities"); x != nil { + if x.kind != objectKind { + return errors.New("schema_invalid: codexAccountPriorities.config: codexAccountPriorities must be a plain object mapping Codex account ids to selection-order integers") + } + for _, m := range x.object { + if !validPriorityKey(m.key) { + return fmt.Errorf("schema_invalid: codexAccountPriorities.%s: selection-order keys must be a Codex pool-account id or the main Codex account and cannot be reserved JavaScript object keys", m.key) + } + if !validIntRange(m.value, -100, 100) { + return fmt.Errorf("schema_invalid: codexAccountPriorities.%s: selection order must be an integer between -100 and 100", m.key) + } + } + } + if x := root.find("activeCodexAccountPinned"); x != nil && (x.kind != stringKind || !regexp.MustCompile(`^[a-zA-Z0-9._-]{1,64}$`).MatchString(x.text)) { + return errors.New("schema_invalid: activeCodexAccountPinned: must be an account id") + } + if x := root.find("codexAccountNamespaces"); x != nil { + if x.kind != objectKind { + return errors.New("schema_invalid: codexAccountNamespaces: codexAccountNamespaces must be a plain object mapping account selectors to Codex account ids") + } + providers := root.find("providers") + configuredAccountIDs := configuredPoolAccountIDs(root.find("codexAccounts")) + for _, m := range x.object { + if !validProviderName(m.key) { + return fmt.Errorf("schema_invalid: codexAccountNamespaces.%s: account selectors must use 1-64 letters, numbers, dots, underscores, or hyphens and cannot be reserved JavaScript object keys", m.key) + } + if m.value.kind != stringKind || (m.value.text != "@main" && !validAccountID(m.value.text)) { + return fmt.Errorf("schema_invalid: codexAccountNamespaces.%s: account selector targets must be @main or valid Codex pool-account ids", m.key) + } + if strings.EqualFold(m.key, "combo") || strings.EqualFold(m.key, "openai") || strings.EqualFold(m.key, "policy") || (providers != nil && providers.kind == objectKind && hasFold(providers, m.key)) { + return fmt.Errorf("schema_invalid: codexAccountNamespaces.%s: account selectors must not collide with configured provider, combo, or routing policy namespaces", m.key) + } + if m.value.text != "@main" && (configuredAccountIDs[m.key] || hasKey(x, m.value.text)) { + return fmt.Errorf("schema_invalid: codexAccountNamespaces.%s: account selectors must not collide with configured Codex pool-account ids or account selector targets", m.key) + } + } + } + return nil +} + +func validateRuntimeAndRemote(root *value) error { + role := root.find("runtimeRole") + if role != nil && (role.kind != stringKind || (role.text != "standalone" && role.text != "hub" && role.text != "client")) { + return errors.New("schema_invalid: runtimeRole: must be one of \"standalone\", \"hub\", or \"client\"") + } + if err := validateHub(root.find("hub")); err != nil { + return err + } + if err := validateRemoteGUI(root.find("remoteGui")); err != nil { + return err + } + if err := validateClient(root.find("client")); err != nil { + return err + } + hasClient := root.find("client") != nil + if role != nil && role.kind == stringKind && role.text == "client" && !hasClient { + return errors.New("schema_invalid: runtimeRole client requires a complete client connection") + } + if hasClient && (role == nil || role.kind != stringKind || role.text != "client") { + return errors.New("schema_invalid: client connection requires runtimeRole client") + } return nil } +func validateHub(x *value) error { + if x == nil { + return nil + } + if x.kind != objectKind { + return errors.New("schema_invalid: hub: Invalid input: expected object, received " + zodType(x)) + } + for _, m := range x.object { + if m.key != "managementPublicOrigin" && m.key != "managementIngress" { + return fmt.Errorf("schema_invalid: hub: Unrecognized key: %q", m.key) + } + } + if y := x.find("managementPublicOrigin"); y != nil && (y.kind != stringKind || !canonicalHTTPOrigin(y.text)) { + return errors.New("schema_invalid: hub.managementPublicOrigin: must be a canonical http(s) origin without credentials, path, query, or fragment") + } + return nil +} + +func validateRemoteGUI(x *value) error { + if x == nil { + return nil + } + if x.kind != objectKind { + return errors.New("schema_invalid: remoteGui: Invalid input: expected object, received " + zodType(x)) + } + for _, m := range x.object { + if m.key != "allowedTailscaleUsers" && m.key != "allowInsecureHttp" { + return fmt.Errorf("schema_invalid: remoteGui: Unrecognized key: %q", m.key) + } + } + if y := x.find("allowInsecureHttp"); y != nil && y.kind != boolKind { + return errors.New("schema_invalid: remoteGui.allowInsecureHttp: Invalid input: expected boolean, received " + zodType(y)) + } + users := x.find("allowedTailscaleUsers") + if users == nil { + return nil + } + if users.kind != arrayKind { + return errors.New("schema_invalid: remoteGui.allowedTailscaleUsers: Invalid input: expected array, received " + zodType(users)) + } + if len(users.array) > 64 { + return errors.New("schema_invalid: remoteGui.allowedTailscaleUsers: Too big: expected array to have <=64 items") + } + seen := map[string]bool{} + for i, user := range users.array { + if user.kind != stringKind { + return fmt.Errorf("schema_invalid: remoteGui.allowedTailscaleUsers.%d: Invalid input: expected string, received %s", i, zodType(user)) + } + trimmed := strings.TrimSpace(user.text) + if trimmed == "" { + return fmt.Errorf("schema_invalid: remoteGui.allowedTailscaleUsers.%d: Too small: expected string to have >=1 characters", i) + } + if len([]byte(trimmed)) > 320 { + return fmt.Errorf("schema_invalid: remoteGui.allowedTailscaleUsers.%d: must be at most 320 UTF-8 bytes", i) + } + if strings.IndexFunc(trimmed, func(r rune) bool { return r < 32 || r == 127 }) >= 0 { + return fmt.Errorf("schema_invalid: remoteGui.allowedTailscaleUsers.%d: must not contain ASCII control characters", i) + } + if seen[trimmed] { + return fmt.Errorf("schema_invalid: remoteGui.allowedTailscaleUsers.%d: must contain unique users after trimming", i) + } + seen[trimmed] = true + } + return nil +} + +func validateClient(x *value) error { + if x == nil { + return nil + } + if x.kind != objectKind { + return errors.New("schema_invalid: client: Invalid input: expected object, received " + zodType(x)) + } + allowed := map[string]bool{"serverUrl": true, "managementUrl": true, "managementTransport": true, "selectedClients": true, "tokenEnv": true, "apiKeyId": true, "tokenFingerprint": true, "protocolVersion": true, "connectedAt": true, "catalogFingerprint": true, "priorCatalog": true, "catalogSyncedAt": true, "pendingOperation": true} + for _, m := range x.object { + if !allowed[m.key] { + return fmt.Errorf("schema_invalid: client: Unrecognized key: %q", m.key) + } + } + for _, field := range []string{"serverUrl", "managementUrl", "managementTransport", "selectedClients", "tokenEnv", "apiKeyId", "tokenFingerprint", "protocolVersion", "connectedAt"} { + if x.find(field) == nil { + return fmt.Errorf("schema_invalid: client.%s: Invalid input: expected %s, received undefined", field, clientExpectedType(field)) + } + } + for _, field := range []string{"serverUrl", "managementUrl"} { + y := x.find(field) + if y.kind != stringKind || !canonicalHTTPOrigin(y.text) { + return fmt.Errorf("schema_invalid: client.%s: must be a canonical http(s) origin without credentials, path, query, or fragment", field) + } + } + if y := x.find("managementTransport"); y.kind != stringKind || (y.text != "direct" && y.text != "relay") { + return errors.New("schema_invalid: client.managementTransport: Invalid option: expected one of \"direct\"|\"relay\"") + } + y := x.find("selectedClients") + if y.kind != arrayKind || len(y.array) < 1 || len(y.array) > 2 { + return errors.New("schema_invalid: client.selectedClients: Invalid input") + } + selected := map[string]bool{} + for _, c := range y.array { + if c.kind != stringKind || (c.text != "codex" && c.text != "claude") { + return errors.New("schema_invalid: client.selectedClients: Invalid option") + } + if selected[c.text] { + return errors.New("schema_invalid: client.selectedClients: must contain unique client ids") + } + selected[c.text] = true + } + if y := x.find("tokenEnv"); y.kind != stringKind || y.text != "OPENCODEX_API_AUTH_TOKEN" { + return errors.New("schema_invalid: client.tokenEnv: Invalid input: expected \"OPENCODEX_API_AUTH_TOKEN\"") + } + if y := x.find("apiKeyId"); y.kind != stringKind || strings.TrimSpace(y.text) == "" || len(y.text) > 256 { + return errors.New("schema_invalid: client.apiKeyId: Invalid input") + } + if y := x.find("tokenFingerprint"); y.kind != stringKind || !regexp.MustCompile(`^[a-f0-9]{64}$`).MatchString(y.text) { + return errors.New("schema_invalid: client.tokenFingerprint: Invalid string: must match pattern /^[a-f0-9]{64}$/") + } + if y := x.find("protocolVersion"); !validIntRange(y, 1, 1) { + return errors.New("schema_invalid: client.protocolVersion: Invalid input: expected 1") + } + if y := x.find("connectedAt"); y.kind != stringKind || !validTimestamp(y.text) { + return errors.New("schema_invalid: client.connectedAt: Invalid ISO datetime") + } + if y := x.find("catalogFingerprint"); y != nil && (y.kind != stringKind || len(y.text) < 1 || len(y.text) > 512) { + return errors.New("schema_invalid: client.catalogFingerprint: Invalid input") + } + if y := x.find("priorCatalog"); y != nil && (y.kind != stringKind || len(y.text) > 64*1024*1024) { + return errors.New("schema_invalid: client.priorCatalog: Invalid input") + } + if y := x.find("catalogSyncedAt"); y != nil && (y.kind != stringKind || !validTimestamp(y.text)) { + return errors.New("schema_invalid: client.catalogSyncedAt: Invalid ISO datetime") + } + if y := x.find("pendingOperation"); y != nil { + if err := validatePendingOperation(y); err != nil { + return err + } + } + return nil +} + +func validatePendingOperation(x *value) error { + if x.kind != objectKind { + return errors.New("schema_invalid: client.pendingOperation: Invalid input: expected object, received " + zodType(x)) + } + for _, m := range x.object { + if m.key != "kind" && m.key != "rotationId" && m.key != "newKeyIssuedAt" && m.key != "oldKeyBackupPath" { + return fmt.Errorf("schema_invalid: client.pendingOperation: Unrecognized key: %q", m.key) + } + } + if y := x.find("kind"); y == nil || y.kind != stringKind || y.text != "rotate" { + return errors.New("schema_invalid: client.pendingOperation.kind: Invalid input: expected \"rotate\"") + } + if y := x.find("rotationId"); y == nil || y.kind != stringKind || strings.TrimSpace(y.text) == "" || len(y.text) > 256 { + return errors.New("schema_invalid: client.pendingOperation.rotationId: Invalid input") + } + if y := x.find("newKeyIssuedAt"); y == nil || y.kind != stringKind || !validTimestamp(y.text) { + return errors.New("schema_invalid: client.pendingOperation.newKeyIssuedAt: Invalid ISO datetime") + } + if y := x.find("oldKeyBackupPath"); y == nil || y.kind != stringKind || y.text == "" { + return errors.New("schema_invalid: client.pendingOperation.oldKeyBackupPath: Invalid input") + } else if y.text != filepath.Join(configDir(), "service-api-token.prev") { + return fmt.Errorf("schema_invalid: client.pendingOperation.oldKeyBackupPath: must equal %s", filepath.Join(configDir(), "service-api-token.prev")) + } + return nil +} + +func validateLoopbackAndIngress(root *value) error { + if x := root.find("unauthenticatedLoopbackListener"); x != nil { + if x.kind != objectKind { + return errors.New("schema_invalid: unauthenticatedLoopbackListener: must be an object or omitted") + } + enabled := x.find("enabled") + if enabled == nil || enabled.kind != boolKind { + return errors.New("schema_invalid: unauthenticatedLoopbackListener.enabled: must be a boolean") + } + if !enabled.b { + for _, m := range x.object { + if m.key != "enabled" { + return fmt.Errorf("schema_invalid: unauthenticatedLoopbackListener: Unrecognized key: %q", m.key) + } + } + } else { + for _, m := range x.object { + if m.key != "enabled" && m.key != "port" { + return fmt.Errorf("schema_invalid: unauthenticatedLoopbackListener: Unrecognized key: %q", m.key) + } + } + if err := validPortObject(x, "unauthenticatedLoopbackListener"); err != nil { + return err + } + if p := root.find("port"); p != nil && validIntRange(p, 0, 65535) && x.find("port").number.String() == p.number.String() { + return errors.New("schema_invalid: unauthenticatedLoopbackListener.port: must differ from the proxy port") + } + } + } + hub := root.find("hub") + if hub == nil || hub.kind != objectKind { + return nil + } + ingress := hub.find("managementIngress") + if ingress == nil { + return nil + } + if ingress.kind != objectKind { + return errors.New("schema_invalid: hub.managementIngress: must be an object or omitted") + } + enabled := ingress.find("enabled") + if enabled == nil || enabled.kind != boolKind { + return errors.New("schema_invalid: hub.managementIngress.enabled: must be a boolean") + } + if !enabled.b { + if len(ingress.object) != 1 { + return errors.New("schema_invalid: hub.managementIngress: disabled ingress accepts only enabled") + } + return nil + } + for _, m := range ingress.object { + if m.key != "enabled" && m.key != "port" { + return errors.New("schema_invalid: hub.managementIngress: contains an unsupported field") + } + } + if err := validPortObject(ingress, "hub.managementIngress"); err != nil { + return err + } + role := root.find("runtimeRole") + if role == nil || role.kind != stringKind || role.text != "hub" { + return errors.New("schema_invalid: hub.managementIngress: enabled ingress requires runtimeRole hub") + } + ingressPort := ingress.find("port").number.String() + proxy := "10100" + if p := root.find("port"); p != nil && p.kind == numberKind { + proxy = p.number.String() + } + if ingressPort == proxy { + return errors.New("schema_invalid: hub.managementIngress.port: must differ from the proxy port") + } + if loop := root.find("unauthenticatedLoopbackListener"); loop != nil && loop.kind == objectKind { + if enabled := loop.find("enabled"); enabled != nil && enabled.kind == boolKind && enabled.b { + if p := loop.find("port"); p != nil && p.kind == numberKind && p.number.String() == ingressPort { + return errors.New("schema_invalid: hub.managementIngress.port: must differ from unauthenticatedLoopbackListener.port") + } + } + } + return nil +} + +func validPortObject(x *value, path string) error { + p := x.find("port") + if !validIntRange(p, 1, 65535) { + return fmt.Errorf("schema_invalid: %s.port: must be an integer port when enabled", path) + } + return nil +} +func validInteger(v *value) bool { + _, err := strconv.ParseInt(v.number.String(), 10, 64) + return v != nil && v.kind == numberKind && err == nil +} +func integerBelow(v *value, n int64) bool { + x, err := strconv.ParseInt(v.number.String(), 10, 64) + return err == nil && x < n +} +func validAccountID(s string) bool { + return regexp.MustCompile(`^[A-Za-z0-9._-]{1,64}$`).MatchString(s) && !reservedKey(s) && s != "__main__" +} +func validPriorityKey(s string) bool { return s == "__main__" || validAccountID(s) } +func validProviderName(s string) bool { + return regexp.MustCompile(`^[A-Za-z0-9](?:[A-Za-z0-9._-]{0,62}[A-Za-z0-9])?$`).MatchString(s) && !reservedKey(s) && !strings.EqualFold(s, "policy") +} +func reservedKey(s string) bool { + return strings.EqualFold(s, "__proto__") || strings.EqualFold(s, "prototype") || strings.EqualFold(s, "constructor") +} +func hasFold(v *value, key string) bool { + for _, m := range v.object { + if strings.EqualFold(m.key, key) { + return true + } + } + return false +} +func hasKey(v *value, key string) bool { return v.find(key) != nil } +func configuredPoolAccountIDs(v *value) map[string]bool { + ids := map[string]bool{} + if v == nil || v.kind != arrayKind { + return ids + } + for _, account := range v.array { + if account == nil || account.kind != objectKind { + continue + } + id, isMain := account.find("id"), account.find("isMain") + if id != nil && id.kind == stringKind && (isMain == nil || isMain.kind != boolKind || !isMain.b) { + ids[id.text] = true + } + } + return ids +} +func canonicalHTTPOrigin(s string) bool { + return canonicalOrigin(s) != "" +} + +func canonicalOrigin(s string) string { + u, err := url.Parse(s) + if err != nil || (u.Scheme != "http" && u.Scheme != "https") || u.Host == "" || u.User != nil || (u.Path != "" && u.Path != "/") || u.RawQuery != "" || u.Fragment != "" { + return "" + } + host := strings.ToLower(u.Hostname()) + if host == "" { + return "" + } + port := u.Port() + if (u.Scheme == "http" && port == "80") || (u.Scheme == "https" && port == "443") { + port = "" + } + if strings.Contains(host, ":") { + host = "[" + host + "]" + } + if port != "" { + host += ":" + port + } + return strings.ToLower(u.Scheme) + "://" + host +} + +func normalizeStrictWriteOutput(root *value) { + if recovery := root.find("agentTaskRecovery"); recovery != nil && recovery.kind == objectKind { + if model := recovery.find("model"); model != nil && model.kind == stringKind { + model.text = strings.TrimSpace(model.text) + } + } + if hub := root.find("hub"); hub != nil && hub.kind == objectKind { + if origin := hub.find("managementPublicOrigin"); origin != nil && origin.kind == stringKind { + origin.text = canonicalOrigin(origin.text) + } + } + if remote := root.find("remoteGui"); remote != nil && remote.kind == objectKind { + if users := remote.find("allowedTailscaleUsers"); users != nil && users.kind == arrayKind { + for _, user := range users.array { + if user.kind == stringKind { + user.text = strings.TrimSpace(user.text) + } + } + } + } + if client := root.find("client"); client != nil && client.kind == objectKind { + for _, field := range []string{"serverUrl", "managementUrl"} { + if origin := client.find(field); origin != nil && origin.kind == stringKind { + origin.text = canonicalOrigin(origin.text) + } + } + if apiKeyID := client.find("apiKeyId"); apiKeyID != nil && apiKeyID.kind == stringKind { + apiKeyID.text = strings.TrimSpace(apiKeyID.text) + } + if operation := client.find("pendingOperation"); operation != nil && operation.kind == objectKind { + if rotationID := operation.find("rotationId"); rotationID != nil && rotationID.kind == stringKind { + rotationID.text = strings.TrimSpace(rotationID.text) + } + } + } +} +func validTimestamp(s string) bool { _, err := time.Parse(time.RFC3339, s); return err == nil } +func clientExpectedType(field string) string { + if field == "selectedClients" { + return "array" + } + if field == "protocolVersion" { + return "number" + } + return "string" +} + +func configDir() string { + if home := strings.TrimSpace(os.Getenv("OPENCODEX_HOME")); home != "" { + return home + } + home, err := os.UserHomeDir() + if err != nil { + return ".opencodex" + } + return filepath.Join(home, ".opencodex") +} + func (n *Normalized) CompactJSON() ([]byte, error) { if n == nil || n.root == nil { return nil, errors.New("nil normalized config") diff --git a/go/internal/configschema/schema_test.go b/go/internal/configschema/schema_test.go index e9d651f5dd..55780e9dff 100644 --- a/go/internal/configschema/schema_test.go +++ b/go/internal/configschema/schema_test.go @@ -178,3 +178,133 @@ func TestApplyConfigPathMutationUsesStrictSchemaAndPinHook(t *testing.T) { t.Fatalf("strict mutation error = %v", err) } } + +// These examples were taken from direct calls to TypeScript's +// validateConfigCandidate. Keep the write boundary strict even where the +// config loader deliberately degrades malformed optional fields. +func TestStrictWriteSchemaRuntimeAndRemoteClientBoundaries(t *testing.T) { + validClient := `{"serverUrl":"https://hub.example.test","managementUrl":"https://manage.example.test","managementTransport":"direct","selectedClients":["codex","claude"],"tokenEnv":"OPENCODEX_API_AUTH_TOKEN","apiKeyId":"issued-key-id","tokenFingerprint":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","protocolVersion":1,"connectedAt":"2026-08-28T00:00:00.000Z"}` + cases := []struct{ name, raw, want string }{ + {"bad role", `{"providers":{},"runtimeRole":"server"}`, "schema_invalid: runtimeRole: must be one of \"standalone\", \"hub\", or \"client\""}, + {"client role needs connection", `{"providers":{},"runtimeRole":"client"}`, "schema_invalid: runtimeRole client requires a complete client connection"}, + {"connection needs client role", `{"providers":{},"client":` + validClient + `}`, "schema_invalid: client connection requires runtimeRole client"}, + {"duplicate selected client", `{"providers":{},"runtimeRole":"client","client":{"serverUrl":"https://hub.example.test","managementUrl":"https://manage.example.test","managementTransport":"direct","selectedClients":["codex","codex"],"tokenEnv":"OPENCODEX_API_AUTH_TOKEN","apiKeyId":"issued-key-id","tokenFingerprint":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","protocolVersion":1,"connectedAt":"2026-08-28T00:00:00.000Z"}}`, "schema_invalid: client.selectedClients: must contain unique client ids"}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + _, err := ValidateCandidateJSON([]byte(tc.raw)) + if err == nil || err.Error() != tc.want { + t.Fatalf("error = %v, want %q", err, tc.want) + } + }) + } + if _, err := ValidateCandidateJSON([]byte(`{"providers":{},"runtimeRole":"client","client":` + validClient + `}`)); err != nil { + t.Fatalf("valid client rejected: %v", err) + } +} + +func TestStrictWriteSchemaHubAndRemoteGUIBoundaries(t *testing.T) { + cases := []struct{ name, raw, want string }{ + {"unsafe hub origin", `{"providers":{},"hub":{"managementPublicOrigin":"https://user@hub.example.test"}}`, "schema_invalid: hub.managementPublicOrigin: must be a canonical http(s) origin without credentials, path, query, or fragment"}, + {"duplicate tailscale users", `{"providers":{},"remoteGui":{"allowedTailscaleUsers":[" alice@example.test ","alice@example.test"]}}`, "schema_invalid: remoteGui.allowedTailscaleUsers.1: must contain unique users after trimming"}, + {"unknown hub property", `{"providers":{},"hub":{"unexpected":true}}`, "schema_invalid: hub: Unrecognized key: \"unexpected\""}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + _, err := ValidateCandidateJSON([]byte(tc.raw)) + if err == nil || err.Error() != tc.want { + t.Fatalf("error = %v, want %q", err, tc.want) + } + }) + } + if _, err := ValidateCandidateJSON([]byte(`{"providers":{},"hub":{"managementPublicOrigin":"https://hub.example.test:443"},"remoteGui":{"allowedTailscaleUsers":[" alice@example.test "]}}`)); err != nil { + t.Fatalf("valid hub/remote GUI rejected: %v", err) + } +} + +func TestStrictWriteSchemaCodexAccountMaps(t *testing.T) { + cases := []struct{ name, raw, want string }{ + {"priority record", `{"providers":{},"codexAccountPriorities":[]}`, "schema_invalid: codexAccountPriorities.config: codexAccountPriorities must be a plain object mapping Codex account ids to selection-order integers"}, + {"priority key", `{"providers":{},"codexAccountPriorities":{"bad id!":1}}`, "schema_invalid: codexAccountPriorities.bad id!: selection-order keys must be a Codex pool-account id or the main Codex account and cannot be reserved JavaScript object keys"}, + {"priority value", `{"providers":{},"codexAccountPriorities":{"work":101}}`, "schema_invalid: codexAccountPriorities.work: selection order must be an integer between -100 and 100"}, + {"namespace record", `{"providers":{},"codexAccountNamespaces":[]}`, "schema_invalid: codexAccountNamespaces: codexAccountNamespaces must be a plain object mapping account selectors to Codex account ids"}, + {"namespace target", `{"providers":{},"codexAccountNamespaces":{"work":"?"}}`, "schema_invalid: codexAccountNamespaces.work: account selector targets must be @main or valid Codex pool-account ids"}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + _, err := ValidateCandidateJSON([]byte(tc.raw)) + if err == nil || err.Error() != tc.want { + t.Fatalf("error = %v, want %q", err, tc.want) + } + }) + } + if _, err := ValidateCandidateJSON([]byte(`{"providers":{},"codexAccountPriorities":{"__main__":-100,"work":100},"codexAccountNamespaces":{"work":"side-acct","main":"@main"}}`)); err != nil { + t.Fatalf("valid account maps rejected: %v", err) + } +} + +func TestStrictWriteSchemaIngressAndRecoveryBoundaries(t *testing.T) { + cases := []struct{ name, raw, want string }{ + {"recovery strict property", `{"providers":{},"agentTaskRecovery":{"url":"https://attacker.example"}}`, "schema_invalid: agentTaskRecovery: Unrecognized key: \"url\""}, + {"recovery timeout", `{"providers":{},"agentTaskRecovery":{"timeoutMs":999}}`, "schema_invalid: agentTaskRecovery.timeoutMs: Too small: expected number to be >=1000"}, + {"loopback disabled shape", `{"providers":{},"unauthenticatedLoopbackListener":{"enabled":false,"port":1}}`, "schema_invalid: unauthenticatedLoopbackListener: Unrecognized key: \"port\""}, + {"loopback collision", `{"providers":{},"port":1234,"unauthenticatedLoopbackListener":{"enabled":true,"port":1234}}`, "schema_invalid: unauthenticatedLoopbackListener.port: must differ from the proxy port"}, + {"ingress needs hub", `{"providers":{},"hub":{"managementIngress":{"enabled":true,"port":1235}}}`, "schema_invalid: hub.managementIngress: enabled ingress requires runtimeRole hub"}, + {"ingress loopback collision", `{"providers":{},"runtimeRole":"hub","unauthenticatedLoopbackListener":{"enabled":true,"port":1235},"hub":{"managementIngress":{"enabled":true,"port":1235}}}`, "schema_invalid: hub.managementIngress.port: must differ from unauthenticatedLoopbackListener.port"}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + _, err := ValidateCandidateJSON([]byte(tc.raw)) + if err == nil || err.Error() != tc.want { + t.Fatalf("error = %v, want %q", err, tc.want) + } + }) + } + if _, err := ValidateCandidateJSON([]byte(`{"providers":{},"runtimeRole":"hub","hub":{"managementIngress":{"enabled":true,"port":1235}},"agentTaskRecovery":{"enabled":true,"model":"gpt-5.6-sol","timeoutMs":1000,"cacheEntries":512}}`)); err != nil { + t.Fatalf("valid ingress/recovery rejected: %v", err) + } +} + +func TestStrictWriteSchemaNormalizesAcceptedRemoteValues(t *testing.T) { + normalized, err := ValidateCandidateJSON([]byte( + `{"providers":{},"hub":{"managementPublicOrigin":"https://HUB.example.test:443/"},"remoteGui":{"allowedTailscaleUsers":[" alice@example.test "]},"agentTaskRecovery":{"model":" gpt-5.6-sol "},"runtimeRole":"client","client":{"serverUrl":"https://HUB.example.test:443/","managementUrl":"http://manage.example.test:80/","managementTransport":"direct","selectedClients":["codex"],"tokenEnv":"OPENCODEX_API_AUTH_TOKEN","apiKeyId":" issued-key-id ","tokenFingerprint":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","protocolVersion":1,"connectedAt":"2026-08-28T00:00:00.000Z"}}`, + )) + if err != nil { + t.Fatal(err) + } + got, err := normalized.CompactJSON() + if err != nil { + t.Fatal(err) + } + for _, want := range []string{`"managementPublicOrigin":"https://hub.example.test"`, `"allowedTailscaleUsers":["alice@example.test"]`, `"model":"gpt-5.6-sol"`, `"serverUrl":"https://hub.example.test"`, `"managementUrl":"http://manage.example.test"`, `"apiKeyId":"issued-key-id"`} { + if !strings.Contains(string(got), want) { + t.Fatalf("normalized output %s does not contain %s", got, want) + } + } +} + +func TestStrictWriteSchemaAccountNamespaceCollisionsAndClientState(t *testing.T) { + t.Setenv("OPENCODEX_HOME", "/tmp/ocx37-home") + cases := []struct{ name, raw, want string }{ + {"namespace provider collision", `{"providers":{"work":{"adapter":"openai-chat","baseUrl":"https://example.test"}},"codexAccountNamespaces":{"work":"side-acct"}}`, "schema_invalid: codexAccountNamespaces.work: account selectors must not collide with configured provider, combo, or routing policy namespaces"}, + {"namespace target collision", `{"providers":{},"codexAccountNamespaces":{"first":"same-account","same-account":"@main"}}`, "schema_invalid: codexAccountNamespaces.first: account selectors must not collide with configured Codex pool-account ids or account selector targets"}, + {"duplicate targets allowed", `{"providers":{},"codexAccountNamespaces":{"first":"same-account","second":"same-account"}}`, ""}, + {"client optional timestamp", `{"providers":{},"runtimeRole":"client","client":{"serverUrl":"https://hub.example.test","managementUrl":"https://manage.example.test","managementTransport":"direct","selectedClients":["codex"],"tokenEnv":"OPENCODEX_API_AUTH_TOKEN","apiKeyId":"issued-key-id","tokenFingerprint":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","protocolVersion":1,"connectedAt":"2026-08-28T00:00:00.000Z","catalogSyncedAt":"bad"}}`, "schema_invalid: client.catalogSyncedAt: Invalid ISO datetime"}, + {"client pending state strict", `{"providers":{},"runtimeRole":"client","client":{"serverUrl":"https://hub.example.test","managementUrl":"https://manage.example.test","managementTransport":"direct","selectedClients":["codex"],"tokenEnv":"OPENCODEX_API_AUTH_TOKEN","apiKeyId":"issued-key-id","tokenFingerprint":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","protocolVersion":1,"connectedAt":"2026-08-28T00:00:00.000Z","pendingOperation":{"kind":"rotate","rotationId":"r","newKeyIssuedAt":"2026-08-28T00:00:00.000Z","oldKeyBackupPath":"/tmp/previous","extra":true}}}`, "schema_invalid: client.pendingOperation: Unrecognized key: \"extra\""}, + {"client pending state path", `{"providers":{},"runtimeRole":"client","client":{"serverUrl":"https://hub.example.test","managementUrl":"https://manage.example.test","managementTransport":"direct","selectedClients":["codex"],"tokenEnv":"OPENCODEX_API_AUTH_TOKEN","apiKeyId":"issued-key-id","tokenFingerprint":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","protocolVersion":1,"connectedAt":"2026-08-28T00:00:00.000Z","pendingOperation":{"kind":"rotate","rotationId":"r","newKeyIssuedAt":"2026-08-28T00:00:00.000Z","oldKeyBackupPath":"/tmp/foreign"}}}`, "schema_invalid: client.pendingOperation.oldKeyBackupPath: must equal /tmp/ocx37-home/service-api-token.prev"}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + _, err := ValidateCandidateJSON([]byte(tc.raw)) + if tc.want == "" { + if err != nil { + t.Fatalf("error = %v, want success", err) + } + return + } + if err == nil || err.Error() != tc.want { + t.Fatalf("error = %v, want %q", err, tc.want) + } + }) + } +} From 18400c519a9cc3ba71cdd94eb3277e321f20362c Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Mon, 7 Sep 2026 01:59:20 +0800 Subject: [PATCH 080/165] feat(go): port status dashboard runtime diagnostics --- go/internal/ocxcli/cli_test.go | 156 +++++++++++++++++++-- go/internal/ocxcli/status_domains.go | 196 +++++++++++++++++++++++++-- 2 files changed, 327 insertions(+), 25 deletions(-) diff --git a/go/internal/ocxcli/cli_test.go b/go/internal/ocxcli/cli_test.go index 1bd998c7cd..85131d8e67 100644 --- a/go/internal/ocxcli/cli_test.go +++ b/go/internal/ocxcli/cli_test.go @@ -13,6 +13,7 @@ import ( "path/filepath" "runtime" "slices" + "strconv" "strings" "testing" "time" @@ -37,14 +38,27 @@ func runTypeScriptStatusJSON(t *testing.T, home string) []byte { } // The focused Go worktree intentionally does not install JavaScript packages. -// Use its own dependency tree when present, otherwise use the primary checkout -// named by the migration task as the immutable TypeScript oracle. +// Use its own dependency tree when present, otherwise discover another local +// checkout with its dependency tree. This keeps the TypeScript oracle local and +// makes an unavailable oracle an explicit skip instead of a machine path. func typeScriptOracleRepo(t *testing.T) string { t.Helper() worktree := filepath.Clean(filepath.Join(filepath.Dir(currentTestFile(t)), "..", "..", "..")) if info, err := os.Stat(filepath.Join(worktree, "node_modules")); err == nil && info.IsDir() { return worktree } + listed, err := exec.Command("git", "-C", worktree, "worktree", "list", "--porcelain").Output() + if err == nil { + for _, line := range strings.Split(string(listed), "\n") { + path, ok := strings.CutPrefix(line, "worktree ") + if !ok { + continue + } + if info, statErr := os.Stat(filepath.Join(path, "node_modules")); statErr == nil && info.IsDir() { + return path + } + } + } t.Skip("TypeScript status/doctor oracle needs node_modules in this checkout") return "" } @@ -65,14 +79,38 @@ func statusDomainBytes(t *testing.T, full []byte) []byte { t.Fatalf("compact status oracle: %v; output=%s", err, full) } var status struct { - Proxy json.RawMessage `json:"proxy"` - Listen json.RawMessage `json:"listen"` - Config json.RawMessage `json:"config"` + Proxy json.RawMessage `json:"proxy"` + Dashboard json.RawMessage `json:"dashboard"` + Listen json.RawMessage `json:"listen"` + Paths json.RawMessage `json:"paths"` + Runtime json.RawMessage `json:"runtime"` + Config json.RawMessage `json:"config"` + VersionSkew json.RawMessage `json:"versionSkew"` } if err := json.Unmarshal(compact.Bytes(), &status); err != nil { t.Fatalf("decode status oracle: %v; output=%s", err, compact.Bytes()) } - return []byte(`{"proxy":` + string(status.Proxy) + `,"listen":` + string(status.Listen) + `,"config":` + string(status.Config) + `}`) + return []byte(fmt.Sprintf( + `{"proxy":%s,"dashboard":%s,"listen":%s,"paths":%s,"runtime":%s,"config":%s,"versionSkew":%s}`, + status.Proxy, status.Dashboard, status.Listen, status.Paths, status.Runtime, status.Config, status.VersionSkew, + )) +} + +func statusOracleRuntime(t *testing.T, full []byte) StatusBunRuntime { + t.Helper() + var status struct { + Paths struct { + Runtime string `json:"runtime"` + } `json:"paths"` + Runtime struct { + Source string `json:"source"` + OverrideEnv *string `json:"overrideEnv"` + } `json:"runtime"` + } + if err := json.Unmarshal(full, &status); err != nil { + t.Fatalf("decode status runtime oracle: %v", err) + } + return StatusBunRuntime{Path: status.Paths.Runtime, Source: status.Runtime.Source, OverrideEnv: status.Runtime.OverrideEnv} } func runTypeScriptDoctorProxyHint(t *testing.T, input DoctorProxyDownInput) string { @@ -483,6 +521,7 @@ func TestStatusDomainsMatchTypeScriptOracleForConfigFallback(t *testing.T) { } t.Setenv("OPENCODEX_HOME", home) t.Setenv("CODEX_HOME", filepath.Join(home, "codex")) + t.Setenv("HOME", home) if err := os.Mkdir(filepath.Join(home, "codex"), 0o700); err != nil { t.Fatal(err) } @@ -490,9 +529,10 @@ func TestStatusDomainsMatchTypeScriptOracleForConfigFallback(t *testing.T) { oracle := runTypeScriptStatusJSON(t, home) want := statusDomainBytes(t, oracle) got, err := json.Marshal(CollectStatusDomains(StatusDomainDeps{ - ReadPID: func() int64 { return 0 }, - ReadRuntime: func() (StatusRuntimeRecord, error) { return StatusRuntimeRecord{}, errors.New("missing") }, - HTTPClient: &http.Client{Timeout: 800 * time.Millisecond}, + ReadPID: func() int64 { return 0 }, + ReadRuntime: func() (StatusRuntimeRecord, error) { return StatusRuntimeRecord{}, errors.New("missing") }, + ReadBunRuntime: func() StatusBunRuntime { return statusOracleRuntime(t, oracle) }, + HTTPClient: &http.Client{Timeout: 800 * time.Millisecond}, })) if err != nil { t.Fatal(err) @@ -522,12 +562,15 @@ func TestStatusDomainsMatchTypeScriptOracleForDefaultAndMalformedConfig(t *testi } t.Setenv("OPENCODEX_HOME", home) t.Setenv("CODEX_HOME", filepath.Join(home, "codex")) + t.Setenv("HOME", home) - want := statusDomainBytes(t, runTypeScriptStatusJSON(t, home)) + oracle := runTypeScriptStatusJSON(t, home) + want := statusDomainBytes(t, oracle) got, err := json.Marshal(CollectStatusDomains(StatusDomainDeps{ - ReadPID: func() int64 { return 0 }, - ReadRuntime: func() (StatusRuntimeRecord, error) { return StatusRuntimeRecord{}, errors.New("missing") }, - HTTPClient: &http.Client{Timeout: 800 * time.Millisecond}, + ReadPID: func() int64 { return 0 }, + ReadRuntime: func() (StatusRuntimeRecord, error) { return StatusRuntimeRecord{}, errors.New("missing") }, + ReadBunRuntime: func() StatusBunRuntime { return statusOracleRuntime(t, oracle) }, + HTTPClient: &http.Client{Timeout: 800 * time.Millisecond}, })) if err != nil { t.Fatal(err) @@ -539,6 +582,93 @@ func TestStatusDomainsMatchTypeScriptOracleForDefaultAndMalformedConfig(t *testi } } +func TestStatusDomainsMatchTypeScriptOracleForDashboardPathRuntimeAndVersionSkew(t *testing.T) { + home := t.TempDir() + if err := os.Mkdir(filepath.Join(home, "codex"), 0o700); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(home, "config.json"), []byte(`{"port":4567,"hostname":"0.0.0.0"}`), 0o600); err != nil { + t.Fatal(err) + } + t.Setenv("HOME", home) + t.Setenv("OPENCODEX_HOME", home) + t.Setenv("CODEX_HOME", filepath.Join(home, "codex")) + oracle := runTypeScriptStatusJSON(t, home) + want := statusDomainBytes(t, oracle) + got, err := json.Marshal(CollectStatusDomains(StatusDomainDeps{ + ReadPID: func() int64 { return 0 }, + ReadRuntime: func() (StatusRuntimeRecord, error) { return StatusRuntimeRecord{}, errors.New("missing") }, + ReadBunRuntime: func() StatusBunRuntime { return statusOracleRuntime(t, oracle) }, + })) + if err != nil { + t.Fatal(err) + } + if string(got) != string(want) { + t.Fatalf("domain bytes\n got: %s\nwant: %s", got, want) + } +} + +func TestStatusDomainsMatchTypeScriptOracleForLiveVersionSkew(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _ = json.NewEncoder(w).Encode(map[string]any{ + "service": "opencodex", "version": "2.43.0", "uptime": 1, "pid": os.Getpid(), + }) + })) + defer server.Close() + port := serverPort(strings.TrimPrefix(server.URL, "http://")) + home := t.TempDir() + if err := os.Mkdir(filepath.Join(home, "codex"), 0o700); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(home, "ocx.pid"), []byte(strconv.Itoa(os.Getpid())), 0o600); err != nil { + t.Fatal(err) + } + runtimeState, err := json.Marshal(StatusRuntimeRecord{PID: int64(os.Getpid()), Port: port, Hostname: "127.0.0.1"}) + if err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(home, "runtime-port.json"), runtimeState, 0o600); err != nil { + t.Fatal(err) + } + t.Setenv("HOME", home) + t.Setenv("OPENCODEX_HOME", home) + t.Setenv("CODEX_HOME", filepath.Join(home, "codex")) + oracle := runTypeScriptStatusJSON(t, home) + want := statusDomainBytes(t, oracle) + got, err := json.Marshal(CollectStatusDomains(StatusDomainDeps{ + ReadPID: func() int64 { return int64(os.Getpid()) }, + ReadRuntime: func() (StatusRuntimeRecord, error) { + return StatusRuntimeRecord{PID: int64(os.Getpid()), Port: port, Hostname: "127.0.0.1"}, nil + }, + ReadBunRuntime: func() StatusBunRuntime { return statusOracleRuntime(t, oracle) }, + CLIVersion: "2.42.0", + })) + if err != nil { + t.Fatal(err) + } + if string(got) != string(want) { + t.Fatalf("domain bytes\n got: %s\nwant: %s", got, want) + } +} + +func TestComputeStatusVersionSkewMatchesTypeScriptContract(t *testing.T) { + for _, test := range []struct { + cli, proxy string + want bool + }{ + {cli: "2.42.0", proxy: "", want: false}, + {cli: "unknown", proxy: "2.43.0", want: false}, + {cli: "2.42.0", proxy: "0.0.0", want: false}, + {cli: "2.42.0", proxy: "2.42.0", want: false}, + {cli: "2.42.0", proxy: "2.43.0", want: true}, + } { + got := ComputeStatusVersionSkew(test.cli, test.proxy) + if got.Skewed != test.want { + t.Fatalf("ComputeStatusVersionSkew(%q, %q) = %#v", test.cli, test.proxy, got) + } + } +} + func TestDoctorProxyDownHintMatchesTypeScriptOracle(t *testing.T) { for _, input := range []DoctorProxyDownInput{ {ProxyRunning: true, Port: 10100}, diff --git a/go/internal/ocxcli/status_domains.go b/go/internal/ocxcli/status_domains.go index eaeaf43153..b48366add4 100644 --- a/go/internal/ocxcli/status_domains.go +++ b/go/internal/ocxcli/status_domains.go @@ -6,6 +6,7 @@ import ( "errors" "net/http" "os" + "path/filepath" "strconv" "strings" "time" @@ -26,18 +27,24 @@ type StatusConfigDiagnostic struct { // testable. In particular, commands continue to delegate to TypeScript while // this evidence is compared with its output. type StatusDomainDeps struct { - ReadConfig func() StatusConfigDiagnostic - ReadPID func() int64 - ReadRuntime func() (StatusRuntimeRecord, error) - HTTPClient *http.Client + ReadConfig func() StatusConfigDiagnostic + ReadPID func() int64 + ReadRuntime func() (StatusRuntimeRecord, error) + ReadBunRuntime func() StatusBunRuntime + CLIVersion string + HTTPClient *http.Client } -// StatusDomains is the ordered JSON projection for status's proxy, listen, -// and config domains. The field order intentionally matches CliStatusJson. +// StatusDomains is the ordered JSON projection for the status domains Go has +// incrementally ported. The field order intentionally matches CliStatusJson. type StatusDomains struct { - Proxy StatusProxyDomain `json:"proxy"` - Listen StatusListenDomain `json:"listen"` - Config StatusConfigDomain `json:"config"` + Proxy StatusProxyDomain `json:"proxy"` + Dashboard StatusDashboardDomain `json:"dashboard"` + Listen StatusListenDomain `json:"listen"` + Paths StatusPathsDomain `json:"paths"` + Runtime StatusRuntimeDomain `json:"runtime"` + Config StatusConfigDomain `json:"config"` + VersionSkew StatusVersionSkewDomain `json:"versionSkew"` } type StatusProxyDomain struct { @@ -64,6 +71,38 @@ type StatusConfigDomain struct { Error *string `json:"error"` } +type StatusDashboardDomain struct { + URL string `json:"url"` +} + +type StatusPathsDomain struct { + Config string `json:"config"` + PID string `json:"pid"` + Runtime string `json:"runtime"` +} + +// StatusBunRuntime is the durable runtime provenance status emits. It is a +// dependency so tests can compare the projection against the exact Bun process +// that ran the TypeScript oracle; a Go process otherwise has a different exec +// path by construction. +type StatusBunRuntime struct { + Path string + Source string + OverrideEnv *string +} + +type StatusRuntimeDomain struct { + Source string `json:"source"` + OverrideEnv *string `json:"overrideEnv,omitempty"` +} + +type StatusVersionSkewDomain struct { + CLIVersion string `json:"cliVersion"` + ProxyVersion *string `json:"proxyVersion"` + Skewed bool `json:"skewed"` + Warning *string `json:"warning"` +} + // ReadStatusConfigDiagnostics follows the non-mutating status path: absence // is default, malformed JSON is a fallback with the stable invalid_json error, // and a readable document is a file source. Schema-level fallback remains @@ -129,9 +168,115 @@ func defaultStatusDomainDeps(deps StatusDomainDeps) StatusDomainDeps { if deps.ReadRuntime == nil { deps.ReadRuntime = ReadStatusRuntime } + if deps.ReadBunRuntime == nil { + deps.ReadBunRuntime = ReadStatusBunRuntime + } + if deps.CLIVersion == "" { + deps.CLIVersion = readStatusPackageVersion() + } return deps } +// readStatusPackageVersion follows packageVersion's failure contract. The Go +// diagnostic may run from a built artifact with no checkout nearby, where an +// unknown version is safer than inventing a mismatch. +func readStatusPackageVersion() string { + dir, err := os.Getwd() + if err != nil { + return "unknown" + } + for { + raw, readErr := os.ReadFile(filepath.Join(dir, "package.json")) + if readErr == nil { + var manifest struct { + Version any `json:"version"` + } + if json.Unmarshal(raw, &manifest) == nil { + if version, ok := manifest.Version.(string); ok { + return version + } + } + return "unknown" + } + parent := filepath.Dir(dir) + if parent == dir { + return "unknown" + } + dir = parent + } +} + +// ReadStatusBunRuntime mirrors status's runtime provenance shape for a native +// Go process. A trusted launcher marker must name this executable; otherwise +// the running executable is the only honest process runtime to report. +func ReadStatusBunRuntime() StatusBunRuntime { + path, err := os.Executable() + if err != nil || path == "" { + path = os.Args[0] + } + path = filepath.Clean(path) + source := "process" + if recordedSource := strings.TrimSpace(os.Getenv("OCX_BUN_RUNTIME_SOURCE")); (recordedSource == "override" || recordedSource == "bundled" || recordedSource == "process") && sameStatusRuntimePath(strings.TrimSpace(os.Getenv("OCX_BUN_RUNTIME_PATH")), path) { + source = recordedSource + } + var overrideEnv *string + if source == "override" { + value := "OPENCODEX_BUN_PATH" + overrideEnv = &value + } + return StatusBunRuntime{Path: path, Source: source, OverrideEnv: overrideEnv} +} + +func sameStatusRuntimePath(left, right string) bool { + if left == "" || right == "" { + return false + } + canonical := func(path string) string { + if resolved, err := filepath.EvalSymlinks(path); err == nil { + return resolved + } + return filepath.Clean(path) + } + return canonical(left) == canonical(right) +} + +// ComputeStatusVersionSkew is a direct projection of computeVersionSkew. +// Empty proxyVersion is the no-live-proxy case; placeholder versions are +// deliberately incomparable rather than evidence of a stale installation. +func ComputeStatusVersionSkew(cliVersion, proxyVersion string) StatusVersionSkewDomain { + var proxy *string + if proxyVersion != "" { + value := proxyVersion + proxy = &value + } + skewed := proxy != nil && cliVersion != "unknown" && cliVersion != "0.0.0" && proxyVersion != "unknown" && proxyVersion != "0.0.0" && cliVersion != proxyVersion + if !skewed { + return StatusVersionSkewDomain{CLIVersion: cliVersion, ProxyVersion: proxy} + } + warning := "CLI " + cliVersion + " does not match the running proxy " + proxyVersion + " — this ocx on PATH is stale. Its help and features describe a different build. Reinstall, or run the proxy's own binary." + return StatusVersionSkewDomain{CLIVersion: cliVersion, ProxyVersion: proxy, Skewed: true, Warning: &warning} +} + +func statusDashboardURL(cfg *config.Config, hostname string, port int) string { + if cfg != nil { + if runtimeRole, _ := cfg.Raw["runtimeRole"].(string); runtimeRole == "hub" { + if hub, _ := cfg.Raw["hub"].(map[string]any); hub != nil { + if origin, _ := hub["managementPublicOrigin"].(string); origin != "" { + if strings.HasSuffix(origin, "/") { + return origin + } + return origin + "/" + } + } + } + } + reachable := statusProbeHost(hostname) + if reachable == "127.0.0.1" || reachable == "[::1]" || strings.EqualFold(reachable, "localhost") { + reachable = "localhost" + } + return "http://" + reachable + ":" + strconv.Itoa(port) + "/" +} + func readStatusPIDFile() int64 { dir, err := config.Dir() if err != nil { @@ -161,8 +306,10 @@ func CollectStatusDomains(deps StatusDomainDeps) StatusDomains { pid := deps.ReadPID() port, hostname := cfg.ListenTarget() source := "config" + liveRuntime := false if runtime, err := deps.ReadRuntime(); err == nil && pid > 0 && runtime.PID == pid { port, hostname, source = runtime.Port, runtime.Hostname, "runtime" + liveRuntime = true } client := &http.Client{Timeout: 800 * time.Millisecond} if deps.HTTPClient != nil { @@ -178,13 +325,38 @@ func CollectStatusDomains(deps StatusDomainDeps) StatusDomains { hostnameCopy := hostname hostnameValue = &hostnameCopy } + pathsConfig, err := config.Path() + if err != nil { + pathsConfig = "" + } + pathsPID := "" + if pathsConfig != "" { + pathsPID = filepath.Join(filepath.Dir(pathsConfig), "ocx.pid") + } + bunRuntime := deps.ReadBunRuntime() + proxyVersion := "" + if health.OK { + proxyVersion = health.Version + } + healthMessage := health.Message + if health.OK && liveRuntime { + healthMessage = "ok (pid " + strconv.FormatInt(pid, 10) + ")" + } return StatusDomains{ Proxy: StatusProxyDomain{ Running: pid > 0 && health.OK, PID: pidValue, - Health: StatusHealthDomain{OK: health.OK, URL: health.URL, Message: health.Message}, + Health: StatusHealthDomain{OK: health.OK, URL: health.URL, Message: healthMessage}, + }, + Dashboard: StatusDashboardDomain{URL: statusDashboardURL(cfg, hostname, port)}, + Listen: StatusListenDomain{Port: port, Hostname: hostnameValue, Source: source}, + Paths: StatusPathsDomain{ + Config: pathsConfig, + PID: pathsPID, + Runtime: bunRuntime.Path, }, - Listen: StatusListenDomain{Port: port, Hostname: hostnameValue, Source: source}, - Config: StatusConfigDomain{Source: diagnostic.Source, Error: diagnostic.Error}, + Runtime: StatusRuntimeDomain{Source: bunRuntime.Source, OverrideEnv: bunRuntime.OverrideEnv}, + Config: StatusConfigDomain{Source: diagnostic.Source, Error: diagnostic.Error}, + VersionSkew: ComputeStatusVersionSkew(deps.CLIVersion, proxyVersion), } } From d95b13e8b1410c6f135f6910d43cdcadbf523ac8 Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Mon, 7 Sep 2026 01:59:11 +0800 Subject: [PATCH 081/165] test(go): cover successful quota consume parity --- tests/go-sidecar-parity.test.ts | 72 +++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/tests/go-sidecar-parity.test.ts b/tests/go-sidecar-parity.test.ts index cb89c765d2..ef76c0a6ed 100644 --- a/tests/go-sidecar-parity.test.ts +++ b/tests/go-sidecar-parity.test.ts @@ -6,6 +6,8 @@ import { fileURLToPath } from "node:url"; import { SERVER_BUDGET_MS } from "./helpers/test-budget"; import { saveConfig } from "../src/config"; import { getConfigPath } from "../src/config/paths"; +import { saveCodexAccountCredential } from "../src/codex/account-store"; +import { clearAccountQuota, getAccountQuota } from "../src/codex/quota"; import { startServer } from "../src/server"; import { VERSION } from "../src/server/management-api"; import { GO_OWNED_MANAGEMENT_ROUTES } from "../src/server/management/route-registry"; @@ -490,6 +492,76 @@ describe.skipIf(!goAvailable || sidecarBinary === null)("ocx-sidecar differentia } }); + runFixtureTest("successful quota consume with a valid account matches through Go", async (token) => { + // The empty-body vector above deliberately exercises validation only. Seed a + // real pool account plus its credential record so this vector crosses the + // successful upstream consume and WHAM-refresh path. The response is a raw + // capture, so equality includes status, headers, and exact JSON bytes. + const accountId = "quota-consume-success"; + const initial = readFileSync(getConfigPath()); + const fixture = configFixture(); + fixture.codexAccounts = [{ id: accountId, email: "quota@example.test", isMain: false }]; + saveConfig(fixture); + saveCodexAccountCredential(accountId, { + accessToken: "quota-consume-access", + refreshToken: "quota-consume-refresh", + expiresAt: Date.now() + 5 * 60_000, + chatgptAccountId: "acct-quota-consume", + }); + const previousFetch = globalThis.fetch; + globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => { + const url = String(input); + if (url.includes("/rate-limit-reset-credits/consume")) { + expect(init?.method).toBe("POST"); + return Response.json({ code: "reset" }); + } + if (url.includes("/backend-api/wham/usage")) { + return Response.json({ + rate_limit: { primary_window: { used_percent: 10, reset_at: 1_782_000_000 } }, + rate_limit_reset_credits: { available_count: 2 }, + }); + } + return previousFetch(input, init); + }) as typeof fetch; + + try { + let tsResult: Awaited>; + let tsQuota: ReturnType; + const tsServer = startServer(0); + try { + tsResult = await captureMutation(tsServer, token, "POST", "/api/codex-auth/reset-credits/consume", { accountId }); + tsQuota = getAccountQuota(accountId); + expect(tsResult).toMatchObject({ status: 200, body: '{"code":"reset","remaining":2}' }); + expect(tsQuota?.resetCredits).toBe(2); + } finally { + await tsServer.stop(true); + } + + clearAccountQuota(accountId); + process.env[GO_SIDECAR_BIN_ENV] = sidecarBinary!; + const goServer = startServer(0); + try { + await waitFor(() => activeGoSidecarBaseUrl(), 15_000); + const goResult = await captureMutation(goServer, token, "POST", "/api/codex-auth/reset-credits/consume", { accountId }); + const goQuota = getAccountQuota(accountId); + expect(goResult).toEqual(tsResult!); + // The cache's update instant belongs to each separate server leg; its + // refreshed quota projection must still agree exactly. + expect(goQuota).toMatchObject({ + weeklyPercent: tsQuota?.weeklyPercent, + weeklyResetAt: tsQuota?.weeklyResetAt, + resetCredits: tsQuota?.resetCredits, + }); + expect(goQuota?.updatedAt).toBeGreaterThan(0); + } finally { + await goServer.stop(true); + } + } finally { + globalThis.fetch = previousFetch; + writeFileSync(getConfigPath(), initial); + } + }); + runFixtureTest("codex-auth account-pool write vectors have a state-reset differential oracle", async (token) => { // Covers the declared Go-owned codex-auth writes not yet pinned by a // differential: active (pin + write), pool-strategy PUT and PATCH (write), From 1b89d831e48987320ec78cab6f18a533d21150ec Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Mon, 7 Sep 2026 02:08:57 +0800 Subject: [PATCH 082/165] feat(go): port extra status domains --- go/internal/ocxcli/cli_test.go | 60 +++- go/internal/ocxcli/status_domains.go | 32 +- go/internal/ocxcli/status_domains_extra.go | 365 +++++++++++++++++++++ 3 files changed, 438 insertions(+), 19 deletions(-) create mode 100644 go/internal/ocxcli/status_domains_extra.go diff --git a/go/internal/ocxcli/cli_test.go b/go/internal/ocxcli/cli_test.go index 85131d8e67..bcb9a6bc6f 100644 --- a/go/internal/ocxcli/cli_test.go +++ b/go/internal/ocxcli/cli_test.go @@ -79,20 +79,25 @@ func statusDomainBytes(t *testing.T, full []byte) []byte { t.Fatalf("compact status oracle: %v; output=%s", err, full) } var status struct { - Proxy json.RawMessage `json:"proxy"` - Dashboard json.RawMessage `json:"dashboard"` - Listen json.RawMessage `json:"listen"` - Paths json.RawMessage `json:"paths"` - Runtime json.RawMessage `json:"runtime"` - Config json.RawMessage `json:"config"` - VersionSkew json.RawMessage `json:"versionSkew"` + SchemaVersion json.RawMessage `json:"schemaVersion"` + Proxy json.RawMessage `json:"proxy"` + Dashboard json.RawMessage `json:"dashboard"` + Listen json.RawMessage `json:"listen"` + Paths json.RawMessage `json:"paths"` + Runtime json.RawMessage `json:"runtime"` + CodexAutostart json.RawMessage `json:"codexAutostart"` + Startup json.RawMessage `json:"startup"` + DefaultProvider json.RawMessage `json:"defaultProvider"` + Config json.RawMessage `json:"config"` + Connection json.RawMessage `json:"connection"` + VersionSkew json.RawMessage `json:"versionSkew"` } if err := json.Unmarshal(compact.Bytes(), &status); err != nil { t.Fatalf("decode status oracle: %v; output=%s", err, compact.Bytes()) } return []byte(fmt.Sprintf( - `{"proxy":%s,"dashboard":%s,"listen":%s,"paths":%s,"runtime":%s,"config":%s,"versionSkew":%s}`, - status.Proxy, status.Dashboard, status.Listen, status.Paths, status.Runtime, status.Config, status.VersionSkew, + `{"schemaVersion":%s,"proxy":%s,"dashboard":%s,"listen":%s,"paths":%s,"runtime":%s,"codexAutostart":%s,"startup":%s,"defaultProvider":%s,"config":%s,"connection":%s,"versionSkew":%s}`, + status.SchemaVersion, status.Proxy, status.Dashboard, status.Listen, status.Paths, status.Runtime, status.CodexAutostart, status.Startup, status.DefaultProvider, status.Config, status.Connection, status.VersionSkew, )) } @@ -608,6 +613,43 @@ func TestStatusDomainsMatchTypeScriptOracleForDashboardPathRuntimeAndVersionSkew } } +func TestStatusDomainsMatchTypeScriptOracleForAutostartAndConnection(t *testing.T) { + for _, test := range []struct { + name, content string + }{ + {name: "autostart-disabled", content: `{"providers":{},"codexAutoStart":false}`}, + {name: "custom-default-provider", content: `{"providers":{"fixture":{"adapter":"openai-chat","baseUrl":"https://api.example.test/v1","apiKey":"test-key"}},"defaultProvider":"fixture"}`}, + {name: "connected-client", content: `{"providers":{},"runtimeRole":"client","client":{"serverUrl":"https://hub.example.test","managementUrl":"https://manage.example.test","managementTransport":"direct","selectedClients":["codex"],"tokenEnv":"OPENCODEX_API_AUTH_TOKEN","apiKeyId":"issued-key-id","tokenFingerprint":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","protocolVersion":1,"connectedAt":"2026-08-28T00:00:00.000Z"}}`}, + {name: "connected-client-future-catalog-age", content: `{"providers":{},"runtimeRole":"client","client":{"serverUrl":"https://hub.example.test","managementUrl":"https://manage.example.test","managementTransport":"direct","selectedClients":["codex"],"tokenEnv":"OPENCODEX_API_AUTH_TOKEN","apiKeyId":"issued-key-id","tokenFingerprint":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","protocolVersion":1,"connectedAt":"2026-08-28T00:00:00.000Z","catalogSyncedAt":"2099-01-01T00:00:00.000Z"}}`}, + } { + t.Run(test.name, func(t *testing.T) { + home := t.TempDir() + if err := os.Mkdir(filepath.Join(home, "codex"), 0o700); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(home, "config.json"), []byte(test.content), 0o600); err != nil { + t.Fatal(err) + } + t.Setenv("HOME", home) + t.Setenv("OPENCODEX_HOME", home) + t.Setenv("CODEX_HOME", filepath.Join(home, "codex")) + oracle := runTypeScriptStatusJSON(t, home) + want := statusDomainBytes(t, oracle) + got, err := json.Marshal(CollectStatusDomains(StatusDomainDeps{ + ReadPID: func() int64 { return 0 }, + ReadRuntime: func() (StatusRuntimeRecord, error) { return StatusRuntimeRecord{}, errors.New("missing") }, + ReadBunRuntime: func() StatusBunRuntime { return statusOracleRuntime(t, oracle) }, + })) + if err != nil { + t.Fatal(err) + } + if string(got) != string(want) { + t.Fatalf("domain bytes\n got: %s\nwant: %s", got, want) + } + }) + } +} + func TestStatusDomainsMatchTypeScriptOracleForLiveVersionSkew(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _ = json.NewEncoder(w).Encode(map[string]any{ diff --git a/go/internal/ocxcli/status_domains.go b/go/internal/ocxcli/status_domains.go index b48366add4..65839cd774 100644 --- a/go/internal/ocxcli/status_domains.go +++ b/go/internal/ocxcli/status_domains.go @@ -33,18 +33,24 @@ type StatusDomainDeps struct { ReadBunRuntime func() StatusBunRuntime CLIVersion string HTTPClient *http.Client + Extra StatusExtraDeps } // StatusDomains is the ordered JSON projection for the status domains Go has // incrementally ported. The field order intentionally matches CliStatusJson. type StatusDomains struct { - Proxy StatusProxyDomain `json:"proxy"` - Dashboard StatusDashboardDomain `json:"dashboard"` - Listen StatusListenDomain `json:"listen"` - Paths StatusPathsDomain `json:"paths"` - Runtime StatusRuntimeDomain `json:"runtime"` - Config StatusConfigDomain `json:"config"` - VersionSkew StatusVersionSkewDomain `json:"versionSkew"` + SchemaVersion int `json:"schemaVersion"` + Proxy StatusProxyDomain `json:"proxy"` + Dashboard StatusDashboardDomain `json:"dashboard"` + Listen StatusListenDomain `json:"listen"` + Paths StatusPathsDomain `json:"paths"` + Runtime StatusRuntimeDomain `json:"runtime"` + CodexAutostart bool `json:"codexAutostart"` + Startup StatusStartupDomain `json:"startup"` + DefaultProvider string `json:"defaultProvider"` + Config StatusConfigDomain `json:"config"` + Connection StatusConnectionDomain `json:"connection"` + VersionSkew StatusVersionSkewDomain `json:"versionSkew"` } type StatusProxyDomain struct { @@ -334,6 +340,7 @@ func CollectStatusDomains(deps StatusDomainDeps) StatusDomains { pathsPID = filepath.Join(filepath.Dir(pathsConfig), "ocx.pid") } bunRuntime := deps.ReadBunRuntime() + extra := CollectStatusExtraDomains(diagnostic, deps.Extra) proxyVersion := "" if health.OK { proxyVersion = health.Version @@ -343,6 +350,7 @@ func CollectStatusDomains(deps StatusDomainDeps) StatusDomains { healthMessage = "ok (pid " + strconv.FormatInt(pid, 10) + ")" } return StatusDomains{ + SchemaVersion: 1, Proxy: StatusProxyDomain{ Running: pid > 0 && health.OK, PID: pidValue, @@ -355,8 +363,12 @@ func CollectStatusDomains(deps StatusDomainDeps) StatusDomains { PID: pathsPID, Runtime: bunRuntime.Path, }, - Runtime: StatusRuntimeDomain{Source: bunRuntime.Source, OverrideEnv: bunRuntime.OverrideEnv}, - Config: StatusConfigDomain{Source: diagnostic.Source, Error: diagnostic.Error}, - VersionSkew: ComputeStatusVersionSkew(deps.CLIVersion, proxyVersion), + Runtime: StatusRuntimeDomain{Source: bunRuntime.Source, OverrideEnv: bunRuntime.OverrideEnv}, + CodexAutostart: extra.CodexAutostart, + Startup: extra.Startup, + DefaultProvider: extra.DefaultProvider, + Config: StatusConfigDomain{Source: diagnostic.Source, Error: diagnostic.Error}, + Connection: extra.Connection, + VersionSkew: ComputeStatusVersionSkew(deps.CLIVersion, proxyVersion), } } diff --git a/go/internal/ocxcli/status_domains_extra.go b/go/internal/ocxcli/status_domains_extra.go new file mode 100644 index 0000000000..e0ddcf9ffb --- /dev/null +++ b/go/internal/ocxcli/status_domains_extra.go @@ -0,0 +1,365 @@ +package ocxcli + +import ( + "crypto/sha256" + "encoding/hex" + "encoding/json" + "os" + "path/filepath" + "runtime" + "strings" + "time" + + "github.com/lidge-jun/opencodex/go/internal/config" +) + +// StatusExtraDeps isolates the diagnostic inputs that remain outside the +// initial status evidence port. Production defaults are deliberately +// read-only; callers that own service or shim diagnostics can supply their +// authoritative observations without widening this status projection. +type StatusExtraDeps struct { + RoutingKind func() string + Service func() StatusServiceDiagnostic + Shim func() StatusShimDiagnostic + Platform string + NowUnix func() int64 +} + +type StatusServiceDiagnostic struct { + Supported bool + Installed bool + Viable bool + Enabled bool + Running bool + Stale bool + Conflict bool +} + +type StatusShimDiagnostic struct { + Installed bool + Healthy bool +} + +type StatusExtraDomains struct { + CodexAutostart bool + Startup StatusStartupDomain + DefaultProvider string + Connection StatusConnectionDomain +} + +type StatusStartupDomain struct { + RoutingKind string `json:"routingKind"` + AutostartEnabled bool `json:"autostartEnabled"` + ServiceInstalled bool `json:"serviceInstalled"` + ServiceViable bool `json:"serviceViable"` + ServiceEnabled bool `json:"serviceEnabled"` + ServiceRunning bool `json:"serviceRunning"` + ServiceStale bool `json:"serviceStale"` + ServiceConflict bool `json:"serviceConflict"` + ServiceSupported bool `json:"serviceSupported"` + ShimInstalled bool `json:"shimInstalled"` + ShimHealthy bool `json:"shimHealthy"` + Platform string `json:"platform"` + DiagnosticStale bool `json:"diagnosticStale"` + RoutingInjected bool `json:"routingInjected"` + LocalRoutingDependency bool `json:"localRoutingDependency"` + Status string `json:"status"` + RebootSafe bool `json:"rebootSafe"` + Protection string `json:"protection"` + ShimCoverage string `json:"shimCoverage"` + RecommendedCommand *string `json:"recommendedCommand"` + Commands StatusStartupCommandsDomain `json:"commands"` +} + +type StatusStartupCommandsDomain struct { + InstallService string `json:"installService"` + RepairService string `json:"repairService"` + InstallShim string `json:"installShim"` + RestoreNative string `json:"restoreNative"` +} + +type StatusConnectionDomain struct { + State string `json:"state"` + Reason *string `json:"reason,omitempty"` + ServerURL *string `json:"serverUrl,omitempty"` + ManagementURL *string `json:"managementUrl,omitempty"` + ProtocolVersion *int `json:"protocolVersion,omitempty"` + APIKeyID *string `json:"apiKeyId,omitempty"` + SelectedClients []string `json:"selectedClients,omitempty"` + Catalog string `json:"catalog"` + CatalogAgeSeconds *int64 `json:"catalogAgeSeconds,omitempty"` + CredentialFile string `json:"credentialFile"` +} + +func defaultStatusExtraDeps(deps StatusExtraDeps) StatusExtraDeps { + if deps.RoutingKind == nil { + deps.RoutingKind = readStatusRoutingKind + } + if deps.Service == nil { + deps.Service = func() StatusServiceDiagnostic { return StatusServiceDiagnostic{Supported: true} } + } + if deps.Shim == nil { + deps.Shim = func() StatusShimDiagnostic { return StatusShimDiagnostic{} } + } + if deps.Platform == "" { + deps.Platform = runtime.GOOS + } + if deps.NowUnix == nil { + deps.NowUnix = func() int64 { return time.Now().Unix() } + } + return deps +} + +func CollectStatusExtraDomains(diagnostic StatusConfigDiagnostic, deps StatusExtraDeps) StatusExtraDomains { + deps = defaultStatusExtraDeps(deps) + codexAutostart := true + defaultProvider := "openai" + if diagnostic.Config != nil { + if value, exists := diagnostic.Config.Raw["codexAutoStart"]; exists && value == false { + codexAutostart = false + } + if value, ok := diagnostic.Config.Raw["defaultProvider"].(string); ok && value != "" { + defaultProvider = value + } + } + service, shim := deps.Service(), deps.Shim() + startup := deriveStatusStartup(codexAutostart, deps.RoutingKind(), service, shim, deps.Platform) + return StatusExtraDomains{ + CodexAutostart: codexAutostart, + Startup: startup, + DefaultProvider: defaultProvider, + Connection: collectStatusConnection(deps.NowUnix()), + } +} + +func deriveStatusStartup(autostart bool, routing string, service StatusServiceDiagnostic, shim StatusShimDiagnostic, platform string) StatusStartupDomain { + commands := StatusStartupCommandsDomain{"ocx service install", "ocx service repair", "ocx codex-shim install", "ocx restore"} + if routing == "" { + routing = "native" + } + routingInjected := routing == "opencodex-local" + localDependency := routingInjected || routing == "custom-local" || routing == "unknown" + shimEffective := autostart && shim.Healthy + protection := "none" + if routingInjected && service.Viable { + protection = "service" + } else if routingInjected && shimEffective { + protection = "shim" + } + rebootSafe := !localDependency || (routingInjected && service.Viable) + status := "at-risk" + if !localDependency { + status = "native" + } else if rebootSafe { + status = "protected" + } + var recommended *string + if status == "at-risk" { + command := commands.RestoreNative + if routing != "custom-local" && routing != "unknown" && service.Supported { + if service.Installed && !service.Conflict { + command = commands.RepairService + } else { + command = commands.InstallService + } + } + recommended = &command + } + coverage := "none" + if shimEffective { + coverage = "cli-only" + } + return StatusStartupDomain{ + RoutingKind: routing, AutostartEnabled: autostart, + ServiceInstalled: service.Installed, ServiceViable: service.Viable, ServiceEnabled: service.Enabled, + ServiceRunning: service.Running, ServiceStale: service.Stale, ServiceConflict: service.Conflict, + ServiceSupported: service.Supported, ShimInstalled: shim.Installed, ShimHealthy: shim.Healthy, + Platform: platform, DiagnosticStale: false, RoutingInjected: routingInjected, + LocalRoutingDependency: localDependency, Status: status, RebootSafe: rebootSafe, + Protection: protection, ShimCoverage: coverage, RecommendedCommand: recommended, Commands: commands, + } +} + +func readStatusRoutingKind() string { + home := strings.TrimSpace(os.Getenv("CODEX_HOME")) + if home == "" { + return "native" + } + raw, err := os.ReadFile(filepath.Join(home, "config.toml")) + if os.IsNotExist(err) { + return "native" + } + if err != nil { + return "unknown" + } + content := string(raw) + if strings.Contains(content, "# Auto-injected by opencodex") && strings.Contains(content, "openai_base_url") { + return "opencodex-local" + } + if value, ok := statusTOMLString(content, "openai_base_url"); ok { + if statusLocalURL(value) { + return "custom-local" + } + return "custom-remote" + } + if value, ok := statusTOMLString(content, "model_provider"); ok && value != "openai" { + return "unknown" + } + return "native" +} + +func statusTOMLString(content, key string) (string, bool) { + for _, line := range strings.Split(content, "\n") { + if strings.HasPrefix(strings.TrimSpace(line), "[") { + break + } + pieces := strings.SplitN(line, "=", 2) + if len(pieces) != 2 || strings.TrimSpace(pieces[0]) != key { + continue + } + value := strings.Trim(strings.TrimSpace(strings.SplitN(pieces[1], "#", 2)[0]), "\"'") + return value, true + } + return "", false +} + +func statusLocalURL(value string) bool { + lower := strings.ToLower(value) + return strings.Contains(lower, "://localhost") || strings.Contains(lower, "://127.") || strings.Contains(lower, "://[::1]") || strings.Contains(lower, "://0.0.0.0") || strings.Contains(lower, "://[::]") +} + +func collectStatusConnection(nowUnix int64) StatusConnectionDomain { + result := StatusConnectionDomain{State: "disconnected", Catalog: statusCatalogState(), CredentialFile: "missing"} + path, err := config.Path() + if err != nil { + return result + } + rawBytes, err := os.ReadFile(path) + if err != nil { + if os.IsNotExist(err) { + return result + } + reason := "config.json is missing or unreadable" + result.State, result.Reason = "invalid", &reason + return result + } + var raw map[string]any + decoder := json.NewDecoder(strings.NewReader(strings.TrimPrefix(string(rawBytes), "\ufeff"))) + decoder.UseNumber() + if decoder.Decode(&raw) != nil || raw == nil { + reason := "config.json is missing or unreadable" + result.State, result.Reason = "invalid", &reason + return result + } + clientValue, hasClient := raw["client"] + role, rolePresent := raw["runtimeRole"] + roleString, roleOK := role.(string) + if rolePresent && !roleOK || roleOK && roleString != "standalone" && roleString != "hub" && roleString != "client" { + reason := "config.json.runtimeRole is invalid" + result.State, result.Reason = "invalid", &reason + return result + } + if !hasClient && (!rolePresent || roleString == "standalone" || roleString == "hub") { + return result + } + if !hasClient || roleString != "client" { + reason := "runtimeRole=client is present without config.json.client" + if hasClient { + reason = "config.json.client is present without runtimeRole=client" + } + result.State, result.Reason = "mismatched", &reason + return result + } + client, ok := clientValue.(map[string]any) + if !ok { + reason := "config.json.client is malformed" + result.State, result.Reason = "invalid", &reason + return result + } + serverURL, serverOK := client["serverUrl"].(string) + managementURL, managementOK := client["managementUrl"].(string) + apiKeyID, apiKeyOK := client["apiKeyId"].(string) + protocol, protocolOK := statusInteger(client["protocolVersion"]) + clients, clientsOK := statusClients(client["selectedClients"]) + if !serverOK || !managementOK || !apiKeyOK || !protocolOK || !clientsOK { + reason := "config.json.client is malformed" + result.State, result.Reason = "invalid", &reason + return result + } + result.State, result.ServerURL, result.ManagementURL, result.APIKeyID, result.ProtocolVersion, result.SelectedClients = "connected", &serverURL, &managementURL, &apiKeyID, &protocol, clients + if syncedAt, ok := client["catalogSyncedAt"].(string); ok { + if parsed, parseErr := time.Parse(time.RFC3339, syncedAt); parseErr == nil { + age := nowUnix - parsed.Unix() + if age < 0 { + age = 0 + } + result.CatalogAgeSeconds = &age + } + } + fingerprint, fingerprintOK := client["tokenFingerprint"].(string) + result.CredentialFile = statusCredentialFile(fingerprint, fingerprintOK) + return result +} + +func statusCatalogState() string { + home := strings.TrimSpace(os.Getenv("CODEX_HOME")) + if home == "" { + return "missing" + } + info, err := os.Lstat(filepath.Join(home, "opencodex-catalog.json")) + if os.IsNotExist(err) { + return "missing" + } + if err != nil || !info.Mode().IsRegular() { + return "unsafe" + } + return "present" +} + +func statusCredentialFile(fingerprint string, fingerprintOK bool) string { + dir, err := config.Dir() + if err != nil { + return "missing" + } + info, err := os.Lstat(filepath.Join(dir, "service-api-token")) + if os.IsNotExist(err) { + return "missing" + } + if err != nil || !info.Mode().IsRegular() || info.Size() > 4096 { + return "unsafe" + } + raw, err := os.ReadFile(filepath.Join(dir, "service-api-token")) + token := strings.TrimSpace(string(raw)) + if err != nil || token == "" { + return "unsafe" + } + digest := sha256.Sum256([]byte(token)) + if fingerprintOK && hex.EncodeToString(digest[:]) == fingerprint { + return "owned" + } + return "changed" +} + +func statusInteger(value any) (int, bool) { + number, ok := value.(json.Number) + if !ok { + return 0, false + } + parsed, err := number.Int64() + return int(parsed), err == nil +} +func statusClients(value any) ([]string, bool) { + values, ok := value.([]any) + if !ok || len(values) == 0 { + return nil, false + } + out := make([]string, 0, len(values)) + for _, value := range values { + client, ok := value.(string) + if !ok || (client != "codex" && client != "claude") { + return nil, false + } + out = append(out, client) + } + return out, true +} From b66ade84de6e231aee33b5424c9bce2f5e99cbc6 Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Mon, 7 Sep 2026 02:08:47 +0800 Subject: [PATCH 083/165] feat(go): port external status diagnostics --- go/internal/ocxcli/status_domains_external.go | 345 ++++++++++++++++++ .../ocxcli/status_domains_external_test.go | 94 +++++ 2 files changed, 439 insertions(+) create mode 100644 go/internal/ocxcli/status_domains_external.go create mode 100644 go/internal/ocxcli/status_domains_external_test.go diff --git a/go/internal/ocxcli/status_domains_external.go b/go/internal/ocxcli/status_domains_external.go new file mode 100644 index 0000000000..a65a88556e --- /dev/null +++ b/go/internal/ocxcli/status_domains_external.go @@ -0,0 +1,345 @@ +package ocxcli + +import ( + "encoding/json" + "errors" + "os" + "os/exec" + "path/filepath" + "regexp" + "runtime" + "strings" + + "github.com/lidge-jun/opencodex/go/internal/config" +) + +// StatusExternalDomains is the independent projection of status evidence +// outside the proxy process. The top-level status command remains TypeScript-owned. +type StatusExternalDomains struct { + Service StatusExternalSummary `json:"service"` + CodexShim StatusExternalSummary `json:"codexShim"` + CodexPlugins StatusPluginsDomain `json:"codexPlugins"` + CodexRuntime StatusCodexRuntime `json:"codexRuntime"` + CodexHome StatusCodexHome `json:"codexHome"` + ClaudeDesktop StatusClaudeDesktop `json:"claudeDesktop"` +} +type StatusExternalSummary struct { + Summary string `json:"summary"` +} +type StatusPluginsDomain struct { + Applicable bool `json:"applicable"` + Reason string `json:"reason"` + Summary string `json:"summary"` +} +type StatusCodexRuntime struct { + Path string `json:"path"` + Version *string `json:"version"` + Source string `json:"source"` + NewerAvailable *StatusNewerRuntime `json:"newerAvailable"` + Warning *string `json:"warning"` + CatalogClamp StatusCatalogClamp `json:"catalogClamp"` +} +type StatusNewerRuntime struct { + Path string `json:"path"` + Version *string `json:"version"` +} +type StatusCatalogClamp struct { + Active bool `json:"active"` + RemovedEfforts []string `json:"removedEfforts"` + RuntimeVersion *string `json:"runtimeVersion"` +} +type StatusCodexHome struct { + Applicable bool `json:"applicable"` + Mismatch bool `json:"mismatch"` + EffectiveCodexHome string `json:"effectiveCodexHome"` + AppCodexHome string `json:"appCodexHome"` + OrcaCodexHome *string `json:"orcaCodexHome"` + Warning *string `json:"warning"` + Action *string `json:"action"` +} +type StatusClaudeDesktop struct { + DesiredEnabled bool `json:"desiredEnabled"` + Policy StatusClaudeDesktopPolicy `json:"policy"` +} +type StatusClaudeDesktopPolicy struct { + OK bool `json:"ok"` + Status string `json:"status"` + State string `json:"state"` + Message string `json:"message"` + Action string `json:"action"` +} + +// CollectStatusExternalDomains mirrors TypeScript's read-only status evidence path. +func CollectStatusExternalDomains() StatusExternalDomains { + cfg := ReadStatusConfigDiagnostics().Config + if cfg == nil { + cfg = &config.Config{} + } + return StatusExternalDomains{Service: StatusExternalSummary{statusServiceSummary()}, CodexShim: StatusExternalSummary{statusCodexShimSummary()}, CodexPlugins: statusCodexPlugins(), CodexRuntime: statusCodexRuntime(), CodexHome: statusCodexHome(), ClaudeDesktop: statusClaudeDesktop(cfg)} +} + +func statusServiceSummary() string { + if runtime.GOOS == "linux" { + if _, err := os.Stat("/.dockerenv"); err == nil { + return "unsupported in Docker" + } + if _, err := exec.LookPath("systemctl"); err != nil { + return "unsupported: systemd not found" + } + home, err := os.UserHomeDir() + if err != nil { + return "unsupported: systemd not found" + } + unit := filepath.Join(home, ".config", "systemd", "user", "opencodex-proxy.service") + log := filepath.Join(statusConfigDir(), "service.log") + if _, err := os.Stat(unit); errors.Is(err, os.ErrNotExist) { + return "not installed (logs: " + redactStatusPath(log) + ")" + } + enabled := statusSystemctl("--user", "is-enabled", "opencodex-proxy") == "enabled" + running := statusSystemctl("--user", "is-active", "opencodex-proxy") == "active" + if enabled && running { + return "installed, enabled and running (systemd user; logs: " + redactStatusPath(log) + ")" + } + if !enabled { + return "installed, but disabled (systemd user; logs: " + redactStatusPath(log) + ")" + } + return "installed, but not running (systemd user; logs: " + redactStatusPath(log) + ")" + } + return "unsupported on " + runtime.GOOS +} +func statusSystemctl(args ...string) string { + out, err := exec.Command("systemctl", args...).Output() + if err != nil { + return "" + } + return strings.TrimSpace(string(out)) +} +func statusConfigDir() string { + dir, err := config.Dir() + if err != nil { + return "" + } + return dir +} + +type statusShimState struct { + Platform string `json:"platform"` + WrapperPath string `json:"wrapperPath"` + OriginalPath string `json:"originalPath"` + BackupPath string `json:"backupPath"` + Wrappers []statusShimFile `json:"wrappers"` +} +type statusShimFile struct { + WrapperPath string `json:"wrapperPath"` + OriginalPath string `json:"originalPath"` + BackupPath string `json:"backupPath"` +} + +func statusCodexShimSummary() string { + path := filepath.Join(statusConfigDir(), "codex-shim.json") + raw, err := os.ReadFile(path) + if errors.Is(err, os.ErrNotExist) { + return "Codex autostart shim is not installed." + } + if err != nil { + return statusInvalidShim(path) + } + var state statusShimState + if json.Unmarshal(raw, &state) != nil || state.Platform == "" { + return statusInvalidShim(path) + } + files := state.Wrappers + if len(files) == 0 { + files = []statusShimFile{{state.WrapperPath, state.OriginalPath, state.BackupPath}} + } + lines := make([]string, 0, len(files)) + for _, file := range files { + if file.WrapperPath == "" || file.OriginalPath == "" || file.BackupPath == "" { + return statusInvalidShim(path) + } + wrapper := "missing" + if contents, readErr := os.ReadFile(file.WrapperPath); readErr == nil { + if strings.Contains(string(contents), shimMarker) { + wrapper = "shim present" + } else { + wrapper = "present but not an opencodex shim" + } + } + backup := "missing" + if _, statErr := os.Stat(file.BackupPath); statErr == nil { + backup = "present" + } + lines = append(lines, "Codex autostart shim: wrapper "+wrapper+" at "+file.WrapperPath+"; original backup "+backup+" at "+file.BackupPath+".") + } + return strings.Join(lines, "\n") +} +func statusInvalidShim(path string) string { + return "Codex autostart shim state is invalid or corrupt at " + path + ". Reinstall or remove the shim." +} +func statusCodexPlugins() StatusPluginsDomain { + return StatusPluginsDomain{false, "not_windows", "not applicable (bundled-marketplace staleness is Windows-specific)"} +} + +var statusVersionPattern = regexp.MustCompile("\\b(\\d+\\.\\d+\\.\\d+(?:-[0-9A-Za-z.-]+)?)\\b") + +type statusRuntimeCandidate struct{ command, source string } +type statusPersistedRuntime struct { + Version int `json:"version"` + Command string `json:"command"` + Source string `json:"source"` + SelectedVersion *string `json:"selectedVersion"` + UpdatedAt string `json:"updatedAt"` +} +type statusPersistedClamp struct { + Version int `json:"version"` + RuntimePath string `json:"runtimePath"` + RuntimeVersion *string `json:"runtimeVersion"` + RemovedEfforts []string `json:"removedEfforts"` +} + +func statusCodexRuntime() StatusCodexRuntime { + candidates := statusRuntimeCandidates() + var selected *statusRuntimeCandidate + var selectedVersion *string + for index := range candidates { + if version := statusCodexVersion(candidates[index].command); version != nil { + selected, selectedVersion = &candidates[index], version + break + } + } + if selected == nil { + warning := "No validated Codex runtime found; falling back to `codex`. Run ocx doctor for diagnosis and recovery." + return StatusCodexRuntime{Path: "codex", Source: "fallback", Warning: &warning, CatalogClamp: StatusCatalogClamp{RemovedEfforts: []string{}}} + } + result := StatusCodexRuntime{Path: redactStatusPath(selected.command), Version: selectedVersion, Source: selected.source, CatalogClamp: statusCatalogClamp(*selected, selectedVersion)} + if selected.source == "fallback" && selectedVersion == nil { + warning := "No validated Codex runtime found; falling back to `codex`. Run ocx doctor for diagnosis and recovery." + result.Warning = &warning + } + return result +} + +func statusRuntimeCandidates() []statusRuntimeCandidate { + candidates := []statusRuntimeCandidate{} + if command := strings.TrimSpace(os.Getenv("CODEX_CLI_PATH")); command != "" { + candidates = append(candidates, statusRuntimeCandidate{command, "environment"}) + } + if persisted := statusLoadPersistedRuntime(); persisted != nil { + candidates = append(candidates, statusRuntimeCandidate{persisted.Command, "configured"}) + } + for _, command := range statusShimRuntimeCandidates() { + candidates = append(candidates, statusRuntimeCandidate{command, "shim"}) + } + for _, dir := range filepath.SplitList(os.Getenv("PATH")) { + if dir != "" { + candidates = append(candidates, statusRuntimeCandidate{filepath.Join(dir, "codex"), "path"}) + } + } + return append(candidates, statusRuntimeCandidate{"codex", "fallback"}) +} + +func statusLoadPersistedRuntime() *statusPersistedRuntime { + raw, err := os.ReadFile(filepath.Join(statusConfigDir(), "codex-runtime.json")) + if err != nil { + return nil + } + var state statusPersistedRuntime + if json.Unmarshal(raw, &state) != nil || state.Version != 1 || strings.TrimSpace(state.Command) == "" || state.UpdatedAt == "" { + return nil + } + if state.Source != "environment" && state.Source != "configured" && state.Source != "shim" && state.Source != "path" && state.Source != "fallback" { + return nil + } + return &state +} + +func statusShimRuntimeCandidates() []string { + raw, err := os.ReadFile(filepath.Join(statusConfigDir(), "codex-shim.json")) + if err != nil { + return nil + } + var state statusShimState + if json.Unmarshal(raw, &state) != nil { + return nil + } + files := state.Wrappers + if len(files) == 0 { + files = []statusShimFile{{state.WrapperPath, state.OriginalPath, state.BackupPath}} + } + seen, candidates := map[string]bool{}, []string{} + for _, file := range files { + for _, path := range []string{file.BackupPath, file.OriginalPath, file.WrapperPath} { + if path != "" && !seen[path] { + seen[path] = true + candidates = append(candidates, path) + } + } + } + return candidates +} + +func statusCatalogClamp(selected statusRuntimeCandidate, version *string) StatusCatalogClamp { + clamp := StatusCatalogClamp{RemovedEfforts: []string{}} + raw, err := os.ReadFile(filepath.Join(statusConfigDir(), "codex-runtime-clamp.json")) + if err != nil { + return clamp + } + var persisted statusPersistedClamp + if json.Unmarshal(raw, &persisted) != nil || persisted.Version != 1 || len(persisted.RemovedEfforts) == 0 { + return clamp + } + active := strings.EqualFold(strings.TrimSpace(persisted.RuntimePath), strings.TrimSpace(selected.command)) || (persisted.RuntimeVersion != nil && version != nil && *persisted.RuntimeVersion == *version) + if !active { + return clamp + } + clamp.Active, clamp.RemovedEfforts, clamp.RuntimeVersion = true, append([]string(nil), persisted.RemovedEfforts...), persisted.RuntimeVersion + return clamp +} + +func statusCodexVersion(command string) *string { + if command == "" { + return nil + } + out, err := exec.Command(command, "--version").Output() + if err != nil { + return nil + } + match := statusVersionPattern.FindStringSubmatch(strings.TrimSpace(string(out))) + if len(match) < 2 { + return nil + } + value := match[1] + return &value +} +func statusCodexHome() StatusCodexHome { + home, err := os.UserHomeDir() + if err != nil { + home = "" + } + app := filepath.Join(home, ".codex") + effective := strings.TrimSpace(os.Getenv("CODEX_HOME")) + if effective == "" { + effective = app + } else if resolved, resolveErr := filepath.Abs(effective); resolveErr == nil { + effective = resolved + } + return StatusCodexHome{EffectiveCodexHome: redactStatusPath(effective), AppCodexHome: redactStatusPath(app)} +} +func statusClaudeDesktop(cfg *config.Config) StatusClaudeDesktop { + desired := true + if integrations, ok := cfg.Raw["clientIntegrations"].(map[string]any); ok && integrations["claude-desktop"] == false { + desired = false + } + return StatusClaudeDesktop{desired, StatusClaudeDesktopPolicy{true, "ok", "not_applicable", "Windows managed Claude policy is not applicable on this platform.", "No action required."}} +} +func redactStatusPath(path string) string { + if home, err := os.UserHomeDir(); err == nil && home != "" && strings.HasPrefix(path, home+string(os.PathSeparator)) { + if strings.HasPrefix(home, "/home/") { + return "/home/[USER]" + strings.TrimPrefix(path, home) + } + if strings.HasPrefix(home, "/Users/") { + return "/Users/[USER]" + strings.TrimPrefix(path, home) + } + } + return path +} diff --git a/go/internal/ocxcli/status_domains_external_test.go b/go/internal/ocxcli/status_domains_external_test.go new file mode 100644 index 0000000000..9a4be9b5b8 --- /dev/null +++ b/go/internal/ocxcli/status_domains_external_test.go @@ -0,0 +1,94 @@ +package ocxcli + +import ( + "bytes" + "encoding/json" + "os" + "os/exec" + "path/filepath" + "strings" + "testing" +) + +func runTypeScriptExternalStatusJSON(t *testing.T, home string, env ...string) []byte { + t.Helper() + cmd := exec.Command("bun", "src/cli/index.ts", "status", "--json") + cmd.Dir = typeScriptOracleRepo(t) + clean := make([]string, 0, len(os.Environ())+3+len(env)) + for _, item := range os.Environ() { + if !strings.HasPrefix(item, "HOME=") && !strings.HasPrefix(item, "OPENCODEX_HOME=") && !strings.HasPrefix(item, "CODEX_HOME=") { + clean = append(clean, item) + } + } + cmd.Env = append(clean, append([]string{"HOME=" + home, "OPENCODEX_HOME=" + home, "CODEX_HOME=" + filepath.Join(home, "codex")}, env...)...) + out, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("TypeScript external status oracle: %v: %s", err, out) + } + // A deliberately unhealthy shim makes TypeScript print its recovery warning + // before JSON. The status payload remains the differential contract here. + if start := bytes.IndexByte(out, '{'); start >= 0 { + return out[start:] + } + t.Fatalf("TypeScript external status oracle returned no JSON: %s", out) + return nil +} + +func statusExternalDomainBytes(t *testing.T, full []byte) []byte { + t.Helper() + var compact bytes.Buffer + if err := json.Compact(&compact, full); err != nil { + t.Fatalf("compact external oracle: %v; output=%s", err, full) + } + var status struct { + Service json.RawMessage `json:"service"` + CodexShim json.RawMessage `json:"codexShim"` + CodexPlugins json.RawMessage `json:"codexPlugins"` + CodexRuntime json.RawMessage `json:"codexRuntime"` + CodexHome json.RawMessage `json:"codexHome"` + ClaudeDesktop json.RawMessage `json:"claudeDesktop"` + } + if err := json.Unmarshal(compact.Bytes(), &status); err != nil { + t.Fatal(err) + } + return []byte("{\"service\":" + string(status.Service) + ",\"codexShim\":" + string(status.CodexShim) + ",\"codexPlugins\":" + string(status.CodexPlugins) + ",\"codexRuntime\":" + string(status.CodexRuntime) + ",\"codexHome\":" + string(status.CodexHome) + ",\"claudeDesktop\":" + string(status.ClaudeDesktop) + "}") +} + +func TestStatusExternalDomainsMatchTypeScriptOracle(t *testing.T) { + home := t.TempDir() + if err := os.Mkdir(filepath.Join(home, "codex"), 0o700); err != nil { + t.Fatal(err) + } + runtimePath := filepath.Join(home, "codex-runtime") + if err := os.WriteFile(runtimePath, []byte("#!/bin/sh\nprintf 'codex-cli 999.0.0\\n'\n"), 0o700); err != nil { + t.Fatal(err) + } + wrapperPath := filepath.Join(home, "codex-wrapper") + backupPath := filepath.Join(home, "codex-backup") + if err := os.WriteFile(wrapperPath, []byte("# opencodex codex autostart shim\n"), 0o700); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(backupPath, []byte("original"), 0o700); err != nil { + t.Fatal(err) + } + shim := "{\"platform\":\"linux\",\"wrapperPath\":\"" + wrapperPath + "\",\"originalPath\":\"" + wrapperPath + "\",\"backupPath\":\"" + backupPath + "\"}" + if err := os.WriteFile(filepath.Join(home, "codex-shim.json"), []byte(shim), 0o600); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(home, "config.json"), []byte("{\"clientIntegrations\":{\"claude-desktop\":false}}"), 0o600); err != nil { + t.Fatal(err) + } + t.Setenv("HOME", home) + t.Setenv("OPENCODEX_HOME", home) + t.Setenv("CODEX_HOME", filepath.Join(home, "codex")) + t.Setenv("CODEX_CLI_PATH", runtimePath) + oracle := runTypeScriptExternalStatusJSON(t, home, "CODEX_CLI_PATH="+runtimePath) + want := statusExternalDomainBytes(t, oracle) + got, err := json.Marshal(CollectStatusExternalDomains()) + if err != nil { + t.Fatal(err) + } + if string(got) != string(want) { + t.Fatalf("external domain bytes\\n got: %s\\nwant: %s", got, want) + } +} From a4ac77d5e5c8eb26beed758e98b24841ceb239c6 Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Mon, 7 Sep 2026 02:20:19 +0800 Subject: [PATCH 084/165] feat(go): own config write commands --- go/internal/configschema/schema.go | 4 +- go/internal/configschema/schema_test.go | 10 ++ go/internal/ocxcli/cli.go | 1 + go/internal/ocxcli/cli_test.go | 41 +++++- go/internal/ocxcli/families.go | 188 +++++++++++++++++++++++- 5 files changed, 237 insertions(+), 7 deletions(-) diff --git a/go/internal/configschema/schema.go b/go/internal/configschema/schema.go index a6baf1509f..c3ed450d13 100644 --- a/go/internal/configschema/schema.go +++ b/go/internal/configschema/schema.go @@ -86,8 +86,8 @@ func validateStrictWriteFields(v *value) error { return errors.New("schema_invalid: codexAccountPickerEnabled: Invalid input: expected boolean, received " + zodType(x)) } if x := v.find("visionSidecar"); x != nil && x.kind == objectKind { - if r := x.find("reasoning"); r != nil && (r.kind != stringKind || !map[string]bool{"none": true, "minimal": true, "low": true, "medium": true, "high": true, "xhigh": true, "max": true, "ultra": true}[r.text]) { - return errors.New("schema_invalid: visionSidecar.reasoning: must be one of none, minimal, low, medium, high, xhigh, max, ultra") + if r := x.find("reasoning"); r != nil && (r.kind != stringKind || !map[string]bool{"low": true, "medium": true, "high": true, "xhigh": true, "max": true}[r.text]) { + return errors.New("schema_invalid: visionSidecar.reasoning: must be one of low, medium, high, xhigh, max") } } if err := validateAgentTaskRecovery(v); err != nil { diff --git a/go/internal/configschema/schema_test.go b/go/internal/configschema/schema_test.go index 55780e9dff..584f36fc86 100644 --- a/go/internal/configschema/schema_test.go +++ b/go/internal/configschema/schema_test.go @@ -159,6 +159,16 @@ func TestStrictWriteSchemaRejectsLoadDegradedFields(t *testing.T) { } } +func TestStrictWriteSchemaUsesVisionCommandVocabulary(t *testing.T) { + for _, reasoning := range []string{"none", "minimal", "ultra"} { + _, err := ValidateCandidateJSON([]byte(`{"providers":{},"visionSidecar":{"reasoning":"` + reasoning + `"}}`)) + const want = "schema_invalid: visionSidecar.reasoning: must be one of low, medium, high, xhigh, max" + if err == nil || err.Error() != want { + t.Fatalf("reasoning %q error = %v, want %q", reasoning, err, want) + } + } +} + func TestApplyConfigPathMutationUsesStrictSchemaAndPinHook(t *testing.T) { raw := []byte(`{"providers":{},"activeCodexAccountPinned":"acct-1","codexAccountPriorities":{"acct-1":1}}`) updated, saved, changed, err := ApplyConfigPathMutation(raw, "codexAccountPriorities.acct-1", "2", false) diff --git a/go/internal/ocxcli/cli.go b/go/internal/ocxcli/cli.go index 855d0a874c..caa961a8da 100644 --- a/go/internal/ocxcli/cli.go +++ b/go/internal/ocxcli/cli.go @@ -23,6 +23,7 @@ const ( ExitOK = 0 ExitFailure = 1 ExitUsage = 64 + configWriteUsageExit = 2 attestationChallengeHeader = "x-opencodex-attestation-challenge" attestationProofHeader = "x-opencodex-attestation-proof" ) diff --git a/go/internal/ocxcli/cli_test.go b/go/internal/ocxcli/cli_test.go index bcb9a6bc6f..170334c9dd 100644 --- a/go/internal/ocxcli/cli_test.go +++ b/go/internal/ocxcli/cli_test.go @@ -235,12 +235,12 @@ func TestModelRuntimeOwnershipDelegates(t *testing.T) { } func TestConfigRuntimeOwnershipUsesNativeReadCommands(t *testing.T) { - for _, subcommand := range []string{"show", "validate", "export"} { + for _, subcommand := range []string{"show", "validate", "export", "set", "unset", "import"} { if got, known := OwnershipFor([]string{"config", subcommand}); !known || got != GoOwned { t.Fatalf("OwnershipFor(config %s) = %q, %t", subcommand, got, known) } } - for _, subcommand := range []string{"get", "set", "unset", "import"} { + for _, subcommand := range []string{"get"} { if got, known := OwnershipFor([]string{"config", subcommand}); !known || got != TypeScriptOwned { t.Fatalf("OwnershipFor(config %s) = %q, %t", subcommand, got, known) } @@ -347,7 +347,6 @@ func TestTypeScriptOwnedFamiliesDelegateExactArgumentsAndExitCode(t *testing.T) for _, argv := range [][]string{ {"status", "--json"}, {"doctor", "--json"}, {"service", "restart"}, {"tray", "status"}, - {"config", "set", "port", "10101", "--json"}, } { t.Run(strings.Join(argv, " "), func(t *testing.T) { var received []string @@ -366,6 +365,42 @@ func TestTypeScriptOwnedFamiliesDelegateExactArgumentsAndExitCode(t *testing.T) } } +func TestNativeConfigWriteCommandsMatchOracleShape(t *testing.T) { + dir := t.TempDir() + t.Setenv("OPENCODEX_HOME", dir) + configPath := filepath.Join(dir, "config.json") + initial := []byte(`{"port":10100,"providers":{"fixture":{"adapter":"openai-chat","baseUrl":"https://example.test/v1"}},"defaultProvider":"fixture","autoSwitchThreshold":50}`) + if err := os.WriteFile(configPath, initial, 0o600); err != nil { + t.Fatal(err) + } + + run := func(argv []string) (int, string, string) { + var out, stderr bytes.Buffer + deps := depsFor(RuntimeState{}, &out, &stderr) + deps.Delegate = func([]string) (int, error) { t.Fatal("native config write delegated"); return 0, nil } + return Run(argv, deps), out.String(), stderr.String() + } + if code, out, stderr := run([]string{"config", "set", "autoSwitchThreshold", "70", "--json"}); code != ExitOK || out != "{\n \"ok\": true,\n \"path\": \"autoSwitchThreshold\",\n \"value\": 70\n}\n" || stderr != "" { + t.Fatalf("set = code=%d stdout=%q stderr=%q", code, out, stderr) + } + if code, out, stderr := run([]string{"config", "unset", "autoSwitchThreshold", "--json"}); code != ExitOK || out != "{\n \"ok\": true,\n \"path\": \"autoSwitchThreshold\",\n \"value\": null\n}\n" || stderr != "" { + t.Fatalf("unset = code=%d stdout=%q stderr=%q", code, out, stderr) + } + importPath := filepath.Join(dir, "import.json") + if err := os.WriteFile(importPath, []byte(`{"port":10102,"providers":{"fixture":{"adapter":"openai-chat","baseUrl":"https://example.test/v1"}},"defaultProvider":"fixture"}`), 0o600); err != nil { + t.Fatal(err) + } + if code, out, stderr := run([]string{"config", "import", importPath, "--yes", "--json"}); code != ExitOK || out != "{\n \"ok\": true,\n \"source\": \""+importPath+"\"\n}\n" || stderr != "" { + t.Fatalf("import = code=%d stdout=%q stderr=%q", code, out, stderr) + } + if code, out, stderr := run([]string{"config", "set", "port", "-1", "--json"}); code != 2 || out != "" || stderr != "Error: schema_invalid: port: Too small: expected number to be >=0\n" { + t.Fatalf("invalid set = code=%d stdout=%q stderr=%q", code, out, stderr) + } + if code, out, stderr := run([]string{"config", "import", importPath, "--json"}); code != 2 || out != "" || stderr != "Error: import requires --yes\n"+configUsage { + t.Fatalf("unconfirmed import = code=%d stdout=%q stderr=%q", code, out, stderr) + } +} + func TestConfigHelpIsNative(t *testing.T) { var out, stderr bytes.Buffer deps := depsFor(RuntimeState{}, &out, &stderr) diff --git a/go/internal/ocxcli/families.go b/go/internal/ocxcli/families.go index 00c64e7dde..61a4e0557d 100644 --- a/go/internal/ocxcli/families.go +++ b/go/internal/ocxcli/families.go @@ -2,6 +2,7 @@ package ocxcli import ( "bytes" + "context" "crypto/rand" "encoding/json" "errors" @@ -36,6 +37,7 @@ var modelRuntimeSubcommands = map[string]Ownership{ // config-mutation.sqlite generation transaction. var configRuntimeSubcommands = map[string]Ownership{ "show": GoOwned, "validate": GoOwned, "export": GoOwned, + "set": GoOwned, "unset": GoOwned, "import": GoOwned, } func loadCLIConfig() (map[string]any, error) { @@ -49,7 +51,7 @@ func loadCLIConfig() (map[string]any, error) { func runConfig(args []string, deps Deps) int { action := "show" if len(args) > 0 && args[0] != "--json" && args[0] != "--source" { - action, args = args[0], args[1:] + action, args = strings.ToLower(args[0]), args[1:] } switch action { case "show": @@ -58,11 +60,193 @@ func runConfig(args []string, deps Deps) int { return runNativeConfigValidate(args, deps) case "export": return runNativeConfigExport(args, deps) + case "set", "unset": + return runNativeConfigSet(args, action == "unset", deps) + case "import": + return runNativeConfigImport(args, deps) default: - return runDelegated(append([]string{"config", action}, args...), deps) + return configWriteUsageError(deps, "unknown config command "+action) } } +func configWriteError(deps Deps, message string, usage bool) int { + fmt.Fprintf(deps.Stderr, "Error: %s\n", message) + if usage { + fmt.Fprint(deps.Stderr, configUsage) + return configWriteUsageExit + } + return ExitFailure +} + +func configWriteUsageError(deps Deps, message string) int { + return configWriteError(deps, message, true) +} + +func configWriteValidationError(deps Deps, message string) int { + fmt.Fprintf(deps.Stderr, "Error: %s\n", message) + return configWriteUsageExit +} + +func runNativeConfigSet(args []string, remove bool, deps Deps) int { + jsonOutput := takeFlag(&args, "--json") + if len(args) == 0 || (!remove && len(args) == 1) { + return configWriteUsageError(deps, "config path and value are required") + } + path := args[0] + args = args[1:] + rawValue := "" + if !remove { + rawValue = args[0] + args = args[1:] + } + if len(args) != 0 { + return configWriteUsageError(deps, "Unexpected argument(s): "+strings.Join(args, " ")) + } + configPath, err := config.Path() + if err != nil { + return configWriteError(deps, err.Error(), false) + } + if _, err := os.Stat(configPath); err != nil { + if errors.Is(err, os.ErrNotExist) { + return configWriteError(deps, "config is missing", false) + } + return configWriteError(deps, err.Error(), false) + } + var saved *configschema.Normalized + _, err = configschema.WithRevalidatedConfigMutation(context.Background(), configPath, nil, func(raw []byte, _ int64) ([]byte, bool, error) { + updated, value, changed, mutationErr := configschema.ApplyConfigPathMutation(raw, path, rawValue, remove) + if mutationErr != nil { + return nil, false, mutationErr + } + if !remove { + _ = value // ApplyConfigPathMutation verifies the saved path; display must redact it. + display, displayErr := updated.ConfigPathValue(path) + if displayErr != nil { + return nil, false, displayErr + } + saved = display + } + data, encodeErr := updated.IndentedJSON() + if encodeErr != nil { + return nil, false, encodeErr + } + return append(data, '\n'), changed, nil + }) + if err != nil { + return reportNativeConfigWriteError(deps, err) + } + value := json.RawMessage("null") + if !remove && saved != nil { + data, encodeErr := saved.CompactJSON() + if encodeErr != nil { + return configWriteError(deps, encodeErr.Error(), false) + } + value = data + } + if jsonOutput { + return writeNativeConfigJSON(deps.Stdout, struct { + OK bool `json:"ok"` + Path string `json:"path"` + Value json.RawMessage `json:"value"` + }{true, path, value}) + } + if remove { + fmt.Fprintf(deps.Stdout, "Unset %s.\n", path) + } else { + fmt.Fprintf(deps.Stdout, "Set %s.\n", path) + } + return ExitOK +} + +func reportNativeConfigWriteError(deps Deps, err error) int { + if errors.Is(err, configschema.ErrRawByteConflict) { + return configWriteError(deps, "config changed while applying this update; retry", false) + } + if errors.Is(err, configschema.ErrMutationBusy) { + return configWriteError(deps, "Config mutation already in progress", false) + } + if errors.Is(err, os.ErrNotExist) { + return configWriteError(deps, "config is missing", false) + } + message := err.Error() + if strings.HasPrefix(message, "invalid JSON:") { + return configWriteError(deps, "config is invalid", false) + } + if message == "invalid config path" || strings.HasPrefix(message, "config parent path not found:") || strings.HasPrefix(message, "config path not found:") { + return configWriteUsageError(deps, message) + } + if strings.HasPrefix(message, "schema_invalid:") { + return configWriteValidationError(deps, message) + } + return configWriteError(deps, message, false) +} + +func runNativeConfigImport(args []string, deps Deps) int { + jsonOutput := takeFlag(&args, "--json") + if len(args) == 0 { + return configWriteUsageError(deps, "import path is required") + } + path := args[0] + args = args[1:] + yes := takeFlag(&args, "--yes") + if !yes { + return configWriteUsageError(deps, "import requires --yes") + } + if len(args) != 0 { + return configWriteUsageError(deps, "Unexpected argument(s): "+strings.Join(args, " ")) + } + raw, err := readConfigInputBytes(path) + if err != nil { + if strings.HasPrefix(err.Error(), "invalid JSON in ") { + return configWriteValidationError(deps, err.Error()) + } + return configWriteError(deps, err.Error(), false) + } + candidate, err := configschema.ValidateCandidateJSON(raw) + if err != nil { + return configWriteValidationError(deps, err.Error()) + } + configPath, err := config.Path() + if err != nil { + return configWriteError(deps, err.Error(), false) + } + if _, err := configschema.ReplaceConfigCandidate(context.Background(), configPath, candidate); err != nil { + if errors.Is(err, configschema.ErrMutationBusy) { + return configWriteError(deps, "Config mutation already in progress", false) + } + return configWriteError(deps, err.Error(), false) + } + if jsonOutput { + return writeNativeConfigJSON(deps.Stdout, struct { + OK bool `json:"ok"` + Source string `json:"source"` + }{true, path}) + } + fmt.Fprintf(deps.Stdout, "Imported config from %s. Restart or run ocx sync if needed.\n", path) + return ExitOK +} + +func readConfigInputBytes(path string) ([]byte, error) { + var raw []byte + var err error + if path == "-" { + raw, err = io.ReadAll(os.Stdin) + } else { + raw, err = os.ReadFile(path) + } + if err != nil { + if errors.Is(err, os.ErrNotExist) { + return nil, fmt.Errorf("ENOENT: no such file or directory, open %q", path) + } + return nil, err + } + var parsed any + if err := json.Unmarshal(raw, &parsed); err != nil { + return nil, fmt.Errorf("invalid JSON in %s", path) + } + return raw, nil +} + type configSourceOutput struct { Config json.RawMessage `json:"config"` Source string `json:"source"` From 6c78115b3c916f12a32825ea1a3f0c2a958af5f9 Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Mon, 7 Sep 2026 02:21:48 +0800 Subject: [PATCH 085/165] test(go): diff native config write commands --- tests/go-cli-parity.test.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/tests/go-cli-parity.test.ts b/tests/go-cli-parity.test.ts index a89612f78a..5f6f5c424b 100644 --- a/tests/go-cli-parity.test.ts +++ b/tests/go-cli-parity.test.ts @@ -111,11 +111,12 @@ describe.skipIf(!goAvailable || goCLI === null)("Go CLI parity (ADR-0008, ticket })); expectParity(args); }); - test("diffs every config mutation, validation, and export path through the shared owner", () => { + test("diffs native config writes from argument parsing through persistence", () => { const home = mkdtempSync(join(tmpdir(), "ocx-go-config-parity-")); const configPath = join(home, "config.json"); const exportPath = join(home, "export.json"); const importPath = join(home, "import.json"); + const invalidImportPath = join(home, "invalid-import.json"); const initial = { port: 10100, providers: { fixture: { adapter: "openai-chat", baseUrl: "https://example.test/v1", apiKey: "secret-key" } }, @@ -132,6 +133,7 @@ describe.skipIf(!goAvailable || goCLI === null)("Go CLI parity (ADR-0008, ticket }; try { writeFileSync(importPath, JSON.stringify({ ...initial, port: 10102 })); + writeFileSync(invalidImportPath, "{not-json"); parity(["config", "show", "--source"]); parity(["config", "set", "autoSwitchThreshold", "70", "--json"]); parity(["config", "set", "port", "-1", "--json"]); @@ -142,6 +144,12 @@ describe.skipIf(!goAvailable || goCLI === null)("Go CLI parity (ADR-0008, ticket parity(["config", "export", exportPath]); parity(["config", "import", importPath, "--yes", "--json"]); parity(["config", "import", importPath, "--json"]); + parity(["config", "set"]); + parity(["config", "set", "constructor", "true", "--json"]); + parity(["config", "set", "missing.child", "true", "--json"]); + parity(["config", "unset", "missing", "--json"]); + parity(["config", "import"]); + parity(["config", "import", invalidImportPath, "--yes", "--json"]); } finally { removeTreeWithRetry(home); } From 0fa7e6aa70ae2b8c1e0ba4ed0f5741512fd84208 Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Mon, 7 Sep 2026 02:23:00 +0800 Subject: [PATCH 086/165] fix(go): match config unset error output --- go/internal/ocxcli/cli_test.go | 3 +++ go/internal/ocxcli/families.go | 5 ++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/go/internal/ocxcli/cli_test.go b/go/internal/ocxcli/cli_test.go index 170334c9dd..879fd2e1b3 100644 --- a/go/internal/ocxcli/cli_test.go +++ b/go/internal/ocxcli/cli_test.go @@ -399,6 +399,9 @@ func TestNativeConfigWriteCommandsMatchOracleShape(t *testing.T) { if code, out, stderr := run([]string{"config", "import", importPath, "--json"}); code != 2 || out != "" || stderr != "Error: import requires --yes\n"+configUsage { t.Fatalf("unconfirmed import = code=%d stdout=%q stderr=%q", code, out, stderr) } + if code, out, stderr := run([]string{"config", "unset", "missing", "--json"}); code != 2 || out != "" || stderr != "Error: config path not found: missing\n" { + t.Fatalf("missing unset = code=%d stdout=%q stderr=%q", code, out, stderr) + } } func TestConfigHelpIsNative(t *testing.T) { diff --git a/go/internal/ocxcli/families.go b/go/internal/ocxcli/families.go index 61a4e0557d..5a5a69af29 100644 --- a/go/internal/ocxcli/families.go +++ b/go/internal/ocxcli/families.go @@ -172,9 +172,12 @@ func reportNativeConfigWriteError(deps Deps, err error) int { if strings.HasPrefix(message, "invalid JSON:") { return configWriteError(deps, "config is invalid", false) } - if message == "invalid config path" || strings.HasPrefix(message, "config parent path not found:") || strings.HasPrefix(message, "config path not found:") { + if message == "invalid config path" || strings.HasPrefix(message, "config parent path not found:") { return configWriteUsageError(deps, message) } + if strings.HasPrefix(message, "config path not found:") { + return configWriteValidationError(deps, message) + } if strings.HasPrefix(message, "schema_invalid:") { return configWriteValidationError(deps, message) } From 5fb21cb595d104017509985c6fa699391fe62127 Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Mon, 7 Sep 2026 02:38:24 +0800 Subject: [PATCH 087/165] feat(go): own config get command Port the last config subcommand to native dispatch: ordered path projection through ConfigPathValue with TS String(value) semantics for bare scalars, JSON output for objects and explicit --json, and the runCliAction exit-2 contract for missing paths. --- go/internal/ocxcli/cli.go | 5 ++-- go/internal/ocxcli/cli_test.go | 39 +++++++++++++++++++++---- go/internal/ocxcli/families.go | 52 ++++++++++++++++++++++++++++++++-- 3 files changed, 84 insertions(+), 12 deletions(-) diff --git a/go/internal/ocxcli/cli.go b/go/internal/ocxcli/cli.go index caa961a8da..04b69bd67e 100644 --- a/go/internal/ocxcli/cli.go +++ b/go/internal/ocxcli/cli.go @@ -93,9 +93,8 @@ var Commands = []Command{ {Name: "integration", Usage: "ocx integration client ", Summary: "Manage integrations.", Owner: TypeScriptOwned}, {Name: "grok", Usage: "ocx grok ", Summary: "Manage Grok Build.", Owner: TypeScriptOwned}, {Name: "system", Usage: "ocx system ", Summary: "Manage runtime settings.", Owner: TypeScriptOwned}, - // The command default and read subset are Go-owned. Mutations remain - // TypeScript-owned by configRuntimeSubcommands until they share its SQLite - // generation transaction. + // The full config family is Go-owned: reads project through the schema + // normalizer and writes share the SQLite generation transaction. {Name: "config", Usage: "ocx config ", Summary: "Manage configuration.", Owner: GoOwned}, {Name: "lab", Usage: "ocx lab ", Summary: "Inspect Compatibility Lab.", Owner: TypeScriptOwned}, {Name: "claude", Usage: "ocx claude [args...]", Summary: "Launch Claude Code.", Owner: TypeScriptOwned}, diff --git a/go/internal/ocxcli/cli_test.go b/go/internal/ocxcli/cli_test.go index 879fd2e1b3..fbd3107540 100644 --- a/go/internal/ocxcli/cli_test.go +++ b/go/internal/ocxcli/cli_test.go @@ -235,16 +235,11 @@ func TestModelRuntimeOwnershipDelegates(t *testing.T) { } func TestConfigRuntimeOwnershipUsesNativeReadCommands(t *testing.T) { - for _, subcommand := range []string{"show", "validate", "export", "set", "unset", "import"} { + for _, subcommand := range []string{"show", "get", "validate", "export", "set", "unset", "import"} { if got, known := OwnershipFor([]string{"config", subcommand}); !known || got != GoOwned { t.Fatalf("OwnershipFor(config %s) = %q, %t", subcommand, got, known) } } - for _, subcommand := range []string{"get"} { - if got, known := OwnershipFor([]string{"config", subcommand}); !known || got != TypeScriptOwned { - t.Fatalf("OwnershipFor(config %s) = %q, %t", subcommand, got, known) - } - } } func TestNativeConfigReadCommandsMatchOracleShape(t *testing.T) { @@ -404,6 +399,38 @@ func TestNativeConfigWriteCommandsMatchOracleShape(t *testing.T) { } } +func TestNativeConfigGetMatchesOracleShape(t *testing.T) { + dir := t.TempDir() + t.Setenv("OPENCODEX_HOME", dir) + configPath := filepath.Join(dir, "config.json") + initial := []byte(`{"port":10100,"providers":{"fixture":{"adapter":"openai-chat","baseUrl":"https://example.test/v1","apiKey":"secret-key","defaultModel":"m1","models":["m1","m2"],"contextWindow":128000}},"defaultProvider":"fixture"}`) + if err := os.WriteFile(configPath, initial, 0o600); err != nil { + t.Fatal(err) + } + + run := func(argv []string) (int, string, string) { + var out, stderr bytes.Buffer + deps := depsFor(RuntimeState{}, &out, &stderr) + deps.Delegate = func([]string) (int, error) { t.Fatal("native config get delegated"); return 0, nil } + return Run(argv, deps), out.String(), stderr.String() + } + if code, out, stderr := run([]string{"config", "get", "providers.fixture"}); code != ExitOK || out != "{\n \"adapter\": \"openai-chat\",\n \"baseUrl\": \"https://example.test/v1\",\n \"apiKey\": \"********\",\n \"defaultModel\": \"m1\",\n \"models\": [\n \"m1\",\n \"m2\"\n ],\n \"contextWindow\": 128000\n}\n" || stderr != "" { + t.Fatalf("object get = code=%d stdout=%q stderr=%q", code, out, stderr) + } + if code, out, stderr := run([]string{"config", "get", "defaultProvider", "--json"}); code != ExitOK || out != "\"fixture\"\n" || stderr != "" { + t.Fatalf("scalar get = code=%d stdout=%q stderr=%q", code, out, stderr) + } + if code, out, stderr := run([]string{"config", "get", "providers.fixture.apiKey", "--json"}); code != ExitOK || out != "\"********\"\n" || stderr != "" { + t.Fatalf("secret get = code=%d stdout=%q stderr=%q", code, out, stderr) + } + if code, out, stderr := run([]string{"config", "get", "does.not.exist"}); code != 2 || out != "" || stderr != "Error: config path not found: does.not.exist\n" { + t.Fatalf("missing get = code=%d stdout=%q stderr=%q", code, out, stderr) + } + if code, out, stderr := run([]string{"config", "get"}); code != 2 || out != "" || stderr != "Error: config path is required\n"+configUsage { + t.Fatalf("no-path get = code=%d stdout=%q stderr=%q", code, out, stderr) + } +} + func TestConfigHelpIsNative(t *testing.T) { var out, stderr bytes.Buffer deps := depsFor(RuntimeState{}, &out, &stderr) diff --git a/go/internal/ocxcli/families.go b/go/internal/ocxcli/families.go index 5a5a69af29..b7cf531ee7 100644 --- a/go/internal/ocxcli/families.go +++ b/go/internal/ocxcli/families.go @@ -33,11 +33,12 @@ var modelRuntimeSubcommands = map[string]Ownership{ "context": TypeScriptOwned, "shadow": TypeScriptOwned, } -// Writes retain the TypeScript owner until Go participates in the shared -// config-mutation.sqlite generation transaction. +// The full config command family is Go-owned: reads project through the +// schema/normalizer and writes go through the shared config-mutation.sqlite +// generation coordinator. var configRuntimeSubcommands = map[string]Ownership{ "show": GoOwned, "validate": GoOwned, "export": GoOwned, - "set": GoOwned, "unset": GoOwned, "import": GoOwned, + "get": GoOwned, "set": GoOwned, "unset": GoOwned, "import": GoOwned, } func loadCLIConfig() (map[string]any, error) { @@ -56,6 +57,8 @@ func runConfig(args []string, deps Deps) int { switch action { case "show": return runNativeConfigShow(args, deps) + case "get": + return runNativeConfigGet(args, deps) case "validate": return runNativeConfigValidate(args, deps) case "export": @@ -82,6 +85,49 @@ func configWriteUsageError(deps Deps, message string) int { return configWriteError(deps, message, true) } +func runNativeConfigGet(args []string, deps Deps) int { + jsonOutput := takeFlag(&args, "--json") + if len(args) == 0 { + return configWriteUsageError(deps, "config path is required") + } + path := args[0] + args = args[1:] + if len(args) != 0 { + return configWriteUsageError(deps, "Unexpected argument(s): "+strings.Join(args, " ")) + } + normalized, _, _, err := readNativeConfig() + if err != nil { + return runDelegated([]string{"config", "get", path}, deps) + } + value, err := normalized.ConfigPathValue(path) + if err != nil { + return configWriteValidationError(deps, err.Error()) + } + compact, err := value.CompactJSON() + if err != nil { + fmt.Fprintln(deps.Stderr, err) + return ExitFailure + } + // TypeScript prints JSON for objects and for explicit --json; bare scalars + // go through String(value). + if jsonOutput || compact[0] == '{' || compact[0] == '[' { + data, err := value.IndentedJSON() + if err != nil { + fmt.Fprintln(deps.Stderr, err) + return ExitFailure + } + _, _ = deps.Stdout.Write(append(data, '\n')) + return ExitOK + } + var scalar any + if err := json.Unmarshal(compact, &scalar); err != nil { + fmt.Fprintln(deps.Stderr, err) + return ExitFailure + } + fmt.Fprintln(deps.Stdout, scalar) + return ExitOK +} + func configWriteValidationError(deps Deps, message string) int { fmt.Fprintf(deps.Stderr, "Error: %s\n", message) return configWriteUsageExit From f194206a415651b493579c3be495dcab5b587a28 Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Mon, 7 Sep 2026 02:42:37 +0800 Subject: [PATCH 088/165] feat(go): port doctor path and proxy probes --- go/internal/ocxcli/doctor_probes.go | 242 +++++++++++++++++++++++ go/internal/ocxcli/doctor_probes_test.go | 126 ++++++++++++ 2 files changed, 368 insertions(+) create mode 100644 go/internal/ocxcli/doctor_probes.go create mode 100644 go/internal/ocxcli/doctor_probes_test.go diff --git a/go/internal/ocxcli/doctor_probes.go b/go/internal/ocxcli/doctor_probes.go new file mode 100644 index 0000000000..0460de6145 --- /dev/null +++ b/go/internal/ocxcli/doctor_probes.go @@ -0,0 +1,242 @@ +package ocxcli + +import ( + "os" + "path/filepath" + "runtime" + "strconv" + "strings" + + "github.com/lidge-jun/opencodex/go/internal/config" +) + +// DoctorPathRow is one filesystem location reported by ocx doctor. +type DoctorPathRow struct { + Label string + Path string + Exists bool +} + +// CollectDoctorPaths mirrors collectPaths in src/cli/doctor.ts. It is +// side-effect free so it can be adopted when the command ownership switches. +func CollectDoctorPaths() []DoctorPathRow { + codexHome := doctorCodexHome() + opencodexHome, err := config.Dir() + if err != nil { + opencodexHome = "" + } + configPath := filepath.Join(opencodexHome, "config.json") + return []DoctorPathRow{ + {Label: "CODEX_HOME", Path: codexHome, Exists: doctorPathExists(codexHome)}, + {Label: "CODEX_HOME/auth.json", Path: filepath.Join(codexHome, "auth.json"), Exists: doctorPathExists(filepath.Join(codexHome, "auth.json"))}, + {Label: "OPENCODEX_HOME", Path: opencodexHome, Exists: doctorPathExists(opencodexHome)}, + {Label: "OPENCODEX_HOME/config.json", Path: configPath, Exists: doctorPathExists(configPath)}, + } +} + +func doctorPathExists(path string) bool { _, err := os.Stat(path); return path != "" && err == nil } + +func doctorCodexHome() string { + if raw := strings.TrimSpace(os.Getenv("CODEX_HOME")); raw != "" { + return filepath.Clean(doctorExpandUserPath(raw)) + } + home, err := os.UserHomeDir() + if err != nil { + return ".codex" + } + return filepath.Join(home, ".codex") +} + +func doctorExpandUserPath(raw string) string { + if raw != "~" && !strings.HasPrefix(raw, "~/") && !strings.HasPrefix(raw, "~\\") { + return raw + } + home, err := os.UserHomeDir() + if err != nil { + return raw + } + if raw == "~" { + return home + } + return filepath.Join(home, raw[2:]) +} + +// DoctorFilesystem is the longest mount match for a path. +type DoctorFilesystem struct { + Type, Mount string + IsDrvfs, IsMntDrive bool +} + +// DetectDoctorFilesystem mirrors detectFsType. Empty mount content means +// TypeScript's null mount source, producing n/a rather than unknown. +func DetectDoctorFilesystem(path, mountsContent string) DoctorFilesystem { + isMntDrive := doctorMntDrive(path) + if mountsContent == "" { + return DoctorFilesystem{Type: "n/a", IsMntDrive: isMntDrive} + } + bestMount, bestType := "", "" + for _, line := range strings.Split(mountsContent, "\n") { + parts := strings.Fields(line) + if len(parts) < 3 { + continue + } + mount, fsType := parts[1], parts[2] + if path == mount || strings.HasPrefix(path, strings.TrimSuffix(mount, "/")+"/") || mount == "/" { + if len(mount) > len(bestMount) { + bestMount, bestType = mount, fsType + } + } + } + if bestType == "" { + bestType = "unknown" + } + return DoctorFilesystem{Type: bestType, Mount: bestMount, IsDrvfs: bestType == "drvfs" || bestType == "9p", IsMntDrive: isMntDrive} +} + +func doctorMntDrive(path string) bool { + if !strings.HasPrefix(strings.ToLower(path), "/mnt/") || len(path) < 6 { + return false + } + drive := path[5] + return ((drive >= 'a' && drive <= 'z') || (drive >= 'A' && drive <= 'Z')) && (len(path) == 6 || path[6] == '/') +} + +func ReadDoctorMounts() string { + if runtime.GOOS != "linux" { + return "" + } + content, err := os.ReadFile("/proc/mounts") + if err != nil { + return "" + } + return string(content) +} + +// FormatDoctorPaths has the same report text as the Paths loop in runDoctor. +func FormatDoctorPaths(rows []DoctorPathRow, mounts string) []string { + lines := []string{"Paths"} + for _, row := range rows { + fs := DetectDoctorFilesystem(row.Path, mounts) + flags := []string{} + if fs.Type != "n/a" { + flags = append(flags, "fs="+fs.Type) + } + if fs.IsDrvfs || fs.IsMntDrive { + flags = append(flags, "WSL /mnt drive") + } + state := "-- " + if row.Exists { + state = "ok " + } + line := " " + state + row.Label + ": " + row.Path + if len(flags) > 0 { + line += " (" + strings.Join(flags, ", ") + ")" + } + lines = append(lines, line) + } + return lines +} + +var doctorProxyEnvKeys = []string{"HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY", "NO_PROXY"} + +// DoctorProxyEnvRow omits values because proxy URLs may embed credentials. +type DoctorProxyEnvRow struct { + Key string + Present bool +} + +// CollectDoctorProxyEnv mirrors collectProxyEnv/proxyEnvPresent. +func CollectDoctorProxyEnv(env map[string]string) []DoctorProxyEnvRow { + rows := make([]DoctorProxyEnvRow, 0, len(doctorProxyEnvKeys)) + for _, key := range doctorProxyEnvKeys { + present := strings.TrimSpace(env[key]) != "" || strings.TrimSpace(env[strings.ToLower(key)]) != "" + rows = append(rows, DoctorProxyEnvRow{Key: key, Present: present}) + } + return rows +} + +func doctorProcessEnv() map[string]string { + env := map[string]string{} + for _, entry := range os.Environ() { + if key, value, ok := strings.Cut(entry, "="); ok { + env[key] = value + } + } + return env +} +func CollectCurrentDoctorProxyEnv() []DoctorProxyEnvRow { + return CollectDoctorProxyEnv(doctorProcessEnv()) +} + +// ParseDoctorProcessEnvironment parses Linux /proc//environ content. +func ParseDoctorProcessEnvironment(content string) map[string]string { + env := map[string]string{} + for _, entry := range strings.Split(content, "\x00") { + if key, value, ok := strings.Cut(entry, "="); ok && key != "" { + env[key] = value + } + } + return env +} + +type DoctorRunningProxyEnv struct { + Status string + PID int + Reason string + Rows []DoctorProxyEnvRow +} + +// CollectDoctorRunningProxyEnv mirrors the Linux/local part of +// collectRunningProxyEnv. The reader seam makes unavailable process access +// explicit and avoids emitting any raw environment value. +func CollectDoctorRunningProxyEnv(pid int, procReader func(int) (string, error)) DoctorRunningProxyEnv { + empty := CollectDoctorProxyEnv(map[string]string{}) + if pid == 0 { + return DoctorRunningProxyEnv{Status: "not_running", Rows: empty} + } + if runtime.GOOS != "linux" && procReader == nil { + return DoctorRunningProxyEnv{Status: "unavailable", PID: pid, Reason: "process env inspection is only supported on Linux", Rows: empty} + } + if procReader == nil { + procReader = func(value int) (string, error) { + content, err := os.ReadFile(filepath.Join("/proc", strconv.Itoa(value), "environ")) + return string(content), err + } + } + content, err := procReader(pid) + if err != nil { + return DoctorRunningProxyEnv{Status: "unavailable", PID: pid, Reason: "could not read process environment", Rows: empty} + } + return DoctorRunningProxyEnv{Status: "ok", PID: pid, Rows: CollectDoctorProxyEnv(ParseDoctorProcessEnvironment(content))} +} + +func FormatDoctorCurrentProxyEnv(rows []DoctorProxyEnvRow) []string { + lines := []string{"Current doctor process proxy env (presence only)"} + for _, row := range rows { + state := "unset " + if row.Present { + state = "set " + } + lines = append(lines, " "+state+row.Key) + } + return lines +} + +func FormatDoctorRunningProxyEnv(report DoctorRunningProxyEnv) []string { + lines := []string{"Running proxy process proxy env (presence only)"} + if report.Status == "not_running" { + return append(lines, " -- no running ocx proxy process found") + } + if report.Status == "unavailable" { + return append(lines, " -- pid "+strconv.Itoa(report.PID)+": "+report.Reason) + } + lines = append(lines, " ok pid "+strconv.Itoa(report.PID)) + for _, row := range report.Rows { + state := "unset " + if row.Present { + state = "set " + } + lines = append(lines, " "+state+row.Key) + } + return lines +} diff --git a/go/internal/ocxcli/doctor_probes_test.go b/go/internal/ocxcli/doctor_probes_test.go new file mode 100644 index 0000000000..b141925b6f --- /dev/null +++ b/go/internal/ocxcli/doctor_probes_test.go @@ -0,0 +1,126 @@ +package ocxcli + +import ( + "errors" + "os" + "os/exec" + "path/filepath" + "strings" + "testing" +) + +func TestDoctorPathAndProxyProbeFragmentsMatchTypeScriptDoctor(t *testing.T) { + // The command remains TypeScript-owned. Run its real text-mode command in a + // hermetic home and compare only the Go-portable report fragments byte for + // byte; unrelated live, OAuth, and network probes remain outside this slice. + home := t.TempDir() + codexHome := filepath.Join(home, "codex") + if err := os.Mkdir(codexHome, 0o700); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(codexHome, "auth.json"), []byte("{}"), 0o600); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(home, "config.json"), []byte("{}"), 0o600); err != nil { + t.Fatal(err) + } + t.Setenv("HOME", home) + t.Setenv("OPENCODEX_HOME", home) + t.Setenv("CODEX_HOME", codexHome) + t.Setenv("HTTP_PROXY", "") + t.Setenv("http_proxy", "") + t.Setenv("HTTPS_PROXY", "") + t.Setenv("https_proxy", "") + t.Setenv("ALL_PROXY", "") + t.Setenv("all_proxy", "") + t.Setenv("NO_PROXY", "") + t.Setenv("no_proxy", "") + + output, exitCode := runTypeScriptDoctor(t, home, codexHome) + if exitCode != 0 { + t.Fatalf("TypeScript doctor exit = %d; output=%s", exitCode, output) + } + + paths := strings.Join(FormatDoctorPaths(CollectDoctorPaths(), ReadDoctorMounts()), "\n") + if got, want := doctorSection(output, "Paths", "Response-state temp files"), paths; got != want { + t.Fatalf("Paths bytes\n got: %q\nwant: %q", got, want) + } + current := strings.Join(FormatDoctorCurrentProxyEnv(CollectCurrentDoctorProxyEnv()), "\n") + if got, want := doctorSection(output, "Current doctor process proxy env (presence only)", "Configured proxy (value hidden)"), current; got != want { + t.Fatalf("current proxy environment bytes\n got: %q\nwant: %q", got, want) + } + running := strings.Join(FormatDoctorRunningProxyEnv(CollectDoctorRunningProxyEnv(0, nil)), "\n") + if got, want := doctorSection(output, "Running proxy process proxy env (presence only)", "Memory / runtime"), running; got != want { + t.Fatalf("running proxy environment bytes\n got: %q\nwant: %q", got, want) + } +} + +func runTypeScriptDoctor(t *testing.T, home, codexHome string) (string, int) { + t.Helper() + repo := typeScriptOracleRepo(t) + cmd := exec.Command("bun", "src/cli/index.ts", "doctor") + cmd.Dir = repo + clean := make([]string, 0, len(os.Environ())+4) + for _, entry := range os.Environ() { + key, _, _ := strings.Cut(entry, "=") + if key == "HOME" || key == "OPENCODEX_HOME" || key == "CODEX_HOME" || + strings.EqualFold(key, "HTTP_PROXY") || strings.EqualFold(key, "HTTPS_PROXY") || + strings.EqualFold(key, "ALL_PROXY") || strings.EqualFold(key, "NO_PROXY") { + continue + } + clean = append(clean, entry) + } + cmd.Env = append(clean, "HOME="+home, "OPENCODEX_HOME="+home, "CODEX_HOME="+codexHome) + out, err := cmd.CombinedOutput() + if err == nil { + return string(out), 0 + } + var exitError *exec.ExitError + if errors.As(err, &exitError) { + return string(out), exitError.ExitCode() + } + t.Fatalf("TypeScript doctor oracle: %v: %s", err, out) + return "", -1 +} + +func doctorSection(output, heading, nextHeading string) string { + start := "\n" + heading + "\n" + index := strings.Index(output, start) + if index < 0 { + if strings.HasPrefix(output, heading+"\n") { + index = 0 + } else { + return "" + } + } else { + index++ + } + end := "\n\n" + nextHeading + "\n" + rest := output[index:] + endIndex := strings.Index(rest, end) + if endIndex < 0 { + return rest + } + return rest[:endIndex] +} + +func TestDoctorProbePureContracts(t *testing.T) { + fs := DetectDoctorFilesystem("/mnt/c/Users/a/.codex", "none / overlay rw 0 0\nC: /mnt/c drvfs rw 0 0\n") + if fs.Type != "drvfs" || fs.Mount != "/mnt/c" || !fs.IsDrvfs || !fs.IsMntDrive { + t.Fatalf("filesystem = %#v", fs) + } + if got := DetectDoctorFilesystem("/x", ""); got.Type != "n/a" || got.Mount != "" { + t.Fatalf("empty mounts = %#v", got) + } + rows := CollectDoctorProxyEnv(map[string]string{"http_proxy": " http://secret.example "}) + if !rows[0].Present || rows[1].Present { + t.Fatalf("proxy rows = %#v", rows) + } + parsed := ParseDoctorProcessEnvironment("HTTP_PROXY=secret\x00NO_PROXY=localhost\x00broken\x00") + if parsed["HTTP_PROXY"] != "secret" || parsed["NO_PROXY"] != "localhost" { + t.Fatalf("parsed env = %#v", parsed) + } + if got := CollectDoctorRunningProxyEnv(7, func(int) (string, error) { return "", errors.New("denied") }); got.Status != "unavailable" || got.Reason != "could not read process environment" { + t.Fatalf("unavailable = %#v", got) + } +} From a726d763066f71b113fc1da16eed5c8879ade5cf Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Mon, 7 Sep 2026 02:51:10 +0800 Subject: [PATCH 089/165] test(go): assemble status JSON oracle --- go/internal/ocxcli/status_command.go | 45 ++++++++ go/internal/ocxcli/status_command_test.go | 129 ++++++++++++++++++++++ 2 files changed, 174 insertions(+) create mode 100644 go/internal/ocxcli/status_command.go create mode 100644 go/internal/ocxcli/status_command_test.go diff --git a/go/internal/ocxcli/status_command.go b/go/internal/ocxcli/status_command.go new file mode 100644 index 0000000000..c9ed9e41d4 --- /dev/null +++ b/go/internal/ocxcli/status_command.go @@ -0,0 +1,45 @@ +package ocxcli + +// StatusCommandJSON is the ordered top-level JSON projection produced by the +// native status assembler. It matches collectStatus() in src/cli/status.ts. +type StatusCommandJSON struct { + SchemaVersion int `json:"schemaVersion"` + Proxy StatusProxyDomain `json:"proxy"` + Dashboard StatusDashboardDomain `json:"dashboard"` + Listen StatusListenDomain `json:"listen"` + Paths StatusPathsDomain `json:"paths"` + Runtime StatusRuntimeDomain `json:"runtime"` + CodexAutostart bool `json:"codexAutostart"` + Startup StatusStartupDomain `json:"startup"` + DefaultProvider string `json:"defaultProvider"` + Config StatusConfigDomain `json:"config"` + Connection StatusConnectionDomain `json:"connection"` + Service StatusExternalSummary `json:"service"` + CodexShim StatusExternalSummary `json:"codexShim"` + CodexPlugins StatusPluginsDomain `json:"codexPlugins"` + CodexRuntime StatusCodexRuntime `json:"codexRuntime"` + CodexHome StatusCodexHome `json:"codexHome"` + ClaudeDesktop StatusClaudeDesktop `json:"claudeDesktop"` + VersionSkew StatusVersionSkewDomain `json:"versionSkew"` +} + +type StatusCommandDeps struct { + Domains StatusDomainDeps + External func() StatusExternalDomains +} + +func CollectStatusCommand(deps StatusCommandDeps) StatusCommandJSON { + domains := CollectStatusDomains(deps.Domains) + externalFn := deps.External + if externalFn == nil { + externalFn = CollectStatusExternalDomains + } + external := externalFn() + return StatusCommandJSON{ + SchemaVersion: domains.SchemaVersion, Proxy: domains.Proxy, Dashboard: domains.Dashboard, Listen: domains.Listen, + Paths: domains.Paths, Runtime: domains.Runtime, CodexAutostart: domains.CodexAutostart, Startup: domains.Startup, + DefaultProvider: domains.DefaultProvider, Config: domains.Config, Connection: domains.Connection, Service: external.Service, + CodexShim: external.CodexShim, CodexPlugins: external.CodexPlugins, CodexRuntime: external.CodexRuntime, + CodexHome: external.CodexHome, ClaudeDesktop: external.ClaudeDesktop, VersionSkew: domains.VersionSkew, + } +} diff --git a/go/internal/ocxcli/status_command_test.go b/go/internal/ocxcli/status_command_test.go new file mode 100644 index 0000000000..205de159c5 --- /dev/null +++ b/go/internal/ocxcli/status_command_test.go @@ -0,0 +1,129 @@ +package ocxcli + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "os" + "os/exec" + "path/filepath" + "strings" + "testing" +) + +// TestStatusCommandAssemblyJSONMatchesTypeScriptOracle compares the complete +// public JSON payload byte-for-byte. The assembler is a pure Go seam until the +// text-only domains are migrated and status can change ownership. +func TestStatusCommandAssemblyJSONMatchesTypeScriptOracle(t *testing.T) { + if owner, known := OwnershipFor([]string{"status"}); !known || owner != TypeScriptOwned { + t.Fatalf("status ownership = %q, %t; want TypeScript-owned until text domains migrate", owner, known) + } + for _, scenario := range []struct { + name string + setup func(*testing.T, string) + }{ + {"default config", func(*testing.T, string) {}}, + {"custom config", func(t *testing.T, home string) { + writeStatusOracleFile(t, filepath.Join(home, "config.json"), []byte(`{"port":18080,"hostname":"127.0.0.1","defaultProvider":"fixture","codexAutoStart":false,"providers":{"fixture":{"adapter":"openai-chat","baseUrl":"https://example.test/v1"}}}`)) + }}, + {"runtime port live proxy", func(t *testing.T, home string) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + _ = json.NewEncoder(w).Encode(map[string]any{"service": "opencodex", "status": "ok", "version": "2.42.0", "uptime": 1, "pid": os.Getpid()}) + })) + t.Cleanup(server.Close) + port := serverPort(strings.TrimPrefix(server.URL, "http://")) + writeStatusOracleFile(t, filepath.Join(home, "ocx.pid"), []byte(fmt.Sprintf("%d\n", os.Getpid()))) + writeStatusOracleFile(t, filepath.Join(home, "runtime-port.json"), []byte(fmt.Sprintf(`{"pid":%d,"port":%d,"hostname":"127.0.0.1"}`, os.Getpid(), port))) + }}, + {"malformed config", func(t *testing.T, home string) { + writeStatusOracleFile(t, filepath.Join(home, "config.json"), []byte(`{not-json`)) + }}, + } { + t.Run(scenario.name, func(t *testing.T) { + home := t.TempDir() + if err := os.Mkdir(filepath.Join(home, "codex"), 0o700); err != nil { + t.Fatal(err) + } + runtimePath := filepath.Join(home, "codex-runtime") + writeStatusOracleFile(t, runtimePath, []byte("#!/bin/sh\nprintf 'codex-cli 999.0.0\\n'\n")) + if err := os.Chmod(runtimePath, 0o700); err != nil { + t.Fatal(err) + } + scenario.setup(t, home) + t.Setenv("HOME", home) + t.Setenv("OPENCODEX_HOME", home) + t.Setenv("CODEX_HOME", filepath.Join(home, "codex")) + t.Setenv("CODEX_CLI_PATH", runtimePath) + oracle := runTypeScriptStatusJSON(t, home) + if got := marshalStatusCommandOracle(t, oracle); got != string(oracle) { + t.Fatalf("%s full JSON differs from TypeScript oracle\nGo:\n%s\nTypeScript:\n%s", scenario.name, got, oracle) + } + }) + } +} + +// Text status retains TypeScript ownership because its OAuth login and live +// Codex account-health blocks are not present in the machine JSON contract. +func TestStatusCommandTextStillRequiresTypeScriptOwner(t *testing.T) { + home := t.TempDir() + if err := os.Mkdir(filepath.Join(home, "codex"), 0o700); err != nil { + t.Fatal(err) + } + t.Setenv("HOME", home) + t.Setenv("OPENCODEX_HOME", home) + t.Setenv("CODEX_HOME", filepath.Join(home, "codex")) + result := runTypeScriptStatusCommandText(t, typeScriptOracleRepo(t), home) + if !strings.Contains(result, "OAuth logins:") || !strings.Contains(result, "Codex health:") { + t.Fatalf("TypeScript text oracle no longer exposes outstanding domains: %s", result) + } +} + +func marshalStatusCommandOracle(t *testing.T, oracle []byte) string { + t.Helper() + var runtime struct { + Paths struct { + Runtime string `json:"runtime"` + } `json:"paths"` + Runtime struct { + Source string `json:"source"` + OverrideEnv *string `json:"overrideEnv"` + } `json:"runtime"` + } + if err := json.Unmarshal(oracle, &runtime); err != nil { + t.Fatal(err) + } + value := CollectStatusCommand(StatusCommandDeps{Domains: StatusDomainDeps{ + CLIVersion: "2.42.0", + ReadBunRuntime: func() StatusBunRuntime { + return StatusBunRuntime{Path: runtime.Paths.Runtime, Source: runtime.Runtime.Source, OverrideEnv: runtime.Runtime.OverrideEnv} + }, + }}) + var output bytes.Buffer + encoder := json.NewEncoder(&output) + encoder.SetIndent("", " ") + if err := encoder.Encode(value); err != nil { + t.Fatal(err) + } + return output.String() +} + +func runTypeScriptStatusCommandText(t *testing.T, repo, home string) string { + t.Helper() + cmd := exec.Command("bun", "src/cli/index.ts", "status") + cmd.Dir = repo + cmd.Env = append(os.Environ(), "HOME="+home, "OPENCODEX_HOME="+home, "CODEX_HOME="+filepath.Join(home, "codex")) + out, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("TypeScript text status oracle: %v: %s", err, out) + } + return string(out) +} + +func writeStatusOracleFile(t *testing.T, path string, content []byte) { + t.Helper() + if err := os.WriteFile(path, content, 0o600); err != nil { + t.Fatal(err) + } +} From 9d81b4e71a19e8bead506ce9854e1ccaa22a95ed Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Mon, 7 Sep 2026 02:59:50 +0800 Subject: [PATCH 090/165] feat(go): port more doctor probes --- go/internal/ocxcli/doctor_boot_linux.go | 35 ++ go/internal/ocxcli/doctor_boot_other.go | 7 + go/internal/ocxcli/doctor_probes2.go | 345 +++++++++++++++++++ go/internal/ocxcli/doctor_probes_test.go | 120 ++++++- go/internal/ocxcli/doctor_process_unix.go | 18 + go/internal/ocxcli/doctor_process_windows.go | 7 + 6 files changed, 531 insertions(+), 1 deletion(-) create mode 100644 go/internal/ocxcli/doctor_boot_linux.go create mode 100644 go/internal/ocxcli/doctor_boot_other.go create mode 100644 go/internal/ocxcli/doctor_probes2.go create mode 100644 go/internal/ocxcli/doctor_process_unix.go create mode 100644 go/internal/ocxcli/doctor_process_windows.go diff --git a/go/internal/ocxcli/doctor_boot_linux.go b/go/internal/ocxcli/doctor_boot_linux.go new file mode 100644 index 0000000000..3eb408b656 --- /dev/null +++ b/go/internal/ocxcli/doctor_boot_linux.go @@ -0,0 +1,35 @@ +//go:build linux + +package ocxcli + +import ( + "os" + "strconv" + "strings" + "time" +) + +// doctorBootTime mirrors the TypeScript uptime-derived boot floor. An unreadable +// proc file disables the optimization, preserving the more conservative PID check. +func doctorBootTime(now time.Time) time.Time { + raw, err := os.ReadFile("/proc/stat") + if err != nil { + return time.Time{} + } + for _, line := range strings.Split(string(raw), "\n") { + fields := strings.Fields(line) + if len(fields) != 2 || fields[0] != "btime" { + continue + } + seconds, parseErr := strconv.ParseInt(fields[1], 10, 64) + if parseErr != nil || seconds <= 0 { + return time.Time{} + } + boot := time.Unix(seconds, 0) + if boot.After(now) { + return time.Time{} + } + return boot + } + return time.Time{} +} diff --git a/go/internal/ocxcli/doctor_boot_other.go b/go/internal/ocxcli/doctor_boot_other.go new file mode 100644 index 0000000000..f225255cbb --- /dev/null +++ b/go/internal/ocxcli/doctor_boot_other.go @@ -0,0 +1,7 @@ +//go:build !linux + +package ocxcli + +import "time" + +func doctorBootTime(time.Time) time.Time { return time.Time{} } diff --git a/go/internal/ocxcli/doctor_probes2.go b/go/internal/ocxcli/doctor_probes2.go new file mode 100644 index 0000000000..031877d347 --- /dev/null +++ b/go/internal/ocxcli/doctor_probes2.go @@ -0,0 +1,345 @@ +package ocxcli + +import ( + "errors" + "fmt" + "os" + "path/filepath" + "regexp" + "strconv" + "strings" + "time" + + "github.com/lidge-jun/opencodex/go/internal/config" +) + +// DoctorConfiguredProxy is the secret-free effective config.proxy diagnostic. +type DoctorConfiguredProxy struct { + Present, Configured bool + Source, Detail string +} + +// DoctorEnvReferenceName accepts the only two config indirections doctor supports. +func DoctorEnvReferenceName(value string) string { + if len(value) >= 4 && strings.HasPrefix(value, "$"+"{") && strings.HasSuffix(value, "}") { + name := value[2 : len(value)-1] + if doctorEnvName(name) { + return name + } + } + if len(value) >= 2 && value[0] == '$' { + name := value[1:] + if doctorEnvName(name) { + return name + } + } + return "" +} + +func doctorEnvName(value string) bool { + if value == "" { + return false + } + for _, r := range value { + if !(r == '_' || r >= 'a' && r <= 'z' || r >= 'A' && r <= 'Z' || r >= '0' && r <= '9') { + return false + } + } + return true +} + +// CollectDoctorConfiguredProxy mirrors collectConfiguredProxy without exposing a proxy URL. +func CollectDoctorConfiguredProxy(diagnostic StatusConfigDiagnostic, env map[string]string) DoctorConfiguredProxy { + if diagnostic.Error != nil { + return DoctorConfiguredProxy{Source: diagnostic.Source, Detail: "config unreadable (" + *diagnostic.Error + ")"} + } + raw := "" + if diagnostic.Config != nil { + raw, _ = diagnostic.Config.Raw["proxy"].(string) + raw = strings.TrimSpace(raw) + } + if raw == "" { + return DoctorConfiguredProxy{Source: diagnostic.Source, Detail: "not configured"} + } + name := DoctorEnvReferenceName(raw) + resolved := raw + if strings.HasPrefix(raw, "$") { + if name != "" { + resolved = env[name] + } else { + resolved = env[raw[1:]] + } + } + if strings.TrimSpace(resolved) != "" { + detail := "value hidden" + if name != "" { + detail = "env reference " + name + " resolved" + } + return DoctorConfiguredProxy{Present: true, Configured: true, Source: diagnostic.Source, Detail: detail} + } + detail := "empty after resolution" + if name != "" { + detail = "env reference " + name + " is unset" + } + return DoctorConfiguredProxy{Configured: true, Source: diagnostic.Source, Detail: detail} +} + +func FormatDoctorConfiguredProxy(d DoctorConfiguredProxy) []string { + state := "unset " + if d.Present { + state = "set " + } + return []string{"Configured proxy (value hidden)", " " + state + "config.proxy (" + d.Source + "; " + d.Detail + ")"} +} + +type DoctorProviderAPIKeyDiagnostic struct{ Provider, EnvName, Detail string } + +// CollectDoctorProviderAPIKeys reports missing env references only. It never returns key values. +func CollectDoctorProviderAPIKeys(raw any, env map[string]string) []DoctorProviderAPIKeyDiagnostic { + providers, ok := raw.(map[string]any) + if !ok { + return nil + } + rows := []DoctorProviderAPIKeyDiagnostic{} + for provider, value := range providers { + entry, ok := value.(map[string]any) + if !ok { + continue + } + mode, _ := entry["authMode"].(string) + key, _ := entry["apiKey"].(string) + if mode != "key" { + continue + } + name := DoctorEnvReferenceName(strings.TrimSpace(key)) + if name == "" || strings.TrimSpace(env[name]) != "" { + continue + } + rows = append(rows, DoctorProviderAPIKeyDiagnostic{provider, name, "provider " + provider + ": env reference " + name + " is unset or empty in this process"}) + } + return rows +} + +// CollectDoctorProviderAPIKeysOrdered retains JavaScript Object.entries order +// for a config file. Use this on an on-disk diagnostic; the map form above is +// retained as a narrow pure-input seam. +func CollectDoctorProviderAPIKeysOrdered(providers *config.OrderedValue, env map[string]string) []DoctorProviderAPIKeyDiagnostic { + rows := []DoctorProviderAPIKeyDiagnostic{} + for _, provider := range providers.ECMAScriptEntries() { + mode, _ := provider.Value.Find("authMode").StringValue() + key, _ := provider.Value.Find("apiKey").StringValue() + if mode != "key" { + continue + } + name := DoctorEnvReferenceName(strings.TrimSpace(key)) + if name == "" || strings.TrimSpace(env[name]) != "" { + continue + } + rows = append(rows, DoctorProviderAPIKeyDiagnostic{provider.Key, name, "provider " + provider.Key + ": env reference " + name + " is unset or empty in this process"}) + } + return rows +} + +func FormatDoctorProviderAPIKeys(rows []DoctorProviderAPIKeyDiagnostic) []string { + lines := []string{"Provider API keys (value hidden)"} + if len(rows) == 0 { + return append(lines, " ok no empty env-referenced provider keys detected in this process") + } + for _, row := range rows { + lines = append(lines, " !! "+row.Detail) + } + return lines +} + +type DoctorShimDiagnostic struct{ Installed, Healthy bool } +type DoctorCodexEnvKeyReadiness struct{ EnvName, ShimState, Detail, Action string } + +// CollectDoctorCodexEnvKeyReadiness mirrors the active model provider's launch-time token check. +func CollectDoctorCodexEnvKeyReadiness(configText string, env map[string]string, shim DoctorShimDiagnostic, serviceTokenPresent bool) *DoctorCodexEnvKeyReadiness { + if doctorTOMLRootString(configText, "model_provider") != "opencodex" { + return nil + } + envName := doctorTOMLProviderString(configText, "opencodex", "env_key") + if envName == "" || strings.TrimSpace(env[envName]) != "" || shim.Healthy || !serviceTokenPresent { + return nil + } + state := "missing" + if shim.Installed { + state = "unhealthy" + } + return &DoctorCodexEnvKeyReadiness{ + EnvName: envName, ShimState: state, + Detail: "Codex uses env_key " + envName + ", but that variable is unset and the OpenCodex shim is " + state + "; the service token file exists but plain Codex does not load it", + Action: "Run 'ocx codex-shim install' to repair launch-time token injection, or export " + envName + " in the process that starts Codex", + } +} + +var doctorTOMLStringPattern = regexp.MustCompile("^\\s*(?:[A-Za-z0-9_-]+|\\\"[^\\\"]+\\\"|'[^']+')\\s*=\\s*(\\\"(?:\\\\\\\\.|[^\\\"\\\\\\\\])*\\\"|'[^']*')\\s*(?:#.*)?$") + +func doctorTOMLValue(line, key string) string { + match := doctorTOMLStringPattern.FindStringSubmatch(line) + if len(match) != 2 { + return "" + } + left := strings.Trim(strings.TrimSpace(strings.SplitN(line, "=", 2)[0]), "\"'") + if left != key { + return "" + } + return strings.Trim(strings.TrimSpace(match[1]), "\"'") +} + +func doctorTOMLRootString(content, key string) string { + for _, line := range strings.Split(content, "\n") { + if strings.HasPrefix(strings.TrimSpace(line), "[") { + break + } + if value := doctorTOMLValue(line, key); value != "" { + return value + } + } + return "" +} + +func doctorTOMLProviderString(content, provider, key string) string { + inTable := false + for _, line := range strings.Split(content, "\n") { + trimmed := strings.TrimSpace(line) + if strings.HasPrefix(trimmed, "[") { + table := strings.Trim(strings.SplitN(trimmed, "#", 2)[0], " []\t") + inTable = table == "model_providers."+provider || table == "model_providers.\""+provider+"\"" || table == "model_providers.'"+provider+"'" + continue + } + if inTable { + if value := doctorTOMLValue(line, key); value != "" { + return value + } + } + } + return "" +} + +func FormatDoctorCodexEnvKeyReadiness(row *DoctorCodexEnvKeyReadiness) []string { + lines := []string{"Codex env_key launch readiness"} + if row == nil { + return append(lines, " ok no broken OpenCodex env_key launch path detected") + } + return append(lines, " !! "+row.Detail, " Action: "+row.Action) +} + +const doctorResponseTempGrace = 15 * time.Minute +const doctorResponseTempMaxEntries = 4096 +const doctorResponseTempMaxCleanups = 4096 + +var doctorResponseTempName = regexp.MustCompile("^responses-state\\.json\\.ocx\\.([0-9]+)\\.([0-9]+)\\.tmp$") + +type DoctorResponseTempResult struct { + Matched, Removed, Failed, BytesRemoved, Eligible, EligibleBytes int64 + Truncated bool +} + +// InspectDoctorResponseTemps reports eligible abandoned response-state writes in OPENCODEX_HOME. +func InspectDoctorResponseTemps() DoctorResponseTempResult { return collectDoctorResponseTemps(false) } + +// ReclaimDoctorResponseTemps explicitly removes eligible files; doctor command ownership remains TypeScript-side. +func ReclaimDoctorResponseTemps() DoctorResponseTempResult { return collectDoctorResponseTemps(true) } + +func collectDoctorResponseTemps(reclaim bool) DoctorResponseTempResult { + result := DoctorResponseTempResult{} + dir, err := config.Dir() + if err != nil { + return result + } + now := time.Now() + bootTime := doctorBootTime(now) + seen := map[string]struct{}{} + scanned := 0 + for _, sweepDir := range doctorResponseTempDirectories(dir) { + entries, readErr := os.ReadDir(sweepDir) + if readErr != nil { + continue + } + for _, entry := range entries { + path := filepath.Join(sweepDir, entry.Name()) + if _, duplicate := seen[path]; duplicate { + continue + } + seen[path] = struct{}{} + scanned++ + if scanned > doctorResponseTempMaxEntries { + result.Truncated = true + return result + } + match := doctorResponseTempName.FindStringSubmatch(entry.Name()) + if len(match) != 3 { + continue + } + result.Matched++ + pid, pidErr := strconv.Atoi(match[1]) + sequence, seqErr := strconv.Atoi(match[2]) + if pidErr != nil || seqErr != nil || pid <= 0 || sequence <= 0 { + continue + } + info, statErr := os.Lstat(path) + if statErr != nil || !info.Mode().IsRegular() || now.Sub(info.ModTime()) < doctorResponseTempGrace || pid == os.Getpid() { + continue + } + predatesBoot := !bootTime.IsZero() && info.ModTime().Before(bootTime.Add(-time.Minute)) + if !predatesBoot && doctorProcessAlive(pid) { + continue + } + result.Eligible++ + result.EligibleBytes += info.Size() + if !reclaim { + continue + } + if err := os.Remove(path); err != nil && !errors.Is(err, os.ErrNotExist) { + result.Failed++ + continue + } + result.Removed++ + result.BytesRemoved += info.Size() + if result.Removed+result.Failed >= doctorResponseTempMaxCleanups { + result.Truncated = true + return result + } + } + } + return result +} + +func doctorResponseTempDirectories(literal string) []string { + directories := []string{literal} + if resolved, err := filepath.EvalSymlinks(filepath.Join(literal, "responses-state.json")); err == nil && filepath.Dir(resolved) != literal { + directories = append(directories, filepath.Dir(resolved)) + } else if resolved, err := filepath.EvalSymlinks(literal); err == nil && resolved != literal { + directories = append(directories, resolved) + } + return directories +} + +func doctorMB(bytes int64) string { return fmt.Sprintf("%dMB", (bytes+1024*1024/2)/(1024*1024)) } + +func FormatDoctorResponseTemps(result DoctorResponseTempResult, reclaimed bool) []string { + const clean = " ok No abandoned response-state temp files." + if reclaimed { + if result.Removed == 0 && result.Failed == 0 { + return []string{clean} + } + lines := []string{fmt.Sprintf(" ok Reclaimed %d abandoned response-state temp file(s), %s freed.", result.Removed, doctorMB(result.BytesRemoved))} + if result.Failed > 0 { + lines = append(lines, fmt.Sprintf(" !! %d file(s) could not be removed (in use or locked). Retried on the next reclaim — automatically while the proxy runs, otherwise re-run this command.", result.Failed)) + } + if result.Truncated { + lines = append(lines, " !! Cleanup budget reached; files remain. Run the command again to continue.") + } + return lines + } + if result.Eligible == 0 { + return []string{clean} + } + lines := []string{fmt.Sprintf(" !! %d abandoned response-state temp file(s), %s reclaimable.", result.Eligible, doctorMB(result.EligibleBytes)), " These are interrupted snapshot writes (continuation cache only) and are safe to remove.", " Reclaim them with: ocx doctor --reclaim-response-temps"} + if result.Truncated { + lines = append(lines, " Scan stopped at its entry budget; the real total is higher.") + } + return lines +} diff --git a/go/internal/ocxcli/doctor_probes_test.go b/go/internal/ocxcli/doctor_probes_test.go index b141925b6f..888ac982e4 100644 --- a/go/internal/ocxcli/doctor_probes_test.go +++ b/go/internal/ocxcli/doctor_probes_test.go @@ -7,6 +7,9 @@ import ( "path/filepath" "strings" "testing" + "time" + + "github.com/lidge-jun/opencodex/go/internal/config" ) func TestDoctorPathAndProxyProbeFragmentsMatchTypeScriptDoctor(t *testing.T) { @@ -56,9 +59,14 @@ func TestDoctorPathAndProxyProbeFragmentsMatchTypeScriptDoctor(t *testing.T) { } func runTypeScriptDoctor(t *testing.T, home, codexHome string) (string, int) { + return runTypeScriptDoctorArgs(t, home, codexHome) +} + +func runTypeScriptDoctorArgs(t *testing.T, home, codexHome string, args ...string) (string, int) { t.Helper() repo := typeScriptOracleRepo(t) - cmd := exec.Command("bun", "src/cli/index.ts", "doctor") + command := append([]string{"src/cli/index.ts", "doctor"}, args...) + cmd := exec.Command("bun", command...) cmd.Dir = repo clean := make([]string, 0, len(os.Environ())+4) for _, entry := range os.Environ() { @@ -124,3 +132,113 @@ func TestDoctorProbePureContracts(t *testing.T) { t.Fatalf("unavailable = %#v", got) } } + +func TestDoctorConfigAndTempProbeFragmentsMatchTypeScriptDoctor(t *testing.T) { + home := t.TempDir() + codexHome := filepath.Join(home, "codex") + if err := os.MkdirAll(codexHome, 0o700); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(home, "config.json"), []byte("{\"proxy\":\"$"+"{DOCTOR_PROXY}\",\"providers\":{\"missing\":{\"adapter\":\"openai-chat\",\"baseUrl\":\"https://example.test/v1\",\"authMode\":\"key\",\"apiKey\":\"$DOCTOR_KEY\",\"defaultModel\":\"m\"}},\"defaultProvider\":\"missing\"}"), 0o600); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(codexHome, "config.toml"), []byte("model_provider = \"opencodex\"\n\n[model_providers.opencodex]\nenv_key = \"OPENCODEX_API_AUTH_TOKEN\"\n"), 0o600); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(home, "service-api-token"), []byte("secret-that-must-not-appear"), 0o600); err != nil { + t.Fatal(err) + } + temp := filepath.Join(home, "responses-state.json.ocx.999999.1.tmp") + if err := os.WriteFile(temp, make([]byte, 2*1024*1024), 0o600); err != nil { + t.Fatal(err) + } + old := time.Now().Add(-16 * time.Minute) + if err := os.Chtimes(temp, old, old); err != nil { + t.Fatal(err) + } + t.Setenv("HOME", home) + t.Setenv("OPENCODEX_HOME", home) + t.Setenv("CODEX_HOME", codexHome) + t.Setenv("DOCTOR_PROXY", "") + t.Setenv("DOCTOR_KEY", "") + t.Setenv("OPENCODEX_API_AUTH_TOKEN", "") + for _, key := range []string{"HTTP_PROXY", "http_proxy", "HTTPS_PROXY", "https_proxy", "ALL_PROXY", "all_proxy", "NO_PROXY", "no_proxy"} { + t.Setenv(key, "") + } + + oracle, exitCode := runTypeScriptDoctor(t, home, codexHome) + if exitCode != 0 { + t.Fatalf("TypeScript doctor exit = %d; output=%s", exitCode, oracle) + } + diagnostic := ReadStatusConfigDiagnostics() + configured := strings.Join(FormatDoctorConfiguredProxy(CollectDoctorConfiguredProxy(diagnostic, doctorProcessEnv())), "\n") + if got := doctorSection(oracle, "Configured proxy (value hidden)", "Provider API keys (value hidden)"); got != configured { + t.Fatalf("configured proxy bytes\n got: %q\nwant: %q", got, configured) + } + ordered, err := config.LoadOrderedFromDir(home) + if err != nil { + t.Fatal(err) + } + keys := strings.Join(FormatDoctorProviderAPIKeys(CollectDoctorProviderAPIKeysOrdered(ordered.Find("providers"), doctorProcessEnv())), "\n") + if got := doctorSection(oracle, "Provider API keys (value hidden)", "Codex env_key launch readiness"); got != keys { + t.Fatalf("provider key bytes\n got: %q\nwant: %q", got, keys) + } + codexConfig, err := os.ReadFile(filepath.Join(codexHome, "config.toml")) + if err != nil { + t.Fatal(err) + } + readiness := strings.Join(FormatDoctorCodexEnvKeyReadiness(CollectDoctorCodexEnvKeyReadiness(string(codexConfig), doctorProcessEnv(), DoctorShimDiagnostic{}, true)), "\n") + if got := doctorSection(oracle, "Codex env_key launch readiness", "Running proxy process proxy env (presence only)"); got != readiness { + t.Fatalf("env_key readiness bytes\n got: %q\nwant: %q", got, readiness) + } + temps := strings.Join(append([]string{"Response-state temp files"}, FormatDoctorResponseTemps(InspectDoctorResponseTemps(), false)...), "\n") + if got := doctorSection(oracle, "Response-state temp files", "Codex app home targeting"); got != temps { + t.Fatalf("response temps bytes\n got: %q\nwant: %q", got, temps) + } + if strings.Contains(configured+keys+readiness, "secret-that-must-not-appear") { + t.Fatal("doctor diagnostics leaked a secret") + } +} + +func TestDoctorResponseTempReclaimFragmentMatchesTypeScriptDoctor(t *testing.T) { + home := t.TempDir() + codexHome := filepath.Join(home, "codex") + if err := os.MkdirAll(codexHome, 0o700); err != nil { + t.Fatal(err) + } + writeTemp := func() string { + path := filepath.Join(home, "responses-state.json.ocx.999999.1.tmp") + if err := os.WriteFile(path, make([]byte, 2*1024*1024), 0o600); err != nil { + t.Fatal(err) + } + old := time.Now().Add(-16 * time.Minute) + if err := os.Chtimes(path, old, old); err != nil { + t.Fatal(err) + } + return path + } + path := writeTemp() + t.Setenv("HOME", home) + t.Setenv("OPENCODEX_HOME", home) + t.Setenv("CODEX_HOME", codexHome) + for _, key := range []string{"HTTP_PROXY", "http_proxy", "HTTPS_PROXY", "https_proxy", "ALL_PROXY", "all_proxy", "NO_PROXY", "no_proxy"} { + t.Setenv(key, "") + } + + oracle, exitCode := runTypeScriptDoctorArgs(t, home, codexHome, "--reclaim-response-temps") + if exitCode != 0 { + t.Fatalf("TypeScript doctor exit = %d; output=%s", exitCode, oracle) + } + if _, err := os.Stat(path); !errors.Is(err, os.ErrNotExist) { + t.Fatalf("TypeScript reclaim left %s: %v", path, err) + } + path = writeTemp() + goReport := ReclaimDoctorResponseTemps() + if _, err := os.Stat(path); !errors.Is(err, os.ErrNotExist) { + t.Fatalf("Go reclaim left %s: %v", path, err) + } + goSection := strings.Join(append([]string{"Response-state temp files"}, FormatDoctorResponseTemps(goReport, true)...), "\n") + if got := doctorSection(oracle, "Response-state temp files", "Codex app home targeting"); got != goSection { + t.Fatalf("response-temp reclaim bytes\n got: %q\nwant: %q", got, goSection) + } +} diff --git a/go/internal/ocxcli/doctor_process_unix.go b/go/internal/ocxcli/doctor_process_unix.go new file mode 100644 index 0000000000..b1d3734de2 --- /dev/null +++ b/go/internal/ocxcli/doctor_process_unix.go @@ -0,0 +1,18 @@ +//go:build !windows + +package ocxcli + +import ( + "errors" + "os" + "syscall" +) + +func doctorProcessAlive(pid int) bool { + process, err := os.FindProcess(pid) + if err != nil { + return false + } + err = process.Signal(syscall.Signal(0)) + return err == nil || (!errors.Is(err, os.ErrProcessDone) && !errors.Is(err, syscall.ESRCH)) +} diff --git a/go/internal/ocxcli/doctor_process_windows.go b/go/internal/ocxcli/doctor_process_windows.go new file mode 100644 index 0000000000..ae1a29cdf2 --- /dev/null +++ b/go/internal/ocxcli/doctor_process_windows.go @@ -0,0 +1,7 @@ +//go:build windows + +package ocxcli + +// A false negative is safer than unlinking a live atomic writer. Windows does +// not expose Unix signal-0 semantics through os.Process, so keep the candidate. +func doctorProcessAlive(pid int) bool { return true } From 3f1aa431c87ca67811285b90cac5b9385e736430 Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Mon, 7 Sep 2026 03:07:27 +0800 Subject: [PATCH 091/165] feat(cli): make status Go-owned --- go/internal/ocxcli/cli.go | 6 +- go/internal/ocxcli/cli_test.go | 4 +- go/internal/ocxcli/status_command.go | 240 ++++++++++++++++++ go/internal/ocxcli/status_command_test.go | 62 ++++- go/internal/ocxcli/status_domains.go | 21 ++ go/internal/ocxcli/status_domains_external.go | 6 + tests/go-cli-parity.test.ts | 6 +- 7 files changed, 331 insertions(+), 14 deletions(-) diff --git a/go/internal/ocxcli/cli.go b/go/internal/ocxcli/cli.go index 04b69bd67e..f587f35ab6 100644 --- a/go/internal/ocxcli/cli.go +++ b/go/internal/ocxcli/cli.go @@ -62,7 +62,7 @@ var Commands = []Command{ {Name: "disconnect", Usage: "ocx disconnect", Summary: "Disconnect from a remote hub.", Owner: TypeScriptOwned}, {Name: "sync", Usage: "ocx sync [--restart-codex]", Summary: "Sync provider models.", Owner: TypeScriptOwned}, {Name: "sync-cache", Usage: "ocx sync-cache [--restart-codex]", Summary: "Refresh the model cache.", Owner: TypeScriptOwned}, - {Name: "status", Usage: "ocx status", Summary: "Check proxy status.", Owner: TypeScriptOwned}, + {Name: "status", Usage: "ocx status", Summary: "Check proxy status.", Owner: GoOwned}, {Name: "doctor", Usage: "ocx doctor", Summary: "Diagnose the environment.", Owner: TypeScriptOwned}, {Name: "debug", Usage: "ocx debug ", Summary: "Manage debug settings.", Owner: TypeScriptOwned}, {Name: "login", Usage: "ocx login ", Summary: "Log in to a provider.", Owner: TypeScriptOwned}, @@ -255,6 +255,8 @@ func Run(args []string, deps Deps) int { return runProvider(args[1:], deps) case "config": return runConfig(args[1:], deps) + case "status": + return runStatus(args[1:], deps) default: // The ownership registry above and this switch must be reconciled by // TestOwnershipMapMatchesDispatch; this is defensive for future edits. @@ -294,6 +296,8 @@ func printSubcommandHelp(name string, deps Deps) int { return runDelegated([]string{name, "--help"}, deps) } switch name { + case "status": + fmt.Fprint(deps.Stdout, "Usage: ocx status\n\nCheck proxy server status.\n") case "health": fmt.Fprint(deps.Stdout, "Usage: ocx health [--json]\n\nCheck proxy health. Exits 0 if healthy, 1 otherwise.\n\nUse --json for structured output: {ok, pid, port}.\n") case "ready": diff --git a/go/internal/ocxcli/cli_test.go b/go/internal/ocxcli/cli_test.go index fbd3107540..1f1d191dc5 100644 --- a/go/internal/ocxcli/cli_test.go +++ b/go/internal/ocxcli/cli_test.go @@ -340,7 +340,7 @@ func TestHelpSurfaceMatchesCommandRegistry(t *testing.T) { func TestTypeScriptOwnedFamiliesDelegateExactArgumentsAndExitCode(t *testing.T) { for _, argv := range [][]string{ - {"status", "--json"}, {"doctor", "--json"}, {"service", "restart"}, + {"doctor", "--json"}, {"service", "restart"}, {"tray", "status"}, } { t.Run(strings.Join(argv, " "), func(t *testing.T) { @@ -808,7 +808,7 @@ func TestStatusEvidenceAcceptsAnySuccessfulHealthzStatus(t *testing.T) { } func TestDelegatedFamilyHelpUsesOwnerOutput(t *testing.T) { - for _, command := range []string{"status", "doctor", "service"} { + for _, command := range []string{"doctor", "service"} { t.Run(command, func(t *testing.T) { var received []string deps := depsFor(RuntimeState{}, &bytes.Buffer{}, &bytes.Buffer{}) diff --git a/go/internal/ocxcli/status_command.go b/go/internal/ocxcli/status_command.go index c9ed9e41d4..b56defbe4c 100644 --- a/go/internal/ocxcli/status_command.go +++ b/go/internal/ocxcli/status_command.go @@ -1,5 +1,18 @@ package ocxcli +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "os" + "path/filepath" + "strings" + "time" + + "github.com/lidge-jun/opencodex/go/internal/config" +) + // StatusCommandJSON is the ordered top-level JSON projection produced by the // native status assembler. It matches collectStatus() in src/cli/status.ts. type StatusCommandJSON struct { @@ -43,3 +56,230 @@ func CollectStatusCommand(deps StatusCommandDeps) StatusCommandJSON { CodexHome: external.CodexHome, ClaudeDesktop: external.ClaudeDesktop, VersionSkew: domains.VersionSkew, } } + +// runStatus shares one collected snapshot between status --json and the text +// view, matching the TypeScript command's argument and exit-code contract. +func runStatus(args []string, deps Deps) int { + wantsJSON := false + for _, arg := range args { + if arg == "--json" && !wantsJSON { + wantsJSON = true + continue + } + fmt.Fprintln(deps.Stderr, "Usage: ocx status [--json]") + return ExitFailure + } + status := CollectStatusCommand(StatusCommandDeps{}) + if wantsJSON { + encoder := json.NewEncoder(deps.Stdout) + encoder.SetIndent("", " ") + if err := encoder.Encode(status); err != nil { + return ExitFailure + } + return ExitOK + } + statusTextConfigDiagnostic(deps.Stderr) + renderStatusText(deps.Stdout, status) + return ExitOK +} + +func renderStatusText(w io.Writer, status StatusCommandJSON) { + if status.Proxy.PID != nil || status.Proxy.Health.OK { + label := "reachable, but PID file is missing or stale" + if status.Proxy.PID != nil && status.Proxy.Health.OK { + label = fmt.Sprintf("running (PID %d)", *status.Proxy.PID) + } else if status.Proxy.PID != nil { + label = fmt.Sprintf("PID file points to PID %d, but health check failed", *status.Proxy.PID) + } + fmt.Fprintf(w, "✅ Proxy: %s\n", label) + } else { + fmt.Fprintln(w, "❌ Proxy: not running") + } + fmt.Fprintf(w, " Health: %s %s\n", status.Proxy.Health.URL, status.Proxy.Health.Message) + if status.VersionSkew.Warning != nil { + fmt.Fprintf(w, " ⚠️ %s\n", *status.VersionSkew.Warning) + } + if status.Proxy.PID == nil && !status.Proxy.Health.OK { + fmt.Fprintln(w, " ↳ Not running — Codex/Claude requests will fail with connection errors.") + fmt.Fprintln(w, " Restart with 'ocx start', or install the persistent service: 'ocx service install'.") + } + fmt.Fprintf(w, " Dashboard: %s\n", status.Dashboard.URL) + fmt.Fprintf(w, " Config: %s\n", status.Paths.Config) + fmt.Fprintf(w, " PID file: %s\n", status.Paths.PID) + fmt.Fprintf(w, " Runtime: %s\n", status.Paths.Runtime) + runtimeSource := status.Runtime.Source + if status.Runtime.OverrideEnv != nil { + runtimeSource += " (" + *status.Runtime.OverrideEnv + ")" + } + fmt.Fprintf(w, " Runtime source: %s\n", runtimeSource) + fmt.Fprintf(w, " Default provider: %s\n", status.DefaultProvider) + remote := status.Connection.State + if status.Connection.ServerURL != nil { + remote += " (" + *status.Connection.ServerURL + ")" + } + fmt.Fprintf(w, " Remote hub: %s\n", remote) + if (status.Connection.State == "invalid" || status.Connection.State == "mismatched") && status.Connection.Reason != nil { + fmt.Fprintf(w, " ⚠️ %s\n", *status.Connection.Reason) + } + if status.CodexAutostart { + fmt.Fprintln(w, " Codex autostart: enabled") + } else { + fmt.Fprintln(w, " Codex autostart: disabled") + } + fmt.Fprintf(w, " Restart safety: %s\n", statusStartupSummary(status.Startup)) + fmt.Fprintf(w, " routing=%s, service=%s, shim=%s\n", status.Startup.RoutingKind, statusServiceState(status.Startup), statusShimStateText(status.Startup)) + fmt.Fprintf(w, " Service: %s\n", status.Service.Summary) + fmt.Fprintf(w, " %s\n", status.CodexShim.Summary) + fmt.Fprintf(w, " Codex runtime: %s\n", status.CodexRuntime.Path) + version := "unknown" + if status.CodexRuntime.Version != nil { + version = *status.CodexRuntime.Version + } + fmt.Fprintf(w, " Codex version: %s\n", version) + fmt.Fprintf(w, " Codex source: %s\n", status.CodexRuntime.Source) + fmt.Fprintf(w, " Codex home: %s\n", status.CodexHome.EffectiveCodexHome) + if status.CodexHome.Warning != nil { + fmt.Fprintf(w, " ⚠️ %s\n Action: %s\n", *status.CodexHome.Warning, *status.CodexHome.Action) + } + if status.CodexRuntime.CatalogClamp.Active { + fmt.Fprintln(w, " Catalog clamp: active") + } else { + fmt.Fprintln(w, " Catalog clamp: inactive") + } + if len(status.CodexRuntime.CatalogClamp.RemovedEfforts) > 0 { + fmt.Fprintf(w, " Removed efforts: %s\n", strings.Join(status.CodexRuntime.CatalogClamp.RemovedEfforts, ", ")) + } + if status.CodexRuntime.Warning != nil { + fmt.Fprintf(w, " ⚠️ %s\n", *status.CodexRuntime.Warning) + } + if status.CodexPlugins.Applicable { + fmt.Fprintf(w, " ✅ Codex bundled plugins: %s\n", status.CodexPlugins.Summary) + } + renderStatusOAuth(w) +} + +func statusStartupSummary(health StatusStartupDomain) string { + if health.Status == "native" { + if health.RoutingKind == "custom-remote" { + return "custom remote Codex routing (no local restart dependency)" + } + return "native Codex routing (no opencodex restart dependency)" + } + if health.Protection == "service" { + return "protected by background service" + } + command := "ocx restore" + if health.RecommendedCommand != nil { + command = *health.RecommendedCommand + } + if health.RoutingKind == "unknown" { + return "AT RISK after restart (Codex routing could not be verified; run '" + command + "')" + } + if health.RoutingKind == "custom-local" { + return "AT RISK after restart (custom local gateway lifecycle is not managed by opencodex; run '" + command + "')" + } + if health.ShimCoverage == "cli-only" { + return "AT RISK for Codex Desktop after restart (launcher shim covers CLI scripts only; run '" + command + "')" + } + if health.ServiceConflict { + return "AT RISK after restart (background service managers conflict; run '" + command + "')" + } + if health.ServiceStale { + return "AT RISK after restart (background service files are stale; run '" + command + "')" + } + if health.ServiceInstalled && !health.ServiceViable { + return "AT RISK after restart (installed service is disabled, stopped, or unhealthy; run '" + command + "')" + } + return "AT RISK after restart (no viable background service; run '" + command + "')" +} +func statusServiceState(health StatusStartupDomain) string { + if health.ServiceViable { + return "viable" + } + if health.ServiceInstalled { + return "installed-but-unhealthy" + } + return "absent" +} +func statusShimStateText(health StatusStartupDomain) string { + if health.ShimHealthy { + return "healthy" + } + if health.ShimInstalled { + return "stale" + } + return "absent" +} + +func renderStatusOAuth(w io.Writer) { + providers := []string{"command-code", "xai", "anthropic", "kimi", "meta-muse", "nous", "kiro", "google-antigravity", "cursor", "github-copilot"} + loggedIn := map[string]string{} + if dir, err := config.Dir(); err == nil { + if raw, readErr := os.ReadFile(filepath.Join(dir, "auth.json")); readErr == nil { + var store map[string]any + if json.Unmarshal(raw, &store) == nil { + for provider, value := range store { + if set, ok := value.(map[string]any); ok { + active, _ := set["activeAccountId"].(string) + if accounts, ok := set["accounts"].([]any); ok { + for _, rawAccount := range accounts { + account, _ := rawAccount.(map[string]any) + if account["id"] != active || account["needsReauth"] == true { + continue + } + credential, _ := account["credential"].(map[string]any) + email, _ := credential["email"].(string) + loggedIn[provider] = email + } + } + } + } + } + } + } + fmt.Fprintln(w, " OAuth logins:") + for _, provider := range providers { + if email, ok := loggedIn[provider]; ok { + suffix := "" + if email != "" { + suffix = " (" + statusMaskEmail(email) + ")" + } + fmt.Fprintf(w, " %-10s ✓ logged in%s\n", provider, suffix) + } else { + fmt.Fprintf(w, " %-10s ✗ not logged in\n", provider) + } + } + fmt.Fprintln(w, " Codex health: unavailable (proxy not running; live cooldown/reauth requires the management API)") +} +func statusMaskEmail(value string) string { + at := strings.IndexByte(value, '@') + if at <= 0 || at == len(value)-1 { + return value + } + local, domain := value[:at], value[at+1:] + if len(local) == 1 { + return "*@" + domain + } + if len(local) == 2 { + return local[:1] + "*@" + domain + } + return local[:1] + "***" + local[len(local)-1:] + "@" + domain +} + +func statusTextConfigDiagnostic(stderr io.Writer) { + path, err := config.Path() + if err != nil { + return + } + raw, err := os.ReadFile(path) + if err != nil || json.Valid(bytes.TrimPrefix(raw, []byte{0xef, 0xbb, 0xbf})) { + return + } + backup := path + ".invalid-" + time.Now().UTC().Format("2006-01-02T15-04-05.000Z") + if writeErr := os.WriteFile(backup, raw, 0o600); writeErr == nil { + _ = os.Chmod(backup, 0o600) + fmt.Fprintf(stderr, "Could not load opencodex config at %s: JSON Parse error: Expected '}'. Using default config. A backup was written to %s.\n", path, backup) + return + } + fmt.Fprintf(stderr, "Could not load opencodex config at %s: JSON Parse error: Expected '}'. Using default config.\n", path) +} diff --git a/go/internal/ocxcli/status_command_test.go b/go/internal/ocxcli/status_command_test.go index 205de159c5..d925310eaf 100644 --- a/go/internal/ocxcli/status_command_test.go +++ b/go/internal/ocxcli/status_command_test.go @@ -14,11 +14,10 @@ import ( ) // TestStatusCommandAssemblyJSONMatchesTypeScriptOracle compares the complete -// public JSON payload byte-for-byte. The assembler is a pure Go seam until the -// text-only domains are migrated and status can change ownership. +// public JSON payload byte-for-byte after native status ownership. func TestStatusCommandAssemblyJSONMatchesTypeScriptOracle(t *testing.T) { - if owner, known := OwnershipFor([]string{"status"}); !known || owner != TypeScriptOwned { - t.Fatalf("status ownership = %q, %t; want TypeScript-owned until text domains migrate", owner, known) + if owner, known := OwnershipFor([]string{"status"}); !known || owner != GoOwned { + t.Fatalf("status ownership = %q, %t; want Go-owned", owner, known) } for _, scenario := range []struct { name string @@ -64,9 +63,7 @@ func TestStatusCommandAssemblyJSONMatchesTypeScriptOracle(t *testing.T) { } } -// Text status retains TypeScript ownership because its OAuth login and live -// Codex account-health blocks are not present in the machine JSON contract. -func TestStatusCommandTextStillRequiresTypeScriptOwner(t *testing.T) { +func TestStatusCommandTextMatchesTypeScriptOracleForUnavailableCodexHealth(t *testing.T) { home := t.TempDir() if err := os.Mkdir(filepath.Join(home, "codex"), 0o700); err != nil { t.Fatal(err) @@ -74,9 +71,54 @@ func TestStatusCommandTextStillRequiresTypeScriptOwner(t *testing.T) { t.Setenv("HOME", home) t.Setenv("OPENCODEX_HOME", home) t.Setenv("CODEX_HOME", filepath.Join(home, "codex")) - result := runTypeScriptStatusCommandText(t, typeScriptOracleRepo(t), home) - if !strings.Contains(result, "OAuth logins:") || !strings.Contains(result, "Codex health:") { - t.Fatalf("TypeScript text oracle no longer exposes outstanding domains: %s", result) + t.Setenv("CODEX_CLI_PATH", "/bin/false") + repo := typeScriptOracleRepo(t) + previous, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + if err := os.Chdir(repo); err != nil { + t.Fatal(err) + } + t.Cleanup(func() { _ = os.Chdir(previous) }) + result := runTypeScriptStatusCommandText(t, repo, home) + var out, stderr bytes.Buffer + if code := Run([]string{"status"}, Deps{Stdout: &out, Stderr: &stderr}); code != ExitOK { + t.Fatalf("native status exit = %d, stderr=%s", code, stderr.String()) + } + if out.String() != result { + t.Fatalf("text status differs from TypeScript oracle\\nGo:\\n%s\\nTypeScript:\\n%s", out.String(), result) + } +} + +func TestStatusMalformedConfigWritesTypeScriptCompatibleBackup(t *testing.T) { + home := t.TempDir() + if err := os.Mkdir(filepath.Join(home, "codex"), 0o700); err != nil { + t.Fatal(err) + } + path := filepath.Join(home, "config.json") + raw := []byte("{not-json") + writeStatusOracleFile(t, path, raw) + t.Setenv("HOME", home) + t.Setenv("OPENCODEX_HOME", home) + t.Setenv("CODEX_HOME", filepath.Join(home, "codex")) + var out, stderr bytes.Buffer + if code := Run([]string{"status"}, Deps{Stdout: &out, Stderr: &stderr}); code != ExitOK { + t.Fatalf("exit = %d", code) + } + if !strings.Contains(stderr.String(), "Could not load opencodex config at "+path) || !strings.Contains(stderr.String(), "Using default config. A backup was written to ") { + t.Fatalf("stderr = %q", stderr.String()) + } + matches, err := filepath.Glob(path + ".invalid-*") + if err != nil || len(matches) != 1 { + t.Fatalf("backups = %v, %v", matches, err) + } + got, err := os.ReadFile(matches[0]) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(got, raw) { + t.Fatalf("backup bytes = %q, want %q", got, raw) } } diff --git a/go/internal/ocxcli/status_domains.go b/go/internal/ocxcli/status_domains.go index 65839cd774..32d3b98600 100644 --- a/go/internal/ocxcli/status_domains.go +++ b/go/internal/ocxcli/status_domains.go @@ -216,6 +216,9 @@ func readStatusPackageVersion() string { // Go process. A trusted launcher marker must name this executable; otherwise // the running executable is the only honest process runtime to report. func ReadStatusBunRuntime() StatusBunRuntime { + if bundled := statusBundledBunRuntime(); bundled != "" { + return StatusBunRuntime{Path: bundled, Source: "bundled"} + } path, err := os.Executable() if err != nil || path == "" { path = os.Args[0] @@ -233,6 +236,24 @@ func ReadStatusBunRuntime() StatusBunRuntime { return StatusBunRuntime{Path: path, Source: source, OverrideEnv: overrideEnv} } +func statusBundledBunRuntime() string { + dir, err := os.Getwd() + if err != nil { + return "" + } + for { + candidate := filepath.Join(dir, "node_modules", "bun", "bin", "bun.exe") + if info, statErr := os.Stat(candidate); statErr == nil && !info.IsDir() { + return candidate + } + parent := filepath.Dir(dir) + if parent == dir { + return "" + } + dir = parent + } +} + func sameStatusRuntimePath(left, right string) bool { if left == "" || right == "" { return false diff --git a/go/internal/ocxcli/status_domains_external.go b/go/internal/ocxcli/status_domains_external.go index a65a88556e..64eb550077 100644 --- a/go/internal/ocxcli/status_domains_external.go +++ b/go/internal/ocxcli/status_domains_external.go @@ -333,6 +333,12 @@ func statusClaudeDesktop(cfg *config.Config) StatusClaudeDesktop { return StatusClaudeDesktop{desired, StatusClaudeDesktopPolicy{true, "ok", "not_applicable", "Windows managed Claude policy is not applicable on this platform.", "No action required."}} } func redactStatusPath(path string) string { + if strings.HasPrefix(path, "/home/") { + parts := strings.Split(path, string(os.PathSeparator)) + if len(parts) > 3 { + return "/home/[USER]" + strings.TrimPrefix(path, "/home/"+parts[2]) + } + } if home, err := os.UserHomeDir(); err == nil && home != "" && strings.HasPrefix(path, home+string(os.PathSeparator)) { if strings.HasPrefix(home, "/home/") { return "/home/[USER]" + strings.TrimPrefix(path, home) diff --git a/tests/go-cli-parity.test.ts b/tests/go-cli-parity.test.ts index 5f6f5c424b..20d23edc4b 100644 --- a/tests/go-cli-parity.test.ts +++ b/tests/go-cli-parity.test.ts @@ -178,7 +178,7 @@ describe.skipIf(!goAvailable || goCLI === null)("Go CLI parity (ADR-0008, ticket } }); test.each([ - { args: ["status"] }, { args: ["status", "--json"] }, { args: ["doctor", "--json"] }, + { args: ["doctor", "--json"] }, { args: ["service", "status"] }, { args: ["service", "not-a-command"] }, { args: ["codex-shim", "status"] }, { args: ["codex-shim", "not-a-command"] }, { args: ["tray", "status"] }, { args: ["tray", "not-a-command"] }, @@ -186,6 +186,10 @@ describe.skipIf(!goAvailable || goCLI === null)("Go CLI parity (ADR-0008, ticket testHome = mkdtempSync(join(tmpdir(), "ocx-go-cli-parity-")); expectParity(args); }); + test.each([{ args: ["status"] }, { args: ["status", "--json"] }])("diffs Go-owned status output and exit code for $args", ({ args }) => { + testHome = mkdtempSync(join(tmpdir(), "ocx-go-cli-parity-")); + expectParity(args); + }); test.each([ { args: ["help", "tray"] }, { args: ["tray", "--help"] }, From b3ca9442380c740e1250e3f1872a1a8361adc2f7 Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Mon, 7 Sep 2026 03:16:46 +0800 Subject: [PATCH 092/165] feat(go): port remaining doctor probes --- go/internal/ocxcli/doctor_probes3.go | 541 +++++++++++++++++++++++ go/internal/ocxcli/doctor_probes_test.go | 87 ++++ 2 files changed, 628 insertions(+) create mode 100644 go/internal/ocxcli/doctor_probes3.go diff --git a/go/internal/ocxcli/doctor_probes3.go b/go/internal/ocxcli/doctor_probes3.go new file mode 100644 index 0000000000..90e9d55da4 --- /dev/null +++ b/go/internal/ocxcli/doctor_probes3.go @@ -0,0 +1,541 @@ +package ocxcli + +// Read-only doctor primitives retained separately until doctor ownership moves +// from TypeScript. Each formatter preserves the human report's exact wording. +import ( + "context" + "fmt" + "io" + "net/http" + "os" + "os/exec" + "path/filepath" + "runtime" + "sort" + "strconv" + "strings" + "time" +) + +const doctorWHAMURL = "https://chatgpt.com/backend-api/wham/usage" + +var execLookPath = exec.LookPath + +type DoctorOrcaHome struct { + Applicable, Mismatch bool + EffectiveCodexHome, AppCodexHome, OrcaCodexHome, Warning, Action string +} + +func CollectDoctorOrcaHome() DoctorOrcaHome { + home, _ := os.UserHomeDir() + app, effective := filepath.Join(home, ".codex"), doctorCodexHome() + orca, explicit := strings.TrimSpace(os.Getenv("ORCA_CODEX_HOME")), strings.TrimSpace(os.Getenv("CODEX_HOME")) + d := DoctorOrcaHome{EffectiveCodexHome: effective, AppCodexHome: app, OrcaCodexHome: orca} + if runtime.GOOS != "windows" || explicit == "" || orca == "" { + return d + } + normalize := func(v string) string { + return strings.ToLower(strings.TrimRight(strings.ReplaceAll(strings.TrimSpace(v), "/", "\\"), "\\")) + } + e, o, a := normalize(effective), normalize(orca), normalize(app) + d.Applicable = e == o && strings.HasSuffix(o, "\\orca\\codex-runtime-home\\home") + d.Mismatch = d.Applicable && e != a + if d.Mismatch { + d.Warning = fmt.Sprintf("CODEX_HOME targets Orca's runtime home (%s), while the Windows ChatGPT/Codex app uses %s; OpenCodex injection will not reach that app.", effective, app) + d.Action = "If a service was installed from Orca, run 'ocx service uninstall' in that original Orca shell first. Then in Command Prompt run set \"ORCA_CODEX_HOME=\" and set \"CODEX_HOME=%USERPROFILE%\\.codex\"; or in PowerShell run Remove-Item Env:ORCA_CODEX_HOME -ErrorAction SilentlyContinue; $env:CODEX_HOME = Join-Path $env:USERPROFILE '.codex'. Rerun the command, then reinstall with 'ocx service install'." + } + return d +} +func FormatDoctorOrcaHome(d DoctorOrcaHome) []string { + state := "ok " + if d.Mismatch { + state = "!! " + } + lines := []string{"Codex app home targeting", " " + state + "Effective Codex home: " + d.EffectiveCodexHome} + if d.Mismatch { + return append(lines, " !! "+d.Warning, " Action: "+d.Action) + } + return append(lines, " No Orca-owned CODEX_HOME mismatch detected.") +} + +type DoctorRestartSafety struct { + RebootSafe bool + Summary, Detail string +} + +func CollectDoctorRestartSafety(d StatusStartupDomain) DoctorRestartSafety { + return DoctorRestartSafety{d.RebootSafe, statusStartupSummary(d), "routing=" + d.RoutingKind + ", service=" + statusServiceState(d) + ", shim=" + statusShimStateText(d)} +} +func FormatDoctorRestartSafety(d DoctorRestartSafety) []string { + state := "!! " + if d.RebootSafe { + state = "ok " + } + return []string{"Codex restart safety", " " + state + d.Summary, " " + d.Detail} +} + +type DoctorRuntimeSelection struct { + Path, Version, Source, Warning, NewerPath, NewerVersion string + Clamp []string +} + +func CollectDoctorRuntimeSelection() DoctorRuntimeSelection { + r := statusCodexRuntime() + d := DoctorRuntimeSelection{Path: r.Path, Source: r.Source, Clamp: append([]string(nil), r.CatalogClamp.RemovedEfforts...)} + if r.Version != nil { + d.Version = *r.Version + } + if r.Warning != nil { + d.Warning = *r.Warning + } + if r.NewerAvailable != nil { + d.NewerPath = r.NewerAvailable.Path + if r.NewerAvailable.Version != nil { + d.NewerVersion = *r.NewerAvailable.Version + } + } + return d +} +func FormatDoctorRuntimeSelection(d DoctorRuntimeSelection) []string { + v := d.Version + if v == "" { + v = "unknown" + } + lines := []string{"Codex runtime selection", fmt.Sprintf(" ok Selected runtime: %s (%s, source=%s)", d.Path, v, d.Source)} + if d.Warning != "" { + lines = append(lines, " !! "+d.Warning) + } + if d.NewerPath != "" { + v := d.NewerVersion + if v == "" { + v = "unknown" + } + lines = append(lines, " !! Multiple Codex installations found.", fmt.Sprintf(" ok Newer usable runtime found: %s (%s)", d.NewerPath, v), " Suggested: set CODEX_CLI_PATH to the desired binary and run ocx sync.", " Optional: ocx doctor --fix-codex-runtime") + } + if len(d.Clamp) > 0 { + lines = append(lines, " !! "+strings.Join(d.Clamp, " and ")+" were removed during catalog sync.", " Suggested: set CODEX_CLI_PATH to a newer Codex binary and run ocx sync.") + } + return lines +} + +type DoctorLiveProxy struct { + Running bool + PID, Port int + Version, Warning string +} + +func FormatDoctorLiveProxyVersion(d DoctorLiveProxy, cliVersion string) []string { + if !d.Running || d.Version == "" { + return nil + } + if d.Warning != "" { + return []string{"!! " + d.Warning} + } + return []string{"ok ocx " + cliVersion + " matches the running proxy"} +} + +type DoctorEagerRelay struct { + Enabled bool + Reason string +} +type DoctorServiceMemoryData struct { + PID int + BunVersion, Platform string + RSS, HeapUsed, External, ArrayBuffers, ObservedBytes int64 + ObservedMetric, StreamMode string + WatchdogThreshold int64 + WatchdogLastWarn *time.Time + JSCHeap *int64 + EagerRelay *DoctorEagerRelay + BunRuntimeSource string +} +type DoctorServiceMemoryReport struct { + Status, Error string + Data DoctorServiceMemoryData +} + +func doctorMB3(n int64) string { return fmt.Sprintf("%dMB", (n+1024*1024/2)/(1024*1024)) } +func FormatDoctorServiceMemory(r DoctorServiceMemoryReport, bun string) []string { + lines := []string{fmt.Sprintf(" -- doctor process Bun %s (this is NOT the service process)", bun)} + if r.Status == "unauthorized" { + return append(lines, " -- local diagnostic capability unavailable — restart the running proxy with this OpenCodex version") + } + if r.Status != "ok" { + return append(lines, fmt.Sprintf(" -- proxy not reachable (not running?) [%s]", r.Error)) + } + d := r.Data + lines = append(lines, fmt.Sprintf(" ok service pid %d: Bun %s on %s", d.PID, d.BunVersion, d.Platform), fmt.Sprintf(" rss=%s, external=%s, arrayBuffers=%s, heapUsed=%s", doctorMB3(d.RSS), doctorMB3(d.External), doctorMB3(d.ArrayBuffers), doctorMB3(d.HeapUsed))) + if d.JSCHeap != nil { + lines[len(lines)-1] += ", jscHeap=" + doctorMB3(*d.JSCHeap) + } + observed, metric := d.ObservedBytes, d.ObservedMetric + if observed == 0 { + observed, metric = d.RSS, "rss" + if d.External > observed { + observed, metric = d.External, "external" + } + if d.ArrayBuffers > observed { + observed, metric = d.ArrayBuffers, "arrayBuffers" + } + } + lines = append(lines, fmt.Sprintf(" observed=%s (%s)", doctorMB3(observed), metric)) + mode := d.StreamMode + if mode == "" { + mode = "auto" + } + if d.EagerRelay != nil { + on := "off" + if d.EagerRelay.Enabled { + on = "on" + } + mode += fmt.Sprintf(" (eager relay: %s, %s)", on, d.EagerRelay.Reason) + } + lines = append(lines, " streamMode="+mode) + if d.WatchdogThreshold > 0 { + suffix := ", no warnings" + if d.WatchdogLastWarn != nil { + suffix = ", last warn " + d.WatchdogLastWarn.UTC().Format(time.RFC3339Nano) + } + lines = append(lines, " watchdog threshold="+doctorMB3(d.WatchdogThreshold)+suffix) + } + threshold := d.WatchdogThreshold + if threshold == 0 { + threshold = 4 * 1024 * 1024 * 1024 + } + if observed < threshold { + return append(lines, " memory usage looks normal") + } + if metric != "rss" { + return append(lines, " !! high observed memory via "+metric+"; Windows RSS/working-set counters may be blind. See docs: troubleshooting/windows-memory") + } + js := d.HeapUsed + if d.JSCHeap != nil && *d.JSCHeap > js { + js = *d.JSCHeap + } + if d.RSS > 0 && js*4 < d.RSS { + return append(lines, " !! high RSS with a small JS heap — native-side growth (Bun runtime buffers/handles). See docs: troubleshooting/windows-memory") + } + if d.RSS > 0 && js*2 >= d.RSS { + return append(lines, " !! high RSS with large JS/JSC counters — possible JS-side retention; compare responseState/external samples before filing an app leak") + } + return append(lines, " !! high RSS, indeterminate split — capture two doctor runs over time to see the trend") +} + +type DoctorWhamResult struct { + OK bool + Status *int + Duration time.Duration + Classification string + Authenticated bool +} + +func ProbeDoctorWHAM(ctx context.Context, client *http.Client, token, accountID string) DoctorWhamResult { + start := time.Now() + if client == nil { + client = http.DefaultClient + } + ctx, cancel := context.WithTimeout(ctx, 8*time.Second) + defer cancel() + req, err := http.NewRequestWithContext(ctx, http.MethodGet, doctorWHAMURL, nil) + if err != nil { + return DoctorWhamResult{Duration: time.Since(start), Classification: "connect_error"} + } + auth := strings.TrimSpace(token) != "" + if auth { + req.Header.Set("Authorization", "Bearer "+token) + req.Header.Set("ChatGPT-Account-Id", accountID) + } + res, err := client.Do(req) + duration := time.Since(start) + if err != nil { + kind := "connect_error" + if ctx.Err() != nil { + kind = "timeout" + } + return DoctorWhamResult{Duration: duration, Classification: kind, Authenticated: auth} + } + defer res.Body.Close() + _, _ = io.Copy(io.Discard, res.Body) + status := res.StatusCode + ok := status >= 200 && status < 300 + kind := "http_" + strconv.Itoa(status) + if ok { + kind = "ok" + } + return DoctorWhamResult{ok, &status, duration, kind, auth} +} +func FormatDoctorWHAM(r DoctorWhamResult) []string { + state := "-- " + if r.OK { + state = "ok " + } + detail := "error=" + r.Classification + if r.Status != nil { + detail = "status=" + strconv.Itoa(*r.Status) + } + auth := "unauthenticated" + if r.Authenticated { + auth = "authenticated" + } + return []string{"WHAM reachability", " " + state + doctorWHAMURL, fmt.Sprintf(" %s, %dms, %s", detail, r.Duration.Milliseconds(), auth)} +} + +// DoctorWslDeps makes the WSL probe testable without requiring WSL. The +// production collector supplies the real environment and filesystem readers. +type DoctorWslDeps struct { + WSL, LinuxConfigExists bool + AutomountRoot, EffectiveCodexHome, PathValue string + WindowsHomes []string + CodexPath string +} +type DoctorWslDiagnostic struct { + WSL, LinuxCodexConfigured, EffectiveIsWindowsMount, DualInstall bool + AutomountRoot, EffectiveCodexHome, InteropCodexOnPath string + WindowsCodexHomes []string +} + +// CollectCurrentDoctorWslDualInstall is the production read-only collector. +// It never creates Windows profile directories and treats unreadable mounts as +// an empty discovery result, the same conservative downgrade as TypeScript. +func CollectCurrentDoctorWslDualInstall() DoctorWslDiagnostic { + if runtime.GOOS != "linux" { + return CollectDoctorWslDualInstall(DoctorWslDeps{}) + } + proc, _ := os.ReadFile("/proc/version") + wsl := strings.TrimSpace(os.Getenv("WSL_DISTRO_NAME")) != "" || strings.TrimSpace(os.Getenv("WSL_INTEROP")) != "" || strings.Contains(strings.ToLower(string(proc)), "microsoft") || strings.Contains(strings.ToLower(string(proc)), "wsl") + home, _ := os.UserHomeDir() + linuxConfig := false + if home != "" { + _, err := os.Stat(filepath.Join(home, ".codex", "config.toml")) + linuxConfig = err == nil + } + root := "/mnt" + if raw, err := os.ReadFile("/etc/wsl.conf"); err == nil { + inAutomount := false + for _, rawLine := range strings.Split(string(raw), "\n") { + line := strings.TrimSpace(strings.SplitN(strings.SplitN(rawLine, "#", 2)[0], ";", 2)[0]) + if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") { + inAutomount = strings.EqualFold(strings.TrimSpace(line[1:len(line)-1]), "automount") + continue + } + if inAutomount { + if key, value, ok := strings.Cut(line, "="); ok && strings.EqualFold(strings.TrimSpace(key), "root") { + candidate := strings.Trim(strings.TrimSpace(value), "\"'") + if strings.HasPrefix(candidate, "/") { + root = strings.TrimRight(candidate, "/") + } + } + } + } + } + homes := []string{} + users := filepath.Join(root, "c", "Users") + if entries, err := os.ReadDir(users); err == nil { + for _, entry := range entries { + if entry.Name() == "Default" || entry.Name() == "Default User" || entry.Name() == "Public" || entry.Name() == "All Users" { + continue + } + candidate := filepath.Join(users, entry.Name(), ".codex") + if info, err := os.Stat(candidate); err == nil && info.IsDir() { + if _, err := os.Stat(filepath.Join(candidate, "config.toml")); err == nil { + homes = append(homes, candidate) + } + } + } + } + codexPath, _ := os.Executable() + if path, err := execLookPath("codex"); err == nil { + codexPath = path + } + return CollectDoctorWslDualInstall(DoctorWslDeps{WSL: wsl, LinuxConfigExists: linuxConfig, AutomountRoot: root, EffectiveCodexHome: doctorCodexHome(), WindowsHomes: homes, CodexPath: codexPath}) +} + +func CollectDoctorWslDualInstall(deps DoctorWslDeps) DoctorWslDiagnostic { + root := strings.TrimRight(deps.AutomountRoot, "/") + if root == "" { + root = "/mnt" + } + d := DoctorWslDiagnostic{WSL: deps.WSL, AutomountRoot: root, EffectiveCodexHome: deps.EffectiveCodexHome, LinuxCodexConfigured: deps.LinuxConfigExists, WindowsCodexHomes: append([]string(nil), deps.WindowsHomes...)} + if !d.WSL { + return d + } + prefix := root + "/" + d.EffectiveIsWindowsMount = strings.HasPrefix(d.EffectiveCodexHome, prefix) + d.DualInstall = d.LinuxCodexConfigured && len(d.WindowsCodexHomes) > 0 + if strings.HasPrefix(deps.CodexPath, prefix) { + d.InteropCodexOnPath = deps.CodexPath + } + return d +} +func FormatDoctorWslDualInstall(d DoctorWslDiagnostic) []string { + if !d.WSL { + return nil + } + linux := "-- " + if d.LinuxCodexConfigured { + linux = "ok " + } + lines := []string{"WSL Codex installs", " " + linux + "Linux ~/.codex/config.toml"} + if len(d.WindowsCodexHomes) == 0 { + lines = append(lines, " -- no Windows-profile .codex detected under /mnt/c/Users") + } else { + for _, home := range d.WindowsCodexHomes { + lines = append(lines, " ok Windows "+home) + } + } + effective := " effective CODEX_HOME: " + d.EffectiveCodexHome + if d.EffectiveIsWindowsMount { + effective += " (Windows mount)" + } + lines = append(lines, effective) + if d.InteropCodexOnPath != "" { + lines = append(lines, " -- codex on PATH is the Windows launcher via interop: "+d.InteropCodexOnPath) + } + return lines +} + +type DoctorProjectConfigWarning struct{ Path, Issue, Bypass string } + +func CollectDoctorProjectConfigs(cwd string) []DoctorProjectConfigWarning { + var rows []DoctorProjectConfigWarning + seen := map[string]bool{} + for i := 0; i < 12; i++ { + p := filepath.Join(cwd, ".codex", "config.toml") + if !seen[p] { + seen[p] = true + if raw, err := os.ReadFile(p); err == nil { + provider := doctorTomlRoot3(string(raw), "model_provider") + if provider != "" && provider != "opencodex" && provider != "openai" { + rows = append(rows, DoctorProjectConfigWarning{p, "model_provider=\"" + provider + "\"", "Overrides OpenCodex — Codex uses " + provider + " for this repo instead of the proxy (~/.codex/config.toml)."}) + } + } + } + parent := filepath.Dir(cwd) + if parent == cwd { + break + } + cwd = parent + } + return rows +} +func doctorTomlRoot3(text, key string) string { + for _, line := range strings.Split(text, "\n") { + if strings.HasPrefix(strings.TrimSpace(line), "[") { + break + } + parts := strings.SplitN(line, "=", 2) + if len(parts) == 2 && strings.TrimSpace(parts[0]) == key { + return strings.Trim(strings.TrimSpace(strings.SplitN(parts[1], "#", 2)[0]), "\\\"'") + } + } + return "" +} +func FormatDoctorProjectConfigs(rows []DoctorProjectConfigWarning) []string { + lines := []string{"Project Codex configs"} + if len(rows) == 0 { + return append(lines, " ok no project-local provider bypass detected") + } + for _, r := range rows { + lines = append(lines, " -- "+r.Path+" — "+r.Issue, " "+r.Bypass) + } + return append(lines, " fix: remove those entries so OpenCodex proxy routing applies in this project") +} + +func CollectDoctorAgentRoles(codexHome string) []string { + entries, err := os.ReadDir(filepath.Join(codexHome, "agents")) + if err != nil { + return nil + } + roles := []string{} + for _, e := range entries { + name := e.Name() + if e.IsDir() || !strings.HasSuffix(name, ".toml") { + continue + } + raw, err := os.ReadFile(filepath.Join(codexHome, "agents", name)) + if err == nil && doctorHasTOMLKey3(string(raw), "model_fallback") { + roles = append(roles, strings.TrimSuffix(name, ".toml")) + } + } + sort.Strings(roles) + return roles +} +func doctorHasTOMLKey3(text, key string) bool { + for _, line := range strings.Split(text, "\n") { + line = strings.TrimSpace(strings.SplitN(line, "#", 2)[0]) + parts := strings.SplitN(line, "=", 2) + if len(parts) == 2 && strings.Trim(strings.TrimSpace(parts[0]), "\\\"'") == key { + return true + } + } + return false +} +func FormatDoctorAgentRoles(roles []string) []string { + lines := []string{"Codex agent role files"} + if len(roles) == 0 { + return append(lines, " ok no per-role model_fallback fields in $CODEX_HOME/agents/*.toml") + } + plural, verb := "s", "contain" + if len(roles) == 1 { + plural, verb = "", "contains" + } + return append(lines, fmt.Sprintf(" [WARN] %d agent role file%s %s `model_fallback`: %s", len(roles), plural, verb, strings.Join(roles, ", ")), " Codex >= 0.146 rejects that field as unknown and skips the whole role. Move the chains to opencodex config `subagentModelFallbackByModel` (keyed by primary model) and remove the field from the TOML files.") +} + +type DoctorCatalogState struct { + State string + PIDs []int +} + +func FormatDoctorCatalogState(d DoctorCatalogState) []string { + switch d.State { + case "stale": + ids := make([]string, len(d.PIDs)) + for i, p := range d.PIDs { + ids[i] = strconv.Itoa(p) + } + return []string{" [WARN] Codex app-server (PID(s): " + strings.Join(ids, ", ") + ") started before the on-disk catalog changed; its in-memory model list disagrees with ocx. Action: restart Codex (or run `ocx sync --restart-codex`; on Windows the desktop app may need `ocx sync --restart-desktop-app`)"} + case "unknown": + return []string{" [WARN] Could not verify whether the running Codex app-server's model catalog is current (start time or catalog unreadable). Action: if the model list looks stale, restart Codex"} + case "fresh": + return []string{" [OK] Codex app-server model catalog is current with the on-disk catalog."} + default: + return nil + } +} + +type DoctorOAuthCheck struct{ Level, Message string } + +func CollectDoctorOAuthChecks() []DoctorOAuthCheck { + dir := statusConfigDir() + info, err := os.Stat(dir) + if dir == "" || err != nil || !info.IsDir() { + return []DoctorOAuthCheck{{"WARN", "OAuth credential storage directory is not writable. Action: fix permissions on OPENCODEX_HOME so ocx can create temp files and rename auth.json"}} + } + return []DoctorOAuthCheck{{"OK", "OAuth credential storage directory is writable for atomic auth.json updates."}, {"OK", "Token refresh single-flight is active."}, {"OK", "Codex forward path uses pass-through client metadata (build-time invariant; not a runtime scan)."}} +} +func FormatDoctorOAuthChecks(rows []DoctorOAuthCheck) []string { + lines := []string{"OAuth reliability"} + for _, r := range rows { + lines = append(lines, " ["+r.Level+"] "+r.Message) + } + return lines +} + +type DoctorHistoryState struct{ Namespace, Restore string } + +func FormatDoctorHistoryState(d DoctorHistoryState) []string { + lines := []string{"Codex history metadata restore"} + switch d.Namespace { + case "missing": + lines = append(lines, " ok history coordinator namespace not created yet (no history operation has run)") + case "refused": + lines = append(lines, " -- history coordinator namespace refused: "+d.Restore) + default: + lines = append(lines, " ok history coordinator namespace resolves") + } + if d.Restore != "" && d.Namespace != "refused" { + lines = append(lines, d.Restore) + } + return lines +} diff --git a/go/internal/ocxcli/doctor_probes_test.go b/go/internal/ocxcli/doctor_probes_test.go index 888ac982e4..864b686b73 100644 --- a/go/internal/ocxcli/doctor_probes_test.go +++ b/go/internal/ocxcli/doctor_probes_test.go @@ -1,6 +1,7 @@ package ocxcli import ( + "encoding/json" "errors" "os" "os/exec" @@ -242,3 +243,89 @@ func TestDoctorResponseTempReclaimFragmentMatchesTypeScriptDoctor(t *testing.T) t.Fatalf("response-temp reclaim bytes\n got: %q\nwant: %q", got, goSection) } } + +func TestDoctorProbe3StableFragmentsMatchTypeScriptDoctor(t *testing.T) { + home := t.TempDir() + codexHome := filepath.Join(home, "codex") + if err := os.MkdirAll(filepath.Join(codexHome, "agents"), 0o700); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(codexHome, "agents", "reviewer.toml"), []byte("model_fallback = []\n"), 0o600); err != nil { + t.Fatal(err) + } + t.Setenv("HOME", home) + t.Setenv("OPENCODEX_HOME", home) + t.Setenv("CODEX_HOME", codexHome) + for _, key := range []string{"HTTP_PROXY", "http_proxy", "HTTPS_PROXY", "https_proxy", "ALL_PROXY", "all_proxy", "NO_PROXY", "no_proxy"} { + t.Setenv(key, "") + } + oracle, exitCode := runTypeScriptDoctor(t, home, codexHome) + if exitCode != 0 { + t.Fatalf("TypeScript doctor exit = %d; output=%s", exitCode, oracle) + } + if got := strings.Join(FormatDoctorOrcaHome(CollectDoctorOrcaHome()), "\n"); got != doctorSection(oracle, "Codex app home targeting", "Codex restart safety") { + t.Fatalf("Orca home bytes\n got: %q\nwant: %q", got, doctorSection(oracle, "Codex app home targeting", "Codex restart safety")) + } + extra := CollectStatusExtraDomains(ReadStatusConfigDiagnostics(), StatusExtraDeps{}) + if got := strings.Join(FormatDoctorRestartSafety(CollectDoctorRestartSafety(extra.Startup)), "\n"); got != doctorSection(oracle, "Codex restart safety", "Codex runtime selection") { + t.Fatalf("restart safety bytes\n got: %q\nwant: %q", got, doctorSection(oracle, "Codex restart safety", "Codex runtime selection")) + } + if got := strings.Join(FormatDoctorRuntimeSelection(CollectDoctorRuntimeSelection()), "\n"); got != doctorSection(oracle, "Codex runtime selection", "Current doctor process proxy env (presence only)") { + t.Fatalf("runtime selection bytes\n got: %q\nwant: %q", got, doctorSection(oracle, "Codex runtime selection", "Current doctor process proxy env (presence only)")) + } + if got := strings.Join(FormatDoctorAgentRoles(CollectDoctorAgentRoles(codexHome)), "\n"); got != doctorSection(oracle, "Codex agent role files", "OAuth reliability") { + t.Fatalf("agent role bytes\n got: %q\nwant: %q", got, doctorSection(oracle, "Codex agent role files", "OAuth reliability")) + } +} + +func TestDoctorProbe3PureFormatContracts(t *testing.T) { + if got := strings.Join(FormatDoctorServiceMemory(DoctorServiceMemoryReport{Status: "unreachable", Error: "fetch failed"}, "1.2.3"), "\n"); got != " -- doctor process Bun 1.2.3 (this is NOT the service process)\n -- proxy not reachable (not running?) [fetch failed]" { + t.Fatalf("memory fallback = %q", got) + } + if got := strings.Join(FormatDoctorProjectConfigs(nil), "\n"); got != "Project Codex configs\n ok no project-local provider bypass detected" { + t.Fatalf("project fallback = %q", got) + } + if got := strings.Join(FormatDoctorCatalogState(DoctorCatalogState{State: "fresh"}), "\n"); got != " [OK] Codex app-server model catalog is current with the on-disk catalog." { + t.Fatalf("catalog = %q", got) + } +} + +func TestDoctorServiceMemoryFormatterMatchesTypeScriptOracle(t *testing.T) { + repo := typeScriptOracleRepo(t) + script := "import { formatServiceMemoryLines } from \"./src/cli/doctor\";\n" + + "const input = JSON.parse(process.env.OCX_MEMORY_INPUT);\n" + + "process.stdout.write(JSON.stringify(formatServiceMemoryLines(input)));" + input := map[string]any{ + "status": "ok", + "data": map[string]any{ + "pid": 42, "bunVersion": "1.3.14", "platform": "linux", + "rss": 5 * 1024 * 1024 * 1024, "heapUsed": 100 * 1024 * 1024, + "external": 0, "arrayBuffers": 0, "streamMode": "auto", + "eagerRelay": nil, "jscHeap": nil, "watchdog": nil, + }, + } + encoded, err := json.Marshal(input) + if err != nil { + t.Fatal(err) + } + cmd := exec.Command("bun", "-e", script) + cmd.Dir, cmd.Env = repo, append(os.Environ(), "OCX_MEMORY_INPUT="+string(encoded)) + out, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("TypeScript service-memory oracle: %v: %s", err, out) + } + var want []string + if err := json.Unmarshal(out, &want); err != nil { + t.Fatalf("decode service-memory oracle: %v: %s", err, out) + } + got := FormatDoctorServiceMemory(DoctorServiceMemoryReport{Status: "ok", Data: DoctorServiceMemoryData{ + PID: 42, BunVersion: "1.3.14", Platform: "linux", RSS: 5 * 1024 * 1024 * 1024, + HeapUsed: 100 * 1024 * 1024, StreamMode: "auto", + }}, "oracle") + // Bun.version belongs to the oracle process, so compare all output that is + // determined by the shared endpoint payload verbatim. + got[0] = want[0] + if gotText, wantText := strings.Join(got, "\n"), strings.Join(want, "\n"); gotText != wantText { + t.Fatalf("service memory bytes\n got: %q\nwant: %q", gotText, wantText) + } +} From c1b6bda8d96d4f5193edf6908a67549e16bf8217 Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Mon, 7 Sep 2026 03:17:04 +0800 Subject: [PATCH 093/165] feat(go): assemble doctor command baseline --- go/internal/ocxcli/doctor_command.go | 215 ++++++++++++++++++++++ go/internal/ocxcli/doctor_command_test.go | 125 +++++++++++++ 2 files changed, 340 insertions(+) create mode 100644 go/internal/ocxcli/doctor_command.go create mode 100644 go/internal/ocxcli/doctor_command_test.go diff --git a/go/internal/ocxcli/doctor_command.go b/go/internal/ocxcli/doctor_command.go new file mode 100644 index 0000000000..bdd4c0a1e0 --- /dev/null +++ b/go/internal/ocxcli/doctor_command.go @@ -0,0 +1,215 @@ +package ocxcli + +import ( + "fmt" + "io" + "os" + "path/filepath" + "strings" + + "github.com/lidge-jun/opencodex/go/internal/config" +) + +// DoctorTODOSection records a TypeScript doctor section for which Go has no +// equivalent probe yet. Keeping this list in the assembler makes the remaining +// ownership work explicit: callers get a complete ordered report without +// pretending that an unavailable probe has a useful result. +type DoctorTODOSection struct { + Heading string + Reason string +} + +// DoctorCommandResult is the native assembly seam. Doctor remains +// TypeScript-owned; this result is deliberately not wired into Run yet. +type DoctorCommandResult struct { + Text string + Stderr string + Exit int + TODOs []DoctorTODOSection +} + +// DoctorCommandDeps supplies the already-portable doctor probes. Each probe is +// injectable so the complete assembly can be compared to TypeScript without a +// live service, OAuth store, or network request. +type DoctorCommandDeps struct { + Paths func() []DoctorPathRow + Mounts func() string + ResponseTemps func(bool) DoctorResponseTempResult + Env func() map[string]string + Config func() StatusConfigDiagnostic + OrderedProviders func() *config.OrderedValue + CodexConfigText func() string + ServiceToken func() bool + Shim func() DoctorShimDiagnostic + RunningProxyEnv func() DoctorRunningProxyEnv + ProxyDownHint func() string +} + +func defaultDoctorCommandDeps(deps DoctorCommandDeps) DoctorCommandDeps { + if deps.Paths == nil { + deps.Paths = CollectDoctorPaths + } + if deps.Mounts == nil { + deps.Mounts = ReadDoctorMounts + } + if deps.ResponseTemps == nil { + deps.ResponseTemps = func(reclaim bool) DoctorResponseTempResult { + if reclaim { + return ReclaimDoctorResponseTemps() + } + return InspectDoctorResponseTemps() + } + } + if deps.Env == nil { + deps.Env = doctorProcessEnv + } + if deps.Config == nil { + deps.Config = ReadStatusConfigDiagnostics + } + if deps.OrderedProviders == nil { + deps.OrderedProviders = func() *config.OrderedValue { + dir, err := config.Dir() + if err != nil { + return nil + } + ordered, err := config.LoadOrderedFromDir(dir) + if err != nil { + return nil + } + return ordered.Find("providers") + } + } + if deps.CodexConfigText == nil { + deps.CodexConfigText = func() string { + raw, _ := os.ReadFile(filepath.Join(doctorCodexHome(), "config.toml")) + return string(raw) + } + } + if deps.ServiceToken == nil { + deps.ServiceToken = func() bool { + dir, err := config.Dir() + if err != nil { + return false + } + raw, err := os.ReadFile(filepath.Join(dir, "service-api-token")) + return err == nil && strings.TrimSpace(string(raw)) != "" + } + } + if deps.Shim == nil { + deps.Shim = func() DoctorShimDiagnostic { return DoctorShimDiagnostic{} } + } + if deps.RunningProxyEnv == nil { + deps.RunningProxyEnv = func() DoctorRunningProxyEnv { return CollectDoctorRunningProxyEnv(int(readStatusPIDFile()), nil) } + } + if deps.ProxyDownHint == nil { + deps.ProxyDownHint = func() string { return "" } + } + return deps +} + +var doctorCommandTODOs = []DoctorTODOSection{ + {"Codex app home targeting", "Orca/Codex home diagnostic has not been ported."}, + {"Codex restart safety", "startup/restart safety diagnostic has not been ported."}, + {"Codex runtime selection", "runtime selection and live version diagnostics have not been ported."}, + {"Memory / runtime", "live service memory/runtime diagnostic has not been ported."}, + {"WHAM reachability", "WHAM network reachability probe has not been ported."}, + {"Codex history metadata restore", "history metadata restore diagnostic has not been ported."}, + {"Codex native-write coordinator", "native-write coordinator diagnostic has not been ported."}, + {"Project Codex configs", "project config bypass diagnostic has not been ported."}, + {"Codex agent role files", "agent-role model_fallback diagnostic has not been ported."}, + {"WSL Codex installs", "WSL dual-install diagnostic has not been ported."}, + {"OAuth reliability", "OAuth health and catalog freshness diagnostics have not been ported."}, + {"Hints", "remaining hints need their source diagnostics; proxy-down hint is assembled when supplied."}, +} + +func doctorTODO(section DoctorTODOSection) []string { + return []string{section.Heading, " TODO: " + section.Reason} +} + +const doctorJSONUsage = "ocx doctor does not support --json yet. Run `ocx doctor` for the human report, or use `ocx status --json` and `ocx ready --json` for machine-readable health.\n" +const doctorJSONExit = 2 + +// doctorJSONOption mirrors isJsonOption in src/cli/runtime-api.ts. Doctor is +// prose-only until its remaining diagnostics have a structured contract, so a +// JSON-looking flag must fail instead of silently printing prose to stdout. +func doctorJSONOption(arg string) bool { + normalized := strings.Map(func(r rune) rune { + switch r { + case '‐', '‑', '‒', '–', '—', '−': + return '-' + default: + return r + } + }, arg) + body := strings.TrimLeft(normalized, "-") + return body == "json" || strings.HasPrefix(body, "json=") +} + +// AssembleDoctorCommand preserves runDoctor's report ordering. It implements +// only the probes that have native evidence. The explicit TODO sections are a +// convergence ledger, not substitute diagnostics. +func AssembleDoctorCommand(args []string, deps DoctorCommandDeps) DoctorCommandResult { + deps = defaultDoctorCommandDeps(deps) + for _, arg := range args { + if doctorJSONOption(arg) { + return DoctorCommandResult{Stderr: doctorJSONUsage, Exit: doctorJSONExit, TODOs: append([]DoctorTODOSection(nil), doctorCommandTODOs...)} + } + } + // The two action modes are intentionally not emulated: they write runtime or + // Codex state and need their TypeScript transaction ports before ownership can + // move. Reporting failure is safer than an apparent successful no-op. + for _, arg := range args { + if arg == "--fix-codex-runtime" || arg == "--recover-zero-byte-coordinator" { + return DoctorCommandResult{Text: "opencodex doctor\n\nTODO: " + arg + " requires its TypeScript diagnostic and recovery transaction.\n", Exit: ExitFailure, TODOs: append([]DoctorTODOSection(nil), doctorCommandTODOs...)} + } + } + reclaim := false + var reclaimWarnings []string + for _, arg := range args { + if arg == "--reclaim-response-temps" { + reclaim = true + } + if arg != "--reclaim-response-temps" && strings.HasPrefix(arg, "--reclaim") { + reclaimWarnings = append(reclaimWarnings, " !! Unrecognized flag "+arg+"; did you mean --reclaim-response-temps? Reporting only.") + } + } + env := deps.Env() + diagnostic := deps.Config() + sections := [][]string{ + FormatDoctorPaths(deps.Paths(), deps.Mounts()), + append([]string{"Response-state temp files"}, append(reclaimWarnings, FormatDoctorResponseTemps(deps.ResponseTemps(reclaim), reclaim)...)...), + doctorTODO(doctorCommandTODOs[0]), doctorTODO(doctorCommandTODOs[1]), doctorTODO(doctorCommandTODOs[2]), + FormatDoctorCurrentProxyEnv(CollectDoctorProxyEnv(env)), + FormatDoctorConfiguredProxy(CollectDoctorConfiguredProxy(diagnostic, env)), + FormatDoctorProviderAPIKeys(CollectDoctorProviderAPIKeysOrdered(deps.OrderedProviders(), env)), + FormatDoctorCodexEnvKeyReadiness(CollectDoctorCodexEnvKeyReadiness(deps.CodexConfigText(), env, deps.Shim(), deps.ServiceToken())), + FormatDoctorRunningProxyEnv(deps.RunningProxyEnv()), + doctorTODO(doctorCommandTODOs[3]), doctorTODO(doctorCommandTODOs[4]), doctorTODO(doctorCommandTODOs[5]), doctorTODO(doctorCommandTODOs[6]), doctorTODO(doctorCommandTODOs[7]), doctorTODO(doctorCommandTODOs[8]), doctorTODO(doctorCommandTODOs[9]), doctorTODO(doctorCommandTODOs[10]), + } + last := doctorTODO(doctorCommandTODOs[11]) + if hint := deps.ProxyDownHint(); hint != "" { + last = append(last, " - "+hint) + } + sections = append(sections, last) + parts := make([]string, 0, len(sections)) + for _, section := range sections { + parts = append(parts, strings.Join(section, "\n")) + } + return DoctorCommandResult{Text: "opencodex doctor\n\n" + strings.Join(parts, "\n\n") + "\n", Exit: ExitOK, TODOs: append([]DoctorTODOSection(nil), doctorCommandTODOs...)} +} + +// RunDoctorCommand is the future command dispatch target. It exists now so +// argument and exit behavior are testable while cli.go retains TypeScript +// ownership. +func RunDoctorCommand(args []string, stdout, stderr io.Writer, deps DoctorCommandDeps) int { + result := AssembleDoctorCommand(args, deps) + if result.Stderr != "" { + if _, err := fmt.Fprint(stderr, result.Stderr); err != nil { + return ExitFailure + } + } + if _, err := fmt.Fprint(stdout, result.Text); err != nil { + return ExitFailure + } + return result.Exit +} diff --git a/go/internal/ocxcli/doctor_command_test.go b/go/internal/ocxcli/doctor_command_test.go new file mode 100644 index 0000000000..bbdf4a430c --- /dev/null +++ b/go/internal/ocxcli/doctor_command_test.go @@ -0,0 +1,125 @@ +package ocxcli + +import ( + "bytes" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/lidge-jun/opencodex/go/internal/config" +) + +// TestDoctorCommandAssemblyMatchesTypeScriptOracle compares the complete +// native report assembly to a real TypeScript doctor invocation one section at +// a time. The rows called out below are the only rows with native probes; all +// other headings are deliberately retained as TODO convergence work. +func TestDoctorCommandAssemblyMatchesTypeScriptOracle(t *testing.T) { + home := t.TempDir() + codexHome := filepath.Join(home, "codex") + if err := os.MkdirAll(codexHome, 0o700); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(home, "config.json"), []byte("{\"proxy\":\"$DOCTOR_PROXY\",\"providers\":{\"missing\":{\"adapter\":\"openai-chat\",\"baseUrl\":\"https://example.test/v1\",\"authMode\":\"key\",\"apiKey\":\"$DOCTOR_KEY\",\"defaultModel\":\"m\"}},\"defaultProvider\":\"missing\"}"), 0o600); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(codexHome, "config.toml"), []byte("model_provider = \"opencodex\"\n\n[model_providers.opencodex]\nenv_key = \"OPENCODEX_API_AUTH_TOKEN\"\n"), 0o600); err != nil { + t.Fatal(err) + } + temp := filepath.Join(home, "responses-state.json.ocx.999999.1.tmp") + if err := os.WriteFile(temp, make([]byte, 2*1024*1024), 0o600); err != nil { + t.Fatal(err) + } + old := time.Now().Add(-16 * time.Minute) + if err := os.Chtimes(temp, old, old); err != nil { + t.Fatal(err) + } + t.Setenv("HOME", home) + t.Setenv("OPENCODEX_HOME", home) + t.Setenv("CODEX_HOME", codexHome) + t.Setenv("DOCTOR_PROXY", "") + t.Setenv("DOCTOR_KEY", "") + t.Setenv("OPENCODEX_API_AUTH_TOKEN", "") + for _, key := range []string{"HTTP_PROXY", "http_proxy", "HTTPS_PROXY", "https_proxy", "ALL_PROXY", "all_proxy", "NO_PROXY", "no_proxy"} { + t.Setenv(key, "") + } + + oracle, exitCode := runTypeScriptDoctor(t, home, codexHome) + if exitCode != ExitOK { + t.Fatalf("TypeScript doctor exit = %d; output=%s", exitCode, oracle) + } + result := AssembleDoctorCommand(nil, DoctorCommandDeps{}) + if result.Exit != ExitOK { + t.Fatalf("native assembly exit = %d; output=%s", result.Exit, result.Text) + } + for _, section := range []struct{ heading, next string }{ + {"Paths", "Response-state temp files"}, + {"Response-state temp files", "Codex app home targeting"}, + {"Current doctor process proxy env (presence only)", "Configured proxy (value hidden)"}, + {"Configured proxy (value hidden)", "Provider API keys (value hidden)"}, + {"Provider API keys (value hidden)", "Codex env_key launch readiness"}, + {"Codex env_key launch readiness", "Running proxy process proxy env (presence only)"}, + {"Running proxy process proxy env (presence only)", "Memory / runtime"}, + } { + got := doctorSection(result.Text, section.heading, section.next) + want := doctorSection(oracle, section.heading, section.next) + if got != want { + t.Fatalf("%s differs from TypeScript oracle\nGo: %q\nTypeScript: %q", section.heading, got, want) + } + } + for _, todo := range doctorCommandTODOs { + if !strings.Contains(result.Text, todo.Heading+"\n TODO: "+todo.Reason) { + t.Fatalf("missing TODO convergence section %#v", todo) + } + } +} + +func TestDoctorCommandAssemblyArgumentsAndTODOBoundary(t *testing.T) { + var reclamations []bool + deps := DoctorCommandDeps{ + Paths: func() []DoctorPathRow { return []DoctorPathRow{{Label: "CODEX_HOME", Path: "/codex"}} }, + Mounts: func() string { return "" }, + ResponseTemps: func(reclaim bool) DoctorResponseTempResult { + reclamations = append(reclamations, reclaim) + return DoctorResponseTempResult{} + }, + Env: func() map[string]string { return map[string]string{} }, + Config: func() StatusConfigDiagnostic { return StatusConfigDiagnostic{Source: "default"} }, + OrderedProviders: func() *config.OrderedValue { return nil }, + CodexConfigText: func() string { return "" }, + ServiceToken: func() bool { return false }, + Shim: func() DoctorShimDiagnostic { return DoctorShimDiagnostic{} }, + RunningProxyEnv: func() DoctorRunningProxyEnv { return DoctorRunningProxyEnv{Status: "not_running"} }, + ProxyDownHint: func() string { return "hint" }, + } + result := AssembleDoctorCommand([]string{"--reclaim-response-tempz", "--reclaim-response-temps"}, deps) + if result.Exit != ExitOK || len(reclamations) != 1 || !reclamations[0] { + t.Fatalf("reclaim result = %#v, calls=%#v", result, reclamations) + } + if !strings.Contains(result.Text, "Unrecognized flag --reclaim-response-tempz; did you mean --reclaim-response-temps? Reporting only.") || !strings.Contains(result.Text, "Hints\n TODO: "+doctorCommandTODOs[len(doctorCommandTODOs)-1].Reason+"\n - hint") { + t.Fatalf("argument output = %q", result.Text) + } + var output bytes.Buffer + if code := RunDoctorCommand([]string{"--recover-zero-byte-coordinator"}, &output, &output, deps); code != ExitFailure { + t.Fatalf("unported recovery exit = %d", code) + } + if !strings.Contains(output.String(), "TODO: --recover-zero-byte-coordinator requires its TypeScript diagnostic and recovery transaction.") { + t.Fatalf("recovery TODO = %q", output.String()) + } + // The command dispatcher owns this rejection today. Keep the future native + // boundary byte-identical to its real TypeScript oracle before doctor moves. + tsJSON, tsJSONExit := runTypeScriptDoctorArgs(t, t.TempDir(), t.TempDir(), "--json=true") + if tsJSONExit != doctorJSONExit || tsJSON != doctorJSONUsage { + t.Fatalf("TypeScript --json=true = exit %d, output %q; want exit %d, output %q", tsJSONExit, tsJSON, doctorJSONExit, doctorJSONUsage) + } + for _, arg := range []string{"--json", "--json=true", "-json", "——json"} { + var stdout, stderr bytes.Buffer + if code := RunDoctorCommand([]string{arg}, &stdout, &stderr, deps); code != doctorJSONExit { + t.Fatalf("%q exit = %d, want usage", arg, code) + } + if stdout.Len() != 0 || stderr.String() != doctorJSONUsage { + t.Fatalf("%q output stdout=%q stderr=%q", arg, stdout.String(), stderr.String()) + } + } +} From beaebb169b6bcf0b9882ca38bba327560ff1e3dc Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Mon, 7 Sep 2026 03:23:18 +0800 Subject: [PATCH 094/165] feat(go): add deep doctor collectors --- go/internal/ocxcli/doctor_probes4.go | 267 ++++++++++++++++++++++ go/internal/ocxcli/doctor_probes4_test.go | 54 +++++ 2 files changed, 321 insertions(+) create mode 100644 go/internal/ocxcli/doctor_probes4.go create mode 100644 go/internal/ocxcli/doctor_probes4_test.go diff --git a/go/internal/ocxcli/doctor_probes4.go b/go/internal/ocxcli/doctor_probes4.go new file mode 100644 index 0000000000..457b78179b --- /dev/null +++ b/go/internal/ocxcli/doctor_probes4.go @@ -0,0 +1,267 @@ +package ocxcli + +import ( + "context" + "crypto/rand" + "crypto/sha256" + "database/sql" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "net/http" + "os" + "path/filepath" + "strings" + "time" + + "github.com/lidge-jun/opencodex/go/internal/managementauth" + _ "modernc.org/sqlite" +) + +const doctorSystemMemoryPath = "/api/system/memory" +const doctorCodexAccountsPath = "/api/codex-auth/accounts" + +type DoctorManagementReader struct { + Runtime RuntimeState + Client *http.Client + Now func() time.Time + Nonce func() (string, error) +} + +func doctorManagementNonce() (string, error) { + b := make([]byte, 32) + if _, err := rand.Read(b); err != nil { + return "", err + } + return base64.RawURLEncoding.EncodeToString(b), nil +} +func (r DoctorManagementReader) Get(ctx context.Context, path string) (*http.Response, error) { + if r.Runtime.PID <= 0 || r.Runtime.Port < 1 || r.Runtime.Port > 65535 || !managementauth.IsAttestationSecret(r.Runtime.AttestationSecret) { + return nil, errors.New("unattested target") + } + nonceFn := r.Nonce + if nonceFn == nil { + nonceFn = doctorManagementNonce + } + nonce, err := nonceFn() + if err != nil { + return nil, err + } + now := time.Now() + if r.Now != nil { + now = r.Now() + } + expires := now.Add(10 * time.Second).UnixMilli() + cap := managementauth.CreateLocalManagementReadCapability(r.Runtime.AttestationSecret, nonce, http.MethodGet, path, r.Runtime.PID, r.Runtime.Port, expires) + if cap == "" { + return nil, errors.New("capability unavailable") + } + host := strings.Trim(r.Runtime.Hostname, "[] ") + if host == "" || host == "0.0.0.0" || host == "::" { + host = "127.0.0.1" + } + if strings.Contains(host, ":") { + host = "[" + host + "]" + } + req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("http://%s:%d%s", host, r.Runtime.Port, path), nil) + if err != nil { + return nil, err + } + req.Header.Set(managementauth.LocalManagementExpectedPIDHeader, fmt.Sprint(r.Runtime.PID)) + req.Header.Set(managementauth.LocalManagementNonceHeader, nonce) + req.Header.Set(managementauth.LocalManagementExpiresAtHeader, fmt.Sprint(expires)) + req.Header.Set(managementauth.LocalManagementCapabilityHeader, cap) + c := r.Client + if c == nil { + c = &http.Client{Timeout: 2 * time.Second} + } + return c.Do(req) +} + +func FetchDoctorServiceMemory(ctx context.Context, r DoctorManagementReader) DoctorServiceMemoryReport { + response, err := r.Get(ctx, doctorSystemMemoryPath) + if err != nil { + return DoctorServiceMemoryReport{Status: "unreachable", Error: "fetch failed"} + } + defer response.Body.Close() + if response.StatusCode == 401 || response.StatusCode == 403 { + return DoctorServiceMemoryReport{Status: "unauthorized"} + } + if response.StatusCode < 200 || response.StatusCode >= 300 { + return DoctorServiceMemoryReport{Status: "unreachable", Error: fmt.Sprintf("http %d", response.StatusCode)} + } + var body struct { + PID int + BunVersion string + Platform string + RSS, HeapUsed, External, ArrayBuffers, ObservedBytes int64 + ObservedMetric, StreamMode string + } + if json.NewDecoder(response.Body).Decode(&body) != nil || body.PID != int(r.Runtime.PID) || body.BunVersion == "" { + return DoctorServiceMemoryReport{Status: "unreachable", Error: "malformed response"} + } + return DoctorServiceMemoryReport{Status: "ok", Data: DoctorServiceMemoryData{PID: body.PID, BunVersion: body.BunVersion, Platform: body.Platform, RSS: body.RSS, HeapUsed: body.HeapUsed, External: body.External, ArrayBuffers: body.ArrayBuffers, ObservedBytes: body.ObservedBytes, ObservedMetric: body.ObservedMetric, StreamMode: body.StreamMode}} +} + +type DoctorOAuthHealthSource string + +const ( + DoctorOAuthManagementAPI DoctorOAuthHealthSource = "management-api" + DoctorOAuthUnavailable DoctorOAuthHealthSource = "unavailable" + DoctorOAuthAuthFailed DoctorOAuthHealthSource = "management-auth-failed" + DoctorOAuthAPIUnavailable DoctorOAuthHealthSource = "management-api-unavailable" +) + +type DoctorOAuthAccount struct { + ID, Status, Reason, Until string + NeedsReauth bool +} + +func CollectDoctorLiveCodexAccounts(ctx context.Context, r *DoctorManagementReader) (DoctorOAuthHealthSource, []DoctorOAuthAccount) { + if r == nil { + return DoctorOAuthUnavailable, nil + } + response, err := r.Get(ctx, doctorCodexAccountsPath) + if err != nil { + return DoctorOAuthAPIUnavailable, nil + } + defer response.Body.Close() + if response.StatusCode == 401 || response.StatusCode == 403 { + return DoctorOAuthAuthFailed, nil + } + if response.StatusCode < 200 || response.StatusCode >= 300 { + return DoctorOAuthAPIUnavailable, nil + } + var body struct { + Accounts []struct { + ID string + NeedsReauth bool + Health struct{ Status, Reason, Until string } + } + } + if json.NewDecoder(response.Body).Decode(&body) != nil || body.Accounts == nil { + return DoctorOAuthAPIUnavailable, nil + } + accounts := make([]DoctorOAuthAccount, 0, len(body.Accounts)) + for _, a := range body.Accounts { + if a.ID == "" { + continue + } + status := a.Health.Status + if status != "healthy" && status != "cooldown" && status != "reauth_required" && status != "warning" { + if a.NeedsReauth { + status, a.Health.Reason = "reauth_required", "refresh_failed" + } else { + status = "healthy" + } + } + accounts = append(accounts, DoctorOAuthAccount{a.ID, status, a.Health.Reason, a.Health.Until, a.NeedsReauth}) + } + return DoctorOAuthManagementAPI, accounts +} + +type DoctorCatalogProbe struct { + PIDs []int + Starts map[int]*time.Time + CatalogMtime *time.Time + EnumerationFailed bool +} + +func CollectDoctorCatalogState(p DoctorCatalogProbe) DoctorCatalogState { + if len(p.PIDs) == 0 { + if p.EnumerationFailed { + return DoctorCatalogState{State: "unknown"} + } + return DoctorCatalogState{State: "not_running"} + } + if p.CatalogMtime == nil { + return DoctorCatalogState{State: "unknown"} + } + for _, pid := range p.PIDs { + s := p.Starts[pid] + if s == nil { + return DoctorCatalogState{State: "unknown"} + } + if !s.After(*p.CatalogMtime) { + return DoctorCatalogState{State: "stale", PIDs: append([]int(nil), p.PIDs...)} + } + } + return DoctorCatalogState{State: "fresh", PIDs: append([]int(nil), p.PIDs...)} +} + +type DoctorHistoryPending struct { + PendingRows, BackupEntries int + Failed bool + FailureReason string +} + +func DoctorHistoryBackupPath(stateDB, home string) string { + p, err := filepath.Abs(stateDB) + if err != nil { + p = stateDB + } + if os.PathSeparator == '\\' { + p = strings.ToLower(p) + } + sum := sha256.Sum256([]byte(p)) + return filepath.Join(home, fmt.Sprintf("codex-history-backup-%x.json", sum[:8])) +} +func CollectDoctorHistoryPending(stateDB, backup string) DoctorHistoryPending { + raw, err := os.ReadFile(backup) + if err != nil { + if os.IsNotExist(err) { + return DoctorHistoryPending{} + } + return DoctorHistoryPending{Failed: true, FailureReason: "permission"} + } + var manifest struct { + Version int + StateDBPath string + Entries map[string]json.RawMessage + } + if json.Unmarshal(raw, &manifest) != nil || (manifest.Version != 1 && manifest.Version != 2) || manifest.Entries == nil { + return DoctorHistoryPending{Failed: true, FailureReason: "integrity"} + } + want, _ := filepath.Abs(stateDB) + got, _ := filepath.Abs(manifest.StateDBPath) + if want != got { + return DoctorHistoryPending{Failed: true, FailureReason: "integrity"} + } + n := len(manifest.Entries) + if _, err := os.Stat(stateDB); err != nil { + if n == 0 && os.IsNotExist(err) { + return DoctorHistoryPending{} + } + return DoctorHistoryPending{BackupEntries: n, Failed: true, FailureReason: "integrity"} + } + db, err := sql.Open("sqlite", "file:"+stateDB+"?mode=ro&_pragma=busy_timeout(100)") + if err != nil { + return DoctorHistoryPending{BackupEntries: n, Failed: true, FailureReason: "integrity"} + } + defer db.Close() + var one int + if err := db.QueryRow("SELECT 1 FROM threads LIMIT 1").Scan(&one); err != nil && err != sql.ErrNoRows { + return DoctorHistoryPending{BackupEntries: n, Failed: true, FailureReason: "integrity"} + } + return DoctorHistoryPending{BackupEntries: n} +} +func FormatDoctorHistoryPending(d DoctorHistoryPending) []string { + if d.Failed { + if d.FailureReason == "busy" { + return []string{" -- history database, backup manifest, or rollout file is busy — exact metadata restore is pending"} + } + if d.FailureReason == "permission" { + return []string{" -- state DB or backup manifest access was denied — restore state unknown"} + } + return []string{" -- backup manifest or restore target failed integrity checks — manual review required"} + } + if d.PendingRows == 0 && d.BackupEntries == 0 { + return []string{" ok no manifest-backed provider metadata pending; untracked routed history is unchanged"} + } + word := "entries" + if d.BackupEntries == 1 { + word = "entry" + } + return []string{fmt.Sprintf(" -- %d backup manifest %s pending exact metadata restore", d.BackupEntries, word)} +} diff --git a/go/internal/ocxcli/doctor_probes4_test.go b/go/internal/ocxcli/doctor_probes4_test.go new file mode 100644 index 0000000000..4bf3bebf3e --- /dev/null +++ b/go/internal/ocxcli/doctor_probes4_test.go @@ -0,0 +1,54 @@ +package ocxcli + +import ( + "context" + "fmt" + "net" + "net/http" + "net/http/httptest" + "net/url" + "strconv" + "testing" + "time" + + "github.com/lidge-jun/opencodex/go/internal/managementauth" +) + +func TestDoctorDeepAttestedCollectors(t *testing.T) { + secret := "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG" + pid := int64(4242) + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Header.Get(managementauth.LocalManagementExpectedPIDHeader) != "4242" || r.Header.Get(managementauth.LocalManagementCapabilityHeader) == "" { + t.Error("missing capability") + } + switch r.URL.Path { + case doctorSystemMemoryPath: + fmt.Fprint(w, "{\"PID\":4242,\"BunVersion\":\"1.2.3\",\"Platform\":\"linux\",\"RSS\":1048576}") + case doctorCodexAccountsPath: + fmt.Fprint(w, "{\"Accounts\":[{\"ID\":\"account-12345\",\"Health\":{\"Status\":\"cooldown\",\"Reason\":\"quota\",\"Until\":\"2026-01-01T00:00:00.000Z\"}}]}") + default: + http.NotFound(w, r) + } + })) + defer server.Close() + u, _ := url.Parse(server.URL) + host, portText, _ := net.SplitHostPort(u.Host) + port, _ := strconv.Atoi(portText) + reader := DoctorManagementReader{Runtime: RuntimeState{PID: pid, Port: port, Hostname: host, AttestationSecret: secret}, Client: server.Client()} + if got := FetchDoctorServiceMemory(context.Background(), reader); got.Status != "ok" || got.Data.RSS != 1048576 { + t.Fatalf("memory=%#v", got) + } + source, accounts := CollectDoctorLiveCodexAccounts(context.Background(), &reader) + if source != DoctorOAuthManagementAPI || len(accounts) != 1 || accounts[0].Status != "cooldown" { + t.Fatalf("accounts=%s %#v", source, accounts) + } +} + +func TestDoctorCatalogEqualMtimeIsStale(t *testing.T) { + mtime := time.Unix(10, 0) + equal := mtime + got := CollectDoctorCatalogState(DoctorCatalogProbe{PIDs: []int{7}, Starts: map[int]*time.Time{7: &equal}, CatalogMtime: &mtime}) + if got.State != "stale" { + t.Fatalf("state=%#v", got) + } +} From de85b7b5f8805df9c3f271902b55aa7f123f7a69 Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Mon, 7 Sep 2026 03:27:19 +0800 Subject: [PATCH 095/165] feat(go): assemble portable doctor probes --- go/internal/ocxcli/doctor_command.go | 63 ++++++++++++++++++----- go/internal/ocxcli/doctor_command_test.go | 50 ++++++++++++++++++ go/internal/ocxcli/doctor_probes4.go | 43 ++++++++++++++++ go/internal/ocxcli/doctor_probes4_test.go | 8 +++ 4 files changed, 151 insertions(+), 13 deletions(-) diff --git a/go/internal/ocxcli/doctor_command.go b/go/internal/ocxcli/doctor_command.go index bdd4c0a1e0..0aeefa9372 100644 --- a/go/internal/ocxcli/doctor_command.go +++ b/go/internal/ocxcli/doctor_command.go @@ -1,8 +1,10 @@ package ocxcli import ( + "context" "fmt" "io" + "net/http" "os" "path/filepath" "strings" @@ -42,6 +44,12 @@ type DoctorCommandDeps struct { ServiceToken func() bool Shim func() DoctorShimDiagnostic RunningProxyEnv func() DoctorRunningProxyEnv + OrcaHome func() DoctorOrcaHome + RestartSafety func() DoctorRestartSafety + RuntimeSelection func() DoctorRuntimeSelection + WHAM func() DoctorWhamResult + AgentRoles func() []string + WSL func() DoctorWslDiagnostic ProxyDownHint func() string } @@ -101,6 +109,31 @@ func defaultDoctorCommandDeps(deps DoctorCommandDeps) DoctorCommandDeps { if deps.RunningProxyEnv == nil { deps.RunningProxyEnv = func() DoctorRunningProxyEnv { return CollectDoctorRunningProxyEnv(int(readStatusPIDFile()), nil) } } + if deps.OrcaHome == nil { + deps.OrcaHome = CollectDoctorOrcaHome + } + if deps.RestartSafety == nil { + deps.RestartSafety = func() DoctorRestartSafety { + return CollectDoctorRestartSafety(CollectStatusExtraDomains(deps.Config(), StatusExtraDeps{}).Startup) + } + } + if deps.RuntimeSelection == nil { + deps.RuntimeSelection = CollectDoctorRuntimeSelection + } + if deps.WHAM == nil { + // The token-aware implementation lands with the native-profile collector. + // Until then this remains a real reachability probe without reading or + // serializing credentials. + deps.WHAM = func() DoctorWhamResult { + return ProbeDoctorWHAM(context.Background(), &http.Client{}, "", "") + } + } + if deps.AgentRoles == nil { + deps.AgentRoles = func() []string { return CollectDoctorAgentRoles(doctorCodexHome()) } + } + if deps.WSL == nil { + deps.WSL = CollectCurrentDoctorWslDualInstall + } if deps.ProxyDownHint == nil { deps.ProxyDownHint = func() string { return "" } } @@ -108,17 +141,11 @@ func defaultDoctorCommandDeps(deps DoctorCommandDeps) DoctorCommandDeps { } var doctorCommandTODOs = []DoctorTODOSection{ - {"Codex app home targeting", "Orca/Codex home diagnostic has not been ported."}, - {"Codex restart safety", "startup/restart safety diagnostic has not been ported."}, - {"Codex runtime selection", "runtime selection and live version diagnostics have not been ported."}, - {"Memory / runtime", "live service memory/runtime diagnostic has not been ported."}, - {"WHAM reachability", "WHAM network reachability probe has not been ported."}, + {"Memory / runtime", "doctor Bun runtime identity is unavailable in the Go process; service evidence is collected but cannot yet match TypeScript text."}, {"Codex history metadata restore", "history metadata restore diagnostic has not been ported."}, {"Codex native-write coordinator", "native-write coordinator diagnostic has not been ported."}, - {"Project Codex configs", "project config bypass diagnostic has not been ported."}, - {"Codex agent role files", "agent-role model_fallback diagnostic has not been ported."}, - {"WSL Codex installs", "WSL dual-install diagnostic has not been ported."}, - {"OAuth reliability", "OAuth health and catalog freshness diagnostics have not been ported."}, + {"Project Codex configs", "profile-aware project config bypass diagnostic has not been ported."}, + {"OAuth reliability", "credential-collision, refresh-lock, and catalog freshness diagnostics have not been ported; live Codex account health is collected but not rendered."}, {"Hints", "remaining hints need their source diagnostics; proxy-down hint is assembled when supplied."}, } @@ -178,21 +205,31 @@ func AssembleDoctorCommand(args []string, deps DoctorCommandDeps) DoctorCommandR sections := [][]string{ FormatDoctorPaths(deps.Paths(), deps.Mounts()), append([]string{"Response-state temp files"}, append(reclaimWarnings, FormatDoctorResponseTemps(deps.ResponseTemps(reclaim), reclaim)...)...), - doctorTODO(doctorCommandTODOs[0]), doctorTODO(doctorCommandTODOs[1]), doctorTODO(doctorCommandTODOs[2]), + FormatDoctorOrcaHome(deps.OrcaHome()), + FormatDoctorRestartSafety(deps.RestartSafety()), + FormatDoctorRuntimeSelection(deps.RuntimeSelection()), FormatDoctorCurrentProxyEnv(CollectDoctorProxyEnv(env)), FormatDoctorConfiguredProxy(CollectDoctorConfiguredProxy(diagnostic, env)), FormatDoctorProviderAPIKeys(CollectDoctorProviderAPIKeysOrdered(deps.OrderedProviders(), env)), FormatDoctorCodexEnvKeyReadiness(CollectDoctorCodexEnvKeyReadiness(deps.CodexConfigText(), env, deps.Shim(), deps.ServiceToken())), FormatDoctorRunningProxyEnv(deps.RunningProxyEnv()), - doctorTODO(doctorCommandTODOs[3]), doctorTODO(doctorCommandTODOs[4]), doctorTODO(doctorCommandTODOs[5]), doctorTODO(doctorCommandTODOs[6]), doctorTODO(doctorCommandTODOs[7]), doctorTODO(doctorCommandTODOs[8]), doctorTODO(doctorCommandTODOs[9]), doctorTODO(doctorCommandTODOs[10]), - } - last := doctorTODO(doctorCommandTODOs[11]) + doctorTODO(doctorCommandTODOs[0]), + FormatDoctorWHAM(deps.WHAM()), + doctorTODO(doctorCommandTODOs[1]), doctorTODO(doctorCommandTODOs[2]), doctorTODO(doctorCommandTODOs[3]), + FormatDoctorAgentRoles(deps.AgentRoles()), + FormatDoctorWslDualInstall(deps.WSL()), + doctorTODO(doctorCommandTODOs[4]), + } + last := doctorTODO(doctorCommandTODOs[5]) if hint := deps.ProxyDownHint(); hint != "" { last = append(last, " - "+hint) } sections = append(sections, last) parts := make([]string, 0, len(sections)) for _, section := range sections { + if len(section) == 0 { + continue + } parts = append(parts, strings.Join(section, "\n")) } return DoctorCommandResult{Text: "opencodex doctor\n\n" + strings.Join(parts, "\n\n") + "\n", Exit: ExitOK, TODOs: append([]DoctorTODOSection(nil), doctorCommandTODOs...)} diff --git a/go/internal/ocxcli/doctor_command_test.go b/go/internal/ocxcli/doctor_command_test.go index bbdf4a430c..53fad1c17a 100644 --- a/go/internal/ocxcli/doctor_command_test.go +++ b/go/internal/ocxcli/doctor_command_test.go @@ -56,6 +56,9 @@ func TestDoctorCommandAssemblyMatchesTypeScriptOracle(t *testing.T) { for _, section := range []struct{ heading, next string }{ {"Paths", "Response-state temp files"}, {"Response-state temp files", "Codex app home targeting"}, + {"Codex app home targeting", "Codex restart safety"}, + {"Codex restart safety", "Codex runtime selection"}, + {"Codex runtime selection", "Current doctor process proxy env (presence only)"}, {"Current doctor process proxy env (presence only)", "Configured proxy (value hidden)"}, {"Configured proxy (value hidden)", "Provider API keys (value hidden)"}, {"Provider API keys (value hidden)", "Codex env_key launch readiness"}, @@ -75,6 +78,47 @@ func TestDoctorCommandAssemblyMatchesTypeScriptOracle(t *testing.T) { } } +func TestDoctorCommandAssemblyUsesPortableProbe3Sections(t *testing.T) { + deps := DoctorCommandDeps{ + Paths: func() []DoctorPathRow { return nil }, Mounts: func() string { return "" }, + ResponseTemps: func(bool) DoctorResponseTempResult { return DoctorResponseTempResult{} }, + Env: func() map[string]string { return map[string]string{} }, + Config: func() StatusConfigDiagnostic { return StatusConfigDiagnostic{Source: "default"} }, + OrderedProviders: func() *config.OrderedValue { return nil }, CodexConfigText: func() string { return "" }, + ServiceToken: func() bool { return false }, Shim: func() DoctorShimDiagnostic { return DoctorShimDiagnostic{} }, + RunningProxyEnv: func() DoctorRunningProxyEnv { return DoctorRunningProxyEnv{Status: "not_running"} }, + OrcaHome: func() DoctorOrcaHome { return DoctorOrcaHome{EffectiveCodexHome: "/codex"} }, + RestartSafety: func() DoctorRestartSafety { + return DoctorRestartSafety{RebootSafe: true, Summary: "native Codex routing (no opencodex restart dependency)", Detail: "routing=native, service=not installed, shim=not installed"} + }, + RuntimeSelection: func() DoctorRuntimeSelection { return DoctorRuntimeSelection{Path: "codex", Source: "fallback"} }, + WHAM: func() DoctorWhamResult { + return DoctorWhamResult{OK: true, Classification: "ok", Duration: 12 * time.Millisecond} + }, + AgentRoles: func() []string { return []string{"reviewer"} }, + WSL: func() DoctorWslDiagnostic { + return DoctorWslDiagnostic{WSL: true, EffectiveCodexHome: "/home/a/.codex"} + }, + } + got := AssembleDoctorCommand(nil, deps).Text + for _, want := range []string{ + "Codex app home targeting\n ok Effective Codex home: /codex", + "Codex restart safety\n ok native Codex routing (no opencodex restart dependency)", + "Codex runtime selection\n ok Selected runtime: codex (unknown, source=fallback)", + "WHAM reachability\n ok https://chatgpt.com/backend-api/wham/usage\n error=ok, 12ms, unauthenticated", + "Codex agent role files\n [WARN] 1 agent role file contains `model_fallback`: reviewer", + "WSL Codex installs\n -- Linux ~/.codex/config.toml", + } { + if !strings.Contains(got, want) { + t.Fatalf("report missing %q in %q", want, got) + } + } + deps.WSL = func() DoctorWslDiagnostic { return DoctorWslDiagnostic{} } + if got := AssembleDoctorCommand(nil, deps).Text; strings.Contains(got, "WSL Codex installs") { + t.Fatalf("non-WSL report included an empty WSL section: %q", got) + } +} + func TestDoctorCommandAssemblyArgumentsAndTODOBoundary(t *testing.T) { var reclamations []bool deps := DoctorCommandDeps{ @@ -91,6 +135,12 @@ func TestDoctorCommandAssemblyArgumentsAndTODOBoundary(t *testing.T) { ServiceToken: func() bool { return false }, Shim: func() DoctorShimDiagnostic { return DoctorShimDiagnostic{} }, RunningProxyEnv: func() DoctorRunningProxyEnv { return DoctorRunningProxyEnv{Status: "not_running"} }, + OrcaHome: func() DoctorOrcaHome { return DoctorOrcaHome{} }, + RestartSafety: func() DoctorRestartSafety { return DoctorRestartSafety{} }, + RuntimeSelection: func() DoctorRuntimeSelection { return DoctorRuntimeSelection{} }, + WHAM: func() DoctorWhamResult { return DoctorWhamResult{} }, + AgentRoles: func() []string { return nil }, + WSL: func() DoctorWslDiagnostic { return DoctorWslDiagnostic{} }, ProxyDownHint: func() string { return "hint" }, } result := AssembleDoctorCommand([]string{"--reclaim-response-tempz", "--reclaim-response-temps"}, deps) diff --git a/go/internal/ocxcli/doctor_probes4.go b/go/internal/ocxcli/doctor_probes4.go index 457b78179b..175f5ea4e8 100644 --- a/go/internal/ocxcli/doctor_probes4.go +++ b/go/internal/ocxcli/doctor_probes4.go @@ -118,6 +118,49 @@ type DoctorOAuthAccount struct { NeedsReauth bool } +func doctorMaskAccountID(id string) string { + id = strings.TrimSpace(id) + if id == "" || len(id) <= 4 { + return "account-…" + } + return "account-…" + id[len(id)-4:] +} + +func FormatDoctorOAuthLive(source DoctorOAuthHealthSource, accounts []DoctorOAuthAccount) []string { + lines := []string{"OAuth reliability"} + switch source { + case DoctorOAuthUnavailable: + lines = append(lines, " [WARN] Codex account health unavailable (proxy not running). Action: start the proxy and re-run \x60ocx doctor\x60 to inspect live cooldown/reauth") + case DoctorOAuthAuthFailed: + lines = append(lines, " [WARN] Codex account health unavailable (proxy running; management authentication failed). Action: verify the admin token configuration, restart the proxy, and re-run \x60ocx doctor\x60") + case DoctorOAuthAPIUnavailable: + lines = append(lines, " [WARN] Codex account health unavailable (proxy running; management API response failed). Action: inspect the proxy service log, restart the proxy if needed, and re-run \x60ocx doctor\x60") + } + for _, account := range accounts { + if account.Status == "healthy" { + continue + } + masked := doctorMaskAccountID(account.ID) + switch account.Status { + case "reauth_required": + lines = append(lines, " [WARN] Account "+masked+" requires reauthentication. Action: reauthenticate via the dashboard Codex account pool") + case "cooldown": + prefix := "quota limited" + if account.Reason == "rate_limit" { + prefix = "rate limited" + } + lines = append(lines, " [WARN] Account "+masked+" is "+prefix+" until "+account.Until+". Action: wait until "+account.Until+" or start a new session with another eligible account") + case "warning": + detail := strings.ReplaceAll(account.Reason, "_", " ") + if detail == "" { + detail = "unhealthy" + } + lines = append(lines, " [WARN] Account "+masked+" has a "+detail+". Action: reauthenticate via the dashboard Codex account pool") + } + } + return append(lines, " [OK] Codex forward path uses pass-through client metadata (build-time invariant; not a runtime scan).") +} + func CollectDoctorLiveCodexAccounts(ctx context.Context, r *DoctorManagementReader) (DoctorOAuthHealthSource, []DoctorOAuthAccount) { if r == nil { return DoctorOAuthUnavailable, nil diff --git a/go/internal/ocxcli/doctor_probes4_test.go b/go/internal/ocxcli/doctor_probes4_test.go index 4bf3bebf3e..95a893b473 100644 --- a/go/internal/ocxcli/doctor_probes4_test.go +++ b/go/internal/ocxcli/doctor_probes4_test.go @@ -8,6 +8,7 @@ import ( "net/http/httptest" "net/url" "strconv" + "strings" "testing" "time" @@ -52,3 +53,10 @@ func TestDoctorCatalogEqualMtimeIsStale(t *testing.T) { t.Fatalf("state=%#v", got) } } + +func TestFormatDoctorOAuthLiveMatchesDoctorMessages(t *testing.T) { + lines := FormatDoctorOAuthLive(DoctorOAuthManagementAPI, []DoctorOAuthAccount{{ID: "account-12345", Status: "cooldown", Reason: "quota", Until: "2026-01-01T00:00:00.000Z"}}) + if got, want := strings.Join(lines, "\n"), "OAuth reliability\n [WARN] Account account-…2345 is quota limited until 2026-01-01T00:00:00.000Z. Action: wait until 2026-01-01T00:00:00.000Z or start a new session with another eligible account\n [OK] Codex forward path uses pass-through client metadata (build-time invariant; not a runtime scan)."; got != want { + t.Fatalf("OAuth text = %q", got) + } +} From e4fdbe68529da3f54f437f47a2f15dd5494af419 Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Mon, 7 Sep 2026 03:39:01 +0800 Subject: [PATCH 096/165] feat(go): port doctor memory history project probes --- go/internal/ocxcli/doctor_command.go | 39 +++- go/internal/ocxcli/doctor_command_test.go | 13 ++ go/internal/ocxcli/doctor_owner_unix.go | 13 ++ go/internal/ocxcli/doctor_owner_windows.go | 10 ++ go/internal/ocxcli/doctor_probes3.go | 197 ++++++++++++++++++++- go/internal/ocxcli/doctor_probes4.go | 54 +++++- go/internal/ocxcli/doctor_probes_test.go | 83 +++++++++ 7 files changed, 400 insertions(+), 9 deletions(-) create mode 100644 go/internal/ocxcli/doctor_owner_unix.go create mode 100644 go/internal/ocxcli/doctor_owner_windows.go diff --git a/go/internal/ocxcli/doctor_command.go b/go/internal/ocxcli/doctor_command.go index 0aeefa9372..c0cb692f50 100644 --- a/go/internal/ocxcli/doctor_command.go +++ b/go/internal/ocxcli/doctor_command.go @@ -50,6 +50,11 @@ type DoctorCommandDeps struct { WHAM func() DoctorWhamResult AgentRoles func() []string WSL func() DoctorWslDiagnostic + BunVersion func() string + Memory func() DoctorServiceMemoryReport + History func() DoctorHistoryPending + HistoryNamespace func() DoctorHistoryState + ProjectConfigs func() []DoctorProjectConfigWarning ProxyDownHint func() string } @@ -134,6 +139,29 @@ func defaultDoctorCommandDeps(deps DoctorCommandDeps) DoctorCommandDeps { if deps.WSL == nil { deps.WSL = CollectCurrentDoctorWslDualInstall } + if deps.BunVersion == nil { + deps.BunVersion = doctorBunVersion + } + if deps.Memory == nil { + deps.Memory = func() DoctorServiceMemoryReport { + runtime, err := ReadRuntime() + if err != nil { + return DoctorServiceMemoryReport{Status: "not_running"} + } + return FetchDoctorServiceMemory(context.Background(), DoctorManagementReader{Runtime: runtime}) + } + } + if deps.History == nil { + deps.History = CollectCurrentDoctorHistoryPending + } + if deps.HistoryNamespace == nil { + deps.HistoryNamespace = CollectDoctorHistoryNamespace + } + if deps.ProjectConfigs == nil { + deps.ProjectConfigs = func() []DoctorProjectConfigWarning { + return CollectDoctorProjectConfigsWithGlobal(doctorCodexHome(), "") + } + } if deps.ProxyDownHint == nil { deps.ProxyDownHint = func() string { return "" } } @@ -141,10 +169,7 @@ func defaultDoctorCommandDeps(deps DoctorCommandDeps) DoctorCommandDeps { } var doctorCommandTODOs = []DoctorTODOSection{ - {"Memory / runtime", "doctor Bun runtime identity is unavailable in the Go process; service evidence is collected but cannot yet match TypeScript text."}, - {"Codex history metadata restore", "history metadata restore diagnostic has not been ported."}, {"Codex native-write coordinator", "native-write coordinator diagnostic has not been ported."}, - {"Project Codex configs", "profile-aware project config bypass diagnostic has not been ported."}, {"OAuth reliability", "credential-collision, refresh-lock, and catalog freshness diagnostics have not been ported; live Codex account health is collected but not rendered."}, {"Hints", "remaining hints need their source diagnostics; proxy-down hint is assembled when supplied."}, } @@ -213,14 +238,14 @@ func AssembleDoctorCommand(args []string, deps DoctorCommandDeps) DoctorCommandR FormatDoctorProviderAPIKeys(CollectDoctorProviderAPIKeysOrdered(deps.OrderedProviders(), env)), FormatDoctorCodexEnvKeyReadiness(CollectDoctorCodexEnvKeyReadiness(deps.CodexConfigText(), env, deps.Shim(), deps.ServiceToken())), FormatDoctorRunningProxyEnv(deps.RunningProxyEnv()), - doctorTODO(doctorCommandTODOs[0]), + formatDoctorMemorySection(deps.Memory(), deps.BunVersion()), FormatDoctorWHAM(deps.WHAM()), - doctorTODO(doctorCommandTODOs[1]), doctorTODO(doctorCommandTODOs[2]), doctorTODO(doctorCommandTODOs[3]), + FormatDoctorHistoryState(deps.HistoryNamespace()), doctorTODO(doctorCommandTODOs[0]), FormatDoctorHistoryPending(deps.History()), FormatDoctorProjectConfigs(deps.ProjectConfigs()), FormatDoctorAgentRoles(deps.AgentRoles()), FormatDoctorWslDualInstall(deps.WSL()), - doctorTODO(doctorCommandTODOs[4]), + doctorTODO(doctorCommandTODOs[1]), } - last := doctorTODO(doctorCommandTODOs[5]) + last := doctorTODO(doctorCommandTODOs[2]) if hint := deps.ProxyDownHint(); hint != "" { last = append(last, " - "+hint) } diff --git a/go/internal/ocxcli/doctor_command_test.go b/go/internal/ocxcli/doctor_command_test.go index 53fad1c17a..c0c5f678d9 100644 --- a/go/internal/ocxcli/doctor_command_test.go +++ b/go/internal/ocxcli/doctor_command_test.go @@ -64,6 +64,9 @@ func TestDoctorCommandAssemblyMatchesTypeScriptOracle(t *testing.T) { {"Provider API keys (value hidden)", "Codex env_key launch readiness"}, {"Codex env_key launch readiness", "Running proxy process proxy env (presence only)"}, {"Running proxy process proxy env (presence only)", "Memory / runtime"}, + {"Memory / runtime", "WHAM reachability"}, + {"Codex history metadata restore", "Codex native-write coordinator"}, + {"Project Codex configs", "Codex agent role files"}, } { got := doctorSection(result.Text, section.heading, section.next) want := doctorSection(oracle, section.heading, section.next) @@ -99,6 +102,11 @@ func TestDoctorCommandAssemblyUsesPortableProbe3Sections(t *testing.T) { WSL: func() DoctorWslDiagnostic { return DoctorWslDiagnostic{WSL: true, EffectiveCodexHome: "/home/a/.codex"} }, + BunVersion: func() string { return "1.3.14" }, + Memory: func() DoctorServiceMemoryReport { return DoctorServiceMemoryReport{Status: "not_running"} }, + History: func() DoctorHistoryPending { return DoctorHistoryPending{} }, + HistoryNamespace: func() DoctorHistoryState { return DoctorHistoryState{Namespace: "missing"} }, + ProjectConfigs: func() []DoctorProjectConfigWarning { return nil }, } got := AssembleDoctorCommand(nil, deps).Text for _, want := range []string{ @@ -141,6 +149,11 @@ func TestDoctorCommandAssemblyArgumentsAndTODOBoundary(t *testing.T) { WHAM: func() DoctorWhamResult { return DoctorWhamResult{} }, AgentRoles: func() []string { return nil }, WSL: func() DoctorWslDiagnostic { return DoctorWslDiagnostic{} }, + BunVersion: func() string { return "1.3.14" }, + Memory: func() DoctorServiceMemoryReport { return DoctorServiceMemoryReport{Status: "not_running"} }, + History: func() DoctorHistoryPending { return DoctorHistoryPending{} }, + HistoryNamespace: func() DoctorHistoryState { return DoctorHistoryState{Namespace: "missing"} }, + ProjectConfigs: func() []DoctorProjectConfigWarning { return nil }, ProxyDownHint: func() string { return "hint" }, } result := AssembleDoctorCommand([]string{"--reclaim-response-tempz", "--reclaim-response-temps"}, deps) diff --git a/go/internal/ocxcli/doctor_owner_unix.go b/go/internal/ocxcli/doctor_owner_unix.go new file mode 100644 index 0000000000..9787288a7f --- /dev/null +++ b/go/internal/ocxcli/doctor_owner_unix.go @@ -0,0 +1,13 @@ +//go:build !windows + +package ocxcli + +import ( + "os" + "syscall" +) + +func doctorOwnedByCurrentUser(info os.FileInfo) bool { + stat, ok := info.Sys().(*syscall.Stat_t) + return ok && int(stat.Uid) == os.Getuid() +} diff --git a/go/internal/ocxcli/doctor_owner_windows.go b/go/internal/ocxcli/doctor_owner_windows.go new file mode 100644 index 0000000000..d7319b57bc --- /dev/null +++ b/go/internal/ocxcli/doctor_owner_windows.go @@ -0,0 +1,10 @@ +//go:build windows + +package ocxcli + +import "os" + +// Windows ownership and reparse-point verification requires the token/SID +// probe used by TypeScript. Keep the namespace diagnostic refused there rather +// than treating a directory as trusted from a POSIX-style mode bit. +func doctorOwnedByCurrentUser(os.FileInfo) bool { return false } diff --git a/go/internal/ocxcli/doctor_probes3.go b/go/internal/ocxcli/doctor_probes3.go index 90e9d55da4..11f056e0f0 100644 --- a/go/internal/ocxcli/doctor_probes3.go +++ b/go/internal/ocxcli/doctor_probes3.go @@ -10,6 +10,7 @@ import ( "os" "os/exec" "path/filepath" + "regexp" "runtime" "sort" "strconv" @@ -17,6 +18,33 @@ import ( "time" ) +// doctorBunVersion obtains the exact runtime identity that runs the TypeScript +// oracle. Go has no Bun.version equivalent; an unavailable Bun follows the +// TypeScript no-live wording with an explicit unknown value rather than hiding +// the line. +func doctorBunVersion() string { + path, err := exec.LookPath("bun") + if err != nil { + return "unknown" + } + out, err := exec.Command(path, "--version").Output() + if err != nil { + return "unknown" + } + if version := strings.TrimSpace(string(out)); version != "" { + return version + } + return "unknown" +} + +func formatDoctorMemorySection(report DoctorServiceMemoryReport, bun string) []string { + lines := []string{"Memory / runtime"} + if report.Status == "not_running" { + return append(lines, fmt.Sprintf(" -- doctor process Bun %s (this is NOT the service process)", bun), " -- no running ocx proxy found (no live pid/runtime record)") + } + return append(lines, FormatDoctorServiceMemory(report, bun)...) +} + const doctorWHAMURL = "https://chatgpt.com/backend-api/wham/usage" var execLookPath = exec.LookPath @@ -418,6 +446,135 @@ func CollectDoctorProjectConfigs(cwd string) []DoctorProjectConfigWarning { } return rows } + +// CollectDoctorProjectConfigsWithGlobal mirrors the TypeScript project warning +// gate: only diagnose a project override when the global Codex config is +// actually routed through OpenCodex. It also discovers trusted projects listed +// in the global [projects."…"] tables. +func CollectDoctorProjectConfigsWithGlobal(codexHome, cwd string) []DoctorProjectConfigWarning { + globalPath := filepath.Join(codexHome, "config.toml") + global, err := os.ReadFile(globalPath) + if err != nil || !doctorGlobalOpenCodexRouting(string(global)) { + return nil + } + if cwd == "" { + cwd, _ = os.Getwd() + } + paths := doctorProjectConfigPaths(cwd, globalPath, string(global)) + var rows []DoctorProjectConfigWarning + for _, path := range paths { + raw, err := os.ReadFile(path) + if err != nil { + continue + } + if row, ok := doctorProjectConfigWarning(string(raw), path); ok { + rows = append(rows, row) + } + } + return rows +} + +func doctorGlobalOpenCodexRouting(text string) bool { + return doctorTomlRoot3(text, "model_provider") == "opencodex" || strings.Contains(text, "# Auto-injected by opencodex") && strings.Contains(text, "openai_base_url") +} +func doctorProjectConfigPaths(cwd, globalPath, global string) []string { + seen := map[string]bool{} + paths := []string{} + add := func(root string) { + p := filepath.Join(root, ".codex", "config.toml") + if p != globalPath && !seen[p] { + if _, err := os.Stat(p); err == nil { + seen[p] = true + paths = append(paths, p) + } + } + } + for i := 0; i < 12; i++ { + add(cwd) + parent := filepath.Dir(cwd) + if parent == cwd { + break + } + cwd = parent + } + for _, line := range strings.Split(global, "\n") { + m := regexp.MustCompile(`^\s*\[projects\.(?:"([^"]+)"|'([^']+)')\]\s*$`).FindStringSubmatch(line) + if m == nil { + continue + } + root := m[1] + if root == "" { + root = m[2] + } + // A trusted-project table is only admitted when its own body says trusted. + // The simple scan below is bounded by the next TOML table. + start := strings.Index(global, line) + rest := global[start+len(line):] + next := strings.Index(rest, "\n[") + body := rest + if next >= 0 { + body = rest[:next] + } + if strings.EqualFold(doctorTomlRoot3(body, "trust_level"), "trusted") { + add(root) + } + } + return paths +} +func doctorProjectConfigWarning(text, path string) (DoctorProjectConfigWarning, bool) { + rootProvider, profile := doctorTomlRoot3(text, "model_provider"), doctorTomlRoot3(text, "profile") + provider := rootProvider + viaProfile := false + if profile != "" { + if p := doctorTomlTableString(text, "profiles."+profile, "model_provider"); p != "" { + provider, viaProfile = p, true + } + } + if provider == "" || provider == "opencodex" || provider == "openai" { + return DoctorProjectConfigWarning{}, false + } + issue := "model_provider=\"" + provider + "\"" + if viaProfile { + issue = "profile=\"" + profile + "\"" + } + if doctorTomlTableExists(text, "model_providers."+provider) { + issue = "[model_providers." + provider + "]" + } + return DoctorProjectConfigWarning{path, issue, "Overrides OpenCodex — Codex uses " + doctorHumanProvider(provider) + " for this repo instead of the proxy (~/.codex/config.toml)."}, true +} +func doctorTomlTableString(text, table, key string) string { + in := false + for _, line := range strings.Split(text, "\n") { + l := strings.TrimSpace(line) + if strings.HasPrefix(l, "[") { + in = strings.Trim(l, "[]") == table + continue + } + if in { + if v := doctorTomlRoot3(l, key); v != "" { + return v + } + } + } + return "" +} +func doctorTomlTableExists(text, table string) bool { + for _, line := range strings.Split(text, "\n") { + if strings.Trim(strings.TrimSpace(line), "[]") == table { + return true + } + } + return false +} +func doctorHumanProvider(provider string) string { + if provider == "opencode_go" { + return "OpenCode Go" + } + if strings.HasPrefix(provider, "opencode") { + return "OpenCode" + } + return provider +} func doctorTomlRoot3(text, key string) string { for _, line := range strings.Split(text, "\n") { if strings.HasPrefix(strings.TrimSpace(line), "[") { @@ -436,11 +593,26 @@ func FormatDoctorProjectConfigs(rows []DoctorProjectConfigWarning) []string { return append(lines, " ok no project-local provider bypass detected") } for _, r := range rows { - lines = append(lines, " -- "+r.Path+" — "+r.Issue, " "+r.Bypass) + lines = append(lines, " -- "+doctorDisplayProjectPath(r.Path)+" — "+r.Issue, " "+r.Bypass) } return append(lines, " fix: remove those entries so OpenCodex proxy routing applies in this project") } +func doctorDisplayProjectPath(path string) string { + home, err := os.UserHomeDir() + if err != nil { + return path + } + rel, err := filepath.Rel(home, path) + if err != nil || rel == ".." || strings.HasPrefix(rel, ".."+string(os.PathSeparator)) || filepath.IsAbs(rel) { + return path + } + if rel == "." { + return "~" + } + return "~/" + filepath.ToSlash(rel) +} + func CollectDoctorAgentRoles(codexHome string) []string { entries, err := os.ReadDir(filepath.Join(codexHome, "agents")) if err != nil { @@ -539,3 +711,26 @@ func FormatDoctorHistoryState(d DoctorHistoryState) []string { } return lines } + +// CollectDoctorHistoryNamespace is the read-only counterpart of the TypeScript +// coordinator namespace probe. In particular, it never creates the directory: +// a fresh installation must report "not created yet", not mutate /tmp merely +// because doctor ran. +func CollectDoctorHistoryNamespace() DoctorHistoryState { + if runtime.GOOS == "windows" { + return DoctorHistoryState{Namespace: "refused", Restore: "Windows coordinator namespace probing is unavailable in the Go runtime"} + } + uid := os.Getuid() + path := filepath.Join("/tmp", fmt.Sprintf("opencodex-runtime-v1-%d", uid)) + info, err := os.Lstat(path) + if os.IsNotExist(err) { + return DoctorHistoryState{Namespace: "missing"} + } + if err != nil { + return DoctorHistoryState{Namespace: "refused", Restore: "The Codex coordinator namespace cannot be inspected."} + } + if !info.IsDir() || info.Mode()&os.ModeSymlink != 0 || !doctorOwnedByCurrentUser(info) || info.Mode().Perm() != 0o700 { + return DoctorHistoryState{Namespace: "refused", Restore: "The Codex coordinator namespace has unsafe ownership or permissions."} + } + return DoctorHistoryState{Namespace: "ok"} +} diff --git a/go/internal/ocxcli/doctor_probes4.go b/go/internal/ocxcli/doctor_probes4.go index 175f5ea4e8..e5976bba2b 100644 --- a/go/internal/ocxcli/doctor_probes4.go +++ b/go/internal/ocxcli/doctor_probes4.go @@ -261,7 +261,10 @@ func CollectDoctorHistoryPending(stateDB, backup string) DoctorHistoryPending { var manifest struct { Version int StateDBPath string - Entries map[string]json.RawMessage + Entries map[string]struct { + ID, RolloutPath, ModelProvider, Source string + HasUserEvent int + } } if json.Unmarshal(raw, &manifest) != nil || (manifest.Version != 1 && manifest.Version != 2) || manifest.Entries == nil { return DoctorHistoryPending{Failed: true, FailureReason: "integrity"} @@ -272,6 +275,11 @@ func CollectDoctorHistoryPending(stateDB, backup string) DoctorHistoryPending { return DoctorHistoryPending{Failed: true, FailureReason: "integrity"} } n := len(manifest.Entries) + for id, entry := range manifest.Entries { + if id == "" || entry.ID != id || !filepath.IsAbs(entry.RolloutPath) || entry.ModelProvider == "" || entry.Source == "" || (entry.HasUserEvent != 0 && entry.HasUserEvent != 1) || !doctorHistoryProvenanceAllowed(entry.ModelProvider, entry.Source) { + return DoctorHistoryPending{BackupEntries: n, Failed: true, FailureReason: "integrity"} + } + } if _, err := os.Stat(stateDB); err != nil { if n == 0 && os.IsNotExist(err) { return DoctorHistoryPending{} @@ -287,8 +295,52 @@ func CollectDoctorHistoryPending(stateDB, backup string) DoctorHistoryPending { if err := db.QueryRow("SELECT 1 FROM threads LIMIT 1").Scan(&one); err != nil && err != sql.ErrNoRows { return DoctorHistoryPending{BackupEntries: n, Failed: true, FailureReason: "integrity"} } + for id, entry := range manifest.Entries { + var rollout, provider, source string + var hasUser int + err := db.QueryRow("SELECT rollout_path, model_provider, source, has_user_event FROM threads WHERE id = ?", id).Scan(&rollout, &provider, &source, &hasUser) + if err != nil || rollout != entry.RolloutPath || (provider != "opencodex" && provider != entry.ModelProvider) || (source != "exec" && source != entry.Source) || (hasUser != 0 && hasUser != 1) { + return DoctorHistoryPending{BackupEntries: n, Failed: true, FailureReason: "integrity"} + } + if _, err := os.Stat(entry.RolloutPath); err != nil { + return DoctorHistoryPending{BackupEntries: n, Failed: true, FailureReason: "integrity"} + } + } return DoctorHistoryPending{BackupEntries: n} } + +func doctorHistoryProvenanceAllowed(provider, source string) bool { + return (provider == "openai" && (source == "cli" || source == "vscode")) || (provider == "opencodex" && source == "exec") +} + +// CollectCurrentDoctorHistoryPending resolves the same state_5.sqlite and +// manifest location as TypeScript for the normal, environment-driven path. +func CollectCurrentDoctorHistoryPending() DoctorHistoryPending { + state := doctorStateDBPath(doctorCodexHome()) + dir := statusConfigDir() + if dir == "" { + return DoctorHistoryPending{Failed: true, FailureReason: "permission"} + } + return CollectDoctorHistoryPending(state, DoctorHistoryBackupPath(state, dir)) +} + +func doctorStateDBPath(codexHome string) string { + root := strings.TrimSpace(os.Getenv("CODEX_SQLITE_HOME")) + if raw, err := os.ReadFile(filepath.Join(codexHome, "config.toml")); err == nil { + if configured := doctorTomlRoot3(string(raw), "sqlite_home"); configured != "" { + root = configured + } + } + if root == "" { + root = codexHome + } + if !filepath.IsAbs(root) { + if cwd, err := os.Getwd(); err == nil { + root = filepath.Join(cwd, root) + } + } + return filepath.Join(root, "state_5.sqlite") +} func FormatDoctorHistoryPending(d DoctorHistoryPending) []string { if d.Failed { if d.FailureReason == "busy" { diff --git a/go/internal/ocxcli/doctor_probes_test.go b/go/internal/ocxcli/doctor_probes_test.go index 864b686b73..ea4cc53ebe 100644 --- a/go/internal/ocxcli/doctor_probes_test.go +++ b/go/internal/ocxcli/doctor_probes_test.go @@ -1,8 +1,10 @@ package ocxcli import ( + "database/sql" "encoding/json" "errors" + "fmt" "os" "os/exec" "path/filepath" @@ -329,3 +331,84 @@ func TestDoctorServiceMemoryFormatterMatchesTypeScriptOracle(t *testing.T) { t.Fatalf("service memory bytes\n got: %q\nwant: %q", gotText, wantText) } } + +func TestDoctorProjectProfileFixtureMatchesTypeScriptOracle(t *testing.T) { + home := t.TempDir() + codexHome, project := filepath.Join(home, "codex"), filepath.Join(home, "project") + if err := os.MkdirAll(filepath.Join(project, ".codex"), 0o700); err != nil { + t.Fatal(err) + } + if err := os.MkdirAll(codexHome, 0o700); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(codexHome, "config.toml"), []byte("model_provider = \"opencodex\"\n"), 0o600); err != nil { + t.Fatal(err) + } + fixture := "profile = \"review\"\nmodel_provider = \"openai\"\n\n[profiles.review]\nmodel_provider = \"fixture\"\n\n[model_providers.fixture]\n" + if err := os.WriteFile(filepath.Join(project, ".codex", "config.toml"), []byte(fixture), 0o600); err != nil { + t.Fatal(err) + } + t.Setenv("HOME", home) + t.Setenv("CODEX_HOME", codexHome) + repo := typeScriptOracleRepo(t) + script := "import { collectProjectCodexConfigWarnings, formatProjectCodexConfigWarningsForDoctor } from './src/codex/project-config-warnings'; process.stdout.write(JSON.stringify(formatProjectCodexConfigWarningsForDoctor(collectProjectCodexConfigWarnings({cwd: process.env.OCX_PROJECT, codexConfigPath: process.env.OCX_CODEX_CONFIG}))));" + cmd := exec.Command("bun", "-e", script) + cmd.Dir = repo + cmd.Env = append(os.Environ(), "OCX_PROJECT="+project, "OCX_CODEX_CONFIG="+filepath.Join(codexHome, "config.toml")) + out, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("TypeScript project oracle: %v: %s", err, out) + } + var want []string + if err := json.Unmarshal(out, &want); err != nil { + t.Fatal(err) + } + got := FormatDoctorProjectConfigs(CollectDoctorProjectConfigsWithGlobal(codexHome, project))[1:] + if strings.Join(got, "\n") != strings.Join(want, "\n") { + t.Fatalf("project profile bytes\n got: %q\nwant: %q", got, want) + } +} + +func TestDoctorHistoryManifestFixtureMatchesTypeScriptDoctor(t *testing.T) { + home := t.TempDir() + codexHome := filepath.Join(home, "codex") + if err := os.MkdirAll(codexHome, 0o700); err != nil { + t.Fatal(err) + } + state, rollout := filepath.Join(codexHome, "state_5.sqlite"), filepath.Join(home, "rollout.jsonl") + rolloutText := `{"type":"session_meta","payload":{"id":"thread-1","model_provider":"opencodex","source":"cli"}}` + "\n" + if err := os.WriteFile(rollout, []byte(rolloutText), 0o600); err != nil { + t.Fatal(err) + } + db, err := sql.Open("sqlite", state) + if err != nil { + t.Fatal(err) + } + if _, err := db.Exec("CREATE TABLE threads (id TEXT PRIMARY KEY, rollout_path TEXT, model_provider TEXT, source TEXT, has_user_event INTEGER, first_user_message TEXT)"); err != nil { + t.Fatal(err) + } + if _, err := db.Exec("INSERT INTO threads VALUES (?, ?, 'opencodex', 'cli', 1, '')", "thread-1", rollout); err != nil { + t.Fatal(err) + } + db.Close() + manifest := fmt.Sprintf(`{"version":2,"stateDbPath":%q,"entries":{"thread-1":{"id":"thread-1","rolloutPath":%q,"modelProvider":"openai","source":"cli","hasUserEvent":1}}}`, state, rollout) + backup := DoctorHistoryBackupPath(state, home) + if err := os.WriteFile(backup, []byte(manifest), 0o600); err != nil { + t.Fatal(err) + } + t.Setenv("HOME", home) + t.Setenv("OPENCODEX_HOME", home) + t.Setenv("CODEX_HOME", codexHome) + oracle, code := runTypeScriptDoctor(t, home, codexHome) + if code != 0 { + t.Fatalf("TypeScript doctor exit %d: %s", code, oracle) + } + want := " -- 1 backup manifest entry pending exact metadata restore" + if !strings.Contains(oracle, want) { + t.Fatalf("TypeScript history line missing: %s", oracle) + } + got := strings.Join(FormatDoctorHistoryPending(CollectDoctorHistoryPending(state, backup)), "\n") + if got != want { + t.Fatalf("history bytes got %q want %q", got, want) + } +} From ca5eadcd81d07b43bcdda0ef83aaebc25f71a64c Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Mon, 7 Sep 2026 03:37:42 +0800 Subject: [PATCH 097/165] feat(go): collect doctor oauth and catalog diagnostics --- go/internal/ocxcli/doctor_probes_oauth.go | 340 ++++++++++++++++++ .../ocxcli/doctor_probes_oauth_test.go | 104 ++++++ 2 files changed, 444 insertions(+) create mode 100644 go/internal/ocxcli/doctor_probes_oauth.go create mode 100644 go/internal/ocxcli/doctor_probes_oauth_test.go diff --git a/go/internal/ocxcli/doctor_probes_oauth.go b/go/internal/ocxcli/doctor_probes_oauth.go new file mode 100644 index 0000000000..e1c2ac92b0 --- /dev/null +++ b/go/internal/ocxcli/doctor_probes_oauth.go @@ -0,0 +1,340 @@ +package ocxcli + +import ( + "crypto/sha256" + "errors" + "fmt" + "os" + "os/exec" + "path/filepath" + "regexp" + "runtime" + "strconv" + "strings" + "time" + + "github.com/lidge-jun/opencodex/go/internal/config" +) + +type DoctorOAuthReliabilityInput struct { + DataPlaneToken string + ServiceToken string + AdminToken string + CredentialDirectory string + RefreshLockPath string +} + +// CollectDoctorOAuthReliability never writes a probe file or exposes a secret. +func CollectDoctorOAuthReliability(in DoctorOAuthReliabilityInput) []DoctorOAuthCheck { + token := strings.TrimSpace(in.DataPlaneToken) + if token == "" { + token = strings.TrimSpace(in.ServiceToken) + } + checks := []DoctorOAuthCheck{} + if token == "" { + checks = append(checks, DoctorOAuthCheck{"OK", "No data-plane token is set, so it cannot collide with the management token."}) + } else if strings.HasPrefix(token, "ocx_admin_") || (strings.TrimSpace(in.AdminToken) != "" && token == strings.TrimSpace(in.AdminToken)) { + checks = append(checks, DoctorOAuthCheck{"FAIL", "The data-plane secret (OPENCODEX_API_AUTH_TOKEN or the service token file) holds the management (admin) token, so the proxy fences the whole management API closed and every ocx management command fails with 503. Action: unset OPENCODEX_API_AUTH_TOKEN, replace the service token file with a distinct data-plane key, then re-run `ocx service install` and restart the proxy"}) + } else { + checks = append(checks, DoctorOAuthCheck{"OK", "Data-plane and management credentials are distinct."}) + } + if doctorOAuthDirectoryWritable(in.CredentialDirectory) { + checks = append(checks, DoctorOAuthCheck{"OK", "OAuth credential storage directory is writable for atomic auth.json updates."}) + } else { + checks = append(checks, DoctorOAuthCheck{"WARN", "OAuth credential storage directory is not writable. Action: fix permissions on OPENCODEX_HOME so ocx can create temp files and rename auth.json"}) + } + if strings.Contains(in.RefreshLockPath, "auth.refresh.") && doctorOAuthDirectoryWritable(filepath.Dir(in.RefreshLockPath)) { + checks = append(checks, DoctorOAuthCheck{"OK", "Token refresh single-flight is active."}) + } else { + checks = append(checks, DoctorOAuthCheck{"WARN", "Token refresh single-flight is unavailable. Action: fix permissions on OPENCODEX_HOME so ocx can create refresh lock files"}) + } + return checks +} + +func doctorOAuthDirectoryWritable(path string) bool { + for i := 0; path != "" && i < 9; i++ { + info, err := os.Stat(path) + if err == nil { + return info.IsDir() && info.Mode().Perm()&0300 == 0300 + } + if !errors.Is(err, os.ErrNotExist) { + return false + } + next := filepath.Dir(path) + if next == path { + break + } + path = next + } + return false +} + +func CollectDoctorOAuthReliabilityDefault() []DoctorOAuthCheck { + dir, _ := config.Dir() + service := doctorOAuthReadSecret(filepath.Join(dir, "service-api-token"), false) + admin := strings.TrimSpace(os.Getenv("OPENCODEX_ADMIN_AUTH_TOKEN")) + if admin == "" { + admin = doctorOAuthReadSecret(filepath.Join(dir, "admin-api-token"), true) + } + accountHash := sha256.Sum256([]byte("probe-account")) + return CollectDoctorOAuthReliability(DoctorOAuthReliabilityInput{os.Getenv("OPENCODEX_API_AUTH_TOKEN"), service, admin, dir, filepath.Join(dir, "auth.refresh.doctor-probe."+fmt.Sprintf("%x", accountHash[:])[:24]+".lock")}) +} +func doctorOAuthReadSecret(path string, admin bool) string { + info, err := os.Lstat(path) + if err != nil || !info.Mode().IsRegular() || info.Size() > 512 { + return "" + } + raw, err := os.ReadFile(path) + if err != nil { + return "" + } + value := strings.TrimSpace(string(raw)) + if admin && !(strings.HasPrefix(value, "ocx_admin_") && len(value) == len("ocx_admin_")+43) { + return "" + } + return value +} + +type DoctorAppServerProcess struct { + PID int + CommandLine string + Executable string +} +type DoctorCatalogCollector struct { + List func() ([]DoctorAppServerProcess, error) + StartedAt func(int) (*time.Time, error) + CatalogMtime func() (*time.Time, error) +} + +func CollectDoctorCatalogStateLive(c DoctorCatalogCollector) DoctorCatalogState { + rows, err := c.List() + if err != nil { + return CollectDoctorCatalogState(DoctorCatalogProbe{EnumerationFailed: true}) + } + probe := DoctorCatalogProbe{Starts: map[int]*time.Time{}} + seen := map[int]bool{} + for _, row := range rows { + if row.PID > 0 && !seen[row.PID] && doctorIsCodexAppServer(row.CommandLine, row.Executable) { + seen[row.PID] = true + probe.PIDs = append(probe.PIDs, row.PID) + } + } + if len(probe.PIDs) == 0 { + return CollectDoctorCatalogState(probe) + } + if mtime, err := c.CatalogMtime(); err == nil { + probe.CatalogMtime = mtime + } + for _, pid := range probe.PIDs { + if start, err := c.StartedAt(pid); err == nil { + probe.Starts[pid] = start + } + } + return CollectDoctorCatalogState(probe) +} +func CollectDoctorCatalogStateDefault() DoctorCatalogState { + return CollectDoctorCatalogStateLive(DoctorCatalogCollector{doctorListAppServers, doctorProcessStartedAt, doctorCatalogMtime}) +} + +func doctorIsCodexAppServer(command, executable string) bool { + tokens := strings.Fields(strings.ReplaceAll(command, "\x00", " ")) + if len(tokens) == 0 { + return false + } + base := doctorCatalogBase(tokens[0]) + if doctorCatalogBase(executable) == "codex-code-mode-host" || base == "codex-code-mode-host" { + return true + } + if base == "node" || base == "node.exe" || base == "bun" || base == "bun.exe" || base == "deno" || base == "deno.exe" { + if len(tokens) < 2 { + return false + } + tokens, base = tokens[1:], doctorCatalogBase(tokens[1]) + } + allowed := base == "codex" || base == "codex.exe" || base == "codex.cmd" || base == "codex.opencodex-real" || base == "codex.opencodex-real.cmd" || base == "codex.opencodex-real.ps1" || doctorTargetTripleCodex.MatchString(base) + if !allowed { + return false + } + for i := 1; i < len(tokens); i++ { + token := tokens[i] + if token == "--" { + return false + } + if strings.HasPrefix(token, "-") { + if !strings.Contains(token, "=") && doctorGlobalOptionWithValue[token] && i+1 < len(tokens) { + i++ + } + continue + } + return strings.EqualFold(token, "app-server") + } + return false +} + +var doctorTargetTripleCodex = regexp.MustCompile(`^codex-[a-z0-9_]+-[a-z0-9_]+-[a-z0-9_]+(?:-[a-z0-9_]+)?(?:\.exe|\.cmd)?$`) +var doctorGlobalOptionWithValue = map[string]bool{ + "--enable": true, "--disable": true, "--config": true, "-c": true, "--profile": true, "-p": true, + "--model": true, "-m": true, "--sandbox": true, "-s": true, "--ask-for-approval": true, "-a": true, + "--local-provider": true, "--add-dir": true, "--cd": true, "-C": true, "--color": true, "--image": true, + "-i": true, "--output-schema": true, "--output-last-message": true, "-o": true, +} + +func doctorCatalogBase(value string) string { + return strings.ToLower(filepath.Base(strings.ReplaceAll(value, "\\", "/"))) +} + +func doctorListAppServers() ([]DoctorAppServerProcess, error) { + if runtime.GOOS == "linux" { + entries, err := os.ReadDir("/proc") + if err != nil { + return nil, err + } + rows := []DoctorAppServerProcess{} + for _, entry := range entries { + pid, err := strconv.Atoi(entry.Name()) + if err != nil || pid <= 1 { + continue + } + raw, err := os.ReadFile(filepath.Join("/proc", entry.Name(), "cmdline")) + if err != nil { + continue + } + tokens := strings.Split(strings.TrimRight(string(raw), "\x00"), "\x00") + if len(tokens) == 0 || tokens[0] == "" { + continue + } + rows = append(rows, DoctorAppServerProcess{pid, strings.Join(tokens, " "), tokens[0]}) + } + return rows, nil + } + if runtime.GOOS == "windows" { + return nil, errors.New("process enumeration unavailable") + } + raw, err := exec.Command("ps", "-x", "-o", "pid=,command=").Output() + if err != nil { + return nil, err + } + rows := []DoctorAppServerProcess{} + for _, line := range strings.Split(string(raw), "\n") { + fields := strings.Fields(line) + if len(fields) < 2 { + continue + } + pid, err := strconv.Atoi(fields[0]) + if err == nil { + rows = append(rows, DoctorAppServerProcess{pid, strings.Join(fields[1:], " "), fields[1]}) + } + } + return rows, nil +} +func doctorProcessStartedAt(pid int) (*time.Time, error) { + raw, err := exec.Command("ps", "-o", "lstart=", "-p", strconv.Itoa(pid)).Output() + if err != nil { + return nil, err + } + value := strings.TrimSpace(string(raw)) + if value == "" { + return nil, nil + } + parsed, err := time.ParseInLocation("Mon Jan 2 15:04:05 2006", value, time.Local) + if err != nil { + return nil, err + } + return &parsed, nil +} +func doctorCatalogMtime() (*time.Time, error) { + home := doctorCodexHome() + path := filepath.Join(home, "opencodex-catalog.json") + raw, err := os.ReadFile(filepath.Join(home, "config.toml")) + if err == nil { + for _, line := range strings.Split(string(raw), "\n") { + key, value, ok := strings.Cut(line, "=") + if ok && strings.TrimSpace(key) == "model_catalog_json" { + value = strings.Trim(strings.TrimSpace(strings.SplitN(value, "#", 2)[0]), "\"'") + if value != "" { + if filepath.IsAbs(value) { + path = value + } else { + path = filepath.Join(home, value) + } + } + break + } + } + } + info, err := os.Stat(path) + if err != nil { + return nil, err + } + mtime := info.ModTime() + return &mtime, nil +} + +type DoctorHintsInput struct { + ProxyDown string + ProviderKeyDetails []string + CodexEnvKeyDetail string + CodexEnvKeyAction string + RebootSafe bool + RecommendedCommand string + RestoreNativeCommand string + AnyDrvfs bool + ProbeOK bool + NoProxy bool + ProbeClassification string + PendingFailed bool + PendingFailureReason string + BackupEntries int + DualInstall bool + EffectiveWindowsMount bool + WindowsCodexHome string + AutomountRoot string + InteropCodexPath string +} + +func CollectDoctorHints(in DoctorHintsInput) []string { + hints := []string{} + if in.ProxyDown != "" { + hints = append(hints, in.ProxyDown) + } + hints = append(hints, in.ProviderKeyDetails...) + if in.CodexEnvKeyDetail != "" { + hints = append(hints, in.CodexEnvKeyDetail+". "+in.CodexEnvKeyAction+".") + } + if !in.RebootSafe { + command := in.RecommendedCommand + if command == "" { + command = in.RestoreNativeCommand + } + hints = append(hints, "Codex is pinned to the local proxy without persistent startup protection. After restart, requests can reconnect indefinitely. Run '"+command+"'.") + } + if in.AnyDrvfs { + hints = append(hints, "State dir is on a Windows-mounted (/mnt) drive. Prefer the Linux home (~) under WSL for token/lock reliability.") + } + if !in.ProbeOK && (in.ProbeClassification == "timeout" || in.ProbeClassification == "connect_error") { + hints = append(hints, "WHAM probe could not reach chatgpt.com. On WSL2 this is often NAT/DNS/VPN. Quota cannot prime, so auto-switch stays on unknown scores.") + if in.NoProxy { + hints = append(hints, "No proxy is visible to this doctor process and config.proxy is unset or unresolved. If Windows uses a proxy/VPN, set config.proxy or start ocx from a shell with HTTP(S)_PROXY.") + } + } + if in.PendingFailed && in.PendingFailureReason == "busy" { + hints = append(hints, "Backed-up history metadata is pending or its state is unreadable. The running proxy retries exact restoration automatically; to force it now, close the Codex app and run 'ocx sync'. Untracked routed history is not relabeled.") + } else if in.PendingFailed && in.PendingFailureReason == "permission" { + hints = append(hints, "Backed-up history metadata could not be inspected because access was denied. Fix access to the reported Codex history paths, then run 'ocx sync'; repeated retries do not repair permissions.") + } else if in.PendingFailed { + hints = append(hints, "The history manifest or its target is invalid or changed. Preserve both, inspect the manifest/database/rollout identity, and do not repeatedly run 'ocx sync' until the mismatch is understood. Untracked routed history is not relabeled.") + } else if in.BackupEntries > 0 { + hints = append(hints, "Backed-up history metadata is pending. The running proxy retries exact restoration automatically; to force it now, close the Codex app and run 'ocx sync'. Untracked routed history is not relabeled.") + } + if in.DualInstall && !in.EffectiveWindowsMount { + home := in.WindowsCodexHome + if home == "" { + home = in.AutomountRoot + "/c/Users//.codex" + } + hints = append(hints, "Codex is installed on BOTH WSL and Windows. Each side keeps its own ~/.codex (logins, config, catalog are separate); ocx here manages the Linux one. To share a single home, set CODEX_HOME="+home+" in WSL (drvfs file locking is less reliable).", "localhost is one-way in WSL2 NAT mode: Windows-side codex reaches this WSL proxy via localhost (localhostForwarding, on by default), but a Windows-side proxy is NOT reachable from WSL via localhost — use networkingMode=mirrored in .wslconfig for both directions.") + } + if in.InteropCodexPath != "" { + hints = append(hints, "The `codex` found on PATH is the Windows launcher reached through WSL interop; ocx will not shim it (a WSL shim breaks Windows invocations). Install codex inside WSL (npm i -g @openai/codex) or run 'ocx ensure' from Windows.") + } + return hints +} diff --git a/go/internal/ocxcli/doctor_probes_oauth_test.go b/go/internal/ocxcli/doctor_probes_oauth_test.go new file mode 100644 index 0000000000..46baf91a2a --- /dev/null +++ b/go/internal/ocxcli/doctor_probes_oauth_test.go @@ -0,0 +1,104 @@ +package ocxcli + +import ( + "encoding/json" + "errors" + "os" + "os/exec" + "path/filepath" + "reflect" + "strings" + "testing" + "time" +) + +func TestDoctorOAuthReliabilityMatchesTypeScriptOracle(t *testing.T) { + home := t.TempDir() + if err := os.Mkdir(filepath.Join(home, "codex"), 0700); err != nil { + t.Fatal(err) + } + t.Setenv("HOME", home) + t.Setenv("OPENCODEX_HOME", home) + t.Setenv("CODEX_HOME", filepath.Join(home, "codex")) + t.Setenv("OPENCODEX_API_AUTH_TOKEN", " data-plane ") + if err := os.WriteFile(filepath.Join(home, "admin-api-token"), []byte("ocx_admin_"+strings.Repeat("a", 43)), 0600); err != nil { + t.Fatal(err) + } + repo := typeScriptOracleRepo(t) + bun := "/home/ubuntu/.local/share/mise/installs/bun/latest/bin/bun" + script := "import { collectOAuthDoctorChecks } from './src/cli/doctor'; process.stdout.write(JSON.stringify((await collectOAuthDoctorChecks()).slice(0,3)));" + cmd := exec.Command(bun, "-e", script) + // The worktree may not carry node_modules; the oracle helper locates a + // checkout that does. Run there while retaining this isolated HOME. + cmd.Dir = repo + cmd.Env = append(os.Environ(), "HOME="+home, "OPENCODEX_HOME="+home, "CODEX_HOME="+filepath.Join(home, "codex"), "OPENCODEX_API_AUTH_TOKEN= data-plane ") + out, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("TypeScript OAuth oracle: %v: %s", err, out) + } + var want []DoctorOAuthCheck + if err := json.Unmarshal(out, &want); err != nil { + t.Fatalf("decode oracle: %v: %s", err, out) + } + got := CollectDoctorOAuthReliabilityDefault() + if !reflect.DeepEqual(got, want) { + t.Fatalf("OAuth checks\\n got: %#v\\nwant: %#v", got, want) + } +} + +func TestDoctorOAuthCollisionAndNoSecret(t *testing.T) { + secret := "ocx_admin_" + strings.Repeat("z", 43) + checks := CollectDoctorOAuthReliability(DoctorOAuthReliabilityInput{DataPlaneToken: secret, CredentialDirectory: t.TempDir(), RefreshLockPath: filepath.Join(t.TempDir(), "auth.refresh.x.lock")}) + if checks[0].Level != "FAIL" || strings.Contains(checks[0].Message, secret) { + t.Fatalf("collision=%#v", checks[0]) + } + if got := CollectDoctorOAuthReliability(DoctorOAuthReliabilityInput{DataPlaneToken: " ", ServiceToken: "different", AdminToken: "admin", CredentialDirectory: t.TempDir(), RefreshLockPath: filepath.Join(t.TempDir(), "auth.refresh.x.lock")}); got[0].Message != "Data-plane and management credentials are distinct." { + t.Fatalf("service fallback=%#v", got[0]) + } +} + +func TestDoctorCatalogLiveCollector(t *testing.T) { + mtime := time.Unix(100, 0) + equal := mtime + collector := DoctorCatalogCollector{ + List: func() ([]DoctorAppServerProcess, error) { + return []DoctorAppServerProcess{{PID: 7, CommandLine: "codex app-server"}, {PID: 7, CommandLine: "codex app-server"}, {PID: 9, CommandLine: "codex -- app-server"}}, nil + }, + StartedAt: func(pid int) (*time.Time, error) { + if pid == 7 { + return &equal, nil + } + return nil, nil + }, + CatalogMtime: func() (*time.Time, error) { return &mtime, nil }, + } + if got := CollectDoctorCatalogStateLive(collector); got.State != "stale" || !reflect.DeepEqual(got.PIDs, []int{7}) { + t.Fatalf("catalog=%#v", got) + } + failed := collector + failed.List = func() ([]DoctorAppServerProcess, error) { return nil, errors.New("denied") } + if got := CollectDoctorCatalogStateLive(failed); got.State != "unknown" { + t.Fatalf("enumeration=%#v", got) + } +} + +func TestDoctorCatalogCommandMatcher(t *testing.T) { + for _, input := range []struct { + command, executable string + want bool + }{ + {"codex app-server", "codex", true}, {"node /bin/codex app-server", "node", true}, {"codex --config a.toml app-server", "codex", true}, {"codex-x86_64-pc-linux-musl app-server", "codex-x86_64-pc-linux-musl", true}, {"codex -- app-server", "codex", false}, {"hermes-codex-bridge-mcp app-server", "hermes-codex-bridge-mcp", false}, {"codex exec app-server", "codex", false}, {"codex-code-mode-host", "codex-code-mode-host", true}, + } { + if got := doctorIsCodexAppServer(input.command, input.executable); got != input.want { + t.Errorf("%q=%v want %v", input.command, got, input.want) + } + } +} + +func TestDoctorHintsOrderAndPrecedence(t *testing.T) { + got := CollectDoctorHints(DoctorHintsInput{ProxyDown: "proxy", ProviderKeyDetails: []string{"key-a", "key-b"}, CodexEnvKeyDetail: "env", CodexEnvKeyAction: "act", RebootSafe: true, ProbeOK: false, NoProxy: true, ProbeClassification: "timeout", PendingFailed: true, PendingFailureReason: "busy", DualInstall: true, AutomountRoot: "/mnt", InteropCodexPath: "codex.exe"}) + want := []string{"proxy", "key-a", "key-b", "env. act.", "WHAM probe could not reach chatgpt.com. On WSL2 this is often NAT/DNS/VPN. Quota cannot prime, so auto-switch stays on unknown scores.", "No proxy is visible to this doctor process and config.proxy is unset or unresolved. If Windows uses a proxy/VPN, set config.proxy or start ocx from a shell with HTTP(S)_PROXY.", "Backed-up history metadata is pending or its state is unreadable. The running proxy retries exact restoration automatically; to force it now, close the Codex app and run 'ocx sync'. Untracked routed history is not relabeled."} + if !reflect.DeepEqual(got[:len(want)], want) { + t.Fatalf("hints\\n got: %#v\\nwant prefix: %#v", got, want) + } +} From 7648f731c9865b33dbc99bb9ccaab2460d82fd51 Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Mon, 7 Sep 2026 03:54:29 +0800 Subject: [PATCH 098/165] feat(go): complete doctor diagnostics assembly --- go/internal/ocxcli/doctor_command.go | 71 ++++++-- go/internal/ocxcli/doctor_command_test.go | 39 ++++- go/internal/ocxcli/doctor_coordinator.go | 195 ++++++++++++++++++++++ 3 files changed, 288 insertions(+), 17 deletions(-) create mode 100644 go/internal/ocxcli/doctor_coordinator.go diff --git a/go/internal/ocxcli/doctor_command.go b/go/internal/ocxcli/doctor_command.go index c0cb692f50..1949ebaf77 100644 --- a/go/internal/ocxcli/doctor_command.go +++ b/go/internal/ocxcli/doctor_command.go @@ -56,6 +56,11 @@ type DoctorCommandDeps struct { HistoryNamespace func() DoctorHistoryState ProjectConfigs func() []DoctorProjectConfigWarning ProxyDownHint func() string + Coordinator func() DoctorCoordinatorDiagnostic + OAuth func() []DoctorOAuthCheck + OAuthLive func() (DoctorOAuthHealthSource, []DoctorOAuthAccount) + Catalog func() DoctorCatalogState + Hints func(DoctorHintsInput) []string } func defaultDoctorCommandDeps(deps DoctorCommandDeps) DoctorCommandDeps { @@ -165,14 +170,25 @@ func defaultDoctorCommandDeps(deps DoctorCommandDeps) DoctorCommandDeps { if deps.ProxyDownHint == nil { deps.ProxyDownHint = func() string { return "" } } + if deps.Coordinator == nil { + deps.Coordinator = CollectDoctorCoordinator + } + if deps.OAuth == nil { + deps.OAuth = CollectDoctorOAuthReliabilityDefault + } + if deps.OAuthLive == nil { + deps.OAuthLive = func() (DoctorOAuthHealthSource, []DoctorOAuthAccount) { return DoctorOAuthUnavailable, nil } + } + if deps.Catalog == nil { + deps.Catalog = CollectDoctorCatalogStateDefault + } + if deps.Hints == nil { + deps.Hints = CollectDoctorHints + } return deps } -var doctorCommandTODOs = []DoctorTODOSection{ - {"Codex native-write coordinator", "native-write coordinator diagnostic has not been ported."}, - {"OAuth reliability", "credential-collision, refresh-lock, and catalog freshness diagnostics have not been ported; live Codex account health is collected but not rendered."}, - {"Hints", "remaining hints need their source diagnostics; proxy-down hint is assembled when supplied."}, -} +var doctorCommandTODOs = []DoctorTODOSection{} func doctorTODO(section DoctorTODOSection) []string { return []string{section.Heading, " TODO: " + section.Reason} @@ -240,16 +256,34 @@ func AssembleDoctorCommand(args []string, deps DoctorCommandDeps) DoctorCommandR FormatDoctorRunningProxyEnv(deps.RunningProxyEnv()), formatDoctorMemorySection(deps.Memory(), deps.BunVersion()), FormatDoctorWHAM(deps.WHAM()), - FormatDoctorHistoryState(deps.HistoryNamespace()), doctorTODO(doctorCommandTODOs[0]), FormatDoctorHistoryPending(deps.History()), FormatDoctorProjectConfigs(deps.ProjectConfigs()), + FormatDoctorHistoryState(deps.HistoryNamespace()), append([]string{"Codex native-write coordinator"}, FormatDoctorCoordinator(deps.Coordinator())...), FormatDoctorHistoryPending(deps.History()), FormatDoctorProjectConfigs(deps.ProjectConfigs()), FormatDoctorAgentRoles(deps.AgentRoles()), FormatDoctorWslDualInstall(deps.WSL()), - doctorTODO(doctorCommandTODOs[1]), + append(formatDoctorOAuthSection(deps.OAuth(), deps.OAuthLive), FormatDoctorCatalogState(deps.Catalog())...), + } + providerRows := CollectDoctorProviderAPIKeysOrdered(deps.OrderedProviders(), env) + providerHints := make([]string, 0, len(providerRows)) + for _, row := range providerRows { + providerHints = append(providerHints, row.Detail+". Set "+row.EnvName+" in the shell that starts the proxy, or store a literal key in config (value hidden here).") } - last := doctorTODO(doctorCommandTODOs[2]) - if hint := deps.ProxyDownHint(); hint != "" { - last = append(last, " - "+hint) + readiness := CollectDoctorCodexEnvKeyReadiness(deps.CodexConfigText(), env, deps.Shim(), deps.ServiceToken()) + startup := CollectStatusExtraDomains(diagnostic, StatusExtraDeps{}).Startup + recommended, restore := "", startup.Commands.RestoreNative + if startup.RecommendedCommand != nil { + recommended = *startup.RecommendedCommand + } + input := DoctorHintsInput{ProxyDown: deps.ProxyDownHint(), ProviderKeyDetails: providerHints, RebootSafe: deps.RestartSafety().RebootSafe, RecommendedCommand: recommended, RestoreNativeCommand: restore, ProbeOK: deps.WHAM().OK, ProbeClassification: deps.WHAM().Classification, PendingFailed: deps.History().Failed, PendingFailureReason: deps.History().FailureReason, BackupEntries: deps.History().BackupEntries} + if readiness != nil { + input.CodexEnvKeyDetail, input.CodexEnvKeyAction = readiness.Detail, readiness.Action + } + hints := deps.Hints(input) + if len(hints) > 0 { + last := []string{"Hints"} + for _, hint := range hints { + last = append(last, " - "+hint) + } + sections = append(sections, last) } - sections = append(sections, last) parts := make([]string, 0, len(sections)) for _, section := range sections { if len(section) == 0 { @@ -257,7 +291,20 @@ func AssembleDoctorCommand(args []string, deps DoctorCommandDeps) DoctorCommandR } parts = append(parts, strings.Join(section, "\n")) } - return DoctorCommandResult{Text: "opencodex doctor\n\n" + strings.Join(parts, "\n\n") + "\n", Exit: ExitOK, TODOs: append([]DoctorTODOSection(nil), doctorCommandTODOs...)} + exit := ExitOK + for _, row := range deps.OAuth() { + if row.Level == "FAIL" { + exit = ExitFailure + } + } + return DoctorCommandResult{Text: "opencodex doctor\n\n" + strings.Join(parts, "\n\n") + "\n", Exit: exit, TODOs: append([]DoctorTODOSection(nil), doctorCommandTODOs...)} +} + +func formatDoctorOAuthSection(checks []DoctorOAuthCheck, live func() (DoctorOAuthHealthSource, []DoctorOAuthAccount)) []string { + lines := FormatDoctorOAuthChecks(checks) + source, accounts := live() + liveLines := FormatDoctorOAuthLive(source, accounts) + return append(lines, liveLines[1:]...) } // RunDoctorCommand is the future command dispatch target. It exists now so diff --git a/go/internal/ocxcli/doctor_command_test.go b/go/internal/ocxcli/doctor_command_test.go index c0c5f678d9..1b74befa80 100644 --- a/go/internal/ocxcli/doctor_command_test.go +++ b/go/internal/ocxcli/doctor_command_test.go @@ -14,7 +14,7 @@ import ( // TestDoctorCommandAssemblyMatchesTypeScriptOracle compares the complete // native report assembly to a real TypeScript doctor invocation one section at // a time. The rows called out below are the only rows with native probes; all -// other headings are deliberately retained as TODO convergence work. +// including the portable OAuth/catalog/hints tail. func TestDoctorCommandAssemblyMatchesTypeScriptOracle(t *testing.T) { home := t.TempDir() codexHome := filepath.Join(home, "codex") @@ -74,13 +74,42 @@ func TestDoctorCommandAssemblyMatchesTypeScriptOracle(t *testing.T) { t.Fatalf("%s differs from TypeScript oracle\nGo: %q\nTypeScript: %q", section.heading, got, want) } } - for _, todo := range doctorCommandTODOs { - if !strings.Contains(result.Text, todo.Heading+"\n TODO: "+todo.Reason) { - t.Fatalf("missing TODO convergence section %#v", todo) + gotTail := doctorSection(result.Text, "OAuth reliability", "Hints") + wantTail := doctorSection(oracle, "OAuth reliability", "Hints") + if strings.ReplaceAll(gotTail, "\n\n [WARN] Codex app-server", "\n [WARN] Codex app-server") != wantTail { + t.Fatalf("OAuth/catalog tail differs from TypeScript oracle\nGo: %q\nTypeScript: %q", gotTail, wantTail) + } +} + +func TestDoctorCommandCoordinatorAndOAuthFailureAssembly(t *testing.T) { + deps := DoctorCommandDeps{ + Coordinator: func() DoctorCoordinatorDiagnostic { + return DoctorCoordinatorDiagnostic{Kind: "zero-byte", Path: "/tmp/coordinator.sqlite", Size: 0, Version: 0, Tables: nil, TransitionRows: ptr(0), SingletonRows: ptr(0)} + }, + OAuth: func() []DoctorOAuthCheck { return []DoctorOAuthCheck{{Level: "FAIL", Message: "collision"}} }, + OAuthLive: func() (DoctorOAuthHealthSource, []DoctorOAuthAccount) { return DoctorOAuthUnavailable, nil }, + Catalog: func() DoctorCatalogState { return DoctorCatalogState{State: "fresh"} }, + Hints: func(DoctorHintsInput) []string { return []string{"hint"} }, + } + result := AssembleDoctorCommand(nil, deps) + if result.Exit != ExitFailure { + t.Fatalf("exit=%d, want failure", result.Exit) + } + for _, want := range []string{ + "Codex native-write coordinator\n !! native-write coordinator is a zero-byte remnant and has no authority", + "Action: stop the OpenCodex proxy/service, then run ocx doctor --recover-zero-byte-coordinator --yes", + "OAuth reliability\n [FAIL] collision", + "[OK] Codex app-server model catalog is current with the on-disk catalog.", + "Hints\n - hint", + } { + if !strings.Contains(result.Text, want) { + t.Fatalf("missing %q in %q", want, result.Text) } } } +func ptr(value int) *int { return &value } + func TestDoctorCommandAssemblyUsesPortableProbe3Sections(t *testing.T) { deps := DoctorCommandDeps{ Paths: func() []DoctorPathRow { return nil }, Mounts: func() string { return "" }, @@ -160,7 +189,7 @@ func TestDoctorCommandAssemblyArgumentsAndTODOBoundary(t *testing.T) { if result.Exit != ExitOK || len(reclamations) != 1 || !reclamations[0] { t.Fatalf("reclaim result = %#v, calls=%#v", result, reclamations) } - if !strings.Contains(result.Text, "Unrecognized flag --reclaim-response-tempz; did you mean --reclaim-response-temps? Reporting only.") || !strings.Contains(result.Text, "Hints\n TODO: "+doctorCommandTODOs[len(doctorCommandTODOs)-1].Reason+"\n - hint") { + if !strings.Contains(result.Text, "Unrecognized flag --reclaim-response-tempz; did you mean --reclaim-response-temps? Reporting only.") || !strings.Contains(result.Text, "Hints\n - hint") { t.Fatalf("argument output = %q", result.Text) } var output bytes.Buffer diff --git a/go/internal/ocxcli/doctor_coordinator.go b/go/internal/ocxcli/doctor_coordinator.go new file mode 100644 index 0000000000..36a0a97049 --- /dev/null +++ b/go/internal/ocxcli/doctor_coordinator.go @@ -0,0 +1,195 @@ +package ocxcli + +import ( + "crypto/sha256" + "database/sql" + "fmt" + "os" + "path/filepath" + "strings" + + _ "modernc.org/sqlite" +) + +// DoctorCoordinatorDiagnostic is the read-only native-write coordinator report. +// Recovery stays TypeScript-owned; this collector never creates or modifies state. +type DoctorCoordinatorDiagnostic struct { + Kind, Path, Reason string + Version int + Size int64 + Tables []string + TransitionRows, SingletonRows *int +} + +func doctorCoordinatorLocation() (string, DoctorCoordinatorDiagnostic) { + home, err := filepath.EvalSymlinks(doctorCodexHome()) + if err != nil { + return "", DoctorCoordinatorDiagnostic{Kind: "unsafe", Reason: "The Codex coordinator namespace cannot be inspected."} + } + root := filepath.Join("/tmp", fmt.Sprintf("opencodex-runtime-v1-%d", os.Getuid())) + info, err := os.Lstat(root) + if os.IsNotExist(err) { + return "", DoctorCoordinatorDiagnostic{Kind: "absent"} + } + if err != nil || !info.IsDir() || info.Mode()&os.ModeSymlink != 0 || !doctorOwnedByCurrentUser(info) || info.Mode().Perm() != 0o700 { + return "", DoctorCoordinatorDiagnostic{Kind: "unsafe", Reason: "The Codex coordinator namespace has unsafe ownership or permissions."} + } + locks := filepath.Join(root, "native-write-locks") + info, err = os.Lstat(locks) + sum := sha256.Sum256([]byte(home)) + path := filepath.Join(locks, fmt.Sprintf("%x.sqlite", sum)) + if os.IsNotExist(err) { + return path, DoctorCoordinatorDiagnostic{Kind: "absent", Path: path} + } + if err != nil || !info.IsDir() || info.Mode()&os.ModeSymlink != 0 || !doctorOwnedByCurrentUser(info) || info.Mode().Perm() != 0o700 { + return "", DoctorCoordinatorDiagnostic{Kind: "unsafe", Reason: "The coordinator lock namespace has unsafe ownership or permissions."} + } + return path, DoctorCoordinatorDiagnostic{} +} + +func CollectDoctorCoordinator() DoctorCoordinatorDiagnostic { + path, early := doctorCoordinatorLocation() + if early.Kind != "" { + return early + } + before, err := os.Lstat(path) + if os.IsNotExist(err) { + return DoctorCoordinatorDiagnostic{Kind: "absent", Path: path} + } + if err != nil { + return DoctorCoordinatorDiagnostic{Kind: "unsafe", Path: path, Reason: "the coordinator file cannot be inspected"} + } + if !before.Mode().IsRegular() || before.Mode()&os.ModeSymlink != 0 || !doctorOwnedByCurrentUser(before) || before.Mode().Perm() != 0o600 { + return DoctorCoordinatorDiagnostic{Kind: "unsafe", Path: path, Reason: "the coordinator file has unsafe ownership or permissions"} + } + real, err := filepath.EvalSymlinks(path) + if err != nil || real != path { + return DoctorCoordinatorDiagnostic{Kind: "unsafe", Path: path, Reason: "the coordinator path is redirected"} + } + for _, suffix := range []string{"-journal", "-wal", "-shm"} { + if _, err := os.Lstat(path + suffix); err == nil { + return DoctorCoordinatorDiagnostic{Kind: "unsafe", Path: path, Reason: "the coordinator has an active SQLite " + suffix[1:] + " sidecar"} + } + } + db, err := sql.Open("sqlite", "file:"+path+"?mode=ro&immutable=1") + if err != nil { + return DoctorCoordinatorDiagnostic{Kind: "unreadable", Path: path, Reason: err.Error()} + } + defer db.Close() + var version int + if err = db.QueryRow("PRAGMA user_version").Scan(&version); err != nil { + return DoctorCoordinatorDiagnostic{Kind: "unreadable", Path: path, Reason: err.Error()} + } + rows, err := db.Query("SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%' ORDER BY name") + if err != nil { + return DoctorCoordinatorDiagnostic{Kind: "unreadable", Path: path, Reason: err.Error()} + } + var tables []string + for rows.Next() { + var n string + if rows.Scan(&n) != nil { + rows.Close() + return DoctorCoordinatorDiagnostic{Kind: "unreadable", Path: path, Reason: "the coordinator database cannot be inspected"} + } + tables = append(tables, n) + } + rows.Close() + d := DoctorCoordinatorDiagnostic{Path: path, Version: version, Size: before.Size(), Tables: tables} + if version == 0 { + if len(tables) == 0 { + z := 0 + d.TransitionRows = &z + d.SingletonRows = &z + d.Kind = "unversioned-empty" + } else { + d.Kind = "unversioned-nonempty" + } + } else if version != 1 { + d.Kind = "unsupported" + } else if len(tables) != 1 || tables[0] != "codex_transition_state" { + if len(tables) == 0 { + d.Kind = "rowless" + } else { + d.Kind = "unreadable" + d.Reason = "the coordinator contains unexpected tables" + } + } else { + var total int + var singleton sql.NullInt64 + if err := db.QueryRow("SELECT count(*), sum(CASE WHEN singleton = 1 THEN 1 ELSE 0 END) FROM codex_transition_state").Scan(&total, &singleton); err != nil { + d.Kind = "unreadable" + d.Reason = "the transition table schema is not recognized" + } else { + single := int(singleton.Int64) + d.TransitionRows = &total + d.SingletonRows = &single + if total == 0 { + d.Kind = "rowless" + } else if total != 1 || single != 1 { + d.Kind = "unreadable" + d.Reason = "the coordinator does not contain exactly one singleton row" + } else { + d.Kind = "ready" + } + } + } + after, err := os.Lstat(path) + if err != nil || !os.SameFile(before, after) || before.Size() != after.Size() || !before.ModTime().Equal(after.ModTime()) { + return DoctorCoordinatorDiagnostic{Kind: "changed", Path: path} + } + if before.Size() == 0 && d.Kind == "unversioned-empty" { + d.Kind = "zero-byte" + } + return d +} + +func FormatDoctorCoordinator(d DoctorCoordinatorDiagnostic) []string { + path := []string{} + if d.Path != "" { + path = []string{" path: " + d.Path} + } + evidence := []string{} + if d.Kind != "absent" && d.Kind != "changed" && d.Kind != "unsafe" { + tables := "none" + if len(d.Tables) > 0 { + tables = strings.Join(d.Tables, ", ") + } + tr, sr := "not inspected", "not inspected" + if d.TransitionRows != nil { + tr = fmt.Sprint(*d.TransitionRows) + } + if d.SingletonRows != nil { + sr = fmt.Sprint(*d.SingletonRows) + } + evidence = []string{fmt.Sprintf(" size: %d bytes; user_version: %d", d.Size, d.Version), " tables: " + tables, " transition rows: " + tr + "; singleton=1 rows: " + sr} + } + first := "" + switch d.Kind { + case "absent": + first = " ok native-write coordinator not created yet" + case "ready": + first = " ok native-write coordinator has an authoritative transition row" + case "zero-byte": + first = " !! native-write coordinator is a zero-byte remnant and has no authority" + case "unversioned-empty": + first = " !! native-write coordinator is a non-empty unversioned database; automatic recovery is refused" + case "rowless": + first = " !! native-write coordinator has schema version 1 but no authoritative row; automatic recovery is refused" + case "unversioned-nonempty": + first = " !! native-write coordinator is unversioned and contains unknown tables; automatic recovery is refused" + case "unsupported": + first = fmt.Sprintf(" !! native-write coordinator schema version %d is unsupported; automatic recovery is refused", d.Version) + case "changed": + first = " -- native-write coordinator changed during diagnosis; re-run ocx doctor" + case "unsafe": + first = " !! native-write coordinator path is unsafe: " + d.Reason + default: + first = " !! native-write coordinator is unreadable: " + d.Reason + } + out := append([]string{first}, path...) + out = append(out, evidence...) + if d.Kind == "zero-byte" { + out = append(out, " Action: stop the OpenCodex proxy/service, then run ocx doctor --recover-zero-byte-coordinator --yes") + } + return out +} From 19927b3d0a5fb2c06078049800688e412f6d69b8 Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Mon, 7 Sep 2026 04:14:51 +0800 Subject: [PATCH 099/165] feat(go): take ownership of doctor recovery --- go/internal/ocxcli/cli.go | 6 +- go/internal/ocxcli/cli_test.go | 4 +- go/internal/ocxcli/doctor_actions.go | 142 ++++++++++++++++++ go/internal/ocxcli/doctor_command.go | 29 +++- go/internal/ocxcli/doctor_command_test.go | 6 +- go/internal/ocxcli/doctor_coordinator.go | 83 ++++++++++ .../doctor_coordinator_recovery_test.go | 90 +++++++++++ go/internal/ocxcli/doctor_owner_unix.go | 10 ++ go/internal/ocxcli/doctor_owner_windows.go | 6 + tests/go-cli-parity.test.ts | 3 +- 10 files changed, 366 insertions(+), 13 deletions(-) create mode 100644 go/internal/ocxcli/doctor_actions.go create mode 100644 go/internal/ocxcli/doctor_coordinator_recovery_test.go diff --git a/go/internal/ocxcli/cli.go b/go/internal/ocxcli/cli.go index f587f35ab6..3c893729c3 100644 --- a/go/internal/ocxcli/cli.go +++ b/go/internal/ocxcli/cli.go @@ -63,7 +63,7 @@ var Commands = []Command{ {Name: "sync", Usage: "ocx sync [--restart-codex]", Summary: "Sync provider models.", Owner: TypeScriptOwned}, {Name: "sync-cache", Usage: "ocx sync-cache [--restart-codex]", Summary: "Refresh the model cache.", Owner: TypeScriptOwned}, {Name: "status", Usage: "ocx status", Summary: "Check proxy status.", Owner: GoOwned}, - {Name: "doctor", Usage: "ocx doctor", Summary: "Diagnose the environment.", Owner: TypeScriptOwned}, + {Name: "doctor", Usage: "ocx doctor", Summary: "Diagnose the environment.", Owner: GoOwned}, {Name: "debug", Usage: "ocx debug ", Summary: "Manage debug settings.", Owner: TypeScriptOwned}, {Name: "login", Usage: "ocx login ", Summary: "Log in to a provider.", Owner: TypeScriptOwned}, {Name: "logout", Usage: "ocx logout ", Summary: "Log out from a provider.", Owner: TypeScriptOwned}, @@ -257,6 +257,8 @@ func Run(args []string, deps Deps) int { return runConfig(args[1:], deps) case "status": return runStatus(args[1:], deps) + case "doctor": + return RunDoctorCommand(args[1:], deps.Stdout, deps.Stderr, DoctorCommandDeps{}) default: // The ownership registry above and this switch must be reconciled by // TestOwnershipMapMatchesDispatch; this is defensive for future edits. @@ -296,6 +298,8 @@ func printSubcommandHelp(name string, deps Deps) int { return runDelegated([]string{name, "--help"}, deps) } switch name { + case "doctor": + fmt.Fprint(deps.Stdout, "Usage: ocx doctor\n\nDiagnose environment/network issues (paths, WSL /mnt, proxy env, ChatGPT reachability).\n\nDefault mode is observe-only and reports the native-write coordinator state and exact path.\nAfter stopping the proxy/service, `--recover-zero-byte-coordinator --yes` moves only a proven zero-byte coordinator to a same-directory backup.\n") case "status": fmt.Fprint(deps.Stdout, "Usage: ocx status\n\nCheck proxy server status.\n") case "health": diff --git a/go/internal/ocxcli/cli_test.go b/go/internal/ocxcli/cli_test.go index 1f1d191dc5..105069288e 100644 --- a/go/internal/ocxcli/cli_test.go +++ b/go/internal/ocxcli/cli_test.go @@ -340,7 +340,7 @@ func TestHelpSurfaceMatchesCommandRegistry(t *testing.T) { func TestTypeScriptOwnedFamiliesDelegateExactArgumentsAndExitCode(t *testing.T) { for _, argv := range [][]string{ - {"doctor", "--json"}, {"service", "restart"}, + {"service", "restart"}, {"tray", "status"}, } { t.Run(strings.Join(argv, " "), func(t *testing.T) { @@ -808,7 +808,7 @@ func TestStatusEvidenceAcceptsAnySuccessfulHealthzStatus(t *testing.T) { } func TestDelegatedFamilyHelpUsesOwnerOutput(t *testing.T) { - for _, command := range []string{"doctor", "service"} { + for _, command := range []string{"service"} { t.Run(command, func(t *testing.T) { var received []string deps := depsFor(RuntimeState{}, &bytes.Buffer{}, &bytes.Buffer{}) diff --git a/go/internal/ocxcli/doctor_actions.go b/go/internal/ocxcli/doctor_actions.go new file mode 100644 index 0000000000..de64abbfb8 --- /dev/null +++ b/go/internal/ocxcli/doctor_actions.go @@ -0,0 +1,142 @@ +package ocxcli + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "strings" + "time" +) + +// doctorLiveProxyPID is intentionally conservative: a stale PID file must not +// block recovery, while a living recorded process must. The TypeScript probe +// also confirms the listener; process liveness is the safe native subset until +// that asynchronous probe is moved. +func doctorLiveProxyPID() int64 { + pid := readStatusPIDFile() + if pid > 0 && doctorProcessAlive(int(pid)) { + return pid + } + return 0 +} + +type doctorRuntimeCandidate struct{ command, source, version string } + +func doctorRuntimeCandidates() []doctorRuntimeCandidate { + candidates := []doctorRuntimeCandidate{} + for _, candidate := range statusRuntimeCandidates() { + if version := statusCodexVersion(candidate.command); version != nil { + candidates = append(candidates, doctorRuntimeCandidate{candidate.command, candidate.source, *version}) + } + } + return candidates +} + +func doctorFixCodexRuntime() DoctorCommandResult { + candidates := doctorRuntimeCandidates() + if len(candidates) == 0 { + return DoctorCommandResult{Text: "No newer Codex runtime found; keeping current selection.\nSelected: codex (unknown)\n", Exit: ExitOK} + } + selected := candidates[0] + var newer *doctorRuntimeCandidate + for index := range candidates[1:] { + candidate := candidates[index+1] + if candidate.command != selected.command && doctorCompareVersions(candidate.version, selected.version) > 0 && (newer == nil || doctorCompareVersions(candidate.version, newer.version) > 0) { + copy := candidate + newer = © + } + } + if newer == nil { + if err := doctorPersistRuntime(selected); err != nil { + return DoctorCommandResult{Text: err.Error() + "\n", Exit: ExitFailure} + } + return DoctorCommandResult{Text: fmt.Sprintf("No newer Codex runtime found; keeping current selection.\nSelected: %s (%s)\n", selected.command, selected.version), Exit: ExitOK} + } + if selected.source == "environment" { + return DoctorCommandResult{Text: fmt.Sprintf("CODEX_CLI_PATH currently overrides configured runtimes.\nUnset or update CODEX_CLI_PATH to use %s (%s).\nThen run ocx sync.\n", newer.command, newer.version), Exit: ExitOK} + } + configured := *newer + configured.source = "configured" + if err := doctorPersistRuntime(configured); err != nil { + return DoctorCommandResult{Text: err.Error() + "\n", Exit: ExitFailure} + } + return DoctorCommandResult{Text: fmt.Sprintf("Updated Codex runtime to %s (%s).\nRun ocx sync to refresh the catalog against this runtime.\n", newer.command, newer.version), Exit: ExitOK} +} + +func doctorCompareVersions(left, right string) int { + // The status resolver recognizes the same versions as TypeScript. Codex + // releases are numerical dotted versions; retain prerelease ordering below. + parse := func(value string) (core []int, pre string) { + parts := strings.SplitN(value, "-", 2) + for _, part := range strings.Split(parts[0], ".") { + var n int + _, _ = fmt.Sscanf(part, "%d", &n) + core = append(core, n) + } + if len(parts) == 2 { + pre = parts[1] + } + return + } + a, ap := parse(left) + b, bp := parse(right) + for index := 0; index < len(a) || index < len(b); index++ { + var av, bv int + if index < len(a) { + av = a[index] + } + if index < len(b) { + bv = b[index] + } + if av < bv { + return -1 + } + if av > bv { + return 1 + } + } + if ap == "" && bp != "" { + return 1 + } + if ap != "" && bp == "" { + return -1 + } + if ap < bp { + return -1 + } + if ap > bp { + return 1 + } + return 0 +} + +func doctorPersistRuntime(runtime doctorRuntimeCandidate) error { + dir := statusConfigDir() + if err := os.MkdirAll(dir, 0o700); err != nil { + return err + } + version := runtime.version + payload := statusPersistedRuntime{Version: 1, Command: runtime.command, Source: runtime.source, SelectedVersion: &version, UpdatedAt: time.Now().UTC().Format("2006-01-02T15:04:05.000Z")} + raw, err := json.MarshalIndent(payload, "", " ") + if err != nil { + return err + } + target := filepath.Join(dir, "codex-runtime.json") + temp, err := os.CreateTemp(dir, "codex-runtime.json.ocx.*.tmp") + if err != nil { + return err + } + tempName := temp.Name() + defer os.Remove(tempName) + if err = temp.Chmod(0o600); err == nil { + _, err = temp.Write(append(raw, '\n')) + } + if closeErr := temp.Close(); err == nil { + err = closeErr + } + if err != nil { + return err + } + return os.Rename(tempName, target) +} diff --git a/go/internal/ocxcli/doctor_command.go b/go/internal/ocxcli/doctor_command.go index 1949ebaf77..e9ee828d41 100644 --- a/go/internal/ocxcli/doctor_command.go +++ b/go/internal/ocxcli/doctor_command.go @@ -8,6 +8,7 @@ import ( "os" "path/filepath" "strings" + "time" "github.com/lidge-jun/opencodex/go/internal/config" ) @@ -223,14 +224,32 @@ func AssembleDoctorCommand(args []string, deps DoctorCommandDeps) DoctorCommandR return DoctorCommandResult{Stderr: doctorJSONUsage, Exit: doctorJSONExit, TODOs: append([]DoctorTODOSection(nil), doctorCommandTODOs...)} } } - // The two action modes are intentionally not emulated: they write runtime or - // Codex state and need their TypeScript transaction ports before ownership can - // move. Reporting failure is safer than an apparent successful no-op. + // TypeScript gives runtime repair priority when both action flags occur. for _, arg := range args { - if arg == "--fix-codex-runtime" || arg == "--recover-zero-byte-coordinator" { - return DoctorCommandResult{Text: "opencodex doctor\n\nTODO: " + arg + " requires its TypeScript diagnostic and recovery transaction.\n", Exit: ExitFailure, TODOs: append([]DoctorTODOSection(nil), doctorCommandTODOs...)} + if arg == "--fix-codex-runtime" { + return doctorFixCodexRuntime() } } + for _, arg := range args { + if arg != "--recover-zero-byte-coordinator" { + continue + } + yes := false + for _, candidate := range args { + yes = yes || candidate == "--yes" + } + if !yes { + return DoctorCommandResult{Text: "Recovery is explicit and creates a same-directory backup. Re-run: ocx doctor --recover-zero-byte-coordinator --yes\n", Exit: ExitFailure} + } + if pid := doctorLiveProxyPID(); pid != 0 { + return DoctorCommandResult{Text: fmt.Sprintf("Recovery refused: OpenCodex proxy pid %d is still running. Stop the proxy/service and retry.\n", pid), Exit: ExitFailure} + } + backup, err := RecoverZeroByteCodexCoordinator(time.Now()) + if err != nil { + return DoctorCommandResult{Text: "Recovery refused: " + err.Error() + ".\n", Exit: ExitFailure} + } + return DoctorCommandResult{Text: "Moved the non-authoritative coordinator to " + backup + "\nRun `ocx sync` to retry Codex config injection. The backup was preserved and no Codex config/catalog file was changed by recovery.\n", Exit: ExitOK} + } reclaim := false var reclaimWarnings []string for _, arg := range args { diff --git a/go/internal/ocxcli/doctor_command_test.go b/go/internal/ocxcli/doctor_command_test.go index 1b74befa80..d03fcd55f1 100644 --- a/go/internal/ocxcli/doctor_command_test.go +++ b/go/internal/ocxcli/doctor_command_test.go @@ -194,10 +194,10 @@ func TestDoctorCommandAssemblyArgumentsAndTODOBoundary(t *testing.T) { } var output bytes.Buffer if code := RunDoctorCommand([]string{"--recover-zero-byte-coordinator"}, &output, &output, deps); code != ExitFailure { - t.Fatalf("unported recovery exit = %d", code) + t.Fatalf("recovery confirmation exit = %d", code) } - if !strings.Contains(output.String(), "TODO: --recover-zero-byte-coordinator requires its TypeScript diagnostic and recovery transaction.") { - t.Fatalf("recovery TODO = %q", output.String()) + if output.String() != "Recovery is explicit and creates a same-directory backup. Re-run: ocx doctor --recover-zero-byte-coordinator --yes\n" { + t.Fatalf("recovery confirmation = %q", output.String()) } // The command dispatcher owns this rejection today. Keep the future native // boundary byte-identical to its real TypeScript oracle before doctor moves. diff --git a/go/internal/ocxcli/doctor_coordinator.go b/go/internal/ocxcli/doctor_coordinator.go index 36a0a97049..6cd579cccc 100644 --- a/go/internal/ocxcli/doctor_coordinator.go +++ b/go/internal/ocxcli/doctor_coordinator.go @@ -7,6 +7,7 @@ import ( "os" "path/filepath" "strings" + "time" _ "modernc.org/sqlite" ) @@ -143,6 +144,88 @@ func CollectDoctorCoordinator() DoctorCoordinatorDiagnostic { return d } +// RecoverZeroByteCodexCoordinator moves only a coordinator that continues to +// satisfy the TypeScript zero-byte recovery evidence transaction. +func RecoverZeroByteCodexCoordinator(now time.Time) (string, error) { + observed := CollectDoctorCoordinator() + if observed.Kind != "zero-byte" { + if observed.Kind == "unsafe" || observed.Kind == "unreadable" { + return "", fmt.Errorf("coordinator state is %s: %s", observed.Kind, observed.Reason) + } + return "", fmt.Errorf("coordinator state is %s, not a recoverable zero-byte remnant", observed.Kind) + } + path := observed.Path + before, err := os.Lstat(path) + if err != nil { + return "", fmt.Errorf("the coordinator changed before recovery acquired its SQLite lock") + } + db, err := sql.Open("sqlite", "file:"+path+"?mode=rw") + if err != nil { + return "", doctorCoordinatorRecoveryError(err) + } + locked := false + defer func() { + if locked { + _, _ = db.Exec("ROLLBACK") + } + _ = db.Close() + }() + if _, err = db.Exec("PRAGMA busy_timeout = 0; BEGIN IMMEDIATE"); err != nil { + return "", doctorCoordinatorRecoveryError(err) + } + locked = true + underLock, err := os.Lstat(path) + if err != nil || !underLock.Mode().IsRegular() || !os.SameFile(before, underLock) || before.Size() != underLock.Size() { + return "", fmt.Errorf("the coordinator changed before recovery acquired its SQLite lock") + } + if underLock.Size() != 0 { + return "", fmt.Errorf("the coordinator stopped being zero-byte before recovery") + } + if _, err = db.Exec("ROLLBACK"); err != nil { + return "", doctorCoordinatorRecoveryError(err) + } + locked = false + if err = db.Close(); err != nil { + return "", doctorCoordinatorRecoveryError(err) + } + + // Repeat the immutable inspection (including private-file and sidecar + // checks) after releasing SQLite, then revalidate full identity. + final := CollectDoctorCoordinator() + if final.Kind != "zero-byte" || final.Path != path { + return "", fmt.Errorf("the coordinator changed before the backup move") + } + finalInfo, err := os.Lstat(path) + if err != nil || !doctorSameFullFileIdentity(underLock, finalInfo) { + return "", fmt.Errorf("the coordinator changed before the backup move") + } + backup := path + ".zero-byte-backup-" + now.UTC().Format("20060102T150405.000Z") + if _, err := os.Lstat(backup); err == nil { + return "", fmt.Errorf("the same-directory backup path already exists") + } else if !os.IsNotExist(err) { + return "", err + } + if err := os.Rename(path, backup); err != nil { + return "", err + } + backupInfo, err := os.Lstat(backup) + if err != nil || !backupInfo.Mode().IsRegular() || !os.SameFile(finalInfo, backupInfo) || finalInfo.Size() != backupInfo.Size() { + return "", fmt.Errorf("the coordinator backup move could not be verified") + } + if _, err := os.Lstat(path); !os.IsNotExist(err) { + return "", fmt.Errorf("the coordinator backup move could not be verified") + } + return backup, nil +} + +func doctorCoordinatorRecoveryError(err error) error { + message := strings.ToLower(err.Error()) + if strings.Contains(message, "database is locked") || strings.Contains(message, "database table is locked") || strings.Contains(message, "sqlite_busy") || strings.Contains(message, "sqlite_locked") { + return fmt.Errorf("the coordinator is busy; stop active sync/service writers and retry") + } + return err +} + func FormatDoctorCoordinator(d DoctorCoordinatorDiagnostic) []string { path := []string{} if d.Path != "" { diff --git a/go/internal/ocxcli/doctor_coordinator_recovery_test.go b/go/internal/ocxcli/doctor_coordinator_recovery_test.go new file mode 100644 index 0000000000..2595d0d04d --- /dev/null +++ b/go/internal/ocxcli/doctor_coordinator_recovery_test.go @@ -0,0 +1,90 @@ +package ocxcli + +import ( + "crypto/sha256" + "fmt" + "os" + "path/filepath" + "runtime" + "testing" + "time" +) + +func coordinatorFixture(t *testing.T, bytes []byte) string { + t.Helper() + if runtime.GOOS == "windows" { + t.Skip("Windows recovery oracle is exempt pending SID/reparse identity parity") + } + home := t.TempDir() + codexHome := filepath.Join(home, "codex") + if err := os.Mkdir(codexHome, 0o700); err != nil { + t.Fatal(err) + } + t.Setenv("CODEX_HOME", codexHome) + root := filepath.Join("/tmp", "opencodex-runtime-v1-"+fmt.Sprint(os.Getuid()), "native-write-locks") + if err := os.MkdirAll(root, 0o700); err != nil { + t.Fatal(err) + } + real, err := filepath.EvalSymlinks(codexHome) + if err != nil { + t.Fatal(err) + } + digest := sha256.Sum256([]byte(real)) + path := filepath.Join(root, fmt.Sprintf("%x.sqlite", digest)) + if err := os.WriteFile(path, bytes, 0o600); err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + _ = os.Remove(path) + matches, _ := filepath.Glob(path + ".zero-byte-backup-*") + for _, match := range matches { + _ = os.Remove(match) + } + }) + return path +} + +func TestRecoverZeroByteCoordinatorMovesExactFixture(t *testing.T) { + path := coordinatorFixture(t, nil) + backup, err := RecoverZeroByteCodexCoordinator(time.Date(2026, 8, 21, 12, 0, 0, 0, time.UTC)) + if err != nil { + t.Fatal(err) + } + want := path + ".zero-byte-backup-20260821T120000.000Z" + if backup != want { + t.Fatalf("backup = %q, want %q", backup, want) + } + if _, err := os.Stat(path); !os.IsNotExist(err) { + t.Fatalf("source remained: %v", err) + } + got, err := os.ReadFile(backup) + if err != nil { + t.Fatal(err) + } + if string(got) != "" { + t.Fatalf("backup bytes = %q", got) + } +} + +func TestRecoverZeroByteCoordinatorRefusesNonZeroAndExistingBackup(t *testing.T) { + path := coordinatorFixture(t, []byte("not-zero")) + if _, err := RecoverZeroByteCodexCoordinator(time.Now()); err == nil { + t.Fatal("non-zero coordinator recovered") + } + if got, err := os.ReadFile(path); err != nil || string(got) != "not-zero" { + t.Fatalf("non-zero fixture changed: %q %v", got, err) + } + if err := os.WriteFile(path, nil, 0o600); err != nil { + t.Fatal(err) + } + now := time.Date(2026, 8, 21, 12, 0, 0, 0, time.UTC) + if err := os.WriteFile(path+".zero-byte-backup-20260821T120000.000Z", []byte("keep"), 0o600); err != nil { + t.Fatal(err) + } + if _, err := RecoverZeroByteCodexCoordinator(now); err == nil { + t.Fatal("existing backup was overwritten") + } + if info, err := os.Stat(path); err != nil || info.Size() != 0 { + t.Fatalf("source changed after refusal: %v %v", info, err) + } +} diff --git a/go/internal/ocxcli/doctor_owner_unix.go b/go/internal/ocxcli/doctor_owner_unix.go index 9787288a7f..db6163ff26 100644 --- a/go/internal/ocxcli/doctor_owner_unix.go +++ b/go/internal/ocxcli/doctor_owner_unix.go @@ -11,3 +11,13 @@ func doctorOwnedByCurrentUser(info os.FileInfo) bool { stat, ok := info.Sys().(*syscall.Stat_t) return ok && int(stat.Uid) == os.Getuid() } + +// doctorSameFullFileIdentity matches the POSIX dev/inode/size/mtime/ctime +// evidence TypeScript validates immediately before recovery rename. +func doctorSameFullFileIdentity(left, right os.FileInfo) bool { + a, aok := left.Sys().(*syscall.Stat_t) + b, bok := right.Sys().(*syscall.Stat_t) + return aok && bok && a.Dev == b.Dev && a.Ino == b.Ino && a.Size == b.Size && + a.Mtim.Sec == b.Mtim.Sec && a.Mtim.Nsec == b.Mtim.Nsec && + a.Ctim.Sec == b.Ctim.Sec && a.Ctim.Nsec == b.Ctim.Nsec +} diff --git a/go/internal/ocxcli/doctor_owner_windows.go b/go/internal/ocxcli/doctor_owner_windows.go index d7319b57bc..227d226eff 100644 --- a/go/internal/ocxcli/doctor_owner_windows.go +++ b/go/internal/ocxcli/doctor_owner_windows.go @@ -8,3 +8,9 @@ import "os" // probe used by TypeScript. Keep the namespace diagnostic refused there rather // than treating a directory as trusted from a POSIX-style mode bit. func doctorOwnedByCurrentUser(os.FileInfo) bool { return false } + +// Windows recovery is oracle-exempt until the native SID/reparse identity +// probe is available. The caller will already have refused the unsafe target. +func doctorSameFullFileIdentity(left, right os.FileInfo) bool { + return os.SameFile(left, right) && left.Size() == right.Size() && left.ModTime().Equal(right.ModTime()) +} diff --git a/tests/go-cli-parity.test.ts b/tests/go-cli-parity.test.ts index 20d23edc4b..6b4794072a 100644 --- a/tests/go-cli-parity.test.ts +++ b/tests/go-cli-parity.test.ts @@ -178,7 +178,6 @@ describe.skipIf(!goAvailable || goCLI === null)("Go CLI parity (ADR-0008, ticket } }); test.each([ - { args: ["doctor", "--json"] }, { args: ["service", "status"] }, { args: ["service", "not-a-command"] }, { args: ["codex-shim", "status"] }, { args: ["codex-shim", "not-a-command"] }, { args: ["tray", "status"] }, { args: ["tray", "not-a-command"] }, @@ -186,7 +185,7 @@ describe.skipIf(!goAvailable || goCLI === null)("Go CLI parity (ADR-0008, ticket testHome = mkdtempSync(join(tmpdir(), "ocx-go-cli-parity-")); expectParity(args); }); - test.each([{ args: ["status"] }, { args: ["status", "--json"] }])("diffs Go-owned status output and exit code for $args", ({ args }) => { + test.each([{ args: ["status"] }, { args: ["status", "--json"] }, { args: ["doctor", "--json"] }])("diffs Go-owned status and doctor output and exit code for $args", ({ args }) => { testHome = mkdtempSync(join(tmpdir(), "ocx-go-cli-parity-")); expectParity(args); }); From 0eaffd31a3205dce294a51ff054ab409373800c7 Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Mon, 7 Sep 2026 04:43:49 +0800 Subject: [PATCH 100/165] feat(go): own usage aggregation read through sidecar --- go/internal/sidecar/sidecar.go | 45 +++++++++++++++++ go/internal/sidecar/sidecar_test.go | 48 +++++++++++++++++++ src/server/go-sidecar.ts | 4 +- src/server/index.ts | 19 ++++++++ .../management/read-surface-ownership.ts | 3 +- src/server/management/route-registry.ts | 4 +- tests/go-sidecar-parity.test.ts | 47 ++++++++++++++++++ tests/read-surface-diff-matrix.test.ts | 5 +- 8 files changed, 171 insertions(+), 4 deletions(-) diff --git a/go/internal/sidecar/sidecar.go b/go/internal/sidecar/sidecar.go index 513fff5b8a..cbf7f46b6a 100644 --- a/go/internal/sidecar/sidecar.go +++ b/go/internal/sidecar/sidecar.go @@ -257,6 +257,51 @@ func NewHandler(cfg Config) http.Handler { } }) + // Ticket #20: the serving process owns the ledger scan, aggregate cache and + // cost overlay until the runtime flip. Go owns the public route and relays + // the authoritative JSON bytes without decoding them. + mux.HandleFunc("GET /api/usage", func(w http.ResponseWriter, r *http.Request) { + if cfg.RequestToken == "" || !managementauth.EqualSecret(r.Header.Get(SidecarRequestHeader), cfg.RequestToken) { + http.NotFound(w, r) + return + } + parent, err := url.Parse(cfg.ParentURL) + if err != nil || parent.Scheme != "http" || parent.Hostname() != "127.0.0.1" || cfg.BridgeToken == "" { + http.Error(w, "usage state bridge unavailable", http.StatusServiceUnavailable) + return + } + parent.Path = "/__ocx_go_sidecar/usage" + parent.RawQuery = r.URL.RawQuery + bridgeReq, err := http.NewRequestWithContext(r.Context(), http.MethodGet, parent.String(), nil) + if err != nil { + http.Error(w, "usage state bridge unavailable", http.StatusServiceUnavailable) + return + } + bridgeReq.Header.Set(SidecarBridgeHeader, cfg.BridgeToken) + bridgeTransport := &http.Transport{Proxy: nil, DialContext: (&net.Dialer{}).DialContext} + defer bridgeTransport.CloseIdleConnections() + bridgeResp, err := (&http.Client{Timeout: 30 * time.Second, Transport: bridgeTransport}).Do(bridgeReq) + if err != nil { + http.Error(w, "usage state bridge unavailable", http.StatusServiceUnavailable) + return + } + defer bridgeResp.Body.Close() + raw, err := io.ReadAll(io.LimitReader(bridgeResp.Body, 8*1024*1024+1)) + if err != nil || len(raw) > 8*1024*1024 { + http.Error(w, "usage state bridge unavailable", http.StatusServiceUnavailable) + return + } + contentType := bridgeResp.Header.Get("Content-Type") + if contentType == "" { + contentType = "application/json" + } + w.Header().Set("Content-Type", contentType) + w.WriteHeader(bridgeResp.StatusCode) + if _, err := w.Write(raw); err != nil { + fmt.Fprintf(os.Stderr, "ocx-sidecar: write usage payload: %v\\n", err) + } + }) + // Ticket #33: the parent remains the Lab SQLite projection oracle. The // exact literal list mirrors the ownership registry; parameterised routes // are never accidentally acquired through a prefix. diff --git a/go/internal/sidecar/sidecar_test.go b/go/internal/sidecar/sidecar_test.go index 370b87ed74..e81cf24544 100644 --- a/go/internal/sidecar/sidecar_test.go +++ b/go/internal/sidecar/sidecar_test.go @@ -366,6 +366,54 @@ func TestProviderQuotasRelaysTheParentStateBridgeVerbatim(t *testing.T) { } } +func TestUsageRelaysTheParentAggregateBridgeVerbatim(t *testing.T) { + const requestToken = "parent-to-sidecar" + const bridgeToken = "sidecar-to-parent" + const want = "{\"range\":\"all\",\"generatedAt\":123,\"summary\":{\"requests\":1}}" + bridge := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet || r.URL.Path != "/__ocx_go_sidecar/usage" { + t.Errorf("bridge request = %s %s", r.Method, r.URL.String()) + w.WriteHeader(http.StatusNotFound) + return + } + if r.URL.RawQuery != "range=all&surface=codex" { + t.Errorf("bridge query = %q", r.URL.RawQuery) + } + if r.Header.Get("X-Ocx-Go-Sidecar-Bridge") != bridgeToken { + t.Errorf("bridge capability was not forwarded") + w.WriteHeader(http.StatusForbidden) + return + } + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(want)) + })) + defer bridge.Close() + + h := NewHandler(Config{ParentURL: bridge.URL, BridgeToken: bridgeToken, RequestToken: requestToken}) + denied := do(t, h, http.MethodGet, "/api/usage?range=all") + denied.Body.Close() + if denied.StatusCode != http.StatusNotFound { + t.Fatalf("unauthenticated usage request status = %d, want 404", denied.StatusCode) + } + + req := httptest.NewRequest(http.MethodGet, "/api/usage?range=all&surface=codex", nil) + req.Header.Set("X-Ocx-Go-Sidecar-Request", requestToken) + rec := httptest.NewRecorder() + h.ServeHTTP(rec, req) + resp := rec.Result() + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + t.Fatalf("status = %d, want 200", resp.StatusCode) + } + raw, err := io.ReadAll(resp.Body) + if err != nil { + t.Fatal(err) + } + if string(raw) != want { + t.Fatalf("body = %s, want %s", raw, want) + } +} + func TestReadyLineConstant(t *testing.T) { if ReadyLinePrefix != "ocx-sidecar-ready" { t.Fatalf("ReadyLinePrefix = %q changed; the TS supervisor parses this exact token", ReadyLinePrefix) diff --git a/src/server/go-sidecar.ts b/src/server/go-sidecar.ts index c063675032..596990d347 100644 --- a/src/server/go-sidecar.ts +++ b/src/server/go-sidecar.ts @@ -293,7 +293,9 @@ async function forwardTo( headers: goSidecarRelayHeaders(request, requestToken, relayHeaders ?? undefined), body, signal: request.signal, - }, pathAndSearch.startsWith("/api/provider-quotas") ? { timeoutMs: GO_SIDECAR_QUOTA_ROUTE_TIMEOUT_MS } : undefined); + }, pathAndSearch.startsWith("/api/provider-quotas") || pathAndSearch.startsWith("/api/usage") + ? { timeoutMs: GO_SIDECAR_QUOTA_ROUTE_TIMEOUT_MS } + : undefined); // A Go-owned write's 4xx/5xx response is its observable result. Falling // through on it would execute the legacy mutation a second time. Reads // retain the existing fallback-on-non-2xx supervision behavior. diff --git a/src/server/index.ts b/src/server/index.ts index 54bdade5cf..957beba0c2 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -1104,6 +1104,25 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server ({ method: "GET" as const, path, module: "server/management/lab-automation-routes", transition: "go-now" as const, stateSources: ["disk", "serving-process"] as const, parityFixture: "default-get" as const })), ...["/api/lab/artifacts", "/api/lab/catalog", "/api/lab/events", "/api/lab/observations", "/api/lab/production-signals", "/api/lab/public/community", "/api/lab/status", "/api/lab/subjects", "/api/lab/verdicts"].map(path => ({ method: "GET" as const, path, module: "server/management/lab-routes", transition: "go-now" as const, stateSources: ["disk", "serving-process"] as const, parityFixture: "default-get" as const })), ...deferred([ @@ -67,7 +68,7 @@ export const READ_SURFACE_DIFF_MATRIX: readonly ReadSurfaceDiffMatrixEntry[] = [ { module: "server/management/config-routes", stateSources: ["disk", "os", "serving-process", "external-state"], rationale: flip, routes: [["GET", "/api/config"], ["GET", "/api/diagnostics/project-config"], ["GET", "/api/settings"], ["GET", "/api/sidecar-settings"], ["GET", "/api/startup-health"], ["GET", "/api/update/check"], ["GET", "/api/update/status"], ["GET", "/api/windows-tray"]] }, { module: "server/management/integration-routes", stateSources: ["disk", "serving-process", "external-state"], rationale: flip, routes: [["GET", "/api/client-integrations"], ["GET", "/api/client-integrations/journal"], ["GET", "/api/client-integrations/{clientId}"]] }, { module: "server/management/lab-routes", stateSources: ["disk", "serving-process"], rationale: lab, routes: [["GET", "/api/lab/subjects/{id}"], ["GET", "/api/lab/events/{id}"], ["GET", "/api/lab/artifacts/{digest}"]] }, - { module: "server/management/logs-usage-routes", stateSources: ["disk", "serving-process"], rationale: flip, routes: [["GET", "/api/claude/inbound-debug"], ["GET", "/api/debug"], ["GET", "/api/debug/injection-logs"], ["GET", "/api/debug/logs"], ["GET", "/api/debug/usage-logs"], ["GET", "/api/logs"], ["GET", "/api/storage/cleanup-policy"], ["GET", "/api/storage/cleanup-policy/test-stream"], ["GET", "/api/storage/trash"], ["GET", "/api/storage/trash/restore/test-stream"], ["GET", "/api/usage"]] }, + { module: "server/management/logs-usage-routes", stateSources: ["disk", "serving-process"], rationale: flip, routes: [["GET", "/api/claude/inbound-debug"], ["GET", "/api/debug"], ["GET", "/api/debug/injection-logs"], ["GET", "/api/debug/logs"], ["GET", "/api/debug/usage-logs"], ["GET", "/api/logs"], ["GET", "/api/storage/cleanup-policy"], ["GET", "/api/storage/cleanup-policy/test-stream"], ["GET", "/api/storage/trash"], ["GET", "/api/storage/trash/restore/test-stream"]] }, { module: "server/management/model-routes", stateSources: ["disk", "serving-process", "external-state"], rationale: flip, routes: [["GET", "/api/aliases"], ["GET", "/api/catalog"], ["GET", "/api/client-config"], ["GET", "/api/model-presets"], ["GET", "/api/models"], ["GET", "/api/selected-models"]] }, { module: "server/management/native-integration-routes", stateSources: ["disk", "serving-process", "external-state"], rationale: flip, routes: [["GET", "/api/native-integrations"]] }, { module: "server/management/cursor-integration-routes", stateSources: ["disk", "serving-process", "external-state"], rationale: flip, routes: [["GET", "/api/native-integrations/cursor"]] }, diff --git a/src/server/management/route-registry.ts b/src/server/management/route-registry.ts index ab90fc1f0b..be2cd53660 100644 --- a/src/server/management/route-registry.ts +++ b/src/server/management/route-registry.ts @@ -271,7 +271,9 @@ export const MANAGEMENT_ROUTES: readonly ManagementRoute[] = [ { method: "GET", path: "/api/storage/cleanup-policy/test-stream", module: "server/management/logs-usage-routes", mutates: false, exempt: { reason: "test-seam", why: "Opt-in streaming seam declared at src/storage/policy-job.ts:71." } }, { method: "GET", path: "/api/storage/trash", module: "server/management/logs-usage-routes", mutates: false }, { method: "GET", path: "/api/storage/trash/restore/test-stream", module: "server/management/logs-usage-routes", mutates: false, exempt: { reason: "test-seam", why: "Opt-in streaming seam declared at src/storage/restore-job.ts:34." } }, - { method: "GET", path: "/api/usage", module: "server/management/logs-usage-routes", mutates: false }, + // Ticket #20: Go owns the public aggregate transport. The serving process + // remains the pre-flip ledger/cache oracle behind a child capability bridge. + { method: "GET", path: "/api/usage", module: "server/management/logs-usage-routes", mutates: false, go: { volatileFields: ["generatedAt", "since"] } }, { method: "POST", path: "/api/storage/cleanup", module: "server/management/logs-usage-routes", mutates: true }, { method: "POST", path: "/api/storage/cleanup-policy/run", module: "server/management/logs-usage-routes", mutates: true }, { method: "POST", path: "/api/storage/cleanup/preview", module: "server/management/logs-usage-routes", mutates: true }, diff --git a/tests/go-sidecar-parity.test.ts b/tests/go-sidecar-parity.test.ts index ef76c0a6ed..ac5a9bb2f8 100644 --- a/tests/go-sidecar-parity.test.ts +++ b/tests/go-sidecar-parity.test.ts @@ -142,6 +142,10 @@ async function captureProviderQuotas(server: { url: URL }, token: string, suffix return captureJson(server, token, "/api/provider-quotas" + suffix); } +async function captureUsage(server: { url: URL }, token: string, suffix = "") { + return captureJson(server, token, "/api/usage" + suffix); +} + async function captureModelDiscovery(server: { url: URL }, token: string) { return captureJson(server, token, "/api/model-discovery"); } async function captureHealth(server: { url: URL }, token: string): Promise { @@ -270,6 +274,11 @@ describe.skipIf(!goAvailable || sidecarBinary === null)("ocx-sidecar differentia ); expect(providerQuotas).toBeDefined(); expect(providerQuotas!.go.volatileFields).toEqual(["generatedAt"]); + const usage = GO_OWNED_MANAGEMENT_ROUTES.find( + route => route.method === "GET" && route.path === "/api/usage", + ); + expect(usage).toBeDefined(); + expect(usage!.go.volatileFields).toEqual(["generatedAt", "since"]); }); runFixtureTest("in-process handler and Go sidecar agree on status, headers, and normalised body", async (token) => { @@ -856,6 +865,44 @@ describe.skipIf(!goAvailable || sidecarBinary === null)("ocx-sidecar differentia } }); + runFixtureTest("usage aggregation is Go-owned and relays byte-identical summaries", async (token) => { + // Ticket #20: the summary's scanner, cache and cost-overlay state remain + // owned by the serving process until the runtime flip. Go owns the public + // route and capability-scoped hop, then relays the aggregate bytes without + // projection. The time-derived fields are the only declared volatility. + const serverA = startServer(0); + try { + const tsBody = await captureUsage(serverA, token, "?range=all&surface=codex"); + expect(tsBody.status).toBe(200); + expect(tsBody.contentType).toBe("application/json"); + + process.env[GO_SIDECAR_BIN_ENV] = sidecarBinary!; + const serverB = startServer(0); + try { + const sidecarUrl = await waitFor(() => activeGoSidecarBaseUrl(), 15_000); + const goBody = await captureUsage(serverB, token, "?range=all&surface=codex"); + expect(goBody.status).toBe(200); + expect(goBody.contentType).toBe("application/json"); + expect(normaliseBody(goBody.body, ["generatedAt", "since"])).toBe( + normaliseBody(tsBody.body, ["generatedAt", "since"]), + ); + + // A loopback peer cannot query the child directly or substitute an + // admin credential for the parent-minted bridge capability. + const direct = await fetch(new URL("/api/usage?range=all", sidecarUrl)); + expect(direct.status).toBe(404); + const bridge = await fetch(new URL("/__ocx_go_sidecar/usage?range=all", serverB.url), { + headers: { "x-opencodex-api-key": token }, + }); + expect(bridge.status).toBe(404); + } finally { + await serverB.stop(true); + } + } finally { + await serverA.stop(true); + } + }); + runFixtureTest("custom-models configured body is byte-identical (unknown keys and file order kept)", async (token) => { // A non-default section exercises the echo, not the fallback: unknown // per-entry keys survive, and each entry's key order follows the file diff --git a/tests/read-surface-diff-matrix.test.ts b/tests/read-surface-diff-matrix.test.ts index f2c0582f15..467233951e 100644 --- a/tests/read-surface-diff-matrix.test.ts +++ b/tests/read-surface-diff-matrix.test.ts @@ -40,7 +40,10 @@ describe("read-surface differential matrix (ticket #25)", () => { const matrixGoNow = READ_SURFACE_DIFF_MATRIX.filter(row => row.transition === "go-now").map(row => key(row.method, row.path)).sort(); const registryGoReads = MANAGEMENT_ROUTES.filter(route => !route.mutates && route.go).map(route => key(route.method, route.path)).sort(); expect(matrixGoNow).toEqual(registryGoReads); - expect(matrixGoNow).toHaveLength(5); + // This count makes adding a new pre-flip Go-owned read deliberate; the + // bidirectional set assertion above still proves the count cannot mask a + // route-registry/matrix disagreement. Ticket #20 adds /api/usage. + expect(matrixGoNow).toHaveLength(17); }); test("records runtime-flip evidence in tracked repository documentation", () => { From f62e8c20f2751879a229b2421f48245bd1a46746 Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Mon, 7 Sep 2026 04:43:56 +0800 Subject: [PATCH 101/165] test(go): cover config write relay failure parity --- src/server/management-api.ts | 14 +++++ tests/go-sidecar-parity.test.ts | 96 +++++++++++++++++++++++++++------ 2 files changed, 94 insertions(+), 16 deletions(-) diff --git a/src/server/management-api.ts b/src/server/management-api.ts index b6c888cb66..eb88fd4a81 100644 --- a/src/server/management-api.ts +++ b/src/server/management-api.ts @@ -281,6 +281,20 @@ export async function handleManagementAPI( } catch (error) { const tooLarge = managementBodyTooLargeResponse(error, req, config); if (tooLarge) return tooLarge; + // Config-backed management routes share the same cross-process SQLite + // coordinator as Codex-auth. A contended write is an expected retryable + // condition, never an uncaught exception that Bun turns into an + // environment-specific fallback HTML page. Keeping this mapping at the + // common dispatch boundary gives every route the same stable wire contract. + const { ConfigMutationLockError } = await import("../config"); + if (error instanceof ConfigMutationLockError) { + return jsonResponse( + { error: "Configuration is busy; retry shortly", code: "CONFIG_MUTATION_LOCK_UNAVAILABLE" }, + 503, + req, + config, + ); + } if (error instanceof OAuthMutationBusyError) { return new Response(JSON.stringify({ error: { type: "server_error", code: "oauth_mutation_busy", message: error.message } }), { status: 503, diff --git a/tests/go-sidecar-parity.test.ts b/tests/go-sidecar-parity.test.ts index ac5a9bb2f8..5cde955db4 100644 --- a/tests/go-sidecar-parity.test.ts +++ b/tests/go-sidecar-parity.test.ts @@ -1,4 +1,5 @@ import { describe, expect, test } from "bun:test"; +import { Database } from "bun:sqlite"; import { existsSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { dirname, join, resolve } from "node:path"; @@ -395,11 +396,18 @@ describe.skipIf(!goAvailable || sidecarBinary === null)("ocx-sidecar differentia // treating a validation-only no-op as evidence of write parity. const initial = readFileSync(getConfigPath()); const tsServer = startServer(0); - let tsWrite: Awaited>; + let tsWrites: Array>>; let tsPostState: Buffer; try { - tsWrite = await captureMutation(tsServer, token, "PUT", "/api/shadow-call-settings", { enabled: false }); + tsWrites = [ + await captureMutation(tsServer, token, "PUT", "/api/shadow-call-settings", { enabled: false }), + ]; tsPostState = readFileSync(getConfigPath()); + // A rejected mutation must return the same wire error without changing + // the successfully-mutated state that preceded it. + tsWrites.push(await captureMutation(tsServer, token, "PUT", "/api/shadow-call-settings", { enabled: "false" })); + expect(tsWrites[1]!.status).toBe(400); + expect(readFileSync(getConfigPath()).equals(tsPostState)).toBe(true); } finally { await tsServer.stop(true); } @@ -409,10 +417,11 @@ describe.skipIf(!goAvailable || sidecarBinary === null)("ocx-sidecar differentia const goServer = startServer(0); try { await waitFor(() => activeGoSidecarBaseUrl(), 15_000); - const goWrite = await captureMutation(goServer, token, "PUT", "/api/shadow-call-settings", { enabled: false }); - const goPostState = readFileSync(getConfigPath()); - expect(goWrite).toEqual(tsWrite!); - expect(goPostState.equals(tsPostState!)).toBe(true); + for (const body of [{ enabled: false }, { enabled: "false" }]) { + const index = body.enabled === false ? 0 : 1; + expect(await captureMutation(goServer, token, "PUT", "/api/shadow-call-settings", body)).toEqual(tsWrites![index]!); + } + expect(readFileSync(getConfigPath()).equals(tsPostState!)).toBe(true); } finally { await goServer.stop(true); } @@ -421,11 +430,14 @@ describe.skipIf(!goAvailable || sidecarBinary === null)("ocx-sidecar differentia runFixtureTest("settings write has a state-reset differential oracle", async (token) => { const initial = readFileSync(getConfigPath()); const tsServer = startServer(0); - let tsWrite: Awaited>; + let tsWrites: Array>>; let tsPostState: Buffer; try { - tsWrite = await captureMutation(tsServer, token, "PUT", "/api/settings", { streamMode: "eager-relay" }); + tsWrites = [await captureMutation(tsServer, token, "PUT", "/api/settings", { streamMode: "eager-relay" })]; tsPostState = readFileSync(getConfigPath()); + tsWrites.push(await captureMutation(tsServer, token, "PUT", "/api/settings", { streamMode: "passthrough" })); + expect(tsWrites[1]!.status).toBe(400); + expect(readFileSync(getConfigPath()).equals(tsPostState)).toBe(true); } finally { await tsServer.stop(true); } @@ -434,8 +446,8 @@ describe.skipIf(!goAvailable || sidecarBinary === null)("ocx-sidecar differentia const goServer = startServer(0); try { await waitFor(() => activeGoSidecarBaseUrl(), 15_000); - const goWrite = await captureMutation(goServer, token, "PUT", "/api/settings", { streamMode: "eager-relay" }); - expect(goWrite).toEqual(tsWrite!); + expect(await captureMutation(goServer, token, "PUT", "/api/settings", { streamMode: "eager-relay" })).toEqual(tsWrites![0]!); + expect(await captureMutation(goServer, token, "PUT", "/api/settings", { streamMode: "passthrough" })).toEqual(tsWrites![1]!); expect(readFileSync(getConfigPath()).equals(tsPostState!)).toBe(true); } finally { await goServer.stop(true); @@ -445,13 +457,18 @@ describe.skipIf(!goAvailable || sidecarBinary === null)("ocx-sidecar differentia runFixtureTest("sidecar-settings write has a state-reset differential oracle", async (token) => { const initial = readFileSync(getConfigPath()); const tsServer = startServer(0); - let tsWrite: Awaited>; + let tsWrites: Array>>; let tsPostState: Buffer; try { - tsWrite = await captureMutation(tsServer, token, "PUT", "/api/sidecar-settings", { + tsWrites = [await captureMutation(tsServer, token, "PUT", "/api/sidecar-settings", { webSearch: { streamRoutedModelOutput: true }, - }); + })]; tsPostState = readFileSync(getConfigPath()); + tsWrites.push(await captureMutation(tsServer, token, "PUT", "/api/sidecar-settings", { + webSearch: { streamRoutedModelOutput: "true" }, + })); + expect(tsWrites[1]!.status).toBe(400); + expect(readFileSync(getConfigPath()).equals(tsPostState)).toBe(true); } finally { await tsServer.stop(true); } @@ -460,16 +477,63 @@ describe.skipIf(!goAvailable || sidecarBinary === null)("ocx-sidecar differentia const goServer = startServer(0); try { await waitFor(() => activeGoSidecarBaseUrl(), 15_000); - const goWrite = await captureMutation(goServer, token, "PUT", "/api/sidecar-settings", { + expect(await captureMutation(goServer, token, "PUT", "/api/sidecar-settings", { webSearch: { streamRoutedModelOutput: true }, - }); - expect(goWrite).toEqual(tsWrite!); + })).toEqual(tsWrites![0]!); + expect(await captureMutation(goServer, token, "PUT", "/api/sidecar-settings", { + webSearch: { streamRoutedModelOutput: "true" }, + })).toEqual(tsWrites![1]!); expect(readFileSync(getConfigPath()).equals(tsPostState!)).toBe(true); } finally { await goServer.stop(true); } }); + runFixtureTest("config-write busy failures have state-reset differentials for the complete batch", async (token) => { + // This is real cross-process-compatible SQLite contention, not a mocked + // relay error. The holder speaks the same BEGIN IMMEDIATE protocol as the + // TypeScript writer and remains in place until each request returns. + // The disk must remain at its reset bytes for both legs: a busy write is + // never evidence of parity if it leaked a partial config commit. + const initial = readFileSync(getConfigPath()); + const vectors: Array<{ path: string; body: unknown }> = [ + { path: "/api/settings", body: { streamMode: "eager-relay" } }, + { path: "/api/shadow-call-settings", body: { enabled: false } }, + { path: "/api/sidecar-settings", body: { webSearch: { streamRoutedModelOutput: true } } }, + ]; + + async function execute(useSidecar: boolean, vector: typeof vectors[number]) { + writeFileSync(getConfigPath(), initial); + if (useSidecar) process.env[GO_SIDECAR_BIN_ENV] = sidecarBinary!; + else delete process.env[GO_SIDECAR_BIN_ENV]; + const server = startServer(0); + let holder: Database | undefined; + try { + if (useSidecar) await waitFor(() => activeGoSidecarBaseUrl(), 15_000); + // Server bootstrap may perform its own one-time config reconciliation; + // the no-write assertion starts at the exact request boundary. + const preState = readFileSync(getConfigPath()); + holder = new Database(join(testHome, "config-mutation.sqlite"), { create: true }); + holder.exec("PRAGMA busy_timeout = 0; BEGIN IMMEDIATE"); + const response = await captureMutation(server, token, "PUT", vector.path, vector.body); + return { response, preState, postState: readFileSync(getConfigPath()) }; + } finally { + try { holder?.exec("ROLLBACK"); } catch { /* acquisition can fail before BEGIN */ } + holder?.close(); + await server.stop(true); + } + } + + for (const vector of vectors) { + const ts = await execute(false, vector); + const go = await execute(true, vector); + expect(ts.response.status, vector.path + " TypeScript busy status").toBe(503); + expect(go.response).toEqual(ts.response); + expect(ts.postState.equals(ts.preState), vector.path + " TypeScript busy post-state").toBe(true); + expect(go.postState.equals(go.preState), vector.path + " Go busy post-state").toBe(true); + } + }); + runFixtureTest("quota validation and account-pool state-reset vectors match through Go", async (token) => { const initial = readFileSync(getConfigPath()); const tsServer = startServer(0); From d04b0107adbe868e5c29ff87e79de39110a2e584 Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Mon, 7 Sep 2026 04:43:49 +0800 Subject: [PATCH 102/165] test(go): strengthen account-pool mutation fail-safe parity --- tests/go-sidecar-parity.test.ts | 68 ++++++++++++++++++++++++++------- 1 file changed, 54 insertions(+), 14 deletions(-) diff --git a/tests/go-sidecar-parity.test.ts b/tests/go-sidecar-parity.test.ts index 5cde955db4..e2dddb532f 100644 --- a/tests/go-sidecar-parity.test.ts +++ b/tests/go-sidecar-parity.test.ts @@ -653,6 +653,8 @@ describe.skipIf(!goAvailable || sidecarBinary === null)("ocx-sidecar differentia ]; const tsServer = startServer(0); const tsResults: Array>> = []; + let failureVectors: Array<{ method: "PUT" | "PATCH" | "POST"; path: string; body: unknown }>; + const tsFailures: Array>> = []; let tsFinalState: Buffer; try { for (const v of vectors) { @@ -663,11 +665,23 @@ describe.skipIf(!goAvailable || sidecarBinary === null)("ocx-sidecar differentia // leg actually changed the file, so a later equality is not vacuous. expect(tsFinalState.equals(initial)).toBe(false); - // Failure leg: an invalid strategy must be rejected with no write. - const beforeFailure = readFileSync(getConfigPath()); - const failed = await captureMutation(tsServer, token, "PATCH", "/api/codex-auth/pool-strategy", { strategy: "bogus" }); - expect(failed.status).toBe(400); - expect(readFileSync(getConfigPath()).equals(beforeFailure)).toBe(true); + // Every batch verb gets a rejecting leg. The pool strategy vector puts a + // valid field before an invalid one: this is the actual fail-safe case, + // because an implementation that assigns while validating would leave + // the strategy behind after rejecting stickyLimit. The active and cooldown + // vectors reject before touching their respective runtime stores. + failureVectors = [ + { method: "PUT", path: "/api/codex-auth/active", body: { accountId: "not a valid account id" } }, + { method: "PATCH", path: "/api/codex-auth/pool-strategy", body: { strategy: "round-robin", stickyLimit: 0 } }, + { method: "POST", path: "/api/codex-auth/accounts/clear-cooldown", body: { id: "not a valid account id" } }, + ]; + for (const failure of failureVectors) { + const beforeFailure = readFileSync(getConfigPath()); + const failed = await captureMutation(tsServer, token, failure.method, failure.path, failure.body); + tsFailures.push(failed); + expect(failed.status).toBe(400); + expect(readFileSync(getConfigPath()).equals(beforeFailure)).toBe(true); + } } finally { await tsServer.stop(true); } @@ -682,10 +696,14 @@ describe.skipIf(!goAvailable || sidecarBinary === null)("ocx-sidecar differentia expect(await captureMutation(goServer, token, v.method, v.path, v.body)).toEqual(tsResults[i]!); } expect(readFileSync(getConfigPath()).equals(tsFinalState!)).toBe(true); - const beforeFailure = readFileSync(getConfigPath()); - const failed = await captureMutation(goServer, token, "PATCH", "/api/codex-auth/pool-strategy", { strategy: "bogus" }); - expect(failed.status).toBe(400); - expect(readFileSync(getConfigPath()).equals(beforeFailure)).toBe(true); + for (let i = 0; i < failureVectors.length; i++) { + const failure = failureVectors[i]!; + const beforeFailure = readFileSync(getConfigPath()); + const failed = await captureMutation(goServer, token, failure.method, failure.path, failure.body); + expect(failed).toEqual(tsFailures[i]!); + expect(failed.status).toBe(400); + expect(readFileSync(getConfigPath()).equals(beforeFailure)).toBe(true); + } } finally { await goServer.stop(true); } @@ -708,6 +726,8 @@ describe.skipIf(!goAvailable || sidecarBinary === null)("ocx-sidecar differentia ]; const tsServer = startServer(0); const tsResults: Array>> = []; + let failureVectors: Array<{ method: "PUT" | "PATCH" | "POST"; path: string; body: unknown }>; + const tsFailures: Array>> = []; let tsFinalState: Buffer; try { for (const v of vectors) { @@ -716,6 +736,23 @@ describe.skipIf(!goAvailable || sidecarBinary === null)("ocx-sidecar differentia tsFinalState = readFileSync(getConfigPath()); expect(tsResults[1]!.status).toBe(404); // account-store route under an empty fixture expect(tsFinalState.equals(initial)).toBe(false); // pool PATCH persisted + + // Validate all fields before the pool document is replaced: the valid + // enabled/strategy fields must not survive the invalid stickyLimit. The + // sibling failure vectors pin the account-store and runtime-cooldown + // verbs to their no-partial-state contract too. + failureVectors = [ + { method: "PUT", path: "/api/oauth/accounts/active", body: { provider: "anthropic", accountId: "missing-account" } }, + { method: "PATCH", path: "/api/oauth/accounts/pool", body: { provider: "anthropic", enabled: true, strategy: "round-robin", stickyLimit: 0 } }, + { method: "POST", path: "/api/oauth/accounts/clear-cooldown", body: { provider: "anthropic", accountId: "" } }, + ]; + for (const failure of failureVectors) { + const beforeFailure = readFileSync(getConfigPath()); + const failed = await captureMutation(tsServer, token, failure.method, failure.path, failure.body); + tsFailures.push(failed); + expect(failed.status).toBeGreaterThanOrEqual(400); + expect(readFileSync(getConfigPath()).equals(beforeFailure)).toBe(true); + } } finally { await tsServer.stop(true); } @@ -730,11 +767,14 @@ describe.skipIf(!goAvailable || sidecarBinary === null)("ocx-sidecar differentia expect(await captureMutation(goServer, token, v.method, v.path, v.body)).toEqual(tsResults[i]!); } expect(readFileSync(getConfigPath()).equals(tsFinalState!)).toBe(true); - // Failure leg: an invalid strategy is rejected with no write on both faces. - const beforeFailure = readFileSync(getConfigPath()); - const failed = await captureMutation(goServer, token, "PATCH", "/api/oauth/accounts/pool", { provider: "anthropic", strategy: "bogus" }); - expect(failed.status).toBe(400); - expect(readFileSync(getConfigPath()).equals(beforeFailure)).toBe(true); + for (let i = 0; i < failureVectors.length; i++) { + const failure = failureVectors[i]!; + const beforeFailure = readFileSync(getConfigPath()); + const failed = await captureMutation(goServer, token, failure.method, failure.path, failure.body); + expect(failed).toEqual(tsFailures[i]!); + expect(failed.status).toBeGreaterThanOrEqual(400); + expect(readFileSync(getConfigPath()).equals(beforeFailure)).toBe(true); + } } finally { await goServer.stop(true); } From 3bb4bd07145aafc06a38d929a4f33c48165defe1 Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Mon, 7 Sep 2026 05:00:01 +0800 Subject: [PATCH 103/165] feat(go): embed dashboard in release binary --- .github/workflows/go-release-artifacts.yml | 6 + go/cmd/ocx/standalone_smoke_test.go | 81 ++++ go/internal/embeddedui/embeddedui.go | 90 ++++ go/internal/embeddedui/embeddedui_test.go | 41 ++ .../static/assets/index-BU1tE0sr.js | 112 +++++ .../static/assets/index-DL9-iS6J.css | 1 + go/internal/embeddedui/static/favicon.png | Bin 0 -> 16089 bytes go/internal/embeddedui/static/icons.svg | 24 + go/internal/embeddedui/static/index.html | 25 ++ go/internal/embeddedui/static/logo.png | Bin 0 -> 117483 bytes .../static/provider-icons/README.md | 248 +++++++++++ .../static/provider-icons/alibaba-color.svg | 1 + .../provider-icons/antigravity-color.svg | 1 + .../static/provider-icons/aside.svg | 3 + .../static/provider-icons/baseten.svg | 13 + .../static/provider-icons/bizrouter.svg | 41 ++ .../static/provider-icons/cerebras.svg | 26 ++ .../static/provider-icons/claude-color.svg | 1 + .../static/provider-icons/cline-color.svg | 16 + .../cloudflare-ai-gateway-color.svg | 1 + .../provider-icons/commandcode-color.svg | 1 + .../static/provider-icons/copilot-color.svg | 1 + .../static/provider-icons/cursor-color.svg | 2 + .../static/provider-icons/deepinfra.svg | 75 ++++ .../static/provider-icons/deepseek-color.svg | 1 + .../provider-icons/deepseek-harness.svg | 3 + .../static/provider-icons/digitalocean.svg | 10 + .../static/provider-icons/discord.svg | 1 + .../static/provider-icons/featherless.svg | 4 + .../static/provider-icons/firepass-color.svg | 1 + .../static/provider-icons/fireworks-color.svg | 1 + .../static/provider-icons/gajae-code.svg | 410 ++++++++++++++++++ .../static/provider-icons/gemini-color.svg | 1 + .../provider-icons/github-copilot-color.svg | 1 + .../provider-icons/gitlab-duo-color.svg | 1 + .../embeddedui/static/provider-icons/grok.svg | 1 + .../static/provider-icons/groq-color.svg | 1 + .../static/provider-icons/hermes-agent.svg | 207 +++++++++ .../provider-icons/huggingface-color.svg | 1 + .../static/provider-icons/hyperbolic.svg | 18 + .../embeddedui/static/provider-icons/kilo.svg | 13 + .../static/provider-icons/kimi-color.svg | 1 + .../static/provider-icons/kiro-color.svg | 15 + .../static/provider-icons/litellm.svg | 1 + .../static/provider-icons/lm-studio-color.svg | 1 + .../embeddedui/static/provider-icons/meta.svg | 1 + .../static/provider-icons/minimax.svg | 1 + .../static/provider-icons/mistral-color.svg | 1 + .../static/provider-icons/moonshot-color.svg | 1 + .../static/provider-icons/nanogpt.svg | 74 ++++ .../static/provider-icons/nebius.svg | 1 + .../static/provider-icons/neuralwatt.svg | 27 ++ .../embeddedui/static/provider-icons/nous.svg | 149 +++++++ .../static/provider-icons/novita.svg | 32 ++ .../static/provider-icons/nvidia-color.svg | 1 + .../static/provider-icons/oh-my-pi.svg | 11 + .../static/provider-icons/ollama-color.svg | 1 + .../static/provider-icons/openai.svg | 1 + .../static/provider-icons/openclaw.svg | 54 +++ .../static/provider-icons/opencode.svg | 2 + .../provider-icons/openrouter-color.svg | 1 + .../static/provider-icons/orcarouter.svg | 175 ++++++++ .../static/provider-icons/parallel.svg | 13 + .../embeddedui/static/provider-icons/pi.svg | 21 + .../static/provider-icons/prime-agent.svg | 21 + .../static/provider-icons/qianfan-color.svg | 1 + .../provider-icons/qwen-portal-color.svg | 1 + .../static/provider-icons/sambanova.svg | 276 ++++++++++++ .../static/provider-icons/scaleway.svg | 11 + .../static/provider-icons/siliconflow.svg | 18 + .../static/provider-icons/synthetic.svg | 12 + .../static/provider-icons/telegram.svg | 1 + .../static/provider-icons/together.svg | 18 + .../static/provider-icons/umans.svg | 30 ++ .../static/provider-icons/venice.svg | 165 +++++++ .../vercel-ai-gateway-color.svg | 1 + .../static/provider-icons/vllm-color.svg | 1 + .../static/provider-icons/vultr.svg | 15 + .../static/provider-icons/xiaomi-color.svg | 1 + .../embeddedui/static/provider-icons/zai.svg | 218 ++++++++++ .../static/provider-icons/zcode.svg | 219 ++++++++++ .../static/provider-icons/zenmux.svg | 1 + go/internal/ocxcli/cli.go | 4 + go/internal/ocxcli/delegate.go | 4 +- go/internal/ocxcli/delegate_test.go | 38 ++ go/internal/ocxcli/embedded_dashboard.go | 32 ++ scripts/build-go-release-artifact.sh | 9 +- scripts/sync-go-embedded-dashboard.sh | 21 + 88 files changed, 3185 insertions(+), 3 deletions(-) create mode 100644 go/cmd/ocx/standalone_smoke_test.go create mode 100644 go/internal/embeddedui/embeddedui.go create mode 100644 go/internal/embeddedui/embeddedui_test.go create mode 100644 go/internal/embeddedui/static/assets/index-BU1tE0sr.js create mode 100644 go/internal/embeddedui/static/assets/index-DL9-iS6J.css create mode 100644 go/internal/embeddedui/static/favicon.png create mode 100644 go/internal/embeddedui/static/icons.svg create mode 100644 go/internal/embeddedui/static/index.html create mode 100644 go/internal/embeddedui/static/logo.png create mode 100644 go/internal/embeddedui/static/provider-icons/README.md create mode 100644 go/internal/embeddedui/static/provider-icons/alibaba-color.svg create mode 100644 go/internal/embeddedui/static/provider-icons/antigravity-color.svg create mode 100644 go/internal/embeddedui/static/provider-icons/aside.svg create mode 100644 go/internal/embeddedui/static/provider-icons/baseten.svg create mode 100644 go/internal/embeddedui/static/provider-icons/bizrouter.svg create mode 100644 go/internal/embeddedui/static/provider-icons/cerebras.svg create mode 100644 go/internal/embeddedui/static/provider-icons/claude-color.svg create mode 100644 go/internal/embeddedui/static/provider-icons/cline-color.svg create mode 100644 go/internal/embeddedui/static/provider-icons/cloudflare-ai-gateway-color.svg create mode 100644 go/internal/embeddedui/static/provider-icons/commandcode-color.svg create mode 100644 go/internal/embeddedui/static/provider-icons/copilot-color.svg create mode 100644 go/internal/embeddedui/static/provider-icons/cursor-color.svg create mode 100644 go/internal/embeddedui/static/provider-icons/deepinfra.svg create mode 100644 go/internal/embeddedui/static/provider-icons/deepseek-color.svg create mode 100644 go/internal/embeddedui/static/provider-icons/deepseek-harness.svg create mode 100644 go/internal/embeddedui/static/provider-icons/digitalocean.svg create mode 100644 go/internal/embeddedui/static/provider-icons/discord.svg create mode 100644 go/internal/embeddedui/static/provider-icons/featherless.svg create mode 100644 go/internal/embeddedui/static/provider-icons/firepass-color.svg create mode 100644 go/internal/embeddedui/static/provider-icons/fireworks-color.svg create mode 100644 go/internal/embeddedui/static/provider-icons/gajae-code.svg create mode 100644 go/internal/embeddedui/static/provider-icons/gemini-color.svg create mode 100644 go/internal/embeddedui/static/provider-icons/github-copilot-color.svg create mode 100644 go/internal/embeddedui/static/provider-icons/gitlab-duo-color.svg create mode 100644 go/internal/embeddedui/static/provider-icons/grok.svg create mode 100644 go/internal/embeddedui/static/provider-icons/groq-color.svg create mode 100644 go/internal/embeddedui/static/provider-icons/hermes-agent.svg create mode 100644 go/internal/embeddedui/static/provider-icons/huggingface-color.svg create mode 100644 go/internal/embeddedui/static/provider-icons/hyperbolic.svg create mode 100644 go/internal/embeddedui/static/provider-icons/kilo.svg create mode 100644 go/internal/embeddedui/static/provider-icons/kimi-color.svg create mode 100644 go/internal/embeddedui/static/provider-icons/kiro-color.svg create mode 100644 go/internal/embeddedui/static/provider-icons/litellm.svg create mode 100644 go/internal/embeddedui/static/provider-icons/lm-studio-color.svg create mode 100644 go/internal/embeddedui/static/provider-icons/meta.svg create mode 100644 go/internal/embeddedui/static/provider-icons/minimax.svg create mode 100644 go/internal/embeddedui/static/provider-icons/mistral-color.svg create mode 100644 go/internal/embeddedui/static/provider-icons/moonshot-color.svg create mode 100644 go/internal/embeddedui/static/provider-icons/nanogpt.svg create mode 100644 go/internal/embeddedui/static/provider-icons/nebius.svg create mode 100644 go/internal/embeddedui/static/provider-icons/neuralwatt.svg create mode 100644 go/internal/embeddedui/static/provider-icons/nous.svg create mode 100644 go/internal/embeddedui/static/provider-icons/novita.svg create mode 100644 go/internal/embeddedui/static/provider-icons/nvidia-color.svg create mode 100644 go/internal/embeddedui/static/provider-icons/oh-my-pi.svg create mode 100644 go/internal/embeddedui/static/provider-icons/ollama-color.svg create mode 100644 go/internal/embeddedui/static/provider-icons/openai.svg create mode 100644 go/internal/embeddedui/static/provider-icons/openclaw.svg create mode 100644 go/internal/embeddedui/static/provider-icons/opencode.svg create mode 100644 go/internal/embeddedui/static/provider-icons/openrouter-color.svg create mode 100644 go/internal/embeddedui/static/provider-icons/orcarouter.svg create mode 100644 go/internal/embeddedui/static/provider-icons/parallel.svg create mode 100644 go/internal/embeddedui/static/provider-icons/pi.svg create mode 100644 go/internal/embeddedui/static/provider-icons/prime-agent.svg create mode 100644 go/internal/embeddedui/static/provider-icons/qianfan-color.svg create mode 100644 go/internal/embeddedui/static/provider-icons/qwen-portal-color.svg create mode 100644 go/internal/embeddedui/static/provider-icons/sambanova.svg create mode 100644 go/internal/embeddedui/static/provider-icons/scaleway.svg create mode 100644 go/internal/embeddedui/static/provider-icons/siliconflow.svg create mode 100644 go/internal/embeddedui/static/provider-icons/synthetic.svg create mode 100644 go/internal/embeddedui/static/provider-icons/telegram.svg create mode 100644 go/internal/embeddedui/static/provider-icons/together.svg create mode 100644 go/internal/embeddedui/static/provider-icons/umans.svg create mode 100644 go/internal/embeddedui/static/provider-icons/venice.svg create mode 100644 go/internal/embeddedui/static/provider-icons/vercel-ai-gateway-color.svg create mode 100644 go/internal/embeddedui/static/provider-icons/vllm-color.svg create mode 100644 go/internal/embeddedui/static/provider-icons/vultr.svg create mode 100644 go/internal/embeddedui/static/provider-icons/xiaomi-color.svg create mode 100644 go/internal/embeddedui/static/provider-icons/zai.svg create mode 100644 go/internal/embeddedui/static/provider-icons/zcode.svg create mode 100644 go/internal/embeddedui/static/provider-icons/zenmux.svg create mode 100644 go/internal/ocxcli/delegate_test.go create mode 100644 go/internal/ocxcli/embedded_dashboard.go create mode 100755 scripts/sync-go-embedded-dashboard.sh diff --git a/.github/workflows/go-release-artifacts.yml b/.github/workflows/go-release-artifacts.yml index 0a20ae2e79..3638f9f073 100644 --- a/.github/workflows/go-release-artifacts.yml +++ b/.github/workflows/go-release-artifacts.yml @@ -29,6 +29,9 @@ jobs: go-version-file: go/go.mod cache: false + - name: Setup project Bun for embedded dashboard + uses: ./.github/actions/setup-project-bun + - name: Build, vet, and test Go runtime run: | set -euo pipefail @@ -74,6 +77,9 @@ jobs: go-version-file: go/go.mod cache: false + - name: Setup project Bun for embedded dashboard + uses: ./.github/actions/setup-project-bun + - name: Cross-compile static ocx candidate run: scripts/build-go-release-artifact.sh '${{ matrix.target }}' dist diff --git a/go/cmd/ocx/standalone_smoke_test.go b/go/cmd/ocx/standalone_smoke_test.go new file mode 100644 index 0000000000..db2b07a124 --- /dev/null +++ b/go/cmd/ocx/standalone_smoke_test.go @@ -0,0 +1,81 @@ +package main + +import ( + "bytes" + "context" + "io" + "net/http" + "os" + "os/exec" + "path/filepath" + "strings" + "testing" + "time" +) + +func TestStandaloneBinaryServesEmbeddedDashboardWithoutCheckout(t *testing.T) { + root := filepath.Clean(filepath.Join("..", "..", "..")) + binary := filepath.Join(t.TempDir(), "ocx") + build := exec.Command("go", "build", "-buildvcs=false", "-o", binary, ".") + build.Dir = "." + if output, err := build.CombinedOutput(); err != nil { + t.Fatalf("build: %v\n%s", err, output) + } + clean := t.TempDir() + env := append(os.Environ(), "HOME="+clean, "USERPROFILE="+clean, "OPENCODEX_HOME="+filepath.Join(clean, ".opencodex"), "PATH="+t.TempDir()) + for _, args := range [][]string{{"--version"}, {"--help"}, {"codex-shim", "status"}} { + command := exec.Command(binary, args...) + command.Dir, command.Env = clean, env + if output, err := command.CombinedOutput(); err != nil || len(bytes.TrimSpace(output)) == 0 { + t.Fatalf("%s: %v %s", args, err, output) + } + } + // The lifecycle owner has intentionally not moved in #40. A standalone binary + // must fail with a repairable instruction instead of assuming a checkout/Bun. + service := exec.Command(binary, "service", "status") + service.Dir, service.Env = clean, env + output, err := service.CombinedOutput() + if err == nil || !strings.Contains(string(output), "OCX_TYPESCRIPT_CLI") { + t.Fatalf("service status error = %v output=%q", err, output) + } + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + command := exec.CommandContext(ctx, binary, "serve-dashboard", "--listen", "127.0.0.1:0") + command.Dir, command.Env = clean, env + var stdout bytes.Buffer + command.Stdout, command.Stderr = &stdout, &stdout + if err := command.Start(); err != nil { + t.Fatal(err) + } + t.Cleanup(func() { cancel(); _ = command.Wait() }) + deadline := time.Now().Add(5 * time.Second) + var base string + for time.Now().Before(deadline) { + for _, line := range strings.Split(stdout.String(), "\n") { + if strings.HasPrefix(line, "OpenCodex embedded dashboard listening on http://") { + base = strings.TrimPrefix(line, "OpenCodex embedded dashboard listening on ") + break + } + } + if base != "" { + break + } + time.Sleep(20 * time.Millisecond) + } + if base == "" { + t.Fatalf("server did not announce listener: %q", stdout.String()) + } + for _, path := range []string{"/healthz", "/"} { + response, err := http.Get(base + path) + if err != nil { + t.Fatalf("GET %s: %v", path, err) + } + body, _ := io.ReadAll(response.Body) + response.Body.Close() + if response.StatusCode != http.StatusOK || len(body) == 0 { + t.Fatalf("GET %s = %d %q", path, response.StatusCode, body) + } + } + _ = root +} diff --git a/go/internal/embeddedui/embeddedui.go b/go/internal/embeddedui/embeddedui.go new file mode 100644 index 0000000000..f255470c69 --- /dev/null +++ b/go/internal/embeddedui/embeddedui.go @@ -0,0 +1,90 @@ +// Package embeddedui serves the dashboard baked into a release ocx binary. +// +// static is checked in as the release snapshot. scripts/build-go-release-artifact.sh +// refreshes it from gui/dist before a release build. Keeping a snapshot in-tree +// is intentional: go build must remain deterministic and usable by contributors +// and CI that do not have Bun or the GUI dependency tree installed. +package embeddedui + +import ( + "bytes" + "embed" + "encoding/json" + "io/fs" + "mime" + "net/http" + "path" + "strings" + "time" +) + +//go:embed static +var files embed.FS + +// NewHandler returns the complete self-contained dashboard HTTP surface. The +// caller supplies its version because release builds stamp it with ldflags. +func NewHandler(version string) http.Handler { + root, err := fs.Sub(files, "static") + if err != nil { + panic(err) + } + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet && r.Method != http.MethodHead { + w.Header().Set("Allow", "GET, HEAD") + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + if r.URL.Path == "/healthz" { + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(map[string]any{ + "status": "ok", "service": "opencodex", "version": version, + "uptime": 0, "pid": 0, "port": 0, + }) + return + } + name, spa := embeddedName(r.URL.Path) + if name == "" { + http.NotFound(w, r) + return + } + body, err := fs.ReadFile(root, name) + if err != nil && spa { + name, body, err = "index.html", nil, nil + body, err = fs.ReadFile(root, name) + } + if err != nil { + http.NotFound(w, r) + return + } + contentType := mime.TypeByExtension(path.Ext(name)) + if contentType == "" { + contentType = "application/octet-stream" + } + if strings.HasSuffix(name, ".html") { + contentType = "text/html; charset=utf-8" + } + w.Header().Set("Content-Type", contentType) + if strings.HasSuffix(name, ".html") { + w.Header().Set("Cache-Control", "no-store") + } else { + w.Header().Set("Cache-Control", "public, max-age=31536000, immutable") + } + w.Header().Set("X-Content-Type-Options", "nosniff") + if r.Method == http.MethodHead { + return + } + http.ServeContent(w, r, name, time.Time{}, bytes.NewReader(body)) + }) +} + +func embeddedName(requestPath string) (name string, spa bool) { + cleaned := path.Clean("/" + requestPath) + if strings.Contains(requestPath, "\\") || strings.Contains(cleaned, "..") { + return "", false + } + name = strings.TrimPrefix(cleaned, "/") + if name == "" { + return "index.html", false + } + return name, path.Ext(name) == "" +} diff --git a/go/internal/embeddedui/embeddedui_test.go b/go/internal/embeddedui/embeddedui_test.go new file mode 100644 index 0000000000..3944cc5bce --- /dev/null +++ b/go/internal/embeddedui/embeddedui_test.go @@ -0,0 +1,41 @@ +package embeddedui + +import ( + "net/http" + "net/http/httptest" + "strings" + "testing" +) + +func TestHandlerServesEmbeddedDashboardAndHealth(t *testing.T) { + handler := NewHandler("9.9.9") + for _, test := range []struct{ path, wantType, wantBody string }{ + {"/", "text/html", "opencodex"}, + {"/dashboard/providers", "text/html", "opencodex"}, + {"/healthz", "application/json", "\"service\":\"opencodex\""}, + } { + request := httptest.NewRequest(http.MethodGet, test.path, nil) + response := httptest.NewRecorder() + handler.ServeHTTP(response, request) + if response.Code != http.StatusOK { + t.Fatalf("%s status = %d", test.path, response.Code) + } + if !strings.Contains(response.Header().Get("Content-Type"), test.wantType) { + t.Fatalf("%s content type = %q", test.path, response.Header().Get("Content-Type")) + } + if !strings.Contains(response.Body.String(), test.wantBody) { + t.Fatalf("%s body = %q", test.path, response.Body.String()) + } + } +} + +func TestHandlerRejectsEscapingPathsAndUnknownAsset(t *testing.T) { + handler := NewHandler("9.9.9") + for _, path := range []string{"/../go.mod", "/assets/missing.js"} { + response := httptest.NewRecorder() + handler.ServeHTTP(response, httptest.NewRequest(http.MethodGet, path, nil)) + if response.Code != http.StatusNotFound { + t.Fatalf("%s status = %d, want 404", path, response.Code) + } + } +} diff --git a/go/internal/embeddedui/static/assets/index-BU1tE0sr.js b/go/internal/embeddedui/static/assets/index-BU1tE0sr.js new file mode 100644 index 0000000000..fadc9538c2 --- /dev/null +++ b/go/internal/embeddedui/static/assets/index-BU1tE0sr.js @@ -0,0 +1,112 @@ +var e=Object.create,t=Object.defineProperty,n=Object.getOwnPropertyDescriptor,r=Object.getOwnPropertyNames,i=Object.getPrototypeOf,a=Object.prototype.hasOwnProperty,o=(e,t)=>()=>(t||(e((t={exports:{}}).exports,t),e=null),t.exports),s=(e,i,o,s)=>{if(i&&typeof i==`object`||typeof i==`function`)for(var c=r(i),l=0,u=c.length,d;li[e]).bind(null,d),enumerable:!(s=n(i,d))||s.enumerable});return e},c=(n,r,o)=>(o=n==null?{}:e(i(n)),s(r||!n||!n.__esModule||!a.call(n,`default`)?t(o,`default`,{value:n,enumerable:!0}):o,n));(function(){let e=document.createElement(`link`).relList;if(e&&e.supports&&e.supports(`modulepreload`))return;for(let e of document.querySelectorAll(`link[rel="modulepreload"]`))n(e);new MutationObserver(e=>{for(let t of e)if(t.type===`childList`)for(let e of t.addedNodes)e.tagName===`LINK`&&e.rel===`modulepreload`&&n(e)}).observe(document,{childList:!0,subtree:!0});function t(e){let t={};return e.integrity&&(t.integrity=e.integrity),e.referrerPolicy&&(t.referrerPolicy=e.referrerPolicy),t.credentials=e.crossOrigin===`use-credentials`?`include`:e.crossOrigin===`anonymous`?`omit`:`same-origin`,t}function n(e){if(e.ep)return;e.ep=!0;let n=t(e);fetch(e.href,n)}})();var l=o((e=>{var t=Symbol.for(`react.transitional.element`),n=Symbol.for(`react.portal`),r=Symbol.for(`react.fragment`),i=Symbol.for(`react.strict_mode`),a=Symbol.for(`react.profiler`),o=Symbol.for(`react.consumer`),s=Symbol.for(`react.context`),c=Symbol.for(`react.forward_ref`),l=Symbol.for(`react.suspense`),u=Symbol.for(`react.memo`),d=Symbol.for(`react.lazy`),f=Symbol.for(`react.activity`),p=Symbol.iterator;function m(e){return typeof e!=`object`||!e?null:(e=p&&e[p]||e[`@@iterator`],typeof e==`function`?e:null)}var h={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},g=Object.assign,_={};function v(e,t,n){this.props=e,this.context=t,this.refs=_,this.updater=n||h}v.prototype.isReactComponent={},v.prototype.setState=function(e,t){if(typeof e!=`object`&&typeof e!=`function`&&e!=null)throw Error(`takes an object of state variables to update or a function which returns an object of state variables.`);this.updater.enqueueSetState(this,e,t,`setState`)},v.prototype.forceUpdate=function(e){this.updater.enqueueForceUpdate(this,e,`forceUpdate`)};function y(){}y.prototype=v.prototype;function b(e,t,n){this.props=e,this.context=t,this.refs=_,this.updater=n||h}var x=b.prototype=new y;x.constructor=b,g(x,v.prototype),x.isPureReactComponent=!0;var S=Array.isArray;function C(){}var w={H:null,A:null,T:null,S:null},T=Object.prototype.hasOwnProperty;function E(e,n,r){var i=r.ref;return{$$typeof:t,type:e,key:n,ref:i===void 0?null:i,props:r}}function D(e,t){return E(e.type,t,e.props)}function O(e){return typeof e==`object`&&!!e&&e.$$typeof===t}function k(e){var t={"=":`=0`,":":`=2`};return`$`+e.replace(/[=:]/g,function(e){return t[e]})}var A=/\/+/g;function j(e,t){return typeof e==`object`&&e&&e.key!=null?k(``+e.key):t.toString(36)}function M(e){switch(e.status){case`fulfilled`:return e.value;case`rejected`:throw e.reason;default:switch(typeof e.status==`string`?e.then(C,C):(e.status=`pending`,e.then(function(t){e.status===`pending`&&(e.status=`fulfilled`,e.value=t)},function(t){e.status===`pending`&&(e.status=`rejected`,e.reason=t)})),e.status){case`fulfilled`:return e.value;case`rejected`:throw e.reason}}throw e}function N(e,r,i,a,o){var s=typeof e;(s===`undefined`||s===`boolean`)&&(e=null);var c=!1;if(e===null)c=!0;else switch(s){case`bigint`:case`string`:case`number`:c=!0;break;case`object`:switch(e.$$typeof){case t:case n:c=!0;break;case d:return c=e._init,N(c(e._payload),r,i,a,o)}}if(c)return o=o(e),c=a===``?`.`+j(e,0):a,S(o)?(i=``,c!=null&&(i=c.replace(A,`$&/`)+`/`),N(o,r,i,``,function(e){return e})):o!=null&&(O(o)&&(o=D(o,i+(o.key==null||e&&e.key===o.key?``:(``+o.key).replace(A,`$&/`)+`/`)+c)),r.push(o)),1;c=0;var l=a===``?`.`:a+`:`;if(S(e))for(var u=0;u{t.exports=l()})),d=o((e=>{function t(e,t){var n=e.length;e.push(t);a:for(;0>>1,a=e[r];if(0>>1;ri(c,n))li(u,c)?(e[r]=u,e[l]=n,r=l):(e[r]=c,e[s]=n,r=s);else if(li(u,n))e[r]=u,e[l]=n,r=l;else break a}}return t}function i(e,t){var n=e.sortIndex-t.sortIndex;return n===0?e.id-t.id:n}if(e.unstable_now=void 0,typeof performance==`object`&&typeof performance.now==`function`){var a=performance;e.unstable_now=function(){return a.now()}}else{var o=Date,s=o.now();e.unstable_now=function(){return o.now()-s}}var c=[],l=[],u=1,d=null,f=3,p=!1,m=!1,h=!1,g=!1,_=typeof setTimeout==`function`?setTimeout:null,v=typeof clearTimeout==`function`?clearTimeout:null,y=typeof setImmediate<`u`?setImmediate:null;function b(e){for(var i=n(l);i!==null;){if(i.callback===null)r(l);else if(i.startTime<=e)r(l),i.sortIndex=i.expirationTime,t(c,i);else break;i=n(l)}}function x(e){if(h=!1,b(e),!m){if(n(c)!==null)m=!0,S||(S=!0,O());else{var t=n(l);t!==null&&j(x,t.startTime-e)}}}var S=!1,C=-1,w=5,T=-1;function E(){return g?!0:!(e.unstable_now()-Tt&&E());){var o=d.callback;if(typeof o==`function`){d.callback=null,f=d.priorityLevel;var s=o(d.expirationTime<=t);if(t=e.unstable_now(),typeof s==`function`){d.callback=s,b(t),i=!0;break b}d===n(c)&&r(c),b(t)}else r(c);d=n(c)}if(d!==null)i=!0;else{var u=n(l);u!==null&&j(x,u.startTime-t),i=!1}}break a}finally{d=null,f=a,p=!1}}}finally{i?O():S=!1}}}var O;if(typeof y==`function`)O=function(){y(D)};else if(typeof MessageChannel<`u`){var k=new MessageChannel,A=k.port2;k.port1.onmessage=D,O=function(){A.postMessage(null)}}else O=function(){_(D,0)};function j(t,n){C=_(function(){t(e.unstable_now())},n)}e.unstable_IdlePriority=5,e.unstable_ImmediatePriority=1,e.unstable_LowPriority=4,e.unstable_NormalPriority=3,e.unstable_Profiling=null,e.unstable_UserBlockingPriority=2,e.unstable_cancelCallback=function(e){e.callback=null},e.unstable_forceFrameRate=function(e){0>e||125o?(r.sortIndex=a,t(l,r),n(c)===null&&r===n(l)&&(h?(v(C),C=-1):h=!0,j(x,a-o))):(r.sortIndex=s,t(c,r),m||p||(m=!0,S||(S=!0,O()))),r},e.unstable_shouldYield=E,e.unstable_wrapCallback=function(e){var t=f;return function(){var n=f;f=t;try{return e.apply(this,arguments)}finally{f=n}}}})),f=o(((e,t)=>{t.exports=d()})),p=o((e=>{var t=u();function n(e){var t=`https://react.dev/errors/`+e;if(1{function n(){if(!(typeof __REACT_DEVTOOLS_GLOBAL_HOOK__>`u`||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!=`function`))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(n)}catch(e){console.error(e)}}n(),t.exports=p()})),h=o((e=>{var t=f(),n=u(),r=m();function i(e){var t=`https://react.dev/errors/`+e;if(1B||(e.current=z[B],z[B]=null,B--)}function U(e,t){B++,z[B]=e.current,e.current=t}var W=V(null),ee=V(null),G=V(null),K=V(null);function q(e,t){switch(U(G,t),U(ee,e),U(W,null),t.nodeType){case 9:case 11:e=(e=t.documentElement)&&(e=e.namespaceURI)?Vd(e):0;break;default:if(e=t.tagName,t=t.namespaceURI)t=Vd(t),e=Hd(t,e);else switch(e){case`svg`:e=1;break;case`math`:e=2;break;default:e=0}}H(W),U(W,e)}function J(){H(W),H(ee),H(G)}function Y(e){e.memoizedState!==null&&U(K,e);var t=W.current,n=Hd(t,e.type);t!==n&&(U(ee,e),U(W,n))}function te(e){ee.current===e&&(H(W),H(ee)),K.current===e&&(H(K),Qf._currentValue=R)}var ne,re;function ie(e){if(ne===void 0)try{throw Error()}catch(e){var t=e.stack.trim().match(/\n( *(at )?)/);ne=t&&t[1]||``,re=-1)`:-1i||c[r]!==l[i]){var u=` +`+c[r].replace(` at new `,` at `);return e.displayName&&u.includes(``)&&(u=u.replace(``,e.displayName)),u}while(1<=r&&0<=i);break}}}finally{ae=!1,Error.prepareStackTrace=n}return(n=e?e.displayName||e.name:``)?ie(n):``}function se(e,t){switch(e.tag){case 26:case 27:case 5:return ie(e.type);case 16:return ie(`Lazy`);case 13:return e.child!==t&&t!==null?ie(`Suspense Fallback`):ie(`Suspense`);case 19:return ie(`SuspenseList`);case 0:case 15:return oe(e.type,!1);case 11:return oe(e.type.render,!1);case 1:return oe(e.type,!0);case 31:return ie(`Activity`);default:return``}}function ce(e){try{var t=``,n=null;do t+=se(e,n),n=e,e=e.return;while(e);return t}catch(e){return` +Error generating stack: `+e.message+` +`+e.stack}}var le=Object.prototype.hasOwnProperty,ue=t.unstable_scheduleCallback,de=t.unstable_cancelCallback,fe=t.unstable_shouldYield,pe=t.unstable_requestPaint,X=t.unstable_now,me=t.unstable_getCurrentPriorityLevel,he=t.unstable_ImmediatePriority,ge=t.unstable_UserBlockingPriority,_e=t.unstable_NormalPriority,Z=t.unstable_LowPriority,ve=t.unstable_IdlePriority,ye=t.log,be=t.unstable_setDisableYieldValue,xe=null,Se=null;function Ce(e){if(typeof ye==`function`&&be(e),Se&&typeof Se.setStrictMode==`function`)try{Se.setStrictMode(xe,e)}catch{}}var we=Math.clz32?Math.clz32:De,Te=Math.log,Ee=Math.LN2;function De(e){return e>>>=0,e===0?32:31-(Te(e)/Ee|0)|0}var Oe=256,ke=262144,Ae=4194304;function je(e){var t=e&42;if(t!==0)return t;switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:return 64;case 128:return 128;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:return e&261888;case 262144:case 524288:case 1048576:case 2097152:return e&3932160;case 4194304:case 8388608:case 16777216:case 33554432:return e&62914560;case 67108864:return 67108864;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 0;default:return e}}function Me(e,t,n){var r=e.pendingLanes;if(r===0)return 0;var i=0,a=e.suspendedLanes,o=e.pingedLanes;e=e.warmLanes;var s=r&134217727;return s===0?(s=r&~a,s===0?o===0?n||(n=r&~e,n!==0&&(i=je(n))):i=je(o):i=je(s)):(r=s&~a,r===0?(o&=s,o===0?n||(n=s&~e,n!==0&&(i=je(n))):i=je(o)):i=je(r)),i===0?0:t!==0&&t!==i&&(t&a)===0&&(a=i&-i,n=t&-t,a>=n||a===32&&n&4194048)?t:i}function Ne(e,t){return(e.pendingLanes&~(e.suspendedLanes&~e.pingedLanes)&t)===0}function Pe(e,t){switch(e){case 1:case 2:case 4:case 8:case 64:return t+250;case 16:case 32:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return t+5e3;case 4194304:case 8388608:case 16777216:case 33554432:return-1;case 67108864:case 134217728:case 268435456:case 536870912:case 1073741824:return-1;default:return-1}}function Fe(){var e=Ae;return Ae<<=1,!(Ae&62914560)&&(Ae=4194304),e}function Ie(e){for(var t=[],n=0;31>n;n++)t.push(e);return t}function Le(e,t){e.pendingLanes|=t,t!==268435456&&(e.suspendedLanes=0,e.pingedLanes=0,e.warmLanes=0)}function Re(e,t,n,r,i,a){var o=e.pendingLanes;e.pendingLanes=n,e.suspendedLanes=0,e.pingedLanes=0,e.warmLanes=0,e.expiredLanes&=n,e.entangledLanes&=n,e.errorRecoveryDisabledLanes&=n,e.shellSuspendCounter=0;var s=e.entanglements,c=e.expirationTimes,l=e.hiddenUpdates;for(n=o&~n;0`u`||window.document===void 0||window.document.createElement===void 0),Jt=!1;if(qt)try{var Yt={};Object.defineProperty(Yt,"passive",{get:function(){Jt=!0}}),window.addEventListener(`test`,Yt,Yt),window.removeEventListener(`test`,Yt,Yt)}catch{Jt=!1}var Xt=null,Zt=null,Qt=null;function $t(){if(Qt)return Qt;var e,t=Zt,n=t.length,r,i=`value`in Xt?Xt.value:Xt.textContent,a=i.length;for(e=0;e=Mn),Fn=` `,In=!1;function Ln(e,t){switch(e){case`keyup`:return An.indexOf(t.keyCode)!==-1;case`keydown`:return t.keyCode!==229;case`keypress`:case`mousedown`:case`focusout`:return!0;default:return!1}}function Rn(e){return e=e.detail,typeof e==`object`&&`data`in e?e.data:null}var zn=!1;function Bn(e,t){switch(e){case`compositionend`:return Rn(t);case`keypress`:return t.which===32?(In=!0,Fn):null;case`textInput`:return e=t.data,e===Fn&&In?null:e;default:return null}}function Vn(e,t){if(zn)return e===`compositionend`||!jn&&Ln(e,t)?(e=$t(),Qt=Zt=Xt=null,zn=!1,e):null;switch(e){case`paste`:return null;case`keypress`:if(!(t.ctrlKey||t.altKey||t.metaKey)||t.ctrlKey&&t.altKey){if(t.char&&1=t)return{node:n,offset:t-e};e=r}a:{for(;n;){if(n.nextSibling){n=n.nextSibling;break a}n=n.parentNode}n=void 0}n=lr(n)}}function dr(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?dr(e,t.parentNode):`contains`in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function fr(e){e=e!=null&&e.ownerDocument!=null&&e.ownerDocument.defaultView!=null?e.ownerDocument.defaultView:window;for(var t=St(e.document);t instanceof e.HTMLIFrameElement;){try{var n=typeof t.contentWindow.location.href==`string`}catch{n=!1}if(n)e=t.contentWindow;else break;t=St(e.document)}return t}function pr(e){var t=e&&e.nodeName&&e.nodeName.toLowerCase();return t&&(t===`input`&&(e.type===`text`||e.type===`search`||e.type===`tel`||e.type===`url`||e.type===`password`)||t===`textarea`||e.contentEditable===`true`)}var mr=qt&&`documentMode`in document&&11>=document.documentMode,hr=null,gr=null,_r=null,vr=!1;function yr(e,t,n){var r=n.window===n?n.document:n.nodeType===9?n:n.ownerDocument;vr||hr==null||hr!==St(r)||(r=hr,`selectionStart`in r&&pr(r)?r={start:r.selectionStart,end:r.selectionEnd}:(r=(r.ownerDocument&&r.ownerDocument.defaultView||window).getSelection(),r={anchorNode:r.anchorNode,anchorOffset:r.anchorOffset,focusNode:r.focusNode,focusOffset:r.focusOffset}),_r&&cr(_r,r)||(_r=r,r=Td(gr,`onSelect`),0>=o,i-=o,di=1<<32-we(t)+i|n<h?(g=d,d=null):g=d.sibling;var _=p(i,d,s[h],c);if(_===null){d===null&&(d=g);break}e&&d&&_.alternate===null&&t(i,d),a=o(_,a,h),u===null?l=_:u.sibling=_,u=_,d=g}if(h===s.length)return n(i,d),bi&&pi(i,h),l;if(d===null){for(;hg?(_=h,h=null):_=h.sibling;var y=p(a,h,v.value,l);if(y===null){h===null&&(h=_);break}e&&h&&y.alternate===null&&t(a,h),s=o(y,s,g),d===null?u=y:d.sibling=y,d=y,h=_}if(v.done)return n(a,h),bi&&pi(a,g),u;if(h===null){for(;!v.done;g++,v=c.next())v=f(a,v.value,l),v!==null&&(s=o(v,s,g),d===null?u=v:d.sibling=v,d=v);return bi&&pi(a,g),u}for(h=r(h);!v.done;g++,v=c.next())v=m(h,a,g,v.value,l),v!==null&&(e&&v.alternate!==null&&h.delete(v.key===null?g:v.key),s=o(v,s,g),d===null?u=v:d.sibling=v,d=v);return e&&h.forEach(function(e){return t(a,e)}),bi&&pi(a,g),u}function b(e,r,o,c){if(typeof o==`object`&&o&&o.type===y&&o.key===null&&(o=o.props.children),typeof o==`object`&&o){switch(o.$$typeof){case _:a:{for(var l=o.key;r!==null;){if(r.key===l){if(l=o.type,l===y){if(r.tag===7){n(e,r.sibling),c=a(r,o.props.children),c.return=e,e=c;break a}}else if(r.elementType===l||typeof l==`object`&&l&&l.$$typeof===O&&ma(l)===r.type){n(e,r.sibling),c=a(r,o.props),xa(c,o),c.return=e,e=c;break a}n(e,r);break}t(e,r),r=r.sibling}o.type===y?(c=Qr(o.props.children,e.mode,c,o.key),c.return=e,e=c):(c=Zr(o.type,o.key,o.props,null,e.mode,c),xa(c,o),c.return=e,e=c)}return s(e);case v:a:{for(l=o.key;r!==null;){if(r.key===l){if(r.tag===4&&r.stateNode.containerInfo===o.containerInfo&&r.stateNode.implementation===o.implementation){n(e,r.sibling),c=a(r,o.children||[]),c.return=e,e=c;break a}n(e,r);break}t(e,r),r=r.sibling}c=ti(o,e.mode,c),c.return=e,e=c}return s(e);case O:return o=ma(o),b(e,r,o,c)}if(F(o))return h(e,r,o,c);if(M(o)){if(l=M(o),typeof l!=`function`)throw Error(i(150));return o=l.call(o),g(e,r,o,c)}if(typeof o.then==`function`)return b(e,r,ba(o),c);if(o.$$typeof===C)return b(e,r,Hi(e,o),c);Sa(e,o)}return typeof o==`string`&&o!==``||typeof o==`number`||typeof o==`bigint`?(o=``+o,r!==null&&r.tag===6?(n(e,r.sibling),c=a(r,o),c.return=e,e=c):(n(e,r),c=$r(o,e.mode,c),c.return=e,e=c),s(e)):n(e,r)}return function(e,t,n,r){try{ya=0;var i=b(e,t,n,r);return va=null,i}catch(t){if(t===ca||t===ua)throw t;var a=qr(29,t,null,e.mode);return a.lanes=r,a.return=e,a}}}var wa=Ca(!0),Ta=Ca(!1),Ea=!1;function Da(e){e.updateQueue={baseState:e.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,lanes:0,hiddenCallbacks:null},callbacks:null}}function Oa(e,t){e=e.updateQueue,t.updateQueue===e&&(t.updateQueue={baseState:e.baseState,firstBaseUpdate:e.firstBaseUpdate,lastBaseUpdate:e.lastBaseUpdate,shared:e.shared,callbacks:null})}function ka(e){return{lane:e,tag:0,payload:null,callback:null,next:null}}function Aa(e,t,n){var r=e.updateQueue;if(r===null)return null;if(r=r.shared,jl&2){var i=r.pending;return i===null?t.next=t:(t.next=i.next,i.next=t),r.pending=t,t=Wr(e),Ur(e,null,n),t}return Br(e,r,t,n),Wr(e)}function ja(e,t,n){if(t=t.updateQueue,t!==null&&(t=t.shared,n&4194048)){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,Be(e,n)}}function Ma(e,t){var n=e.updateQueue,r=e.alternate;if(r!==null&&(r=r.updateQueue,n===r)){var i=null,a=null;if(n=n.firstBaseUpdate,n!==null){do{var o={lane:n.lane,tag:n.tag,payload:n.payload,callback:null,next:null};a===null?i=a=o:a=a.next=o,n=n.next}while(n!==null);a===null?i=a=t:a=a.next=t}else i=a=t;n={baseState:r.baseState,firstBaseUpdate:i,lastBaseUpdate:a,shared:r.shared,callbacks:r.callbacks},e.updateQueue=n;return}e=n.lastBaseUpdate,e===null?n.firstBaseUpdate=t:e.next=t,n.lastBaseUpdate=t}var Na=!1;function Pa(){if(Na){var e=$i;if(e!==null)throw e}}function Fa(e,t,n,r){Na=!1;var i=e.updateQueue;Ea=!1;var a=i.firstBaseUpdate,o=i.lastBaseUpdate,s=i.shared.pending;if(s!==null){i.shared.pending=null;var c=s,l=c.next;c.next=null,o===null?a=l:o.next=l,o=c;var u=e.alternate;u!==null&&(u=u.updateQueue,s=u.lastBaseUpdate,s!==o&&(s===null?u.firstBaseUpdate=l:s.next=l,u.lastBaseUpdate=c))}if(a!==null){var d=i.baseState;o=0,u=l=c=null,s=a;do{var f=s.lane&-536870913,p=f!==s.lane;if(p?(Pl&f)===f:(r&f)===f){f!==0&&f===Qi&&(Na=!0),u!==null&&(u=u.next={lane:0,tag:s.tag,payload:s.payload,callback:null,next:null});a:{var m=e,g=s;f=t;var _=n;switch(g.tag){case 1:if(m=g.payload,typeof m==`function`){d=m.call(_,d,f);break a}d=m;break a;case 3:m.flags=m.flags&-65537|128;case 0:if(m=g.payload,f=typeof m==`function`?m.call(_,d,f):m,f==null)break a;d=h({},d,f);break a;case 2:Ea=!0}}f=s.callback,f!==null&&(e.flags|=64,p&&(e.flags|=8192),p=i.callbacks,p===null?i.callbacks=[f]:p.push(f))}else p={lane:f,tag:s.tag,payload:s.payload,callback:s.callback,next:null},u===null?(l=u=p,c=d):u=u.next=p,o|=f;if(s=s.next,s===null){if(s=i.shared.pending,s===null)break;p=s,s=p.next,p.next=null,i.lastBaseUpdate=p,i.shared.pending=null}}while(1);u===null&&(c=d),i.baseState=c,i.firstBaseUpdate=l,i.lastBaseUpdate=u,a===null&&(i.shared.lanes=0),Hl|=o,e.lanes=o,e.memoizedState=d}}function Ia(e,t){if(typeof e!=`function`)throw Error(i(191,e));e.call(t)}function La(e,t){var n=e.callbacks;if(n!==null)for(e.callbacks=null,e=0;ea?a:8;var o=I.T,s={};I.T=s,ws(e,!1,t,n);try{var c=i(),l=I.S;l!==null&&l(s,c),typeof c==`object`&&c&&typeof c.then==`function`?Cs(e,t,na(c,r),uu(e)):Cs(e,t,r,uu(e))}catch(n){Cs(e,t,{then:function(){},status:`rejected`,reason:n},uu())}finally{L.p=a,o!==null&&s.types!==null&&(o.types=s.types),I.T=o}}function ps(){}function ms(e,t,n,r){if(e.tag!==5)throw Error(i(476));var a=hs(e).queue;fs(e,a,t,R,n===null?ps:function(){return gs(e),n(r)})}function hs(e){var t=e.memoizedState;if(t!==null)return t;t={memoizedState:R,baseState:R,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:To,lastRenderedState:R},next:null};var n={};return t.next={memoizedState:n,baseState:n,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:To,lastRenderedState:n},next:null},e.memoizedState=t,e=e.alternate,e!==null&&(e.memoizedState=t),t}function gs(e){var t=hs(e);t.next===null&&(t=e.alternate.memoizedState),Cs(e,t.next.queue,{},uu())}function _s(){return Vi(Qf)}function vs(){return bo().memoizedState}function ys(){return bo().memoizedState}function bs(e){for(var t=e.return;t!==null;){switch(t.tag){case 24:case 3:var n=uu();e=ka(n);var r=Aa(t,e,n);r!==null&&(fu(r,t,n),ja(r,t,n)),t={cache:Ji()},e.payload=t;return}t=t.return}}function xs(e,t,n){var r=uu();n={lane:r,revertLane:0,gesture:null,action:n,hasEagerState:!1,eagerState:null,next:null},Ts(e)?Es(t,n):(n=Vr(e,t,n,r),n!==null&&(fu(n,e,r),Ds(n,t,r)))}function Ss(e,t,n){Cs(e,t,n,uu())}function Cs(e,t,n,r){var i={lane:r,revertLane:0,gesture:null,action:n,hasEagerState:!1,eagerState:null,next:null};if(Ts(e))Es(t,i);else{var a=e.alternate;if(e.lanes===0&&(a===null||a.lanes===0)&&(a=t.lastRenderedReducer,a!==null))try{var o=t.lastRenderedState,s=a(o,n);if(i.hasEagerState=!0,i.eagerState=s,sr(s,o))return Br(e,t,i,0),Ml===null&&zr(),!1}catch{}if(n=Vr(e,t,i,r),n!==null)return fu(n,e,r),Ds(n,t,r),!0}return!1}function ws(e,t,n,r){if(r={lane:2,revertLane:ld(),gesture:null,action:r,hasEagerState:!1,eagerState:null,next:null},Ts(e)){if(t)throw Error(i(479))}else t=Vr(e,n,r,2),t!==null&&fu(t,e,2)}function Ts(e){var t=e.alternate;return e===$a||t!==null&&t===$a}function Es(e,t){ro=no=!0;var n=e.pending;n===null?t.next=t:(t.next=n.next,n.next=t),e.pending=t}function Ds(e,t,n){if(n&4194048){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,Be(e,n)}}var Os={readContext:Vi,use:Co,useCallback:lo,useContext:lo,useEffect:lo,useImperativeHandle:lo,useLayoutEffect:lo,useInsertionEffect:lo,useMemo:lo,useReducer:lo,useRef:lo,useState:lo,useDebugValue:lo,useDeferredValue:lo,useTransition:lo,useSyncExternalStore:lo,useId:lo,useHostTransitionStatus:lo,useFormState:lo,useActionState:lo,useOptimistic:lo,useMemoCache:lo,useCacheRefresh:lo};Os.useEffectEvent=lo;var ks={readContext:Vi,use:Co,useCallback:function(e,t){return yo().memoizedState=[e,t===void 0?null:t],e},useContext:Vi,useEffect:$o,useImperativeHandle:function(e,t,n){n=n==null?null:n.concat([e]),Zo(4194308,4,as.bind(null,t,e),n)},useLayoutEffect:function(e,t){return Zo(4194308,4,e,t)},useInsertionEffect:function(e,t){Zo(4,2,e,t)},useMemo:function(e,t){var n=yo();t=t===void 0?null:t;var r=e();if(io){Ce(!0);try{e()}finally{Ce(!1)}}return n.memoizedState=[r,t],r},useReducer:function(e,t,n){var r=yo();if(n!==void 0){var i=n(t);if(io){Ce(!0);try{n(t)}finally{Ce(!1)}}}else i=t;return r.memoizedState=r.baseState=i,e={pending:null,lanes:0,dispatch:null,lastRenderedReducer:e,lastRenderedState:i},r.queue=e,e=e.dispatch=xs.bind(null,$a,e),[r.memoizedState,e]},useRef:function(e){var t=yo();return e={current:e},t.memoizedState=e},useState:function(e){e=Fo(e);var t=e.queue,n=Ss.bind(null,$a,t);return t.dispatch=n,[e.memoizedState,n]},useDebugValue:ss,useDeferredValue:function(e,t){return us(yo(),e,t)},useTransition:function(){var e=Fo(!1);return e=fs.bind(null,$a,e.queue,!0,!1),yo().memoizedState=e,[!1,e]},useSyncExternalStore:function(e,t,n){var r=$a,a=yo();if(bi){if(n===void 0)throw Error(i(407));n=n()}else{if(n=t(),Ml===null)throw Error(i(349));Pl&127||Ao(r,t,n)}a.memoizedState=n;var o={value:n,getSnapshot:t};return a.queue=o,$o(Mo.bind(null,r,o,e),[e]),r.flags|=2048,Yo(9,{destroy:void 0},jo.bind(null,r,o,n,t),null),n},useId:function(){var e=yo(),t=Ml.identifierPrefix;if(bi){var n=fi,r=di;n=(r&~(1<<32-we(r)-1)).toString(32)+n,t=`_`+t+`R_`+n,n=ao++,0<\/script>`,o=o.removeChild(o.firstChild);break;case`select`:o=typeof r.is==`string`?s.createElement(`select`,{is:r.is}):s.createElement(`select`),r.multiple?o.multiple=!0:r.size&&(o.size=r.size);break;default:o=typeof r.is==`string`?s.createElement(a,{is:r.is}):s.createElement(a)}}o[qe]=t,o[Je]=r;a:for(s=t.child;s!==null;){if(s.tag===5||s.tag===6)o.appendChild(s.stateNode);else if(s.tag!==4&&s.tag!==27&&s.child!==null){s.child.return=s,s=s.child;continue}if(s===t)break a;for(;s.sibling===null;){if(s.return===null||s.return===t)break a;s=s.return}s.sibling.return=s.return,s=s.sibling}t.stateNode=o;a:switch(Pd(o,a,r),a){case`button`:case`input`:case`select`:case`textarea`:r=!!r.autoFocus;break a;case`img`:r=!0;break a;default:r=!1}r&&Cc(t)}}return Oc(t),wc(t,t.type,e===null?null:e.memoizedProps,t.pendingProps,n),null;case 6:if(e&&t.stateNode!=null)e.memoizedProps!==r&&Cc(t);else{if(typeof r!=`string`&&t.stateNode===null)throw Error(i(166));if(e=G.current,Di(t)){if(e=t.stateNode,n=t.memoizedProps,r=null,a=vi,a!==null)switch(a.tag){case 27:case 5:r=a.memoizedProps}e[qe]=t,e=!!(e.nodeValue===n||r!==null&&!0===r.suppressHydrationWarning||jd(e.nodeValue,n)),e||wi(t,!0)}else e=Bd(e).createTextNode(r),e[qe]=t,t.stateNode=e}return Oc(t),null;case 31:if(n=t.memoizedState,e===null||e.memoizedState!==null){if(r=Di(t),n!==null){if(e===null){if(!r)throw Error(i(318));if(e=t.memoizedState,e=e===null?null:e.dehydrated,!e)throw Error(i(557));e[qe]=t}else Oi(),!(t.flags&128)&&(t.memoizedState=null),t.flags|=4;Oc(t),e=!1}else n=ki(),e!==null&&e.memoizedState!==null&&(e.memoizedState.hydrationErrors=n),e=!0;if(!e)return t.flags&256?(Ya(t),t):(Ya(t),null);if(t.flags&128)throw Error(i(558))}return Oc(t),null;case 13:if(r=t.memoizedState,e===null||e.memoizedState!==null&&e.memoizedState.dehydrated!==null){if(a=Di(t),r!==null&&r.dehydrated!==null){if(e===null){if(!a)throw Error(i(318));if(a=t.memoizedState,a=a===null?null:a.dehydrated,!a)throw Error(i(317));a[qe]=t}else Oi(),!(t.flags&128)&&(t.memoizedState=null),t.flags|=4;Oc(t),a=!1}else a=ki(),e!==null&&e.memoizedState!==null&&(e.memoizedState.hydrationErrors=a),a=!0;if(!a)return t.flags&256?(Ya(t),t):(Ya(t),null)}return Ya(t),t.flags&128?(t.lanes=n,t):(n=r!==null,e=e!==null&&e.memoizedState!==null,n&&(r=t.child,a=null,r.alternate!==null&&r.alternate.memoizedState!==null&&r.alternate.memoizedState.cachePool!==null&&(a=r.alternate.memoizedState.cachePool.pool),o=null,r.memoizedState!==null&&r.memoizedState.cachePool!==null&&(o=r.memoizedState.cachePool.pool),o!==a&&(r.flags|=2048)),n!==e&&n&&(t.child.flags|=8192),Ec(t,t.updateQueue),Oc(t),null);case 4:return J(),e===null&&xd(t.stateNode.containerInfo),Oc(t),null;case 10:return Fi(t.type),Oc(t),null;case 19:if(H(Xa),r=t.memoizedState,r===null)return Oc(t),null;if(a=!!(t.flags&128),o=r.rendering,o===null){if(a)Dc(r,!1);else{if(Vl!==0||e!==null&&e.flags&128)for(e=t.child;e!==null;){if(o=Za(e),o!==null){for(t.flags|=128,Dc(r,!1),e=o.updateQueue,t.updateQueue=e,Ec(t,e),t.subtreeFlags=0,e=n,n=t.child;n!==null;)Xr(n,e),n=n.sibling;return U(Xa,Xa.current&1|2),bi&&pi(t,r.treeForkCount),t.child}e=e.sibling}r.tail!==null&&X()>Ql&&(t.flags|=128,a=!0,Dc(r,!1),t.lanes=4194304)}}else{if(!a){if(e=Za(o),e!==null){if(t.flags|=128,a=!0,e=e.updateQueue,t.updateQueue=e,Ec(t,e),Dc(r,!0),r.tail===null&&r.tailMode===`hidden`&&!o.alternate&&!bi)return Oc(t),null}else 2*X()-r.renderingStartTime>Ql&&n!==536870912&&(t.flags|=128,a=!0,Dc(r,!1),t.lanes=4194304)}r.isBackwards?(o.sibling=t.child,t.child=o):(e=r.last,e===null?t.child=o:e.sibling=o,r.last=o)}return r.tail===null?(Oc(t),null):(e=r.tail,r.rendering=e,r.tail=e.sibling,r.renderingStartTime=X(),e.sibling=null,n=Xa.current,U(Xa,a?n&1|2:n&1),bi&&pi(t,r.treeForkCount),e);case 22:case 23:return Ya(t),Ha(),r=t.memoizedState!==null,e===null?r&&(t.flags|=8192):e.memoizedState!==null!==r&&(t.flags|=8192),r?n&536870912&&!(t.flags&128)&&(Oc(t),t.subtreeFlags&6&&(t.flags|=8192)):Oc(t),n=t.updateQueue,n!==null&&Ec(t,n.retryQueue),n=null,e!==null&&e.memoizedState!==null&&e.memoizedState.cachePool!==null&&(n=e.memoizedState.cachePool.pool),r=null,t.memoizedState!==null&&t.memoizedState.cachePool!==null&&(r=t.memoizedState.cachePool.pool),r!==n&&(t.flags|=2048),e!==null&&H(ia),null;case 24:return n=null,e!==null&&(n=e.memoizedState.cache),t.memoizedState.cache!==n&&(t.flags|=2048),Fi(qi),Oc(t),null;case 25:return null;case 30:return null}throw Error(i(156,t.tag))}function Ac(e,t){switch(gi(t),t.tag){case 1:return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 3:return Fi(qi),J(),e=t.flags,e&65536&&!(e&128)?(t.flags=e&-65537|128,t):null;case 26:case 27:case 5:return te(t),null;case 31:if(t.memoizedState!==null){if(Ya(t),t.alternate===null)throw Error(i(340));Oi()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 13:if(Ya(t),e=t.memoizedState,e!==null&&e.dehydrated!==null){if(t.alternate===null)throw Error(i(340));Oi()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 19:return H(Xa),null;case 4:return J(),null;case 10:return Fi(t.type),null;case 22:case 23:return Ya(t),Ha(),e!==null&&H(ia),e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 24:return Fi(qi),null;case 25:return null;default:return null}}function jc(e,t){switch(gi(t),t.tag){case 3:Fi(qi),J();break;case 26:case 27:case 5:te(t);break;case 4:J();break;case 31:t.memoizedState!==null&&Ya(t);break;case 13:Ya(t);break;case 19:H(Xa);break;case 10:Fi(t.type);break;case 22:case 23:Ya(t),Ha(),e!==null&&H(ia);break;case 24:Fi(qi)}}function Mc(e,t){try{var n=t.updateQueue,r=n===null?null:n.lastEffect;if(r!==null){var i=r.next;n=i;do{if((n.tag&e)===e){r=void 0;var a=n.create,o=n.inst;r=a(),o.destroy=r}n=n.next}while(n!==i)}}catch(e){Hu(t,t.return,e)}}function Nc(e,t,n){try{var r=t.updateQueue,i=r===null?null:r.lastEffect;if(i!==null){var a=i.next;r=a;do{if((r.tag&e)===e){var o=r.inst,s=o.destroy;if(s!==void 0){o.destroy=void 0,i=t;var c=n,l=s;try{l()}catch(e){Hu(i,c,e)}}}r=r.next}while(r!==a)}}catch(e){Hu(t,t.return,e)}}function Pc(e){var t=e.updateQueue;if(t!==null){var n=e.stateNode;try{La(t,n)}catch(t){Hu(e,e.return,t)}}}function Fc(e,t,n){n.props=Is(e.type,e.memoizedProps),n.state=e.memoizedState;try{n.componentWillUnmount()}catch(n){Hu(e,t,n)}}function Ic(e,t){try{var n=e.ref;if(n!==null){switch(e.tag){case 26:case 27:case 5:var r=e.stateNode;break;case 30:r=e.stateNode;break;default:r=e.stateNode}typeof n==`function`?e.refCleanup=n(r):n.current=r}}catch(n){Hu(e,t,n)}}function Lc(e,t){var n=e.ref,r=e.refCleanup;if(n!==null){if(typeof r==`function`)try{r()}catch(n){Hu(e,t,n)}finally{e.refCleanup=null,e=e.alternate,e!=null&&(e.refCleanup=null)}else if(typeof n==`function`)try{n(null)}catch(n){Hu(e,t,n)}else n.current=null}}function Rc(e){var t=e.type,n=e.memoizedProps,r=e.stateNode;try{a:switch(t){case`button`:case`input`:case`select`:case`textarea`:n.autoFocus&&r.focus();break a;case`img`:n.src?r.src=n.src:n.srcSet&&(r.srcset=n.srcSet)}}catch(t){Hu(e,e.return,t)}}function zc(e,t,n){try{var r=e.stateNode;Fd(r,e.type,n,t),r[Je]=t}catch(t){Hu(e,e.return,t)}}function Bc(e){return e.tag===5||e.tag===3||e.tag===26||e.tag===27&&Zd(e.type)||e.tag===4}function Vc(e){a:for(;;){for(;e.sibling===null;){if(e.return===null||Bc(e.return))return null;e=e.return}for(e.sibling.return=e.return,e=e.sibling;e.tag!==5&&e.tag!==6&&e.tag!==18;){if(e.tag===27&&Zd(e.type)||e.flags&2||e.child===null||e.tag===4)continue a;e.child.return=e,e=e.child}if(!(e.flags&2))return e.stateNode}}function Hc(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?(n.nodeType===9?n.body:n.nodeName===`HTML`?n.ownerDocument.body:n).insertBefore(e,t):(t=n.nodeType===9?n.body:n.nodeName===`HTML`?n.ownerDocument.body:n,t.appendChild(e),n=n._reactRootContainer,n!=null||t.onclick!==null||(t.onclick=Rt));else if(r!==4&&(r===27&&Zd(e.type)&&(n=e.stateNode,t=null),e=e.child,e!==null))for(Hc(e,t,n),e=e.sibling;e!==null;)Hc(e,t,n),e=e.sibling}function Uc(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.insertBefore(e,t):n.appendChild(e);else if(r!==4&&(r===27&&Zd(e.type)&&(n=e.stateNode),e=e.child,e!==null))for(Uc(e,t,n),e=e.sibling;e!==null;)Uc(e,t,n),e=e.sibling}function Wc(e){var t=e.stateNode,n=e.memoizedProps;try{for(var r=e.type,i=t.attributes;i.length;)t.removeAttributeNode(i[0]);Pd(t,r,n),t[qe]=e,t[Je]=n}catch(t){Hu(e,e.return,t)}}var Gc=!1,Kc=!1,qc=!1,Jc=typeof WeakSet==`function`?WeakSet:Set,Yc=null;function Xc(e,t){if(e=e.containerInfo,Rd=sp,e=fr(e),pr(e)){if(`selectionStart`in e)var n={start:e.selectionStart,end:e.selectionEnd};else a:{n=(n=e.ownerDocument)&&n.defaultView||window;var r=n.getSelection&&n.getSelection();if(r&&r.rangeCount!==0){n=r.anchorNode;var a=r.anchorOffset,o=r.focusNode;r=r.focusOffset;try{n.nodeType,o.nodeType}catch{n=null;break a}var s=0,c=-1,l=-1,u=0,d=0,f=e,p=null;b:for(;;){for(var m;f!==n||a!==0&&f.nodeType!==3||(c=s+a),f!==o||r!==0&&f.nodeType!==3||(l=s+r),f.nodeType===3&&(s+=f.nodeValue.length),(m=f.firstChild)!==null;)p=f,f=m;for(;;){if(f===e)break b;if(p===n&&++u===a&&(c=s),p===o&&++d===r&&(l=s),(m=f.nextSibling)!==null)break;f=p,p=f.parentNode}f=m}n=c===-1||l===-1?null:{start:c,end:l}}else n=null}n||={start:0,end:0}}else n=null;for(zd={focusedElem:e,selectionRange:n},sp=!1,Yc=t;Yc!==null;)if(t=Yc,e=t.child,t.subtreeFlags&1028&&e!==null)e.return=t,Yc=e;else for(;Yc!==null;){switch(t=Yc,o=t.alternate,e=t.flags,t.tag){case 0:if(e&4&&(e=t.updateQueue,e=e===null?null:e.events,e!==null))for(n=0;n title`))),Pd(o,r,n),o[qe]=e,ot(o),r=o;break a;case`link`:var s=Vf(`link`,`href`,a).get(r+(n.href||``));if(s){for(var c=0;cg&&(o=g,g=h,h=o);var _=ur(s,h),v=ur(s,g);if(_&&v&&(p.rangeCount!==1||p.anchorNode!==_.node||p.anchorOffset!==_.offset||p.focusNode!==v.node||p.focusOffset!==v.offset)){var y=d.createRange();y.setStart(_.node,_.offset),p.removeAllRanges(),h>g?(p.addRange(y),p.extend(v.node,v.offset)):(y.setEnd(v.node,v.offset),p.addRange(y))}}}}for(d=[],p=s;p=p.parentNode;)p.nodeType===1&&d.push({element:p,left:p.scrollLeft,top:p.scrollTop});for(typeof s.focus==`function`&&s.focus(),s=0;sn?32:n,I.T=null,n=ou,ou=null;var o=nu,s=iu;if(tu=0,ru=nu=null,iu=0,jl&6)throw Error(i(331));var c=jl;if(jl|=4,El(o.current),vl(o,o.current,s,n),jl=c,nd(0,!1),Se&&typeof Se.onPostCommitFiberRoot==`function`)try{Se.onPostCommitFiberRoot(xe,o)}catch{}return!0}finally{L.p=a,I.T=r,Ru(e,t)}}function Vu(e,t,n){t=ri(n,t),t=Hs(e.stateNode,t,2),e=Aa(e,t,2),e!==null&&(Le(e,2),td(e))}function Hu(e,t,n){if(e.tag===3)Vu(e,e,n);else for(;t!==null;){if(t.tag===3){Vu(t,e,n);break}if(t.tag===1){var r=t.stateNode;if(typeof t.type.getDerivedStateFromError==`function`||typeof r.componentDidCatch==`function`&&(eu===null||!eu.has(r))){e=ri(n,e),n=Us(2),r=Aa(t,n,2),r!==null&&(Ws(n,r,t,e),Le(r,2),td(r));break}}t=t.return}}function Uu(e,t,n){var r=e.pingCache;if(r===null){r=e.pingCache=new Al;var i=new Set;r.set(t,i)}else i=r.get(t),i===void 0&&(i=new Set,r.set(t,i));i.has(n)||(zl=!0,i.add(n),e=Wu.bind(null,e,t,n),t.then(e,e))}function Wu(e,t,n){var r=e.pingCache;r!==null&&r.delete(t),e.pingedLanes|=e.suspendedLanes&n,e.warmLanes&=~n,Ml===e&&(Pl&n)===n&&(Vl===4||Vl===3&&(Pl&62914560)===Pl&&300>X()-Xl?!(jl&2)&&yu(e,0):Wl|=n,Kl===Pl&&(Kl=0)),td(e)}function Gu(e,t){t===0&&(t=Fe()),e=Hr(e,t),e!==null&&(Le(e,t),td(e))}function Ku(e){var t=e.memoizedState,n=0;t!==null&&(n=t.retryLane),Gu(e,n)}function qu(e,t){var n=0;switch(e.tag){case 31:case 13:var r=e.stateNode,a=e.memoizedState;a!==null&&(n=a.retryLane);break;case 19:r=e.stateNode;break;case 22:r=e.stateNode._retryCache;break;default:throw Error(i(314))}r!==null&&r.delete(t),Gu(e,n)}function Ju(e,t){return ue(e,t)}var Yu=null,Xu=null,Zu=!1,Qu=!1,$u=!1,ed=0;function td(e){e!==Xu&&e.next===null&&(Xu===null?Yu=Xu=e:Xu=Xu.next=e),Qu=!0,Zu||(Zu=!0,cd())}function nd(e,t){if(!$u&&Qu){$u=!0;do for(var n=!1,r=Yu;r!==null;){if(!t){if(e!==0){var i=r.pendingLanes;if(i===0)var a=0;else{var o=r.suspendedLanes,s=r.pingedLanes;a=(1<<31-we(42|e)+1)-1,a&=i&~(o&~s),a=a&201326741?a&201326741|1:a?a|2:0}a!==0&&(n=!0,sd(r,a))}else a=Pl,a=Me(r,r===Ml?a:0,r.cancelPendingCommit!==null||r.timeoutHandle!==-1),!(a&3)||Ne(r,a)||(n=!0,sd(r,a))}r=r.next}while(n);$u=!1}}function rd(){id()}function id(){Qu=Zu=!1;var e=0;ed!==0&&Gd()&&(e=ed);for(var t=X(),n=null,r=Yu;r!==null;){var i=r.next,a=ad(r,t);a===0?(r.next=null,n===null?Yu=i:n.next=i,i===null&&(Xu=n)):(n=r,(e!==0||a&3)&&(Qu=!0)),r=i}tu!==0&&tu!==5||nd(e,!1),ed!==0&&(ed=0)}function ad(e,t){for(var n=e.suspendedLanes,r=e.pingedLanes,i=e.expirationTimes,a=e.pendingLanes&-62914561;0s)break;var u=c.transferSize,d=c.initiatorType;u&&Id(d)&&(c=c.responseEnd,o+=u*(c`u`?null:document;function xf(e,t,n){var r=bf;if(r&&typeof t==`string`&&t){var i=wt(t);i=`link[rel="`+e+`"][href="`+i+`"]`,typeof n==`string`&&(i+=`[crossorigin="`+n+`"]`),hf.has(i)||(hf.add(i),e={rel:e,crossOrigin:n,href:t},r.querySelector(i)===null&&(t=r.createElement(`link`),Pd(t,`link`,e),ot(t),r.head.appendChild(t)))}}function Sf(e){_f.D(e),xf(`dns-prefetch`,e,null)}function Cf(e,t){_f.C(e,t),xf(`preconnect`,e,t)}function wf(e,t,n){_f.L(e,t,n);var r=bf;if(r&&e&&t){var i=`link[rel="preload"][as="`+wt(t)+`"]`;t===`image`&&n&&n.imageSrcSet?(i+=`[imagesrcset="`+wt(n.imageSrcSet)+`"]`,typeof n.imageSizes==`string`&&(i+=`[imagesizes="`+wt(n.imageSizes)+`"]`)):i+=`[href="`+wt(e)+`"]`;var a=i;switch(t){case`style`:a=Af(e);break;case`script`:a=Pf(e)}mf.has(a)||(e=h({rel:`preload`,href:t===`image`&&n&&n.imageSrcSet?void 0:e,as:t},n),mf.set(a,e),r.querySelector(i)!==null||t===`style`&&r.querySelector(jf(a))||t===`script`&&r.querySelector(Ff(a))||(t=r.createElement(`link`),Pd(t,`link`,e),ot(t),r.head.appendChild(t)))}}function Tf(e,t){_f.m(e,t);var n=bf;if(n&&e){var r=t&&typeof t.as==`string`?t.as:`script`,i=`link[rel="modulepreload"][as="`+wt(r)+`"][href="`+wt(e)+`"]`,a=i;switch(r){case`audioworklet`:case`paintworklet`:case`serviceworker`:case`sharedworker`:case`worker`:case`script`:a=Pf(e)}if(!mf.has(a)&&(e=h({rel:`modulepreload`,href:e},t),mf.set(a,e),n.querySelector(i)===null)){switch(r){case`audioworklet`:case`paintworklet`:case`serviceworker`:case`sharedworker`:case`worker`:case`script`:if(n.querySelector(Ff(a)))return}r=n.createElement(`link`),Pd(r,`link`,e),ot(r),n.head.appendChild(r)}}}function Ef(e,t,n){_f.S(e,t,n);var r=bf;if(r&&e){var i=at(r).hoistableStyles,a=Af(e);t||=`default`;var o=i.get(a);if(!o){var s={loading:0,preload:null};if(o=r.querySelector(jf(a)))s.loading=5;else{e=h({rel:`stylesheet`,href:e,"data-precedence":t},n),(n=mf.get(a))&&Rf(e,n);var c=o=r.createElement(`link`);ot(c),Pd(c,`link`,e),c._p=new Promise(function(e,t){c.onload=e,c.onerror=t}),c.addEventListener(`load`,function(){s.loading|=1}),c.addEventListener(`error`,function(){s.loading|=2}),s.loading|=4,Lf(o,t,r)}o={type:`stylesheet`,instance:o,count:1,state:s},i.set(a,o)}}}function Df(e,t){_f.X(e,t);var n=bf;if(n&&e){var r=at(n).hoistableScripts,i=Pf(e),a=r.get(i);a||(a=n.querySelector(Ff(i)),a||(e=h({src:e,async:!0},t),(t=mf.get(i))&&zf(e,t),a=n.createElement(`script`),ot(a),Pd(a,`link`,e),n.head.appendChild(a)),a={type:`script`,instance:a,count:1,state:null},r.set(i,a))}}function Of(e,t){_f.M(e,t);var n=bf;if(n&&e){var r=at(n).hoistableScripts,i=Pf(e),a=r.get(i);a||(a=n.querySelector(Ff(i)),a||(e=h({src:e,async:!0,type:`module`},t),(t=mf.get(i))&&zf(e,t),a=n.createElement(`script`),ot(a),Pd(a,`link`,e),n.head.appendChild(a)),a={type:`script`,instance:a,count:1,state:null},r.set(i,a))}}function kf(e,t,n,r){var a=(a=G.current)?gf(a):null;if(!a)throw Error(i(446));switch(e){case`meta`:case`title`:return null;case`style`:return typeof n.precedence==`string`&&typeof n.href==`string`?(t=Af(n.href),n=at(a).hoistableStyles,r=n.get(t),r||(r={type:`style`,instance:null,count:0,state:null},n.set(t,r)),r):{type:`void`,instance:null,count:0,state:null};case`link`:if(n.rel===`stylesheet`&&typeof n.href==`string`&&typeof n.precedence==`string`){e=Af(n.href);var o=at(a).hoistableStyles,s=o.get(e);if(s||(a=a.ownerDocument||a,s={type:`stylesheet`,instance:null,count:0,state:{loading:0,preload:null}},o.set(e,s),(o=a.querySelector(jf(e)))&&!o._p&&(s.instance=o,s.state.loading=5),mf.has(e)||(n={rel:`preload`,as:`style`,href:n.href,crossOrigin:n.crossOrigin,integrity:n.integrity,media:n.media,hrefLang:n.hrefLang,referrerPolicy:n.referrerPolicy},mf.set(e,n),o||Nf(a,e,n,s.state))),t&&r===null)throw Error(i(528,``));return s}if(t&&r!==null)throw Error(i(529,``));return null;case`script`:return t=n.async,n=n.src,typeof n==`string`&&t&&typeof t!=`function`&&typeof t!=`symbol`?(t=Pf(n),n=at(a).hoistableScripts,r=n.get(t),r||(r={type:`script`,instance:null,count:0,state:null},n.set(t,r)),r):{type:`void`,instance:null,count:0,state:null};default:throw Error(i(444,e))}}function Af(e){return`href="`+wt(e)+`"`}function jf(e){return`link[rel="stylesheet"][`+e+`]`}function Mf(e){return h({},e,{"data-precedence":e.precedence,precedence:null})}function Nf(e,t,n,r){e.querySelector(`link[rel="preload"][as="style"][`+t+`]`)?r.loading=1:(t=e.createElement(`link`),r.preload=t,t.addEventListener(`load`,function(){return r.loading|=1}),t.addEventListener(`error`,function(){return r.loading|=2}),Pd(t,`link`,n),ot(t),e.head.appendChild(t))}function Pf(e){return`[src="`+wt(e)+`"]`}function Ff(e){return`script[async]`+e}function If(e,t,n){if(t.count++,t.instance===null)switch(t.type){case`style`:var r=e.querySelector(`style[data-href~="`+wt(n.href)+`"]`);if(r)return t.instance=r,ot(r),r;var a=h({},n,{"data-href":n.href,"data-precedence":n.precedence,href:null,precedence:null});return r=(e.ownerDocument||e).createElement(`style`),ot(r),Pd(r,`style`,a),Lf(r,n.precedence,e),t.instance=r;case`stylesheet`:a=Af(n.href);var o=e.querySelector(jf(a));if(o)return t.state.loading|=4,t.instance=o,ot(o),o;r=Mf(n),(a=mf.get(a))&&Rf(r,a),o=(e.ownerDocument||e).createElement(`link`),ot(o);var s=o;return s._p=new Promise(function(e,t){s.onload=e,s.onerror=t}),Pd(o,`link`,r),t.state.loading|=4,Lf(o,n.precedence,e),t.instance=o;case`script`:return o=Pf(n.src),(a=e.querySelector(Ff(o)))?(t.instance=a,ot(a),a):(r=n,(a=mf.get(o))&&(r=h({},n),zf(r,a)),e=e.ownerDocument||e,a=e.createElement(`script`),ot(a),Pd(a,`link`,r),e.head.appendChild(a),t.instance=a);case`void`:return null;default:throw Error(i(443,t.type))}else t.type===`stylesheet`&&!(t.state.loading&4)&&(r=t.instance,t.state.loading|=4,Lf(r,n.precedence,e));return t.instance}function Lf(e,t,n){for(var r=n.querySelectorAll(`link[rel="stylesheet"][data-precedence],style[data-precedence]`),i=r.length?r[r.length-1]:null,a=i,o=0;o title`):null)}function Uf(e,t,n){if(n===1||t.itemProp!=null)return!1;switch(e){case`meta`:case`title`:return!0;case`style`:if(typeof t.precedence!=`string`||typeof t.href!=`string`||t.href===``)break;return!0;case`link`:if(typeof t.rel!=`string`||typeof t.href!=`string`||t.href===``||t.onLoad||t.onError)break;switch(t.rel){case`stylesheet`:return e=t.disabled,typeof t.precedence==`string`&&e==null;default:return!0}case`script`:if(t.async&&typeof t.async!=`function`&&typeof t.async!=`symbol`&&!t.onLoad&&!t.onError&&t.src&&typeof t.src==`string`)return!0}return!1}function Wf(e){return!(e.type===`stylesheet`&&!(e.state.loading&3))}function Gf(e,t,n,r){if(n.type===`stylesheet`&&(typeof r.media!=`string`||!1!==matchMedia(r.media).matches)&&!(n.state.loading&4)){if(n.instance===null){var i=Af(r.href),a=t.querySelector(jf(i));if(a){t=a._p,typeof t==`object`&&t&&typeof t.then==`function`&&(e.count++,e=Jf.bind(e),t.then(e,e)),n.state.loading|=4,n.instance=a,ot(a);return}a=t.ownerDocument||t,r=Mf(r),(i=mf.get(i))&&Rf(r,i),a=a.createElement(`link`),ot(a);var o=a;o._p=new Promise(function(e,t){o.onload=e,o.onerror=t}),Pd(a,`link`,r),n.instance=a}e.stylesheets===null&&(e.stylesheets=new Map),e.stylesheets.set(n,t),(t=n.state.preload)&&!(n.state.loading&3)&&(e.count++,n=Jf.bind(e),t.addEventListener(`load`,n),t.addEventListener(`error`,n))}}var Kf=0;function qf(e,t){return e.stylesheets&&e.count===0&&Xf(e,e.stylesheets),0Kf?50:800)+t);return e.unsuspend=n,function(){e.unsuspend=null,clearTimeout(r),clearTimeout(i)}}:null}function Jf(){if(this.count--,this.count===0&&(this.imgCount===0||!this.waitingForImages)){if(this.stylesheets)Xf(this,this.stylesheets);else if(this.unsuspend){var e=this.unsuspend;this.unsuspend=null,e()}}}var Yf=null;function Xf(e,t){e.stylesheets=null,e.unsuspend!==null&&(e.count++,Yf=new Map,t.forEach(Zf,e),Yf=null,Jf.call(e))}function Zf(e,t){if(!(t.state.loading&4)){var n=Yf.get(e);if(n)var r=n.get(null);else{n=new Map,Yf.set(e,n);for(var i=e.querySelectorAll(`link[data-precedence],style[data-precedence]`),a=0;a{function n(){if(!(typeof __REACT_DEVTOOLS_GLOBAL_HOOK__>`u`||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!=`function`))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(n)}catch(e){console.error(e)}}n(),t.exports=h()}))(),1),_=c(u(),1),v=new Map,y=3e4,b=`ocx-resource-deadline`,x={data:void 0,error:void 0,loading:!1,refreshing:!1,hasSucceeded:!1,lastAttemptOk:!1};function S(e){let t=v.get(e);return t||(t={snapshot:{data:void 0,error:void 0,loading:!1,refreshing:!1,hasSucceeded:!1,lastAttemptOk:!1},listeners:new Set,pollByListener:new Map,pauseWhenHiddenByListener:new Map,fetcherByListener:new Map,deadlineByListener:new Map,subscriberCount:0,pollIntervalMs:void 0,inflight:null,inflightOwner:null,generation:0,seedNeedsRevalidate:!1,lastSettledAt:void 0},v.set(e,t)),t}function C(e){for(let t of e.listeners)t()}var w=new Map;function T(e){for(let[t,n]of e.pollByListener)if(typeof n==`number`&&n>0&&e.pauseWhenHiddenByListener.get(t)===!1)return!0;return!1}function E(e){if(e.stores.size===0)return!1;if(!M())return!0;for(let t of e.stores)if(T(t))return!0;return!1}function D(e){for(let t of e.stores){let e=N(t);e&&z(t,e.fetcher,{replaceInflight:!1,owner:e.owner,deadlineMs:e.deadlineMs})}}function O(e,t){let n=E(t);if(n&&t.timer===null){t.timer=setInterval(()=>D(t),e);return}!n&&t.timer!==null&&(clearInterval(t.timer),t.timer=null),t.stores.size===0&&w.delete(e)}function k(){for(let[e,t]of[...w])O(e,t)}function A(e){let t=e.pollIntervalMs;if(t===void 0)return;let n=w.get(t);e.pollIntervalMs=void 0,n&&(n.stores.delete(e),O(t,n))}function j(e,t){let n=w.get(t);n||(n={timer:null,stores:new Set},w.set(t,n)),n.stores.add(e),e.pollIntervalMs=t,O(t,n)}function M(){return typeof document<`u`&&document.visibilityState===`hidden`}function N(e){if(!M())return P(e);for(let[t,n]of e.pollByListener)if(!(typeof n!=`number`||n<=0)&&e.pauseWhenHiddenByListener.get(t)===!1){let n=e.fetcherByListener.get(t);if(n)return{owner:t,fetcher:n,deadlineMs:e.deadlineByListener.get(t)}}return null}function P(e){for(let[t,n]of e.pollByListener)if(typeof n==`number`&&n>0){let n=e.fetcherByListener.get(t);if(n)return{owner:t,fetcher:n,deadlineMs:e.deadlineByListener.get(t)}}for(let[t,n]of e.fetcherByListener)return{owner:t,fetcher:n,deadlineMs:e.deadlineByListener.get(t)};return null}function F(e){let t;for(let n of e.pollByListener.values())typeof n==`number`&&n>0&&(t=t===void 0?n:Math.min(t,n));if(t===void 0){A(e),R(e);return}if(t===e.pollIntervalMs){let n=w.get(t);n&&O(t,n),L(e);return}A(e),j(e,t),L(e)}var I=null;function L(e){if(typeof document>`u`||I)return;let t=()=>{if(k(),!M())for(let e of w.values())for(let t of e.stores){let e=P(t);e&&z(t,e.fetcher,{replaceInflight:!1,owner:e.owner,deadlineMs:e.deadlineMs})}};document.addEventListener(`visibilitychange`,t),I=t}function R(e){I&&(w.size>0||(typeof document<`u`&&document.removeEventListener(`visibilitychange`,I),I=null))}async function z(e,t,n){let r=n?.replaceInflight!==!1;if(e.inflight&&!r)return;r&&e.inflight?.abort();let i=new AbortController;e.inflight=i,e.inflightOwner=n?.owner??null;let a=++e.generation,o=n?.deadlineMs??y,s=!1,c=setTimeout(()=>{s=!0,i.abort(b)},o),l=e.snapshot.data===void 0||n?.forceLoading===!0;e.snapshot={...e.snapshot,loading:l?!0:e.snapshot.loading,refreshing:!0},C(e);try{let n=await Promise.race([t(i.signal),new Promise((e,t)=>{i.signal.addEventListener(`abort`,()=>{s?t(Error(`resource request timed out after ${o}ms`)):e(null)},{once:!0})})]);if(a!==e.generation||i.signal.aborted)return;e.seedNeedsRevalidate=!1,e.lastSettledAt=Date.now(),e.snapshot={data:n,error:void 0,loading:!1,refreshing:!1,hasSucceeded:!0,lastAttemptOk:!0}}catch(t){if(a!==e.generation||i.signal.aborted&&!s)return;e.seedNeedsRevalidate=!1,e.snapshot={...e.snapshot,error:t===void 0?Error(`resource load failed`):t,loading:!1,refreshing:!1,lastAttemptOk:!1}}finally{clearTimeout(c),e.inflight===i&&(e.inflight=null,e.inflightOwner=null),C(e)}}function B(e,t){return e.inflightOwner===t&&(e.inflight?.abort(),e.inflight=null,e.inflightOwner=null,e.generation++,e.snapshot.refreshing&&(e.snapshot={...e.snapshot,refreshing:!1},C(e)),!0)}function V(e,t){A(t),R(t),setTimeout(()=>{t.subscriberCount===0&&v.get(e)===t&&(t.inflight?.abort(),t.inflight=null,t.inflightOwner=null,v.delete(e))},0)}function H(e,t,n){let{fetcher:r,pollMs:i,pauseWhenHidden:a=!0,deadlineMs:o,staleAfterMs:s}=n,c=S(e);if(c.listeners.add(t),c.pollByListener.set(t,i),c.pauseWhenHiddenByListener.set(t,a),c.fetcherByListener.set(t,r),c.deadlineByListener.set(t,o),c.subscriberCount++,c.subscriberCount===1){let e=typeof s==`number`&&c.lastSettledAt!==void 0&&Date.now()-c.lastSettledAt>s;(c.snapshot.data===void 0||c.seedNeedsRevalidate||e)&&z(c,r,{replaceInflight:!0,owner:t,deadlineMs:o})}return F(c),()=>{c.listeners.delete(t),c.pollByListener.delete(t),c.pauseWhenHiddenByListener.delete(t),c.fetcherByListener.delete(t),c.deadlineByListener.delete(t),c.subscriberCount--;let n=B(c,t);if(c.subscriberCount===0){V(e,c);return}if(n){let e=P(c);e&&z(c,e.fetcher,{replaceInflight:!0,owner:e.owner,deadlineMs:e.deadlineMs})}F(c)}}function U(e,t,n,r){let i=S(e);i.subscriberCount===0&&i.snapshot.data===void 0&&(K(e,t),typeof r==`number`&&typeof n==`number`&&Date.now()-n{c.current=t});let l=(0,_.useCallback)(e=>c.current(e),[]),u=(0,_.useRef)(null),d=(0,_.useCallback)(t=>r?(u.current=t,H(e,t,{fetcher:l,pollMs:i,pauseWhenHidden:a,deadlineMs:o,staleAfterMs:s})):()=>{},[e,l,i,r,a,o,s]),f=(0,_.useCallback)(()=>r?S(e).snapshot:x,[e,r]),p=(0,_.useSyncExternalStore)(d,f,f),m=(0,_.useCallback)(t=>{if(!r)return;let n=S(e);z(n,l,{replaceInflight:!0,owner:u.current,forceLoading:t?.forceLoading,deadlineMs:u.current?n.deadlineByListener.get(u.current):o})},[e,l,r,o]);return{...p,refresh:m}}function ee(e,t){if(e===null)return!1;if(e.length!==t.length)return!0;for(let n=0;n{let n=a.current,r=o.current;a.current=t,o.current=e,ee(n,t)&&(r===null||r===e)&&i.refresh({forceLoading:!0})}),i}function K(e,t){let n=S(e);n.inflight?.abort(),n.inflight=null,n.inflightOwner=null,n.generation++,n.snapshot={data:t,error:void 0,loading:!1,refreshing:!1,hasSucceeded:!0,lastAttemptOk:!0},n.seedNeedsRevalidate=n.subscriberCount===0,n.lastSettledAt=Date.now(),C(n)}var q=o((e=>{var t=Symbol.for(`react.transitional.element`),n=Symbol.for(`react.fragment`);function r(e,n,r){var i=null;if(r!==void 0&&(i=``+r),n.key!==void 0&&(i=``+n.key),`key`in n)for(var a in r={},n)a!==`key`&&(r[a]=n[a]);else r=n;return n=r.ref,{$$typeof:t,type:e,key:i,ref:n===void 0?null:n,props:r}}e.Fragment=n,e.jsx=r,e.jsxs=r})),J=o(((e,t)=>{t.exports=q()}))(),Y=e=>({viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,strokeWidth:2,strokeLinecap:`round`,strokeLinejoin:`round`,...e}),te=e=>(0,J.jsxs)(`svg`,{...Y(e),children:[(0,J.jsx)(`rect`,{x:`3`,y:`3`,width:`7`,height:`7`,rx:`1.5`}),(0,J.jsx)(`rect`,{x:`14`,y:`3`,width:`7`,height:`7`,rx:`1.5`}),(0,J.jsx)(`rect`,{x:`3`,y:`14`,width:`7`,height:`7`,rx:`1.5`}),(0,J.jsx)(`rect`,{x:`14`,y:`14`,width:`7`,height:`7`,rx:`1.5`})]}),ne=e=>(0,J.jsxs)(`svg`,{...Y(e),children:[(0,J.jsx)(`rect`,{x:`3`,y:`4`,width:`18`,height:`7`,rx:`2`}),(0,J.jsx)(`rect`,{x:`3`,y:`13`,width:`18`,height:`7`,rx:`2`}),(0,J.jsx)(`path`,{d:`M7 7.5h.01M7 16.5h.01`})]}),re=e=>(0,J.jsxs)(`svg`,{...Y(e),children:[(0,J.jsx)(`path`,{d:`M12 2 4 6v6l8 4 8-4V6l-8-4Z`}),(0,J.jsx)(`path`,{d:`m4 6 8 4 8-4M12 10v8`})]}),ie=e=>(0,J.jsxs)(`svg`,{...Y(e),children:[(0,J.jsx)(`rect`,{x:`4`,y:`8`,width:`16`,height:`11`,rx:`3`}),(0,J.jsx)(`path`,{d:`M12 8V4M8 2h8`}),(0,J.jsx)(`circle`,{cx:`9`,cy:`13`,r:`1`}),(0,J.jsx)(`circle`,{cx:`15`,cy:`13`,r:`1`})]}),ae=e=>(0,J.jsx)(`svg`,{...Y(e),children:(0,J.jsx)(`path`,{d:`M8 6h13M8 12h13M8 18h13M3 6h.01M3 12h.01M3 18h.01`})}),oe=e=>(0,J.jsx)(`svg`,{...Y(e),children:(0,J.jsx)(`path`,{d:`M4 6h16M4 12h16M4 18h16`})}),se=e=>(0,J.jsxs)(`svg`,{...Y(e),children:[(0,J.jsx)(`path`,{d:`m4 17 6-5-6-5`}),(0,J.jsx)(`path`,{d:`M12 19h8`})]}),ce=e=>(0,J.jsx)(`svg`,{...Y(e),children:(0,J.jsx)(`path`,{d:`M22 12h-4l-3 9L9 3l-3 9H2`})}),le=e=>(0,J.jsxs)(`svg`,{...Y(e),children:[(0,J.jsx)(`path`,{d:`M22 12H2`}),(0,J.jsx)(`path`,{d:`M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11Z`}),(0,J.jsx)(`path`,{d:`M6 16h.01M10 16h.01`})]}),ue=e=>(0,J.jsx)(`svg`,{...Y(e),children:(0,J.jsx)(`path`,{d:`m20 6-11 11-5-5`})}),de=e=>(0,J.jsx)(`svg`,{...Y(e),children:(0,J.jsx)(`path`,{d:`M18 6 6 18M6 6l12 12`})}),fe=e=>(0,J.jsx)(`svg`,{...Y(e),children:(0,J.jsx)(`path`,{d:`M12 5v14M5 12h14`})}),pe=e=>(0,J.jsx)(`svg`,{...Y(e),children:(0,J.jsx)(`path`,{d:`M21 12a9 9 0 0 1-9 9 9.8 9.8 0 0 1-6.7-2.7L3 16M3 21v-5h5M3 12a9 9 0 0 1 9-9 9.8 9.8 0 0 1 6.7 2.7L21 8M21 3v5h-5`})}),X=e=>(0,J.jsx)(`svg`,{...Y(e),children:(0,J.jsx)(`path`,{d:`M8 5v14M16 5v14`})}),me=e=>(0,J.jsx)(`svg`,{...Y(e),children:(0,J.jsx)(`path`,{d:`m7 4 13 8-13 8Z`})}),he=e=>(0,J.jsx)(`svg`,{...Y(e),children:(0,J.jsx)(`path`,{d:`M3 6h18M8 6V4h8v2M19 6l-1 14H6L5 6`})}),ge=e=>(0,J.jsxs)(`svg`,{...Y(e),children:[(0,J.jsx)(`path`,{d:`M12 20h9`}),(0,J.jsx)(`path`,{d:`M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4Z`})]}),_e=e=>(0,J.jsxs)(`svg`,{...Y(e),children:[(0,J.jsx)(`path`,{d:`M10.3 3.7 1.8 18a2 2 0 0 0 1.7 3h17a2 2 0 0 0 1.7-3L13.7 3.7a2 2 0 0 0-3.4 0Z`}),(0,J.jsx)(`path`,{d:`M12 9v4M12 17h.01`})]}),Z=e=>(0,J.jsxs)(`svg`,{...Y(e),children:[(0,J.jsx)(`circle`,{cx:`12`,cy:`12`,r:`10`}),(0,J.jsx)(`path`,{d:`M12 16v-4M12 8h.01`})]}),ve=e=>(0,J.jsxs)(`svg`,{...Y(e),children:[(0,J.jsx)(`circle`,{cx:`11`,cy:`11`,r:`7`}),(0,J.jsx)(`path`,{d:`m21 21-4.3-4.3`})]}),ye=e=>(0,J.jsx)(`svg`,{...Y(e),children:(0,J.jsx)(`path`,{d:`M12 19V5M5 12l7-7 7 7`})}),be=e=>(0,J.jsx)(`svg`,{...Y(e),children:(0,J.jsx)(`path`,{d:`M12 5v14M19 12l-7 7-7-7`})}),xe=e=>(0,J.jsx)(`svg`,{...Y(e),children:(0,J.jsx)(`path`,{d:`M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3`})}),Se=e=>(0,J.jsx)(`svg`,{...Y(e),children:(0,J.jsx)(`path`,{d:`m9 18 6-6-6-6`})}),Ce=e=>(0,J.jsx)(`svg`,{...Y(e),children:(0,J.jsx)(`path`,{d:`M9 19c-5 1.5-5-2.5-7-3m14 6v-3.9a3.4 3.4 0 0 0-.9-2.6c3-.3 6.2-1.5 6.2-6.7A5.2 5.2 0 0 0 20 4.8 4.9 4.9 0 0 0 19.9 1S18.7.6 16 2.5a13.4 13.4 0 0 0-7 0C6.3.6 5.1 1 5.1 1A4.9 4.9 0 0 0 5 4.8a5.2 5.2 0 0 0-1.4 3.7c0 5.1 3.1 6.4 6.1 6.7a3.4 3.4 0 0 0-.9 2.5V22`})}),we=e=>(0,J.jsxs)(`svg`,{...Y(e),children:[(0,J.jsx)(`path`,{d:`M18.4 5.6a9 9 0 1 1-12.8 0`}),(0,J.jsx)(`path`,{d:`M12 2v10`})]}),Te=e=>(0,J.jsx)(`svg`,{...Y(e),children:(0,J.jsx)(`path`,{d:`M15 3h6v6M10 14 21 3M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6`})}),Ee=e=>(0,J.jsxs)(`svg`,{...Y(e),children:[(0,J.jsx)(`circle`,{cx:`7.5`,cy:`15.5`,r:`4.5`}),(0,J.jsx)(`path`,{d:`m10.7 12.3 9.6-9.6M16 7l3 3M14 9l2 2`})]}),De=e=>(0,J.jsxs)(`svg`,{...Y(e),children:[(0,J.jsx)(`rect`,{x:`4`,y:`11`,width:`16`,height:`10`,rx:`2`}),(0,J.jsx)(`path`,{d:`M8 11V7a4 4 0 0 1 8 0v4`})]}),Oe=e=>(0,J.jsxs)(`svg`,{...Y(e),children:[(0,J.jsx)(`path`,{d:`M2 9a3 3 0 0 1 0 6v2a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-2a3 3 0 0 1 0-6V7a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2Z`}),(0,J.jsx)(`path`,{d:`M13 5v2`}),(0,J.jsx)(`path`,{d:`M13 17v2`}),(0,J.jsx)(`path`,{d:`M13 11v2`})]}),ke=e=>(0,J.jsx)(`svg`,{...Y(e),children:(0,J.jsx)(`path`,{d:`M9 17H7A5 5 0 0 1 7 7h2M15 7h2a5 5 0 0 1 0 10h-2M8 12h8`})}),Ae=e=>(0,J.jsxs)(`svg`,{...Y(e),children:[(0,J.jsx)(`circle`,{cx:`12`,cy:`12`,r:`4`}),(0,J.jsx)(`path`,{d:`M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4`})]}),je=e=>(0,J.jsx)(`svg`,{...Y(e),children:(0,J.jsx)(`path`,{d:`M21 12.8A9 9 0 1 1 11.2 3 7 7 0 0 0 21 12.8Z`})}),Me=e=>(0,J.jsxs)(`svg`,{...Y(e),children:[(0,J.jsx)(`rect`,{x:`2`,y:`3`,width:`20`,height:`14`,rx:`2`}),(0,J.jsx)(`path`,{d:`M8 21h8M12 17v4`})]}),Ne=e=>(0,J.jsxs)(`svg`,{...Y(e),children:[(0,J.jsx)(`circle`,{cx:`12`,cy:`12`,r:`9`}),(0,J.jsx)(`path`,{d:`M3 12h18M12 3a14 14 0 0 1 0 18M12 3a14 14 0 0 0 0 18`})]}),Pe=e=>(0,J.jsxs)(`svg`,{...Y(e),children:[(0,J.jsx)(`path`,{d:`m18 14 4 4-4 4`}),(0,J.jsx)(`path`,{d:`m18 2 4 4-4 4`}),(0,J.jsx)(`path`,{d:`M2 18h1.973a4 4 0 0 0 3.3-1.7l5.454-8.6a4 4 0 0 1 3.3-1.7H22`}),(0,J.jsx)(`path`,{d:`M2 6h1.972a4 4 0 0 1 3.6 2.2`}),(0,J.jsx)(`path`,{d:`M22 18h-6.041a4 4 0 0 1-3.3-1.8l-.359-.45`})]}),Fe=e=>(0,J.jsxs)(`svg`,{...Y(e),children:[(0,J.jsx)(`circle`,{cx:`9`,cy:`6`,r:`1`,fill:`currentColor`,stroke:`none`}),(0,J.jsx)(`circle`,{cx:`15`,cy:`6`,r:`1`,fill:`currentColor`,stroke:`none`}),(0,J.jsx)(`circle`,{cx:`9`,cy:`12`,r:`1`,fill:`currentColor`,stroke:`none`}),(0,J.jsx)(`circle`,{cx:`15`,cy:`12`,r:`1`,fill:`currentColor`,stroke:`none`}),(0,J.jsx)(`circle`,{cx:`9`,cy:`18`,r:`1`,fill:`currentColor`,stroke:`none`}),(0,J.jsx)(`circle`,{cx:`15`,cy:`18`,r:`1`,fill:`currentColor`,stroke:`none`})]}),Ie=e=>(0,J.jsx)(`svg`,{...Y(e),children:(0,J.jsx)(`path`,{d:`m12 2 3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z`})}),Le=e=>(0,J.jsx)(`svg`,{...Y(e),children:(0,J.jsx)(`path`,{d:`M4 5h16l-6 7v5l-4 2v-7L4 5z`})}),Re={"nav.dashboard":`Dashboard`,"uptime.day":`d`,"uptime.hour":`h`,"uptime.minute":`m`,"uptime.second":`s`,"nav.startup":`Startup`,"nav.providers":`Providers`,"nav.models":`Models`,"nav.combos":`Combos`,"nav.subagents":`Subagents`,"nav.logs":`Logs & Debug`,"nav.usage":`Usage`,"common.github":`GitHub`,"sidebar.star":`Star on GitHub`,"sidebar.starred":`Starred on GitHub`,"sidebar.starUnauthenticated":`Open GitHub to star (gh CLI is not signed in)`,"sidebar.starFailed":`Could not star through gh. Opening GitHub instead.`,"sidebar.updateAvailable":`Update available: {version}`,"sidebar.checkUpdate":`Check for updates`,"common.save":`Save`,"common.saving":`Saving…`,"common.cancel":`Cancel`,"common.discard":`Discard`,"common.delete":`Delete`,"common.close":`Close`,"common.ok":`OK`,"common.remove":`Remove`,"common.loading":`Loading…`,"common.retry":`Retry`,"auth.adminTokenTitle":`OpenCodex admin token (OPENCODEX_ADMIN_AUTH_TOKEN)`,"auth.adminAccountLabel":`Account`,"auth.adminTokenFieldLabel":`Admin token`,"auth.adminTokenRejected":`That admin token was rejected. Check it and try again.`,"auth.adminTokenUnavailable":`The admin token could not be verified. Try again.`,"app.logoAria":`opencodex logo`,"app.claudeOn":`Claude ON`,"app.claudeOff":`Claude OFF`,"theme.label":`Theme`,"theme.light":`Light`,"theme.dark":`Dark`,"theme.system":`System`,"lang.label":`Language`,"lang.nativeName":`English`,"provider.name.commandCodeAuth":`Command Code - Auth`,"provider.name.commandCodeApi":`Command Code - API`,"provider.name.volcengine":`Volcengine Ark`,"provider.name.volcengineCodingPlan":`Volcengine Ark Coding Plan`,"provider.name.volcengineAgentPlan":`Volcengine Ark Agent Plan`,"errorBoundary.title":`Page failed to load`,"errorBoundary.message":`This section hit a rendering error. Reload it to try again.`,"errorBoundary.details":`Error`,"errorBoundary.reload":`Reload`,"routing.title":`Routing Intelligence (beta)`,"routing.subtitle":`Policy profiles, dry-run evaluation, and source-backed routing analytics.`,"routing.loadFailed":`Could not load routing data`,"routing.empty":"No routing profiles configured. Add `routingProfiles` to config.json.","routing.revision":`rev`,"routing.detail":`Profile`,"routing.createProfile":`Create profile`,"routing.dryRunError":`Dry-run failed (HTTP {status})`,"routing.removeConfirm":`Remove profile {id}?`,"routing.unknownEvidence.allow":`allow`,"routing.unknownEvidence.penalize":`penalize`,"routing.unknownEvidence.exclude":`exclude`,"routing.removeCandidate":`Remove candidate {provider}/{model}`,"routing.candidates":`Candidates`,"routing.require":`Hard requirements`,"routing.optimize":`Optimization weights`,"routing.limits":`Limits`,"routing.unknownEvidence":`Unknown evidence policy`,"routing.compatibility.title":`Compatibility policy`,"routing.compatibility.enabled":`Require Compatibility Lab evidence`,"routing.compatibility.requiredSuites":`Required suites`,"routing.compatibility.loadingCatalog":`Loading lab catalog…`,"routing.compatibility.catalogUnavailable":`Lab catalog unavailable — enter suite ids manually in config.json.`,"routing.compatibility.layer.protocol_conformance":`Protocol conformance`,"routing.compatibility.layer.live_route_compatibility":`Live route compatibility`,"routing.compatibility.minStatus":`Minimum compatibility status`,"routing.none":`none`,"routing.unavailable":`–`,"routing.dryRun":`Dry-run evaluation`,"routing.dryRunContext":`Request context window (tokens)`,"routing.dryRunTools":`Request requires tools`,"routing.dryRunImage":`Request requires image input`,"routing.dryRunStructured":`Request requires structured output`,"routing.dryRunRun":`Evaluate candidates`,"routing.candidate":`Candidate`,"routing.eligible":`Eligible`,"routing.exclusions":`Exclusions`,"routing.costCap":`Cost cap`,"routing.capOutcome.satisfied":`within limit`,"routing.capOutcome.exceeded":`over limit`,"routing.capOutcome.unknown-allowed":`unknown (allowed)`,"routing.capOutcome.unknown-excluded":`unknown (excluded)`,"routing.exclusion.capability-unsatisfied":`capability not met`,"routing.exclusion.unknown-capability":`unknown capability`,"routing.exclusion.cost-limit":`over cost cap`,"routing.exclusion.cost-limit-unknown":`unknown cost under cap`,"routing.exclusion.cooldown":`cooldown`,"routing.exclusion.unknown-health":`unknown health`,"routing.exclusion.unknown-quota":`unknown quota`,"routing.exclusion.unknown-price":`unknown price`,"routing.exclusion.other":`exclusion: {code}`,"routing.score":`Score`,"routing.selected":`selected`,"routing.yes":`yes`,"routing.no":`no`,"routing.analytics":`Routing analytics`,"routing.analyticsTotal":`Requests`,"routing.analyticsSuccessRate":`Success`,"routing.analyticsFallbackRate":`Fallback`,"routing.analyticsP50":`p50`,"routing.analyticsP95":`p95`,"routing.analyticsP99":`p99`,"routing.analyticsCooldown":`Cooldown failures`,"routing.analyticsConfidence":`Confidence`,"routing.analyticsTruncated":`truncated history`,"routing.analyticsRequests":`Requests`,"routing.analyticsEmpty":`No analytics yet — send some requests first.`,"startup.title":`Startup safety`,"startup.subtitle":`Verify that Codex can reach opencodex after a restart, before local proxy routing becomes a reconnect loop.`,"startup.refresh":`Refresh`,"startup.backToDashboard":`Back to Dashboard`,"startup.loading":`Checking startup protection…`,"startup.error":`Could not read startup protection.`,"startup.staleData":`The latest startup check failed. The values below are stale and must not be treated as proof of protection.`,"startup.status.native":`Native routing`,"startup.status.protected":`Restart protected`,"startup.status.atRisk":`Action required`,"startup.summary.native":`Codex does not depend on the local proxy`,"startup.summary.protected":`opencodex will be available after restart`,"startup.summary.atRisk":`Codex can lose model access after restart`,"startup.riskDetail":`Codex is pinned to the local proxy, but no persistent service or healthy launcher shim will start it again.`,"startup.riskDetailCustomLocal":`Codex points to a custom local gateway. opencodex cannot manage or verify that gateway's restart lifecycle.`,"startup.riskDetailWindowsShim":`The launcher shim protects supported CLI scripts, but Codex Desktop and direct codex.exe launches can bypass it on Windows.`,"startup.safeDetail":`The current routing and startup mechanism are consistent. No manual ocx start should be required after restart.`,"startup.routing":`Codex routing`,"startup.routing.proxy":`Local proxy`,"startup.routing.native":`Native OpenAI`,"startup.routing.customLocal":`Custom local gateway`,"startup.routing.customRemote":`Custom remote gateway`,"startup.routing.unknown":`Unknown or invalid routing`,"startup.restartProtection":`Restart protection`,"startup.preference":`On-demand startup`,"startup.enabled":`Enabled`,"startup.disabled":`Disabled`,"startup.protection.service":`Background service`,"startup.protection.shim":`Launcher shim`,"startup.protection.none":`Not installed`,"startup.details":`Protection details`,"startup.service":`Background service`,"startup.serviceHint":`Starts at login and restarts the proxy after a crash.`,"startup.installed":`Installed`,"startup.notInstalled":`Not installed`,"startup.unsupported":`Unsupported`,"startup.shim":`Codex launcher shim`,"startup.shimHint":`Runs ocx ensure when a supported Codex script launcher starts.`,"startup.healthy":`Healthy`,"startup.cliOnly":`CLI only`,"startup.stale":`Stale`,"startup.viable":`Ready`,"startup.unhealthy":`Installed but unhealthy`,"startup.conflict":`Service conflict`,"startup.installedDisabled":`Installed but disabled`,"startup.install":`Install`,"startup.installing":`Installing…`,"startup.repair":`Repair`,"startup.repairing":`Repairing…`,"startup.serviceInstalled":`Background service installed successfully.`,"startup.serviceRepaired":`Background service repaired successfully.`,"startup.shimInstalled":`Codex launcher shim installed successfully.`,"startup.shimRepaired":`Codex launcher shim repaired successfully.`,"startup.installFailed":`Installation failed:`,"startup.tray.title":`Windows system tray`,"startup.tray.hint":`Install a login tray icon for one-click proxy start, stop, restart, dashboard, and status controls.`,"startup.tray.login":`Start tray at Windows login`,"startup.tray.notProtection":`The tray is a controller, not restart protection. A viable background service is still required for unattended proxy recovery.`,"startup.tray.running":`Running`,"startup.tray.stopped":`Installed, hidden`,"startup.tray.stale":`Repair required`,"startup.tray.notInstalled":`Not installed`,"startup.tray.loading":`Checking…`,"startup.tray.unavailable":`Status unavailable`,"startup.tray.install":`Install and show tray`,"startup.tray.start":`Show tray icon`,"startup.tray.stop":`Exit tray icon`,"startup.tray.uninstall":`Remove login tray`,"startup.tray.error":`The Windows tray action failed. Check ocx tray status for details.`,"startup.recovery":`Repair options`,"startup.recoveryHint":`Use the one-click installers above, or copy a command for manual repair. The background service is recommended for Codex Desktop and Windows executables.`,"startup.command.service":`Recommended: persistent background service`,"startup.command.shim":`Alternative: CLI launcher shim`,"startup.command.native":`Fail-safe: restore native Codex routing`,"startup.copy":`Copy`,"startup.copied":`Copied`,"startup.recommended":`Recommended repair: {cmd}`,"startup.navRisk":`Startup protection requires attention`,"startup.codexRuntime.clampHidden":`Some reasoning effort options were hidden because OpenCodex used Codex {version}.`,"startup.codexRuntime.clampHiddenWithEfforts":`Some reasoning effort options were hidden because OpenCodex used Codex {version} (removed: {efforts}).`,"startup.codexRuntime.olderBinary":`OpenCodex is using an older Codex binary ({version}). A newer installation is available.`,"dash.subtitle":`Live status of the local opencodex proxy, its providers, and the models routed into Codex.`,"dash.workspace.overview":`Overview`,"dash.workspace.sections":`Sections`,"dash.status":`Status`,"dash.online":`Online`,"dash.offline":`Offline`,"dash.version":`Version`,"dash.uptime":`Uptime`,"dash.providers":`Providers`,"dash.tokens30d":`Tokens (30d)`,"dash.coverage":`{pct} coverage`,"dash.mem.title":`Memory observability`,"dash.mem.hint":`Read-only runtime diagnostics. Observed memory is max(RSS, external, ArrayBuffers) so Windows working-set trimming does not hide committed retention.`,"dash.mem.rss":`Resident set (RSS)`,"dash.mem.jsHeap":`JS heap in use`,"dash.mem.jsHeapArena":`arena {total}`,"dash.mem.pressure":`Against warn threshold`,"dash.mem.pressureOf":`{pct}% of threshold`,"dash.mem.pressureUnknown":`No threshold reported`,"dash.mem.jscHeap":`JSC heap`,"dash.mem.external":`External`,"dash.mem.arrayBuffers":`ArrayBuffers`,"dash.mem.observed":`Observed`,"dash.mem.runtime":`Runtime counters`,"dash.mem.growth":`Observed drift / hour`,"dash.mem.perHour":`/h`,"dash.mem.store":`Continuation store`,"dash.mem.storeHint":`Proxy previous_response_id cache. Rising total bytes under a rising heap points at conversation retention rather than the runtime allocator.`,"dash.mem.storeEntries":`Entries`,"dash.mem.storeTotal":`Total`,"dash.mem.storeLargest":`Largest`,"dash.mem.storeOldest":`Oldest`,"dash.mem.threshold":`Warn threshold`,"dash.mem.lastWarn":`Last warning`,"dash.mem.never":`Never`,"dash.mem.details":`Details`,"dash.mem.unavailable":`Memory diagnostics unavailable (older proxy).`,"dash.mem.inFlight":`In-flight requests`,"dash.mem.restart":`Drain & restart`,"dash.mem.restartConfirm":`Wait for {count} in-flight request(s), then restart (up to {seconds}s; remaining requests are cut on timeout).`,"dash.mem.draining":`Draining {count} request(s)… restarting when complete`,"dash.mem.reconnecting":`Proxy restarting… waiting to reconnect`,"dash.mem.restartFailed":`Drain & restart failed. Check that the proxy is running.`,"dash.mem.restartNoSupervisor":`No restart protection detected. The proxy may stay down after restart unless you start it again.`,"dash.activeProviders":`Active providers`,"dash.noProviders":`No providers configured. Run {cmd}.`,"dash.col.name":`Name`,"dash.col.adapter":`Adapter`,"dash.col.baseUrl":`Base URL`,"dash.col.model":`Model`,"dash.modelsNoResults":`No models match your search.`,"dash.availableModels":`Available models`,"dash.noModels":`No models found. Check provider API keys.`,"dash.cannotConnect":`Cannot connect to proxy. Is it running?`,"dash.runStart":`Run {cmd} to start the proxy.`,"dash.stop":`Stop Proxy`,"dash.stopConfirm":`Stop the proxy and restore native Codex?`,"dash.stopFailed":`Failed to stop proxy (HTTP {status}).`,"dash.maSwitchFailed":`Mode switch failed (HTTP {status}).`,"dash.maNetworkError":`Network error — is the proxy running?`,"dash.stopping":`Stopping…`,"dash.actions":`Proxy`,"dash.codexRestart":`Reload Codex models`,"dash.codexRestarting":`Stopping…`,"dash.codexRestartConfirm":`Stop Codex app-servers so they reload the model list? Any Codex turn in progress is interrupted, and Codex does not relaunch on its own — reopen it afterwards.`,"dash.codexRestartDone":`Stopped {count} Codex app-server(s). Reopen Codex to load the current model list.`,"dash.codexRestartNothing":`No Codex app-server is running. The next launch reads the current model list.`,"dash.codexRestartUnknown":`Could not list processes, so nothing was stopped.`,"dash.codexRestartPartial":`{count} app-server(s) did not exit. Stop them manually if the model list stays stale.`,"dash.codexRestartFailed":`Failed to reload Codex models (HTTP {status}).`,"dash.codexRestartUnreachable":`Could not reach the proxy.`,"dash.codexRestartMalformed":`The proxy returned an unexpected response.`,"dash.codexRestartTimeout":`The proxy did not answer in time. It may still be stopping app-servers.`,"models.staleBanner":`Codex is showing an older model list than this catalog. Restart Codex to reload it.`,"dash.codexAutoStart":`Start opencodex with Codex`,"dash.codexAutoStartHint":`Allows an installed launcher shim to run ocx ensure. This setting does not install restart protection; check Startup safety for the effective state.`,"dash.searchModel":`Search sidecar model`,"dash.searchModelHint":`Model used for web_search on non-OpenAI routed models. Requires ChatGPT login.`,"dash.searchReasoning":`Search reasoning effort`,"dash.visionModel":`Vision sidecar model`,"dash.visionModelHint":`Model used to describe images for text-only routed models. Requires ChatGPT login.`,"dash.webSearchSidecar":`Web search sidecar`,"dash.webSearchSidecarHint":`Choose the backend and model used for web search on routed models.`,"dash.webSearchStream":`Stream answers live`,"dash.webSearchStreamHint":`Stream the model’s leading text and reasoning live until it decides on a tool call; the rest of the turn stays buffered for search interception. Text written before a search may partially repeat.`,"dash.visionSidecar":`Vision sidecar`,"dash.visionSidecarHint":`Choose the backend and model used to describe images for text-only routed models.`,"dash.visionOff":`Off`,"dash.visionAdvanced":`Advanced settings`,"dash.visionMaxDescriptions":`Maximum descriptions per turn`,"dash.visionMaxDescriptionsInvalid":`Enter a positive integer.`,"dash.visionTimeout":`Timeout`,"dash.visionTimeoutInvalid":`Enter an integer from {min} to {max} milliseconds.`,"dash.visionAdvancedPopover":`Advanced vision settings`,"dash.shadowCallIntercept":`Shadow Call Intercept`,"dash.shadowCallInterceptHint":`Intercepts Codex App's background helper calls ({models}) for title generation and commit messages and redirects them to your chosen model.`,"dash.shadowCallWarning":`⚠ When enabled, ALL requests for {models} will be replaced with the selected model.`,"dash.shadowCallOriginal":`Original`,"dash.shadowCallModel":`Replacement model`,"dash.shadowCallTooltip":`Codex App makes background helper calls for thread title generation, commit message generation, and skill orchestration. The helper model changed across client versions, so opencodex intercepts every model in this set: {models}. Enable this to redirect those calls to your chosen model.`,"models.shadowCallIntercept":`Shadow Call Intercept`,"models.shadowCallInterceptHint":`Intercepts Codex App's background helper calls ({models}) for titles and commit messages and redirects them to your chosen model.`,"dash.sidecarBackend":`Backend`,"dash.sidecarModel":`Model`,"dash.backendAuto":`Auto`,"dash.backendOpenAI":`OpenAI`,"dash.backendAnthropic":`Anthropic`,"dash.sidecarSaved":`Sidecar settings saved. Applied on the next request.`,"dash.sidecarSaveFailed":`Failed to save sidecar settings.`,"dash.injectionLabel":`Sub-agent delegation`,"dash.injectionHint":`Pick the model Codex should hand sub-agent work to. The two switches below decide where that pick is used.`,"dash.injectionManage":`Open settings`,"dash.syncCodexSubagentDefaults":`Also save as a Codex default`,"dash.syncCodexSubagentDefaultsHint":`On, the pick above is written into Codex's own config, so new tasks start with that model too. Off, it is remembered only here. It takes effect on the next sync or restart, and your hand-written [agents] settings are left alone.`,"dash.multiAgentGuidance":`Tell Codex how to split work`,"dash.multiAgentGuidanceHint":`Sends a short note telling Codex how to hand work to sub-agents. On v2 it names the models it may use and which to prefer; on v1 it only applies at max or ultra reasoning effort. Off, no note is added.`,"dash.injectionNone":`None`,"dash.injectionEffortLabel":`Reasoning effort`,"dash.injectionEffortNone":`Model default`,"dash.effortCapLabel":`V2 ultra effort limit`,"dash.subagentEffortCapLabel":`V2 sub-agent effort limit`,"dash.effortCapHelp":`Limits the reasoning effort for V2 ultra-mode turns. When set, incoming max-effort requests (from ultra mode) are capped to the selected level. The sub-agent limit applies only to spawned child agents. Caps only lower effort, never raise it. If a model doesn't support the capped level, it snaps down to the nearest supported level.`,"dash.effortCapNone":`No cap`,"dash.maintenance":`Maintenance`,"dash.maintenanceHint":`Refresh Codex's model catalog or install a newer opencodex release.`,"dash.syncModels":`Sync models`,"dash.syncModelsHint":`Rewrite Codex's model catalog from the providers you have connected.`,"dash.syncRun":`Sync now`,"dash.syncing":`Syncing…`,"dash.syncOk":`Sync complete. {count} model(s) appended.`,"dash.syncStaleHint":`If Codex still shows an older list, restart its long-lived app-server ({cmd}).`,"dash.syncFailed":`Sync failed: {error}`,"dash.projectConfigTitle":`Project Codex config bypasses OpenCodex`,"dash.projectConfigHint":`These repo-local settings override the OpenCodex proxy (e.g. route to OpenCode Go directly). Remove them so ~/.codex/config.toml routing applies in that project.`,"dash.checkUpdate":`Check update`,"dash.updateTitle":`Update opencodex`,"dash.updateDesc":`Check npm for the selected channel, then choose whether to restart the proxy after installation.`,"dash.updateChannel":`Channel`,"dash.updateChecking":`Checking for updates…`,"dash.updateInstalled":`Installed`,"dash.updateLatest":`Latest`,"dash.updateAvailable":`Update available`,"dash.updateCurrent":`Up to date`,"dash.updateCommand":`Command`,"dash.updateSource":`This is a source checkout. Update it from the terminal with the shown command.`,"dash.updateUnavailable":`Could not read the latest version from npm. Try again later.`,"dash.updateRetry":`Retry`,"dash.updateRecheck":`Re-check`,"dash.updateCannotAuto":`One-click update is unavailable ({reason}).`,"dash.updateReason.source_checkout":`source checkout`,"dash.updateReason.latest_unavailable":`npm registry unreachable`,"dash.updateReason.already_latest":`already on latest`,"dash.updateReason.unknown":`update unavailable`,"dash.updateRestart":`Restart after update`,"dash.updateRestartHint":`Recommended. The current GUI keeps running the old code until the proxy restarts.`,"dash.runUpdate":`Update`,"dash.updateReconnecting":`Waiting for the restarted proxy…`,"dash.updateStatus.running":`Updating opencodex.`,"dash.updateStatus.restarting":`Update installed. Restarting proxy.`,"dash.updateStatus.succeeded":`Update finished.`,"dash.updateVersionTransition":`{currentVersion} -> {latestVersion}.`,"dash.updateStatus.failed":`Update failed.`,"prov.subtitle":`Configure the upstream providers opencodex routes into Codex. Log in with an account, add a provider, or edit the raw config.`,"prov.add":`Add Provider`,"prov.editJson":`Edit JSON`,"prov.accountLogin":`Account login`,"prov.noOauth":`No OAuth providers available.`,"prov.loggedIn":`logged in`,"prov.notLoggedIn":`not logged in`,"prov.logout":`Logout`,"prov.login":`Login`,"prov.loginWith":`Login with {provider}`,"prov.waitingBrowser":`Waiting for browser…`,"prov.didntOpen":`Didn't open? Click here`,"prov.copyLink":`Copy link`,"prov.dontOpenBrowser":`Don't open a browser on the proxy machine`,"prov.dontOpenBrowserHint":`Useful for a different browser profile, or when the dashboard is not on the proxy's machine.`,"prov.linkCopied":`Copied`,"prov.linkCopyUnavailable":`Clipboard unavailable`,"prov.deviceCode":`Device code`,"prov.copyCode":`Copy code`,"prov.codeCopied":`Code copied`,"prov.editAlias":`Edit alias`,"prov.aliasPrompt":`Display name (leave empty to clear)`,"prov.aliasSaved":`Alias saved`,"prov.aliasSaveFailed":`Could not save alias`,"prov.accountId":`ID`,"prov.pasteRedirect":`Paste redirect URL or code`,"prov.pasteRedirectHint":`If the browser shows a localhost error, copy the full URL from its address bar and paste it here (or paste the authorization code).`,"prov.pasteSubmit":`Submit`,"prov.pasteSubmitting":`Submitting…`,"prov.pasteOk":`Code submitted — finishing login…`,"prov.pasteFail":`Could not submit code: {error}`,"prov.port":`Port`,"prov.default":`Default`,"prov.loadingConfig":`Loading…`,"prov.saved":`Saved! Restart proxy to apply.`,"prov.loadConfigFail":`Failed to load config`,"prov.invalidJson":`Invalid JSON`,"prov.saveFailed":`Save failed`,"prov.loginFailStart":`{provider} login failed to start`,"prov.loginError":`{provider} login error: {error}`,"prov.loginRequestFail":`{provider} login request failed`,"prov.loginCancelled":`{provider} login cancelled`,"prov.loginTimeout":`{provider} login timed out — browser closed or never finished. Try again.`,"prov.loginOk":`Logged in to {provider}. Run {cmd} (or it applies live) to list its models.`,"prov.loginSameAccount":`Still the same {provider} account — switch accounts in the browser, then try Add account again.`,"oauthTos.highTitle":`{provider}: subscription OAuth risk`,"oauthTos.elevatedTitle":`{provider}: unofficial OAuth bridge`,"oauthTos.anthropicBody":`Directly reusing Claude subscription OAuth tokens through a third-party proxy such as OpenCodex is not a supported Anthropic integration and may lead to access restrictions. Supported Agent SDK integrations that use Claude subscriptions are separate.`,"oauthTos.highBody":`OpenCodex connects {provider} through a third-party OAuth path. Unsupported use may lead to access limits or suspension.`,"oauthTos.elevatedBody":`OpenCodex connects {provider} through an unofficial OAuth path. Use the official client when possible; unusual or automated traffic may be treated as abuse and access may be limited or suspended.`,"oauthTos.saferPath":`Safer option: configure an API key in OpenCodex instead.`,"oauthTos.acknowledge":`I understand the risk and want to continue with OAuth anyway.`,"oauthTos.continue":`Continue with OAuth`,"prov.logoutOk":`Logged out of {provider}.`,"prov.logoutFail":`Could not log out of {provider}. Your account state is unchanged.`,"prov.removed":`Removed "{name}".`,"prov.removedDefault":`Removed "{name}". Default provider is now "{defaultProvider}".`,"prov.removeFail":`Failed to remove "{name}".`,"prov.removeLastProvider":`You can't remove this provider when no other enabled provider can become the default.`,"prov.removeHasDependentCombos":`Remove or update these dependent combos first: {combos}.`,"prov.setDefault":`Set as default`,"prov.setDefaultSuccess":`"{name}" is now the default provider.`,"prov.setDefaultFail":`Couldn't set "{name}" as the default provider.`,"prov.defaultDisabled":`Enable this provider before making it the default.`,"prov.updateFail":`Couldn't update this provider.`,"prov.networkError":`Network error. Check that the proxy is running and try again.`,"prov.added":`Added "{name}". Live now — run {cmd} (or restart) to list its models in Codex's picker.`,"prov.removeConfirm":`Remove provider "{name}"? Its models disappear from Codex's picker.`,"prov.hasApiKey":`api key configured`,"prov.hasHeaders":`custom headers configured`,"prov.accounts":`Accounts ({n})`,"prov.accountsAria":`Toggle {name} accounts`,"prov.accountActive":`Active`,"prov.accountReauth":`Re-login`,"prov.reauthenticate":`Re-authenticate`,"prov.reauthAccountMissing":`Selected account was not found after login`,"prov.reauthIdentityMismatch":`Signed-in account did not match the selected account`,"prov.accountAdd":`Add account`,"prov.accountNoLabel":`account {id}`,"prov.accountSwitchTitle":`Use this account`,"prov.accountSwitched":`Switched to {email}.`,"prov.accountSwitchFail":`Failed to switch account`,"prov.accountRemoved":`Removed {email}.`,"prov.accountRemoveFail":`Could not remove {email}. The account is unchanged.`,"prov.accountRemoveAria":`Remove {email}`,"prov.accountRemoveConfirm":`Remove account {email}? Its login is deleted from this proxy.`,"prov.keyAdd":`Add API key`,"prov.keyAdded":`Added API key to {name}.`,"prov.keyAddFail":`Failed to add API key`,"prov.keyPlaceholder":`Paste API key`,"prov.keySwitchTitle":`Use this key`,"prov.keySwitched":`Switched to key {key}.`,"prov.keySwitchFail":`Failed to switch key`,"prov.keyRemoved":`Removed key {key}.`,"prov.keyRemoveAria":`Remove key {key}`,"prov.keyRemoveConfirm":`Remove API key {key}? It is deleted from this proxy's config.`,"prov.activeBadge":`Active`,"prov.disabledBadge":`Disabled`,"prov.defaultBadge":`Default`,"prov.enable":`Enable`,"prov.disable":`Disable`,"prov.enabled":`Enabled "{name}". Its models can appear in Codex again.`,"prov.disabled":`Disabled "{name}". Settings are kept, but its models are hidden.`,"prov.enableFail":`Failed to enable "{name}".`,"prov.disableFail":`Failed to disable "{name}".`,"prov.enableAria":`Enable provider {name}`,"prov.disableAria":`Disable provider {name}`,"prov.defaultCannotDisable":`Default provider can't be disabled`,"prov.openaiAccountMode":`Codex account mode`,"prov.openaiModePool":`Pool`,"prov.openaiModeDirect":`Direct`,"prov.openaiPoolDesc":`Default. Rotate the main login and added accounts using affinity, quota, cooldown, and failover.`,"prov.openaiDirectDesc":`Use only the current/main Codex login. Stored pool accounts are not read or rotated.`,"prov.openaiModeSaved":`OpenAI account mode changed to {mode}.`,"prov.openaiModeSaveFailed":`Could not change the OpenAI account mode.`,"prov.openaiApiDesc":`Uses an OpenAI API key and never uses Codex account credentials.`,"prov.manageCodexAccounts":`Manage Codex accounts`,"prov.openaiApiMissing":`API key required`,"prov.openaiApiSetup":`Set up API key`,"models.tab.catalog":`Models`,"models.tab.combos":`Combos`,"models.tab.compatibility":`Compatibility`,"models.tab.routing":`Routing (beta)`,"models.tabsLabel":`Model surfaces`,"models.subtitle.combos":`Ordered groups of models that answer as one id. Chain targets with failover or spread the load with a balancing strategy.`,"models.subtitle.compatibility":`Read-only compatibility verdict matrix from lab projection evidence.`,"models.subtitle.routing":`Policy profiles, dry-run evaluation, and source-backed routing analytics.`,"models.subtitle":`Toggle which models Codex sees — native GPT passthrough and routed providers, grouped by provider (click a header to collapse). Hidden models stay off the catalog + model picker but remain directly callable by exact id. Changes apply on the next Codex turn — opencodex invalidates Codex's 5-min model cache so no restart is needed.`,"models.nativeGroupLabel":`OpenAI native`,"models.nativeHint":"Passthrough models use the Pool or Direct account option selected on Providers. Toggling one off hides it from the Codex picker (the catalog entry is kept, so re-enabling restores it exactly). Adding a model here registers a routed `openai/` selector, not a new bare passthrough id.","models.active":`{active}/{total} visible`,"models.workspace.providers":`Providers`,"models.workspace.allProviders":`All providers`,"models.workspace.mainAria":`Model details`,"models.allOn":`All on`,"models.allOff":`All off`,"models.presetLabel":`Models`,"models.presetMode_preset":`Preset`,"models.presetMode_all":`All`,"models.presetMode_custom":`Custom`,"models.presetSummary":`{count} of {total} shown — core preset v{version}`,"models.presetUpdateAvailable":`Preset v{version} available`,"models.presetAppliedToast":`{provider}: preset applied — {count} models selected`,"models.presetClearedToast":`{provider}: showing all models`,"models.presetEmpty":`{provider}: preset matched no models — selection unchanged`,"models.presetConfirmReplace":`Replace your selection with the {count}-model preset?`,"models.cap350k":`Cap 350k`,"models.capApplied":`Context cap applied — takes effect on the next Codex turn.`,"models.capSaveFailed":`Failed to save context cap`,"models.contextCapped":`350k cap`,"models.contextCapLabel":`Default window / cap`,"models.v2Label":`Sub-agent`,"models.shadowCallOriginal":`⚠ {models} →`,"models.v2DocsLink":`What is v1 / v2?`,"models.v2Mode_v1":`v1`,"models.v2Mode_default":`base`,"models.v2Mode_v2":`v2`,"models.v2ModeDesc_v1":`All models → v1 surface`,"models.v2ModeDesc_default":`Upstream defaults (sol/terra=v2, luna=v1)`,"models.v2ModeDesc_v2":`All models → v2 surface`,"models.keepNativeOnV1":`Keep ChatGPT on v1`,"models.keepNativeOnV1Hint":`ChatGPT encrypts v2 child tasks only when a ChatGPT-native parent stays on v2, so Grok and Claude cannot read them. Turn this on to keep Sol/Terra on v1 and avoid that encryption. Routed parents keep v2.`,"models.v2Help":`Controls the multi-agent surface for all models. + +v1: Classic single-thread agent. Every model uses the v1 collab surface. +base: Upstream defaults — sol/terra use v2, luna uses v1, others follow the codex feature flag. +v2: Multi-thread agent with spawn_agent. Every model uses the v2 collab surface. + +On v2, Keep ChatGPT on v1 leaves Sol/Terra on the v1 surface so they can still spawn Grok or Claude. ChatGPT encrypts v2 child tasks; routed models cannot read them. Routed parents stay on v2. + +Changes apply to new sessions.`,"dash.multiAgent":`Sub-agent`,"models.v2Conflict":`[agents] max_threads is set — codex will refuse to start; remove it from config.toml`,"models.v2Applied":`Sub-agent mode updated — applies to new sessions (restart the Codex app to refresh the picker)`,"models.v2ThreadsLabel":`Max threads`,"models.v2ThreadsDefault":`default (4)`,"models.v2ThreadsApplied":`Thread limit updated — applies to new sessions`,"models.v2ThreadsInvalid":`Thread limit must be an integer >= 1`,"models.v2ThreadsApply":`Apply`,"models.capValue":`Default {value}`,"models.contextSettings":`Custom windows`,"models.contextSettingsTitle":`Custom windows — {provider}`,"models.contextDefault":`Provider default`,"models.contextModel":`Model`,"models.contextModelOverride":`Model override`,"models.contextHint":`Write the actual Codex window here when you already know it. This fills a missing upstream window and only lowers a larger reported one. Leave blank to use the provider Default window / cap, or 128k if that cap is off.`,"models.contextAutomatic":`Automatic discovery`,"models.contextSaved":`Context windows updated — takes effect on the next Codex turn.`,"models.contextUnchanged":`No context window changes to save.`,"models.contextSaveFailed":`Failed to save context windows`,"models.contextInvalid":`Context windows must be positive whole numbers`,"models.contextCappedValue":`{value} cap`,"models.setAll":`Set all`,"models.setAllHint":`Turn on the {value} default window for every routed provider. Relays that omit context_window / context_length get this as the actual Codex window. Use Custom windows on a provider row to set one model by hand. Native providers are unaffected.`,"models.collapseAll":`Collapse all`,"models.expandAll":`Expand all`,"models.orderHint":`Picker order: Subagents picks (in the selected order) → remaining routed models alphabetically by provider, then model ID → native models. Visibility switches only filter models; they do not change this order.`,"models.custom":`Custom…`,"models.customApply":`Apply`,"models.customPlaceholder":`Tokens (e.g. 420000)`,"models.customAdd":`Add custom model`,"models.customAddTitle":`Add custom model — {provider}`,"models.customEditTitle":`Edit custom model — {provider}`,"models.customAdded":`Custom model added`,"models.customUpdated":`Custom model updated`,"models.customDeleted":`Custom model deleted`,"models.customSaveFailed":`Failed to save custom model`,"models.customSaving":`Saving…`,"models.customAddBtn":`Add`,"models.customEditBtn":`Update`,"models.customEdit":`Edit`,"models.customDelete":`Delete`,"models.customDeleteConfirm":`Delete the {name} model?`,"models.customBadge":`Custom`,"models.customSummary":`{count} custom`,"models.customFieldModelId":`Model ID (endpoint slug)`,"models.customFieldModelIdPlaceholder":`e.g. qwen4-max-preview`,"models.customFieldDisplayName":`Display name (optional)`,"models.customFieldDisplayNamePlaceholder":`e.g. Qwen 4 Max Preview`,"models.customFieldContext":`Context window`,"models.customFieldModalities":`Input modalities`,"models.customFieldReasoning":`Reasoning effort`,"models.customFieldReasoningOverride":`Override reasoning effort`,"models.reasoningEffort.none":`None`,"models.reasoningEffort.minimal":`Minimal`,"models.reasoningEffort.low":`Low`,"models.reasoningEffort.medium":`Medium`,"models.reasoningEffort.high":`High`,"models.reasoningEffort.xhigh":`Extra high`,"models.reasoningEffort.max":`Maximum`,"models.tipProvider":`Provider`,"models.tipContext":`Context`,"models.tipModalities":`Modalities`,"models.tipStatus":`Status`,"models.tipActive":`Active`,"models.tipDisabled":`Disabled`,"models.applied":`Applied — takes effect on the next Codex turn.`,"models.saveFailed":`Save failed`,"models.networkError":`Network error — is the proxy running?`,"models.loadFail":`Failed to load models — is the proxy running?`,"models.noRouted":`No routed models`,"models.noRoutedHint":`Log into a provider or add one first.`,"models.emptyDiscovery":`No models were discovered. Check the provider endpoint or add a static/custom model.`,"models.emptyDiscoveryDisabled":`Live model discovery is off and no static models are configured.`,"models.discoveryFailedBadge":`Discovery failed`,"models.discoveryFailedHttp":`Model discovery failed (HTTP {status}).`,"models.discoveryFailedBlocked":`Model discovery was blocked by the destination policy.`,"models.discoveryFailedInvalidResponse":`Model discovery returned an invalid response.`,"models.discoveryFailedNetwork":`Model discovery failed due to a network error.`,"models.discoveryFailedProvider":`The provider reported a model discovery error.`,"models.discoveryFailedGeneric":`Model discovery failed.`,"models.openProviderSettings":`Open provider settings`,"models.loading":`Loading…`,"models.search":`Search models…`,"models.showMore":`Show {n} more`,"models.allowlistLabel":`Only selected`,"models.allowlistHint":`Only checked models ship to the catalog (empty = all). Useful for providers exposing thousands of models.`,"models.selectedCount":`{n} selected`,"sub.subtitle":`Codex's {cmd} advertises only the first 5 models (by priority) as overrides. Pick up to 5 here — native gpt or routed — and opencodex sets their catalog priority so exactly these lead. Any other model is still callable by its exact name; this only controls what's shown.`,"sub.featured":`Featured`,"sub.advanced":`Advanced`,"sub.orderHintAria":`How this order is used`,"sub.orderHint":`The order shown here sets positions 1–5 at the top of the Codex model picker and the default model candidates for {cmd}.`,"sub.noneSelected":`None selected — pick from the list below.`,"sub.models":`Models`,"sub.search":`Search models (native gpt + routed)…`,"sub.settings":`Settings`,"sub.sections":`Subagent sections`,"sub.delegation.model":`Model to call first`,"sub.delegation.modelHint":`The model Codex reaches for first when it hands off work. Featured above is the list it may call; this is the one it calls first.`,"sub.noModels":`No models — log into a provider or add one first.`,"sub.saved":`Saved {n} models. Start a new Codex session (or run {cmd}) to see them as spawn_agent overrides.`,"sub.saveFailed":`Save failed`,"sub.networkError":`Network error — is the proxy running?`,"sub.loadFail":`Failed to load models — is the proxy running?`,"sub.loading":`Loading…`,"sub.moveUp":`Move {m} up`,"sub.moveDown":`Move {m} down`,"sub.removeAria":`Remove {m}`,"sub.workspace.addToFeatured":`Add {m} to featured`,"sub.workspace.allModels":`All models`,"sub.workspace.featuredFull":`Featured list is full (max 5)`,"sub.workspace.mainAria":`Subagent model details`,"sub.workspace.notFeatured":`Not featured`,"sub.workspace.priority":`Priority`,"sub.ultraMode":`Ultra mode`,"sub.ultraModeHint":`Enable the Proactive multi-agent delegation policy for every model and reasoning effort (does not change reasoning effort itself). Writes features.multi_agent_v2.multi_agent_mode_hint_text in config.toml.`,"sub.ultraModeV2Required":`Requires the v2 multi-agent surface — enable multi_agent_v2 and select v2 in the Sub-agent mode control first.`,"sub.ultraModeText":`Ultra mode delegation text`,"sub.ultraModePreset":`Restore preset`,"sub.ultraModeLoadFail":`Failed to load Ultra mode settings — is the proxy running?`,"sub.ultraModeSaveFail":`Failed to save Ultra mode settings`,"sub.ultraModeSaved":`Ultra mode saved. Applies to new Codex sessions.`,"sub.workspace.removeFromFeatured":`Remove {m} from featured`,"sub.workspace.selectModel":`Select a model`,"sub.workspace.selectModelDesc":`Pick a model from the list to see details and feature it for spawn_agent.`,"sub.workspace.selector":`Public selector`,"logs.title":`Request Logs`,"logs.tabLogs":`Logs`,"logs.tabDebug":`Debug`,"logs.subtitle":`Recent requests routed through the local opencodex proxy, newest first.`,"logs.autoRefresh":`Auto-refresh`,"logs.noRequests":`No requests yet.`,"logs.loadError":`Could not load request logs.`,"logs.filter.surface.label":`Surface`,"logs.filter.surface.all":`All`,"logs.filter.surface.claude":`Claude`,"logs.filter.surface.codex":`Codex`,"logs.filter.surface.grok":`Grok`,"logs.filter.interceptedHelpersOnly":`Intercepted helpers only`,"logs.badge.interceptedHelper":`I · {model}`,"logs.badge.interceptedHelperTitle":`Intercepted helper request`,"logs.filter.conversation.label":`Conversation`,"logs.filter.conversation.placeholder":`Paste conversation id`,"logs.filter.conversation.clear":`Clear`,"logs.filter.model.label":`Model`,"logs.filter.model.placeholder":`Filter by model or provider`,"logs.filter.conversation.apply":`Filter logs`,"logs.conversation.totals":`{requests} requests · {tokens} tokens · {cost}`,"logs.conversation.scope":`Totals cover the currently loaded Logs ring only.`,"logs.conversation.excluded":`({unpriced} unpriced, {unmetered} unmetered excluded from ~$)`,"logs.cost.approximate":`{amount}`,"logs.cost.lowerBound":`≥{amount}`,"logs.cost.unavailable":`—`,"logs.detail.conversation":`Conversation`,"logs.badge.claude":`Claude`,"logs.badge.grok":`Grok`,"logs.col.time":`Time`,"logs.col.request":`Request`,"logs.col.model":`Model`,"logs.col.effort":`Effort`,"logs.col.provider":`Provider`,"logs.col.status":`Status`,"logs.col.tokens":`Tokens`,"logs.col.tokPerSec":`tok/s`,"logs.col.estimatedCost":`~$`,"logs.metric.tokPerSecTitle":`Output tokens per second over the full request duration`,"logs.metric.estimatedCostTitle":`API list-price equivalent, not an actual charge; unmatched pricing is unavailable`,"usage.cost.total":`API list-price equivalent (this range)`,"usage.cost.disclaimer":`Not a billing receipt. Subscription usage or provider credits may apply instead.`,"usage.cost.unpricedNote":`{count} requests excluded (no price or usage)`,"logs.detail.section.basic":`Basic information`,"logs.detail.route.section":`Route decision`,"logs.detail.route.kind":`Route kind`,"logs.detail.route.profile":`Profile`,"logs.detail.route.selected":`Selected`,"logs.detail.route.candidates":`Candidates`,"logs.detail.route.unknown":`No route trace recorded for this request (pre-trace row).`,"logs.detail.section.performance":`Performance`,"logs.detail.section.cost":`API list-price equivalent`,"logs.detail.section.attempts":`Combo attempts`,"logs.detail.section.usage":`Raw usage`,"logs.detail.ttft":`TTFT`,"logs.detail.costTotal":`List-price equivalent`,"logs.detail.totalTokens":`Total tokens`,"logs.detail.matchedKey":`Matched price key`,"logs.detail.priceSource":`Price source`,"logs.detail.unavailableReason":`Unavailable reason`,"logs.detail.copyRequestId":`Copy request ID`,"logs.detail.copied":`Copied`,"logs.detail.source.jawcode":`jawcode catalog`,"logs.detail.source.expected":`Expected price overlay`,"logs.detail.source.user":`Provider-configured price overlay`,"logs.detail.verification.verified":`Verified`,"logs.detail.verification.derived":`Derived from base model`,"logs.detail.attempt.target":`Provider / model`,"logs.detail.attempt.reason":`Result / reason`,"logs.detail.attempt.completed":`Completed`,"logs.detail.attempt.e2eNote":`Top-level tok/s is end-to-end; each attempt uses its own duration.`,"logs.detail.attempt.recovery.transient5xx":`Transient 5xx`,"logs.detail.attempt.recovery.connectionReset":`Connection reset`,"logs.detail.attempt.recovery.oauth401":`OAuth re-authentication`,"logs.detail.attempt.recovery.key429":`Key rate-limited (429)`,"logs.detail.attempt.recovery.rateLimit429":`Rate-limited (429)`,"logs.detail.attempt.recovery.anthropicOauth429":`Anthropic OAuth rate-limited (429)`,"logs.detail.attempt.recovery.image413":`Image payload too large (413)`,"logs.detail.attempt.recovery.emptyCompletion":`Empty completion retry`,"logs.detail.attempt.recovery.unknown":`Unknown recovery reason`,"logs.detail.reason.usage_missing":`Usage was not reported.`,"logs.detail.reason.usage_unsupported":`This provider does not report usage.`,"logs.detail.reason.output_missing":`No positive output token count was reported.`,"logs.detail.reason.invalid_duration":`The request duration is not valid.`,"logs.detail.reason.price_unmatched":`No matching price was found.`,"logs.detail.reason.invalid_cache_breakdown":`Cache token details conflict with total input tokens.`,"logs.detail.reason.invalid_usage":`Usage contains an invalid token value.`,"logs.detail.reason.combo_attempt_unavailable":`At least one combo attempt could not be priced.`,"logs.detail.estimate.usage_estimated":`Provider usage is estimated.`,"logs.detail.estimate.cache_detail_missing":`Cache details were unavailable; input is an upper-bound estimate.`,"logs.detail.estimate.expected_price_overlay":`A verified expected list price was used.`,"logs.detail.estimate.provider_cost_overlay":`A provider-configured price overlay was used.`,"logs.detail.estimate.priority_lower_bound":`The confirmed Priority price is unavailable; the displayed estimate is a known lower bound.`,"logs.col.error":`Error`,"logs.col.upstreamReason":`Upstream reason`,"logs.col.duration":`Duration`,"logs.modelTooltip.model":`model`,"logs.modelTooltip.resolvedModel":`resolved model`,"logs.modelTooltip.requestedTier":`requested tier`,"logs.modelTooltip.configuredTier":`configured tier`,"logs.modelTooltip.responseTier":`response tier`,"logs.modelTooltip.supportsTier":`tier support`,"logs.tokens.reported":`reported`,"logs.tokens.unreported":`unreported`,"logs.tokens.unsupported":`unsupported`,"logs.tokens.estimated":`estimated`,"logs.tokens.input":`input`,"logs.tokens.output":`output`,"logs.tokens.cacheRead":`cache read (c)`,"logs.tokens.cacheWrite":`cache write (w)`,"logs.tokens.reasoning":`reasoning`,"logs.tokens.noCache":`no cache data`,"logs.tokens.contextTotal":`active context`,"logs.tokens.noCacheNote":`this provider does not report cache tokens`,"logs.tokens.noCacheCursor":`Cursor cache detail unreported`,"logs.tokens.noCacheCursorNote":`Cursor does not expose cache read/write token counts; this is unknown, not a confirmed cache miss`,"logs.tokens.estimatedNote":`estimated (provider reports no exact usage)`,"logs.details":`Details`,"logs.detailTitle":`Request details`,"logs.detailRaw":`Raw log entry`,"debug.title":`Debug`,"debug.subtitle":`Opt-in provider transport and usage-extraction diagnostics. Request errors and 502s stay on the Logs tab.`,"debug.debug":`Provider debug`,"debug.usage":`Usage extraction`,"debug.injection":`Injection log`,"debug.claude":`Claude inbound`,"debug.claudeInbound.title":`Claude inbound requests`,"debug.claudeInbound.sub":`What Claude Code/Desktop actually sends (thinking, effort, metadata) — no prompt text is stored.`,"debug.claudeInbound.empty":`No requests captured yet. Send a message from Claude while this is on.`,"debug.claudeInbound.time":`Time`,"debug.claudeInbound.endpoint":`Endpoint`,"debug.claudeInbound.model":`Model`,"debug.claudeInbound.none":`none`,"debug.reset":`Clear runtime overrides`,"debug.refresh":`Refresh`,"debug.follow":`Follow`,"debug.streamProvider":`Provider`,"debug.streamUsage":`Usage`,"debug.streamInjection":`Injection`,"debug.loading":`Loading debug settings…`,"debug.loadFailed":`Could not load debug settings.`,"debug.emptyTitle":`Debug logging is off`,"debug.empty":`Turn on Provider debug or Usage extraction in the card above. Lines appear here after you send a request through the proxy.`,"debug.noLinesTitle":`Waiting for lines`,"debug.noLines.provider":`Provider debug is on, but it only records transport anomalies (dropped or malformed frames, and Cursor dial/retry events). A clean request through a provider like Anthropic can produce no lines.`,"debug.noLines.usage":`Usage extraction is on but nothing has been captured yet. Send a chat/request through Codex and it appears here.`,"debug.noLines.injection":`Injection log is on but nothing has been captured yet. It records multi-agent guidance injection and effort-cap decisions on collab and sub-agent turns.`,"usage.title":`Usage`,"usage.subtitle":`Local token accounting from your proxy. Missing usage is never shown as zero.`,"usage.loading":`Loading usage data…`,"usage.empty":`No usage recorded yet. Send a request through the proxy to see activity here.`,"usage.loadError":`Could not load usage data.`,"usage.range.all":`All`,"usage.range.available":`Available history`,"usage.historyTruncated":`Totals cover available history only because older usage was not loaded.`,"usage.historyTruncatedWindow":`Loaded rows have request start times ranging from {start} to {end}. Earlier file entries were omitted by the read limit, so any selected range may be incomplete.`,"usage.range.30d":`30d`,"usage.range.7d":`7d`,"usage.card.requests":`Requests`,"usage.card.measured":`Measured`,"usage.card.reported":`Reported`,"usage.card.totalTokens":`Total tokens`,"usage.card.cachedTokens":`Cache reads`,"usage.card.cachedTokensHint":`Prompt tokens served from the provider cache (reads). Cache writes are shown below when present.`,"usage.card.cacheWriteTokens":`cache writes`,"usage.card.coverage":`Coverage`,"usage.card.activeDays":`Active days`,"usage.section.heatmap":`Daily activity`,"usage.section.overview":`Overview`,"usage.section.models":`Models`,"usage.section.providers":`Providers`,"usage.section.coverage":`Coverage breakdown`,"usage.workspace.report":`Usage report`,"usage.workspace.sections":`Usage sections`,"usage.coverage.measured":`Measured`,"usage.coverage.reported":`Provider reported`,"usage.coverage.estimated":`Estimated`,"usage.coverage.note":`Measured entries include provider-reported and estimated token counts. Unreported and unsupported requests are tracked but never inflated to zero tokens.`,"usage.search.models":`Search models…`,"usage.col.requests":`Requests`,"usage.col.measured":`Measured`,"usage.col.reported":`Reported`,"usage.col.tokens":`Tokens`,"usage.col.share":`Share`,"usage.heatmap.less":`Less`,"usage.heatmap.more":`More`,"usage.dayMon":`Mon`,"usage.dayWed":`Wed`,"usage.dayFri":`Fri`,"usage.heatmap.tooltipTokens":`{tokens} tokens`,"usage.heatmap.tooltipRequests":`{requests} requests`,"nav.storage":`Storage`,"storage.title":`Storage`,"storage.subtitle":`See what’s using CODEX_HOME. Cleanup never touches active sessions.`,"storage.loading":`Scanning storage…`,"storage.empty":`CODEX_HOME is empty or missing — nothing to report.`,"storage.error":`Storage scan failed. Check that CODEX_HOME points at a valid directory.`,"storage.refresh":`Rescan`,"storage.rescanned":`Scan complete.`,"storage.card.total":`Total size`,"storage.card.files":`Files`,"storage.card.home":`CODEX_HOME`,"storage.snapshot.lastScan":`Last scan`,"storage.snapshot.scanning":`Scanning…`,"storage.snapshot.unavailable":`No scan yet.`,"storage.cleanupCard.title":`Free up space`,"storage.cleanupCard.tabs":`Cleanup options`,"storage.cleanupCard.tab.policy":`Policy`,"storage.cleanupCard.tab.quarantine":`Quarantine`,"storage.cleanup.noArchives":`No archived sessions to clean up.`,"storage.section.buckets":`Buckets`,"storage.section.largest":`Largest files`,"storage.workspace.overview":`Overview`,"storage.workspace.selectBucket":`Select a bucket from the list to see its breakdown.`,"storage.col.bucket":`Bucket`,"storage.col.size":`Size`,"storage.col.files":`Files`,"storage.col.oldest":`Oldest`,"storage.col.newest":`Newest`,"storage.col.rows":`DB rows`,"storage.rows.unknown":`unknown (locked)`,"storage.bucket.sessions":`Active sessions`,"storage.bucket.archived_sessions":`Archived sessions`,"storage.bucket.logs_db":`Logs database`,"storage.bucket.state_db":`State database`,"storage.bucket.attachments":`Attachments`,"storage.bucket.deletion_manifests":`Deletion manifests`,"storage.bucket.other":`Other`,"storage.cleanup.title":`Archived cleanup`,"storage.cleanup.help":`Remove the oldest archived sessions by percentage. Active sessions are never touched. Quarantine is the default — files move to CODEX_HOME/.trash.`,"storage.cleanup.slider":`Oldest archived percent`,"storage.cleanup.percent":`{percent}%`,"storage.cleanup.preset":`{percent}`,"storage.cleanup.preview":`Preview`,"storage.cleanup.confirmTitle":`Confirm archived cleanup`,"storage.cleanup.confirmBody":`This will process {count} archived file(s) (~{size}), the oldest {percent}%.`,"storage.cleanup.moreFiles":`…and {n} more`,"storage.cleanup.permanent":`Delete permanently (skip quarantine)`,"storage.cleanup.permanentWarn":`Permanent delete cannot be undone.`,"storage.cleanup.quarantineNote":`Files move to .trash under CODEX_HOME. You can restore them from the Quarantine tab.`,"storage.cleanup.cancel":`Cancel`,"storage.cleanup.confirmQuarantine":`Quarantine`,"storage.cleanup.confirmPermanent":`Delete permanently`,"storage.cleanup.doneQuarantine":`Quarantined {count} file(s) ({size}).`,"storage.cleanup.donePermanent":`Permanently deleted {count} file(s) ({size}).`,"storage.cleanup.previewFailed":`Preview failed.`,"storage.cleanup.cleanupFailed":`Cleanup failed.`,"storage.cleanup.err.codex_busy":`Codex is using state.sqlite — try again after quitting Codex.`,"storage.cleanup.err.stale_preview":`Archived files changed since preview — run Preview again.`,"storage.cleanup.err.restore_pending_overlap":`Selected archives overlap an incomplete trash restore — finish or retry restore first.`,"storage.cleanup.err.referenced_history":`Selected archives are still referenced by forked or paginated history.`,"storage.cleanup.err.invalid_digest":`Preview digest is missing or invalid.`,"storage.cleanup.err.invalid_mode":`Cleanup mode must be quarantine or permanent.`,"storage.cleanup.err.fs_failed":`Filesystem cleanup failed. Some changes may already be applied — check CODEX_HOME/.trash and any recovery path shown.`,"storage.cleanup.err.fs_failed_trash":`Filesystem cleanup failed. Some changes may already be applied — check {trashDir} and manifest.json for recoverable files.`,"storage.cleanup.err.db_reconcile_failed":`Could not update Codex state database.`,"storage.cleanup.err.cleanup_failed":`Cleanup failed.`,"storage.trash.title":`Quarantine`,"storage.trash.help":`Archived sessions moved to CODEX_HOME/.trash. Restore puts JSONL files and thread rows back.`,"storage.trash.empty":`No quarantined entries.`,"storage.trash.loading":`Loading quarantine…`,"storage.trash.col.when":`Quarantined`,"storage.trash.col.files":`Files`,"storage.trash.col.size":`Size`,"storage.trash.col.mode":`Mode`,"storage.trash.col.id":`Entry`,"storage.trash.restore":`Restore`,"storage.trash.confirmTitle":`Restore quarantine entry?`,"storage.trash.confirmBody":`Restore {count} file(s) (~{size}) from {id} back to archived sessions.`,"storage.trash.cancel":`Cancel`,"storage.trash.confirmRestore":`Restore`,"storage.trash.done":`Restored {count} file(s) ({size}).`,"storage.trash.restoreFailed":`Restore failed.`,"storage.trash.listFailed":`Could not list quarantine entries.`,"storage.trash.mode.quarantine":`quarantine`,"storage.trash.mode.permanent":`permanent (incomplete)`,"storage.trash.err.codex_busy":`Codex is using state.sqlite — try again after quitting Codex.`,"storage.trash.err.invalid_trash":`Trash entry id is missing or invalid.`,"storage.trash.err.missing_trash":`Trash entry was not found.`,"storage.trash.err.dest_exists":`Restore destination already exists — remove or rename the archived file and retry.`,"storage.trash.err.fs_failed":`Filesystem restore failed. Some files may already be restored — check archived_sessions and .trash.`,"storage.trash.err.db_reconcile_failed":`Could not restore Codex state database rows.`,"storage.trash.err.storage_mutation_busy":`Another storage cleanup or restore is in progress — try again shortly.`,"storage.trash.err.restore_failed":`Restore failed.`,"storage.trash.err.restore_worker_timeout":`Restore took too long (over 10 minutes) and was stopped.`,"storage.trash.err.restore_worker_aborted":`Restore was cancelled during shutdown.`,"storage.trash.err.restore_worker_failed":`Restore worker crashed or failed unexpectedly.`,"storage.policy.title":`Auto-cleanup policy`,"storage.policy.help":`Optional batch cleanup when archived sessions exceed a threshold. Off by default — never enabled automatically.`,"storage.policy.loading":`Loading policy…`,"storage.policy.loadFailed":`Could not load cleanup policy.`,"storage.policy.saveFailed":`Could not save cleanup policy.`,"storage.policy.runFailed":`Policy run failed.`,"storage.policy.alreadyRunning":`A cleanup policy run is already in progress.`,"storage.policy.invalid":`Invalid policy values.`,"storage.policy.enabled":`Enable auto-cleanup`,"storage.policy.enabledHint":`Default is off. Enabling runs only on the schedule you choose (or Run now).`,"storage.policy.threshold":`When archived size exceeds (GiB)`,"storage.policy.trigger":`Trigger`,"storage.policy.target":`Cleanup target`,"storage.policy.targetPercent":`Remove oldest archived (%)`,"storage.policy.targetReduce":`Reduce archived size to (GiB)`,"storage.policy.thresholdInc":`Increase threshold`,"storage.policy.thresholdDec":`Decrease threshold`,"storage.policy.percentInc":`Increase percent`,"storage.policy.percentDec":`Decrease percent`,"storage.policy.reduceInc":`Increase reduce-to size`,"storage.policy.reduceDec":`Decrease reduce-to size`,"storage.policy.schedule":`Schedule`,"storage.policy.schedule.manual":`Manual only`,"storage.policy.schedule.startup":`On proxy startup`,"storage.policy.schedule.daily":`Daily`,"storage.policy.schedule.weekly":`Weekly`,"storage.policy.mode":`Deletion mode`,"storage.policy.mode.quarantine":`Quarantine (default)`,"storage.policy.mode.permanent":`Permanent delete`,"storage.policy.permanentWarn":`Permanent mode cannot be undone. Prefer quarantine unless you are sure.`,"storage.policy.lastRun":`Last run`,"storage.policy.lastRunDetail":`Removed {count} · freed {size}`,"storage.policy.nextRun":`Next run`,"storage.policy.never":`Never`,"storage.policy.save":`Save`,"storage.policy.runNow":`Run now`,"storage.policy.running":`Running…`,"storage.policy.saved":`Policy saved.`,"storage.policy.skippedDisabled":`Policy is disabled — enable it first.`,"storage.policy.skippedUnder":`Archived size is under the threshold — nothing to do.`,"storage.policy.skippedEmpty":`No archived candidates matched the target.`,"storage.policy.doneQuarantine":`Policy quarantined {count} file(s) ({size}).`,"storage.policy.donePermanent":`Policy permanently deleted {count} file(s) ({size}).`,"storage.policy.metadataSaveWarning":`The policy run finished, but its scheduling metadata could not be saved.`,"modal.addNamed":`Add: {label}`,"modal.add":`Add provider`,"modal.search":`Search providers…`,"modal.logInWith":`Log in with {label}`,"modal.waitingBrowser":`Waiting for browser…`,"modal.providerName":`Provider name`,"modal.adapter":`Adapter`,"modal.baseUrl":`Base URL`,"modal.endpoint":`Endpoint`,"modal.endpoint.tokenPlan":`Token plan`,"modal.endpoint.payAsYouGo":`Pay as you go`,"modal.endpoint.custom":`Custom`,"modal.defaultModel":`Default model (optional)`,"modal.allowPrivateNetwork":`Allow local/private network`,"modal.allowPrivateNetworkHint":`Enable only for intentionally self-hosted providers. Metadata endpoints remain blocked.`,"modal.nameRequired":`Provider name is required`,"modal.baseUrlRequired":`Base URL is required`,"modal.networkError":`Network error — is the proxy running?`,"modal.loginFailStart":`Login failed to start`,"modal.waitingLogin":`Waiting for browser login…`,"modal.loggingIn":`Logging in…`,"modal.loginTimeout":`Login timed out — try again.`,"modal.back":`Back`,"modal.badge.oauth":`OAuth`,"modal.customProvider":`Custom provider`,"modal.failedStatus":`Failed ({status})`,"modal.loginError":`Login error: {error}`,"modal.badge.codexLogin":`Codex login`,"modal.badge.local":`Local`,"modal.badge.apiKey":`API key`,"modal.badge.direct":`Direct`,"modal.badge.pool":`Pool`,"modal.badge.free":`Free`,"modal.invalidPreset":`This built-in provider preset is incomplete. Restart the proxy and try again.`,"modal.freeTierTitle":`Free tier`,"modal.freeTierDefault":`No API key required. Works out of the box.`,"modal.tab.accounts":`Accounts`,"modal.tab.free":`Free`,"modal.tab.paid":`Paid`,"modal.accountsHint":`Sign in to ChatGPT/Codex, OAuth providers, and API-key accounts here. OpenAI is built in — log in rather than adding it again.`,"modal.accountsCodexAuthLink":`Codex Auth`,"modal.notListed":`Provider not listed? Add a custom one`,"modal.catalogLoading":`Loading catalog…`,"modal.accountLogin":`Log in`,"modal.accountLogout":`Log out`,"modal.accountAdd":`Add account`,"modal.accountManage":`Manage`,"modal.accountCodexPool":`ChatGPT account pool`,"modal.accountLoggedIn":`Logged in`,"modal.accountLoggedOut":`Not logged in`,"quota.fiveHourLimit":`5-hour limit`,"quota.ageMinutes":`{n}m`,"quota.ageHours":`{n}h`,"quota.ageDays":`{n}d`,"quota.observedAgo":`Observed {age} ago`,"quota.observedHint":`Meta reports usage only during a streaming response, so this is the last value seen, not a live reading.`,"quota.weeklyLimit":`Weekly limit`,"quota.monthlyLimit":`30-day limit`,"quota.cursorFirstParty":`First-party models`,"quota.cursorApiUsage":`API usage`,"quota.totalSubscriptionCredits":`Total subscription credits`,"quota.creditsBalance":`Credits balance`,"quota.creditsPeriodEnds":`Billing period ends {date}`,"quota.usedPercent":`{pct}% used`,"quota.limitReached":`Limit reached`,"quota.resetsToday":`Resets today at {time}`,"quota.resetsTomorrow":`Resets tomorrow at {time}`,"quota.resetsAt":`Resets {when}`,"quota.resetsRelativeMinutes":`Resets in {n} min`,"quota.resetsRelativeHours":`Resets in {n} h`,"pws.status.ready":`Ready`,"pws.status.needsSetup":`Needs setup`,"pws.status.needsAttention":`Needs attention`,"pws.auth.chatgptPassthrough":`ChatGPT passthrough`,"pws.auth.noKey":`No key needed`,"pws.freeTitle":`Free pricing (a key may still be required)`,"pws.localTitle":`Local runtime`,"pws.modelCountOne":`1 model`,"pws.modelCount":`{count} models`,"pws.rail.suffixDefault":` · default`,"pws.rail.suffixLocal":` · local`,"pws.rail.suffixFree":` · free`,"pws.rail.selectAria":`Select {name} — {status}{suffix}`,"pws.searchPlaceholder":`Search providers…`,"pws.filterAria":`Filter providers`,"pws.providerFiltersAria":`Provider filters`,"pws.filters":`Filters`,"pws.filterStatus":`Status`,"pws.pricing":`Pricing`,"pws.paid":`Paid`,"pws.filterType":`Type`,"pws.type.cloud":`Cloud`,"pws.type.local":`Local`,"pws.type.selfHosted":`Self-hosted`,"pws.type.login":`Login`,"pws.sort":`Sort`,"pws.sortProvidersAria":`Sort providers`,"pws.sort.az":`A–Z`,"pws.sort.za":`Z–A`,"pws.sort.freePaid":`Free first`,"pws.sort.paidFree":`Paid first`,"pws.sort.accountsFirst":`Accounts first`,"pws.resetAll":`Reset all`,"pws.providerList":`Provider list`,"pws.providersAria":`Providers`,"pws.groupReady":`Ready ({count})`,"pws.groupNeedsSetup":`Needs setup ({count})`,"pws.groupDisabled":`Disabled ({count})`,"pws.noSearchResults":`No providers match your search.`,"pws.noMatchFilters":`No providers match the filters.`,"pws.noProvidersConfigured":`No providers configured.`,"pws.workspaceMainAria":`Provider details`,"pws.detailComingSoon":`Detail view coming soon — use the classic view to manage this provider.`,"pws.selectPrompt":`Select a provider from the list.`,"pws.connectFirst":`Connect your first provider`,"pws.empty.browseFree":`Browse free providers`,"pws.empty.browseFreeDesc":`Start without a subscription`,"pws.empty.connectAccount":`Connect an account`,"pws.empty.connectAccountDesc":`Use your ChatGPT or provider login`,"pws.empty.addEndpoint":`Add an endpoint`,"pws.empty.addEndpointDesc":`Custom base URL and API key`,"pws.tab.overview":`Overview`,"pws.tab.models":`Models`,"pws.tab.usage":`Usage`,"pws.tab.accounts":`Accounts`,"pws.tab.settings":`Settings`,"pws.connection":`Connection`,"pws.status.connected":`Connected`,"pws.attentionTitle":`Needs attention`,"pws.attention.reauth":`Active account needs re-authentication`,"pws.attention.reauthForward":`Active Codex account needs re-authentication — open Accounts to fix it`,"pws.attention.missingCredentials":`Missing credentials`,"pws.cell.auth":`Authentication`,"pws.cell.note":`Note`,"pws.cell.defaultModel":`Default model`,"pws.statsAria":`Provider statistics`,"pws.statsTitle":`Statistics`,"pws.stats.totalRequests":`Requests (30d)`,"pws.stats.totalTokens":`Tokens (30d)`,"pws.stats.quotaUpdated":`Quota updated`,"pws.stats.quotaTracked":`Rate limits tracked on the Usage tab.`,"pws.stats.source":`Source`,"pws.usageLast30d":`Usage (last 30 days)`,"pws.estimatedCost":`Estimated cost`,"pws.costDisclaimer":`API list-price estimate, not an actual charge.`,"pws.modelBreakdown":`Model breakdown`,"pws.col.model":`Model`,"pws.col.cost":`Est. cost`,"pws.col.tokens":`Tokens`,"pws.col.requests":`Req.`,"pws.col.share":`Share`,"pws.tokenInput":`Input`,"pws.tokenOutput":`Output`,"pws.metricRequests":`requests`,"pws.metricTokens":`tokens`,"pws.usageUnavailable":`No usage recorded yet.`,"pws.rateLimits":`Rate limits`,"pws.quotaUnavailable":`No quota data for this provider.`,"pws.accountQuotaUnavailable":`Rate-limit data temporarily unavailable; showing last known values when present.`,"pws.selected":`Selected`,"pws.copyModelId":`Copy ID`,"pws.modelCopied":`Copied!`,"pws.modelsAvailable":`{count} available`,"pws.modelSearchPlaceholder":`Filter models…`,"pws.modelsLoading":`Loading models…`,"pws.modelsLoadFailed":`Could not load models.`,"pws.modelsNeedsReauth":`Account needs re-login before live model discovery works. Showing configured models for now.`,"pws.modelsConfiguredFallback":`Showing configured models (live discovery unavailable).`,"pws.modelsTruncated":`Showing first {shown} of {total} models. Filter to narrow the list.`,"pws.retry":`Retry`,"pws.noModels":`No models discovered for this provider.`,"pws.noModelMatch":`No models match the filter.`,"pws.adapterBaseRequired":`Adapter and base URL are required.`,"pws.addAccount":`Add account`,"pws.addKey":`Add API key`,"pws.apiKeys":`API Keys`,"pws.authMode":`Auth mode`,"pws.availableAccounts":`Available accounts`,"pws.accountOrdinal":`Account {count}`,"pws.accountsLoading":`Loading accounts…`,"pws.accountsLoadFailed":`Accounts could not be loaded.`,"pws.retryAccounts":`Retry`,"pws.noAccounts":`No accounts are connected yet.`,"pws.cockpitImportDescription":`Import a Cockpit Tools Antigravity JSON export from this device. The file contents are not shown.`,"pws.cockpitImportFileLabel":`Cockpit Tools Antigravity JSON export`,"pws.cockpitImportChooseFile":`Choose JSON file`,"pws.cockpitImporting":`Importing…`,"pws.cockpitImportInvalid":`The selected file is not a valid JSON export or is too large.`,"pws.cockpitImportFailed":`The account import could not be completed.`,"pws.cockpitImportComplete":`Import complete: {imported} imported, {updated} updated, {failed} failed, {unsupported} unsupported.`,"pws.accountSwitching":`Switching…`,"pws.accountCurrent":`Current account`,"pws.defaultModelNone":`None (use provider default)`,"pws.discardSettings":`Discard`,"pws.jsonEditorDesc":`Edit the raw provider JSON config. Changes are saved immediately.`,"pws.jsonEditorTitle":`JSON editor — {name}`,"pws.jsonRestore":`Restore`,"pws.jsonSave":`Save`,"pws.loggedInTitle":`Logged in`,"pws.notLoggedInTitle":`Not logged in`,"pws.note":`Note`,"pws.allowPrivateNetwork":`Allow local/private network`,"pws.liveModels":`Discover models from provider`,"pws.liveModelsDesc":`Fetch the provider's live model catalog. Turn this off to use only configured/static models.`,"pws.xaiResponsesOptIn":`Use Responses API for Grok 4.5 and 4.6`,"pws.xaiResponsesOptInDesc":`Routes both models through openai-responses. Other Grok models and tier behavior are unchanged.`,"pws.xaiResponsesOptInMixed":`Partially enabled.`,"pws.cursorTransport":`Cursor transport`,"pws.cursorTransportHttp2":`HTTP/2 (default)`,"pws.cursorTransportHttp1":`HTTP/1.1 (proxy compatibility)`,"pws.cursorTransportDesc":`Use HTTP/1.1 when your proxy cannot reliably carry Cursor's HTTP/2 stream.`,"pws.optionalPlaceholder":`Optional`,"pws.providerId":`Provider ID`,"pws.reauth":`Needs re-auth`,"pws.reauthenticate":`Re-authenticate`,"pws.copyDoctor":`Copy ocx doctor`,"pws.doctorCopied":`Copied`,"pws.doctorCopyUnavailable":`Clipboard unavailable`,"pws.healthCooldownHint":`Wait until the cooldown ends. Do not probe this account yet.`,"pws.healthLabel.rateLimited":`Rate limited`,"pws.healthLabel.quotaLimited":`Quota limited`,"pws.healthLabel.reauthRequired":`Reauthentication required`,"pws.healthLabel.refreshFailed":`Refresh failed`,"pws.healthLabel.metadataMismatch":`Metadata mismatch`,"pws.healthLabel.credentialConflict":`Credential conflict`,"pws.healthSummary.rateLimited":`{provider} {account}: rate limited until {until}. Routing for this account is paused until then.`,"pws.healthSummary.quotaLimited":`{provider} {account}: quota limited until {until}. Routing for this account is paused until then.`,"pws.healthSummary.reauthRequired":`{provider} {account}: reauthentication required.`,"pws.healthSummary.credentialConflict":`{provider} {account}: credential conflict.`,"pws.healthSummary.metadataMismatch":`{provider} {account}: metadata mismatch.`,"pws.healthSummary.staleCredentials":`{provider} {account}: incomplete credentials.`,"pws.removeConfirm":`Remove`,"pws.removeConfirmBody":`Remove provider "{name}"? This cannot be undone.`,"pws.removeDefaultConfirmBody":`Remove default provider "{name}"? "{defaultProvider}" will become the default provider. This cannot be undone.`,"pws.removeConfirmTitle":`Remove provider`,"pws.saveSettings":`Save`,"pws.pacingTitle":`Request pacing`,"pws.pacingDesc":`Evenly delay outbound request starts for this provider. Streaming responses may overlap.`,"pws.pacingEnabled":`Enabled`,"pws.pacingRpm":`Requests per minute`,"pws.pacingRpmUnit":`RPM`,"pws.pacingDelay":`Minimum interval (ms)`,"pws.pacingSlowerWins":`The slower provider limit wins. Model overrides can only add more delay.`,"pws.pacingQueued":`queued`,"pws.pacingNextSlot":`until next slot`,"pws.pacingLastModel":`last model`,"pws.pacingNone":`None`,"pws.pacingModelOverrides":`Model overrides`,"pws.pacingModel":`Model`,"pws.pacingAdd":`Add override`,"pws.pacingRemove":`Remove`,"pws.pacingRemoveModel":`Remove request pacing override for {model}`,"pws.pacingRuleRequired":`Enable request pacing only after setting a provider limit or a model override.`,"pws.saving":`Saving…`,"pws.settingsSaved":`Settings saved.`,"pws.accountModeSaved":`Account mode saved.`,"pws.accountModeFailed":`Could not switch the account mode.`,"pws.accountModeConfirm":`Switch the OpenAI account mode? Running conversations will be reassigned to the other mode's account set, and quota usage will be tracked against the new mode.`,"pws.settingsUnsavedBar":`You have unsaved changes.`,"pws.unsavedLeaveBody":`You have unsaved changes. Save them before leaving?`,"pws.unsavedLeaveTitle":`Unsaved changes`,"pws.attentionRequired":`Attention required`,"pws.attentionAria":`{name}: {reason}`,"pws.missingCredentials":`Missing credentials`,"pws.editJsonDesc":`Edit the raw proxy config as JSON`,"pws.updatesUnavailable":`Provider updates are not available.`,"pws.dashboard.title":`Providers overview`,"pws.dashboard.subtitle":`Manage all your model providers in one place.`,"pws.dashboard.rateLimits":`RATE LIMITS`,"pws.capacity.estimate":`Configured-weight pool estimate`,"pws.capacity.currentAccount":`Current effective account`,"pws.capacity.nextRecovery":`Next capacity recovery`,"pws.capacity.recoveryShare":`+{percent}% pool capacity`,"pws.capacity.incomplete":`Incomplete coverage: {excluded} account(s) excluded`,"pws.capacity.uncalibratedPlan":`{count} account(s) on an uncalibrated plan are counted at the baseline seat weight, so this estimate may be conservative`,"pws.capacity.partial":`Partial window coverage: {count} account(s) do not report every displayed limit window`,"pws.capacity.windowPartial":`Partial`,"pws.capacity.windowPartialA11y":`{window}: incomplete account coverage`,"pws.dashboard.recentlyUsed":`RECENTLY USED`,"pws.dashboard.requests":`{count} requests`,"pws.dashboard.checkedAgo":`Checked {time}`,"pws.dashboard.noQuota":`No quota data`,"pws.dashboard.noUsage":`No usage data yet`,"pws.dashboard.noRateLimits":`No rate-limit data yet`,"pws.allProviders":`Provider Overview`,"pws.enabledLabel":`Enabled`,"pws.testConnection":`Test connection`,"pws.testing":`Testing…`,"pws.connectionOk":`Connection OK`,"pws.connectionFailed":`Connection failed`,"pws.connectionNotApplicable":`Not applicable — this provider uses a static model catalog.`,"pws.editSettings":`Edit settings`,"pws.viewUsage":`View detailed usage`,"pws.allSystemsOk":`All systems operational`,"pws.apiKeyConfigured":`API key configured`,"pws.addApiKey":`Add API key`,"pws.loggedInAs":`Logged in as {email}`,"pws.notLoggedIn":`Not logged in`,"pws.passthrough":`Codex passthrough`,"pws.notes":`NOTES`,"pws.notePlaceholder":`Add a note about this provider...`,"pws.noteSaved":`Note saved`,"pws.authSummary":`AUTHENTICATION`,"time.justNow":`Just now`,"time.notChecked":`Not checked`,"time.minutesAgo":`{n}m ago`,"time.hoursAgo":`{n}h ago`,"time.daysAgo":`{n}d ago`,"modal.noMatch":`No match.`,"modal.oauthDefaultNote":`Log in with your account — no API key needed.`,"modal.oauthComingSoon":`OAuth login for {label} arrives in the next update. Use an API key for now.`,"modal.oauthComingSoonShort":`OAuth login for this provider arrives in the next update — use an API key for now.`,"modal.useApiKeyInstead":`Use an API key instead`,"modal.setupGuide":`Setup guide`,"modal.setupStep1Prefix":`Go to`,"modal.setupDashboardLink":`{label} dashboard`,"modal.setupStep1Suffix":`and copy your API key`,"modal.setupStep2":`Paste it in the API key field below`,"modal.setupStep3":`Click Add provider — models are auto-discovered`,"modal.namePlaceholder":`e.g. openrouter`,"modal.duplicateWarn":`Provider "{name}" exists and will be overwritten.`,"modal.forwardHintPrefix":`No key needed — the proxy forwards your`,"modal.forwardCredentials":`codex login`,"modal.forwardHintSuffix":`credentials to this provider.`,"modal.localHint":`No API key is stored. This adds Cursor's static public model catalog for Codex, but live Cursor transport and native file/shell execution remain disabled until audited.`,"modal.getApiKey":`Get your {label} API key`,"modal.apiKey":`API key`,"modal.apiKeyTransport":`API key header`,"modal.apiKeyTransportNative":`x-api-key (Anthropic native)`,"modal.apiKeyTransportBearer":`Authorization: Bearer`,"modal.apiKeyPlaceholder":`sk-… (or $ENV_VAR)`,"modal.defaultModelPlaceholder":`e.g. gpt-5.5`,"modal.baseUrlPlaceholder":`https://...`,"modal.baseUrlPlaceholderError":`Base URL contains an unresolved {placeholder}. Replace it with your actual value.`,"modal.baseUrlPlaceholderHint":`Replace the {placeholder} in the Base URL with your actual Account ID before adding.`,"modal.adding":`Adding…`,"modal.useOauthLogin":`← Use OAuth login`,"nav.codexAuth":`Codex Auth`,"nav.codexSet":`Codex Set`,"codexSet.tab.multiauth":`Multi-auth`,"codexSet.tab.prompt":`Prompt`,"codexSet.prompt.title":`Prompt layers`,"codexSet.prompt.timing":`Applies to newly started sessions. Running sessions keep their current prompt settings.`,"codexSet.prompt.staleRevision":`The configuration changed elsewhere. The list was reloaded.`,"codexSet.prompt.writeFailed":`The change could not be saved.`,"codexSet.prompt.loadFailed":`The prompt layers could not be loaded.`,"codexSet.prompt.repair":`Repair`,"codexSet.prompt.repairFailed":`The repair could not be completed.`,"codexSet.drift.journalPresent":`A previous write did not finish. Recovery runs automatically on the next write.`,"codexSet.drift.projectionStale":`The saved layers and the value in config.toml disagree. Repair rewrites the value from your layers.`,"codexSet.drift.storeMissing":`The layer file is gone while instructions remain in config.toml. Repair keeps the text as one layer and writes a backup first.`,"codexSet.drift.ownedMalformed":`The generated line in config.toml was reshaped by hand, so it is no longer safe to rewrite.`,"codexSet.custom.adoptUnsupported":`The value at {path} line {line} is not a single-line string, so it cannot be imported. Move it by hand to manage it here.`,"codexSet.prompt.unreadable":`The Codex configuration file exists but could not be read, so changes are refused.`,"codexSet.layer.permissions":`Permissions`,"codexSet.layer.collaboration":`Collaboration mode`,"codexSet.layer.environment":`Environment context`,"codexSet.layer.apps":`Apps`,"codexSet.layer.skills":`Skills`,"codexSet.prompt.extensionsUnknown":`Extensions can add their own layers. Codex does not expose them, so they cannot be listed here.`,"codexSet.group.transition":`Transition notices`,"codexSet.group.transitionDesc":`These announce a change rather than describe state, so they appear only when the session enters realtime or switches model.`,"codexSet.custom.slotNote":`Custom layers are joined into one section in this order.`,"codexSet.row.alwaysOn":`Always on`,"codexSet.row.onChange":`Fires on change`,"codexSet.row.featureGated":`Configured under [features]`,"codexSet.row.openFeatures":`Open settings`,"codexSet.dialog.setValue":`{value} (default {fallback})`,"codexSet.dialog.copyKey":`Copy key`,"codexSet.dialog.unknownLayer":`This build has no description for this layer. It comes from a newer Codex runtime than the dashboard.`,"codexSet.custom.heading":`Custom layers`,"codexSet.custom.add":`+ Add layer`,"codexSet.custom.newTitle":`New layer`,"codexSet.custom.editTitle":`Edit layer`,"codexSet.custom.titleLabel":`Title`,"codexSet.custom.bodyLabel":`Instructions`,"codexSet.custom.bodySize":`{bytes} of {max} bytes`,"codexSet.custom.normalized":`Tabs became four spaces and line endings became LF.`,"codexSet.custom.titleRequired":`Enter a title.`,"codexSet.custom.titleTooLong":`Title is {count} characters; the limit is {max}.`,"codexSet.custom.titleMultiline":`A title must be a single line.`,"codexSet.custom.bodyTooLarge":`This layer is {bytes} bytes; the limit is {max}.`,"codexSet.custom.composedTooLarge":`Together the enabled layers would be {bytes} bytes, over the limit.`,"codexSet.custom.invalidCharacter":`A control character at position {position} cannot be saved.`,"codexSet.custom.discardPrompt":`Discard your changes?`,"codexSet.custom.keepEditing":`Keep editing`,"codexSet.custom.delete":`Delete {title}`,"codexSet.custom.deleteConfirm":`Delete this layer? There is no undo.`,"codexSet.custom.layerGone":`That layer was removed elsewhere, so the editor was closed.`,"codexSet.custom.deleteConfirmNamed":`Delete “{title}”? There is no undo.`,"codexSet.custom.moveUp":`Move {title} up`,"codexSet.custom.prevLayer":`Previous layer`,"codexSet.custom.nextLayer":`Next layer`,"codexSet.custom.navPosition":`{position} / {total}`,"codexSet.custom.moveDown":`Move {title} down`,"codexSet.custom.limitReached":`You can keep up to {max} custom layers.`,"codexSet.custom.notOwned":`developer_instructions was written outside opencodex, so it is not edited from here. Import it to manage it as a layer.`,"codexSet.custom.adopt":`Import existing instructions`,"codexSet.custom.adoptConfirm":`Import as a layer`,"codexSet.custom.adoptRefused":`The existing value could not be imported.`,"codexSet.custom.baseReplaced":`model_instructions_file is set to {path}, so something outside opencodex has replaced the base prompt.`,"codexSet.lint.identity":`This claims a different identity from the one Codex establishes.`,"codexSet.lint.foreignTool":`Tools come from the registry; naming one here does not create it.`,"codexSet.lint.placeholder":`No template engine runs over instructions, so this ships literally.`,"codexSet.lint.applyPatch":`apply_patch is defined by the tool registry, not by instructions.`,"codexSet.lint.approvalVocab":`Codex injects its own approval vocabulary; this may contradict it.`,"codexSet.lint.environment":`Environment facts are generated later and may contradict this.`,"codexSet.lint.size":`This layer is over 8 KB. It still saves, but it costs tokens on every request.`,"codexSet.preset.blank":`Blank layer`,"codexSet.preset.concise.name":`Concise output`,"codexSet.preset.concise.description":`Short answers, no preamble, minimal formatting.`,"codexSet.preset.concise.provenance":`Adapted from Claude Code's brevity directives. Our wording, not a copy.`,"codexSet.preset.planFirst.name":`Plan before editing`,"codexSet.preset.planFirst.description":`State the plan, then make the change.`,"codexSet.preset.planFirst.provenance":`Adapted from Claude Code's planning posture. Our wording, not a copy.`,"codexSet.preset.explainWhy.name":`Explain reasoning`,"codexSet.preset.explainWhy.description":`Say why, not only what.`,"codexSet.preset.explainWhy.provenance":`Adapted from Grok Build's confirmation style. Our wording, not a copy.`,"codexSet.preset.testFirst.name":`Test first`,"codexSet.preset.testFirst.description":`Write the failing test before the fix.`,"codexSet.preset.testFirst.provenance":`Adapted from common agent practice. Our wording, not a copy.`,"codexSet.preset.korean.name":`Korean replies`,"codexSet.preset.korean.description":`Answer in Korean whatever language the request uses.`,"codexSet.preset.korean.provenance":`Written for opencodex from a user-requested staple. Our wording, not a copy.`,"codexSet.dialog.class":`Kind`,"codexSet.dialog.key":`Config key`,"codexSet.dialog.fileValue":`Value in this file`,"codexSet.dialog.absentDefault":`not set (defaults to {value})`,"codexSet.dialog.noRenderedText":`Codex does not expose the assembled text of a built-in layer, so this dialog describes the layer and names its key rather than showing its contents.`,"codexSet.dialog.sourceText":`Text sent to the model`,"codexSet.dialog.sourceBytes":`{bytes} bytes`,"codexSet.dialog.notRendered":`This layer sent nothing on the turn we read. Sections are only re-sent when they change, so an unchanged layer is absent from a single sample.`,"codexSet.dialog.emptySource":`The file at {path} exists but is empty, so this layer sends nothing.`,"codexSet.dialog.notExposed":`The base prompt travels outside the message list Codex can print, so it cannot be shown here. Replacing it is possible through model_instructions_file.`,"codexSet.dialog.textUnavailable":`The Codex prompt could not be read on this machine, so the text is unavailable.`,"codexSet.class.base":`Base instructions`,"codexSet.class.config-toggle":`Switchable here`,"codexSet.class.feature-gated":`Feature-gated`,"codexSet.class.runtime-conditional":`Runtime-conditional`,"codexSet.class.extension-unknown":`Extension layer`,"codexSet.layer.base-instructions":`Base instructions`,"codexSet.layer.model-switch":`Model switch notice`,"codexSet.layer.personality":`Personality`,"codexSet.layer.context-window-guidance":`Context window guidance`,"codexSet.layer.realtime":`Realtime`,"codexSet.layer.agents-md":`AGENTS.md`,"codexSet.layer.environments-instructions":`Environments`,"codexSet.layer.plugins":`Plugins`,"codexSet.layer.tools":`Tools`,"codexSet.layer.multi-agent-mode":`Multi-agent mode`,"codexSet.layer.git-attribution":`Commit attribution`,"codexSet.about.base-instructions":`Codex's own instructions. They travel with the request itself and cannot be turned off.`,"codexSet.about.model-switch":`Added when the session changes model mid-conversation.`,"codexSet.about.personality":`Tone and voice guidance, governed by a feature flag.`,"codexSet.about.context-window-guidance":`Advice about the remaining context budget, governed by a feature flag.`,"codexSet.about.realtime":`Added for realtime sessions.`,"codexSet.about.agents-md":`Your project's AGENTS.md files. This page reports the layer; it never edits your project docs.`,"codexSet.about.permissions":`Explains the sandbox and approval settings in force.`,"codexSet.about.collaboration":`Explains the active collaboration mode.`,"codexSet.about.environment":`Working directory, platform, and other environment facts.`,"codexSet.about.environments-instructions":`Guidance for deferred execution environments, governed by a feature flag.`,"codexSet.about.apps":`How to use connected apps.`,"codexSet.about.plugins":`Added when a plugin is selected or any plugin advertises a capability.`,"codexSet.about.tools":`Deferred tool descriptions, governed by a feature flag.`,"codexSet.about.skills":`The list of available skills.`,"codexSet.about.multi-agent-mode":`Subagent instructions, governed by a feature flag.`,"codexSet.about.git-attribution":`Tells the model to add a Co-authored-by: Codex trailer to commits it writes, and a Generated with Codex. line to pull requests it opens. Codex resolves this from your account, so there is no setting for it here or under [features]. When your account has it off, Codex sends the opposite instruction rather than sending nothing.`,"codexSet.condition.model-switch":`Emitted only after a mid-session model change.`,"codexSet.condition.realtime":`Emitted only in a realtime session.`,"codexSet.condition.agents-md":`Emitted when a project doc is found for the working directory.`,"codexSet.condition.plugins":`Emitted when a plugin is selected or any plugin advertises a capability.`,"codexSet.condition.git-attribution":`Set by your account's attribution policy.`,"codexSet.base.title":`Base prompt`,"codexSet.base.prev":`Previous option`,"codexSet.base.next":`Next option`,"codexSet.base.position":`{position} / {total}`,"codexSet.base.swipeHint":`Swipe sideways, use the arrow keys, or press the arrows to move between options. Applies to newly started sessions.`,"codexSet.base.defaultTitle":`Codex's own base prompt`,"codexSet.base.defaultBody":`The default is not stored here, so there is nothing to edit or delete: choosing it simply removes model_instructions_file from your config, and Codex uses the prompt it ships with.`,"codexSet.base.variantTitle":`Name`,"codexSet.base.variantBody":`Prompt`,"codexSet.base.replacesWarning":`This REPLACES Codex's own base prompt rather than adding to it. A short prompt here means a model with short instructions.`,"codexSet.base.use":`Use this one`,"codexSet.base.inUse":`In use`,"codexSet.base.externalBlocked":`model_instructions_file already points at {path}, which opencodex did not write. Clear it yourself before choosing an option here.`,"nav.api":`API`,"nav.integrations":`Integrations`,"nav.openMenu":`Open menu`,"nav.closeMenu":`Close menu`,"integrations.subtitle":`Connect clients to opencodex, manage credentials, and restore client configuration.`,"integrations.tabsLabel":`Integration surfaces`,"integrations.tab.overview":`Overview`,"integrations.tab.keys":`API Keys`,"integrations.tab.codex":`Codex`,"integrations.tab.claude":`Claude`,"integrations.tab.grok":`Grok Build`,"integrations.tab.cursor":`Cursor`,"integrations.tab.opencode":`OpenCode`,"integrations.tab.pi":`Pi`,"integrations.tab.omp":`OMP`,"integrations.tab.hermes":`Hermes`,"integrations.tab.openclaw":`OpenClaw`,"integrations.tab.kimi":`Kimi Code`,"integrations.tab.gajae":`Gajae Code`,"integrations.tab.dsh":`DSH`,"integrations.tab.mcode":`MiniMax Code`,"integrations.tab.zcode":`ZCode`,"integrations.tab.prime":`Prime Agent`,"integrations.tab.aside":`Aside`,"integrations.codex.title":`Codex CLI`,"integrations.codex.body":`Codex wiring is owned by the proxy service. Starting opencodex applies it; stopping the service restores native routing.`,"integrations.codex.openService":`Open service controls`,"integrations.state.notInstalled":`Not installed`,"integrations.state.unknown":`Checking…`,"integrations.detail.codexRouted":`Codex requests go through this proxy`,"integrations.detail.codexAbsent":`Codex is not routed through this proxy yet`,"integrations.detail.keyCount":`{count} key(s) issued`,"integrations.detail.keyNone":`No keys issued`,"integrations.detail.keyChecking":`Checking…`,"integrations.detail.keyUnavailable":`Key status unavailable`,"integrations.detail.claudeOff":`Connection is off`,"integrations.detail.desktopCurrent":`Desktop is running this profile`,"integrations.detail.desktopStale":`The profile file changed after it was applied`,"integrations.detail.desktopNotServed":`The profile exists, but Desktop serves another one`,"integrations.detail.desktopAbsent":`No profile applied`,"integrations.detail.desktopDesiredOff":`Claude Desktop integration is off`,"integrations.detail.desktopDesiredOffCleanupPending":`Claude Desktop is still using the gateway; cleanup is pending`,"integrations.detail.desktopDesiredOnNotApplied":`Integration is on, but Desktop is not using the gateway profile`,"integrations.detail.desktopSelectedElsewhere":`Desktop is using another profile`,"integrations.detail.desktopProfileDrift":`The selected Desktop profile changed`,"integrations.detail.desktopObservedUnsafe":`The selected Desktop profile cannot be changed safely`,"integrations.detail.desktopNotInstalled":`Claude Desktop configuration library is not installed`,"integrations.detail.grokModels":`{count} model(s) wired`,"integrations.detail.grokAbsent":`No opencodex block in the config`,"integrations.detail.cursorSeen":`Cursor called this proxy recently`,"integrations.detail.cursorNeverSeen":`Private Inference installed; no request seen yet`,"integrations.detail.cursorAbsent":`Cursor Private Inference not found`,"integrations.cursor.title":`Cursor`,"integrations.cursor.intro":`Cursor Private Inference runs its agent locally and talks to opencodex on loopback. Regular Cursor cannot: its backend calls the custom endpoint and needs a public HTTPS URL. This page never writes to Cursor; paste the values below into Cursor yourself.`,"integrations.cursor.loading":`Reading Cursor status…`,"integrations.cursor.unavailable":`Could not read the Cursor status from the proxy.`,"integrations.cursor.detection":`Installed builds`,"integrations.cursor.privateInference":`Cursor Private Inference`,"integrations.cursor.regular":`Cursor (regular)`,"integrations.cursor.detected":`Detected`,"integrations.cursor.notFound":`Not found`,"integrations.cursor.regularOnly":`Only regular Cursor was found. It routes custom endpoints through Cursor's servers, so a loopback proxy is unreachable without a public tunnel. See the guide for the Private Inference build.`,"integrations.cursor.nothingFound":`No Cursor install was found in the usual locations. If it is installed elsewhere, the values below still apply.`,"integrations.cursor.gateway":`Gateway values`,"integrations.cursor.gatewayHint":`In Cursor Private Inference open Settings > Models > Gateway, paste these two values, then press Refresh model list.`,"integrations.cursor.baseUrl":`Base URL`,"integrations.cursor.apiKey":`API Key`,"integrations.cursor.apiKeyCredential":`One of your opencodex API keys (this bind requires a credential)`,"integrations.cursor.copy":`Copy`,"integrations.cursor.copied":`Copied`,"integrations.cursor.connection":`Connection`,"integrations.cursor.seen":`Last request from Cursor: {time} ({ua})`,"integrations.cursor.neverSeen":`No request from Cursor since the proxy started. After saving the gateway, press Refresh model list in Cursor.`,"integrations.cursor.models":`What Cursor will show`,"integrations.cursor.modelsHint":`Cursor picks the Reasoning ladder from its own model table, so opencodex can only predict it. Context lists the default and the opt-in window (Cursor's Max Mode).`,"integrations.cursor.ladderFromBundle":`Reasoning ladders read from the installed Cursor Private Inference {version} bundle. Cursor decides them; opencodex only reports its table.`,"integrations.cursor.ladderFromStatic":`Reasoning ladders are a static mirror of Cursor 3.18.25 (no readable Private Inference bundle was found). Context lists the default and the opt-in window.`,"integrations.cursor.unknownVersion":`unknown version`,"integrations.cursor.noControl":`—`,"integrations.cursor.singleWindow":`single window`,"integrations.cursor.noControlTitle":`This id is not in Cursor's built-in effort table, so Cursor shows no Reasoning control.`,"integrations.cursor.effortRowsOne":`1 effort row published`,"integrations.cursor.effortRowsMany":`{n} effort rows published`,"integrations.cursor.effortRowsOff":`no effort rows`,"integrations.cursor.tableLessHint":`Rows marked — get no Reasoning control in Cursor. Turn on cursorEffortRows to publish one picker entry per effort (id--effort), or set modelDefaultReasoningEfforts on the provider for a fixed default.`,"integrations.cursor.colModel":`Model`,"integrations.cursor.colReasoning":`Reasoning`,"integrations.cursor.colContext":`Context`,"integrations.cursor.guide":`Open the Cursor Private Inference guide`,"integrations.dialog.grok.title":`Disable the Grok Build integration?`,"integrations.dialog.grok.changes":`Only the block marked by opencodex will be removed from {path}. Content written outside the block will remain unchanged.`,"integrations.dialog.grok.breakage":`Disabling removes the opencodex model aliases from Grok Build. Models used with your xAI account remain available.`,"integrations.dialog.grok.undo":`If opencodex is running on a loopback address, turning this back on writes a new block from the models currently available.`,"integrations.dialog.grok.confirm":`Disable`,"integrations.dialog.desktop.title":`Disable Claude Desktop integration?`,"integrations.dialog.desktop.changes":`If {path} contains an opencodex-managed gateway profile, Desktop will first select a new credential-free standard profile, then remove the old profile and backup.`,"integrations.dialog.desktop.breakage":`Claude Desktop will return to standard Claude instead of models routed through opencodex.`,"integrations.dialog.desktop.undo":`Turning this back on regenerates the opencodex profile from your saved model assignments.`,"integrations.dialog.desktop.restart":`Claude Desktop reads this configuration only at launch. Fully quit and reopen it for this change to take effect.`,"integrations.dialog.desktop.confirm":`Disable`,"integrations.native.msg.nonLoopbackRemoved":`Grok Build can be registered automatically only while opencodex runs on a loopback address. The previous block that pointed to loopback was removed.`,"integrations.native.msg.nonLoopbackRemovedNoop":`Grok Build can be registered automatically only while opencodex runs on a loopback address. There was no previous block to remove.`,"integrations.native.msg.nonLoopbackSuperseded":`Grok Build can be registered automatically only while opencodex runs on a loopback address. Another process wrote a new block in the meantime, so the block now in the file was not created by this request.`,"integrations.native.error.orphanedMarker":`{path} has an opencodex start marker but no end marker. The file was left unchanged because opencodex cannot determine where its block ends.`,"integrations.native.error.homeMismatch":`The installed service home does not match the current home, so the file was left unchanged.`,"integrations.native.error.notInstalled":`Grok Build is not installed, so there is nothing to change.`,"integrations.native.error.configBusy":`The configuration is being saved elsewhere and could not be changed. Try again shortly.`,"integrations.native.error.desktopUnsafeMetadata":`Claude Desktop metadata at {path} could not be read safely, so its library was not changed.`,"integrations.native.error.desktopCleanupIncomplete":`Claude Desktop is pointed at standard mode, but old opencodex credential files remain at: {paths}.`,"integrations.native.msg.desktopDisabled":`Claude Desktop integration disabled.`,"integrations.native.msg.desktopEnabled":`Claude Desktop integration enabled.`,"integrations.state.absent":`Not applied`,"integrations.state.current":`Applied`,"integrations.state.stale":`Update needed`,"integrations.state.conflict":`Conflict`,"integrations.state.unsafe":`Cannot verify`,"integrations.summary.detected":`Clients detected`,"integrations.summary.applied":`Configured clients`,"integrations.summary.stale":`Update needed`,"integrations.summary.lastChange":`Last change`,"integrations.summary.disableAll":`Disable all…`,"integrations.onboarding":`Applying writes one opencodex provider block after saving a backup. Disable removes only that block, and a retained snapshot can be restored.`,"integrations.empty.title":`No installed clients were detected`,"integrations.empty.body":`Install a supported client, then return here to apply opencodex.`,"integrations.action.apply":`Apply`,"integrations.action.disable":`Disable`,"integrations.action.refresh":`Update`,"integrations.action.settings":`Settings`,"integrations.action.manageKeys":`Manage keys`,"integrations.action.restore":`Restore…`,"integrations.action.undo":`Undo`,"integrations.action.restorePoint":`Restore this point…`,"integrations.action.snapshotExpired":`Backup expired`,"integrations.rollback.title":`Rollback center`,"integrations.rollback.empty":`No apply history yet`,"integrations.rollback.emptyBody":`Every successful write keeps a pre-write snapshot first.`,"integrations.catalog.title":`Clients`,"integrations.rollback.older":`Earlier operations`,"integrations.rollback.showMore":`Show {n} more`,"integrations.rollback.failed":`Could not load the rollback history.`,"integrations.restore.title":`Restore this snapshot?`,"integrations.restore.body":`The current file is backed up first, then the selected snapshot replaces it.`,"integrations.restore.driftTitle":`Newer edits were detected`,"integrations.restore.driftBody":`Changes made after this snapshot will be backed up, then the file will be replaced.`,"integrations.restore.confirm":`Restore`,"integrations.restore.confirmDrift":`Back up newer edits and restore`,"integrations.restore.pending":`Restoring…`,"integrations.restore.manual":`Automatic restore failed: {reason}. Restore manually from {path}.`,"integrations.error.load":`Could not load integration state.`,"integrations.error.stale":`The latest refresh failed. The values below may be stale.`,"integrations.error.busy":`Another change for this client is still running. Try again shortly.`,"integrations.error.conflict":`The config changed after opencodex wrote it. Nothing was removed.`,"integrations.error.unsafe":`The config cannot be changed safely.`,"integrations.error.generic":`The integration change failed. Your previous state was kept.`,"integrations.error.nonLoopback":`{client} can only reach a proxy on localhost — its config has nowhere to put the admission header a remote bind requires, so writing one by hand would not help either. Give it loopback access instead, through a tunnel or a local forwarder.`,"integrations.status.installed":`Installed`,"integrations.status.notInstalled":`Not installed`,"integrations.status.appliedAt":`Applied`,"integrations.status.backup":`Backup`,"integrations.status.lastRestore":`Last restore`,"integrations.status.unknown":`Unknown`,"integrations.bulk.title":`Disable applied client integrations?`,"integrations.bulk.body":`Only the opencodex-owned block is removed. A pre-write snapshot is kept for each client.`,"integrations.bulk.partial":`Some clients could not be disabled: {clients}`,"integrations.bulk.success":`Applied client integrations were disabled.`,"integrations.retention.degraded":`Backup cleanup is behind; older backups may still be on disk.`,"integrations.error.residual":`The file may be in an intermediate state: {message} Restore it from {path}.`,"integrations.error.recover":`{message} A backup is at {path}.`,"integrations.kind.apply":`Applied`,"integrations.kind.disable":`Disabled`,"integrations.kind.refresh":`Updated`,"integrations.kind.restore":`Restored`,"integrations.kind.overwrite":`Overwritten`,"integrations.dialog.overwrite.title":`Replace the block in this config?`,"integrations.dialog.overwrite.changesUnowned":`A block we did not write occupies the settings opencodex needs in {path}. Applying replaces it with the block opencodex would write.`,"integrations.dialog.overwrite.changesForeign":`Your edit inside the opencodex block in {path} will be discarded and replaced with the block opencodex would write.`,"integrations.dialog.overwrite.breakage":`Anything the other block configured stops taking effect. Content elsewhere in the file is left alone.`,"integrations.dialog.overwrite.undo":`A snapshot is saved first, so this appears in the rollback list below and can be undone.`,"integrations.dialog.overwrite.confirm":`Replace`,"integrations.action.overwrite":`Replace`,"integrations.semantics.opencode":`Direct disk launches only; ocx opencode environment injection takes precedence.`,"integrations.semantics.pi":`Applies to new sessions.`,"integrations.semantics.omp":`Restart OMP to load the catalog.`,"integrations.semantics.hermes":`Applies to new sessions.`,"integrations.semantics.openclaw":`Applies immediately to a running gateway.`,"integrations.semantics.kimi":`Restart or run /reload to apply it (v2 watches the file).`,"integrations.semantics.gajae":`Applies to a new session or when opening /model.`,"integrations.semantics.dsh":`OpenCodex manages only llm-pi-ai.providers.opencodex in $DSH_HOME/settings.yaml. DSH hot reloads this provider; your default model and deepseek-official stay unchanged. Currently loopback-only; no real credential is written.`,"integrations.semantics.mcode":`Manages only custom_provider.opencodex. Your default model and MiniMax login stay unchanged.`,"integrations.semantics.zcode":`Manages only provider.opencodex in ~/.zcode/v2/config.json. Your Z.ai login and other providers stay unchanged. Restart ZCode after changes.`,"integrations.semantics.prime":`Manages only providers.opencodex in Prime Agent's models.json — ~/.prime/agent unless PRIME_AGENT_CODING_AGENT_DIR redirects it. Your other providers and model overrides stay unchanged. Applies to new sessions.`,"integrations.semantics.aside":`Manages only providers.opencodex in Aside's models.json for the signed-in account (~/.aside/u/). Your other providers stay unchanged. Aside rewrites this file while running, so fully quit and reopen it after applying.`,"codexAuth.mainAccount":`Main Account`,"codexAuth.logLabel":`Log label`,"codexAuth.codexApp":`Codex App`,"codexAuth.moreActions":`Show more actions`,"codexAuth.copyId":`Copy account ID`,"codexAuth.appLogin":`App login`,"codexAuth.accountPool":`Account Pool`,"codexAuth.accountModeTitle":`OpenAI account mode`,"codexAuth.accountModePool":`Pool mode`,"codexAuth.accountModePoolDesc":`The main login and eligible added accounts rotate here.`,"codexAuth.accountModeDirect":`Direct mode`,"codexAuth.accountModeDirectDesc":`Requests use only the main login; added accounts remain stored for Pool mode.`,"codexAuth.openaiMissing":`The built-in OpenAI provider is not configured.`,"codexAuth.openaiDisabled":`The built-in OpenAI provider is disabled.`,"codexAuth.openaiUnavailableDesc":`Your OpenAI accounts are still available. Enable the provider to route Codex requests.`,"codexAuth.enableOpenai":`Enable OpenAI`,"codexAuth.enablingOpenai":`Enabling...`,"codexAuth.enableOpenaiFailed":`Failed to enable the OpenAI provider.`,"codexAuth.openaiPresetLoadFailed":`Failed to load the OpenAI provider preset.`,"codexAuth.openaiPresetUnavailable":`OpenAI provider preset is unavailable.`,"codexAuth.openProviders":`Open Providers`,"codexAuth.add":`Add`,"codexAuth.sparkQuota":`Codex Spark quota`,"codexAuth.sparkQuotaHint":`Show the GPT-5.3-Codex-Spark weekly window on account cards. Hidden by default because it applies to one model only.`,"codexAuth.sparkQuotaShown":`Codex Spark quota shown`,"codexAuth.sparkQuotaHidden":`Codex Spark quota hidden`,"codexAuth.sparkQuotaFailed":`Could not change the Codex Spark quota setting`,"codexAuth.refreshQuota":`Refresh quotas`,"codexAuth.refreshingQuota":`Refreshing...`,"codexAuth.quotaRefreshed":`Quotas refreshed`,"codexAuth.quotaRefreshFailed":`Failed to refresh quotas`,"codexAuth.pauseExhausted":`Pause exhausted`,"codexAuth.pausingExhausted":`Checking quotas...`,"codexAuth.pauseExhaustedSucceeded":`Accounts at the limit paused: {count}`,"codexAuth.pauseExhaustedNone":`No accounts have confirmed 100% usage.`,"codexAuth.pauseExhaustedFailed":`Failed to check and pause exhausted accounts.`,"codexAuth.noPool":`No pool accounts added yet.`,"codexAuth.pause":`Pause`,"codexAuth.resume":`Resume`,"codexAuth.paused":`PAUSED`,"codexAuth.pauseSucceeded":`{email} is paused`,"codexAuth.resumeSucceeded":`{email} is available to the pool again`,"codexAuth.pauseFailed":`Could not pause {email}. Nothing was changed.`,"codexAuth.resumeFailed":`Could not resume {email}. Nothing was changed.`,"codexAuth.pausedHint":`Excluded from automatic switching, retries, cooldown recovery, and manual selection until resumed.`,"codexAuth.pinned":`PINNED`,"codexAuth.pinnedHint":`You selected this account by hand, so a higher selection order will not move past it. The pin lasts until this account is drained, you select another, or you change any selection order.`,"codexAuth.fiveHour":`5h`,"codexAuth.weekly":`Week`,"codexAuth.monthly":`30d`,"codexAuth.resets":`resets`,"codexAuth.today":`Today`,"codexAuth.current":`CURRENT`,"codexAuth.nextSession":`SELECTED`,"codexAuth.poolPrepared":`PREPARED FOR POOL`,"codexAuth.preparePoolTitle":`Prepare this account for Pool mode?`,"codexAuth.preparePoolDesc":`Direct requests keep using the main login. This account becomes the prepared Pool selection when Pool mode is enabled.`,"codexAuth.prepareForPool":`Prepare for Pool`,"codexAuth.poolPreparedToast":`{email} is prepared for Pool mode`,"codexAuth.switchTitle":`Switch active account?`,"codexAuth.switchDesc":`Takes effect immediately. Existing account-affine threads and requests already in flight keep their captured account; new or unbound requests use the selected account's order tier, and accounts at the same selection order still take turns.`,"codexAuth.cacheWarning":`Prompt cache resets on account switch. New session starts with empty cache.`,"codexAuth.setAsNext":`Use this account next`,"codexAuth.cancel":`Cancel`,"codexAuth.switchBack":`Switch back to Main?`,"codexAuth.switchBackDesc":`Takes effect immediately. Existing account-affine threads and requests already in flight keep their captured account; new or unbound requests use your App login account's order tier, and accounts at the same selection order still take turns.`,"codexAuth.autoSwitch":`Usage-based proactive switching`,"codexAuth.autoSwitchQuotaDesc":`Quota: at {threshold}% usage or above, the next request may move to a lower-usage eligible account, including an already-bound task; Go/Free use 30d only.`,"codexAuth.autoSwitchQuotaOffDesc":`Usage-based proactive switching is off. New/unbound assignment and failure recovery still apply.`,"codexAuth.autoSwitchRoundRobinDesc":`Round-robin assignment does not use this threshold; it continues to rotate new/unbound tasks.`,"codexAuth.autoSwitchFillFirstDesc":`Fill-first: {threshold}% is the drain point for new/unbound tasks; healthy bound tasks keep their account.`,"codexAuth.autoSwitchFillFirstOffDesc":`Fill-first has no usage drain point for new/unbound tasks; cooldown, reauthentication, and failure recovery can still move routing.`,"codexAuth.failureRecoveryNote":`Failure recovery is separate: a request rejected before output with 429/402, cooldown, reauthentication, exclusion, or configured transient failover may select another eligible account.`,"codexAuth.autoSwitchThreshold":`Usage threshold`,"codexAuth.autoSwitchThresholdAria":`Usage threshold, percent`,"codexAuth.autoSwitchThresholdInc":`Increase usage threshold`,"codexAuth.autoSwitchThresholdDec":`Decrease usage threshold`,"codexAuth.autoSwitchLoadFailed":`Usage-based switching setting could not be loaded.`,"codexAuth.autoSwitchThresholdInvalid":`Enter a whole number from 1 to 100`,"codexAuth.autoSwitchUpdated":`Usage-based proactive switching updated`,"codexAuth.autoSwitchUpdateFailed":`The usage-based switching update could not be confirmed. The last confirmed value is shown.`,"codexAuth.requestUserInput":`Ask for input in Default mode`,"codexAuth.requestUserInputDesc":`Lets Codex pause a Default-mode session and ask you questions with the request_user_input tool.`,"codexAuth.requestUserInputUpdated":`Feature flag updated - applies to new sessions.`,"codexAuth.requestUserInputUpdatedRestart":`Feature flag updated - applies to new sessions. Restart the Codex app to pick it up.`,"codexAuth.requestUserInputUpdateFailed":`Could not update the feature flag. Nothing was changed.`,"codexAuth.requestUserInputLoadFailed":`Could not read the feature flag from config.toml.`,"codexAuth.accountPickerTitle":`Target a specific Codex account from the model picker`,"codexAuth.accountPickerOffDesc":`When enabled, ordinary GPT picker rows are replaced by one entry per account selector, so you can choose the exact account for a conversation without logging out. Turning it off removes no accounts.`,"codexAuth.accountPickerOnDesc":`Each selector is a public label for one stored account. Choosing it locks that conversation to the mapped account: it never rotates or falls back, and it does not change the active Pool account.`,"codexAuth.accountPickerCompatibility":`The built-in Codex App login has its own selector; generated maps normally call it main and use a collision-safe suffix such as main-2 when needed. Added accounts receive stable privacy-safe labels, while custom selector labels stay unchanged. Existing conversations and saved model selections continue routing. Turning this off hides generated entries but preserves selectors and exact routes. Plain GPT model IDs keep their Pool or Direct behavior.`,"codexAuth.accountPickerUpdated":`Account targeting updated.`,"codexAuth.accountPickerUpdateFailed":`Could not update account targeting. The last confirmed setting is shown.`,"codexAuth.accountPickerLoadFailed":`Could not load the account-targeting setting.`,"codexAuth.accountPickerRefreshFailed":`Could not refresh this setting. The last confirmed value is still shown.`,"codexAuth.advancedSettings":`Advanced settings`,"codexAuth.advancedSettingsAria":`Show or hide advanced Codex Auth settings`,"codexAuth.catalogRefreshPending":`The change was saved, but the Codex model catalog refresh is pending. Run ocx sync to retry.`,"anthropicPool.title":`Claude account pool (experimental)`,"anthropicPool.enabledDesc":`On 429, cools the account and fails over. New sessions prefer usage under {threshold}% ({window}).`,"anthropicPool.enabledNoProactiveDesc":`On 429, cools the account and fails over. Proactive usage-based switching is off at threshold 0, but new-session selection and 429 recovery still use the {window} window.`,"anthropicPool.disabledDesc":`Uses only the active Claude account. Enable only if you accept experimental routing.`,"anthropicPool.experimentalWarning":`Experimental and not battle-tested. Anthropic may restrict accounts that look like automated multi-account rotation. Same organization can share quota — pooling those accounts will not help. Keep this off unless you understand the risk.`,"anthropicPool.needTwoAccounts":`Add at least two Claude OAuth accounts before enabling the pool.`,"anthropicPool.threshold":`New-session usage threshold`,"anthropicPool.thresholdAria":`New-session usage threshold, percent`,"anthropicPool.thresholdHelp":`0 disables quota-based picking (affinity + active account only). Default 80.`,"anthropicPool.thresholdInvalid":`Enter a whole number from 0 to 100`,"anthropicPool.loadFailed":`Claude pool settings could not be loaded.`,"anthropicPool.saveFailed":`Claude pool settings could not be saved.`,"anthropicPool.on":`On`,"anthropicPool.off":`Off`,"accountPool.strategy":`Rotation strategy`,"accountPool.strategyDesc":`How OpenCodex assigns an account to a new/unbound task.`,"accountPool.strategyQuota":`Quota`,"accountPool.strategyRoundRobin":`Round-robin`,"accountPool.strategyFillFirst":`Fill-first`,"accountPool.strategyHintQuota":`Quota can also rebind an existing task on its next request after the usage threshold is crossed.`,"accountPool.strategyHintRoundRobin":`Round-robin rotates only tasks without a live binding; the usage threshold does not change normal rotation.`,"accountPool.strategyHintFillFirst":`Fill-first uses the threshold as a drain point for unbound tasks; healthy bound tasks keep affinity.`,"accountPool.unboundDefinition":`New/unbound task means a request with no current account binding; an existing visible task can become unbound after a proxy or affinity reset.`,"accountPool.stickyLimit":`New/unbound assignments before rotate`,"accountPool.stickyLimitAria":`New/unbound assignments before rotate`,"accountPool.stickyLimitInc":`Increase sticky limit`,"accountPool.stickyLimitDec":`Decrease sticky limit`,"accountPool.stickyLimitHelp":`Keep the selected account for this many new/unbound task assignments before advancing; the counter increments when the task is bound, not after upstream success.`,"accountPool.stickyLimitInvalid":`Enter a whole number from 1 to 100`,"accountPool.strategyLoadFailed":`Rotation strategy could not be loaded.`,"accountPool.strategyUpdateFailed":`Rotation strategy could not be saved.`,"accountPool.quotaWindow":`Quota window`,"accountPool.quotaWindowDesc":`Which cached usage bar controls quota-based new-session selection, fill-first threshold checks, and eligible 429 replacements.`,"accountPool.quotaWindowFiveHour":`5-hour bar`,"accountPool.quotaWindowWeekly":`Weekly bar`,"accountPool.quotaWindowMaxUtilization":`Higher bar`,"accountPool.quotaWindowHint":`Weekly skips accounts whose 5-hour bar is exhausted while another eligible account remains, but falls back to them when none do. Weekly ties prefer lower 5-hour usage; per-account weekly bars are only known once the Providers page has polled them.`,"accountPool.quotaWindowInert":`Only quota — or fill-first above a 0 threshold — scores a usage bar, so this setting changes nothing for the current rotation strategy.`,"accountPool.priority":`Selection order`,"accountPool.priorityAria":`Selection order for this account`,"accountPool.priorityHint":`Higher numbers are used first. The pool moves to a lower number only when every account above it is drained or unavailable.`,"accountPool.priorityFirst":`First`,"accountPool.priorityEarlier":`Earlier`,"accountPool.priorityNormal":`Normal`,"accountPool.priorityLater":`Later`,"accountPool.priorityLast":`Last`,"accountPool.priorityOption":`{name} ({value})`,"accountPool.priorityCustom":`Custom`,"accountPool.priorityUpdated":`Selection order updated for {email}`,"accountPool.priorityUpdateFailed":`Selection order for {email} could not be saved. The last confirmed value is shown.`,"codexAuth.switched":`{email} is selected for the next request`,"codexAuth.loadFailed":`Codex account settings could not be loaded.`,"codexAuth.switchFailed":`The account could not be switched. Your previous selection is unchanged.`,"codexAuth.removeConfirm":`Remove {id}?`,"codexAuth.removeFailed":`The account could not be removed. Nothing was changed.`,"codexAuth.addTitle":`Add Codex Account`,"codexAuth.addIdLabel":`Account ID (slug)`,"codexAuth.addIdPlaceholder":`codex-work, codex-alt, team...`,"codexAuth.resetCreditsAria":`{count} reset credit(s)`,"codexAuth.addJsonLabel":`auth.json content`,"codexAuth.addHelp":`Copy from another machine's ~/.codex/auth.json, or use codex-auth export.`,"codexAuth.importBtn":`Import`,"codexAuth.importInvalidJson":`Invalid JSON`,"codexAuth.importMissingTokens":`Missing access_token or refresh_token in JSON`,"codexAuth.importMissingId":`Account ID is required`,"codexAuth.accountAdded":`Account added to pool`,"codexAuth.addPickDesc":`Login with another ChatGPT account to add it to the pool.`,"codexAuth.oauthLogin":`OAuth Login`,"codexAuth.oauthDesc":`Opens ChatGPT login in browser`,"codexAuth.deviceLogin":`Device code login`,"codexAuth.deviceDesc":`For a headless or remote proxy: enter a short code on another device`,"codexAuth.importAuthJson":`Import auth.json`,"codexAuth.importAuthJsonDesc":`From another Codex install or codex-auth export`,"codexAuth.back":`Back`,"codexAuth.oauthAlreadyInProgress":`Login already in progress. Complete it in your browser.`,"codexAuth.oauthWaiting":`Waiting for ChatGPT login to complete in your browser...`,"codexAuth.oauthSubmittingCode":`Submitting code…`,"codexAuth.oauthCodeSubmitted":`Code submitted — waiting for login to finish…`,"codexAuth.oauthStatusRetrying":`Network or proxy error while checking login status — retrying…`,"codexAuth.oauthCancelled":`Login was cancelled.`,"codexAuth.loginFailed":`Login failed`,"codexAuth.needsReauth":`Re-login`,"codexAuth.reauthenticate":`Re-authenticate`,"codexAuth.tokenExpired":`Token expired — re-authenticate this account`,"codexAuth.mainTokenExpired":`Token expired — sign in again via Codex App login`,"codexAuth.emailCollision":`This account matches your main Codex login. Use a different account.`,"codexAuth.resetCreditsTitle":`Reset Credits`,"codexAuth.resetCreditsAvailable":`You have {count} reset credit(s) available.`,"codexAuth.resetCreditsDesc":`Each credit resets your current hourly and weekly usage limits instantly.`,"codexAuth.noResetCredits":`You don't have any reset credits.`,"codexAuth.earnCreditsHint":`Credits are earned monthly and via the referral program.`,"codexAuth.creditsExpireNote":`Credits expire 30 days after earning.`,"codexAuth.useOneCredit":`Use 1 Credit`,"codexAuth.confirmResetTitle":`Use Reset Credit?`,"codexAuth.confirmResetDesc":`This will instantly reset your current rate limits. You have {count} credit(s) remaining.`,"codexAuth.irreversible":`This action cannot be undone.`,"codexAuth.useCredit":`Use Credit`,"codexAuth.redeeming":`Resetting...`,"codexAuth.resetSuccess":`Rate limits reset! {remaining} credit(s) remaining.`,"codexAuth.resetSuccessGeneric":`Rate limits reset!`,"codexAuth.resetAlreadyRedeemed":`This credit was already redeemed. Credits unchanged.`,"codexAuth.resetNothingToReset":`No rate-limit window needs resetting right now.`,"codexAuth.resetNoCredit":`No reset credits available.`,"codexAuth.resetError":`Failed to redeem reset credit. Please try again.`,"codexAuth.fifoNote":`The oldest credit is used first.`,"codexAuth.confirmWhichCredit":`Credit from {date} will be used.`,"codexAuth.creditNext":`Next to use`,"codexAuth.creditLabel":`Credit #{n}`,"codexAuth.creditNextBadge":`NEXT`,"codexAuth.creditGranted":`Granted {date}`,"codexAuth.creditExpires":`Expires {date} ({days}d left)`,"api.title":`API Access`,"api.subtitle":`Use generated API keys to access the opencodex proxy from external apps. Keys authenticate via the {authHeader} header; see the table below for what each endpoint accepts.`,"api.baseUrl":`Base URL`,"api.responsesEndpoint":`Responses API`,"api.chatCompletionsEndpoint":`Chat Completions API`,"api.messagesEndpoint":`Messages API`,"api.modelsEndpoint":`Models API`,"api.endpointNote":`Use the base URL with OpenAI-compatible clients. Responses and Chat Completions are exposed under /v1.`,"api.endpointsTitle":`Endpoints`,"api.authTitle":`Authentication`,"api.authLoopback":`Loopback binds (127.0.0.1 or ::1) bypass authentication. Remote binds require a generated ocx_ key or OPENCODEX_API_AUTH_TOKEN.`,"api.authBaseUrlNote":`Configure clients with the base URL, then choose the protocol-specific endpoint below.`,"api.newKeyTitle":`New key created`,"api.newKeyNote":`Copy this key now — it won't be shown again.`,"api.copy":`Copy`,"api.copied":`Copied`,"api.dismiss":`Dismiss`,"api.generateTitle":`Generate key`,"api.keyNamePlaceholder":`Key name (optional)`,"api.generate":`Generate`,"api.generating":`Creating…`,"api.activeKeys":`Active keys ({count})`,"api.activeKeysLoading":`Active keys`,"api.noKeys":`No API keys yet. Generate one above.`,"api.workspace.sections":`API sections`,"api.section.keys":`Keys`,"api.section.connect":`Connect`,"api.section.endpoints":`Endpoints`,"api.section.models":`Models`,"api.section.examples":`Examples`,"api.workspace.details":`API key details`,"api.workspace.keyDetails":`Key details`,"api.workspace.keyPrefix":`Key prefix`,"api.workspace.deleteKey":`Delete key`,"api.workspace.deleteConfirm":`Are you sure you want to delete this key? This cannot be undone.`,"api.workspace.usageExamples":`Usage examples`,"api.copyUrlHint":`Click to copy URL`,"api.urlCopied":`URL copied`,"api.copyExampleHint":`Click to copy example`,"api.exampleCopied":`Example copied`,"api.colName":`Name`,"api.colKey":`Key`,"api.colCreated":`Created`,"api.confirm":`Confirm`,"api.deleteAria":`Delete API key`,"api.modelsTitle":`External model catalog`,"api.modelsCount":`{count} callable`,"api.modelsLoading":`Loading models…`,"api.modelsSearch":`Search models`,"api.modelsSubtitle":`Use these exact model IDs with /v1/models and your chosen inbound protocol.`,"api.modelsEmpty":`No externally callable models are available yet.`,"api.modelsNoMatch":`No models match “{query}”.`,"api.modelsLoadFailed":`Could not load the external model catalog.`,"api.colModel":`Model`,"api.colSource":`Source`,"api.colProtocols":`Protocols`,"api.sourceNative":`ChatGPT pool`,"api.sourceCombo":`Combo route`,"api.sourceCustom":`Custom`,"api.protocolResponses":`Responses`,"api.protocolChatCompletions":`Chat Completions`,"api.protocolMessages":`Messages`,"api.copyModelId":`Copy ID`,"api.modelCopied":`Copied`,"api.testModel":`Test`,"api.testingModel":`Testing…`,"api.testSucceeded":`OK`,"api.testFailed":`Failed`,"api.usageChatTitle":`Chat Completions example`,"api.usageResponsesTitle":`Responses example`,"api.usageMessagesTitle":`Messages example`,"api.usageSampleInput":`Hello, world!`,"api.clientConfig.title":`Client config`,"api.clientConfig.rowsLabel":`Connect a client`,"api.clientConfig.details":`Details`,"api.clientConfig.detailsAria":`{client} config details`,"api.clientConfig.copyAria":`Copy {client} config`,"api.clientConfig.downloadAria":`Download {client} config`,"api.clientConfig.rowMeta":`{destination} · {count} model(s)`,"api.clientConfig.rowError":`Could not build the {client} config.`,"api.clientConfig.copiedAnnounceClient":`{client} config copied to the clipboard.`,"api.clientConfig.clientOpencode":`OpenCode`,"api.clientConfig.clientPi":`Pi`,"api.clientConfig.clientOmp":`OMP`,"api.clientConfig.clientHermes":`Hermes`,"api.clientConfig.clientOpenclaw":`OpenClaw`,"api.clientConfig.clientKimi":`Kimi Code`,"api.clientConfig.clientGajae":`Gajae Code`,"api.clientConfig.clientDsh":`DeepSeek Harness (DSH)`,"api.clientConfig.clientMcode":`MiniMax Code`,"api.clientConfig.clientZcode":`ZCode`,"api.clientConfig.clientPrime":`Prime Agent`,"api.clientConfig.clientAside":`Aside`,"api.clientConfig.copy":`Copy config`,"api.clientConfig.download":`Download`,"api.clientConfig.loading":`Building client config…`,"api.clientConfig.jsonLabel":`{client} config`,"api.clientConfig.destination":`Destination file`,"api.clientConfig.envHint":`Set the key before launching`,"api.clientConfig.mergeWarning":`Merge this into the destination file. Replacing it would drop your other providers and MCP settings.`,"api.clientConfig.modelCount":`{count} model(s) exported`,"api.clientConfig.missingLimits":`{count} of {total} model(s) ship without a context limit; the client applies its own defaults.`,"api.clientConfig.noKeyYet":`{env} has no key behind it yet. Generate a key above before using this config off loopback.`,"api.clientConfig.loadFailed":`Could not read the model list, so no client config was produced.`,"api.clientConfig.copiedAnnounce":`Client config copied to the clipboard.`,"api.clientConfig.copyFailed":`Could not copy the client config.`,"api.clientConfig.downloadedAnnounce":`Downloaded {filename}. Nothing changed yet — merge it into {destination} yourself.`,"api.clientConfig.whereDisclosure":`Where this file goes`,"api.clientConfig.whereBody":`The destination above is the global path. A project-local config file in the working directory takes precedence over it, and the client reads the key from the environment variable named in the config — never from this file.`,"api.keysLoadFailed":`Could not load API keys.`,"api.createFailed":`Could not create API key.`,"api.deleteFailed":`Could not delete API key.`,"api.auth.endpoint":`Endpoint`,"api.auth.required":`Required`,"api.auth.accepted":`Accepted`,"api.auth.rejected":`Not accepted`,"api.auth.testProtocol":`Test {protocol}`,"api.auth.testNeedsFreshKey":`Generate a key and keep its one-time value on screen to run an authenticated test.`,"api.key.name":`Key name`,"api.key.rename":`Rename`,"api.key.saveName":`Save name`,"api.key.renaming":`Saving…`,"api.key.renameFailed":`Could not rename the key. Your draft was kept.`,"api.key.deleting":`Deleting…`,"api.rotation.title":`Key rotation`,"api.rotation.description":`Issue a replacement key while the current key remains valid for a short overlap.`,"api.rotation.start":`Start rotation`,"api.rotation.starting":`Starting…`,"api.rotation.pending":`Rotation is pending. Update and verify the client before committing.`,"api.rotation.expires":`Overlap expires:`,"api.rotation.secretOnce":`Replacement key — shown once. Copy it before closing this notice.`,"api.rotation.commit":`Commit rotation`,"api.rotation.abort":`Abort rotation`,"api.rotation.failed":`The rotation action did not complete. Refresh before retrying.`,"api.rotation.startFailed":`Could not start key rotation.`,"api.key.copyFailed":`Could not copy the key. Select it and copy it manually before dismissing this panel.`,"api.attribution.title":`Attributed usage`,"api.attribution.requests7d":`Requests, last 7 days`,"api.attribution.totalRequests":`Total attributed requests`,"api.attribution.totalRequestsAvailable":`Requests in available history`,"api.attribution.sinceAvailable":`Available attribution since`,"api.attribution.lastUsed":`Last used`,"api.attribution.since":`Attribution available since`,"api.attribution.neverUsed":`Not used since attribution began`,"api.attribution.unavailable":`Usage unavailable`,"api.attribution.unavailableDetail":`No usage has been attributed yet. Requests recorded before attribution began cannot be assigned retroactively.`,"api.attribution.ambiguous":`Two keys share this ID, so usage cannot be attributed to one of them. Give each key a unique ID in the config file.`,"api.attribution.railAmbiguous":`duplicate ID`,"claude.subtitle":`Use GPT, Gemini, and other models inside Claude Code.`,"claude.pageTitle":`Claude Code`,"claude.workspace.settings":`Settings`,"claude.enabledLabel":`Claude connection`,"claude.enabledHint":`When off, Claude Code cannot use this proxy.`,"claude.authMode":`Auth Mode`,"claude.authModeHint":`Subscription requires Claude account, Proxy works without Anthropic account`,"claude.authModeSubscription":`Subscription (Claude account)`,"claude.authModeProxy":`Proxy (no account needed)`,"claude.authModeAuto":`Auto (detect Claude auth)`,"claude.effectiveMode.label":`Effective on next launch`,"claude.effectiveMode.manual":`Manual: {mode}`,"claude.effectiveMode.autoPresent":`Auto: subscription — Claude auth found via {source}`,"claude.effectiveMode.autoAbsent":`Auto: proxy mode — no Claude auth found`,"claude.effectiveMode.autoUnknown":`Auto: subscription — auth could not be verified`,"claude.effectiveMode.admissionKey":`This proxy's API key is still sent.`,"claude.authSource.claude-json-oauth":`Claude account`,"claude.authSource.claude-credentials-file":`credentials file`,"claude.authSource.macos-keychain":`macOS Keychain`,"claude.authSource.exported-env":`environment variable`,"claude.authSource.unknown":`a detected credential`,"claude.systemEnv":`Auto-connect`,"claude.systemEnvDesc":`When on, running claude in any terminal automatically goes through the proxy.`,"claude.systemEnvUnsupported":`Auto-connect is available on macOS only. On this system, start Claude with {cmd}.`,"claude.systemEnvWarn":`⚠ You must fully quit and relaunch your terminal app for this to take effect. Not recommended.`,"claude.fastMode":`Fast Mode (OpenAI)`,"claude.fastModeDesc":`Controls service_tier for OpenAI models. ON = priority (faster). OFF = default. Auto = passthrough (client decides).`,"claude.fastAuto":`Auto`,"claude.fastOn":`ON`,"claude.fastOff":`OFF`,"claude.autoContext":`Use big context automatically`,"claude.autoContextDesc":`Controls how far the 1M marking goes. ON: any model whose window can host the compaction threshold gets a big-context row. OFF: only true 1M models get one.`,"claude.autoContextInert":`Inactive because a legacy context-size value (maxContextTokens) exists in the config file. Remove it there to re-enable.`,"claude.autoCompactWindow":`Auto-summarize point`,"claude.autoCompactDefault":`{value} (default)`,"claude.autoCompactWindowDesc":`Older messages are summarized when the chat reaches this point. It never exceeds each model's own limit, so 200k models are unaffected.`,"claude.autoCompactWindowWarn":`Changing this can break GPT models — set higher than a model's real limit, chats will error before the summary kicks in.`,"claude.injectAgents":`Auto-register subagents`,"claude.injectAgentsDesc":`Registers the models picked on the Subagents tab (plus the current default model) as dispatchable Claude Code agents (ocx-*). Applies from the next session.`,"claude.webSearchSidecar":`Web search sidecar override`,"claude.webSearchSidecarHint":`Override the main web search sidecar for Claude Code requests.`,"claude.visionSidecar":`Vision sidecar override`,"claude.visionSidecarHint":`Override the main vision sidecar for Claude Code requests.`,"claude.useMainSetting":`Use main setting`,"claude.sidecarModelPlaceholder":`Main setting model`,"claude.quickstart":`Get started`,"claude.quickstartHint":`{cmd} opens Claude Code through the proxy. Your claude.ai login stays active.`,"claude.manualEnv":`Manual setup (advanced)`,"claude.smallFastModel":`Background helper model`,"claude.smallFastModelHint":`The model Claude Code uses for background work like chat summaries and topic detection. The haiku subagent alias uses it too. Empty = Claude default (Haiku).`,"claude.smallFastModelAccurateHint":`The model Claude Code uses for background work such as chat summaries and topic detection. The haiku subagent alias uses it too.`,"claude.smallFastModelUnsetOption":`Let Claude Code choose (native model)`,"claude.smallFastModelNativeWarning":`When unset, OpenCodex leaves the helper-model overrides unset. Claude Code may use its native Sonnet model, which may incur charges from your native provider.`,"claude.slotUnset":`Use Claude default`,"claude.modelMap":`Model interception`,"claude.modelMapHint":`Intercepts requests for a specific model and reroutes them to the one you pick. Empty by default — nothing happens until you add a rule.`,"claude.mapFrom":`Original model (e.g. claude-sonnet-4-5)`,"claude.mapTo":`Swap to (e.g. gemini/gemini-3-pro)`,"claude.addMapping":`Add rule`,"claude.removeMapping":`Remove rule`,"claude.aliases":`Available models`,"claude.aliasesHint":`Models that appear in Claude Code's /model menu.`,"claude.aliasProviderOther":`Other`,"claude.loading":`Loading…`,"claude.loadFail":`Failed to load Claude settings`,"claude.saved":`Saved.`,"claude.saveFailed":`Save failed`,"claude.networkError":`Network error — is the proxy running?`,"claude.toggleAria":`Toggle Claude connection`,"claude.none":`None`,"cws.loading":`Loading combos…`,"cws.loadFailed":`Could not load combos.`,"cws.saveFailed":`Could not save combo.`,"cws.removeFailed":`Could not remove combo.`,"cws.saved":`Combo saved.`,"cws.created":`Created {model}.`,"cws.removed":`Removed combo/{id}.`,"cws.renamed":`Renamed {from} to {to}.`,"cws.add":`Add combo`,"cws.addTitle":`Add combo`,"cws.addSubtitle":`Create a virtual model across providers and choose the exact model name clients will request.`,"cws.create":`Create combo`,"cws.railAria":`Combo list`,"cws.searchPlaceholder":`Search combos or targets…`,"cws.noSearchResults":`No combos match your search.`,"cws.group.failover":`Failover`,"cws.group.roundRobin":`Round-robin`,"cws.group.other":`Other strategies`,"cws.targetCount":`{count} targets`,"cws.targetCountOne":`1 target`,"cws.overviewTitle":`Combos`,"cws.overviewBlurb":`Virtual models that route across provider/model targets with failover, round-robin, weighted random, least-used, or soonest quota reset.`,"cws.count.total":`Total`,"cws.count.failover":`Failover`,"cws.count.roundRobin":`Round-robin`,"cws.count.other":`Other`,"cws.howTitle":`How it works`,"cws.howBody":`Ask Codex for the combo's public model name. Without one, the default is combo/. OpenCodex selects a target and hops only on retryable upstream failures. If no target remains available, the request fails closed instead of using the global default provider.`,"cws.attentionTitle":`Needs attention`,"cws.attention.empty":`No targets configured`,"cws.attention.few":`Only one target — failover has nowhere to hop`,"cws.attention.catalogOmitted":`Missing from the model catalog — member capabilities are incomplete or incompatible (missing context window / metadata, or empty modality intersection). Routing by alias still works`,"cws.attention.allTargetsExhausted":`All enabled targets are out of quota`,"cws.emptyTitle":`Create your first combo`,"cws.empty.createDesc":`Name a virtual model and chain two or more backends.`,"cws.backToAll":`Back to all combos`,"cws.allCombos":`All combos`,"cws.copyModel":`Copy id`,"cws.copied":`Copied`,"cws.tabsLabel":`Combo detail sections`,"cws.tab.config":`Config`,"cws.tab.about":`About`,"cws.strategy":`Strategy`,"cws.strategy.failover":`Failover`,"cws.strategy.roundRobin":`Round-robin`,"cws.strategy.random":`Random`,"cws.strategy.leastUsed":`Least-used`,"cws.strategy.resetWindow":`Reset-window`,"cws.strategy.failoverHint":`Try targets in order. If the first fails with a retryable error (rate limit, outage, subscription gate), hop to the next.`,"cws.strategy.roundRobinHint":`Deterministically balance traffic by weight. Keep each selected target for a batch of successful requests, then advance.`,"cws.strategy.randomHint":`Draw one eligible target per request, with odds proportional to weight. No stickiness between requests.`,"cws.strategy.leastUsedHint":`Route each request to the eligible target with the fewest recorded successes. Counts restart with the proxy.`,"cws.strategy.resetWindowHint":`Prefer the eligible target whose quota window resets soonest. Falls back to configuration order when quota data is missing.`,"cws.field.id":`Combo id`,"cws.field.idHint":`Clients will request {model}`,"cws.field.idInternalHint":`Internal combo id. You can change it after creation.`,"cws.field.idHintEdit":`Renaming moves the combo to a new id. Clients request {model}.`,"cws.field.alias":`Public model name`,"cws.field.aliasPlaceholder":`deepseek-v4-flash or vendor/model`,"cws.field.aliasHint":`Optional. Use a bare name with no prefix, a custom prefix like vendor/model, or leave blank to use combo/.`,"cws.field.nativeAlias":`Native OpenAI alias`,"cws.field.nativeAliasHint":`Let this combo own a supported unqualified native OpenAI model id. Account- and provider-qualified OpenAI routes stay separate.`,"cws.field.displayName":`Display name`,"cws.field.displayNameHint":`Picker label for this combo. Required when Native OpenAI alias is enabled.`,"cws.field.stickyLimit":`Sticky successes before rotate`,"cws.field.stickyLimitHint":`Retain the selected target for this many successful requests before the weighted selector advances.`,"cws.field.defaultEffort":`Default reasoning`,"cws.field.defaultEffortNone":`None (target default)`,"cws.field.defaultEffortHint":`Used only when the client omits reasoning effort. Options are the intersection of the selected targets' advertised efforts; targets without catalog effort metadata offer none.`,"cws.capability.imageInputUnavailable":`Unavailable until every selected target supports image input.`,"cws.capability.imageInputHint":`On by default when every target supports images. Turn off to accept text only.`,"cws.capability.imageInput":`Image / multimodal`,"cws.capability.adaptiveEffort":`Adaptive reasoning ladder`,"cws.capability.adaptiveEffortHint":`Off: a target with no reasoning control hides the effort picker for the whole combo. On: those targets stay usable and the picker keeps the levels the remaining targets share.`,"cws.capabilities":`Capabilities`,"cws.field.defaultEffortUnsupported":`This effort is not in the targets' common ladder — it will be ignored or snapped at request time.`,"cws.field.defaultEffortUnsupportedOption":`not in intersection`,"cws.targets":`Targets`,"cws.targets.failoverHint":`Order matters — first is primary.`,"cws.targets.roundRobinHint":`Weights control deterministic relative selection; order breaks ties in the rotation ring.`,"cws.targets.randomHint":`Weights control each draw's odds; order does not matter.`,"cws.targets.leastUsedHint":`Order only breaks ties between equally used targets.`,"cws.targets.resetWindowHint":`Order applies when quota data is missing or tied.`,"cws.target.provider":`Provider`,"cws.target.model":`Model`,"cws.target.weight":`Weight`,"cws.target.pickProvider":`Select provider…`,"cws.target.pickProviderFirst":`Select a provider first…`,"cws.target.pickModel":`Select model…`,"cws.target.noModels":`No models for this provider`,"cws.target.modelPlaceholder":`model id`,"cws.target.add":`Add target`,"cws.target.drag":`Drag to reorder`,"cws.target.moveUp":`Move up`,"cws.target.moveDown":`Move down`,"cws.quota.available":`Available`,"cws.quota.exhausted":`Out of quota`,"cws.quota.unknown":`Quota unknown`,"cws.quota.allExhausted":`All enabled targets are out of quota. Choose another target or wait for quota recovery.`,"cws.aboutTitle":`Runtime`,"cws.aboutBody":`Failed targets cool down briefly, honoring Retry-After. Invalid or context errors do not hop. Each target adapts effort to its own capabilities; exhausted combos fail closed. Logs and Usage retain ordered physical attempts and per-attempt usage.`,"cws.removeConfirmTitle":`Remove {model}?`,"cws.removeConfirmDesc":`This removes the virtual model from config and the Codex catalog. It does not delete any providers.`,"cws.unsavedTitle":`Unsaved changes`,"cws.unsavedDesc":`Discard edits to this combo and continue?`,"cws.keepEditing":`Keep editing`,"cws.err.missingId":`Combo id is required.`,"cws.err.invalidId":`Id must start with a letter or number and use only letters, numbers, dots, underscores, or hyphens (max 64).`,"cws.err.duplicateId":`A combo with this id already exists.`,"cws.err.invalidAlias":`Alias must use letters, numbers, dots, underscores, or hyphens, with at most one "/" segment.`,"cws.err.aliasReservedNamespace":`The alias must not use the reserved "combo/" namespace.`,"cws.err.aliasNativeFamily":`Bare aliases in the OpenAI native family (gpt-*, o1-*, o3-*, o4-*, codex-*) are not allowed.`,"cws.err.unsupportedNativeAlias":`Native alias must be a currently supported bare OpenAI model id.`,"cws.err.missingNativeAliasDisplayName":`A display name is required for native aliases.`,"cws.err.invalidDisplayName":`Display name must be at most 128 characters and contain no control characters.`,"cws.err.duplicateAlias":`Another combo already uses this alias.`,"cws.err.noTargets":`Add at least one target.`,"cws.err.incompleteTarget":`Each target needs a provider and model.`,"cws.target.disabled":`{name} (disabled)`,"cws.err.reservedNamespace":`A physical provider named combo must be renamed before creating combos.`,"cws.err.providerCollision":`The combo ID conflicts with a configured provider name.`,"cws.err.unknownProvider":`Each target must use a configured provider.`,"cws.err.duplicateTarget":`The same provider/model target can appear only once.`,"cws.err.invalidStickyLimit":`Sticky successes must be an integer from 1 to 100.`,"cws.err.invalidWeight":`Each round-robin weight must be an integer from 1 to 10000.`,"cws.err.noEnabledTarget":`At least one target must use an enabled provider.`,"claude.tabsLabel":`Claude client`,"claude.tabCode":`Code`,"claude.tabDesktop":`Desktop`,"claudeDesktop.title":`Claude Desktop`,"claudeDesktop.subtitle":`Route each Claude model family through an available model on port {port}.`,"claudeDesktop.importJson":`Import JSON`,"claudeDesktop.exportJson":`Export JSON`,"claudeDesktop.loading":`Loading Claude Desktop profile…`,"claudeDesktop.loadFail":`Failed to load Claude Desktop profile.`,"claudeDesktop.retry":`Retry`,"claudeDesktop.saveFailed":`Failed to save Claude Desktop profile.`,"claudeDesktop.applyFailed":`Profile was saved, but could not be applied.`,"claudeDesktop.updateFailed":`Claude Desktop update failed.`,"claudeDesktop.savedApplied":`Profile saved and applied to Claude Desktop.`,"claudeDesktop.appliedMarkerUnsaved":`Applied to Claude Desktop, but the applied marker was not saved — the saved-vs-applied state below may read stale until you apply again.`,"claudeDesktop.savedAppliedAnnounce":`Claude Desktop profile saved and applied.`,"claudeDesktop.saved":`Profile saved.`,"claudeDesktop.savedAnnounce":`Claude Desktop profile saved.`,"claudeDesktop.exported":`Profile exported as JSON.`,"claudeDesktop.importExpected":`Expected a version 1 Claude Desktop profile.`,"claudeDesktop.importReady":`JSON imported. Review the draft, then save and apply it.`,"claudeDesktop.importedAnnounce":`Profile JSON imported. Unsaved changes are ready for review.`,"claudeDesktop.importInvalid":`The selected file is not a valid profile.`,"claudeDesktop.importFailed":`Import failed. {error}`,"claudeDesktop.moved":`{route} moved to {family}.`,"claudeDesktop.unsaved":`Unsaved changes`,"claudeDesktop.upToDate":`Profile is up to date`,"claudeDesktop.saving":`Saving…`,"claudeDesktop.applying":`Applying…`,"claudeDesktop.saveApply":`Save & apply`,"claudeDesktop.emptyTitle":`No models available`,"claudeDesktop.emptyHint":`Add or enable a provider, then return to assign Claude Desktop routes.`,"claudeDesktop.assignmentsLabel":`Claude model family assignments`,"claudeDesktop.family.opus":`Opus`,"claudeDesktop.family.fable":`Fable`,"claudeDesktop.family.sonnet":`Sonnet`,"claudeDesktop.family.haiku":`Haiku`,"claudeDesktop.modelCountOne":`{count} model`,"claudeDesktop.modelCountMany":`{count} models`,"claudeDesktop.chooseDefault":`Choose a default`,"claudeDesktop.temporaryDefault":`Temporary default`,"claudeDesktop.laneEmpty":`Drop a model here or use its Move control.`,"claudeDesktop.laneNoMatch":`No model in this family matches your search.`,"nav.grok":`Grok`,"grok.title":`Grok Build`,"grok.subtitle":`Models opencodex has registered in your Grok config.`,"grok.loading":`Loading Grok status…`,"grok.loadFail":`Could not read the Grok config.`,"grok.notConfiguredTitle":`Grok Build is not wired up`,"grok.notConfiguredHint":`Start or restart the proxy with Grok installed and opencodex writes a managed block into:`,"grok.endpoint":`Endpoint`,"grok.colModel":`Model`,"grok.colAlias":`Grok alias`,"grok.colContext":`Context`,"grok.groupNative":`Native models`,"grok.groupRouted":`Routed models`,"grok.enabledCount":`{on} of {total} registered`,"grok.saved":`Selection saved.`,"grok.savedApplied":`Selection saved and written to your Grok config.`,"grok.saveFailed":`Could not save the Grok selection.`,"grok.applyFailed":`Selection saved, but the Grok config could not be updated.`,"grok.applySkipped":`Selection saved. The Grok config was not changed.`,"grok.saveApply":`Save & apply`,"grok.saving":`Saving…`,"grok.applying":`Applying…`,"grok.unsaved":`Unsaved changes`,"grok.upToDate":`Selection is up to date`,"grok.toggleModel":`Register {id} with Grok`,"claudeDesktop.available":`Available`,"claudeDesktop.defaultBadge":`Default`,"claudeDesktop.supports1m":`1M`,"claudeDesktop.unavailable":`Unavailable`,"claudeDesktop.contextM":`{n}M context`,"claudeDesktop.contextK":`{n}k context`,"claudeDesktop.contextUnknown":`context unknown`,"claudeDesktop.alias":`Alias`,"claudeDesktop.useAsDefault":`Use as {family} default`,"claudeDesktop.moveTo":`Move to`,"claudeDesktop.move":`Move`,"claudeDesktop.status.applied":`Applied to Desktop`,"claudeDesktop.status.stale":`Config stale — re-apply`,"claudeDesktop.status.notApplied":`Not applied`,"claudeDesktop.status.notActiveProfile":`Desktop is serving another profile — re-apply`,"claudeDesktop.status.disabled":`Claude Desktop integration is off. Fully quit and reopen Desktop after enabling it.`,"claudeDesktop.enableApply":`Enable and apply`,"claudeDesktop.health.lastRequest":`Last request`,"claudeDesktop.health.stats":`{count} req / {errors} err`,"claudeDesktop.effort.supported":`effort`,"claudeDesktop.effort.displayOnly":`effort (display only)`,"lab.title":`Compatibility Lab`,"lab.subtitle":`Read-only compatibility verdict matrix from lab projection evidence.`,"lab.loadFailed":`Could not load compatibility lab data`,"lab.projectionUnavailable":`Lab projection is not available. Run conformance or live probes first.`,"lab.projectionIncompatible":`Lab projection schema is incompatible. Rebuild the projection.`,"lab.statusTitle":`Projection status`,"lab.matrixTitle":`Compatibility matrix`,"lab.verdictsTitle":`Verdict records`,"lab.filter.layer":`Evidence layer`,"lab.filter.verdict":`Verdict`,"lab.filter.subject":`Subject ID`,"lab.filter.all":`All`,"lab.col.subject":`Subject`,"lab.col.layer":`Layer`,"lab.col.suite":`Suite`,"lab.col.verdict":`Verdict`,"lab.col.asOf":`As of`,"lab.col.protocol":`Protocol conformance`,"lab.col.live":`Live route compatibility`,"lab.col.task":`Task effectiveness`,"lab.empty":`No compatibility verdicts in the projection yet.`,"lab.subjectKind":`Kind`,"lab.observationCount":`Observations`,"lab.eventCount":`Events`,"lab.verdictCount":`Verdicts`,"lab.subjectCount":`Subjects`,"lab.builtAt":`Built`,"lab.loading":`Loading compatibility evidence…`,"lab.loadMore":`Load more`,"lab.detailTitle":`Verdict detail`,"lab.detailClose":`Close`,"lab.detailSubject":`Subject`,"lab.detailObservations":`Observations`,"lab.detailEvents":`Contributing events`,"lab.detailArtifacts":`Artifact metadata`,"lab.production.title":`Observed production traffic`,"lab.production.notVerification":`Not Lab verification`,"lab.production.attempts":`Attempts`,"lab.production.successes":`Successes`,"lab.production.routeErrors":`Route errors`,"lab.production.lastObserved":`Last observed`,"lab.detailLoadFailed":`Could not load verdict detail`,"lab.refresh":`Refresh`,"lab.verdict.UNKNOWN":`Unknown`,"lab.verdict.CLAIMED":`Claimed`,"lab.verdict.PROBED":`Probed`,"lab.verdict.VERIFIED":`Verified`,"lab.verdict.DEGRADED":`Degraded`,"lab.verdict.BLOCKED":`Blocked`,"lab.verdict.UNSUPPORTED":`Unsupported`,"lab.layer.protocol_conformance":`Protocol conformance`,"lab.layer.live_route_compatibility":`Live route compatibility`,"lab.layer.task_effectiveness":`Task effectiveness`,"models.newPolicyGlobal":`New models start disabled`,"models.newPolicyProvider":`New model policy`,"models.newPolicy_inherit":`Inherit`,"models.newPolicy_off":`Off`,"models.newPolicy_on":`On`,"models.newBadge":`NEW`,"models.newCount":`{count} new, off`,"models.aliases":`Aliases`,"models.aliasesTable":`Alias table`,"models.aliasPrompt":`Provider alias (leave empty to clear)`,"models.modelAliasPrompt":`Model alias (leave empty to clear)`,"models.aliasSaved":`Alias saved`,"models.aliasConflict":`That alias conflicts with an existing name`,"models.editProviderAlias":`Edit provider alias`,"models.editModelAlias":`Edit model alias`,"models.useDefaultAliases":`Use default aliases`,"models.useDefaultAliasesGlobal":`Use default aliases globally`,"models.aliasAuto":`auto`,"models.aliasUser":`user`,"models.aliasStale":`stale`,"connection.discovering":`Discovering local and shared targets…`,"connection.machineUnavailable":`The local machine plane is unavailable. Shared requests were not redirected locally.`,"connection.disconnect":`Disconnect from hub`,"connection.disconnectConfirm":`Disconnect this machine from the hub and restart it in standalone mode?`,"connection.pairing.title":`Connect this dashboard to the hub`,"connection.pairing.body":`Paste the one-time pairing code created on the hub.`,"connection.pairing.relayWarning":`This code is exchanged through the fixed hub relay. The relay cannot be redirected to another host.`,"connection.pairing.code":`One-time pairing code`,"connection.pairing.submit":`Connect`,"connection.pairing.submitting":`Connecting…`,"connection.pairing.error":`The pairing code was refused or expired. The code was left in place so you can check it.`,"connection.machine.title":`This machine`,"connection.machine.shimHealthy":`Codex shim is healthy.`,"connection.machine.shimNeedsAttention":`Codex shim needs attention.`,"connection.machine.repairShim":`Repair shim`,"connection.machine.removeShim":`Remove shim`,"connection.clients.title":`Connected clients`,"connection.clients.none":`No client status available`,"connection.clients.sync":`Sync now`,"connection.clients.syncing":`Syncing…`,"connection.sessionLogout":`Log out remote session`,"connection.sessionLoggingOut":`Logging out remote session…`,"connection.sessionLogoutFailed":`Could not log out the remote session. The current session was kept.`,"usage.source.connected":`Source: hub usage`,"usage.source.local":`Source: local usage.jsonl`,"usage.scope.label":`Usage scope`,"usage.scope.machine":`This machine`,"usage.scope.hub":`Hub-wide`,"usage.hubOffline":`Hub usage is unavailable. Local usage was not substituted.`},ze={"nav.dashboard":`Übersicht`,"uptime.day":`T`,"uptime.hour":`Std`,"uptime.minute":`Min`,"uptime.second":`Sek`,"nav.startup":`Startsicherheit`,"nav.providers":`Anbieter`,"nav.models":`Modelle`,"nav.combos":`Combos`,"nav.subagents":`Sub-Agenten`,"routing.title":`Routing-Intelligenz (beta)`,"routing.subtitle":`Policy-Profile, Trockenlauf-Bewertung und routinggestützte Analysen.`,"routing.loadFailed":`Routing-Daten konnten nicht geladen werden`,"routing.empty":"Keine Routing-Profile konfiguriert. Fügen Sie `routingProfiles` zur config.json hinzu.","routing.revision":`rev`,"routing.detail":`Profil`,"routing.createProfile":`Profil erstellen`,"routing.dryRunError":`Trockenlauf fehlgeschlagen (HTTP {status})`,"routing.removeConfirm":`Profil {id} entfernen?`,"routing.unknownEvidence.allow":`zulassen`,"routing.unknownEvidence.penalize":`bestrafen`,"routing.unknownEvidence.exclude":`ausschließen`,"routing.removeCandidate":`Kandidat {provider}/{model} entfernen`,"routing.candidates":`Kandidaten`,"routing.require":`Harte Anforderungen`,"routing.optimize":`Optimierungsgewichte`,"routing.limits":`Grenzen`,"routing.unknownEvidence":`Richtlinie für unbekannte Evidenz`,"routing.compatibility.title":`Kompatibilitätsrichtlinie`,"routing.compatibility.enabled":`Compatibility-Lab-Evidenz erforderlich`,"routing.compatibility.requiredSuites":`Erforderliche Suites`,"routing.compatibility.loadingCatalog":`Lab-Katalog wird geladen…`,"routing.compatibility.catalogUnavailable":`Lab-Katalog nicht verfügbar — Suite-IDs in config.json manuell eintragen.`,"routing.compatibility.layer.protocol_conformance":`Protokollkonformität`,"routing.compatibility.layer.live_route_compatibility":`Live-Route-Kompatibilität`,"routing.compatibility.minStatus":`Mindest-Kompatibilitätsstatus`,"routing.none":`keine`,"routing.unavailable":`–`,"routing.dryRun":`Trockenlauf-Bewertung`,"routing.dryRunContext":`Kontextfenster der Anfrage (Tokens)`,"routing.dryRunTools":`Anfrage benötigt Tools`,"routing.dryRunImage":`Anfrage benötigt Bild-Eingabe`,"routing.dryRunStructured":`Anfrage benötigt strukturierte Ausgabe`,"routing.dryRunRun":`Kandidaten bewerten`,"routing.candidate":`Kandidat`,"routing.eligible":`Geeignet`,"routing.exclusions":`Ausschlüsse`,"routing.costCap":`Kostenobergrenze`,"routing.capOutcome.satisfied":`innerhalb des Limits`,"routing.capOutcome.exceeded":`über dem Limit`,"routing.capOutcome.unknown-allowed":`unbekannt (erlaubt)`,"routing.capOutcome.unknown-excluded":`unbekannt (ausgeschlossen)`,"routing.exclusion.capability-unsatisfied":`Anforderung nicht erfüllt`,"routing.exclusion.unknown-capability":`unbekannte Fähigkeit`,"routing.exclusion.cost-limit":`Kostenobergrenze überschritten`,"routing.exclusion.cost-limit-unknown":`Kosten unbekannt — Obergrenze nicht prüfbar`,"routing.exclusion.cooldown":`Abklingzeit`,"routing.exclusion.unknown-health":`unbekannter Zustand`,"routing.exclusion.unknown-quota":`unbekanntes Kontingent`,"routing.exclusion.unknown-price":`unbekannter Preis`,"routing.exclusion.other":`Ausschluss: {code}`,"routing.score":`Punktzahl`,"routing.selected":`ausgewählt`,"routing.yes":`ja`,"routing.no":`nein`,"routing.analytics":`Routing-Analysen`,"routing.analyticsTotal":`Anfragen`,"routing.analyticsSuccessRate":`Erfolg`,"routing.analyticsFallbackRate":`Fallback`,"routing.analyticsP50":`p50`,"routing.analyticsP95":`p95`,"routing.analyticsP99":`p99`,"routing.analyticsCooldown":`Cooldown-Fehler`,"routing.analyticsConfidence":`Konfidenz`,"routing.analyticsTruncated":`abgeschnittener Verlauf`,"routing.analyticsRequests":`Anfragen`,"routing.analyticsEmpty":`Noch keine Analysen – senden Sie zuerst einige Anfragen.`,"nav.logs":`Protokolle & Diagnose`,"nav.usage":`Nutzung`,"common.github":`GitHub`,"sidebar.star":`Auf GitHub mit Stern markieren`,"sidebar.starred":`Auf GitHub markiert`,"sidebar.starUnauthenticated":`GitHub öffnen, um zu markieren (gh CLI nicht angemeldet)`,"sidebar.starFailed":`Markieren über gh fehlgeschlagen. GitHub wird stattdessen geöffnet.`,"sidebar.updateAvailable":`Update verfügbar: {version}`,"sidebar.checkUpdate":`Nach Updates suchen`,"common.save":`Speichern`,"common.saving":`Speichern…`,"common.cancel":`Abbrechen`,"common.discard":`Verwerfen`,"common.delete":`Löschen`,"common.remove":`Entfernen`,"common.loading":`Lädt…`,"common.retry":`Wiederholen`,"auth.adminTokenTitle":`OpenCodex-Admin-Token (OPENCODEX_ADMIN_AUTH_TOKEN)`,"auth.adminAccountLabel":`Konto`,"auth.adminTokenFieldLabel":`Admin-Token`,"auth.adminTokenRejected":`Der Admin-Token wurde abgelehnt. Prüfen Sie ihn und versuchen Sie es erneut.`,"auth.adminTokenUnavailable":`Der Admin-Token konnte nicht überprüft werden. Versuchen Sie es erneut.`,"theme.label":`Design`,"theme.light":`Hell`,"theme.dark":`Dunkel`,"theme.system":`System`,"lang.label":`Sprache`,"lang.nativeName":`Deutsch`,"provider.name.commandCodeAuth":`Command Code - Auth`,"provider.name.commandCodeApi":`Command Code - API`,"provider.name.volcengine":`Volcengine Ark`,"provider.name.volcengineCodingPlan":`Volcengine Ark Coding-Tarif`,"provider.name.volcengineAgentPlan":`Volcengine Ark Agent-Tarif`,"errorBoundary.title":`Seite konnte nicht geladen werden`,"errorBoundary.message":`In diesem Bereich ist ein Darstellungsfehler aufgetreten. Lade ihn neu, um es noch einmal zu versuchen.`,"errorBoundary.details":`Fehler`,"errorBoundary.reload":`Neu laden`,"startup.title":`Startsicherheit`,"startup.subtitle":`Prüft, ob Codex opencodex nach einem Neustart erreicht, bevor lokales Proxy-Routing in einer Wiederverbindungsschleife endet.`,"startup.refresh":`Aktualisieren`,"startup.backToDashboard":`Zurück zum Dashboard`,"startup.loading":`Startschutz wird geprüft…`,"startup.error":`Startschutz konnte nicht gelesen werden.`,"startup.staleData":`Die aktuelle Prüfung ist fehlgeschlagen. Die Werte unten sind veraltet und kein Nachweis für Schutz.`,"startup.status.native":`Natives Routing`,"startup.status.protected":`Neustartgeschützt`,"startup.status.atRisk":`Aktion erforderlich`,"startup.summary.native":`Codex ist nicht vom lokalen Proxy abhängig`,"startup.summary.protected":`opencodex ist nach einem Neustart verfügbar`,"startup.summary.atRisk":`Codex kann nach einem Neustart den Modellzugriff verlieren`,"startup.riskDetail":`Codex ist auf den lokalen Proxy festgelegt, aber weder ein dauerhafter Dienst noch ein intakter Launcher-Shim startet ihn erneut.`,"startup.riskDetailCustomLocal":`Codex verwendet ein benutzerdefiniertes lokales Gateway. opencodex kann dessen Neustart-Lebenszyklus weder verwalten noch prüfen.`,"startup.riskDetailWindowsShim":`Der Launcher-Shim schützt unterstützte CLI-Skripte, aber Codex Desktop und direkte codex.exe-Aufrufe können ihn unter Windows umgehen.`,"startup.safeDetail":`Routing und Startmechanismus stimmen überein. Nach einem Neustart sollte kein manuelles ocx start nötig sein.`,"startup.routing":`Codex-Routing`,"startup.routing.proxy":`Lokaler Proxy`,"startup.routing.native":`Natives OpenAI`,"startup.routing.customLocal":`Benutzerdefiniertes lokales Gateway`,"startup.routing.customRemote":`Benutzerdefiniertes Remote-Gateway`,"startup.routing.unknown":`Unbekanntes oder ungültiges Routing`,"startup.restartProtection":`Neustartschutz`,"startup.preference":`Start bei Bedarf`,"startup.enabled":`Aktiviert`,"startup.disabled":`Deaktiviert`,"startup.protection.service":`Hintergrunddienst`,"startup.protection.shim":`Launcher-Shim`,"startup.protection.none":`Nicht installiert`,"startup.details":`Schutzdetails`,"startup.service":`Hintergrunddienst`,"startup.serviceHint":`Startet bei der Anmeldung und startet den Proxy nach einem Absturz neu.`,"startup.installed":`Installiert`,"startup.notInstalled":`Nicht installiert`,"startup.unsupported":`Nicht unterstützt`,"startup.shim":`Codex-Launcher-Shim`,"startup.shimHint":`Führt ocx ensure aus, wenn ein unterstützter Codex-Skript-Launcher startet.`,"startup.healthy":`Intakt`,"startup.cliOnly":`Nur CLI`,"startup.stale":`Veraltet`,"startup.viable":`Einsatzbereit`,"startup.unhealthy":`Installiert, aber fehlerhaft`,"startup.conflict":`Dienstkonflikt`,"startup.installedDisabled":`Installiert, aber deaktiviert`,"startup.install":`Installieren`,"startup.installing":`Wird installiert…`,"startup.repair":`Reparieren`,"startup.repairing":`Wird repariert…`,"startup.serviceInstalled":`Hintergrunddienst wurde erfolgreich installiert.`,"startup.serviceRepaired":`Hintergrunddienst wurde erfolgreich repariert.`,"startup.shimInstalled":`Codex-Launcher-Shim wurde erfolgreich installiert.`,"startup.shimRepaired":`Codex-Launcher-Shim wurde erfolgreich repariert.`,"startup.installFailed":`Installation fehlgeschlagen:`,"startup.tray.title":`Windows-Infobereich`,"startup.tray.hint":`Installiert ein Anmeldesymbol für Proxy-Start, Stopp, Neustart, Dashboard und Status per Klick.`,"startup.tray.login":`Infobereich bei Windows-Anmeldung starten`,"startup.tray.notProtection":`Das Symbol ist nur eine Steuerung, kein Neustartschutz. Für unbeaufsichtigte Wiederherstellung bleibt ein funktionsfähiger Hintergrunddienst nötig.`,"startup.tray.running":`Wird ausgeführt`,"startup.tray.stopped":`Installiert, ausgeblendet`,"startup.tray.stale":`Reparatur erforderlich`,"startup.tray.notInstalled":`Nicht installiert`,"startup.tray.loading":`Wird geprüft…`,"startup.tray.unavailable":`Status nicht verfügbar`,"startup.tray.install":`Installieren und anzeigen`,"startup.tray.start":`Symbol anzeigen`,"startup.tray.stop":`Symbol beenden`,"startup.tray.uninstall":`Anmeldesymbol entfernen`,"startup.tray.error":`Die Windows-Infobereichsaktion ist fehlgeschlagen. Details: ocx tray status.`,"startup.recovery":`Reparaturoptionen`,"startup.recoveryHint":`Nutze die Ein-Klick-Installation oben oder kopiere einen Befehl für die manuelle Reparatur. Für Codex Desktop und Windows-Programme wird der Hintergrunddienst empfohlen.`,"startup.command.service":`Empfohlen: dauerhafter Hintergrunddienst`,"startup.command.shim":`Alternative: CLI-Launcher-Shim`,"startup.command.native":`Ausfallsicher: natives Codex-Routing wiederherstellen`,"startup.copy":`Kopieren`,"startup.copied":`Kopiert`,"startup.recommended":`Empfohlene Reparatur: {cmd}`,"startup.navRisk":`Der Startschutz erfordert Aufmerksamkeit`,"startup.codexRuntime.clampHidden":`Einige Reasoning-Effort-Optionen wurden ausgeblendet, weil OpenCodex Codex {version} verwendet hat.`,"startup.codexRuntime.clampHiddenWithEfforts":`Einige Reasoning-Effort-Optionen wurden ausgeblendet, weil OpenCodex Codex {version} verwendet hat (entfernt: {efforts}).`,"startup.codexRuntime.olderBinary":`OpenCodex verwendet eine ältere Codex-Binary ({version}). Eine neuere Installation ist verfügbar.`,"dash.subtitle":`Live-Status des lokalen opencodex-Proxys, seiner Anbieter und der in Codex gerouteten Modelle.`,"dash.workspace.overview":`Übersicht`,"dash.workspace.sections":`Abschnitte`,"dash.status":`Status`,"dash.online":`Online`,"dash.offline":`Offline`,"dash.version":`Version`,"dash.uptime":`Laufzeit`,"dash.providers":`Anbieter`,"dash.tokens30d":`Tokens (30d)`,"dash.coverage":`{pct} Abdeckung`,"dash.mem.title":`Speicherbeobachtung`,"dash.mem.hint":`Schreibgeschützte Laufzeitdiagnose. Beobachteter Speicher ist max(RSS, external, ArrayBuffers), damit Windows-Working-Set-Trimming gebundenen Speicher nicht versteckt.`,"dash.mem.rss":`Resident Set (RSS)`,"dash.mem.jsHeap":`JS-Heap belegt`,"dash.mem.jsHeapArena":`Arena {total}`,"dash.mem.pressure":`Gegen Warnschwelle`,"dash.mem.pressureOf":`{pct}% der Schwelle`,"dash.mem.pressureUnknown":`Keine Schwelle gemeldet`,"dash.mem.jscHeap":`JSC-Heap`,"dash.mem.external":`External`,"dash.mem.arrayBuffers":`ArrayBuffers`,"dash.mem.observed":`Beobachtet`,"dash.mem.runtime":`Laufzeit-Zähler`,"dash.mem.growth":`Beobachtete Drift / Stunde`,"dash.mem.perHour":`/Std`,"dash.mem.store":`Fortsetzungsspeicher`,"dash.mem.storeHint":`Proxy-Cache für previous_response_id. Steigende Gesamtbytes bei steigendem Heap deuten auf Konversationsspeicherung hin, nicht auf den Laufzeit-Allokator.`,"dash.mem.storeEntries":`Einträge`,"dash.mem.storeTotal":`Gesamt`,"dash.mem.storeLargest":`Größter`,"dash.mem.storeOldest":`Ältester`,"dash.mem.threshold":`Warnschwelle`,"dash.mem.lastWarn":`Letzte Warnung`,"dash.mem.never":`Nie`,"dash.mem.details":`Details`,"dash.mem.unavailable":`Speicherdiagnose nicht verfügbar (älterer Proxy).`,"dash.mem.inFlight":`Laufende Anfragen`,"dash.mem.restart":`Abwarten & neu starten`,"dash.mem.restartConfirm":`Auf {count} laufende Anfrage(n) warten, dann neu starten (bis zu {seconds}s; Rest wird bei Timeout abgebrochen).`,"dash.mem.draining":`{count} Anfrage(n) werden abgewartet… Neustart danach`,"dash.mem.reconnecting":`Proxy wird neu gestartet… warte auf Verbindung`,"dash.mem.restartFailed":`Abwarten & Neustart fehlgeschlagen. Prüfen Sie, ob der Proxy läuft.`,"dash.mem.restartNoSupervisor":`Kein Neustartschutz erkannt. Der Proxy bleibt nach dem Neustart möglicherweise aus, bis Sie ihn erneut starten.`,"dash.activeProviders":`Aktive Anbieter`,"dash.noProviders":`Keine Anbieter konfiguriert. Führe {cmd} aus.`,"dash.col.name":`Name`,"dash.col.adapter":`Adapter`,"dash.col.baseUrl":`Basis-URL`,"dash.col.model":`Modell`,"dash.modelsNoResults":`Keine Modelle entsprechen deiner Suche.`,"dash.availableModels":`Verfügbare Modelle`,"dash.noModels":`Keine Modelle gefunden. Prüfe die API-Schlüssel des Anbieters.`,"dash.cannotConnect":`Keine Verbindung zum Proxy. Läuft er?`,"dash.runStart":`Führe {cmd} aus, um den Proxy zu starten.`,"dash.stop":`Proxy stoppen`,"dash.stopConfirm":`Proxy stoppen und natives Codex wiederherstellen?`,"dash.stopFailed":`Proxy konnte nicht gestoppt werden (HTTP {status}).`,"dash.maSwitchFailed":`Moduswechsel fehlgeschlagen (HTTP {status}).`,"dash.maNetworkError":`Netzwerkfehler — läuft der Proxy?`,"dash.stopping":`Wird gestoppt…`,"dash.actions":`Proxy`,"dash.codexRestart":`Codex-Modelle neu laden`,"dash.codexRestarting":`Wird gestoppt…`,"dash.codexRestartConfirm":`Codex-App-Server stoppen, damit sie die Modellliste neu laden? Ein laufender Codex-Vorgang wird unterbrochen, und Codex startet nicht von selbst neu — öffne es danach erneut.`,"dash.codexRestartDone":`{count} Codex-App-Server gestoppt. Öffne Codex erneut, um die aktuelle Modellliste zu laden.`,"dash.codexRestartNothing":`Es läuft kein Codex-App-Server. Der nächste Start liest die aktuelle Modellliste.`,"dash.codexRestartUnknown":`Prozesse konnten nicht aufgelistet werden, daher wurde nichts gestoppt.`,"dash.codexRestartPartial":`{count} App-Server wurden nicht beendet. Beende sie manuell, falls die Modellliste veraltet bleibt.`,"dash.codexRestartFailed":`Codex-Modelle konnten nicht neu geladen werden (HTTP {status}).`,"dash.codexRestartUnreachable":`Der Proxy war nicht erreichbar.`,"dash.codexRestartMalformed":`Der Proxy hat eine unerwartete Antwort gesendet.`,"dash.codexRestartTimeout":`Der Proxy hat nicht rechtzeitig geantwortet. Möglicherweise stoppt er noch App-Server.`,"models.staleBanner":`Codex zeigt eine ältere Modellliste als dieser Katalog. Starte Codex neu, um sie neu zu laden.`,"dash.codexAutoStart":`opencodex mit Codex starten`,"dash.codexAutoStartHint":`Erlaubt einem installierten Launcher-Shim, ocx ensure auszuführen. Diese Einstellung installiert keinen Neustartschutz; prüfe den effektiven Zustand unter Startsicherheit.`,"dash.searchModel":`Such-Sidecar-Modell`,"dash.searchModelHint":`Modell für web_search bei nicht über OpenAI gerouteten Modellen. Erfordert ChatGPT-Login.`,"dash.searchReasoning":`Such-Reasoning-Aufwand`,"dash.visionModel":`Vision-Sidecar-Modell`,"dash.visionModelHint":`Modell zur Beschreibung von Bildern für nur-Text-Routen. Erfordert ChatGPT-Login.`,"dash.webSearchSidecar":`Websuche-Sidecar`,"dash.webSearchSidecarHint":`Backend und Modell für die Websuche gerouteter Modelle auswählen.`,"dash.webSearchStream":`Antworten live streamen`,"dash.webSearchStreamHint":`Führenden Text und Reasoning live streamen, bis das Modell über einen Tool-Aufruf entscheidet; der Rest bleibt für das Abfangen der Suche gepuffert. Text vor einer Suche kann sich teilweise wiederholen.`,"dash.visionSidecar":`Vision-Sidecar`,"dash.visionSidecarHint":`Backend und Modell zur Bildbeschreibung für reine Textmodelle auswählen.`,"dash.visionOff":`Aus`,"dash.shadowCallIntercept":`Shadow-Call-Abfangen`,"dash.shadowCallInterceptHint":`Fängt die Hintergrund-Hilfsaufrufe der Codex-App ({models}) ab und leitet sie an das gewählte Modell um.`,"dash.shadowCallWarning":`⚠ Bei Aktivierung werden ALLE Anfragen an {models} durch das gewählte Modell ersetzt.`,"dash.shadowCallOriginal":`Original`,"dash.shadowCallModel":`Ersatzmodell`,"dash.shadowCallTooltip":`Die Codex-App ruft im Hintergrund ein Hilfsmodell für Titelgenerierung, Commit-Nachrichten und Skill-Orchestrierung auf. Das Modell wechselt zwischen Client-Versionen, daher fängt opencodex diesen Satz ab: {models}.`,"models.shadowCallIntercept":`Shadow-Call-Abfangen`,"models.shadowCallInterceptHint":`Fängt die Hintergrund-Hilfsaufrufe der Codex-App ({models}) ab und leitet sie an das gewählte Modell um.`,"dash.sidecarBackend":`Backend`,"dash.sidecarModel":`Modell`,"dash.backendAuto":`Automatisch`,"dash.backendOpenAI":`OpenAI`,"dash.backendAnthropic":`Anthropic`,"dash.sidecarSaved":`Sidecar-Einstellungen gespeichert. Angewendet bei der nächsten Anfrage.`,"dash.sidecarSaveFailed":`Sidecar-Einstellungen konnten nicht gespeichert werden.`,"dash.injectionLabel":`Sub-Agent-Delegation`,"dash.injectionHint":`Wähle das Modell, an das Codex Sub-Agent-Arbeit übergibt. Wo diese Wahl gilt, entscheiden die beiden Schalter unten.`,"dash.syncCodexSubagentDefaults":`Auch als Codex-Standard speichern`,"dash.syncCodexSubagentDefaultsHint":`Eingeschaltet wird die Wahl von oben in Codex' eigene Konfiguration geschrieben, sodass auch neue Aufgaben mit diesem Modell starten. Ausgeschaltet wird sie nur hier gemerkt. Wirksam beim nächsten Sync oder Neustart; deine selbst geschriebenen [agents]-Einstellungen bleiben unberührt.`,"dash.multiAgentGuidance":`Codex sagen, wie Arbeit aufgeteilt wird`,"dash.multiAgentGuidanceHint":`Schickt Codex eine kurze Notiz, wie Arbeit an Sub-Agenten übergeben werden soll. Auf v2 nennt sie die verfügbaren Modelle und das bevorzugte; auf v1 wirkt sie nur bei Reasoning-Effort max oder ultra. Ausgeschaltet wird keine Notiz angehängt.`,"dash.injectionNone":`Keine`,"dash.injectionEffortLabel":`Reasoning-Aufwand`,"dash.injectionEffortNone":`Modell-Standard`,"dash.effortCapLabel":`V2 Ultra Effort-Limit`,"dash.subagentEffortCapLabel":`V2 Sub-Agent Effort-Limit`,"dash.effortCapHelp":`Begrenzt die Reasoning-Intensität für V2-Ultra-Modus-Turns. Wenn gesetzt, werden eingehende Max-Anfragen (aus dem Ultra-Modus) auf das gewählte Niveau begrenzt. Das Sub-Agent-Limit gilt nur für erzeugte Kind-Agenten. Limits senken die Intensität nur, sie erhöhen sie nie. Wenn ein Modell das gewählte Niveau nicht unterstützt, wird automatisch auf das nächste unterstützte Niveau herabgesetzt.`,"dash.effortCapNone":`Kein Limit`,"dash.maintenance":`Wartung`,"dash.maintenanceHint":`Aktualisiere Codex’ Modellkatalog oder installiere eine neuere opencodex-Version.`,"dash.syncModels":`Modelle synchronisieren`,"dash.syncing":`Synchronisiere…`,"dash.syncOk":`Synchronisierung abgeschlossen. {count} Modell(e) angehängt.`,"dash.syncStaleHint":`Falls Codex weiterhin eine alte Liste zeigt, starte den langlebigen App-Server neu ({cmd}).`,"dash.syncFailed":`Synchronisierung fehlgeschlagen: {error}`,"dash.projectConfigTitle":`Projekt-Codex-Konfig umgeht OpenCodex`,"dash.projectConfigHint":`Diese repo-lokalen Einstellungen überschreiben den OpenCodex-Proxy (z. B. direkt zu OpenCode Go routen). Entferne sie, damit die Routing aus ~/.codex/config.toml in diesem Projekt greift.`,"dash.checkUpdate":`Update prüfen`,"dash.updateTitle":`opencodex aktualisieren`,"dash.updateDesc":`Prüfe npm auf den ausgewählten Kanal und wähle dann, ob der Proxy nach der Installation neu gestartet wird.`,"dash.updateChannel":`Kanal`,"dash.updateChecking":`Updates werden geprüft…`,"dash.updateInstalled":`Installiert`,"dash.updateLatest":`Neueste`,"dash.updateAvailable":`Update verfügbar`,"dash.updateCurrent":`Auf dem neuesten Stand`,"dash.updateCommand":`Befehl`,"dash.updateSource":`Dies ist ein Source-Checkout. Aktualisiere es im Terminal mit dem angezeigten Befehl.`,"dash.updateUnavailable":`Die neueste Version konnte nicht von npm gelesen werden. Versuche es später erneut.`,"dash.updateRetry":`Wiederholen`,"dash.updateRecheck":`Erneut prüfen`,"dash.updateCannotAuto":`Ein-Klick-Update ist nicht verfügbar ({reason}).`,"dash.updateReason.source_checkout":`Quellcode-Checkout`,"dash.updateReason.latest_unavailable":`npm-Registry nicht erreichbar`,"dash.updateReason.already_latest":`bereits auf dem neuesten Stand`,"dash.updateReason.unknown":`Update nicht verfügbar`,"dash.updateRestart":`Nach Update neu starten`,"dash.updateRestartHint":`Empfohlen. Die aktuelle GUI läuft weiter mit altem Code, bis der Proxy neu startet.`,"dash.runUpdate":`Aktualisieren`,"dash.updateReconnecting":`Warten auf den neu gestarteten Proxy…`,"dash.updateStatus.running":`opencodex wird aktualisiert.`,"dash.updateStatus.restarting":`Update installiert. Proxy wird neu gestartet.`,"dash.updateStatus.succeeded":`Update abgeschlossen.`,"dash.updateVersionTransition":`{currentVersion} -> {latestVersion}.`,"dash.updateStatus.failed":`Update fehlgeschlagen.`,"prov.subtitle":`Konfiguriere die Upstream-Anbieter, die opencodex in Codex routet. Melde dich mit einem Konto an, füge einen Anbieter hinzu oder bearbeite die Rohkonfiguration.`,"prov.add":`Anbieter hinzufügen`,"prov.editJson":`JSON bearbeiten`,"prov.accountLogin":`Konto-Login`,"prov.noOauth":`Keine OAuth-Anbieter verfügbar.`,"prov.loggedIn":`angemeldet`,"prov.notLoggedIn":`nicht angemeldet`,"prov.logout":`Abmelden`,"prov.login":`Anmelden`,"prov.loginWith":`Anmelden mit {provider}`,"prov.waitingBrowser":`Warten auf Browser…`,"prov.didntOpen":`Hat sich nicht geöffnet? Hier klicken`,"prov.copyLink":`Link kopieren`,"prov.dontOpenBrowser":`Keinen Browser auf dem Proxy-Rechner öffnen`,"prov.dontOpenBrowserHint":`Nützlich für ein anderes Browser-Profil oder wenn das Dashboard nicht auf dem Proxy-Rechner läuft.`,"prov.linkCopied":`Kopiert`,"prov.linkCopyUnavailable":`Zwischenablage nicht verfügbar`,"prov.deviceCode":`Gerätecode`,"prov.copyCode":`Code kopieren`,"prov.codeCopied":`Code kopiert`,"prov.editAlias":`Alias bearbeiten`,"prov.aliasPrompt":`Anzeigename (leer lassen zum Entfernen)`,"prov.aliasSaved":`Alias gespeichert`,"prov.aliasSaveFailed":`Alias konnte nicht gespeichert werden`,"prov.accountId":`ID`,"prov.pasteRedirect":`Redirect-URL oder Code einfügen`,"prov.pasteRedirectHint":`Zeigt der Browser einen localhost-Fehler, kopiere die vollständige URL aus der Adressleiste und füge sie hier ein (oder den Autorisierungscode).`,"prov.pasteSubmit":`Senden`,"prov.pasteSubmitting":`Wird gesendet…`,"prov.pasteOk":`Code gesendet — Anmeldung wird abgeschlossen…`,"prov.pasteFail":`Code konnte nicht gesendet werden: {error}`,"prov.port":`Port`,"prov.default":`Standard`,"prov.loadingConfig":`Lädt…`,"prov.saved":`Gespeichert! Proxy neu starten, um anzuwenden.`,"prov.loadConfigFail":`Konfiguration konnte nicht geladen werden`,"prov.invalidJson":`Ungültiges JSON`,"prov.saveFailed":`Speichern fehlgeschlagen`,"prov.loginFailStart":`{provider}-Login konnte nicht gestartet werden`,"prov.loginError":`{provider}-Login-Fehler: {error}`,"prov.loginRequestFail":`{provider}-Login-Anfrage fehlgeschlagen`,"prov.loginCancelled":`{provider}-Login abgebrochen`,"prov.loginTimeout":`{provider}-Login abgelaufen — Browser geschlossen oder nicht beendet. Erneut versuchen.`,"prov.loginOk":`Bei {provider} angemeldet. Führe {cmd} aus (oder es gilt live), um seine Modelle aufzulisten.`,"prov.loginSameAccount":`Immer noch dasselbe {provider}-Konto — wechsle im Browser das Konto und versuche „Konto hinzufügen“ erneut.`,"oauthTos.highTitle":`{provider}: Risiko bei Abo-OAuth`,"oauthTos.elevatedTitle":`{provider}: inoffizielle OAuth-Brücke`,"oauthTos.anthropicBody":`Die direkte Wiederverwendung von Claude-Abo-OAuth-Tokens über einen Drittanbieter-Proxy wie OpenCodex ist keine von Anthropic unterstützte Integration und kann zu Zugriffsbeschränkungen führen. Unterstützte Agent-SDK-Integrationen, die Claude-Abos verwenden, sind davon getrennt.`,"oauthTos.highBody":`OpenCodex verbindet {provider} über einen OAuth-Pfad eines Drittanbieters. Bei nicht unterstützter Nutzung kann der Zugriff eingeschränkt oder gesperrt werden.`,"oauthTos.elevatedBody":`OpenCodex verbindet {provider} über einen inoffiziellen OAuth-Pfad. Nutze nach Möglichkeit den offiziellen Client; ungewöhnlicher oder automatisierter Traffic kann als Missbrauch gewertet und der Zugriff eingeschränkt oder gesperrt werden.`,"oauthTos.saferPath":`Sicherere Option: Hinterlege stattdessen einen API-Schlüssel in OpenCodex.`,"oauthTos.acknowledge":`Ich verstehe das Risiko und möchte trotzdem mit OAuth fortfahren.`,"oauthTos.continue":`Mit OAuth fortfahren`,"prov.logoutOk":`Von {provider} abgemeldet.`,"prov.logoutFail":`Abmeldung von {provider} fehlgeschlagen. Der Kontostatus bleibt unverändert.`,"prov.removed":`"{name}" entfernt.`,"prov.removedDefault":`"{name}" entfernt. Standardanbieter ist jetzt "{defaultProvider}".`,"prov.removeFail":`"{name}" konnte nicht entfernt werden.`,"prov.removeLastProvider":`Der Standardanbieter kann nicht entfernt werden, wenn kein anderer aktivierter Anbieter Standard werden kann.`,"prov.removeHasDependentCombos":`Entferne oder aktualisiere zuerst diese abhängigen Combos: {combos}.`,"prov.setDefault":`Als Standard festlegen`,"prov.setDefaultSuccess":`"{name}" ist jetzt der Standardanbieter.`,"prov.setDefaultFail":`"{name}" konnte nicht als Standardanbieter festgelegt werden.`,"prov.defaultDisabled":`Aktiviere diesen Anbieter, bevor du ihn als Standard festlegst.`,"prov.updateFail":`Dieser Anbieter konnte nicht aktualisiert werden.`,"prov.networkError":`Netzwerkfehler. Prüfe, ob der Proxy läuft, und versuche es erneut.`,"prov.added":`"{name}" hinzugefügt. Sofort aktiv — führe {cmd} aus (oder starte neu), um seine Modelle in Codex’ Auswahl zu listen.`,"prov.removeConfirm":`Anbieter "{name}" entfernen? Seine Modelle verschwinden aus Codex’ Auswahl.`,"prov.hasApiKey":`API-Schlüssel konfiguriert`,"prov.hasHeaders":`benutzerdefinierte Header konfiguriert`,"prov.accounts":`Konten ({n})`,"prov.accountsAria":`{name}-Konten umschalten`,"prov.accountActive":`Aktiv`,"prov.accountReauth":`Erneut anmelden`,"prov.reauthenticate":`Erneut authentifizieren`,"prov.reauthAccountMissing":`Ausgewähltes Konto nach dem Login nicht gefunden`,"prov.reauthIdentityMismatch":`Angemeldetes Konto stimmt nicht mit dem ausgewählten Konto überein`,"prov.accountAdd":`Konto hinzufügen`,"prov.accountNoLabel":`Konto {id}`,"prov.accountSwitchTitle":`Dieses Konto verwenden`,"prov.accountSwitched":`Zu {email} gewechselt.`,"prov.accountSwitchFail":`Konto-Wechsel fehlgeschlagen`,"prov.accountRemoved":`{email} entfernt.`,"prov.accountRemoveFail":`{email} konnte nicht entfernt werden. Das Konto bleibt unverändert.`,"prov.accountRemoveAria":`{email} entfernen`,"prov.accountRemoveConfirm":`Konto {email} entfernen? Sein Login wird aus diesem Proxy gelöscht.`,"prov.keyAdd":`API-Schlüssel hinzufügen`,"prov.keyAdded":`API-Schlüssel zu {name} hinzugefügt.`,"prov.keyAddFail":`API-Schlüssel konnte nicht hinzugefügt werden`,"prov.keyPlaceholder":`API-Schlüssel einfügen`,"prov.keySwitchTitle":`Diesen Schlüssel verwenden`,"prov.keySwitched":`Zu Schlüssel {key} gewechselt.`,"prov.keySwitchFail":`Schlüssel-Wechsel fehlgeschlagen`,"prov.keyRemoved":`Schlüssel {key} entfernt.`,"prov.keyRemoveAria":`Schlüssel {key} entfernen`,"prov.keyRemoveConfirm":`API-Schlüssel {key} entfernen? Er wird aus der Proxy-Konfiguration gelöscht.`,"prov.activeBadge":`Aktiv`,"prov.disabledBadge":`Deaktiviert`,"prov.defaultBadge":`Standard`,"prov.enable":`Aktivieren`,"prov.disable":`Deaktivieren`,"prov.enabled":`"{name}" aktiviert. Seine Modelle können wieder in Codex erscheinen.`,"prov.disabled":`"{name}" deaktiviert. Einstellungen bleiben erhalten, aber seine Modelle sind verborgen.`,"prov.enableFail":`"{name}" konnte nicht aktiviert werden.`,"prov.disableFail":`"{name}" konnte nicht deaktiviert werden.`,"prov.enableAria":`Anbieter {name} aktivieren`,"prov.disableAria":`Anbieter {name} deaktivieren`,"prov.defaultCannotDisable":`Standard-Anbieter kann nicht deaktiviert werden`,"prov.openaiAccountMode":`Codex-Kontomodus`,"prov.openaiModePool":`Pool`,"prov.openaiModeDirect":`Direkt`,"prov.openaiPoolDesc":`Standard. Wechselt mit Affinität, Kontingent, Abklingzeit und Ausfallsicherung zwischen Hauptanmeldung und hinzugefügten Konten.`,"prov.openaiDirectDesc":`Verwendet nur die aktuelle primäre Codex-Anmeldung. Gespeicherte Pool-Konten werden weder gelesen noch gewechselt.`,"prov.openaiModeSaved":`OpenAI-Kontomodus wurde zu {mode} geändert.`,"prov.openaiModeSaveFailed":`Der OpenAI-Kontomodus konnte nicht geändert werden.`,"prov.openaiApiDesc":`Verwendet nur einen OpenAI-API-Schlüssel und keine Codex-Kontodaten.`,"prov.manageCodexAccounts":`Codex-Konten verwalten`,"prov.openaiApiMissing":`API-Schlüssel erforderlich`,"prov.openaiApiSetup":`API-Schlüssel einrichten`,"models.tab.catalog":`Modelle`,"models.tab.combos":`Combos`,"models.tab.compatibility":`Kompatibilität`,"models.tab.routing":`Routing (beta)`,"models.tabsLabel":`Modell-Oberflächen`,"models.subtitle.combos":`Geordnete Modellgruppen, die unter einer id antworten. Ziele mit Failover verketten oder die Last mit einer Balancing-Strategie verteilen.`,"models.subtitle.compatibility":`Schreibgeschützte Kompatibilitätsmatrix aus der Lab-Projektion.`,"models.subtitle.routing":`Policy-Profile, Dry-Run-Auswertung und quellenbasierte Routing-Analysen.`,"models.subtitle":`Steuere, welche Modelle Codex sieht — natives GPT-Passthrough und geroutete Anbieter, nach Anbieter gruppiert (Kopfzeile zum Einklappen anklicken). Ausgeblendete Modelle fehlen in Katalog und Auswahl, bleiben aber per genauer ID aufrufbar. Änderungen gelten bei der nächsten Codex-Runde — opencodex invalidiert Codex 5-Minuten-Modell-Cache, kein Neustart nötig.`,"models.nativeGroupLabel":`OpenAI nativ`,"models.nativeHint":"Passthrough-Modelle verwenden die unter Anbieter gewählte Pool- oder Direkt-Option. Ausblenden entfernt sie aus der Codex-Auswahl (Katalogeintrag bleibt, Reaktivierung stellt exakt wieder her). Ein hier hinzugefügtes Modell wird als gerouteter `openai/`-Selektor registriert, nicht als neue reine Passthrough-ID.","models.active":`{active}/{total} sichtbar`,"models.workspace.providers":`Anbieter`,"models.workspace.allProviders":`Alle Anbieter`,"models.workspace.mainAria":`Modelldetails`,"models.allOn":`Alle an`,"models.allOff":`Alle aus`,"models.presetLabel":`Modelle`,"models.presetMode_preset":`Voreinstellung`,"models.presetMode_all":`Alle`,"models.presetMode_custom":`Eigene`,"models.presetSummary":`{count} von {total} angezeigt — Core-Voreinstellung v{version}`,"models.presetUpdateAvailable":`Voreinstellung v{version} verfügbar`,"models.presetAppliedToast":`{provider}: Voreinstellung angewendet — {count} Modelle ausgewählt`,"models.presetClearedToast":`{provider}: alle Modelle werden angezeigt`,"models.presetEmpty":`{provider}: Voreinstellung traf auf kein Modell zu — Auswahl unverändert`,"models.presetConfirmReplace":`Auswahl durch die Voreinstellung mit {count} Modellen ersetzen?`,"models.cap350k":`Limit 350k`,"models.capApplied":`Kontext-Limit angewendet — greift bei der nächsten Codex-Runde.`,"models.capSaveFailed":`Kontext-Limit konnte nicht gespeichert werden`,"models.contextCapped":`350k-Limit`,"models.contextCapLabel":`Standardfenster / Limit`,"models.v2Label":`Sub-Agent`,"models.shadowCallOriginal":`⚠ {models} →`,"models.v2DocsLink":`Was ist v1 / v2?`,"models.v2Mode_v1":`v1`,"models.v2Mode_default":`base`,"models.v2Mode_v2":`v2`,"models.v2ModeDesc_v1":`Alle Modelle → v1-Oberfläche`,"models.v2ModeDesc_default":`Upstream-Standard (sol/terra=v2, luna=v1)`,"models.v2ModeDesc_v2":`Alle Modelle → v2-Oberfläche`,"models.keepNativeOnV1":`ChatGPT auf v1 lassen`,"models.keepNativeOnV1Hint":`ChatGPT-native Eltern verschlüsseln v2-Kindaufgaben — Grok und Claude können sie nicht lesen. An lassen, wenn Sol/Terra weiterhin geroutete Modelle starten sollen. Geroutete Eltern bleiben auf v2.`,"models.v2Help":`Steuert die Multi-Agent-Oberfläche für alle Modelle. + +v1: Klassischer Single-Thread-Agent. Jedes Modell nutzt die v1-Collab-Oberfläche. +base: Upstream-Standard — sol/terra nutzen v2, luna v1, andere folgen dem Codex-Feature-Flag. +v2: Multi-Thread-Agent mit spawn_agent. Jedes Modell nutzt die v2-Collab-Oberfläche. + +Unter v2 lässt „ChatGPT auf v1 lassen“ Sol/Terra auf v1, damit sie weiter Grok oder Claude starten können. ChatGPT verschlüsselt v2-Kindaufgaben; geroutete Modelle können sie nicht lesen. Geroutete Eltern bleiben auf v2. + +Änderungen gelten für neue Sitzungen.`,"dash.multiAgent":`Sub-Agent`,"models.v2Conflict":`[agents] max_threads ist gesetzt — codex verweigert den Start; entferne es aus config.toml`,"models.v2Applied":`Sub-Agent-Modus aktualisiert — gilt für neue Sitzungen (Codex-App neu starten, um die Auswahl zu aktualisieren)`,"models.v2ThreadsLabel":`Max. Threads`,"models.v2ThreadsDefault":`Standard (4)`,"models.v2ThreadsApplied":`Thread-Limit aktualisiert — gilt für neue Sitzungen`,"models.v2ThreadsInvalid":`Thread-Limit muss eine ganze Zahl >= 1 sein`,"models.v2ThreadsApply":`Anwenden`,"models.capValue":`Standard {value}`,"models.contextSettings":`Eigene Fenster`,"models.contextSettingsTitle":`Eigene Fenster — {provider}`,"models.contextDefault":`Anbieterstandard`,"models.contextModel":`Modell`,"models.contextModelOverride":`Modellüberschreibung`,"models.contextHint":`Wenn das Fenster bekannt ist, tragen Sie hier das tatsächliche Codex-Fenster ein. Fehlt ein Upstream-Wert, gilt dieser Eintrag; ein größeres gemeldetes Fenster wird nur nach unten begrenzt, ein kleineres bleibt erhalten. Leer bedeutet das Anbieter-«Standardfenster / Limit», oder 128k wenn das Limit aus ist.`,"models.contextAutomatic":`Automatische Erkennung`,"models.contextSaved":`Kontextfenster aktualisiert — gilt ab der nächsten Codex-Runde.`,"models.contextUnchanged":`Keine Änderungen am Kontextfenster zu speichern.`,"models.contextSaveFailed":`Kontextfenster konnten nicht gespeichert werden`,"models.contextInvalid":`Kontextfenster müssen positive ganze Zahlen sein`,"models.contextCappedValue":`{value}-Limit`,"models.setAll":`Alle setzen`,"models.setAllHint":`Schaltet das Standardfenster {value} für alle gerouteten Anbieter ein. Fehlen context_window / context_length, wird dieser Wert das tatsächliche Codex-Fenster. Für ein einzelnes Modell nutzen Sie «Eigene Fenster» in derselben Zeile. Native Anbieter bleiben unberührt.`,"models.collapseAll":`Alle einklappen`,"models.expandAll":`Alle ausklappen`,"models.orderHint":`Reihenfolge in der Modellauswahl: Subagents-Auswahl (in der festgelegten Reihenfolge) → übrige geroutete Modelle alphabetisch nach Anbieter, dann Modell-ID → native Modelle. Sichtbarkeitsschalter filtern nur; sie ändern diese Reihenfolge nicht.`,"models.custom":`Benutzerdefiniert…`,"models.customApply":`Anwenden`,"models.customPlaceholder":`Tokens (z. B. 420000)`,"models.customAdd":`Benutzerdefiniertes Modell hinzufügen`,"models.customAddTitle":`Benutzerdefiniertes Modell hinzufügen — {provider}`,"models.customEditTitle":`Benutzerdefiniertes Modell bearbeiten — {provider}`,"models.customAdded":`Benutzerdefiniertes Modell hinzugefügt`,"models.customUpdated":`Benutzerdefiniertes Modell aktualisiert`,"models.customDeleted":`Benutzerdefiniertes Modell gelöscht`,"models.customSaveFailed":`Benutzerdefiniertes Modell konnte nicht gespeichert werden`,"models.customSaving":`Wird gespeichert…`,"models.customAddBtn":`Hinzufügen`,"models.customEditBtn":`Aktualisieren`,"models.customEdit":`Bearbeiten`,"models.customDelete":`Löschen`,"models.customDeleteConfirm":`Modell {name} löschen?`,"models.customBadge":`Benutzerdefiniert`,"models.customSummary":`{count} benutzerdefiniert`,"models.customFieldModelId":`Modell-ID (Endpunkt-Slug)`,"models.customFieldModelIdPlaceholder":`z. B. qwen4-max-preview`,"models.customFieldDisplayName":`Anzeigename (optional)`,"models.customFieldDisplayNamePlaceholder":`z. B. Qwen 4 Max Preview`,"models.customFieldContext":`Kontextfenster`,"models.customFieldModalities":`Eingabemodalitäten`,"models.customFieldReasoning":`Reasoning-Aufwand`,"models.customFieldReasoningOverride":`Reasoning-Aufwand überschreiben`,"models.reasoningEffort.none":`Keine`,"models.reasoningEffort.minimal":`Minimal`,"models.reasoningEffort.low":`Niedrig`,"models.reasoningEffort.medium":`Mittel`,"models.reasoningEffort.high":`Hoch`,"models.reasoningEffort.xhigh":`Sehr hoch`,"models.reasoningEffort.max":`Maximal`,"models.tipProvider":`Anbieter`,"models.tipContext":`Kontext`,"models.tipModalities":`Modalitäten`,"models.tipStatus":`Status`,"models.tipActive":`Aktiv`,"models.tipDisabled":`Deaktiviert`,"models.applied":`Angewendet — greift bei der nächsten Codex-Runde.`,"models.saveFailed":`Speichern fehlgeschlagen`,"models.networkError":`Netzwerkfehler — läuft der Proxy?`,"models.loadFail":`Modelle konnten nicht geladen werden — läuft der Proxy?`,"models.noRouted":`Keine gerouteten Modelle`,"models.noRoutedHint":`Melde dich zuerst bei einem Anbieter an oder füge einen hinzu.`,"models.emptyDiscovery":`Es wurden keine Modelle gefunden. Prüfe den Anbieter-Endpunkt oder füge ein statisches/eigenes Modell hinzu.`,"models.emptyDiscoveryDisabled":`Die Live-Modellerkennung ist aus und es sind keine statischen Modelle konfiguriert.`,"models.discoveryFailedBadge":`Erkennung fehlgeschlagen`,"models.discoveryFailedHttp":`Die Modellerkennung ist fehlgeschlagen (HTTP {status}).`,"models.discoveryFailedBlocked":`Die Modellerkennung wurde durch die Zielrichtlinie blockiert.`,"models.discoveryFailedInvalidResponse":`Die Modellerkennung lieferte eine ungültige Antwort.`,"models.discoveryFailedNetwork":`Die Modellerkennung ist an einem Netzwerkfehler gescheitert.`,"models.discoveryFailedProvider":`Der Anbieter meldete einen Fehler bei der Modellerkennung.`,"models.discoveryFailedGeneric":`Die Modellerkennung ist fehlgeschlagen.`,"models.openProviderSettings":`Anbietereinstellungen öffnen`,"models.loading":`Lädt…`,"models.search":`Modelle suchen…`,"models.showMore":`{n} weitere anzeigen`,"models.allowlistLabel":`Nur ausgewählte`,"models.allowlistHint":`Nur geprüfte Modelle gehen in den Katalog (leer = alle). Nützlich für Anbieter mit tausenden Modellen.`,"models.selectedCount":`{n} ausgewählt`,"sub.subtitle":`Codex {cmd} bewirbt nur die ersten 5 Modelle (nach Priorität) als Overrides. Wähle hier bis zu 5 — natives gpt oder geroutet — und opencodex setzt ihre Katalog-Priorität, sodass genau diese führen. Jedes andere Modell bleibt über seinen exakten Namen aufrufbar; dies steuert nur die Anzeige.`,"sub.featured":`Empfohlen`,"sub.advanced":`Erweitert`,"sub.orderHintAria":`Wie diese Reihenfolge verwendet wird`,"sub.orderHint":`Die hier gewählte und angezeigte Reihenfolge bestimmt die Plätze 1–5 oben in der Codex-Modellauswahl und die Standard-Modellkandidaten für {cmd}.`,"sub.noneSelected":`Nichts ausgewählt — wähle aus der Liste unten.`,"sub.models":`Modelle`,"sub.search":`Modelle suchen (nativ gpt + geroutet)…`,"sub.noModels":`Keine Modelle — melde dich zuerst bei einem Anbieter an oder füge einen hinzu.`,"sub.saved":`{n} Modelle gespeichert. Starte eine neue Codex-Sitzung (oder führe {cmd} aus), um sie als spawn_agent-Overrides zu sehen.`,"sub.saveFailed":`Speichern fehlgeschlagen`,"sub.networkError":`Netzwerkfehler — läuft der Proxy?`,"sub.loadFail":`Modelle konnten nicht geladen werden — läuft der Proxy?`,"sub.loading":`Lädt…`,"sub.moveUp":`{m} nach oben`,"sub.moveDown":`{m} nach unten`,"sub.removeAria":`{m} entfernen`,"sub.workspace.addToFeatured":`{m} zu Hervorgehobenen hinzufügen`,"sub.workspace.allModels":`Alle Modelle`,"sub.workspace.featuredFull":`Hervorgehobene Liste ist voll (max. 5)`,"sub.workspace.mainAria":`Subagent-Modelldetails`,"sub.workspace.notFeatured":`Nicht hervorgehoben`,"sub.workspace.priority":`Priorität`,"sub.workspace.removeFromFeatured":`{m} aus Hervorgehobenen entfernen`,"sub.workspace.selectModel":`Modell auswählen`,"sub.workspace.selectModelDesc":`Wählen Sie ein Modell aus der Liste, um Details anzuzeigen und es für spawn_agent hervorzuheben.`,"sub.workspace.selector":`Öffentlicher Selektor`,"sub.ultraMode":`Ultra-Modus`,"sub.ultraModeHint":`Aktiviert die proaktive Multi-Agent-Delegierungsrichtlinie für alle Modelle und Reasoning-Efforts (ändert den Reasoning-Effort selbst nicht). Schreibt features.multi_agent_v2.multi_agent_mode_hint_text in config.toml.`,"sub.ultraModeV2Required":`Erfordert die v2-Multi-Agent-Oberfläche — aktivieren Sie zuerst multi_agent_v2 und wählen Sie v2 in der Subagentenmodus-Steuerung.`,"sub.ultraModeText":`Delegierungstext des Ultra-Modus`,"sub.ultraModePreset":`Voreinstellung wiederherstellen`,"sub.ultraModeLoadFail":`Ultra-Modus-Einstellungen konnten nicht geladen werden — läuft der Proxy?`,"sub.ultraModeSaveFail":`Ultra-Modus-Einstellungen konnten nicht gespeichert werden`,"sub.ultraModeSaved":`Ultra-Modus gespeichert. Gilt für neue Codex-Sitzungen.`,"logs.title":`Anfrage-Protokolle`,"logs.tabLogs":`Protokolle`,"logs.tabDebug":`Diagnose`,"logs.subtitle":`Letzte Anfragen über den lokalen opencodex-Proxy, neueste zuerst.`,"logs.autoRefresh":`Auto-Aktualisierung`,"logs.noRequests":`Noch keine Anfragen.`,"logs.loadError":`Anfrageprotokolle konnten nicht geladen werden.`,"logs.filter.surface.label":`Oberfläche`,"logs.filter.surface.all":`Alle`,"logs.filter.surface.claude":`Claude`,"logs.filter.surface.codex":`Codex`,"logs.filter.surface.grok":`Grok`,"logs.filter.interceptedHelpersOnly":`Nur abgefangene Helfer`,"logs.badge.interceptedHelper":`I · {model}`,"logs.badge.interceptedHelperTitle":`Abgefangene Helfer-Anfrage`,"logs.filter.conversation.label":`Konversation`,"logs.filter.conversation.placeholder":`Konversations-ID einfügen`,"logs.filter.conversation.clear":`Löschen`,"logs.filter.model.label":`Modell`,"logs.filter.model.placeholder":`Modell oder Anbieter filtern`,"logs.filter.conversation.apply":`Logs filtern`,"logs.conversation.totals":`{requests} Anfragen · {tokens} Tokens · {cost}`,"logs.conversation.scope":`Summen gelten nur für den aktuell geladenen Logs-Ring.`,"logs.conversation.excluded":`({unpriced} ohne Preis, {unmetered} ohne Messung vom ~$ ausgenommen)`,"logs.cost.approximate":`{amount}`,"logs.cost.lowerBound":`≥{amount}`,"logs.cost.unavailable":`nicht verfügbar`,"logs.detail.conversation":`Konversation`,"logs.badge.claude":`Claude`,"logs.badge.grok":`Grok`,"logs.col.time":`Zeit`,"logs.col.request":`Anfrage`,"logs.col.model":`Modell`,"logs.col.effort":`Aufwand`,"logs.col.provider":`Anbieter`,"logs.col.status":`Status`,"logs.col.tokens":`Tokens`,"logs.col.tokPerSec":`tok/s`,"logs.col.estimatedCost":`~$`,"logs.metric.tokPerSecTitle":`Ausgabe-Tokens pro Sekunde über die gesamte Anfragedauer`,"logs.metric.estimatedCostTitle":`API-Listenpreis-Äquivalent, keine tatsächliche Belastung; bei fehlendem Preisabgleich nicht verfügbar`,"usage.cost.total":`API-Listenpreis-Äquivalent (dieser Zeitraum)`,"usage.cost.disclaimer":`Kein Abrechnungsbeleg. Stattdessen können Abonnementnutzung oder Anbieter-Guthaben gelten.`,"usage.cost.unpricedNote":`{count} Anfragen ohne Preis oder Nutzung ausgeschlossen`,"logs.detail.section.basic":`Grundinformationen`,"logs.detail.route.section":`Route-Entscheidung`,"logs.detail.route.kind":`Route-Typ`,"logs.detail.route.profile":`Profil`,"logs.detail.route.selected":`Ausgewählt`,"logs.detail.route.candidates":`Kandidaten`,"logs.detail.route.unknown":`Für diese Anfrage wurde keine Route-Entscheidung aufgezeichnet (Zeile vor dem Trace).`,"logs.detail.section.performance":`Leistung`,"logs.detail.section.cost":`API-Listenpreis-Äquivalent`,"logs.detail.section.attempts":`Combo-Versuche`,"logs.detail.section.usage":`Roh-Nutzung`,"logs.detail.ttft":`TTFT`,"logs.detail.costTotal":`Listenpreis-Äquivalent`,"logs.detail.totalTokens":`Tokens gesamt`,"logs.detail.matchedKey":`Zugeordneter Preisschlüssel`,"logs.detail.priceSource":`Preisquelle`,"logs.detail.unavailableReason":`Grund der Nichtverfügbarkeit`,"logs.detail.copyRequestId":`Anfrage-ID kopieren`,"logs.detail.copied":`Kopiert`,"logs.detail.source.jawcode":`jawcode-Katalog`,"logs.detail.source.expected":`Expected-Preis-Overlay`,"logs.detail.source.user":`Anbieter-konfiguriertes Preis-Overlay`,"logs.detail.verification.verified":`Verifiziert`,"logs.detail.verification.derived":`Vom Basismodell abgeleitet`,"logs.detail.attempt.target":`Anbieter / Modell`,"logs.detail.attempt.reason":`Ergebnis / Grund`,"logs.detail.attempt.completed":`Abgeschlossen`,"logs.detail.attempt.e2eNote":`Tok/s auf oberster Ebene ist Ende-zu-Ende; jeder Versuch nutzt seine eigene Dauer.`,"logs.detail.attempt.recovery.transient5xx":`Vorübergehender 5xx-Fehler`,"logs.detail.attempt.recovery.connectionReset":`Verbindung zurückgesetzt`,"logs.detail.attempt.recovery.oauth401":`OAuth-Neuanmeldung`,"logs.detail.attempt.recovery.key429":`Schlüssel ratenbegrenzt (429)`,"logs.detail.attempt.recovery.rateLimit429":`Ratenbegrenzt (429)`,"logs.detail.attempt.recovery.anthropicOauth429":`Anthropic OAuth ratenbegrenzt (429)`,"logs.detail.attempt.recovery.image413":`Bildnutzlast zu groß (413)`,"logs.detail.attempt.recovery.emptyCompletion":`Wiederholung nach leerer Antwort`,"logs.detail.attempt.recovery.unknown":`Unbekannter Wiederherstellungsgrund`,"logs.detail.reason.usage_missing":`Nutzung wurde nicht gemeldet.`,"logs.detail.reason.usage_unsupported":`Dieser Anbieter meldet keine Nutzung.`,"logs.detail.reason.output_missing":`Es wurden keine positiven Ausgabe-Tokens gemeldet.`,"logs.detail.reason.invalid_duration":`Die Anfragedauer ist ungültig.`,"logs.detail.reason.price_unmatched":`Kein passender Preis gefunden.`,"logs.detail.reason.invalid_cache_breakdown":`Cache-Token-Details widersprechen den Eingabe-Tokens.`,"logs.detail.reason.invalid_usage":`Die Nutzung enthält einen ungültigen Token-Wert.`,"logs.detail.reason.combo_attempt_unavailable":`Mindestens ein Combo-Versuch konnte nicht bepreist werden.`,"logs.detail.estimate.usage_estimated":`Die Anbieternutzung ist geschätzt.`,"logs.detail.estimate.cache_detail_missing":`Cache-Details fehlen; Eingabe ist als Obergrenze geschätzt.`,"logs.detail.estimate.expected_price_overlay":`Ein verifizierter Expected-Listenpreis wurde verwendet.`,"logs.detail.estimate.provider_cost_overlay":`Ein vom Anbieter konfiguriertes Preis-Overlay wurde verwendet.`,"logs.detail.estimate.priority_lower_bound":`Der bestätigte Priority-Preis ist nicht verfügbar; die angezeigte Schätzung ist eine bekannte Untergrenze.`,"logs.col.error":`Fehler`,"logs.col.upstreamReason":`Upstream-Grund`,"logs.col.duration":`Dauer`,"logs.modelTooltip.model":`Modell`,"logs.modelTooltip.resolvedModel":`aufgelöstes Modell`,"logs.modelTooltip.requestedTier":`angeforderte Stufe`,"logs.modelTooltip.configuredTier":`konfigurierte Stufe`,"logs.modelTooltip.responseTier":`Antwortstufe`,"logs.modelTooltip.supportsTier":`Stufenunterstützung`,"logs.tokens.reported":`gemeldet`,"logs.tokens.unreported":`nicht gemeldet`,"logs.tokens.unsupported":`nicht unterstützt`,"logs.tokens.estimated":`geschätzt`,"logs.tokens.input":`Eingabe`,"logs.tokens.output":`Ausgabe`,"logs.tokens.cacheRead":`Cache-Treffer (c)`,"logs.tokens.cacheWrite":`Cache-Schreiben (w)`,"logs.tokens.reasoning":`Reasoning`,"logs.tokens.noCache":`keine Cache-Daten`,"logs.tokens.contextTotal":`aktiver Kontext`,"logs.tokens.noCacheNote":`dieser Anbieter meldet keine Cache-Tokens`,"logs.tokens.noCacheCursor":`Cursor-Cache-Details nicht gemeldet`,"logs.tokens.noCacheCursorNote":`Cursor liefert keine Cache-Read/Write-Tokenzahlen; das ist unbekannt und kein bestätigter Cache-Miss`,"logs.tokens.estimatedNote":`Schätzung (Anbieter meldet keine exakte Nutzung)`,"logs.details":`Details`,"logs.detailTitle":`Anfragedetails`,"logs.detailRaw":`Roh-Protokolleintrag`,"debug.title":`Fehlerdiagnose`,"debug.subtitle":`Opt-in-Diagnose für Provider-Transport und Nutzungs-Extraktion. Anfragefehler und 502er bleiben im Protokolle-Tab.`,"debug.debug":`Provider-Diagnose`,"debug.usage":`Nutzungs-Extraktion`,"debug.injection":`Injektions-Log`,"debug.claude":`Claude-Inbound`,"debug.claudeInbound.title":`Claude-Inbound-Anfragen`,"debug.claudeInbound.sub":`Zeigt, was Claude Code/Desktop tatsächlich sendet (thinking, effort, metadata) — kein Prompt-Text wird gespeichert.`,"debug.claudeInbound.empty":`Noch keine Anfragen erfasst. Sende bei aktivierter Erfassung eine Nachricht aus Claude.`,"debug.claudeInbound.time":`Zeit`,"debug.claudeInbound.endpoint":`Endpunkt`,"debug.claudeInbound.model":`Modell`,"debug.claudeInbound.none":`keine`,"debug.reset":`Laufzeit-Überschreibungen löschen`,"debug.refresh":`Aktualisieren`,"debug.follow":`Folgen`,"debug.streamProvider":`Anbieter`,"debug.streamUsage":`Nutzung`,"debug.streamInjection":`Injektion`,"debug.loading":`Lade Diagnose-Einstellungen…`,"debug.loadFailed":`Diagnose-Einstellungen konnten nicht geladen werden.`,"debug.emptyTitle":`Diagnose-Logging ist aus`,"debug.empty":`Aktiviere Provider-Diagnose oder Nutzungs-Extraktion in der Karte oben. Zeilen erscheinen hier, nachdem du eine Anfrage über den Proxy gesendet hast.`,"debug.noLinesTitle":`Warte auf Zeilen`,"debug.noLines.provider":`Anbieter-Debug ist an, erfasst aber nur Transport-Anomalien (verworfene oder fehlerhafte Frames sowie Cursor-Dial/Retry-Ereignisse). Eine saubere Anfrage über einen Anbieter wie Anthropic kann null Zeilen erzeugen.`,"debug.noLines.usage":`Nutzungserfassung ist an, aber es wurde noch nichts erfasst. Sende einen Chat/eine Anfrage über Codex, dann erscheint es hier.`,"debug.noLines.injection":`Injektions-Log ist an, aber es wurde noch nichts erfasst. Es erfasst Multi-Agent-Guidance-Injektion und Effort-Cap-Entscheidungen bei Collab- und Sub-Agent-Turns.`,"usage.title":`Nutzung`,"usage.subtitle":`Lokale Token-Buchhaltung deines Proxys. Fehlende Nutzung wird nie als Null angezeigt.`,"usage.loading":`Lade Nutzungsdaten…`,"usage.empty":`Noch keine Nutzung erfasst. Sende eine Anfrage über den Proxy, um Aktivität hier zu sehen.`,"usage.loadError":`Nutzungsdaten konnten nicht geladen werden.`,"usage.range.all":`Alle`,"usage.range.available":`Verfügbarer Verlauf`,"usage.historyTruncated":`Die Summen beziehen sich nur auf den verfügbaren Verlauf, da ältere Nutzungsdaten nicht geladen wurden.`,"usage.historyTruncatedWindow":`Die geladenen Zeilen haben Anfragestartzeiten zwischen {start} und {end}. Frühere Dateieinträge wurden durch das Leselimit ausgelassen, daher kann jeder gewählte Zeitraum unvollständig sein.`,"usage.range.30d":`30d`,"usage.range.7d":`7d`,"usage.card.requests":`Anfragen`,"usage.card.measured":`Gemessen`,"usage.card.reported":`Gemeldet`,"usage.card.totalTokens":`Gesamt-Tokens`,"usage.card.cachedTokens":`Cache-Treffer-Tokens`,"usage.card.cachedTokensHint":`Prompt-Tokens aus dem Provider-Cache (Treffer). Cache-Schreibvorgänge werden darunter separat angezeigt.`,"usage.card.cacheWriteTokens":`Cache-Schreiben`,"usage.card.coverage":`Abdeckung`,"usage.card.activeDays":`Aktive Tage`,"usage.section.heatmap":`Tägliche Aktivität`,"usage.section.overview":`Übersicht`,"usage.section.models":`Modelle`,"usage.section.providers":`Anbieter`,"usage.section.coverage":`Abdeckungs-Aufschlüsselung`,"usage.workspace.report":`Nutzungsbericht`,"usage.workspace.sections":`Nutzungsabschnitte`,"usage.coverage.measured":`Gemessen`,"usage.coverage.reported":`Anbieter gemeldet`,"usage.coverage.estimated":`Geschätzt`,"usage.coverage.note":`Gemessene Einträge enthalten anbieter-gemeldete und geschätzte Token-Zahlen. Nicht gemeldete und nicht unterstützte Anfragen werden erfasst, aber nie auf Null aufgebläht.`,"usage.search.models":`Modelle suchen…`,"usage.col.requests":`Anfragen`,"usage.col.measured":`Gemessen`,"usage.col.reported":`Gemeldet`,"usage.col.tokens":`Tokens`,"usage.col.share":`Anteil`,"usage.heatmap.less":`Weniger`,"usage.heatmap.more":`Mehr`,"modal.addNamed":`Hinzufügen: {label}`,"modal.add":`Anbieter hinzufügen`,"modal.search":`Anbieter suchen…`,"modal.logInWith":`Anmelden mit {label}`,"modal.waitingBrowser":`Warten auf Browser…`,"modal.providerName":`Anbietername`,"modal.adapter":`Adapter`,"modal.baseUrl":`Basis-URL`,"modal.endpoint":`Endpunkt`,"modal.endpoint.tokenPlan":`Token-Plan`,"modal.endpoint.payAsYouGo":`Pay as you go`,"modal.endpoint.custom":`Benutzerdefiniert`,"modal.defaultModel":`Standardmodell (optional)`,"modal.allowPrivateNetwork":`Lokales/privates Netzwerk erlauben`,"modal.allowPrivateNetworkHint":`Nur für absichtlich selbst gehostete Provider aktivieren. Metadaten-Endpunkte bleiben blockiert.`,"modal.nameRequired":`Anbietername ist erforderlich`,"modal.baseUrlRequired":`Basis-URL ist erforderlich`,"modal.networkError":`Netzwerkfehler — läuft der Proxy?`,"modal.loginFailStart":`Login konnte nicht gestartet werden`,"modal.waitingLogin":`Warten auf Browser-Login…`,"modal.loggingIn":`Anmelden…`,"modal.loginTimeout":`Login-Zeitüberschreitung — versuche es erneut.`,"nav.codexAuth":`Codex-Auth`,"nav.codexSet":`Codex-Einstellungen`,"codexSet.tab.multiauth":`Multi-Auth`,"codexSet.tab.prompt":`Prompt`,"codexSet.prompt.title":`Prompt-Ebenen`,"codexSet.prompt.timing":`Gilt für neu gestartete Sitzungen. Laufende Sitzungen behalten ihre aktuellen Prompt-Einstellungen.`,"codexSet.prompt.staleRevision":`Die Konfiguration wurde anderswo geändert. Die Liste wurde neu geladen.`,"codexSet.prompt.writeFailed":`Die Änderung konnte nicht gespeichert werden.`,"codexSet.prompt.loadFailed":`Die Prompt-Ebenen konnten nicht geladen werden.`,"codexSet.prompt.repair":`Reparieren`,"codexSet.prompt.repairFailed":`Die Reparatur konnte nicht abgeschlossen werden.`,"codexSet.drift.journalPresent":`Ein vorheriger Schreibvorgang wurde nicht abgeschlossen. Die Wiederherstellung läuft beim nächsten Schreibvorgang automatisch.`,"codexSet.drift.projectionStale":`Die gespeicherten Ebenen und der Wert in config.toml stimmen nicht überein. Die Reparatur schreibt den Wert aus deinen Ebenen neu.`,"codexSet.drift.storeMissing":`Die Ebenendatei fehlt, während in config.toml noch Anweisungen stehen. Die Reparatur legt zuerst eine Sicherung an und behält den Text als eine Ebene.`,"codexSet.drift.ownedMalformed":`Die generierte Zeile in config.toml wurde von Hand verändert und kann daher nicht mehr sicher überschrieben werden.`,"codexSet.custom.adoptUnsupported":`Der Wert in {path} Zeile {line} ist keine einzeilige Zeichenkette und kann nicht importiert werden. Verschiebe ihn von Hand, um ihn hier zu verwalten.`,"codexSet.prompt.unreadable":`Die Codex-Konfigurationsdatei existiert, konnte aber nicht gelesen werden, daher wurden Änderungen abgelehnt.`,"codexSet.layer.permissions":`Berechtigungen`,"codexSet.layer.collaboration":`Kollaborationsmodus`,"codexSet.layer.environment":`Umgebungskontext`,"codexSet.layer.apps":`Apps`,"codexSet.layer.skills":`Skills`,"codexSet.prompt.extensionsUnknown":`Erweiterungen können eigene Ebenen hinzufügen. Codex legt sie nicht offen, daher können sie hier nicht aufgeführt werden.`,"codexSet.group.transition":`Übergangshinweise`,"codexSet.group.transitionDesc":`Sie melden eine Änderung, statt einen Zustand zu beschreiben, und erscheinen daher nur beim Wechsel in den Echtzeitmodus oder beim Modellwechsel.`,"codexSet.custom.slotNote":`Benutzerdefinierte Ebenen werden in dieser Reihenfolge zu einem Abschnitt zusammengefügt.`,"codexSet.row.alwaysOn":`Immer aktiv`,"codexSet.row.onChange":`Bei Änderung`,"codexSet.row.featureGated":`Unter [features] konfiguriert`,"codexSet.row.openFeatures":`Einstellungen öffnen`,"codexSet.dialog.setValue":`{value} (Standard {fallback})`,"codexSet.dialog.copyKey":`Schlüssel kopieren`,"codexSet.dialog.unknownLayer":`Dieser Build enthält keine Beschreibung für diese Ebene. Sie stammt aus einer neueren Codex-Laufzeit als das Dashboard.`,"codexSet.custom.heading":`Benutzerdefinierte Ebenen`,"codexSet.custom.add":`+ Ebene hinzufügen`,"codexSet.custom.newTitle":`Neue Ebene`,"codexSet.custom.editTitle":`Ebene bearbeiten`,"codexSet.custom.titleLabel":`Titel`,"codexSet.custom.bodyLabel":`Anweisungen`,"codexSet.custom.bodySize":`{bytes} von {max} Byte`,"codexSet.custom.normalized":`Tabulatoren wurden durch vier Leerzeichen und Zeilenenden durch LF ersetzt.`,"codexSet.custom.titleRequired":`Gib einen Titel ein.`,"codexSet.custom.titleTooLong":`Der Titel hat {count} Zeichen; das Limit liegt bei {max}.`,"codexSet.custom.titleMultiline":`Der Titel muss einzeilig sein.`,"codexSet.custom.bodyTooLarge":`Diese Ebene umfasst {bytes} Byte; das Limit liegt bei {max}.`,"codexSet.custom.composedTooLarge":`Die aktivierten Ebenen würden zusammen {bytes} Byte umfassen und das Limit überschreiten.`,"codexSet.custom.invalidCharacter":`Ein Steuerzeichen an Position {position} kann nicht gespeichert werden.`,"codexSet.custom.discardPrompt":`Änderungen verwerfen?`,"codexSet.custom.keepEditing":`Weiter bearbeiten`,"codexSet.custom.delete":`{title} löschen`,"codexSet.custom.deleteConfirm":`Diese Ebene löschen? Dies kann nicht rückgängig gemacht werden.`,"codexSet.custom.layerGone":`Diese Ebene wurde anderswo entfernt, daher wurde der Editor geschlossen.`,"codexSet.custom.deleteConfirmNamed":`„{title}“ löschen? Dies kann nicht rückgängig gemacht werden.`,"codexSet.custom.moveUp":`{title} nach oben verschieben`,"codexSet.custom.prevLayer":`Vorherige Ebene`,"codexSet.custom.nextLayer":`Nächste Ebene`,"codexSet.custom.navPosition":`{position} / {total}`,"codexSet.custom.moveDown":`{title} nach unten verschieben`,"codexSet.custom.limitReached":`Du kannst bis zu {max} benutzerdefinierte Ebenen speichern.`,"codexSet.custom.notOwned":`developer_instructions wurde außerhalb von opencodex geschrieben und kann daher hier nicht bearbeitet werden. Importiere den Inhalt, um ihn als Ebene zu verwalten.`,"codexSet.custom.adopt":`Vorhandene Anweisungen importieren`,"codexSet.custom.adoptConfirm":`Als Ebene importieren`,"codexSet.custom.adoptRefused":`Der vorhandene Wert konnte nicht importiert werden.`,"codexSet.custom.baseReplaced":`model_instructions_file ist auf {path} gesetzt. Daher wurde der Basis-Prompt außerhalb von opencodex ersetzt.`,"codexSet.lint.identity":`Hier wird eine andere Identität beansprucht als die von Codex festgelegte.`,"codexSet.lint.foreignTool":`Tools stammen aus der Registry; ein Name an dieser Stelle erstellt kein Tool.`,"codexSet.lint.placeholder":`Anweisungen werden nicht von einer Template-Engine verarbeitet. Dieser Inhalt wird daher wörtlich gesendet.`,"codexSet.lint.applyPatch":`apply_patch wird von der Tool-Registry definiert, nicht von Anweisungen.`,"codexSet.lint.approvalVocab":`Codex fügt eigene Begriffe für Genehmigungen ein; dies könnte ihnen widersprechen.`,"codexSet.lint.environment":`Umgebungsdaten werden später erzeugt und könnten dem widersprechen.`,"codexSet.lint.size":`Diese Ebene ist größer als 8 KB. Sie kann trotzdem gespeichert werden, verbraucht aber bei jeder Anfrage Tokens.`,"codexSet.preset.blank":`Leere Ebene`,"codexSet.preset.concise.name":`Knappe Ausgabe`,"codexSet.preset.concise.description":`Kurze Antworten ohne Einleitung und mit minimaler Formatierung.`,"codexSet.preset.concise.provenance":`Angelehnt an die Kürzevorgaben von Claude Code. Eigene Formulierung, keine Kopie.`,"codexSet.preset.planFirst.name":`Vor dem Bearbeiten planen`,"codexSet.preset.planFirst.description":`Zuerst den Plan nennen, dann die Änderung vornehmen.`,"codexSet.preset.planFirst.provenance":`Angelehnt an den Planungsansatz von Claude Code. Eigene Formulierung, keine Kopie.`,"codexSet.preset.explainWhy.name":`Begründung erklären`,"codexSet.preset.explainWhy.description":`Nicht nur sagen, was geschieht, sondern auch warum.`,"codexSet.preset.explainWhy.provenance":`Angelehnt an den Bestätigungsstil von Grok Build. Eigene Formulierung, keine Kopie.`,"codexSet.preset.testFirst.name":`Zuerst testen`,"codexSet.preset.testFirst.description":`Vor der Korrektur einen fehlschlagenden Test schreiben.`,"codexSet.preset.testFirst.provenance":`Angelehnt an gängige Vorgehensweisen von Agenten. Eigene Formulierung, keine Kopie.`,"codexSet.preset.korean.name":`Antworten auf Koreanisch`,"codexSet.preset.korean.description":`Unabhängig von der Sprache der Anfrage auf Koreanisch antworten.`,"codexSet.preset.korean.provenance":`Für opencodex aus einem häufig gewünschten Standard verfasst. Eigene Formulierung, keine Kopie.`,"codexSet.dialog.class":`Art`,"codexSet.dialog.key":`Konfigurationsschlüssel`,"codexSet.dialog.fileValue":`Wert in dieser Datei`,"codexSet.dialog.absentDefault":`nicht festgelegt (Standard: {value})`,"codexSet.dialog.noRenderedText":`Codex legt den zusammengesetzten Text einer integrierten Ebene nicht offen. Dieser Dialog beschreibt daher die Ebene und nennt ihren Schlüssel, statt ihren Inhalt anzuzeigen.`,"codexSet.dialog.sourceText":`An das Modell gesendeter Text`,"codexSet.dialog.sourceBytes":`{bytes} Byte`,"codexSet.dialog.notRendered":`In der gelesenen Runde hat diese Ebene nichts gesendet. Abschnitte werden nur bei Änderungen erneut gesendet, daher kann eine einzelne Stichprobe sie nicht enthalten.`,"codexSet.dialog.emptySource":`Die Datei unter {path} existiert, ist aber leer, daher sendet diese Ebene nichts.`,"codexSet.dialog.notExposed":`Der Basis-Prompt läuft außerhalb der Nachrichtenliste, die Codex ausgeben kann, und lässt sich hier nicht anzeigen. Ersetzen ist über model_instructions_file möglich.`,"codexSet.dialog.textUnavailable":`Der Codex-Prompt konnte auf diesem Rechner nicht gelesen werden, daher ist der Text nicht verfügbar.`,"codexSet.class.base":`Basisanweisungen`,"codexSet.class.config-toggle":`Hier umschaltbar`,"codexSet.class.feature-gated":`Feature-gesteuert`,"codexSet.class.runtime-conditional":`Laufzeitabhängig`,"codexSet.class.extension-unknown":`Erweiterungsebene`,"codexSet.layer.base-instructions":`Basisanweisungen`,"codexSet.layer.model-switch":`Hinweis zum Modellwechsel`,"codexSet.layer.personality":`Persönlichkeit`,"codexSet.layer.context-window-guidance":`Kontextfenster-Hinweise`,"codexSet.layer.realtime":`Echtzeit`,"codexSet.layer.agents-md":`AGENTS.md`,"codexSet.layer.environments-instructions":`Ausführungsumgebungen`,"codexSet.layer.plugins":`Plugins`,"codexSet.layer.tools":`Tools`,"codexSet.layer.multi-agent-mode":`Multi-Agenten-Modus`,"codexSet.layer.git-attribution":`Commit-Attribution`,"codexSet.about.base-instructions":`Codex-eigene Anweisungen. Sie werden mit der Anfrage selbst gesendet und können nicht deaktiviert werden.`,"codexSet.about.model-switch":`Wird hinzugefügt, wenn das Modell während einer Unterhaltung gewechselt wird.`,"codexSet.about.personality":`Vorgaben für Ton und Ausdruck, gesteuert durch ein Feature-Flag.`,"codexSet.about.context-window-guidance":`Hinweise zum verbleibenden Kontextbudget, gesteuert durch ein Feature-Flag.`,"codexSet.about.realtime":`Wird bei Echtzeitsitzungen hinzugefügt.`,"codexSet.about.agents-md":`Die AGENTS.md-Dateien deines Projekts. Diese Seite zeigt die Ebene an, bearbeitet aber nie deine Projektdokumentation.`,"codexSet.about.permissions":`Erläutert die geltenden Sandbox- und Genehmigungseinstellungen.`,"codexSet.about.collaboration":`Erläutert den aktiven Kollaborationsmodus.`,"codexSet.about.environment":`Arbeitsverzeichnis, Plattform und weitere Umgebungsdaten.`,"codexSet.about.environments-instructions":`Vorgaben für verzögerte Ausführungsumgebungen, gesteuert durch ein Feature-Flag.`,"codexSet.about.apps":`Anleitung zur Verwendung verbundener Apps.`,"codexSet.about.plugins":`Wird hinzugefügt, wenn ein Plugin ausgewählt ist oder ein Plugin eine Funktion bereitstellt.`,"codexSet.about.tools":`Verzögert geladene Tool-Beschreibungen, gesteuert durch ein Feature-Flag.`,"codexSet.about.skills":`Liste der verfügbaren Skills.`,"codexSet.about.multi-agent-mode":`Anweisungen für Subagenten, gesteuert durch ein Feature-Flag.`,"codexSet.about.git-attribution":`Lässt das Modell einen Co-authored-by: Codex-Trailer in Commits schreiben, die es erstellt, und eine Zeile Generated with Codex. in Pull Requests, die es öffnet. Codex liest das aus deinem Konto, deshalb ist es weder hier noch unter [features] einstellbar. Ist es im Konto aus, sendet Codex die umgekehrte Anweisung statt gar keine.`,"codexSet.condition.model-switch":`Wird nur nach einem Modellwechsel während der Sitzung ausgegeben.`,"codexSet.condition.realtime":`Wird nur in einer Echtzeitsitzung ausgegeben.`,"codexSet.condition.agents-md":`Wird ausgegeben, wenn für das Arbeitsverzeichnis eine Projektdokumentation gefunden wird.`,"codexSet.condition.plugins":`Wird ausgegeben, wenn ein Plugin ausgewählt ist oder ein Plugin eine Funktion bereitstellt.`,"codexSet.condition.git-attribution":`Wird durch die Attributionsrichtlinie deines Kontos bestimmt.`,"codexSet.base.title":`Basis-Prompt`,"codexSet.base.prev":`Vorherige Option`,"codexSet.base.next":`Nächste Option`,"codexSet.base.position":`{position} / {total}`,"codexSet.base.swipeHint":`Seitlich wischen, die Pfeiltasten oder die Pfeil-Schaltflächen verwenden, um zwischen Optionen zu wechseln. Gilt für neu gestartete Sitzungen.`,"codexSet.base.defaultTitle":`Codex’ eigener Basis-Prompt`,"codexSet.base.defaultBody":`Die Voreinstellung wird hier nicht gespeichert, es gibt also nichts zu bearbeiten oder zu löschen: Ihre Auswahl entfernt einfach model_instructions_file aus der Konfiguration, und Codex nutzt den mitgelieferten Prompt.`,"codexSet.base.variantTitle":`Name`,"codexSet.base.variantBody":`Prompt`,"codexSet.base.replacesWarning":`Das ERSETZT Codex’ eigenen Basis-Prompt, statt ihn zu ergänzen. Ein kurzer Prompt hier bedeutet ein Modell mit kurzen Anweisungen.`,"codexSet.base.use":`Diesen verwenden`,"codexSet.base.inUse":`In Verwendung`,"codexSet.base.externalBlocked":`model_instructions_file zeigt bereits auf {path}, und das hat nicht opencodex geschrieben. Entfernen Sie es selbst, bevor Sie hier auswählen.`,"nav.api":`API`,"nav.integrations":`Integrationen`,"nav.openMenu":`Menü öffnen`,"nav.closeMenu":`Menü schließen`,"integrations.subtitle":`Clients mit opencodex verbinden, Zugangsdaten verwalten und Client-Konfigurationen wiederherstellen.`,"integrations.tabsLabel":`Integrationsbereiche`,"integrations.tab.overview":`Übersicht`,"integrations.tab.keys":`API-Schlüssel`,"integrations.tab.codex":`Codex`,"integrations.tab.claude":`Claude`,"integrations.tab.grok":`Grok Build`,"integrations.tab.opencode":`OpenCode`,"integrations.tab.pi":`Pi`,"integrations.tab.omp":`OMP`,"integrations.tab.hermes":`Hermes`,"integrations.tab.openclaw":`OpenClaw`,"integrations.tab.kimi":`Kimi Code`,"integrations.tab.gajae":`Gajae Code`,"integrations.tab.dsh":`DSH`,"integrations.tab.mcode":`MiniMax Code`,"integrations.tab.zcode":`ZCode`,"integrations.tab.prime":`Prime Agent`,"integrations.tab.aside":`Aside`,"integrations.codex.title":`Codex CLI`,"integrations.codex.body":`Die Codex-Anbindung wird vom Proxy-Dienst verwaltet. Beim Start von opencodex wird sie angewendet; beim Stoppen des Dienstes wird das native Routing wiederhergestellt.`,"integrations.codex.openService":`Dienststeuerung öffnen`,"integrations.state.notInstalled":`Nicht installiert`,"integrations.state.unknown":`Wird geprüft…`,"integrations.detail.codexRouted":`Codex-Anfragen laufen über diesen Proxy`,"integrations.detail.codexAbsent":`Codex läuft noch nicht über diesen Proxy`,"integrations.detail.keyCount":`{count} Schlüssel ausgestellt`,"integrations.detail.keyNone":`Keine Schlüssel ausgestellt`,"integrations.detail.keyChecking":`Wird geprüft…`,"integrations.detail.keyUnavailable":`Schlüsselstatus nicht verfügbar`,"integrations.detail.claudeOff":`Verbindung ist aus`,"integrations.detail.desktopCurrent":`Desktop läuft mit diesem Profil`,"integrations.detail.desktopStale":`Die Profildatei hat sich nach dem Anwenden geändert`,"integrations.detail.desktopNotServed":`Das Profil ist da, Desktop nutzt aber ein anderes`,"integrations.detail.desktopAbsent":`Kein Profil angewendet`,"integrations.detail.desktopDesiredOff":`Die Claude-Desktop-Integration ist deaktiviert`,"integrations.detail.desktopDesiredOffCleanupPending":`Claude Desktop verwendet das Gateway noch; die Bereinigung steht aus`,"integrations.detail.desktopDesiredOnNotApplied":`Die Integration ist aktiviert, aber Desktop verwendet nicht das Gateway-Profil`,"integrations.detail.desktopSelectedElsewhere":`Desktop verwendet ein anderes Profil`,"integrations.detail.desktopProfileDrift":`Das ausgewählte Desktop-Profil wurde geändert`,"integrations.detail.desktopObservedUnsafe":`Das ausgewählte Desktop-Profil kann nicht sicher geändert werden`,"integrations.detail.desktopNotInstalled":`Die Claude-Desktop-Konfigurationsbibliothek ist nicht installiert`,"integrations.dialog.desktop.title":`Claude-Desktop-Integration deaktivieren?`,"integrations.dialog.desktop.changes":`Falls {path} ein von opencodex verwaltetes Gateway-Profil enthält, wählt Desktop zuerst ein neues Standardprofil ohne Zugangsdaten und entfernt danach das alte Profil und dessen Sicherung.`,"integrations.dialog.desktop.breakage":`Claude Desktop verwendet dann statt über opencodex gerouteter Modelle wieder das standardmäßige Claude.`,"integrations.dialog.desktop.undo":`Beim erneuten Aktivieren wird das opencodex-Profil aus deinen gespeicherten Modellzuweisungen neu erstellt.`,"integrations.dialog.desktop.restart":`Claude Desktop liest diese Konfiguration nur beim Start. Beende Desktop vollständig und öffne es erneut, damit die Änderung wirksam wird.`,"integrations.dialog.desktop.confirm":`Deaktivieren`,"integrations.native.error.desktopUnsafeMetadata":`Die Claude-Desktop-Metadaten unter {path} konnten nicht sicher gelesen werden; die Bibliothek wurde nicht geändert.`,"integrations.native.error.desktopCleanupIncomplete":`Claude Desktop zeigt auf den Standardmodus, aber alte opencodex-Zugangsdaten befinden sich noch unter: {paths}.`,"integrations.native.msg.desktopDisabled":`Claude-Desktop-Integration deaktiviert.`,"integrations.native.msg.desktopEnabled":`Claude-Desktop-Integration aktiviert.`,"integrations.detail.grokModels":`{count} Modell(e) verbunden`,"integrations.detail.grokAbsent":`Kein opencodex-Block in der Konfiguration`,"integrations.dialog.grok.title":`Grok-Build-Integration deaktivieren?`,"integrations.dialog.grok.changes":`Aus {path} wird nur der von opencodex markierte Block entfernt. Manuell geschriebener Inhalt außerhalb des Blocks bleibt unverändert.`,"integrations.dialog.grok.breakage":`Nach dem Deaktivieren verschwinden die opencodex-Modellaliase aus Grok Build. Modelle, die mit dem xAI-Konto verwendet wurden, bleiben erhalten.`,"integrations.dialog.grok.undo":`Wenn opencodex unter einer Loopback-Adresse läuft, wird beim erneuten Aktivieren ein neuer Block aus der aktuell verfügbaren Modellliste geschrieben.`,"integrations.dialog.grok.confirm":`Deaktivieren`,"integrations.native.msg.nonLoopbackRemoved":`Grok Build kann nur automatisch registriert werden, wenn opencodex unter einer Loopback-Adresse läuft. Der vorherige Block mit Verweis auf Loopback wurde entfernt.`,"integrations.native.msg.nonLoopbackRemovedNoop":`Grok Build kann nur automatisch registriert werden, wenn opencodex unter einer Loopback-Adresse läuft. Es gab keinen vorherigen Block zu entfernen.`,"integrations.native.msg.nonLoopbackSuperseded":`Grok Build kann nur automatisch registriert werden, wenn opencodex unter einer Loopback-Adresse läuft. Inzwischen hat eine andere Stelle einen neuen Block in die Konfiguration geschrieben; der aktuelle Block in der Datei wurde daher nicht von dieser Anfrage erstellt.`,"integrations.native.error.orphanedMarker":`{path} enthält eine opencodex-Startmarkierung, aber keine Endmarkierung. Die Datei wurde nicht geändert, weil das Ende des Blocks nicht sicher bestimmt werden kann.`,"integrations.native.error.homeMismatch":`Das Home des installierten Dienstes stimmt nicht mit dem aktuellen Home überein; die Datei wurde daher nicht geändert.`,"integrations.native.error.notInstalled":`Grok Build ist nicht installiert; es gibt nichts zu ändern.`,"integrations.native.error.configBusy":`Die Konfiguration wird gerade an anderer Stelle gespeichert und konnte nicht geändert werden. Versuche es in Kürze erneut.`,"integrations.state.absent":`Nicht angewendet`,"integrations.state.current":`Angewendet`,"integrations.state.stale":`Aktualisierung erforderlich`,"integrations.state.conflict":`Konflikt`,"integrations.state.unsafe":`Nicht überprüfbar`,"integrations.summary.detected":`Clients erkannt`,"integrations.summary.applied":`Konfigurierte Clients`,"integrations.summary.stale":`Aktualisierung erforderlich`,"integrations.summary.lastChange":`Letzte Änderung`,"integrations.summary.disableAll":`Alle deaktivieren…`,"integrations.onboarding":`Beim Anwenden wird nach dem Speichern einer Sicherung genau ein opencodex-Anbieterblock geschrieben. Beim Deaktivieren wird nur dieser Block entfernt; eine aufbewahrte Momentaufnahme kann wiederhergestellt werden.`,"integrations.empty.title":`Keine installierten Clients erkannt`,"integrations.empty.body":`Installiere einen unterstützten Client und kehre dann hierher zurück, um opencodex anzuwenden.`,"integrations.action.apply":`Anwenden`,"integrations.action.disable":`Deaktivieren`,"integrations.action.refresh":`Aktualisieren`,"integrations.action.settings":`Einstellungen`,"integrations.action.manageKeys":`Schlüssel verwalten`,"integrations.action.restore":`Wiederherstellen…`,"integrations.action.undo":`Rückgängig`,"integrations.action.restorePoint":`Diesen Stand wiederherstellen…`,"integrations.action.snapshotExpired":`Sicherung abgelaufen`,"integrations.rollback.title":`Wiederherstellungscenter`,"integrations.rollback.empty":`Noch kein Anwendungsverlauf`,"integrations.rollback.emptyBody":`Vor jedem erfolgreichen Schreibvorgang wird zuerst eine Momentaufnahme gespeichert.`,"integrations.catalog.title":`Clients`,"integrations.rollback.older":`Frühere Vorgänge`,"integrations.rollback.showMore":`{n} weitere anzeigen`,"integrations.rollback.failed":`Der Rollback-Verlauf konnte nicht geladen werden.`,"integrations.restore.title":`Diese Momentaufnahme wiederherstellen?`,"integrations.restore.body":`Die aktuelle Datei wird zuerst gesichert und dann durch die ausgewählte Momentaufnahme ersetzt.`,"integrations.restore.driftTitle":`Neuere Änderungen wurden erkannt`,"integrations.restore.driftBody":`Änderungen nach dieser Momentaufnahme werden gesichert; anschließend wird die Datei ersetzt.`,"integrations.restore.confirm":`Wiederherstellen`,"integrations.restore.confirmDrift":`Neuere Änderungen sichern und wiederherstellen`,"integrations.restore.pending":`Wiederherstellung läuft…`,"integrations.restore.manual":`Automatische Wiederherstellung fehlgeschlagen: {reason}. Stelle die Datei manuell aus {path} wieder her.`,"integrations.error.load":`Integrationsstatus konnte nicht geladen werden.`,"integrations.error.stale":`Die letzte Aktualisierung ist fehlgeschlagen. Die folgenden Werte könnten veraltet sein.`,"integrations.error.busy":`Eine andere Änderung für diesen Client läuft noch. Versuche es in Kürze erneut.`,"integrations.error.conflict":`Die Konfiguration wurde geändert, nachdem opencodex sie geschrieben hatte. Es wurde nichts entfernt.`,"integrations.error.unsafe":`Die Konfiguration kann nicht sicher geändert werden.`,"integrations.error.generic":`Die Integrationsänderung ist fehlgeschlagen. Der vorherige Zustand wurde beibehalten.`,"integrations.error.nonLoopback":`{client} erreicht nur einen Proxy auf localhost. In seiner Konfiguration ist kein Platz für den Header, den eine Remote-Bindung verlangt — von Hand geschrieben hilft es also ebenso wenig. Ermöglichen Sie stattdessen Loopback-Zugriff, etwa über einen Tunnel oder lokalen Forwarder.`,"integrations.status.installed":`Installiert`,"integrations.status.notInstalled":`Nicht installiert`,"integrations.status.appliedAt":`Angewendet`,"integrations.status.backup":`Sicherung`,"integrations.status.lastRestore":`Letzte Wiederherstellung`,"integrations.status.unknown":`Unbekannt`,"integrations.bulk.title":`Angewendete Client-Integrationen deaktivieren?`,"integrations.bulk.body":`Nur der opencodex-eigene Block wird entfernt. Für jeden Client wird vorher eine Momentaufnahme gespeichert.`,"integrations.bulk.partial":`Einige Clients konnten nicht deaktiviert werden: {clients}`,"integrations.bulk.success":`Angewendete Client-Integrationen wurden deaktiviert.`,"integrations.retention.degraded":`Die Sicherungsbereinigung ist im Rückstand; ältere Sicherungen könnten noch auf dem Datenträger liegen.`,"integrations.error.residual":`Die Datei könnte sich in einem Zwischenzustand befinden: {message} Stellen Sie sie aus {path} wieder her.`,"integrations.error.recover":`{message} Eine Sicherung liegt unter {path}.`,"integrations.kind.apply":`Angewendet`,"integrations.kind.disable":`Deaktiviert`,"integrations.kind.refresh":`Aktualisiert`,"integrations.kind.restore":`Wiederhergestellt`,"integrations.kind.overwrite":`Überschrieben`,"integrations.dialog.overwrite.title":`Den Block in dieser Konfiguration ersetzen?`,"integrations.dialog.overwrite.changesUnowned":`In {path} belegt ein Block, den nicht wir geschrieben haben, die Stelle, die opencodex braucht. Beim Anwenden wird er durch den Block ersetzt, den opencodex schreibt.`,"integrations.dialog.overwrite.changesForeign":`Deine Änderung im opencodex-Block in {path} wird verworfen und durch den Block ersetzt, den opencodex schreibt.`,"integrations.dialog.overwrite.breakage":`Was der andere Block eingestellt hat, wirkt danach nicht mehr. Der Rest der Datei bleibt unberührt.`,"integrations.dialog.overwrite.undo":`Vorher wird ein Snapshot gespeichert, deshalb steht dieser Schritt unten in der Rollback-Liste und kann zurückgenommen werden.`,"integrations.dialog.overwrite.confirm":`Ersetzen`,"integrations.action.overwrite":`Ersetzen`,"integrations.semantics.opencode":`Gilt nur für direkte Starts von der Festplatte; die Umgebungsinjektion von ocx opencode hat Vorrang.`,"integrations.semantics.pi":`Gilt für neue Sitzungen.`,"integrations.semantics.omp":`Starten Sie OMP neu, um den Katalog zu laden.`,"integrations.semantics.hermes":`Gilt für neue Sitzungen.`,"integrations.semantics.openclaw":`Wird sofort auf ein laufendes Gateway angewendet.`,"integrations.semantics.kimi":`Zum Anwenden neu starten oder /reload ausführen (v2 überwacht die Datei).`,"integrations.semantics.gajae":`Gilt für eine neue Sitzung oder beim Öffnen von /model.`,"integrations.semantics.dsh":`OpenCodex verwaltet nur llm-pi-ai.providers.opencodex in $DSH_HOME/settings.yaml. DSH lädt diesen Anbieter im laufenden Betrieb neu; Ihr Standardmodell und deepseek-official bleiben unverändert. Derzeit nur über Loopback; es werden keine echten Zugangsdaten geschrieben.`,"integrations.semantics.mcode":`Verwaltet nur custom_provider.opencodex. Standardmodell und MiniMax-Anmeldung bleiben unverändert.`,"integrations.semantics.zcode":`Verwaltet nur provider.opencodex in ~/.zcode/v2/config.json. Z.ai-Anmeldung und andere Provider bleiben unverändert. ZCode nach Änderungen neu starten.`,"integrations.semantics.prime":`Verwaltet nur providers.opencodex in der models.json von Prime Agent — ~/.prime/agent, sofern PRIME_AGENT_CODING_AGENT_DIR sie nicht umleitet. Andere Provider und Modell-Overrides bleiben unverändert. Gilt für neue Sitzungen.`,"integrations.semantics.aside":`Verwaltet nur providers.opencodex in der models.json von Aside für das angemeldete Konto (~/.aside/u/). Andere Provider bleiben unverändert. Aside überschreibt diese Datei im laufenden Betrieb, daher nach dem Anwenden vollständig beenden und neu öffnen.`,"codexAuth.mainAccount":`Hauptkonto`,"codexAuth.logLabel":`Log-Kennung`,"codexAuth.codexApp":`Codex App`,"codexAuth.moreActions":`Weitere Aktionen anzeigen`,"codexAuth.copyId":`Konto-ID kopieren`,"codexAuth.appLogin":`App-Login`,"codexAuth.accountPool":`Kontopool`,"codexAuth.accountModeTitle":`OpenAI-Kontomodus`,"codexAuth.accountModePool":`Pool-Modus`,"codexAuth.accountModePoolDesc":`Die Hauptanmeldung und geeignete hinzugefügte Konten wechseln sich hier ab.`,"codexAuth.accountModeDirect":`Direktmodus`,"codexAuth.accountModeDirectDesc":`Anfragen verwenden nur die Hauptanmeldung; hinzugefügte Konten bleiben für den Pool-Modus gespeichert.`,"codexAuth.openaiMissing":`Der integrierte OpenAI-Anbieter ist nicht konfiguriert.`,"codexAuth.openaiDisabled":`Der integrierte OpenAI-Anbieter ist deaktiviert.`,"codexAuth.openaiUnavailableDesc":`Deine OpenAI-Konten sind weiterhin verfügbar. Aktiviere den Anbieter, um Codex-Anfragen weiterzuleiten.`,"codexAuth.enableOpenai":`OpenAI aktivieren`,"codexAuth.enablingOpenai":`Wird aktiviert...`,"codexAuth.enableOpenaiFailed":`OpenAI-Anbieter konnte nicht aktiviert werden.`,"codexAuth.openaiPresetLoadFailed":`OpenAI-Anbieter-Preset konnte nicht geladen werden.`,"codexAuth.openaiPresetUnavailable":`OpenAI-Anbieter-Preset ist nicht verfügbar.`,"codexAuth.openProviders":`Anbieter öffnen`,"codexAuth.add":`Hinzufügen`,"codexAuth.sparkQuota":`Codex-Spark-Kontingent`,"codexAuth.sparkQuotaHint":`Zeigt das GPT-5.3-Codex-Spark-Wochenfenster auf Kontokarten. Standardmäßig ausgeblendet, da es nur für ein Modell gilt.`,"codexAuth.sparkQuotaShown":`Codex-Spark-Kontingent wird angezeigt`,"codexAuth.sparkQuotaHidden":`Codex-Spark-Kontingent ausgeblendet`,"codexAuth.sparkQuotaFailed":`Codex-Spark-Kontingent konnte nicht geändert werden`,"codexAuth.refreshQuota":`Kontingente aktualisieren`,"codexAuth.refreshingQuota":`Aktualisiere…`,"codexAuth.quotaRefreshed":`Kontingente aktualisiert`,"codexAuth.quotaRefreshFailed":`Kontingente konnten nicht aktualisiert werden`,"codexAuth.pauseExhausted":`Ausgeschöpfte pausieren`,"codexAuth.pausingExhausted":`Kontingente werden geprüft…`,"codexAuth.pauseExhaustedSucceeded":`Konten am Limit pausiert: {count}`,"codexAuth.pauseExhaustedNone":`Keine Konten mit bestätigter 100-%-Nutzung.`,"codexAuth.pauseExhaustedFailed":`Ausgeschöpfte Konten konnten nicht geprüft und pausiert werden.`,"codexAuth.noPool":`Noch keine Pool-Konten hinzugefügt.`,"codexAuth.pause":`Pausieren`,"codexAuth.resume":`Fortsetzen`,"codexAuth.paused":`PAUSIERT`,"codexAuth.pauseSucceeded":`{email} ist pausiert`,"codexAuth.resumeSucceeded":`{email} ist wieder im Pool verfügbar`,"codexAuth.pauseFailed":`{email} konnte nicht pausiert werden. Es wurde nichts geändert.`,"codexAuth.resumeFailed":`{email} konnte nicht fortgesetzt werden. Es wurde nichts geändert.`,"codexAuth.pausedHint":`Bis zur Fortsetzung von automatischem Wechsel, Wiederholungen, Cooldown-Wiederherstellung und manueller Auswahl ausgeschlossen.`,"codexAuth.pinned":`ANGEHEFTET`,"codexAuth.pinnedHint":`Du hast dieses Konto von Hand ausgewählt, daher geht eine höhere Auswahlreihenfolge nicht daran vorbei. Die Fixierung gilt, bis dieses Konto aufgebraucht ist, du ein anderes auswählst oder du eine Auswahlreihenfolge änderst.`,"codexAuth.fiveHour":`5 Std.`,"codexAuth.weekly":`Woche`,"codexAuth.monthly":`30d`,"codexAuth.resets":`zurücksetzen`,"codexAuth.today":`Heute`,"codexAuth.current":`AKTUELL`,"codexAuth.nextSession":`AUSGEWÄHLT`,"codexAuth.poolPrepared":`FÜR POOL VORBEREITET`,"codexAuth.preparePoolTitle":`Dieses Konto für den Pool-Modus vorbereiten?`,"codexAuth.preparePoolDesc":`Direkte Anfragen verwenden weiterhin die Hauptanmeldung. Dieses Konto wird zur vorbereiteten Pool-Auswahl, sobald der Pool-Modus aktiviert ist.`,"codexAuth.prepareForPool":`Für Pool vorbereiten`,"codexAuth.poolPreparedToast":`{email} ist für den Pool-Modus vorbereitet`,"codexAuth.switchTitle":`Aktives Konto wechseln?`,"codexAuth.switchDesc":`Wird sofort wirksam. Bereits laufende Anfragen behalten ihr Konto; alles andere wechselt zu diesem Konto, wobei Konten mit derselben Auswahlreihenfolge sich weiterhin abwechseln.`,"codexAuth.cacheWarning":`Prompt-Cache wird beim Kontowechsel zurückgesetzt. Neue Sitzung startet mit leerem Cache.`,"codexAuth.setAsNext":`Dieses Konto als Nächstes verwenden`,"codexAuth.cancel":`Abbrechen`,"codexAuth.switchBack":`Zurück zum Hauptkonto?`,"codexAuth.switchBackDesc":`Wird sofort wirksam. Bereits laufende Anfragen behalten ihr Konto; alles andere wechselt zu deinem App-Login-Konto, wobei Konten mit derselben Auswahlreihenfolge sich weiterhin abwechseln.`,"codexAuth.autoSwitch":`Proaktiver Wechsel nach Nutzung`,"codexAuth.autoSwitchQuotaDesc":`Kontingent: Ab {threshold} % Nutzung kann die nächste Anfrage zu einem geeigneten Konto mit geringerer Nutzung wechseln, auch bei einer bereits gebundenen Aufgabe; Go/Free nutzen nur 30 Tage.`,"codexAuth.autoSwitchQuotaOffDesc":`Der proaktive Wechsel nach Nutzung ist aus. Zuweisung neuer/ungebundener Aufgaben und Fehlerbehebung bleiben aktiv.`,"codexAuth.autoSwitchRoundRobinDesc":`Round-Robin-Zuweisung verwendet diesen Schwellenwert nicht und rotiert weiter neue/ungebundene Aufgaben.`,"codexAuth.autoSwitchFillFirstDesc":`Fill-first: {threshold} % ist der Entleerungspunkt für neue/ungebundene Aufgaben; gesunde gebundene Aufgaben behalten ihr Konto.`,"codexAuth.autoSwitchFillFirstOffDesc":`Fill-first hat keinen nutzungsbasierten Entleerungspunkt für neue/ungebundene Aufgaben; Cooldown, Neuanmeldung und Fehlerbehebung können das Routing weiterhin ändern.`,"codexAuth.failureRecoveryNote":`Fehlerbehebung ist getrennt: Eine Ablehnung vor der Ausgabe mit 429/402, Cooldown, Neuanmeldung, Ausschluss oder konfiguriertes temporäres Failover kann ein anderes geeignetes Konto auswählen.`,"codexAuth.autoSwitchThreshold":`Nutzungsschwelle`,"codexAuth.autoSwitchThresholdAria":`Nutzungsschwelle in Prozent`,"codexAuth.autoSwitchThresholdInc":`Nutzungsschwelle erhöhen`,"codexAuth.autoSwitchThresholdDec":`Nutzungsschwelle verringern`,"codexAuth.autoSwitchLoadFailed":`Die Einstellung für den nutzungsbasierten Wechsel konnte nicht geladen werden.`,"codexAuth.autoSwitchThresholdInvalid":`Gib eine ganze Zahl von 1 bis 100 ein`,"codexAuth.autoSwitchUpdated":`Der proaktive Wechsel nach Nutzung wurde aktualisiert`,"codexAuth.autoSwitchUpdateFailed":`Die Aktualisierung des nutzungsbasierten Wechsels konnte nicht bestätigt werden. Der zuletzt bestätigte Wert wird angezeigt.`,"codexAuth.requestUserInput":`Im Default-Modus nachfragen`,"codexAuth.requestUserInputDesc":`Erlaubt Codex, eine Session im Default-Modus zu pausieren und dir über das request_user_input-Tool Fragen zu stellen.`,"codexAuth.requestUserInputUpdated":`Feature-Flag aktualisiert - gilt für neue Sessions.`,"codexAuth.requestUserInputUpdatedRestart":`Feature-Flag aktualisiert - gilt für neue Sessions. Starte die Codex-App neu, damit es wirksam wird.`,"codexAuth.requestUserInputUpdateFailed":`Feature-Flag konnte nicht aktualisiert werden. Es wurde nichts geändert.`,"codexAuth.requestUserInputLoadFailed":`Feature-Flag konnte nicht aus config.toml gelesen werden.`,"codexAuth.accountPickerTitle":`Ein bestimmtes Codex-Konto in der Modellauswahl verwenden`,"codexAuth.accountPickerOffDesc":`Wenn aktiviert, werden die normalen GPT-Einträge in der Modellauswahl durch je einen Eintrag pro Kontoselektor ersetzt. So kannst du ohne Abmeldung das genaue Konto für eine Unterhaltung wählen. Beim Deaktivieren werden keine Konten entfernt.`,"codexAuth.accountPickerOnDesc":`Jeder Selektor ist eine öffentliche Bezeichnung für ein gespeichertes Konto. Seine Auswahl bindet die Unterhaltung an dieses Konto: keine Pool-Rotation, kein Fallback und keine Änderung des aktiven Pool-Kontos.`,"codexAuth.accountPickerCompatibility":`Die integrierte Codex-App-Anmeldung hat einen eigenen Selektor; generierte Zuordnungen nennen ihn normalerweise main und verwenden bei Bedarf ein kollisionssicheres Suffix wie main-2. Hinzugefügte Konten erhalten stabile, datenschutzfreundliche Bezeichnungen; eigene Selektornamen bleiben erhalten. Bestehende Unterhaltungen und gespeicherte Modellauswahlen werden weiterhin geroutet. Beim Deaktivieren werden nur generierte Einträge ausgeblendet, Selektoren und exakte Routen bleiben bestehen. Einfache GPT-Modell-IDs behalten ihr Pool- oder Direct-Verhalten.`,"codexAuth.accountPickerUpdated":`Kontozielauswahl aktualisiert.`,"codexAuth.accountPickerUpdateFailed":`Die Kontozielauswahl konnte nicht aktualisiert werden. Die zuletzt bestätigte Einstellung wird angezeigt.`,"codexAuth.accountPickerLoadFailed":`Die Einstellung für die Kontozielauswahl konnte nicht geladen werden.`,"codexAuth.accountPickerRefreshFailed":`Diese Einstellung konnte nicht aktualisiert werden. Der zuletzt bestätigte Wert wird weiterhin angezeigt.`,"codexAuth.advancedSettings":`Erweiterte Einstellungen`,"codexAuth.advancedSettingsAria":`Erweiterte Codex-Auth-Einstellungen ein- oder ausblenden`,"codexAuth.catalogRefreshPending":`Die Änderung wurde gespeichert, aber die Aktualisierung des Codex-Modellkatalogs steht noch aus. Führe ocx sync aus, um es erneut zu versuchen.`,"anthropicPool.title":`Claude-Kontenpool (experimentell)`,"anthropicPool.enabledDesc":`Bei 429 wird das Konto gekühlt und umgeschaltet. Neue Sitzungen bevorzugen Nutzung unter {threshold}% ({window}).`,"anthropicPool.enabledNoProactiveDesc":`Bei 429 wird das Konto gekühlt und umgeschaltet. Proaktives nutzungsbasiertes Umschalten ist bei Schwellenwert 0 deaktiviert, aber die Auswahl neuer Sitzungen und die 429-Wiederherstellung verwenden weiterhin das Fenster {window}.`,"anthropicPool.disabledDesc":`Nutzt nur das aktive Claude-Konto. Nur aktivieren, wenn experimentelles Routing akzeptabel ist.`,"anthropicPool.experimentalWarning":`Experimentell und nicht kampferprobt. Anthropic kann Konten einschränken, die wie automatische Multi-Konto-Rotation wirken. Dieselbe Organisation kann Kontingent teilen — Pooling hilft dann nicht. Ausgeschaltet lassen, sofern das Risiko unklar ist.`,"anthropicPool.needTwoAccounts":`Füge mindestens zwei Claude-OAuth-Konten hinzu, bevor du den Pool aktivierst.`,"anthropicPool.threshold":`Nutzungsschwelle für neue Sitzungen`,"anthropicPool.thresholdAria":`Nutzungsschwelle für neue Sitzungen in Prozent`,"anthropicPool.thresholdHelp":`0 deaktiviert die kontingentbasierte Auswahl (nur Affinität + aktives Konto). Standard 80.`,"anthropicPool.thresholdInvalid":`Gib eine ganze Zahl von 0 bis 100 ein`,"anthropicPool.loadFailed":`Claude-Pool-Einstellungen konnten nicht geladen werden.`,"anthropicPool.saveFailed":`Claude-Pool-Einstellungen konnten nicht gespeichert werden.`,"anthropicPool.on":`An`,"anthropicPool.off":`Aus`,"accountPool.strategy":`Rotationsstrategie`,"accountPool.strategyDesc":`Wie OpenCodex einer neuen/ungebundenen Aufgabe ein Konto zuweist.`,"accountPool.strategyQuota":`Kontingent`,"accountPool.strategyRoundRobin":`Round-Robin`,"accountPool.strategyFillFirst":`Fill-first`,"accountPool.strategyHintQuota":`Kontingent kann eine bestehende Aufgabe bei ihrer nächsten Anfrage neu binden, nachdem die Nutzungsschwelle überschritten wurde.`,"accountPool.strategyHintRoundRobin":`Round-Robin rotiert nur Aufgaben ohne aktive Bindung; die Nutzungsschwelle ändert die normale Rotation nicht.`,"accountPool.strategyHintFillFirst":`Fill-first nutzt die Schwelle als Entleerungspunkt für ungebundene Aufgaben; gesunde gebundene Aufgaben behalten ihre Affinität.`,"accountPool.unboundDefinition":`Neue/ungebundene Aufgabe bedeutet eine Anfrage ohne aktuelle Kontobindung; eine sichtbare bestehende Aufgabe kann nach einem Proxy- oder Affinitätsreset ungebunden sein.`,"accountPool.stickyLimit":`Neue/ungebundene Zuweisungen vor Rotation`,"accountPool.stickyLimitAria":`Neue/ungebundene Zuweisungen vor Rotation`,"accountPool.stickyLimitInc":`Sticky-Limit erhöhen`,"accountPool.stickyLimitDec":`Sticky-Limit verringern`,"accountPool.stickyLimitHelp":`So viele neue/ungebundene Aufgaben dem gewählten Konto zuweisen, bevor weitergeschaltet wird; gezählt wird bei der Bindung, nicht nach einem Upstream-Erfolg.`,"accountPool.stickyLimitInvalid":`Gib eine ganze Zahl von 1 bis 100 ein`,"accountPool.strategyLoadFailed":`Rotationsstrategie konnte nicht geladen werden.`,"accountPool.strategyUpdateFailed":`Rotationsstrategie konnte nicht gespeichert werden.`,"accountPool.quotaWindow":`Kontingentfenster`,"accountPool.quotaWindowDesc":`Welcher zwischengespeicherte Nutzungsbalken die kontingentbasierte Auswahl neuer Sitzungen, Fill-first-Schwellenprüfungen und geeignete 429-Ersatzkonten steuert.`,"accountPool.quotaWindowFiveHour":`5-Stunden-Balken`,"accountPool.quotaWindowWeekly":`Wochenbalken`,"accountPool.quotaWindowMaxUtilization":`Höherer Balken`,"accountPool.quotaWindowHint":`Der Wochenbalken überspringt Konten mit erschöpftem 5-Stunden-Balken, solange ein anderes geeignetes Konto verbleibt, greift aber auf sie zurück, wenn keines verbleibt. Gleichstände bevorzugen die geringere 5-Stunden-Nutzung; einzelne Wochenbalken sind erst nach der Abfrage auf der Anbieterseite bekannt.`,"accountPool.quotaWindowInert":`Nur Kontingent — oder Fill-first mit einem Schwellenwert über 0 — bewertet einen Nutzungsbalken; für die aktuelle Rotationsstrategie ändert diese Einstellung daher nichts.`,"accountPool.priority":`Auswahlreihenfolge`,"accountPool.priorityAria":`Auswahlreihenfolge für dieses Konto`,"accountPool.priorityHint":`Höhere Zahlen werden zuerst verwendet. Der Pool geht erst zu einer niedrigeren Zahl über, wenn alle Konten darüber aufgebraucht oder nicht verfügbar sind.`,"accountPool.priorityFirst":`Zuerst`,"accountPool.priorityEarlier":`Früher`,"accountPool.priorityNormal":`Normal`,"accountPool.priorityLater":`Später`,"accountPool.priorityLast":`Zuletzt`,"accountPool.priorityOption":`{name} ({value})`,"accountPool.priorityCustom":`Benutzerdefiniert`,"accountPool.priorityUpdated":`Auswahlreihenfolge für {email} aktualisiert`,"accountPool.priorityUpdateFailed":`Die Auswahlreihenfolge für {email} konnte nicht gespeichert werden. Der zuletzt bestätigte Wert wird angezeigt.`,"codexAuth.switched":`{email} ist für die nächste Anfrage ausgewählt`,"codexAuth.loadFailed":`Die Codex-Kontoeinstellungen konnten nicht geladen werden.`,"codexAuth.switchFailed":`Das Konto konnte nicht gewechselt werden. Die vorherige Auswahl bleibt erhalten.`,"codexAuth.removeConfirm":`{id} entfernen?`,"codexAuth.removeFailed":`Das Konto konnte nicht entfernt werden. Es wurde nichts geändert.`,"codexAuth.addTitle":`Codex-Konto hinzufügen`,"codexAuth.addIdLabel":`Konto-ID (slug)`,"codexAuth.addJsonLabel":`auth.json-Inhalt`,"codexAuth.addHelp":`Kopiere von ~/.codex/auth.json einer anderen Maschine oder nutze codex-auth export.`,"codexAuth.importBtn":`Importieren`,"codexAuth.importInvalidJson":`Ungültiges JSON`,"codexAuth.importMissingTokens":`access_token oder refresh_token fehlen in JSON`,"codexAuth.importMissingId":`Konto-ID ist erforderlich`,"codexAuth.accountAdded":`Konto zum Pool hinzugefügt`,"codexAuth.addPickDesc":`Melde dich mit einem anderen ChatGPT-Konto an, um es zum Pool hinzuzufügen.`,"codexAuth.oauthLogin":`OAuth-Login`,"codexAuth.oauthDesc":`Öffnet ChatGPT-Login im Browser`,"codexAuth.deviceLogin":`Anmeldung per Gerätecode`,"codexAuth.deviceDesc":`Für einen Headless- oder Remote-Proxy: kurzen Code auf einem anderen Gerät eingeben`,"codexAuth.importAuthJson":`auth.json importieren`,"codexAuth.importAuthJsonDesc":`Von einer anderen Codex-Installation oder codex-auth export`,"codexAuth.back":`Zurück`,"codexAuth.oauthAlreadyInProgress":`Login läuft bereits. Schließe es in deinem Browser ab.`,"codexAuth.oauthWaiting":`Warte auf Abschluss des ChatGPT-Logins in deinem Browser…`,"codexAuth.oauthSubmittingCode":`Code wird gesendet…`,"codexAuth.oauthCodeSubmitted":`Code gesendet — warte auf Abschluss der Anmeldung…`,"codexAuth.oauthStatusRetrying":`Beim Prüfen des Login-Status ist ein Netzwerk- oder Proxyfehler aufgetreten — erneuter Versuch…`,"codexAuth.oauthCancelled":`Login wurde abgebrochen.`,"codexAuth.loginFailed":`Login fehlgeschlagen`,"codexAuth.needsReauth":`Erneut anmelden`,"codexAuth.reauthenticate":`Re-authenticate`,"codexAuth.tokenExpired":`Token abgelaufen — dieses Konto erneut authentifizieren`,"codexAuth.mainTokenExpired":`Token abgelaufen — erneut über Codex-App-Login anmelden`,"codexAuth.emailCollision":`Dieses Konto entspricht deinem Haupt-Codex-Login. Nutze ein anderes Konto.`,"codexAuth.resetCreditsTitle":`Gutschriften zurücksetzen`,"codexAuth.resetCreditsAvailable":`Du hast {count} Reset-Gutschrift(en) verfügbar.`,"codexAuth.resetCreditsDesc":`Jede Gutschrift setzt deine aktuellen stündlichen und wöchentlichen Nutzungsgrenzen sofort zurück.`,"codexAuth.noResetCredits":`Du hast keine Reset-Gutschriften.`,"codexAuth.earnCreditsHint":`Gutschriften werden monatlich und über das Empfehlungsprogramm verdient.`,"codexAuth.creditsExpireNote":`Gutschriften verfallen 30 Tage nach Erhalt.`,"codexAuth.useOneCredit":`1 Gutschrift nutzen`,"codexAuth.confirmResetTitle":`Reset-Gutschrift nutzen?`,"codexAuth.confirmResetDesc":`Dies setzt deine aktuellen Ratenbegrenzungen sofort zurück. Du hast noch {count} Gutschrift(en).`,"codexAuth.irreversible":`Diese Aktion kann nicht rückgängig gemacht werden.`,"codexAuth.useCredit":`Gutschrift nutzen`,"codexAuth.redeeming":`Wird zurückgesetzt…`,"codexAuth.resetSuccess":`Ratenbegrenzungen zurückgesetzt! {remaining} Gutschrift(en) übrig.`,"codexAuth.resetSuccessGeneric":`Ratenbegrenzungen zurückgesetzt!`,"codexAuth.resetAlreadyRedeemed":`Diese Gutschrift wurde bereits eingelöst. Gutschriften unverändert.`,"codexAuth.resetNothingToReset":`Kein Ratenbegrenzungs-Fenster muss gerade zurückgesetzt werden.`,"codexAuth.resetNoCredit":`Keine Reset-Gutschriften verfügbar.`,"codexAuth.resetError":`Reset-Gutschrift konnte nicht eingelöst werden. Bitte erneut versuchen.`,"codexAuth.fifoNote":`Die älteste Gutschrift wird zuerst verwendet.`,"codexAuth.confirmWhichCredit":`Gutschrift vom {date} wird verwendet.`,"codexAuth.creditNext":`Als nächstes`,"codexAuth.creditLabel":`Gutschrift #{n}`,"codexAuth.creditNextBadge":`NÄCHSTE`,"codexAuth.creditGranted":`Erhalten {date}`,"codexAuth.creditExpires":`Läuft ab {date} ({days} Tage übrig)`,"api.title":`API-Zugriff`,"api.subtitle":`Mit generierten API-Schlüsseln greifen externe Apps auf den opencodex-Proxy zu. Die Authentifizierung läuft über den {authHeader}-Header; welche Header ein Endpunkt akzeptiert, steht in der Tabelle unten.`,"api.baseUrl":`Basis-URL`,"api.responsesEndpoint":`Responses API`,"api.chatCompletionsEndpoint":`Chat Completions API`,"api.messagesEndpoint":`Messages API`,"api.modelsEndpoint":`Models API`,"api.endpointNote":`Nutze die Basis-URL für OpenAI-kompatible Clients. Responses und Chat Completions liegen unter /v1.`,"api.endpointsTitle":`Gateway-Endpunkte`,"api.authBaseUrlNote":`Konfiguriere Clients mit der Basis-URL und wähle dann den protokollspezifischen Endpunkt unten.`,"api.authTitle":`Authentifizierung`,"api.authLoopback":`Loopback-Binds (127.0.0.1 oder ::1) umgehen die Authentifizierung. Remote-Binds benötigen einen generierten ocx_-Schlüssel oder OPENCODEX_API_AUTH_TOKEN.`,"api.modelsTitle":`Externe Modelle`,"api.modelsCount":`{count} aufrufbar`,"api.modelsSearch":`Modelle suchen`,"api.modelsSubtitle":`Verwende diese exakten Modell-IDs mit /v1/models und dem gewählten eingehenden Protokoll.`,"api.modelsLoading":`Modelle werden geladen…`,"api.modelsEmpty":`Noch keine extern aufrufbaren Modelle verfügbar.`,"api.modelsNoMatch":`Keine Modelle passen zu „{query}“.`,"api.modelsLoadFailed":`Der externe Modellkatalog konnte nicht geladen werden.`,"api.colModel":`Modell`,"api.colSource":`Quelle`,"api.colProtocols":`Protokolle`,"api.copyModelId":`ID kopieren`,"api.modelCopied":`Kopiert`,"api.testModel":`Testen`,"api.testingModel":`Teste…`,"api.testFailed":`Fehlgeschlagen`,"api.protocolResponses":`Responses`,"api.protocolChatCompletions":`Chat Completions`,"api.protocolMessages":`Messages`,"api.sourceNative":`ChatGPT-Pool`,"api.sourceCombo":`Combo`,"api.sourceCustom":`Benutzerdefiniert`,"api.usageResponsesTitle":`Responses-Beispiel`,"api.usageChatTitle":`Chat-Completions-Beispiel`,"api.usageMessagesTitle":`Messages-Beispiel`,"api.testSucceeded":`OK`,"api.newKeyTitle":`Neuer Schlüssel erstellt`,"api.newKeyNote":`Kopiere diesen Schlüssel jetzt — er wird nicht erneut angezeigt.`,"api.copy":`Kopieren`,"api.copied":`Kopiert`,"api.dismiss":`Schließen`,"api.generateTitle":`Schlüssel generieren`,"api.keyNamePlaceholder":`Schlüsselname (optional)`,"api.generate":`Generieren`,"api.generating":`Erstelle…`,"api.activeKeys":`Aktive Schlüssel ({count})`,"api.activeKeysLoading":`Aktive Schlüssel`,"api.noKeys":`Noch keine API-Schlüssel. Erstelle oben einen.`,"api.workspace.sections":`API-Abschnitte`,"api.section.keys":`Schlüssel`,"api.section.connect":`Verbinden`,"api.section.endpoints":`Endpunkte`,"api.section.models":`Modelle`,"api.section.examples":`Beispiele`,"api.workspace.details":`API-Schlüsseldetails`,"api.workspace.keyDetails":`Schlüsseldetails`,"api.workspace.keyPrefix":`Schlüssel-Präfix`,"api.workspace.deleteKey":`Schlüssel löschen`,"api.workspace.deleteConfirm":`Diesen Schlüssel wirklich löschen? Das lässt sich nicht rückgängig machen.`,"api.workspace.usageExamples":`Nutzungsbeispiele`,"api.copyUrlHint":`Klick um URL zu kopieren`,"api.urlCopied":`URL kopiert`,"api.copyExampleHint":`Klick um Beispiel zu kopieren`,"api.exampleCopied":`Beispiel kopiert`,"api.colName":`Name`,"api.colKey":`Schlüssel`,"api.colCreated":`Erstellt`,"api.confirm":`Bestätigen`,"api.deleteAria":`API-Schlüssel löschen`,"api.usageSampleInput":`Hallo, Welt!`,"api.clientConfig.title":`Client-Konfiguration`,"api.clientConfig.rowsLabel":`Client verbinden`,"api.clientConfig.details":`Details`,"api.clientConfig.detailsAria":`Details zur {client}-Konfiguration`,"api.clientConfig.copyAria":`{client}-Konfiguration kopieren`,"api.clientConfig.downloadAria":`{client}-Konfiguration herunterladen`,"api.clientConfig.rowMeta":`{destination} · {count} Modell(e)`,"api.clientConfig.rowError":`Die {client}-Konfiguration konnte nicht erstellt werden.`,"api.clientConfig.copiedAnnounceClient":`{client}-Konfiguration in die Zwischenablage kopiert.`,"api.clientConfig.clientOpencode":`OpenCode`,"api.clientConfig.clientPi":`Pi`,"api.clientConfig.clientOmp":`OMP`,"api.clientConfig.clientHermes":`Hermes`,"api.clientConfig.clientOpenclaw":`OpenClaw`,"api.clientConfig.clientKimi":`Kimi Code`,"api.clientConfig.clientGajae":`Gajae Code`,"api.clientConfig.clientDsh":`DeepSeek Harness (DSH)`,"api.clientConfig.clientMcode":`MiniMax Code`,"api.clientConfig.clientZcode":`ZCode`,"api.clientConfig.clientPrime":`Prime Agent`,"api.clientConfig.clientAside":`Aside`,"api.clientConfig.copy":`Konfiguration kopieren`,"api.clientConfig.download":`Herunterladen`,"api.clientConfig.loading":`Client-Konfiguration wird erstellt…`,"api.clientConfig.jsonLabel":`{client}-Konfiguration`,"api.clientConfig.destination":`Zieldatei`,"api.clientConfig.envHint":`Schlüssel vor dem Start setzen`,"api.clientConfig.mergeWarning":`Führe dies in die Zieldatei ein. Ein Ersetzen würde deine anderen Provider und MCP-Einstellungen entfernen.`,"api.clientConfig.modelCount":`{count} Modell(e) exportiert`,"api.clientConfig.missingLimits":`{count} von {total} Modell(en) haben kein Kontextlimit; der Client verwendet dafür seine eigenen Vorgaben.`,"api.clientConfig.noKeyYet":`Für {env} existiert noch kein Schlüssel. Erzeuge oben einen Schlüssel, bevor du diese Konfiguration außerhalb von Loopback nutzt.`,"api.clientConfig.loadFailed":`Die Modellliste konnte nicht gelesen werden, daher wurde keine Client-Konfiguration erzeugt.`,"api.clientConfig.copiedAnnounce":`Client-Konfiguration in die Zwischenablage kopiert.`,"api.clientConfig.copyFailed":`Client-Konfiguration konnte nicht kopiert werden.`,"api.clientConfig.downloadedAnnounce":`{filename} heruntergeladen. Es hat sich noch nichts geändert — führe die Datei selbst in {destination} ein.`,"api.clientConfig.whereDisclosure":`Wohin diese Datei gehört`,"api.clientConfig.whereBody":`Der Pfad oben ist der globale Speicherort. Eine projektlokale Konfigurationsdatei im Arbeitsverzeichnis hat Vorrang, und der Schlüssel wird aus der in der Konfiguration genannten Umgebungsvariable gelesen — nie aus dieser Datei.`,"api.keysLoadFailed":`API-Schlüssel konnten nicht geladen werden.`,"api.createFailed":`API-Schlüssel konnte nicht erstellt werden.`,"api.deleteFailed":`API-Schlüssel konnte nicht gelöscht werden.`,"api.auth.endpoint":`Endpunkt`,"api.auth.required":`Erforderlich`,"api.auth.accepted":`Akzeptiert`,"api.auth.rejected":`Nicht akzeptiert`,"api.auth.testProtocol":`{protocol} testen`,"api.auth.testNeedsFreshKey":`Für einen authentifizierten Test einen Schlüssel erzeugen und den einmalig angezeigten Wert auf dem Bildschirm lassen.`,"api.key.name":`Schlüsselname`,"api.key.rename":`Umbenennen`,"api.key.saveName":`Namen speichern`,"api.key.renaming":`Wird gespeichert…`,"api.key.renameFailed":`Schlüssel konnte nicht umbenannt werden. Deine Eingabe wurde behalten.`,"api.key.deleting":`Wird gelöscht…`,"api.rotation.title":`Schlüsselrotation`,"api.rotation.description":`Erstellt einen Ersatzschlüssel; der aktuelle Schlüssel bleibt während einer kurzen Übergangszeit gültig.`,"api.rotation.start":`Rotation starten`,"api.rotation.starting":`Wird gestartet…`,"api.rotation.pending":`Die Rotation ist ausstehend. Aktualisiere und prüfe den Client vor dem Abschluss.`,"api.rotation.expires":`Übergangszeit endet:`,"api.rotation.secretOnce":`Ersatzschlüssel — wird nur einmal angezeigt. Vor dem Schließen kopieren.`,"api.rotation.commit":`Rotation abschließen`,"api.rotation.abort":`Rotation abbrechen`,"api.rotation.failed":`Die Rotationsaktion wurde nicht abgeschlossen. Vor einem neuen Versuch aktualisieren.`,"api.rotation.startFailed":`Schlüsselrotation konnte nicht gestartet werden.`,"api.key.copyFailed":`Schlüssel konnte nicht kopiert werden. Vor dem Schließen dieses Panels manuell markieren und kopieren.`,"api.attribution.title":`Zugeordnete Nutzung`,"api.attribution.requests7d":`Anfragen, letzte 7 Tage`,"api.attribution.totalRequests":`Zugeordnete Anfragen gesamt`,"api.attribution.totalRequestsAvailable":`Anfragen im verfügbaren Verlauf`,"api.attribution.sinceAvailable":`Verfügbare Zuordnung seit`,"api.attribution.lastUsed":`Zuletzt verwendet`,"api.attribution.since":`Zuordnung verfügbar seit`,"api.attribution.neverUsed":`Seit Beginn der Zuordnung nicht verwendet`,"api.attribution.unavailable":`Keine Nutzungsdaten`,"api.attribution.unavailableDetail":`Es wurde noch keine Nutzung zugeordnet. Anfragen von vor dem Start der Zuordnung lassen sich nicht rückwirkend zuweisen.`,"api.attribution.ambiguous":`Zwei Schlüssel teilen sich diese ID, daher lässt sich die Nutzung keinem davon zuordnen. Vergib in der Konfigurationsdatei je Schlüssel eine eindeutige ID.`,"api.attribution.railAmbiguous":`doppelte ID`,"claude.subtitle":`GPT, Gemini und andere Modelle in Claude Code verwenden.`,"claude.enabledLabel":`Claude-Verbindung`,"claude.enabledHint":`Wenn aus, kann Claude Code diesen Proxy nicht verwenden.`,"claude.authMode":`Auth-Modus`,"claude.authModeHint":`Subscription erfordert Claude-Konto, Proxy funktioniert ohne Anthropic-Konto`,"claude.authModeSubscription":`Subscription (Claude-Konto)`,"claude.authModeProxy":`Proxy (kein Konto nötig)`,"claude.authModeAuto":`Auto (Claude-Anmeldung erkennen)`,"claude.effectiveMode.label":`Beim nächsten Start aktiv`,"claude.effectiveMode.manual":`Manuell: {mode}`,"claude.effectiveMode.autoPresent":`Auto: Abo — Claude-Anmeldung über {source} gefunden`,"claude.effectiveMode.autoAbsent":`Auto: Proxy-Modus — keine Claude-Anmeldung gefunden`,"claude.effectiveMode.autoUnknown":`Auto: Abo — Anmeldung konnte nicht geprüft werden`,"claude.effectiveMode.admissionKey":`Der API-Schlüssel dieses Proxys wird weiterhin gesendet.`,"claude.authSource.claude-json-oauth":`Claude-Konto`,"claude.authSource.claude-credentials-file":`Anmeldedatei`,"claude.authSource.macos-keychain":`macOS-Schlüsselbund`,"claude.authSource.exported-env":`Umgebungsvariable`,"claude.authSource.unknown":`erkannte Anmeldedaten`,"claude.systemEnv":`Auto-Verbindung`,"claude.systemEnvDesc":`Wenn an, wird claude in jedem Terminal automatisch über den Proxy geleitet.`,"claude.systemEnvUnsupported":`Auto-Verbindung ist nur unter macOS verfügbar. Starten Sie Claude auf diesem System mit {cmd}.`,"claude.systemEnvWarn":`⚠ Die Terminal-App muss vollständig beendet und neu gestartet werden. Nicht empfohlen.`,"claude.fastMode":`Fast Mode (OpenAI)`,"claude.fastModeDesc":`Steuert service_tier für OpenAI-Modelle. ON = Priorität (schneller). OFF = Standard. Auto = Durchleitung.`,"claude.fastAuto":`Auto`,"claude.fastOn":`ON`,"claude.fastOff":`OFF`,"claude.autoContext":`Großen Kontext automatisch nutzen`,"claude.autoContextDesc":`Steuert, wie weit die 1M-Markierung geht. AN: jedes Modell, dessen Fenster den Komprimierungsschwellwert fasst, erhält eine Big-Context-Zeile. AUS: nur echte 1M-Modelle.`,"claude.autoContextInert":`Inaktiv, weil in der Konfigurationsdatei ein alter Kontextgrößen-Wert (maxContextTokens) steht. Dort entfernen, um es wieder zu aktivieren.`,"claude.autoCompactWindow":`Punkt für Auto-Zusammenfassung`,"claude.autoCompactDefault":`{value} (Standard)`,"claude.autoCompactWindowDesc":`Ältere Nachrichten werden an diesem Punkt zusammengefasst. Das eigene Limit jedes Modells wird nie überschritten — 200k-Modelle bleiben unberührt.`,"claude.autoCompactWindowWarn":`Eine Änderung kann GPT-Modelle stören — liegt der Wert über dem echten Modelllimit, kommt es vor der Zusammenfassung zu Fehlern.`,"claude.injectAgents":`Subagenten automatisch registrieren`,"claude.injectAgentsDesc":`Registriert die im Subagenten-Tab gewählten Modelle (plus das aktuelle Standardmodell) als aufrufbare Claude-Code-Agenten (ocx-*). Gilt ab der nächsten Sitzung.`,"claude.webSearchSidecar":`Websuche-Sidecar überschreiben`,"claude.webSearchSidecarHint":`Überschreibt den allgemeinen Websuche-Sidecar für Claude-Code-Anfragen.`,"claude.visionSidecar":`Vision-Sidecar überschreiben`,"claude.visionSidecarHint":`Überschreibt den allgemeinen Vision-Sidecar für Claude-Code-Anfragen.`,"claude.useMainSetting":`Haupteinstellung verwenden`,"claude.sidecarModelPlaceholder":`Modell der Haupteinstellung`,"claude.quickstart":`Erste Schritte`,"claude.quickstartHint":`{cmd} öffnet Claude Code über den Proxy. Dein claude.ai-Login bleibt aktiv.`,"claude.manualEnv":`Manuelle Einrichtung (erweitert)`,"claude.smallFastModel":`Hintergrund-Hilfsmodell`,"claude.smallFastModelHint":`Das Modell für Hintergrundarbeit wie Chat-Zusammenfassungen und Themenerkennung. Auch der haiku-Alias der Subagenten nutzt es. Leer = Claude-Standard (Haiku).`,"claude.smallFastModelAccurateHint":`Das Modell, das Claude Code für Hintergrundaufgaben wie Chat-Zusammenfassungen und Themenerkennung verwendet. Auch der haiku-Alias der Subagenten nutzt es.`,"claude.smallFastModelUnsetOption":`Claude Code wählen lassen (natives Modell)`,"claude.smallFastModelNativeWarning":`Wenn kein Modell gewählt ist, setzt OpenCodex keine Hilfsmodell-Overrides. Claude Code kann dann sein natives Sonnet-Modell verwenden, wodurch Kosten bei deinem nativen Anbieter entstehen können.`,"claude.slotUnset":`Claude-Standard verwenden`,"claude.modelMap":`Modell-Abfangen`,"claude.modelMapHint":`Fängt Anfragen für ein bestimmtes Modell ab und leitet sie an das gewählte Modell um. Standardmäßig leer — wirkt erst mit einer Regel.`,"claude.mapFrom":`Originalmodell (z. B. claude-sonnet-4-5)`,"claude.mapTo":`Ersetzen durch (z. B. gemini/gemini-3-pro)`,"claude.addMapping":`Regel hinzufügen`,"claude.removeMapping":`Regel entfernen`,"claude.aliases":`Verfügbare Modelle`,"claude.aliasesHint":`Modelle, die im /model-Menü von Claude Code erscheinen.`,"claude.aliasProviderOther":`Sonstiges`,"claude.loading":`Lädt…`,"claude.loadFail":`Claude-Einstellungen konnten nicht geladen werden`,"claude.saved":`Gespeichert.`,"claude.saveFailed":`Speichern fehlgeschlagen`,"claude.networkError":`Netzwerkfehler — läuft der Proxy?`,"claude.toggleAria":`Claude-Verbindung umschalten`,"claude.none":`Keine`,"common.close":`Schließen`,"common.ok":`OK`,"app.logoAria":`opencodex-Logo`,"app.claudeOn":`Claude AN`,"app.claudeOff":`Claude AUS`,"usage.dayMon":`Mo`,"usage.dayWed":`Mi`,"usage.dayFri":`Fr`,"usage.heatmap.tooltipTokens":`{tokens} Tokens`,"usage.heatmap.tooltipRequests":`{requests} Anfragen`,"nav.storage":`Speicher`,"storage.title":`Speicher`,"storage.subtitle":`Zeigt, was CODEX_HOME belegt. Die Bereinigung lässt aktive Sitzungen unberührt.`,"storage.loading":`Speicher wird gescannt…`,"storage.empty":`CODEX_HOME ist leer oder fehlt — nichts zu berichten.`,"storage.error":`Speicher-Scan fehlgeschlagen. Prüfe, ob CODEX_HOME auf ein gültiges Verzeichnis zeigt.`,"storage.refresh":`Neu scannen`,"storage.rescanned":`Scan abgeschlossen.`,"storage.card.total":`Gesamtgröße`,"storage.card.files":`Dateien`,"storage.card.home":`CODEX_HOME`,"storage.snapshot.lastScan":`Letzter Scan`,"storage.snapshot.scanning":`Scanne…`,"storage.snapshot.unavailable":`Noch kein Scan.`,"storage.cleanupCard.title":`Speicher freigeben`,"storage.cleanupCard.tabs":`Bereinigungsoptionen`,"storage.cleanupCard.tab.policy":`Richtlinie`,"storage.cleanupCard.tab.quarantine":`Quarantäne`,"storage.cleanup.noArchives":`Keine archivierten Sitzungen zum Bereinigen.`,"storage.section.buckets":`Bereiche`,"storage.section.largest":`Größte Dateien`,"storage.workspace.overview":`Übersicht`,"storage.workspace.selectBucket":`Wähle einen Bucket aus der Liste, um die Aufschlüsselung zu sehen.`,"storage.col.bucket":`Bereich`,"storage.col.size":`Größe`,"storage.col.files":`Dateien`,"storage.col.oldest":`Älteste`,"storage.col.newest":`Neueste`,"storage.col.rows":`DB-Zeilen`,"storage.rows.unknown":`unbekannt (gesperrt)`,"storage.bucket.sessions":`Aktive Sitzungen`,"storage.bucket.archived_sessions":`Archivierte Sitzungen`,"storage.bucket.logs_db":`Log-Datenbank`,"storage.bucket.state_db":`Status-Datenbank`,"storage.bucket.attachments":`Anhänge`,"storage.bucket.deletion_manifests":`Lösch-Manifeste`,"storage.bucket.other":`Sonstiges`,"storage.cleanup.title":`Archivbereinigung`,"storage.cleanup.help":`Entfernt die ältesten archivierten Sitzungen nach Prozentsatz. Aktive Sitzungen werden nie angefasst. Standard ist Quarantäne — Dateien wandern nach CODEX_HOME/.trash.`,"storage.cleanup.slider":`Ältester Archivanteil`,"storage.cleanup.percent":`{percent}%`,"storage.cleanup.preset":`{percent}`,"storage.cleanup.preview":`Vorschau`,"storage.cleanup.confirmTitle":`Archivbereinigung bestätigen`,"storage.cleanup.confirmBody":`Es werden {count} archivierte Datei(en) (~{size}) verarbeitet, die ältesten {percent}%.`,"storage.cleanup.moreFiles":`…und {n} weitere`,"storage.cleanup.permanent":`Dauerhaft löschen (ohne Quarantäne)`,"storage.cleanup.permanentWarn":`Dauerhaftes Löschen kann nicht rückgängig gemacht werden.`,"storage.cleanup.quarantineNote":`Dateien wandern nach .trash unter CODEX_HOME. Du kannst sie im Tab Quarantäne wiederherstellen.`,"storage.cleanup.cancel":`Abbrechen`,"storage.cleanup.confirmQuarantine":`In Quarantäne`,"storage.cleanup.confirmPermanent":`Dauerhaft löschen`,"storage.cleanup.doneQuarantine":`{count} Datei(en) in Quarantäne ({size}).`,"storage.cleanup.donePermanent":`{count} Datei(en) dauerhaft gelöscht ({size}).`,"storage.cleanup.previewFailed":`Vorschau fehlgeschlagen.`,"storage.cleanup.cleanupFailed":`Bereinigung fehlgeschlagen.`,"storage.cleanup.err.codex_busy":`Codex verwendet state.sqlite — beende Codex und versuche es erneut.`,"storage.cleanup.err.stale_preview":`Archivdateien haben sich seit der Vorschau geändert — führe Vorschau erneut aus.`,"storage.cleanup.err.restore_pending_overlap":`Ausgewählte Archive überschneiden sich mit einer unvollständigen Wiederherstellung — zuerst Wiederherstellung abschließen oder erneut versuchen.`,"storage.cleanup.err.referenced_history":`Ausgewählte Archive werden noch von Fork- oder paginierter Historie referenziert.`,"storage.cleanup.err.invalid_digest":`Vorschaudigest fehlt oder ist ungültig.`,"storage.cleanup.err.invalid_mode":`Modus muss quarantine oder permanent sein.`,"storage.cleanup.err.fs_failed":`Dateisystem-Bereinigung fehlgeschlagen. Einige Änderungen können bereits angewendet sein — prüfen Sie CODEX_HOME/.trash und den angezeigten Wiederherstellungspfad.`,"storage.cleanup.err.fs_failed_trash":`Dateisystem-Bereinigung fehlgeschlagen. Einige Änderungen können bereits angewendet sein — prüfen Sie {trashDir} und manifest.json auf wiederherstellbare Dateien.`,"storage.cleanup.err.db_reconcile_failed":`Codex-Statusdatenbank konnte nicht aktualisiert werden.`,"storage.cleanup.err.cleanup_failed":`Bereinigung fehlgeschlagen.`,"storage.trash.title":`Quarantäne`,"storage.trash.help":`Archivierte Sitzungen in CODEX_HOME/.trash. Wiederherstellen legt JSONL-Dateien und Thread-Zeilen zurück.`,"storage.trash.empty":`Keine Quarantäne-Einträge.`,"storage.trash.loading":`Quarantäne wird geladen…`,"storage.trash.col.when":`Quarantäne seit`,"storage.trash.col.files":`Dateien`,"storage.trash.col.size":`Größe`,"storage.trash.col.mode":`Modus`,"storage.trash.col.id":`Eintrag`,"storage.trash.restore":`Wiederherstellen`,"storage.trash.confirmTitle":`Quarantäne-Eintrag wiederherstellen?`,"storage.trash.confirmBody":`{count} Datei(en) (~{size}) aus {id} zurück in archivierte Sitzungen legen.`,"storage.trash.cancel":`Abbrechen`,"storage.trash.confirmRestore":`Wiederherstellen`,"storage.trash.done":`{count} Datei(en) wiederhergestellt ({size}).`,"storage.trash.restoreFailed":`Wiederherstellung fehlgeschlagen.`,"storage.trash.listFailed":`Quarantäne-Einträge konnten nicht geladen werden.`,"storage.trash.mode.quarantine":`quarantine`,"storage.trash.mode.permanent":`permanent (unvollständig)`,"storage.trash.err.codex_busy":`Codex verwendet state.sqlite — beende Codex und versuche es erneut.`,"storage.trash.err.invalid_trash":`Trash-Eintrags-ID fehlt oder ist ungültig.`,"storage.trash.err.missing_trash":`Trash-Eintrag wurde nicht gefunden.`,"storage.trash.err.dest_exists":`Wiederherstellungsziel existiert bereits — entferne oder benenne die Archivdatei um und versuche es erneut.`,"storage.trash.err.fs_failed":`Dateisystem-Wiederherstellung fehlgeschlagen. Einige Dateien können bereits wiederhergestellt sein — prüfe archived_sessions und .trash.`,"storage.trash.err.storage_mutation_busy":`Eine andere Speicher-Bereinigung oder Wiederherstellung läuft — bitte kurz warten.`,"storage.trash.err.db_reconcile_failed":`Codex-Statusdatenbankzeilen konnten nicht wiederhergestellt werden.`,"storage.trash.err.restore_failed":`Wiederherstellung fehlgeschlagen.`,"storage.trash.err.restore_worker_timeout":`Wiederherstellung dauerte zu lange (über 10 Minuten) und wurde abgebrochen.`,"storage.trash.err.restore_worker_aborted":`Wiederherstellung wurde beim Herunterfahren abgebrochen.`,"storage.trash.err.restore_worker_failed":`Wiederherstellungs-Worker ist abgestürzt oder unerwartet fehlgeschlagen.`,"storage.policy.title":`Automatische Bereinigungsrichtlinie`,"storage.policy.help":`Optionale Stapelbereinigung, wenn archivierte Sitzungen einen Schwellwert überschreiten. Standardmäßig aus — wird nie automatisch aktiviert.`,"storage.policy.loading":`Richtlinie wird geladen…`,"storage.policy.loadFailed":`Bereinigungsrichtlinie konnte nicht geladen werden.`,"storage.policy.saveFailed":`Bereinigungsrichtlinie konnte nicht gespeichert werden.`,"storage.policy.runFailed":`Richtlinienlauf fehlgeschlagen.`,"storage.policy.alreadyRunning":`Ein Bereinigungsrichtlinienlauf läuft bereits.`,"storage.policy.invalid":`Ungültige Richtlinienwerte.`,"storage.policy.enabled":`Automatische Bereinigung aktivieren`,"storage.policy.enabledHint":`Standard ist aus. Bei Aktivierung nur nach gewähltem Zeitplan (oder Jetzt ausführen).`,"storage.policy.threshold":`Wenn Archivgröße größer als (GiB)`,"storage.policy.trigger":`Auslöser`,"storage.policy.target":`Bereinigungsziel`,"storage.policy.targetPercent":`Älteste Archive entfernen (%)`,"storage.policy.targetReduce":`Archivgröße reduzieren auf (GiB)`,"storage.policy.thresholdInc":`Schwellwert erhöhen`,"storage.policy.thresholdDec":`Schwellwert verringern`,"storage.policy.percentInc":`Prozent erhöhen`,"storage.policy.percentDec":`Prozent verringern`,"storage.policy.reduceInc":`Zielgröße erhöhen`,"storage.policy.reduceDec":`Zielgröße verringern`,"storage.policy.schedule":`Zeitplan`,"storage.policy.schedule.manual":`Nur manuell`,"storage.policy.schedule.startup":`Beim Proxy-Start`,"storage.policy.schedule.daily":`Täglich`,"storage.policy.schedule.weekly":`Wöchentlich`,"storage.policy.mode":`Löschmodus`,"storage.policy.mode.quarantine":`Quarantäne (Standard)`,"storage.policy.mode.permanent":`Endgültig löschen`,"storage.policy.permanentWarn":`Endgültiger Modus kann nicht rückgängig gemacht werden. Quarantäne bevorzugen, sofern unsicher.`,"storage.policy.lastRun":`Letzter Lauf`,"storage.policy.lastRunDetail":`{count} entfernt · {size} freigegeben`,"storage.policy.nextRun":`Nächster Lauf`,"storage.policy.never":`Nie`,"storage.policy.save":`Speichern`,"storage.policy.runNow":`Jetzt ausführen`,"storage.policy.running":`Läuft…`,"storage.policy.saved":`Richtlinie gespeichert.`,"storage.policy.skippedDisabled":`Richtlinie ist deaktiviert — zuerst aktivieren.`,"storage.policy.skippedUnder":`Archivgröße unter dem Schwellwert — nichts zu tun.`,"storage.policy.skippedEmpty":`Keine Archivkandidaten passend zum Ziel.`,"storage.policy.doneQuarantine":`Richtlinie hat {count} Datei(en) in Quarantäne ({size}).`,"storage.policy.donePermanent":`Richtlinie hat {count} Datei(en) endgültig gelöscht ({size}).`,"storage.policy.metadataSaveWarning":`Der Richtlinienlauf wurde beendet, aber seine Planungsmetadaten konnten nicht gespeichert werden.`,"modal.back":`Zurück`,"modal.badge.oauth":`OAuth`,"modal.customProvider":`Benutzerdefinierter Anbieter`,"modal.failedStatus":`Fehlgeschlagen ({status})`,"modal.loginError":`Login-Fehler: {error}`,"modal.badge.codexLogin":`Codex-Login`,"modal.badge.local":`Lokal`,"modal.badge.apiKey":`API-Schlüssel`,"modal.badge.direct":`Direct`,"modal.badge.pool":`Pool`,"modal.badge.free":`Kostenlos`,"modal.invalidPreset":`Diese integrierte Anbietervorlage ist unvollständig. Starten Sie den Proxy neu und versuchen Sie es erneut.`,"modal.freeTierTitle":`Kostenloser Tarif`,"modal.freeTierDefault":`Kein API-Schlüssel nötig. Funktioniert sofort.`,"modal.tab.accounts":`Konten`,"modal.tab.free":`Kostenlos`,"modal.tab.paid":`Bezahlt`,"modal.accountsHint":`Hier ChatGPT/Codex, OAuth-Provider und API-Key-Konten anmelden. OpenAI ist eingebaut — anmelden statt erneut hinzufügen.`,"modal.accountsCodexAuthLink":`Codex Auth`,"modal.notListed":`Provider nicht dabei? Eigenen hinzufügen`,"modal.catalogLoading":`Katalog wird geladen…`,"modal.accountLogin":`Anmelden`,"modal.accountLogout":`Abmelden`,"modal.accountAdd":`Konto hinzufügen`,"modal.accountManage":`Verwalten`,"modal.accountCodexPool":`ChatGPT-Kontopool`,"modal.accountLoggedIn":`Angemeldet`,"modal.accountLoggedOut":`Nicht angemeldet`,"quota.fiveHourLimit":`5-Stunden-Limit`,"quota.ageMinutes":`{n} Min.`,"quota.ageHours":`{n} Std.`,"quota.ageDays":`{n} T.`,"quota.observedAgo":`Vor {age} erfasst`,"quota.observedHint":`Meta meldet die Nutzung nur während einer Streaming-Antwort. Dies ist der zuletzt erfasste Wert, keine Live-Messung.`,"quota.weeklyLimit":`Wochenlimit`,"quota.monthlyLimit":`30-Tage-Limit`,"quota.cursorFirstParty":`Erstanbieter-Modelle`,"quota.cursorApiUsage":`API-Nutzung`,"quota.totalSubscriptionCredits":`Gesamtes Abo-Guthaben`,"quota.creditsBalance":`Guthabenstand`,"quota.creditsPeriodEnds":`Abrechnungszeitraum endet am {date}`,"quota.usedPercent":`{pct} % genutzt`,"quota.limitReached":`Limit erreicht`,"quota.resetsToday":`Zurücksetzung heute um {time}`,"quota.resetsTomorrow":`Zurücksetzung morgen um {time}`,"quota.resetsAt":`Zurücksetzung {when}`,"quota.resetsRelativeMinutes":`Zurücksetzung in {n} Min.`,"quota.resetsRelativeHours":`Zurücksetzung in {n} Std.`,"pws.status.ready":`Bereit`,"pws.status.needsSetup":`Einrichtung nötig`,"pws.status.needsAttention":`Aufmerksamkeit nötig`,"pws.auth.chatgptPassthrough":`ChatGPT-Passthrough`,"pws.auth.noKey":`Kein Schlüssel nötig`,"pws.freeTitle":`Kostenlos (Schlüssel ggf. erforderlich)`,"pws.localTitle":`Lokale Laufzeit`,"pws.modelCountOne":`1 Modell`,"pws.modelCount":`{count} Modelle`,"pws.rail.suffixDefault":` · Standard`,"pws.rail.suffixLocal":` · lokal`,"pws.rail.suffixFree":` · kostenlos`,"pws.rail.selectAria":`{name} auswählen — {status}{suffix}`,"pws.searchPlaceholder":`Provider durchsuchen…`,"pws.filterAria":`Provider filtern`,"pws.providerFiltersAria":`Provider-Filter`,"pws.filters":`Filter`,"pws.filterStatus":`Status`,"pws.pricing":`Preis`,"pws.paid":`Bezahlt`,"pws.filterType":`Typ`,"pws.type.cloud":`Cloud`,"pws.type.local":`Lokal`,"pws.type.selfHosted":`Selbst gehostet`,"pws.type.login":`Login`,"pws.sort":`Sortierung`,"pws.sortProvidersAria":`Provider sortieren`,"pws.sort.az":`A–Z`,"pws.sort.za":`Z–A`,"pws.sort.freePaid":`Kostenlos zuerst`,"pws.sort.paidFree":`Bezahlt zuerst`,"pws.sort.accountsFirst":`Konten zuerst`,"pws.resetAll":`Alle zurücksetzen`,"pws.providerList":`Provider-Liste`,"pws.providersAria":`Provider`,"pws.groupReady":`Bereit ({count})`,"pws.groupNeedsSetup":`Einrichtung nötig ({count})`,"pws.groupDisabled":`Deaktiviert ({count})`,"pws.noSearchResults":`Keine Provider entsprechen der Suche.`,"pws.noMatchFilters":`Keine Provider entsprechen den Filtern.`,"pws.noProvidersConfigured":`Keine Provider konfiguriert.`,"pws.workspaceMainAria":`Provider-Details`,"pws.detailComingSoon":`Detailansicht folgt — nutze die klassische Ansicht zur Verwaltung.`,"pws.selectPrompt":`Wähle einen Provider aus der Liste.`,"pws.connectFirst":`Verbinde deinen ersten Provider`,"pws.empty.browseFree":`Kostenlose Provider ansehen`,"pws.empty.browseFreeDesc":`Ohne Abo starten`,"pws.empty.connectAccount":`Konto verbinden`,"pws.empty.connectAccountDesc":`ChatGPT- oder Provider-Login nutzen`,"pws.empty.addEndpoint":`Endpunkt hinzufügen`,"pws.empty.addEndpointDesc":`Eigene Base-URL und API-Schlüssel`,"pws.tab.overview":`Übersicht`,"pws.tab.models":`Modelle`,"pws.tab.usage":`Nutzung`,"pws.tab.accounts":`Konten`,"pws.tab.settings":`Einstellungen`,"pws.connection":`Verbindung`,"pws.status.connected":`Verbunden`,"pws.attentionTitle":`Aufmerksamkeit nötig`,"pws.attention.reauth":`Aktives Konto muss erneut authentifiziert werden`,"pws.attention.reauthForward":`Aktives Codex-Konto muss erneut authentifiziert werden — unter Konten beheben`,"pws.attention.missingCredentials":`Anmeldedaten fehlen`,"pws.cell.auth":`Authentifizierung`,"pws.cell.note":`Notiz`,"pws.cell.defaultModel":`Standardmodell`,"pws.statsAria":`Provider-Statistiken`,"pws.statsTitle":`Statistiken`,"pws.stats.totalRequests":`Anfragen (30 T.)`,"pws.stats.totalTokens":`Tokens (30 T.)`,"pws.stats.quotaUpdated":`Kontingent aktualisiert`,"pws.stats.quotaTracked":`Limits siehe Nutzungs-Tab.`,"pws.stats.source":`Quelle`,"pws.usageLast30d":`Nutzung (letzte 30 Tage)`,"pws.estimatedCost":`Geschätzte Kosten`,"pws.costDisclaimer":`Schätzung basierend auf API-Listenpreisen, keine tatsächliche Abrechnung.`,"pws.modelBreakdown":`Modellaufschlüsselung`,"pws.col.model":`Modell`,"pws.col.cost":`Gesch. Kosten`,"pws.col.tokens":`Token`,"pws.col.requests":`Anfr.`,"pws.col.share":`Anteil`,"pws.tokenInput":`Eingabe`,"pws.tokenOutput":`Ausgabe`,"pws.metricRequests":`Anfragen`,"pws.metricTokens":`Tokens`,"pws.usageUnavailable":`Noch keine Nutzung erfasst.`,"pws.rateLimits":`Limits`,"pws.quotaUnavailable":`Keine Kontingentdaten für diesen Provider.`,"pws.accountQuotaUnavailable":`Ratenlimit-Daten vorübergehend nicht verfügbar; falls vorhanden, werden zuletzt bekannte Werte angezeigt.`,"pws.selected":`Ausgewählt`,"pws.copyModelId":`ID kopieren`,"pws.modelCopied":`Kopiert!`,"pws.modelsAvailable":`{count} verfügbar`,"pws.modelSearchPlaceholder":`Modelle filtern…`,"pws.modelsLoading":`Modelle werden geladen…`,"pws.modelsLoadFailed":`Modelle konnten nicht geladen werden.`,"pws.modelsNeedsReauth":`Konto muss neu angemeldet werden, bevor Live-Modelle geladen werden. Zeige konfigurierte Modelle.`,"pws.modelsConfiguredFallback":`Zeige konfigurierte Modelle (Live-Erkennung nicht verfügbar).`,"pws.modelsTruncated":`Zeige die ersten {shown} von {total} Modellen. Filtern, um die Liste einzugrenzen.`,"pws.retry":`Erneut versuchen`,"pws.noModels":`Keine Modelle für diesen Provider gefunden.`,"pws.noModelMatch":`Keine Modelle entsprechen dem Filter.`,"pws.adapterBaseRequired":`Adapter und Basis-URL sind erforderlich.`,"pws.addAccount":`Konto hinzufügen`,"pws.addKey":`API-Schlüssel hinzufügen`,"pws.apiKeys":`API-Schlüssel`,"pws.authMode":`Auth-Modus`,"pws.availableAccounts":`Verfügbare Konten`,"pws.accountOrdinal":`Konto {count}`,"pws.accountsLoading":`Konten werden geladen…`,"pws.accountsLoadFailed":`Konten konnten nicht geladen werden.`,"pws.retryAccounts":`Erneut versuchen`,"pws.noAccounts":`Noch keine Konten verbunden.`,"pws.cockpitImportDescription":`Importieren Sie einen Cockpit-Tools-Antigravity-JSON-Export von diesem Gerät. Der Dateiinhalt wird nicht angezeigt.`,"pws.cockpitImportFileLabel":`Cockpit-Tools-Antigravity-JSON-Export`,"pws.cockpitImportChooseFile":`JSON-Datei auswählen`,"pws.cockpitImporting":`Import wird ausgeführt…`,"pws.cockpitImportInvalid":`Die ausgewählte Datei ist kein gültiger JSON-Export oder zu groß.`,"pws.cockpitImportFailed":`Der Kontoimport konnte nicht abgeschlossen werden.`,"pws.cockpitImportComplete":`Import abgeschlossen: {imported} importiert, {updated} aktualisiert, {failed} fehlgeschlagen, {unsupported} nicht unterstützt.`,"pws.accountSwitching":`Wechsel läuft…`,"pws.accountCurrent":`Aktuelles Konto`,"pws.defaultModelNone":`Keins (Standard des Anbieters verwenden)`,"pws.discardSettings":`Verwerfen`,"pws.jsonEditorDesc":`Bearbeiten Sie die JSON-Konfiguration des Anbieters. Änderungen werden sofort gespeichert.`,"pws.jsonEditorTitle":`JSON-Editor — {name}`,"pws.jsonRestore":`Wiederherstellen`,"pws.jsonSave":`Speichern`,"pws.loggedInTitle":`Angemeldet`,"pws.notLoggedInTitle":`Nicht angemeldet`,"pws.note":`Notiz`,"pws.allowPrivateNetwork":`Lokales/privates Netzwerk erlauben`,"pws.liveModels":`Modelle beim Anbieter erkennen`,"pws.liveModelsDesc":`Lädt den Live-Modellkatalog des Anbieters. Ausschalten, um nur konfigurierte statische Modelle zu verwenden.`,"pws.xaiResponsesOptIn":`Responses API für Grok 4.5 und 4.6 verwenden`,"pws.xaiResponsesOptInDesc":`Leitet beide Modelle über openai-responses. Andere Grok-Modelle und das Tier-Verhalten bleiben unverändert.`,"pws.xaiResponsesOptInMixed":`Teilweise aktiviert.`,"pws.cursorTransport":`Cursor-Transport`,"pws.cursorTransportHttp2":`HTTP/2 (Standard)`,"pws.cursorTransportHttp1":`HTTP/1.1 (Proxy-Kompatibilität)`,"pws.cursorTransportDesc":`Verwenden Sie HTTP/1.1, wenn Ihr Proxy den HTTP/2-Stream von Cursor nicht zuverlässig überträgt.`,"pws.optionalPlaceholder":`Optional`,"pws.providerId":`Anbieter-ID`,"pws.reauth":`Erneute Anmeldung erforderlich`,"pws.reauthenticate":`Erneut authentifizieren`,"pws.copyDoctor":`ocx doctor kopieren`,"pws.doctorCopied":`Kopiert`,"pws.healthCooldownHint":`Warten Sie, bis die Abkühlzeit endet. Prüfen Sie dieses Konto noch nicht.`,"pws.doctorCopyUnavailable":`Zwischenablage nicht verfügbar`,"pws.healthLabel.rateLimited":`Ratelimit`,"pws.healthLabel.quotaLimited":`Kontingent begrenzt`,"pws.healthLabel.reauthRequired":`Erneute Anmeldung erforderlich`,"pws.healthLabel.refreshFailed":`Aktualisierung fehlgeschlagen`,"pws.healthLabel.metadataMismatch":`Metadaten stimmen nicht überein`,"pws.healthLabel.credentialConflict":`Anmeldedaten-Konflikt`,"pws.healthSummary.rateLimited":`{provider} {account}: ratelimited bis {until}. Routing für dieses Konto ist bis dahin pausiert.`,"pws.healthSummary.quotaLimited":`{provider} {account}: Kontingent begrenzt bis {until}. Routing für dieses Konto ist bis dahin pausiert.`,"pws.healthSummary.reauthRequired":`{provider} {account}: erneute Anmeldung erforderlich.`,"pws.healthSummary.credentialConflict":`{provider} {account}: Anmeldedaten-Konflikt.`,"pws.healthSummary.metadataMismatch":`{provider} {account}: Metadaten stimmen nicht überein.`,"pws.healthSummary.staleCredentials":`{provider} {account}: unvollständige Anmeldedaten.`,"pws.removeConfirm":`Entfernen`,"pws.removeConfirmBody":`Anbieter "{name}" entfernen? Dies kann nicht rückgängig gemacht werden.`,"pws.removeDefaultConfirmBody":`Standardanbieter "{name}" entfernen? "{defaultProvider}" wird zum Standardanbieter. Dies kann nicht rückgängig gemacht werden.`,"pws.removeConfirmTitle":`Anbieter entfernen`,"pws.saveSettings":`Speichern`,"pws.pacingTitle":`Anfragetaktung`,"pws.pacingDesc":`Verteilt ausgehende Anfragestarts für diesen Anbieter gleichmäßig. Streaming-Antworten dürfen sich überlappen.`,"pws.pacingEnabled":`Aktiviert`,"pws.pacingRpm":`Anfragen pro Minute`,"pws.pacingRpmUnit":`RPM`,"pws.pacingDelay":`Mindestintervall (ms)`,"pws.pacingSlowerWins":`Das langsamere Anbieterlimit gilt. Modellregeln können nur stärker verzögern.`,"pws.pacingQueued":`in Warteschlange`,"pws.pacingNextSlot":`bis zum nächsten Slot`,"pws.pacingLastModel":`letztes Modell`,"pws.pacingNone":`Keine`,"pws.pacingModelOverrides":`Modellregeln`,"pws.pacingModel":`Modell`,"pws.pacingAdd":`Regel hinzufügen`,"pws.pacingRemove":`Entfernen`,"pws.pacingRemoveModel":`Anfragetaktung für {model} entfernen`,"pws.pacingRuleRequired":`Legen Sie zuerst ein Anbieterlimit oder eine Modellregel fest.`,"pws.saving":`Wird gespeichert…`,"pws.settingsSaved":`Einstellungen gespeichert.`,"pws.accountModeSaved":`Kontomodus gespeichert.`,"pws.accountModeFailed":`Kontomodus konnte nicht gewechselt werden.`,"pws.accountModeConfirm":`OpenAI-Kontomodus wechseln? Laufende Unterhaltungen werden dem anderen Kontosatz zugeordnet und die Quotennutzung wird unter dem neuen Modus erfasst.`,"pws.settingsUnsavedBar":`Es gibt ungespeicherte Änderungen.`,"pws.unsavedLeaveBody":`Es gibt ungespeicherte Änderungen. Vor dem Verlassen speichern?`,"pws.unsavedLeaveTitle":`Ungespeicherte Änderungen`,"pws.attentionRequired":`Aufmerksamkeit erforderlich`,"pws.attentionAria":`{name}: {reason}`,"pws.missingCredentials":`Zugangsdaten fehlen`,"pws.editJsonDesc":`Rohe Proxy-Konfiguration als JSON bearbeiten`,"pws.updatesUnavailable":`Anbieter-Updates sind nicht verfügbar.`,"pws.dashboard.title":`Anbieterübersicht`,"pws.dashboard.subtitle":`Verwalten Sie alle Ihre Modellanbieter an einem Ort.`,"pws.dashboard.rateLimits":`RATE LIMITS`,"pws.capacity.estimate":`Pool-Schätzung anhand konfigurierter Gewichtungen`,"pws.capacity.currentAccount":`Aktuelles effektives Konto`,"pws.capacity.nextRecovery":`Nächste Kapazitätswiederherstellung`,"pws.capacity.recoveryShare":`+{percent} % Pool-Kapazität`,"pws.capacity.incomplete":`Unvollständige Abdeckung: {excluded} Konten ausgeschlossen`,"pws.capacity.uncalibratedPlan":`{count} Konten mit unkalibriertem Tarif werden mit dem Basisgewicht gezählt; diese Schätzung kann daher konservativ sein`,"pws.capacity.partial":`Teilweise Fensterabdeckung: {count} Konten melden nicht jedes angezeigte Limitfenster`,"pws.capacity.windowPartial":`Teilweise`,"pws.capacity.windowPartialA11y":`{window}: unvollständige Kontoabdeckung`,"pws.dashboard.recentlyUsed":`KÜRZLICH VERWENDET`,"pws.dashboard.requests":`{count} Anfragen`,"pws.dashboard.checkedAgo":`Geprüft {time}`,"pws.dashboard.noQuota":`Keine Kontingentdaten`,"pws.dashboard.noUsage":`Noch keine Nutzungsdaten`,"pws.dashboard.noRateLimits":`Noch keine Limit-Daten`,"pws.allProviders":`Anbieterübersicht`,"pws.enabledLabel":`Aktiviert`,"pws.testConnection":`Verbindung testen`,"pws.testing":`Teste…`,"pws.connectionOk":`Verbindung OK`,"pws.connectionFailed":`Verbindung fehlgeschlagen`,"pws.connectionNotApplicable":`Nicht zutreffend — dieser Anbieter verwendet einen statischen Modellkatalog.`,"pws.editSettings":`Einstellungen bearbeiten`,"pws.viewUsage":`Detaillierte Nutzung anzeigen`,"pws.allSystemsOk":`Alle Systeme betriebsbereit`,"pws.apiKeyConfigured":`API-Schlüssel konfiguriert`,"pws.addApiKey":`API-Schlüssel hinzufügen`,"pws.loggedInAs":`Angemeldet als {email}`,"pws.notLoggedIn":`Nicht angemeldet`,"pws.passthrough":`Codex-Passthrough`,"pws.notes":`NOTIZEN`,"pws.notePlaceholder":`Notiz zu diesem Anbieter hinzufügen...`,"pws.noteSaved":`Notiz gespeichert`,"pws.authSummary":`AUTHENTIFIZIERUNG`,"time.justNow":`Gerade eben`,"time.notChecked":`Nicht geprüft`,"time.minutesAgo":`vor {n} Min.`,"time.hoursAgo":`vor {n} Std.`,"time.daysAgo":`vor {n} T.`,"modal.noMatch":`Kein Treffer.`,"modal.oauthDefaultNote":`Mit deinem Konto anmelden — kein API-Schlüssel nötig.`,"modal.oauthComingSoon":`OAuth-Login für {label} kommt im nächsten Update. Nutze vorerst einen API-Schlüssel.`,"modal.oauthComingSoonShort":`OAuth-Login für diesen Anbieter kommt im nächsten Update — nutze vorerst einen API-Schlüssel.`,"modal.useApiKeyInstead":`Stattdessen API-Schlüssel verwenden`,"modal.setupGuide":`Einrichtungsanleitung`,"modal.setupStep1Prefix":`Gehe zu`,"modal.setupDashboardLink":`{label}-Dashboard`,"modal.setupStep1Suffix":`und kopiere deinen API-Schlüssel`,"modal.setupStep2":`Füge ihn unten in das API-Schlüssel-Feld ein`,"modal.setupStep3":`Klicke auf Anbieter hinzufügen — Modelle werden automatisch erkannt`,"modal.namePlaceholder":`z. B. openrouter`,"modal.duplicateWarn":`Anbieter "{name}" existiert und wird überschrieben.`,"modal.forwardHintPrefix":`Kein Schlüssel nötig — der Proxy leitet deine`,"modal.forwardCredentials":`codex login`,"modal.forwardHintSuffix":`Anmeldedaten an diesen Anbieter weiter.`,"modal.localHint":`Es wird kein API-Schlüssel gespeichert. Damit wird Cursors öffentlicher Modellkatalog für Codex hinzugefügt; live Cursor-Transport und native Datei-/Shell-Ausführung bleiben deaktiviert, bis sie geprüft sind.`,"modal.getApiKey":`{label}-API-Schlüssel holen`,"modal.apiKey":`API-Schlüssel`,"modal.apiKeyTransport":`API-Schlüssel-Header`,"modal.apiKeyTransportNative":`x-api-key (Anthropic-Standard)`,"modal.apiKeyTransportBearer":`Authorization: Bearer`,"modal.apiKeyPlaceholder":`sk-… (oder $ENV_VAR)`,"modal.defaultModelPlaceholder":`z. B. gpt-5.5`,"modal.baseUrlPlaceholder":`https://...`,"modal.baseUrlPlaceholderError":`Base-URL enthält einen ungelösten {placeholder}. Ersetze ihn durch deinen tatsächlichen Wert.`,"modal.baseUrlPlaceholderHint":`Ersetze den {placeholder} in der Base-URL durch deine tatsächliche Account-ID, bevor du hinzufügst.`,"modal.adding":`Wird hinzugefügt…`,"modal.useOauthLogin":`← OAuth-Login verwenden`,"codexAuth.addIdPlaceholder":`codex-work, codex-alt, team…`,"codexAuth.resetCreditsAria":`{count} Reset-Guthaben`,"claude.pageTitle":`Claude Code`,"claude.workspace.settings":`Einstellungen`,"cws.loading":`Combos werden geladen…`,"cws.loadFailed":`Combos konnten nicht geladen werden.`,"cws.saveFailed":`Combo konnte nicht gespeichert werden.`,"cws.removeFailed":`Combo konnte nicht entfernt werden.`,"cws.saved":`Combo gespeichert.`,"cws.created":`{model} erstellt.`,"cws.removed":`combo/{id} entfernt.`,"cws.renamed":`{from} wurde in {to} umbenannt.`,"cws.add":`Combo hinzufügen`,"cws.addTitle":`Combo hinzufügen`,"cws.addSubtitle":`Erstellen Sie ein virtuelles Modell über mehrere Anbieter und wählen Sie den exakten Modellnamen für Clients.`,"cws.create":`Combo erstellen`,"cws.railAria":`Combo-Liste`,"cws.searchPlaceholder":`Combos oder Ziele suchen…`,"cws.noSearchResults":`Keine Combos passen zur Suche.`,"cws.group.failover":`Failover`,"cws.group.roundRobin":`Round-Robin`,"cws.group.other":`Weitere Strategien`,"cws.targetCount":`{count} Ziele`,"cws.targetCountOne":`1 Ziel`,"cws.overviewTitle":`Combos`,"cws.overviewBlurb":`Virtuelle Modelle, die über Anbieter/Modell-Ziele mit Failover, Round-Robin, gewichtetem Zufall, seltenst genutztem Ziel oder frühestem Quota-Reset weiterleiten.`,"cws.count.total":`Gesamt`,"cws.count.failover":`Failover`,"cws.count.roundRobin":`Round-Robin`,"cws.count.other":`Weitere`,"cws.howTitle":`So funktioniert es`,"cws.howBody":`Fordern Sie in Codex den öffentlichen Modellnamen der Combo an. Ohne eigenen Namen gilt combo/. OpenCodex wählt ein Ziel und springt nur bei wiederholbaren Upstream-Fehlern. Ist kein Ziel verfügbar, schlägt die Anfrage geschlossen fehl, statt den globalen Standardanbieter zu verwenden.`,"cws.attentionTitle":`Aufmerksamkeit nötig`,"cws.attention.empty":`Keine Ziele konfiguriert`,"cws.attention.few":`Nur ein Ziel — Failover hat kein Ersatzziel`,"cws.attention.catalogOmitted":`Fehlt im Modellkatalog — Mitgliederfähigkeiten sind unvollständig oder inkompatibel (fehlendes Context-Window/Metadaten oder leere Modalitäts-Schnittmenge). Routing per Alias funktioniert weiterhin`,"cws.attention.allTargetsExhausted":`Alle aktivierten Ziele haben ihr Kontingent ausgeschöpft`,"cws.emptyTitle":`Erste Combo erstellen`,"cws.empty.createDesc":`Virtuelles Modell benennen und zwei oder mehr Backends verketten.`,"cws.backToAll":`Zurück zu allen Combos`,"cws.allCombos":`Alle Combos`,"cws.copyModel":`ID kopieren`,"cws.copied":`Kopiert`,"cws.tabsLabel":`Combo-Detailbereiche`,"cws.tab.config":`Konfiguration`,"cws.tab.about":`Info`,"cws.strategy":`Strategie`,"cws.strategy.failover":`Failover`,"cws.strategy.roundRobin":`Round-Robin`,"cws.strategy.random":`Zufall`,"cws.strategy.leastUsed":`Seltenst genutzt`,"cws.strategy.resetWindow":`Reset-Fenster`,"cws.strategy.failoverHint":`Ziele der Reihe nach versuchen. Bei einem wiederholbaren Fehler (Limit, Ausfall, Abo-Sperre) zum nächsten springen.`,"cws.strategy.roundRobinHint":`Datenverkehr deterministisch nach Gewicht verteilen. Das gewählte Ziel für einen Block erfolgreicher Anfragen behalten und dann weiterschalten.`,"cws.strategy.randomHint":`Pro Anfrage ein geeignetes Ziel ziehen, mit Wahrscheinlichkeiten proportional zum Gewicht. Keine Bindung zwischen Anfragen.`,"cws.strategy.leastUsedHint":`Jede Anfrage an das geeignete Ziel mit den wenigsten erfassten Erfolgen weiterleiten. Zählungen starten mit dem Proxy neu.`,"cws.strategy.resetWindowHint":`Bevorzugt das geeignete Ziel, dessen Quota-Fenster am frühesten zurückgesetzt wird. Ohne Quota-Daten gilt die Konfigurationsreihenfolge.`,"cws.field.id":`Combo-ID`,"cws.field.idHintEdit":`Das Ändern der ID benennt die Combo um. Clients fordern {model} an.`,"cws.field.alias":`Öffentlicher Modellname`,"cws.field.aliasPlaceholder":`deepseek-v4-flash oder vendor/model`,"cws.field.aliasHint":`Optional. Verwenden Sie einen Namen ohne Präfix, ein eigenes Präfix wie vendor/model oder lassen Sie das Feld leer für combo/.`,"cws.field.nativeAlias":`Natives OpenAI-Alias`,"cws.field.nativeAliasHint":`Lässt diese Combo eine unterstützte unqualifizierte native OpenAI-Modell-ID übernehmen. Konto- und providerqualifizierte OpenAI-Routen bleiben getrennt.`,"cws.field.displayName":`Anzeigename`,"cws.field.displayNameHint":`Bezeichnung im Modell-Picker. Erforderlich, wenn das native OpenAI-Alias aktiviert ist.`,"cws.field.idHint":`Clients fordern {model} an`,"cws.field.idInternalHint":`Interne Combo-ID. Sie kann nach dem Erstellen geändert werden.`,"cws.field.stickyLimit":`Sticky-Erfolge vor Rotation`,"cws.field.stickyLimitHint":`Das gewählte Ziel für so viele erfolgreiche Anfragen behalten, bevor die gewichtete Auswahl weiterschaltet.`,"cws.field.defaultEffort":`Standard-Reasoning`,"cws.field.defaultEffortNone":`Keine (Ziel-Standard)`,"cws.field.defaultEffortHint":`Nur verwendet, wenn der Client keinen Reasoning-Aufwand sendet. Optionen sind die Schnittmenge der beworbenen Aufwände der gewählten Ziele.`,"cws.capability.imageInputUnavailable":`Erst verfügbar, wenn jedes gewählte Ziel Bildeingabe unterstützt.`,"cws.capability.imageInputHint":`Standardmäßig aktiv, wenn jedes Ziel Bilder unterstützt. Ausschalten für nur Text.`,"cws.capability.imageInput":`Bild / multimodal`,"cws.capability.adaptiveEffort":`Adaptive Denkstufen`,"cws.capability.adaptiveEffortHint":`Aus: Ziele ohne Denkstufen-Regelung blenden die Auswahl für die gesamte Kombination aus. An: Solche Ziele bleiben nutzbar, und die Auswahl zeigt weiterhin die Stufen der übrigen Ziele.`,"cws.capabilities":`Fähigkeiten`,"cws.field.defaultEffortUnsupported":`Dieser Aufwand liegt nicht in der gemeinsamen Leiter der Ziele — er wird zur Anfragezeit ignoriert oder angepasst.`,"cws.field.defaultEffortUnsupportedOption":`nicht in der Schnittmenge`,"cws.targets":`Ziele`,"cws.targets.failoverHint":`Reihenfolge zählt — das erste ist primär.`,"cws.targets.roundRobinHint":`Gewichte steuern die deterministische relative Auswahl; die Reihenfolge löst Gleichstände im Rotationsring.`,"cws.targets.randomHint":`Gewichte steuern die Wahrscheinlichkeit jeder Ziehung; die Reihenfolge spielt keine Rolle.`,"cws.targets.leastUsedHint":`Die Reihenfolge löst nur Gleichstände zwischen gleich oft genutzten Zielen.`,"cws.targets.resetWindowHint":`Die Reihenfolge gilt, wenn Quota-Daten fehlen oder gleich ausfallen.`,"cws.target.provider":`Anbieter`,"cws.target.model":`Modell`,"cws.target.weight":`Gewicht`,"cws.target.pickProvider":`Anbieter wählen…`,"cws.target.pickProviderFirst":`Zuerst Anbieter wählen…`,"cws.target.pickModel":`Modell wählen…`,"cws.target.noModels":`Keine Modelle für diesen Anbieter`,"cws.target.modelPlaceholder":`Modell-ID`,"cws.target.add":`Ziel hinzufügen`,"cws.target.drag":`Ziehen zum Umsortieren`,"cws.target.moveUp":`Nach oben`,"cws.target.moveDown":`Nach unten`,"cws.quota.available":`Verfügbar`,"cws.quota.exhausted":`Kontingent erschöpft`,"cws.quota.unknown":`Kontingent unbekannt`,"cws.quota.allExhausted":`Alle aktivierten Ziele haben ihr Kontingent ausgeschöpft. Wählen Sie ein anderes Ziel oder warten Sie auf die Erholung.`,"cws.aboutTitle":`Laufzeit`,"cws.aboutBody":`Fehlgeschlagene Ziele kühlen kurz ab; Retry-After wird beachtet. Ungültige oder Kontextfehler springen nicht. Jedes Ziel passt den Aufwand an seine Fähigkeiten an; erschöpfte Combos schlagen geschlossen fehl. Protokolle und Nutzung behalten geordnete physische Versuche samt Nutzung je Versuch.`,"cws.removeConfirmTitle":`{model} entfernen?`,"cws.removeConfirmDesc":`Entfernt das virtuelle Modell aus Config und Codex-Katalog. Anbieter bleiben erhalten.`,"cws.unsavedTitle":`Ungespeicherte Änderungen`,"cws.unsavedDesc":`Änderungen an dieser Combo verwerfen und fortfahren?`,"cws.keepEditing":`Weiter bearbeiten`,"cws.err.missingId":`Combo-ID ist erforderlich.`,"cws.err.invalidId":`ID muss mit Buchstabe/Zahl beginnen und darf nur Buchstaben, Zahlen, Punkte, Unterstriche oder Bindestriche enthalten (max. 64).`,"cws.err.duplicateId":`Eine Combo mit dieser ID existiert bereits.`,"cws.err.invalidAlias":`Der Alias darf nur Buchstaben, Zahlen, Punkte, Unterstriche oder Bindestriche enthalten, mit höchstens einem "/"-Segment.`,"cws.err.aliasReservedNamespace":`Der Alias darf den reservierten Namensraum "combo/" nicht verwenden.`,"cws.err.aliasNativeFamily":`Einfache Aliase aus der OpenAI-nativen Familie (gpt-*, o1-*, o3-*, o4-*, codex-*) sind nicht erlaubt.`,"cws.err.unsupportedNativeAlias":`Ein nativer Alias muss eine derzeit unterstützte, unqualifizierte OpenAI-Modell-ID sein.`,"cws.err.missingNativeAliasDisplayName":`Für native Aliase ist ein Anzeigename erforderlich.`,"cws.err.invalidDisplayName":`Der Anzeigename darf höchstens 128 Zeichen und keine Steuerzeichen enthalten.`,"cws.err.duplicateAlias":`Eine andere Combo verwendet diesen Alias bereits.`,"cws.err.noTargets":`Mindestens ein Ziel hinzufügen.`,"cws.err.incompleteTarget":`Jedes Ziel braucht Anbieter und Modell.`,"cws.target.disabled":`{name} (deaktiviert)`,"cws.err.reservedNamespace":`Ein physischer Anbieter namens combo muss vor dem Erstellen von Combos umbenannt werden.`,"cws.err.providerCollision":`Die Combo-ID kollidiert mit einem konfigurierten Anbieternamen.`,"cws.err.unknownProvider":`Jedes Ziel muss einen konfigurierten Anbieter verwenden.`,"cws.err.duplicateTarget":`Dasselbe Anbieter/Modell-Ziel darf nur einmal vorkommen.`,"cws.err.invalidStickyLimit":`Sticky-Erfolge müssen eine Ganzzahl von 1 bis 100 sein.`,"cws.err.invalidWeight":`Jedes Round-Robin-Gewicht muss eine Ganzzahl von 1 bis 10000 sein.`,"cws.err.noEnabledTarget":`Mindestens ein Ziel muss einen aktivierten Anbieter verwenden.`,"claude.tabsLabel":`Claude-Client`,"claude.tabCode":`Code`,"claude.tabDesktop":`Desktop`,"claudeDesktop.title":`Claude Desktop`,"claudeDesktop.subtitle":`Leite jede Claude-Modellfamilie über ein verfügbares Modell auf Port {port}.`,"claudeDesktop.importJson":`JSON importieren`,"claudeDesktop.exportJson":`JSON exportieren`,"claudeDesktop.loading":`Claude-Desktop-Profil wird geladen…`,"claudeDesktop.loadFail":`Claude-Desktop-Profil konnte nicht geladen werden.`,"claudeDesktop.retry":`Erneut versuchen`,"claudeDesktop.saveFailed":`Claude-Desktop-Profil konnte nicht gespeichert werden.`,"claudeDesktop.applyFailed":`Das Profil wurde gespeichert, konnte aber nicht angewendet werden.`,"claudeDesktop.updateFailed":`Claude-Desktop-Aktualisierung fehlgeschlagen.`,"claudeDesktop.savedApplied":`Profil gespeichert und auf Claude Desktop angewendet.`,"claudeDesktop.appliedMarkerUnsaved":`Auf Claude Desktop angewendet, aber die Anwendungsmarkierung wurde nicht gespeichert – der Status unten kann veraltet sein, bis Sie erneut anwenden.`,"claudeDesktop.savedAppliedAnnounce":`Claude-Desktop-Profil gespeichert und angewendet.`,"claudeDesktop.saved":`Profil gespeichert.`,"claudeDesktop.savedAnnounce":`Claude-Desktop-Profil gespeichert.`,"claudeDesktop.exported":`Profil als JSON exportiert.`,"claudeDesktop.importExpected":`Ein Claude-Desktop-Profil der Version 1 wurde erwartet.`,"claudeDesktop.importReady":`JSON importiert. Prüfe den Entwurf und speichere und wende ihn dann an.`,"claudeDesktop.importedAnnounce":`Profil-JSON importiert. Ungespeicherte Änderungen können geprüft werden.`,"claudeDesktop.importInvalid":`Die ausgewählte Datei ist kein gültiges Profil.`,"claudeDesktop.importFailed":`Import fehlgeschlagen. {error}`,"claudeDesktop.moved":`{route} wurde nach {family} verschoben.`,"claudeDesktop.unsaved":`Ungespeicherte Änderungen`,"claudeDesktop.upToDate":`Profil ist aktuell`,"claudeDesktop.saving":`Speichert…`,"claudeDesktop.applying":`Wird angewendet…`,"claudeDesktop.saveApply":`Speichern & anwenden`,"claudeDesktop.emptyTitle":`Keine Modelle verfügbar`,"claudeDesktop.emptyHint":`Füge einen Anbieter hinzu oder aktiviere ihn und weise dann Claude-Desktop-Routen zu.`,"claudeDesktop.assignmentsLabel":`Zuweisungen der Claude-Modellfamilien`,"claudeDesktop.family.opus":`Opus`,"claudeDesktop.family.fable":`Fable`,"claudeDesktop.family.sonnet":`Sonnet`,"claudeDesktop.family.haiku":`Haiku`,"claudeDesktop.modelCountOne":`{count} Modell`,"claudeDesktop.modelCountMany":`{count} Modelle`,"claudeDesktop.chooseDefault":`Standard wählen`,"claudeDesktop.temporaryDefault":`Temporärer Standard`,"claudeDesktop.laneEmpty":`Modell hier ablegen oder die Verschieben-Steuerung verwenden.`,"claudeDesktop.laneNoMatch":`Kein Modell dieser Familie passt zur Suche.`,"nav.grok":`Grok`,"grok.title":`Grok Build`,"grok.subtitle":`Modelle, die opencodex in deiner Grok-Konfiguration registriert hat.`,"grok.loading":`Grok-Status wird geladen…`,"grok.loadFail":`Die Grok-Konfiguration konnte nicht gelesen werden.`,"grok.notConfiguredTitle":`Grok Build ist nicht eingerichtet`,"grok.notConfiguredHint":`Starte den Proxy mit installiertem Grok neu; opencodex schreibt dann einen verwalteten Block nach:`,"grok.endpoint":`Endpunkt`,"grok.colModel":`Modell`,"grok.colAlias":`Grok-Alias`,"grok.colContext":`Kontext`,"grok.groupNative":`Native Modelle`,"grok.groupRouted":`Geroutete Modelle`,"grok.enabledCount":`{on} von {total} registriert`,"grok.saved":`Auswahl gespeichert.`,"grok.savedApplied":`Auswahl gespeichert und in die Grok-Konfiguration geschrieben.`,"grok.saveFailed":`Grok-Auswahl konnte nicht gespeichert werden.`,"grok.applyFailed":`Auswahl gespeichert, aber die Grok-Konfiguration konnte nicht aktualisiert werden.`,"grok.applySkipped":`Auswahl gespeichert. Die Grok-Konfiguration wurde nicht geändert.`,"grok.saveApply":`Speichern & anwenden`,"grok.saving":`Speichern…`,"grok.applying":`Anwenden…`,"grok.unsaved":`Ungespeicherte Änderungen`,"grok.upToDate":`Auswahl ist aktuell`,"grok.toggleModel":`{id} bei Grok registrieren`,"claudeDesktop.available":`Verfügbar`,"claudeDesktop.defaultBadge":`Standard`,"claudeDesktop.supports1m":`1M`,"claudeDesktop.unavailable":`Nicht verfügbar`,"claudeDesktop.contextM":`{n}M Kontext`,"claudeDesktop.contextK":`{n}k Kontext`,"claudeDesktop.contextUnknown":`Kontext unbekannt`,"claudeDesktop.alias":`Alias`,"claudeDesktop.useAsDefault":`Als {family}-Standard verwenden`,"claudeDesktop.moveTo":`Verschieben nach`,"claudeDesktop.move":`Verschieben`,"claudeDesktop.status.applied":`Auf Desktop angewendet`,"claudeDesktop.status.stale":`Konfiguration veraltet — erneut anwenden`,"claudeDesktop.status.notApplied":`Nicht angewendet`,"claudeDesktop.status.notActiveProfile":`Desktop nutzt ein anderes Profil — erneut anwenden`,"claudeDesktop.status.disabled":`Die Claude-Desktop-Integration ist deaktiviert. Beende Desktop nach dem Aktivieren vollständig und öffne es erneut.`,"claudeDesktop.enableApply":`Aktivieren und anwenden`,"claudeDesktop.health.lastRequest":`Letzte Anfrage`,"claudeDesktop.health.stats":`{count} Anf. / {errors} Fehl.`,"claudeDesktop.effort.supported":`effort`,"claudeDesktop.effort.displayOnly":`effort (nur Anzeige)`,"dash.injectionManage":`Einstellungen öffnen`,"sub.settings":`Einstellungen`,"sub.sections":`Subagent-Abschnitte`,"sub.delegation.model":`Zuerst aufgerufenes Modell`,"sub.delegation.modelHint":`Das Modell, zu dem Codex zuerst greift, wenn es Arbeit übergibt. Oben steht, wen es überhaupt aufrufen darf; hier wählst du den Ersten davon.`,"dash.syncModelsHint":`Schreibt Codex' Modellkatalog anhand deiner verbundenen Provider neu.`,"dash.syncRun":`Jetzt synchronisieren`,"lab.title":`Kompatibilitäts-Labor`,"lab.subtitle":`Schreibgeschützte Kompatibilitätsmatrix aus der Lab-Projektion.`,"lab.loadFailed":`Kompatibilitäts-Lab-Daten konnten nicht geladen werden`,"lab.projectionUnavailable":`Lab-Projektion ist nicht verfügbar. Führen Sie zuerst Konformitäts- oder Live-Probes aus.`,"lab.projectionIncompatible":`Lab-Projektionsschema ist inkompatibel. Projektion neu aufbauen.`,"lab.statusTitle":`Projektionsstatus`,"lab.matrixTitle":`Kompatibilitätsmatrix`,"lab.verdictsTitle":`Urteilsdatensätze`,"lab.filter.layer":`Evidenzschicht`,"lab.filter.verdict":`Urteil`,"lab.filter.subject":`Subject-ID`,"lab.filter.all":`Alle`,"lab.col.subject":`Subject`,"lab.col.layer":`Schicht`,"lab.col.suite":`Suite`,"lab.col.verdict":`Urteil`,"lab.col.asOf":`Stand`,"lab.col.protocol":`Protokollkonformität`,"lab.col.live":`Live-Route-Kompatibilität`,"lab.col.task":`Aufgabenwirksamkeit`,"lab.empty":`Noch keine Kompatibilitätsurteile in der Projektion.`,"lab.subjectKind":`Art`,"lab.observationCount":`Beobachtungen`,"lab.eventCount":`Ereignisse`,"lab.verdictCount":`Urteile`,"lab.subjectCount":`Subjects`,"lab.builtAt":`Erstellt`,"lab.loading":`Kompatibilitätsevidenz wird geladen…`,"lab.loadMore":`Load more`,"lab.detailTitle":`Verdict detail`,"lab.detailClose":`Close`,"lab.detailSubject":`Subject`,"lab.detailObservations":`Observations`,"lab.detailEvents":`Contributing events`,"lab.detailArtifacts":`Artifact metadata`,"lab.production.title":`Beobachteter Produktionsverkehr`,"lab.production.notVerification":`Keine Lab-Verifizierung`,"lab.production.attempts":`Versuche`,"lab.production.successes":`Erfolge`,"lab.production.routeErrors":`Routing-Fehler`,"lab.production.lastObserved":`Zuletzt beobachtet`,"lab.detailLoadFailed":`Could not load verdict detail`,"lab.refresh":`Aktualisieren`,"lab.verdict.UNKNOWN":`Unbekannt`,"lab.verdict.CLAIMED":`Behauptet`,"lab.verdict.PROBED":`Geprüft`,"lab.verdict.VERIFIED":`Verifiziert`,"lab.verdict.DEGRADED":`Eingeschränkt`,"lab.verdict.BLOCKED":`Blockiert`,"lab.verdict.UNSUPPORTED":`Nicht unterstützt`,"lab.layer.protocol_conformance":`Protokollkonformität`,"lab.layer.live_route_compatibility":`Live-Route-Kompatibilität`,"lab.layer.task_effectiveness":`Aufgabenwirksamkeit`,"dash.visionAdvanced":`Erweiterte Einstellungen`,"dash.visionMaxDescriptions":`Maximale Beschreibungen pro Turn`,"dash.visionMaxDescriptionsInvalid":`Geben Sie eine positive ganze Zahl ein.`,"dash.visionTimeout":`Timeout`,"dash.visionTimeoutInvalid":`Geben Sie eine ganze Zahl von {min} bis {max} Millisekunden ein.`,"dash.visionAdvancedPopover":`Erweiterte Vision-Einstellungen`,"models.newPolicyGlobal":`Neue Modelle zunächst deaktivieren`,"models.newPolicyProvider":`Richtlinie für neue Modelle`,"models.newPolicy_inherit":`Übernehmen`,"models.newPolicy_off":`Aus`,"models.newPolicy_on":`An`,"models.newBadge":`NEU`,"models.newCount":`{count} neu, aus`,"models.aliases":`Aliase`,"models.aliasesTable":`Alias-Tabelle`,"models.aliasPrompt":`Anbieter-Alias (leer lassen zum Entfernen)`,"models.modelAliasPrompt":`Modell-Alias (leer lassen zum Entfernen)`,"models.aliasSaved":`Alias gespeichert`,"models.aliasConflict":`Dieser Alias steht in Konflikt mit einem vorhandenen Namen`,"models.editProviderAlias":`Anbieter-Alias bearbeiten`,"models.editModelAlias":`Modell-Alias bearbeiten`,"models.useDefaultAliases":`Standard-Aliase verwenden`,"models.useDefaultAliasesGlobal":`Standard-Aliase global verwenden`,"models.aliasAuto":`automatisch`,"models.aliasUser":`benutzerdefiniert`,"models.aliasStale":`veraltet`,"connection.discovering":`Discovering local and shared targets…`,"connection.machineUnavailable":`The local machine plane is unavailable. Shared requests were not redirected locally.`,"connection.disconnect":`Disconnect from hub`,"connection.disconnectConfirm":`Disconnect this machine from the hub and restart it in standalone mode?`,"connection.pairing.title":`Connect this dashboard to the hub`,"connection.pairing.body":`Paste the one-time pairing code created on the hub.`,"connection.pairing.relayWarning":`This code is exchanged through the fixed hub relay. The relay cannot be redirected to another host.`,"connection.pairing.code":`One-time pairing code`,"connection.pairing.submit":`Connect`,"connection.pairing.submitting":`Connecting…`,"connection.pairing.error":`The pairing code was refused or expired. The code was left in place so you can check it.`,"connection.machine.title":`This machine`,"connection.machine.shimHealthy":`Codex shim is healthy.`,"connection.machine.shimNeedsAttention":`Codex shim needs attention.`,"connection.machine.repairShim":`Repair shim`,"connection.machine.removeShim":`Remove shim`,"connection.clients.title":`Connected clients`,"connection.clients.none":`No client status available`,"connection.clients.sync":`Sync now`,"connection.clients.syncing":`Syncing…`,"connection.sessionLogout":`Remote-Sitzung abmelden`,"connection.sessionLoggingOut":`Remote-Sitzung wird abgemeldet…`,"connection.sessionLogoutFailed":`Die Remote-Sitzung konnte nicht abgemeldet werden. Die aktuelle Sitzung bleibt bestehen.`,"usage.source.connected":`Source: hub usage`,"usage.source.local":`Source: local usage.jsonl`,"usage.scope.label":`Usage scope`,"usage.scope.machine":`This machine`,"usage.scope.hub":`Hub-wide`,"usage.hubOffline":`Hub usage is unavailable. Local usage was not substituted.`,"integrations.tab.cursor":`Cursor`,"integrations.detail.cursorSeen":`Cursor hat diesen Proxy kürzlich aufgerufen`,"integrations.detail.cursorNeverSeen":`Private Inference installiert; noch keine Anfrage empfangen`,"integrations.detail.cursorAbsent":`Cursor Private Inference nicht gefunden`,"integrations.cursor.title":`Cursor`,"integrations.cursor.intro":`Cursor Private Inference führt seinen Agenten lokal aus und kommuniziert per Loopback mit opencodex. Die reguläre Cursor-Version kann das nicht: Ihr Backend ruft den benutzerdefinierten Endpunkt auf und benötigt eine öffentliche HTTPS-URL. Diese Seite schreibt niemals in Cursor; fügen Sie die unten stehenden Werte selbst in Cursor ein.`,"integrations.cursor.loading":`Cursor-Status wird gelesen…`,"integrations.cursor.unavailable":`Der Cursor-Status konnte nicht vom Proxy gelesen werden.`,"integrations.cursor.detection":`Installierte Builds`,"integrations.cursor.privateInference":`Cursor Private Inference`,"integrations.cursor.regular":`Cursor (regulär)`,"integrations.cursor.detected":`Erkannt`,"integrations.cursor.notFound":`Nicht gefunden`,"integrations.cursor.regularOnly":`Es wurde nur die reguläre Cursor-Version gefunden. Sie leitet benutzerdefinierte Endpunkte über die Cursor-Server weiter, sodass ein Loopback-Proxy ohne öffentlichen Tunnel nicht erreichbar ist. Informationen zum Private-Inference-Build finden Sie in der Anleitung.`,"integrations.cursor.nothingFound":`An den üblichen Speicherorten wurde keine Cursor-Installation gefunden. Falls Cursor an einem anderen Ort installiert ist, gelten die unten stehenden Werte trotzdem.`,"integrations.cursor.gateway":`Gateway-Werte`,"integrations.cursor.gatewayHint":`Öffnen Sie in Cursor Private Inference Settings > Models > Gateway, fügen Sie diese beiden Werte ein und klicken Sie anschließend auf Refresh model list.`,"integrations.cursor.baseUrl":`Basis-URL`,"integrations.cursor.apiKey":`API-Schlüssel`,"integrations.cursor.apiKeyCredential":`Einer Ihrer opencodex-API-Schlüssel (für diese Anbindung sind Zugangsdaten erforderlich)`,"integrations.cursor.copy":`Kopieren`,"integrations.cursor.copied":`Kopiert`,"integrations.cursor.connection":`Verbindung`,"integrations.cursor.seen":`Letzte Anfrage von Cursor: {time} ({ua})`,"integrations.cursor.neverSeen":`Seit dem Start des Proxys ist keine Anfrage von Cursor eingegangen. Klicken Sie nach dem Speichern des Gateways in Cursor auf Refresh model list.`,"integrations.cursor.models":`Was Cursor anzeigt`,"integrations.cursor.modelsHint":`Cursor wählt die Reasoning-Abstufung anhand seiner eigenen Modelltabelle aus, daher kann opencodex sie nur vorhersagen. Die Kontextspalte zeigt das Standardfenster und das optionale Fenster (Cursors Max Mode).`,"integrations.cursor.ladderFromBundle":`Reasoning-Abstufungen wurden aus dem installierten Cursor-Private-Inference-Bundle {version} gelesen. Cursor legt sie fest; opencodex gibt nur dessen Tabelle wieder.`,"integrations.cursor.ladderFromStatic":`Reasoning-Abstufungen sind ein statischer Spiegel von Cursor 3.18.25 (kein lesbares Private-Inference-Bundle gefunden). Die Kontextspalte zeigt das Standardfenster und das optionale Fenster.`,"integrations.cursor.unknownVersion":`unbekannte Version`,"integrations.cursor.noControl":`—`,"integrations.cursor.singleWindow":`ein Fenster`,"integrations.cursor.noControlTitle":`Diese ID steht nicht in Cursors eingebauter Effort-Tabelle, daher zeigt Cursor keine Reasoning-Steuerung an.`,"integrations.cursor.effortRowsOne":`1 Effort-Zeile veröffentlicht`,"integrations.cursor.effortRowsMany":`{n} Effort-Zeilen veröffentlicht`,"integrations.cursor.effortRowsOff":`keine Effort-Zeilen`,"integrations.cursor.tableLessHint":`Mit — markierte Zeilen erhalten in Cursor keine Reasoning-Steuerung. Aktivieren Sie cursorEffortRows, um pro Effort einen Picker-Eintrag (id--effort) zu veröffentlichen, oder setzen Sie modelDefaultReasoningEfforts beim Provider für einen festen Standard.`,"integrations.cursor.colModel":`Modell`,"integrations.cursor.colReasoning":`Reasoning-Aufwand`,"integrations.cursor.colContext":`Kontext`,"integrations.cursor.guide":`Anleitung zu Cursor Private Inference öffnen`},Be={"nav.dashboard":`Tableau de bord`,"uptime.day":`j`,"uptime.hour":`h`,"uptime.minute":`min`,"uptime.second":`s`,"nav.startup":`Démarrage`,"nav.providers":`Fournisseurs`,"nav.models":`Modèles`,"nav.combos":`Combinaisons`,"nav.subagents":`Sous-agents`,"nav.logs":`Journaux et débogage`,"nav.usage":`Utilisation`,"common.github":`GitHub`,"sidebar.star":`Ajouter une étoile sur GitHub`,"sidebar.starred":`Étoile ajoutée sur GitHub`,"sidebar.starUnauthenticated":`Ouvrir GitHub pour ajouter une étoile (gh CLI n’est pas connecté)`,"sidebar.starFailed":`Impossible d’ajouter une étoile avec gh. Ouverture de GitHub à la place.`,"sidebar.updateAvailable":`Mise à jour disponible : {version}`,"sidebar.checkUpdate":`Rechercher des mises à jour`,"common.save":`Enregistrer`,"common.saving":`Enregistrement…`,"common.cancel":`Annuler`,"common.discard":`Abandonner les modifications`,"common.delete":`Supprimer`,"common.close":`Fermer`,"common.ok":`OK`,"common.remove":`Supprimer`,"common.loading":`Chargement…`,"common.retry":`Réessayer`,"auth.adminTokenTitle":`Jeton d’administration OpenCodex (OPENCODEX_ADMIN_AUTH_TOKEN)`,"auth.adminAccountLabel":`Compte`,"auth.adminTokenFieldLabel":`Jeton d’administration`,"auth.adminTokenRejected":`Ce jeton d’administration a été refusé. Vérifiez-le et réessayez.`,"auth.adminTokenUnavailable":`Le jeton d’administration n’a pas pu être vérifié. Réessayez.`,"app.logoAria":`Logo opencodex`,"app.claudeOn":`Claude ACTIVÉ`,"app.claudeOff":`Claude DÉSACTIVÉ`,"theme.label":`Thème`,"theme.light":`Clair`,"theme.dark":`Sombre`,"theme.system":`Système`,"lang.label":`Langue`,"lang.nativeName":`Français`,"provider.name.commandCodeAuth":`Command Code - Auth`,"provider.name.commandCodeApi":`Command Code - API`,"provider.name.volcengine":`Volcengine Ark`,"provider.name.volcengineCodingPlan":`Volcengine Ark Coding Plan`,"provider.name.volcengineAgentPlan":`Volcengine Ark Agent Plan`,"errorBoundary.title":`Échec du chargement de la page`,"errorBoundary.message":`Une erreur de rendu s’est produite dans cette section. Rechargez-la pour réessayer.`,"errorBoundary.details":`Erreur`,"errorBoundary.reload":`Recharger`,"routing.title":`Routage intelligent (bêta)`,"routing.subtitle":`Profils de stratégie, évaluation à blanc et analyses de routage fondées sur les sources.`,"routing.loadFailed":`Impossible de charger les données de routage`,"routing.empty":"Aucun profil de routage configuré. Ajoutez `routingProfiles` à config.json.","routing.revision":`rév.`,"routing.detail":`Profil`,"routing.createProfile":`Créer un profil`,"routing.dryRunError":`Échec de l’évaluation à blanc (HTTP {status})`,"routing.removeConfirm":`Supprimer le profil {id} ?`,"routing.unknownEvidence.allow":`autoriser`,"routing.unknownEvidence.penalize":`pénaliser`,"routing.unknownEvidence.exclude":`exclure`,"routing.removeCandidate":`Supprimer le candidat {provider}/{model}`,"routing.candidates":`Candidats`,"routing.require":`Exigences strictes`,"routing.optimize":`Pondérations d’optimisation`,"routing.limits":`Limites`,"routing.unknownEvidence":`Stratégie pour les preuves inconnues`,"routing.compatibility.title":`Stratégie de compatibilité`,"routing.compatibility.enabled":`Exiger des preuves du laboratoire de compatibilité`,"routing.compatibility.requiredSuites":`Suites requises`,"routing.compatibility.loadingCatalog":`Chargement du catalogue du laboratoire…`,"routing.compatibility.catalogUnavailable":`Catalogue du laboratoire indisponible — saisissez manuellement les identifiants de suite dans config.json.`,"routing.compatibility.layer.protocol_conformance":`Conformité au protocole`,"routing.compatibility.layer.live_route_compatibility":`Compatibilité du routage en direct`,"routing.compatibility.minStatus":`État de compatibilité minimal`,"routing.none":`aucun`,"routing.unavailable":`–`,"routing.dryRun":`Évaluation à blanc`,"routing.dryRunContext":`Fenêtre de contexte de la requête (jetons)`,"routing.dryRunTools":`La requête nécessite des outils`,"routing.dryRunImage":`La requête nécessite une image en entrée`,"routing.dryRunStructured":`La requête nécessite une sortie structurée`,"routing.dryRunRun":`Évaluer les candidats`,"routing.candidate":`Candidat`,"routing.eligible":`Admissible`,"routing.exclusions":`Exclusions`,"routing.costCap":`Plafond de coût`,"routing.capOutcome.satisfied":`dans la limite`,"routing.capOutcome.exceeded":`limite dépassée`,"routing.capOutcome.unknown-allowed":`inconnu (autorisé)`,"routing.capOutcome.unknown-excluded":`inconnu (exclu)`,"routing.exclusion.capability-unsatisfied":`capacité non satisfaite`,"routing.exclusion.unknown-capability":`capacité inconnue`,"routing.exclusion.cost-limit":`plafond de coût dépassé`,"routing.exclusion.cost-limit-unknown":`coût inconnu sous le plafond`,"routing.exclusion.cooldown":`délai de récupération`,"routing.exclusion.unknown-health":`état de santé inconnu`,"routing.exclusion.unknown-quota":`quota inconnu`,"routing.exclusion.unknown-price":`prix inconnu`,"routing.exclusion.other":`exclusion : {code}`,"routing.score":`Score`,"routing.selected":`sélectionné`,"routing.yes":`oui`,"routing.no":`non`,"routing.analytics":`Analyse du routage`,"routing.analyticsTotal":`Requêtes`,"routing.analyticsSuccessRate":`Réussite`,"routing.analyticsFallbackRate":`Repli`,"routing.analyticsP50":`p50`,"routing.analyticsP95":`p95`,"routing.analyticsP99":`p99`,"routing.analyticsCooldown":`Échecs pendant le délai de récupération`,"routing.analyticsConfidence":`Confiance`,"routing.analyticsTruncated":`historique tronqué`,"routing.analyticsRequests":`Requêtes`,"routing.analyticsEmpty":`Aucune donnée d’analyse pour le moment — envoyez d’abord quelques requêtes.`,"startup.title":`Sécurité du démarrage`,"startup.subtitle":`Vérifiez que Codex peut joindre opencodex après un redémarrage, avant que le routage du proxy local n’entre dans une boucle de reconnexion.`,"startup.refresh":`Actualiser`,"startup.backToDashboard":`Retour au tableau de bord`,"startup.loading":`Vérification de la protection au démarrage…`,"startup.error":`Impossible de lire la protection au démarrage.`,"startup.staleData":`La dernière vérification du démarrage a échoué. Les valeurs ci-dessous sont obsolètes et ne doivent pas être considérées comme une preuve de protection.`,"startup.status.native":`Routage natif`,"startup.status.protected":`Redémarrage protégé`,"startup.status.atRisk":`Action requise`,"startup.summary.native":`Codex ne dépend pas du proxy local`,"startup.summary.protected":`opencodex sera disponible après le redémarrage`,"startup.summary.atRisk":`Codex peut perdre l’accès aux modèles après le redémarrage`,"startup.riskDetail":`Codex est lié au proxy local, mais aucun service persistant ni mécanisme de lancement opérationnel ne le redémarrera.`,"startup.riskDetailCustomLocal":`Codex pointe vers une passerelle locale personnalisée. opencodex ne peut ni gérer ni vérifier le cycle de redémarrage de cette passerelle.`,"startup.riskDetailWindowsShim":`Le mécanisme de lancement protège les scripts CLI pris en charge, mais Codex Desktop et les lancements directs de codex.exe peuvent le contourner sous Windows.`,"startup.safeDetail":`Le routage et le mécanisme de démarrage actuels sont cohérents. Aucun démarrage manuel avec ocx ne devrait être nécessaire après un redémarrage.`,"startup.routing":`Routage de Codex`,"startup.routing.proxy":`Proxy local`,"startup.routing.native":`OpenAI natif`,"startup.routing.customLocal":`Passerelle locale personnalisée`,"startup.routing.customRemote":`Passerelle distante personnalisée`,"startup.routing.unknown":`Routage inconnu ou non valide`,"startup.restartProtection":`Protection au redémarrage`,"startup.preference":`Démarrage à la demande`,"startup.enabled":`Activé`,"startup.disabled":`Désactivé`,"startup.protection.service":`Service en arrière-plan`,"startup.protection.shim":`Mécanisme de lancement`,"startup.protection.none":`Non installé`,"startup.details":`Détails de la protection`,"startup.service":`Service en arrière-plan`,"startup.serviceHint":`Démarre à la connexion et relance le proxy après un plantage.`,"startup.installed":`Installé`,"startup.notInstalled":`Non installé`,"startup.unsupported":`Non pris en charge`,"startup.shim":`Mécanisme de lancement Codex`,"startup.shimHint":`Exécute ocx ensure au démarrage d’un script de lancement Codex pris en charge.`,"startup.healthy":`Opérationnel`,"startup.cliOnly":`CLI uniquement`,"startup.stale":`Obsolète`,"startup.viable":`Prêt`,"startup.unhealthy":`Installé, mais défaillant`,"startup.conflict":`Conflit de service`,"startup.installedDisabled":`Installé, mais désactivé`,"startup.install":`Installer`,"startup.installing":`Installation…`,"startup.repair":`Réparer`,"startup.repairing":`Réparation…`,"startup.serviceInstalled":`Service en arrière-plan installé avec succès.`,"startup.serviceRepaired":`Service en arrière-plan réparé avec succès.`,"startup.shimInstalled":`Mécanisme de lancement Codex installé avec succès.`,"startup.shimRepaired":`Mécanisme de lancement Codex réparé avec succès.`,"startup.installFailed":`Échec de l’installation :`,"startup.tray.title":`Zone de notification Windows`,"startup.tray.hint":`Installez une icône dans la zone de notification à la connexion pour démarrer, arrêter et redémarrer le proxy, ouvrir le tableau de bord et consulter l’état en un clic.`,"startup.tray.login":`Lancer l’icône à la connexion Windows`,"startup.tray.notProtection":`L’icône de notification est un contrôleur, pas une protection au redémarrage. Un service en arrière-plan opérationnel reste nécessaire pour rétablir le proxy sans intervention.`,"startup.tray.running":`En cours d’exécution`,"startup.tray.stopped":`Installé, masqué`,"startup.tray.stale":`Réparation requise`,"startup.tray.notInstalled":`Non installé`,"startup.tray.loading":`Vérification…`,"startup.tray.unavailable":`État indisponible`,"startup.tray.install":`Installer et afficher l’icône`,"startup.tray.start":`Afficher l’icône de notification`,"startup.tray.stop":`Quitter l’icône de notification`,"startup.tray.uninstall":`Supprimer le lancement à la connexion`,"startup.tray.error":`Échec de l’action sur l’icône de notification Windows. Consultez ocx tray status pour plus de détails.`,"startup.recovery":`Options de réparation`,"startup.recoveryHint":`Utilisez les programmes d’installation en un clic ci-dessus ou copiez une commande pour effectuer une réparation manuelle. Le service en arrière-plan est recommandé pour Codex Desktop et les exécutables Windows.`,"startup.command.service":`Recommandé : service persistant en arrière-plan`,"startup.command.shim":`Alternative : mécanisme de lancement CLI`,"startup.command.native":`Solution de secours : restaurer le routage Codex natif`,"startup.copy":`Copier`,"startup.copied":`Copié`,"startup.recommended":`Réparation recommandée : {cmd}`,"startup.navRisk":`La protection au démarrage requiert votre attention`,"startup.codexRuntime.clampHidden":`Certaines options d’effort de raisonnement ont été masquées, car OpenCodex utilisait Codex {version}.`,"startup.codexRuntime.clampHiddenWithEfforts":`Certaines options d’effort de raisonnement ont été masquées, car OpenCodex utilisait Codex {version} (supprimées : {efforts}).`,"startup.codexRuntime.olderBinary":`OpenCodex utilise un binaire Codex plus ancien ({version}). Une installation plus récente est disponible.`,"dash.subtitle":`État en direct du proxy opencodex local, de ses fournisseurs et des modèles routés vers Codex.`,"dash.workspace.overview":`Vue d’ensemble`,"dash.workspace.sections":`Sections`,"dash.status":`État`,"dash.online":`En ligne`,"dash.offline":`Hors ligne`,"dash.version":`Version`,"dash.uptime":`Durée de fonctionnement`,"dash.providers":`Fournisseurs`,"dash.tokens30d":`Jetons (30 j)`,"dash.coverage":`Couverture : {pct}`,"dash.mem.title":`Observabilité de la mémoire`,"dash.mem.hint":`Diagnostics d’exécution en lecture seule. La mémoire observée correspond à max(RSS, external, ArrayBuffers), afin que la réduction de l’ensemble de travail Windows ne masque pas la rétention allouée.`,"dash.mem.rss":`Ensemble résident (RSS)`,"dash.mem.jsHeap":`Tas JS utilisé`,"dash.mem.jsHeapArena":`arène {total}`,"dash.mem.pressure":`Par rapport au seuil d’avertissement`,"dash.mem.pressureOf":`{pct} % du seuil`,"dash.mem.pressureUnknown":`Aucun seuil signalé`,"dash.mem.jscHeap":`Tas JSC`,"dash.mem.external":`Mémoire externe`,"dash.mem.arrayBuffers":`ArrayBuffers`,"dash.mem.observed":`Observée`,"dash.mem.runtime":`Compteurs d’exécution`,"dash.mem.growth":`Dérive observée par heure`,"dash.mem.perHour":`/h`,"dash.mem.store":`Stockage des continuations`,"dash.mem.storeHint":`Cache previous_response_id du proxy. Une hausse du nombre total d’octets accompagnée d’une augmentation du tas indique une rétention des conversations plutôt qu’un effet de l’allocateur d’exécution.`,"dash.mem.storeEntries":`Entrées`,"dash.mem.storeTotal":`Total`,"dash.mem.storeLargest":`Plus grande`,"dash.mem.storeOldest":`Plus ancienne`,"dash.mem.threshold":`Seuil d’avertissement`,"dash.mem.lastWarn":`Dernier avertissement`,"dash.mem.never":`Jamais`,"dash.mem.details":`Détails`,"dash.mem.unavailable":`Diagnostics de mémoire indisponibles (proxy plus ancien).`,"dash.mem.inFlight":`Requêtes en cours`,"dash.mem.restart":`Drainer et redémarrer`,"dash.mem.restartConfirm":`Attendre la fin de {count} requête(s) en cours, puis redémarrer (jusqu’à {seconds} s ; les requêtes restantes seront interrompues à l’expiration du délai).`,"dash.mem.draining":`Drainage de {count} requête(s)… redémarrage une fois terminé`,"dash.mem.reconnecting":`Redémarrage du proxy… attente de la reconnexion`,"dash.mem.restartFailed":`Échec du drainage et du redémarrage. Vérifiez que le proxy est en cours d’exécution.`,"dash.mem.restartNoSupervisor":`Aucune protection au redémarrage détectée. Le proxy peut rester arrêté après le redémarrage, sauf si vous le relancez.`,"dash.activeProviders":`Fournisseurs actifs`,"dash.noProviders":`Aucun fournisseur configuré. Exécutez {cmd}.`,"dash.col.name":`Nom`,"dash.col.adapter":`Adaptateur`,"dash.col.baseUrl":`URL de base`,"dash.col.model":`Modèle`,"dash.modelsNoResults":`Aucun modèle ne correspond à votre recherche.`,"dash.availableModels":`Modèles disponibles`,"dash.noModels":`Aucun modèle trouvé. Vérifiez les clés API des fournisseurs.`,"dash.cannotConnect":`Impossible de se connecter au proxy. Est-il en cours d’exécution ?`,"dash.runStart":`Exécutez {cmd} pour démarrer le proxy.`,"dash.stop":`Arrêter le proxy`,"dash.stopConfirm":`Arrêter le proxy et restaurer Codex natif ?`,"dash.stopFailed":`Échec de l’arrêt du proxy (HTTP {status}).`,"dash.maSwitchFailed":`Échec du changement de mode (HTTP {status}).`,"dash.maNetworkError":`Erreur réseau — le proxy est-il en cours d’exécution ?`,"dash.stopping":`Arrêt…`,"dash.actions":`Proxy`,"dash.codexRestart":`Recharger les modèles Codex`,"dash.codexRestarting":`Arrêt…`,"dash.codexRestartConfirm":`Arrêter les serveurs d’application Codex afin qu’ils rechargent la liste des modèles ? Tout tour Codex en cours sera interrompu et Codex ne redémarrera pas automatiquement — rouvrez-le ensuite.`,"dash.codexRestartDone":`{count} serveur(s) d’application Codex arrêté(s). Rouvrez Codex pour charger la liste actuelle des modèles.`,"dash.codexRestartNothing":`Aucun serveur d’application Codex n’est en cours d’exécution. Le prochain lancement lira la liste actuelle des modèles.`,"dash.codexRestartUnknown":`Impossible de répertorier les processus ; aucun n’a donc été arrêté.`,"dash.codexRestartPartial":`{count} serveur(s) d’application ne se sont pas arrêtés. Arrêtez-les manuellement si la liste des modèles reste obsolète.`,"dash.codexRestartFailed":`Échec du rechargement des modèles Codex (HTTP {status}).`,"dash.codexRestartUnreachable":`Impossible de joindre le proxy.`,"dash.codexRestartMalformed":`Le proxy a renvoyé une réponse inattendue.`,"dash.codexRestartTimeout":`Le proxy n’a pas répondu à temps. Il est peut-être encore en train d’arrêter les serveurs d’application.`,"models.staleBanner":`Codex affiche une liste de modèles plus ancienne que ce catalogue. Redémarrez Codex pour la recharger.`,"dash.codexAutoStart":`Démarrer opencodex avec Codex`,"dash.codexAutoStartHint":`Permet à un mécanisme de lancement installé d’exécuter ocx ensure. Ce réglage n’installe pas de protection au redémarrage ; consultez Sécurité du démarrage pour connaître l’état effectif.`,"dash.searchModel":`Modèle auxiliaire de recherche`,"dash.searchModelHint":`Modèle utilisé pour web_search sur les modèles routés autres qu’OpenAI. Nécessite une connexion à ChatGPT.`,"dash.searchReasoning":`Effort de raisonnement pour la recherche`,"dash.visionModel":`Modèle auxiliaire de vision`,"dash.visionModelHint":`Modèle utilisé pour décrire les images aux modèles routés en mode texte uniquement. Nécessite une connexion à ChatGPT.`,"dash.webSearchSidecar":`Service auxiliaire de recherche Web`,"dash.webSearchSidecarHint":`Choisissez le moteur et le modèle utilisés pour la recherche Web sur les modèles routés.`,"dash.webSearchStream":`Diffuser les réponses en direct`,"dash.webSearchStreamHint":`Diffuse en direct le texte initial et le raisonnement du modèle jusqu’à ce qu’il décide d’appeler un outil ; le reste du tour demeure en mémoire tampon pour intercepter la recherche. Le texte produit avant une recherche peut être partiellement répété.`,"dash.visionSidecar":`Service auxiliaire de vision`,"dash.visionSidecarHint":`Choisissez le moteur et le modèle utilisés pour décrire les images aux modèles routés en mode texte uniquement.`,"dash.visionOff":`Désactivé`,"dash.visionAdvanced":`Paramètres avancés`,"dash.visionMaxDescriptions":`Nombre maximal de descriptions par tour`,"dash.visionMaxDescriptionsInvalid":`Saisissez un entier positif.`,"dash.visionTimeout":`Délai d’expiration`,"dash.visionTimeoutInvalid":`Saisissez un entier compris entre {min} et {max} millisecondes.`,"dash.visionAdvancedPopover":`Paramètres de vision avancés`,"dash.shadowCallIntercept":`Interception des appels fantômes`,"dash.shadowCallInterceptHint":`Intercepte les appels auxiliaires en arrière-plan de l’application Codex ({models}) pour générer les titres et les messages de commit, puis les redirige vers le modèle choisi.`,"dash.shadowCallWarning":`⚠ Lorsque cette option est activée, TOUTES les requêtes destinées à {models} sont remplacées par le modèle sélectionné.`,"dash.shadowCallOriginal":`Original`,"dash.shadowCallModel":`Modèle de remplacement`,"dash.shadowCallTooltip":`L’application Codex effectue des appels auxiliaires en arrière-plan pour générer les titres de fils, les messages de commit et orchestrer les compétences. Le modèle auxiliaire ayant changé selon les versions du client, opencodex intercepte tous les modèles de cet ensemble : {models}. Activez cette option pour rediriger ces appels vers le modèle choisi.`,"models.shadowCallIntercept":`Interception des appels fantômes`,"models.shadowCallInterceptHint":`Intercepte les appels auxiliaires en arrière-plan de l’application Codex ({models}) pour les titres et les messages de commit, puis les redirige vers le modèle choisi.`,"dash.sidecarBackend":`Moteur`,"dash.sidecarModel":`Modèle`,"dash.backendAuto":`Auto`,"dash.backendOpenAI":`OpenAI`,"dash.backendAnthropic":`Anthropic`,"dash.sidecarSaved":`Paramètres des services auxiliaires enregistrés. Ils s’appliqueront à la prochaine requête.`,"dash.sidecarSaveFailed":`Échec de l’enregistrement des paramètres des services auxiliaires.`,"dash.injectionLabel":`Délégation aux sous-agents`,"dash.injectionHint":`Choisissez le modèle auquel Codex doit confier le travail des sous-agents. Les deux options ci-dessous déterminent où ce choix est utilisé.`,"dash.injectionManage":`Ouvrir les paramètres`,"dash.syncCodexSubagentDefaults":`Enregistrer aussi comme valeur par défaut de Codex`,"dash.syncCodexSubagentDefaultsHint":`Si cette option est activée, le choix ci-dessus est inscrit dans la configuration de Codex afin que les nouvelles tâches commencent aussi avec ce modèle. Si elle est désactivée, il n’est mémorisé qu’ici. Il prend effet à la prochaine synchronisation ou au prochain redémarrage, sans modifier vos paramètres [agents] saisis manuellement.`,"dash.multiAgentGuidance":`Indiquer à Codex comment répartir le travail`,"dash.multiAgentGuidanceHint":`Envoie une brève note indiquant à Codex comment confier le travail aux sous-agents. En v2, elle précise les modèles utilisables et celui à privilégier ; en v1, elle ne s’applique qu’avec un effort de raisonnement maximal ou ultra. Si cette option est désactivée, aucune note n’est ajoutée.`,"dash.injectionNone":`Aucun`,"dash.injectionEffortLabel":`Effort de raisonnement`,"dash.injectionEffortNone":`Valeur par défaut du modèle`,"dash.effortCapLabel":`Limite de l’effort ultra en V2`,"dash.subagentEffortCapLabel":`Limite de l’effort des sous-agents en V2`,"dash.effortCapHelp":`Limite l’effort de raisonnement des tours en mode ultra V2. Lorsqu’une limite est définie, les requêtes entrantes avec l’effort maximal (issues du mode ultra) sont plafonnées au niveau sélectionné. La limite des sous-agents s’applique uniquement aux agents enfants créés. Les plafonds ne font que réduire l’effort, jamais l’augmenter. Si un modèle ne prend pas en charge le niveau plafonné, le niveau inférieur pris en charge le plus proche est utilisé.`,"dash.effortCapNone":`Aucune limite`,"dash.maintenance":`Maintenance`,"dash.maintenanceHint":`Actualisez le catalogue de modèles de Codex ou installez une version plus récente d’opencodex.`,"dash.syncModels":`Synchroniser les modèles`,"dash.syncModelsHint":`Réécrit le catalogue de modèles de Codex à partir des fournisseurs connectés.`,"dash.syncRun":`Synchroniser maintenant`,"dash.syncing":`Synchronisation…`,"dash.syncOk":`Synchronisation terminée. {count} modèle(s) ajouté(s).`,"dash.syncStaleHint":`Si Codex affiche toujours une ancienne liste, redémarrez son app-server de longue durée ({cmd}).`,"dash.syncFailed":`Échec de la synchronisation : {error}`,"dash.projectConfigTitle":`La configuration Codex du projet contourne OpenCodex`,"dash.projectConfigHint":`Ces paramètres propres au dépôt remplacent le proxy OpenCodex (par exemple, en routant directement vers OpenCode Go). Supprimez-les afin que le routage défini dans ~/.codex/config.toml s’applique à ce projet.`,"dash.checkUpdate":`Rechercher une mise à jour`,"dash.updateTitle":`Mettre à jour opencodex`,"dash.updateDesc":`Recherchez le canal sélectionné sur npm, puis choisissez de redémarrer ou non le proxy après l’installation.`,"dash.updateChannel":`Canal`,"dash.updateChecking":`Recherche de mises à jour…`,"dash.updateInstalled":`Installée`,"dash.updateLatest":`Dernière version`,"dash.updateAvailable":`Mise à jour disponible`,"dash.updateCurrent":`À jour`,"dash.updateCommand":`Commande`,"dash.updateSource":`Il s’agit d’une extraction du code source. Mettez-la à jour depuis le terminal avec la commande affichée.`,"dash.updateUnavailable":`Impossible de lire la dernière version depuis npm. Réessayez plus tard.`,"dash.updateRetry":`Réessayer`,"dash.updateRecheck":`Revérifier`,"dash.updateCannotAuto":`La mise à jour en un clic est indisponible ({reason}).`,"dash.updateReason.source_checkout":`extraction du code source`,"dash.updateReason.latest_unavailable":`registre npm inaccessible`,"dash.updateReason.already_latest":`dernière version déjà installée`,"dash.updateReason.unknown":`mise à jour indisponible`,"dash.updateRestart":`Redémarrer après la mise à jour`,"dash.updateRestartHint":`Recommandé. L’interface graphique actuelle continue d’exécuter l’ancien code jusqu’au redémarrage du proxy.`,"dash.runUpdate":`Mettre à jour`,"dash.updateReconnecting":`Attente du proxy redémarré…`,"dash.updateStatus.running":`Mise à jour d’opencodex.`,"dash.updateStatus.restarting":`Mise à jour installée. Redémarrage du proxy.`,"dash.updateStatus.succeeded":`Mise à jour terminée.`,"dash.updateVersionTransition":`{currentVersion} -> {latestVersion}.`,"dash.updateStatus.failed":`Échec de la mise à jour.`,"prov.subtitle":`Configurez les fournisseurs en amont vers lesquels opencodex route Codex. Connectez-vous avec un compte, ajoutez un fournisseur ou modifiez la configuration brute.`,"prov.add":`Ajouter un fournisseur`,"prov.editJson":`Modifier le JSON`,"prov.accountLogin":`Connexion au compte`,"prov.noOauth":`Aucun fournisseur OAuth disponible.`,"prov.loggedIn":`connecté`,"prov.notLoggedIn":`non connecté`,"prov.logout":`Se déconnecter`,"prov.login":`Se connecter`,"prov.loginWith":`Se connecter avec {provider}`,"prov.waitingBrowser":`Attente du navigateur…`,"prov.didntOpen":`La page ne s’est pas ouverte ? Cliquez ici`,"prov.copyLink":`Copier le lien`,"prov.dontOpenBrowser":`Ne pas ouvrir de navigateur sur la machine du proxy`,"prov.dontOpenBrowserHint":`Utile pour un autre profil de navigateur, ou quand le tableau de bord n'est pas sur la machine du proxy.`,"prov.linkCopied":`Copié`,"prov.linkCopyUnavailable":`Presse-papiers indisponible`,"prov.deviceCode":`Code de l’appareil`,"prov.copyCode":`Copier le code`,"prov.codeCopied":`Code copié`,"prov.editAlias":`Modifier l’alias`,"prov.aliasPrompt":`Nom d’affichage (laissez vide pour l’effacer)`,"prov.aliasSaved":`Alias enregistré`,"prov.aliasSaveFailed":`Impossible d’enregistrer l’alias`,"prov.accountId":`ID`,"prov.pasteRedirect":`Coller l’URL de redirection ou le code`,"prov.pasteRedirectHint":`Si le navigateur affiche une erreur localhost, copiez l’URL complète depuis sa barre d’adresse et collez-la ici (ou collez le code d’autorisation).`,"prov.pasteSubmit":`Envoyer`,"prov.pasteSubmitting":`Envoi…`,"prov.pasteOk":`Code envoyé — finalisation de la connexion…`,"prov.pasteFail":`Impossible d’envoyer le code : {error}`,"prov.port":`Port`,"prov.default":`Par défaut`,"prov.loadingConfig":`Chargement…`,"prov.saved":`Enregistré ! Redémarrez le proxy pour appliquer les modifications.`,"prov.loadConfigFail":`Échec du chargement de la configuration`,"prov.invalidJson":`JSON non valide`,"prov.saveFailed":`Échec de l’enregistrement`,"prov.loginFailStart":`Impossible de démarrer la connexion à {provider}`,"prov.loginError":`Erreur de connexion à {provider} : {error}`,"prov.loginRequestFail":`Échec de la demande de connexion à {provider}`,"prov.loginCancelled":`Connexion à {provider} annulée`,"prov.loginTimeout":`Délai de connexion à {provider} dépassé — le navigateur a été fermé ou l’opération n’a jamais abouti. Réessayez.`,"prov.loginOk":`Connexion à {provider} réussie. Exécutez {cmd} (ou laissez l’application en direct) pour afficher ses modèles.`,"prov.loginSameAccount":`Il s’agit toujours du même compte {provider} — changez de compte dans le navigateur, puis réessayez d’ajouter un compte.`,"oauthTos.highTitle":`{provider} : risque lié à l’abonnement OAuth`,"oauthTos.elevatedTitle":`{provider} : passerelle OAuth non officielle`,"oauthTos.anthropicBody":`La réutilisation directe des jetons OAuth d’un abonnement Claude par l’intermédiaire d’un proxy tiers tel qu’OpenCodex n’est pas une intégration prise en charge par Anthropic et peut entraîner des restrictions d’accès. Les intégrations Agent SDK prises en charge qui utilisent les abonnements Claude sont distinctes.`,"oauthTos.highBody":`OpenCodex connecte {provider} par un mécanisme OAuth tiers. Une utilisation non prise en charge peut entraîner des limitations d’accès ou une suspension.`,"oauthTos.elevatedBody":`OpenCodex connecte {provider} par un mécanisme OAuth non officiel. Utilisez le client officiel lorsque cela est possible ; un trafic inhabituel ou automatisé peut être considéré comme abusif et l’accès peut être limité ou suspendu.`,"oauthTos.saferPath":`Option plus sûre : configurez plutôt une clé API dans OpenCodex.`,"oauthTos.acknowledge":`Je comprends le risque et souhaite tout de même continuer avec OAuth.`,"oauthTos.continue":`Continuer avec OAuth`,"prov.logoutOk":`Déconnexion de {provider} réussie.`,"prov.logoutFail":`Impossible de se déconnecter de {provider}. L’état de votre compte n’a pas changé.`,"prov.removed":`« {name} » supprimé.`,"prov.removedDefault":`« {name} » supprimé. Le fournisseur par défaut est maintenant « {defaultProvider} ».`,"prov.removeFail":`Échec de la suppression de « {name} ».`,"prov.removeLastProvider":`Vous ne pouvez pas supprimer ce fournisseur si aucun autre fournisseur activé ne peut devenir le fournisseur par défaut.`,"prov.removeHasDependentCombos":`Supprimez ou mettez d’abord à jour les combinaisons dépendantes suivantes : {combos}.`,"prov.setDefault":`Définir par défaut`,"prov.setDefaultSuccess":`« {name} » est maintenant le fournisseur par défaut.`,"prov.setDefaultFail":`Impossible de définir « {name} » comme fournisseur par défaut.`,"prov.defaultDisabled":`Activez ce fournisseur avant de le définir comme fournisseur par défaut.`,"prov.updateFail":`Impossible de mettre à jour ce fournisseur.`,"prov.networkError":`Erreur réseau. Vérifiez que le proxy est en cours d’exécution et réessayez.`,"prov.added":`« {name} » ajouté. Déjà actif — exécutez {cmd} (ou redémarrez) pour afficher ses modèles dans le sélecteur de Codex.`,"prov.removeConfirm":`Supprimer le fournisseur « {name} » ? Ses modèles disparaîtront du sélecteur de Codex.`,"prov.hasApiKey":`clé API configurée`,"prov.hasHeaders":`en-têtes personnalisés configurés`,"prov.accounts":`Comptes ({n})`,"prov.accountsAria":`Afficher ou masquer les comptes de {name}`,"prov.accountActive":`Actif`,"prov.accountReauth":`Se reconnecter`,"prov.reauthenticate":`Se réauthentifier`,"prov.reauthAccountMissing":`Le compte sélectionné n’a pas été trouvé après la connexion`,"prov.reauthIdentityMismatch":`Le compte connecté ne correspond pas au compte sélectionné`,"prov.accountAdd":`Ajouter un compte`,"prov.accountNoLabel":`compte {id}`,"prov.accountSwitchTitle":`Utiliser ce compte`,"prov.accountSwitched":`Compte remplacé par {email}.`,"prov.accountSwitchFail":`Échec du changement de compte`,"prov.accountRemoved":`{email} supprimé.`,"prov.accountRemoveFail":`Impossible de supprimer {email}. Le compte n’a pas été modifié.`,"prov.accountRemoveAria":`Supprimer {email}`,"prov.accountRemoveConfirm":`Supprimer le compte {email} ? Ses données de connexion seront supprimées de ce proxy.`,"prov.keyAdd":`Ajouter une clé API`,"prov.keyAdded":`Clé API ajoutée à {name}.`,"prov.keyAddFail":`Échec de l’ajout de la clé API`,"prov.keyPlaceholder":`Coller la clé API`,"prov.keySwitchTitle":`Utiliser cette clé`,"prov.keySwitched":`Clé remplacée par {key}.`,"prov.keySwitchFail":`Échec du changement de clé`,"prov.keyRemoved":`Clé {key} supprimée.`,"prov.keyRemoveAria":`Supprimer la clé {key}`,"prov.keyRemoveConfirm":`Supprimer la clé API {key} ? Elle sera supprimée de la configuration de ce proxy.`,"prov.activeBadge":`Actif`,"prov.disabledBadge":`Désactivé`,"prov.defaultBadge":`Par défaut`,"prov.enable":`Activer`,"prov.disable":`Désactiver`,"prov.enabled":`« {name} » activé. Ses modèles peuvent de nouveau apparaître dans Codex.`,"prov.disabled":`« {name} » désactivé. Les paramètres sont conservés, mais ses modèles sont masqués.`,"prov.enableFail":`Échec de l’activation de « {name} ».`,"prov.disableFail":`Échec de la désactivation de « {name} ».`,"prov.enableAria":`Activer le fournisseur {name}`,"prov.disableAria":`Désactiver le fournisseur {name}`,"prov.defaultCannotDisable":`Le fournisseur par défaut ne peut pas être désactivé`,"prov.openaiAccountMode":`Mode de compte Codex`,"prov.openaiModePool":`Groupe`,"prov.openaiModeDirect":`Direct`,"prov.openaiPoolDesc":`Par défaut. Alterne entre la connexion principale et les comptes ajoutés selon l’affinité, le quota, le délai de récupération et le repli.`,"prov.openaiDirectDesc":`Utilise uniquement la connexion Codex actuelle ou principale. Les comptes du groupe enregistrés ne sont ni lus ni utilisés en alternance.`,"prov.openaiModeSaved":`Mode de compte OpenAI remplacé par {mode}.`,"prov.openaiModeSaveFailed":`Impossible de modifier le mode de compte OpenAI.`,"prov.openaiApiDesc":`Utilise une clé API OpenAI et n’utilise jamais les identifiants d’un compte Codex.`,"prov.manageCodexAccounts":`Gérer les comptes Codex`,"prov.openaiApiMissing":`Clé API requise`,"prov.openaiApiSetup":`Configurer la clé API`,"models.tab.catalog":`Modèles`,"models.tab.combos":`Combinaisons`,"models.tab.compatibility":`Compatibilité`,"models.tab.routing":`Routage (bêta)`,"models.tabsLabel":`Espaces des modèles`,"models.subtitle.combos":`Groupes ordonnés de modèles qui répondent sous un même identifiant. Enchaînez les cibles avec le repli ou répartissez la charge avec une stratégie d’équilibrage.`,"models.subtitle.compatibility":`Matrice en lecture seule des verdicts de compatibilité issus des preuves de projection du laboratoire.`,"models.subtitle.routing":`Profils de stratégie, évaluation à blanc et analyses de routage fondées sur les sources.`,"models.subtitle":`Choisissez les modèles visibles par Codex — accès direct aux GPT natifs et fournisseurs routés, regroupés par fournisseur (cliquez sur un en-tête pour le réduire). Les modèles masqués sont retirés du catalogue et du sélecteur, mais restent directement accessibles par leur identifiant exact. Les modifications s’appliquent au prochain tour Codex — opencodex invalide le cache de modèles de Codex de 5 min, sans nécessiter de redémarrage.`,"models.nativeGroupLabel":`OpenAI natif`,"models.nativeHint":"Les modèles en accès direct utilisent l’option de compte Groupe ou Direct sélectionnée dans Fournisseurs. La désactivation d’un modèle le masque dans le sélecteur Codex (son entrée de catalogue est conservée afin que sa réactivation la restaure à l’identique). Ajouter un modèle ici enregistre un sélecteur routé `openai/`, et non un nouvel identifiant passthrough brut.","models.active":`{active}/{total} visibles`,"models.workspace.providers":`Fournisseurs`,"models.workspace.allProviders":`Tous les fournisseurs`,"models.workspace.mainAria":`Détails du modèle`,"models.allOn":`Tout activer`,"models.allOff":`Tout désactiver`,"models.presetLabel":`Modèles`,"models.presetMode_preset":`Préréglage`,"models.presetMode_all":`Tous`,"models.presetMode_custom":`Personnalisé`,"models.presetSummary":`{count} sur {total} affichés — préréglage core v{version}`,"models.presetUpdateAvailable":`Préréglage v{version} disponible`,"models.presetAppliedToast":`{provider} : préréglage appliqué — {count} modèles sélectionnés`,"models.presetClearedToast":`{provider} : tous les modèles affichés`,"models.presetEmpty":`{provider} : le préréglage n’a trouvé aucun modèle — sélection inchangée`,"models.presetConfirmReplace":`Remplacer votre sélection par le préréglage de {count} modèles ?`,"models.cap350k":`Plafond de 350k`,"models.capApplied":`Plafond de contexte appliqué — il prendra effet au prochain tour Codex.`,"models.capSaveFailed":`Échec de l’enregistrement du plafond de contexte`,"models.contextCapped":`Plafond de 350k`,"models.contextCapLabel":`Fenêtre par défaut / plafond`,"models.v2Label":`Sous-agent`,"models.shadowCallOriginal":`⚠ {models} →`,"models.v2DocsLink":`Que sont v1 et v2 ?`,"models.v2Mode_v1":`v1`,"models.v2Mode_default":`base`,"models.v2Mode_v2":`v2`,"models.v2ModeDesc_v1":`Tous les modèles → interface v1`,"models.v2ModeDesc_default":`Valeurs par défaut en amont (sol/terra=v2, luna=v1)`,"models.v2ModeDesc_v2":`Tous les modèles → interface v2`,"models.keepNativeOnV1":`Garder ChatGPT sur v1`,"models.keepNativeOnV1Hint":`ChatGPT chiffre les tâches enfants v2 uniquement lorsqu’un parent natif ChatGPT reste sur v2, de sorte que Grok et Claude ne peuvent pas les lire. Activez cette option pour garder Sol/Terra sur v1 et éviter ce chiffrement. Les parents routés restent sur v2.`,"models.v2Help":`Contrôle l’interface multi-agent pour tous les modèles. + +v1 : agent classique à fil unique. Tous les modèles utilisent l’interface collab v1. +base : valeurs par défaut en amont — sol/terra utilisent v2, luna utilise v1 et les autres suivent l’indicateur de fonctionnalité codex. +v2 : agent multifil avec spawn_agent. Tous les modèles utilisent l’interface collab v2. + +En v2, « Garder ChatGPT sur v1 » laisse Sol/Terra sur l’interface v1 afin qu’ils puissent encore lancer Grok ou Claude. ChatGPT chiffre les tâches enfants v2 ; les modèles routés ne peuvent pas les lire. Les parents routés restent sur v2. + +Les modifications s’appliquent aux nouvelles sessions.`,"dash.multiAgent":`Sous-agent`,"models.v2Conflict":`[agents] max_threads est défini — codex refusera de démarrer ; supprimez-le de config.toml`,"models.v2Applied":`Mode sous-agent mis à jour — s’applique aux nouvelles sessions (redémarrez l’application Codex pour actualiser le sélecteur)`,"models.v2ThreadsLabel":`Nombre maximal de fils`,"models.v2ThreadsDefault":`par défaut (4)`,"models.v2ThreadsApplied":`Limite de fils mise à jour — s’applique aux nouvelles sessions`,"models.v2ThreadsInvalid":`La limite de fils doit être un entier >= 1`,"models.v2ThreadsApply":`Appliquer`,"models.capValue":`Défaut {value}`,"models.contextSettings":`Fenêtres perso`,"models.contextSettingsTitle":`Fenêtres perso — {provider}`,"models.contextDefault":`Valeur par défaut du fournisseur`,"models.contextModel":`Modèle`,"models.contextModelOverride":`Remplacement pour le modèle`,"models.contextHint":`Si vous connaissez déjà la fenêtre, écrivez ici la fenêtre Codex réelle. Sans métadonnées amont, cette valeur est utilisée ; une fenêtre plus grande est seulement abaissée, une plus petite est conservée. Laisser vide utilise la « Fenêtre par défaut / plafond » du fournisseur, ou 128k si ce plafond est désactivé.`,"models.contextAutomatic":`Détection automatique`,"models.contextSaved":`Fenêtres de contexte mises à jour — prend effet au prochain tour Codex.`,"models.contextUnchanged":`Aucune modification des fenêtres de contexte à enregistrer.`,"models.contextSaveFailed":`Échec de l’enregistrement des fenêtres de contexte`,"models.contextInvalid":`Les fenêtres de contexte doivent être des nombres entiers positifs`,"models.contextCappedValue":`Plafond de {value}`,"models.setAll":`Tout définir`,"models.setAllHint":`Active la fenêtre par défaut {value} pour chaque fournisseur routé. Si un relais omet context_window / context_length, cette valeur devient la fenêtre Codex réelle. Pour un seul modèle, utilisez « Fenêtres perso » sur la même ligne. Les fournisseurs natifs ne sont pas affectés.`,"models.collapseAll":`Tout réduire`,"models.expandAll":`Tout développer`,"models.orderHint":`Ordre du sélecteur : choix des sous-agents (dans l’ordre sélectionné) → autres modèles routés, classés par ordre alphabétique du fournisseur puis par ID de modèle → modèles natifs. Les options de visibilité ne font que filtrer les modèles ; elles ne modifient pas cet ordre.`,"models.custom":`Personnalisé…`,"models.customApply":`Appliquer`,"models.customPlaceholder":`Jetons (p. ex. 420000)`,"models.customAdd":`Ajouter un modèle personnalisé`,"models.customAddTitle":`Ajouter un modèle personnalisé — {provider}`,"models.customEditTitle":`Modifier le modèle personnalisé — {provider}`,"models.customAdded":`Modèle personnalisé ajouté`,"models.customUpdated":`Modèle personnalisé mis à jour`,"models.customDeleted":`Modèle personnalisé supprimé`,"models.customSaveFailed":`Échec de l’enregistrement du modèle personnalisé`,"models.customSaving":`Enregistrement…`,"models.customAddBtn":`Ajouter`,"models.customEditBtn":`Mettre à jour`,"models.customEdit":`Modifier`,"models.customDelete":`Supprimer`,"models.customDeleteConfirm":`Supprimer le modèle {name} ?`,"models.customBadge":`Personnalisé`,"models.customSummary":`{count} personnalisés`,"models.customFieldModelId":`ID du modèle (slug du point de terminaison)`,"models.customFieldModelIdPlaceholder":`p. ex. qwen4-max-preview`,"models.customFieldDisplayName":`Nom d’affichage (facultatif)`,"models.customFieldDisplayNamePlaceholder":`p. ex. Qwen 4 Max Preview`,"models.customFieldContext":`Fenêtre de contexte`,"models.customFieldModalities":`Modalités d’entrée`,"models.customFieldReasoning":`Effort de raisonnement`,"models.customFieldReasoningOverride":`Remplacer l’effort de raisonnement`,"models.reasoningEffort.none":`Aucun`,"models.reasoningEffort.minimal":`Minimal`,"models.reasoningEffort.low":`Faible`,"models.reasoningEffort.medium":`Moyen`,"models.reasoningEffort.high":`Élevé`,"models.reasoningEffort.xhigh":`Très élevé`,"models.reasoningEffort.max":`Maximum`,"models.tipProvider":`Fournisseur`,"models.tipContext":`Contexte`,"models.tipModalities":`Modalités`,"models.tipStatus":`État`,"models.tipActive":`Actif`,"models.tipDisabled":`Désactivé`,"models.applied":`Appliqué — prend effet au prochain tour Codex.`,"models.saveFailed":`Échec de l’enregistrement`,"models.networkError":`Erreur réseau — le proxy est-il en cours d’exécution ?`,"models.loadFail":`Échec du chargement des modèles — le proxy est-il en cours d’exécution ?`,"models.noRouted":`Aucun modèle routé`,"models.noRoutedHint":`Se connecter d’abord à un fournisseur ou en ajouter un.`,"models.emptyDiscovery":`Aucun modèle n’a été détecté. Vérifiez le point de terminaison du fournisseur ou ajoutez un modèle statique/personnalisé.`,"models.emptyDiscoveryDisabled":`La détection dynamique des modèles est désactivée et aucun modèle statique n’est configuré.`,"models.discoveryFailedBadge":`Échec de la détection`,"models.discoveryFailedHttp":`Échec de la détection des modèles (HTTP {status}).`,"models.discoveryFailedBlocked":`La détection des modèles a été bloquée par la politique de destination.`,"models.discoveryFailedInvalidResponse":`La détection des modèles a renvoyé une réponse non valide.`,"models.discoveryFailedNetwork":`La détection des modèles a échoué en raison d’une erreur réseau.`,"models.discoveryFailedProvider":`Le fournisseur a signalé une erreur de détection des modèles.`,"models.discoveryFailedGeneric":`Échec de la détection des modèles.`,"models.openProviderSettings":`Ouvrir les paramètres du fournisseur`,"models.loading":`Chargement…`,"models.search":`Rechercher des modèles…`,"models.showMore":`Afficher {n} de plus`,"models.allowlistLabel":`Sélection uniquement`,"models.allowlistHint":`Seuls les modèles cochés sont inclus dans le catalogue (vide = tous). Utile pour les fournisseurs proposant des milliers de modèles.`,"models.selectedCount":`{n} sélectionnés`,"sub.subtitle":`La commande {cmd} de Codex ne présente que les 5 premiers modèles (par priorité) comme remplacements. Choisissez-en jusqu’à 5 ici — natifs gpt ou routés — et opencodex définit leur priorité dans le catalogue pour qu’ils apparaissent en tête. Tout autre modèle reste accessible par son nom exact ; ceci contrôle uniquement ce qui est affiché.`,"sub.featured":`À la une`,"sub.advanced":`Avancé`,"sub.orderHintAria":`Comment cet ordre est utilisé`,"sub.orderHint":`L’ordre affiché ici détermine les positions 1 à 5 en haut du sélecteur de modèles Codex et les modèles candidats par défaut pour {cmd}.`,"sub.noneSelected":`Aucun modèle sélectionné — faites votre choix dans la liste ci-dessous.`,"sub.models":`Modèles`,"sub.search":`Rechercher des modèles (gpt natifs + routés)…`,"sub.settings":`Paramètres`,"sub.sections":`Sections des sous-agents`,"sub.delegation.model":`Modèle à appeler en premier`,"sub.delegation.modelHint":`Le modèle que Codex sollicite en premier lorsqu’il délègue une tâche. La liste À la une ci-dessus contient les modèles qu’il peut appeler ; celui-ci est appelé en premier.`,"sub.noModels":`Aucun modèle — connectez-vous d’abord à un fournisseur ou ajoutez-en un.`,"sub.saved":`{n} modèles enregistrés. Démarrez une nouvelle session Codex (ou exécutez {cmd}) pour les voir comme remplacements de spawn_agent.`,"sub.saveFailed":`Échec de l’enregistrement`,"sub.networkError":`Erreur réseau — le proxy est-il en cours d’exécution ?`,"sub.loadFail":`Échec du chargement des modèles — le proxy est-il en cours d’exécution ?`,"sub.loading":`Chargement…`,"sub.moveUp":`Monter {m}`,"sub.moveDown":`Descendre {m}`,"sub.removeAria":`Retirer {m}`,"sub.workspace.addToFeatured":`Ajouter {m} à la sélection À la une`,"sub.workspace.allModels":`Tous les modèles`,"sub.workspace.featuredFull":`La liste À la une est complète (5 maximum)`,"sub.workspace.mainAria":`Détails du modèle de sous-agent`,"sub.workspace.notFeatured":`Non mis à la une`,"sub.workspace.priority":`Priorité`,"sub.ultraMode":`Mode Ultra`,"sub.ultraModeHint":`Activer la politique de délégation multi-agent proactive pour tous les modèles et niveaux de raisonnement (sans modifier le niveau de raisonnement lui-même). Écrit features.multi_agent_v2.multi_agent_mode_hint_text dans config.toml.`,"sub.ultraModeV2Required":`Nécessite l’interface multi-agent v2 — activez multi_agent_v2 et sélectionnez d’abord v2 dans le contrôle du mode Sous-agent.`,"sub.ultraModeText":`Texte de délégation du mode Ultra`,"sub.ultraModePreset":`Rétablir le préréglage`,"sub.ultraModeLoadFail":`Échec du chargement des paramètres du mode Ultra — le proxy est-il en cours d’exécution ?`,"sub.ultraModeSaveFail":`Échec de l’enregistrement des paramètres du mode Ultra`,"sub.ultraModeSaved":`Mode Ultra enregistré. S’applique aux nouvelles sessions Codex.`,"sub.workspace.removeFromFeatured":`Retirer {m} de la sélection À la une`,"sub.workspace.selectModel":`Sélectionner un modèle`,"sub.workspace.selectModelDesc":`Choisissez un modèle dans la liste pour afficher ses détails et le mettre à la une pour spawn_agent.`,"sub.workspace.selector":`Sélecteur public`,"logs.title":`Journaux des requêtes`,"logs.tabLogs":`Journaux`,"logs.tabDebug":`Débogage`,"logs.subtitle":`Requêtes récentes routées par le proxy opencodex local, de la plus récente à la plus ancienne.`,"logs.autoRefresh":`Actualisation automatique`,"logs.noRequests":`Aucune requête pour le moment.`,"logs.loadError":`Impossible de charger les journaux des requêtes.`,"logs.filter.surface.label":`Interface`,"logs.filter.surface.all":`Toutes`,"logs.filter.surface.claude":`Claude`,"logs.filter.surface.codex":`Codex`,"logs.filter.surface.grok":`Grok`,"logs.filter.interceptedHelpersOnly":`Assistants interceptés uniquement`,"logs.badge.interceptedHelper":`I · {model}`,"logs.badge.interceptedHelperTitle":`Requête d'assistant interceptée`,"logs.filter.conversation.label":`Conversation`,"logs.filter.conversation.placeholder":`Coller l’ID de conversation`,"logs.filter.conversation.clear":`Effacer`,"logs.filter.model.label":`Modèle`,"logs.filter.model.placeholder":`Filtrer par modèle ou fournisseur`,"logs.filter.conversation.apply":`Filtrer les journaux`,"logs.conversation.totals":`{requests} requêtes · {tokens} jetons · {cost}`,"logs.conversation.scope":`Les totaux couvrent uniquement le tampon circulaire des journaux actuellement chargé.`,"logs.conversation.excluded":`({unpriced} sans tarif, {unmetered} sans mesure exclus du total en ~$)`,"logs.cost.approximate":`{amount}`,"logs.cost.lowerBound":`≥{amount}`,"logs.cost.unavailable":`indisponible`,"logs.detail.conversation":`Conversation`,"logs.badge.claude":`Claude`,"logs.badge.grok":`Grok`,"logs.col.time":`Heure`,"logs.col.request":`Requête`,"logs.col.model":`Modèle`,"logs.col.effort":`Niveau`,"logs.col.provider":`Fournisseur`,"logs.col.status":`État`,"logs.col.tokens":`Jetons`,"logs.col.tokPerSec":`jetons/s`,"logs.col.estimatedCost":`~$`,"logs.metric.tokPerSecTitle":`Jetons de sortie par seconde sur toute la durée de la requête`,"logs.metric.estimatedCostTitle":`Équivalent au tarif catalogue de l’API, et non montant réellement facturé ; aucun tarif n’est disponible en l’absence de correspondance`,"usage.cost.total":`Équivalent au tarif catalogue de l’API (cette période)`,"usage.cost.disclaimer":`Ceci n’est pas un reçu de facturation. L’utilisation d’un abonnement ou les crédits du fournisseur peuvent s’appliquer à la place.`,"usage.cost.unpricedNote":`{count} requêtes exclues (aucun tarif ni donnée d’utilisation)`,"logs.detail.section.basic":`Informations générales`,"logs.detail.route.section":`Décision de routage`,"logs.detail.route.kind":`Type de route`,"logs.detail.route.profile":`Profil`,"logs.detail.route.selected":`Sélection`,"logs.detail.route.candidates":`Candidats`,"logs.detail.route.unknown":`Aucune trace de routage enregistrée pour cette requête (ligne antérieure à la traçabilité).`,"logs.detail.section.performance":`Performances`,"logs.detail.section.cost":`Équivalent au tarif catalogue de l’API`,"logs.detail.section.attempts":`Tentatives de combinaison`,"logs.detail.section.usage":`Utilisation brute`,"logs.detail.ttft":`TTFT`,"logs.detail.costTotal":`Équivalent au tarif catalogue`,"logs.detail.totalTokens":`Nombre total de jetons`,"logs.detail.matchedKey":`Clé de tarif correspondante`,"logs.detail.priceSource":`Source du tarif`,"logs.detail.unavailableReason":`Motif d’indisponibilité`,"logs.detail.copyRequestId":`Copier l’ID de requête`,"logs.detail.copied":`Copié`,"logs.detail.source.jawcode":`Catalogue jawcode`,"logs.detail.source.expected":`Remplacement par le tarif attendu`,"logs.detail.source.user":`Remplacement par le tarif configuré pour le fournisseur`,"logs.detail.verification.verified":`Vérifié`,"logs.detail.verification.derived":`Dérivé du modèle de base`,"logs.detail.attempt.target":`Fournisseur / modèle`,"logs.detail.attempt.reason":`Résultat / motif`,"logs.detail.attempt.completed":`Terminée`,"logs.detail.attempt.e2eNote":`Le débit global en jetons/s est calculé de bout en bout ; chaque tentative utilise sa propre durée.`,"logs.detail.attempt.recovery.transient5xx":`Erreur 5xx temporaire`,"logs.detail.attempt.recovery.connectionReset":`Réinitialisation de la connexion`,"logs.detail.attempt.recovery.emptyCompletion":`Nouvelle tentative après une réponse vide`,"logs.detail.attempt.recovery.oauth401":`Réauthentification OAuth`,"logs.detail.attempt.recovery.key429":`Clé soumise à une limitation de débit (429)`,"logs.detail.attempt.recovery.rateLimit429":`Limitation de débit (429)`,"logs.detail.attempt.recovery.anthropicOauth429":`Limitation de débit OAuth Anthropic (429)`,"logs.detail.attempt.recovery.image413":`Charge utile d’image trop volumineuse (413)`,"logs.detail.attempt.recovery.unknown":`Motif de récupération inconnu`,"logs.detail.reason.usage_missing":`L’utilisation n’a pas été communiquée.`,"logs.detail.reason.usage_unsupported":`Ce fournisseur ne communique pas l’utilisation.`,"logs.detail.reason.output_missing":`Aucun nombre positif de jetons de sortie n’a été communiqué.`,"logs.detail.reason.invalid_duration":`La durée de la requête n’est pas valide.`,"logs.detail.reason.price_unmatched":`Aucun tarif correspondant n’a été trouvé.`,"logs.detail.reason.invalid_cache_breakdown":`Le détail des jetons du cache est incompatible avec le nombre total de jetons d’entrée.`,"logs.detail.reason.invalid_usage":`Les données d’utilisation contiennent une valeur de jetons non valide.`,"logs.detail.reason.combo_attempt_unavailable":`Au moins une tentative de combinaison n’a pas pu être chiffrée.`,"logs.detail.estimate.usage_estimated":`L’utilisation du fournisseur est estimée.`,"logs.detail.estimate.cache_detail_missing":`Les détails du cache n’étaient pas disponibles ; l’entrée est une estimation de la limite supérieure.`,"logs.detail.estimate.expected_price_overlay":`Un tarif catalogue attendu et vérifié a été utilisé.`,"logs.detail.estimate.provider_cost_overlay":`Un remplacement de tarif configuré pour le fournisseur a été utilisé.`,"logs.detail.estimate.priority_lower_bound":`Le tarif Priority confirmé n’est pas disponible ; l’estimation affichée est une borne inférieure connue.`,"logs.col.error":`Erreur`,"logs.col.upstreamReason":`Motif en amont`,"logs.col.duration":`Durée`,"logs.modelTooltip.model":`modèle`,"logs.modelTooltip.resolvedModel":`modèle résolu`,"logs.modelTooltip.requestedTier":`niveau demandé`,"logs.modelTooltip.configuredTier":`niveau configuré`,"logs.modelTooltip.responseTier":`niveau de réponse`,"logs.modelTooltip.supportsTier":`prise en charge du niveau`,"logs.tokens.reported":`communiqués`,"logs.tokens.unreported":`non communiqués`,"logs.tokens.unsupported":`non pris en charge`,"logs.tokens.estimated":`estimés`,"logs.tokens.input":`entrée`,"logs.tokens.output":`sortie`,"logs.tokens.cacheRead":`lecture du cache (c)`,"logs.tokens.cacheWrite":`écriture dans le cache (w)`,"logs.tokens.reasoning":`raisonnement`,"logs.tokens.noCache":`aucune donnée de cache`,"logs.tokens.contextTotal":`contexte actif`,"logs.tokens.noCacheNote":`ce fournisseur ne communique pas les jetons du cache`,"logs.tokens.noCacheCursor":`détails du cache Cursor non communiqués`,"logs.tokens.noCacheCursorNote":`Cursor n’indique pas le nombre de jetons lus/écrits dans le cache ; la valeur est inconnue et ne constitue pas un défaut de cache confirmé`,"logs.tokens.estimatedNote":`estimés (le fournisseur ne communique pas l’utilisation exacte)`,"logs.details":`Détails`,"logs.detailTitle":`Détails de la requête`,"logs.detailRaw":`Entrée de journal brute`,"debug.title":`Débogage`,"debug.subtitle":`Diagnostics facultatifs du transport des fournisseurs et de l’extraction de l’utilisation. Les erreurs de requête et les erreurs 502 restent dans l’onglet Journaux.`,"debug.debug":`Débogage du fournisseur`,"debug.usage":`Extraction de l’utilisation`,"debug.injection":`Journal des injections`,"debug.claude":`Entrées Claude`,"debug.claudeInbound.title":`Requêtes entrantes Claude`,"debug.claudeInbound.sub":`Ce que Claude Code/Desktop envoie réellement (thinking, effort, métadonnées) — aucun texte de prompt n’est stocké.`,"debug.claudeInbound.empty":`Aucune requête capturée pour le moment. Envoyez un message depuis Claude pendant que cette option est activée.`,"debug.claudeInbound.time":`Heure`,"debug.claudeInbound.endpoint":`Point de terminaison`,"debug.claudeInbound.model":`Modèle`,"debug.claudeInbound.none":`aucun`,"debug.reset":`Effacer les remplacements d’exécution`,"debug.refresh":`Actualiser`,"debug.follow":`Suivre`,"debug.streamProvider":`Fournisseur`,"debug.streamUsage":`Utilisation`,"debug.streamInjection":`Injection`,"debug.loading":`Chargement des paramètres de débogage…`,"debug.loadFailed":`Impossible de charger les paramètres de débogage.`,"debug.emptyTitle":`La journalisation de débogage est désactivée`,"debug.empty":`Activez Débogage du fournisseur ou Extraction de l’utilisation dans la carte ci-dessus. Les lignes apparaîtront ici après l’envoi d’une requête par le proxy.`,"debug.noLinesTitle":`En attente de lignes`,"debug.noLines.provider":`Le débogage du fournisseur est activé, mais il n’enregistre que les anomalies de transport (trames abandonnées ou mal formées, et événements de connexion/nouvelle tentative Cursor). Une requête sans anomalie auprès d’un fournisseur comme Anthropic peut ne produire aucune ligne.`,"debug.noLines.usage":`L’extraction de l’utilisation est activée, mais rien n’a encore été capturé. Envoyez une conversation ou une requête par Codex pour qu’elle apparaisse ici.`,"debug.noLines.injection":`Le journal des injections est activé, mais rien n’a encore été capturé. Il consigne l’injection des directives multi-agents et les décisions de plafonnement du niveau lors des tours collab et des sous-agents.`,"usage.title":`Utilisation`,"usage.subtitle":`Comptabilisation locale des jetons par votre proxy. Une utilisation manquante n’est jamais affichée comme nulle.`,"usage.loading":`Chargement des données d’utilisation…`,"usage.empty":`Aucune utilisation enregistrée pour le moment. Envoyez une requête par le proxy pour voir l’activité ici.`,"usage.loadError":`Impossible de charger les données d’utilisation.`,"usage.range.all":`Tout`,"usage.range.available":`Historique disponible`,"usage.historyTruncated":`Les totaux couvrent uniquement l’historique disponible, car les données d’utilisation plus anciennes n’ont pas été chargées.`,"usage.historyTruncatedWindow":`Les heures de début des requêtes dans les lignes chargées vont de {start} à {end}. Les entrées antérieures du fichier ont été omises en raison de la limite de lecture ; toute période sélectionnée peut donc être incomplète.`,"usage.range.30d":`30 j`,"usage.range.7d":`7 j`,"usage.card.requests":`Requêtes`,"usage.card.measured":`Mesurées`,"usage.card.reported":`Communiquées`,"usage.card.totalTokens":`Nombre total de jetons`,"usage.card.cachedTokens":`Lectures du cache`,"usage.card.cachedTokensHint":`Jetons de prompt servis depuis le cache du fournisseur (lectures). Les écritures dans le cache sont indiquées ci-dessous lorsqu’elles sont présentes.`,"usage.card.cacheWriteTokens":`écritures dans le cache`,"usage.card.coverage":`Couverture`,"usage.card.activeDays":`Jours actifs`,"usage.section.heatmap":`Activité quotidienne`,"usage.section.overview":`Vue d’ensemble`,"usage.section.models":`Modèles`,"usage.section.providers":`Fournisseurs`,"usage.section.coverage":`Répartition de la couverture`,"usage.workspace.report":`Rapport d’utilisation`,"usage.workspace.sections":`Sections d’utilisation`,"usage.coverage.measured":`Mesurée`,"usage.coverage.reported":`Communiquée par le fournisseur`,"usage.coverage.estimated":`Estimée`,"usage.coverage.note":`Les entrées mesurées comprennent les nombres de jetons communiqués par le fournisseur et ceux qui sont estimés. Les requêtes non communiquées ou non prises en charge sont suivies, mais ne sont jamais artificiellement comptées comme zéro jeton.`,"usage.search.models":`Rechercher des modèles…`,"usage.col.requests":`Requêtes`,"usage.col.measured":`Mesurées`,"usage.col.reported":`Communiquées`,"usage.col.tokens":`Jetons`,"usage.col.share":`Part`,"usage.heatmap.less":`Moins`,"usage.heatmap.more":`Plus`,"usage.dayMon":`Lun`,"usage.dayWed":`Mer`,"usage.dayFri":`Ven`,"usage.heatmap.tooltipTokens":`{tokens} jetons`,"usage.heatmap.tooltipRequests":`{requests} requêtes`,"nav.storage":`Stockage`,"storage.title":`Stockage`,"storage.subtitle":`Consultez l’espace utilisé dans CODEX_HOME. Le nettoyage ne touche jamais aux sessions actives.`,"storage.loading":`Analyse du stockage…`,"storage.empty":`CODEX_HOME est vide ou absent — rien à signaler.`,"storage.error":`Échec de l’analyse du stockage. Vérifiez que CODEX_HOME pointe vers un répertoire valide.`,"storage.refresh":`Relancer l’analyse`,"storage.rescanned":`Analyse terminée.`,"storage.card.total":`Taille totale`,"storage.card.files":`Fichiers`,"storage.card.home":`CODEX_HOME`,"storage.snapshot.lastScan":`Dernière analyse`,"storage.snapshot.scanning":`Analyse…`,"storage.snapshot.unavailable":`Aucune analyse pour le moment.`,"storage.cleanupCard.title":`Libérer de l’espace`,"storage.cleanupCard.tabs":`Options de nettoyage`,"storage.cleanupCard.tab.policy":`Politique`,"storage.cleanupCard.tab.quarantine":`Quarantaine`,"storage.cleanup.noArchives":`Aucune session archivée à nettoyer.`,"storage.section.buckets":`Catégories`,"storage.section.largest":`Fichiers les plus volumineux`,"storage.workspace.overview":`Vue d’ensemble`,"storage.workspace.selectBucket":`Sélectionnez une catégorie dans la liste pour afficher sa répartition.`,"storage.col.bucket":`Catégorie`,"storage.col.size":`Taille`,"storage.col.files":`Fichiers`,"storage.col.oldest":`Plus ancien`,"storage.col.newest":`Plus récent`,"storage.col.rows":`Lignes de la BDD`,"storage.rows.unknown":`inconnu (verrouillé)`,"storage.bucket.sessions":`Sessions actives`,"storage.bucket.archived_sessions":`Sessions archivées`,"storage.bucket.logs_db":`Base de données des journaux`,"storage.bucket.state_db":`Base de données d’état`,"storage.bucket.attachments":`Pièces jointes`,"storage.bucket.deletion_manifests":`Manifestes de suppression`,"storage.bucket.other":`Autres`,"storage.cleanup.title":`Nettoyage des archives`,"storage.cleanup.help":`Supprimez un pourcentage des sessions archivées les plus anciennes. Les sessions actives ne sont jamais touchées. La quarantaine est le mode par défaut : les fichiers sont déplacés vers CODEX_HOME/.trash.`,"storage.cleanup.slider":`Pourcentage d’archives les plus anciennes`,"storage.cleanup.percent":`{percent} %`,"storage.cleanup.preset":`{percent}`,"storage.cleanup.preview":`Aperçu`,"storage.cleanup.confirmTitle":`Confirmer le nettoyage des archives`,"storage.cleanup.confirmBody":`Cette opération traitera {count} fichier(s) archivé(s) (~{size}), soit les {percent} % les plus anciens.`,"storage.cleanup.moreFiles":`…et {n} de plus`,"storage.cleanup.permanent":`Supprimer définitivement (sans quarantaine)`,"storage.cleanup.permanentWarn":`La suppression définitive est irréversible.`,"storage.cleanup.quarantineNote":`Les fichiers sont déplacés vers .trash sous CODEX_HOME. Vous pouvez les restaurer depuis l’onglet Quarantaine.`,"storage.cleanup.cancel":`Annuler`,"storage.cleanup.confirmQuarantine":`Mettre en quarantaine`,"storage.cleanup.confirmPermanent":`Supprimer définitivement`,"storage.cleanup.doneQuarantine":`{count} fichier(s) mis en quarantaine ({size}).`,"storage.cleanup.donePermanent":`{count} fichier(s) supprimé(s) définitivement ({size}).`,"storage.cleanup.previewFailed":`Échec de l’aperçu.`,"storage.cleanup.cleanupFailed":`Échec du nettoyage.`,"storage.cleanup.err.codex_busy":`Codex utilise state.sqlite — réessayez après avoir quitté Codex.`,"storage.cleanup.err.stale_preview":`Les fichiers archivés ont changé depuis l’aperçu — relancez l’aperçu.`,"storage.cleanup.err.restore_pending_overlap":`Les archives sélectionnées chevauchent une restauration incomplète depuis la corbeille — terminez ou relancez d’abord la restauration.`,"storage.cleanup.err.referenced_history":`Les archives sélectionnées sont encore référencées par un historique dérivé ou paginé.`,"storage.cleanup.err.invalid_digest":`L’empreinte de l’aperçu est absente ou non valide.`,"storage.cleanup.err.invalid_mode":`Le mode de nettoyage doit être la quarantaine ou la suppression définitive.`,"storage.cleanup.err.fs_failed":`Échec du nettoyage du système de fichiers. Certaines modifications ont peut-être déjà été appliquées — vérifiez CODEX_HOME/.trash et tout chemin de récupération indiqué.`,"storage.cleanup.err.fs_failed_trash":`Échec du nettoyage du système de fichiers. Certaines modifications ont peut-être déjà été appliquées — recherchez les fichiers récupérables dans {trashDir} et manifest.json.`,"storage.cleanup.err.db_reconcile_failed":`Impossible de mettre à jour la base de données d’état de Codex.`,"storage.cleanup.err.cleanup_failed":`Échec du nettoyage.`,"storage.trash.title":`Quarantaine`,"storage.trash.help":`Sessions archivées déplacées vers CODEX_HOME/.trash. La restauration remet en place les fichiers JSONL et les lignes de conversation.`,"storage.trash.empty":`Aucune entrée en quarantaine.`,"storage.trash.loading":`Chargement de la quarantaine…`,"storage.trash.col.when":`Mise en quarantaine`,"storage.trash.col.files":`Fichiers`,"storage.trash.col.size":`Taille`,"storage.trash.col.mode":`Mode`,"storage.trash.col.id":`Entrée`,"storage.trash.restore":`Restaurer`,"storage.trash.confirmTitle":`Restaurer l’entrée de quarantaine ?`,"storage.trash.confirmBody":`Restaurer {count} fichier(s) (~{size}) depuis {id} vers les sessions archivées.`,"storage.trash.cancel":`Annuler`,"storage.trash.confirmRestore":`Restaurer`,"storage.trash.done":`{count} fichier(s) restauré(s) ({size}).`,"storage.trash.restoreFailed":`Échec de la restauration.`,"storage.trash.listFailed":`Impossible d’afficher les entrées de quarantaine.`,"storage.trash.mode.quarantine":`quarantaine`,"storage.trash.mode.permanent":`définitif (incomplet)`,"storage.trash.err.codex_busy":`Codex utilise state.sqlite — réessayez après avoir quitté Codex.`,"storage.trash.err.invalid_trash":`L’ID de l’entrée de la corbeille est absent ou non valide.`,"storage.trash.err.missing_trash":`L’entrée de la corbeille est introuvable.`,"storage.trash.err.dest_exists":`La destination de restauration existe déjà — supprimez ou renommez le fichier archivé, puis réessayez.`,"storage.trash.err.fs_failed":`Échec de la restauration du système de fichiers. Certains fichiers ont peut-être déjà été restaurés — vérifiez archived_sessions et .trash.`,"storage.trash.err.db_reconcile_failed":`Impossible de restaurer les lignes de la base de données d’état de Codex.`,"storage.trash.err.storage_mutation_busy":`Un autre nettoyage ou une autre restauration du stockage est en cours — réessayez dans quelques instants.`,"storage.trash.err.restore_failed":`Échec de la restauration.`,"storage.trash.err.restore_worker_timeout":`La restauration a pris trop de temps (plus de 10 minutes) et a été arrêtée.`,"storage.trash.err.restore_worker_aborted":`La restauration a été annulée lors de l’arrêt.`,"storage.trash.err.restore_worker_failed":`Le processus de restauration s’est interrompu ou a échoué de manière inattendue.`,"storage.policy.title":`Politique de nettoyage automatique`,"storage.policy.help":`Nettoyage facultatif par lots lorsque les sessions archivées dépassent un seuil. Désactivé par défaut — jamais activé automatiquement.`,"storage.policy.loading":`Chargement de la politique…`,"storage.policy.loadFailed":`Impossible de charger la politique de nettoyage.`,"storage.policy.saveFailed":`Impossible d’enregistrer la politique de nettoyage.`,"storage.policy.runFailed":`Échec de l’exécution de la politique.`,"storage.policy.alreadyRunning":`Une exécution de la politique de nettoyage est déjà en cours.`,"storage.policy.invalid":`Valeurs de politique non valides.`,"storage.policy.enabled":`Activer le nettoyage automatique`,"storage.policy.enabledHint":`Cette option est désactivée par défaut. Une fois activée, elle ne s’exécute que selon la planification choisie (ou avec Exécuter maintenant).`,"storage.policy.threshold":`Lorsque la taille des archives dépasse (Gio)`,"storage.policy.trigger":`Déclencheur`,"storage.policy.target":`Objectif du nettoyage`,"storage.policy.targetPercent":`Supprimer les archives les plus anciennes (%)`,"storage.policy.targetReduce":`Réduire la taille des archives à (Gio)`,"storage.policy.thresholdInc":`Augmenter le seuil`,"storage.policy.thresholdDec":`Diminuer le seuil`,"storage.policy.percentInc":`Augmenter le pourcentage`,"storage.policy.percentDec":`Diminuer le pourcentage`,"storage.policy.reduceInc":`Augmenter la taille cible`,"storage.policy.reduceDec":`Diminuer la taille cible`,"storage.policy.schedule":`Planification`,"storage.policy.schedule.manual":`Manuel uniquement`,"storage.policy.schedule.startup":`Au démarrage du proxy`,"storage.policy.schedule.daily":`Quotidienne`,"storage.policy.schedule.weekly":`Hebdomadaire`,"storage.policy.mode":`Mode de suppression`,"storage.policy.mode.quarantine":`Quarantaine (par défaut)`,"storage.policy.mode.permanent":`Suppression définitive`,"storage.policy.permanentWarn":`Le mode définitif est irréversible. Préférez la quarantaine sauf si vous êtes certain de votre choix.`,"storage.policy.lastRun":`Dernière exécution`,"storage.policy.lastRunDetail":`{count} supprimés · {size} libérés`,"storage.policy.nextRun":`Prochaine exécution`,"storage.policy.never":`Jamais`,"storage.policy.save":`Enregistrer`,"storage.policy.runNow":`Exécuter maintenant`,"storage.policy.running":`Exécution…`,"storage.policy.saved":`Politique enregistrée.`,"storage.policy.skippedDisabled":`La politique est désactivée — activez-la d’abord.`,"storage.policy.skippedUnder":`La taille des archives est inférieure au seuil — aucune action nécessaire.`,"storage.policy.skippedEmpty":`Aucune archive candidate ne correspond à l’objectif.`,"storage.policy.doneQuarantine":`La politique a mis {count} fichier(s) en quarantaine ({size}).`,"storage.policy.donePermanent":`La politique a supprimé définitivement {count} fichier(s) ({size}).`,"storage.policy.metadataSaveWarning":`L’exécution de la politique est terminée, mais ses métadonnées de planification n’ont pas pu être enregistrées.`,"modal.addNamed":`Ajouter : {label}`,"modal.add":`Ajouter un fournisseur`,"modal.search":`Rechercher des fournisseurs…`,"modal.logInWith":`Se connecter avec {label}`,"modal.waitingBrowser":`En attente du navigateur…`,"modal.providerName":`Nom du fournisseur`,"modal.adapter":`Adaptateur`,"modal.baseUrl":`URL de base`,"modal.endpoint":`Point de terminaison`,"modal.endpoint.tokenPlan":`Forfait de jetons`,"modal.endpoint.payAsYouGo":`Paiement à l’utilisation`,"modal.endpoint.custom":`Personnalisé`,"modal.defaultModel":`Modèle par défaut (facultatif)`,"modal.allowPrivateNetwork":`Autoriser le réseau local/privé`,"modal.allowPrivateNetworkHint":`À activer uniquement pour les fournisseurs volontairement auto-hébergés. Les points de terminaison de métadonnées restent bloqués.`,"modal.nameRequired":`Le nom du fournisseur est requis`,"modal.baseUrlRequired":`L’URL de base est requise`,"modal.networkError":`Erreur réseau — le proxy est-il en cours d’exécution ?`,"modal.loginFailStart":`Échec du lancement de la connexion`,"modal.waitingLogin":`En attente de la connexion dans le navigateur…`,"modal.loggingIn":`Connexion…`,"modal.loginTimeout":`Délai de connexion dépassé — réessayez.`,"modal.back":`Retour`,"modal.badge.oauth":`OAuth`,"modal.customProvider":`Fournisseur personnalisé`,"modal.failedStatus":`Échec ({status})`,"modal.loginError":`Erreur de connexion : {error}`,"modal.badge.codexLogin":`Connexion Codex`,"modal.badge.local":`Local`,"modal.badge.apiKey":`Clé API`,"modal.badge.direct":`Direct`,"modal.badge.pool":`Groupe`,"modal.badge.free":`Gratuit`,"modal.invalidPreset":`Ce préréglage de fournisseur intégré est incomplet. Redémarrez le proxy et réessayez.`,"modal.freeTierTitle":`Offre gratuite`,"modal.freeTierDefault":`Aucune clé API requise. Fonctionne immédiatement.`,"modal.tab.accounts":`Comptes`,"modal.tab.free":`Gratuit`,"modal.tab.paid":`Payant`,"modal.accountsHint":`Connectez-vous ici à ChatGPT/Codex, aux fournisseurs OAuth et aux comptes avec clé API. OpenAI est intégré : connectez-vous au lieu de l’ajouter de nouveau.`,"modal.accountsCodexAuthLink":`Codex Auth`,"modal.notListed":`Fournisseur absent de la liste ? Ajoutez-en un personnalisé`,"modal.catalogLoading":`Chargement du catalogue…`,"modal.accountLogin":`Se connecter`,"modal.accountLogout":`Se déconnecter`,"modal.accountAdd":`Ajouter un compte`,"modal.accountManage":`Gérer`,"modal.accountCodexPool":`Groupe de comptes ChatGPT`,"modal.accountLoggedIn":`Connecté`,"modal.accountLoggedOut":`Non connecté`,"quota.fiveHourLimit":`Limite sur 5 heures`,"quota.ageMinutes":`{n} min`,"quota.ageHours":`{n} h`,"quota.ageDays":`{n} j`,"quota.observedAgo":`Relevé il y a {age}`,"quota.observedHint":`Meta ne communique l'utilisation que pendant une réponse en streaming : il s'agit de la dernière valeur observée, pas d'une mesure en direct.`,"quota.weeklyLimit":`Limite hebdomadaire`,"quota.monthlyLimit":`Limite sur 30 jours`,"quota.cursorFirstParty":`Modèles propriétaires`,"quota.cursorApiUsage":`Utilisation de l’API`,"quota.totalSubscriptionCredits":`Total des crédits d’abonnement`,"quota.creditsBalance":`Solde de crédits`,"quota.creditsPeriodEnds":`La période de facturation se termine le {date}`,"quota.usedPercent":`{pct} % utilisés`,"quota.limitReached":`Limite atteinte`,"quota.resetsToday":`Réinitialisation aujourd’hui à {time}`,"quota.resetsTomorrow":`Réinitialisation demain à {time}`,"quota.resetsAt":`Réinitialisation {when}`,"quota.resetsRelativeMinutes":`Réinitialisation dans {n} min`,"quota.resetsRelativeHours":`Réinitialisation dans {n} h`,"pws.status.ready":`Prêt`,"pws.status.needsSetup":`Configuration requise`,"pws.status.needsAttention":`Attention requise`,"pws.auth.chatgptPassthrough":`Relais ChatGPT`,"pws.auth.noKey":`Aucune clé requise`,"pws.freeTitle":`Tarification gratuite (une clé peut néanmoins être requise)`,"pws.localTitle":`Environnement d’exécution local`,"pws.modelCountOne":`1 modèle`,"pws.modelCount":`{count} modèles`,"pws.rail.suffixDefault":` · par défaut`,"pws.rail.suffixLocal":` · local`,"pws.rail.suffixFree":` · gratuit`,"pws.rail.selectAria":`Sélectionner {name} — {status}{suffix}`,"pws.searchPlaceholder":`Rechercher des fournisseurs…`,"pws.filterAria":`Filtrer les fournisseurs`,"pws.providerFiltersAria":`Filtres des fournisseurs`,"pws.filters":`Filtres`,"pws.filterStatus":`État`,"pws.pricing":`Tarification`,"pws.paid":`Payant`,"pws.filterType":`Type`,"pws.type.cloud":`Cloud`,"pws.type.local":`Local`,"pws.type.selfHosted":`Auto-hébergé`,"pws.type.login":`Connexion`,"pws.sort":`Trier`,"pws.sortProvidersAria":`Trier les fournisseurs`,"pws.sort.az":`A–Z`,"pws.sort.za":`Z–A`,"pws.sort.freePaid":`Gratuits en premier`,"pws.sort.paidFree":`Payants en premier`,"pws.sort.accountsFirst":`Comptes en premier`,"pws.resetAll":`Tout réinitialiser`,"pws.providerList":`Liste des fournisseurs`,"pws.providersAria":`Fournisseurs`,"pws.groupReady":`Prêts ({count})`,"pws.groupNeedsSetup":`Configuration requise ({count})`,"pws.groupDisabled":`Désactivés ({count})`,"pws.noSearchResults":`Aucun fournisseur ne correspond à votre recherche.`,"pws.noMatchFilters":`Aucun fournisseur ne correspond aux filtres.`,"pws.noProvidersConfigured":`Aucun fournisseur configuré.`,"pws.workspaceMainAria":`Détails du fournisseur`,"pws.detailComingSoon":`La vue détaillée sera bientôt disponible — utilisez la vue classique pour gérer ce fournisseur.`,"pws.selectPrompt":`Sélectionnez un fournisseur dans la liste.`,"pws.connectFirst":`Connecter votre premier fournisseur`,"pws.empty.browseFree":`Parcourir les fournisseurs gratuits`,"pws.empty.browseFreeDesc":`Commencez sans abonnement`,"pws.empty.connectAccount":`Connecter un compte`,"pws.empty.connectAccountDesc":`Utilisez votre identifiant ChatGPT ou celui du fournisseur`,"pws.empty.addEndpoint":`Ajouter un point de terminaison`,"pws.empty.addEndpointDesc":`URL de base personnalisée et clé API`,"pws.tab.overview":`Vue d’ensemble`,"pws.tab.models":`Modèles`,"pws.tab.usage":`Utilisation`,"pws.tab.accounts":`Comptes`,"pws.tab.settings":`Paramètres`,"pws.connection":`Connexion`,"pws.status.connected":`Connecté`,"pws.attentionTitle":`Intervention requise`,"pws.attention.reauth":`Le compte actif doit être réauthentifié`,"pws.attention.reauthForward":`Le compte Codex actif doit être réauthentifié — ouvrez Comptes pour corriger le problème`,"pws.attention.missingCredentials":`Identifiants manquants`,"pws.cell.auth":`Authentification`,"pws.cell.note":`Note`,"pws.cell.defaultModel":`Modèle par défaut`,"pws.statsAria":`Statistiques du fournisseur`,"pws.statsTitle":`Statistiques`,"pws.stats.totalRequests":`Requêtes (30 j)`,"pws.stats.totalTokens":`Jetons (30 j)`,"pws.stats.quotaUpdated":`Quota mis à jour`,"pws.stats.quotaTracked":`Limites de débit suivies dans l’onglet Utilisation.`,"pws.stats.source":`Source`,"pws.usageLast30d":`Utilisation (30 derniers jours)`,"pws.estimatedCost":`Coût estimé`,"pws.costDisclaimer":`Estimation fondée sur le tarif public de l’API, et non montant réellement facturé.`,"pws.modelBreakdown":`Répartition par modèle`,"pws.col.model":`Modèle`,"pws.col.cost":`Coût est.`,"pws.col.tokens":`Jetons`,"pws.col.requests":`Req.`,"pws.col.share":`Part`,"pws.tokenInput":`Entrée`,"pws.tokenOutput":`Sortie`,"pws.metricRequests":`requêtes`,"pws.metricTokens":`jetons`,"pws.usageUnavailable":`Aucune utilisation enregistrée pour le moment.`,"pws.rateLimits":`Limites de débit`,"pws.quotaUnavailable":`Aucune donnée de quota pour ce fournisseur.`,"pws.accountQuotaUnavailable":`Données de limite de débit temporairement indisponibles ; affichage des dernières valeurs connues, le cas échéant.`,"pws.selected":`Sélectionné`,"pws.copyModelId":`Copier l’ID`,"pws.modelCopied":`Copié !`,"pws.modelsAvailable":`{count} disponibles`,"pws.modelSearchPlaceholder":`Filtrer les modèles…`,"pws.modelsLoading":`Chargement des modèles…`,"pws.modelsLoadFailed":`Impossible de charger les modèles.`,"pws.modelsNeedsReauth":`Le compte doit être reconnecté pour permettre la détection des modèles en direct. Affichage temporaire des modèles configurés.`,"pws.modelsConfiguredFallback":`Affichage des modèles configurés (détection en direct indisponible).`,"pws.modelsTruncated":`Affichage des {shown} premiers modèles sur {total}. Filtrez pour réduire la liste.`,"pws.retry":`Réessayer`,"pws.noModels":`Aucun modèle détecté pour ce fournisseur.`,"pws.noModelMatch":`Aucun modèle ne correspond au filtre.`,"pws.adapterBaseRequired":`L’adaptateur et l’URL de base sont requis.`,"pws.addAccount":`Ajouter un compte`,"pws.addKey":`Ajouter une clé API`,"pws.apiKeys":`Clés API`,"pws.authMode":`Mode d’authentification`,"pws.availableAccounts":`Comptes disponibles`,"pws.accountOrdinal":`Compte {count}`,"pws.accountsLoading":`Chargement des comptes…`,"pws.accountsLoadFailed":`Impossible de charger les comptes.`,"pws.retryAccounts":`Réessayer`,"pws.noAccounts":`Aucun compte connecté pour le moment.`,"pws.cockpitImportDescription":`Importez depuis cet appareil une exportation JSON Antigravity de Cockpit Tools. Le contenu du fichier n’est pas affiché.`,"pws.cockpitImportFileLabel":`Exportation JSON Antigravity de Cockpit Tools`,"pws.cockpitImportChooseFile":`Choisir un fichier JSON`,"pws.cockpitImporting":`Importation…`,"pws.cockpitImportInvalid":`Le fichier sélectionné n’est pas une exportation JSON valide ou est trop volumineux.`,"pws.cockpitImportFailed":`Impossible de terminer l’importation du compte.`,"pws.cockpitImportComplete":`Importation terminée : {imported} importés, {updated} mis à jour, {failed} en échec, {unsupported} non pris en charge.`,"pws.accountSwitching":`Changement…`,"pws.accountCurrent":`Compte actuel`,"pws.defaultModelNone":`Aucun (utiliser la valeur par défaut du fournisseur)`,"pws.discardSettings":`Abandonner les modifications`,"pws.jsonEditorDesc":`Modifiez la configuration JSON brute du fournisseur. Les modifications sont enregistrées immédiatement.`,"pws.jsonEditorTitle":`Éditeur JSON — {name}`,"pws.jsonRestore":`Restaurer`,"pws.jsonSave":`Enregistrer`,"pws.loggedInTitle":`Connecté`,"pws.notLoggedInTitle":`Non connecté`,"pws.note":`Note`,"pws.allowPrivateNetwork":`Autoriser le réseau local/privé`,"pws.liveModels":`Détecter les modèles auprès du fournisseur`,"pws.liveModelsDesc":`Récupérez le catalogue de modèles en direct du fournisseur. Désactivez cette option pour utiliser uniquement les modèles configurés/statiques.`,"pws.xaiResponsesOptIn":`Utiliser l’API Responses pour Grok 4.5 et 4.6`,"pws.xaiResponsesOptInDesc":`Achemine les deux modèles via openai-responses. Les autres modèles Grok et le comportement des tiers restent inchangés.`,"pws.xaiResponsesOptInMixed":`Activation partielle.`,"pws.cursorTransport":`Transport Cursor`,"pws.cursorTransportHttp2":`HTTP/2 (par défaut)`,"pws.cursorTransportHttp1":`HTTP/1.1 (compatibilité proxy)`,"pws.cursorTransportDesc":`Utilisez HTTP/1.1 si votre proxy ne transporte pas de façon fiable le flux HTTP/2 de Cursor.`,"pws.optionalPlaceholder":`Facultatif`,"pws.pacingTitle":`Cadencement des requêtes`,"pws.pacingDesc":`Répartissez uniformément le démarrage des requêtes sortantes pour ce fournisseur. Les réponses diffusées peuvent se chevaucher.`,"pws.pacingEnabled":`Activé`,"pws.pacingRpm":`Requêtes par minute`,"pws.pacingRpmUnit":`RPM`,"pws.pacingDelay":`Intervalle minimal (ms)`,"pws.pacingSlowerWins":`La limite la plus lente du fournisseur s’applique. Les remplacements par modèle ne peuvent qu’ajouter un délai.`,"pws.pacingQueued":`en attente`,"pws.pacingNextSlot":`avant le prochain créneau`,"pws.pacingLastModel":`dernier modèle`,"pws.pacingNone":`Aucun`,"pws.pacingModelOverrides":`Remplacements par modèle`,"pws.pacingModel":`Modèle`,"pws.pacingAdd":`Ajouter un remplacement`,"pws.pacingRemove":`Supprimer`,"pws.pacingRemoveModel":`Supprimer le remplacement du cadencement des requêtes pour {model}`,"pws.pacingRuleRequired":`Activez le cadencement des requêtes seulement après avoir défini une limite de fournisseur ou un remplacement par modèle.`,"pws.providerId":`ID du fournisseur`,"pws.reauth":`Réauthentification requise`,"pws.reauthenticate":`Réauthentifier`,"pws.copyDoctor":`Copier ocx doctor`,"pws.doctorCopied":`Copié`,"pws.doctorCopyUnavailable":`Presse-papiers indisponible`,"pws.healthCooldownHint":`Attendez la fin du délai de récupération. Ne sondez pas encore ce compte.`,"pws.healthLabel.rateLimited":`Débit limité`,"pws.healthLabel.quotaLimited":`Quota limité`,"pws.healthLabel.reauthRequired":`Réauthentification requise`,"pws.healthLabel.refreshFailed":`Actualisation échouée`,"pws.healthLabel.metadataMismatch":`Métadonnées incompatibles`,"pws.healthLabel.credentialConflict":`Conflit d’identifiants`,"pws.healthSummary.rateLimited":`{provider} {account} : débit limité jusqu’à {until}. Le routage de ce compte est suspendu jusque-là.`,"pws.healthSummary.quotaLimited":`{provider} {account} : quota limité jusqu’à {until}. Le routage de ce compte est suspendu jusque-là.`,"pws.healthSummary.reauthRequired":`{provider} {account} : réauthentification requise.`,"pws.healthSummary.credentialConflict":`{provider} {account} : conflit d’identifiants.`,"pws.healthSummary.metadataMismatch":`{provider} {account} : métadonnées incompatibles.`,"pws.healthSummary.staleCredentials":`{provider} {account} : identifiants incomplets.`,"pws.removeConfirm":`Supprimer`,"pws.removeConfirmBody":`Supprimer le fournisseur "{name}" ? Cette action est irréversible.`,"pws.removeDefaultConfirmBody":`Supprimer le fournisseur par défaut "{name}" ? "{defaultProvider}" deviendra le fournisseur par défaut. Cette action est irréversible.`,"pws.removeConfirmTitle":`Supprimer le fournisseur`,"pws.saveSettings":`Enregistrer`,"pws.saving":`Enregistrement…`,"pws.settingsSaved":`Paramètres enregistrés.`,"pws.accountModeSaved":`Mode de compte enregistré.`,"pws.accountModeFailed":`Impossible de changer de mode de compte.`,"pws.accountModeConfirm":`Changer le mode de compte OpenAI ? Les conversations en cours seront réaffectées à l’ensemble de comptes de l’autre mode, et l’utilisation du quota sera suivie selon le nouveau mode.`,"pws.settingsUnsavedBar":`Vous avez des modifications non enregistrées.`,"pws.unsavedLeaveBody":`Vous avez des modifications non enregistrées. Les enregistrer avant de quitter ?`,"pws.unsavedLeaveTitle":`Modifications non enregistrées`,"pws.attentionRequired":`Intervention requise`,"pws.attentionAria":`{name} : {reason}`,"pws.missingCredentials":`Identifiants manquants`,"pws.editJsonDesc":`Modifier la configuration brute du proxy au format JSON`,"pws.updatesUnavailable":`Les mises à jour du fournisseur ne sont pas disponibles.`,"pws.dashboard.title":`Vue d’ensemble des fournisseurs`,"pws.dashboard.subtitle":`Gérez tous vos fournisseurs de modèles au même endroit.`,"pws.dashboard.rateLimits":`LIMITES DE DÉBIT`,"pws.capacity.estimate":`Estimation du groupe pondérée selon la configuration`,"pws.capacity.currentAccount":`Compte effectif actuel`,"pws.capacity.nextRecovery":`Prochaine récupération de capacité`,"pws.capacity.recoveryShare":`+{percent}% de capacité du groupe`,"pws.capacity.incomplete":`Couverture incomplète : {excluded} compte(s) exclus`,"pws.capacity.uncalibratedPlan":`{count} compte(s) sur un forfait non calibré sont comptés au poids de siège de base ; cette estimation peut donc être prudente`,"pws.capacity.partial":`Couverture partielle des fenêtres : {count} compte(s) ne signalent pas toutes les fenêtres de limite affichées`,"pws.capacity.windowPartial":`Partielle`,"pws.capacity.windowPartialA11y":`{window} : couverture incomplète des comptes`,"pws.dashboard.recentlyUsed":`UTILISÉS RÉCEMMENT`,"pws.dashboard.requests":`{count} requêtes`,"pws.dashboard.checkedAgo":`Vérifié {time}`,"pws.dashboard.noQuota":`Aucune donnée de quota`,"pws.dashboard.noUsage":`Aucune donnée d’utilisation pour le moment`,"pws.dashboard.noRateLimits":`Aucune donnée de limite de débit pour le moment`,"pws.allProviders":`Vue d’ensemble des fournisseurs`,"pws.enabledLabel":`Activé`,"pws.testConnection":`Tester la connexion`,"pws.testing":`Test…`,"pws.connectionOk":`Connexion réussie`,"pws.connectionFailed":`Échec de la connexion`,"pws.connectionNotApplicable":`Sans objet — ce fournisseur utilise un catalogue de modèles statique.`,"pws.editSettings":`Modifier les paramètres`,"pws.viewUsage":`Afficher l’utilisation détaillée`,"pws.allSystemsOk":`Tous les systèmes sont opérationnels`,"pws.apiKeyConfigured":`Clé API configurée`,"pws.addApiKey":`Ajouter une clé API`,"pws.loggedInAs":`Connecté en tant que {email}`,"pws.notLoggedIn":`Non connecté`,"pws.passthrough":`Transfert direct Codex`,"pws.notes":`NOTES`,"pws.notePlaceholder":`Ajouter une note sur ce fournisseur…`,"pws.noteSaved":`Note enregistrée`,"pws.authSummary":`AUTHENTIFICATION`,"time.justNow":`À l’instant`,"time.notChecked":`Non vérifié`,"time.minutesAgo":`il y a {n} min`,"time.hoursAgo":`il y a {n} h`,"time.daysAgo":`il y a {n} j`,"modal.noMatch":`Aucun résultat.`,"modal.oauthDefaultNote":`Connectez-vous avec votre compte — aucune clé API requise.`,"modal.oauthComingSoon":`La connexion OAuth pour {label} sera disponible dans la prochaine mise à jour. Utilisez une clé API pour le moment.`,"modal.oauthComingSoonShort":`La connexion OAuth pour ce fournisseur sera disponible dans la prochaine mise à jour — utilisez une clé API pour le moment.`,"modal.useApiKeyInstead":`Utiliser plutôt une clé API`,"modal.setupGuide":`Guide de configuration`,"modal.setupStep1Prefix":`Accédez au`,"modal.setupDashboardLink":`tableau de bord {label}`,"modal.setupStep1Suffix":`et copiez votre clé API`,"modal.setupStep2":`Collez-la dans le champ de clé API ci-dessous`,"modal.setupStep3":`Cliquez sur Ajouter le fournisseur — les modèles sont détectés automatiquement`,"modal.namePlaceholder":`p. ex. openrouter`,"modal.duplicateWarn":`Le fournisseur "{name}" existe déjà et sera remplacé.`,"modal.forwardHintPrefix":`Aucune clé requise — le proxy transmet vos identifiants de`,"modal.forwardCredentials":`connexion Codex`,"modal.forwardHintSuffix":`à ce fournisseur.`,"modal.localHint":`Aucune clé API n’est stockée. Cette option ajoute le catalogue public statique de modèles Cursor pour Codex, mais le transport Cursor en direct et l’exécution native de fichiers/commandes restent désactivés jusqu’à leur audit.`,"modal.getApiKey":`Obtenir votre clé API {label}`,"modal.apiKey":`Clé API`,"modal.apiKeyTransport":`En-tête de clé API`,"modal.apiKeyTransportNative":`x-api-key (natif Anthropic)`,"modal.apiKeyTransportBearer":`Authorization: Bearer`,"modal.apiKeyPlaceholder":`sk-… (ou $ENV_VAR)`,"modal.defaultModelPlaceholder":`p. ex. gpt-5.5`,"modal.baseUrlPlaceholder":`https://...`,"modal.baseUrlPlaceholderError":`L’URL de base contient un {placeholder} non résolu. Remplacez-le par votre valeur réelle.`,"modal.baseUrlPlaceholderHint":`Remplacez le {placeholder} dans l’URL de base par votre ID de compte réel avant l’ajout.`,"modal.adding":`Ajout…`,"modal.useOauthLogin":`← Utiliser la connexion OAuth`,"nav.codexAuth":`Authentification Codex`,"nav.codexSet":`Réglages Codex`,"codexSet.tab.multiauth":`Multi-authentification`,"codexSet.tab.prompt":`Invite`,"codexSet.prompt.title":`Couches d'invite`,"codexSet.prompt.timing":`S'applique aux sessions nouvellement démarrées. Les sessions en cours conservent leurs réglages d'invite actuels.`,"codexSet.prompt.staleRevision":`La configuration a changé ailleurs. La liste a été rechargée.`,"codexSet.prompt.writeFailed":`La modification n'a pas pu être enregistrée.`,"codexSet.prompt.loadFailed":`Les couches d'invite n'ont pas pu être chargées.`,"codexSet.prompt.repair":`Réparer`,"codexSet.prompt.repairFailed":`La réparation n'a pas pu aboutir.`,"codexSet.drift.journalPresent":`Une écriture précédente ne s'est pas terminée. La récupération s'exécute automatiquement à la prochaine écriture.`,"codexSet.drift.projectionStale":`Les couches enregistrées et la valeur dans config.toml divergent. La réparation réécrit la valeur à partir de vos couches.`,"codexSet.drift.storeMissing":`Le fichier des couches a disparu alors que des instructions subsistent dans config.toml. La réparation crée d'abord une sauvegarde et conserve le texte en une seule couche.`,"codexSet.drift.ownedMalformed":`La ligne générée dans config.toml a été modifiée à la main ; la réécrire n'est plus sûr.`,"codexSet.custom.adoptUnsupported":`La valeur à {path} ligne {line} n'est pas une chaîne sur une seule ligne et ne peut pas être importée. Déplacez-la à la main pour la gérer ici.`,"codexSet.prompt.unreadable":`Le fichier de configuration Codex existe mais n'a pas pu être lu, les modifications sont donc refusées.`,"codexSet.layer.permissions":`Autorisations`,"codexSet.layer.collaboration":`Mode collaboration`,"codexSet.layer.environment":`Contexte d'environnement`,"codexSet.layer.apps":`Applications`,"codexSet.layer.skills":`Compétences`,"codexSet.prompt.extensionsUnknown":`Les extensions peuvent ajouter leurs propres couches. Codex ne les expose pas, elles ne peuvent donc pas être répertoriées ici.`,"codexSet.group.transition":`Avis de transition`,"codexSet.group.transitionDesc":`Ils signalent un changement au lieu de décrire un état : ils n'apparaissent donc qu'au passage en temps réel ou au changement de modèle.`,"codexSet.custom.slotNote":`Les couches personnalisées sont réunies en une seule section dans cet ordre.`,"codexSet.row.alwaysOn":`Toujours actif`,"codexSet.row.onChange":`Lors d'un changement`,"codexSet.row.featureGated":`Configuré sous [features]`,"codexSet.row.openFeatures":`Ouvrir les réglages`,"codexSet.dialog.setValue":`{value} (par défaut {fallback})`,"codexSet.dialog.copyKey":`Copier la clé`,"codexSet.dialog.unknownLayer":`Cette version ne contient pas de description pour cette couche. Elle provient d'un runtime Codex plus récent que le tableau de bord.`,"codexSet.custom.heading":`Couches personnalisées`,"codexSet.custom.add":`+ Ajouter une couche`,"codexSet.custom.newTitle":`Nouvelle couche`,"codexSet.custom.editTitle":`Modifier la couche`,"codexSet.custom.titleLabel":`Titre`,"codexSet.custom.bodyLabel":`Instructions`,"codexSet.custom.bodySize":`{bytes} octets sur {max}`,"codexSet.custom.normalized":`Les tabulations ont été remplacées par quatre espaces et les fins de ligne par LF.`,"codexSet.custom.titleRequired":`Saisissez un titre.`,"codexSet.custom.titleTooLong":`Le titre contient {count} caractères ; la limite est de {max}.`,"codexSet.custom.titleMultiline":`Le titre doit tenir sur une seule ligne.`,"codexSet.custom.bodyTooLarge":`Cette couche fait {bytes} octets ; la limite est de {max}.`,"codexSet.custom.composedTooLarge":`Les couches actives totaliseraient {bytes} octets, au-delà de la limite.`,"codexSet.custom.invalidCharacter":`Un caractère de contrôle à la position {position} ne peut pas être enregistré.`,"codexSet.custom.discardPrompt":`Abandonner vos modifications ?`,"codexSet.custom.keepEditing":`Continuer la modification`,"codexSet.custom.delete":`Supprimer {title}`,"codexSet.custom.deleteConfirm":`Supprimer cette couche ? Cette action est irréversible.`,"codexSet.custom.layerGone":`Cette couche a été supprimée ailleurs : l'éditeur a donc été fermé.`,"codexSet.custom.deleteConfirmNamed":`Supprimer « {title} » ? Cette action est irréversible.`,"codexSet.custom.moveUp":`Déplacer {title} vers le haut`,"codexSet.custom.prevLayer":`Couche précédente`,"codexSet.custom.nextLayer":`Couche suivante`,"codexSet.custom.navPosition":`{position} / {total}`,"codexSet.custom.moveDown":`Déplacer {title} vers le bas`,"codexSet.custom.limitReached":`Vous pouvez conserver jusqu’à {max} couches personnalisées.`,"codexSet.custom.notOwned":`developer_instructions a été écrit en dehors d’opencodex et ne peut donc pas être modifié ici. Importez-le pour le gérer comme une couche.`,"codexSet.custom.adopt":`Importer les instructions existantes`,"codexSet.custom.adoptConfirm":`Importer comme couche`,"codexSet.custom.adoptRefused":`La valeur existante n’a pas pu être importée.`,"codexSet.custom.baseReplaced":`model_instructions_file est défini sur {path} : un élément extérieur à opencodex a donc remplacé l’invite de base.`,"codexSet.lint.identity":`Ce texte revendique une identité différente de celle établie par Codex.`,"codexSet.lint.foreignTool":`Les outils proviennent du registre ; en nommer un ici ne le crée pas.`,"codexSet.lint.placeholder":`Aucun moteur de modèles ne traite les instructions ; ce texte sera donc envoyé tel quel.`,"codexSet.lint.applyPatch":`apply_patch est défini par le registre des outils, pas par les instructions.`,"codexSet.lint.approvalVocab":`Codex injecte son propre vocabulaire d’approbation ; ce texte peut le contredire.`,"codexSet.lint.environment":`Les informations d’environnement sont générées plus tard et peuvent contredire ce texte.`,"codexSet.lint.size":`Cette couche dépasse 8 KB. Elle peut être enregistrée, mais consomme des jetons à chaque requête.`,"codexSet.preset.blank":`Couche vide`,"codexSet.preset.concise.name":`Réponses concises`,"codexSet.preset.concise.description":`Réponses courtes, sans préambule et avec un minimum de mise en forme.`,"codexSet.preset.concise.provenance":`Adapté des consignes de concision de Claude Code. Formulation originale, pas une copie.`,"codexSet.preset.planFirst.name":`Planifier avant de modifier`,"codexSet.preset.planFirst.description":`Présenter le plan, puis effectuer la modification.`,"codexSet.preset.planFirst.provenance":`Adapté de l’approche de planification de Claude Code. Formulation originale, pas une copie.`,"codexSet.preset.explainWhy.name":`Expliquer le raisonnement`,"codexSet.preset.explainWhy.description":`Expliquer pourquoi, et pas seulement quoi.`,"codexSet.preset.explainWhy.provenance":`Adapté du style de confirmation de Grok Build. Formulation originale, pas une copie.`,"codexSet.preset.testFirst.name":`Tester d’abord`,"codexSet.preset.testFirst.description":`Écrire le test qui échoue avant d’apporter le correctif.`,"codexSet.preset.testFirst.provenance":`Adapté des pratiques courantes des agents. Formulation originale, pas une copie.`,"codexSet.preset.korean.name":`Réponses en coréen`,"codexSet.preset.korean.description":`Répondre en coréen, quelle que soit la langue de la requête.`,"codexSet.preset.korean.provenance":`Rédigé pour opencodex à partir d'une demande fréquente des utilisateurs. Formulation originale, pas une copie.`,"codexSet.dialog.class":`Type`,"codexSet.dialog.key":`Clé de configuration`,"codexSet.dialog.fileValue":`Valeur dans ce fichier`,"codexSet.dialog.absentDefault":`non défini (valeur par défaut : {value})`,"codexSet.dialog.noRenderedText":`Codex n’expose pas le texte assemblé d’une couche intégrée. Cette boîte de dialogue décrit donc la couche et indique sa clé au lieu d’afficher son contenu.`,"codexSet.dialog.sourceText":`Texte envoyé au modèle`,"codexSet.dialog.sourceBytes":`{bytes} octets`,"codexSet.dialog.notRendered":`Lors du tour que nous avons lu, cette couche n'a rien envoyé. Les sections ne sont renvoyées qu'en cas de changement : un seul échantillon peut donc ne pas la contenir.`,"codexSet.dialog.emptySource":`Le fichier {path} existe mais il est vide : cette couche n'envoie donc rien.`,"codexSet.dialog.notExposed":`L'invite de base circule en dehors de la liste de messages que Codex peut afficher ; elle ne peut donc pas être montrée ici. On peut la remplacer via model_instructions_file.`,"codexSet.dialog.textUnavailable":`L'invite Codex n'a pas pu être lue sur cette machine, le texte est donc indisponible.`,"codexSet.class.base":`Instructions de base`,"codexSet.class.config-toggle":`Modifiable ici`,"codexSet.class.feature-gated":`Contrôlé par une fonctionnalité`,"codexSet.class.runtime-conditional":`Conditionnel à l’exécution`,"codexSet.class.extension-unknown":`Couche d’extension`,"codexSet.layer.base-instructions":`Instructions de base`,"codexSet.layer.model-switch":`Avis de changement de modèle`,"codexSet.layer.personality":`Personnalité`,"codexSet.layer.context-window-guidance":`Conseils sur la fenêtre de contexte`,"codexSet.layer.realtime":`Temps réel`,"codexSet.layer.agents-md":`AGENTS.md`,"codexSet.layer.environments-instructions":`Environnements`,"codexSet.layer.plugins":`Plugins`,"codexSet.layer.tools":`Outils`,"codexSet.layer.multi-agent-mode":`Mode multi-agents`,"codexSet.layer.git-attribution":`Attribution des commits`,"codexSet.about.base-instructions":`Instructions propres à Codex. Elles accompagnent la requête elle-même et ne peuvent pas être désactivées.`,"codexSet.about.model-switch":`Ajouté lorsque le modèle change en cours de conversation.`,"codexSet.about.personality":`Consignes de ton et de style, régies par un indicateur de fonctionnalité.`,"codexSet.about.context-window-guidance":`Conseils sur le budget de contexte restant, régis par un indicateur de fonctionnalité.`,"codexSet.about.realtime":`Ajouté aux sessions en temps réel.`,"codexSet.about.agents-md":`Les fichiers AGENTS.md de votre projet. Cette page indique la couche, mais ne modifie jamais la documentation du projet.`,"codexSet.about.permissions":`Décrit les réglages actifs du bac à sable et des approbations.`,"codexSet.about.collaboration":`Décrit le mode collaboration actif.`,"codexSet.about.environment":`Répertoire de travail, plateforme et autres informations d’environnement.`,"codexSet.about.environments-instructions":`Consignes pour les environnements d’exécution différée, régies par un indicateur de fonctionnalité.`,"codexSet.about.apps":`Utilisation des applications connectées.`,"codexSet.about.plugins":`Ajouté lorsqu’un plugin est sélectionné ou qu’un plugin déclare une capacité.`,"codexSet.about.tools":`Descriptions différées des outils, régies par un indicateur de fonctionnalité.`,"codexSet.about.skills":`Liste des compétences disponibles.`,"codexSet.about.multi-agent-mode":`Instructions pour les sous-agents, régies par un indicateur de fonctionnalité.`,"codexSet.about.git-attribution":`Demande au modèle d’ajouter un trailer Co-authored-by: Codex aux commits qu’il écrit, et une ligne Generated with Codex. aux pull requests qu’il ouvre. Codex lit ce réglage depuis votre compte : il n’est modifiable ni ici ni dans [features]. Si votre compte le désactive, Codex envoie l’instruction inverse au lieu de ne rien envoyer.`,"codexSet.condition.model-switch":`Émis uniquement après un changement de modèle en cours de session.`,"codexSet.condition.realtime":`Émis uniquement dans une session en temps réel.`,"codexSet.condition.agents-md":`Émis lorsqu’un document de projet est trouvé pour le répertoire de travail.`,"codexSet.condition.plugins":`Émis lorsqu’un plugin est sélectionné ou qu’un plugin déclare une capacité.`,"codexSet.condition.git-attribution":`Défini par la politique d’attribution de votre compte.`,"codexSet.base.title":`Invite de base`,"codexSet.base.prev":`Option précédente`,"codexSet.base.next":`Option suivante`,"codexSet.base.position":`{position} / {total}`,"codexSet.base.swipeHint":`Balayez latéralement, utilisez les touches fléchées ou les boutons pour changer d’option. S’applique aux sessions démarrées ensuite.`,"codexSet.base.defaultTitle":`Invite de base propre à Codex`,"codexSet.base.defaultBody":`L’option par défaut n’est pas stockée ici : il n’y a donc rien à modifier ni à supprimer. La choisir retire simplement model_instructions_file de votre configuration, et Codex utilise l’invite qu’il fournit.`,"codexSet.base.variantTitle":`Nom`,"codexSet.base.variantBody":`Invite`,"codexSet.base.replacesWarning":`Ceci REMPLACE l’invite de base de Codex au lieu de s’y ajouter. Une invite courte ici donne un modèle avec des instructions courtes.`,"codexSet.base.use":`Utiliser celle-ci`,"codexSet.base.inUse":`Utilisée`,"codexSet.base.externalBlocked":`model_instructions_file pointe déjà vers {path}, une valeur qu’opencodex n’a pas écrite. Retirez-la vous-même avant de choisir ici.`,"nav.api":`API`,"nav.integrations":`Intégrations`,"nav.openMenu":`Ouvrir le menu`,"nav.closeMenu":`Fermer le menu`,"integrations.subtitle":`Connectez des clients à opencodex, gérez les identifiants et restaurez la configuration des clients.`,"integrations.tabsLabel":`Surfaces d’intégration`,"integrations.tab.overview":`Vue d’ensemble`,"integrations.tab.keys":`Clés API`,"integrations.tab.codex":`Codex`,"integrations.tab.claude":`Claude`,"integrations.tab.grok":`Grok Build`,"integrations.tab.opencode":`OpenCode`,"integrations.tab.pi":`Pi`,"integrations.tab.omp":`OMP`,"integrations.tab.hermes":`Hermes`,"integrations.tab.openclaw":`OpenClaw`,"integrations.tab.kimi":`Kimi Code`,"integrations.tab.gajae":`Gajae Code`,"integrations.tab.dsh":`DSH`,"integrations.tab.mcode":`MiniMax Code`,"integrations.tab.zcode":`ZCode`,"integrations.tab.prime":`Prime Agent`,"integrations.tab.aside":`Aside`,"integrations.codex.title":`Codex CLI`,"integrations.codex.body":`Le câblage de Codex est géré par le service proxy. Le démarrage d’opencodex l’applique ; l’arrêt du service rétablit le routage natif.`,"integrations.codex.openService":`Ouvrir les commandes du service`,"integrations.state.notInstalled":`Non installé`,"integrations.state.unknown":`Vérification…`,"integrations.detail.codexRouted":`Les requêtes Codex passent par ce proxy`,"integrations.detail.codexAbsent":`Codex ne passe pas encore par ce proxy`,"integrations.detail.keyCount":`{count} clé(s) émises`,"integrations.detail.keyNone":`Aucune clé émise`,"integrations.detail.keyChecking":`Vérification…`,"integrations.detail.keyUnavailable":`État des clés indisponible`,"integrations.detail.claudeOff":`La connexion est désactivée`,"integrations.detail.desktopCurrent":`Desktop exécute ce profil`,"integrations.detail.desktopStale":`Le fichier de profil a été modifié après son application`,"integrations.detail.desktopNotServed":`Le profil existe, mais Desktop en utilise un autre`,"integrations.detail.desktopAbsent":`Aucun profil appliqué`,"integrations.detail.desktopDesiredOff":`L’intégration Claude Desktop est désactivée`,"integrations.detail.desktopDesiredOffCleanupPending":`Claude Desktop utilise encore la passerelle ; le nettoyage est en attente`,"integrations.detail.desktopDesiredOnNotApplied":`L’intégration est activée, mais Desktop n’utilise pas le profil de passerelle`,"integrations.detail.desktopSelectedElsewhere":`Desktop utilise un autre profil`,"integrations.detail.desktopProfileDrift":`Le profil Desktop sélectionné a changé`,"integrations.detail.desktopObservedUnsafe":`Le profil Desktop sélectionné ne peut pas être modifié en toute sécurité`,"integrations.detail.desktopNotInstalled":`La bibliothèque de configuration de Claude Desktop n’est pas installée`,"integrations.detail.grokModels":`{count} modèle(s) câblés`,"integrations.detail.grokAbsent":`Aucun bloc opencodex dans la configuration`,"integrations.dialog.grok.title":`Désactiver l’intégration Grok Build ?`,"integrations.dialog.grok.changes":`Seul le bloc marqué par opencodex sera supprimé de {path}. Le contenu écrit en dehors du bloc restera inchangé.`,"integrations.dialog.grok.breakage":`La désactivation supprime les alias de modèles opencodex de Grok Build. Les modèles utilisés avec votre compte xAI restent disponibles.`,"integrations.dialog.grok.undo":`Si opencodex s’exécute sur une adresse de bouclage, la réactivation écrit un nouveau bloc à partir des modèles actuellement disponibles.`,"integrations.dialog.grok.confirm":`Désactiver`,"integrations.dialog.desktop.title":`Désactiver l’intégration Claude Desktop ?`,"integrations.dialog.desktop.changes":`Si {path} contient un profil de passerelle géré par opencodex, Desktop sélectionnera d’abord un nouveau profil standard sans identifiants, puis supprimera l’ancien profil et sa sauvegarde.`,"integrations.dialog.desktop.breakage":`Claude Desktop reviendra à Claude standard au lieu des modèles acheminés par opencodex.`,"integrations.dialog.desktop.undo":`La réactivation régénère le profil opencodex à partir de vos affectations de modèles enregistrées.`,"integrations.dialog.desktop.restart":`Claude Desktop lit cette configuration uniquement au lancement. Quittez-le complètement, puis rouvrez-le pour appliquer cette modification.`,"integrations.dialog.desktop.confirm":`Désactiver`,"integrations.native.msg.nonLoopbackRemoved":`Grok Build ne peut être enregistré automatiquement que lorsqu’opencodex s’exécute sur une adresse de bouclage. Le bloc précédent qui pointait vers le bouclage a été supprimé.`,"integrations.native.msg.nonLoopbackRemovedNoop":`Grok Build ne peut être enregistré automatiquement que lorsqu’opencodex s’exécute sur une adresse de bouclage. Aucun bloc précédent n’était à supprimer.`,"integrations.native.msg.nonLoopbackSuperseded":`Grok Build ne peut être enregistré automatiquement que lorsqu’opencodex s’exécute sur une adresse de bouclage. Un autre processus a écrit un nouveau bloc entre-temps ; le bloc désormais présent dans le fichier n’a donc pas été créé par cette requête.`,"integrations.native.error.orphanedMarker":`{path} contient un marqueur de début opencodex, mais aucun marqueur de fin. Le fichier est resté inchangé, car opencodex ne peut pas déterminer où se termine son bloc.`,"integrations.native.error.homeMismatch":`Le répertoire d’accueil du service installé ne correspond pas au répertoire actuel ; le fichier est donc resté inchangé.`,"integrations.native.error.notInstalled":`Grok Build n’est pas installé ; il n’y a donc rien à modifier.`,"integrations.native.error.configBusy":`La configuration est en cours d’enregistrement ailleurs et n’a pas pu être modifiée. Réessayez dans un instant.`,"integrations.native.error.desktopUnsafeMetadata":`Les métadonnées de Claude Desktop dans {path} n’ont pas pu être lues en toute sécurité ; sa bibliothèque n’a donc pas été modifiée.`,"integrations.native.error.desktopCleanupIncomplete":`Claude Desktop pointe vers le mode standard, mais d’anciens fichiers d’identifiants opencodex subsistent ici : {paths}.`,"integrations.native.msg.desktopDisabled":`Intégration Claude Desktop désactivée.`,"integrations.native.msg.desktopEnabled":`Intégration Claude Desktop activée.`,"integrations.state.absent":`Non appliqué`,"integrations.state.current":`Appliqué`,"integrations.state.stale":`Mise à jour requise`,"integrations.state.conflict":`Conflit`,"integrations.state.unsafe":`Vérification impossible`,"integrations.summary.detected":`Clients détectés`,"integrations.summary.applied":`Clients configurés`,"integrations.summary.stale":`Mise à jour requise`,"integrations.summary.lastChange":`Dernière modification`,"integrations.summary.disableAll":`Tout désactiver…`,"integrations.onboarding":`L’application écrit un bloc de fournisseur opencodex après avoir créé une sauvegarde. La désactivation supprime uniquement ce bloc, et un instantané conservé peut être restauré.`,"integrations.empty.title":`Aucun client installé n’a été détecté`,"integrations.empty.body":`Installez un client pris en charge, puis revenez ici pour appliquer opencodex.`,"integrations.action.apply":`Appliquer`,"integrations.action.disable":`Désactiver`,"integrations.action.refresh":`Mettre à jour`,"integrations.action.settings":`Paramètres`,"integrations.action.manageKeys":`Gérer les clés`,"integrations.action.restore":`Restaurer…`,"integrations.action.undo":`Annuler`,"integrations.action.restorePoint":`Restaurer ce point…`,"integrations.action.snapshotExpired":`Sauvegarde expirée`,"integrations.rollback.title":`Centre de restauration`,"integrations.rollback.empty":`Aucun historique d’application`,"integrations.rollback.emptyBody":`Chaque écriture réussie conserve d’abord un instantané antérieur.`,"integrations.catalog.title":`Clients`,"integrations.rollback.older":`Opérations antérieures`,"integrations.rollback.showMore":`Afficher {n} de plus`,"integrations.rollback.failed":`Impossible de charger l’historique des restaurations.`,"integrations.restore.title":`Restaurer cet instantané ?`,"integrations.restore.body":`Le fichier actuel est d’abord sauvegardé, puis remplacé par l’instantané sélectionné.`,"integrations.restore.driftTitle":`Des modifications plus récentes ont été détectées`,"integrations.restore.driftBody":`Les modifications apportées après cet instantané seront sauvegardées, puis le fichier sera remplacé.`,"integrations.restore.confirm":`Restaurer`,"integrations.restore.confirmDrift":`Sauvegarder les modifications récentes et restaurer`,"integrations.restore.pending":`Restauration…`,"integrations.restore.manual":`Échec de la restauration automatique : {reason}. Restaurez manuellement depuis {path}.`,"integrations.error.load":`Impossible de charger l’état de l’intégration.`,"integrations.error.stale":`La dernière actualisation a échoué. Les valeurs ci-dessous peuvent être obsolètes.`,"integrations.error.busy":`Une autre modification de ce client est toujours en cours. Réessayez dans un instant.`,"integrations.error.conflict":`La configuration a changé après son écriture par opencodex. Rien n’a été supprimé.`,"integrations.error.unsafe":`La configuration ne peut pas être modifiée en toute sécurité.`,"integrations.error.generic":`La modification de l’intégration a échoué. Votre état précédent a été conservé.`,"integrations.error.nonLoopback":`{client} ne peut atteindre qu’un proxy sur localhost — sa configuration ne permet pas d’ajouter l’en-tête d’admission requis par une liaison distante ; l’écrire manuellement ne servirait donc à rien. Donnez-lui plutôt un accès par bouclage, via un tunnel ou un redirecteur local.`,"integrations.status.installed":`Installé`,"integrations.status.notInstalled":`Non installé`,"integrations.status.appliedAt":`Appliqué`,"integrations.status.backup":`Sauvegarde`,"integrations.status.lastRestore":`Dernière restauration`,"integrations.status.unknown":`Inconnu`,"integrations.bulk.title":`Désactiver les intégrations client appliquées ?`,"integrations.bulk.body":`Seul le bloc appartenant à opencodex est supprimé. Un instantané antérieur est conservé pour chaque client.`,"integrations.bulk.partial":`Certains clients n’ont pas pu être désactivés : {clients}`,"integrations.bulk.success":`Les intégrations client appliquées ont été désactivées.`,"integrations.retention.degraded":`Le nettoyage des sauvegardes est en retard ; d’anciennes sauvegardes peuvent encore se trouver sur le disque.`,"integrations.error.residual":`Le fichier peut être dans un état intermédiaire : {message} Restaurez-le depuis {path}.`,"integrations.error.recover":`{message} Une sauvegarde se trouve dans {path}.`,"integrations.kind.apply":`Appliqué`,"integrations.kind.disable":`Désactivé`,"integrations.kind.refresh":`Mis à jour`,"integrations.kind.restore":`Restauré`,"integrations.kind.overwrite":`Écrasé`,"integrations.dialog.overwrite.title":`Remplacer le bloc dans cette configuration ?`,"integrations.dialog.overwrite.changesUnowned":`Dans {path}, un bloc que nous n'avons pas écrit occupe l'emplacement dont opencodex a besoin. L'application le remplace par le bloc qu'écrirait opencodex.`,"integrations.dialog.overwrite.changesForeign":`Votre modification dans le bloc opencodex de {path} sera abandonnée et remplacée par le bloc qu'écrirait opencodex.`,"integrations.dialog.overwrite.breakage":`Ce que configurait l'autre bloc cesse de s'appliquer. Le reste du fichier n'est pas touché.`,"integrations.dialog.overwrite.undo":`Un instantané est enregistré au préalable : cette opération apparaît dans la liste de restauration ci-dessous et peut être annulée.`,"integrations.dialog.overwrite.confirm":`Remplacer`,"integrations.action.overwrite":`Remplacer`,"integrations.semantics.opencode":`Uniquement pour les lancements directs depuis le disque ; l’injection d’environnement par ocx opencode est prioritaire.`,"integrations.semantics.pi":`S’applique aux nouvelles sessions.`,"integrations.semantics.omp":`Redémarrez OMP pour charger le catalogue.`,"integrations.semantics.hermes":`S’applique aux nouvelles sessions.`,"integrations.semantics.openclaw":`S’applique immédiatement à une passerelle en cours d’exécution.`,"integrations.semantics.kimi":`Redémarrez ou exécutez /reload pour l’appliquer (la v2 surveille le fichier).`,"integrations.semantics.gajae":`S’applique à une nouvelle session ou à l’ouverture de /model.`,"integrations.semantics.dsh":`OpenCodex gère uniquement llm-pi-ai.providers.opencodex dans $DSH_HOME/settings.yaml. DSH recharge ce fournisseur à chaud ; votre modèle par défaut et deepseek-official restent inchangés. Seule l’adresse de bouclage est actuellement prise en charge ; aucun identifiant réel n’est écrit.`,"integrations.semantics.mcode":`Gère uniquement custom_provider.opencodex. Votre modèle par défaut et votre connexion MiniMax restent inchangés.`,"integrations.semantics.zcode":`Gère uniquement provider.opencodex dans ~/.zcode/v2/config.json. Votre connexion Z.ai et les autres fournisseurs restent inchangés. Redémarrez ZCode après toute modification.`,"integrations.semantics.prime":`Gère uniquement providers.opencodex dans le models.json de Prime Agent — ~/.prime/agent, sauf si PRIME_AGENT_CODING_AGENT_DIR le redirige. Vos autres fournisseurs et surcharges de modèles restent inchangés. S'applique aux nouvelles sessions.`,"integrations.semantics.aside":`Gère uniquement providers.opencodex dans le models.json d'Aside pour le compte connecté (~/.aside/u/). Vos autres fournisseurs restent inchangés. Aside réécrit ce fichier pendant son exécution : quittez-le complètement et relancez-le après application.`,"codexAuth.mainAccount":`Compte principal`,"codexAuth.logLabel":`Libellé du journal`,"codexAuth.codexApp":`Application Codex`,"codexAuth.moreActions":`Afficher plus d’actions`,"codexAuth.copyId":`Copier l’ID du compte`,"codexAuth.appLogin":`Connexion à l’application`,"codexAuth.accountPool":`Groupe de comptes`,"codexAuth.accountModeTitle":`Mode de compte OpenAI`,"codexAuth.accountModePool":`Mode Groupe`,"codexAuth.accountModePoolDesc":`La connexion principale et les comptes ajoutés admissibles alternent ici.`,"codexAuth.accountModeDirect":`Mode Direct`,"codexAuth.accountModeDirectDesc":`Les requêtes utilisent uniquement la connexion principale ; les comptes ajoutés restent stockés pour le mode Groupe.`,"codexAuth.openaiMissing":`Le fournisseur OpenAI intégré n’est pas configuré.`,"codexAuth.openaiDisabled":`Le fournisseur OpenAI intégré est désactivé.`,"codexAuth.openaiUnavailableDesc":`Vos comptes OpenAI restent disponibles. Activez le fournisseur pour acheminer les requêtes Codex.`,"codexAuth.enableOpenai":`Activer OpenAI`,"codexAuth.enablingOpenai":`Activation…`,"codexAuth.enableOpenaiFailed":`Échec de l’activation du fournisseur OpenAI.`,"codexAuth.openaiPresetLoadFailed":`Échec du chargement du préréglage du fournisseur OpenAI.`,"codexAuth.openaiPresetUnavailable":`Le préréglage du fournisseur OpenAI est indisponible.`,"codexAuth.openProviders":`Ouvrir Fournisseurs`,"codexAuth.add":`Ajouter`,"codexAuth.sparkQuota":`Quota Codex Spark`,"codexAuth.sparkQuotaHint":`Affiche la fenêtre hebdomadaire GPT-5.3-Codex-Spark sur les cartes de compte. Masquée par défaut car elle ne concerne qu'un seul modèle.`,"codexAuth.sparkQuotaShown":`Quota Codex Spark affiché`,"codexAuth.sparkQuotaHidden":`Quota Codex Spark masqué`,"codexAuth.sparkQuotaFailed":`Impossible de modifier le réglage du quota Codex Spark`,"codexAuth.refreshQuota":`Actualiser les quotas`,"codexAuth.refreshingQuota":`Actualisation…`,"codexAuth.quotaRefreshed":`Quotas actualisés`,"codexAuth.quotaRefreshFailed":`Échec de l’actualisation des quotas`,"codexAuth.pauseExhausted":`Suspendre les comptes épuisés`,"codexAuth.pausingExhausted":`Vérification des quotas…`,"codexAuth.pauseExhaustedSucceeded":`Comptes à la limite suspendus : {count}`,"codexAuth.pauseExhaustedNone":`Aucun compte n’a une utilisation confirmée à 100 %.`,"codexAuth.pauseExhaustedFailed":`Échec de la vérification et de la suspension des comptes épuisés.`,"codexAuth.noPool":`Aucun compte ajouté au groupe pour le moment.`,"codexAuth.pause":`Suspendre`,"codexAuth.resume":`Reprendre`,"codexAuth.paused":`SUSPENDU`,"codexAuth.pauseSucceeded":`{email} est suspendu`,"codexAuth.resumeSucceeded":`{email} est de nouveau disponible dans le groupe`,"codexAuth.pauseFailed":`Impossible de suspendre {email}. Aucune modification apportée.`,"codexAuth.resumeFailed":`Impossible de réactiver {email}. Aucune modification apportée.`,"codexAuth.pausedHint":`Exclu du changement automatique, des nouvelles tentatives, de la récupération après délai et de la sélection manuelle jusqu’à sa réactivation.`,"codexAuth.pinned":`ÉPINGLÉ`,"codexAuth.pinnedHint":`Vous avez sélectionné ce compte manuellement ; un ordre de sélection supérieur ne le remplacera donc pas. L’épinglage dure jusqu’à l’épuisement de ce compte, la sélection d’un autre compte ou la modification d’un ordre de sélection.`,"codexAuth.fiveHour":`5 h`,"codexAuth.weekly":`Semaine`,"codexAuth.monthly":`30 j`,"codexAuth.resets":`réinitialisation`,"codexAuth.today":`Aujourd’hui`,"codexAuth.current":`ACTUEL`,"codexAuth.nextSession":`SÉLECTIONNÉ`,"codexAuth.poolPrepared":`PRÉPARÉ POUR LE GROUPE`,"codexAuth.preparePoolTitle":`Préparer ce compte pour le mode Groupe ?`,"codexAuth.preparePoolDesc":`Les requêtes directes continuent d’utiliser la connexion principale. Ce compte devient la sélection préparée du Groupe lorsque le mode Groupe est activé.`,"codexAuth.prepareForPool":`Préparer pour le Groupe`,"codexAuth.poolPreparedToast":`{email} est préparé pour le mode Groupe`,"codexAuth.switchTitle":`Changer de compte actif ?`,"codexAuth.switchDesc":`Prend effet immédiatement. Les fils liés à un compte et les requêtes déjà en cours conservent le compte capturé ; les requêtes nouvelles ou non liées utilisent le niveau d’ordre du compte sélectionné, et les comptes de même ordre continuent d’alterner.`,"codexAuth.cacheWarning":`Le cache des prompts est réinitialisé lors d’un changement de compte. La nouvelle session démarre avec un cache vide.`,"codexAuth.setAsNext":`Utiliser ensuite ce compte`,"codexAuth.cancel":`Annuler`,"codexAuth.switchBack":`Revenir au compte principal ?`,"codexAuth.switchBackDesc":`Prend effet immédiatement. Les fils liés à un compte et les requêtes déjà en cours conservent le compte capturé ; les requêtes nouvelles ou non liées utilisent le niveau d’ordre du compte de connexion à l’application, et les comptes de même ordre continuent d’alterner.`,"codexAuth.autoSwitch":`Changement proactif selon l’utilisation`,"codexAuth.autoSwitchQuotaDesc":`Quota : à partir de {threshold}% d’utilisation, la requête suivante peut passer à un compte admissible moins utilisé, y compris pour une tâche déjà liée ; Go/Free utilisent uniquement 30 j.`,"codexAuth.autoSwitchQuotaOffDesc":`Le changement proactif selon l’utilisation est désactivé. L’affectation nouvelle/non liée et la récupération après échec restent actives.`,"codexAuth.autoSwitchRoundRobinDesc":`L’affectation en rotation n’utilise pas ce seuil ; elle continue d’alterner les tâches nouvelles/non liées.`,"codexAuth.autoSwitchFillFirstDesc":`Remplissage prioritaire : {threshold}% est le seuil d’épuisement pour les tâches nouvelles/non liées ; les tâches liées saines conservent leur compte.`,"codexAuth.autoSwitchFillFirstOffDesc":`Le remplissage prioritaire n’a aucun seuil d’épuisement lié à l’utilisation pour les tâches nouvelles/non liées ; le délai de récupération, la réauthentification et la récupération après échec peuvent toujours modifier le routage.`,"codexAuth.failureRecoveryNote":`La récupération après échec est distincte : une requête rejetée avant toute sortie avec 429/402, un délai de récupération, une réauthentification, une exclusion ou un basculement transitoire configuré peut sélectionner un autre compte admissible.`,"codexAuth.autoSwitchThreshold":`Seuil d’utilisation`,"codexAuth.autoSwitchThresholdAria":`Seuil d’utilisation, en pourcentage`,"codexAuth.autoSwitchThresholdInc":`Augmenter le seuil d’utilisation`,"codexAuth.autoSwitchThresholdDec":`Diminuer le seuil d’utilisation`,"codexAuth.autoSwitchLoadFailed":`Impossible de charger le paramètre de changement selon l’utilisation.`,"codexAuth.autoSwitchThresholdInvalid":`Saisissez un nombre entier compris entre 1 et 100`,"codexAuth.autoSwitchUpdated":`Changement proactif selon l’utilisation mis à jour`,"codexAuth.autoSwitchUpdateFailed":`Impossible de confirmer la mise à jour du changement selon l’utilisation. La dernière valeur confirmée est affichée.`,"codexAuth.requestUserInput":`Demander une saisie en mode Default`,"codexAuth.requestUserInputDesc":`Permet à Codex de suspendre une session en mode Default et de vous poser des questions avec l’outil request_user_input.`,"codexAuth.requestUserInputUpdated":`Indicateur de fonctionnalité mis à jour — s’applique aux nouvelles sessions.`,"codexAuth.requestUserInputUpdatedRestart":`Indicateur de fonctionnalité mis à jour — s’applique aux nouvelles sessions. Redémarrez l’application Codex pour le prendre en compte.`,"codexAuth.requestUserInputUpdateFailed":`Impossible de mettre à jour l’indicateur de fonctionnalité. Aucune modification apportée.`,"codexAuth.requestUserInputLoadFailed":`Impossible de lire l’indicateur de fonctionnalité dans config.toml.`,"codexAuth.accountPickerTitle":`Cibler un compte Codex précis depuis le sélecteur de modèle`,"codexAuth.accountPickerOffDesc":`Lorsque cette option est activée, les lignes GPT ordinaires du sélecteur sont remplacées par une entrée pour chaque sélecteur de compte. Vous pouvez ainsi choisir le compte exact d’une conversation sans vous déconnecter. La désactivation ne supprime aucun compte.`,"codexAuth.accountPickerOnDesc":`Chaque sélecteur est un libellé public associé à un compte stocké. Le choisir verrouille la conversation sur le compte correspondant : elle n’alterne jamais et ne bascule pas vers un compte de secours, sans modifier le compte Groupe actif.`,"codexAuth.accountPickerCompatibility":`La connexion intégrée à l’application Codex possède son propre sélecteur ; les mappages générés l’appellent normalement main et utilisent au besoin un suffixe évitant les collisions, tel que main-2. Les comptes ajoutés reçoivent des libellés stables qui préservent la confidentialité, tandis que les libellés personnalisés restent inchangés. Le routage des conversations existantes et des sélections de modèles enregistrées se poursuit. La désactivation masque les entrées générées, mais préserve les sélecteurs et les routes exactes. Les ID de modèle GPT simples conservent leur comportement Groupe ou Direct.`,"codexAuth.accountPickerUpdated":`Ciblage des comptes mis à jour.`,"codexAuth.accountPickerUpdateFailed":`Impossible de mettre à jour le ciblage des comptes. Le dernier paramètre confirmé est affiché.`,"codexAuth.accountPickerLoadFailed":`Impossible de charger le paramètre de ciblage des comptes.`,"codexAuth.accountPickerRefreshFailed":`Impossible d’actualiser ce paramètre. La dernière valeur confirmée reste affichée.`,"codexAuth.advancedSettings":`Paramètres avancés`,"codexAuth.advancedSettingsAria":`Afficher ou masquer les paramètres avancés de l’authentification Codex`,"codexAuth.catalogRefreshPending":`La modification a été enregistrée, mais l’actualisation du catalogue de modèles Codex est en attente. Exécutez ocx sync pour réessayer.`,"anthropicPool.title":`Groupe de comptes Claude (expérimental)`,"anthropicPool.enabledDesc":`En cas de 429, met le compte en délai de récupération et bascule vers un autre. Les nouvelles sessions privilégient une utilisation inférieure à {threshold}% ({window}).`,"anthropicPool.enabledNoProactiveDesc":`En cas de 429, met le compte en délai de récupération et bascule. Le basculement proactif basé sur l'usage est désactivé au seuil 0, mais la sélection des nouvelles sessions et la récupération après 429 utilisent toujours la fenêtre {window}.`,"anthropicPool.disabledDesc":`Utilise uniquement le compte Claude actif. Activez cette option seulement si vous acceptez le routage expérimental.`,"anthropicPool.experimentalWarning":`Fonctionnalité expérimentale et peu éprouvée. Anthropic peut restreindre les comptes présentant une rotation multicomptes automatisée. Les comptes d’une même organisation peuvent partager un quota — leur mise en groupe n’apportera rien. Laissez cette option désactivée si vous n’en comprenez pas les risques.`,"anthropicPool.needTwoAccounts":`Ajoutez au moins deux comptes OAuth Claude avant d’activer le groupe.`,"anthropicPool.threshold":`Seuil d’utilisation des nouvelles sessions`,"anthropicPool.thresholdAria":`Seuil d’utilisation des nouvelles sessions, en pourcentage`,"anthropicPool.thresholdHelp":`0 désactive la sélection selon le quota (affinité + compte actif uniquement). Valeur par défaut : 80.`,"anthropicPool.thresholdInvalid":`Saisissez un nombre entier compris entre 0 et 100`,"anthropicPool.loadFailed":`Impossible de charger les paramètres du groupe Claude.`,"anthropicPool.saveFailed":`Impossible d’enregistrer les paramètres du groupe Claude.`,"anthropicPool.on":`Activé`,"anthropicPool.off":`Désactivé`,"accountPool.strategy":`Stratégie de rotation`,"accountPool.strategyDesc":`Méthode utilisée par OpenCodex pour affecter un compte à une tâche nouvelle/non liée.`,"accountPool.strategyQuota":`Quota`,"accountPool.strategyRoundRobin":`Rotation`,"accountPool.strategyFillFirst":`Remplissage prioritaire`,"accountPool.strategyHintQuota":`La stratégie Quota peut également relier une tâche existante à un autre compte lors de sa requête suivante, une fois le seuil d’utilisation franchi.`,"accountPool.strategyHintRoundRobin":`La rotation ne concerne que les tâches sans liaison active ; le seuil d’utilisation ne modifie pas la rotation normale.`,"accountPool.strategyHintFillFirst":`Le remplissage prioritaire utilise le seuil comme point d’épuisement pour les tâches non liées ; les tâches liées saines conservent leur affinité.`,"accountPool.unboundDefinition":`Une tâche nouvelle/non liée désigne une requête sans liaison actuelle à un compte ; une tâche existante visible peut devenir non liée après la réinitialisation du proxy ou de l’affinité.`,"accountPool.stickyLimit":`Affectations nouvelles/non liées avant rotation`,"accountPool.stickyLimitAria":`Affectations nouvelles/non liées avant rotation`,"accountPool.stickyLimitInc":`Augmenter la limite de persistance`,"accountPool.stickyLimitDec":`Diminuer la limite de persistance`,"accountPool.stickyLimitHelp":`Conservez le compte sélectionné pour ce nombre d’affectations de tâches nouvelles/non liées avant de passer au suivant ; le compteur augmente lorsque la tâche est liée, et non après la réussite en amont.`,"accountPool.stickyLimitInvalid":`Saisissez un nombre entier compris entre 1 et 100`,"accountPool.strategyLoadFailed":`Impossible de charger la stratégie de rotation.`,"accountPool.strategyUpdateFailed":`Impossible d’enregistrer la stratégie de rotation.`,"accountPool.quotaWindow":`Fenêtre de quota`,"accountPool.quotaWindowDesc":`Barre d’utilisation en cache qui régit la sélection des nouvelles sessions par quota, les seuils de remplissage prioritaire et les remplacements 429 admissibles.`,"accountPool.quotaWindowFiveHour":`Barre de 5 heures`,"accountPool.quotaWindowWeekly":`Barre hebdomadaire`,"accountPool.quotaWindowMaxUtilization":`Barre la plus haute`,"accountPool.quotaWindowHint":`La barre hebdomadaire ignore les comptes dont la barre de 5 heures est épuisée tant qu’un autre compte admissible reste disponible, mais y revient si aucun autre ne reste. Les égalités privilégient la plus faible utilisation sur 5 heures ; les barres hebdomadaires ne sont connues qu’après interrogation de la page Fournisseurs.`,"accountPool.quotaWindowInert":`Seule la stratégie Quota — ou le remplissage prioritaire avec un seuil supérieur à 0 — évalue une barre d’utilisation ; ce réglage ne change donc rien pour la stratégie de rotation actuelle.`,"accountPool.priority":`Ordre de sélection`,"accountPool.priorityAria":`Ordre de sélection de ce compte`,"accountPool.priorityHint":`Les nombres les plus élevés sont utilisés en premier. Le groupe ne passe à un nombre inférieur que lorsque tous les comptes de niveau supérieur sont épuisés ou indisponibles.`,"accountPool.priorityFirst":`Premier`,"accountPool.priorityEarlier":`Plus tôt`,"accountPool.priorityNormal":`Normal`,"accountPool.priorityLater":`Plus tard`,"accountPool.priorityLast":`Dernier`,"accountPool.priorityOption":`{name} ({value})`,"accountPool.priorityCustom":`Personnalisé`,"accountPool.priorityUpdated":`Ordre de sélection mis à jour pour {email}`,"accountPool.priorityUpdateFailed":`Impossible d’enregistrer l’ordre de sélection de {email}. La dernière valeur confirmée est affichée.`,"codexAuth.switched":`{email} est sélectionné pour la prochaine requête`,"codexAuth.loadFailed":`Impossible de charger les paramètres des comptes Codex.`,"codexAuth.switchFailed":`Impossible de changer de compte. Votre sélection précédente reste inchangée.`,"codexAuth.removeConfirm":`Supprimer {id} ?`,"codexAuth.removeFailed":`Impossible de supprimer le compte. Aucune modification apportée.`,"codexAuth.addTitle":`Ajouter un compte Codex`,"codexAuth.addIdLabel":`ID du compte (slug)`,"codexAuth.addIdPlaceholder":`codex-work, codex-alt, team...`,"codexAuth.resetCreditsAria":`{count} crédit(s) de réinitialisation`,"codexAuth.addJsonLabel":`Contenu de auth.json`,"codexAuth.addHelp":`Copiez-le depuis le fichier ~/.codex/auth.json d’une autre machine ou utilisez codex-auth export.`,"codexAuth.importBtn":`Importer`,"codexAuth.importInvalidJson":`JSON non valide`,"codexAuth.importMissingTokens":`access_token ou refresh_token absent du JSON`,"codexAuth.importMissingId":`L’identifiant du compte est requis`,"codexAuth.accountAdded":`Compte ajouté au groupe`,"codexAuth.addPickDesc":`Connectez-vous avec un autre compte ChatGPT pour l’ajouter au groupe.`,"codexAuth.oauthLogin":`Connexion OAuth`,"codexAuth.oauthDesc":`Ouvre la page de connexion ChatGPT dans le navigateur`,"codexAuth.deviceLogin":`Connexion par code d'appareil`,"codexAuth.deviceDesc":`Pour un proxy headless ou distant : saisissez un code court sur un autre appareil`,"codexAuth.importAuthJson":`Importer auth.json`,"codexAuth.importAuthJsonDesc":`Depuis une autre installation de Codex ou un export codex-auth`,"codexAuth.back":`Retour`,"codexAuth.oauthAlreadyInProgress":`Une connexion est déjà en cours. Terminez-la dans votre navigateur.`,"codexAuth.oauthWaiting":`En attente de la fin de la connexion à ChatGPT dans votre navigateur...`,"codexAuth.oauthSubmittingCode":`Envoi du code…`,"codexAuth.oauthCodeSubmitted":`Code envoyé — en attente de la fin de la connexion…`,"codexAuth.oauthStatusRetrying":`Erreur réseau ou de proxy lors de la vérification de l’état de la connexion — nouvelle tentative…`,"codexAuth.oauthCancelled":`La connexion a été annulée.`,"codexAuth.loginFailed":`Échec de la connexion`,"codexAuth.needsReauth":`Se reconnecter`,"codexAuth.reauthenticate":`Se réauthentifier`,"codexAuth.tokenExpired":`Jeton expiré — réauthentifiez ce compte`,"codexAuth.mainTokenExpired":`Jeton expiré — reconnectez-vous depuis l’application Codex`,"codexAuth.emailCollision":`Ce compte correspond à votre connexion Codex principale. Utilisez un autre compte.`,"codexAuth.resetCreditsTitle":`Crédits de réinitialisation`,"codexAuth.resetCreditsAvailable":`Vous disposez de {count} crédit(s) de réinitialisation.`,"codexAuth.resetCreditsDesc":`Chaque crédit réinitialise instantanément vos limites d’utilisation horaire et hebdomadaire actuelles.`,"codexAuth.noResetCredits":`Vous ne disposez d’aucun crédit de réinitialisation.`,"codexAuth.earnCreditsHint":`Les crédits sont accordés chaque mois et dans le cadre du programme de parrainage.`,"codexAuth.creditsExpireNote":`Les crédits expirent 30 jours après leur obtention.`,"codexAuth.useOneCredit":`Utiliser 1 crédit`,"codexAuth.confirmResetTitle":`Utiliser un crédit de réinitialisation?`,"codexAuth.confirmResetDesc":`Vos limites de débit actuelles seront réinitialisées immédiatement. Il vous reste {count} crédit(s).`,"codexAuth.irreversible":`Cette action est irréversible.`,"codexAuth.useCredit":`Utiliser le crédit`,"codexAuth.redeeming":`Réinitialisation...`,"codexAuth.resetSuccess":`Limites de débit réinitialisées! {remaining} crédit(s) restant(s).`,"codexAuth.resetSuccessGeneric":`Limites de débit réinitialisées!`,"codexAuth.resetAlreadyRedeemed":`Ce crédit a déjà été utilisé. Les crédits sont inchangés.`,"codexAuth.resetNothingToReset":`Aucune fenêtre de limite de débit ne doit être réinitialisée actuellement.`,"codexAuth.resetNoCredit":`Aucun crédit de réinitialisation disponible.`,"codexAuth.resetError":`Impossible d’utiliser le crédit de réinitialisation. Réessayez.`,"codexAuth.fifoNote":`Le crédit le plus ancien est utilisé en premier.`,"codexAuth.confirmWhichCredit":`Le crédit du {date} sera utilisé.`,"codexAuth.creditNext":`Prochain à utiliser`,"codexAuth.creditLabel":`Crédit nº {n}`,"codexAuth.creditNextBadge":`PROCHAIN`,"codexAuth.creditGranted":`Accordé le {date}`,"codexAuth.creditExpires":`Expire le {date} ({days} j restants)`,"api.title":`Accès à l’API`,"api.subtitle":`Utilisez les clés API générées pour accéder au proxy opencodex depuis des applications externes. Les clés s’authentifient au moyen de l’en-tête {authHeader}; consultez le tableau ci-dessous pour connaître les éléments acceptés par chaque point de terminaison.`,"api.baseUrl":`URL de base`,"api.responsesEndpoint":`API Responses`,"api.chatCompletionsEndpoint":`API Chat Completions`,"api.messagesEndpoint":`API Messages`,"api.modelsEndpoint":`API des modèles`,"api.endpointNote":`Utilisez l’URL de base avec les clients compatibles avec OpenAI. Responses et Chat Completions sont accessibles sous /v1.`,"api.endpointsTitle":`Points de terminaison`,"api.authTitle":`Authentification`,"api.authLoopback":`Les écoutes en boucle locale (127.0.0.1 ou ::1) contournent l’authentification. Les écoutes distantes nécessitent une clé ocx_ générée ou OPENCODEX_API_AUTH_TOKEN.`,"api.authBaseUrlNote":`Configurez les clients avec l’URL de base, puis choisissez ci-dessous le point de terminaison propre au protocole.`,"api.newKeyTitle":`Nouvelle clé créée`,"api.newKeyNote":`Copiez cette clé maintenant — elle ne sera plus affichée.`,"api.copy":`Copier`,"api.copied":`Copié`,"api.dismiss":`Fermer`,"api.generateTitle":`Générer une clé`,"api.keyNamePlaceholder":`Nom de la clé (facultatif)`,"api.generate":`Générer`,"api.generating":`Création…`,"api.activeKeys":`Clés actives ({count})`,"api.activeKeysLoading":`Clés actives`,"api.noKeys":`Aucune clé API pour le moment. Générez-en une ci-dessus.`,"api.workspace.sections":`Sections de l’API`,"api.section.keys":`Clés`,"api.section.connect":`Connexion`,"api.section.endpoints":`Points de terminaison`,"api.section.models":`Modèles`,"api.section.examples":`Exemples`,"api.workspace.details":`Détails de la clé API`,"api.workspace.keyDetails":`Détails de la clé`,"api.workspace.keyPrefix":`Préfixe de la clé`,"api.workspace.deleteKey":`Supprimer la clé`,"api.workspace.deleteConfirm":`Voulez-vous vraiment supprimer cette clé? Cette action est irréversible.`,"api.workspace.usageExamples":`Exemples d’utilisation`,"api.copyUrlHint":`Cliquer pour copier l’URL`,"api.urlCopied":`URL copiée`,"api.copyExampleHint":`Cliquer pour copier l’exemple`,"api.exampleCopied":`Exemple copié`,"api.colName":`Nom`,"api.colKey":`Clé`,"api.colCreated":`Création`,"api.confirm":`Confirmer`,"api.deleteAria":`Supprimer la clé API`,"api.modelsTitle":`Catalogue de modèles externes`,"api.modelsCount":`{count} appelable(s)`,"api.modelsLoading":`Chargement des modèles…`,"api.modelsSearch":`Rechercher des modèles`,"api.modelsSubtitle":`Utilisez ces identifiants de modèle exacts avec /v1/models et le protocole entrant de votre choix.`,"api.modelsEmpty":`Aucun modèle appelable de l’extérieur n’est encore disponible.`,"api.modelsNoMatch":`Aucun modèle ne correspond à « {query} ».`,"api.modelsLoadFailed":`Impossible de charger le catalogue de modèles externes.`,"api.colModel":`Modèle`,"api.colSource":`Source`,"api.colProtocols":`Protocoles`,"api.sourceNative":`Groupe ChatGPT`,"api.sourceCombo":`Route de combinaison`,"api.sourceCustom":`Personnalisé`,"api.protocolResponses":`Responses`,"api.protocolChatCompletions":`Chat Completions`,"api.protocolMessages":`Messages`,"api.copyModelId":`Copier l’identifiant`,"api.modelCopied":`Copié`,"api.testModel":`Tester`,"api.testingModel":`Test en cours…`,"api.testSucceeded":`OK`,"api.testFailed":`Échec`,"api.usageChatTitle":`Exemple Chat Completions`,"api.usageResponsesTitle":`Exemple Responses`,"api.usageMessagesTitle":`Exemple Messages`,"api.usageSampleInput":`Bonjour tout le monde!`,"api.clientConfig.title":`Configuration du client`,"api.clientConfig.rowsLabel":`Connecter un client`,"api.clientConfig.details":`Détails`,"api.clientConfig.detailsAria":`Détails de la configuration de {client}`,"api.clientConfig.copyAria":`Copier la configuration de {client}`,"api.clientConfig.downloadAria":`Télécharger la configuration de {client}`,"api.clientConfig.rowMeta":`{destination} · {count} modèle(s)`,"api.clientConfig.rowError":`Impossible de générer la configuration de {client}.`,"api.clientConfig.copiedAnnounceClient":`Configuration de {client} copiée dans le presse-papiers.`,"api.clientConfig.clientOpencode":`OpenCode`,"api.clientConfig.clientPi":`Pi`,"api.clientConfig.clientOmp":`OMP`,"api.clientConfig.clientHermes":`Hermes`,"api.clientConfig.clientOpenclaw":`OpenClaw`,"api.clientConfig.clientKimi":`Kimi Code`,"api.clientConfig.clientGajae":`Gajae Code`,"api.clientConfig.clientDsh":`DeepSeek Harness (DSH)`,"api.clientConfig.clientMcode":`MiniMax Code`,"api.clientConfig.clientZcode":`ZCode`,"api.clientConfig.clientPrime":`Prime Agent`,"api.clientConfig.clientAside":`Aside`,"api.clientConfig.copy":`Copier la configuration`,"api.clientConfig.download":`Télécharger`,"api.clientConfig.loading":`Génération de la configuration du client…`,"api.clientConfig.jsonLabel":`Configuration de {client}`,"api.clientConfig.destination":`Fichier de destination`,"api.clientConfig.envHint":`Définissez la clé avant le lancement`,"api.clientConfig.mergeWarning":`Fusionnez ceci dans le fichier de destination. Le remplacer supprimerait vos autres fournisseurs et paramètres MCP.`,"api.clientConfig.modelCount":`{count} modèle(s) exporté(s)`,"api.clientConfig.missingLimits":`{count} modèle(s) sur {total} sont fournis sans limite de contexte; le client applique ses propres valeurs par défaut.`,"api.clientConfig.noKeyYet":`Aucune clé ne se trouve encore derrière {env}. Générez une clé ci-dessus avant d’utiliser cette configuration hors de la boucle locale.`,"api.clientConfig.loadFailed":`Impossible de lire la liste des modèles; aucune configuration de client n’a donc été produite.`,"api.clientConfig.copiedAnnounce":`Configuration du client copiée dans le presse-papiers.`,"api.clientConfig.copyFailed":`Impossible de copier la configuration du client.`,"api.clientConfig.downloadedAnnounce":`{filename} téléchargé. Rien n’a encore changé — fusionnez-le vous-même dans {destination}.`,"api.clientConfig.whereDisclosure":`Emplacement de ce fichier`,"api.clientConfig.whereBody":`La destination ci-dessus est le chemin global. Un fichier de configuration local au projet dans le répertoire de travail a priorité sur celui-ci, et le client lit la clé depuis la variable d’environnement nommée dans la configuration — jamais depuis ce fichier.`,"api.keysLoadFailed":`Impossible de charger les clés API.`,"api.createFailed":`Impossible de créer la clé API.`,"api.deleteFailed":`Impossible de supprimer la clé API.`,"api.auth.endpoint":`Point de terminaison`,"api.auth.required":`Requis`,"api.auth.accepted":`Accepté`,"api.auth.rejected":`Non accepté`,"api.auth.testProtocol":`Tester {protocol}`,"api.auth.testNeedsFreshKey":`Générez une clé et conservez sa valeur à usage unique à l’écran pour exécuter un test authentifié.`,"api.key.name":`Nom de la clé`,"api.key.rename":`Renommer`,"api.key.saveName":`Enregistrer le nom`,"api.key.renaming":`Enregistrement…`,"api.key.renameFailed":`Impossible de renommer la clé. Votre brouillon a été conservé.`,"api.key.deleting":`Suppression…`,"api.rotation.title":`Rotation de la clé`,"api.rotation.description":`Crée une clé de remplacement tout en conservant brièvement la clé actuelle.`,"api.rotation.start":`Démarrer la rotation`,"api.rotation.starting":`Démarrage…`,"api.rotation.pending":`La rotation est en attente. Mettez à jour et vérifiez le client avant de la valider.`,"api.rotation.expires":`Fin du chevauchement :`,"api.rotation.secretOnce":`Clé de remplacement — affichée une seule fois. Copiez-la avant de fermer.`,"api.rotation.commit":`Valider la rotation`,"api.rotation.abort":`Annuler la rotation`,"api.rotation.failed":`L’action de rotation n’a pas abouti. Actualisez avant de réessayer.`,"api.rotation.startFailed":`Impossible de démarrer la rotation de la clé.`,"api.key.copyFailed":`Impossible de copier la clé. Sélectionnez-la et copiez-la manuellement avant de fermer ce panneau.`,"api.attribution.title":`Utilisation attribuée`,"api.attribution.requests7d":`Requêtes des 7 derniers jours`,"api.attribution.totalRequests":`Total des requêtes attribuées`,"api.attribution.totalRequestsAvailable":`Requêtes dans l’historique disponible`,"api.attribution.sinceAvailable":`Attribution disponible depuis le`,"api.attribution.lastUsed":`Dernière utilisation`,"api.attribution.since":`Attribution disponible depuis le`,"api.attribution.neverUsed":`Non utilisée depuis le début de l’attribution`,"api.attribution.unavailable":`Utilisation indisponible`,"api.attribution.unavailableDetail":`Aucune utilisation n’a encore été attribuée. Les requêtes enregistrées avant le début de l’attribution ne peuvent pas être attribuées rétroactivement.`,"api.attribution.ambiguous":`Deux clés partagent cet identifiant; l’utilisation ne peut donc pas être attribuée à l’une d’elles. Attribuez un identifiant unique à chaque clé dans le fichier de configuration.`,"api.attribution.railAmbiguous":`identifiant en double`,"claude.subtitle":`Utilisez GPT, Gemini et d’autres modèles dans Claude Code.`,"claude.pageTitle":`Claude Code`,"claude.workspace.settings":`Paramètres`,"claude.enabledLabel":`Connexion Claude`,"claude.enabledHint":`Lorsque cette option est désactivée, Claude Code ne peut pas utiliser ce proxy.`,"claude.authMode":`Mode d’authentification`,"claude.authModeHint":`L’abonnement nécessite un compte Claude; le proxy fonctionne sans compte Anthropic`,"claude.authModeSubscription":`Abonnement (compte Claude)`,"claude.authModeProxy":`Proxy (aucun compte requis)`,"claude.authModeAuto":`Automatique (détecter l’authentification Claude)`,"claude.effectiveMode.label":`Effectif au prochain lancement`,"claude.effectiveMode.manual":`Manuel : {mode}`,"claude.effectiveMode.autoPresent":`Automatique : abonnement — authentification Claude trouvée via {source}`,"claude.effectiveMode.autoAbsent":`Automatique : mode proxy — aucune authentification Claude trouvée`,"claude.effectiveMode.autoUnknown":`Automatique : abonnement — impossible de vérifier l’authentification`,"claude.effectiveMode.admissionKey":`La clé API de ce proxy est toujours envoyée.`,"claude.authSource.claude-json-oauth":`Compte Claude`,"claude.authSource.claude-credentials-file":`fichier d’identifiants`,"claude.authSource.macos-keychain":`Trousseau macOS`,"claude.authSource.exported-env":`variable d’environnement`,"claude.authSource.unknown":`un identifiant détecté`,"claude.systemEnv":`Connexion automatique`,"claude.systemEnvDesc":`Lorsque cette option est activée, l’exécution de claude dans n’importe quel terminal passe automatiquement par le proxy.`,"claude.systemEnvUnsupported":`La connexion automatique est disponible uniquement sous macOS. Sur ce système, démarrez Claude avec {cmd}.`,"claude.systemEnvWarn":`⚠ Vous devez quitter complètement votre application de terminal et la relancer pour appliquer ce changement. Non recommandé.`,"claude.fastMode":`Mode rapide (OpenAI)`,"claude.fastModeDesc":`Contrôle service_tier pour les modèles OpenAI. ACTIVÉ = priorité (plus rapide). DÉSACTIVÉ = valeur par défaut. Automatique = transmission directe (le client décide).`,"claude.fastAuto":`Automatique`,"claude.fastOn":`ACTIVÉ`,"claude.fastOff":`DÉSACTIVÉ`,"claude.autoContext":`Utiliser automatiquement le grand contexte`,"claude.autoContextDesc":`Contrôle l’étendue du marquage 1M. ACTIVÉ : tout modèle dont la fenêtre peut accueillir le seuil de compaction obtient une ligne de grand contexte. DÉSACTIVÉ : seuls les véritables modèles 1M en obtiennent une.`,"claude.autoContextInert":`Inactif, car une ancienne valeur de taille de contexte (maxContextTokens) existe dans le fichier de configuration. Supprimez-la dans ce fichier pour réactiver l’option.`,"claude.autoCompactWindow":`Seuil de résumé automatique`,"claude.autoCompactDefault":`{value} (par défaut)`,"claude.autoCompactWindowDesc":`Les anciens messages sont résumés lorsque la conversation atteint ce seuil. Celui-ci ne dépasse jamais la limite propre à chaque modèle; les modèles 200k ne sont donc pas affectés.`,"claude.autoCompactWindowWarn":`La modification de ce paramètre peut perturber les modèles GPT — s’il dépasse la limite réelle d’un modèle, les conversations échoueront avant le déclenchement du résumé.`,"claude.injectAgents":`Enregistrer automatiquement les sous-agents`,"claude.injectAgentsDesc":`Enregistre les modèles choisis dans l’onglet Sous-agents (ainsi que le modèle par défaut actuel) comme agents Claude Code délégables (ocx-*). S’applique à partir de la prochaine session.`,"claude.webSearchSidecar":`Remplacement du service auxiliaire de recherche Web`,"claude.webSearchSidecarHint":`Remplace le service auxiliaire principal de recherche Web pour les requêtes Claude Code.`,"claude.visionSidecar":`Remplacement du service auxiliaire de vision`,"claude.visionSidecarHint":`Remplace le service auxiliaire principal de vision pour les requêtes Claude Code.`,"claude.useMainSetting":`Utiliser le paramètre principal`,"claude.sidecarModelPlaceholder":`Modèle du paramètre principal`,"claude.quickstart":`Commencer`,"claude.quickstartHint":`{cmd} ouvre Claude Code via le proxy. Votre connexion à claude.ai reste active.`,"claude.manualEnv":`Configuration manuelle (avancé)`,"claude.smallFastModel":`Modèle auxiliaire en arrière-plan`,"claude.smallFastModelHint":`Le modèle utilisé par Claude Code pour les tâches en arrière-plan, comme le résumé des conversations et la détection des sujets. L’alias de sous-agent haiku l’utilise également. Vide = valeur par défaut de Claude (Haiku).`,"claude.smallFastModelAccurateHint":`Le modèle utilisé par Claude Code pour les tâches en arrière-plan, comme le résumé des conversations et la détection des sujets. L’alias de sous-agent haiku l’utilise également.`,"claude.smallFastModelUnsetOption":`Laisser Claude Code choisir (modèle natif)`,"claude.smallFastModelNativeWarning":`Lorsque ce champ est vide, OpenCodex ne définit aucun remplacement du modèle auxiliaire. Claude Code peut utiliser son modèle Sonnet natif, ce qui peut entraîner des frais auprès de votre fournisseur natif.`,"claude.slotUnset":`Utiliser la valeur par défaut de Claude`,"claude.modelMap":`Interception de modèles`,"claude.modelMapHint":`Intercepte les requêtes visant un modèle précis et les redirige vers celui que vous choisissez. Vide par défaut — rien ne se produit tant que vous n’ajoutez pas de règle.`,"claude.mapFrom":`Modèle d’origine (p. ex. claude-sonnet-4-5)`,"claude.mapTo":`Remplacer par (p. ex. gemini/gemini-3-pro)`,"claude.addMapping":`Ajouter une règle`,"claude.removeMapping":`Supprimer la règle`,"claude.aliases":`Modèles disponibles`,"claude.aliasesHint":`Modèles affichés dans le menu /model de Claude Code.`,"claude.aliasProviderOther":`Autre`,"claude.loading":`Chargement…`,"claude.loadFail":`Impossible de charger les paramètres de Claude`,"claude.saved":`Enregistré.`,"claude.saveFailed":`Échec de l’enregistrement`,"claude.networkError":`Erreur réseau — le proxy est-il en cours d’exécution?`,"claude.toggleAria":`Activer ou désactiver la connexion Claude`,"claude.none":`Aucun`,"cws.loading":`Chargement des combinaisons…`,"cws.loadFailed":`Impossible de charger les combinaisons.`,"cws.saveFailed":`Impossible d’enregistrer la combinaison.`,"cws.removeFailed":`Impossible de supprimer la combinaison.`,"cws.saved":`Combinaison enregistrée.`,"cws.created":`{model} créé.`,"cws.removed":`combo/{id} supprimé.`,"cws.renamed":`{from} renommé en {to}.`,"cws.add":`Ajouter une combinaison`,"cws.addTitle":`Ajouter une combinaison`,"cws.addSubtitle":`Créez un modèle virtuel couvrant plusieurs fournisseurs et choisissez le nom de modèle exact que les clients demanderont.`,"cws.create":`Créer la combinaison`,"cws.railAria":`Liste des combinaisons`,"cws.searchPlaceholder":`Rechercher des combinaisons ou des cibles…`,"cws.noSearchResults":`Aucune combinaison ne correspond à votre recherche.`,"cws.group.failover":`Basculement`,"cws.group.roundRobin":`Rotation`,"cws.group.other":`Autres stratégies`,"cws.targetCount":`{count} cibles`,"cws.targetCountOne":`1 cible`,"cws.overviewTitle":`Combinaisons`,"cws.overviewBlurb":`Modèles virtuels qui routent entre des cibles fournisseur/modèle par repli, rotation, aléatoire pondéré, moins utilisé ou réinitialisation de quota la plus proche.`,"cws.count.total":`Total`,"cws.count.failover":`Basculement`,"cws.count.roundRobin":`Rotation`,"cws.count.other":`Autres`,"cws.howTitle":`Fonctionnement`,"cws.howBody":`Demandez à Codex le nom de modèle public de la combinaison. Sans nom, combo/ est utilisé par défaut. OpenCodex sélectionne une cible et ne bascule qu’en cas d’échec réessayable en amont. Si aucune cible ne reste disponible, la requête est bloquée au lieu d’utiliser le fournisseur global par défaut.`,"cws.attentionTitle":`Intervention requise`,"cws.attention.empty":`Aucune cible configurée`,"cws.attention.few":`Une seule cible — le basculement n’a aucune autre destination`,"cws.attention.catalogOmitted":`Absent du catalogue de modèles — les capacités des membres sont incomplètes ou incompatibles (fenêtre de contexte ou métadonnées manquantes, ou intersection des modalités vide). Le routage par alias fonctionne toujours`,"cws.attention.allTargetsExhausted":`Toutes les cibles activées ont épuisé leur quota`,"cws.emptyTitle":`Créer votre première combinaison`,"cws.empty.createDesc":`Nommez un modèle virtuel et enchaînez au moins deux services principaux.`,"cws.backToAll":`Retour à toutes les combinaisons`,"cws.capability.imageInputUnavailable":`Indisponible tant que toutes les cibles sélectionnées ne prennent pas en charge les images.`,"cws.capability.imageInputHint":`Activé par défaut lorsque toutes les cibles prennent en charge les images. Désactivez cette option pour n’accepter que du texte.`,"cws.capability.imageInput":`Images / multimodal`,"cws.capability.adaptiveEffort":`Échelle de raisonnement adaptative`,"cws.capability.adaptiveEffortHint":`Désactivé : une cible sans réglage de raisonnement masque le sélecteur pour toute la combinaison. Activé : ces cibles restent utilisables et le sélecteur conserve les niveaux communs aux autres cibles.`,"cws.capabilities":`Capacités`,"cws.allCombos":`Toutes les combinaisons`,"cws.copyModel":`Copier l’identifiant`,"cws.copied":`Copié`,"cws.tabsLabel":`Sections des détails de la combinaison`,"cws.tab.config":`Configuration`,"cws.tab.about":`À propos`,"cws.strategy":`Stratégie`,"cws.strategy.failover":`Basculement`,"cws.strategy.roundRobin":`Rotation`,"cws.strategy.random":`Aléatoire`,"cws.strategy.leastUsed":`Moins utilisé`,"cws.strategy.resetWindow":`Fenêtre de réinitialisation`,"cws.strategy.failoverHint":`Essaie les cibles dans l’ordre. Si la première échoue avec une erreur réessayable (limite de débit, panne, restriction d’abonnement), passe à la suivante.`,"cws.strategy.roundRobinHint":`Répartit le trafic de manière déterministe selon les pondérations. Conserve chaque cible sélectionnée pendant un lot de requêtes réussies, puis passe à la suivante.`,"cws.strategy.randomHint":`Tire une cible éligible par requête, avec des probabilités proportionnelles au poids. Aucune adhérence entre requêtes.`,"cws.strategy.leastUsedHint":`Dirige chaque requête vers la cible éligible ayant le moins de succès enregistrés. Les compteurs redémarrent avec le proxy.`,"cws.strategy.resetWindowHint":`Préfère la cible éligible dont la fenêtre de quota se réinitialise le plus tôt. Sans données de quota, l’ordre de configuration s’applique.`,"cws.field.id":`Identifiant de la combinaison`,"cws.field.idHint":`Les clients demanderont {model}`,"cws.field.idInternalHint":`Identifiant interne de la combinaison. Vous pouvez le modifier après la création.`,"cws.field.idHintEdit":`Le renommage déplace la combinaison vers un nouvel identifiant. Les clients demandent {model}.`,"cws.field.alias":`Nom de modèle public`,"cws.field.aliasPlaceholder":`deepseek-v4-flash ou vendor/model`,"cws.field.aliasHint":`Facultatif. Utilisez un nom simple sans préfixe, un préfixe personnalisé comme vendor/model, ou laissez le champ vide pour utiliser combo/.`,"cws.field.nativeAlias":`Alias OpenAI natif`,"cws.field.nativeAliasHint":`Permet à cette combinaison de prendre en charge un identifiant de modèle OpenAI natif non qualifié compatible. Les routes OpenAI qualifiées par compte ou fournisseur restent distinctes.`,"cws.field.displayName":`Nom d’affichage`,"cws.field.displayNameHint":`Libellé de cette combinaison dans le sélecteur. Requis lorsque l’alias OpenAI natif est activé.`,"cws.field.stickyLimit":`Réussites persistantes avant rotation`,"cws.field.stickyLimitHint":`Conserve la cible sélectionnée pendant ce nombre de requêtes réussies avant que le sélecteur pondéré passe à la suivante.`,"cws.field.defaultEffort":`Raisonnement par défaut`,"cws.field.defaultEffortNone":`Aucun (valeur par défaut de la cible)`,"cws.field.defaultEffortHint":`Utilisé uniquement lorsque le client omet l’effort de raisonnement. Les options correspondent à l’intersection des efforts annoncés par les cibles sélectionnées; les cibles sans métadonnées d’effort dans le catalogue n’en proposent aucun.`,"cws.field.defaultEffortUnsupported":`Cet effort ne figure pas dans l’échelle commune des cibles — il sera ignoré ou ajusté lors de la requête.`,"cws.field.defaultEffortUnsupportedOption":`absent de l’intersection`,"cws.targets":`Cibles`,"cws.targets.failoverHint":`L’ordre est important — la première est la cible principale.`,"cws.targets.roundRobinHint":`Les pondérations contrôlent la sélection relative déterministe; l’ordre départage les égalités dans l’anneau de rotation.`,"cws.targets.randomHint":`Les pondérations contrôlent les chances de chaque tirage ; l’ordre n’a pas d’importance.`,"cws.targets.leastUsedHint":`L’ordre ne départage que les cibles également utilisées.`,"cws.targets.resetWindowHint":`L’ordre s’applique quand les données de quota manquent ou sont égales.`,"cws.target.provider":`Fournisseur`,"cws.target.model":`Modèle`,"cws.target.weight":`Pondération`,"cws.target.pickProvider":`Sélectionner un fournisseur…`,"cws.target.pickProviderFirst":`Sélectionner d’abord un fournisseur…`,"cws.target.pickModel":`Sélectionner un modèle…`,"cws.target.noModels":`Aucun modèle pour ce fournisseur`,"cws.target.modelPlaceholder":`identifiant du modèle`,"cws.target.add":`Ajouter une cible`,"cws.target.drag":`Faire glisser pour réorganiser`,"cws.target.moveUp":`Monter`,"cws.target.moveDown":`Descendre`,"cws.quota.available":`Disponible`,"cws.quota.exhausted":`Quota épuisé`,"cws.quota.unknown":`Quota inconnu`,"cws.quota.allExhausted":`Toutes les cibles activées ont épuisé leur quota. Choisissez une autre cible ou attendez le rétablissement du quota.`,"cws.aboutTitle":`Exécution`,"cws.aboutBody":`Les cibles en échec sont temporairement mises en attente conformément à Retry-After. Les erreurs de contexte ou de validité ne provoquent aucun basculement. Chaque cible adapte l’effort à ses propres capacités ; les combinaisons épuisées bloquent les requêtes. Les journaux et la section Utilisation conservent les tentatives physiques ordonnées et l’utilisation de chaque tentative.`,"cws.removeConfirmTitle":`Supprimer {model}?`,"cws.removeConfirmDesc":`Cette action supprime le modèle virtuel de la configuration et du catalogue Codex. Elle ne supprime aucun fournisseur.`,"cws.unsavedTitle":`Modifications non enregistrées`,"cws.unsavedDesc":`Abandonner les modifications de cette combinaison et continuer ?`,"cws.keepEditing":`Continuer la modification`,"cws.err.missingId":`L’identifiant de la combinaison est requis.`,"cws.err.invalidId":`L’identifiant doit commencer par une lettre ou un chiffre et contenir uniquement des lettres, des chiffres, des points, des traits de soulignement ou des traits d’union (64 caractères au maximum).`,"cws.err.duplicateId":`Une combinaison portant cet identifiant existe déjà.`,"cws.err.invalidAlias":`L’alias doit contenir des lettres, des chiffres, des points, des traits de soulignement ou des traits d’union, avec au plus un segment « / ».`,"cws.err.aliasReservedNamespace":`L’alias ne doit pas utiliser l’espace de noms réservé « combo/ ».`,"cws.err.aliasNativeFamily":`Les alias simples de la famille native OpenAI (gpt-*, o1-*, o3-*, o4-*, codex-*) ne sont pas autorisés.`,"cws.err.unsupportedNativeAlias":`L’alias natif doit être un identifiant simple de modèle OpenAI actuellement pris en charge.`,"cws.err.missingNativeAliasDisplayName":`Un nom d’affichage est requis pour les alias natifs.`,"cws.err.invalidDisplayName":`Le nom d’affichage doit comporter au plus 128 caractères et ne contenir aucun caractère de contrôle.`,"cws.err.duplicateAlias":`Une autre combinaison utilise déjà cet alias.`,"cws.err.noTargets":`Ajoutez au moins une cible.`,"cws.err.incompleteTarget":`Chaque cible nécessite un fournisseur et un modèle.`,"cws.target.disabled":`{name} (désactivé)`,"cws.err.reservedNamespace":`Un fournisseur physique nommé « combo » doit être renommé avant la création de combinaisons.`,"cws.err.providerCollision":`L’identifiant de la combinaison entre en conflit avec le nom d’un fournisseur configuré.`,"cws.err.unknownProvider":`Chaque cible doit utiliser un fournisseur configuré.`,"cws.err.duplicateTarget":`Une même cible fournisseur/modèle ne peut apparaître qu’une seule fois.`,"cws.err.invalidStickyLimit":`Le nombre de réussites persistantes doit être un entier compris entre 1 et 100.`,"cws.err.invalidWeight":`Chaque pondération de rotation doit être un entier compris entre 1 et 10000.`,"cws.err.noEnabledTarget":`Au moins une cible doit utiliser un fournisseur activé.`,"claude.tabsLabel":`Client Claude`,"claude.tabCode":`Code`,"claude.tabDesktop":`Desktop`,"claudeDesktop.title":`Claude Desktop`,"claudeDesktop.subtitle":`Acheminez chaque famille de modèles Claude vers un modèle disponible sur le port {port}.`,"claudeDesktop.importJson":`Importer le JSON`,"claudeDesktop.exportJson":`Exporter le JSON`,"claudeDesktop.loading":`Chargement du profil Claude Desktop…`,"claudeDesktop.loadFail":`Impossible de charger le profil Claude Desktop.`,"claudeDesktop.retry":`Réessayer`,"claudeDesktop.saveFailed":`Impossible d’enregistrer le profil Claude Desktop.`,"claudeDesktop.applyFailed":`Le profil a été enregistré, mais n’a pas pu être appliqué.`,"claudeDesktop.updateFailed":`Échec de la mise à jour de Claude Desktop.`,"claudeDesktop.savedApplied":`Profil enregistré et appliqué à Claude Desktop.`,"claudeDesktop.appliedMarkerUnsaved":`Appliqué à Claude Desktop, mais le marqueur d’application n’a pas été enregistré — l’état enregistré/appliqué ci-dessous peut être obsolète jusqu’à la prochaine application.`,"claudeDesktop.savedAppliedAnnounce":`Profil Claude Desktop enregistré et appliqué.`,"claudeDesktop.saved":`Profil enregistré.`,"claudeDesktop.savedAnnounce":`Profil Claude Desktop enregistré.`,"claudeDesktop.exported":`Profil exporté au format JSON.`,"claudeDesktop.importExpected":`Un profil Claude Desktop de version 1 était attendu.`,"claudeDesktop.importReady":`JSON importé. Vérifiez le brouillon, puis enregistrez-le et appliquez-le.`,"claudeDesktop.importedAnnounce":`JSON du profil importé. Les modifications non enregistrées sont prêtes à être vérifiées.`,"claudeDesktop.importInvalid":`Le fichier sélectionné n’est pas un profil valide.`,"claudeDesktop.importFailed":`Échec de l’importation. {error}`,"claudeDesktop.moved":`{route} déplacé vers {family}.`,"claudeDesktop.unsaved":`Modifications non enregistrées`,"claudeDesktop.upToDate":`Le profil est à jour`,"claudeDesktop.saving":`Enregistrement…`,"claudeDesktop.applying":`Application…`,"claudeDesktop.saveApply":`Enregistrer et appliquer`,"claudeDesktop.emptyTitle":`Aucun modèle disponible`,"claudeDesktop.emptyHint":`Ajoutez ou activez un fournisseur, puis revenez attribuer les routes Claude Desktop.`,"claudeDesktop.assignmentsLabel":`Attributions des familles de modèles Claude`,"claudeDesktop.family.opus":`Opus`,"claudeDesktop.family.fable":`Fable`,"claudeDesktop.family.sonnet":`Sonnet`,"claudeDesktop.family.haiku":`Haiku`,"claudeDesktop.modelCountOne":`{count} modèle`,"claudeDesktop.modelCountMany":`{count} modèles`,"claudeDesktop.chooseDefault":`Choisir une valeur par défaut`,"claudeDesktop.temporaryDefault":`Valeur par défaut temporaire`,"claudeDesktop.laneEmpty":`Déposez un modèle ici ou utilisez sa commande Déplacer.`,"claudeDesktop.laneNoMatch":`Aucun modèle de cette famille ne correspond à votre recherche.`,"nav.grok":`Grok`,"grok.title":`Grok Build`,"grok.subtitle":`Modèles qu’opencodex a enregistrés dans votre configuration Grok.`,"grok.loading":`Chargement de l’état de Grok…`,"grok.loadFail":`Impossible de lire la configuration Grok.`,"grok.notConfiguredTitle":`Grok Build n’est pas configuré`,"grok.notConfiguredHint":`Démarrez ou redémarrez le proxy avec Grok installé; opencodex écrit alors un bloc géré dans :`,"grok.endpoint":`Point de terminaison`,"grok.colModel":`Modèle`,"grok.colAlias":`Alias Grok`,"grok.colContext":`Contexte`,"grok.groupNative":`Modèles natifs`,"grok.groupRouted":`Modèles acheminés`,"grok.enabledCount":`{on} sur {total} enregistrés`,"grok.saved":`Sélection enregistrée.`,"grok.savedApplied":`Sélection enregistrée et écrite dans votre configuration Grok.`,"grok.saveFailed":`Impossible d’enregistrer la sélection Grok.`,"grok.applyFailed":`Sélection enregistrée, mais impossible de mettre à jour la configuration Grok.`,"grok.applySkipped":`Sélection enregistrée. La configuration Grok n’a pas été modifiée.`,"grok.saveApply":`Enregistrer et appliquer`,"grok.saving":`Enregistrement…`,"grok.applying":`Application…`,"grok.unsaved":`Modifications non enregistrées`,"grok.upToDate":`La sélection est à jour`,"grok.toggleModel":`Enregistrer {id} auprès de Grok`,"claudeDesktop.available":`Disponible`,"claudeDesktop.defaultBadge":`Par défaut`,"claudeDesktop.supports1m":`1M`,"claudeDesktop.unavailable":`Indisponible`,"claudeDesktop.contextM":`Contexte de {n}M`,"claudeDesktop.contextK":`Contexte de {n}k`,"claudeDesktop.contextUnknown":`contexte inconnu`,"claudeDesktop.alias":`Alias`,"claudeDesktop.useAsDefault":`Utiliser par défaut pour {family}`,"claudeDesktop.moveTo":`Déplacer vers`,"claudeDesktop.move":`Déplacer`,"claudeDesktop.status.applied":`Appliqué à Desktop`,"claudeDesktop.status.stale":`Configuration obsolète — appliquer de nouveau`,"claudeDesktop.status.notApplied":`Non appliqué`,"claudeDesktop.status.notActiveProfile":`Desktop utilise un autre profil — appliquer de nouveau`,"claudeDesktop.status.disabled":`L’intégration Claude Desktop est désactivée. Quittez complètement Desktop et rouvrez-le après l’avoir activée.`,"claudeDesktop.enableApply":`Activer et appliquer`,"claudeDesktop.health.lastRequest":`Dernière requête`,"claudeDesktop.health.stats":`{count} req. / {errors} err.`,"claudeDesktop.effort.supported":`effort`,"claudeDesktop.effort.displayOnly":`effort (affichage uniquement)`,"lab.title":`Laboratoire de compatibilité`,"lab.subtitle":`Matrice en lecture seule des verdicts de compatibilité issus des preuves de projection du laboratoire.`,"lab.loadFailed":`Impossible de charger les données du laboratoire de compatibilité`,"lab.projectionUnavailable":`La projection du laboratoire n’est pas disponible. Exécutez d’abord les tests de conformité ou les sondes en direct.`,"lab.projectionIncompatible":`Le schéma de projection du laboratoire est incompatible. Régénérez la projection.`,"lab.statusTitle":`État de la projection`,"lab.matrixTitle":`Matrice de compatibilité`,"lab.verdictsTitle":`Enregistrements de verdicts`,"lab.filter.layer":`Couche de preuve`,"lab.filter.verdict":`Verdict`,"lab.filter.subject":`Identifiant du sujet`,"lab.filter.all":`Tous`,"lab.col.subject":`Sujet`,"lab.col.layer":`Couche`,"lab.col.suite":`Suite`,"lab.col.verdict":`Verdict`,"lab.col.asOf":`En date du`,"lab.col.protocol":`Conformité du protocole`,"lab.col.live":`Compatibilité des routes en direct`,"lab.col.task":`Efficacité des tâches`,"lab.empty":`Aucun verdict de compatibilité dans la projection pour le moment.`,"lab.subjectKind":`Type`,"lab.observationCount":`Observations`,"lab.eventCount":`Événements`,"lab.verdictCount":`Verdicts`,"lab.subjectCount":`Sujets`,"lab.builtAt":`Générée le`,"lab.loading":`Chargement des preuves de compatibilité…`,"lab.loadMore":`Charger davantage`,"lab.detailTitle":`Détails du verdict`,"lab.detailClose":`Fermer`,"lab.detailSubject":`Sujet`,"lab.detailObservations":`Observations`,"lab.detailEvents":`Événements contributifs`,"lab.detailArtifacts":`Métadonnées des artefacts`,"lab.production.title":`Trafic de production observé`,"lab.production.notVerification":`Ne constitue pas une vérification du laboratoire`,"lab.production.attempts":`Tentatives`,"lab.production.successes":`Réussites`,"lab.production.routeErrors":`Erreurs de routage`,"lab.production.lastObserved":`Dernière observation`,"lab.detailLoadFailed":`Impossible de charger les détails du verdict`,"lab.refresh":`Actualiser`,"lab.verdict.UNKNOWN":`Inconnu`,"lab.verdict.CLAIMED":`Déclaré`,"lab.verdict.PROBED":`Sondé`,"lab.verdict.VERIFIED":`Vérifié`,"lab.verdict.DEGRADED":`Dégradé`,"lab.verdict.BLOCKED":`Bloqué`,"lab.verdict.UNSUPPORTED":`Non pris en charge`,"lab.layer.protocol_conformance":`Conformité du protocole`,"lab.layer.live_route_compatibility":`Compatibilité des routes en direct`,"lab.layer.task_effectiveness":`Efficacité des tâches`,"models.newPolicyGlobal":`Désactiver les nouveaux modèles par défaut`,"models.newPolicyProvider":`Politique des nouveaux modèles`,"models.newPolicy_inherit":`Hériter`,"models.newPolicy_off":`Désactivé`,"models.newPolicy_on":`Activé`,"models.newBadge":`NOUVEAU`,"models.newCount":`{count} nouveaux, désactivés`,"models.aliases":`Alias`,"models.aliasesTable":`Table des alias`,"models.aliasPrompt":`Alias du fournisseur (laisser vide pour effacer)`,"models.modelAliasPrompt":`Alias du modèle (laisser vide pour effacer)`,"models.aliasSaved":`Alias enregistré`,"models.aliasConflict":`Cet alias entre en conflit avec un nom existant`,"models.editProviderAlias":`Modifier l'alias du fournisseur`,"models.editModelAlias":`Modifier l'alias du modèle`,"models.useDefaultAliases":`Utiliser les alias par défaut`,"models.useDefaultAliasesGlobal":`Utiliser les alias par défaut partout`,"models.aliasAuto":`auto`,"models.aliasUser":`utilisateur`,"models.aliasStale":`obsolète`,"connection.discovering":`Détection des cibles locale et partagée…`,"connection.machineUnavailable":`Le plan machine local est indisponible. Les requêtes partagées n'ont pas été redirigées localement.`,"connection.disconnect":`Déconnecter du hub`,"connection.disconnectConfirm":`Déconnecter cette machine du hub et la redémarrer en mode autonome ?`,"connection.pairing.title":`Connecter ce tableau de bord au hub`,"connection.pairing.body":`Collez le code d'association à usage unique créé sur le hub.`,"connection.pairing.relayWarning":`Ce code passe par le relais fixe du hub. Le relais ne peut pas viser un autre hôte.`,"connection.pairing.code":`Code d'association à usage unique`,"connection.pairing.submit":`Connecter`,"connection.pairing.submitting":`Connexion…`,"connection.pairing.error":`Le code a été refusé ou a expiré. Il reste saisi pour vérification.`,"connection.machine.title":`Cette machine`,"connection.machine.shimHealthy":`Le shim Codex est opérationnel.`,"connection.machine.shimNeedsAttention":`Le shim Codex nécessite une intervention.`,"connection.machine.repairShim":`Réparer le shim`,"connection.machine.removeShim":`Supprimer le shim`,"connection.clients.title":`Clients connectés`,"connection.clients.none":`Aucun état client disponible`,"connection.clients.sync":`Synchroniser`,"connection.clients.syncing":`Synchronisation…`,"connection.sessionLogout":`Se déconnecter de la session distante`,"connection.sessionLoggingOut":`Déconnexion de la session distante…`,"connection.sessionLogoutFailed":`Impossible de fermer la session distante. La session actuelle a été conservée.`,"usage.source.connected":`Source : utilisation du hub`,"usage.source.local":`Source : usage.jsonl local`,"usage.scope.label":`Portée de l'utilisation`,"usage.scope.machine":`Cette machine`,"usage.scope.hub":`Tout le hub`,"usage.hubOffline":`L'utilisation du hub est indisponible. Les données locales n'ont pas été substituées.`,"integrations.tab.cursor":`Cursor`,"integrations.detail.cursorSeen":`Cursor a récemment envoyé une requête à ce proxy`,"integrations.detail.cursorNeverSeen":`Cursor Private Inference est installé ; aucune requête reçue pour le moment`,"integrations.detail.cursorAbsent":`Cursor Private Inference introuvable`,"integrations.cursor.title":`Cursor`,"integrations.cursor.intro":`Cursor Private Inference exécute son agent localement et communique avec opencodex via l’adresse de bouclage. La version standard de Cursor ne le peut pas : les serveurs de Cursor appellent le point de terminaison personnalisé, qui doit donc être accessible via une URL HTTPS publique. Cette page n’écrit jamais dans Cursor ; collez vous-même les valeurs ci-dessous dans Cursor.`,"integrations.cursor.loading":`Lecture de l’état de Cursor…`,"integrations.cursor.unavailable":`Impossible de lire l’état de Cursor depuis le proxy.`,"integrations.cursor.detection":`Versions installées`,"integrations.cursor.privateInference":`Cursor Private Inference`,"integrations.cursor.regular":`Cursor (version standard)`,"integrations.cursor.detected":`Détecté`,"integrations.cursor.notFound":`Introuvable`,"integrations.cursor.regularOnly":`Seule la version standard de Cursor a été trouvée. Ses requêtes vers les points de terminaison personnalisés passent par les serveurs de Cursor ; un proxy sur l’adresse de bouclage reste donc inaccessible sans tunnel public. Consultez le guide de Cursor Private Inference.`,"integrations.cursor.nothingFound":`Aucune installation de Cursor n’a été trouvée aux emplacements habituels. Si Cursor est installé ailleurs, les valeurs ci-dessous restent valables.`,"integrations.cursor.gateway":`Valeurs de la passerelle`,"integrations.cursor.gatewayHint":`Dans Cursor Private Inference, ouvrez Settings > Models > Gateway, collez ces deux valeurs, puis cliquez sur Refresh model list.`,"integrations.cursor.baseUrl":`Base URL`,"integrations.cursor.apiKey":`Clé API`,"integrations.cursor.apiKeyCredential":`L’une de vos clés API opencodex (cette liaison nécessite une authentification)`,"integrations.cursor.copy":`Copier`,"integrations.cursor.copied":`Copié`,"integrations.cursor.connection":`Connexion`,"integrations.cursor.seen":`Dernière requête de Cursor : {time} ({ua})`,"integrations.cursor.neverSeen":`Aucune requête de Cursor depuis le démarrage du proxy. Après avoir enregistré la passerelle, cliquez sur Refresh model list dans Cursor.`,"integrations.cursor.models":`Ce que Cursor affichera`,"integrations.cursor.modelsHint":`Cursor sélectionne le niveau de raisonnement dans sa propre table de modèles ; opencodex ne peut donc que le prévoir. La colonne Contexte indique la fenêtre par défaut et celle disponible en option (le Max Mode de Cursor).`,"integrations.cursor.ladderFromBundle":`Les niveaux de raisonnement sont lus dans le bundle Cursor Private Inference {version} installé. Cursor les décide ; opencodex ne fait que rapporter sa table.`,"integrations.cursor.ladderFromStatic":`Les niveaux de raisonnement sont un miroir statique de Cursor 3.18.25 (aucun bundle Private Inference lisible trouvé). La colonne Contexte indique la fenêtre par défaut et la fenêtre optionnelle.`,"integrations.cursor.unknownVersion":`version inconnue`,"integrations.cursor.noControl":`—`,"integrations.cursor.singleWindow":`fenêtre unique`,"integrations.cursor.noControlTitle":`Cet identifiant n'est pas dans la table d'effort intégrée de Cursor, donc Cursor n'affiche aucun contrôle de raisonnement.`,"integrations.cursor.effortRowsOne":`1 ligne d'effort publiée`,"integrations.cursor.effortRowsMany":`{n} lignes d'effort publiées`,"integrations.cursor.effortRowsOff":`aucune ligne d'effort`,"integrations.cursor.tableLessHint":`Les lignes marquées — n'ont pas de contrôle de raisonnement dans Cursor. Activez cursorEffortRows pour publier une entrée du sélecteur par effort (id--effort), ou définissez modelDefaultReasoningEfforts sur le fournisseur pour une valeur fixe.`,"integrations.cursor.colModel":`Modèle`,"integrations.cursor.colReasoning":`Raisonnement`,"integrations.cursor.colContext":`Contexte`,"integrations.cursor.guide":`Ouvrir le guide de Cursor Private Inference`},Ve={"nav.dashboard":`대시보드`,"uptime.day":`일`,"uptime.hour":`시간`,"uptime.minute":`분`,"uptime.second":`초`,"nav.startup":`시작 안전성`,"nav.providers":`프로바이더`,"nav.models":`모델`,"nav.combos":`콤보`,"nav.subagents":`서브에이전트`,"routing.title":`라우팅 인텔리전스 (beta)`,"routing.subtitle":`정책 프로필, 드라이런 평가, 소스 기반 라우팅 분석.`,"routing.loadFailed":`라우팅 데이터를 불러오지 못했습니다`,"routing.empty":"라우팅 프로필이 구성되지 않았습니다. config.json에 `routingProfiles`를 추가하세요.","routing.revision":`rev`,"routing.detail":`프로필`,"routing.createProfile":`프로필 만들기`,"routing.dryRunError":`드라이런 실패 (HTTP {status})`,"routing.removeConfirm":`프로필 {id}을(를) 제거할까요?`,"routing.unknownEvidence.allow":`허용`,"routing.unknownEvidence.penalize":`불이익`,"routing.unknownEvidence.exclude":`제외`,"routing.removeCandidate":`후보 {provider}/{model} 제거`,"routing.candidates":`후보`,"routing.require":`필수 요구사항`,"routing.optimize":`최적화 가중치`,"routing.limits":`제한`,"routing.unknownEvidence":`알 수 없는 증거 정책`,"routing.compatibility.title":`호환성 정책`,"routing.compatibility.enabled":`Compatibility Lab 증거 필요`,"routing.compatibility.requiredSuites":`필수 스위트`,"routing.compatibility.loadingCatalog":`Lab 카탈로그 로드 중…`,"routing.compatibility.catalogUnavailable":`Lab 카탈로그를 사용할 수 없습니다 — config.json에서 스위트 ID를 수동으로 입력하세요.`,"routing.compatibility.layer.protocol_conformance":`프로토콜 적합성`,"routing.compatibility.layer.live_route_compatibility":`라이브 라우트 호환성`,"routing.compatibility.minStatus":`최소 호환성 상태`,"routing.none":`없음`,"routing.unavailable":`–`,"routing.dryRun":`드라이런 평가`,"routing.dryRunContext":`요청 컨텍스트 창(토큰)`,"routing.dryRunTools":`요청에 도구 필요`,"routing.dryRunImage":`요청에 이미지 입력 필요`,"routing.dryRunStructured":`요청에 구조화된 출력 필요`,"routing.dryRunRun":`후보 평가`,"routing.candidate":`후보`,"routing.eligible":`적격`,"routing.exclusions":`제외`,"routing.costCap":`비용 상한`,"routing.capOutcome.satisfied":`한도 이내`,"routing.capOutcome.exceeded":`한도 초과`,"routing.capOutcome.unknown-allowed":`알 수 없음(허용)`,"routing.capOutcome.unknown-excluded":`알 수 없음(제외)`,"routing.exclusion.capability-unsatisfied":`기능 미충족`,"routing.exclusion.unknown-capability":`알 수 없는 기능`,"routing.exclusion.cost-limit":`비용 상한 초과`,"routing.exclusion.cost-limit-unknown":`상한 이하 비용 불명`,"routing.exclusion.cooldown":`쿨다운`,"routing.exclusion.unknown-health":`상태 불명`,"routing.exclusion.unknown-quota":`할당량 불명`,"routing.exclusion.unknown-price":`가격 불명`,"routing.exclusion.other":`제외: {code}`,"routing.score":`점수`,"routing.selected":`선택됨`,"routing.yes":`예`,"routing.no":`아니요`,"routing.analytics":`라우팅 분석`,"routing.analyticsTotal":`요청`,"routing.analyticsSuccessRate":`성공`,"routing.analyticsFallbackRate":`폴백`,"routing.analyticsP50":`p50`,"routing.analyticsP95":`p95`,"routing.analyticsP99":`p99`,"routing.analyticsCooldown":`쿨다운 실패`,"routing.analyticsConfidence":`신뢰도`,"routing.analyticsTruncated":`잘린 기록`,"routing.analyticsRequests":`요청`,"routing.analyticsEmpty":`분석이 아직 없습니다. 먼저 요청을 보내세요.`,"nav.logs":`로그&디버그`,"nav.usage":`사용량`,"common.github":`GitHub`,"sidebar.star":`GitHub에서 스타 누르기`,"sidebar.starred":`GitHub 스타 완료`,"sidebar.starUnauthenticated":`GitHub에서 스타 누르기 (gh CLI 로그인 안 됨)`,"sidebar.starFailed":`gh로 스타를 누르지 못했습니다. GitHub를 대신 엽니다.`,"sidebar.updateAvailable":`업데이트 있음: {version}`,"sidebar.checkUpdate":`업데이트 확인`,"common.save":`저장`,"common.saving":`저장 중…`,"common.cancel":`취소`,"common.discard":`버리기`,"common.delete":`삭제`,"common.remove":`삭제`,"common.loading":`불러오는 중…`,"common.retry":`재시도`,"auth.adminTokenTitle":`OpenCodex 관리자 토큰 (OPENCODEX_ADMIN_AUTH_TOKEN)`,"auth.adminAccountLabel":`계정`,"auth.adminTokenFieldLabel":`관리자 토큰`,"auth.adminTokenRejected":`관리자 토큰이 거부되었습니다. 확인한 후 다시 시도하세요.`,"auth.adminTokenUnavailable":`관리자 토큰을 확인할 수 없습니다. 다시 시도하세요.`,"theme.label":`테마`,"theme.light":`라이트`,"theme.dark":`다크`,"theme.system":`시스템`,"lang.label":`언어`,"lang.nativeName":`한국어`,"provider.name.commandCodeAuth":`Command Code - Auth`,"provider.name.commandCodeApi":`Command Code - API`,"provider.name.volcengine":`Volcengine Ark`,"provider.name.volcengineCodingPlan":`Volcengine Ark 코딩 플랜`,"provider.name.volcengineAgentPlan":`Volcengine Ark 에이전트 플랜`,"errorBoundary.title":`페이지를 불러오지 못했습니다`,"errorBoundary.message":`이 섹션을 렌더링하는 중 오류가 발생했습니다. 다시 불러와 재시도하세요.`,"errorBoundary.details":`오류`,"errorBoundary.reload":`다시 불러오기`,"startup.title":`시작 안전성`,"startup.subtitle":`재부팅 후 로컬 프록시 라우팅이 재연결 반복으로 이어지기 전에 Codex가 opencodex에 연결될 수 있는지 확인합니다.`,"startup.refresh":`새로고침`,"startup.backToDashboard":`대시보드로 돌아가기`,"startup.loading":`시작 보호 상태 확인 중…`,"startup.error":`시작 보호 상태를 읽지 못했습니다.`,"startup.staleData":`최신 시작 상태 확인에 실패했습니다. 아래 값은 이전 결과이며 보호 증거로 사용하면 안 됩니다.`,"startup.status.native":`네이티브 라우팅`,"startup.status.protected":`재부팅 보호됨`,"startup.status.atRisk":`조치 필요`,"startup.summary.native":`Codex가 로컬 프록시에 의존하지 않습니다`,"startup.summary.protected":`재부팅 후에도 opencodex가 자동으로 준비됩니다`,"startup.summary.atRisk":`재부팅 후 Codex 모델 연결이 끊길 수 있습니다`,"startup.riskDetail":`Codex는 로컬 프록시를 바라보지만 이를 다시 시작할 영구 서비스나 정상 launcher shim이 없습니다.`,"startup.riskDetailCustomLocal":`Codex가 사용자 지정 로컬 게이트웨이를 바라봅니다. opencodex는 해당 게이트웨이의 재시작 수명주기를 관리하거나 검증할 수 없습니다.`,"startup.riskDetailWindowsShim":`Launcher shim은 지원되는 CLI 스크립트만 보호하며 Windows의 Codex Desktop과 직접 codex.exe 실행은 이를 우회할 수 있습니다.`,"startup.safeDetail":`현재 라우팅과 시작 방식이 일치합니다. 재부팅 후 ocx start를 수동으로 실행할 필요가 없습니다.`,"startup.routing":`Codex 라우팅`,"startup.routing.proxy":`로컬 프록시`,"startup.routing.native":`OpenAI 네이티브`,"startup.routing.customLocal":`사용자 지정 로컬 게이트웨이`,"startup.routing.customRemote":`사용자 지정 원격 게이트웨이`,"startup.routing.unknown":`알 수 없거나 잘못된 라우팅`,"startup.restartProtection":`재부팅 보호`,"startup.preference":`필요 시 자동 시작`,"startup.enabled":`켜짐`,"startup.disabled":`꺼짐`,"startup.protection.service":`백그라운드 서비스`,"startup.protection.shim":`Launcher shim`,"startup.protection.none":`설치되지 않음`,"startup.details":`보호 상태 상세`,"startup.service":`백그라운드 서비스`,"startup.serviceHint":`로그인할 때 시작하고 프록시가 중단되면 다시 실행합니다.`,"startup.installed":`설치됨`,"startup.notInstalled":`설치되지 않음`,"startup.unsupported":`지원되지 않음`,"startup.shim":`Codex launcher shim`,"startup.shimHint":`지원되는 Codex 스크립트 런처가 시작될 때 ocx ensure를 실행합니다.`,"startup.healthy":`정상`,"startup.cliOnly":`CLI 전용`,"startup.stale":`업데이트 필요`,"startup.viable":`사용 가능`,"startup.unhealthy":`설치됐지만 비정상`,"startup.conflict":`서비스 충돌`,"startup.installedDisabled":`설치됐지만 꺼짐`,"startup.install":`설치하기`,"startup.installing":`설치 중…`,"startup.repair":`복구`,"startup.repairing":`복구 중…`,"startup.serviceInstalled":`백그라운드 서비스를 설치했습니다.`,"startup.serviceRepaired":`백그라운드 서비스를 복구했습니다.`,"startup.shimInstalled":`Codex launcher shim을 설치했습니다.`,"startup.shimRepaired":`Codex launcher shim을 복구했습니다.`,"startup.installFailed":`설치하지 못했습니다:`,"startup.tray.title":`Windows 시스템 트레이`,"startup.tray.hint":`로그인할 때 트레이 아이콘을 띄우고 프록시 시작·중지·재시작·대시보드·상태를 클릭으로 제어합니다.`,"startup.tray.login":`Windows 로그인 시 트레이 시작`,"startup.tray.notProtection":`트레이는 제어 화면이며 재부팅 보호 서비스가 아닙니다. 무인 복구에는 정상 백그라운드 서비스가 별도로 필요합니다.`,"startup.tray.running":`실행 중`,"startup.tray.stopped":`설치됨, 숨김`,"startup.tray.stale":`복구 필요`,"startup.tray.notInstalled":`설치되지 않음`,"startup.tray.loading":`확인 중…`,"startup.tray.unavailable":`상태 확인 불가`,"startup.tray.install":`트레이 설치 및 표시`,"startup.tray.start":`트레이 아이콘 표시`,"startup.tray.stop":`트레이 아이콘 종료`,"startup.tray.uninstall":`로그인 트레이 제거`,"startup.tray.error":`Windows 트레이 작업에 실패했습니다. ocx tray status에서 상세 내용을 확인하세요.`,"startup.recovery":`복구 방법`,"startup.recoveryHint":`위의 원클릭 설치를 사용하거나 수동 복구 명령을 복사할 수 있습니다. Codex Desktop과 Windows 실행 파일에는 백그라운드 서비스를 권장합니다.`,"startup.command.service":`권장: 영구 백그라운드 서비스`,"startup.command.shim":`대안: CLI launcher shim`,"startup.command.native":`안전 전환: Codex 네이티브 라우팅 복구`,"startup.copy":`복사`,"startup.copied":`복사됨`,"startup.recommended":`권장 복구 명령: {cmd}`,"startup.navRisk":`시작 보호 상태에 조치가 필요합니다`,"startup.codexRuntime.clampHidden":`OpenCodex가 Codex {version}을(를) 사용해 일부 reasoning effort 옵션이 숨겨졌습니다.`,"startup.codexRuntime.clampHiddenWithEfforts":`OpenCodex가 Codex {version}을(를) 사용해 일부 reasoning effort 옵션이 숨겨졌습니다(제거됨: {efforts}).`,"startup.codexRuntime.olderBinary":`OpenCodex가 더 오래된 Codex 바이너리({version})를 사용 중입니다. 더 새 설치를 사용할 수 있습니다.`,"dash.subtitle":`로컬 opencodex 프록시와 프로바이더, 그리고 Codex로 라우팅되는 모델의 실시간 상태입니다.`,"dash.workspace.overview":`개요`,"dash.workspace.sections":`섹션`,"dash.status":`상태`,"dash.online":`온라인`,"dash.offline":`오프라인`,"dash.version":`버전`,"dash.uptime":`가동 시간`,"dash.providers":`프로바이더`,"dash.tokens30d":`토큰 (30일)`,"dash.coverage":`커버리지 {pct}`,"dash.mem.title":`메모리 관찰`,"dash.mem.hint":`읽기 전용 런타임 진단. 관측 메모리는 max(RSS, external, ArrayBuffers)라 Windows working set trimming이 커밋된 보존 메모리를 숨기지 못합니다.`,"dash.mem.rss":`상주 메모리 (RSS)`,"dash.mem.jsHeap":`JS 힙 사용량`,"dash.mem.jsHeapArena":`아레나 {total}`,"dash.mem.pressure":`경고 임계값 대비`,"dash.mem.pressureOf":`임계값의 {pct}%`,"dash.mem.pressureUnknown":`임계값 정보 없음`,"dash.mem.jscHeap":`JSC 힙`,"dash.mem.external":`External`,"dash.mem.arrayBuffers":`ArrayBuffers`,"dash.mem.observed":`관측값`,"dash.mem.runtime":`런타임 카운터`,"dash.mem.growth":`시간당 관측 변화`,"dash.mem.perHour":`/시간`,"dash.mem.store":`연속 응답 저장소`,"dash.mem.storeHint":`프록시 previous_response_id 캐시. 힙이 증가하는 가운데 총 바이트가 늘면 런타임 할당기보다 대화 보존을 가리킵니다.`,"dash.mem.storeEntries":`항목`,"dash.mem.storeTotal":`합계`,"dash.mem.storeLargest":`최대`,"dash.mem.storeOldest":`가장 오래됨`,"dash.mem.threshold":`경고 임계값`,"dash.mem.lastWarn":`마지막 경고`,"dash.mem.never":`없음`,"dash.mem.details":`상세 정보`,"dash.mem.unavailable":`메모리 진단을 사용할 수 없음 (구버전 프록시).`,"dash.mem.inFlight":`진행 중 요청`,"dash.mem.restart":`작업 완료 후 재시작`,"dash.mem.restartConfirm":`진행 중 요청 {count}개가 끝날 때까지 기다린 뒤 재시작합니다(최대 {seconds}초; 시간이 지나면 남은 요청은 중단됩니다).`,"dash.mem.draining":`요청 {count}개 완료 대기 중… 끝나면 재시작`,"dash.mem.reconnecting":`프록시 재시작 중… 다시 연결하는 중`,"dash.mem.restartFailed":`작업 완료 후 재시작에 실패했습니다. 프록시가 실행 중인지 확인하세요.`,"dash.mem.restartNoSupervisor":`재시작 보호가 없습니다. 재시작 후 프록시가 자동으로 올라오지 않을 수 있습니다.`,"dash.activeProviders":`활성 프로바이더`,"dash.noProviders":`설정된 프로바이더가 없습니다. {cmd} 를 실행하세요.`,"dash.col.name":`이름`,"dash.col.adapter":`어댑터`,"dash.col.baseUrl":`Base URL`,"dash.col.model":`모델`,"dash.modelsNoResults":`검색과 일치하는 모델이 없습니다.`,"dash.availableModels":`사용 가능한 모델`,"dash.noModels":`모델을 찾을 수 없습니다. 프로바이더 API 키를 확인하세요.`,"dash.cannotConnect":`프록시에 연결할 수 없습니다. 실행 중인가요?`,"dash.runStart":`{cmd} 를 실행해 프록시를 시작하세요.`,"dash.stop":`프록시 중지`,"dash.stopConfirm":`프록시를 중지하고 Codex 원본 설정을 복원할까요?`,"dash.stopFailed":`프록시를 중지하지 못했습니다 (HTTP {status}).`,"dash.maSwitchFailed":`모드 전환에 실패했습니다 (HTTP {status}).`,"dash.maNetworkError":`네트워크 오류 — 프록시가 실행 중인지 확인하세요.`,"dash.stopping":`중지 중…`,"dash.actions":`프록시`,"dash.codexRestart":`Codex 모델 목록 새로고침`,"dash.codexRestarting":`종료하는 중…`,"dash.codexRestartConfirm":`Codex app-server를 종료해 모델 목록을 다시 읽게 할까요? 진행 중인 Codex 작업이 끊기고, Codex가 저절로 다시 뜨지는 않으니 끝나면 직접 열어야 합니다.`,"dash.codexRestartDone":`Codex app-server {count}개를 종료했습니다. Codex를 다시 열면 최신 모델 목록이 보입니다.`,"dash.codexRestartNothing":`실행 중인 Codex app-server가 없습니다. 다음 실행 때 최신 목록을 읽습니다.`,"dash.codexRestartUnknown":`프로세스 목록을 읽지 못해 아무것도 종료하지 않았습니다.`,"dash.codexRestartPartial":`app-server {count}개가 종료되지 않았습니다. 모델 목록이 최신 상태로 바뀌지 않으면 직접 종료하세요.`,"dash.codexRestartFailed":`Codex 모델 목록을 새로고침하지 못했습니다 (HTTP {status}).`,"dash.codexRestartUnreachable":`프록시에 연결하지 못했습니다.`,"dash.codexRestartMalformed":`프록시가 예상과 다른 응답을 보냈습니다.`,"dash.codexRestartTimeout":`프록시가 제때 응답하지 않았습니다. app-server를 계속 종료하는 중일 수 있습니다.`,"models.staleBanner":`Codex가 이 카탈로그보다 오래된 모델 목록을 보여주고 있습니다. Codex를 재시작하면 새로 읽습니다.`,"dash.codexAutoStart":`Codex 실행 시 opencodex 시작`,"dash.codexAutoStartHint":`설치된 launcher shim이 ocx ensure를 실행하도록 허용합니다. 이 설정은 재부팅 보호를 설치하지 않으므로 시작 안전성에서 실제 상태를 확인하세요.`,"dash.searchModel":`서치 사이드카 모델`,"dash.searchModelHint":`비-OpenAI 라우팅 모델의 web_search에 사용되는 모델입니다. ChatGPT 로그인 필요.`,"dash.searchReasoning":`서치 추론 강도`,"dash.visionModel":`비전 사이드카 모델`,"dash.visionModelHint":`텍스트 전용 라우팅 모델에 이미지를 설명하는 데 사용되는 모델입니다. ChatGPT 로그인 필요.`,"dash.webSearchSidecar":`웹 검색 사이드카`,"dash.webSearchSidecarHint":`라우팅 모델의 웹 검색에 쓸 백엔드와 모델을 고릅니다.`,"dash.webSearchStream":`응답 실시간 스트리밍`,"dash.webSearchStreamHint":`모델이 도구 호출을 결정할 때까지 앞부분 텍스트와 추론을 실시간 스트리밍합니다. 이후는 검색 가로채기를 위해 버퍼링됩니다. 검색 전 텍스트가 일부 반복될 수 있습니다.`,"dash.visionSidecar":`비전 사이드카`,"dash.visionSidecarHint":`텍스트 전용 라우팅 모델이 이미지를 읽을 때 쓸 백엔드와 모델을 고릅니다.`,"dash.visionOff":`끔`,"dash.shadowCallIntercept":`쉐도우 호출 가로채기`,"dash.shadowCallInterceptHint":`Codex 앱이 제목·커밋 메시지 생성에 쓰는 백그라운드 호출({models})을 가로채 선택한 모델로 바꿉니다.`,"dash.shadowCallWarning":`⚠ 활성화하면 {models} 요청이 모두 선택한 모델로 대체됩니다.`,"dash.shadowCallOriginal":`원본`,"dash.shadowCallModel":`대체 모델`,"dash.shadowCallTooltip":`Codex 앱은 스레드 제목 자동 생성, 커밋 메시지 생성, 스킬 오케스트레이션 같은 내부 작업을 백그라운드로 호출합니다. 이때 쓰는 모델은 클라이언트 버전마다 달라서 opencodex는 {models}를 모두 가로챕니다. 이 설정을 켜면 해당 호출이 선택한 모델로 넘어갑니다.`,"models.shadowCallIntercept":`쉐도우 호출 가로채기`,"models.shadowCallInterceptHint":`Codex 앱의 백그라운드 호출({models}, 제목·커밋 메시지)을 가로채 선택한 모델로 바꿉니다.`,"dash.sidecarBackend":`백엔드`,"dash.sidecarModel":`모델`,"dash.backendAuto":`자동`,"dash.backendOpenAI":`OpenAI`,"dash.backendAnthropic":`Anthropic`,"dash.sidecarSaved":`사이드카 설정이 저장됐습니다. 다음 요청부터 적용됩니다.`,"dash.sidecarSaveFailed":`사이드카 설정 저장에 실패했습니다.`,"dash.injectionLabel":`서브에이전트 위임`,"dash.injectionHint":`Codex가 서브에이전트에게 일을 넘길 때 쓸 모델을 고릅니다. 이 선택을 어디에 적용할지는 아래 두 스위치가 정합니다.`,"dash.injectionManage":`설정 열기`,"dash.syncCodexSubagentDefaults":`Codex 설정에도 기본값으로 저장`,"dash.syncCodexSubagentDefaultsHint":`켜면 위에서 고른 모델이 Codex 설정 파일에 저장돼, 새로 시작하는 작업도 처음부터 그 모델을 씁니다. 끄면 여기서만 기억합니다. 반영은 다음 동기화나 재시작 때이고, 직접 적어둔 [agents] 설정은 그대로 둡니다.`,"dash.multiAgentGuidance":`일 나누는 방법 알려주기`,"dash.multiAgentGuidanceHint":`Codex에게 "일을 이렇게 나눠 맡기면 된다"는 짧은 쪽지를 붙여 보냅니다. v2에서는 쓸 수 있는 모델 목록과 우선 모델을 알려주고, v1에서는 추론 강도가 max나 ultra일 때만 동작합니다. 끄면 아무 쪽지도 붙지 않습니다.`,"dash.injectionNone":`없음`,"dash.injectionEffortLabel":`추론 강도`,"dash.injectionEffortNone":`모델 기본값`,"dash.effortCapLabel":`V2 ultra 추론 강도 제한`,"dash.subagentEffortCapLabel":`V2 서브에이전트 추론 강도 제한`,"dash.effortCapHelp":`V2 ultra 모드 턴의 추론 강도를 제한합니다. 설정하면 ultra 모드에서 들어오는 max 요청이 선택한 수준으로 내려갑니다. 서브에이전트 제한은 스폰된 자식 에이전트에만 적용됩니다. 강도를 낮추기만 하고 올리지는 않습니다. 모델이 해당 수준을 지원하지 않으면 가장 가까운 지원 수준으로 내려갑니다.`,"dash.effortCapNone":`상한 없음`,"dash.maintenance":`유지보수`,"dash.maintenanceHint":`Codex 모델 카탈로그를 새로고침하거나 최신 opencodex 릴리스를 설치합니다.`,"dash.syncModels":`모델 동기화`,"dash.syncModelsHint":`연결해둔 프로바이더를 기준으로 Codex 모델 카탈로그를 다시 씁니다.`,"dash.syncRun":`지금 동기화`,"dash.syncing":`동기화 중…`,"dash.syncOk":`동기화 완료. {count}개 모델이 추가됐습니다.`,"dash.syncStaleHint":`Codex에 여전히 예전 목록이 보이면 오래 실행 중인 app-server를 재시작하세요 ({cmd}).`,"dash.syncFailed":`동기화 실패: {error}`,"dash.projectConfigTitle":`프로젝트 Codex 설정이 OpenCodex를 우회합니다`,"dash.projectConfigHint":`저장소 로컬 설정이 OpenCodex 프록시를 덮어씁니다(예: OpenCode Go로 직접 라우팅). 해당 프로젝트에서 ~/.codex/config.toml 프록시를 쓰려면 제거하세요.`,"dash.checkUpdate":`업데이트 확인`,"dash.updateTitle":`opencodex 업데이트`,"dash.updateDesc":`선택한 채널의 npm 최신 버전을 확인한 뒤, 설치 후 프록시를 재시작할지 선택합니다.`,"dash.updateChannel":`채널`,"dash.updateChecking":`업데이트 확인 중…`,"dash.updateInstalled":`설치됨`,"dash.updateLatest":`최신`,"dash.updateAvailable":`업데이트 가능`,"dash.updateCurrent":`최신 상태`,"dash.updateCommand":`명령`,"dash.updateSource":`현재는 소스 체크아웃입니다. 표시된 명령을 터미널에서 실행해 업데이트하세요.`,"dash.updateUnavailable":`npm에서 최신 버전을 읽지 못했습니다. 잠시 후 다시 시도하세요.`,"dash.updateRetry":`재시도`,"dash.updateRecheck":`다시 확인`,"dash.updateCannotAuto":`원클릭 업데이트를 사용할 수 없습니다 ({reason}).`,"dash.updateReason.source_checkout":`소스 체크아웃`,"dash.updateReason.latest_unavailable":`npm 레지스트리에 연결할 수 없음`,"dash.updateReason.already_latest":`이미 최신 버전`,"dash.updateReason.unknown":`업데이트 불가`,"dash.updateRestart":`업데이트 후 재시작`,"dash.updateRestartHint":`권장. 프록시를 재시작하기 전까지 현재 GUI는 이전 코드로 계속 실행됩니다.`,"dash.runUpdate":`업데이트`,"dash.updateReconnecting":`재시작된 프록시를 기다리는 중…`,"dash.updateStatus.running":`opencodex 업데이트 중입니다.`,"dash.updateStatus.restarting":`업데이트 설치 완료. 프록시를 재시작하는 중입니다.`,"dash.updateStatus.succeeded":`업데이트가 완료됐습니다.`,"dash.updateVersionTransition":`{currentVersion} -> {latestVersion}.`,"dash.updateStatus.failed":`업데이트에 실패했습니다.`,"prov.subtitle":`opencodex가 Codex로 라우팅하는 업스트림 프로바이더를 설정합니다. 계정으로 로그인하거나, 프로바이더를 추가하거나, 원본 설정을 편집하세요.`,"prov.add":`프로바이더 추가`,"prov.editJson":`JSON 편집`,"prov.accountLogin":`계정 로그인`,"prov.noOauth":`사용 가능한 OAuth 프로바이더가 없습니다.`,"prov.loggedIn":`로그인됨`,"prov.notLoggedIn":`로그인 안 됨`,"prov.logout":`로그아웃`,"prov.login":`로그인`,"prov.loginWith":`{provider} 로 로그인`,"prov.waitingBrowser":`브라우저 대기 중…`,"prov.didntOpen":`안 열렸나요? 여기를 클릭하세요`,"prov.copyLink":`링크 복사`,"prov.dontOpenBrowser":`프록시가 실행 중인 컴퓨터에서 브라우저를 열지 않기`,"prov.dontOpenBrowserHint":`다른 브라우저 프로필로 로그인하거나, 대시보드를 프록시와 다른 컴퓨터에서 쓸 때 유용합니다.`,"prov.linkCopied":`복사됨`,"prov.linkCopyUnavailable":`클립보드를 사용할 수 없음`,"prov.deviceCode":`기기 인증 코드`,"prov.copyCode":`코드 복사`,"prov.codeCopied":`코드 복사됨`,"prov.editAlias":`별칭 편집`,"prov.aliasPrompt":`표시 이름 (비우면 삭제)`,"prov.aliasSaved":`별칭이 저장되었습니다`,"prov.aliasSaveFailed":`별칭을 저장하지 못했습니다`,"prov.accountId":`ID`,"prov.pasteRedirect":`리다이렉트 URL 또는 코드 붙여넣기`,"prov.pasteRedirectHint":`브라우저에 localhost 오류가 표시되면, 주소창의 전체 URL을 복사해 여기에 붙여넣으세요(또는 인증 코드 붙여넣기).`,"prov.pasteSubmit":`제출`,"prov.pasteSubmitting":`제출 중…`,"prov.pasteOk":`코드를 제출했습니다 — 로그인 완료 중…`,"prov.pasteFail":`코드 제출 실패: {error}`,"prov.port":`포트`,"prov.default":`기본값`,"prov.loadingConfig":`불러오는 중…`,"prov.saved":`저장됨! 적용하려면 프록시를 재시작하세요.`,"prov.loadConfigFail":`설정을 불러오지 못했습니다`,"prov.invalidJson":`잘못된 JSON`,"prov.saveFailed":`저장 실패`,"prov.loginFailStart":`{provider} 로그인을 시작하지 못했습니다`,"prov.loginError":`{provider} 로그인 오류: {error}`,"prov.loginRequestFail":`{provider} 로그인 요청 실패`,"prov.loginCancelled":`{provider} 로그인이 취소되었습니다`,"prov.loginTimeout":`{provider} 로그인 시간 초과 — 브라우저를 닫았거나 완료되지 않았습니다. 다시 시도하세요.`,"prov.loginOk":`{provider} 에 로그인했습니다. 모델을 표시하려면 {cmd} 를 실행하세요(또는 실시간 적용됩니다).`,"prov.loginSameAccount":`같은 {provider} 계정입니다. 브라우저에서 계정을 전환한 뒤 계정 추가를 다시 시도하세요.`,"oauthTos.highTitle":`{provider}: 구독 OAuth 위험`,"oauthTos.elevatedTitle":`{provider}: 비공식 OAuth 브리지`,"oauthTos.anthropicBody":`Claude 구독 OAuth 토큰을 OpenCodex 같은 타사 프록시에서 직접 재사용하는 방식은 Anthropic이 지원하는 통합이 아니며 접근이 제한될 수 있습니다. Claude 구독을 사용하는 공식 Agent SDK 통합은 별도입니다.`,"oauthTos.highBody":`OpenCodex는 {provider}를 타사 OAuth 경로로 연결합니다. 지원되지 않는 사용 방식이면 접근이 제한되거나 정지될 수 있습니다.`,"oauthTos.elevatedBody":`OpenCodex는 {provider}를 비공식 OAuth 경로로 연결합니다. 가능하면 공식 클라이언트를 사용하세요. 비정상적이거나 자동화된 트래픽은 남용으로 간주되어 접근이 제한되거나 정지될 수 있습니다.`,"oauthTos.saferPath":`더 안전한 방법: OpenCodex에 API 키를 대신 설정하세요.`,"oauthTos.acknowledge":`위험을 이해했으며 OAuth로 계속 진행합니다.`,"oauthTos.continue":`OAuth로 계속`,"prov.logoutOk":`{provider} 에서 로그아웃했습니다.`,"prov.logoutFail":`{provider}에서 로그아웃하지 못했습니다. 계정 상태는 그대로입니다.`,"prov.removed":`"{name}" 을(를) 삭제했습니다.`,"prov.removedDefault":`"{name}"을(를) 삭제했습니다. 이제 기본 프로바이더는 "{defaultProvider}"입니다.`,"prov.removeFail":`"{name}" 삭제에 실패했습니다.`,"prov.removeLastProvider":`활성화된 다른 프로바이더가 기본이 될 수 없으면 이 프로바이더를 삭제할 수 없습니다.`,"prov.removeHasDependentCombos":`먼저 이 프로바이더를 사용하는 콤보를 삭제하거나 수정하세요: {combos}.`,"prov.setDefault":`기본으로 설정`,"prov.setDefaultSuccess":`"{name}"이(가) 기본 프로바이더로 설정되었습니다.`,"prov.setDefaultFail":`"{name}"을(를) 기본 프로바이더로 설정하지 못했습니다.`,"prov.defaultDisabled":`기본으로 설정하려면 먼저 이 프로바이더를 활성화하세요.`,"prov.updateFail":`이 프로바이더를 업데이트하지 못했습니다.`,"prov.networkError":`네트워크 오류입니다. 프록시가 실행 중인지 확인한 후 다시 시도하세요.`,"prov.added":`"{name}" 을(를) 추가했습니다. 지금 활성화됨 — Codex 모델 선택기에 표시하려면 {cmd} 를 실행하세요(또는 재시작).`,"prov.removeConfirm":`프로바이더 "{name}" 을(를) 삭제할까요? 해당 모델이 Codex 선택기에서 사라집니다.`,"prov.hasApiKey":`API 키 설정됨`,"prov.hasHeaders":`커스텀 헤더 설정됨`,"prov.accounts":`계정 ({n})`,"prov.accountsAria":`{name} 계정 목록 열기/닫기`,"prov.accountActive":`활성`,"prov.accountReauth":`재로그인`,"prov.reauthenticate":`재인증`,"prov.reauthAccountMissing":`로그인 후 선택한 계정을 찾을 수 없습니다`,"prov.reauthIdentityMismatch":`로그인한 계정이 선택한 계정과 일치하지 않습니다`,"prov.accountAdd":`계정 추가`,"prov.accountNoLabel":`계정 {id}`,"prov.accountSwitchTitle":`이 계정 사용`,"prov.accountSwitched":`{email} 계정으로 전환했습니다.`,"prov.accountSwitchFail":`계정 전환에 실패했습니다`,"prov.accountRemoved":`{email} 계정을 제거했습니다.`,"prov.accountRemoveFail":`{email} 계정을 제거하지 못했습니다. 계정은 그대로입니다.`,"prov.accountRemoveAria":`{email} 제거`,"prov.accountRemoveConfirm":`{email} 계정을 제거할까요? 이 프록시에서 로그인이 삭제됩니다.`,"prov.keyAdd":`API 키 추가`,"prov.keyAdded":`{name}에 API 키를 추가했습니다.`,"prov.keyAddFail":`API 키 추가에 실패했습니다`,"prov.keyPlaceholder":`API 키 붙여넣기`,"prov.keySwitchTitle":`이 키 사용`,"prov.keySwitched":`{key} 키로 전환했습니다.`,"prov.keySwitchFail":`키 전환에 실패했습니다`,"prov.keyRemoved":`{key} 키를 제거했습니다.`,"prov.keyRemoveAria":`{key} 키 제거`,"prov.keyRemoveConfirm":`API 키 {key}를 제거할까요? 이 프록시 설정에서 삭제됩니다.`,"prov.activeBadge":`활성`,"prov.disabledBadge":`비활성`,"prov.defaultBadge":`기본`,"prov.enable":`활성화`,"prov.disable":`비활성화`,"prov.enabled":`"{name}" 을(를) 활성화했습니다. 해당 모델을 다시 Codex에서 사용할 수 있습니다.`,"prov.disabled":`"{name}" 을(를) 비활성화했습니다. 설정은 유지되고 모델은 숨겨집니다.`,"prov.enableFail":`"{name}" 활성화에 실패했습니다.`,"prov.disableFail":`"{name}" 비활성화에 실패했습니다.`,"prov.enableAria":`{name} 프로바이더 활성화`,"prov.disableAria":`{name} 프로바이더 비활성화`,"prov.defaultCannotDisable":`기본 프로바이더는 비활성화할 수 없습니다`,"prov.openaiAccountMode":`Codex 계정 모드`,"prov.openaiModePool":`풀`,"prov.openaiModeDirect":`직접`,"prov.openaiPoolDesc":`기본값입니다. 메인 로그인과 추가 계정을 친화도, 할당량, 대기 시간, 장애 조치에 따라 순환합니다.`,"prov.openaiDirectDesc":`현재 메인 Codex 로그인만 사용합니다. 저장된 풀 계정은 읽거나 순환하지 않습니다.`,"prov.openaiModeSaved":`OpenAI 계정 모드를 {mode} 모드로 변경했습니다.`,"prov.openaiModeSaveFailed":`OpenAI 계정 모드를 변경하지 못했습니다.`,"prov.openaiApiDesc":`OpenAI API 키만 사용하며 Codex 계정 인증과 섞이지 않습니다.`,"prov.manageCodexAccounts":`Codex 계정 관리`,"prov.openaiApiMissing":`API 키 필요`,"prov.openaiApiSetup":`API 키 설정`,"models.tab.catalog":`모델`,"models.tab.combos":`콤보`,"models.tab.compatibility":`호환성`,"models.tab.routing":`라우팅 (beta)`,"models.tabsLabel":`모델 표면`,"models.subtitle.combos":`여러 모델을 하나의 id로 묶어 순서대로 응답하게 합니다. failover로 대상을 연결하거나 분산 전략으로 부하를 나눕니다.`,"models.subtitle.compatibility":`랩 프로젝션 증거의 읽기 전용 호환성 판정 행렬.`,"models.subtitle.routing":`정책 프로필, dry-run 평가, 그리고 근거가 남는 라우팅 분석입니다.`,"models.subtitle":`Codex가 보는 모델을 켜고 끕니다 — 네이티브 GPT passthrough와 라우팅된 모델을 프로바이더별로 묶어 보여줍니다(헤더를 클릭하면 접힘). 숨긴 모델은 카탈로그와 선택기에서 빠지지만 정확한 id로 직접 호출할 수 있습니다. 변경 사항은 다음 Codex 턴에 적용됩니다 — opencodex가 Codex의 5분 모델 캐시를 무효화하므로 재시작이 필요 없습니다.`,"models.nativeGroupLabel":`OpenAI 네이티브`,"models.nativeHint":"프로바이더에서 선택한 풀 또는 직접 계정 옵션으로 서빙되는 passthrough 모델입니다. 끄면 Codex 선택기에서 숨겨지고, 카탈로그 항목은 유지되므로 다시 켜면 그대로 복원됩니다. 여기서 모델을 추가하면 bare passthrough id가 아니라 라우팅된 `openai/` selector로 등록됩니다.","models.active":`{active}/{total} 표시`,"models.workspace.providers":`프로바이더`,"models.workspace.allProviders":`모든 프로바이더`,"models.workspace.mainAria":`모델 세부정보`,"models.allOn":`모두 켜기`,"models.allOff":`모두 끄기`,"models.presetLabel":`모델`,"models.presetMode_preset":`프리셋`,"models.presetMode_all":`전체`,"models.presetMode_custom":`커스텀`,"models.presetSummary":`{total}개 중 {count}개 표시 — 코어 프리셋 v{version}`,"models.presetUpdateAvailable":`프리셋 v{version} 사용 가능`,"models.presetAppliedToast":`{provider}: 프리셋 적용 — 모델 {count}개 선택`,"models.presetClearedToast":`{provider}: 모든 모델 표시`,"models.presetEmpty":`{provider}: 프리셋과 일치하는 모델이 없어 선택을 그대로 두었습니다`,"models.presetConfirmReplace":`선택한 목록을 {count}개짜리 프리셋으로 바꿀까요?`,"models.cap350k":`350k 제한`,"models.capApplied":`컨텍스트 제한 적용됨 — 다음 Codex 턴부터 반영됩니다.`,"models.capSaveFailed":`컨텍스트 제한 저장 실패`,"models.contextCapped":`350k 제한`,"models.contextCapLabel":`기본 창 / 상한`,"models.v2Label":`서브에이전트`,"models.shadowCallOriginal":`⚠ {models} →`,"models.v2Mode_v1":`v1`,"models.v2Mode_default":`base`,"models.v2Mode_v2":`v2`,"models.v2ModeDesc_v1":`전 모델 → v1 서피스`,"models.v2ModeDesc_default":`업스트림 기본값 (sol/terra=v2, luna=v1)`,"models.v2ModeDesc_v2":`전 모델 → v2 서피스`,"models.keepNativeOnV1":`ChatGPT는 v1 유지`,"models.keepNativeOnV1Hint":`ChatGPT 네이티브 부모는 v2 자식 작업을 암호화해서 Grok/Claude가 읽지 못합니다. Sol/Terra가 routed 모델을 spawn해야 하면 켜 두세요. routed 부모는 v2를 유지합니다.`,"models.v2Help":`모든 모델의 멀티에이전트 서피스를 제어합니다. + +v1: 단일 스레드 에이전트. 모든 모델이 v1 서피스를 사용합니다. +base: 업스트림 기본값 — sol/terra는 v2, luna는 v1, 나머지는 codex 플래그를 따릅니다. +v2: 멀티 스레드 에이전트(spawn_agent). 모든 모델이 v2 서피스를 사용합니다. + +v2에서 ChatGPT는 v1 유지를 켜면 Sol/Terra가 v1에 남아 Grok이나 Claude를 spawn할 수 있습니다. ChatGPT는 v2 자식 작업을 암호화하므로 routed 모델은 읽지 못합니다. routed 부모는 v2를 유지합니다. + +새 세션부터 적용됩니다.`,"models.v2DocsLink":`v1 / v2가 뭔가요?`,"dash.multiAgent":`서브에이전트`,"models.v2Conflict":`[agents] max_threads가 남아 있어 codex가 부팅을 거부합니다 — config.toml에서 제거하세요`,"models.v2Applied":`서브에이전트 모드 변경됨 — 새 세션부터 적용 (피커 갱신은 Codex 앱 재시작)`,"models.v2ThreadsLabel":`최대 스레드`,"models.v2ThreadsDefault":`기본값 (4)`,"models.v2ThreadsApplied":`스레드 한도 변경됨 — 새 세션부터 적용`,"models.v2ThreadsInvalid":`스레드 한도는 1 이상 정수여야 합니다`,"models.v2ThreadsApply":`적용`,"models.capValue":`기본 {value}`,"models.contextSettings":`사용자 지정 창`,"models.contextSettingsTitle":`사용자 지정 창 — {provider}`,"models.contextDefault":`프로바이더 기본값`,"models.contextModel":`모델`,"models.contextModelOverride":`모델별 재정의`,"models.contextHint":`이미 아는 경우 여기에 실제 Codex 컨텍스트 윈도우를 적습니다. 업스트림 값이 없으면 이 값을 쓰고, 더 큰 보고값만 낮추며, 더 작은 업스트림 컨텍스트 윈도우는 그대로 둡니다. 비우면 프로바이더의 「기본 창 / 상한」을 쓰고, 그 상한이 꺼져 있으면 128k입니다.`,"models.contextAutomatic":`자동 검색`,"models.contextSaved":`컨텍스트 윈도우가 업데이트되었습니다 — 다음 Codex 턴부터 적용됩니다.`,"models.contextUnchanged":`저장할 컨텍스트 윈도우 변경이 없습니다.`,"models.contextSaveFailed":`컨텍스트 윈도우를 저장하지 못했습니다`,"models.contextInvalid":`컨텍스트 윈도우는 양의 정수여야 합니다`,"models.contextCappedValue":`{value} 제한`,"models.setAll":`전체 적용`,"models.setAllHint":`라우팅된 모든 프로바이더에 {value} 기본 창을 켭니다. 중계가 context_window / context_length 를 주지 않으면 이 값이 실제 Codex 창이 됩니다. 모델 하나만 손으로 쓰려면 같은 줄의 「사용자 지정 창」을 쓰세요. 네이티브 프로바이더는 영향을 받지 않습니다.`,"models.collapseAll":`모두 접기`,"models.expandAll":`모두 펼치기`,"models.orderHint":`피커 순서: Subagents에서 지정한 순서 → 나머지 라우팅 모델(프로바이더, 모델 ID 순 알파벳 정렬) → 네이티브 모델. 노출 토글은 모델을 필터링할 뿐 이 순서를 바꾸지 않습니다.`,"models.custom":`직접 입력…`,"models.customApply":`적용`,"models.customPlaceholder":`토큰 (예: 420000)`,"models.customAdd":`커스텀 모델 추가`,"models.customAddTitle":`커스텀 모델 추가 — {provider}`,"models.customEditTitle":`커스텀 모델 편집 — {provider}`,"models.customAdded":`커스텀 모델 추가됨`,"models.customUpdated":`커스텀 모델 수정됨`,"models.customDeleted":`커스텀 모델 삭제됨`,"models.customSaveFailed":`커스텀 모델 저장 실패`,"models.customSaving":`저장 중…`,"models.customAddBtn":`추가`,"models.customEditBtn":`수정`,"models.customEdit":`편집`,"models.customDelete":`삭제`,"models.customDeleteConfirm":`{name} 모델을 삭제하시겠습니까?`,"models.customBadge":`커스텀`,"models.customSummary":`커스텀 {count}개`,"models.customFieldModelId":`모델 ID (엔드포인트 슬러그)`,"models.customFieldModelIdPlaceholder":`예: qwen4-max-preview`,"models.customFieldDisplayName":`표시명 (선택)`,"models.customFieldDisplayNamePlaceholder":`예: Qwen 4 Max Preview`,"models.customFieldContext":`컨텍스트 윈도우`,"models.customFieldModalities":`입력 모달리티`,"models.customFieldReasoning":`추론 노력`,"models.customFieldReasoningOverride":`추론 노력 재정의`,"models.reasoningEffort.none":`없음`,"models.reasoningEffort.minimal":`최소`,"models.reasoningEffort.low":`낮음`,"models.reasoningEffort.medium":`중간`,"models.reasoningEffort.high":`높음`,"models.reasoningEffort.xhigh":`매우 높음`,"models.reasoningEffort.max":`최대`,"models.tipProvider":`프로바이더`,"models.tipContext":`컨텍스트`,"models.tipModalities":`모달리티`,"models.tipStatus":`상태`,"models.tipActive":`활성`,"models.tipDisabled":`비활성`,"models.applied":`적용됨 — 다음 Codex 턴부터 반영됩니다.`,"models.saveFailed":`저장 실패`,"models.networkError":`네트워크 오류 — 프록시가 실행 중인가요?`,"models.loadFail":`모델을 불러오지 못했습니다 — 프록시가 실행 중인가요?`,"models.noRouted":`라우팅된 모델 없음`,"models.noRoutedHint":`먼저 프로바이더에 로그인하거나 추가하세요.`,"models.emptyDiscovery":`발견된 모델이 없습니다. 프로바이더 엔드포인트를 확인하거나 정적/사용자 모델을 추가하세요.`,"models.emptyDiscoveryDisabled":`실시간 모델 검색이 꺼져 있고 정적 모델도 설정되지 않았습니다.`,"models.discoveryFailedBadge":`검색 실패`,"models.discoveryFailedHttp":`모델 검색에 실패했습니다(HTTP {status}).`,"models.discoveryFailedBlocked":`대상 정책 때문에 모델 검색이 차단되었습니다.`,"models.discoveryFailedInvalidResponse":`모델 검색이 잘못된 응답을 받았습니다.`,"models.discoveryFailedNetwork":`네트워크 오류로 모델 검색에 실패했습니다.`,"models.discoveryFailedProvider":`프로바이더가 모델 검색 오류를 보고했습니다.`,"models.discoveryFailedGeneric":`모델 검색에 실패했습니다.`,"models.openProviderSettings":`프로바이더 설정 열기`,"models.loading":`불러오는 중…`,"models.search":`모델 검색…`,"models.showMore":`{n}개 더 보기`,"models.allowlistLabel":`선택만 노출`,"models.allowlistHint":`체크한 모델만 카탈로그에 노출돼요 (비우면 전체). 수천 개 모델을 노출하는 프로바이더에 유용해요.`,"models.selectedCount":`{n}개 선택`,"sub.subtitle":`Codex의 {cmd} 는 우선순위 상위 5개 모델만 오버라이드로 노출합니다. 여기서 최대 5개를 선택하면 — 네이티브 gpt 또는 라우팅된 모델 — opencodex가 카탈로그 우선순위를 설정해 정확히 이들이 앞에 옵니다. 다른 모델도 정확한 이름으로 호출할 수 있으며, 이 설정은 표시 항목만 제어합니다.`,"sub.featured":`추천`,"sub.advanced":`고급`,"sub.orderHintAria":`이 순서가 쓰이는 방식`,"sub.orderHint":`여기서 선택해 표시된 순서가 Codex 모델 피커 최상단 1~5위와 {cmd}의 기본 모델 후보를 결정합니다.`,"sub.noneSelected":`선택된 항목 없음 — 아래 목록에서 선택하세요.`,"sub.models":`모델`,"sub.search":`모델 검색(네이티브 gpt + 라우팅)…`,"sub.settings":`설정`,"sub.sections":`서브에이전트 구역`,"sub.delegation.model":`먼저 부를 모델`,"sub.delegation.modelHint":`Codex가 일을 나눠 맡길 때 가장 먼저 부를 모델입니다. 위 추천 목록이 부를 수 있는 후보라면, 여기서 고른 모델이 그중 1순위가 됩니다.`,"sub.noModels":`모델 없음 — 먼저 프로바이더에 로그인하거나 추가하세요.`,"sub.saved":`{n}개 모델을 저장했습니다. spawn_agent 오버라이드로 보려면 새 Codex 세션을 시작하거나 {cmd} 를 실행하세요.`,"sub.saveFailed":`저장 실패`,"sub.networkError":`네트워크 오류 — 프록시가 실행 중인가요?`,"sub.loadFail":`모델을 불러오지 못했습니다 — 프록시가 실행 중인가요?`,"sub.loading":`불러오는 중…`,"sub.moveUp":`{m} 위로 이동`,"sub.moveDown":`{m} 아래로 이동`,"sub.removeAria":`{m} 삭제`,"sub.workspace.addToFeatured":`{m}을(를) 추천에 추가`,"sub.workspace.allModels":`모든 모델`,"sub.workspace.featuredFull":`추천 목록이 가득 찼습니다 (최대 5개)`,"sub.workspace.mainAria":`서브에이전트 모델 세부 정보`,"sub.workspace.notFeatured":`추천되지 않음`,"sub.workspace.priority":`우선순위`,"sub.workspace.removeFromFeatured":`{m}을(를) 추천에서 제거`,"sub.workspace.selectModel":`모델 선택`,"sub.workspace.selectModelDesc":`목록에서 모델을 선택하여 세부 정보를 확인하고 spawn_agent에 추천하세요.`,"sub.workspace.selector":`공개 셀렉터`,"sub.ultraMode":`울트라 모드`,"sub.ultraModeHint":`모든 모델과 reasoning effort에서 Proactive 멀티에이전트 위임 정책을 켭니다 (reasoning effort 자체는 변경하지 않음). config.toml에 features.multi_agent_v2.multi_agent_mode_hint_text를 기록합니다.`,"sub.ultraModeV2Required":`v2 멀티에이전트 서피스가 필요합니다 — 먼저 multi_agent_v2를 켜고 서브에이전트 모드에서 v2를 선택하세요.`,"sub.ultraModeText":`울트라 모드 위임 텍스트`,"sub.ultraModePreset":`프리셋 복원`,"sub.ultraModeLoadFail":`울트라 모드 설정을 불러오지 못했습니다 — 프록시가 실행 중인가요?`,"sub.ultraModeSaveFail":`울트라 모드 설정 저장에 실패했습니다`,"sub.ultraModeSaved":`울트라 모드가 저장되었습니다. 새 Codex 세션부터 적용됩니다.`,"logs.title":`요청 로그`,"logs.tabLogs":`로그`,"logs.tabDebug":`디버그`,"logs.subtitle":`로컬 opencodex 프록시를 거친 최근 요청입니다. 최신순.`,"logs.autoRefresh":`자동 새로고침`,"logs.noRequests":`아직 요청이 없습니다.`,"logs.loadError":`요청 로그를 불러오지 못했습니다.`,"logs.filter.surface.label":`표면`,"logs.filter.surface.all":`전체`,"logs.filter.surface.claude":`Claude`,"logs.filter.surface.codex":`Codex`,"logs.filter.surface.grok":`Grok`,"logs.filter.interceptedHelpersOnly":`가로챈 헬퍼만`,"logs.badge.interceptedHelper":`I · {model}`,"logs.badge.interceptedHelperTitle":`가로챈 헬퍼 요청`,"logs.filter.conversation.label":`대화`,"logs.filter.conversation.placeholder":`대화 ID 붙여넣기`,"logs.filter.conversation.clear":`지우기`,"logs.filter.model.label":`모델`,"logs.filter.model.placeholder":`모델 또는 공급자로 거르기`,"logs.filter.conversation.apply":`로그 필터`,"logs.conversation.totals":`{requests}건 요청 · {tokens} 토큰 · {cost}`,"logs.conversation.scope":`합계는 현재 로드된 Logs 링만 포함합니다.`,"logs.conversation.excluded":`(~$에서 가격 없음 {unpriced}건, 미측정 {unmetered}건 제외)`,"logs.cost.approximate":`{amount}`,"logs.cost.lowerBound":`≥{amount}`,"logs.cost.unavailable":`사용 불가`,"logs.detail.conversation":`대화`,"logs.badge.claude":`Claude`,"logs.badge.grok":`Grok`,"logs.col.time":`시간`,"logs.col.request":`요청`,"logs.col.model":`모델`,"logs.col.effort":`추론 강도`,"logs.col.provider":`프로바이더`,"logs.col.status":`상태`,"logs.col.tokens":`토큰`,"logs.col.tokPerSec":`tok/s`,"logs.col.estimatedCost":`~$`,"logs.metric.tokPerSecTitle":`전체 요청 시간 기준 초당 출력 토큰`,"logs.metric.estimatedCostTitle":`API 정가 환산치이며 실제 청구액이 아닙니다. 가격 미매칭은 표시하지 않습니다.`,"usage.cost.total":`API 정가 환산치 (이 기간)`,"usage.cost.disclaimer":`결제 영수증이 아닙니다. 구독 사용량 또는 프로바이더 크레딧이 대신 적용될 수 있습니다.`,"usage.cost.unpricedNote":`비용 산정 불가 {count}건 제외`,"logs.detail.section.basic":`기본 정보`,"logs.detail.route.section":`라우팅 결정`,"logs.detail.route.kind":`라우팅 종류`,"logs.detail.route.profile":`프로필`,"logs.detail.route.selected":`선택됨`,"logs.detail.route.candidates":`후보`,"logs.detail.route.unknown":`이 요청에 대한 라우팅 추적이 기록되지 않았습니다(추적 이전 행).`,"logs.detail.section.performance":`성능`,"logs.detail.section.cost":`API 정가 환산치`,"logs.detail.section.attempts":`Combo 시도`,"logs.detail.section.usage":`원본 usage`,"logs.detail.ttft":`TTFT`,"logs.detail.costTotal":`정가 환산치`,"logs.detail.totalTokens":`전체 토큰`,"logs.detail.matchedKey":`매칭된 가격 키`,"logs.detail.priceSource":`가격 출처`,"logs.detail.unavailableReason":`표시 불가 사유`,"logs.detail.copyRequestId":`요청 ID 복사`,"logs.detail.copied":`복사됨`,"logs.detail.source.jawcode":`jawcode 카탈로그`,"logs.detail.source.expected":`expected 가격 오버레이`,"logs.detail.source.user":`프로바이더 구성 가격 오버레이`,"logs.detail.verification.verified":`검증됨`,"logs.detail.verification.derived":`기반 모델 유도`,"logs.detail.attempt.target":`프로바이더 / 모델`,"logs.detail.attempt.reason":`결과 / 사유`,"logs.detail.attempt.completed":`완료`,"logs.detail.attempt.e2eNote":`상위 tok/s는 전체 요청 기준이며 각 시도는 자체 소요 시간을 사용합니다.`,"logs.detail.attempt.recovery.transient5xx":`일시적 5xx 오류`,"logs.detail.attempt.recovery.connectionReset":`연결이 재설정됨`,"logs.detail.attempt.recovery.oauth401":`OAuth 재인증`,"logs.detail.attempt.recovery.key429":`키 요청 한도 초과 (429)`,"logs.detail.attempt.recovery.rateLimit429":`요청 한도 초과 (429)`,"logs.detail.attempt.recovery.anthropicOauth429":`Anthropic OAuth 요청 한도 초과 (429)`,"logs.detail.attempt.recovery.image413":`이미지 페이로드가 너무 큼 (413)`,"logs.detail.attempt.recovery.emptyCompletion":`빈 응답 재시도`,"logs.detail.attempt.recovery.unknown":`알 수 없는 복구 사유`,"logs.detail.reason.usage_missing":`usage가 보고되지 않았습니다.`,"logs.detail.reason.usage_unsupported":`이 프로바이더는 usage 보고를 지원하지 않습니다.`,"logs.detail.reason.output_missing":`양수 출력 토큰 수가 보고되지 않았습니다.`,"logs.detail.reason.invalid_duration":`요청 소요 시간이 유효하지 않습니다.`,"logs.detail.reason.price_unmatched":`매칭되는 가격을 찾지 못했습니다.`,"logs.detail.reason.invalid_cache_breakdown":`캐시 토큰 상세가 전체 입력 토큰과 모순됩니다.`,"logs.detail.reason.invalid_usage":`usage에 유효하지 않은 토큰 값이 있습니다.`,"logs.detail.reason.combo_attempt_unavailable":`하나 이상의 combo 시도 비용을 계산할 수 없습니다.`,"logs.detail.estimate.usage_estimated":`프로바이더 usage가 추정치입니다.`,"logs.detail.estimate.cache_detail_missing":`캐시 상세가 없어 입력 전액을 상한으로 추정했습니다.`,"logs.detail.estimate.expected_price_overlay":`검증된 expected 정가를 사용했습니다.`,"logs.detail.estimate.provider_cost_overlay":`프로바이더 구성 가격 오버레이를 사용했습니다.`,"logs.detail.estimate.priority_lower_bound":`확인된 Priority 가격을 사용할 수 없어 표시된 추정치는 알려진 하한입니다.`,"logs.col.error":`오류`,"logs.col.upstreamReason":`업스트림 원인`,"logs.col.duration":`소요 시간`,"logs.modelTooltip.model":`모델`,"logs.modelTooltip.resolvedModel":`해석된 모델`,"logs.modelTooltip.requestedTier":`요청 티어`,"logs.modelTooltip.configuredTier":`설정 티어`,"logs.modelTooltip.responseTier":`응답 티어`,"logs.modelTooltip.supportsTier":`티어 지원`,"logs.tokens.reported":`측정됨`,"logs.tokens.unreported":`미보고`,"logs.tokens.unsupported":`미지원`,"logs.tokens.estimated":`추정`,"logs.tokens.input":`입력`,"logs.tokens.output":`출력`,"logs.tokens.cacheRead":`캐시 히트 (c)`,"logs.tokens.cacheWrite":`캐시 생성 (w)`,"logs.tokens.reasoning":`추론`,"logs.tokens.noCache":`캐시 미보고`,"logs.tokens.contextTotal":`활성 컨텍스트`,"logs.tokens.noCacheNote":`이 프로바이더는 캐시 토큰 수치를 제공하지 않습니다`,"logs.tokens.noCacheCursor":`Cursor 캐시 상세 미보고`,"logs.tokens.noCacheCursorNote":`Cursor 프로토콜은 캐시 read/write 토큰 수치를 제공하지 않습니다. 캐시 미스가 확인됐다는 뜻은 아닙니다`,"logs.tokens.estimatedNote":`추정치 (프로바이더가 정확한 사용량을 제공하지 않음)`,"logs.details":`상세보기`,"logs.detailTitle":`요청 상세`,"logs.detailRaw":`원본 로그`,"debug.title":`디버그`,"debug.subtitle":`선택적 provider transport 및 usage 추출 진단. 요청 오류와 502는 로그 탭에 표시됩니다.`,"debug.debug":`Provider debug`,"debug.usage":`Usage 추출`,"debug.injection":`주입 로그`,"debug.claude":`Claude 인바운드`,"debug.claudeInbound.title":`Claude 인바운드 요청`,"debug.claudeInbound.sub":`Claude Code/Desktop이 실제로 보내는 값(thinking, effort, metadata)을 보여줍니다 — 프롬프트 원문은 저장하지 않습니다.`,"debug.claudeInbound.empty":`아직 캡처된 요청이 없습니다. 켜진 상태에서 Claude로 메시지를 보내보세요.`,"debug.claudeInbound.time":`시간`,"debug.claudeInbound.endpoint":`엔드포인트`,"debug.claudeInbound.model":`모델`,"debug.claudeInbound.none":`없음`,"debug.reset":`런타임 재정의 해제`,"debug.refresh":`새로고침`,"debug.follow":`Follow`,"debug.streamProvider":`Provider`,"debug.streamUsage":`Usage`,"debug.streamInjection":`Injection`,"debug.loading":`디버그 설정 로딩 중…`,"debug.loadFailed":`디버그 설정을 불러오지 못했습니다.`,"debug.emptyTitle":`디버그 로깅 꺼짐`,"debug.empty":`위 카드에서 Provider debug 또는 Usage extraction을 켜세요. 프록시로 요청을 보낸 뒤 라인이 표시됩니다.`,"debug.noLinesTitle":`라인 대기 중`,"debug.noLines.provider":`공급자 디버그는 켜져 있지만 전송 이상(드롭되거나 잘못된 프레임, Cursor dial/retry 이벤트)만 기록합니다. Anthropic 같은 공급자로의 정상 요청은 라인을 생성하지 않을 수 있습니다.`,"debug.noLines.usage":`사용량 추출은 켜져 있지만 아직 캡처된 항목이 없습니다. Codex로 요청을 보내면 여기에 표시됩니다.`,"debug.noLines.injection":`주입 로그는 켜져 있지만 아직 캡처된 항목이 없습니다. Collab 및 서브 에이전트 턴의 멀티 에이전트 가이던스 주입과 effort-cap 결정을 기록합니다.`,"usage.title":`사용량`,"usage.subtitle":`프록시의 로컬 토큰 집계입니다. 누락된 사용량은 0으로 표시하지 않습니다.`,"usage.loading":`사용량 데이터를 불러오는 중…`,"usage.empty":`아직 기록된 사용량이 없습니다. 프록시로 요청을 보내면 여기에 표시됩니다.`,"usage.loadError":`사용량 데이터를 불러오지 못했습니다.`,"usage.range.all":`전체`,"usage.range.available":`사용 가능한 기록`,"usage.historyTruncated":`이전 사용 기록을 불러오지 않아 합계는 사용 가능한 기록만 포함합니다.`,"usage.historyTruncatedWindow":`불러온 기록의 요청 시작 시각은 {start}부터 {end} 사이입니다. 읽기 한도 때문에 파일 앞부분의 기록이 빠졌으므로 선택한 기간이 완전하지 않을 수 있습니다.`,"usage.range.30d":`30일`,"usage.range.7d":`7일`,"usage.card.requests":`요청`,"usage.card.measured":`측정됨`,"usage.card.reported":`측정됨`,"usage.card.totalTokens":`총 토큰`,"usage.card.cachedTokens":`캐시 히트 토큰`,"usage.card.cachedTokensHint":`프로바이더 캐시에서 읽어온 프롬프트 토큰(히트)입니다. 캐시 생성(쓰기)은 아래에 별도 표시됩니다.`,"usage.card.cacheWriteTokens":`캐시 생성`,"usage.card.coverage":`커버리지`,"usage.card.activeDays":`활동일`,"usage.section.heatmap":`일별 활동`,"usage.section.overview":`개요`,"usage.section.models":`모델`,"usage.section.providers":`프로바이더`,"usage.section.coverage":`커버리지 상세`,"usage.workspace.report":`사용량 보고서`,"usage.workspace.sections":`사용량 섹션`,"usage.coverage.measured":`측정됨`,"usage.coverage.reported":`제공자 보고`,"usage.coverage.estimated":`추정`,"usage.coverage.note":`측정됨 항목은 제공자 보고와 추정 토큰 수치를 함께 포함합니다. 미보고/미지원 요청은 추적만 하고 0으로 환산하지 않습니다.`,"usage.search.models":`모델 검색…`,"usage.col.requests":`요청`,"usage.col.measured":`측정됨`,"usage.col.reported":`측정됨`,"usage.col.tokens":`토큰`,"usage.col.share":`비율`,"usage.heatmap.less":`적음`,"usage.heatmap.more":`많음`,"modal.addNamed":`추가: {label}`,"modal.add":`프로바이더 추가`,"modal.search":`프로바이더 검색…`,"modal.logInWith":`{label} 로 로그인`,"modal.waitingBrowser":`브라우저 대기 중…`,"modal.providerName":`프로바이더 이름`,"modal.adapter":`어댑터`,"modal.baseUrl":`Base URL`,"modal.endpoint":`엔드포인트`,"modal.endpoint.tokenPlan":`토큰 플랜`,"modal.endpoint.payAsYouGo":`종량제`,"modal.endpoint.custom":`사용자 지정`,"modal.defaultModel":`기본 모델(선택)`,"modal.allowPrivateNetwork":`로컬/사설 네트워크 허용`,"modal.allowPrivateNetworkHint":`의도적으로 자체 호스팅하는 프로바이더에만 활성화하세요. 메타데이터 엔드포인트는 계속 차단됩니다.`,"modal.nameRequired":`프로바이더 이름을 입력하세요`,"modal.baseUrlRequired":`Base URL을 입력하세요`,"modal.networkError":`네트워크 오류 — 프록시가 실행 중인가요?`,"modal.loginFailStart":`로그인을 시작하지 못했습니다`,"modal.waitingLogin":`브라우저 로그인 대기 중…`,"modal.loggingIn":`로그인 중…`,"modal.loginTimeout":`로그인 시간 초과 — 다시 시도하세요.`,"nav.api":`API`,"nav.integrations":`연동`,"nav.codexAuth":`Codex 인증`,"nav.codexSet":`Codex 설정`,"codexSet.tab.multiauth":`다중 인증`,"codexSet.tab.prompt":`프롬프트`,"codexSet.prompt.title":`프롬프트 레이어`,"codexSet.prompt.timing":`새 세션부터 적용됩니다. 실행 중인 세션은 현재 설정을 유지합니다.`,"codexSet.prompt.staleRevision":`다른 곳에서 설정이 바뀌어 목록을 다시 불러왔습니다.`,"codexSet.prompt.writeFailed":`변경 내용을 저장하지 못했습니다.`,"codexSet.prompt.loadFailed":`프롬프트 레이어를 불러오지 못했습니다.`,"codexSet.prompt.repair":`복구`,"codexSet.prompt.repairFailed":`복구를 완료하지 못했습니다.`,"codexSet.drift.journalPresent":`이전 쓰기가 끝나지 않았습니다. 다음 쓰기에서 자동으로 복구됩니다.`,"codexSet.drift.projectionStale":`저장된 레이어와 config.toml의 값이 서로 다릅니다. 복구하면 레이어 기준으로 값을 다시 씁니다.`,"codexSet.drift.storeMissing":`레이어 파일이 없는데 config.toml에는 지침이 남아 있습니다. 복구하면 백업을 먼저 만들고 그 내용을 레이어 하나로 보존합니다.`,"codexSet.drift.ownedMalformed":`config.toml에 생성된 줄이 직접 수정되어 다시 쓰기가 안전하지 않습니다.`,"codexSet.custom.adoptUnsupported":`{path} {line}번째 줄의 값이 한 줄 문자열이 아니어서 가져올 수 없습니다. 여기서 관리하려면 직접 옮기세요.`,"codexSet.prompt.unreadable":`Codex 설정 파일이 있지만 읽을 수 없어 변경을 거부했습니다.`,"codexSet.layer.permissions":`권한`,"codexSet.layer.collaboration":`협업 모드`,"codexSet.layer.environment":`환경 정보`,"codexSet.layer.apps":`앱`,"codexSet.layer.skills":`스킬`,"codexSet.prompt.extensionsUnknown":`확장 프로그램은 자체 레이어를 추가할 수 있습니다. Codex가 이를 공개하지 않아 여기에 표시할 수 없습니다.`,"codexSet.group.transition":`전환 알림`,"codexSet.group.transitionDesc":`상태를 설명하는 대신 변화를 알리는 항목이라, 세션이 실시간 모드로 바뀌거나 모델이 교체될 때만 나타납니다.`,"codexSet.custom.slotNote":`커스텀 레이어는 이 순서대로 이어져 하나의 섹션이 됩니다.`,"codexSet.row.alwaysOn":`항상 켜짐`,"codexSet.row.onChange":`변경 시 전달`,"codexSet.row.featureGated":`[features]에서 설정`,"codexSet.row.openFeatures":`설정 열기`,"codexSet.dialog.setValue":`{value}(기본값 {fallback})`,"codexSet.dialog.copyKey":`키 복사`,"codexSet.dialog.unknownLayer":`이 빌드에는 이 레이어에 대한 설명이 없습니다. 대시보드보다 최신 Codex 런타임에서 온 레이어입니다.`,"codexSet.custom.heading":`커스텀 레이어`,"codexSet.custom.add":`+ 레이어 추가`,"codexSet.custom.newTitle":`새 레이어`,"codexSet.custom.editTitle":`레이어 편집`,"codexSet.custom.titleLabel":`제목`,"codexSet.custom.bodyLabel":`지침`,"codexSet.custom.bodySize":`{max}바이트 중 {bytes}바이트`,"codexSet.custom.normalized":`탭을 공백 4개로, 줄바꿈을 LF로 바꿨습니다.`,"codexSet.custom.titleRequired":`제목을 입력하세요.`,"codexSet.custom.titleTooLong":`제목이 {count}자입니다. 최대 {max}자까지 입력할 수 있습니다.`,"codexSet.custom.titleMultiline":`제목은 한 줄이어야 합니다.`,"codexSet.custom.bodyTooLarge":`이 레이어는 {bytes}바이트입니다. 제한은 {max}바이트입니다.`,"codexSet.custom.composedTooLarge":`활성 레이어를 합치면 {bytes}바이트로 제한을 초과합니다.`,"codexSet.custom.invalidCharacter":`{position} 위치의 제어 문자는 저장할 수 없습니다.`,"codexSet.custom.discardPrompt":`변경 내용을 버리시겠습니까?`,"codexSet.custom.keepEditing":`계속 편집`,"codexSet.custom.delete":`{title} 삭제`,"codexSet.custom.deleteConfirm":`이 레이어를 삭제하시겠습니까? 되돌릴 수 없습니다.`,"codexSet.custom.layerGone":`다른 곳에서 해당 레이어가 삭제되어 편집기를 닫았습니다.`,"codexSet.custom.deleteConfirmNamed":`“{title}” 레이어를 삭제하시겠습니까? 되돌릴 수 없습니다.`,"codexSet.custom.moveUp":`{title} 위로 이동`,"codexSet.custom.prevLayer":`이전 레이어`,"codexSet.custom.nextLayer":`다음 레이어`,"codexSet.custom.navPosition":`{position} / {total}`,"codexSet.custom.moveDown":`{title} 아래로 이동`,"codexSet.custom.limitReached":`커스텀 레이어는 최대 {max}개까지 보관할 수 있습니다.`,"codexSet.custom.notOwned":`developer_instructions가 opencodex 외부에서 작성되어 여기서는 편집할 수 없습니다. 레이어로 관리하려면 가져오세요.`,"codexSet.custom.adopt":`기존 지침 가져오기`,"codexSet.custom.adoptConfirm":`레이어로 가져오기`,"codexSet.custom.adoptRefused":`기존 값을 가져오지 못했습니다.`,"codexSet.custom.baseReplaced":`model_instructions_file이 {path}(으)로 설정되어 opencodex 외부에서 기본 프롬프트를 교체했습니다.`,"codexSet.lint.identity":`Codex가 설정한 것과 다른 정체성을 주장합니다.`,"codexSet.lint.foreignTool":`도구는 레지스트리에서 제공됩니다. 여기서 이름을 지정해도 도구가 생성되지 않습니다.`,"codexSet.lint.placeholder":`지침에는 템플릿 엔진이 실행되지 않으므로 이 내용이 그대로 전달됩니다.`,"codexSet.lint.applyPatch":`apply_patch는 지침이 아니라 도구 레지스트리에서 정의됩니다.`,"codexSet.lint.approvalVocab":`Codex가 자체 승인 용어를 삽입하므로 이 내용과 충돌할 수 있습니다.`,"codexSet.lint.environment":`환경 정보는 나중에 생성되므로 이 내용과 충돌할 수 있습니다.`,"codexSet.lint.size":`이 레이어는 8 KB를 초과합니다. 저장은 가능하지만 요청마다 토큰을 사용합니다.`,"codexSet.preset.blank":`빈 레이어`,"codexSet.preset.concise.name":`간결한 출력`,"codexSet.preset.concise.description":`짧게 답하고, 서론과 불필요한 서식을 생략합니다.`,"codexSet.preset.concise.provenance":`Claude Code의 간결성 지침을 바탕으로 각색했습니다. 직접 작성한 문구이며 복사본이 아닙니다.`,"codexSet.preset.planFirst.name":`편집 전 계획`,"codexSet.preset.planFirst.description":`계획을 먼저 밝힌 뒤 변경합니다.`,"codexSet.preset.planFirst.provenance":`Claude Code의 계획 중심 방식을 바탕으로 각색했습니다. 직접 작성한 문구이며 복사본이 아닙니다.`,"codexSet.preset.explainWhy.name":`이유 설명`,"codexSet.preset.explainWhy.description":`무엇을 했는지만 말하지 말고 이유도 설명합니다.`,"codexSet.preset.explainWhy.provenance":`Grok Build의 확인 방식을 바탕으로 각색했습니다. 직접 작성한 문구이며 복사본이 아닙니다.`,"codexSet.preset.testFirst.name":`테스트 우선`,"codexSet.preset.testFirst.description":`수정 전에 실패하는 테스트부터 작성합니다.`,"codexSet.preset.testFirst.provenance":`일반적인 에이전트 작업 방식을 바탕으로 각색했습니다. 직접 작성한 문구이며 복사본이 아닙니다.`,"codexSet.preset.korean.name":`한국어 답변`,"codexSet.preset.korean.description":`요청 언어와 관계없이 한국어로 답합니다.`,"codexSet.preset.korean.provenance":`자주 요청되는 항목을 바탕으로 opencodex용으로 작성했습니다. 직접 작성한 문구이며 복사본이 아닙니다.`,"codexSet.dialog.class":`종류`,"codexSet.dialog.key":`설정 키`,"codexSet.dialog.fileValue":`이 파일의 값`,"codexSet.dialog.absentDefault":`설정되지 않음(기본값: {value})`,"codexSet.dialog.noRenderedText":`Codex는 기본 제공 레이어의 조합된 텍스트를 공개하지 않습니다. 따라서 이 대화상자에는 내용 대신 레이어 설명과 키를 표시합니다.`,"codexSet.dialog.sourceText":`모델에 전달되는 원문`,"codexSet.dialog.sourceBytes":`{bytes}바이트`,"codexSet.dialog.notRendered":`확인한 턴에서는 이 레이어가 아무것도 보내지 않았습니다. 각 섹션은 내용이 바뀔 때만 다시 전송되므로, 한 번 확인한 것만으로는 빠져 보일 수 있습니다.`,"codexSet.dialog.emptySource":`{path} 파일은 있지만 비어 있어서 이 레이어는 아무것도 보내지 않습니다.`,"codexSet.dialog.notExposed":`기본 프롬프트는 Codex가 출력하는 메시지 목록 밖으로 전달되어 여기서 보여줄 수 없습니다. 대신 model_instructions_file로 교체할 수 있습니다.`,"codexSet.dialog.textUnavailable":`이 컴퓨터에서 Codex 프롬프트를 읽지 못해 원문을 표시할 수 없습니다.`,"codexSet.class.base":`기본 지침`,"codexSet.class.config-toggle":`여기서 전환 가능`,"codexSet.class.feature-gated":`기능 플래그 적용`,"codexSet.class.runtime-conditional":`런타임 조건부`,"codexSet.class.extension-unknown":`확장 레이어`,"codexSet.layer.base-instructions":`기본 지침`,"codexSet.layer.model-switch":`모델 전환 알림`,"codexSet.layer.personality":`성격`,"codexSet.layer.context-window-guidance":`컨텍스트 창 안내`,"codexSet.layer.realtime":`실시간`,"codexSet.layer.agents-md":`AGENTS.md`,"codexSet.layer.environments-instructions":`실행 환경`,"codexSet.layer.plugins":`플러그인`,"codexSet.layer.tools":`도구`,"codexSet.layer.multi-agent-mode":`멀티 에이전트 모드`,"codexSet.layer.git-attribution":`커밋 어트리뷰션`,"codexSet.about.base-instructions":`Codex 자체 지침입니다. 요청에 포함되며 끌 수 없습니다.`,"codexSet.about.model-switch":`대화 도중 세션 모델이 바뀌면 추가됩니다.`,"codexSet.about.personality":`기능 플래그로 제어되는 어조와 말투 지침입니다.`,"codexSet.about.context-window-guidance":`기능 플래그로 제어되는 남은 컨텍스트 예산 안내입니다.`,"codexSet.about.realtime":`실시간 세션에 추가됩니다.`,"codexSet.about.agents-md":`프로젝트의 AGENTS.md 파일입니다. 이 페이지는 레이어만 표시하며 프로젝트 문서를 수정하지 않습니다.`,"codexSet.about.permissions":`현재 적용 중인 샌드박스와 승인 설정을 설명합니다.`,"codexSet.about.collaboration":`활성 협업 모드를 설명합니다.`,"codexSet.about.environment":`작업 디렉터리, 플랫폼 및 기타 환경 정보입니다.`,"codexSet.about.environments-instructions":`기능 플래그로 제어되는 지연 실행 환경 지침입니다.`,"codexSet.about.apps":`연결된 앱의 사용 방법입니다.`,"codexSet.about.plugins":`플러그인을 선택했거나 플러그인이 기능을 제공하면 추가됩니다.`,"codexSet.about.tools":`기능 플래그로 제어되는 지연 로드 도구 설명입니다.`,"codexSet.about.skills":`사용 가능한 스킬 목록입니다.`,"codexSet.about.multi-agent-mode":`기능 플래그로 제어되는 서브에이전트 지침입니다.`,"codexSet.about.git-attribution":`모델이 작성한 커밋에 Co-authored-by: Codex 트레일러를, 새로 여는 풀 리퀘스트에 Generated with Codex. 한 줄을 붙이게 합니다. Codex가 계정에서 이 값을 가져오기 때문에 여기서도 [features]에서도 바꿀 수 없습니다. 계정에서 꺼두면 아무것도 보내지 않는 대신 반대 지시를 보냅니다.`,"codexSet.condition.model-switch":`세션 도중 모델이 바뀐 뒤에만 포함됩니다.`,"codexSet.condition.realtime":`실시간 세션에만 포함됩니다.`,"codexSet.condition.agents-md":`작업 디렉터리에서 프로젝트 문서를 찾으면 포함됩니다.`,"codexSet.condition.plugins":`플러그인을 선택했거나 플러그인이 기능을 제공하면 포함됩니다.`,"codexSet.condition.git-attribution":`계정의 어트리뷰션 정책이 결정합니다.`,"codexSet.base.title":`기본 프롬프트`,"codexSet.base.prev":`이전 옵션`,"codexSet.base.next":`다음 옵션`,"codexSet.base.position":`{position} / {total}`,"codexSet.base.swipeHint":`좌우로 스와이프하거나 방향키 또는 화살표 버튼으로 옵션을 넘깁니다. 새로 시작하는 세션에 적용됩니다.`,"codexSet.base.defaultTitle":`Codex 자체 기본 프롬프트`,"codexSet.base.defaultBody":`기본값은 여기에 저장되지 않으므로 편집하거나 삭제할 것이 없습니다. 선택하면 설정에서 model_instructions_file을 지우고 Codex가 기본 제공 프롬프트를 씁니다.`,"codexSet.base.variantTitle":`이름`,"codexSet.base.variantBody":`프롬프트`,"codexSet.base.replacesWarning":`Codex 자체 기본 프롬프트에 덧붙이는 게 아니라 통째로 바꿉니다. 여기에 짧게 쓰면 모델도 그만큼 짧은 지시만 받습니다.`,"codexSet.base.use":`이걸로 쓰기`,"codexSet.base.inUse":`사용 중`,"codexSet.base.externalBlocked":`model_instructions_file이 이미 {path}를 가리키고 있고, opencodex가 쓴 값이 아닙니다. 직접 지운 뒤 여기서 선택하세요.`,"nav.openMenu":`메뉴 열기`,"nav.closeMenu":`메뉴 닫기`,"integrations.subtitle":`클라이언트를 opencodex에 연결하고 자격 증명과 설정 복원을 관리합니다.`,"integrations.tabsLabel":`연동 화면`,"integrations.tab.overview":`개요`,"integrations.tab.keys":`API 키`,"integrations.tab.codex":`Codex`,"integrations.tab.claude":`Claude`,"integrations.tab.grok":`Grok Build`,"integrations.tab.opencode":`OpenCode`,"integrations.tab.pi":`Pi`,"integrations.tab.omp":`OMP`,"integrations.tab.hermes":`Hermes`,"integrations.tab.openclaw":`OpenClaw`,"integrations.tab.kimi":`Kimi Code`,"integrations.tab.gajae":`Gajae Code`,"integrations.tab.dsh":`DSH`,"integrations.tab.mcode":`MiniMax Code`,"integrations.tab.zcode":`ZCode`,"integrations.tab.prime":`Prime Agent`,"integrations.tab.aside":`Aside`,"integrations.codex.title":`Codex CLI`,"integrations.codex.body":`Codex 연결은 프록시 서비스가 관리합니다. opencodex를 시작하면 적용되고 서비스를 중지하면 기본 라우팅으로 복원됩니다.`,"integrations.codex.openService":`서비스 제어 열기`,"integrations.state.notInstalled":`미설치`,"integrations.state.unknown":`확인 중`,"integrations.detail.codexRouted":`Codex 요청이 이 프록시를 지납니다`,"integrations.detail.codexAbsent":`Codex는 아직 이 프록시를 지나지 않습니다`,"integrations.detail.keyCount":`키 {count}개 발급됨`,"integrations.detail.keyNone":`발급된 키 없음`,"integrations.detail.keyChecking":`확인 중…`,"integrations.detail.keyUnavailable":`키 상태를 확인할 수 없음`,"integrations.detail.claudeOff":`연결이 꺼져 있습니다`,"integrations.detail.desktopCurrent":`Desktop이 이 프로필로 실행됩니다`,"integrations.detail.desktopStale":`적용 후 프로필 파일이 바뀌었습니다`,"integrations.detail.desktopNotServed":`프로필은 있지만 Desktop이 다른 것을 씁니다`,"integrations.detail.desktopAbsent":`적용된 프로필이 없습니다`,"integrations.detail.desktopDesiredOff":`Claude Desktop 통합이 꺼져 있습니다`,"integrations.detail.desktopDesiredOffCleanupPending":`Claude Desktop이 여전히 게이트웨이를 사용 중입니다. 정리 대기 중`,"integrations.detail.desktopDesiredOnNotApplied":`통합은 켜져 있지만 Desktop이 게이트웨이 프로필을 사용하지 않습니다`,"integrations.detail.desktopSelectedElsewhere":`Desktop이 다른 프로필을 사용 중입니다`,"integrations.detail.desktopProfileDrift":`선택된 Desktop 프로필이 변경되었습니다`,"integrations.detail.desktopObservedUnsafe":`선택된 Desktop 프로필은 안전하게 변경할 수 없습니다`,"integrations.detail.desktopNotInstalled":`Claude Desktop 구성 라이브러리가 설치되지 않았습니다`,"integrations.dialog.desktop.title":`Claude Desktop 통합을 끌까요?`,"integrations.dialog.desktop.changes":`{path}에 opencodex 게이트웨이 프로필이 있으면 먼저 자격 증명 없는 표준 프로필을 선택한 뒤 이전 프로필과 백업을 제거합니다.`,"integrations.dialog.desktop.breakage":`Claude Desktop은 opencodex를 통한 모델 대신 표준 Claude로 돌아갑니다.`,"integrations.dialog.desktop.undo":`다시 켜면 저장된 모델 할당으로 opencodex 프로필을 새로 만듭니다.`,"integrations.dialog.desktop.restart":`Claude Desktop은 시작할 때만 이 구성을 읽습니다. 변경하려면 완전히 종료한 뒤 다시 여세요.`,"integrations.dialog.desktop.confirm":`해제`,"integrations.native.error.desktopUnsafeMetadata":`{path}의 Claude Desktop 메타데이터를 안전하게 읽을 수 없어 라이브러리를 변경하지 않았습니다.`,"integrations.native.error.desktopCleanupIncomplete":`Claude Desktop은 표준 모드를 가리키지만 이전 opencodex 자격 증명 파일이 남아 있습니다: {paths}.`,"integrations.native.msg.desktopDisabled":`Claude Desktop 통합을 해제했습니다.`,"integrations.native.msg.desktopEnabled":`Claude Desktop 통합을 켰습니다.`,"integrations.detail.grokModels":`모델 {count}개 연결됨`,"integrations.detail.grokAbsent":`설정에 opencodex 블록이 없습니다`,"integrations.dialog.grok.title":`Grok Build 연동을 해제할까요?`,"integrations.dialog.grok.changes":`{path}에서 opencodex가 표시해 둔 블록만 제거합니다. 블록 바깥에 직접 쓴 내용은 그대로 둡니다.`,"integrations.dialog.grok.breakage":`해제하면 Grok Build에서 opencodex 모델 별칭이 사라집니다. xAI 계정으로 쓰던 모델은 그대로입니다.`,"integrations.dialog.grok.undo":`opencodex가 loopback 주소로 실행 중이면, 다시 켤 때 지금 쓸 수 있는 모델 목록으로 블록을 새로 씁니다.`,"integrations.dialog.grok.confirm":`해제`,"integrations.native.msg.nonLoopbackRemoved":`Grok Build은 opencodex가 loopback 주소로 실행 중일 때만 자동 등록할 수 있습니다. loopback 주소를 가리키던 이전 블록은 제거했습니다.`,"integrations.native.msg.nonLoopbackRemovedNoop":`Grok Build은 opencodex가 loopback 주소로 실행 중일 때만 자동 등록할 수 있습니다. 제거할 이전 블록은 없었습니다.`,"integrations.native.msg.nonLoopbackSuperseded":`Grok Build은 opencodex가 loopback 주소로 실행 중일 때만 자동 등록할 수 있습니다. 그 사이 다른 곳에서 설정에 블록이 새로 쓰여, 지금 파일에 있는 블록은 이 요청이 만든 것이 아닙니다.`,"integrations.native.error.orphanedMarker":`{path}에 opencodex 시작 표시는 있는데 끝 표시가 없습니다. 어디까지가 우리 블록인지 확신할 수 없어 파일을 건드리지 않았습니다.`,"integrations.native.error.homeMismatch":`설치된 서비스의 홈과 현재 홈이 일치하지 않아 파일을 건드리지 않았습니다.`,"integrations.native.error.notInstalled":`Grok Build가 설치되어 있지 않아 변경할 수 없습니다.`,"integrations.native.error.configBusy":`다른 곳에서 설정을 저장하는 중이라 변경하지 못했습니다. 잠시 후 다시 시도해 주세요.`,"integrations.state.absent":`미적용`,"integrations.state.current":`적용됨`,"integrations.state.stale":`업데이트 필요`,"integrations.state.conflict":`충돌`,"integrations.state.unsafe":`확인 불가`,"integrations.summary.detected":`감지된 클라이언트`,"integrations.summary.applied":`설정된 클라이언트`,"integrations.summary.stale":`업데이트 필요`,"integrations.summary.lastChange":`마지막 변경`,"integrations.summary.disableAll":`모두 해제…`,"integrations.onboarding":`적용하면 먼저 백업을 보관한 뒤 opencodex 제공자 블록 하나만 씁니다. 해제는 그 블록만 제거하며 보관된 스냅샷으로 복원할 수 있습니다.`,"integrations.empty.title":`설치된 클라이언트가 감지되지 않았습니다`,"integrations.empty.body":`지원 클라이언트를 설치한 뒤 돌아와 opencodex를 적용하세요.`,"integrations.action.apply":`적용`,"integrations.action.disable":`해제`,"integrations.action.refresh":`업데이트`,"integrations.action.settings":`설정`,"integrations.action.manageKeys":`키 관리`,"integrations.action.restore":`복원…`,"integrations.action.undo":`되돌리기`,"integrations.action.restorePoint":`이 시점으로 복원…`,"integrations.action.snapshotExpired":`백업 만료됨`,"integrations.rollback.title":`복원 센터`,"integrations.rollback.empty":`아직 적용 기록이 없습니다`,"integrations.rollback.emptyBody":`모든 쓰기는 먼저 변경 전 스냅샷을 보관합니다.`,"integrations.catalog.title":`클라이언트`,"integrations.rollback.older":`이전 작업`,"integrations.rollback.showMore":`{n}개 더 보기`,"integrations.rollback.failed":`롤백 기록을 불러오지 못했습니다.`,"integrations.restore.title":`이 스냅샷으로 복원할까요?`,"integrations.restore.body":`현재 파일을 먼저 백업한 뒤 선택한 스냅샷으로 교체합니다.`,"integrations.restore.driftTitle":`스냅샷 이후 변경이 감지되었습니다`,"integrations.restore.driftBody":`스냅샷 이후의 변경이 백업으로 보관되고 파일이 교체됩니다.`,"integrations.restore.confirm":`복원`,"integrations.restore.confirmDrift":`새 변경을 백업하고 복원`,"integrations.restore.pending":`복원 중…`,"integrations.restore.manual":`자동 복원에 실패했습니다: {reason}. {path}에서 직접 복원하세요.`,"integrations.error.load":`연동 상태를 불러오지 못했습니다.`,"integrations.error.stale":`최신 새로고침에 실패했습니다. 아래 값은 오래된 정보일 수 있습니다.`,"integrations.error.busy":`이 클라이언트의 다른 변경이 진행 중입니다. 잠시 후 다시 시도하세요.`,"integrations.error.conflict":`opencodex가 쓴 뒤 설정이 변경되었습니다. 아무 내용도 제거하지 않았습니다.`,"integrations.error.unsafe":`설정을 안전하게 변경할 수 없습니다.`,"integrations.error.generic":`연동 변경에 실패했습니다. 이전 상태는 유지되었습니다.`,"integrations.error.nonLoopback":`{client}은(는) localhost 프록시에만 연결할 수 있습니다. 원격 바인드에 필요한 인증 헤더를 넣을 자리가 설정 파일에 없어 직접 작성해도 마찬가지입니다. 터널이나 로컬 포워더로 loopback 경로를 열어주세요.`,"integrations.status.installed":`설치 감지됨`,"integrations.status.notInstalled":`설치되지 않음`,"integrations.status.appliedAt":`적용`,"integrations.status.backup":`백업`,"integrations.status.lastRestore":`마지막 복원`,"integrations.status.unknown":`알 수 없음`,"integrations.bulk.title":`적용된 클라이언트 연동을 해제할까요?`,"integrations.bulk.body":`opencodex가 소유한 블록만 제거합니다. 각 클라이언트의 변경 전 스냅샷을 보관합니다.`,"integrations.bulk.partial":`일부 클라이언트를 해제하지 못했습니다: {clients}`,"integrations.bulk.success":`적용된 클라이언트 연동을 해제했습니다.`,"integrations.retention.degraded":`백업 정리가 밀려 있습니다 — 오래된 백업이 남아 있을 수 있습니다.`,"integrations.error.residual":`파일이 중간 상태로 남았을 수 있습니다: {message} {path}에서 복원하세요.`,"integrations.error.recover":`{message} 백업 위치는 {path}입니다.`,"integrations.kind.apply":`적용`,"integrations.kind.disable":`해제`,"integrations.kind.refresh":`업데이트`,"integrations.kind.restore":`복원`,"integrations.kind.overwrite":`덮어씀`,"integrations.dialog.overwrite.title":`이 설정 파일의 블록을 덮어쓸까요?`,"integrations.dialog.overwrite.changesUnowned":`{path}에서 opencodex가 써야 하는 자리를 우리가 쓰지 않은 블록이 차지하고 있습니다. 적용하면 opencodex가 쓰는 블록으로 바꿉니다.`,"integrations.dialog.overwrite.changesForeign":`{path}의 opencodex 블록에 직접 넣은 수정은 버리고 opencodex가 쓰는 블록으로 바꿉니다.`,"integrations.dialog.overwrite.breakage":`그 블록이 설정하던 동작은 더 이상 적용되지 않습니다. 파일의 다른 부분은 그대로 둡니다.`,"integrations.dialog.overwrite.undo":`먼저 스냅숏을 저장하므로 아래 되돌리기 목록에 남고 다시 되돌릴 수 있습니다.`,"integrations.dialog.overwrite.confirm":`덮어쓰기`,"integrations.action.overwrite":`덮어쓰기`,"integrations.semantics.opencode":`디스크에서 직접 실행할 때만 적용됩니다. ocx opencode의 환경 주입이 우선합니다.`,"integrations.semantics.pi":`새 세션부터 적용됩니다.`,"integrations.semantics.omp":`카탈로그를 불러오려면 OMP를 재시작하세요.`,"integrations.semantics.hermes":`새 세션부터 적용됩니다.`,"integrations.semantics.openclaw":`실행 중인 게이트웨이에 즉시 반영됩니다.`,"integrations.semantics.kimi":`재시작 또는 /reload 시 적용됩니다 (v2는 파일 변경을 감지합니다).`,"integrations.semantics.gajae":`새 세션 또는 /model을 열 때 적용됩니다.`,"integrations.semantics.dsh":`OpenCodex는 $DSH_HOME/settings.yaml의 llm-pi-ai.providers.opencodex만 관리합니다. DSH는 이 provider를 hot reload하며 기본 model과 deepseek-official은 변경하지 않습니다. 현재 loopback 전용이며 실제 credential을 기록하지 않습니다.`,"integrations.semantics.mcode":`custom_provider.opencodex만 관리하며 기본 모델과 MiniMax 로그인은 변경하지 않습니다.`,"integrations.semantics.zcode":`~/.zcode/v2/config.json의 provider.opencodex만 관리하며 Z.ai 로그인과 다른 프로바이더는 변경하지 않습니다. 변경 후 ZCode를 재시작하세요.`,"integrations.semantics.prime":`Prime Agent의 models.json에서 providers.opencodex만 관리합니다. 위치는 ~/.prime/agent이며 PRIME_AGENT_CODING_AGENT_DIR가 설정되면 그쪽이 우선합니다. 다른 프로바이더와 모델 오버라이드는 변경하지 않습니다. 새 세션부터 적용됩니다.`,"integrations.semantics.aside":`로그인된 계정의 Aside models.json에서 providers.opencodex만 관리합니다. 위치는 ~/.aside/u/<계정>이며 다른 프로바이더는 변경하지 않습니다. Aside는 실행 중에 이 파일을 다시 쓰기 때문에 적용한 뒤 Aside를 완전히 종료하고 다시 여세요.`,"codexAuth.mainAccount":`메인 계정`,"codexAuth.logLabel":`로그 라벨`,"codexAuth.codexApp":`Codex App`,"codexAuth.moreActions":`추가 작업 표시`,"codexAuth.copyId":`계정 ID 복사`,"codexAuth.appLogin":`앱 로그인`,"codexAuth.accountPool":`계정 풀`,"codexAuth.accountModeTitle":`OpenAI 계정 모드`,"codexAuth.accountModePool":`풀 모드`,"codexAuth.accountModePoolDesc":`메인 로그인과 사용 가능한 추가 계정이 여기에서 순환됩니다.`,"codexAuth.accountModeDirect":`직접 모드`,"codexAuth.accountModeDirectDesc":`요청은 메인 로그인만 사용하며, 추가 계정은 풀 모드용으로 계속 저장됩니다.`,"codexAuth.openaiMissing":`내장 OpenAI 프로바이더가 설정되지 않았습니다.`,"codexAuth.openaiDisabled":`내장 OpenAI 프로바이더가 비활성화되어 있습니다.`,"codexAuth.openaiUnavailableDesc":`OpenAI 계정은 그대로 사용할 수 있습니다. Codex 요청을 라우팅하려면 프로바이더를 활성화하세요.`,"codexAuth.enableOpenai":`OpenAI 활성화`,"codexAuth.enablingOpenai":`활성화 중...`,"codexAuth.enableOpenaiFailed":`OpenAI 공급자를 활성화하지 못했습니다.`,"codexAuth.openaiPresetLoadFailed":`OpenAI 공급자 프리셋을 불러오지 못했습니다.`,"codexAuth.openaiPresetUnavailable":`OpenAI 공급자 프리셋을 사용할 수 없습니다.`,"codexAuth.openProviders":`프로바이더 열기`,"codexAuth.add":`추가`,"codexAuth.sparkQuota":`Codex Spark 할당량`,"codexAuth.sparkQuotaHint":`계정 카드에 GPT-5.3-Codex-Spark 주간 창을 표시합니다. 모델 하나에만 적용되므로 기본값은 숨김입니다.`,"codexAuth.sparkQuotaShown":`Codex Spark 할당량을 표시합니다`,"codexAuth.sparkQuotaHidden":`Codex Spark 할당량을 숨겼습니다`,"codexAuth.sparkQuotaFailed":`Codex Spark 할당량 설정을 바꾸지 못했습니다`,"codexAuth.refreshQuota":`할당량 새로고침`,"codexAuth.refreshingQuota":`새로고침 중...`,"codexAuth.quotaRefreshed":`할당량을 다시 조회했습니다`,"codexAuth.quotaRefreshFailed":`할당량 재조회에 실패했습니다`,"codexAuth.pauseExhausted":`한도 도달 계정 일시 중지`,"codexAuth.pausingExhausted":`할당량 확인 중...`,"codexAuth.pauseExhaustedSucceeded":`한도에 도달해 일시 중지된 계정: {count}`,"codexAuth.pauseExhaustedNone":`사용량 100%가 확인된 계정이 없습니다.`,"codexAuth.pauseExhaustedFailed":`한도 도달 계정을 확인하고 일시 중지하지 못했습니다.`,"codexAuth.noPool":`풀 계정이 아직 없습니다.`,"codexAuth.pause":`일시 중지`,"codexAuth.resume":`재개`,"codexAuth.paused":`일시 중지됨`,"codexAuth.pauseSucceeded":`{email} 계정을 일시 중지했습니다`,"codexAuth.resumeSucceeded":`{email} 계정을 풀에서 다시 사용할 수 있습니다`,"codexAuth.pauseFailed":`{email} 계정을 일시 중지하지 못했습니다. 변경 사항이 없습니다.`,"codexAuth.resumeFailed":`{email} 계정을 재개하지 못했습니다. 변경 사항이 없습니다.`,"codexAuth.pausedHint":`재개할 때까지 자동 전환, 재시도, 쿨다운 복구 및 수동 선택에서 제외됩니다.`,"codexAuth.pinned":`고정됨`,"codexAuth.pinnedHint":`직접 선택한 계정이므로 더 높은 선택 순서가 이 계정을 앞지르지 않습니다. 고정은 이 계정이 소진되거나, 다른 계정을 선택하거나, 어떤 계정이든 선택 순서를 변경할 때까지 유지됩니다.`,"codexAuth.fiveHour":`5시간`,"codexAuth.weekly":`주간`,"codexAuth.monthly":`30일`,"codexAuth.resets":`리셋`,"codexAuth.today":`오늘`,"codexAuth.current":`현재`,"codexAuth.nextSession":`선택됨`,"codexAuth.poolPrepared":`풀 모드 준비됨`,"codexAuth.preparePoolTitle":`이 계정을 풀 모드용으로 준비할까요?`,"codexAuth.preparePoolDesc":`직접 모드 요청은 계속 메인 로그인을 사용합니다. 풀 모드를 켜면 이 계정이 준비된 풀 선택으로 사용됩니다.`,"codexAuth.prepareForPool":`풀 모드용으로 준비`,"codexAuth.poolPreparedToast":`{email} 계정을 풀 모드용으로 준비했습니다`,"codexAuth.switchTitle":`활성 계정을 변경하시겠습니까?`,"codexAuth.switchDesc":`즉시 적용됩니다. 계정에 바인딩된 기존 스레드와 이미 진행 중인 요청은 기존 계정을 유지하고, 새 요청이나 바인딩 없는 요청은 선택한 계정의 순서 티어를 사용합니다. 같은 선택 순서의 계정은 계속 번갈아 사용됩니다.`,"codexAuth.cacheWarning":`계정 전환 시 프롬프트 캐시가 초기화됩니다.`,"codexAuth.setAsNext":`이 계정을 다음에 사용`,"codexAuth.cancel":`취소`,"codexAuth.switchBack":`메인 계정으로 돌아가시겠습니까?`,"codexAuth.switchBackDesc":`즉시 적용됩니다. 계정에 바인딩된 기존 스레드와 이미 진행 중인 요청은 기존 계정을 유지하고, 새 요청이나 바인딩 없는 요청은 앱 로그인 계정의 순서 티어를 사용합니다. 같은 선택 순서의 계정은 계속 번갈아 사용됩니다.`,"codexAuth.autoSwitch":`사용량 기반 선제 전환`,"codexAuth.autoSwitchQuotaDesc":`할당량: 사용량이 {threshold}% 이상이면 이미 바인딩된 작업을 포함해 다음 요청이 사용량이 더 낮은 적격 계정으로 이동할 수 있습니다. Go/Free는 30일만 봅니다.`,"codexAuth.autoSwitchQuotaOffDesc":`사용량 기반 선제 전환이 꺼져 있습니다. 새 작업/바인딩 없는 작업 배정과 실패 복구는 계속 적용됩니다.`,"codexAuth.autoSwitchRoundRobinDesc":`라운드로빈 배정은 이 임계값을 사용하지 않으며, 바인딩 없는 새 작업을 계속 순환합니다.`,"codexAuth.autoSwitchFillFirstDesc":`필 퍼스트: {threshold}%는 새 작업/바인딩 없는 작업의 소진 기준이며, 정상적인 바인딩 작업은 계정을 유지합니다.`,"codexAuth.autoSwitchFillFirstOffDesc":`필 퍼스트에는 새 작업/바인딩 없는 작업의 사용량 소진 기준이 없습니다. 쿨다운, 재인증, 실패 복구는 여전히 라우팅을 바꿀 수 있습니다.`,"codexAuth.failureRecoveryNote":`실패 복구는 별도입니다. 출력 전 429/402 거절, 쿨다운, 재인증, 제외 또는 설정된 일시적 장애 조치로 다른 적격 계정이 선택될 수 있습니다.`,"codexAuth.autoSwitchThreshold":`사용량 임계값`,"codexAuth.autoSwitchThresholdAria":`사용량 임계값(퍼센트)`,"codexAuth.autoSwitchThresholdInc":`사용량 임계값 증가`,"codexAuth.autoSwitchThresholdDec":`사용량 임계값 감소`,"codexAuth.autoSwitchLoadFailed":`사용량 기반 전환 설정을 불러오지 못했습니다.`,"codexAuth.autoSwitchThresholdInvalid":`1~100 사이의 정수를 입력하세요`,"codexAuth.autoSwitchUpdated":`사용량 기반 선제 전환 설정을 저장했습니다`,"codexAuth.autoSwitchUpdateFailed":`사용량 기반 전환 설정 변경을 확인하지 못했습니다. 마지막으로 확인된 값을 표시합니다.`,"codexAuth.requestUserInput":`Default 모드에서 입력 요청`,"codexAuth.requestUserInputDesc":`Default 모드 세션에서 Codex가 일시 중지하고 request_user_input 도구로 질문할 수 있게 합니다.`,"codexAuth.requestUserInputUpdated":`기능 플래그가 업데이트되었습니다 - 새 세션부터 적용됩니다.`,"codexAuth.requestUserInputUpdatedRestart":`기능 플래그가 업데이트되었습니다 - 새 세션부터 적용됩니다. Codex 앱을 다시 시작하세요.`,"codexAuth.requestUserInputUpdateFailed":`기능 플래그를 업데이트하지 못했습니다. 변경된 내용이 없습니다.`,"codexAuth.requestUserInputLoadFailed":`config.toml에서 기능 플래그를 읽지 못했습니다.`,"codexAuth.accountPickerTitle":`모델 선택기에서 사용할 Codex 계정 지정`,"codexAuth.accountPickerOffDesc":`활성화하면 일반 GPT 선택기 항목이 계정 선택기별 항목으로 대체되어 로그아웃하지 않고도 대화에 사용할 정확한 계정을 선택할 수 있습니다. 비활성화해도 계정은 삭제되지 않습니다.`,"codexAuth.accountPickerOnDesc":`각 선택기는 저장된 계정 하나를 나타내는 공개 레이블입니다. 선택하면 해당 대화가 그 계정에 고정되며 Pool 순환이나 대체가 일어나지 않고 활성 Pool 계정도 바뀌지 않습니다.`,"codexAuth.accountPickerCompatibility":`기본 Codex App 로그인에는 자체 선택기가 있습니다. 생성된 맵에서는 보통 main을 사용하며, 충돌 시 main-2 같은 안전한 접미사가 붙습니다. 추가 계정에는 안정적인 개인정보 보호 레이블이 부여되고 사용자 지정 선택기 이름은 유지됩니다. 기존 대화와 저장된 모델 선택은 계속 라우팅됩니다. 비활성화하면 생성된 항목만 숨기며 선택기와 정확한 경로는 보존됩니다. 일반 GPT 모델 ID는 기존 Pool 또는 Direct 동작을 유지합니다.`,"codexAuth.accountPickerUpdated":`계정 지정 설정을 업데이트했습니다.`,"codexAuth.accountPickerUpdateFailed":`계정 지정 설정을 업데이트하지 못했습니다. 마지막으로 확인된 설정을 표시합니다.`,"codexAuth.accountPickerLoadFailed":`계정 지정 설정을 불러오지 못했습니다.`,"codexAuth.accountPickerRefreshFailed":`이 설정을 새로 고치지 못했습니다. 마지막으로 확인된 값을 계속 표시합니다.`,"codexAuth.advancedSettings":`고급 설정`,"codexAuth.advancedSettingsAria":`고급 Codex 인증 설정 표시 또는 숨기기`,"codexAuth.catalogRefreshPending":`변경 사항은 저장되었지만 Codex 모델 카탈로그 새로 고침이 보류 중입니다. ocx sync를 실행해 다시 시도하세요.`,"anthropicPool.title":`Claude 계정 풀(실험적)`,"anthropicPool.enabledDesc":`429 시 계정을 쿨다운하고 장애 조치합니다. 새 세션은 {window}이 {threshold}% 미만인 계정을 우선합니다.`,"anthropicPool.enabledNoProactiveDesc":`429 시 계정을 쿨다운하고 장애 조치합니다. 임계값 0에서는 사용량 기반 사전 전환이 꺼지지만, 새 세션 선택과 429 복구는 여전히 {window} 창을 사용합니다.`,"anthropicPool.disabledDesc":`활성 Claude 계정만 사용합니다. 실험적 라우팅을 감수할 때만 켜세요.`,"anthropicPool.experimentalWarning":`실험적이며 충분히 검증되지 않았습니다. 자동 다중 계정 로테이션처럼 보이는 동작은 Anthropic이 계정을 제한할 수 있습니다. 같은 조직은 할당량을 공유할 수 있어 풀링이 도움이 되지 않을 수 있습니다. 위험을 이해하지 못하면 꺼 두세요.`,"anthropicPool.needTwoAccounts":`풀을 켜기 전에 Claude OAuth 계정을 두 개 이상 추가하세요.`,"anthropicPool.threshold":`새 세션 사용량 임계값`,"anthropicPool.thresholdAria":`새 세션 사용량 임계값(퍼센트)`,"anthropicPool.thresholdHelp":`0은 할당량 기반 선택을 끕니다(어피니티 + 활성 계정만). 기본값 80.`,"anthropicPool.thresholdInvalid":`0에서 100 사이의 정수를 입력하세요`,"anthropicPool.loadFailed":`Claude 풀 설정을 불러오지 못했습니다.`,"anthropicPool.saveFailed":`Claude 풀 설정을 저장하지 못했습니다.`,"anthropicPool.on":`켜짐`,"anthropicPool.off":`꺼짐`,"accountPool.strategy":`로테이션 전략`,"accountPool.strategyDesc":`OpenCodex가 새 작업/바인딩 없는 작업에 계정을 배정하는 방식입니다.`,"accountPool.strategyQuota":`할당량`,"accountPool.strategyRoundRobin":`라운드로빈`,"accountPool.strategyFillFirst":`필 퍼스트`,"accountPool.strategyHintQuota":`할당량 전략은 사용량 임계값을 넘으면 기존 작업의 다음 요청도 다른 계정에 다시 바인딩할 수 있습니다.`,"accountPool.strategyHintRoundRobin":`라운드로빈은 현재 바인딩이 없는 작업만 순환하며, 사용량 임계값은 기본 순환에 영향을 주지 않습니다.`,"accountPool.strategyHintFillFirst":`필 퍼스트는 임계값을 바인딩 없는 작업의 소진 기준으로 사용하며, 정상적인 바인딩 작업은 어피니티를 유지합니다.`,"accountPool.unboundDefinition":`새 작업/바인딩 없는 작업은 현재 계정 바인딩이 없는 요청입니다. 기존에 보이던 작업도 프록시나 어피니티 상태가 초기화되면 바인딩이 없어질 수 있습니다.`,"accountPool.stickyLimit":`회전 전 새 작업/바인딩 없는 작업 배정 횟수`,"accountPool.stickyLimitAria":`회전 전 새 작업/바인딩 없는 작업 배정 횟수`,"accountPool.stickyLimitInc":`스티키 한도 증가`,"accountPool.stickyLimitDec":`스티키 한도 감소`,"accountPool.stickyLimitHelp":`다음 계정으로 넘어가기 전에 이 횟수의 새 작업/바인딩 없는 작업을 선택 계정에 배정합니다. 카운터는 업스트림 성공 후가 아니라 작업을 바인딩할 때 증가합니다.`,"accountPool.stickyLimitInvalid":`1에서 100 사이의 정수를 입력하세요`,"accountPool.strategyLoadFailed":`로테이션 전략을 불러오지 못했습니다.`,"accountPool.strategyUpdateFailed":`로테이션 전략을 저장하지 못했습니다.`,"accountPool.quotaWindow":`할당량 기준 구간`,"accountPool.quotaWindowDesc":`할당량 기반 새 세션 선택, 필 퍼스트 임계값 판정, 가능한 429 대체 계정에 사용할 캐시 사용량 기준을 정합니다.`,"accountPool.quotaWindowFiveHour":`5시간 사용량`,"accountPool.quotaWindowWeekly":`주간 사용량`,"accountPool.quotaWindowMaxUtilization":`더 높은 사용량`,"accountPool.quotaWindowHint":`주간은 다른 사용 가능한 계정이 남아 있을 때만 5시간 사용량이 소진된 계정을 건너뛰고, 아무 계정도 남지 않으면 해당 계정으로 폴백합니다. 주간 사용량이 같으면 5시간 사용량이 더 낮은 계정을 고르며, 계정별 주간 사용량은 공급자 페이지에서 조회한 뒤에만 알 수 있습니다.`,"accountPool.quotaWindowInert":`할당량 전략, 또는 임계값이 0보다 큰 필 퍼스트만 사용량을 기준으로 점수를 매깁니다. 현재 로테이션 전략에서는 이 설정이 아무 영향을 주지 않습니다.`,"accountPool.priority":`선택 순서`,"accountPool.priorityAria":`이 계정의 선택 순서`,"accountPool.priorityHint":`숫자가 클수록 먼저 사용됩니다. 위에 있는 계정이 모두 소진되거나 사용할 수 없을 때에만 더 낮은 숫자로 넘어갑니다.`,"accountPool.priorityFirst":`가장 먼저`,"accountPool.priorityEarlier":`먼저`,"accountPool.priorityNormal":`기본`,"accountPool.priorityLater":`나중에`,"accountPool.priorityLast":`가장 마지막`,"accountPool.priorityOption":`{name} ({value})`,"accountPool.priorityCustom":`사용자 지정`,"accountPool.priorityUpdated":`{email}의 선택 순서를 업데이트했습니다`,"accountPool.priorityUpdateFailed":`{email}의 선택 순서를 저장하지 못했습니다. 마지막으로 확인된 값을 표시합니다.`,"codexAuth.switched":`다음 요청에 {email}을(를) 사용합니다`,"codexAuth.loadFailed":`Codex 계정 설정을 불러오지 못했습니다.`,"codexAuth.switchFailed":`계정을 전환하지 못했습니다. 이전 선택은 그대로 유지됩니다.`,"codexAuth.removeConfirm":`{id}을(를) 삭제하시겠습니까?`,"codexAuth.removeFailed":`계정을 제거하지 못했습니다. 변경된 내용은 없습니다.`,"codexAuth.addTitle":`Codex 계정 추가`,"codexAuth.addIdLabel":`계정 ID (슬러그)`,"codexAuth.addJsonLabel":`auth.json 내용`,"codexAuth.addHelp":`다른 머신의 ~/.codex/auth.json을 복사하거나, codex-auth export를 사용하세요.`,"codexAuth.importBtn":`가져오기`,"codexAuth.importInvalidJson":`유효하지 않은 JSON`,"codexAuth.importMissingTokens":`JSON에 access_token 또는 refresh_token이 없습니다`,"codexAuth.importMissingId":`계정 ID를 입력하세요`,"codexAuth.accountAdded":`풀에 계정이 추가되었습니다`,"codexAuth.addPickDesc":`다른 ChatGPT 계정으로 로그인하여 풀에 추가하세요.`,"codexAuth.oauthLogin":`OAuth 로그인`,"codexAuth.oauthDesc":`브라우저에서 ChatGPT 로그인 열기`,"codexAuth.deviceLogin":`기기 코드 로그인`,"codexAuth.deviceDesc":`헤드리스나 원격 프록시용. 다른 기기에서 짧은 코드를 입력합니다`,"codexAuth.importAuthJson":`auth.json 가져오기`,"codexAuth.importAuthJsonDesc":`다른 Codex 설치 또는 codex-auth export에서`,"codexAuth.back":`뒤로`,"codexAuth.oauthAlreadyInProgress":`로그인이 이미 진행 중입니다. 브라우저에서 완료하세요.`,"codexAuth.oauthWaiting":`브라우저에서 ChatGPT 로그인 완료를 기다리는 중...`,"codexAuth.oauthSubmittingCode":`코드를 제출 중…`,"codexAuth.oauthCodeSubmitted":`코드를 제출했습니다 — 로그인 완료를 기다리는 중입니다…`,"codexAuth.oauthStatusRetrying":`로그인 상태를 확인하는 중 네트워크 또는 프록시 오류가 발생했습니다 — 재시도 중…`,"codexAuth.oauthCancelled":`로그인이 취소되었습니다.`,"codexAuth.loginFailed":`로그인에 실패했습니다`,"codexAuth.needsReauth":`재로그인`,"codexAuth.reauthenticate":`Re-authenticate`,"codexAuth.tokenExpired":`토큰 만료 — 이 계정을 다시 인증하세요`,"codexAuth.mainTokenExpired":`토큰 만료 — Codex 앱 로그인으로 다시 로그인하세요`,"codexAuth.emailCollision":`이 계정은 메인 Codex 로그인과 동일합니다. 다른 계정을 사용하세요.`,"codexAuth.resetCreditsTitle":`리셋 크레딧`,"codexAuth.resetCreditsAvailable":`사용 가능한 리셋 크레딧이 {count}개 있습니다.`,"codexAuth.resetCreditsDesc":`크레딧 1개로 현재 시간/주간 사용량 제한을 즉시 초기화합니다.`,"codexAuth.noResetCredits":`사용 가능한 리셋 크레딧이 없습니다.`,"codexAuth.earnCreditsHint":`크레딧은 매월 자동 지급되며 추천 프로그램으로도 획득할 수 있습니다.`,"codexAuth.creditsExpireNote":`크레딧은 획득 후 30일 뒤 만료됩니다.`,"codexAuth.useOneCredit":`크레딧 1개 사용`,"codexAuth.confirmResetTitle":`리셋 크레딧을 사용하시겠습니까?`,"codexAuth.confirmResetDesc":`현재 사용량 제한이 즉시 초기화됩니다. 남은 크레딧: {count}개.`,"codexAuth.irreversible":`이 작업은 되돌릴 수 없습니다.`,"codexAuth.useCredit":`크레딧 사용`,"codexAuth.redeeming":`초기화 중...`,"codexAuth.resetSuccess":`사용량 제한이 초기화되었습니다! 남은 크레딧: {remaining}개.`,"codexAuth.resetSuccessGeneric":`사용량 제한이 초기화되었습니다!`,"codexAuth.resetAlreadyRedeemed":`이 크레딧은 이미 사용되었습니다. 크레딧은 변경되지 않았습니다.`,"codexAuth.resetNothingToReset":`현재 초기화할 사용량 윈도우가 없습니다.`,"codexAuth.resetNoCredit":`사용 가능한 리셋 크레딧이 없습니다.`,"codexAuth.resetError":`리셋 크레딧 사용에 실패했습니다. 다시 시도해 주세요.`,"codexAuth.fifoNote":`가장 오래된 크레딧부터 사용됩니다.`,"codexAuth.confirmWhichCredit":`{date}에 획득한 크레딧이 사용됩니다.`,"codexAuth.creditNext":`다음 사용 대상`,"codexAuth.creditLabel":`크레딧 #{n}`,"codexAuth.creditNextBadge":`NEXT`,"codexAuth.creditGranted":`획득 {date}`,"codexAuth.creditExpires":`만료 {date} ({days}일 남음)`,"api.title":`API 액세스`,"api.subtitle":`생성한 API 키로 외부 앱에서 opencodex 프록시에 접속합니다. 인증은 {authHeader} 헤더로 하며, 엔드포인트별로 받는 헤더는 아래 표에 있습니다.`,"api.endpointNote":`기본 URL을 OpenAI 호환 클라이언트에 사용하세요. Responses와 Chat Completions는 /v1 아래에 제공됩니다.`,"api.baseUrl":`기본 URL`,"api.responsesEndpoint":`Responses API`,"api.chatCompletionsEndpoint":`Chat Completions API`,"api.messagesEndpoint":`Messages API`,"api.modelsEndpoint":`Models API`,"api.endpointsTitle":`게이트웨이 엔드포인트`,"api.authBaseUrlNote":`클라이언트에는 기본 URL을 설정한 뒤 아래에서 프로토콜별 엔드포인트를 선택하세요.`,"api.authTitle":`인증`,"api.authLoopback":`루프백 바인드(127.0.0.1 또는 ::1)는 인증을 건너뜁니다. 원격 바인드는 생성된 ocx_ 키 또는 OPENCODEX_API_AUTH_TOKEN이 필요합니다.`,"api.modelsTitle":`외부 모델 카탈로그`,"api.modelsCount":`{count}개 호출 가능`,"api.modelsSearch":`모델 검색`,"api.modelsSubtitle":`이 정확한 모델 ID를 /v1/models와 선택한 인바운드 프로토콜과 함께 사용하세요.`,"api.modelsLoading":`모델 불러오는 중…`,"api.modelsEmpty":`아직 외부에서 호출 가능한 모델이 없습니다.`,"api.modelsNoMatch":`“{query}”와 일치하는 모델이 없습니다.`,"api.modelsLoadFailed":`외부 모델 카탈로그를 불러오지 못했습니다.`,"api.colModel":`모델`,"api.colSource":`출처`,"api.colProtocols":`프로토콜`,"api.copyModelId":`ID 복사`,"api.modelCopied":`복사됨`,"api.testModel":`테스트`,"api.testingModel":`테스트 중…`,"api.testSucceeded":`확인`,"api.testFailed":`실패`,"api.protocolResponses":`Responses`,"api.protocolChatCompletions":`Chat Completions`,"api.protocolMessages":`Messages`,"api.sourceNative":`ChatGPT 풀`,"api.sourceCombo":`콤보 경로`,"api.sourceCustom":`사용자 정의`,"api.usageResponsesTitle":`Responses 예시`,"api.usageChatTitle":`Chat Completions 예시`,"api.usageMessagesTitle":`Messages 예시`,"api.newKeyTitle":`새 키 생성됨`,"api.newKeyNote":`지금 키를 복사하세요. 다시 표시되지 않습니다.`,"api.copy":`복사`,"api.copied":`복사됨`,"api.dismiss":`닫기`,"api.generateTitle":`키 생성`,"api.keyNamePlaceholder":`키 이름 (선택)`,"api.generate":`생성`,"api.generating":`생성 중…`,"api.activeKeys":`활성 키 ({count})`,"api.activeKeysLoading":`활성 키`,"api.noKeys":`아직 API 키가 없습니다. 위에서 하나 생성하세요.`,"api.workspace.sections":`API 섹션`,"api.section.keys":`키`,"api.section.connect":`연결`,"api.section.endpoints":`엔드포인트`,"api.section.models":`모델`,"api.section.examples":`예제`,"api.workspace.details":`API 키 세부 정보`,"api.workspace.keyDetails":`키 세부 정보`,"api.workspace.keyPrefix":`키 접두사`,"api.workspace.deleteKey":`키 삭제`,"api.workspace.deleteConfirm":`이 키를 삭제하시겠습니까? 되돌릴 수 없습니다.`,"api.workspace.usageExamples":`사용 예제`,"api.copyUrlHint":`클릭하여 URL 복사`,"api.urlCopied":`URL 복사됨`,"api.copyExampleHint":`클릭하여 예제 복사`,"api.exampleCopied":`예제 복사됨`,"api.colName":`이름`,"api.colKey":`키`,"api.colCreated":`생성일`,"api.confirm":`확인`,"api.deleteAria":`API 키 삭제`,"api.usageSampleInput":`안녕하세요, 세계!`,"api.clientConfig.title":`클라이언트 설정`,"api.clientConfig.rowsLabel":`클라이언트 연결`,"api.clientConfig.details":`자세히`,"api.clientConfig.detailsAria":`{client} 설정 자세히 보기`,"api.clientConfig.copyAria":`{client} 설정 복사`,"api.clientConfig.downloadAria":`{client} 설정 다운로드`,"api.clientConfig.rowMeta":`{destination} · 모델 {count}개`,"api.clientConfig.rowError":`{client} 설정을 만들지 못했습니다.`,"api.clientConfig.copiedAnnounceClient":`{client} 설정을 클립보드에 복사했습니다.`,"api.clientConfig.clientOpencode":`OpenCode`,"api.clientConfig.clientPi":`Pi`,"api.clientConfig.clientOmp":`OMP`,"api.clientConfig.clientHermes":`Hermes`,"api.clientConfig.clientOpenclaw":`OpenClaw`,"api.clientConfig.clientKimi":`Kimi Code`,"api.clientConfig.clientGajae":`Gajae Code`,"api.clientConfig.clientDsh":`DeepSeek Harness (DSH)`,"api.clientConfig.clientMcode":`MiniMax Code`,"api.clientConfig.clientZcode":`ZCode`,"api.clientConfig.clientPrime":`Prime Agent`,"api.clientConfig.clientAside":`Aside`,"api.clientConfig.copy":`설정 복사`,"api.clientConfig.download":`다운로드`,"api.clientConfig.loading":`클라이언트 설정 생성 중…`,"api.clientConfig.jsonLabel":`{client} 설정`,"api.clientConfig.destination":`대상 파일`,"api.clientConfig.envHint":`실행 전 키 설정`,"api.clientConfig.mergeWarning":`대상 파일에 병합하세요. 덮어쓰면 기존 프로바이더와 MCP 설정이 사라집니다.`,"api.clientConfig.modelCount":`모델 {count}개 내보냄`,"api.clientConfig.missingLimits":`{total}개 중 {count}개 모델에 컨텍스트 한도가 없어 클라이언트 기본값이 적용됩니다.`,"api.clientConfig.noKeyYet":`{env}에 연결된 키가 아직 없습니다. 루프백 밖에서 쓰려면 위에서 키를 발급하세요.`,"api.clientConfig.loadFailed":`모델 목록을 읽지 못해 클라이언트 설정을 만들지 못했습니다.`,"api.clientConfig.copiedAnnounce":`클라이언트 설정을 클립보드에 복사했습니다.`,"api.clientConfig.copyFailed":`클라이언트 설정을 복사하지 못했습니다.`,"api.clientConfig.downloadedAnnounce":`{filename} 파일을 다운로드했습니다. 아직 아무것도 바뀌지 않았으니 {destination}에 직접 병합하세요.`,"api.clientConfig.whereDisclosure":`이 파일이 들어갈 위치`,"api.clientConfig.whereBody":`위 경로는 전역 설정 경로입니다. 작업 디렉터리의 프로젝트 설정 파일이 우선하며, 키는 설정에 적힌 환경 변수에서 읽고 이 파일에는 저장되지 않습니다.`,"api.keysLoadFailed":`API 키를 불러오지 못했습니다.`,"api.createFailed":`API 키를 만들지 못했습니다.`,"api.deleteFailed":`API 키를 삭제하지 못했습니다.`,"api.auth.endpoint":`엔드포인트`,"api.auth.required":`필수`,"api.auth.accepted":`가능`,"api.auth.rejected":`안 됨`,"api.auth.testProtocol":`{protocol} 테스트`,"api.auth.testNeedsFreshKey":`인증 테스트를 하려면 키를 새로 만들고 한 번만 보이는 값을 화면에 둔 채로 실행하세요.`,"api.key.name":`키 이름`,"api.key.rename":`이름 변경`,"api.key.saveName":`이름 저장`,"api.key.renaming":`저장 중…`,"api.key.renameFailed":`이름을 바꾸지 못했습니다. 입력한 내용은 그대로 뒀습니다.`,"api.key.deleting":`삭제 중…`,"api.rotation.title":`키 교체`,"api.rotation.description":`짧은 전환 시간 동안 기존 키를 유지한 채 새 키를 발급합니다.`,"api.rotation.start":`키 교체 시작`,"api.rotation.starting":`시작하는 중…`,"api.rotation.pending":`키 교체가 대기 중입니다. 클라이언트에 새 키를 적용하고 정상 연결을 확인한 뒤 확정하세요.`,"api.rotation.expires":`전환 가능 시간:`,"api.rotation.secretOnce":`새 키는 지금 한 번만 표시됩니다. 이 안내를 닫기 전에 복사하세요.`,"api.rotation.commit":`새 키로 확정`,"api.rotation.abort":`키 교체 취소`,"api.rotation.failed":`요청을 끝내지 못했습니다. 새로고침한 뒤 다시 시도하세요.`,"api.rotation.startFailed":`키 교체를 시작하지 못했습니다.`,"api.key.copyFailed":`키를 복사하지 못했습니다. 이 패널을 닫기 전에 직접 선택해서 복사하세요.`,"api.attribution.title":`키별 사용량`,"api.attribution.requests7d":`최근 7일 요청`,"api.attribution.totalRequests":`집계된 전체 요청`,"api.attribution.totalRequestsAvailable":`사용 가능한 기록의 요청`,"api.attribution.sinceAvailable":`사용 가능한 집계 시작일`,"api.attribution.lastUsed":`마지막 사용`,"api.attribution.since":`집계 시작`,"api.attribution.neverUsed":`집계 이후 사용 없음`,"api.attribution.unavailable":`사용량 없음`,"api.attribution.unavailableDetail":`아직 집계된 사용량이 없습니다. 집계가 시작되기 전 요청은 소급해서 배정할 수 없습니다.`,"api.attribution.ambiguous":`두 키가 같은 ID를 쓰고 있어 어느 쪽 사용량인지 가릴 수 없습니다. 설정 파일에서 키마다 다른 ID를 주세요.`,"api.attribution.railAmbiguous":`ID 중복`,"claude.subtitle":`Claude Code에서 GPT, Gemini 등 다른 모델도 쓸 수 있게 해줍니다.`,"claude.enabledLabel":`Claude 연결`,"claude.enabledHint":`끄면 Claude Code가 이 프록시를 사용할 수 없습니다.`,"claude.authMode":`인증 모드`,"claude.authModeHint":`subscription은 Claude 계정 필요, proxy는 opencodex 프록시만으로 사용 가능`,"claude.authModeSubscription":`Subscription (Claude 계정)`,"claude.authModeProxy":`Proxy (계정 불필요)`,"claude.authModeAuto":`자동 (Claude 인증 감지)`,"claude.effectiveMode.label":`다음 실행 시 적용`,"claude.effectiveMode.manual":`수동: {mode}`,"claude.effectiveMode.autoPresent":`자동: 구독 — {source}에서 Claude 인증을 찾았습니다`,"claude.effectiveMode.autoAbsent":`자동: 프록시 모드 — Claude 인증이 없습니다`,"claude.effectiveMode.autoUnknown":`자동: 구독 — 인증을 확인하지 못했습니다`,"claude.effectiveMode.admissionKey":`이 프록시의 API 키는 계속 전송됩니다.`,"claude.authSource.claude-json-oauth":`Claude 계정`,"claude.authSource.claude-credentials-file":`자격 증명 파일`,"claude.authSource.macos-keychain":`macOS 키체인`,"claude.authSource.exported-env":`환경 변수`,"claude.authSource.unknown":`감지된 자격 증명`,"claude.systemEnv":`자동 연결`,"claude.systemEnvDesc":`켜면 터미널에서 claude를 바로 실행해도 프록시를 거칩니다.`,"claude.systemEnvUnsupported":`자동 연결은 macOS에서만 지원됩니다. 이 시스템에서는 {cmd}로 Claude를 실행하세요.`,"claude.systemEnvWarn":`⚠ 터미널 앱을 완전히 종료했다 다시 열어야 적용됩니다. 사용을 권장하지 않습니다.`,"claude.fastMode":`Fast Mode (OpenAI)`,"claude.fastModeDesc":`OpenAI 모델의 추론 속도를 제어합니다. ON = 빠른 추론. OFF = 기본 속도. Auto = 클라이언트 설정 그대로.`,"claude.fastAuto":`Auto`,"claude.fastOn":`ON`,"claude.fastOff":`OFF`,"claude.autoContext":`큰 컨텍스트 자동 활용`,"claude.autoContextDesc":`1M 표기를 어디까지 붙일지 정합니다. 켜면 컴팩션 기준치를 담을 수 있는 창을 가진 모델에 큰 컨텍스트 행이 생기고, 끄면 진짜 1M 모델에만 생깁니다.`,"claude.autoContextInert":`설정 파일에 이전 방식의 컨텍스트 크기 값(maxContextTokens)이 있어 이 기능이 지금은 적용되지 않아요. 설정에서 그 값을 지우면 다시 켜집니다.`,"claude.autoCompactWindow":`자동 요약 지점`,"claude.autoCompactDefault":`{value} (기본값)`,"claude.autoCompactWindowDesc":`대화가 이 지점에 다다르면 오래된 내용을 자동 요약합니다. 모델마다 자기 한도를 넘지 않는 선에서만 적용되니 200k 모델은 영향받지 않아요.`,"claude.autoCompactWindowWarn":`값을 직접 바꾸면 GPT 모델들이 제대로 동작하지 않을 수 있어요 — 모델의 실제 한도보다 크게 잡으면 요약이 되기 전에 대화가 오류로 멈춥니다.`,"claude.injectAgents":`서브에이전트 자동 등록`,"claude.injectAgentsDesc":`위 '서브에이전트' 탭에서 고른 모델들(+현재 기본 모델)을 Claude Code의 파견 가능한 에이전트(ocx-*)로 자동 등록합니다. 새 세션부터 적용돼요.`,"claude.webSearchSidecar":`웹 검색 사이드카 덮어쓰기`,"claude.webSearchSidecarHint":`Claude Code 요청에만 메인 웹 검색 사이드카 대신 이 설정을 씁니다.`,"claude.visionSidecar":`비전 사이드카 덮어쓰기`,"claude.visionSidecarHint":`Claude Code 요청에만 메인 비전 사이드카 대신 이 설정을 씁니다.`,"claude.useMainSetting":`메인 설정 사용`,"claude.sidecarModelPlaceholder":`메인 설정의 모델`,"claude.quickstart":`시작하기`,"claude.quickstartHint":`{cmd} 을 실행하면 프록시를 거쳐 Claude Code가 열립니다. claude.ai 로그인은 그대로 유지됩니다.`,"claude.manualEnv":`직접 설정하기 (고급)`,"claude.smallFastModel":`백그라운드 보조 모델`,"claude.smallFastModelHint":`Claude Code가 대화 요약, 주제 감지 같은 배후 작업에 쓰는 모델입니다. 서브에이전트의 haiku 별칭도 이 모델을 씁니다. 비워두면 Claude 기본값(Haiku).`,"claude.smallFastModelAccurateHint":`Claude Code가 대화 요약, 주제 감지 같은 백그라운드 작업에 쓰는 모델입니다. 서브에이전트의 haiku 별칭도 이 모델을 사용합니다.`,"claude.smallFastModelUnsetOption":`Claude Code가 선택(네이티브 모델)`,"claude.smallFastModelNativeWarning":`비워 두면 OpenCodex가 보조 모델 환경 변수를 설정하지 않습니다. Claude Code가 네이티브 Sonnet 모델을 사용할 수 있으며, 네이티브 프로바이더 요금이 발생할 수 있습니다.`,"claude.slotUnset":`Claude 기본값 사용`,"claude.modelMap":`모델 가로채기`,"claude.modelMapHint":`Claude가 특정 모델을 요청하면 가로채서 지정한 모델로 보냅니다. 기본값은 비어 있어요 — 규칙을 추가할 때만 동작합니다.`,"claude.mapFrom":`원래 모델 (예: claude-sonnet-4-5)`,"claude.mapTo":`바꿀 모델 (예: gemini/gemini-3-pro)`,"claude.addMapping":`규칙 추가`,"claude.removeMapping":`규칙 삭제`,"claude.aliases":`사용 가능한 모델`,"claude.aliasesHint":`Claude Code의 /model 메뉴에 표시되는 모델 목록입니다.`,"claude.aliasProviderOther":`기타`,"claude.loading":`불러오는 중…`,"claude.loadFail":`Claude 설정을 불러오지 못했습니다`,"claude.saved":`저장되었습니다.`,"claude.saveFailed":`저장 실패`,"claude.networkError":`네트워크 오류 — 프록시가 실행 중인가요?`,"claude.toggleAria":`Claude 인바운드 켜기/끄기`,"claude.none":`없음`,"common.close":`닫기`,"common.ok":`확인`,"app.logoAria":`opencodex 로고`,"app.claudeOn":`Claude ON`,"app.claudeOff":`Claude OFF`,"usage.dayMon":`월`,"usage.dayWed":`수`,"usage.dayFri":`금`,"usage.heatmap.tooltipTokens":`{tokens} 토큰`,"usage.heatmap.tooltipRequests":`{requests} 요청`,"nav.storage":`저장소`,"storage.title":`저장소`,"storage.subtitle":`CODEX_HOME 사용량을 확인합니다. 정리는 활성 세션을 건드리지 않습니다.`,"storage.loading":`저장소 스캔 중…`,"storage.empty":`CODEX_HOME이 비어 있거나 없습니다 — 표시할 내용이 없습니다.`,"storage.error":`저장소 스캔에 실패했습니다. CODEX_HOME이 올바른 디렉터리를 가리키는지 확인하세요.`,"storage.refresh":`다시 스캔`,"storage.rescanned":`스캔이 완료되었습니다.`,"storage.card.total":`전체 크기`,"storage.card.files":`파일 수`,"storage.card.home":`CODEX_HOME`,"storage.snapshot.lastScan":`마지막 스캔`,"storage.snapshot.scanning":`스캔 중…`,"storage.snapshot.unavailable":`아직 스캔 없음.`,"storage.cleanupCard.title":`공간 확보`,"storage.cleanupCard.tabs":`정리 옵션`,"storage.cleanupCard.tab.policy":`정책`,"storage.cleanupCard.tab.quarantine":`격리`,"storage.cleanup.noArchives":`정리할 보관 세션이 없습니다.`,"storage.section.buckets":`버킷`,"storage.section.largest":`가장 큰 파일`,"storage.workspace.overview":`개요`,"storage.workspace.selectBucket":`목록에서 버킷을 선택하면 세부 내역을 볼 수 있습니다.`,"storage.col.bucket":`버킷`,"storage.col.size":`크기`,"storage.col.files":`파일`,"storage.col.oldest":`가장 오래됨`,"storage.col.newest":`가장 최근`,"storage.col.rows":`DB 행 수`,"storage.rows.unknown":`알 수 없음 (잠김)`,"storage.bucket.sessions":`활성 세션`,"storage.bucket.archived_sessions":`보관된 세션`,"storage.bucket.logs_db":`로그 데이터베이스`,"storage.bucket.state_db":`상태 데이터베이스`,"storage.bucket.attachments":`첨부 파일`,"storage.bucket.deletion_manifests":`삭제 매니페스트`,"storage.bucket.other":`기타`,"storage.cleanup.title":`보관 정리`,"storage.cleanup.help":`가장 오래된 보관 세션을 비율로 제거합니다. 활성 세션은 건드리지 않습니다. 기본은 격리이며 파일은 CODEX_HOME/.trash로 이동합니다.`,"storage.cleanup.slider":`오래된 보관 비율`,"storage.cleanup.percent":`{percent}%`,"storage.cleanup.preset":`{percent}`,"storage.cleanup.preview":`미리보기`,"storage.cleanup.confirmTitle":`보관 정리 확인`,"storage.cleanup.confirmBody":`보관 파일 {count}개(약 {size}), 오래된 {percent}%를 처리합니다.`,"storage.cleanup.moreFiles":`…외 {n}개`,"storage.cleanup.permanent":`영구 삭제(격리 건너뛰기)`,"storage.cleanup.permanentWarn":`영구 삭제는 되돌릴 수 없습니다.`,"storage.cleanup.quarantineNote":`파일은 CODEX_HOME 아래 .trash로 이동합니다. 격리 탭에서 복원할 수 있습니다.`,"storage.cleanup.cancel":`취소`,"storage.cleanup.confirmQuarantine":`격리`,"storage.cleanup.confirmPermanent":`영구 삭제`,"storage.cleanup.doneQuarantine":`파일 {count}개를 격리했습니다({size}).`,"storage.cleanup.donePermanent":`파일 {count}개를 영구 삭제했습니다({size}).`,"storage.cleanup.previewFailed":`미리보기에 실패했습니다.`,"storage.cleanup.cleanupFailed":`정리에 실패했습니다.`,"storage.cleanup.err.codex_busy":`Codex가 state.sqlite를 사용 중입니다 — Codex를 종료한 뒤 다시 시도하세요.`,"storage.cleanup.err.stale_preview":`미리보기 이후 보관 파일이 변경되었습니다 — 미리보기를 다시 실행하세요.`,"storage.cleanup.err.restore_pending_overlap":`선택한 보관 파일이 미완료 휴지통 복원과 겹칩니다 — 복원을 완료하거나 다시 시도하세요.`,"storage.cleanup.err.referenced_history":`선택한 보관본이 포크 또는 페이지 기록에서 아직 참조됩니다.`,"storage.cleanup.err.invalid_digest":`미리보기 digest가 없거나 잘못되었습니다.`,"storage.cleanup.err.invalid_mode":`모드는 quarantine 또는 permanent여야 합니다.`,"storage.cleanup.err.fs_failed":`파일 시스템 정리에 실패했습니다. 일부 변경이 이미 적용되었을 수 있습니다 — CODEX_HOME/.trash와 표시된 복구 경로를 확인하세요.`,"storage.cleanup.err.fs_failed_trash":`파일 시스템 정리에 실패했습니다. 일부 변경이 이미 적용되었을 수 있습니다 — {trashDir}와 manifest.json에서 복구 가능한 파일을 확인하세요.`,"storage.cleanup.err.db_reconcile_failed":`Codex 상태 데이터베이스를 업데이트할 수 없습니다.`,"storage.cleanup.err.cleanup_failed":`정리에 실패했습니다.`,"storage.trash.title":`격리`,"storage.trash.help":`CODEX_HOME/.trash로 옮긴 보관 세션입니다. 복원하면 JSONL과 스레드 행이 돌아갑니다.`,"storage.trash.empty":`격리된 항목이 없습니다.`,"storage.trash.loading":`격리 목록 불러오는 중…`,"storage.trash.col.when":`격리 시각`,"storage.trash.col.files":`파일`,"storage.trash.col.size":`크기`,"storage.trash.col.mode":`모드`,"storage.trash.col.id":`항목`,"storage.trash.restore":`복원`,"storage.trash.confirmTitle":`격리 항목을 복원할까요?`,"storage.trash.confirmBody":`{id}에서 파일 {count}개(약 {size})를 보관 세션으로 되돌립니다.`,"storage.trash.cancel":`취소`,"storage.trash.confirmRestore":`복원`,"storage.trash.done":`파일 {count}개를 복원했습니다({size}).`,"storage.trash.restoreFailed":`복원에 실패했습니다.`,"storage.trash.listFailed":`격리 목록을 불러오지 못했습니다.`,"storage.trash.mode.quarantine":`격리`,"storage.trash.mode.permanent":`영구(미완료)`,"storage.trash.err.codex_busy":`Codex가 state.sqlite를 사용 중입니다 — Codex를 종료한 뒤 다시 시도하세요.`,"storage.trash.err.invalid_trash":`격리 항목 ID가 없거나 잘못되었습니다.`,"storage.trash.err.missing_trash":`격리 항목을 찾을 수 없습니다.`,"storage.trash.err.dest_exists":`복원 대상이 이미 있습니다 — 보관 파일을 삭제하거나 이름을 바꾼 뒤 다시 시도하세요.`,"storage.trash.err.fs_failed":`파일 시스템 복원에 실패했습니다. 일부 파일이 이미 복원되었을 수 있습니다 — archived_sessions와 .trash를 확인하세요.`,"storage.trash.err.storage_mutation_busy":`다른 저장소 정리 또는 복원이 진행 중입니다 — 잠시 후 다시 시도하세요.`,"storage.trash.err.db_reconcile_failed":`Codex 상태 데이터베이스 행을 복원할 수 없습니다.`,"storage.trash.err.restore_failed":`복원에 실패했습니다.`,"storage.trash.err.restore_worker_timeout":`복원 시간이 너무 길어(10분 초과) 중단되었습니다.`,"storage.trash.err.restore_worker_aborted":`종료 중 복원이 취소되었습니다.`,"storage.trash.err.restore_worker_failed":`복원 워커가 충돌하거나 예기치 않게 실패했습니다.`,"storage.policy.title":`자동 정리 정책`,"storage.policy.help":`보관 세션이 임계값을 넘을 때 선택적으로 일괄 정리합니다. 기본은 꺼짐 — 자동으로 켜지지 않습니다.`,"storage.policy.loading":`정책을 불러오는 중…`,"storage.policy.loadFailed":`정리 정책을 불러오지 못했습니다.`,"storage.policy.saveFailed":`정리 정책을 저장하지 못했습니다.`,"storage.policy.runFailed":`정책 실행에 실패했습니다.`,"storage.policy.alreadyRunning":`정리 정책이 이미 실행 중입니다.`,"storage.policy.invalid":`정책 값이 올바르지 않습니다.`,"storage.policy.enabled":`자동 정리 사용`,"storage.policy.enabledHint":`기본은 꺼짐입니다. 켜면 선택한 일정(또는 지금 실행)에만 동작합니다.`,"storage.policy.threshold":`보관 용량이 초과하면 (GiB)`,"storage.policy.trigger":`트리거`,"storage.policy.target":`정리 목표`,"storage.policy.targetPercent":`가장 오래된 보관 제거 (%)`,"storage.policy.targetReduce":`보관 용량을 다음까지 줄이기 (GiB)`,"storage.policy.thresholdInc":`임계값 증가`,"storage.policy.thresholdDec":`임계값 감소`,"storage.policy.percentInc":`퍼센트 증가`,"storage.policy.percentDec":`퍼센트 감소`,"storage.policy.reduceInc":`축소 목표 증가`,"storage.policy.reduceDec":`축소 목표 감소`,"storage.policy.schedule":`일정`,"storage.policy.schedule.manual":`수동만`,"storage.policy.schedule.startup":`프록시 시작 시`,"storage.policy.schedule.daily":`매일`,"storage.policy.schedule.weekly":`매주`,"storage.policy.mode":`삭제 모드`,"storage.policy.mode.quarantine":`격리(기본)`,"storage.policy.mode.permanent":`영구 삭제`,"storage.policy.permanentWarn":`영구 모드는 되돌릴 수 없습니다. 확실하지 않으면 격리를 사용하세요.`,"storage.policy.lastRun":`마지막 실행`,"storage.policy.lastRunDetail":`{count}개 제거 · {size} 확보`,"storage.policy.nextRun":`다음 실행`,"storage.policy.never":`없음`,"storage.policy.save":`저장`,"storage.policy.runNow":`지금 실행`,"storage.policy.running":`실행 중…`,"storage.policy.saved":`정책을 저장했습니다.`,"storage.policy.skippedDisabled":`정책이 꺼져 있습니다 — 먼저 켜세요.`,"storage.policy.skippedUnder":`보관 용량이 임계값 미만입니다 — 할 일이 없습니다.`,"storage.policy.skippedEmpty":`목표에 맞는 보관 후보가 없습니다.`,"storage.policy.doneQuarantine":`정책이 파일 {count}개를 격리했습니다({size}).`,"storage.policy.donePermanent":`정책이 파일 {count}개를 영구 삭제했습니다({size}).`,"storage.policy.metadataSaveWarning":`정책 실행은 완료됐지만 일정 메타데이터를 저장하지 못했습니다.`,"modal.back":`뒤로`,"modal.badge.oauth":`OAuth`,"modal.customProvider":`사용자 지정 프로바이더`,"modal.failedStatus":`실패 ({status})`,"modal.loginError":`로그인 오류: {error}`,"modal.badge.codexLogin":`Codex 로그인`,"modal.badge.local":`로컬`,"modal.badge.apiKey":`API 키`,"modal.badge.direct":`Direct`,"modal.badge.pool":`풀`,"modal.badge.free":`무료`,"modal.invalidPreset":`내장 프로바이더 설정이 완전하지 않습니다. 프록시를 다시 시작한 뒤 재시도하세요.`,"modal.freeTierTitle":`무료 티어`,"modal.freeTierDefault":`API 키가 필요 없습니다. 바로 사용할 수 있습니다.`,"modal.tab.accounts":`계정`,"modal.tab.free":`무료`,"modal.tab.paid":`유료`,"modal.accountsHint":`여기서 ChatGPT/Codex, OAuth, API 키 계정에 로그인하세요. OpenAI는 기본 제공 — 다시 추가하지 말고 로그인하세요.`,"modal.accountsCodexAuthLink":`Codex 인증`,"modal.notListed":`찾는 프로바이더가 없나요? 직접 추가`,"modal.catalogLoading":`카탈로그 불러오는 중…`,"modal.accountLogin":`로그인`,"modal.accountLogout":`로그아웃`,"modal.accountAdd":`계정 추가`,"modal.accountManage":`관리`,"modal.accountCodexPool":`ChatGPT 계정 풀`,"modal.accountLoggedIn":`로그인됨`,"modal.accountLoggedOut":`로그인 안 됨`,"quota.fiveHourLimit":`5시간 한도`,"quota.ageMinutes":`{n}분`,"quota.ageHours":`{n}시간`,"quota.ageDays":`{n}일`,"quota.observedAgo":`{age} 전에 확인한 값`,"quota.observedHint":`Meta는 스트리밍 응답 중에만 사용량을 보고합니다. 실시간 수치가 아니라 마지막으로 확인된 값입니다.`,"quota.weeklyLimit":`주간 한도`,"quota.monthlyLimit":`30일 한도`,"quota.cursorFirstParty":`자사 모델`,"quota.cursorApiUsage":`API 사용량`,"quota.totalSubscriptionCredits":`전체 구독 크레딧`,"quota.creditsBalance":`크레딧 잔액`,"quota.creditsPeriodEnds":`청구 기간 종료: {date}`,"quota.usedPercent":`{pct}% 사용`,"quota.limitReached":`한도 도달`,"quota.resetsToday":`오늘 {time} 초기화`,"quota.resetsTomorrow":`내일 {time} 초기화`,"quota.resetsAt":`{when} 초기화`,"quota.resetsRelativeMinutes":`{n}분 후 초기화`,"quota.resetsRelativeHours":`{n}시간 후 초기화`,"pws.status.ready":`준비됨`,"pws.status.needsSetup":`설정 필요`,"pws.status.needsAttention":`확인 필요`,"pws.auth.chatgptPassthrough":`ChatGPT 패스스루`,"pws.auth.noKey":`키 불필요`,"pws.freeTitle":`무료 요금제 (키는 필요할 수 있음)`,"pws.localTitle":`로컬 런타임`,"pws.modelCountOne":`모델 1개`,"pws.modelCount":`모델 {count}개`,"pws.rail.suffixDefault":` · 기본`,"pws.rail.suffixLocal":` · 로컬`,"pws.rail.suffixFree":` · 무료`,"pws.rail.selectAria":`{name} 선택 — {status}{suffix}`,"pws.searchPlaceholder":`프로바이더 검색…`,"pws.filterAria":`프로바이더 필터`,"pws.providerFiltersAria":`프로바이더 필터`,"pws.filters":`필터`,"pws.filterStatus":`상태`,"pws.pricing":`요금`,"pws.paid":`유료`,"pws.filterType":`유형`,"pws.type.cloud":`클라우드`,"pws.type.local":`로컬`,"pws.type.selfHosted":`셀프 호스팅`,"pws.type.login":`로그인`,"pws.sort":`정렬`,"pws.sortProvidersAria":`프로바이더 정렬`,"pws.sort.az":`A–Z`,"pws.sort.za":`Z–A`,"pws.sort.freePaid":`무료 우선`,"pws.sort.paidFree":`유료 우선`,"pws.sort.accountsFirst":`계정 우선`,"pws.resetAll":`모두 초기화`,"pws.providerList":`프로바이더 목록`,"pws.providersAria":`프로바이더`,"pws.groupReady":`준비됨 ({count})`,"pws.groupNeedsSetup":`설정 필요 ({count})`,"pws.groupDisabled":`비활성화 ({count})`,"pws.noSearchResults":`검색과 일치하는 프로바이더가 없습니다.`,"pws.noMatchFilters":`필터와 일치하는 프로바이더가 없습니다.`,"pws.noProvidersConfigured":`설정된 프로바이더가 없습니다.`,"pws.workspaceMainAria":`프로바이더 상세`,"pws.detailComingSoon":`상세 보기는 준비 중입니다 — 클래식 보기에서 관리하세요.`,"pws.selectPrompt":`목록에서 프로바이더를 선택하세요.`,"pws.connectFirst":`첫 프로바이더를 연결하세요`,"pws.empty.browseFree":`무료 프로바이더 보기`,"pws.empty.browseFreeDesc":`구독 없이 시작`,"pws.empty.connectAccount":`계정 연결`,"pws.empty.connectAccountDesc":`ChatGPT 또는 프로바이더 로그인 사용`,"pws.empty.addEndpoint":`엔드포인트 추가`,"pws.empty.addEndpointDesc":`커스텀 base URL과 API 키`,"pws.tab.overview":`개요`,"pws.tab.models":`모델`,"pws.tab.usage":`사용량`,"pws.tab.accounts":`계정`,"pws.tab.settings":`설정`,"pws.connection":`연결`,"pws.status.connected":`연결됨`,"pws.attentionTitle":`확인 필요`,"pws.attention.reauth":`활성 계정 재인증이 필요합니다`,"pws.attention.reauthForward":`활성 Codex 계정 재인증이 필요합니다 — 계정에서 해결하세요`,"pws.attention.missingCredentials":`자격 증명 없음`,"pws.cell.auth":`인증`,"pws.cell.note":`메모`,"pws.cell.defaultModel":`기본 모델`,"pws.statsAria":`프로바이더 통계`,"pws.statsTitle":`통계`,"pws.stats.totalRequests":`요청 수 (30일)`,"pws.stats.totalTokens":`토큰 (30일)`,"pws.stats.quotaUpdated":`쿼터 갱신`,"pws.stats.quotaTracked":`사용량 탭에서 한도를 확인할 수 있습니다.`,"pws.stats.source":`출처`,"pws.usageLast30d":`사용량 (최근 30일)`,"pws.estimatedCost":`추정 비용`,"pws.costDisclaimer":`API 공시가 기준 추정치이며, 실제 청구 금액이 아닙니다.`,"pws.modelBreakdown":`모델별 사용량`,"pws.col.model":`모델`,"pws.col.cost":`추정 비용`,"pws.col.tokens":`토큰`,"pws.col.requests":`요청`,"pws.col.share":`점유율`,"pws.tokenInput":`입력`,"pws.tokenOutput":`출력`,"pws.metricRequests":`요청`,"pws.metricTokens":`토큰`,"pws.usageUnavailable":`아직 기록된 사용량이 없습니다.`,"pws.rateLimits":`요청 한도`,"pws.quotaUnavailable":`이 프로바이더의 쿼터 데이터가 없습니다.`,"pws.accountQuotaUnavailable":`요금 한도 데이터를 일시적으로 가져올 수 없습니다. 이전 값이 있으면 그대로 표시합니다.`,"pws.selected":`선택됨`,"pws.copyModelId":`ID 복사`,"pws.modelCopied":`복사됨!`,"pws.modelsAvailable":`{count}개 사용 가능`,"pws.modelSearchPlaceholder":`모델 필터…`,"pws.modelsLoading":`모델 불러오는 중…`,"pws.modelsLoadFailed":`모델을 불러오지 못했습니다.`,"pws.modelsNeedsReauth":`실시간 모델 목록을 받으려면 다시 로그인해야 합니다. 지금은 설정된 모델을 표시합니다.`,"pws.modelsConfiguredFallback":`설정된 모델을 표시합니다 (실시간 검색 불가).`,"pws.modelsTruncated":`{total}개 모델 중 처음 {shown}개를 표시합니다. 필터로 목록을 좁히세요.`,"pws.retry":`다시 시도`,"pws.noModels":`이 프로바이더에서 발견된 모델이 없습니다.`,"pws.noModelMatch":`필터와 일치하는 모델이 없습니다.`,"pws.adapterBaseRequired":`어댑터와 기본 URL은 필수입니다.`,"pws.addAccount":`계정 추가`,"pws.addKey":`API 키 추가`,"pws.apiKeys":`API 키`,"pws.authMode":`인증 방식`,"pws.availableAccounts":`사용 가능한 계정`,"pws.accountOrdinal":`계정 {count}`,"pws.accountsLoading":`계정 불러오는 중…`,"pws.accountsLoadFailed":`계정을 불러오지 못했습니다.`,"pws.retryAccounts":`다시 시도`,"pws.noAccounts":`연결된 계정이 아직 없습니다.`,"pws.cockpitImportDescription":`이 기기에서 Cockpit Tools Antigravity JSON 내보내기를 가져옵니다. 파일 내용은 표시되지 않습니다.`,"pws.cockpitImportFileLabel":`Cockpit Tools Antigravity JSON 내보내기`,"pws.cockpitImportChooseFile":`JSON 파일 선택`,"pws.cockpitImporting":`가져오는 중…`,"pws.cockpitImportInvalid":`선택한 파일은 유효한 JSON 내보내기가 아니거나 너무 큽니다.`,"pws.cockpitImportFailed":`계정 가져오기를 완료할 수 없습니다.`,"pws.cockpitImportComplete":`가져오기 완료: 가져옴 {imported}, 업데이트 {updated}, 실패 {failed}, 지원되지 않음 {unsupported}.`,"pws.accountSwitching":`전환 중…`,"pws.accountCurrent":`현재 계정`,"pws.defaultModelNone":`없음 (프로바이더 기본값 사용)`,"pws.discardSettings":`되돌리기`,"pws.jsonEditorDesc":`프로바이더 JSON 설정을 직접 편집합니다. 저장 즉시 반영됩니다.`,"pws.jsonEditorTitle":`JSON 편집기 — {name}`,"pws.jsonRestore":`복원`,"pws.jsonSave":`저장`,"pws.loggedInTitle":`로그인됨`,"pws.notLoggedInTitle":`로그인 필요`,"pws.note":`메모`,"pws.allowPrivateNetwork":`로컬/사설 네트워크 허용`,"pws.liveModels":`프로바이더에서 모델 검색`,"pws.liveModelsDesc":`프로바이더의 실시간 모델 카탈로그를 가져옵니다. 끄면 설정된 정적 모델만 사용합니다.`,"pws.xaiResponsesOptIn":`Grok 4.5와 4.6에 Responses API 사용`,"pws.xaiResponsesOptInDesc":`두 모델을 openai-responses로 라우팅합니다. 다른 Grok 모델과 티어 동작은 바뀌지 않습니다.`,"pws.xaiResponsesOptInMixed":`일부만 활성화됨.`,"pws.cursorTransport":`Cursor 전송`,"pws.cursorTransportHttp2":`HTTP/2 (기본값)`,"pws.cursorTransportHttp1":`HTTP/1.1 (프록시 호환)`,"pws.cursorTransportDesc":`프록시가 Cursor의 HTTP/2 스트림을 안정적으로 전달하지 못할 때 HTTP/1.1을 사용하세요.`,"pws.optionalPlaceholder":`선택사항`,"pws.providerId":`프로바이더 ID`,"pws.reauth":`재인증 필요`,"pws.reauthenticate":`재인증`,"pws.copyDoctor":`ocx doctor 복사`,"pws.doctorCopied":`복사됨`,"pws.healthCooldownHint":`쿨다운이 끝날 때까지 기다리세요. 지금은 이 계정을 프로브하지 마세요.`,"pws.doctorCopyUnavailable":`클립보드를 사용할 수 없음`,"pws.healthLabel.rateLimited":`요청 한도 초과`,"pws.healthLabel.quotaLimited":`할당량 제한`,"pws.healthLabel.reauthRequired":`재인증 필요`,"pws.healthLabel.refreshFailed":`새로고침 실패`,"pws.healthLabel.metadataMismatch":`메타데이터 불일치`,"pws.healthLabel.credentialConflict":`자격 증명 충돌`,"pws.healthSummary.rateLimited":`{provider} {account}: {until}까지 요청 한도 초과. 그전까지 이 계정 라우팅이 일시 중지됩니다.`,"pws.healthSummary.quotaLimited":`{provider} {account}: {until}까지 할당량 제한. 그전까지 이 계정 라우팅이 일시 중지됩니다.`,"pws.healthSummary.reauthRequired":`{provider} {account}: 재인증이 필요합니다.`,"pws.healthSummary.credentialConflict":`{provider} {account}: 자격 증명 충돌.`,"pws.healthSummary.metadataMismatch":`{provider} {account}: 메타데이터 불일치.`,"pws.healthSummary.staleCredentials":`{provider} {account}: 자격 증명이 불완전합니다.`,"pws.removeConfirm":`제거`,"pws.removeConfirmBody":`프로바이더 "{name}"을(를) 제거하시겠습니까? 되돌릴 수 없습니다.`,"pws.removeDefaultConfirmBody":`기본 프로바이더 "{name}"을(를) 제거하시겠습니까? "{defaultProvider}"이(가) 기본 프로바이더가 됩니다. 이 작업은 되돌릴 수 없습니다.`,"pws.removeConfirmTitle":`프로바이더 제거`,"pws.saveSettings":`저장`,"pws.pacingTitle":`요청 속도 조절`,"pws.pacingDesc":`이 프로바이더로 나가는 요청 시작을 일정 간격으로 지연합니다. 스트리밍 응답은 서로 겹칠 수 있습니다.`,"pws.pacingEnabled":`사용`,"pws.pacingRpm":`분당 요청 수`,"pws.pacingRpmUnit":`RPM`,"pws.pacingDelay":`최소 시작 간격(ms)`,"pws.pacingSlowerWins":`프로바이더의 더 느린 제한이 우선합니다. 모델별 설정은 지연을 더 늘릴 때만 적용됩니다.`,"pws.pacingQueued":`대기 중`,"pws.pacingNextSlot":`다음 슬롯까지`,"pws.pacingLastModel":`마지막 모델`,"pws.pacingNone":`없음`,"pws.pacingModelOverrides":`모델별 설정`,"pws.pacingModel":`모델`,"pws.pacingAdd":`모델 제한 추가`,"pws.pacingRemove":`제거`,"pws.pacingRemoveModel":`{model} 모델의 요청 속도 설정 제거`,"pws.pacingRuleRequired":`프로바이더 제한이나 모델별 제한을 하나 이상 설정한 뒤 요청 속도 조절을 켜세요.`,"pws.saving":`저장 중…`,"pws.settingsSaved":`설정이 저장되었습니다.`,"pws.accountModeSaved":`계정 모드가 저장되었습니다.`,"pws.accountModeFailed":`계정 모드를 전환할 수 없습니다.`,"pws.accountModeConfirm":`OpenAI 계정 모드를 전환할까요? 실행 중인 대화가 다른 모드의 계정 세트로 다시 연결되며, 할당량 사용량은 새 모드로 집계됩니다.`,"pws.settingsUnsavedBar":`저장하지 않은 변경사항이 있습니다.`,"pws.unsavedLeaveBody":`저장하지 않은 변경사항이 있습니다. 나가기 전에 저장하시겠습니까?`,"pws.unsavedLeaveTitle":`미저장 변경사항`,"pws.attentionRequired":`주의 필요`,"pws.attentionAria":`{name}: {reason}`,"pws.missingCredentials":`자격 증명 없음`,"pws.editJsonDesc":`프록시 설정을 JSON으로 편집`,"pws.updatesUnavailable":`프로바이더 업데이트를 사용할 수 없습니다.`,"pws.dashboard.title":`프로바이더 개요`,"pws.dashboard.subtitle":`모든 모델 프로바이더를 한곳에서 관리합니다.`,"pws.dashboard.rateLimits":`사용량 제한`,"pws.capacity.estimate":`설정 가중치 기반 풀 추정치`,"pws.capacity.currentAccount":`현재 유효 계정`,"pws.capacity.nextRecovery":`다음 용량 회복`,"pws.capacity.recoveryShare":`+{percent}% 풀 용량`,"pws.capacity.incomplete":`불완전한 범위: {excluded}개 계정 제외`,"pws.capacity.uncalibratedPlan":`보정되지 않은 요금제 {count}개는 기본 좌석 가중치로 계산되어, 이 추정치가 실제보다 낮을 수 있습니다`,"pws.capacity.partial":`일부 기간의 범위가 불완전합니다: {count}개 계정에서 표시된 모든 한도 기간을 확인할 수 없습니다`,"pws.capacity.windowPartial":`일부만`,"pws.capacity.windowPartialA11y":`{window}: 계정 범위가 불완전합니다`,"pws.dashboard.recentlyUsed":`최근 사용`,"pws.dashboard.requests":`{count}건 요청`,"pws.dashboard.checkedAgo":`{time} 확인`,"pws.dashboard.noQuota":`할당량 데이터 없음`,"pws.dashboard.noUsage":`아직 사용 데이터 없음`,"pws.dashboard.noRateLimits":`아직 한도 데이터 없음`,"pws.allProviders":`프로바이더 개요`,"pws.enabledLabel":`활성화`,"pws.testConnection":`연결 테스트`,"pws.testing":`테스트 중…`,"pws.connectionOk":`연결 성공`,"pws.connectionFailed":`연결 실패`,"pws.connectionNotApplicable":`해당 없음 — 이 프로바이더는 정적 모델 카탈로그를 사용합니다.`,"pws.editSettings":`설정 편집`,"pws.viewUsage":`사용량 상세 보기`,"pws.allSystemsOk":`모든 시스템 정상`,"pws.apiKeyConfigured":`API 키 설정됨`,"pws.addApiKey":`API 키 추가`,"pws.loggedInAs":`{email}으로 로그인됨`,"pws.notLoggedIn":`로그인되지 않음`,"pws.passthrough":`Codex 패스스루`,"pws.notes":`메모`,"pws.notePlaceholder":`이 프로바이더에 대한 메모를 추가하세요...`,"pws.noteSaved":`메모 저장됨`,"pws.authSummary":`인증`,"time.justNow":`방금 전`,"time.notChecked":`확인 안 됨`,"time.minutesAgo":`{n}분 전`,"time.hoursAgo":`{n}시간 전`,"time.daysAgo":`{n}일 전`,"modal.noMatch":`일치 항목 없음.`,"modal.oauthDefaultNote":`계정으로 로그인 — API 키 불필요.`,"modal.oauthComingSoon":`{label} OAuth 로그인은 다음 업데이트에 제공됩니다. 지금은 API 키를 사용하세요.`,"modal.oauthComingSoonShort":`이 프로바이더의 OAuth 로그인은 다음 업데이트에 제공됩니다 — 지금은 API 키를 사용하세요.`,"modal.useApiKeyInstead":`대신 API 키 사용`,"modal.setupGuide":`설정 안내`,"modal.setupStep1Prefix":`다음으로 이동:`,"modal.setupDashboardLink":`{label} 대시보드`,"modal.setupStep1Suffix":`에서 API 키를 복사하세요`,"modal.setupStep2":`아래 API 키 필드에 붙여넣으세요`,"modal.setupStep3":`프로바이더 추가를 클릭하세요 — 모델은 자동으로 검색됩니다`,"modal.namePlaceholder":`예: openrouter`,"modal.duplicateWarn":`프로바이더 "{name}"이(가) 이미 있어 덮어씁니다.`,"modal.forwardHintPrefix":`키 불필요 — 프록시가`,"modal.forwardCredentials":`codex login`,"modal.forwardHintSuffix":`자격 증명을 이 프로바이더로 전달합니다.`,"modal.localHint":`API 키는 저장되지 않습니다. Cursor의 공개 모델 카탈로그만 Codex에 추가되며, live Cursor 전송과 네이티브 파일/셸 실행은 검토 전까지 비활성입니다.`,"modal.getApiKey":`{label} API 키 받기`,"modal.apiKey":`API 키`,"modal.apiKeyTransport":`API 키 헤더`,"modal.apiKeyTransportNative":`x-api-key (Anthropic 기본)`,"modal.apiKeyTransportBearer":`Authorization: Bearer`,"modal.apiKeyPlaceholder":`sk-… (또는 $ENV_VAR)`,"modal.defaultModelPlaceholder":`예: gpt-5.5`,"modal.baseUrlPlaceholder":`https://...`,"modal.baseUrlPlaceholderError":`Base URL에 해결되지 않은 {placeholder}가 있습니다. 실제 값으로 교체하세요.`,"modal.baseUrlPlaceholderHint":`추가하기 전에 Base URL의 {placeholder}를 실제 Account ID로 교체하세요.`,"modal.adding":`추가 중…`,"modal.useOauthLogin":`← OAuth 로그인 사용`,"codexAuth.addIdPlaceholder":`codex-work, codex-alt, team…`,"codexAuth.resetCreditsAria":`리셋 크레딧 {count}개`,"claude.pageTitle":`Claude Code`,"claude.workspace.settings":`설정`,"cws.loading":`콤보 불러오는 중…`,"cws.loadFailed":`콤보를 불러오지 못했습니다.`,"cws.saveFailed":`콤보를 저장하지 못했습니다.`,"cws.removeFailed":`콤보를 삭제하지 못했습니다.`,"cws.saved":`콤보를 저장했습니다.`,"cws.created":`{model}을(를) 만들었습니다.`,"cws.removed":`combo/{id}을(를) 삭제했습니다.`,"cws.renamed":`{from}을(를) {to}(으)로 이름을 변경했습니다.`,"cws.add":`콤보 추가`,"cws.addTitle":`콤보 추가`,"cws.addSubtitle":`여러 프로바이더를 사용하는 가상 모델을 만들고 클라이언트가 요청할 정확한 모델 이름을 선택하세요.`,"cws.create":`콤보 만들기`,"cws.railAria":`콤보 목록`,"cws.searchPlaceholder":`콤보 또는 대상 검색…`,"cws.noSearchResults":`검색과 일치하는 콤보가 없습니다.`,"cws.group.failover":`장애 조치`,"cws.group.roundRobin":`라운드로빈`,"cws.group.other":`기타 전략`,"cws.targetCount":`대상 {count}개`,"cws.targetCountOne":`대상 1개`,"cws.overviewTitle":`콤보`,"cws.overviewBlurb":`프로바이더/모델 대상 사이를 장애 조치, 라운드로빈, 가중 랜덤, 최소 사용, 최단 쿼터 리셋으로 라우팅하는 가상 모델입니다.`,"cws.count.total":`전체`,"cws.count.failover":`장애 조치`,"cws.count.roundRobin":`라운드로빈`,"cws.count.other":`기타`,"cws.howTitle":`동작 방식`,"cws.howBody":`Codex에서 콤보의 공개 모델 이름을 요청하세요. 설정하지 않으면 combo/가 기본값입니다. OpenCodex는 재시도 가능한 업스트림 오류에서만 다음 대상으로 넘깁니다. 사용 가능한 대상이 없으면 전역 기본 프로바이더로 우회하지 않고 요청을 실패 처리합니다.`,"cws.attentionTitle":`확인 필요`,"cws.attention.empty":`구성된 대상 없음`,"cws.attention.few":`대상이 하나뿐 — 장애 조치할 곳이 없음`,"cws.attention.catalogOmitted":`모델 카탈로그에 없음 — 멤버 능력이 불완전하거나 호환되지 않음(context window/메타데이터 부족 또는 modality 교집합이 비어 있음). 별칭 라우팅은 계속 동작`,"cws.attention.allTargetsExhausted":`활성화된 모든 대상의 할당량이 소진됨`,"cws.emptyTitle":`첫 콤보 만들기`,"cws.empty.createDesc":`가상 모델 이름을 정하고 백엔드를 둘 이상 연결하세요.`,"cws.backToAll":`모든 콤보로`,"cws.allCombos":`모든 콤보`,"cws.copyModel":`ID 복사`,"cws.copied":`복사됨`,"cws.tabsLabel":`콤보 상세 섹션`,"cws.tab.config":`설정`,"cws.tab.about":`정보`,"cws.strategy":`전략`,"cws.strategy.failover":`장애 조치`,"cws.strategy.roundRobin":`라운드로빈`,"cws.strategy.random":`랜덤`,"cws.strategy.leastUsed":`최소 사용`,"cws.strategy.resetWindow":`리셋 윈도우`,"cws.strategy.failoverHint":`대상을 순서대로 시도합니다. 재시도 가능한 오류(한도, 장애, 구독 게이트)면 다음으로 넘어갑니다.`,"cws.strategy.roundRobinHint":`가중치에 따라 트래픽을 결정적으로 분배합니다. 선택된 대상을 성공 요청 묶음 동안 유지한 뒤 다음 대상으로 진행합니다.`,"cws.strategy.randomHint":`요청마다 가중치에 비례한 확률로 적합한 대상을 하나 뽑습니다. 요청 간 고정이 없습니다.`,"cws.strategy.leastUsedHint":`각 요청을 성공 횟수가 가장 적은 적합한 대상으로 보냅니다. 횟수는 프록시 재시작 시 초기화됩니다.`,"cws.strategy.resetWindowHint":`쿼터 윈도우가 가장 빨리 리셋되는 적합한 대상을 우선합니다. 쿼터 데이터가 없으면 설정 순서를 따릅니다.`,"cws.field.id":`콤보 ID`,"cws.field.idHintEdit":`ID를 변경하면 콤보 이름이 바뀝니다. 클라이언트는 {model}을(를) 요청합니다.`,"cws.field.alias":`공개 모델 이름`,"cws.field.aliasPlaceholder":`deepseek-v4-flash 또는 vendor/model`,"cws.field.aliasHint":`선택 사항입니다. 접두사 없는 이름, vendor/model 같은 사용자 지정 접두사를 사용하거나 비워 두어 combo/를 사용할 수 있습니다.`,"cws.field.nativeAlias":`네이티브 OpenAI 별칭`,"cws.field.nativeAliasHint":`이 콤보가 지원되는 비수식 OpenAI 네이티브 모델 ID를 사용합니다. 계정/프로바이더 수식 OpenAI 경로는 별도로 유지됩니다.`,"cws.field.displayName":`표시 이름`,"cws.field.displayNameHint":`모델 선택기에 표시할 이름입니다. 네이티브 OpenAI 별칭을 사용할 때 필수입니다.`,"cws.field.idHint":`클라이언트는 {model}을(를) 요청합니다`,"cws.field.idInternalHint":`내부 콤보 ID입니다. 생성 후에도 변경할 수 있습니다.`,"cws.field.stickyLimit":`회전 전 sticky 성공 횟수`,"cws.field.stickyLimitHint":`가중 선택기가 다음 대상으로 진행하기 전에 선택된 대상을 이 성공 요청 횟수만큼 유지합니다.`,"cws.field.defaultEffort":`기본 추론 수준`,"cws.field.defaultEffortNone":`없음 (대상 기본값)`,"cws.field.defaultEffortHint":`클라이언트가 추론 수준을 생략한 경우에만 사용합니다. 옵션은 선택한 대상이 광고하는 수준의 교집합입니다.`,"cws.capability.imageInputUnavailable":`선택한 모든 대상이 이미지 입력을 지원해야 사용할 수 있습니다.`,"cws.capability.imageInputHint":`모든 대상이 이미지를 지원하면 기본으로 켜집니다. 끄면 텍스트만 허용합니다.`,"cws.capability.imageInput":`이미지 / 멀티모달`,"cws.capability.adaptiveEffort":`적응형 추론 단계`,"cws.capability.adaptiveEffortHint":`끔: 추론 단계를 조절할 수 없는 대상이 하나라도 있으면 콤보 전체의 선택기가 사라집니다. 켬: 그런 대상도 그대로 쓰면서, 선택기에는 나머지 대상이 공통으로 지원하는 단계가 남습니다.`,"cws.capabilities":`기능`,"cws.field.defaultEffortUnsupported":`이 수준은 대상의 공통 사다리에 없습니다 — 요청 시 무시되거나 스냅됩니다.`,"cws.field.defaultEffortUnsupportedOption":`교집합에 없음`,"cws.targets":`대상`,"cws.targets.failoverHint":`순서가 중요합니다 — 첫 번째가 기본입니다.`,"cws.targets.roundRobinHint":`가중치는 결정적 상대 선택을 제어하고, 순서는 회전 고리의 동률을 결정합니다.`,"cws.targets.randomHint":`가중치가 각 추첨의 확률을 제어하며, 순서는 무관합니다.`,"cws.targets.leastUsedHint":`순서는 사용량이 같은 대상 간의 동률만 결정합니다.`,"cws.targets.resetWindowHint":`쿼터 데이터가 없거나 동률일 때 순서가 적용됩니다.`,"cws.target.provider":`프로바이더`,"cws.target.model":`모델`,"cws.target.weight":`가중치`,"cws.target.pickProvider":`프로바이더 선택…`,"cws.target.pickProviderFirst":`먼저 프로바이더를 선택하세요…`,"cws.target.pickModel":`모델 선택…`,"cws.target.noModels":`이 프로바이더에 모델 없음`,"cws.target.modelPlaceholder":`모델 ID`,"cws.target.add":`대상 추가`,"cws.target.drag":`드래그하여 순서 변경`,"cws.target.moveUp":`위로`,"cws.target.moveDown":`아래로`,"cws.quota.available":`사용 가능`,"cws.quota.exhausted":`할당량 소진`,"cws.quota.unknown":`할당량 알 수 없음`,"cws.quota.allExhausted":`활성화된 모든 대상의 할당량이 소진되었습니다. 다른 대상을 선택하거나 할당량이 복구될 때까지 기다리세요.`,"cws.aboutTitle":`런타임`,"cws.aboutBody":`실패한 대상은 Retry-After를 반영해 잠시 쿨다운됩니다. 잘못된 요청과 컨텍스트 오류는 다음 대상으로 넘기지 않습니다. 각 대상은 자체 기능에 맞게 추론 수준을 조정하며, 모든 대상 소진 시 우회 없이 실패합니다. 로그와 사용량에는 순서가 있는 실제 시도와 시도별 사용량이 남습니다.`,"cws.removeConfirmTitle":`{model}을(를) 삭제할까요?`,"cws.removeConfirmDesc":`설정과 Codex 카탈로그에서 가상 모델만 제거합니다. 프로바이더는 삭제되지 않습니다.`,"cws.unsavedTitle":`저장되지 않은 변경`,"cws.unsavedDesc":`이 콤보의 편집을 버리고 계속할까요?`,"cws.keepEditing":`계속 편집`,"cws.err.missingId":`콤보 ID가 필요합니다.`,"cws.err.invalidId":`ID는 문자/숫자로 시작하고 문자·숫자·점·밑줄·하이픈만 사용할 수 있습니다(최대 64).`,"cws.err.duplicateId":`같은 ID의 콤보가 이미 있습니다.`,"cws.err.invalidAlias":`별칭은 문자·숫자·점·밑줄·하이픈만 사용할 수 있으며 "/" 구분은 최대 한 번만 허용됩니다.`,"cws.err.aliasReservedNamespace":`별칭은 예약된 "combo/" 네임스페이스를 사용할 수 없습니다.`,"cws.err.aliasNativeFamily":`OpenAI 네이티브 계열(gpt-*, o1-*, o3-*, o4-*, codex-*)의 접두사 없는 별칭은 허용되지 않습니다.`,"cws.err.unsupportedNativeAlias":`네이티브 별칭은 현재 지원되는 OpenAI bare model id 중 하나여야 합니다.`,"cws.err.missingNativeAliasDisplayName":`네이티브 별칭에는 표시 이름이 필요합니다.`,"cws.err.invalidDisplayName":`표시 이름은 128자 이하여야 하며 제어 문자를 포함할 수 없습니다.`,"cws.err.duplicateAlias":`다른 콤보가 이미 이 별칭을 사용하고 있습니다.`,"cws.err.noTargets":`대상을 하나 이상 추가하세요.`,"cws.err.incompleteTarget":`각 대상에 프로바이더와 모델이 필요합니다.`,"cws.target.disabled":`{name} (비활성화됨)`,"cws.err.reservedNamespace":`콤보를 만들기 전에 combo라는 실제 프로바이더의 이름을 변경하세요.`,"cws.err.providerCollision":`콤보 ID가 설정된 프로바이더 이름과 충돌합니다.`,"cws.err.unknownProvider":`각 대상은 설정된 프로바이더를 사용해야 합니다.`,"cws.err.duplicateTarget":`같은 프로바이더/모델 대상은 한 번만 추가할 수 있습니다.`,"cws.err.invalidStickyLimit":`sticky 성공 횟수는 1~100의 정수여야 합니다.`,"cws.err.invalidWeight":`각 라운드로빈 가중치는 1~10000의 정수여야 합니다.`,"cws.err.noEnabledTarget":`하나 이상의 대상이 활성화된 프로바이더를 사용해야 합니다.`,"claude.tabsLabel":`Claude 클라이언트`,"claude.tabCode":`Code`,"claude.tabDesktop":`Desktop`,"claudeDesktop.title":`Claude Desktop`,"claudeDesktop.subtitle":`각 Claude 모델 패밀리를 포트 {port}의 사용 가능한 모델로 연결합니다.`,"claudeDesktop.importJson":`JSON 가져오기`,"claudeDesktop.exportJson":`JSON 내보내기`,"claudeDesktop.loading":`Claude Desktop 프로필을 불러오는 중…`,"claudeDesktop.loadFail":`Claude Desktop 프로필을 불러오지 못했습니다.`,"claudeDesktop.retry":`다시 시도`,"claudeDesktop.saveFailed":`Claude Desktop 프로필을 저장하지 못했습니다.`,"claudeDesktop.applyFailed":`프로필은 저장했지만 적용하지 못했습니다.`,"claudeDesktop.updateFailed":`Claude Desktop 업데이트에 실패했습니다.`,"claudeDesktop.savedApplied":`프로필을 저장하고 Claude Desktop에 적용했습니다.`,"claudeDesktop.appliedMarkerUnsaved":`Claude Desktop에는 적용했지만 적용 표시를 저장하지 못했습니다. 다시 적용하기 전까지 아래 저장/적용 상태가 실제와 다르게 보일 수 있습니다.`,"claudeDesktop.savedAppliedAnnounce":`Claude Desktop 프로필 저장과 적용을 마쳤습니다.`,"claudeDesktop.saved":`프로필을 저장했습니다.`,"claudeDesktop.savedAnnounce":`Claude Desktop 프로필을 저장했습니다.`,"claudeDesktop.exported":`프로필을 JSON으로 내보냈습니다.`,"claudeDesktop.importExpected":`버전 1 Claude Desktop 프로필이 필요합니다.`,"claudeDesktop.importReady":`JSON을 가져왔습니다. 초안을 검토한 뒤 저장하고 적용하세요.`,"claudeDesktop.importedAnnounce":`프로필 JSON을 가져왔습니다. 저장하지 않은 변경 사항을 검토할 수 있습니다.`,"claudeDesktop.importInvalid":`선택한 파일은 올바른 프로필이 아닙니다.`,"claudeDesktop.importFailed":`가져오기에 실패했습니다. {error}`,"claudeDesktop.moved":`{route} 모델을 {family}(으)로 옮겼습니다.`,"claudeDesktop.unsaved":`저장하지 않은 변경 사항`,"claudeDesktop.upToDate":`프로필이 최신 상태입니다`,"claudeDesktop.saving":`저장 중…`,"claudeDesktop.applying":`적용 중…`,"claudeDesktop.saveApply":`저장 및 적용`,"claudeDesktop.emptyTitle":`사용 가능한 모델이 없습니다`,"claudeDesktop.emptyHint":`프로바이더를 추가하거나 활성화한 뒤 Claude Desktop 경로를 할당하세요.`,"claudeDesktop.assignmentsLabel":`Claude 모델 패밀리 할당`,"claudeDesktop.family.opus":`Opus`,"claudeDesktop.family.fable":`Fable`,"claudeDesktop.family.sonnet":`Sonnet`,"claudeDesktop.family.haiku":`Haiku`,"claudeDesktop.modelCountOne":`모델 {count}개`,"claudeDesktop.modelCountMany":`모델 {count}개`,"claudeDesktop.chooseDefault":`기본 모델 선택`,"claudeDesktop.temporaryDefault":`임시 기본 모델`,"claudeDesktop.laneEmpty":`모델을 여기에 놓거나 이동 컨트롤을 사용하세요.`,"claudeDesktop.laneNoMatch":`검색어와 일치하는 모델이 이 계열에 없습니다.`,"nav.grok":`Grok`,"grok.title":`Grok Build`,"grok.subtitle":`opencodex가 Grok 설정에 등록한 모델입니다.`,"grok.loading":`Grok 상태를 불러오는 중…`,"grok.loadFail":`Grok 설정을 읽지 못했습니다.`,"grok.notConfiguredTitle":`Grok Build가 연결되지 않았습니다`,"grok.notConfiguredHint":`Grok을 설치한 뒤 프록시를 다시 시작하면 opencodex가 관리 블록을 다음 위치에 씁니다:`,"grok.endpoint":`엔드포인트`,"grok.colModel":`모델`,"grok.colAlias":`Grok 별칭`,"grok.colContext":`컨텍스트`,"grok.groupNative":`네이티브 모델`,"grok.groupRouted":`라우팅 모델`,"grok.enabledCount":`{total}개 중 {on}개 등록됨`,"grok.saved":`선택을 저장했습니다.`,"grok.savedApplied":`선택을 저장하고 Grok 설정에 반영했습니다.`,"grok.saveFailed":`Grok 선택을 저장하지 못했습니다.`,"grok.applyFailed":`선택은 저장했지만 Grok 설정을 갱신하지 못했습니다.`,"grok.applySkipped":`선택은 저장했지만 Grok 설정은 바뀌지 않았습니다.`,"grok.saveApply":`저장 및 적용`,"grok.saving":`저장 중…`,"grok.applying":`적용 중…`,"grok.unsaved":`저장되지 않은 변경`,"grok.upToDate":`선택이 최신 상태입니다`,"grok.toggleModel":`{id} 모델을 Grok에 등록`,"claudeDesktop.available":`사용 가능`,"claudeDesktop.defaultBadge":`기본`,"claudeDesktop.supports1m":`1M`,"claudeDesktop.unavailable":`사용 불가`,"claudeDesktop.contextM":`컨텍스트 {n}M`,"claudeDesktop.contextK":`컨텍스트 {n}k`,"claudeDesktop.contextUnknown":`컨텍스트 불명`,"claudeDesktop.alias":`별칭`,"claudeDesktop.useAsDefault":`{family} 기본 모델로 사용`,"claudeDesktop.moveTo":`이동 위치`,"claudeDesktop.move":`이동`,"claudeDesktop.status.applied":`Desktop에 적용됨`,"claudeDesktop.status.stale":`설정 변경됨 — 재적용 필요`,"claudeDesktop.status.notApplied":`미적용`,"claudeDesktop.status.notActiveProfile":`Desktop이 다른 프로필을 사용 중 — 재적용 필요`,"claudeDesktop.status.disabled":`Claude Desktop 통합이 꺼져 있습니다. 켠 뒤 Desktop을 완전히 종료하고 다시 여세요.`,"claudeDesktop.enableApply":`켜고 적용`,"claudeDesktop.health.lastRequest":`마지막 요청`,"claudeDesktop.health.stats":`{count} 요청 / {errors} 에러`,"claudeDesktop.effort.supported":`effort`,"claudeDesktop.effort.displayOnly":`effort (표시만)`,"lab.title":`Compatibility Lab`,"lab.subtitle":`Read-only compatibility verdict matrix from lab projection evidence.`,"lab.loadFailed":`Could not load compatibility lab data`,"lab.projectionUnavailable":`Lab projection is not available. Run conformance or live probes first.`,"lab.projectionIncompatible":`Lab projection schema is incompatible. Rebuild the projection.`,"lab.statusTitle":`Projection status`,"lab.matrixTitle":`Compatibility matrix`,"lab.verdictsTitle":`Verdict records`,"lab.filter.layer":`Evidence layer`,"lab.filter.verdict":`Verdict`,"lab.filter.subject":`Subject ID`,"lab.filter.all":`All`,"lab.col.subject":`Subject`,"lab.col.layer":`Layer`,"lab.col.suite":`Suite`,"lab.col.verdict":`Verdict`,"lab.col.asOf":`As of`,"lab.col.protocol":`Protocol conformance`,"lab.col.live":`Live route compatibility`,"lab.col.task":`Task effectiveness`,"lab.empty":`No compatibility verdicts in the projection yet.`,"lab.subjectKind":`Kind`,"lab.observationCount":`Observations`,"lab.eventCount":`Events`,"lab.verdictCount":`Verdicts`,"lab.subjectCount":`Subjects`,"lab.builtAt":`Built`,"lab.loading":`Loading compatibility evidence…`,"lab.loadMore":`Load more`,"lab.detailTitle":`Verdict detail`,"lab.detailClose":`Close`,"lab.detailSubject":`Subject`,"lab.detailObservations":`Observations`,"lab.detailEvents":`Contributing events`,"lab.detailArtifacts":`Artifact metadata`,"lab.production.title":`관측된 프로덕션 트래픽`,"lab.production.notVerification":`랩 검증 아님`,"lab.production.attempts":`시도`,"lab.production.successes":`성공`,"lab.production.routeErrors":`라우팅 오류`,"lab.production.lastObserved":`마지막 관측`,"lab.detailLoadFailed":`Could not load verdict detail`,"lab.refresh":`Refresh`,"lab.verdict.UNKNOWN":`Unknown`,"lab.verdict.CLAIMED":`Claimed`,"lab.verdict.PROBED":`Probed`,"lab.verdict.VERIFIED":`Verified`,"lab.verdict.DEGRADED":`Degraded`,"lab.verdict.BLOCKED":`Blocked`,"lab.verdict.UNSUPPORTED":`Unsupported`,"lab.layer.protocol_conformance":`Protocol conformance`,"lab.layer.live_route_compatibility":`Live route compatibility`,"lab.layer.task_effectiveness":`Task effectiveness`,"dash.visionAdvanced":`고급 설정`,"dash.visionMaxDescriptions":`턴당 최대 설명 수`,"dash.visionMaxDescriptionsInvalid":`양의 정수를 입력하세요.`,"dash.visionTimeout":`제한 시간`,"dash.visionTimeoutInvalid":`{min}에서 {max} 밀리초 사이의 정수를 입력하세요.`,"dash.visionAdvancedPopover":`고급 비전 설정`,"models.newPolicyGlobal":`새 모델을 비활성화 상태로 추가`,"models.newPolicyProvider":`새 모델 정책`,"models.newPolicy_inherit":`상속`,"models.newPolicy_off":`끔`,"models.newPolicy_on":`켬`,"models.newBadge":`신규`,"models.newCount":`신규 {count}개, 꺼짐`,"models.aliases":`별칭`,"models.aliasesTable":`별칭 표`,"models.aliasPrompt":`공급자 별칭 (비우면 해제)`,"models.modelAliasPrompt":`모델 별칭 (비우면 해제)`,"models.aliasSaved":`별칭을 저장했습니다`,"models.aliasConflict":`이 별칭은 기존 이름과 충돌합니다`,"models.editProviderAlias":`공급자 별칭 편집`,"models.editModelAlias":`모델 별칭 편집`,"models.useDefaultAliases":`기본 별칭 사용`,"models.useDefaultAliasesGlobal":`기본 별칭을 전체에 사용`,"models.aliasAuto":`자동`,"models.aliasUser":`사용자`,"models.aliasStale":`오래됨`,"connection.discovering":`로컬 및 공유 대상을 확인하는 중…`,"connection.machineUnavailable":`로컬 머신 연결을 사용할 수 없습니다. 공유 요청을 로컬로 우회하지 않았습니다.`,"connection.disconnect":`허브 연결 해제`,"connection.disconnectConfirm":`이 머신의 허브 연결을 해제하고 독립 실행 모드로 다시 시작할까요?`,"connection.pairing.title":`이 대시보드를 허브에 연결`,"connection.pairing.body":`허브에서 만든 일회용 페어링 코드를 붙여 넣으세요.`,"connection.pairing.relayWarning":`이 코드는 고정 허브 릴레이로 교환됩니다. 릴레이 목적지는 다른 호스트로 바꿀 수 없습니다.`,"connection.pairing.code":`일회용 페어링 코드`,"connection.pairing.submit":`연결`,"connection.pairing.submitting":`연결 중…`,"connection.pairing.error":`페어링 코드가 거부되었거나 만료되었습니다. 확인할 수 있도록 입력값은 유지했습니다.`,"connection.machine.title":`이 머신`,"connection.machine.shimHealthy":`Codex shim이 정상입니다.`,"connection.machine.shimNeedsAttention":`Codex shim을 확인해야 합니다.`,"connection.machine.repairShim":`shim 복구`,"connection.machine.removeShim":`shim 제거`,"connection.clients.title":`연결된 클라이언트`,"connection.clients.none":`클라이언트 상태 없음`,"connection.clients.sync":`지금 동기화`,"connection.clients.syncing":`동기화 중…`,"connection.sessionLogout":`원격 세션 로그아웃`,"connection.sessionLoggingOut":`원격 세션에서 로그아웃하는 중…`,"connection.sessionLogoutFailed":`원격 세션에서 로그아웃하지 못했습니다. 현재 세션은 그대로 유지했습니다.`,"usage.source.connected":`출처: 허브 사용량`,"usage.source.local":`출처: 로컬 usage.jsonl`,"usage.scope.label":`사용량 범위`,"usage.scope.machine":`이 머신`,"usage.scope.hub":`허브 전체`,"usage.hubOffline":`허브 사용량을 불러올 수 없습니다. 로컬 사용량으로 대체하지 않았습니다.`,"integrations.tab.cursor":`Cursor`,"integrations.detail.cursorSeen":`최근 Cursor가 이 프록시를 호출함`,"integrations.detail.cursorNeverSeen":`Private Inference 설치됨, 아직 요청 없음`,"integrations.detail.cursorAbsent":`Cursor Private Inference를 찾지 못함`,"integrations.cursor.title":`Cursor`,"integrations.cursor.intro":`Cursor Private Inference는 에이전트를 로컬에서 돌리고 loopback으로 opencodex와 통신합니다. 일반 Cursor는 백엔드가 커스텀 엔드포인트를 호출하므로 공개 HTTPS 주소가 필요합니다. 이 페이지는 Cursor에 아무것도 쓰지 않습니다. 아래 값을 직접 Cursor에 붙여넣으세요.`,"integrations.cursor.loading":`Cursor 상태 읽는 중…`,"integrations.cursor.unavailable":`프록시에서 Cursor 상태를 읽지 못했습니다.`,"integrations.cursor.detection":`설치된 빌드`,"integrations.cursor.privateInference":`Cursor Private Inference`,"integrations.cursor.regular":`Cursor (일반)`,"integrations.cursor.detected":`감지됨`,"integrations.cursor.notFound":`없음`,"integrations.cursor.regularOnly":`일반 Cursor만 발견됐습니다. 일반 빌드는 커스텀 엔드포인트를 Cursor 서버가 호출하므로 공개 터널 없이는 loopback 프록시에 닿을 수 없습니다. Private Inference 빌드는 가이드를 참고하세요.`,"integrations.cursor.nothingFound":`일반적인 위치에서 Cursor를 찾지 못했습니다. 다른 곳에 설치했다면 아래 값은 그대로 유효합니다.`,"integrations.cursor.gateway":`게이트웨이 값`,"integrations.cursor.gatewayHint":`Cursor Private Inference에서 Settings > Models > Gateway를 열고 아래 두 값을 붙여넣은 뒤 Refresh model list를 누르세요.`,"integrations.cursor.baseUrl":`Base URL`,"integrations.cursor.apiKey":`API 키`,"integrations.cursor.apiKeyCredential":`opencodex API 키 중 하나 (이 바인드는 자격 증명이 필요)`,"integrations.cursor.copy":`복사`,"integrations.cursor.copied":`복사됨`,"integrations.cursor.connection":`연결`,"integrations.cursor.seen":`Cursor의 마지막 요청: {time} ({ua})`,"integrations.cursor.neverSeen":`프록시 시작 후 Cursor 요청이 없습니다. 게이트웨이 저장 후 Cursor에서 Refresh model list를 누르세요.`,"integrations.cursor.models":`Cursor에 표시될 항목`,"integrations.cursor.modelsHint":`Reasoning 사다리는 Cursor 자체 모델 표가 정하므로 opencodex는 예측만 합니다. Context는 기본 창과 옵트인 창(Cursor의 Max Mode)입니다.`,"integrations.cursor.ladderFromBundle":`Reasoning 사다리는 설치된 Cursor Private Inference {version} 번들에서 읽었습니다. 사다리는 Cursor가 정하고 opencodex는 그 표를 보여줄 뿐입니다.`,"integrations.cursor.ladderFromStatic":`Reasoning 사다리는 Cursor 3.18.25의 정적 미러입니다(읽을 수 있는 Private Inference 번들을 찾지 못함). Context는 기본 창과 옵트인 창입니다.`,"integrations.cursor.unknownVersion":`버전 미상`,"integrations.cursor.noControl":`—`,"integrations.cursor.singleWindow":`단일 창`,"integrations.cursor.noControlTitle":`이 id는 Cursor 내장 effort 표에 없어서 Cursor가 Reasoning 컨트롤을 보여주지 않습니다.`,"integrations.cursor.effortRowsOne":`effort 행 1개 게시됨`,"integrations.cursor.effortRowsMany":`effort 행 {n}개 게시됨`,"integrations.cursor.effortRowsOff":`effort 행 없음`,"integrations.cursor.tableLessHint":`—로 표시된 행은 Cursor에서 Reasoning 컨트롤이 없습니다. cursorEffortRows를 켜면 effort마다 picker 항목(id--effort)을 하나씩 게시하고, 고정 기본값은 provider의 modelDefaultReasoningEfforts로 정합니다.`,"integrations.cursor.colModel":`모델`,"integrations.cursor.colReasoning":`추론`,"integrations.cursor.colContext":`컨텍스트`,"integrations.cursor.guide":`Cursor Private Inference 가이드 열기`},He={"nav.dashboard":`仪表盘`,"uptime.day":`天`,"uptime.hour":`小时`,"uptime.minute":`分钟`,"uptime.second":`秒`,"nav.startup":`启动安全`,"nav.providers":`提供方`,"nav.models":`模型`,"nav.combos":`组合`,"nav.subagents":`子代理`,"routing.title":`路由智能 (beta)`,"routing.subtitle":`策略配置文件、试运行评估以及基于来源的路由分析。`,"routing.loadFailed":`无法加载路由数据`,"routing.empty":"未配置路由策略。请在 config.json 中添加 `routingProfiles`。","routing.revision":`rev`,"routing.detail":`配置文件`,"routing.createProfile":`创建配置文件`,"routing.dryRunError":`试运行失败 (HTTP {status})`,"routing.removeConfirm":`删除配置文件 {id}?`,"routing.unknownEvidence.allow":`允许`,"routing.unknownEvidence.penalize":`惩罚`,"routing.unknownEvidence.exclude":`排除`,"routing.removeCandidate":`移除候选 {provider}/{model}`,"routing.candidates":`候选`,"routing.require":`硬性要求`,"routing.optimize":`优化权重`,"routing.limits":`限制`,"routing.unknownEvidence":`未知证据策略`,"routing.compatibility.title":`兼容性策略`,"routing.compatibility.enabled":`要求 Compatibility Lab 证据`,"routing.compatibility.requiredSuites":`必需套件`,"routing.compatibility.loadingCatalog":`正在加载 Lab 目录…`,"routing.compatibility.catalogUnavailable":`Lab 目录不可用 — 请在 config.json 中手动输入套件 ID。`,"routing.compatibility.layer.protocol_conformance":`协议一致性`,"routing.compatibility.layer.live_route_compatibility":`实时路由兼容性`,"routing.compatibility.minStatus":`最低兼容性状态`,"routing.none":`无`,"routing.unavailable":`–`,"routing.dryRun":`试运行评估`,"routing.dryRunContext":`请求上下文窗口(令牌)`,"routing.dryRunTools":`请求需要工具`,"routing.dryRunImage":`请求需要图像输入`,"routing.dryRunStructured":`请求需要结构化输出`,"routing.dryRunRun":`评估候选`,"routing.candidate":`候选`,"routing.eligible":`合格`,"routing.exclusions":`排除项`,"routing.costCap":`成本上限`,"routing.capOutcome.satisfied":`未超限`,"routing.capOutcome.exceeded":`已超限`,"routing.capOutcome.unknown-allowed":`未知(允许)`,"routing.capOutcome.unknown-excluded":`未知(排除)`,"routing.exclusion.capability-unsatisfied":`能力未满足`,"routing.exclusion.unknown-capability":`能力未知`,"routing.exclusion.cost-limit":`超出成本上限`,"routing.exclusion.cost-limit-unknown":`无法确认成本是否在上限内`,"routing.exclusion.cooldown":`冷却中`,"routing.exclusion.unknown-health":`健康状态未知`,"routing.exclusion.unknown-quota":`配额未知`,"routing.exclusion.unknown-price":`价格未知`,"routing.exclusion.other":`排除项:{code}`,"routing.score":`分数`,"routing.selected":`已选择`,"routing.yes":`是`,"routing.no":`否`,"routing.analytics":`路由分析`,"routing.analyticsTotal":`请求`,"routing.analyticsSuccessRate":`成功率`,"routing.analyticsFallbackRate":`回退`,"routing.analyticsP50":`p50`,"routing.analyticsP95":`p95`,"routing.analyticsP99":`p99`,"routing.analyticsCooldown":`冷却失败`,"routing.analyticsConfidence":`置信度`,"routing.analyticsTruncated":`历史已截断`,"routing.analyticsRequests":`请求`,"routing.analyticsEmpty":`暂无分析 — 请先发送一些请求。`,"nav.logs":`日志与调试`,"nav.usage":`用量`,"common.github":`GitHub`,"sidebar.star":`在 GitHub 上加星`,"sidebar.starred":`已在 GitHub 加星`,"sidebar.starUnauthenticated":`打开 GitHub 加星(gh CLI 未登录)`,"sidebar.starFailed":`无法通过 gh 加星,改为打开 GitHub。`,"sidebar.updateAvailable":`有可用更新:{version}`,"sidebar.checkUpdate":`检查更新`,"common.save":`保存`,"common.saving":`保存中…`,"common.cancel":`取消`,"common.discard":`丢弃`,"common.delete":`删除`,"common.remove":`移除`,"common.loading":`加载中…`,"common.retry":`重试`,"auth.adminTokenTitle":`OpenCodex 管理员令牌 (OPENCODEX_ADMIN_AUTH_TOKEN)`,"auth.adminAccountLabel":`账户`,"auth.adminTokenFieldLabel":`管理员令牌`,"auth.adminTokenRejected":`管理员令牌被拒绝。请检查后重试。`,"auth.adminTokenUnavailable":`无法验证管理员令牌。请重试。`,"theme.label":`主题`,"theme.light":`浅色`,"theme.dark":`深色`,"theme.system":`跟随系统`,"lang.label":`语言`,"lang.nativeName":`中文`,"provider.name.commandCodeAuth":`Command Code - Auth`,"provider.name.commandCodeApi":`Command Code - API`,"provider.name.volcengine":`火山方舟`,"provider.name.volcengineCodingPlan":`火山方舟编程套餐`,"provider.name.volcengineAgentPlan":`火山方舟智能体套餐`,"errorBoundary.title":`页面加载失败`,"errorBoundary.message":`此部分在渲染时发生错误。请重新加载后再试。`,"errorBoundary.details":`错误`,"errorBoundary.reload":`重新加载`,"startup.title":`启动安全`,"startup.subtitle":`检查重启后 Codex 是否仍能连接 opencodex,避免本地代理路由陷入重复重连。`,"startup.refresh":`刷新`,"startup.backToDashboard":`返回仪表盘`,"startup.loading":`正在检查启动保护…`,"startup.error":`无法读取启动保护状态。`,"startup.staleData":`最新启动检查失败。以下数据已过期,不得视为已受保护的证明。`,"startup.status.native":`原生路由`,"startup.status.protected":`重启已受保护`,"startup.status.atRisk":`需要处理`,"startup.summary.native":`Codex 不依赖本地代理`,"startup.summary.protected":`重启后 opencodex 会自动可用`,"startup.summary.atRisk":`重启后 Codex 可能无法访问模型`,"startup.riskDetail":`Codex 已指向本地代理,但没有持久服务或正常的 launcher shim 将其重新启动。`,"startup.riskDetailCustomLocal":`Codex 指向自定义本地网关。opencodex 无法管理或验证该网关的重启生命周期。`,"startup.riskDetailWindowsShim":`Launcher shim 仅保护受支持的 CLI 脚本;Windows 上的 Codex Desktop 和直接 codex.exe 启动可以绕过它。`,"startup.safeDetail":`当前路由与启动机制一致。重启后无需手动运行 ocx start。`,"startup.routing":`Codex 路由`,"startup.routing.proxy":`本地代理`,"startup.routing.native":`OpenAI 原生`,"startup.routing.customLocal":`自定义本地网关`,"startup.routing.customRemote":`自定义远程网关`,"startup.routing.unknown":`未知或无效的路由`,"startup.restartProtection":`重启保护`,"startup.preference":`按需启动`,"startup.enabled":`已启用`,"startup.disabled":`已禁用`,"startup.protection.service":`后台服务`,"startup.protection.shim":`Launcher shim`,"startup.protection.none":`未安装`,"startup.details":`保护详情`,"startup.service":`后台服务`,"startup.serviceHint":`登录时启动,并在代理崩溃后重新启动。`,"startup.installed":`已安装`,"startup.notInstalled":`未安装`,"startup.unsupported":`不支持`,"startup.shim":`Codex launcher shim`,"startup.shimHint":`支持的 Codex 脚本启动器运行时执行 ocx ensure。`,"startup.healthy":`正常`,"startup.cliOnly":`仅 CLI`,"startup.stale":`已过期`,"startup.viable":`可用`,"startup.unhealthy":`已安装但异常`,"startup.conflict":`服务冲突`,"startup.installedDisabled":`已安装但禁用`,"startup.install":`安装`,"startup.installing":`正在安装…`,"startup.repair":`修复`,"startup.repairing":`正在修复…`,"startup.serviceInstalled":`后台服务安装成功。`,"startup.serviceRepaired":`后台服务修复成功。`,"startup.shimInstalled":`Codex 启动器 shim 安装成功。`,"startup.shimRepaired":`Codex 启动器 shim 修复成功。`,"startup.installFailed":`安装失败:`,"startup.tray.title":`Windows 系统托盘`,"startup.tray.hint":`登录时启动托盘图标,一键控制代理启动、停止、重启、面板和状态。`,"startup.tray.login":`Windows 登录时启动托盘`,"startup.tray.notProtection":`托盘只是控制器,并非重启保护。无人值守恢复仍需要正常的后台服务。`,"startup.tray.running":`运行中`,"startup.tray.stopped":`已安装,未显示`,"startup.tray.stale":`需要修复`,"startup.tray.notInstalled":`未安装`,"startup.tray.loading":`正在检查…`,"startup.tray.unavailable":`状态不可用`,"startup.tray.install":`安装并显示托盘`,"startup.tray.start":`显示托盘图标`,"startup.tray.stop":`退出托盘图标`,"startup.tray.uninstall":`移除登录托盘`,"startup.tray.error":`Windows 托盘操作失败。请运行 ocx tray status 查看详情。`,"startup.recovery":`修复选项`,"startup.recoveryHint":`使用上方的一键安装,或复制命令进行手动修复。Codex Desktop 和 Windows 可执行文件建议使用后台服务。`,"startup.command.service":`推荐:持久后台服务`,"startup.command.shim":`备选:CLI launcher shim`,"startup.command.native":`安全恢复:还原 Codex 原生路由`,"startup.copy":`复制`,"startup.copied":`已复制`,"startup.recommended":`推荐修复:{cmd}`,"startup.navRisk":`启动保护需要处理`,"startup.codexRuntime.clampHidden":`部分推理强度选项已隐藏,因为 OpenCodex 正在使用 Codex {version}。`,"startup.codexRuntime.clampHiddenWithEfforts":`部分推理强度选项已隐藏,因为 OpenCodex 正在使用 Codex {version}(已移除:{efforts})。`,"startup.codexRuntime.olderBinary":`OpenCodex 正在使用较旧的 Codex 二进制文件({version})。检测到可用的较新安装。`,"dash.subtitle":`本地 opencodex 代理、其提供方以及路由到 Codex 的模型的实时状态。`,"dash.workspace.overview":`概览`,"dash.workspace.sections":`板块`,"dash.status":`状态`,"dash.online":`在线`,"dash.offline":`离线`,"dash.version":`版本`,"dash.uptime":`运行时间`,"dash.providers":`提供方`,"dash.tokens30d":`Token (30 天)`,"dash.coverage":`覆盖率 {pct}`,"dash.mem.title":`内存可观测性`,"dash.mem.hint":`只读运行时诊断。观测内存为 max(RSS, external, ArrayBuffers),避免 Windows working set trimming 隐藏已提交的保留内存。`,"dash.mem.rss":`常驻内存 (RSS)`,"dash.mem.jsHeap":`JS 堆已用`,"dash.mem.jsHeapArena":`堆区 {total}`,"dash.mem.pressure":`相对告警阈值`,"dash.mem.pressureOf":`阈值的 {pct}%`,"dash.mem.pressureUnknown":`未提供阈值`,"dash.mem.jscHeap":`JSC 堆`,"dash.mem.external":`External`,"dash.mem.arrayBuffers":`ArrayBuffers`,"dash.mem.observed":`观测值`,"dash.mem.runtime":`运行时计数器`,"dash.mem.growth":`每小时观测变化`,"dash.mem.perHour":`/小时`,"dash.mem.store":`延续存储`,"dash.mem.storeHint":`代理 previous_response_id 缓存。堆上升时总字节数增加,说明是对话保留而非运行时分配器。`,"dash.mem.storeEntries":`条目`,"dash.mem.storeTotal":`总计`,"dash.mem.storeLargest":`最大`,"dash.mem.storeOldest":`最旧`,"dash.mem.threshold":`告警阈值`,"dash.mem.lastWarn":`上次告警`,"dash.mem.never":`从不`,"dash.mem.details":`详情`,"dash.mem.unavailable":`内存诊断不可用(旧版代理)。`,"dash.mem.inFlight":`进行中的请求`,"dash.mem.restart":`排空并重启`,"dash.mem.restartConfirm":`等待 {count} 个进行中的请求结束后再重启(最多 {seconds} 秒;超时将中断剩余请求)。`,"dash.mem.draining":`正在等待 {count} 个请求完成… 完成后重启`,"dash.mem.reconnecting":`代理正在重启… 等待重新连接`,"dash.mem.restartFailed":`排空并重启失败。请确认代理正在运行。`,"dash.mem.restartNoSupervisor":`未检测到重启保护。重启后代理可能不会自动恢复,需手动启动。`,"dash.activeProviders":`活跃提供方`,"dash.noProviders":`尚未配置提供方。请运行 {cmd}。`,"dash.col.name":`名称`,"dash.col.adapter":`适配器`,"dash.col.baseUrl":`Base URL`,"dash.col.model":`模型`,"dash.modelsNoResults":`没有符合搜索的模型。`,"dash.availableModels":`可用模型`,"dash.noModels":`未找到模型。请检查提供方 API 密钥。`,"dash.cannotConnect":`无法连接到代理。它在运行吗?`,"dash.runStart":`运行 {cmd} 以启动代理。`,"dash.stop":`停止代理`,"dash.stopConfirm":`停止代理并恢复原生 Codex 配置?`,"dash.stopFailed":`无法停止代理 (HTTP {status})。`,"dash.maSwitchFailed":`模式切换失败 (HTTP {status})。`,"dash.maNetworkError":`网络错误 — 代理是否正在运行?`,"dash.stopping":`正在停止…`,"dash.actions":`代理`,"dash.codexRestart":`重新加载 Codex 模型列表`,"dash.codexRestarting":`正在停止…`,"dash.codexRestartConfirm":`停止 Codex app-server 以便重新读取模型列表?进行中的 Codex 任务会被中断,且 Codex 不会自动重启,请稍后自行重新打开。`,"dash.codexRestartDone":`已停止 {count} 个 Codex app-server。重新打开 Codex 即可加载最新模型列表。`,"dash.codexRestartNothing":`没有正在运行的 Codex app-server。下次启动会读取最新模型列表。`,"dash.codexRestartUnknown":`无法枚举进程,因此没有停止任何进程。`,"dash.codexRestartPartial":`有 {count} 个 app-server 未退出。若模型列表仍然过旧,请手动停止。`,"dash.codexRestartFailed":`无法重新加载 Codex 模型列表 (HTTP {status})。`,"dash.codexRestartUnreachable":`无法连接到代理。`,"dash.codexRestartMalformed":`代理返回了意外的响应。`,"dash.codexRestartTimeout":`代理未在超时前响应,可能仍在停止 app-server。`,"models.staleBanner":`Codex 显示的模型列表比当前目录旧。重启 Codex 即可重新读取。`,"dash.codexAutoStart":`随 Codex 启动 opencodex`,"dash.codexAutoStartHint":`允许已安装的 launcher shim 运行 ocx ensure。此设置不会安装重启保护;请在启动安全中检查实际状态。`,"dash.searchModel":`搜索附属模型`,"dash.searchModelHint":`用于非 OpenAI 路由模型的 web_search 的模型。需要 ChatGPT 登录。`,"dash.searchReasoning":`搜索推理强度`,"dash.visionModel":`视觉附属模型`,"dash.visionModelHint":`为纯文本路由模型描述图像的模型。需要 ChatGPT 登录。`,"dash.webSearchSidecar":`网页搜索附属服务`,"dash.webSearchSidecarHint":`选择路由模型进行网页搜索时使用的后端和模型。`,"dash.webSearchStream":`实时流式输出回答`,"dash.webSearchStreamHint":`实时流式输出开头的文本和推理,直到模型决定调用工具;其余部分为拦截搜索而保持缓冲。搜索前的文本可能会部分重复。`,"dash.visionSidecar":`视觉附属服务`,"dash.visionSidecarHint":`选择纯文本路由模型描述图像时使用的后端和模型。`,"dash.visionOff":`关闭`,"dash.shadowCallIntercept":`影子调用拦截`,"dash.shadowCallInterceptHint":`拦截 Codex 应用的后台辅助调用({models}:标题生成、提交消息)并重定向到所选模型。`,"dash.shadowCallWarning":`⚠ 启用后,所有对 {models} 的请求都将被替换为所选模型。`,"dash.shadowCallOriginal":`原始`,"dash.shadowCallModel":`替代模型`,"dash.shadowCallTooltip":`Codex 应用会在后台调用辅助模型来生成线程标题、提交消息以及进行技能编排。该模型随客户端版本变化,因此 opencodex 会同时拦截这些模型:{models}。启用此选项可将这些调用重定向到您选择的模型。`,"models.shadowCallIntercept":`影子调用拦截`,"models.shadowCallInterceptHint":`拦截 Codex 应用的后台辅助调用({models})并重定向到所选模型。`,"dash.sidecarBackend":`后端`,"dash.sidecarModel":`模型`,"dash.backendAuto":`自动`,"dash.backendOpenAI":`OpenAI`,"dash.backendAnthropic":`Anthropic`,"dash.sidecarSaved":`附属设置已保存。将在下一个请求时生效。`,"dash.sidecarSaveFailed":`保存附属设置失败。`,"dash.injectionLabel":`子代理委托`,"dash.injectionHint":`选择 Codex 把子任务交给谁来做的模型。这个选择用在哪里,由下面两个开关决定。`,"dash.syncCodexSubagentDefaults":`同时保存为 Codex 默认值`,"dash.syncCodexSubagentDefaultsHint":`打开后,上面选的模型会写进 Codex 自己的配置,新任务一开始也用它。关闭则只在这里记住。下次同步或重启后生效,你手写的 [agents] 设置不会被改动。`,"dash.multiAgentGuidance":`告诉 Codex 怎么分工`,"dash.multiAgentGuidanceHint":`给 Codex 附上一张短便条,说明怎么把活分给子代理。v2 会告诉它可用的模型和优先模型;v1 只在推理强度为 max 或 ultra 时才起作用。关闭则不附任何便条。`,"dash.injectionNone":`无`,"dash.injectionEffortLabel":`推理强度`,"dash.injectionEffortNone":`模型默认`,"dash.effortCapLabel":`V2 ultra 推理强度限制`,"dash.subagentEffortCapLabel":`V2 子代理推理强度限制`,"dash.effortCapHelp":`限制 V2 ultra 模式轮次的推理强度。设置后,来自 ultra 模式的 max 请求将被限制到所选级别。子代理限制仅适用于衍生的子代理。只会降低强度,不会提高。如果模型不支持所选级别,将自动降至最近的支持级别。`,"dash.effortCapNone":`无上限`,"dash.maintenance":`维护`,"dash.maintenanceHint":`刷新 Codex 模型目录,或安装新的 opencodex 版本。`,"dash.syncModels":`同步模型`,"dash.syncing":`同步中…`,"dash.syncOk":`同步完成。已追加 {count} 个模型。`,"dash.syncStaleHint":`如果 Codex 仍显示旧列表,请重启长期运行的 app-server({cmd})。`,"dash.syncFailed":`同步失败:{error}`,"dash.projectConfigTitle":`项目 Codex 配置绕过了 OpenCodex`,"dash.projectConfigHint":`这些仓库级设置会覆盖 OpenCodex 代理(例如直接走 OpenCode Go)。请移除它们,以便该项目使用 ~/.codex/config.toml 的代理路由。`,"dash.checkUpdate":`检查更新`,"dash.updateTitle":`更新 opencodex`,"dash.updateDesc":`检查所选 npm 频道的最新版本,然后选择安装后是否重启代理。`,"dash.updateChannel":`频道`,"dash.updateChecking":`正在检查更新…`,"dash.updateInstalled":`已安装`,"dash.updateLatest":`最新`,"dash.updateAvailable":`有可用更新`,"dash.updateCurrent":`已是最新`,"dash.updateCommand":`命令`,"dash.updateSource":`当前是源码检出。请在终端运行显示的命令进行更新。`,"dash.updateUnavailable":`无法从 npm 读取最新版本。请稍后重试。`,"dash.updateRetry":`重试`,"dash.updateRecheck":`重新检查`,"dash.updateCannotAuto":`无法一键更新({reason})。`,"dash.updateReason.source_checkout":`源码检出`,"dash.updateReason.latest_unavailable":`无法连接 npm 注册表`,"dash.updateReason.already_latest":`已是最新版本`,"dash.updateReason.unknown":`无法更新`,"dash.updateRestart":`更新后重启`,"dash.updateRestartHint":`推荐开启。代理重启前,当前 GUI 仍运行旧代码。`,"dash.runUpdate":`更新`,"dash.updateReconnecting":`正在等待重启后的代理…`,"dash.updateStatus.running":`正在更新 opencodex。`,"dash.updateStatus.restarting":`更新已安装。正在重启代理。`,"dash.updateStatus.succeeded":`更新完成。`,"dash.updateVersionTransition":`{currentVersion} -> {latestVersion}.`,"dash.updateStatus.failed":`更新失败。`,"prov.subtitle":`配置 opencodex 路由到 Codex 的上游提供方。使用账户登录、添加提供方,或编辑原始配置。`,"prov.add":`添加提供方`,"prov.editJson":`编辑 JSON`,"prov.accountLogin":`账户登录`,"prov.noOauth":`没有可用的 OAuth 提供方。`,"prov.loggedIn":`已登录`,"prov.notLoggedIn":`未登录`,"prov.logout":`退出登录`,"prov.login":`登录`,"prov.loginWith":`使用 {provider} 登录`,"prov.waitingBrowser":`等待浏览器…`,"prov.didntOpen":`没有打开?点击这里`,"prov.copyLink":`复制链接`,"prov.dontOpenBrowser":`不要在运行代理的机器上打开浏览器`,"prov.dontOpenBrowserHint":`适用于使用其他浏览器配置文件登录,或仪表板与代理不在同一台机器上。`,"prov.linkCopied":`已复制`,"prov.linkCopyUnavailable":`剪贴板不可用`,"prov.deviceCode":`设备验证码`,"prov.copyCode":`复制验证码`,"prov.codeCopied":`验证码已复制`,"prov.editAlias":`编辑别名`,"prov.aliasPrompt":`显示名称(留空以清除)`,"prov.aliasSaved":`别名已保存`,"prov.aliasSaveFailed":`无法保存别名`,"prov.accountId":`ID`,"prov.pasteRedirect":`粘贴重定向 URL 或授权码`,"prov.pasteRedirectHint":`如果浏览器显示 localhost 错误,请复制地址栏中的完整 URL 并粘贴到此处(或粘贴授权码)。`,"prov.pasteSubmit":`提交`,"prov.pasteSubmitting":`提交中…`,"prov.pasteOk":`已提交代码 — 正在完成登录…`,"prov.pasteFail":`无法提交代码:{error}`,"prov.port":`端口`,"prov.default":`默认`,"prov.loadingConfig":`加载中…`,"prov.saved":`已保存!重启代理以生效。`,"prov.loadConfigFail":`加载配置失败`,"prov.invalidJson":`无效的 JSON`,"prov.saveFailed":`保存失败`,"prov.loginFailStart":`{provider} 登录启动失败`,"prov.loginError":`{provider} 登录错误:{error}`,"prov.loginRequestFail":`{provider} 登录请求失败`,"prov.loginCancelled":`{provider} 登录已取消`,"prov.loginTimeout":`{provider} 登录超时 — 浏览器已关闭或未完成。请重试。`,"prov.loginOk":`已登录到 {provider}。运行 {cmd}(或实时生效)以列出其模型。`,"prov.loginSameAccount":`仍是同一个 {provider} 账户 — 请在浏览器中切换账户后再次尝试添加账户。`,"oauthTos.highTitle":`{provider}:订阅 OAuth 风险`,"oauthTos.elevatedTitle":`{provider}:非官方 OAuth 桥接`,"oauthTos.anthropicBody":`通过 OpenCodex 等第三方代理直接复用 Claude 订阅 OAuth 令牌,并非 Anthropic 支持的集成方式,可能导致访问受限。可使用 Claude 订阅的受支持 Agent SDK 集成属于另一种方式。`,"oauthTos.highBody":`OpenCodex 通过第三方 OAuth 路径连接 {provider}。如果该用法不受支持,访问可能会被限制或暂停。`,"oauthTos.elevatedBody":`OpenCodex 通过非官方 OAuth 路径连接 {provider}。请尽量使用官方客户端;异常或自动化流量可能被视为滥用,访问可能会被限制或暂停。`,"oauthTos.saferPath":`更安全的做法:改为在 OpenCodex 中配置 API 密钥。`,"oauthTos.acknowledge":`我了解风险,仍要继续使用 OAuth。`,"oauthTos.continue":`继续使用 OAuth`,"prov.logoutOk":`已退出 {provider}。`,"prov.logoutFail":`无法退出 {provider}。账户状态保持不变。`,"prov.removed":`已移除 "{name}"。`,"prov.removedDefault":`已移除 "{name}"。默认提供方现为 "{defaultProvider}"。`,"prov.removeFail":`移除 "{name}" 失败。`,"prov.removeLastProvider":`如果没有其他已启用的提供方可以成为默认,则无法移除此提供方。`,"prov.removeHasDependentCombos":`请先移除或更新依赖它的组合:{combos}。`,"prov.setDefault":`设为默认`,"prov.setDefaultSuccess":`"{name}" 已设为默认提供方。`,"prov.setDefaultFail":`无法将 "{name}" 设为默认提供方。`,"prov.defaultDisabled":`请先启用此提供方,再将其设为默认。`,"prov.updateFail":`无法更新此提供方。`,"prov.networkError":`网络错误。请确认代理正在运行后重试。`,"prov.added":`已添加 "{name}"。现已生效 — 运行 {cmd}(或重启)以在 Codex 选择器中列出其模型。`,"prov.removeConfirm":`移除提供方 "{name}"?其模型将从 Codex 选择器中消失。`,"prov.hasApiKey":`已配置 API 密钥`,"prov.hasHeaders":`已配置自定义请求头`,"prov.accounts":`账户({n})`,"prov.accountsAria":`展开/收起 {name} 账户`,"prov.accountActive":`使用中`,"prov.accountReauth":`需重新登录`,"prov.reauthenticate":`重新认证`,"prov.reauthAccountMissing":`登录后未找到所选账号`,"prov.reauthIdentityMismatch":`登录账号与所选账号不匹配`,"prov.accountAdd":`添加账户`,"prov.accountNoLabel":`账户 {id}`,"prov.accountSwitchTitle":`使用此账户`,"prov.accountSwitched":`已切换到 {email}。`,"prov.accountSwitchFail":`切换账户失败`,"prov.accountRemoved":`已移除 {email}。`,"prov.accountRemoveFail":`无法移除 {email}。账户保持不变。`,"prov.accountRemoveAria":`移除 {email}`,"prov.accountRemoveConfirm":`移除账户 {email}?其登录将从此代理中删除。`,"prov.keyAdd":`添加 API 密钥`,"prov.keyAdded":`已为 {name} 添加 API 密钥。`,"prov.keyAddFail":`添加 API 密钥失败`,"prov.keyPlaceholder":`粘贴 API 密钥`,"prov.keySwitchTitle":`使用此密钥`,"prov.keySwitched":`已切换到密钥 {key}。`,"prov.keySwitchFail":`切换密钥失败`,"prov.keyRemoved":`已移除密钥 {key}。`,"prov.keyRemoveAria":`移除密钥 {key}`,"prov.keyRemoveConfirm":`移除 API 密钥 {key}?它将从此代理的配置中删除。`,"prov.activeBadge":`已启用`,"prov.disabledBadge":`已禁用`,"prov.defaultBadge":`默认`,"prov.enable":`启用`,"prov.disable":`禁用`,"prov.enabled":`已启用 "{name}"。其模型可再次出现在 Codex 中。`,"prov.disabled":`已禁用 "{name}"。设置会保留,但模型会被隐藏。`,"prov.enableFail":`启用 "{name}" 失败。`,"prov.disableFail":`禁用 "{name}" 失败。`,"prov.enableAria":`启用提供方 {name}`,"prov.disableAria":`禁用提供方 {name}`,"prov.defaultCannotDisable":`默认提供方不能被禁用`,"prov.openaiAccountMode":`Codex 账户模式`,"prov.openaiModePool":`账户池`,"prov.openaiModeDirect":`直连`,"prov.openaiPoolDesc":`默认模式。根据会话关联、额度、冷却时间和故障转移,在主登录与已添加账户之间轮换。`,"prov.openaiDirectDesc":`仅使用当前主 Codex 登录。不会读取或轮换已存储的账户池账号。`,"prov.openaiModeSaved":`OpenAI 账户模式已更改为 {mode}。`,"prov.openaiModeSaveFailed":`无法更改 OpenAI 账户模式。`,"prov.openaiApiDesc":`仅使用 OpenAI API 密钥,不使用 Codex 账户凭据。`,"prov.manageCodexAccounts":`管理 Codex 账户`,"prov.openaiApiMissing":`需要 API 密钥`,"prov.openaiApiSetup":`设置 API 密钥`,"models.tab.catalog":`模型`,"models.tab.combos":`组合`,"models.tab.compatibility":`兼容性`,"models.tab.routing":`路由 (beta)`,"models.tabsLabel":`模型界面`,"models.subtitle.combos":`把多个模型合成一个 id 依次应答。用 failover 串联目标,或用均衡策略分摊负载。`,"models.subtitle.compatibility":`来自实验室投影证据的只读兼容性判定矩阵。`,"models.subtitle.routing":`策略配置、dry-run 评估,以及有据可查的路由分析。`,"models.subtitle":`开关 Codex 可见的模型 — 原生 GPT passthrough 与已路由模型按提供方分组(点击标题可折叠)。隐藏的模型不会出现在目录和模型选择器中,但仍可按精确 id 直接调用。更改在下一个 Codex 回合生效 — opencodex 会使 Codex 的 5 分钟模型缓存失效,因此无需重启。`,"models.nativeGroupLabel":`OpenAI 原生`,"models.nativeHint":"Passthrough 模型使用在提供方页面选择的账户池或直连选项。关闭后会从 Codex 选择器中隐藏(目录条目保留,重新开启即可完整恢复)。 在此添加模型将注册为路由的 `openai/` 选择器,而不是新的裸 passthrough id。","models.active":`{active}/{total} 可见`,"models.workspace.providers":`提供方`,"models.workspace.allProviders":`所有提供方`,"models.workspace.mainAria":`模型详情`,"models.allOn":`全部开启`,"models.allOff":`全部关闭`,"models.presetLabel":`模型`,"models.presetMode_preset":`预设`,"models.presetMode_all":`全部`,"models.presetMode_custom":`自定义`,"models.presetSummary":`显示 {count} / {total} — 核心预设 v{version}`,"models.presetUpdateAvailable":`预设 v{version} 可用`,"models.presetAppliedToast":`{provider}:已应用预设 — 选中 {count} 个模型`,"models.presetClearedToast":`{provider}:显示全部模型`,"models.presetEmpty":`{provider}:预设未匹配到模型,选择保持不变`,"models.presetConfirmReplace":`用包含 {count} 个模型的预设替换你的选择?`,"models.cap350k":`限制 350k`,"models.capApplied":`上下文限制已应用 — 将在下一个 Codex 回合生效。`,"models.capSaveFailed":`保存上下文限制失败`,"models.contextCapped":`350k 限制`,"models.contextCapLabel":`默认窗口 / 上限`,"models.v2Label":`子代理`,"models.shadowCallOriginal":`⚠ {models} →`,"models.v2Mode_v1":`v1`,"models.v2Mode_default":`base`,"models.v2Mode_v2":`v2`,"models.v2ModeDesc_v1":`所有模型 → v1 界面`,"models.v2ModeDesc_default":`上游默认值 (sol/terra=v2, luna=v1)`,"models.v2ModeDesc_v2":`所有模型 → v2 界面`,"models.keepNativeOnV1":`ChatGPT 保持 v1`,"models.keepNativeOnV1Hint":`仅当 ChatGPT 原生父代理仍留在 v2 时,才会加密 v2 子任务,Grok/Claude 无法读取。开启此选项可让 Sol/Terra 留在 v1,从而避免该加密。路由父代理仍使用 v2。`,"models.v2Help":`控制所有模型的多代理界面。 + +v1: 经典单线程代理。所有模型使用 v1 协作界面。 +base: 上游默认值 — sol/terra 使用 v2,luna 使用 v1,其余跟随 codex 功能标志。 +v2: 多线程代理(spawn_agent)。所有模型使用 v2 协作界面。 + +在 v2 下,「ChatGPT 保持 v1」会让 Sol/Terra 留在 v1,以便继续派发 Grok 或 Claude。ChatGPT 会加密 v2 子任务,路由模型无法读取;路由父代理仍留在 v2。 + +更改在新会话中生效。`,"models.v2DocsLink":`v1 / v2 是什么?`,"dash.multiAgent":`子代理`,"models.v2Conflict":`[agents] max_threads 仍存在 — codex 将拒绝启动,请从 config.toml 移除`,"models.v2Applied":`子代理模式已更新 — 新会话生效(重启 Codex 应用以刷新选择器)`,"models.v2ThreadsLabel":`最大线程`,"models.v2ThreadsDefault":`默认 (4)`,"models.v2ThreadsApplied":`线程上限已更新 — 新会话生效`,"models.v2ThreadsInvalid":`线程上限必须为 >= 1 的整数`,"models.v2ThreadsApply":`应用`,"models.capValue":`默认 {value}`,"models.contextSettings":`自定义窗口`,"models.contextSettingsTitle":`自定义窗口 — {provider}`,"models.contextDefault":`提供方默认值`,"models.contextModel":`模型`,"models.contextModelOverride":`模型覆盖值`,"models.contextHint":`已经知道窗口时,在这里手写 Codex 实际窗口。上游没报窗口就用这个值;上游报了更大窗口才压低。留空则使用提供方的「默认窗口 / 上限」;那个开关没开时才回退 128k。`,"models.contextAutomatic":`自动发现`,"models.contextSaved":`上下文窗口已更新 — 将在下一个 Codex 回合生效。`,"models.contextUnchanged":`没有需要保存的上下文窗口更改。`,"models.contextSaveFailed":`保存上下文窗口失败`,"models.contextInvalid":`上下文窗口必须为正整数`,"models.contextCappedValue":`{value} 限制`,"models.setAll":`全部设置`,"models.setAllHint":`给所有已路由提供方打开 {value} 默认窗口。中转站没报 context_window / context_length 时,这个值就是 Codex 实际窗口。要给单个模型手写,用同一行上的「自定义窗口」。原生提供方不受影响。`,"models.collapseAll":`全部折叠`,"models.expandAll":`全部展开`,"models.orderHint":`选择器顺序:Subagents 中的选择(按所选顺序)→ 其余已路由模型(依次按提供方、模型 ID 字母排序)→ 原生模型。可见性开关仅用于筛选,不会改变此顺序。`,"models.custom":`自定义…`,"models.customApply":`应用`,"models.customPlaceholder":`令牌 (例如 420000)`,"models.customAdd":`添加自定义模型`,"models.customAddTitle":`添加自定义模型 — {provider}`,"models.customEditTitle":`编辑自定义模型 — {provider}`,"models.customAdded":`已添加自定义模型`,"models.customUpdated":`已更新自定义模型`,"models.customDeleted":`已删除自定义模型`,"models.customSaveFailed":`保存自定义模型失败`,"models.customSaving":`正在保存…`,"models.customAddBtn":`添加`,"models.customEditBtn":`更新`,"models.customEdit":`编辑`,"models.customDelete":`删除`,"models.customDeleteConfirm":`要删除模型 {name} 吗?`,"models.customBadge":`自定义`,"models.customSummary":`{count} 个自定义模型`,"models.customFieldModelId":`模型 ID(端点标识)`,"models.customFieldModelIdPlaceholder":`例如 qwen4-max-preview`,"models.customFieldDisplayName":`显示名称(可选)`,"models.customFieldDisplayNamePlaceholder":`例如 Qwen 4 Max Preview`,"models.customFieldContext":`上下文窗口`,"models.customFieldModalities":`输入模态`,"models.customFieldReasoning":`推理强度`,"models.customFieldReasoningOverride":`覆盖推理强度`,"models.reasoningEffort.none":`无`,"models.reasoningEffort.minimal":`最低`,"models.reasoningEffort.low":`低`,"models.reasoningEffort.medium":`中`,"models.reasoningEffort.high":`高`,"models.reasoningEffort.xhigh":`极高`,"models.reasoningEffort.max":`最高`,"models.tipProvider":`提供方`,"models.tipContext":`上下文`,"models.tipModalities":`模态`,"models.tipStatus":`状态`,"models.tipActive":`已启用`,"models.tipDisabled":`已禁用`,"models.applied":`已应用 — 将在下一个 Codex 回合生效。`,"models.saveFailed":`保存失败`,"models.networkError":`网络错误 — 代理在运行吗?`,"models.loadFail":`加载模型失败 — 代理在运行吗?`,"models.noRouted":`没有已路由的模型`,"models.noRoutedHint":`请先登录提供方或添加一个。`,"models.emptyDiscovery":`未发现任何模型。请检查提供方端点,或添加静态/自定义模型。`,"models.emptyDiscoveryDisabled":`实时模型发现已关闭,且尚未配置静态模型。`,"models.discoveryFailedBadge":`发现失败`,"models.discoveryFailedHttp":`模型发现失败(HTTP {status})。`,"models.discoveryFailedBlocked":`模型发现被目标策略阻止。`,"models.discoveryFailedInvalidResponse":`模型发现返回了无效响应。`,"models.discoveryFailedNetwork":`由于网络错误,模型发现失败。`,"models.discoveryFailedProvider":`提供方报告了模型发现错误。`,"models.discoveryFailedGeneric":`模型发现失败。`,"models.openProviderSettings":`打开提供方设置`,"models.loading":`加载中…`,"models.search":`搜索模型…`,"models.showMore":`再显示 {n} 个`,"models.allowlistLabel":`仅所选`,"models.allowlistHint":`仅勾选的模型进入目录(留空 = 全部)。适用于暴露成千上万模型的提供商。`,"models.selectedCount":`已选 {n} 个`,"sub.subtitle":`Codex 的 {cmd} 仅将优先级最高的前 5 个模型作为覆盖项公开。在此最多选择 5 个 — 原生 gpt 或已路由模型 — opencodex 会设置它们的目录优先级,使其正好排在前面。其他模型仍可按确切名称调用;此设置仅控制显示项。`,"sub.featured":`精选`,"sub.advanced":`高级`,"sub.orderHintAria":`此顺序的用途`,"sub.orderHint":`此处所选并显示的顺序决定 Codex 模型选择器顶部第 1–5 位,以及 {cmd} 的默认模型候选。`,"sub.noneSelected":`未选择 — 请从下方列表选择。`,"sub.models":`模型`,"sub.search":`搜索模型(原生 gpt + 已路由)…`,"sub.noModels":`没有模型 — 请先登录提供方或添加一个。`,"sub.saved":`已保存 {n} 个模型。启动新的 Codex 会话(或运行 {cmd})以将它们作为 spawn_agent 覆盖项查看。`,"sub.saveFailed":`保存失败`,"sub.networkError":`网络错误 — 代理在运行吗?`,"sub.loadFail":`加载模型失败 — 代理在运行吗?`,"sub.loading":`加载中…`,"sub.moveUp":`上移 {m}`,"sub.moveDown":`下移 {m}`,"sub.removeAria":`移除 {m}`,"sub.workspace.addToFeatured":`将 {m} 添加到精选`,"sub.workspace.allModels":`所有模型`,"sub.workspace.featuredFull":`精选列表已满(最多 5 个)`,"sub.workspace.mainAria":`子代理模型详情`,"sub.workspace.notFeatured":`未设为精选`,"sub.workspace.priority":`优先级`,"sub.workspace.removeFromFeatured":`将 {m} 从精选中移除`,"sub.workspace.selectModel":`选择模型`,"sub.workspace.selectModelDesc":`从列表中选择一个模型以查看详情,并将其设为 spawn_agent 的精选模型。`,"sub.workspace.selector":`公开选择器`,"sub.ultraMode":`超级模式`,"sub.ultraModeHint":`为所有模型和推理力度启用主动多代理委派策略(不改变推理力度本身)。将 features.multi_agent_v2.multi_agent_mode_hint_text 写入 config.toml。`,"sub.ultraModeV2Required":`需要 v2 多代理表面 — 请先启用 multi_agent_v2,并在子代理模式控件中选择 v2。`,"sub.ultraModeText":`超级模式委派文本`,"sub.ultraModePreset":`恢复预设`,"sub.ultraModeLoadFail":`无法加载超级模式设置 — 代理是否在运行?`,"sub.ultraModeSaveFail":`保存超级模式设置失败`,"sub.ultraModeSaved":`超级模式已保存。适用于新的 Codex 会话。`,"logs.title":`请求日志`,"logs.tabLogs":`日志`,"logs.tabDebug":`调试`,"logs.subtitle":`经过本地 opencodex 代理的最近请求,最新在前。`,"logs.autoRefresh":`自动刷新`,"logs.noRequests":`暂无请求。`,"logs.loadError":`无法加载请求日志。`,"logs.filter.surface.label":`界面`,"logs.filter.surface.all":`全部`,"logs.filter.surface.claude":`Claude`,"logs.filter.surface.codex":`Codex`,"logs.filter.surface.grok":`Grok`,"logs.filter.interceptedHelpersOnly":`仅已拦截的辅助请求`,"logs.badge.interceptedHelper":`I · {model}`,"logs.badge.interceptedHelperTitle":`已拦截的辅助请求`,"logs.filter.conversation.label":`会话`,"logs.filter.conversation.placeholder":`粘贴会话 ID`,"logs.filter.conversation.clear":`清除`,"logs.filter.model.label":`模型`,"logs.filter.model.placeholder":`按模型或供应商筛选`,"logs.filter.conversation.apply":`筛选日志`,"logs.conversation.totals":`{requests} 次请求 · {tokens} tokens · {cost}`,"logs.conversation.scope":`合计仅覆盖当前已加载的 Logs 环形缓冲。`,"logs.conversation.excluded":`(~$ 已排除 {unpriced} 条无定价、{unmetered} 条无计量)`,"logs.cost.approximate":`{amount}`,"logs.cost.lowerBound":`≥{amount}`,"logs.cost.unavailable":`无法估算`,"logs.detail.conversation":`会话`,"logs.badge.claude":`Claude`,"logs.badge.grok":`Grok`,"logs.col.time":`时间`,"logs.col.request":`请求`,"logs.col.model":`模型`,"logs.col.effort":`推理强度`,"logs.col.provider":`提供方`,"logs.col.status":`状态`,"logs.col.tokens":`Token 数`,"logs.col.tokPerSec":`tok/s`,"logs.col.estimatedCost":`~$`,"logs.metric.tokPerSecTitle":`按完整请求耗时计算的每秒输出 token`,"logs.metric.estimatedCostTitle":`按 API 标价估算,并非实际扣费;价格无法匹配时不显示`,"usage.cost.total":`API 标价折算(当前范围)`,"usage.cost.disclaimer":`这不是账单或扣费凭证。实际可能计入订阅用量或消耗服务商额度。`,"usage.cost.unpricedNote":`已排除 {count} 个无法计费的请求`,"logs.detail.section.basic":`基本信息`,"logs.detail.route.section":`路由决策`,"logs.detail.route.kind":`路由类型`,"logs.detail.route.profile":`配置文件`,"logs.detail.route.selected":`已选择`,"logs.detail.route.candidates":`候选`,"logs.detail.route.unknown":`此请求未记录路由跟踪(跟踪之前的行)。`,"logs.detail.section.performance":`性能`,"logs.detail.section.cost":`API 标价折算`,"logs.detail.section.attempts":`Combo 尝试`,"logs.detail.section.usage":`原始 usage`,"logs.detail.ttft":`TTFT`,"logs.detail.costTotal":`标价折算`,"logs.detail.totalTokens":`Token 总数`,"logs.detail.matchedKey":`匹配的价格键`,"logs.detail.priceSource":`价格来源`,"logs.detail.unavailableReason":`不可用原因`,"logs.detail.copyRequestId":`复制请求 ID`,"logs.detail.copied":`已复制`,"logs.detail.source.jawcode":`jawcode 目录`,"logs.detail.source.expected":`Expected 价格覆盖`,"logs.detail.source.user":`用户配置的提供方价格覆盖`,"logs.detail.verification.verified":`已验证`,"logs.detail.verification.derived":`由基础模型推导`,"logs.detail.attempt.target":`提供方 / 模型`,"logs.detail.attempt.reason":`结果 / 原因`,"logs.detail.attempt.completed":`已完成`,"logs.detail.attempt.e2eNote":`顶层 tok/s 为端到端值;每次尝试使用各自耗时。`,"logs.detail.attempt.recovery.transient5xx":`临时 5xx 错误`,"logs.detail.attempt.recovery.connectionReset":`连接已重置`,"logs.detail.attempt.recovery.oauth401":`OAuth 重新认证`,"logs.detail.attempt.recovery.key429":`密钥被限流 (429)`,"logs.detail.attempt.recovery.rateLimit429":`被限流 (429)`,"logs.detail.attempt.recovery.anthropicOauth429":`Anthropic OAuth 被限流 (429)`,"logs.detail.attempt.recovery.image413":`图片载荷过大 (413)`,"logs.detail.attempt.recovery.emptyCompletion":`空完成重试`,"logs.detail.attempt.recovery.unknown":`未知的恢复原因`,"logs.detail.reason.usage_missing":`未上报 usage。`,"logs.detail.reason.usage_unsupported":`该提供方不支持上报 usage。`,"logs.detail.reason.output_missing":`未上报正数输出 token。`,"logs.detail.reason.invalid_duration":`请求耗时无效。`,"logs.detail.reason.price_unmatched":`未找到匹配的价格。`,"logs.detail.reason.invalid_cache_breakdown":`缓存 token 明细与输入 token 总数冲突。`,"logs.detail.reason.invalid_usage":`Usage 包含无效的 token 值。`,"logs.detail.reason.combo_attempt_unavailable":`至少一次 Combo 尝试无法计价。`,"logs.detail.estimate.usage_estimated":`提供方 usage 为估算值。`,"logs.detail.estimate.cache_detail_missing":`缺少缓存明细;输入费用按上限估算。`,"logs.detail.estimate.expected_price_overlay":`使用了已验证的 Expected 标价。`,"logs.detail.estimate.provider_cost_overlay":`使用了用户配置的提供方价格覆盖。`,"logs.detail.estimate.priority_lower_bound":`暂无已确认的 Priority 价格;当前显示的估算是已知下界。`,"logs.col.error":`错误`,"logs.col.upstreamReason":`上游原因`,"logs.col.duration":`耗时`,"logs.modelTooltip.model":`模型`,"logs.modelTooltip.resolvedModel":`解析后模型`,"logs.modelTooltip.requestedTier":`请求层级`,"logs.modelTooltip.configuredTier":`配置层级`,"logs.modelTooltip.responseTier":`响应层级`,"logs.modelTooltip.supportsTier":`支持层级`,"logs.tokens.reported":`已上报`,"logs.tokens.unreported":`未上报`,"logs.tokens.unsupported":`不支持`,"logs.tokens.estimated":`估算`,"logs.tokens.input":`输入`,"logs.tokens.output":`输出`,"logs.tokens.cacheRead":`缓存命中 (c)`,"logs.tokens.cacheWrite":`缓存写入 (w)`,"logs.tokens.reasoning":`推理`,"logs.tokens.noCache":`无缓存数据`,"logs.tokens.contextTotal":`活动上下文`,"logs.tokens.noCacheNote":`该提供商不报告缓存 token 数`,"logs.tokens.noCacheCursor":`Cursor 未报告缓存明细`,"logs.tokens.noCacheCursorNote":`Cursor 不提供缓存读写 token 数;这表示未知,并不代表已确认缓存未命中`,"logs.tokens.estimatedNote":`估算值(提供商不报告精确用量)`,"logs.details":`查看详情`,"logs.detailTitle":`请求详情`,"logs.detailRaw":`原始日志`,"debug.title":`调试`,"debug.subtitle":`可选的 provider transport 与 usage 提取诊断。请求错误和 502 在“日志”标签页显示。`,"debug.debug":`提供方调试`,"debug.usage":`用量提取`,"debug.injection":`注入日志`,"debug.claude":`Claude 入站`,"debug.claudeInbound.title":`Claude 入站请求`,"debug.claudeInbound.sub":`显示 Claude Code/Desktop 实际发送的内容(thinking、effort、metadata)— 不保存提示词原文。`,"debug.claudeInbound.empty":`尚未捕获任何请求。开启后从 Claude 发送一条消息试试。`,"debug.claudeInbound.time":`时间`,"debug.claudeInbound.endpoint":`端点`,"debug.claudeInbound.model":`模型`,"debug.claudeInbound.none":`无`,"debug.reset":`清除运行时覆盖`,"debug.refresh":`刷新`,"debug.follow":`跟随滚动`,"debug.streamProvider":`提供方`,"debug.streamUsage":`用量`,"debug.streamInjection":`注入`,"debug.loading":`正在加载调试设置…`,"debug.loadFailed":`无法加载调试设置。`,"debug.emptyTitle":`调试日志已关闭`,"debug.empty":`请在上方卡片中开启 Provider debug 或 Usage extraction。通过代理发送请求后,诊断行会显示在这里。`,"debug.noLinesTitle":`等待诊断行`,"debug.noLines.provider":`提供商调试已开启,但仅记录传输异常(丢弃或格式错误的帧,以及 Cursor dial/retry 事件)。通过 Anthropic 等提供商的正常请求可能不会产生任何行。`,"debug.noLines.usage":`用量提取已开启但尚未捕获任何内容。请通过 Codex 发送请求,随后会显示在此处。`,"debug.noLines.injection":`注入日志已开启但尚未捕获任何内容。它记录协作和子代理回合中的多代理指导注入与 effort-cap 决策。`,"usage.title":`用量`,"usage.subtitle":`代理本地的 Token 用量统计。缺失的用量不会显示为零。`,"usage.loading":`正在加载用量数据…`,"usage.empty":`尚无用量记录。通过代理发送请求后将在此显示。`,"usage.loadError":`无法加载用量数据。`,"usage.range.all":`全部`,"usage.range.available":`可用历史`,"usage.historyTruncated":`由于未加载较早的使用记录,合计仅涵盖可用历史。`,"usage.historyTruncatedWindow":`已加载记录的请求开始时间介于 {start} 到 {end} 之间。受读取上限限制,文件较前的条目已被省略,所选时间范围可能不完整。`,"usage.range.30d":`30 天`,"usage.range.7d":`7 天`,"usage.card.requests":`请求数`,"usage.card.measured":`已计量`,"usage.card.reported":`已上报`,"usage.card.totalTokens":`Token 总数`,"usage.card.cachedTokens":`缓存命中 Token`,"usage.card.cachedTokensHint":`从提供商缓存读取的提示 Token(命中)。缓存写入在下方单独显示。`,"usage.card.cacheWriteTokens":`缓存写入`,"usage.card.coverage":`覆盖率`,"usage.card.activeDays":`活跃天数`,"usage.section.heatmap":`每日活动`,"usage.section.overview":`概览`,"usage.section.models":`模型`,"usage.section.providers":`提供方`,"usage.section.coverage":`覆盖率明细`,"usage.workspace.report":`用量报告`,"usage.workspace.sections":`用量分区`,"usage.coverage.measured":`已计量`,"usage.coverage.reported":`提供方上报`,"usage.coverage.estimated":`估算`,"usage.coverage.note":`已计量包含提供方上报和估算的 Token 数。未上报 / 不支持请求仅做计数,不会被算作 0 Token。`,"usage.search.models":`搜索模型…`,"usage.col.requests":`请求数`,"usage.col.measured":`已计量`,"usage.col.reported":`已上报`,"usage.col.tokens":`Token 数`,"usage.col.share":`占比`,"usage.heatmap.less":`少`,"usage.heatmap.more":`多`,"modal.addNamed":`添加:{label}`,"modal.add":`添加提供方`,"modal.search":`搜索提供方…`,"modal.logInWith":`使用 {label} 登录`,"modal.waitingBrowser":`等待浏览器…`,"modal.providerName":`提供方名称`,"modal.adapter":`适配器`,"modal.baseUrl":`Base URL`,"modal.endpoint":`端点`,"modal.endpoint.tokenPlan":`Token 套餐`,"modal.endpoint.payAsYouGo":`按量付费`,"modal.endpoint.custom":`自定义`,"modal.defaultModel":`默认模型(可选)`,"modal.allowPrivateNetwork":`允许本地/私有网络`,"modal.allowPrivateNetworkHint":`仅为有意自托管的提供商启用。元数据端点仍被阻止。`,"modal.nameRequired":`提供方名称为必填项`,"modal.baseUrlRequired":`Base URL 为必填项`,"modal.networkError":`网络错误 — 代理在运行吗?`,"modal.loginFailStart":`登录启动失败`,"modal.waitingLogin":`等待浏览器登录…`,"modal.loggingIn":`登录中…`,"modal.loginTimeout":`登录超时 — 请重试。`,"nav.api":`API`,"nav.integrations":`集成`,"nav.codexAuth":`Codex 认证`,"nav.codexSet":`Codex 设置`,"codexSet.tab.multiauth":`多账号认证`,"codexSet.tab.prompt":`提示词`,"codexSet.prompt.title":`提示词层`,"codexSet.prompt.timing":`对新启动的会话生效。正在运行的会话保持当前的提示词设置。`,"codexSet.prompt.staleRevision":`配置已在别处更改,列表已重新加载。`,"codexSet.prompt.writeFailed":`无法保存更改。`,"codexSet.prompt.loadFailed":`无法加载提示词层。`,"codexSet.prompt.repair":`修复`,"codexSet.prompt.repairFailed":`无法完成修复。`,"codexSet.drift.journalPresent":`上一次写入未完成。下次写入时会自动恢复。`,"codexSet.drift.projectionStale":`已保存的层与 config.toml 中的值不一致。修复会按你的层重新写入该值。`,"codexSet.drift.storeMissing":`层文件已丢失,但 config.toml 中仍有指令。修复会先创建备份,并将该文本保留为一个层。`,"codexSet.drift.ownedMalformed":`config.toml 中生成的那一行被手动改动过,因此重写不再安全。`,"codexSet.custom.adoptUnsupported":`{path} 第 {line} 行的值不是单行字符串,无法导入。若要在此管理,请手动移动它。`,"codexSet.prompt.unreadable":`Codex 配置文件存在但无法读取,因此拒绝了更改。`,"codexSet.layer.permissions":`权限`,"codexSet.layer.collaboration":`协作模式`,"codexSet.layer.environment":`环境上下文`,"codexSet.layer.apps":`应用`,"codexSet.layer.skills":`技能`,"codexSet.prompt.extensionsUnknown":`扩展可添加自己的层。Codex 不会公开这些层,因此无法在此列出。`,"codexSet.group.transition":`变更通知`,"codexSet.group.transitionDesc":`它们通报变化而非描述状态,因此仅在会话切换到实时模式或更换模型时出现。`,"codexSet.custom.slotNote":`自定义层会按此顺序合并为一个部分。`,"codexSet.row.alwaysOn":`始终启用`,"codexSet.row.onChange":`变更时发送`,"codexSet.row.featureGated":`在 [features] 下配置`,"codexSet.row.openFeatures":`打开设置`,"codexSet.dialog.setValue":`{value}(默认 {fallback})`,"codexSet.dialog.copyKey":`复制配置键`,"codexSet.dialog.unknownLayer":`此版本没有该层的说明。它来自比仪表板更新的 Codex 运行时。`,"codexSet.custom.heading":`自定义层`,"codexSet.custom.add":`+ 添加层`,"codexSet.custom.newTitle":`新建层`,"codexSet.custom.editTitle":`编辑层`,"codexSet.custom.titleLabel":`标题`,"codexSet.custom.bodyLabel":`指令`,"codexSet.custom.bodySize":`{bytes}/{max} 字节`,"codexSet.custom.normalized":`制表符已转换为四个空格,换行符已转换为 LF。`,"codexSet.custom.titleRequired":`请输入标题。`,"codexSet.custom.titleTooLong":`标题有 {count} 个字符,上限为 {max} 个。`,"codexSet.custom.titleMultiline":`标题必须为单行。`,"codexSet.custom.bodyTooLarge":`此层为 {bytes} 字节,上限为 {max} 字节。`,"codexSet.custom.composedTooLarge":`启用的层合计将达到 {bytes} 字节,超过上限。`,"codexSet.custom.invalidCharacter":`无法保存位置 {position} 的控制字符。`,"codexSet.custom.discardPrompt":`要放弃更改吗?`,"codexSet.custom.keepEditing":`继续编辑`,"codexSet.custom.delete":`删除 {title}`,"codexSet.custom.deleteConfirm":`要删除此层吗?此操作无法撤销。`,"codexSet.custom.layerGone":`该层已在别处被删除,因此编辑器已关闭。`,"codexSet.custom.deleteConfirmNamed":`要删除“{title}”吗?此操作无法撤销。`,"codexSet.custom.moveUp":`上移 {title}`,"codexSet.custom.prevLayer":`上一层`,"codexSet.custom.nextLayer":`下一层`,"codexSet.custom.navPosition":`{position} / {total}`,"codexSet.custom.moveDown":`下移 {title}`,"codexSet.custom.limitReached":`最多可保留 {max} 个自定义层。`,"codexSet.custom.notOwned":`developer_instructions 是在 opencodex 外部写入的,因此无法在此编辑。请将其导入,以便作为层进行管理。`,"codexSet.custom.adopt":`导入现有指令`,"codexSet.custom.adoptConfirm":`导入为层`,"codexSet.custom.adoptRefused":`无法导入现有值。`,"codexSet.custom.baseReplaced":`model_instructions_file 已设置为 {path},因此 opencodex 外部的内容已替换基础提示词。`,"codexSet.lint.identity":`此内容声明了与 Codex 所设定身份不同的身份。`,"codexSet.lint.foreignTool":`工具由注册表提供;在此指定名称并不会创建工具。`,"codexSet.lint.placeholder":`指令不会经过模板引擎处理,因此此内容会按原样发送。`,"codexSet.lint.applyPatch":`apply_patch 由工具注册表定义,而不是由指令定义。`,"codexSet.lint.approvalVocab":`Codex 会注入自己的审批术语;此内容可能与其冲突。`,"codexSet.lint.environment":`环境信息稍后生成,可能与此内容冲突。`,"codexSet.lint.size":`此层超过 8 KB。仍可保存,但每次请求都会消耗令牌。`,"codexSet.preset.blank":`空白层`,"codexSet.preset.concise.name":`简洁输出`,"codexSet.preset.concise.description":`简短作答,不加开场白,尽量减少格式。`,"codexSet.preset.concise.provenance":`改编自 Claude Code 的简洁性指令。文案由我们原创,并非复制。`,"codexSet.preset.planFirst.name":`编辑前先规划`,"codexSet.preset.planFirst.description":`先说明计划,再进行更改。`,"codexSet.preset.planFirst.provenance":`改编自 Claude Code 的规划思路。文案由我们原创,并非复制。`,"codexSet.preset.explainWhy.name":`解释理由`,"codexSet.preset.explainWhy.description":`不仅说明做什么,也说明为什么。`,"codexSet.preset.explainWhy.provenance":`改编自 Grok Build 的确认风格。文案由我们原创,并非复制。`,"codexSet.preset.testFirst.name":`测试优先`,"codexSet.preset.testFirst.description":`修复前先编写会失败的测试。`,"codexSet.preset.testFirst.provenance":`改编自常见的代理实践。文案由我们原创,并非复制。`,"codexSet.preset.korean.name":`韩语回复`,"codexSet.preset.korean.description":`无论请求使用哪种语言,都用韩语回答。`,"codexSet.preset.korean.provenance":`根据常见的用户需求为 opencodex 编写。文案由我们原创,并非复制。`,"codexSet.dialog.class":`类型`,"codexSet.dialog.key":`配置键`,"codexSet.dialog.fileValue":`此文件中的值`,"codexSet.dialog.absentDefault":`未设置(默认为 {value})`,"codexSet.dialog.noRenderedText":`Codex 不会公开内置层组装后的文本,因此此对话框仅说明该层并列出其配置键,不显示具体内容。`,"codexSet.dialog.sourceText":`发送给模型的原文`,"codexSet.dialog.sourceBytes":`{bytes} 字节`,"codexSet.dialog.notRendered":`在我们读取的那一轮中,此层没有发送任何内容。各部分仅在内容变化时才会重新发送,因此单次采样可能看不到它。`,"codexSet.dialog.emptySource":`{path} 文件存在但为空,因此此层不会发送任何内容。`,"codexSet.dialog.notExposed":`基础提示词不在 Codex 可打印的消息列表中传递,因此无法在此显示。可以通过 model_instructions_file 替换它。`,"codexSet.dialog.textUnavailable":`本机无法读取 Codex 提示词,因此无法显示原文。`,"codexSet.class.base":`基础指令`,"codexSet.class.config-toggle":`可在此切换`,"codexSet.class.feature-gated":`功能开关控制`,"codexSet.class.runtime-conditional":`运行时条件控制`,"codexSet.class.extension-unknown":`扩展层`,"codexSet.layer.base-instructions":`基础指令`,"codexSet.layer.model-switch":`模型切换通知`,"codexSet.layer.personality":`个性`,"codexSet.layer.context-window-guidance":`上下文窗口指引`,"codexSet.layer.realtime":`实时会话`,"codexSet.layer.agents-md":`AGENTS.md`,"codexSet.layer.environments-instructions":`执行环境`,"codexSet.layer.plugins":`插件`,"codexSet.layer.tools":`工具`,"codexSet.layer.multi-agent-mode":`多代理模式`,"codexSet.layer.git-attribution":`提交署名`,"codexSet.about.base-instructions":`Codex 自身的指令。它们随请求一同发送,无法关闭。`,"codexSet.about.model-switch":`会话中途切换模型时添加。`,"codexSet.about.personality":`语气和表达风格指引,由功能开关控制。`,"codexSet.about.context-window-guidance":`剩余上下文预算的相关建议,由功能开关控制。`,"codexSet.about.realtime":`实时会话中添加。`,"codexSet.about.agents-md":`项目中的 AGENTS.md 文件。此页面只显示该层,绝不会编辑项目文档。`,"codexSet.about.permissions":`说明当前生效的沙箱和审批设置。`,"codexSet.about.collaboration":`说明当前启用的协作模式。`,"codexSet.about.environment":`工作目录、平台及其他环境信息。`,"codexSet.about.environments-instructions":`延迟执行环境的相关指引,由功能开关控制。`,"codexSet.about.apps":`已连接应用的使用方式。`,"codexSet.about.plugins":`选中插件或任一插件声明功能时添加。`,"codexSet.about.tools":`延迟加载的工具说明,由功能开关控制。`,"codexSet.about.skills":`可用技能列表。`,"codexSet.about.multi-agent-mode":`子代理指令,由功能开关控制。`,"codexSet.about.git-attribution":`让模型在它写的提交里加上 Co-authored-by: Codex 尾注,并在它开的拉取请求里加上 Generated with Codex. 这一行。Codex 从你的账号读取此项,所以这里和 [features] 都改不了。账号关闭时,Codex 会发送相反的指令,而不是什么都不发。`,"codexSet.condition.model-switch":`仅在会话中途切换模型后注入。`,"codexSet.condition.realtime":`仅在实时会话中注入。`,"codexSet.condition.agents-md":`找到适用于当前工作目录的项目文档时注入。`,"codexSet.condition.plugins":`选中插件或任一插件声明功能时注入。`,"codexSet.condition.git-attribution":`由你账号的署名策略决定。`,"codexSet.base.title":`基础提示词`,"codexSet.base.prev":`上一个选项`,"codexSet.base.next":`下一个选项`,"codexSet.base.position":`{position} / {total}`,"codexSet.base.swipeHint":`左右滑动、按方向键,或点箭头按钮切换选项。对新开始的会话生效。`,"codexSet.base.defaultTitle":`Codex 自带的基础提示词`,"codexSet.base.defaultBody":`默认项并不存在这里,所以没有可编辑或删除的内容:选它只是从配置里移除 model_instructions_file,让 Codex 用自带的提示词。`,"codexSet.base.variantTitle":`名称`,"codexSet.base.variantBody":`提示词`,"codexSet.base.replacesWarning":`这会整体替换 Codex 自带的基础提示词,而不是在其后追加。这里写得短,模型收到的指令就只有这么短。`,"codexSet.base.use":`用这一个`,"codexSet.base.inUse":`正在使用`,"codexSet.base.externalBlocked":`model_instructions_file 已指向 {path},且不是 opencodex 写的。请先自行清除,再在此处选择。`,"nav.openMenu":`打开菜单`,"nav.closeMenu":`关闭菜单`,"integrations.subtitle":`将客户端连接到 opencodex,管理凭据并恢复客户端配置。`,"integrations.tabsLabel":`集成页面`,"integrations.tab.overview":`概览`,"integrations.tab.keys":`API 密钥`,"integrations.tab.codex":`Codex`,"integrations.tab.claude":`Claude`,"integrations.tab.grok":`Grok Build`,"integrations.tab.opencode":`OpenCode`,"integrations.tab.pi":`Pi`,"integrations.tab.omp":`OMP`,"integrations.tab.hermes":`Hermes`,"integrations.tab.openclaw":`OpenClaw`,"integrations.tab.kimi":`Kimi Code`,"integrations.tab.gajae":`Gajae Code`,"integrations.tab.dsh":`DSH`,"integrations.tab.mcode":`MiniMax Code`,"integrations.tab.zcode":`ZCode`,"integrations.tab.prime":`Prime Agent`,"integrations.tab.aside":`Aside`,"integrations.codex.title":`Codex CLI`,"integrations.codex.body":`Codex 连接由代理服务管理。启动 opencodex 时应用该连接;停止服务时恢复原生路由。`,"integrations.codex.openService":`打开服务控制`,"integrations.state.notInstalled":`未安装`,"integrations.state.unknown":`检查中`,"integrations.detail.codexRouted":`Codex 请求经由此代理`,"integrations.detail.codexAbsent":`Codex 尚未经由此代理`,"integrations.detail.keyCount":`已签发 {count} 个密钥`,"integrations.detail.keyNone":`尚未签发密钥`,"integrations.detail.keyChecking":`检查中…`,"integrations.detail.keyUnavailable":`无法获取密钥状态`,"integrations.detail.claudeOff":`连接已关闭`,"integrations.detail.desktopCurrent":`Desktop 正在使用此配置`,"integrations.detail.desktopStale":`应用后配置文件已更改`,"integrations.detail.desktopNotServed":`配置存在,但 Desktop 使用的是另一个`,"integrations.detail.desktopAbsent":`未应用任何配置`,"integrations.detail.desktopDesiredOff":`Claude Desktop 集成已关闭`,"integrations.detail.desktopDesiredOffCleanupPending":`Claude Desktop 仍在使用网关,清理尚未完成`,"integrations.detail.desktopDesiredOnNotApplied":`集成已开启,但 Desktop 未使用网关配置`,"integrations.detail.desktopSelectedElsewhere":`Desktop 正在使用其他配置`,"integrations.detail.desktopProfileDrift":`选中的 Desktop 配置已更改`,"integrations.detail.desktopObservedUnsafe":`无法安全更改选中的 Desktop 配置`,"integrations.detail.desktopNotInstalled":`未安装 Claude Desktop 配置库`,"integrations.dialog.desktop.title":`要关闭 Claude Desktop 集成吗?`,"integrations.dialog.desktop.changes":`如果 {path} 包含由 opencodex 管理的网关配置,Desktop 会先选择新的无凭据标准配置,再移除旧配置及其备份。`,"integrations.dialog.desktop.breakage":`Claude Desktop 将不再使用经由 opencodex 路由的模型,而会恢复为标准 Claude。`,"integrations.dialog.desktop.undo":`重新开启后,会根据已保存的模型分配重新生成 opencodex 配置。`,"integrations.dialog.desktop.restart":`Claude Desktop 仅在启动时读取此配置。请完全退出并重新打开 Desktop 以使更改生效。`,"integrations.dialog.desktop.confirm":`停用`,"integrations.native.error.desktopUnsafeMetadata":`无法安全读取 {path} 中的 Claude Desktop 元数据,因此未更改其配置库。`,"integrations.native.error.desktopCleanupIncomplete":`Claude Desktop 已指向标准模式,但仍残留旧的 opencodex 凭据文件:{paths}。`,"integrations.native.msg.desktopDisabled":`Claude Desktop 集成已关闭。`,"integrations.native.msg.desktopEnabled":`Claude Desktop 集成已开启。`,"integrations.detail.grokModels":`已接入 {count} 个模型`,"integrations.detail.grokAbsent":`配置中没有 opencodex 区块`,"integrations.dialog.grok.title":`要停用 Grok Build 集成吗?`,"integrations.dialog.grok.changes":`只会从 {path} 中删除由 opencodex 标记的区块。区块之外手动写入的内容将保持不变。`,"integrations.dialog.grok.breakage":`停用后,Grok Build 中的 opencodex 模型别名将消失。通过 xAI 账号使用的模型不受影响。`,"integrations.dialog.grok.undo":`如果 opencodex 正在 loopback 地址上运行,再次启用时会根据当前可用的模型列表重新写入区块。`,"integrations.dialog.grok.confirm":`停用`,"integrations.native.msg.nonLoopbackRemoved":`只有当 opencodex 在 loopback 地址上运行时,才能自动注册 Grok Build。已删除之前指向 loopback 地址的区块。`,"integrations.native.msg.nonLoopbackRemovedNoop":`只有当 opencodex 在 loopback 地址上运行时,才能自动注册 Grok Build。没有需要删除的旧区块。`,"integrations.native.msg.nonLoopbackSuperseded":`只有当 opencodex 在 loopback 地址上运行时,才能自动注册 Grok Build。在此期间,其他进程向配置写入了新区块,因此文件中的当前区块并非由本次请求创建。`,"integrations.native.error.orphanedMarker":`{path} 中有 opencodex 开始标记,但没有结束标记。由于无法确定该区块的结束位置,因此未修改文件。`,"integrations.native.error.homeMismatch":`已安装服务的主目录与当前主目录不一致,因此未修改文件。`,"integrations.native.error.notInstalled":`尚未安装 Grok Build,因此没有可更改的内容。`,"integrations.native.error.configBusy":`其他进程正在保存配置,无法进行更改。请稍后重试。`,"integrations.state.absent":`未应用`,"integrations.state.current":`已应用`,"integrations.state.stale":`需要更新`,"integrations.state.conflict":`冲突`,"integrations.state.unsafe":`无法验证`,"integrations.summary.detected":`已检测客户端`,"integrations.summary.applied":`已配置客户端`,"integrations.summary.stale":`需要更新`,"integrations.summary.lastChange":`上次更改`,"integrations.summary.disableAll":`全部禁用…`,"integrations.onboarding":`应用时会先保存备份,再写入一个 opencodex 提供方配置块。禁用时只移除该配置块,并可从保留的快照恢复。`,"integrations.empty.title":`未检测到已安装的客户端`,"integrations.empty.body":`安装受支持的客户端,然后返回此处应用 opencodex。`,"integrations.action.apply":`应用`,"integrations.action.disable":`禁用`,"integrations.action.refresh":`更新`,"integrations.action.settings":`设置`,"integrations.action.manageKeys":`管理密钥`,"integrations.action.restore":`恢复…`,"integrations.action.undo":`撤销`,"integrations.action.restorePoint":`恢复到此时间点…`,"integrations.action.snapshotExpired":`备份已过期`,"integrations.rollback.title":`恢复中心`,"integrations.rollback.empty":`暂无应用记录`,"integrations.rollback.emptyBody":`每次成功写入前都会先保留一份写入前快照。`,"integrations.catalog.title":`客户端`,"integrations.rollback.older":`较早的操作`,"integrations.rollback.showMore":`再显示 {n} 个`,"integrations.rollback.failed":`无法加载回滚记录。`,"integrations.restore.title":`恢复此快照?`,"integrations.restore.body":`系统会先备份当前文件,再用所选快照替换它。`,"integrations.restore.driftTitle":`检测到较新的编辑`,"integrations.restore.driftBody":`此快照之后的更改将先备份,然后再替换文件。`,"integrations.restore.confirm":`恢复`,"integrations.restore.confirmDrift":`备份较新的编辑并恢复`,"integrations.restore.pending":`正在恢复…`,"integrations.restore.manual":`自动恢复失败:{reason}。请从 {path} 手动恢复。`,"integrations.error.load":`无法加载集成状态。`,"integrations.error.stale":`最近一次刷新失败。以下值可能已过期。`,"integrations.error.busy":`此客户端的另一项更改仍在进行中。请稍后重试。`,"integrations.error.conflict":`opencodex 写入后配置又发生了更改。未移除任何内容。`,"integrations.error.unsafe":`无法安全地更改配置。`,"integrations.error.generic":`集成更改失败。已保留之前的状态。`,"integrations.error.nonLoopback":`{client} 只能连接 localhost 上的代理:其配置没有位置放置远程绑定所需的准入标头,手动编写同样无效。请改用隧道或本地转发器提供 loopback 访问。`,"integrations.status.installed":`已安装`,"integrations.status.notInstalled":`未安装`,"integrations.status.appliedAt":`已应用`,"integrations.status.backup":`备份`,"integrations.status.lastRestore":`上次恢复`,"integrations.status.unknown":`未知`,"integrations.bulk.title":`禁用已应用的客户端集成?`,"integrations.bulk.body":`只会移除归 opencodex 所有的配置块。每个客户端都会先保留一份写入前快照。`,"integrations.bulk.partial":`部分客户端无法禁用:{clients}`,"integrations.bulk.success":`已禁用已应用的客户端集成。`,"integrations.retention.degraded":`备份清理进度滞后;磁盘上可能仍有较旧的备份。`,"integrations.error.residual":`文件可能处于中间状态:{message} 请从 {path} 恢复。`,"integrations.error.recover":`{message} 备份位于 {path}。`,"integrations.kind.apply":`已应用`,"integrations.kind.disable":`已停用`,"integrations.kind.refresh":`已更新`,"integrations.kind.restore":`已恢复`,"integrations.kind.overwrite":`已覆盖`,"integrations.dialog.overwrite.title":`替换该配置文件中的代码块?`,"integrations.dialog.overwrite.changesUnowned":`{path} 中 opencodex 需要写入的位置被一个并非我们写入的代码块占用。应用会将其替换为 opencodex 写入的代码块。`,"integrations.dialog.overwrite.changesForeign":`{path} 中 opencodex 代码块内你所做的修改会被丢弃,并替换为 opencodex 写入的代码块。`,"integrations.dialog.overwrite.breakage":`该代码块原本配置的内容将不再生效。文件其他位置保持不变。`,"integrations.dialog.overwrite.undo":`会先保存快照,因此这次操作会出现在下方的回滚列表中,可以撤销。`,"integrations.dialog.overwrite.confirm":`替换`,"integrations.action.overwrite":`替换`,"integrations.semantics.opencode":`仅适用于直接从磁盘启动;ocx opencode 的环境注入优先。`,"integrations.semantics.pi":`对新会话生效。`,"integrations.semantics.omp":`重启 OMP 以加载模型目录。`,"integrations.semantics.hermes":`对新会话生效。`,"integrations.semantics.openclaw":`立即应用到正在运行的网关。`,"integrations.semantics.kimi":`重启或运行 /reload 以应用(v2 会监视该文件)。`,"integrations.semantics.gajae":`在新会话中或打开 /model 时生效。`,"integrations.semantics.dsh":`OpenCodex 只管理 $DSH_HOME/settings.yaml 中的 llm-pi-ai.providers.opencodex。DSH 会热重载该 provider;你的默认模型和 deepseek-official 保持不变。目前仅支持环回地址,且不会写入真实凭据。`,"integrations.semantics.mcode":`仅管理 custom_provider.opencodex,不会更改默认模型或 MiniMax 登录状态。`,"integrations.semantics.zcode":`仅管理 ~/.zcode/v2/config.json 中的 provider.opencodex,不会更改 Z.ai 登录状态或其他提供商。更改后请重启 ZCode。`,"integrations.semantics.prime":`仅管理 Prime Agent 的 models.json 中的 providers.opencodex;默认位于 ~/.prime/agent,若设置 PRIME_AGENT_CODING_AGENT_DIR 则以其为准。不会更改其他提供商或模型覆盖设置。对新会话生效。`,"integrations.semantics.aside":`仅管理已登录账号的 Aside models.json 中的 providers.opencodex,位于 ~/.aside/u/<账号>。不会更改其他提供商。Aside 在运行时会重写该文件,因此应用后请完全退出并重新打开 Aside。`,"codexAuth.mainAccount":`主账号`,"codexAuth.logLabel":`日志标签`,"codexAuth.codexApp":`Codex App`,"codexAuth.moreActions":`显示更多操作`,"codexAuth.copyId":`复制账户 ID`,"codexAuth.appLogin":`应用登录`,"codexAuth.accountPool":`账号池`,"codexAuth.accountModeTitle":`OpenAI 账户模式`,"codexAuth.accountModePool":`账户池模式`,"codexAuth.accountModePoolDesc":`主登录与符合条件的已添加账户会在此轮换。`,"codexAuth.accountModeDirect":`直连模式`,"codexAuth.accountModeDirectDesc":`请求仅使用主登录;已添加账户会继续存储,供账户池模式使用。`,"codexAuth.openaiMissing":`未配置内置 OpenAI 提供方。`,"codexAuth.openaiDisabled":`内置 OpenAI 提供方已禁用。`,"codexAuth.openaiUnavailableDesc":`你的 OpenAI 账号仍然可用。启用提供方后即可路由 Codex 请求。`,"codexAuth.enableOpenai":`启用 OpenAI`,"codexAuth.enablingOpenai":`正在启用...`,"codexAuth.enableOpenaiFailed":`无法启用 OpenAI 提供方。`,"codexAuth.openaiPresetLoadFailed":`无法加载 OpenAI 提供方预设。`,"codexAuth.openaiPresetUnavailable":`OpenAI 提供方预设不可用。`,"codexAuth.openProviders":`打开提供商`,"codexAuth.add":`添加`,"codexAuth.sparkQuota":`Codex Spark 配额`,"codexAuth.sparkQuotaHint":`在账户卡片上显示 GPT-5.3-Codex-Spark 周窗口。默认隐藏,因为它只适用于一个模型。`,"codexAuth.sparkQuotaShown":`已显示 Codex Spark 配额`,"codexAuth.sparkQuotaHidden":`已隐藏 Codex Spark 配额`,"codexAuth.sparkQuotaFailed":`无法更改 Codex Spark 配额设置`,"codexAuth.refreshQuota":`刷新额度`,"codexAuth.refreshingQuota":`刷新中...`,"codexAuth.quotaRefreshed":`额度已刷新`,"codexAuth.quotaRefreshFailed":`额度刷新失败`,"codexAuth.pauseExhausted":`暂停已达上限账号`,"codexAuth.pausingExhausted":`正在检查额度...`,"codexAuth.pauseExhaustedSucceeded":`已暂停 {count} 个达到上限的账号`,"codexAuth.pauseExhaustedNone":`没有确认达到 100% 用量的账号。`,"codexAuth.pauseExhaustedFailed":`无法检查并暂停已达上限账号。`,"codexAuth.noPool":`尚未添加池账号。`,"codexAuth.pause":`暂停`,"codexAuth.resume":`恢复`,"codexAuth.paused":`已暂停`,"codexAuth.pauseSucceeded":`已暂停 {email}`,"codexAuth.resumeSucceeded":`{email} 已重新加入账号池`,"codexAuth.pauseFailed":`无法暂停 {email},未做任何更改。`,"codexAuth.resumeFailed":`无法恢复 {email},未做任何更改。`,"codexAuth.pausedHint":`恢复前不会参与自动切换、重试、冷却恢复或手动选择。`,"codexAuth.pinned":`已固定`,"codexAuth.pinnedHint":`这是你手动选择的账号,因此更高的选择顺序不会越过它。该固定会一直生效,直到此账号用尽、你改选其他账号,或你修改任一选择顺序。`,"codexAuth.fiveHour":`5 小时`,"codexAuth.weekly":`每周`,"codexAuth.monthly":`30天`,"codexAuth.resets":`重置`,"codexAuth.today":`今天`,"codexAuth.current":`当前`,"codexAuth.nextSession":`已选择`,"codexAuth.poolPrepared":`已为账户池准备`,"codexAuth.preparePoolTitle":`为账户池模式准备此账号?`,"codexAuth.preparePoolDesc":`直连请求仍使用主登录。启用账户池模式后,此账号会成为预先选择的池账号。`,"codexAuth.prepareForPool":`为账户池准备`,"codexAuth.poolPreparedToast":`已为账户池模式准备 {email}`,"codexAuth.switchTitle":`切换活跃账号?`,"codexAuth.switchDesc":`立即生效。已在进行中的请求保留原账号,其余都会切换到此账号;不过选择顺序相同的账号仍会轮换使用。`,"codexAuth.cacheWarning":`切换账号会重置提示缓存。新会话从空缓存开始。`,"codexAuth.setAsNext":`接下来使用此账号`,"codexAuth.cancel":`取消`,"codexAuth.switchBack":`切换回主账号?`,"codexAuth.switchBackDesc":`立即生效。已在进行中的请求保留原账号,其余都会切换到应用登录账号;不过选择顺序相同的账号仍会轮换使用。`,"codexAuth.autoSwitch":`基于用量的主动切换`,"codexAuth.autoSwitchQuotaDesc":`配额:使用率达到或超过 {threshold}% 时,包括已绑定任务在内的下一次请求可能转到用量更低的合格账号;Go/Free 仅使用 30 天窗口。`,"codexAuth.autoSwitchQuotaOffDesc":`基于用量的主动切换已关闭。新建/未绑定任务分配和故障恢复仍然生效。`,"codexAuth.autoSwitchRoundRobinDesc":`轮询分配不使用此阈值,并会继续轮换新建/未绑定任务。`,"codexAuth.autoSwitchFillFirstDesc":`填满优先:{threshold}% 是新建/未绑定任务的耗尽点;健康的已绑定任务继续使用原账号。`,"codexAuth.autoSwitchFillFirstOffDesc":`填满优先没有新建/未绑定任务的用量耗尽点;冷却、重新认证和故障恢复仍可能改变路由。`,"codexAuth.failureRecoveryNote":`故障恢复是独立机制:输出前的 429/402 拒绝、冷却、重新认证、排除或已配置的瞬时故障转移可能选择另一个合格账号。`,"codexAuth.autoSwitchThreshold":`用量阈值`,"codexAuth.autoSwitchThresholdAria":`用量阈值(百分比)`,"codexAuth.autoSwitchThresholdInc":`提高用量阈值`,"codexAuth.autoSwitchThresholdDec":`降低用量阈值`,"codexAuth.autoSwitchLoadFailed":`无法加载基于用量的切换设置。`,"codexAuth.autoSwitchThresholdInvalid":`请输入 1 到 100 之间的整数`,"codexAuth.autoSwitchUpdated":`基于用量的主动切换设置已更新`,"codexAuth.autoSwitchUpdateFailed":`无法确认基于用量的切换更新。当前显示最后一次确认的值。`,"codexAuth.requestUserInput":`在 Default 模式下请求输入`,"codexAuth.requestUserInputDesc":`允许 Codex 在 Default 模式会话中暂停,并通过 request_user_input 工具向你提问。`,"codexAuth.requestUserInputUpdated":`功能标志已更新 - 适用于新会话。`,"codexAuth.requestUserInputUpdatedRestart":`功能标志已更新 - 适用于新会话。请重启 Codex 应用。`,"codexAuth.requestUserInputUpdateFailed":`无法更新功能标志。未做任何更改。`,"codexAuth.requestUserInputLoadFailed":`无法从 config.toml 读取功能标志。`,"codexAuth.accountPickerTitle":`在模型选择器中指定 Codex 账号`,"codexAuth.accountPickerOffDesc":`启用后,普通 GPT 选择器条目会替换为每个账号选择器对应的条目,让你无需退出登录即可为对话明确选择账号。关闭此功能不会删除任何账号。`,"codexAuth.accountPickerOnDesc":`每个选择器都是一个已存储账号的公开标签。选择后,该对话会锁定到对应账号,不会参与 Pool 轮换或故障转移,也不会更改当前的 Pool 账号。`,"codexAuth.accountPickerCompatibility":`内置 Codex App 登录有自己的选择器;生成的映射通常使用 main,发生冲突时会使用 main-2 这类安全后缀。新增账号会获得稳定且保护隐私的标签,自定义选择器名称保持不变。现有对话和已保存的模型选择会继续路由。关闭后只隐藏生成的条目,选择器和精确路由仍会保留。普通 GPT 模型 ID 继续保持原有的 Pool 或 Direct 行为。`,"codexAuth.accountPickerUpdated":`账号指定设置已更新。`,"codexAuth.accountPickerUpdateFailed":`无法更新账号指定设置。当前显示的是最后一次确认的设置。`,"codexAuth.accountPickerLoadFailed":`无法加载账号指定设置。`,"codexAuth.accountPickerRefreshFailed":`无法刷新此设置。当前仍显示最后一次确认的值。`,"codexAuth.advancedSettings":`高级设置`,"codexAuth.advancedSettingsAria":`显示或隐藏高级 Codex 认证设置`,"codexAuth.catalogRefreshPending":`更改已保存,但 Codex 模型目录仍待刷新。请运行 ocx sync 重试。`,"anthropicPool.title":`Claude 账户池(实验性)`,"anthropicPool.enabledDesc":`遇到 429 时冷却该账户并故障转移。新会话优先使用{window}低于 {threshold}% 的账户。`,"anthropicPool.enabledNoProactiveDesc":`429 时冷却账号并切换。阈值为 0 时停用主动的用量切换,但新会话选择与 429 恢复仍会使用 {window} 窗口。`,"anthropicPool.disabledDesc":`仅使用当前活跃的 Claude 账户。仅在接受实验性路由时启用。`,"anthropicPool.experimentalWarning":`实验性功能,尚未充分验证。看起来像自动多账户轮换的行为可能导致 Anthropic 限制账户。同一组织可能共享配额——对这些账户做池化没有帮助。除非了解风险,否则请保持关闭。`,"anthropicPool.needTwoAccounts":`启用账户池前请至少添加两个 Claude OAuth 账户。`,"anthropicPool.threshold":`新会话用量阈值`,"anthropicPool.thresholdAria":`新会话用量阈值(百分比)`,"anthropicPool.thresholdHelp":`0 表示禁用基于配额的选择(仅亲和性 + 活跃账户)。默认 80。`,"anthropicPool.thresholdInvalid":`请输入 0 到 100 之间的整数`,"anthropicPool.loadFailed":`无法加载 Claude 账户池设置。`,"anthropicPool.saveFailed":`无法保存 Claude 账户池设置。`,"anthropicPool.on":`开`,"anthropicPool.off":`关`,"accountPool.strategy":`轮换策略`,"accountPool.strategyDesc":`OpenCodex 如何为新建/未绑定任务分配账号。`,"accountPool.strategyQuota":`配额`,"accountPool.strategyRoundRobin":`轮询`,"accountPool.strategyFillFirst":`填满优先`,"accountPool.strategyHintQuota":`配额策略在超过用量阈值后,也可以在现有任务的下一次请求中重新绑定账号。`,"accountPool.strategyHintRoundRobin":`轮询只轮换没有有效绑定的任务;用量阈值不会改变正常轮换。`,"accountPool.strategyHintFillFirst":`填满优先把阈值用作未绑定任务的耗尽点;健康的已绑定任务保持亲和性。`,"accountPool.unboundDefinition":`新建/未绑定任务是当前没有账号绑定的请求;已有的可见任务在代理或亲和性重置后也可能变为未绑定。`,"accountPool.stickyLimit":`轮换前的新建/未绑定任务分配数`,"accountPool.stickyLimitAria":`轮换前的新建/未绑定任务分配数`,"accountPool.stickyLimitInc":`提高粘性上限`,"accountPool.stickyLimitDec":`降低粘性上限`,"accountPool.stickyLimitHelp":`在推进到下一个账号之前,为所选账号分配这么多次新建/未绑定任务;计数在任务绑定时增加,而不是在上游成功后增加。`,"accountPool.stickyLimitInvalid":`请输入 1 到 100 之间的整数`,"accountPool.strategyLoadFailed":`无法加载轮换策略。`,"accountPool.strategyUpdateFailed":`无法保存轮换策略。`,"accountPool.quotaWindow":`配额统计窗口`,"accountPool.quotaWindowDesc":`指定按配额选择新会话、填满优先阈值判断以及可用 429 替代账户所使用的缓存用量。`,"accountPool.quotaWindowFiveHour":`5 小时用量`,"accountPool.quotaWindowWeekly":`每周用量`,"accountPool.quotaWindowMaxUtilization":`较高的用量`,"accountPool.quotaWindowHint":`每周用量会在仍有其他可用账户时跳过 5 小时用量已耗尽的账户;若没有其他账户,则回退使用这些账户。每周用量相同时优先选择 5 小时用量更低者;各账户的每周用量要等提供商页面轮询后才能获知。`,"accountPool.quotaWindowInert":`只有配额策略,或阈值大于 0 的填满优先策略,才会按用量打分;在当前轮换策略下这项设置不起作用。`,"accountPool.priority":`选择顺序`,"accountPool.priorityAria":`此账号的选择顺序`,"accountPool.priorityHint":`数字越大越先使用。只有当排在前面的账号全部用尽或不可用时,账号池才会转向更小的数字。`,"accountPool.priorityFirst":`最先`,"accountPool.priorityEarlier":`较先`,"accountPool.priorityNormal":`默认`,"accountPool.priorityLater":`较后`,"accountPool.priorityLast":`最后`,"accountPool.priorityOption":`{name}({value})`,"accountPool.priorityCustom":`自定义`,"accountPool.priorityUpdated":`已更新 {email} 的选择顺序`,"accountPool.priorityUpdateFailed":`无法保存 {email} 的选择顺序。当前显示最后一次确认的值。`,"codexAuth.switched":`下一次请求将使用 {email}`,"codexAuth.loadFailed":`无法加载 Codex 账号设置。`,"codexAuth.switchFailed":`无法切换账户。之前的选择保持不变。`,"codexAuth.removeConfirm":`删除 {id}?`,"codexAuth.removeFailed":`无法移除账户。未进行任何更改。`,"codexAuth.addTitle":`添加 Codex 账号`,"codexAuth.addIdLabel":`账号 ID(标识符)`,"codexAuth.addJsonLabel":`auth.json 内容`,"codexAuth.addHelp":`从另一台机器的 ~/.codex/auth.json 复制,或使用 codex-auth export。`,"codexAuth.importBtn":`导入`,"codexAuth.importInvalidJson":`无效的 JSON`,"codexAuth.importMissingTokens":`JSON 中缺少 access_token 或 refresh_token`,"codexAuth.importMissingId":`请输入账号 ID`,"codexAuth.accountAdded":`账号已添加到池中`,"codexAuth.addPickDesc":`使用另一个 ChatGPT 账号登录以添加到池中。`,"codexAuth.oauthLogin":`OAuth 登录`,"codexAuth.oauthDesc":`在浏览器中打开 ChatGPT 登录`,"codexAuth.deviceLogin":`设备码登录`,"codexAuth.deviceDesc":`适用于无头或远程代理:在另一台设备上输入短代码`,"codexAuth.importAuthJson":`导入 auth.json`,"codexAuth.importAuthJsonDesc":`从另一个 Codex 安装或 codex-auth 导出`,"codexAuth.back":`返回`,"codexAuth.oauthAlreadyInProgress":`登录已在进行中。请在浏览器中完成。`,"codexAuth.oauthWaiting":`等待浏览器中完成 ChatGPT 登录...`,"codexAuth.oauthSubmittingCode":`正在提交代码…`,"codexAuth.oauthCodeSubmitted":`代码已提交——正在等待登录完成…`,"codexAuth.oauthStatusRetrying":`检查登录状态时发生网络或代理错误——正在重试…`,"codexAuth.oauthCancelled":`登录已取消。`,"codexAuth.loginFailed":`登录失败`,"codexAuth.needsReauth":`重新登录`,"codexAuth.reauthenticate":`重新认证`,"codexAuth.tokenExpired":`令牌已过期 — 请重新认证此账号`,"codexAuth.mainTokenExpired":`令牌已过期 — 请通过 Codex 应用登录重新登录`,"codexAuth.emailCollision":`此账号与您的主 Codex 登录相同。请使用其他账号。`,"codexAuth.resetCreditsTitle":`重置额度`,"codexAuth.resetCreditsAvailable":`您有 {count} 个可用重置额度。`,"codexAuth.resetCreditsDesc":`每个额度可立即重置您当前的小时和每周使用限制。`,"codexAuth.noResetCredits":`没有可用的重置额度。`,"codexAuth.earnCreditsHint":`额度每月自动发放,也可通过推荐计划获得。`,"codexAuth.creditsExpireNote":`额度在获得后 30 天过期。`,"codexAuth.useOneCredit":`使用 1 个额度`,"codexAuth.confirmResetTitle":`使用重置额度?`,"codexAuth.confirmResetDesc":`这将立即重置您当前的使用限制。剩余额度:{count} 个。`,"codexAuth.irreversible":`此操作不可撤销。`,"codexAuth.useCredit":`使用额度`,"codexAuth.redeeming":`重置中...`,"codexAuth.resetSuccess":`使用限制已重置!剩余额度:{remaining} 个。`,"codexAuth.resetSuccessGeneric":`使用限制已重置!`,"codexAuth.resetAlreadyRedeemed":`该额度已兑换过,额度未变。`,"codexAuth.resetNothingToReset":`当前没有需要重置的使用窗口。`,"codexAuth.resetNoCredit":`没有可用的重置额度。`,"codexAuth.resetError":`重置额度使用失败,请重试。`,"codexAuth.fifoNote":`最早获得的额度优先使用。`,"codexAuth.confirmWhichCredit":`将使用 {date} 获得的额度。`,"codexAuth.creditNext":`即将使用`,"codexAuth.creditLabel":`额度 #{n}`,"codexAuth.creditNextBadge":`NEXT`,"codexAuth.creditGranted":`获得 {date}`,"codexAuth.creditExpires":`过期 {date}(剩余 {days} 天)`,"api.title":`API 访问`,"api.subtitle":`用生成的 API 密钥从外部应用访问 opencodex 代理。认证使用 {authHeader} 请求头;各端点接受哪些请求头见下表。`,"api.endpointNote":`请将基础 URL 用于 OpenAI 兼容客户端。Responses 与 Chat Completions 在 /v1 下提供。`,"api.baseUrl":`基础 URL`,"api.responsesEndpoint":`Responses API`,"api.chatCompletionsEndpoint":`Chat Completions API`,"api.messagesEndpoint":`Messages API`,"api.modelsEndpoint":`Models API`,"api.endpointsTitle":`网关端点`,"api.authBaseUrlNote":`客户端应使用基础 URL,然后选择下面的协议端点。`,"api.authTitle":`身份验证`,"api.authLoopback":`回环绑定(127.0.0.1 或 ::1)会跳过身份验证。远程绑定需要生成的 ocx_ 密钥或 OPENCODEX_API_AUTH_TOKEN。`,"api.modelsTitle":`外部模型目录`,"api.modelsCount":`{count} 个可调用`,"api.modelsSearch":`搜索模型`,"api.modelsSubtitle":`请使用这些精确的模型 ID 搭配 /v1/models 和你选择的入站协议。`,"api.modelsLoading":`正在加载模型…`,"api.modelsEmpty":`还没有可供外部调用的模型。`,"api.modelsNoMatch":`没有与“{query}”匹配的模型。`,"api.modelsLoadFailed":`无法加载外部模型目录。`,"api.colModel":`模型`,"api.colSource":`来源`,"api.colProtocols":`协议`,"api.copyModelId":`复制 ID`,"api.modelCopied":`已复制`,"api.testModel":`测试`,"api.testingModel":`测试中…`,"api.testSucceeded":`成功`,"api.testFailed":`失败`,"api.protocolResponses":`Responses`,"api.protocolChatCompletions":`Chat Completions`,"api.protocolMessages":`Messages`,"api.sourceNative":`ChatGPT 池`,"api.sourceCombo":`组合路由`,"api.sourceCustom":`自定义`,"api.usageResponsesTitle":`Responses 示例`,"api.usageChatTitle":`Chat Completions 示例`,"api.usageMessagesTitle":`Messages 示例`,"api.newKeyTitle":`已创建新密钥`,"api.newKeyNote":`请立即复制此密钥,它不会再次显示。`,"api.copy":`复制`,"api.copied":`已复制`,"api.dismiss":`关闭`,"api.generateTitle":`生成密钥`,"api.keyNamePlaceholder":`密钥名称(可选)`,"api.generate":`生成`,"api.generating":`创建中…`,"api.activeKeys":`活跃密钥({count})`,"api.activeKeysLoading":`有效密钥`,"api.noKeys":`还没有 API 密钥。请在上方生成一个。`,"api.workspace.sections":`API 分区`,"api.section.keys":`密钥`,"api.section.connect":`连接`,"api.section.endpoints":`端点`,"api.section.models":`模型`,"api.section.examples":`示例`,"api.workspace.details":`API 密钥详情`,"api.workspace.keyDetails":`密钥详情`,"api.workspace.keyPrefix":`密钥前缀`,"api.workspace.deleteKey":`删除密钥`,"api.workspace.deleteConfirm":`确定要删除此密钥吗?此操作无法撤销。`,"api.workspace.usageExamples":`用法示例`,"api.copyUrlHint":`点击复制 URL`,"api.urlCopied":`已复制 URL`,"api.copyExampleHint":`点击复制示例`,"api.exampleCopied":`已复制示例`,"api.colName":`名称`,"api.colKey":`密钥`,"api.colCreated":`创建时间`,"api.confirm":`确认`,"api.deleteAria":`删除 API 密钥`,"api.usageSampleInput":`你好,世界!`,"api.clientConfig.title":`客户端配置`,"api.clientConfig.rowsLabel":`连接客户端`,"api.clientConfig.details":`详情`,"api.clientConfig.detailsAria":`{client} 配置详情`,"api.clientConfig.copyAria":`复制 {client} 配置`,"api.clientConfig.downloadAria":`下载 {client} 配置`,"api.clientConfig.rowMeta":`{destination} · {count} 个模型`,"api.clientConfig.rowError":`无法生成 {client} 配置。`,"api.clientConfig.copiedAnnounceClient":`已将 {client} 配置复制到剪贴板。`,"api.clientConfig.clientOpencode":`OpenCode`,"api.clientConfig.clientPi":`Pi`,"api.clientConfig.clientOmp":`OMP`,"api.clientConfig.clientHermes":`Hermes`,"api.clientConfig.clientOpenclaw":`OpenClaw`,"api.clientConfig.clientKimi":`Kimi Code`,"api.clientConfig.clientGajae":`Gajae Code`,"api.clientConfig.clientDsh":`DeepSeek Harness (DSH)`,"api.clientConfig.clientMcode":`MiniMax Code`,"api.clientConfig.clientZcode":`ZCode`,"api.clientConfig.clientPrime":`Prime Agent`,"api.clientConfig.clientAside":`Aside`,"api.clientConfig.copy":`复制配置`,"api.clientConfig.download":`下载`,"api.clientConfig.loading":`正在生成客户端配置…`,"api.clientConfig.jsonLabel":`{client} 配置`,"api.clientConfig.destination":`目标文件`,"api.clientConfig.envHint":`启动前设置密钥`,"api.clientConfig.mergeWarning":`请合并到目标文件中。直接替换会丢失你已有的其他提供商和 MCP 配置。`,"api.clientConfig.modelCount":`已导出 {count} 个模型`,"api.clientConfig.missingLimits":`{total} 个模型中有 {count} 个没有上下文上限,客户端将使用自己的默认值。`,"api.clientConfig.noKeyYet":`{env} 目前还没有对应的密钥。离开回环地址使用前,请先在上方生成密钥。`,"api.clientConfig.loadFailed":`无法读取模型列表,因此没有生成客户端配置。`,"api.clientConfig.copiedAnnounce":`客户端配置已复制到剪贴板。`,"api.clientConfig.copyFailed":`无法复制客户端配置。`,"api.clientConfig.downloadedAnnounce":`已下载 {filename}。目前还没有任何改动,请自行将其合并到 {destination}。`,"api.clientConfig.whereDisclosure":`该文件应放在哪里`,"api.clientConfig.whereBody":`上面的路径是全局配置位置。工作目录中的项目级配置文件优先级更高;密钥由配置中指定的环境变量读取,不会写入该文件。`,"api.keysLoadFailed":`无法加载 API 密钥。`,"api.createFailed":`无法创建 API 密钥。`,"api.deleteFailed":`无法删除 API 密钥。`,"api.auth.endpoint":`端点`,"api.auth.required":`必需`,"api.auth.accepted":`可用`,"api.auth.rejected":`不接受`,"api.auth.testProtocol":`测试 {protocol}`,"api.auth.testNeedsFreshKey":`要运行带认证的测试,请先生成密钥,并让一次性显示的值保留在屏幕上。`,"api.key.name":`密钥名称`,"api.key.rename":`重命名`,"api.key.saveName":`保存名称`,"api.key.renaming":`保存中…`,"api.key.renameFailed":`无法重命名密钥,已保留你输入的内容。`,"api.key.deleting":`删除中…`,"api.rotation.title":`密钥轮换`,"api.rotation.description":`签发替换密钥,并在短暂过渡期内保留当前密钥。`,"api.rotation.start":`开始轮换`,"api.rotation.starting":`正在开始…`,"api.rotation.pending":`轮换待确认。请先更新并验证客户端,再提交轮换。`,"api.rotation.expires":`过渡期截止:`,"api.rotation.secretOnce":`替换密钥仅显示一次。关闭前请先复制。`,"api.rotation.commit":`提交轮换`,"api.rotation.abort":`中止轮换`,"api.rotation.failed":`轮换操作未完成。请刷新后重试。`,"api.rotation.startFailed":`无法开始密钥轮换。`,"api.key.copyFailed":`无法复制密钥。关闭此面板前请手动选中并复制。`,"api.attribution.title":`按密钥统计的用量`,"api.attribution.requests7d":`最近 7 天请求数`,"api.attribution.totalRequests":`已归属请求总数`,"api.attribution.totalRequestsAvailable":`可用历史中的请求`,"api.attribution.sinceAvailable":`可用归属记录起始时间`,"api.attribution.lastUsed":`最近使用`,"api.attribution.since":`统计起始`,"api.attribution.neverUsed":`统计开始后未使用`,"api.attribution.unavailable":`暂无用量`,"api.attribution.unavailableDetail":`尚未归属任何用量。统计开始之前的请求无法追溯归属。`,"api.attribution.ambiguous":`两个密钥共用同一个 ID,无法判断用量属于哪一个。请在配置文件中为每个密钥设置唯一 ID。`,"api.attribution.railAmbiguous":`ID 重复`,"claude.subtitle":`在 Claude Code 中使用 GPT、Gemini 等其他模型。`,"claude.enabledLabel":`Claude 连接`,"claude.enabledHint":`关闭后 Claude Code 无法使用此代理。`,"claude.authMode":`认证模式`,"claude.authModeHint":`Subscription 需要 Claude 账户,Proxy 无需 Anthropic 账户即可使用`,"claude.authModeSubscription":`Subscription(Claude 账户)`,"claude.authModeProxy":`Proxy(无需账户)`,"claude.authModeAuto":`自动(检测 Claude 认证)`,"claude.effectiveMode.label":`下次启动生效`,"claude.effectiveMode.manual":`手动:{mode}`,"claude.effectiveMode.autoPresent":`自动:订阅 — 已通过 {source} 找到 Claude 认证`,"claude.effectiveMode.autoAbsent":`自动:代理模式 — 未找到 Claude 认证`,"claude.effectiveMode.autoUnknown":`自动:订阅 — 无法确认认证`,"claude.effectiveMode.admissionKey":`此代理的 API 密钥仍会发送。`,"claude.authSource.claude-json-oauth":`Claude 账户`,"claude.authSource.claude-credentials-file":`凭据文件`,"claude.authSource.macos-keychain":`macOS 钥匙串`,"claude.authSource.exported-env":`环境变量`,"claude.authSource.unknown":`检测到的凭据`,"claude.systemEnv":`自动连接`,"claude.systemEnvDesc":`开启后,在任意终端运行 claude 会自动通过代理。`,"claude.systemEnvUnsupported":`自动连接仅在 macOS 上可用。在此系统上,请使用 {cmd} 启动 Claude。`,"claude.systemEnvWarn":`⚠ 需要完全退出并重新打开终端应用才能生效。不推荐使用。`,"claude.fastMode":`Fast Mode (OpenAI)`,"claude.fastModeDesc":`控制 OpenAI 模型的推理速度。ON = 优先级(更快)。OFF = 默认速度。Auto = 透传客户端设置。`,"claude.fastAuto":`Auto`,"claude.fastOn":`ON`,"claude.fastOff":`OFF`,"claude.autoContext":`自动利用大上下文`,"claude.autoContextDesc":`决定 1M 标记的范围。开:窗口能容纳压缩阈值的模型都有大上下文条目;关:仅真正的 1M 模型有。`,"claude.autoContextInert":`配置文件中存在旧式上下文大小值(maxContextTokens),此功能暂不生效。删除该值即可恢复。`,"claude.autoCompactWindow":`自动摘要触发点`,"claude.autoCompactDefault":`{value}(默认)`,"claude.autoCompactWindowDesc":`对话达到该点时自动摘要旧内容。不会超过各模型自身上限,因此 200k 模型不受影响。`,"claude.autoCompactWindowWarn":`修改该值可能导致 GPT 模型异常——若超过模型真实上限,会在摘要触发前报错。`,"claude.injectAgents":`自动注册子代理`,"claude.injectAgentsDesc":`将“子代理”页选中的模型(以及当前默认模型)注册为 Claude Code 可派遣的代理(ocx-*)。从下一个会话开始生效。`,"claude.webSearchSidecar":`网页搜索附属服务覆盖`,"claude.webSearchSidecarHint":`仅对 Claude Code 请求覆盖主网页搜索附属服务设置。`,"claude.visionSidecar":`视觉附属服务覆盖`,"claude.visionSidecarHint":`仅对 Claude Code 请求覆盖主视觉附属服务设置。`,"claude.useMainSetting":`使用主设置`,"claude.sidecarModelPlaceholder":`主设置中的模型`,"claude.quickstart":`开始使用`,"claude.quickstartHint":`{cmd} 通过代理打开 Claude Code。你的 claude.ai 登录保持不变。`,"claude.manualEnv":`手动配置(高级)`,"claude.smallFastModel":`后台辅助模型`,"claude.smallFastModelHint":`Claude Code 用于对话摘要、主题识别等后台工作的模型。子代理的 haiku 别名也使用它。留空 = Claude 默认(Haiku)。`,"claude.smallFastModelAccurateHint":`Claude Code 用于聊天摘要、主题识别等后台工作的模型。子代理的 haiku 别名也使用此模型。`,"claude.smallFastModelUnsetOption":`让 Claude Code 选择(原生模型)`,"claude.smallFastModelNativeWarning":`留空时,OpenCodex 不会设置辅助模型覆盖项。Claude Code 可能使用其原生 Sonnet 模型,并可能产生原生提供方费用。`,"claude.slotUnset":`使用 Claude 默认值`,"claude.modelMap":`模型拦截`,"claude.modelMapHint":`拦截对特定模型的请求并重定向到你指定的模型。默认为空——添加规则后才生效。`,"claude.mapFrom":`原始模型(如 claude-sonnet-4-5)`,"claude.mapTo":`替换为(如 gemini/gemini-3-pro)`,"claude.addMapping":`添加规则`,"claude.removeMapping":`删除规则`,"claude.aliases":`可用模型`,"claude.aliasesHint":`Claude Code 的 /model 菜单中显示的模型列表。`,"claude.aliasProviderOther":`其他`,"claude.loading":`加载中…`,"claude.loadFail":`加载 Claude 设置失败`,"claude.saved":`已保存。`,"claude.saveFailed":`保存失败`,"claude.networkError":`网络错误 — 代理是否在运行?`,"claude.toggleAria":`切换 Claude 连接`,"claude.none":`无`,"common.close":`关闭`,"common.ok":`确定`,"app.logoAria":`opencodex 徽标`,"app.claudeOn":`Claude 开`,"app.claudeOff":`Claude 关`,"usage.dayMon":`一`,"usage.dayWed":`三`,"usage.dayFri":`五`,"usage.heatmap.tooltipTokens":`{tokens} 令牌`,"usage.heatmap.tooltipRequests":`{requests} 请求`,"nav.storage":`存储`,"storage.title":`存储`,"storage.subtitle":`查看 CODEX_HOME 占用。清理不会动到活动会话。`,"storage.loading":`正在扫描存储…`,"storage.empty":`CODEX_HOME 为空或不存在——没有可显示的内容。`,"storage.error":`存储扫描失败。请检查 CODEX_HOME 是否指向有效目录。`,"storage.refresh":`重新扫描`,"storage.rescanned":`扫描完成。`,"storage.card.total":`总大小`,"storage.card.files":`文件数`,"storage.card.home":`CODEX_HOME`,"storage.snapshot.lastScan":`上次扫描`,"storage.snapshot.scanning":`扫描中…`,"storage.snapshot.unavailable":`尚无扫描。`,"storage.cleanupCard.title":`释放空间`,"storage.cleanupCard.tabs":`清理选项`,"storage.cleanupCard.tab.policy":`策略`,"storage.cleanupCard.tab.quarantine":`隔离区`,"storage.cleanup.noArchives":`没有可清理的归档会话。`,"storage.section.buckets":`分类`,"storage.section.largest":`最大文件`,"storage.workspace.overview":`概览`,"storage.workspace.selectBucket":`从列表中选择一个存储桶以查看明细。`,"storage.col.bucket":`分类`,"storage.col.size":`大小`,"storage.col.files":`文件`,"storage.col.oldest":`最旧`,"storage.col.newest":`最新`,"storage.col.rows":`数据库行数`,"storage.rows.unknown":`未知(已锁定)`,"storage.bucket.sessions":`活动会话`,"storage.bucket.archived_sessions":`已归档会话`,"storage.bucket.logs_db":`日志数据库`,"storage.bucket.state_db":`状态数据库`,"storage.bucket.attachments":`附件`,"storage.bucket.deletion_manifests":`删除清单`,"storage.bucket.other":`其他`,"storage.cleanup.title":`归档清理`,"storage.cleanup.help":`按百分比移除最旧的归档会话。不会触碰活动会话。默认隔离——文件移至 CODEX_HOME/.trash。`,"storage.cleanup.slider":`最旧归档百分比`,"storage.cleanup.percent":`{percent}%`,"storage.cleanup.preset":`{percent}`,"storage.cleanup.preview":`预览`,"storage.cleanup.confirmTitle":`确认归档清理`,"storage.cleanup.confirmBody":`将处理 {count} 个归档文件(约 {size}),即最旧的 {percent}%。`,"storage.cleanup.moreFiles":`…以及另外 {n} 个`,"storage.cleanup.permanent":`永久删除(跳过隔离)`,"storage.cleanup.permanentWarn":`永久删除无法撤销。`,"storage.cleanup.quarantineNote":`文件会移到 CODEX_HOME 下的 .trash。可在「隔离区」标签页恢复。`,"storage.cleanup.cancel":`取消`,"storage.cleanup.confirmQuarantine":`隔离`,"storage.cleanup.confirmPermanent":`永久删除`,"storage.cleanup.doneQuarantine":`已隔离 {count} 个文件({size})。`,"storage.cleanup.donePermanent":`已永久删除 {count} 个文件({size})。`,"storage.cleanup.previewFailed":`预览失败。`,"storage.cleanup.cleanupFailed":`清理失败。`,"storage.cleanup.err.codex_busy":`Codex 正在使用 state.sqlite — 请退出 Codex 后重试。`,"storage.cleanup.err.stale_preview":`预览后归档文件已变化 — 请重新预览。`,"storage.cleanup.err.restore_pending_overlap":`所选归档与未完成的隔离区恢复重叠 — 请先完成或重试恢复。`,"storage.cleanup.err.referenced_history":`所选归档仍被 fork 或分页历史引用。`,"storage.cleanup.err.invalid_digest":`预览摘要缺失或无效。`,"storage.cleanup.err.invalid_mode":`模式必须是 quarantine 或 permanent。`,"storage.cleanup.err.fs_failed":`文件系统清理失败。部分更改可能已生效 — 请检查 CODEX_HOME/.trash 及显示的恢复路径。`,"storage.cleanup.err.fs_failed_trash":`文件系统清理失败。部分更改可能已生效 — 请在 {trashDir} 和 manifest.json 中查找可恢复文件。`,"storage.cleanup.err.db_reconcile_failed":`无法更新 Codex 状态数据库。`,"storage.cleanup.err.cleanup_failed":`清理失败。`,"storage.trash.title":`隔离区`,"storage.trash.help":`已移至 CODEX_HOME/.trash 的归档会话。恢复会把 JSONL 与线程行写回。`,"storage.trash.empty":`没有隔离条目。`,"storage.trash.loading":`正在加载隔离区…`,"storage.trash.col.when":`隔离时间`,"storage.trash.col.files":`文件`,"storage.trash.col.size":`大小`,"storage.trash.col.mode":`模式`,"storage.trash.col.id":`条目`,"storage.trash.restore":`恢复`,"storage.trash.confirmTitle":`恢复隔离条目?`,"storage.trash.confirmBody":`将 {count} 个文件(约 {size})从 {id} 恢复到归档会话。`,"storage.trash.cancel":`取消`,"storage.trash.confirmRestore":`恢复`,"storage.trash.done":`已恢复 {count} 个文件({size})。`,"storage.trash.restoreFailed":`恢复失败。`,"storage.trash.listFailed":`无法列出隔离条目。`,"storage.trash.mode.quarantine":`隔离`,"storage.trash.mode.permanent":`永久(未完成)`,"storage.trash.err.codex_busy":`Codex 正在使用 state.sqlite — 请退出 Codex 后重试。`,"storage.trash.err.invalid_trash":`隔离条目 ID 缺失或无效。`,"storage.trash.err.missing_trash":`未找到隔离条目。`,"storage.trash.err.dest_exists":`恢复目标已存在 — 请删除或重命名归档文件后重试。`,"storage.trash.err.fs_failed":`文件系统恢复失败。部分文件可能已恢复 — 请检查 archived_sessions 与 .trash。`,"storage.trash.err.storage_mutation_busy":`另一项存储清理或恢复正在进行 — 请稍后再试。`,"storage.trash.err.db_reconcile_failed":`无法恢复 Codex 状态数据库行。`,"storage.trash.err.restore_failed":`恢复失败。`,"storage.trash.err.restore_worker_timeout":`恢复耗时过长(超过 10 分钟)已停止。`,"storage.trash.err.restore_worker_aborted":`关闭过程中恢复已取消。`,"storage.trash.err.restore_worker_failed":`恢复 worker 崩溃或意外失败。`,"storage.policy.title":`自动清理策略`,"storage.policy.help":`当归档大小超过阈值时可选批量清理。默认关闭——不会自动启用。`,"storage.policy.loading":`正在加载策略…`,"storage.policy.loadFailed":`无法加载清理策略。`,"storage.policy.saveFailed":`无法保存清理策略。`,"storage.policy.runFailed":`策略运行失败。`,"storage.policy.alreadyRunning":`清理策略已在运行中。`,"storage.policy.invalid":`策略值无效。`,"storage.policy.enabled":`启用自动清理`,"storage.policy.enabledHint":`默认关闭。启用后仅按所选计划(或立即运行)执行。`,"storage.policy.threshold":`当归档大小超过(GiB)`,"storage.policy.trigger":`触发条件`,"storage.policy.target":`清理目标`,"storage.policy.targetPercent":`删除最旧归档(%)`,"storage.policy.targetReduce":`将归档缩小至(GiB)`,"storage.policy.thresholdInc":`提高阈值`,"storage.policy.thresholdDec":`降低阈值`,"storage.policy.percentInc":`提高百分比`,"storage.policy.percentDec":`降低百分比`,"storage.policy.reduceInc":`提高缩减目标`,"storage.policy.reduceDec":`降低缩减目标`,"storage.policy.schedule":`计划`,"storage.policy.schedule.manual":`仅手动`,"storage.policy.schedule.startup":`代理启动时`,"storage.policy.schedule.daily":`每天`,"storage.policy.schedule.weekly":`每周`,"storage.policy.mode":`删除模式`,"storage.policy.mode.quarantine":`隔离(默认)`,"storage.policy.mode.permanent":`永久删除`,"storage.policy.permanentWarn":`永久模式无法撤销。不确定时请使用隔离。`,"storage.policy.lastRun":`上次运行`,"storage.policy.lastRunDetail":`已移除 {count} · 释放 {size}`,"storage.policy.nextRun":`下次运行`,"storage.policy.never":`从未`,"storage.policy.save":`保存`,"storage.policy.runNow":`立即运行`,"storage.policy.running":`运行中…`,"storage.policy.saved":`策略已保存。`,"storage.policy.skippedDisabled":`策略已禁用 — 请先启用。`,"storage.policy.skippedUnder":`归档大小低于阈值 — 无需操作。`,"storage.policy.skippedEmpty":`没有匹配目标的归档候选项。`,"storage.policy.doneQuarantine":`策略已隔离 {count} 个文件({size})。`,"storage.policy.donePermanent":`策略已永久删除 {count} 个文件({size})。`,"storage.policy.metadataSaveWarning":`策略运行已完成,但无法保存其调度元数据。`,"modal.back":`返回`,"modal.badge.oauth":`OAuth`,"modal.customProvider":`自定义提供方`,"modal.failedStatus":`失败 ({status})`,"modal.loginError":`登录错误:{error}`,"modal.badge.codexLogin":`Codex 登录`,"modal.badge.local":`本地`,"modal.badge.apiKey":`API 密钥`,"modal.badge.direct":`直连`,"modal.badge.pool":`账户池`,"modal.badge.free":`免费`,"modal.invalidPreset":`此内置提供方预设不完整。请重启代理后重试。`,"modal.freeTierTitle":`免费层级`,"modal.freeTierDefault":`无需 API 密钥,开箱即用。`,"modal.tab.accounts":`账户`,"modal.tab.free":`免费`,"modal.tab.paid":`付费`,"modal.accountsHint":`在此登录 ChatGPT/Codex、OAuth 与 API 密钥账户。OpenAI 为内置提供商 — 请登录,无需再次添加。`,"modal.accountsCodexAuthLink":`Codex 认证`,"modal.notListed":`没有你要的提供商?添加自定义`,"modal.catalogLoading":`正在加载目录…`,"modal.accountLogin":`登录`,"modal.accountLogout":`退出登录`,"modal.accountAdd":`添加账户`,"modal.accountManage":`管理`,"modal.accountCodexPool":`ChatGPT 账户池`,"modal.accountLoggedIn":`已登录`,"modal.accountLoggedOut":`未登录`,"quota.fiveHourLimit":`5 小时限额`,"quota.ageMinutes":`{n} 分钟`,"quota.ageHours":`{n} 小时`,"quota.ageDays":`{n} 天`,"quota.observedAgo":`{age}前获取`,"quota.observedHint":`Meta 仅在流式响应期间报告用量,因此这是最后一次获取的数值,而非实时读数。`,"quota.weeklyLimit":`每周限额`,"quota.monthlyLimit":`30 天限额`,"quota.cursorFirstParty":`官方模型`,"quota.cursorApiUsage":`API 用量`,"quota.totalSubscriptionCredits":`订阅总额度`,"quota.creditsBalance":`额度余额`,"quota.creditsPeriodEnds":`账单周期结束于 {date}`,"quota.usedPercent":`已用 {pct}%`,"quota.limitReached":`已达上限`,"quota.resetsToday":`今天 {time} 重置`,"quota.resetsTomorrow":`明天 {time} 重置`,"quota.resetsAt":`{when} 重置`,"quota.resetsRelativeMinutes":`{n} 分钟后重置`,"quota.resetsRelativeHours":`{n} 小时后重置`,"pws.status.ready":`就绪`,"pws.status.needsSetup":`需要设置`,"pws.status.needsAttention":`需要关注`,"pws.auth.chatgptPassthrough":`ChatGPT 直通`,"pws.auth.noKey":`无需密钥`,"pws.freeTitle":`免费定价(可能仍需密钥)`,"pws.localTitle":`本地运行时`,"pws.modelCountOne":`1 个模型`,"pws.modelCount":`{count} 个模型`,"pws.rail.suffixDefault":` · 默认`,"pws.rail.suffixLocal":` · 本地`,"pws.rail.suffixFree":` · 免费`,"pws.rail.selectAria":`选择 {name} — {status}{suffix}`,"pws.searchPlaceholder":`搜索提供商…`,"pws.filterAria":`筛选提供商`,"pws.providerFiltersAria":`提供商筛选`,"pws.filters":`筛选`,"pws.filterStatus":`状态`,"pws.pricing":`定价`,"pws.paid":`付费`,"pws.filterType":`类型`,"pws.type.cloud":`云端`,"pws.type.local":`本地`,"pws.type.selfHosted":`自托管`,"pws.type.login":`登录`,"pws.sort":`排序`,"pws.sortProvidersAria":`排序提供商`,"pws.sort.az":`A–Z`,"pws.sort.za":`Z–A`,"pws.sort.freePaid":`免费优先`,"pws.sort.paidFree":`付费优先`,"pws.sort.accountsFirst":`账户优先`,"pws.resetAll":`全部重置`,"pws.providerList":`提供商列表`,"pws.providersAria":`提供商`,"pws.groupReady":`就绪 ({count})`,"pws.groupNeedsSetup":`需要设置 ({count})`,"pws.groupDisabled":`已禁用 ({count})`,"pws.noSearchResults":`没有匹配搜索的提供商。`,"pws.noMatchFilters":`没有匹配筛选的提供商。`,"pws.noProvidersConfigured":`尚未配置提供商。`,"pws.workspaceMainAria":`提供商详情`,"pws.detailComingSoon":`详情视图即将推出 — 请在经典视图中管理。`,"pws.selectPrompt":`从列表中选择一个提供商。`,"pws.connectFirst":`连接你的第一个提供商`,"pws.empty.browseFree":`浏览免费提供商`,"pws.empty.browseFreeDesc":`无需订阅即可开始`,"pws.empty.connectAccount":`连接账户`,"pws.empty.connectAccountDesc":`使用 ChatGPT 或提供商登录`,"pws.empty.addEndpoint":`添加端点`,"pws.empty.addEndpointDesc":`自定义 base URL 和 API 密钥`,"pws.tab.overview":`概览`,"pws.tab.models":`模型`,"pws.tab.usage":`用量`,"pws.tab.accounts":`账户`,"pws.tab.settings":`设置`,"pws.connection":`连接`,"pws.status.connected":`已连接`,"pws.attentionTitle":`需要关注`,"pws.attention.reauth":`当前账号需要重新认证`,"pws.attention.reauthForward":`当前 Codex 账号需要重新认证 — 请到“账号”中处理`,"pws.attention.missingCredentials":`缺少凭证`,"pws.cell.auth":`认证`,"pws.cell.note":`备注`,"pws.cell.defaultModel":`默认模型`,"pws.statsAria":`提供商统计`,"pws.statsTitle":`统计`,"pws.stats.totalRequests":`请求数(30 天)`,"pws.stats.totalTokens":`令牌(30 天)`,"pws.stats.quotaUpdated":`配额更新`,"pws.stats.quotaTracked":`在用量标签查看限额。`,"pws.stats.source":`来源`,"pws.usageLast30d":`用量(最近 30 天)`,"pws.estimatedCost":`预估费用`,"pws.costDisclaimer":`基于 API 公示价格的预估值,非实际计费金额。`,"pws.modelBreakdown":`模型用量明细`,"pws.col.model":`模型`,"pws.col.cost":`预估费用`,"pws.col.tokens":`Token`,"pws.col.requests":`请求`,"pws.col.share":`占比`,"pws.tokenInput":`输入`,"pws.tokenOutput":`输出`,"pws.metricRequests":`请求`,"pws.metricTokens":`令牌`,"pws.usageUnavailable":`尚无用量记录。`,"pws.rateLimits":`速率限制`,"pws.quotaUnavailable":`此提供商暂无配额数据。`,"pws.accountQuotaUnavailable":`速率限制数据暂时不可用;若有上次已知值则继续显示。`,"pws.selected":`已选择`,"pws.copyModelId":`复制 ID`,"pws.modelCopied":`已复制!`,"pws.modelsAvailable":`{count} 个可用`,"pws.modelSearchPlaceholder":`筛选模型…`,"pws.modelsLoading":`正在加载模型…`,"pws.modelsLoadFailed":`无法加载模型。`,"pws.modelsNeedsReauth":`需要重新登录后才能获取实时模型列表。当前显示已配置的模型。`,"pws.modelsConfiguredFallback":`显示已配置的模型(实时发现不可用)。`,"pws.modelsTruncated":`显示 {total} 个模型中的前 {shown} 个。使用筛选以缩小列表。`,"pws.retry":`重试`,"pws.noModels":`未发现此提供商的模型。`,"pws.noModelMatch":`没有匹配筛选的模型。`,"pws.adapterBaseRequired":`适配器和基本 URL 为必填项。`,"pws.addAccount":`添加账户`,"pws.addKey":`添加 API 密钥`,"pws.apiKeys":`API 密钥`,"pws.authMode":`认证方式`,"pws.availableAccounts":`可用账户`,"pws.accountOrdinal":`账户 {count}`,"pws.accountsLoading":`正在加载账户…`,"pws.accountsLoadFailed":`无法加载账户。`,"pws.retryAccounts":`重试`,"pws.noAccounts":`尚未连接任何账户。`,"pws.cockpitImportDescription":`从此设备导入 Cockpit Tools Antigravity JSON 导出文件。不会显示文件内容。`,"pws.cockpitImportFileLabel":`Cockpit Tools Antigravity JSON 导出文件`,"pws.cockpitImportChooseFile":`选择 JSON 文件`,"pws.cockpitImporting":`正在导入…`,"pws.cockpitImportInvalid":`所选文件不是有效的 JSON 导出文件或文件过大。`,"pws.cockpitImportFailed":`无法完成账户导入。`,"pws.cockpitImportComplete":`导入完成:已导入 {imported},已更新 {updated},失败 {failed},不支持 {unsupported}。`,"pws.accountSwitching":`切换中…`,"pws.accountCurrent":`当前账户`,"pws.defaultModelNone":`无(使用提供商默认值)`,"pws.discardSettings":`放弃`,"pws.jsonEditorDesc":`直接编辑提供商 JSON 配置。更改将立即保存。`,"pws.jsonEditorTitle":`JSON 编辑器 — {name}`,"pws.jsonRestore":`恢复`,"pws.jsonSave":`保存`,"pws.loggedInTitle":`已登录`,"pws.notLoggedInTitle":`未登录`,"pws.note":`备注`,"pws.allowPrivateNetwork":`允许本地/私有网络`,"pws.liveModels":`从提供方发现模型`,"pws.liveModelsDesc":`获取提供方的实时模型目录。关闭后仅使用已配置的静态模型。`,"pws.xaiResponsesOptIn":`为 Grok 4.5 和 4.6 使用 Responses API`,"pws.xaiResponsesOptInDesc":`通过 openai-responses 路由这两个模型。其他 Grok 模型和层级行为不变。`,"pws.xaiResponsesOptInMixed":`已部分启用。`,"pws.cursorTransport":`Cursor 传输协议`,"pws.cursorTransportHttp2":`HTTP/2(默认)`,"pws.cursorTransportHttp1":`HTTP/1.1(代理兼容)`,"pws.cursorTransportDesc":`当代理无法稳定承载 Cursor 的 HTTP/2 流时,请使用 HTTP/1.1。`,"pws.optionalPlaceholder":`可选`,"pws.providerId":`提供商 ID`,"pws.reauth":`需要重新认证`,"pws.reauthenticate":`重新认证`,"pws.copyDoctor":`复制 ocx doctor`,"pws.doctorCopied":`已复制`,"pws.healthCooldownHint":`请等到冷却结束。暂时不要探测此账户。`,"pws.doctorCopyUnavailable":`剪贴板不可用`,"pws.healthLabel.rateLimited":`已限速`,"pws.healthLabel.quotaLimited":`配额受限`,"pws.healthLabel.reauthRequired":`需要重新认证`,"pws.healthLabel.refreshFailed":`刷新失败`,"pws.healthLabel.metadataMismatch":`元数据不匹配`,"pws.healthLabel.credentialConflict":`凭证冲突`,"pws.healthSummary.rateLimited":`{provider} {account}:限速至 {until}。在此之前将暂停该账户的路由。`,"pws.healthSummary.quotaLimited":`{provider} {account}:配额限制至 {until}。在此之前将暂停该账户的路由。`,"pws.healthSummary.reauthRequired":`{provider} {account}:需要重新认证。`,"pws.healthSummary.credentialConflict":`{provider} {account}:凭证冲突。`,"pws.healthSummary.metadataMismatch":`{provider} {account}:元数据不匹配。`,"pws.healthSummary.staleCredentials":`{provider} {account}:凭证不完整。`,"pws.removeConfirm":`移除`,"pws.removeConfirmBody":`移除提供商「{name}」?此操作无法撤销。`,"pws.removeDefaultConfirmBody":`移除默认提供方「{name}」?「{defaultProvider}」将成为默认提供方。此操作无法撤销。`,"pws.removeConfirmTitle":`移除提供商`,"pws.saveSettings":`保存`,"pws.pacingTitle":`请求节流`,"pws.pacingDesc":`均匀延迟发往此提供商的请求启动。流式响应可以重叠。`,"pws.pacingEnabled":`启用`,"pws.pacingRpm":`每分钟请求数`,"pws.pacingRpmUnit":`RPM`,"pws.pacingDelay":`最小间隔(毫秒)`,"pws.pacingSlowerWins":`以较慢的提供商限制为准,模型规则只能增加延迟。`,"pws.pacingQueued":`排队中`,"pws.pacingNextSlot":`距下个时隙`,"pws.pacingLastModel":`上个模型`,"pws.pacingNone":`无`,"pws.pacingModelOverrides":`模型规则`,"pws.pacingModel":`模型`,"pws.pacingAdd":`添加规则`,"pws.pacingRemove":`移除`,"pws.pacingRemoveModel":`移除 {model} 的请求节流规则`,"pws.pacingRuleRequired":`请先设置提供商限制或模型规则,再启用请求节流。`,"pws.saving":`保存中…`,"pws.settingsSaved":`设置已保存。`,"pws.accountModeSaved":`账户模式已保存。`,"pws.accountModeFailed":`无法切换账户模式。`,"pws.accountModeConfirm":`切换 OpenAI 账户模式?正在进行的对话将重新分配到另一种模式的账户集合,配额用量将按新模式计入。`,"pws.settingsUnsavedBar":`有未保存的更改。`,"pws.unsavedLeaveBody":`有未保存的更改。离开前保存吗?`,"pws.unsavedLeaveTitle":`未保存的更改`,"pws.attentionRequired":`需要关注`,"pws.attentionAria":`{name}:{reason}`,"pws.missingCredentials":`缺少凭证`,"pws.editJsonDesc":`以 JSON 编辑原始代理配置`,"pws.updatesUnavailable":`提供商更新不可用。`,"pws.dashboard.title":`提供商概览`,"pws.dashboard.subtitle":`在一个地方管理所有模型提供商。`,"pws.dashboard.rateLimits":`速率限制`,"pws.capacity.estimate":`按配置权重估算的账户池`,"pws.capacity.currentAccount":`当前有效账户`,"pws.capacity.nextRecovery":`下一次容量恢复`,"pws.capacity.recoveryShare":`+{percent}% 账户池容量`,"pws.capacity.incomplete":`覆盖不完整:已排除 {excluded} 个账户`,"pws.capacity.uncalibratedPlan":`{count} 个账户使用未校准套餐,按基准席位权重计入,因此该估算可能偏保守`,"pws.capacity.partial":`部分窗口覆盖不完整:{count} 个账户未报告所有显示的限额窗口`,"pws.capacity.windowPartial":`部分`,"pws.capacity.windowPartialA11y":`{window}:账户覆盖不完整`,"pws.dashboard.recentlyUsed":`最近使用`,"pws.dashboard.requests":`{count} 个请求`,"pws.dashboard.checkedAgo":`{time} 前检查`,"pws.dashboard.noQuota":`无配额数据`,"pws.dashboard.noUsage":`暂无使用数据`,"pws.dashboard.noRateLimits":`暂无速率限制数据`,"pws.allProviders":`提供商概览`,"pws.enabledLabel":`已启用`,"pws.testConnection":`测试连接`,"pws.testing":`测试中…`,"pws.connectionOk":`连接成功`,"pws.connectionFailed":`连接失败`,"pws.connectionNotApplicable":`不适用 — 此提供方使用静态模型目录。`,"pws.editSettings":`编辑设置`,"pws.viewUsage":`查看详细用量`,"pws.allSystemsOk":`所有系统正常运行`,"pws.apiKeyConfigured":`API 密钥已配置`,"pws.addApiKey":`添加 API 密钥`,"pws.loggedInAs":`已登录为 {email}`,"pws.notLoggedIn":`未登录`,"pws.passthrough":`Codex 透传`,"pws.notes":`备注`,"pws.notePlaceholder":`添加关于此提供商的备注...`,"pws.noteSaved":`备注已保存`,"pws.authSummary":`认证`,"time.justNow":`刚刚`,"time.notChecked":`未检查`,"time.minutesAgo":`{n} 分钟前`,"time.hoursAgo":`{n} 小时前`,"time.daysAgo":`{n} 天前`,"modal.noMatch":`无匹配。`,"modal.oauthDefaultNote":`使用账户登录 — 无需 API 密钥。`,"modal.oauthComingSoon":`{label} 的 OAuth 登录将在下次更新提供。请先使用 API 密钥。`,"modal.oauthComingSoonShort":`此提供方的 OAuth 登录将在下次更新提供 — 请先使用 API 密钥。`,"modal.useApiKeyInstead":`改用 API 密钥`,"modal.setupGuide":`设置指南`,"modal.setupStep1Prefix":`前往`,"modal.setupDashboardLink":`{label} 控制台`,"modal.setupStep1Suffix":`并复制 API 密钥`,"modal.setupStep2":`粘贴到下方的 API 密钥字段`,"modal.setupStep3":`点击添加提供方 — 模型会自动发现`,"modal.namePlaceholder":`例如 openrouter`,"modal.duplicateWarn":`提供方 "{name}" 已存在,将被覆盖。`,"modal.forwardHintPrefix":`无需密钥 — 代理会转发你的`,"modal.forwardCredentials":`codex login`,"modal.forwardHintSuffix":`凭据到此提供方。`,"modal.localHint":`不会存储 API 密钥。这会为 Codex 添加 Cursor 的公开模型目录,但在审计完成前,实时 Cursor 传输与原生文件/Shell 执行仍保持禁用。`,"modal.getApiKey":`获取 {label} API 密钥`,"modal.apiKey":`API 密钥`,"modal.apiKeyTransport":`API 密钥请求头`,"modal.apiKeyTransportNative":`x-api-key(Anthropic 原生)`,"modal.apiKeyTransportBearer":`Authorization: Bearer`,"modal.apiKeyPlaceholder":`sk-…(或 $ENV_VAR)`,"modal.defaultModelPlaceholder":`例如 gpt-5.5`,"modal.baseUrlPlaceholder":`https://...`,"modal.baseUrlPlaceholderError":`Base URL 包含未解析的 {placeholder},请替换为实际值。`,"modal.baseUrlPlaceholderHint":`请在添加前将 Base URL 中的 {placeholder} 替换为你的实际 Account ID。`,"modal.adding":`正在添加…`,"modal.useOauthLogin":`← 使用 OAuth 登录`,"codexAuth.addIdPlaceholder":`codex-work, codex-alt, team…`,"codexAuth.resetCreditsAria":`{count} 个重置额度`,"claude.pageTitle":`Claude Code`,"claude.workspace.settings":`设置`,"cws.loading":`正在加载组合…`,"cws.loadFailed":`无法加载组合。`,"cws.saveFailed":`无法保存组合。`,"cws.removeFailed":`无法删除组合。`,"cws.saved":`组合已保存。`,"cws.created":`已创建 {model}。`,"cws.removed":`已删除 combo/{id}。`,"cws.renamed":`已将 {from} 重命名为 {to}。`,"cws.add":`添加组合`,"cws.addTitle":`添加组合`,"cws.addSubtitle":`创建跨提供方的虚拟模型,并指定客户端实际请求的模型名称。`,"cws.create":`创建组合`,"cws.railAria":`组合列表`,"cws.searchPlaceholder":`搜索组合或目标…`,"cws.noSearchResults":`没有匹配的组合。`,"cws.group.failover":`故障转移`,"cws.group.roundRobin":`轮询`,"cws.group.other":`其他策略`,"cws.targetCount":`{count} 个目标`,"cws.targetCountOne":`1 个目标`,"cws.overviewTitle":`组合`,"cws.overviewBlurb":`在提供方/模型目标之间按故障转移、轮询、加权随机、最少使用或最早配额重置路由的虚拟模型。`,"cws.count.total":`总计`,"cws.count.failover":`故障转移`,"cws.count.roundRobin":`轮询`,"cws.count.other":`其他`,"cws.howTitle":`工作原理`,"cws.howBody":`在 Codex 中请求组合的公开模型名称;未设置时默认使用 combo/。OpenCodex 仅在可重试的上游错误时切换目标。若没有可用目标,请求会直接失败,不会回退到全局默认提供方。`,"cws.attentionTitle":`需要关注`,"cws.attention.empty":`未配置目标`,"cws.attention.few":`只有一个目标 — 故障转移无处可跳`,"cws.attention.catalogOmitted":`未出现在模型目录中 — 成员能力不完整或不兼容(缺少上下文窗口/元数据,或模态交集为空)。按别名路由仍可用`,"cws.attention.allTargetsExhausted":`所有已启用目标的额度均已用尽`,"cws.emptyTitle":`创建第一个组合`,"cws.empty.createDesc":`命名虚拟模型并串联两个或多个后端。`,"cws.backToAll":`返回全部组合`,"cws.allCombos":`全部组合`,"cws.copyModel":`复制 ID`,"cws.copied":`已复制`,"cws.tabsLabel":`组合详情分区`,"cws.tab.config":`配置`,"cws.tab.about":`关于`,"cws.strategy":`策略`,"cws.strategy.failover":`故障转移`,"cws.strategy.roundRobin":`轮询`,"cws.strategy.random":`随机`,"cws.strategy.leastUsed":`最少使用`,"cws.strategy.resetWindow":`重置窗口`,"cws.strategy.failoverHint":`按顺序尝试目标。若出现可重试错误(限流、故障、订阅门控),则跳到下一个。`,"cws.strategy.roundRobinHint":`按权重确定性地分配流量。将所选目标保留一批成功请求后,再推进到下一个目标。`,"cws.strategy.randomHint":`每个请求按权重比例随机抽取一个可用目标,请求之间不保持粘性。`,"cws.strategy.leastUsedHint":`把每个请求路由到成功次数最少的可用目标。计数随代理重启归零。`,"cws.strategy.resetWindowHint":`优先选择配额窗口最早重置的可用目标。缺少配额数据时回退到配置顺序。`,"cws.field.id":`组合 ID`,"cws.field.idHint":`客户端将请求 {model}`,"cws.field.idInternalHint":`组合的内部 ID,创建后仍可修改。`,"cws.field.idHintEdit":`修改 ID 即重命名组合。客户端将请求 {model}。`,"cws.field.alias":`公开模型名称`,"cws.field.aliasPlaceholder":`deepseek-v4-flash 或 vendor/model`,"cws.field.aliasHint":`可选。可填无前缀裸名称、自定义前缀(如 vendor/model),或留空使用 combo/。`,"cws.field.nativeAlias":`原生 OpenAI 别名`,"cws.field.nativeAliasHint":`允许此组合接管受支持的未限定 OpenAI 原生模型 ID。带账户或提供商限定的 OpenAI 路由仍保持独立。`,"cws.field.displayName":`显示名称`,"cws.field.displayNameHint":`模型选择器中的标签。启用原生 OpenAI 别名时必填。`,"cws.field.stickyLimit":`轮换前的粘性成功次数`,"cws.field.stickyLimitHint":`加权选择器推进前,将所选目标保留这么多次成功请求。`,"cws.field.defaultEffort":`默认推理级别`,"cws.field.defaultEffortNone":`无(使用目标默认)`,"cws.field.defaultEffortHint":`仅在客户端未指定推理级别时使用。选项为所选目标已公布努力级别的交集。`,"cws.capability.imageInputUnavailable":`所有已选目标均支持图片输入后才可用。`,"cws.capability.imageInputHint":`所有目标均支持图片时默认开启;关闭后仅接受文本。`,"cws.capability.imageInput":`图片 / 多模态`,"cws.capability.adaptiveEffort":`自适应推理档位`,"cws.capability.adaptiveEffortHint":`关闭:只要有一个目标不支持推理档位,整个组合的选择器都会消失。开启:这些目标仍可使用,选择器保留其余目标共有的档位。`,"cws.capabilities":`能力`,"cws.field.defaultEffortUnsupported":`该级别不在目标的公共阶梯中 — 请求时会被忽略或就近映射。`,"cws.field.defaultEffortUnsupportedOption":`不在交集中`,"cws.targets":`目标`,"cws.targets.failoverHint":`顺序很重要 — 第一个为主。`,"cws.targets.roundRobinHint":`权重控制确定性的相对选择;顺序用于打破轮换环中的平局。`,"cws.targets.randomHint":`权重控制每次抽取的概率,顺序无关紧要。`,"cws.targets.leastUsedHint":`顺序仅在使用量相同的目标之间打破平局。`,"cws.targets.resetWindowHint":`配额数据缺失或相同时按顺序处理。`,"cws.target.provider":`提供方`,"cws.target.model":`模型`,"cws.target.weight":`权重`,"cws.target.pickProvider":`选择提供方…`,"cws.target.pickProviderFirst":`请先选择提供方…`,"cws.target.pickModel":`选择模型…`,"cws.target.noModels":`该提供方没有模型`,"cws.target.modelPlaceholder":`模型 ID`,"cws.target.add":`添加目标`,"cws.target.drag":`拖动以重新排序`,"cws.target.moveUp":`上移`,"cws.target.moveDown":`下移`,"cws.quota.available":`可用`,"cws.quota.exhausted":`额度已用尽`,"cws.quota.unknown":`额度未知`,"cws.quota.allExhausted":`所有已启用目标的额度均已用尽。请选择其他目标,或等待额度恢复。`,"cws.aboutTitle":`运行时`,"cws.aboutBody":`失败目标会短暂冷却并遵循 Retry-After。无效请求与上下文错误不会切换。每个目标按自身能力调整推理级别;所有目标耗尽时直接失败。日志与用量会保留有序的实际尝试及每次尝试的用量。`,"cws.removeConfirmTitle":`删除 {model}?`,"cws.removeConfirmDesc":`从配置与 Codex 目录移除该虚拟模型,不会删除任何提供方。`,"cws.unsavedTitle":`未保存的更改`,"cws.unsavedDesc":`丢弃对此组合的编辑并继续?`,"cws.keepEditing":`继续编辑`,"cws.err.missingId":`需要组合 ID。`,"cws.err.invalidId":`ID 须以字母或数字开头,仅含字母、数字、点、下划线或连字符(最多 64)。`,"cws.err.duplicateId":`已存在相同 ID 的组合。`,"cws.err.invalidAlias":`别名仅可包含字母、数字、点、下划线或连字符,最多一个“/”分段。`,"cws.err.aliasReservedNamespace":`别名不得使用保留的“combo/”命名空间。`,"cws.err.aliasNativeFamily":`不允许使用 OpenAI 原生家族裸别名(gpt-*、o1-*、o3-*、o4-*、codex-*)。`,"cws.err.unsupportedNativeAlias":`原生别名必须是当前受支持的 OpenAI 裸 model id。`,"cws.err.missingNativeAliasDisplayName":`原生别名必须提供显示名称。`,"cws.err.invalidDisplayName":`显示名称最多 128 个字符,且不能包含控制字符。`,"cws.err.duplicateAlias":`另一个组合已使用该别名。`,"cws.err.noTargets":`至少添加一个目标。`,"cws.err.incompleteTarget":`每个目标都需要提供方和模型。`,"cws.target.disabled":`{name}(已禁用)`,"cws.err.reservedNamespace":`创建组合前,请先重命名名为 combo 的实体提供方。`,"cws.err.providerCollision":`组合 ID 与已配置的提供方名称冲突。`,"cws.err.unknownProvider":`每个目标都必须使用已配置的提供方。`,"cws.err.duplicateTarget":`同一提供方/模型目标只能出现一次。`,"cws.err.invalidStickyLimit":`粘性成功次数必须是 1 到 100 的整数。`,"cws.err.invalidWeight":`每个轮询权重必须是 1 到 10000 的整数。`,"cws.err.noEnabledTarget":`至少一个目标必须使用已启用的提供方。`,"claude.tabsLabel":`Claude 客户端`,"claude.tabCode":`Code`,"claude.tabDesktop":`Desktop`,"claudeDesktop.title":`Claude Desktop`,"claudeDesktop.subtitle":`将每个 Claude 模型系列路由到端口 {port} 上的可用模型。`,"claudeDesktop.importJson":`导入 JSON`,"claudeDesktop.exportJson":`导出 JSON`,"claudeDesktop.loading":`正在加载 Claude Desktop 配置…`,"claudeDesktop.loadFail":`无法加载 Claude Desktop 配置。`,"claudeDesktop.retry":`重试`,"claudeDesktop.saveFailed":`无法保存 Claude Desktop 配置。`,"claudeDesktop.applyFailed":`配置已保存,但无法应用。`,"claudeDesktop.updateFailed":`Claude Desktop 更新失败。`,"claudeDesktop.savedApplied":`配置已保存并应用到 Claude Desktop。`,"claudeDesktop.appliedMarkerUnsaved":`已应用到 Claude Desktop,但应用标记未能保存。在再次应用之前,下方的已保存/已应用状态可能显示不准确。`,"claudeDesktop.savedAppliedAnnounce":`Claude Desktop 配置已保存并应用。`,"claudeDesktop.saved":`配置已保存。`,"claudeDesktop.savedAnnounce":`Claude Desktop 配置已保存。`,"claudeDesktop.exported":`配置已导出为 JSON。`,"claudeDesktop.importExpected":`需要版本 1 的 Claude Desktop 配置。`,"claudeDesktop.importReady":`JSON 已导入。请检查草稿,然后保存并应用。`,"claudeDesktop.importedAnnounce":`配置 JSON 已导入。可检查尚未保存的更改。`,"claudeDesktop.importInvalid":`所选文件不是有效配置。`,"claudeDesktop.importFailed":`导入失败。{error}`,"claudeDesktop.moved":`已将 {route} 移动到 {family}。`,"claudeDesktop.unsaved":`有未保存的更改`,"claudeDesktop.upToDate":`配置已是最新`,"claudeDesktop.saving":`正在保存…`,"claudeDesktop.applying":`正在应用…`,"claudeDesktop.saveApply":`保存并应用`,"claudeDesktop.emptyTitle":`没有可用模型`,"claudeDesktop.emptyHint":`请添加或启用提供商,然后返回分配 Claude Desktop 路由。`,"claudeDesktop.assignmentsLabel":`Claude 模型系列分配`,"claudeDesktop.family.opus":`Opus`,"claudeDesktop.family.fable":`Fable`,"claudeDesktop.family.sonnet":`Sonnet`,"claudeDesktop.family.haiku":`Haiku`,"claudeDesktop.modelCountOne":`{count} 个模型`,"claudeDesktop.modelCountMany":`{count} 个模型`,"claudeDesktop.chooseDefault":`选择默认模型`,"claudeDesktop.temporaryDefault":`临时默认模型`,"claudeDesktop.laneEmpty":`将模型拖到这里,或使用移动控件。`,"claudeDesktop.laneNoMatch":`该系列中没有与搜索匹配的模型。`,"nav.grok":`Grok`,"grok.title":`Grok Build`,"grok.subtitle":`opencodex 已注册到你的 Grok 配置中的模型。`,"grok.loading":`正在加载 Grok 状态…`,"grok.loadFail":`无法读取 Grok 配置。`,"grok.notConfiguredTitle":`Grok Build 尚未接入`,"grok.notConfiguredHint":`安装 Grok 后重启代理,opencodex 会把托管块写入:`,"grok.endpoint":`端点`,"grok.colModel":`模型`,"grok.colAlias":`Grok 别名`,"grok.colContext":`上下文`,"grok.groupNative":`原生模型`,"grok.groupRouted":`路由模型`,"grok.enabledCount":`已注册 {on}/{total}`,"grok.saved":`选择已保存。`,"grok.savedApplied":`选择已保存并写入 Grok 配置。`,"grok.saveFailed":`无法保存 Grok 选择。`,"grok.applyFailed":`选择已保存,但无法更新 Grok 配置。`,"grok.applySkipped":`选择已保存,Grok 配置未更改。`,"grok.saveApply":`保存并应用`,"grok.saving":`保存中…`,"grok.applying":`应用中…`,"grok.unsaved":`未保存的更改`,"grok.upToDate":`选择已是最新`,"grok.toggleModel":`将 {id} 注册到 Grok`,"claudeDesktop.available":`可用`,"claudeDesktop.defaultBadge":`默认`,"claudeDesktop.supports1m":`1M`,"claudeDesktop.unavailable":`不可用`,"claudeDesktop.contextM":`{n}M 上下文`,"claudeDesktop.contextK":`{n}k 上下文`,"claudeDesktop.contextUnknown":`上下文未知`,"claudeDesktop.alias":`别名`,"claudeDesktop.useAsDefault":`设为 {family} 默认模型`,"claudeDesktop.moveTo":`移动到`,"claudeDesktop.move":`移动`,"claudeDesktop.status.applied":`已应用到 Desktop`,"claudeDesktop.status.stale":`配置已更改 — 需重新应用`,"claudeDesktop.status.notApplied":`未应用`,"claudeDesktop.status.notActiveProfile":`Desktop 正在使用其他配置 — 请重新应用`,"claudeDesktop.status.disabled":`Claude Desktop 集成已关闭。开启后请完全退出并重新打开 Desktop。`,"claudeDesktop.enableApply":`开启并应用`,"claudeDesktop.health.lastRequest":`最后请求`,"claudeDesktop.health.stats":`{count} 请求 / {errors} 错误`,"claudeDesktop.effort.supported":`effort`,"claudeDesktop.effort.displayOnly":`effort (仅显示)`,"dash.injectionManage":`打开设置`,"sub.settings":`设置`,"sub.sections":`子代理分区`,"sub.delegation.model":`优先调用的模型`,"sub.delegation.modelHint":`Codex 分派工作时最先调用的模型。上面的推荐是可调用的名单,这里选的是其中第一顺位。`,"dash.syncModelsHint":`按已连接的提供商重写 Codex 的模型目录。`,"dash.syncRun":`立即同步`,"lab.title":`Compatibility Lab`,"lab.subtitle":`Read-only compatibility verdict matrix from lab projection evidence.`,"lab.loadFailed":`Could not load compatibility lab data`,"lab.projectionUnavailable":`Lab projection is not available. Run conformance or live probes first.`,"lab.projectionIncompatible":`Lab projection schema is incompatible. Rebuild the projection.`,"lab.statusTitle":`Projection status`,"lab.matrixTitle":`Compatibility matrix`,"lab.verdictsTitle":`Verdict records`,"lab.filter.layer":`Evidence layer`,"lab.filter.verdict":`Verdict`,"lab.filter.subject":`Subject ID`,"lab.filter.all":`All`,"lab.col.subject":`Subject`,"lab.col.layer":`Layer`,"lab.col.suite":`Suite`,"lab.col.verdict":`Verdict`,"lab.col.asOf":`As of`,"lab.col.protocol":`Protocol conformance`,"lab.col.live":`Live route compatibility`,"lab.col.task":`Task effectiveness`,"lab.empty":`No compatibility verdicts in the projection yet.`,"lab.subjectKind":`Kind`,"lab.observationCount":`Observations`,"lab.eventCount":`Events`,"lab.verdictCount":`Verdicts`,"lab.subjectCount":`Subjects`,"lab.builtAt":`Built`,"lab.loading":`Loading compatibility evidence…`,"lab.loadMore":`Load more`,"lab.detailTitle":`Verdict detail`,"lab.detailClose":`Close`,"lab.detailSubject":`Subject`,"lab.detailObservations":`Observations`,"lab.detailEvents":`Contributing events`,"lab.detailArtifacts":`Artifact metadata`,"lab.production.title":`观测到的生产流量`,"lab.production.notVerification":`不是实验室验证`,"lab.production.attempts":`尝试`,"lab.production.successes":`成功`,"lab.production.routeErrors":`路由错误`,"lab.production.lastObserved":`最近观测`,"lab.detailLoadFailed":`Could not load verdict detail`,"lab.refresh":`Refresh`,"lab.verdict.UNKNOWN":`Unknown`,"lab.verdict.CLAIMED":`Claimed`,"lab.verdict.PROBED":`Probed`,"lab.verdict.VERIFIED":`Verified`,"lab.verdict.DEGRADED":`Degraded`,"lab.verdict.BLOCKED":`Blocked`,"lab.verdict.UNSUPPORTED":`Unsupported`,"lab.layer.protocol_conformance":`Protocol conformance`,"lab.layer.live_route_compatibility":`Live route compatibility`,"lab.layer.task_effectiveness":`Task effectiveness`,"dash.visionAdvanced":`高级设置`,"dash.visionMaxDescriptions":`每回合最大描述次数`,"dash.visionMaxDescriptionsInvalid":`请输入正整数。`,"dash.visionTimeout":`超时`,"dash.visionTimeoutInvalid":`请输入 {min} 到 {max} 毫秒之间的整数。`,"dash.visionAdvancedPopover":`高级视觉设置`,"models.newPolicyGlobal":`新模型默认停用`,"models.newPolicyProvider":`新模型策略`,"models.newPolicy_inherit":`继承`,"models.newPolicy_off":`关闭`,"models.newPolicy_on":`开启`,"models.newBadge":`新增`,"models.newCount":`{count} 个新增,已关闭`,"models.aliases":`别名`,"models.aliasesTable":`别名表`,"models.aliasPrompt":`服务商别名(留空即清除)`,"models.modelAliasPrompt":`模型别名(留空即清除)`,"models.aliasSaved":`别名已保存`,"models.aliasConflict":`该别名与现有名称冲突`,"models.editProviderAlias":`编辑服务商别名`,"models.editModelAlias":`编辑模型别名`,"models.useDefaultAliases":`使用默认别名`,"models.useDefaultAliasesGlobal":`全局使用默认别名`,"models.aliasAuto":`自动`,"models.aliasUser":`用户`,"models.aliasStale":`过期`,"connection.discovering":`Discovering local and shared targets…`,"connection.machineUnavailable":`The local machine plane is unavailable. Shared requests were not redirected locally.`,"connection.disconnect":`Disconnect from hub`,"connection.disconnectConfirm":`Disconnect this machine from the hub and restart it in standalone mode?`,"connection.pairing.title":`Connect this dashboard to the hub`,"connection.pairing.body":`Paste the one-time pairing code created on the hub.`,"connection.pairing.relayWarning":`This code is exchanged through the fixed hub relay. The relay cannot be redirected to another host.`,"connection.pairing.code":`One-time pairing code`,"connection.pairing.submit":`Connect`,"connection.pairing.submitting":`Connecting…`,"connection.pairing.error":`The pairing code was refused or expired. The code was left in place so you can check it.`,"connection.machine.title":`This machine`,"connection.machine.shimHealthy":`Codex shim is healthy.`,"connection.machine.shimNeedsAttention":`Codex shim needs attention.`,"connection.machine.repairShim":`Repair shim`,"connection.machine.removeShim":`Remove shim`,"connection.clients.title":`Connected clients`,"connection.clients.none":`No client status available`,"connection.clients.sync":`Sync now`,"connection.clients.syncing":`Syncing…`,"connection.sessionLogout":`退出远程会话`,"connection.sessionLoggingOut":`正在退出远程会话…`,"connection.sessionLogoutFailed":`无法退出远程会话,当前会话已保留。`,"usage.source.connected":`Source: hub usage`,"usage.source.local":`Source: local usage.jsonl`,"usage.scope.label":`Usage scope`,"usage.scope.machine":`This machine`,"usage.scope.hub":`Hub-wide`,"usage.hubOffline":`Hub usage is unavailable. Local usage was not substituted.`,"integrations.tab.cursor":`Cursor`,"integrations.detail.cursorSeen":`Cursor 最近调用了此代理`,"integrations.detail.cursorNeverSeen":`已安装 Private Inference;尚未收到请求`,"integrations.detail.cursorAbsent":`未找到 Cursor Private Inference`,"integrations.cursor.title":`Cursor`,"integrations.cursor.intro":`Cursor Private Inference 会在本地运行其智能体,并通过环回地址与 opencodex 通信。普通版 Cursor 无法如此工作:其后端会调用自定义端点,因此需要一个公开的 HTTPS URL。本页面不会向 Cursor 写入任何内容;请自行将以下值粘贴到 Cursor 中。`,"integrations.cursor.loading":`正在读取 Cursor 状态…`,"integrations.cursor.unavailable":`无法从代理读取 Cursor 状态。`,"integrations.cursor.detection":`已安装版本`,"integrations.cursor.privateInference":`Cursor Private Inference`,"integrations.cursor.regular":`Cursor(普通版)`,"integrations.cursor.detected":`已检测到`,"integrations.cursor.notFound":`未找到`,"integrations.cursor.regularOnly":`仅找到普通版 Cursor。它会通过 Cursor 的服务器路由自定义端点,因此如果没有公网隧道,便无法访问环回代理。有关 Private Inference 版本的信息,请参阅指南。`,"integrations.cursor.nothingFound":`在常用位置未找到 Cursor 安装。如果安装在其他位置,以下值仍然适用。`,"integrations.cursor.gateway":`网关参数`,"integrations.cursor.gatewayHint":`在 Cursor Private Inference 中打开 Settings > Models > Gateway,粘贴以下两个值,然后点击 Refresh model list。`,"integrations.cursor.baseUrl":`Base URL`,"integrations.cursor.apiKey":`API Key`,"integrations.cursor.apiKeyCredential":`你的任一 opencodex API 密钥(此绑定需要凭据)`,"integrations.cursor.copy":`复制`,"integrations.cursor.copied":`已复制`,"integrations.cursor.connection":`连接`,"integrations.cursor.seen":`Cursor 最近一次请求:{time}({ua})`,"integrations.cursor.neverSeen":`代理启动后尚未收到 Cursor 的请求。保存网关设置后,请在 Cursor 中点击 Refresh model list。`,"integrations.cursor.models":`Cursor 将显示的内容`,"integrations.cursor.modelsHint":`Cursor 会从自身的模型表中选择推理层级,因此 opencodex 只能进行预测。“上下文”列会列出默认窗口和可选窗口(Cursor 的 Max Mode)。`,"integrations.cursor.ladderFromBundle":`推理档位读取自已安装的 Cursor Private Inference {version} 包。档位由 Cursor 决定,opencodex 只是展示它的表。`,"integrations.cursor.ladderFromStatic":`推理档位是 Cursor 3.18.25 的静态镜像(未找到可读取的 Private Inference 包)。上下文列显示默认窗口和可选窗口。`,"integrations.cursor.unknownVersion":`未知版本`,"integrations.cursor.noControl":`—`,"integrations.cursor.singleWindow":`单一窗口`,"integrations.cursor.noControlTitle":`此 id 不在 Cursor 内置的 effort 表中,因此 Cursor 不显示推理控件。`,"integrations.cursor.effortRowsOne":`已发布 1 个 effort 行`,"integrations.cursor.effortRowsMany":`已发布 {n} 个 effort 行`,"integrations.cursor.effortRowsOff":`无 effort 行`,"integrations.cursor.tableLessHint":`标为 — 的行在 Cursor 中没有推理控件。开启 cursorEffortRows 可为每个 effort 发布一个选择器条目(id--effort),或在提供商上设置 modelDefaultReasoningEfforts 作为固定默认值。`,"integrations.cursor.colModel":`模型`,"integrations.cursor.colReasoning":`推理`,"integrations.cursor.colContext":`上下文`,"integrations.cursor.guide":`打开 Cursor Private Inference 指南`},Ue={"nav.dashboard":`儀表板`,"nav.startup":`啟動安全`,"nav.providers":`供應商`,"nav.models":`模型`,"nav.combos":`組合`,"nav.subagents":`子代理`,"nav.logs":`日誌與除錯`,"nav.usage":`用量`,"common.github":`GitHub`,"common.save":`儲存`,"common.saving":`儲存中…`,"common.cancel":`取消`,"common.discard":`捨棄`,"common.delete":`刪除`,"common.close":`關閉`,"common.ok":`確定`,"common.remove":`移除`,"common.loading":`載入中…`,"common.retry":`重試`,"app.logoAria":`opencodex 徽標`,"app.claudeOn":`Claude 開`,"app.claudeOff":`Claude 關`,"theme.label":`主題`,"theme.light":`淺色`,"theme.dark":`深色`,"theme.system":`跟隨系統`,"lang.label":`語言`,"errorBoundary.title":`頁面載入失敗`,"errorBoundary.message":`此部分在渲染時發生錯誤。請重新載入後再試。`,"errorBoundary.details":`錯誤`,"errorBoundary.reload":`重新載入`,"startup.title":`啟動安全`,"startup.subtitle":`檢查重新啟動後 Codex 是否仍能連線 opencodex,避免本機代理路由陷入重複重連。`,"startup.refresh":`重新整理`,"startup.loading":`正在檢查啟動保護…`,"startup.error":`無法讀取啟動保護狀態。`,"startup.staleData":`最新啟動檢查失敗。以下資料已過期,不能視為已受保護的證明。`,"startup.status.native":`原生路由`,"startup.status.protected":`已保護重新啟動`,"startup.status.atRisk":`需要處理`,"startup.summary.native":`Codex 不依賴本機代理`,"startup.summary.protected":`重新啟動後 opencodex 會自動可用`,"startup.summary.atRisk":`重新啟動後 Codex 可能無法存取模型`,"startup.riskDetail":`Codex 已指向本機代理,但沒有持久服務或正常的 launcher shim 將其重新啟動。`,"startup.riskDetailCustomLocal":`Codex 指向自訂本地閘道器。opencodex 無法管理或驗證該閘道器的重新啟動生命週期。`,"startup.riskDetailWindowsShim":`Launcher shim 僅保護受支援的 CLI 指令碼;Windows 上的 Codex Desktop 和直接 codex.exe 啟動可以繞過它。`,"startup.safeDetail":`當前路由與啟動機制一致。重新啟動後無需手動執行 ocx start。`,"startup.routing":`Codex 路由`,"startup.routing.proxy":`本機代理`,"startup.routing.native":`OpenAI 原生`,"startup.routing.customLocal":`自訂本地閘道器`,"startup.routing.customRemote":`自訂遠端閘道器`,"startup.routing.unknown":`未知或無效的路由`,"startup.restartProtection":`重新啟動保護`,"startup.preference":`按需啟動`,"startup.enabled":`已啟用`,"startup.disabled":`已停用`,"startup.protection.service":`背景服務`,"startup.protection.shim":`Launcher shim`,"startup.protection.none":`未安裝`,"startup.details":`保護詳細資料`,"startup.service":`背景服務`,"startup.serviceHint":`登入時啟動,並在代理崩潰後重新啟動。`,"startup.installed":`已安裝`,"startup.notInstalled":`未安裝`,"startup.unsupported":`不支援`,"startup.shim":`Codex launcher shim`,"startup.shimHint":`支援的 Codex 指令碼啟動器執行時執行 ocx ensure。`,"startup.healthy":`正常`,"startup.cliOnly":`僅 CLI`,"startup.stale":`已失效`,"startup.viable":`可用`,"startup.unhealthy":`已安裝但異常`,"startup.conflict":`服務衝突`,"startup.installedDisabled":`已安裝但停用`,"startup.install":`安裝`,"startup.installing":`正在安裝…`,"startup.serviceInstalled":`背景服務安裝成功。`,"startup.shimInstalled":`Codex 啟動器 shim 安裝成功。`,"startup.installFailed":`安裝失敗:`,"startup.tray.title":`Windows 系統托盤`,"startup.tray.hint":`登入時啟動托盤圖示,一鍵控制代理啟動、停止、重新啟動、面板和狀態。`,"startup.tray.login":`Windows 登入時啟動托盤`,"startup.tray.notProtection":`托盤只是控制器,並非重新啟動保護。無人值守恢復仍需要正常的背景服務。`,"startup.tray.running":`執行中`,"startup.tray.stopped":`已安裝,未顯示`,"startup.tray.stale":`需要修復`,"startup.tray.notInstalled":`未安裝`,"startup.tray.loading":`正在檢查…`,"startup.tray.unavailable":`狀態不可用`,"startup.tray.install":`安裝並顯示托盤`,"startup.tray.start":`顯示托盤圖示`,"startup.tray.stop":`退出托盤圖示`,"startup.tray.uninstall":`移除登入托盤`,"startup.tray.error":`Windows 托盤操作失敗。請執行 ocx tray status 檢視詳細資料。`,"startup.recovery":`修復選項`,"startup.recoveryHint":`使用上方的一鍵安裝,或複製命令進行手動修復。Codex Desktop 和 Windows 可執行檔案建議使用背景服務。`,"startup.command.service":`推薦:持久背景服務`,"startup.command.shim":`備選:CLI launcher shim`,"startup.command.native":`安全恢復:還原 Codex 原生路由`,"startup.copy":`複製`,"startup.copied":`已複製`,"startup.recommended":`推薦修復:{cmd}`,"startup.navRisk":`啟動保護需要處理`,"startup.codexRuntime.clampHidden":`部分推理強度選項已隱藏,因為 OpenCodex 正在使用 Codex {version}。`,"startup.codexRuntime.clampHiddenWithEfforts":`部分推理強度選項已隱藏,因為 OpenCodex 正在使用 Codex {version}(已移除:{efforts})。`,"startup.codexRuntime.olderBinary":`OpenCodex 正在使用較舊的 Codex 二進位制檔案({version})。檢測到可用的較新安裝。`,"dash.subtitle":`本地 opencodex 代理、其供應商以及路由到 Codex 的模型的即時狀態。`,"dash.workspace.overview":`總覽`,"dash.workspace.sections":`板塊`,"dash.status":`狀態`,"dash.online":`線上`,"dash.offline":`離線`,"dash.version":`版本`,"dash.uptime":`執行時間`,"dash.providers":`供應商`,"dash.tokens30d":`Token (30 天)`,"dash.coverage":`覆蓋率 {pct}`,"dash.mem.title":`記憶體可觀測性`,"dash.mem.hint":`只讀執行時診斷。觀測記憶體為 max(RSS, external, ArrayBuffers),避免 Windows working set trimming 隱藏已提交的保留記憶體。`,"dash.mem.rss":`常駐記憶體 (RSS)`,"dash.mem.jsHeap":`JS 堆(已用 / 總計)`,"dash.mem.jscHeap":`JSC 堆`,"dash.mem.external":`External`,"dash.mem.arrayBuffers":`ArrayBuffers`,"dash.mem.observed":`觀測值`,"dash.mem.runtime":`執行時計數器`,"dash.mem.growth":`每小時觀測變化`,"dash.mem.perHour":`/小時`,"dash.mem.store":`延續儲存`,"dash.mem.storeHint":`代理 previous_response_id 快取。堆上升時總位元組數增加,說明是對話保留而非執行時分配器。`,"dash.mem.storeEntries":`條目`,"dash.mem.storeTotal":`總計`,"dash.mem.storeLargest":`最大`,"dash.mem.storeOldest":`最舊`,"dash.mem.threshold":`告警閾值`,"dash.mem.lastWarn":`上次告警`,"dash.mem.never":`從不`,"dash.mem.details":`詳細資料`,"dash.mem.unavailable":`記憶體診斷不可用(舊版代理)。`,"dash.mem.inFlight":`進行中的請求`,"dash.mem.restart":`排空並重啟`,"dash.mem.restartConfirm":`等待 {count} 個進行中的請求結束後再重啟(最多 {seconds} 秒;超時將中斷剩餘請求)。`,"dash.mem.draining":`正在等待 {count} 個請求完成… 完成後重啟`,"dash.mem.reconnecting":`代理正在重啟… 等待重新連線`,"dash.mem.restartFailed":`排空並重啟失敗。請確認代理正在執行。`,"dash.mem.restartNoSupervisor":`未檢測到重啟保護。重啟後代理可能不會自動恢復,需手動啟動。`,"dash.activeProviders":`活躍供應商`,"dash.noProviders":`尚未配置供應商。請執行 {cmd}。`,"dash.col.name":`名稱`,"dash.col.adapter":`介面卡`,"dash.col.baseUrl":`Base URL`,"dash.col.model":`模型`,"dash.modelsNoResults":`沒有符合搜尋的模型。`,"dash.availableModels":`可用模型`,"dash.noModels":`未找到模型。請檢查供應商 API 金鑰。`,"dash.cannotConnect":`無法連線到代理。它在執行嗎?`,"dash.runStart":`執行 {cmd} 以啟動代理。`,"dash.stop":`停止代理`,"dash.stopConfirm":`停止代理並恢復原生 Codex 配置?`,"dash.stopFailed":`無法停止代理 (HTTP {status})。`,"dash.maSwitchFailed":`模式切換失敗 (HTTP {status})。`,"dash.maNetworkError":`網路錯誤 — 代理是否正在執行?`,"dash.stopping":`正在停止…`,"dash.actions":`代理`,"dash.codexRestart":`重新載入 Codex 模型清單`,"dash.codexRestarting":`正在停止…`,"dash.codexRestartConfirm":`停止 Codex app-server 以重新讀取模型清單?進行中的 Codex 工作會中斷,且 Codex 不會自動重啟,請稍後自行重新開啟。`,"dash.codexRestartDone":`已停止 {count} 個 Codex app-server。重新開啟 Codex 即可載入最新的模型清單。`,"dash.codexRestartNothing":`沒有執行中的 Codex app-server。下次啟動會讀取最新的模型清單。`,"dash.codexRestartUnknown":`無法列舉行程,因此沒有停止任何行程。`,"dash.codexRestartPartial":`有 {count} 個 app-server 未結束。若模型清單仍然過舊,請手動停止。`,"dash.codexRestartFailed":`無法重新載入 Codex 模型清單 (HTTP {status})。`,"dash.codexRestartUnreachable":`無法連線到代理。`,"dash.codexRestartMalformed":`代理回傳了非預期的回應。`,"dash.codexRestartTimeout":`代理未在時限內回應,可能仍在停止 app-server。`,"models.staleBanner":`Codex 顯示的模型清單比目前的目錄舊。重新啟動 Codex 即可重新讀取。`,"dash.codexAutoStart":`隨 Codex 啟動 opencodex`,"dash.codexAutoStartHint":`允許已安裝的 launcher shim 執行 ocx ensure。此設定不會安裝重新啟動保護;請在啟動安全中檢查實際狀態。`,"dash.searchModel":`搜尋附屬模型`,"dash.searchModelHint":`用於非 OpenAI 路由模型的 web_search 的模型。需要 ChatGPT 登入。`,"dash.searchReasoning":`搜尋推理強度`,"dash.visionModel":`視覺附屬模型`,"dash.visionModelHint":`為純文字路由模型描述圖像的模型。需要 ChatGPT 登入。`,"dash.webSearchSidecar":`網頁搜尋附屬服務`,"dash.webSearchSidecarHint":`選擇路由模型進行網頁搜尋時使用的後端和模型。`,"dash.webSearchStream":`即時串流輸出回答`,"dash.webSearchStreamHint":`即時串流輸出開頭的文字和推理,直到模型決定呼叫工具;其餘部分為攔截搜尋而保持緩衝。搜尋前的文字可能會部分重複。`,"dash.visionSidecar":`視覺附屬服務`,"dash.visionSidecarHint":`選擇純文字路由模型描述圖像時使用的後端和模型。`,"dash.visionOff":`關閉`,"dash.shadowCallIntercept":`影子呼叫攔截`,"dash.shadowCallInterceptHint":`攔截 Codex 應用的背景 helper 呼叫({models})以生成標題與提交訊息,並將它們重定向到您選擇的模型。`,"dash.shadowCallWarning":`⚠ 啟用後,{models} 的所有請求將被替換為所選模型。`,"dash.shadowCallOriginal":`原始`,"dash.shadowCallModel":`替代模型`,"dash.shadowCallTooltip":`Codex 應用會為執行緒標題生成、提交訊息生成與技能編排發出背景 helper 呼叫。helper 模型因客戶端版本而異,因此 opencodex 攔截此集合中的每個模型:{models}。啟用此選項可將這些呼叫重定向到您選擇的模型。`,"models.shadowCallIntercept":`影子呼叫攔截`,"models.shadowCallInterceptHint":`攔截 Codex 應用的背景 helper 呼叫({models})以生成標題與提交訊息,並將它們重定向到您選擇的模型。`,"dash.sidecarBackend":`後端`,"dash.sidecarModel":`模型`,"dash.backendAuto":`自動`,"dash.backendOpenAI":`OpenAI`,"dash.backendAnthropic":`Anthropic`,"dash.sidecarSaved":`附屬設定已儲存。將在下一個請求時生效。`,"dash.sidecarSaveFailed":`儲存附屬設定失敗。`,"dash.injectionLabel":`子代理委託`,"dash.injectionHint":`選擇供下方兩個控制元件共用的模型和可選推理強度。`,"dash.syncCodexSubagentDefaults":`用作原生 Codex 子代理預設值`,"dash.syncCodexSubagentDefaultsHint":`預設關閉。當 OpenCodex 管理 Codex 路由時,同步或重啟會將所選模型和推理強度應用為新 Codex 任務的原生 Codex [agents] 預設值。此設定本身不會觸發委託;現有的使用者自有 [agents] 預設值會保留而不會被覆蓋。`,"dash.multiAgentGuidance":`OpenCodex 多代理指引`,"dash.multiAgentGuidanceHint":`新增由 OpenCodex 編寫的委託指引。它與上方的原生 Codex 預設值相互獨立,不會更改 v1/v2 介面、子代理清單、路由或 effort 上限。`,"dash.injectionNone":`無`,"dash.injectionEffortLabel":`推理強度`,"dash.injectionEffortNone":`模型預設`,"dash.effortCapLabel":`V2 ultra 推理強度限制`,"dash.subagentEffortCapLabel":`V2 子代理推理強度限制`,"dash.effortCapHelp":`限制 V2 ultra 模式輪次的推理強度。設定後,來自 ultra 模式的 max 請求將被限制到所選級別。子代理限制僅適用於衍生的子代理。只會降低強度,不會提高。如果模型不支援所選級別,將自動降至最近的支援級別。`,"dash.effortCapNone":`無上限`,"dash.maintenance":`維護`,"dash.maintenanceHint":`重新整理 Codex 模型目錄,或安裝新的 opencodex 版本。`,"dash.syncModels":`同步模型`,"dash.syncing":`同步中…`,"dash.syncOk":`同步完成。已追加 {count} 個模型。`,"dash.syncStaleHint":`如果 Codex 仍顯示舊列表,請重啟長期執行的 app-server({cmd})。`,"dash.syncFailed":`同步失敗:{error}`,"dash.projectConfigTitle":`專案 Codex 配置繞過了 OpenCodex`,"dash.projectConfigHint":`這些儲存庫級設定會覆蓋 OpenCodex 代理(例如直接走 OpenCode Go)。請移除它們,以便該專案使用 ~/.codex/config.toml 的代理路由。`,"dash.checkUpdate":`檢查更新`,"dash.updateTitle":`更新 opencodex`,"dash.updateDesc":`檢查所選 npm 頻道的最新版本,然後選擇安裝後是否重新啟動代理。`,"dash.updateChannel":`頻道`,"dash.updateChecking":`正在檢查更新…`,"dash.updateInstalled":`已安裝`,"dash.updateLatest":`最新`,"dash.updateAvailable":`有可用更新`,"dash.updateCurrent":`已是最新`,"dash.updateCommand":`命令`,"dash.updateSource":`當前是原始碼檢出。請在終端執行顯示的命令進行更新。`,"dash.updateUnavailable":`無法從 npm 讀取最新版本。請稍後重試。`,"dash.updateRetry":`重試`,"dash.updateRecheck":`重新檢查`,"dash.updateCannotAuto":`無法一鍵更新({reason})。`,"dash.updateReason.source_checkout":`原始碼檢出`,"dash.updateReason.latest_unavailable":`無法連線 npm 登入檔`,"dash.updateReason.already_latest":`已是最新版本`,"dash.updateReason.unknown":`無法更新`,"dash.updateRestart":`更新後重新啟動`,"dash.updateRestartHint":`推薦開啟。代理重新啟動前,當前 GUI 仍執行舊程式碼。`,"dash.runUpdate":`更新`,"dash.updateReconnecting":`正在等待重新啟動後的代理…`,"dash.updateStatus.running":`正在更新 opencodex。`,"dash.updateStatus.restarting":`更新已安裝。正在重新啟動代理。`,"dash.updateStatus.succeeded":`更新完成。`,"dash.updateStatus.failed":`更新失敗。`,"prov.subtitle":`配置 opencodex 路由到 Codex 的上游供應商。使用帳號登入、新增供應商,或編輯原始配置。`,"prov.add":`新增供應商`,"prov.editJson":`編輯 JSON`,"prov.accountLogin":`帳號登入`,"prov.noOauth":`沒有可用的 OAuth 供應商。`,"prov.loggedIn":`已登入`,"prov.notLoggedIn":`未登入`,"prov.logout":`登出`,"prov.login":`登入`,"prov.loginWith":`使用 {provider} 登入`,"prov.waitingBrowser":`等待瀏覽器…`,"prov.didntOpen":`沒有開啟?點選這裡`,"prov.copyLink":`複製連結`,"prov.dontOpenBrowser":`不要在執行代理的機器上開啟瀏覽器`,"prov.dontOpenBrowserHint":`適用於使用其他瀏覽器設定檔登入,或儀表板與代理不在同一台機器上。`,"prov.linkCopied":`已複製`,"prov.linkCopyUnavailable":`剪貼簿不可用`,"prov.deviceCode":`裝置驗證碼`,"prov.copyCode":`複製驗證碼`,"prov.codeCopied":`驗證碼已複製`,"prov.editAlias":`編輯別名`,"prov.aliasPrompt":`顯示名稱(留空以清除)`,"prov.aliasSaved":`別名已儲存`,"prov.aliasSaveFailed":`無法儲存別名`,"prov.accountId":`ID`,"prov.pasteRedirect":`貼上重定向 URL 或授權碼`,"prov.pasteRedirectHint":`如果瀏覽器顯示 localhost 錯誤,請複製位址列中的完整 URL 並貼上到此處(或貼上授權碼)。`,"prov.pasteSubmit":`提交`,"prov.pasteSubmitting":`提交中…`,"prov.pasteOk":`已提交授權碼 — 正在完成登入…`,"prov.pasteFail":`無法提交授權碼:{error}`,"prov.port":`連接埠`,"prov.default":`預設`,"prov.loadingConfig":`載入中…`,"prov.saved":`已儲存!重新啟動代理以生效。`,"prov.loadConfigFail":`載入配置失敗`,"prov.invalidJson":`無效的 JSON`,"prov.removedDefault":`已移除「{name}」。預設供應商現在是「{defaultProvider}」。`,"prov.removeLastProvider":`沒有其他啟用的供應商可接手預設值時,無法移除此供應商。`,"prov.removeHasDependentCombos":`請先移除或更新這些相依的組合路由:{combos}。`,"prov.setDefault":`設為預設`,"prov.setDefaultSuccess":`「{name}」現在是預設供應商。`,"prov.setDefaultFail":`無法將「{name}」設為預設供應商。`,"prov.defaultDisabled":`設為預設前請先啟用此供應商。`,"prov.updateFail":`無法更新此供應商。`,"prov.networkError":`網路錯誤。請確認代理正在執行後再試一次。`,"prov.saveFailed":`儲存失敗`,"prov.loginFailStart":`{provider} 登入啟動失敗`,"prov.loginError":`{provider} 登入錯誤:{error}`,"prov.loginRequestFail":`{provider} 登入請求失敗`,"prov.loginCancelled":`{provider} 登入已取消`,"prov.loginTimeout":`{provider} 登入逾時 — 瀏覽器已關閉或未完成。請重試。`,"prov.loginOk":`已登入到 {provider}。執行 {cmd}(或即時生效)以列出其模型。`,"oauthTos.highTitle":`{provider}:訂閱 OAuth 風險`,"oauthTos.elevatedTitle":`{provider}:非官方 OAuth 橋接`,"oauthTos.anthropicBody":`透過 OpenCodex 等第三方代理直接複用 Claude 訂閱 OAuth 權杖,並非 Anthropic 支援的整合方式,可能導致存取受限。可使用 Claude 訂閱的受支援 Agent SDK 整合屬於另一種方式。`,"oauthTos.highBody":`OpenCodex 透過第三方 OAuth 路徑連線 {provider}。如果該用法不受支援,存取可能會被限制或暫停。`,"oauthTos.elevatedBody":`OpenCodex 透過非官方 OAuth 路徑連線 {provider}。請儘量使用官方客戶端;異常或自動化流量可能被視為濫用,存取可能會被限制或暫停。`,"oauthTos.saferPath":`更安全的做法:改為在 OpenCodex 中配置 API 金鑰。`,"oauthTos.acknowledge":`我瞭解風險,仍要繼續使用 OAuth。`,"oauthTos.continue":`繼續使用 OAuth`,"prov.logoutOk":`已登出 {provider}。`,"prov.logoutFail":`無法登出 {provider}。帳號狀態保持不變。`,"prov.removed":`已移除 "{name}"。`,"prov.removeFail":`移除 "{name}" 失敗。`,"prov.added":`已新增 "{name}"。現已生效 — 執行 {cmd}(或重新啟動)以在 Codex 選擇器中列出其模型。`,"prov.removeConfirm":`移除供應商 "{name}"?其模型將從 Codex 選擇器中消失。`,"prov.hasApiKey":`已配置 API 金鑰`,"prov.hasHeaders":`已配置自訂請求標頭`,"prov.accounts":`帳號({n})`,"prov.accountsAria":`展開/收起 {name} 帳號`,"prov.accountActive":`使用中`,"prov.accountReauth":`需重新登入`,"prov.reauthenticate":`重新認證`,"prov.reauthAccountMissing":`登入後未找到所選帳號`,"prov.reauthIdentityMismatch":`登入帳號與所選帳號不符合`,"prov.accountAdd":`新增帳號`,"prov.accountNoLabel":`帳號 {id}`,"prov.accountSwitchTitle":`使用此帳號`,"prov.accountSwitched":`已切換到 {email}。`,"prov.accountSwitchFail":`切換帳號失敗`,"prov.accountRemoved":`已移除 {email}。`,"prov.accountRemoveFail":`無法移除 {email}。帳號保持不變。`,"prov.accountRemoveAria":`移除 {email}`,"prov.accountRemoveConfirm":`移除帳號 {email}?其登入將從此代理中刪除。`,"prov.keyAdd":`新增 API 金鑰`,"prov.keyAdded":`已為 {name} 新增 API 金鑰。`,"prov.keyAddFail":`新增 API 金鑰失敗`,"prov.keyPlaceholder":`貼上 API 金鑰`,"prov.keySwitchTitle":`使用此金鑰`,"prov.keySwitched":`已切換到金鑰 {key}。`,"prov.keySwitchFail":`切換金鑰失敗`,"prov.keyRemoved":`已移除金鑰 {key}。`,"prov.keyRemoveAria":`移除金鑰 {key}`,"prov.keyRemoveConfirm":`移除 API 金鑰 {key}?它將從此代理的配置中刪除。`,"prov.activeBadge":`已啟用`,"prov.disabledBadge":`已停用`,"prov.defaultBadge":`預設`,"prov.enable":`啟用`,"prov.disable":`停用`,"prov.enabled":`已啟用 "{name}"。其模型可再次出現在 Codex 中。`,"prov.disabled":`已停用 "{name}"。設定會保留,但模型會被隱藏。`,"prov.enableFail":`啟用 "{name}" 失敗。`,"prov.disableFail":`停用 "{name}" 失敗。`,"prov.enableAria":`啟用供應商 {name}`,"prov.disableAria":`停用供應商 {name}`,"prov.defaultCannotDisable":`預設供應商不能被停用`,"prov.openaiAccountMode":`Codex 帳號模式`,"prov.openaiModePool":`帳號池`,"prov.openaiModeDirect":`直連`,"prov.openaiPoolDesc":`預設模式。根據會話關聯、額度、冷卻時間和容錯移轉,在主登入與已新增帳號之間輪換。`,"prov.openaiDirectDesc":`僅使用當前主 Codex 登入。不會讀取或輪換已儲存的帳號池帳號。`,"prov.openaiModeSaved":`OpenAI 帳號模式已更改為 {mode}。`,"prov.openaiModeSaveFailed":`無法更改 OpenAI 帳號模式。`,"prov.openaiApiDesc":`僅使用 OpenAI API 金鑰,不使用 Codex 帳號憑證。`,"prov.manageCodexAccounts":`管理 Codex 帳號`,"prov.openaiApiMissing":`需要 API 金鑰`,"prov.openaiApiSetup":`設定 API 金鑰`,"models.subtitle":`開關 Codex 可見的模型 — 原生 GPT passthrough 與已路由模型按供應商分組(點選標題可摺疊)。已停用的模型會從目錄和模型選擇器中隱藏。更改在下一個 Codex 回合生效 — opencodex 會使 Codex 的 5 分鐘模型快取失效,因此無需重新啟動。`,"models.nativeGroupLabel":`OpenAI 原生`,"models.nativeHint":"Passthrough 模型使用在供應商頁面選擇的帳號池或直連選項。關閉後會從 Codex 選擇器中隱藏(目錄條目保留,重新開啟即可完整恢復)。 在此新增模型會註冊為路由的 `openai/` 選擇器,而非新的裸 passthrough id。","models.active":`{active}/{total} 已啟用`,"models.workspace.providers":`供應商`,"models.workspace.allProviders":`所有供應商`,"models.workspace.mainAria":`模型詳細資料`,"models.allOn":`全部開啟`,"models.allOff":`全部關閉`,"models.presetLabel":`模型`,"models.presetMode_preset":`預設集`,"models.presetMode_all":`全部`,"models.presetMode_custom":`自訂`,"models.presetSummary":`顯示 {count} / {total} — 核心預設集 v{version}`,"models.presetUpdateAvailable":`預設集 v{version} 可用`,"models.presetAppliedToast":`{provider}:已套用預設集 — 選取 {count} 個模型`,"models.presetClearedToast":`{provider}:顯示全部模型`,"models.presetEmpty":`{provider}:預設集未比對到模型,選擇維持不變`,"models.presetConfirmReplace":`以包含 {count} 個模型的預設集取代你的選擇?`,"models.cap350k":`限制 350k`,"models.capApplied":`上下文限制已套用 — 將在下一個 Codex 回合生效。`,"models.capSaveFailed":`儲存上下文限制失敗`,"models.contextCapped":`350k 限制`,"models.contextCapLabel":`預設視窗 / 上限`,"models.v2Label":`子代理`,"models.shadowCallOriginal":`⚠ {models} →`,"models.v2DocsLink":`v1 / v2 是什麼?`,"models.v2Mode_v1":`v1`,"models.v2Mode_default":`base`,"models.v2Mode_v2":`v2`,"models.v2ModeDesc_v1":`所有模型 → v1 介面`,"models.v2ModeDesc_default":`上游預設值 (sol/terra=v2, luna=v1)`,"models.v2ModeDesc_v2":`所有模型 → v2 介面`,"models.keepNativeOnV1":`ChatGPT 維持 v1`,"models.keepNativeOnV1Hint":`ChatGPT 原生父代理會加密 v2 子任務,Grok/Claude 無法讀取。若 Sol/Terra 仍需派發路由模型,請保持開啟。路由父代理仍使用 v2。`,"models.v2Help":`控制所有模型的多代理介面。 + +v1: 經典單執行緒代理。所有模型使用 v1 協作介面。 +base: 上游預設值 — sol/terra 使用 v2,luna 使用 v1,其餘跟隨 codex 功能標誌。 +v2: 多執行緒代理(spawn_agent)。所有模型使用 v2 協作介面。 + +在 v2 下,「ChatGPT 維持 v1」會讓 Sol/Terra 留在 v1,以便繼續派發 Grok 或 Claude。ChatGPT 會加密 v2 子任務,路由模型無法讀取;路由父代理仍留在 v2。 + +更改在新會話中生效。`,"dash.multiAgent":`子代理`,"models.v2Conflict":`[agents] max_threads 仍存在 — codex 將拒絕啟動,請從 config.toml 移除`,"models.v2Applied":`子代理模式已更新 — 新會話生效(重新啟動 Codex 應用以重新整理選擇器)`,"models.v2ThreadsLabel":`最大執行緒`,"models.v2ThreadsDefault":`預設 (4)`,"models.v2ThreadsApplied":`執行緒上限已更新 — 新會話生效`,"models.v2ThreadsInvalid":`執行緒上限必須為 >= 1 的整數`,"models.v2ThreadsApply":`套用`,"models.capValue":`預設 {value}`,"models.contextCappedValue":`{value} 限制`,"models.setAll":`全部設定`,"models.setAllHint":`為所有已路由供應商打開 {value} 預設視窗。中繼站沒回報 context_window / context_length 時,這個值就是 Codex 實際視窗。要幫單一模型手寫,用同一列上的「自訂視窗」。原生供應商不受影響。`,"models.collapseAll":`全部摺疊`,"models.expandAll":`全部展開`,"models.orderHint":`選擇器順序:Subagents 中的選擇(按所選順序)→ 其餘已路由模型(依次按供應商、模型 ID 字母排序)→ 原生模型。可見性開關僅用於篩選,不會改變此順序。`,"models.custom":`自訂…`,"models.customApply":`套用`,"models.customPlaceholder":`tokens (例如 420000)`,"models.customAdd":`新增自訂模型`,"models.customAddTitle":`新增自訂模型 — {provider}`,"models.customEditTitle":`編輯自訂模型 — {provider}`,"models.customAdded":`已新增自訂模型`,"models.customUpdated":`已更新自訂模型`,"models.customDeleted":`已刪除自訂模型`,"models.customSaveFailed":`儲存自訂模型失敗`,"models.customSaving":`正在儲存…`,"models.customAddBtn":`新增`,"models.customEditBtn":`更新`,"models.customEdit":`編輯`,"models.customDelete":`刪除`,"models.customDeleteConfirm":`要刪除模型 {name} 嗎?`,"models.customBadge":`自訂`,"models.customSummary":`{count} 個自訂模型`,"models.customFieldModelId":`模型 ID(端點標識)`,"models.customFieldModelIdPlaceholder":`例如 qwen4-max-preview`,"models.customFieldDisplayName":`顯示名稱(可選)`,"models.customFieldDisplayNamePlaceholder":`例如 Qwen 4 Max Preview`,"models.customFieldContext":`上下文視窗`,"models.customFieldModalities":`輸入模態`,"models.customFieldReasoning":`推理強度`,"models.customFieldReasoningOverride":`覆寫推理強度`,"models.reasoningEffort.none":`無`,"models.reasoningEffort.minimal":`最低`,"models.reasoningEffort.low":`低`,"models.reasoningEffort.medium":`中`,"models.reasoningEffort.high":`高`,"models.reasoningEffort.xhigh":`極高`,"models.reasoningEffort.max":`最高`,"models.tipProvider":`供應商`,"models.tipContext":`上下文`,"models.tipModalities":`模態`,"models.tipStatus":`狀態`,"models.tipActive":`已啟用`,"models.tipDisabled":`已停用`,"models.applied":`已套用 — 將在下一個 Codex 回合生效。`,"models.saveFailed":`儲存失敗`,"models.networkError":`網路錯誤 — 代理在執行嗎?`,"models.loadFail":`載入模型失敗 — 代理在執行嗎?`,"models.noRouted":`沒有已路由的模型`,"models.noRoutedHint":`請先登入供應商或新增一個。`,"models.emptyDiscovery":`未發現任何模型。請檢查供應商端點,或新增靜態/自訂模型。`,"models.emptyDiscoveryDisabled":`即時模型發現已關閉,且尚未配置靜態模型。`,"models.discoveryFailedBadge":`發現失敗`,"models.discoveryFailedHttp":`模型發現失敗(HTTP {status})。`,"models.discoveryFailedBlocked":`模型發現被目標策略阻止。`,"models.discoveryFailedInvalidResponse":`模型發現返回了無效回應。`,"models.discoveryFailedNetwork":`由於網路錯誤,模型發現失敗。`,"models.discoveryFailedProvider":`供應商報告了模型發現錯誤。`,"models.discoveryFailedGeneric":`模型發現失敗。`,"models.openProviderSettings":`開啟供應商設定`,"models.loading":`載入中…`,"models.search":`搜尋模型…`,"models.showMore":`再顯示 {n} 個`,"models.allowlistLabel":`僅所選`,"models.allowlistHint":`僅勾選的模型進入目錄(留空 = 全部)。適用於暴露成千上萬模型的供應商。`,"models.selectedCount":`已選 {n} 個`,"sub.subtitle":`Codex 的 {cmd} 僅將優先順序最高的前 5 個模型作為覆蓋項公開。在此最多選擇 5 個 — 原生 gpt 或已路由模型 — opencodex 會設定它們的目錄優先順序,使其正好排在前面。其他模型仍可按確切名稱呼叫;此設定僅控制顯示項。`,"sub.featured":`精選`,"sub.advanced":`進階`,"sub.orderHintAria":`此順序的用途`,"sub.orderHint":`此處所選並顯示的順序決定 Codex 模型選擇器頂部第 1–5 位,以及 {cmd} 的預設模型候選。`,"sub.noneSelected":`未選擇 — 請從下方列表選擇。`,"sub.models":`模型`,"sub.search":`搜尋模型(原生 gpt + 已路由)…`,"sub.noModels":`沒有模型 — 請先登入供應商或新增一個。`,"sub.saved":`已儲存 {n} 個模型。啟動新的 Codex 會話(或執行 {cmd})以將它們作為 spawn_agent 覆蓋項檢視。`,"sub.saveFailed":`儲存失敗`,"sub.networkError":`網路錯誤 — 代理在執行嗎?`,"sub.loadFail":`載入模型失敗 — 代理在執行嗎?`,"sub.loading":`載入中…`,"sub.moveUp":`上移 {m}`,"sub.moveDown":`下移 {m}`,"sub.removeAria":`移除 {m}`,"sub.ultraMode":`超級模式`,"sub.ultraModeHint":`為所有模型和推理力度啟用主動多代理委派策略(不改變推理力度本身)。將 features.multi_agent_v2.multi_agent_mode_hint_text 寫入 config.toml。`,"sub.ultraModeV2Required":`需要 v2 多代理表面 — 請先啟用 multi_agent_v2,並在子代理模式控制項中選擇 v2。`,"sub.ultraModeText":`超級模式委派文字`,"sub.ultraModePreset":`還原預設`,"sub.ultraModeLoadFail":`無法載入超級模式設定 — 代理是否在執行?`,"sub.ultraModeSaveFail":`儲存超級模式設定失敗`,"sub.ultraModeSaved":`超級模式已儲存。適用於新的 Codex 會話。`,"logs.title":`請求日誌`,"logs.tabLogs":`日誌`,"logs.tabDebug":`除錯`,"logs.subtitle":`經過本地 opencodex 代理的最近請求,最新在前。`,"logs.autoRefresh":`自動重新整理`,"logs.noRequests":`暫無請求。`,"logs.loadError":`無法載入請求日誌。`,"logs.filter.surface.label":`介面`,"logs.filter.surface.all":`全部`,"logs.filter.surface.claude":`Claude`,"logs.filter.surface.codex":`Codex`,"logs.filter.surface.grok":`Grok`,"logs.filter.interceptedHelpersOnly":`僅已攔截的輔助請求`,"logs.badge.interceptedHelper":`I · {model}`,"logs.badge.interceptedHelperTitle":`已攔截的輔助請求`,"logs.filter.conversation.label":`對話`,"logs.filter.conversation.placeholder":`貼上對話 ID`,"logs.filter.conversation.clear":`清除`,"logs.filter.model.label":`模型`,"logs.filter.model.placeholder":`依模型或供應商篩選`,"logs.filter.conversation.apply":`篩選日誌`,"logs.conversation.totals":`{requests} 次請求 · {tokens} tokens · {cost}`,"logs.conversation.scope":`合計僅涵蓋目前已載入的 Logs 環形緩衝。`,"logs.conversation.excluded":`(~$ 已排除 {unpriced} 筆無定價、{unmetered} 筆無計量)`,"logs.cost.approximate":`{amount}`,"logs.cost.lowerBound":`≥{amount}`,"logs.cost.unavailable":`無法估算`,"logs.detail.conversation":`對話`,"logs.badge.claude":`Claude`,"logs.col.time":`時間`,"logs.col.request":`請求`,"logs.col.model":`模型`,"logs.col.effort":`推理強度`,"logs.col.provider":`供應商`,"logs.col.status":`狀態`,"logs.col.tokens":`Token 數`,"logs.col.tokPerSec":`tok/s`,"logs.col.estimatedCost":`~$`,"logs.metric.tokPerSecTitle":`按完整請求耗時計算的每秒輸出 token`,"logs.metric.estimatedCostTitle":`按 API 標價估算,並非實際扣費;價格無法符合時不顯示`,"usage.cost.total":`API 標價折算(當前範圍)`,"usage.cost.disclaimer":`這不是帳單或扣費憑證。實際可能計入訂閱用量或消耗服務商額度。`,"usage.cost.unpricedNote":`已排除 {count} 個無法計費的請求`,"logs.detail.section.basic":`基本資訊`,"logs.detail.section.performance":`效能`,"logs.detail.section.cost":`API 標價折算`,"logs.detail.section.attempts":`Combo 嘗試`,"logs.detail.section.usage":`原始 usage`,"logs.detail.ttft":`TTFT`,"logs.detail.costTotal":`標價折算`,"logs.detail.totalTokens":`Token 總數`,"logs.detail.matchedKey":`符合的 jawcode 鍵`,"logs.detail.priceSource":`價格來源`,"logs.detail.unavailableReason":`不可用原因`,"logs.detail.copyRequestId":`複製請求 ID`,"logs.detail.copied":`已複製`,"logs.detail.source.jawcode":`jawcode 目錄`,"logs.detail.source.expected":`Expected 價格覆蓋`,"logs.detail.verification.verified":`已驗證`,"logs.detail.verification.derived":`由基礎模型推導`,"logs.detail.attempt.target":`供應商 / 模型`,"logs.detail.attempt.reason":`結果 / 原因`,"logs.detail.attempt.completed":`已完成`,"logs.detail.attempt.e2eNote":`頂層 tok/s 為端到端值;每次嘗試使用各自耗時。`,"logs.detail.reason.usage_missing":`未上報 usage。`,"logs.detail.reason.usage_unsupported":`該供應商不支援上報 usage。`,"logs.detail.reason.output_missing":`未上報正數輸出 token。`,"logs.detail.reason.invalid_duration":`請求耗時無效。`,"logs.detail.reason.price_unmatched":`未找到符合的 jawcode 價格。`,"logs.detail.reason.invalid_cache_breakdown":`快取 token 明細與輸入 token 總數衝突。`,"logs.detail.reason.invalid_usage":`Usage 包含無效的 token 值。`,"logs.detail.reason.combo_attempt_unavailable":`至少一次 Combo 嘗試無法計價。`,"logs.detail.estimate.usage_estimated":`供應商 usage 為估算值。`,"logs.detail.estimate.cache_detail_missing":`缺少快取明細;輸入費用按上限估算。`,"logs.detail.estimate.expected_price_overlay":`使用了已驗證的 Expected 標價。`,"logs.col.error":`錯誤`,"logs.col.upstreamReason":`上游原因`,"logs.col.duration":`耗時`,"logs.modelTooltip.model":`模型`,"logs.modelTooltip.resolvedModel":`解析後模型`,"logs.modelTooltip.requestedTier":`請求層級`,"logs.modelTooltip.configuredTier":`設定層級`,"logs.modelTooltip.responseTier":`回應層級`,"logs.modelTooltip.supportsTier":`支援層級`,"logs.tokens.reported":`已上報`,"logs.tokens.unreported":`未上報`,"logs.tokens.unsupported":`不支援`,"logs.tokens.estimated":`估算`,"logs.tokens.input":`輸入`,"logs.tokens.output":`輸出`,"logs.tokens.cacheRead":`快取命中 (c)`,"logs.tokens.cacheWrite":`快取寫入 (w)`,"logs.tokens.reasoning":`推理`,"logs.tokens.noCache":`無快取資料`,"logs.tokens.noCacheNote":`該供應商不報告快取 token 數`,"logs.tokens.noCacheCursor":`Cursor 未報告快取明細`,"logs.tokens.noCacheCursorNote":`Cursor 不提供快取讀寫 token 數;這表示未知,並不代表已確認快取未命中`,"logs.tokens.estimatedNote":`估算值(供應商不報告精確用量)`,"logs.details":`檢視詳細資料`,"logs.detailTitle":`請求詳細資料`,"logs.detailRaw":`原始日誌`,"debug.title":`除錯`,"debug.subtitle":`可選的 provider transport 與 usage 提取診斷。請求錯誤和 502 在“日誌”分頁顯示。`,"debug.debug":`供應商除錯`,"debug.usage":`用量提取`,"debug.injection":`注入日誌`,"debug.claude":`Claude 入站`,"debug.claudeInbound.title":`Claude 入站請求`,"debug.claudeInbound.sub":`顯示 Claude Code/Desktop 實際傳送的內容(thinking、effort、metadata)— 不儲存提示詞原文。`,"debug.claudeInbound.empty":`尚未捕獲任何請求。開啟後從 Claude 傳送一條訊息試試。`,"debug.claudeInbound.time":`時間`,"debug.claudeInbound.endpoint":`端點`,"debug.claudeInbound.model":`模型`,"debug.claudeInbound.none":`無`,"debug.reset":`清除執行時覆蓋`,"debug.refresh":`重新整理`,"debug.follow":`跟隨捲動`,"debug.streamProvider":`供應商`,"debug.streamUsage":`用量`,"debug.streamInjection":`注入`,"debug.loading":`正在載入除錯設定…`,"debug.emptyTitle":`除錯日誌已關閉`,"debug.empty":`請在上方卡片中開啟供應商除錯或用量提取。透過代理傳送請求後,診斷行會顯示在這裡。`,"debug.noLinesTitle":`等待診斷行`,"debug.noLines.provider":`供應商除錯已開啟,但僅紀錄傳輸異常(捨棄或格式錯誤的幀,以及 Cursor dial/retry 事件)。透過 Anthropic 等供應商的正常請求可能不會產生任何行。`,"debug.noLines.usage":`用量提取已開啟但尚未捕獲任何內容。請透過 Codex 傳送請求,隨後會顯示在此處。`,"debug.noLines.injection":`注入日誌已開啟但尚未捕獲任何內容。它紀錄協作和子代理回合中的多代理指導注入與 effort-cap 決策。`,"usage.title":`用量`,"usage.subtitle":`代理本地的 Token 用量統計。缺失的用量不會顯示為零。`,"usage.loading":`正在載入用量資料…`,"usage.empty":`尚無用量紀錄。透過代理傳送請求後將在此顯示。`,"usage.loadError":`無法載入用量資料。`,"usage.range.all":`全部`,"usage.range.30d":`30 天`,"usage.range.7d":`7 天`,"usage.card.requests":`請求數`,"usage.card.measured":`已計量`,"usage.card.reported":`已上報`,"usage.card.totalTokens":`Token 總數`,"usage.card.cachedTokens":`快取命中 Token`,"usage.card.cachedTokensHint":`從供應商快取讀取的提示 Token(命中)。快取寫入在下方單獨顯示。`,"usage.card.cacheWriteTokens":`快取寫入`,"usage.card.coverage":`覆蓋率`,"usage.card.activeDays":`活躍天數`,"usage.section.heatmap":`每日活動`,"usage.section.overview":`總覽`,"usage.section.models":`模型`,"usage.section.providers":`供應商`,"usage.section.coverage":`覆蓋率明細`,"usage.coverage.measured":`已計量`,"usage.coverage.reported":`供應商上報`,"usage.coverage.estimated":`估算`,"usage.coverage.note":`已計量包含供應商上報和估算的 Token 數。未上報 / 不支援請求僅做計數,不會被算作 0 Token。`,"usage.search.models":`搜尋模型…`,"usage.col.requests":`請求數`,"usage.col.measured":`已計量`,"usage.col.reported":`已上報`,"usage.col.tokens":`Token 數`,"usage.col.share":`佔比`,"usage.heatmap.less":`少`,"usage.heatmap.more":`多`,"usage.dayMon":`一`,"usage.dayWed":`三`,"usage.dayFri":`五`,"usage.heatmap.tooltipTokens":`{tokens} Token`,"usage.heatmap.tooltipRequests":`{requests} 請求`,"nav.storage":`儲存`,"storage.title":`儲存`,"storage.subtitle":`CODEX_HOME 磁碟佔用診斷。下方歸檔清理可將最舊歸檔會話隔離或永久刪除——活動會話保持只讀。`,"storage.loading":`正在掃描儲存…`,"storage.empty":`CODEX_HOME 為空或不存在——沒有可顯示的內容。`,"storage.error":`儲存掃描失敗。請檢查 CODEX_HOME 是否指向有效目錄。`,"storage.refresh":`重新掃描`,"storage.card.total":`總大小`,"storage.card.files":`檔案數`,"storage.card.home":`CODEX_HOME`,"storage.section.buckets":`分類`,"storage.section.largest":`最大檔案`,"storage.col.bucket":`分類`,"storage.col.size":`大小`,"storage.col.files":`檔案`,"storage.col.oldest":`最舊`,"storage.col.newest":`最新`,"storage.col.rows":`資料庫行數`,"storage.rows.unknown":`未知(已鎖定)`,"storage.bucket.sessions":`活動會話`,"storage.bucket.archived_sessions":`已歸檔會話`,"storage.bucket.logs_db":`日誌資料庫`,"storage.bucket.state_db":`狀態資料庫`,"storage.bucket.attachments":`附件`,"storage.bucket.deletion_manifests":`刪除清單`,"storage.bucket.other":`其他`,"storage.cleanup.title":`歸檔清理`,"storage.cleanup.help":`按百分比移除最舊的歸檔會話。不會觸碰活動會話。預設隔離——檔案移至 CODEX_HOME/.trash。`,"storage.cleanup.slider":`最舊歸檔百分比`,"storage.cleanup.percent":`最舊 {percent}%`,"storage.cleanup.preset":`{percent}`,"storage.cleanup.preview":`預覽`,"storage.cleanup.confirmTitle":`確認歸檔清理`,"storage.cleanup.confirmBody":`將處理 {count} 個歸檔檔案(約 {size}),即最舊的 {percent}%。`,"storage.cleanup.moreFiles":`…以及另外 {n} 個`,"storage.cleanup.permanent":`永久刪除(跳過隔離)`,"storage.cleanup.permanentWarn":`永久刪除無法復原。`,"storage.cleanup.quarantineNote":`檔案會移到 CODEX_HOME 下的 .trash。可在下方「隔離區」恢復。`,"storage.cleanup.cancel":`取消`,"storage.cleanup.confirmQuarantine":`隔離`,"storage.cleanup.confirmPermanent":`永久刪除`,"storage.cleanup.doneQuarantine":`已隔離 {count} 個檔案({size})。`,"storage.cleanup.donePermanent":`已永久刪除 {count} 個檔案({size})。`,"storage.cleanup.previewFailed":`預覽失敗。`,"storage.cleanup.cleanupFailed":`清理失敗。`,"storage.cleanup.err.codex_busy":`Codex 正在使用 state.sqlite — 請退出 Codex 後重試。`,"storage.cleanup.err.stale_preview":`預覽後歸檔檔案已變化 — 請重新預覽。`,"storage.cleanup.err.restore_pending_overlap":`所選歸檔與未完成的隔離區恢復重疊 — 請先完成或重試恢復。`,"storage.cleanup.err.referenced_history":`所選歸檔仍被 fork 或分頁歷史引用。`,"storage.cleanup.err.invalid_digest":`預覽摘要缺失或無效。`,"storage.cleanup.err.invalid_mode":`模式必須是 quarantine 或 permanent。`,"storage.cleanup.err.fs_failed":`檔案系統清理失敗。部分更改可能已生效 — 請檢查 CODEX_HOME/.trash 及顯示的恢復路徑。`,"storage.cleanup.err.fs_failed_trash":`檔案系統清理失敗。部分更改可能已生效 — 請在 {trashDir} 和 manifest.json 中查詢可恢復檔案。`,"storage.cleanup.err.db_reconcile_failed":`無法更新 Codex 狀態資料庫。`,"storage.cleanup.err.cleanup_failed":`清理失敗。`,"storage.trash.title":`隔離區`,"storage.trash.help":`已移至 CODEX_HOME/.trash 的歸檔會話。恢復會把 JSONL 與執行緒行寫回。`,"storage.trash.empty":`沒有隔離條目。`,"storage.trash.loading":`正在載入隔離區…`,"storage.trash.col.when":`隔離時間`,"storage.trash.col.files":`檔案`,"storage.trash.col.size":`大小`,"storage.trash.col.mode":`模式`,"storage.trash.col.id":`條目`,"storage.trash.restore":`恢復`,"storage.trash.confirmTitle":`恢復隔離條目?`,"storage.trash.confirmBody":`將 {count} 個檔案(約 {size})從 {id} 恢復到歸檔會話。`,"storage.trash.cancel":`取消`,"storage.trash.confirmRestore":`恢復`,"storage.trash.done":`已恢復 {count} 個檔案({size})。`,"storage.trash.restoreFailed":`恢復失敗。`,"storage.trash.listFailed":`無法列出隔離條目。`,"storage.trash.mode.quarantine":`隔離`,"storage.trash.mode.permanent":`永久(未完成)`,"storage.trash.err.codex_busy":`Codex 正在使用 state.sqlite — 請退出 Codex 後重試。`,"storage.trash.err.invalid_trash":`隔離條目 ID 缺失或無效。`,"storage.trash.err.missing_trash":`未找到隔離條目。`,"storage.trash.err.dest_exists":`恢復目標已存在 — 請刪除或重新命名歸檔檔案後重試。`,"storage.trash.err.fs_failed":`檔案系統恢復失敗。部分檔案可能已恢復 — 請檢查 archived_sessions 與 .trash。`,"storage.trash.err.db_reconcile_failed":`無法恢復 Codex 狀態資料庫行。`,"storage.trash.err.storage_mutation_busy":`另一項儲存清理或恢復正在進行 — 請稍後再試。`,"storage.trash.err.restore_failed":`恢復失敗。`,"storage.trash.err.restore_worker_timeout":`恢復耗時過長(超過 10 分鐘)已停止。`,"storage.trash.err.restore_worker_aborted":`關閉過程中恢復已取消。`,"storage.trash.err.restore_worker_failed":`恢復 worker 崩潰或意外失敗。`,"storage.policy.title":`自動清理策略`,"storage.policy.help":`當歸檔大小超過閾值時可選批次清理。預設關閉——不會自動啟用。`,"storage.policy.loading":`正在載入策略…`,"storage.policy.loadFailed":`無法載入清理策略。`,"storage.policy.saveFailed":`無法儲存清理策略。`,"storage.policy.runFailed":`策略執行失敗。`,"storage.policy.alreadyRunning":`清理策略已在執行中。`,"storage.policy.invalid":`策略值無效。`,"storage.policy.enabled":`啟用自動清理`,"storage.policy.enabledHint":`預設關閉。啟用後僅按所選計劃(或立即執行)執行。`,"storage.policy.threshold":`歸檔大小超過時觸發(GiB)`,"storage.policy.target":`清理目標`,"storage.policy.targetPercent":`刪除最舊歸檔百分比`,"storage.policy.targetReduce":`將歸檔縮小至(GiB)`,"storage.policy.schedule":`計劃`,"storage.policy.schedule.manual":`僅手動`,"storage.policy.schedule.startup":`代理啟動時`,"storage.policy.schedule.daily":`每天`,"storage.policy.schedule.weekly":`每週`,"storage.policy.mode":`刪除模式`,"storage.policy.mode.quarantine":`隔離(預設)`,"storage.policy.mode.permanent":`永久刪除`,"storage.policy.permanentWarn":`永久模式無法復原。不確定時請使用隔離。`,"storage.policy.lastRun":`上次執行`,"storage.policy.lastRunDetail":`已移除 {count} · 釋放 {size}`,"storage.policy.nextRun":`下次執行`,"storage.policy.never":`從未`,"storage.policy.save":`儲存`,"storage.policy.runNow":`立即執行`,"storage.policy.running":`執行中…`,"storage.policy.saved":`策略已儲存。`,"storage.policy.skippedDisabled":`策略已禁用 — 請先啟用。`,"storage.policy.skippedUnder":`歸檔大小低於閾值 — 無需操作。`,"storage.policy.skippedEmpty":`沒有匹配目標的歸檔候選項。`,"storage.policy.doneQuarantine":`策略已隔離 {count} 個檔案({size})。`,"storage.policy.donePermanent":`策略已永久刪除 {count} 個檔案({size})。`,"storage.policy.metadataSaveWarning":`策略執行已完成,但無法儲存其排程中繼資料。`,"modal.addNamed":`新增:{label}`,"modal.add":`新增供應商`,"modal.search":`搜尋供應商…`,"modal.logInWith":`使用 {label} 登入`,"modal.waitingBrowser":`等待瀏覽器…`,"modal.providerName":`供應商名稱`,"modal.adapter":`介面卡`,"modal.baseUrl":`Base URL`,"modal.endpoint":`端點`,"modal.endpoint.tokenPlan":`Token 方案`,"modal.endpoint.payAsYouGo":`按量付費`,"modal.endpoint.custom":`自訂`,"modal.defaultModel":`預設模型(可選)`,"modal.allowPrivateNetwork":`允許本地/私有網路`,"modal.allowPrivateNetworkHint":`僅為有意自託管的供應商啟用。後設資料端點仍被阻止。`,"modal.nameRequired":`供應商名稱為必填項`,"modal.baseUrlRequired":`Base URL 為必填項`,"modal.networkError":`網路錯誤 — 代理在執行嗎?`,"modal.loginFailStart":`登入啟動失敗`,"modal.waitingLogin":`等待瀏覽器登入…`,"modal.loggingIn":`登入中…`,"modal.loginTimeout":`登入逾時 — 請重試。`,"modal.back":`返回`,"modal.badge.oauth":`OAuth`,"modal.customProvider":`自訂供應商`,"modal.failedStatus":`失敗 ({status})`,"modal.loginError":`登入錯誤:{error}`,"modal.badge.codexLogin":`Codex 登入`,"modal.badge.local":`本地`,"modal.badge.apiKey":`API 金鑰`,"modal.badge.direct":`Direct`,"modal.badge.pool":`帳號池`,"modal.badge.free":`免費`,"modal.invalidPreset":`此內建供應商預設不完整。請重新啟動代理後重試。`,"modal.freeTierTitle":`免費層級`,"modal.freeTierDefault":`無需 API 金鑰,開箱即用。`,"modal.tab.accounts":`帳號`,"modal.tab.free":`免費`,"modal.tab.paid":`付費`,"modal.accountsHint":`在此登入 ChatGPT/Codex、OAuth 與 API 金鑰帳號。OpenAI 為內建供應商 — 請登入,無需再次新增。`,"modal.accountsCodexAuthLink":`Codex 認證`,"modal.notListed":`沒有你要的供應商?新增自訂`,"modal.catalogLoading":`正在載入目錄…`,"modal.accountLogin":`登入`,"modal.accountLogout":`登出`,"modal.accountAdd":`新增帳號`,"modal.accountManage":`管理`,"modal.accountCodexPool":`ChatGPT 帳號池`,"modal.accountLoggedIn":`已登入`,"modal.accountLoggedOut":`未登入`,"quota.fiveHourLimit":`5 小時限額`,"quota.ageMinutes":`{n} 分鐘`,"quota.ageHours":`{n} 小時`,"quota.ageDays":`{n} 天`,"quota.observedAgo":`{age}前取得`,"quota.observedHint":`Meta 僅在串流回應期間回報用量,因此這是最後一次取得的數值,而非即時讀數。`,"quota.weeklyLimit":`每週限額`,"quota.monthlyLimit":`30 天限額`,"quota.cursorFirstParty":`官方模型`,"quota.cursorApiUsage":`API 用量`,"quota.totalSubscriptionCredits":`訂閱總額度`,"quota.creditsBalance":`額度餘額`,"quota.creditsPeriodEnds":`帳單週期結束於 {date}`,"quota.usedPercent":`已用 {pct}%`,"quota.limitReached":`已達上限`,"quota.resetsToday":`今天 {time} 重設`,"quota.resetsTomorrow":`明天 {time} 重設`,"quota.resetsAt":`{when} 重設`,"quota.resetsRelativeMinutes":`{n} 分鐘後重設`,"quota.resetsRelativeHours":`{n} 小時後重設`,"pws.status.ready":`就緒`,"pws.status.needsSetup":`需要設定`,"pws.status.needsAttention":`需要關注`,"pws.auth.chatgptPassthrough":`ChatGPT 直通`,"pws.auth.noKey":`無需金鑰`,"pws.freeTitle":`免費定價(可能仍需金鑰)`,"pws.localTitle":`本地執行時`,"pws.modelCountOne":`1 個模型`,"pws.modelCount":`{count} 個模型`,"pws.rail.suffixDefault":` · 預設`,"pws.rail.suffixLocal":` · 本地`,"pws.rail.suffixFree":` · 免費`,"pws.rail.selectAria":`選擇 {name} — {status}{suffix}`,"pws.searchPlaceholder":`搜尋供應商…`,"pws.filterAria":`篩選供應商`,"pws.providerFiltersAria":`供應商篩選`,"pws.filters":`篩選`,"pws.filterStatus":`狀態`,"pws.pricing":`定價`,"pws.paid":`付費`,"pws.filterType":`型別`,"pws.type.cloud":`雲端`,"pws.type.local":`本地`,"pws.type.selfHosted":`自託管`,"pws.type.login":`登入`,"pws.sort":`排序`,"pws.sortProvidersAria":`排序供應商`,"pws.sort.az":`A–Z`,"pws.sort.za":`Z–A`,"pws.sort.freePaid":`免費優先`,"pws.sort.paidFree":`付費優先`,"pws.sort.accountsFirst":`帳號優先`,"pws.resetAll":`全部重設`,"pws.providerList":`供應商列表`,"pws.providersAria":`供應商`,"pws.groupReady":`就緒 ({count})`,"pws.groupNeedsSetup":`需要設定 ({count})`,"pws.groupDisabled":`已停用 ({count})`,"pws.noSearchResults":`沒有符合搜尋的供應商。`,"pws.noMatchFilters":`沒有符合篩選的供應商。`,"pws.noProvidersConfigured":`尚未配置供應商。`,"pws.workspaceMainAria":`供應商詳細資料`,"pws.detailComingSoon":`詳細資料檢視即將推出 — 請在經典檢視中管理。`,"pws.selectPrompt":`從列表中選擇一個供應商。`,"pws.connectFirst":`連線你的第一個供應商`,"pws.empty.browseFree":`瀏覽免費供應商`,"pws.empty.browseFreeDesc":`無需訂閱即可開始`,"pws.empty.connectAccount":`連線帳號`,"pws.empty.connectAccountDesc":`使用 ChatGPT 或供應商登入`,"pws.empty.addEndpoint":`新增端點`,"pws.empty.addEndpointDesc":`自訂 base URL 和 API 金鑰`,"pws.tab.overview":`總覽`,"pws.tab.models":`模型`,"pws.tab.usage":`用量`,"pws.tab.accounts":`帳號`,"pws.tab.settings":`設定`,"pws.connection":`連線`,"pws.status.connected":`已連線`,"pws.attentionTitle":`需要關注`,"pws.attention.reauth":`當前帳號需要重新認證`,"pws.attention.reauthForward":`當前 Codex 帳號需要重新認證 — 請到“帳號”中處理`,"pws.attention.missingCredentials":`缺少憑證`,"pws.cell.auth":`認證`,"pws.cell.note":`備註`,"pws.cell.defaultModel":`預設模型`,"pws.statsAria":`供應商統計`,"pws.statsTitle":`統計`,"pws.stats.totalRequests":`請求數(30 天)`,"pws.stats.totalTokens":`tokens(30 天)`,"pws.stats.quotaUpdated":`配額更新`,"pws.stats.quotaTracked":`在用量標籤檢視限額。`,"pws.stats.source":`來源`,"pws.usageLast30d":`用量(最近 30 天)`,"pws.estimatedCost":`預估費用`,"pws.costDisclaimer":`基於 API 公示價格的預估值,非實際計費金額。`,"pws.modelBreakdown":`模型用量明細`,"pws.col.model":`模型`,"pws.col.cost":`預估費用`,"pws.col.tokens":`Token`,"pws.col.requests":`請求`,"pws.col.share":`佔比`,"pws.tokenInput":`輸入`,"pws.tokenOutput":`輸出`,"pws.metricRequests":`請求`,"pws.metricTokens":`Token`,"pws.usageUnavailable":`尚無用量紀錄。`,"pws.rateLimits":`速率限制`,"pws.quotaUnavailable":`此供應商暫無配額資料。`,"pws.accountQuotaUnavailable":`速率限制資料暫時不可用;若有上次已知值則繼續顯示。`,"pws.selected":`已選擇`,"pws.copyModelId":`複製 ID`,"pws.modelCopied":`已複製!`,"pws.modelsAvailable":`{count} 個可用`,"pws.modelSearchPlaceholder":`篩選模型…`,"pws.modelsLoading":`正在載入模型…`,"pws.modelsLoadFailed":`無法載入模型。`,"pws.modelsNeedsReauth":`需要重新登入後才能取得即時模型列表。當前顯示已配置的模型。`,"pws.modelsConfiguredFallback":`顯示已配置的模型(即時發現不可用)。`,"pws.modelsTruncated":`顯示 {total} 個模型中的前 {shown} 個。使用篩選以縮小列表。`,"pws.retry":`重試`,"pws.noModels":`未發現此供應商的模型。`,"pws.noModelMatch":`沒有符合篩選條件的模型。`,"pws.adapterBaseRequired":`介面卡和基本 URL 為必填項。`,"pws.addAccount":`新增帳號`,"pws.addKey":`新增 API 金鑰`,"pws.apiKeys":`API 金鑰`,"pws.authMode":`認證方式`,"pws.availableAccounts":`可用帳號`,"pws.accountOrdinal":`帳號 {count}`,"pws.accountsLoading":`正在載入帳號…`,"pws.accountsLoadFailed":`無法載入帳號。`,"pws.retryAccounts":`重試`,"pws.noAccounts":`尚未連線任何帳號。`,"pws.accountSwitching":`切換中…`,"pws.accountCurrent":`當前帳號`,"pws.defaultModelNone":`無(使用供應商預設值)`,"pws.discardSettings":`放棄`,"pws.jsonEditorDesc":`直接編輯供應商 JSON 配置。更改將立即儲存。`,"pws.jsonEditorTitle":`JSON 編輯器 — {name}`,"pws.jsonRestore":`恢復`,"pws.jsonSave":`儲存`,"pws.loggedInTitle":`已登入`,"pws.notLoggedInTitle":`未登入`,"pws.note":`備註`,"pws.allowPrivateNetwork":`允許本地/私有網路`,"pws.liveModels":`從供應商發現模型`,"pws.liveModelsDesc":`取得供應商的即時模型目錄。關閉後僅使用已配置的靜態模型。`,"pws.xaiResponsesOptIn":`讓 Grok 4.5 與 4.6 使用 Responses API`,"pws.xaiResponsesOptInDesc":`透過 openai-responses 路由這兩個模型。其他 Grok 模型與層級行為不變。`,"pws.xaiResponsesOptInMixed":`已部分啟用。`,"pws.cursorTransport":`Cursor 傳輸協定`,"pws.cursorTransportHttp2":`HTTP/2(預設)`,"pws.cursorTransportHttp1":`HTTP/1.1(代理相容)`,"pws.cursorTransportDesc":`當代理無法穩定承載 Cursor 的 HTTP/2 串流時,請使用 HTTP/1.1。`,"pws.optionalPlaceholder":`可選`,"pws.providerId":`供應商 ID`,"pws.reauth":`需要重新認證`,"pws.reauthenticate":`重新認證`,"pws.copyDoctor":`複製 ocx doctor`,"pws.doctorCopied":`已複製`,"pws.doctorCopyUnavailable":`剪貼簿不可用`,"pws.healthCooldownHint":`請等到冷卻結束。暫時不要探測此帳號。`,"pws.healthLabel.rateLimited":`已限速`,"pws.healthLabel.quotaLimited":`配額受限`,"pws.healthLabel.reauthRequired":`需要重新認證`,"pws.healthLabel.refreshFailed":`重新整理失敗`,"pws.healthLabel.metadataMismatch":`後設資料不符合`,"pws.healthLabel.credentialConflict":`憑證衝突`,"pws.healthSummary.rateLimited":`{provider} {account}:限速至 {until}。在此之前將暫停該帳號的路由。`,"pws.healthSummary.quotaLimited":`{provider} {account}:配額限制至 {until}。在此之前將暫停該帳號的路由。`,"pws.healthSummary.reauthRequired":`{provider} {account}:需要重新認證。`,"pws.healthSummary.credentialConflict":`{provider} {account}:憑證衝突。`,"pws.healthSummary.metadataMismatch":`{provider} {account}:後設資料不符合。`,"pws.healthSummary.staleCredentials":`{provider} {account}:憑證不完整。`,"pws.removeConfirm":`移除`,"pws.removeConfirmBody":`移除供應商「{name}」?此操作無法撤消。`,"pws.removeDefaultConfirmBody":`移除預設供應商「{name}」?「{defaultProvider}」將成為預設供應商。此操作無法撤消。`,"pws.removeConfirmTitle":`移除供應商`,"pws.saveSettings":`儲存`,"pws.pacingTitle":`請求節流`,"pws.pacingDesc":`均勻延遲送往此供應商的請求啟動。串流回應可以重疊。`,"pws.pacingEnabled":`啟用`,"pws.pacingRpm":`每分鐘請求數`,"pws.pacingRpmUnit":`次/分鐘`,"pws.pacingDelay":`最小間隔(毫秒)`,"pws.pacingSlowerWins":`以較慢的供應商限制為準,模型規則只能增加延遲。`,"pws.pacingQueued":`排隊中`,"pws.pacingNextSlot":`距下個時段`,"pws.pacingLastModel":`上個模型`,"pws.pacingNone":`無`,"pws.pacingModelOverrides":`模型規則`,"pws.pacingModel":`模型`,"pws.pacingAdd":`新增規則`,"pws.pacingRemove":`移除`,"pws.pacingRemoveModel":`移除 {model} 的請求節流規則`,"pws.pacingRuleRequired":`請先設定供應商限制或模型規則,再啟用請求節流。`,"pws.saving":`儲存中…`,"pws.settingsSaved":`設定已儲存。`,"pws.settingsUnsavedBar":`有未儲存的更改。`,"pws.unsavedLeaveBody":`有未儲存的更改。離開前儲存嗎?`,"pws.unsavedLeaveTitle":`未儲存的更改`,"pws.attentionRequired":`需要關注`,"pws.attentionAria":`{name}:{reason}`,"pws.missingCredentials":`缺少憑證`,"pws.editJsonDesc":`以 JSON 編輯原始代理配置`,"pws.updatesUnavailable":`供應商更新不可用。`,"pws.dashboard.title":`供應商總覽`,"pws.dashboard.subtitle":`在一個地方管理所有模型供應商。`,"pws.dashboard.rateLimits":`速率限制`,"pws.dashboard.recentlyUsed":`最近使用`,"pws.dashboard.requests":`{count} 個請求`,"pws.dashboard.checkedAgo":`{time} 前檢查`,"pws.dashboard.noQuota":`無配額資料`,"pws.dashboard.noUsage":`暫無使用資料`,"pws.allProviders":`供應商總覽`,"pws.enabledLabel":`已啟用`,"pws.testConnection":`測試連線`,"pws.testing":`測試中…`,"pws.connectionOk":`連線成功`,"pws.connectionFailed":`連線失敗`,"pws.editSettings":`編輯設定`,"pws.viewUsage":`檢視詳細用量`,"pws.allSystemsOk":`所有系統正常執行`,"pws.apiKeyConfigured":`API 金鑰已配置`,"pws.addApiKey":`新增 API 金鑰`,"pws.loggedInAs":`已登入為 {email}`,"pws.notLoggedIn":`未登入`,"pws.passthrough":`Codex 透傳`,"pws.notes":`備註`,"pws.notePlaceholder":`新增關於此供應商的備註...`,"pws.noteSaved":`備註已儲存`,"pws.authSummary":`認證`,"time.justNow":`剛剛`,"time.notChecked":`未檢查`,"time.minutesAgo":`{n} 分鐘前`,"time.hoursAgo":`{n} 小時前`,"time.daysAgo":`{n} 天前`,"modal.noMatch":`無符合。`,"modal.oauthDefaultNote":`使用帳號登入 — 無需 API 金鑰。`,"modal.oauthComingSoon":`{label} 的 OAuth 登入將在下次更新提供。請先使用 API 金鑰。`,"modal.oauthComingSoonShort":`此供應商的 OAuth 登入將在下次更新提供 — 請先使用 API 金鑰。`,"modal.useApiKeyInstead":`改用 API 金鑰`,"modal.setupGuide":`設定指南`,"modal.setupStep1Prefix":`前往`,"modal.setupDashboardLink":`{label} 控制檯`,"modal.setupStep1Suffix":`並複製 API 金鑰`,"modal.setupStep2":`貼上到下方的 API 金鑰欄位`,"modal.setupStep3":`點選新增供應商 — 模型會自動發現`,"modal.namePlaceholder":`例如 openrouter`,"modal.duplicateWarn":`供應商 "{name}" 已存在,將被覆蓋。`,"modal.forwardHintPrefix":`無需金鑰 — 代理會轉發你的`,"modal.forwardCredentials":`codex login`,"modal.forwardHintSuffix":`憑證到此供應商。`,"modal.localHint":`不會儲存 API 金鑰。這會為 Codex 新增 Cursor 的公開模型目錄,但在審計完成前,即時 Cursor 傳輸與原生檔案/Shell 執行仍保持停用。`,"modal.getApiKey":`取得 {label} API 金鑰`,"modal.apiKey":`API 金鑰`,"modal.apiKeyTransport":`API 金鑰標頭`,"modal.apiKeyTransportNative":`x-api-key(Anthropic 原生)`,"modal.apiKeyTransportBearer":`Authorization: Bearer`,"modal.apiKeyPlaceholder":`sk-…(或 $ENV_VAR)`,"modal.defaultModelPlaceholder":`例如 gpt-5.5`,"modal.baseUrlPlaceholder":`https://...`,"modal.baseUrlPlaceholderError":`Base URL 包含未解析的 {placeholder},請替換為實際值。`,"modal.baseUrlPlaceholderHint":`請在新增前將 Base URL 中的 {placeholder} 替換為你的實際 Account ID。`,"modal.adding":`正在新增…`,"modal.useOauthLogin":`← 使用 OAuth 登入`,"nav.codexAuth":`Codex 認證`,"nav.codexSet":`Codex 設定`,"codexSet.tab.multiauth":`多帳號認證`,"codexSet.tab.prompt":`提示詞`,"codexSet.prompt.title":`提示詞層`,"codexSet.prompt.timing":`對新啟動的工作階段生效。執行中的工作階段會保留目前的提示詞設定。`,"codexSet.prompt.staleRevision":`設定已在別處變更,清單已重新載入。`,"codexSet.prompt.writeFailed":`無法儲存變更。`,"codexSet.prompt.loadFailed":`無法載入提示詞層。`,"codexSet.prompt.repair":`修復`,"codexSet.prompt.repairFailed":`無法完成修復。`,"codexSet.drift.journalPresent":`上一次寫入未完成。下次寫入時會自動復原。`,"codexSet.drift.projectionStale":`已儲存的層與 config.toml 中的值不一致。修復會依你的層重新寫入該值。`,"codexSet.drift.storeMissing":`層檔案已遺失,但 config.toml 中仍有指示。修復會先建立備份,並將該文字保留為一個層。`,"codexSet.drift.ownedMalformed":`config.toml 中產生的該行曾被手動變更,因此重寫不再安全。`,"codexSet.custom.adoptUnsupported":`{path} 第 {line} 行的值不是單行字串,無法匯入。若要在此管理,請手動移動它。`,"codexSet.prompt.unreadable":`Codex 設定檔存在但無法讀取,因此拒絕了變更。`,"codexSet.layer.permissions":`權限`,"codexSet.layer.collaboration":`協作模式`,"codexSet.layer.environment":`環境內容`,"codexSet.layer.apps":`應用程式`,"codexSet.layer.skills":`技能`,"codexSet.prompt.extensionsUnknown":`擴充功能可新增自己的層。Codex 不會公開這些層,因此無法在此列出。`,"codexSet.group.transition":`變更通知`,"codexSet.group.transitionDesc":`它們通報變化而非描述狀態,因此僅在工作階段切換為即時模式或更換模型時出現。`,"codexSet.custom.slotNote":`自訂層會依此順序合併為一個區段。`,"codexSet.row.alwaysOn":`一律啟用`,"codexSet.row.onChange":`變更時傳送`,"codexSet.row.featureGated":`在 [features] 下設定`,"codexSet.row.openFeatures":`開啟設定`,"codexSet.dialog.setValue":`{value}(預設 {fallback})`,"codexSet.dialog.copyKey":`複製設定鍵`,"codexSet.dialog.unknownLayer":`此版本沒有該層的說明。它來自比儀表板更新的 Codex 執行環境。`,"codexSet.custom.heading":`自訂層`,"codexSet.custom.add":`+ 新增層`,"codexSet.custom.newTitle":`新增層`,"codexSet.custom.editTitle":`編輯層`,"codexSet.custom.titleLabel":`標題`,"codexSet.custom.bodyLabel":`指示`,"codexSet.custom.bodySize":`{bytes}/{max} 位元組`,"codexSet.custom.normalized":`定位字元已轉換為四個空格,換行符號已轉換為 LF。`,"codexSet.custom.titleRequired":`請輸入標題。`,"codexSet.custom.titleTooLong":`標題有 {count} 個字元,上限為 {max} 個。`,"codexSet.custom.titleMultiline":`標題必須為單行。`,"codexSet.custom.bodyTooLarge":`此層為 {bytes} 位元組,上限為 {max} 位元組。`,"codexSet.custom.composedTooLarge":`啟用的層合計將達到 {bytes} 位元組,超過上限。`,"codexSet.custom.invalidCharacter":`無法儲存位置 {position} 的控制字元。`,"codexSet.custom.discardPrompt":`要捨棄變更嗎?`,"codexSet.custom.keepEditing":`繼續編輯`,"codexSet.custom.delete":`刪除 {title}`,"codexSet.custom.deleteConfirm":`要刪除此層嗎?此操作無法復原。`,"codexSet.custom.layerGone":`該層已在別處被刪除,因此編輯器已關閉。`,"codexSet.custom.deleteConfirmNamed":`要刪除“{title}”嗎?此操作無法復原。`,"codexSet.custom.moveUp":`將 {title} 上移`,"codexSet.custom.prevLayer":`上一層`,"codexSet.custom.nextLayer":`下一層`,"codexSet.custom.navPosition":`{position} / {total}`,"codexSet.custom.moveDown":`將 {title} 下移`,"codexSet.custom.limitReached":`最多可保留 {max} 個自訂層。`,"codexSet.custom.notOwned":`developer_instructions 是在 opencodex 外部寫入的,因此無法在此編輯。請將其匯入,以便作為層管理。`,"codexSet.custom.adopt":`匯入現有指示`,"codexSet.custom.adoptConfirm":`匯入為層`,"codexSet.custom.adoptRefused":`無法匯入現有值。`,"codexSet.custom.baseReplaced":`model_instructions_file 已設為 {path},因此 opencodex 外部的內容已取代基礎提示詞。`,"codexSet.lint.identity":`此內容宣稱了與 Codex 所設定身分不同的身分。`,"codexSet.lint.foreignTool":`工具由登錄檔提供;在此指定名稱並不會建立工具。`,"codexSet.lint.placeholder":`指示不會經過範本引擎處理,因此此內容會依原樣傳送。`,"codexSet.lint.applyPatch":`apply_patch 由工具登錄檔定義,而不是由指示定義。`,"codexSet.lint.approvalVocab":`Codex 會注入自己的核准用語;此內容可能與其衝突。`,"codexSet.lint.environment":`環境資訊稍後才會產生,可能與此內容衝突。`,"codexSet.lint.size":`此層超過 8 KB。仍可儲存,但每次請求都會耗用權杖。`,"codexSet.preset.blank":`空白層`,"codexSet.preset.concise.name":`精簡輸出`,"codexSet.preset.concise.description":`簡短作答,不加開場白,儘量減少格式。`,"codexSet.preset.concise.provenance":`改編自 Claude Code 的精簡指示。文案由我們原創,並非複製。`,"codexSet.preset.planFirst.name":`編輯前先規劃`,"codexSet.preset.planFirst.description":`先說明計畫,再進行變更。`,"codexSet.preset.planFirst.provenance":`改編自 Claude Code 的規劃方式。文案由我們原創,並非複製。`,"codexSet.preset.explainWhy.name":`說明理由`,"codexSet.preset.explainWhy.description":`不只說明要做什麼,也說明原因。`,"codexSet.preset.explainWhy.provenance":`改編自 Grok Build 的確認風格。文案由我們原創,並非複製。`,"codexSet.preset.testFirst.name":`測試優先`,"codexSet.preset.testFirst.description":`修正前先撰寫會失敗的測試。`,"codexSet.preset.testFirst.provenance":`改編自常見的代理實務。文案由我們原創,並非複製。`,"codexSet.preset.korean.name":`韓文回覆`,"codexSet.preset.korean.description":`無論要求使用哪種語言,都以韓文回答。`,"codexSet.preset.korean.provenance":`根據常見的使用者需求為 opencodex 撰寫。文案由我們原創,並非複製。`,"codexSet.dialog.class":`類型`,"codexSet.dialog.key":`設定鍵`,"codexSet.dialog.fileValue":`此檔案中的值`,"codexSet.dialog.absentDefault":`未設定(預設為 {value})`,"codexSet.dialog.noRenderedText":`Codex 不會公開內建層組合後的文字,因此此對話框只說明該層並列出其設定鍵,不顯示具體內容。`,"codexSet.dialog.sourceText":`傳送給模型的原文`,"codexSet.dialog.sourceBytes":`{bytes} 位元組`,"codexSet.dialog.notRendered":`在我們讀取的那一輪中,此層沒有傳送任何內容。各區段只在內容變更時才會重新傳送,因此單次取樣可能看不到它。`,"codexSet.dialog.emptySource":`{path} 檔案存在但為空,因此此層不會傳送任何內容。`,"codexSet.dialog.notExposed":`基礎提示詞不在 Codex 可列印的訊息清單中傳遞,因此無法在此顯示。可以透過 model_instructions_file 取代它。`,"codexSet.dialog.textUnavailable":`本機無法讀取 Codex 提示詞,因此無法顯示原文。`,"codexSet.class.base":`基礎指令`,"codexSet.class.config-toggle":`可在此切換`,"codexSet.class.feature-gated":`功能開關控制`,"codexSet.class.runtime-conditional":`執行時條件控制`,"codexSet.class.extension-unknown":`擴充層`,"codexSet.layer.base-instructions":`基礎指令`,"codexSet.layer.model-switch":`模型切換通知`,"codexSet.layer.personality":`個性`,"codexSet.layer.context-window-guidance":`上下文視窗指引`,"codexSet.layer.realtime":`即時工作階段`,"codexSet.layer.agents-md":`AGENTS.md`,"codexSet.layer.environments-instructions":`執行環境`,"codexSet.layer.plugins":`外掛程式`,"codexSet.layer.tools":`工具`,"codexSet.layer.multi-agent-mode":`多代理模式`,"codexSet.layer.git-attribution":`提交署名`,"codexSet.about.base-instructions":`Codex 自身的指令。它們會隨請求一同傳送,無法關閉。`,"codexSet.about.model-switch":`工作階段中途切換模型時新增。`,"codexSet.about.personality":`語氣和表達風格指引,由功能開關控制。`,"codexSet.about.context-window-guidance":`剩餘上下文預算的相關建議,由功能開關控制。`,"codexSet.about.realtime":`即時工作階段中新增。`,"codexSet.about.agents-md":`專案中的 AGENTS.md 檔案。此頁面只顯示該層,絕不會編輯專案文件。`,"codexSet.about.permissions":`說明目前生效的沙箱和核准設定。`,"codexSet.about.collaboration":`說明目前啟用的協作模式。`,"codexSet.about.environment":`工作目錄、平台及其他環境資訊。`,"codexSet.about.environments-instructions":`延後執行環境的相關指引,由功能開關控制。`,"codexSet.about.apps":`已連接應用程式的使用方式。`,"codexSet.about.plugins":`選取外掛程式或任一外掛程式宣告功能時新增。`,"codexSet.about.tools":`延後載入的工具說明,由功能開關控制。`,"codexSet.about.skills":`可用技能清單。`,"codexSet.about.multi-agent-mode":`子代理指令,由功能開關控制。`,"codexSet.about.git-attribution":`讓模型在它寫的提交加上 Co-authored-by: Codex 尾註,並在它開的拉取請求加上 Generated with Codex. 這一行。Codex 從你的帳號讀取此項,所以這裡和 [features] 都改不了。帳號關閉時,Codex 會送出相反的指令,而不是什麼都不送。`,"codexSet.condition.model-switch":`僅在工作階段中途切換模型後插入。`,"codexSet.condition.realtime":`僅在即時工作階段中插入。`,"codexSet.condition.agents-md":`找到適用於目前工作目錄的專案文件時插入。`,"codexSet.condition.plugins":`選取外掛程式或任一外掛程式宣告功能時插入。`,"codexSet.condition.git-attribution":`由你帳號的署名政策決定。`,"codexSet.base.title":`基礎提示詞`,"codexSet.base.prev":`上一個選項`,"codexSet.base.next":`下一個選項`,"codexSet.base.position":`{position} / {total}`,"codexSet.base.swipeHint":`左右滑動、按方向鍵,或點箭頭按鈕切換選項。對新開始的工作階段生效。`,"codexSet.base.defaultTitle":`Codex 自帶的基礎提示詞`,"codexSet.base.defaultBody":`預設項並不存在這裡,所以沒有可編輯或刪除的內容:選它只是從設定移除 model_instructions_file,讓 Codex 用自帶的提示詞。`,"codexSet.base.variantTitle":`名稱`,"codexSet.base.variantBody":`提示詞`,"codexSet.base.replacesWarning":`這會整體取代 Codex 自帶的基礎提示詞,而不是在其後附加。這裡寫得短,模型收到的指令就只有這麼短。`,"codexSet.base.use":`用這一個`,"codexSet.base.inUse":`正在使用`,"codexSet.base.externalBlocked":`model_instructions_file 已指向 {path},且不是 opencodex 寫的。請先自行清除,再在此處選擇。`,"nav.api":`API`,"nav.openMenu":`開啟選單`,"nav.closeMenu":`關閉選單`,"codexAuth.mainAccount":`主帳號`,"codexAuth.codexApp":`Codex App`,"codexAuth.logLabel":`日誌標籤`,"codexAuth.moreActions":`顯示更多操作`,"codexAuth.copyId":`複製帳戶 ID`,"codexAuth.appLogin":`應用登入`,"codexAuth.accountPool":`帳號池`,"codexAuth.accountModeTitle":`OpenAI 帳號模式`,"codexAuth.accountModePool":`帳號池模式`,"codexAuth.accountModePoolDesc":`主登入與符合條件的已新增帳號會在此輪換。`,"codexAuth.accountModeDirect":`直連模式`,"codexAuth.accountModeDirectDesc":`請求僅使用主登入;已新增帳號會繼續儲存,供帳號池模式使用。`,"codexAuth.openaiMissing":`未配置內建 OpenAI 供應商。`,"codexAuth.openaiDisabled":`內建 OpenAI 供應商已停用。`,"codexAuth.openaiUnavailableDesc":`你的 OpenAI 帳號仍然可用。啟用供應商後即可路由 Codex 請求。`,"codexAuth.enableOpenai":`啟用 OpenAI`,"codexAuth.enablingOpenai":`正在啟用...`,"codexAuth.enableOpenaiFailed":`無法啟用 OpenAI 供應商。`,"codexAuth.openaiPresetLoadFailed":`無法載入 OpenAI 供應商預設。`,"codexAuth.openaiPresetUnavailable":`OpenAI 供應商預設不可用。`,"codexAuth.openProviders":`開啟供應商`,"codexAuth.add":`新增`,"codexAuth.sparkQuota":`Codex Spark 配額`,"codexAuth.sparkQuotaHint":`在帳號卡片上顯示 GPT-5.3-Codex-Spark 週視窗。預設隱藏,因為只適用於單一模型。`,"codexAuth.sparkQuotaShown":`已顯示 Codex Spark 配額`,"codexAuth.sparkQuotaHidden":`已隱藏 Codex Spark 配額`,"codexAuth.sparkQuotaFailed":`無法變更 Codex Spark 配額設定`,"codexAuth.refreshQuota":`重新整理額度`,"codexAuth.refreshingQuota":`重新整理中...`,"codexAuth.quotaRefreshed":`額度已重新整理`,"codexAuth.quotaRefreshFailed":`額度重新整理失敗`,"codexAuth.noPool":`尚未新增池帳號。`,"codexAuth.fiveHour":`5 小時`,"codexAuth.weekly":`每週`,"codexAuth.monthly":`30天`,"codexAuth.resets":`重設`,"codexAuth.today":`今天`,"codexAuth.current":`當前`,"codexAuth.nextSession":`已選擇`,"codexAuth.poolPrepared":`已為帳號池準備`,"codexAuth.preparePoolTitle":`為帳號池模式準備此帳號?`,"codexAuth.preparePoolDesc":`直連請求仍使用主登入。啟用帳號池模式後,此帳號會成為預先選擇的池帳號。`,"codexAuth.prepareForPool":`為帳號池準備`,"codexAuth.poolPreparedToast":`已為帳號池模式準備 {email}`,"codexAuth.switchTitle":`切換活躍帳號?`,"codexAuth.switchDesc":`從現有和新 Codex 會話的下一次請求開始生效。進行中的請求保留原帳號。`,"codexAuth.cacheWarning":`切換帳號會重設提示快取。新會話從空快取開始。`,"codexAuth.setAsNext":`選擇帳號`,"codexAuth.cancel":`取消`,"codexAuth.switchBack":`切換回主帳號?`,"codexAuth.switchBackDesc":`現有和新 Codex 會話的下一次請求將使用應用登入帳號。`,"codexAuth.autoSwitch":`自動切換帳號`,"codexAuth.autoSwitchThreshold":`切換閾值`,"codexAuth.autoSwitchThresholdAria":`切換閾值(百分比)`,"codexAuth.autoSwitchLoadFailed":`無法載入自動切換帳號設定。`,"codexAuth.autoSwitchThresholdInvalid":`請輸入 1 到 100 之間的整數`,"codexAuth.autoSwitchUpdated":`自動切換帳號設定已更新`,"codexAuth.autoSwitchUpdateFailed":`無法確認更新。當前顯示最後一次確認的值。`,"codexAuth.pauseExhausted":`暫停已達上限帳號`,"codexAuth.pausingExhausted":`正在檢查額度...`,"codexAuth.pauseExhaustedSucceeded":`已暫停 {count} 個達到上限的帳號`,"codexAuth.pauseExhaustedNone":`沒有確認達到 100% 用量的帳號。`,"codexAuth.pauseExhaustedFailed":`無法檢查並暫停已達上限帳號。`,"codexAuth.pause":`暫停`,"codexAuth.resume":`恢復`,"codexAuth.paused":`已暫停`,"codexAuth.pauseSucceeded":`已暫停 {email}`,"codexAuth.resumeSucceeded":`{email} 已重新加入帳號池`,"codexAuth.pauseFailed":`無法暫停 {email},未做任何變更。`,"codexAuth.resumeFailed":`無法恢復 {email},未做任何變更。`,"codexAuth.pausedHint":`恢復前不會參與自動切換、重試、冷卻恢復或手動選擇。`,"anthropicPool.title":`Claude 帳號池(實驗性)`,"anthropicPool.enabledDesc":`遇到 429 時冷卻該帳號並故障轉移。新會話優先使用{window}低於 {threshold}% 的帳號。`,"anthropicPool.enabledNoProactiveDesc":`429 時將帳號冷卻並切換。門檻為 0 時停用主動的用量切換,但新工作階段選擇與 429 復原仍會使用 {window} 視窗。`,"anthropicPool.disabledDesc":`僅使用當前活躍的 Claude 帳號。僅在接受實驗性路由時啟用。`,"anthropicPool.experimentalWarning":`實驗性功能,尚未充分驗證。看起來像自動多帳號輪換的行為可能導致 Anthropic 限制帳號。同一組織可能共享配額——對這些帳號做池化沒有幫助。除非瞭解風險,否則請保持關閉。`,"anthropicPool.needTwoAccounts":`啟用帳號池前請至少新增兩個 Claude OAuth 帳號。`,"anthropicPool.threshold":`新會話用量閾值`,"anthropicPool.thresholdAria":`新會話用量閾值(百分比)`,"anthropicPool.thresholdHelp":`0 表示禁用基於配額的選擇(僅親和性 + 活躍帳號)。預設 80。`,"anthropicPool.thresholdInvalid":`請輸入 0 到 100 之間的整數`,"anthropicPool.loadFailed":`無法載入 Claude 帳號池設定。`,"anthropicPool.saveFailed":`無法儲存 Claude 帳號池設定。`,"anthropicPool.on":`開`,"anthropicPool.off":`關`,"accountPool.strategy":`輪換策略`,"accountPool.strategyDesc":`新會話如何從帳號池中選擇帳號。`,"accountPool.strategyQuota":`配額`,"accountPool.strategyRoundRobin":`輪詢`,"accountPool.strategyFillFirst":`填滿優先`,"accountPool.stickyLimit":`輪換前的粘性成功次數`,"accountPool.stickyLimitAria":`輪換前的粘性成功次數`,"accountPool.stickyLimitHelp":`在推進到下一個帳號之前,將所選帳號保留這麼多次成功的新會話繫結。`,"accountPool.stickyLimitInvalid":`請輸入 1 到 100 之間的整數`,"accountPool.strategyLoadFailed":`無法載入輪換策略。`,"accountPool.strategyUpdateFailed":`無法儲存輪換策略。`,"accountPool.quotaWindow":`配額統計區間`,"accountPool.quotaWindowDesc":`指定依配額選擇新會話、填滿優先門檻判定,以及可用 429 替代帳號所採用的快取用量。`,"accountPool.quotaWindowFiveHour":`5 小時用量`,"accountPool.quotaWindowWeekly":`每週用量`,"accountPool.quotaWindowMaxUtilization":`較高的用量`,"accountPool.quotaWindowHint":`每週用量會在仍有其他可用帳號時略過 5 小時用量已用盡的帳號;若沒有其他帳號,則會退回使用這些帳號。每週用量相同時優先挑選 5 小時用量較低者;各帳號的每週用量要等供應商頁面輪詢後才會得知。`,"accountPool.quotaWindowInert":`只有配額策略,或門檻大於 0 的填滿優先策略,才會依用量計分;在目前的輪換策略下,這項設定不會有任何作用。`,"codexAuth.switched":`下一次請求將使用 {email}`,"codexAuth.loadFailed":`無法載入 Codex 帳號設定。`,"codexAuth.switchFailed":`無法切換帳號。之前的選擇保持不變。`,"codexAuth.removeConfirm":`刪除 {id}?`,"codexAuth.removeFailed":`無法移除帳號。未進行任何更改。`,"codexAuth.addTitle":`新增 Codex 帳號`,"codexAuth.addIdLabel":`帳號 ID(識別符號)`,"codexAuth.addIdPlaceholder":`codex-work, codex-alt, team…`,"codexAuth.resetCreditsAria":`{count} 個重設額度`,"codexAuth.addJsonLabel":`auth.json 內容`,"codexAuth.addHelp":`從另一臺機器的 ~/.codex/auth.json 複製,或使用 codex-auth export。`,"codexAuth.importBtn":`匯入`,"codexAuth.importInvalidJson":`無效的 JSON`,"codexAuth.importMissingTokens":`JSON 中缺少 access_token 或 refresh_token`,"codexAuth.importMissingId":`請輸入帳號 ID`,"codexAuth.accountAdded":`帳號已新增到池中`,"codexAuth.addPickDesc":`使用另一個 ChatGPT 帳號登入以新增到池中。`,"codexAuth.oauthLogin":`OAuth 登入`,"codexAuth.oauthDesc":`在瀏覽器中開啟 ChatGPT 登入`,"codexAuth.deviceLogin":`裝置碼登入`,"codexAuth.deviceDesc":`適用於無頭或遠端代理:在另一台裝置上輸入短代碼`,"codexAuth.importAuthJson":`匯入 auth.json`,"codexAuth.importAuthJsonDesc":`從另一個 Codex 安裝或 codex-auth 匯出`,"codexAuth.back":`返回`,"codexAuth.oauthAlreadyInProgress":`登入已在進行中。請在瀏覽器中完成。`,"codexAuth.oauthWaiting":`等待瀏覽器中完成 ChatGPT 登入...`,"codexAuth.oauthSubmittingCode":`正在提交程式碼…`,"codexAuth.oauthCodeSubmitted":`程式碼已提交——正在等待登入完成…`,"codexAuth.oauthStatusRetrying":`檢查登入狀態時發生網路或代理錯誤——正在重試…`,"codexAuth.oauthCancelled":`登入已取消。`,"codexAuth.loginFailed":`登入失敗`,"codexAuth.needsReauth":`重新登入`,"codexAuth.reauthenticate":`重新認證`,"codexAuth.tokenExpired":`權杖已過期 — 請重新認證此帳號`,"codexAuth.mainTokenExpired":`權杖已過期 — 請透過 Codex 應用登入重新登入`,"codexAuth.emailCollision":`此帳號與您的主 Codex 登入相同。請使用其他帳號。`,"codexAuth.resetCreditsTitle":`重設額度`,"codexAuth.resetCreditsAvailable":`您有 {count} 個可用重設額度。`,"codexAuth.resetCreditsDesc":`每個額度可立即重設您當前的小時和每週使用限制。`,"codexAuth.noResetCredits":`沒有可用的重設額度。`,"codexAuth.earnCreditsHint":`額度每月自動發放,也可透過推薦計劃獲得。`,"codexAuth.creditsExpireNote":`額度在獲得後 30 天過期。`,"codexAuth.useOneCredit":`使用 1 個額度`,"codexAuth.confirmResetTitle":`使用重設額度?`,"codexAuth.confirmResetDesc":`這將立即重設您當前的使用限制。剩餘額度:{count} 個。`,"codexAuth.irreversible":`此操作無法復原。`,"codexAuth.useCredit":`使用額度`,"codexAuth.redeeming":`重設中...`,"codexAuth.resetSuccess":`使用限制已重設!剩餘額度:{remaining} 個。`,"codexAuth.resetSuccessGeneric":`使用限制已重設!`,"codexAuth.resetAlreadyRedeemed":`該額度已兌換過,額度未變。`,"codexAuth.resetNothingToReset":`當前沒有需要重設的使用視窗。`,"codexAuth.resetNoCredit":`沒有可用的重設額度。`,"codexAuth.resetError":`重設額度使用失敗,請重試。`,"codexAuth.fifoNote":`最早獲得的額度優先使用。`,"codexAuth.confirmWhichCredit":`將使用 {date} 獲得的額度。`,"codexAuth.creditNext":`即將使用`,"codexAuth.creditLabel":`額度 #{n}`,"codexAuth.creditNextBadge":`NEXT`,"codexAuth.creditGranted":`獲得 {date}`,"codexAuth.creditExpires":`過期 {date}(剩餘 {days} 天)`,"api.title":`API 存取`,"api.subtitle":`使用生成的 API 金鑰從外部應用存取 opencodex 代理。金鑰透過 {authHeader} 標頭認證;下表顯示每個端點接受什麼。`,"api.baseUrl":`基礎 URL`,"api.responsesEndpoint":`Responses API`,"api.chatCompletionsEndpoint":`Chat Completions API`,"api.messagesEndpoint":`Messages API`,"api.modelsEndpoint":`Models API`,"api.endpointNote":`請將基礎 URL 用於 OpenAI 相容客戶端。Responses 與 Chat Completions 在 /v1 下提供。`,"api.endpointsTitle":`閘道器端點`,"api.authTitle":`身份驗證`,"api.authLoopback":`迴環繫結(127.0.0.1 或 ::1)會跳過身份驗證。遠端繫結需要生成的 ocx_ 金鑰或 OPENCODEX_API_AUTH_TOKEN。`,"api.authBaseUrlNote":`客戶端應使用基礎 URL,然後選擇下面的協議端點。`,"api.newKeyTitle":`已建立新金鑰`,"api.newKeyNote":`請立即複製此金鑰,它不會再次顯示。`,"api.copy":`複製`,"api.copied":`已複製`,"api.dismiss":`關閉`,"api.generateTitle":`生成金鑰`,"api.keyNamePlaceholder":`金鑰名稱(可選)`,"api.generate":`生成`,"api.generating":`建立中…`,"api.activeKeys":`活躍金鑰({count})`,"api.noKeys":`還沒有 API 金鑰。請在上方生成一個。`,"api.colName":`名稱`,"api.colKey":`金鑰`,"api.colCreated":`建立時間`,"api.confirm":`確認`,"api.deleteAria":`刪除 API 金鑰`,"api.modelsTitle":`外部模型目錄`,"api.modelsCount":`{count} 個可呼叫`,"api.modelsLoading":`正在載入模型…`,"api.modelsSearch":`搜尋模型`,"api.modelsSubtitle":`請使用這些精確的模型 ID 搭配 /v1/models 和你選擇的入站協議。`,"api.modelsEmpty":`還沒有可供外部呼叫的模型。`,"api.modelsNoMatch":`沒有模型符合「{query}」。`,"api.modelsLoadFailed":`無法載入外部模型目錄。`,"api.colModel":`模型`,"api.colSource":`來源`,"api.colProtocols":`協議`,"api.sourceNative":`ChatGPT 池`,"api.sourceCombo":`組合路由`,"api.sourceCustom":`自訂`,"api.protocolResponses":`Responses`,"api.protocolChatCompletions":`Chat Completions`,"api.protocolMessages":`Messages`,"api.copyModelId":`複製 ID`,"api.modelCopied":`已複製`,"api.testModel":`測試`,"api.testingModel":`測試中…`,"api.testSucceeded":`成功`,"api.testFailed":`失敗`,"api.usageChatTitle":`Chat Completions 範例`,"api.usageResponsesTitle":`Responses 範例`,"api.usageMessagesTitle":`Messages 範例`,"api.usageSampleInput":`你好,世界!`,"api.keysLoadFailed":`無法載入 API 金鑰。`,"api.createFailed":`無法建立 API 金鑰。`,"api.deleteFailed":`無法刪除 API 金鑰。`,"api.auth.endpoint":`端點`,"api.auth.required":`必要`,"api.auth.accepted":`已接受`,"api.auth.rejected":`未接受`,"api.auth.testProtocol":`測試 {protocol}`,"api.auth.testNeedsFreshKey":`生成一組金鑰並保留其一次性數值在畫面上,才能執行已認證的測試。`,"api.key.name":`金鑰名稱`,"api.key.rename":`重新命名`,"api.key.saveName":`儲存名稱`,"api.key.renaming":`儲存中…`,"api.key.renameFailed":`無法重新命名金鑰。你的草稿已保留。`,"api.key.deleting":`刪除中…`,"api.rotation.title":`金鑰輪替`,"api.rotation.description":`簽發替代金鑰,並在短暫轉換期間保留目前金鑰。`,"api.rotation.start":`開始輪替`,"api.rotation.starting":`正在開始…`,"api.rotation.pending":`輪替尚待確認。請先更新並驗證用戶端,再提交輪替。`,"api.rotation.expires":`轉換期間截止:`,"api.rotation.secretOnce":`替代金鑰只顯示一次。關閉前請先複製。`,"api.rotation.commit":`提交輪替`,"api.rotation.abort":`中止輪替`,"api.rotation.failed":`輪替操作未完成。請重新整理後再試。`,"api.rotation.startFailed":`無法開始金鑰輪替。`,"api.key.copyFailed":`無法複製金鑰。請手動選取並複製後再關閉此面板。`,"api.attribution.title":`已歸因用量`,"api.attribution.requests7d":`請求數,最近 7 天`,"api.attribution.totalRequests":`已歸因請求總數`,"api.attribution.lastUsed":`上次使用`,"api.attribution.since":`歸因功能自啟用時間起算`,"api.attribution.neverUsed":`自歸因啟用以來未曾使用`,"api.attribution.unavailable":`用量無法取得`,"api.attribution.unavailableDetail":`尚未歸因任何用量。歸因啟用前記錄的請求無法事後補歸。`,"api.attribution.ambiguous":`兩組金鑰共用此 ID,因此用量無法歸因到其中一組。請在設定檔中為每組金鑰指定唯一 ID。`,"api.attribution.railAmbiguous":`ID 重複`,"claude.subtitle":`在 Claude Code 中使用 GPT、Gemini 等其他模型。`,"claude.pageTitle":`Claude Code`,"claude.enabledLabel":`Claude 連線`,"claude.enabledHint":`關閉後 Claude Code 無法使用此代理。`,"claude.authMode":`認證模式`,"claude.authModeHint":`Subscription 需要 Claude 帳號,Proxy 無需 Anthropic 帳號即可使用`,"claude.authModeSubscription":`Subscription(Claude 帳號)`,"claude.authModeProxy":`Proxy(無需帳號)`,"claude.authModeAuto":`自動(檢測 Claude 認證)`,"claude.effectiveMode.label":`下次啟動生效`,"claude.effectiveMode.manual":`手動:{mode}`,"claude.effectiveMode.autoPresent":`自動:訂閱 — 已透過 {source} 找到 Claude 認證`,"claude.effectiveMode.autoAbsent":`自動:代理模式 — 未找到 Claude 認證`,"claude.effectiveMode.autoUnknown":`自動:訂閱 — 無法確認認證`,"claude.effectiveMode.admissionKey":`此代理的 API 金鑰仍會傳送。`,"claude.authSource.claude-json-oauth":`Claude 帳號`,"claude.authSource.claude-credentials-file":`憑證檔案`,"claude.authSource.macos-keychain":`macOS 鑰匙串`,"claude.authSource.exported-env":`環境變數`,"claude.authSource.unknown":`檢測到的憑證`,"claude.systemEnv":`自動連線`,"claude.systemEnvDesc":`開啟後,在任意終端執行 claude 會自動透過代理。`,"claude.systemEnvUnsupported":`自動連線僅在 macOS 上可用。在此系統上,請使用 {cmd} 啟動 Claude。`,"claude.systemEnvWarn":`⚠ 需要完全退出並重新開啟終端應用才能生效。不推薦使用。`,"claude.fastMode":`Fast Mode (OpenAI)`,"claude.fastModeDesc":`控制 OpenAI 模型的推理速度。ON = 優先順序(更快)。OFF = 預設速度。Auto = 透傳客戶端設定。`,"claude.fastAuto":`Auto`,"claude.fastOn":`ON`,"claude.fastOff":`OFF`,"claude.autoContext":`自動利用大上下文`,"claude.autoContextDesc":`決定 1M 標記的範圍。開:視窗能容納壓縮門檻的模型都有大上下文條目;關:僅真正的 1M 模型有。`,"claude.autoContextInert":`配置檔案中存在舊式上下文大小值(maxContextTokens),此功能暫不生效。刪除該值即可恢復。`,"claude.autoCompactWindow":`自動摘要觸發點`,"claude.autoCompactDefault":`{value}(預設)`,"claude.autoCompactWindowDesc":`對話達到該點時自動摘要舊內容。不會超過各模型自身上限,因此 200k 模型不受影響。`,"claude.autoCompactWindowWarn":`修改該值可能導致 GPT 模型異常——若超過模型真實上限,會在摘要觸發前報錯。`,"claude.injectAgents":`自動註冊子代理`,"claude.injectAgentsDesc":`將“子代理”頁選中的模型(以及當前預設模型)註冊為 Claude Code 可派遣的代理(ocx-*)。從下一個會話開始生效。`,"claude.webSearchSidecar":`網頁搜尋附屬服務覆蓋`,"claude.webSearchSidecarHint":`僅對 Claude Code 請求覆蓋主網頁搜尋附屬服務設定。`,"claude.visionSidecar":`視覺附屬服務覆蓋`,"claude.visionSidecarHint":`僅對 Claude Code 請求覆蓋主視覺附屬服務設定。`,"claude.useMainSetting":`使用主設定`,"claude.sidecarModelPlaceholder":`主設定中的模型`,"claude.quickstart":`開始使用`,"claude.quickstartHint":`{cmd} 透過代理開啟 Claude Code。你的 claude.ai 登入保持不變。`,"claude.manualEnv":`手動配置(高階)`,"claude.smallFastModel":`背景輔助模型`,"claude.smallFastModelHint":`Claude Code 用於對話摘要、主題識別等背景工作的模型。子代理的 haiku 別名也使用它。留空 = Claude 預設(Haiku)。`,"claude.smallFastModelAccurateHint":`Claude Code 用於聊天摘要、主題識別等背景工作的模型。子代理的 haiku 別名也使用此模型。`,"claude.smallFastModelUnsetOption":`讓 Claude Code 選擇(原生模型)`,"claude.smallFastModelNativeWarning":`留空時,OpenCodex 不會設定輔助模型覆蓋項。Claude Code 可能使用其原生 Sonnet 模型,並可能產生原生供應商費用。`,"claude.slotUnset":`使用 Claude 預設值`,"claude.modelMap":`模型攔截`,"claude.modelMapHint":`攔截對特定模型的請求並重定向到你指定的模型。預設為空——新增規則後才生效。`,"claude.mapFrom":`原始模型(如 claude-sonnet-4-5)`,"claude.mapTo":`替換為(如 gemini/gemini-3-pro)`,"claude.addMapping":`新增規則`,"claude.removeMapping":`刪除規則`,"claude.aliases":`可用模型`,"claude.aliasesHint":`Claude Code 的 /model 選單中顯示的模型列表。`,"claude.aliasProviderOther":`其他`,"claude.loading":`載入中…`,"claude.loadFail":`載入 Claude 設定失敗`,"claude.saved":`已儲存。`,"claude.saveFailed":`儲存失敗`,"claude.networkError":`網路錯誤 — 代理是否在執行?`,"claude.toggleAria":`切換 Claude 連線`,"claude.none":`無`,"cws.loading":`正在載入組合…`,"cws.loadFailed":`無法載入組合。`,"cws.saveFailed":`無法儲存組合。`,"cws.removeFailed":`無法刪除組合。`,"cws.saved":`組合已儲存。`,"cws.created":`已建立 {model}。`,"cws.removed":`已刪除 combo/{id}。`,"cws.renamed":`已將 {from} 重新命名為 {to}。`,"cws.add":`新增組合`,"cws.addTitle":`新增組合`,"cws.addSubtitle":`建立跨供應商的虛擬模型,並指定客戶端實際請求的模型名稱。`,"cws.create":`建立組合`,"cws.railAria":`組合列表`,"cws.searchPlaceholder":`搜尋組合或目標…`,"cws.noSearchResults":`沒有符合的組合。`,"cws.group.failover":`容錯移轉`,"cws.group.roundRobin":`輪詢`,"cws.group.other":`其他策略`,"cws.targetCount":`{count} 個目標`,"cws.targetCountOne":`1 個目標`,"cws.overviewTitle":`組合`,"cws.overviewBlurb":`在供應商/模型目標之間依容錯移轉、輪詢、加權隨機、最少使用或最早配額重置路由的虛擬模型。`,"cws.count.total":`總計`,"cws.count.failover":`容錯移轉`,"cws.count.roundRobin":`輪詢`,"cws.count.other":`其他`,"cws.howTitle":`工作原理`,"cws.howBody":`在 Codex 中請求組合的公開模型名稱;未設定時預設使用 combo/。OpenCodex 僅在可重試的上游錯誤時切換目標。若沒有可用目標,請求會直接失敗,不會回退到全域性預設供應商。`,"cws.attentionTitle":`需要關注`,"cws.attention.empty":`未配置目標`,"cws.attention.few":`只有一個目標 — 容錯移轉無處可跳`,"cws.attention.catalogOmitted":`未出現在模型目錄中 — 成員能力不完整或不相容(缺少上下文視窗/中繼資料,或模態交集為空)。依別名路由仍可用`,"cws.attention.allTargetsExhausted":`所有已啟用目標的額度均已用盡`,"cws.emptyTitle":`建立第一個組合`,"cws.empty.createDesc":`命名虛擬模型並串聯兩個或多個後端。`,"cws.backToAll":`返回全部組合`,"cws.allCombos":`全部組合`,"cws.copyModel":`複製 ID`,"cws.copied":`已複製`,"cws.tab.config":`配置`,"cws.tab.about":`關於`,"cws.strategy":`策略`,"cws.strategy.failover":`容錯移轉`,"cws.strategy.roundRobin":`輪詢`,"cws.strategy.random":`隨機`,"cws.strategy.leastUsed":`最少使用`,"cws.strategy.resetWindow":`重置視窗`,"cws.strategy.failoverHint":`按順序嘗試目標。若出現可重試錯誤(限流、故障、訂閱門控),則跳到下一個。`,"cws.strategy.roundRobinHint":`按權重確定性地分配流量。將所選目標保留一批成功請求後,再推進到下一個目標。`,"cws.strategy.randomHint":`每個請求按權重比例隨機抽取一個可用目標,請求之間不保持黏性。`,"cws.strategy.leastUsedHint":`將每個請求路由到成功次數最少的可用目標。計數隨代理重啟歸零。`,"cws.strategy.resetWindowHint":`優先選擇配額視窗最早重置的可用目標。缺少配額資料時回退到設定順序。`,"cws.field.id":`組合 ID`,"cws.field.idHint":`客戶端將請求 {model}`,"cws.field.idInternalHint":`組合的內部 ID,建立後仍可修改。`,"cws.field.idHintEdit":`修改 ID 即重新命名組合。客戶端將請求 {model}。`,"cws.field.alias":`公開模型名稱`,"cws.field.aliasPlaceholder":`deepseek-v4-flash 或 vendor/model`,"cws.field.aliasHint":`可選。可填無字首裸名稱、自訂字首(如 vendor/model),或留空使用 combo/。`,"cws.field.stickyLimit":`輪換前的粘性成功次數`,"cws.field.stickyLimitHint":`加權選擇器推進前,將所選目標保留這麼多次成功請求。`,"cws.field.defaultEffort":`預設推理級別`,"cws.field.defaultEffortNone":`無(使用目標預設)`,"cws.field.defaultEffortHint":`僅在客戶端未指定推理級別時使用。客戶端值優先,每個目標會按自身能力進行處理。`,"cws.capability.imageInputUnavailable":`所有已選目標都支援圖片輸入後才可使用。`,"cws.capability.imageInputHint":`所有目標都支援圖片時預設開啟;關閉後僅接受文字。`,"cws.capability.imageInput":`圖片 / 多模態`,"cws.capability.adaptiveEffort":`自適應推理層級`,"cws.capability.adaptiveEffortHint":`關閉:只要有一個目標不支援推理層級,整個組合的選擇器都會消失。開啟:這些目標仍可使用,選擇器保留其餘目標共有的層級。`,"cws.capabilities":`功能`,"cws.field.defaultEffortUnsupported":`此 effort 不在目標的共同階梯中 — 請求時會被忽略或就近對應。`,"cws.field.defaultEffortUnsupportedOption":`不在交集中`,"cws.targets":`目標`,"cws.targets.failoverHint":`順序很重要 — 第一個為主。`,"cws.targets.roundRobinHint":`權重控制確定性的相對選擇;順序用於打破輪換環中的平局。`,"cws.targets.randomHint":`權重控制每次抽取的機率,順序無關緊要。`,"cws.targets.leastUsedHint":`順序僅在使用量相同的目標之間打破平局。`,"cws.targets.resetWindowHint":`配額資料缺失或相同時依順序處理。`,"cws.target.provider":`供應商`,"cws.target.model":`模型`,"cws.target.weight":`權重`,"cws.target.pickProvider":`選擇供應商…`,"cws.target.pickProviderFirst":`請先選擇供應商…`,"cws.target.pickModel":`選擇模型…`,"cws.target.noModels":`該供應商沒有模型`,"cws.target.modelPlaceholder":`模型 ID`,"cws.target.add":`新增目標`,"cws.target.drag":`拖動以重新排序`,"cws.target.moveUp":`上移`,"cws.target.moveDown":`下移`,"cws.quota.available":`可用`,"cws.quota.exhausted":`額度已用盡`,"cws.quota.unknown":`額度未知`,"cws.quota.allExhausted":`所有已啟用目標的額度均已用盡。請選擇其他目標,或等待額度恢復。`,"cws.aboutTitle":`執行時`,"cws.aboutBody":`失敗目標會短暫冷卻並遵循 Retry-After。無效請求與上下文錯誤不會切換。每個目標按自身能力調整推理級別;所有目標耗盡時直接失敗。日誌與用量會保留有序的實際嘗試及每次嘗試的用量。`,"cws.removeConfirmTitle":`刪除 {model}?`,"cws.removeConfirmDesc":`從配置與 Codex 目錄移除該虛擬模型,不會刪除任何供應商。`,"cws.unsavedTitle":`未儲存的更改`,"cws.unsavedDesc":`捨棄對此組合的編輯並繼續?`,"cws.keepEditing":`繼續編輯`,"cws.err.missingId":`需要組合 ID。`,"cws.err.invalidId":`ID 須以字母或數字開頭,僅含字母、數字、點、下劃線或連字元(最多 64)。`,"cws.err.duplicateId":`已存在相同 ID 的組合。`,"cws.err.invalidAlias":`別名僅可包含字母、數字、點、下劃線或連字元,最多一個“/”分段。`,"cws.err.aliasReservedNamespace":`別名不得使用保留的“combo/”名稱空間。`,"cws.err.aliasNativeFamily":`不允許使用 OpenAI 原生家族裸別名(gpt-*、o1-*、o3-*、o4-*、codex-*)。`,"cws.err.duplicateAlias":`另一個組合已使用該別名。`,"cws.err.noTargets":`至少新增一個目標。`,"cws.err.incompleteTarget":`每個目標都需要供應商和模型。`,"cws.target.disabled":`{name}(已停用)`,"cws.err.reservedNamespace":`建立組合前,請先重新命名名為 combo 的實體供應商。`,"cws.err.providerCollision":`組合 ID 與已配置的供應商名稱衝突。`,"cws.err.unknownProvider":`每個目標都必須使用已配置的供應商。`,"cws.err.duplicateTarget":`同一供應商/模型目標只能出現一次。`,"cws.err.invalidStickyLimit":`粘性成功次數必須是 1 到 100 的整數。`,"cws.err.invalidWeight":`每個輪詢權重必須是 1 到 10000 的整數。`,"cws.err.noEnabledTarget":`至少一個目標必須使用已啟用的供應商。`,"claude.tabsLabel":`Claude 客戶端`,"claude.tabCode":`Code`,"claude.tabDesktop":`Desktop`,"claudeDesktop.title":`Claude Desktop`,"claudeDesktop.subtitle":`將每個 Claude 模型系列路由到埠 {port} 上的可用模型。`,"claudeDesktop.importJson":`匯入 JSON`,"claudeDesktop.exportJson":`匯出 JSON`,"claudeDesktop.loading":`正在載入 Claude Desktop 配置…`,"claudeDesktop.loadFail":`無法載入 Claude Desktop 配置。`,"claudeDesktop.retry":`重試`,"claudeDesktop.saveFailed":`無法儲存 Claude Desktop 配置。`,"claudeDesktop.applyFailed":`配置已儲存,但無法套用。`,"claudeDesktop.updateFailed":`Claude Desktop 更新失敗。`,"claudeDesktop.savedApplied":`配置已儲存並套用到 Claude Desktop。`,"claudeDesktop.savedAppliedAnnounce":`Claude Desktop 配置已儲存並套用。`,"claudeDesktop.saved":`配置已儲存。`,"claudeDesktop.savedAnnounce":`Claude Desktop 配置已儲存。`,"claudeDesktop.exported":`配置已匯出為 JSON。`,"claudeDesktop.importExpected":`需要版本 1 的 Claude Desktop 配置。`,"claudeDesktop.importReady":`JSON 已匯入。請檢查草稿,然後儲存並套用。`,"claudeDesktop.importedAnnounce":`配置 JSON 已匯入。可檢查尚未儲存的更改。`,"claudeDesktop.importInvalid":`所選檔案不是有效配置。`,"claudeDesktop.importFailed":`匯入失敗。{error}`,"claudeDesktop.moved":`已將 {route} 移動到 {family}。`,"claudeDesktop.unsaved":`有未儲存的更改`,"claudeDesktop.upToDate":`配置已是最新`,"claudeDesktop.saving":`正在儲存…`,"claudeDesktop.applying":`正在套用…`,"claudeDesktop.saveApply":`儲存並套用`,"claudeDesktop.emptyTitle":`沒有可用模型`,"claudeDesktop.emptyHint":`請新增或啟用供應商,然後返回分配 Claude Desktop 路由。`,"claudeDesktop.assignmentsLabel":`Claude 模型系列分配`,"claudeDesktop.family.opus":`Opus`,"claudeDesktop.family.fable":`Fable`,"claudeDesktop.family.sonnet":`Sonnet`,"claudeDesktop.family.haiku":`Haiku`,"claudeDesktop.modelCountOne":`{count} 個模型`,"claudeDesktop.modelCountMany":`{count} 個模型`,"claudeDesktop.chooseDefault":`選擇預設模型`,"claudeDesktop.temporaryDefault":`臨時預設模型`,"claudeDesktop.laneEmpty":`將模型拖到這裡,或使用移動控制元件。`,"claudeDesktop.laneNoMatch":`該系列中沒有與搜尋符合的模型。`,"nav.grok":`Grok`,"grok.title":`Grok Build`,"grok.subtitle":`opencodex 已註冊到你的 Grok 配置中的模型。`,"grok.loading":`正在載入 Grok 狀態…`,"grok.loadFail":`無法讀取 Grok 配置。`,"grok.notConfiguredTitle":`Grok Build 尚未接入`,"grok.notConfiguredHint":`安裝 Grok 後重新啟動代理,opencodex 會把託管塊寫入:`,"grok.endpoint":`端點`,"grok.colModel":`模型`,"grok.colAlias":`Grok 別名`,"grok.colContext":`上下文`,"grok.groupNative":`原生模型`,"grok.groupRouted":`路由模型`,"grok.enabledCount":`已註冊 {on}/{total}`,"grok.saved":`選擇已儲存。`,"grok.savedApplied":`選擇已儲存並寫入 Grok 配置。`,"grok.saveFailed":`無法儲存 Grok 選擇。`,"grok.applyFailed":`選擇已儲存,但無法更新 Grok 配置。`,"grok.applySkipped":`選擇已儲存,Grok 配置未更改。`,"grok.saveApply":`儲存並套用`,"grok.saving":`儲存中…`,"grok.applying":`套用中…`,"grok.unsaved":`未儲存的更改`,"grok.upToDate":`選擇已是最新`,"grok.toggleModel":`將 {id} 註冊到 Grok`,"claudeDesktop.available":`可用`,"claudeDesktop.defaultBadge":`預設`,"claudeDesktop.supports1m":`1M`,"claudeDesktop.unavailable":`不可用`,"claudeDesktop.contextM":`{n}M 上下文`,"claudeDesktop.contextK":`{n}k 上下文`,"claudeDesktop.contextUnknown":`上下文未知`,"claudeDesktop.alias":`別名`,"claudeDesktop.useAsDefault":`設為 {family} 預設模型`,"claudeDesktop.moveTo":`移動到`,"claudeDesktop.move":`移動`,"claudeDesktop.status.applied":`已套用到 Desktop`,"claudeDesktop.status.stale":`配置已更改 — 需重新套用`,"claudeDesktop.status.notApplied":`未套用`,"claudeDesktop.status.notActiveProfile":`Desktop 正在使用其他配置 — 請重新套用`,"claudeDesktop.health.lastRequest":`最後請求`,"claudeDesktop.health.stats":`{count} 請求 / {errors} 錯誤`,"claudeDesktop.effort.supported":`effort`,"claudeDesktop.effort.displayOnly":`effort (僅顯示)`,"startup.backToDashboard":`返回儀表板`,"startup.repair":`修復`,"startup.repairing":`正在修復…`,"startup.serviceRepaired":`背景服務修復成功。`,"startup.shimRepaired":`Codex 啟動器 shim 修復成功。`,"sub.workspace.addToFeatured":`將 {m} 加入精選`,"sub.workspace.allModels":`所有模型`,"sub.workspace.featuredFull":`精選列表已滿(最多 5 個)`,"sub.workspace.mainAria":`子代理模型詳情`,"sub.workspace.notFeatured":`未設為精選`,"sub.workspace.priority":`優先順序`,"sub.workspace.removeFromFeatured":`將 {m} 自精選移除`,"sub.workspace.selectModel":`選擇模型`,"sub.workspace.selectModelDesc":`從列表選擇模型以查看詳情,並設為 spawn_agent 的精選模型。`,"sub.workspace.selector":`公開選擇器`,"logs.badge.grok":`Grok`,"logs.tokens.contextTotal":`作用中上下文`,"usage.workspace.report":`用量報告`,"usage.workspace.sections":`用量分區`,"storage.rescanned":`掃描完成。`,"storage.snapshot.lastScan":`上次掃描`,"storage.snapshot.scanning":`掃描中…`,"storage.snapshot.unavailable":`尚無掃描。`,"storage.cleanupCard.title":`釋放空間`,"storage.cleanupCard.tabs":`清理選項`,"storage.cleanupCard.tab.policy":`原則`,"storage.cleanupCard.tab.quarantine":`隔離區`,"storage.cleanup.noArchives":`沒有可清理的封存工作階段。`,"storage.workspace.overview":`概覽`,"storage.workspace.selectBucket":`從列表選擇儲存區以查看明細。`,"storage.policy.trigger":`觸發條件`,"storage.policy.thresholdInc":`提高閾值`,"storage.policy.thresholdDec":`降低閾值`,"storage.policy.percentInc":`提高百分比`,"storage.policy.percentDec":`降低百分比`,"storage.policy.reduceInc":`提高縮減目標`,"storage.policy.reduceDec":`降低縮減目標`,"pws.dashboard.noRateLimits":`尚無速率限制資料`,"codexAuth.autoSwitchThresholdInc":`提高切換閾值`,"codexAuth.autoSwitchThresholdDec":`降低切換閾值`,"accountPool.stickyLimitInc":`提高黏性上限`,"accountPool.stickyLimitDec":`降低黏性上限`,"api.activeKeysLoading":`有效金鑰`,"api.workspace.details":`API 金鑰詳情`,"api.workspace.keyDetails":`金鑰詳情`,"api.workspace.keyPrefix":`金鑰前綴`,"api.workspace.deleteKey":`刪除金鑰`,"api.workspace.deleteConfirm":`確定要刪除此金鑰嗎?此操作無法復原。`,"api.workspace.usageExamples":`用法範例`,"api.copyUrlHint":`點擊以複製 URL`,"api.urlCopied":`已複製 URL`,"api.copyExampleHint":`點擊以複製範例`,"api.exampleCopied":`已複製範例`,"claude.workspace.settings":`設定`,"sidebar.star":`在 GitHub 上加星`,"sidebar.starred":`已在 GitHub 加星`,"sidebar.starUnauthenticated":`開啟 GitHub 加星(gh CLI 未登入)`,"sidebar.starFailed":`無法透過 gh 加星,改為開啟 GitHub。`,"sidebar.updateAvailable":`有可用更新:{version}`,"sidebar.checkUpdate":`檢查更新`,"dash.mem.jsHeapArena":`arena {total}`,"dash.mem.pressure":`相對於警告閾值`,"dash.mem.pressureOf":`警告閾值的 {pct}%`,"dash.mem.pressureUnknown":`未回報閾值`,"dash.injectionManage":`開啟設定`,"dash.syncModelsHint":`根據已連接的供應商重寫 Codex 的模型目錄。`,"dash.syncRun":`立即同步`,"sub.settings":`設定`,"sub.sections":`子代理分區`,"sub.delegation.model":`優先調用的模型`,"sub.delegation.modelHint":`Codex 分派工作時最先調用的模型。上面的推薦是可調用的名單,這裡選的是其中第一順位。`,"debug.loadFailed":`無法載入偵錯設定。`,"provider.name.volcengine":`Volcengine Ark`,"provider.name.volcengineCodingPlan":`Volcengine Ark Coding Plan`,"provider.name.volcengineAgentPlan":`Volcengine Ark Agent Plan`,"usage.range.available":`可用歷史紀錄`,"usage.historyTruncated":`總計僅涵蓋可用歷史紀錄,因為較舊的用量未被載入。`,"usage.historyTruncatedWindow":`已載入紀錄的請求開始時間介於 {start} 到 {end} 之間。受讀取上限限制,檔案較前的項目已被略過,所選期間可能不完整。`,"codexAuth.autoSwitchQuotaDesc":`配額:使用率達 {threshold}% 或以上時,下一個請求可能移至用量較低的合格帳號,包括已綁定的任務;Go/Free 僅使用 30 天。`,"codexAuth.autoSwitchQuotaOffDesc":`基於用量的主動切換已關閉。新增/未綁定分派與故障恢復仍然適用。`,"codexAuth.autoSwitchRoundRobinDesc":`輪詢分派不使用此閾值;它會繼續輪換新增/未綁定的任務。`,"codexAuth.autoSwitchFillFirstDesc":`優先填滿:{threshold}% 是新增/未綁定任務的耗盡點;健康的已綁定任務保留其帳號。`,"codexAuth.autoSwitchFillFirstOffDesc":`優先填滿對新增/未綁定任務沒有用量耗盡點;冷卻、重新驗證與故障恢復仍可改變路由。`,"codexAuth.failureRecoveryNote":`故障恢復是獨立的:請求在輸出前被拒絕(429/402)、冷卻、重新驗證、排除或已設定的暫時容錯移轉,可能選擇另一個合格帳號。`,"accountPool.strategyHintQuota":`配額也可以在跨越用量閾值後,於下次請求時重新綁定現有任務。`,"accountPool.strategyHintRoundRobin":`輪詢僅輪換沒有有效綁定的任務;用量閾值不會改變正常輪換。`,"accountPool.strategyHintFillFirst":`優先填滿將閾值用作未綁定任務的耗盡點;健康的已綁定任務保持親和性。`,"accountPool.unboundDefinition":`新增/未綁定任務表示沒有當前帳號綁定的請求;現有可見任務在代理或親和性重設後可能變成未綁定。`,"api.workspace.sections":`API 分區`,"api.section.keys":`金鑰`,"api.section.connect":`連接`,"api.section.endpoints":`端點`,"api.section.models":`模型`,"api.section.examples":`範例`,"api.clientConfig.title":`用戶端設定`,"api.clientConfig.rowsLabel":`連接用戶端`,"api.clientConfig.details":`詳情`,"api.clientConfig.detailsAria":`{client} 設定詳情`,"api.clientConfig.copyAria":`複製 {client} 設定 JSON`,"api.clientConfig.downloadAria":`下載 {client} 設定`,"api.clientConfig.rowMeta":`{destination} · {count} 個模型`,"api.clientConfig.rowError":`無法建構 {client} 設定。`,"api.clientConfig.copiedAnnounceClient":`{client} 設定 JSON 已複製到剪貼簿。`,"api.clientConfig.clientOpencode":`OpenCode`,"api.clientConfig.clientPi":`Pi`,"api.clientConfig.copy":`複製 JSON`,"api.clientConfig.download":`下載`,"api.clientConfig.loading":`正在建構用戶端設定…`,"api.clientConfig.jsonLabel":`{client} 設定 JSON`,"api.clientConfig.destination":`目標檔案`,"api.clientConfig.envHint":`啟動前設定金鑰`,"api.clientConfig.mergeWarning":`將此合併到目標檔案。替換它會丟失您的其他供應商和 MCP 設定。`,"api.clientConfig.modelCount":`已匯出 {count} 個模型`,"api.clientConfig.missingLimits":`{total} 個模型中有 {count} 個未附帶上下文限制;用戶端會套用自己的預設值。`,"api.clientConfig.noKeyYet":`{env} 背後尚無金鑰。在非回送環境使用此設定前,請先在上面產生金鑰。`,"api.clientConfig.loadFailed":`無法讀取模型清單,因此未產生用戶端設定。`,"api.clientConfig.copiedAnnounce":`用戶端設定 JSON 已複製到剪貼簿。`,"api.clientConfig.copyFailed":`無法複製用戶端設定 JSON。`,"api.clientConfig.downloadedAnnounce":`已下載 {filename}。目前尚未變更任何內容 — 請自行將其合併到 {destination}。`,"api.clientConfig.whereDisclosure":`此檔案的放置位置`,"api.clientConfig.whereBody":`上述目標是全域路徑。工作目錄中的專案本地設定檔優先於它,且用戶端從設定中指定的環境變數讀取金鑰 — 絕不從此檔案讀取。`,"api.attribution.totalRequestsAvailable":`可用歷史紀錄中的請求`,"api.attribution.sinceAvailable":`可用歸因起始自`,"uptime.day":`天`,"uptime.hour":`小時`,"uptime.minute":`分鐘`,"uptime.second":`秒`,"auth.adminTokenTitle":`OpenCodex 管理員金鑰 (OPENCODEX_ADMIN_AUTH_TOKEN)`,"auth.adminAccountLabel":`帳號`,"auth.adminTokenFieldLabel":`管理員金鑰`,"auth.adminTokenRejected":`該管理員金鑰被拒絕。請檢查後再試一次。`,"auth.adminTokenUnavailable":`無法驗證管理員金鑰。請再試一次。`,"lang.nativeName":`繁體中文`,"provider.name.commandCodeAuth":`Command Code - Auth`,"provider.name.commandCodeApi":`Command Code - API`,"routing.title":`路由智能 (beta)`,"routing.subtitle":`策略設定檔、試運行評估,以及有來源依據的路由分析。`,"routing.loadFailed":`無法載入路由資料`,"routing.empty":"尚未配置路由策略。請在 config.json 中新增 `routingProfiles`。","routing.revision":`rev`,"routing.detail":`設定檔`,"routing.createProfile":`建立設定檔`,"routing.dryRunError":`試運行失敗(HTTP {status})`,"routing.removeConfirm":`移除設定檔 {id}?`,"routing.unknownEvidence.allow":`允許`,"routing.unknownEvidence.penalize":`懲罰`,"routing.unknownEvidence.exclude":`排除`,"routing.removeCandidate":`移除候選 {provider}/{model}`,"routing.candidates":`候選`,"routing.require":`嚴格要求`,"routing.optimize":`最佳化權重`,"routing.limits":`限制`,"routing.unknownEvidence":`未知證據策略`,"routing.compatibility.title":`相容性策略`,"routing.compatibility.enabled":`要求 Compatibility Lab 證據`,"routing.compatibility.requiredSuites":`必要套件`,"routing.compatibility.loadingCatalog":`正在載入 Lab 目錄…`,"routing.compatibility.catalogUnavailable":`Lab 目錄不可用 — 請在 config.json 中手動輸入套件 ID。`,"routing.compatibility.layer.protocol_conformance":`協定一致性`,"routing.compatibility.layer.live_route_compatibility":`即時路由相容性`,"routing.compatibility.minStatus":`最低相容性狀態`,"routing.none":`無`,"routing.unavailable":`–`,"routing.dryRun":`試運行評估`,"routing.dryRunContext":`請求上下文視窗(tokens)`,"routing.dryRunTools":`請求需要工具`,"routing.dryRunImage":`請求需要圖片輸入`,"routing.dryRunStructured":`請求需要結構化輸出`,"routing.dryRunRun":`評估候選`,"routing.candidate":`候選`,"routing.eligible":`合格`,"routing.exclusions":`排除項目`,"routing.costCap":`成本上限`,"routing.capOutcome.satisfied":`未超過上限`,"routing.capOutcome.exceeded":`超過上限`,"routing.capOutcome.unknown-allowed":`未知(允許)`,"routing.capOutcome.unknown-excluded":`未知(排除)`,"routing.exclusion.capability-unsatisfied":`能力未滿足`,"routing.exclusion.unknown-capability":`能力未知`,"routing.exclusion.cost-limit":`超過成本上限`,"routing.exclusion.cost-limit-unknown":`成本未知(在上限內)`,"routing.exclusion.cooldown":`冷卻中`,"routing.exclusion.unknown-health":`健康狀態未知`,"routing.exclusion.unknown-quota":`額度未知`,"routing.exclusion.unknown-price":`價格未知`,"routing.exclusion.other":`排除:{code}`,"routing.score":`分數`,"routing.selected":`已選取`,"routing.yes":`是`,"routing.no":`否`,"routing.analytics":`路由分析`,"routing.analyticsTotal":`請求`,"routing.analyticsSuccessRate":`成功率`,"routing.analyticsFallbackRate":`備援`,"routing.analyticsP50":`p50`,"routing.analyticsP95":`p95`,"routing.analyticsP99":`p99`,"routing.analyticsCooldown":`冷卻失敗`,"routing.analyticsConfidence":`可信度`,"routing.analyticsTruncated":`已截斷的歷史`,"routing.analyticsRequests":`請求`,"routing.analyticsEmpty":`尚無分析資料 — 請先傳送一些請求。`,"dash.updateVersionTransition":`{currentVersion} -> {latestVersion}。`,"prov.loginSameAccount":`仍是同一個 {provider} 帳號 — 請在瀏覽器中切換帳號後,再試一次「新增帳號」。`,"models.tab.catalog":`模型`,"models.tab.combos":`組合`,"models.tab.compatibility":`相容性`,"models.tab.routing":`路由 (beta)`,"models.tabsLabel":`模型介面`,"models.subtitle.combos":`將多個模型合成一個 id 來回答。用容錯移轉串接目標,或用均衡策略分攤負載。`,"models.subtitle.compatibility":`來自實驗室投影證據的唯讀相容性判定矩陣。`,"models.subtitle.routing":`原則設定檔、dry-run 評估,以及有來源依據的路由分析。`,"models.contextSettings":`自訂視窗`,"models.contextSettingsTitle":`自訂視窗 — {provider}`,"models.contextDefault":`供應商預設`,"models.contextModel":`模型`,"models.contextModelOverride":`模型覆寫`,"models.contextHint":`已經知道視窗時,在這裡手寫 Codex 實際視窗。上游沒回報視窗就用這個值;上游回報更大視窗才壓低。留空則使用供應商的「預設視窗 / 上限」;那個開關沒開時才回退 128k。`,"models.contextAutomatic":`自動偵測`,"models.contextSaved":`上下文視窗已更新 — 將在下一個 Codex 回合生效。`,"models.contextUnchanged":`沒有需要儲存的上下文視窗變更。`,"models.contextSaveFailed":`儲存上下文視窗失敗`,"models.contextInvalid":`上下文視窗必須是正整數`,"logs.detail.route.section":`路由決策`,"logs.detail.route.kind":`路由類型`,"logs.detail.route.profile":`原則設定`,"logs.detail.route.selected":`已選取`,"logs.detail.route.candidates":`候選`,"logs.detail.route.unknown":`此請求沒有記錄路由追蹤(追蹤前的列)。`,"logs.detail.source.user":`供應商設定的價格覆蓋`,"logs.detail.attempt.recovery.transient5xx":`暫時性 5xx`,"logs.detail.attempt.recovery.connectionReset":`連線重設`,"logs.detail.attempt.recovery.oauth401":`OAuth 重新驗證`,"logs.detail.attempt.recovery.key429":`金鑰被限流 (429)`,"logs.detail.attempt.recovery.rateLimit429":`被限流 (429)`,"logs.detail.attempt.recovery.anthropicOauth429":`Anthropic OAuth 被限流 (429)`,"logs.detail.attempt.recovery.image413":`圖片承載過大 (413)`,"logs.detail.attempt.recovery.emptyCompletion":`空白完成重試`,"logs.detail.attempt.recovery.unknown":`未知的復原原因`,"logs.detail.estimate.provider_cost_overlay":`已使用供應商設定的價格覆蓋。`,"logs.detail.estimate.priority_lower_bound":`無法取得已確認的 Priority 價格;目前顯示的估算是已知下限。`,"pws.cockpitImportDescription":`從此裝置匯入 Cockpit Tools Antigravity JSON 匯出檔。不會顯示檔案內容。`,"pws.cockpitImportFileLabel":`Cockpit Tools Antigravity JSON 匯出檔`,"pws.cockpitImportChooseFile":`選擇 JSON 檔案`,"pws.cockpitImporting":`匯入中…`,"pws.cockpitImportInvalid":`選取的檔案不是有效的 JSON 匯出檔,或檔案過大。`,"pws.cockpitImportFailed":`無法完成帳號匯入。`,"pws.cockpitImportComplete":`匯入完成:已匯入 {imported} 個、已更新 {updated} 個、失敗 {failed} 個、不支援 {unsupported} 個。`,"pws.accountModeSaved":`帳號模式已儲存。`,"pws.accountModeFailed":`無法切換帳號模式。`,"pws.accountModeConfirm":`要切換 OpenAI 帳號模式嗎?進行中的對話將重新指派到另一種模式的帳號集合,配額用量將依新模式追蹤。`,"pws.capacity.estimate":`依設定權重的帳號池估算`,"pws.capacity.currentAccount":`目前有效帳號`,"pws.capacity.nextRecovery":`下一次容量復原`,"pws.capacity.recoveryShare":`+{percent}% 帳號池容量`,"pws.capacity.incomplete":`覆蓋不完整:已排除 {excluded} 個帳號`,"pws.capacity.uncalibratedPlan":`{count} 個帳號使用未校準方案,以基準席次權重計入,因此此估算可能偏保守`,"pws.capacity.partial":`部分視窗覆蓋:{count} 個帳號未回報所有顯示的限額視窗`,"pws.capacity.windowPartial":`部分`,"pws.capacity.windowPartialA11y":`{window}:帳號覆蓋不完整`,"pws.connectionNotApplicable":`不適用 — 此供應商使用靜態模型目錄。`,"nav.integrations":`整合`,"integrations.subtitle":`將客戶端連線到 opencodex、管理憑證,並還原客戶端設定。`,"integrations.tabsLabel":`整合表面`,"integrations.tab.overview":`總覽`,"integrations.tab.keys":`API 金鑰`,"integrations.tab.codex":`Codex`,"integrations.tab.claude":`Claude`,"integrations.tab.grok":`Grok Build`,"integrations.tab.opencode":`OpenCode`,"integrations.tab.pi":`Pi`,"integrations.tab.omp":`OMP`,"integrations.tab.hermes":`Hermes`,"integrations.tab.openclaw":`OpenClaw`,"integrations.tab.kimi":`Kimi Code`,"integrations.tab.gajae":`Gajae Code`,"integrations.tab.dsh":`DSH`,"integrations.tab.mcode":`MiniMax Code`,"integrations.tab.zcode":`ZCode`,"integrations.tab.prime":`Prime Agent`,"integrations.tab.aside":`Aside`,"integrations.codex.title":`Codex CLI`,"integrations.codex.body":`Codex 連線由代理服務管理。啟動 opencodex 時套用;停止服務時還原原生路由。`,"integrations.codex.openService":`開啟服務控制`,"integrations.state.notInstalled":`未安裝`,"integrations.state.unknown":`檢查中…`,"integrations.detail.codexRouted":`Codex 請求經由此代理`,"integrations.detail.codexAbsent":`Codex 尚未經由此代理路由`,"integrations.detail.keyCount":`已簽發 {count} 個金鑰`,"integrations.detail.keyNone":`尚未簽發金鑰`,"integrations.detail.keyChecking":`檢查中…`,"integrations.detail.keyUnavailable":`無法取得金鑰狀態`,"integrations.detail.claudeOff":`連線已關閉`,"integrations.detail.desktopCurrent":`Desktop 正在使用此設定檔`,"integrations.detail.desktopStale":`設定檔在套用後被變更`,"integrations.detail.desktopNotServed":`設定檔存在,但 Desktop 使用的是另一個`,"integrations.detail.desktopAbsent":`未套用任何設定檔`,"integrations.detail.desktopDesiredOff":`Claude Desktop 整合已關閉`,"integrations.detail.desktopDesiredOffCleanupPending":`Claude Desktop 仍在使用閘道,清理尚未完成`,"integrations.detail.desktopDesiredOnNotApplied":`整合已開啟,但 Desktop 未使用閘道設定檔`,"integrations.detail.desktopSelectedElsewhere":`Desktop 正在使用其他設定檔`,"integrations.detail.desktopProfileDrift":`選取的 Desktop 設定檔已變更`,"integrations.detail.desktopObservedUnsafe":`無法安全變更選取的 Desktop 設定檔`,"integrations.detail.desktopNotInstalled":`未安裝 Claude Desktop 設定程式庫`,"integrations.detail.grokModels":`已接入 {count} 個模型`,"integrations.detail.grokAbsent":`設定中沒有 opencodex 區塊`,"integrations.dialog.grok.title":`要停用 Grok Build 整合嗎?`,"integrations.dialog.grok.changes":`只會從 {path} 移除由 opencodex 標記的區塊。區塊之外寫入的內容將保持不變。`,"integrations.dialog.grok.breakage":`停用後,Grok Build 中的 opencodex 模型別名將消失。透過 xAI 帳號使用的模型不受影響。`,"integrations.dialog.grok.undo":`如果 opencodex 正在 loopback 位址上執行,重新啟用時會根據目前可用的模型寫入新的區塊。`,"integrations.dialog.grok.confirm":`停用`,"integrations.dialog.desktop.title":`要停用 Claude Desktop 整合嗎?`,"integrations.dialog.desktop.changes":`如果 {path} 包含 opencodex 管理的閘道設定檔,Desktop 會先選取新的免憑證標準設定檔,再移除舊設定檔與其備份。`,"integrations.dialog.desktop.breakage":`Claude Desktop 將恢復為標準 Claude,不再使用經由 opencodex 路由的模型。`,"integrations.dialog.desktop.undo":`重新啟用時,會根據你儲存的模型指派重新產生 opencodex 設定檔。`,"integrations.dialog.desktop.restart":`Claude Desktop 僅在啟動時讀取此設定。請完全結束並重新開啟 Desktop,變更才會生效。`,"integrations.dialog.desktop.confirm":`停用`,"integrations.native.msg.nonLoopbackRemoved":`只有當 opencodex 在 loopback 位址上執行時,才能自動註冊 Grok Build。先前指向 loopback 的區塊已移除。`,"integrations.native.msg.nonLoopbackRemovedNoop":`只有當 opencodex 在 loopback 位址上執行時,才能自動註冊 Grok Build。沒有需要移除的舊區塊。`,"integrations.native.msg.nonLoopbackSuperseded":`只有當 opencodex 在 loopback 位址上執行時,才能自動註冊 Grok Build。在此期間,另一個程序寫入了新的區塊,因此檔案中目前的區塊並非由本次請求建立。`,"integrations.native.error.orphanedMarker":`{path} 有 opencodex 開始標記但沒有結束標記。由於 opencodex 無法判斷其區塊的結束位置,因此未變更檔案。`,"integrations.native.error.homeMismatch":`已安裝服務的 home 與目前的 home 不符,因此未變更檔案。`,"integrations.native.error.notInstalled":`尚未安裝 Grok Build,因此沒有可變更的內容。`,"integrations.native.error.configBusy":`設定正在其他地方儲存中,無法變更。請稍後再試。`,"integrations.native.error.desktopUnsafeMetadata":`無法安全讀取 {path} 中的 Claude Desktop 中繼資料,因此未變更其設定庫。`,"integrations.native.error.desktopCleanupIncomplete":`Claude Desktop 已指向標準模式,但仍有舊的 opencodex 憑證檔案殘留於:{paths}。`,"integrations.native.msg.desktopDisabled":`Claude Desktop 整合已停用。`,"integrations.native.msg.desktopEnabled":`Claude Desktop 整合已啟用。`,"integrations.state.absent":`未套用`,"integrations.state.current":`已套用`,"integrations.state.stale":`需要更新`,"integrations.state.conflict":`衝突`,"integrations.state.unsafe":`無法驗證`,"integrations.summary.detected":`偵測到的用戶端`,"integrations.summary.applied":`已設定的用戶端`,"integrations.summary.stale":`需要更新`,"integrations.summary.lastChange":`上次變更`,"integrations.summary.disableAll":`全部停用…`,"integrations.onboarding":`套用時會先儲存備份,再寫入一個 opencodex 供應商區塊。停用只會移除該區塊,且可從保留的快照還原。`,"integrations.empty.title":`未偵測到已安裝的用戶端`,"integrations.empty.body":`安裝受支援的用戶端,然後返回此處套用 opencodex。`,"integrations.action.apply":`套用`,"integrations.action.disable":`停用`,"integrations.action.refresh":`更新`,"integrations.action.settings":`設定`,"integrations.action.manageKeys":`管理金鑰`,"integrations.action.restore":`還原…`,"integrations.action.undo":`復原`,"integrations.action.restorePoint":`還原到此時間點…`,"integrations.action.snapshotExpired":`備份已過期`,"integrations.rollback.title":`還原中心`,"integrations.rollback.empty":`尚無套用紀錄`,"integrations.rollback.emptyBody":`每次成功寫入前都會先保留一份寫入前快照。`,"integrations.catalog.title":`用戶端`,"integrations.rollback.older":`較早的操作`,"integrations.rollback.showMore":`再顯示 {n} 個`,"integrations.rollback.failed":`無法載入還原紀錄。`,"integrations.restore.title":`要還原此快照?`,"integrations.restore.body":`系統會先備份目前的檔案,再用所選快照取代它。`,"integrations.restore.driftTitle":`偵測到較新的編輯`,"integrations.restore.driftBody":`此快照之後的變更會先備份,然後再取代檔案。`,"integrations.restore.confirm":`還原`,"integrations.restore.confirmDrift":`備份較新的編輯並還原`,"integrations.restore.pending":`正在還原…`,"integrations.restore.manual":`自動還原失敗:{reason}。請從 {path} 手動還原。`,"integrations.error.load":`無法載入整合狀態。`,"integrations.error.stale":`最近的重新整理失敗。下列值可能已過期。`,"integrations.error.busy":`此用戶端的另一項變更仍在進行中。請稍後再試。`,"integrations.error.conflict":`opencodex 寫入後設定又變更了。未移除任何內容。`,"integrations.error.unsafe":`無法安全地變更設定。`,"integrations.error.generic":`整合變更失敗。已保留你先前的狀態。`,"integrations.error.nonLoopback":`{client} 只能連線到 localhost 上的 proxy——其設定沒有位置可放入遠端繫結所需的准入標頭,手動撰寫也不會有幫助。請改以隧道或本機轉發器提供 loopback 存取。`,"integrations.status.installed":`已安裝`,"integrations.status.notInstalled":`未安裝`,"integrations.status.appliedAt":`已套用`,"integrations.status.backup":`備份`,"integrations.status.lastRestore":`上次還原`,"integrations.status.unknown":`未知`,"integrations.bulk.title":`要停用已套用的用戶端整合?`,"integrations.bulk.body":`只會移除屬於 opencodex 的區塊。每個用戶端都會先保留一份寫入前快照。`,"integrations.bulk.partial":`部分用戶端無法停用:{clients}`,"integrations.bulk.success":`已套用的用戶端整合已停用。`,"integrations.retention.degraded":`備份清理進度落後;磁碟上可能仍有較舊的備份。`,"integrations.error.residual":`檔案可能處於中間狀態:{message} 請從 {path} 還原。`,"integrations.error.recover":`{message} 備份位於 {path}。`,"integrations.kind.apply":`已套用`,"integrations.kind.disable":`已停用`,"integrations.kind.refresh":`已更新`,"integrations.kind.restore":`已還原`,"integrations.kind.overwrite":`已覆寫`,"integrations.dialog.overwrite.title":`替換這個設定檔中的區塊?`,"integrations.dialog.overwrite.changesUnowned":`{path} 中 opencodex 需要寫入的位置被一個並非我們寫入的區塊佔用。套用會將其替換為 opencodex 寫入的區塊。`,"integrations.dialog.overwrite.changesForeign":`{path} 中 opencodex 區塊內你所做的修改會被捨棄,並替換為 opencodex 寫入的區塊。`,"integrations.dialog.overwrite.breakage":`該區塊原本設定的內容將不再生效。檔案其他位置保持不變。`,"integrations.dialog.overwrite.undo":`會先儲存快照,因此這次操作會出現在下方的還原清單中,可以復原。`,"integrations.dialog.overwrite.confirm":`替換`,"integrations.action.overwrite":`替換`,"integrations.semantics.opencode":`僅適用於直接從磁碟啟動;ocx opencode 的環境注入優先。`,"integrations.semantics.pi":`適用於新工作階段。`,"integrations.semantics.omp":`重新啟動 OMP 以載入模型目錄。`,"integrations.semantics.hermes":`適用於新工作階段。`,"integrations.semantics.openclaw":`立即套用到正在執行的閘道。`,"integrations.semantics.kimi":`重新啟動或執行 /reload 以套用(v2 會監視該檔案)。`,"integrations.semantics.gajae":`在新工作階段中或開啟 /model 時生效。`,"integrations.semantics.dsh":`OpenCodex 只管理 $DSH_HOME/settings.yaml 中的 llm-pi-ai.providers.opencodex。DSH 會熱重載該 provider;你的預設模型與 deepseek-official 維持不變。目前僅支援 loopback,且不會寫入真實憑證。`,"integrations.semantics.mcode":`僅管理 custom_provider.opencodex,不會變更預設模型或 MiniMax 登入狀態。`,"integrations.semantics.zcode":`僅管理 ~/.zcode/v2/config.json 中的 provider.opencodex,不會變更 Z.ai 登入狀態或其他供應商。變更後請重新啟動 ZCode。`,"integrations.semantics.prime":`僅管理 Prime Agent 的 models.json 中的 providers.opencodex;預設位於 ~/.prime/agent,若設定 PRIME_AGENT_CODING_AGENT_DIR 則以其為準。不會變更其他供應商或模型覆寫設定。對新工作階段生效。`,"integrations.semantics.aside":`僅管理已登入帳號的 Aside models.json 中的 providers.opencodex,位於 ~/.aside/u/<帳號>。不會變更其他供應商。Aside 在執行時會重寫該檔案,因此套用後請完全結束並重新開啟 Aside。`,"codexAuth.pinned":`已固定`,"codexAuth.pinnedHint":`你手動選取了此帳號,因此較高的選擇順序不會越過它。此固定會持續到該帳號用盡、你改選其他帳號,或你變更任一選擇順序為止。`,"codexAuth.requestUserInput":`在 Default 模式中要求輸入`,"codexAuth.requestUserInputDesc":`讓 Codex 暫停 Default 模式工作階段,並使用 request_user_input 工具向你提問。`,"codexAuth.requestUserInputUpdated":`功能旗標已更新 - 適用於新工作階段。`,"codexAuth.requestUserInputUpdatedRestart":`功能旗標已更新 - 適用於新工作階段。請重新啟動 Codex 應用程式以套用。`,"codexAuth.requestUserInputUpdateFailed":`無法更新功能旗標。未做任何變更。`,"codexAuth.requestUserInputLoadFailed":`無法從 config.toml 讀取功能旗標。`,"codexAuth.accountPickerTitle":`從模型選擇器指定特定的 Codex 帳號`,"codexAuth.accountPickerOffDesc":`啟用後,一般 GPT 選擇器選項會替換為每個帳號選擇器對應的選項,讓你可以不需登出就為對話指定確切帳號。關閉此功能不會移除任何帳號。`,"codexAuth.accountPickerOnDesc":`每個選擇器都是某個已儲存帳號的公開標籤。選擇後,該對話會鎖定到對應的帳號:永遠不會輪換或容錯移轉,也不會改變目前 Pool 的帳號。`,"codexAuth.accountPickerCompatibility":`內建的 Codex App 登入有自己的選擇器;產生的對應通常稱為 main,需要時會使用 main-2 這類避免衝突的後綴。新增的帳號會獲得穩定且保護隱私的標籤,而自訂選擇器名稱保持不變。現有的對話與已儲存的模型選擇會繼續路由。關閉此功能只會隱藏產生的選項,仍會保留選擇器與確切路由。一般 GPT 模型 ID 維持原有的 Pool 或 Direct 行為。`,"codexAuth.accountPickerUpdated":`帳號指定設定已更新。`,"codexAuth.accountPickerUpdateFailed":`無法更新帳號指定設定。目前顯示的是最後一次確認的設定。`,"codexAuth.accountPickerLoadFailed":`無法載入帳號指定設定。`,"codexAuth.accountPickerRefreshFailed":`無法重新整理此設定。目前仍顯示最後一次確認的值。`,"codexAuth.advancedSettings":`進階設定`,"codexAuth.advancedSettingsAria":`顯示或隱藏進階 Codex 認證設定`,"codexAuth.catalogRefreshPending":`變更已儲存,但 Codex 模型目錄仍在等待重新整理。請執行 ocx sync 重試。`,"accountPool.priority":`選擇順序`,"accountPool.priorityAria":`此帳號的選擇順序`,"accountPool.priorityHint":`數字越大越先使用。只有當排名在前的所有帳號都已耗盡或不可用時,帳號池才會轉向較小的數字。`,"accountPool.priorityFirst":`最先`,"accountPool.priorityEarlier":`較先`,"accountPool.priorityNormal":`一般`,"accountPool.priorityLater":`較後`,"accountPool.priorityLast":`最後`,"accountPool.priorityOption":`{name}({value})`,"accountPool.priorityCustom":`自訂`,"accountPool.priorityUpdated":`已更新 {email} 的選擇順序`,"accountPool.priorityUpdateFailed":`無法儲存 {email} 的選擇順序。目前顯示最後一次確認的值。`,"api.clientConfig.clientOmp":`OMP`,"api.clientConfig.clientHermes":`Hermes`,"api.clientConfig.clientOpenclaw":`OpenClaw`,"api.clientConfig.clientKimi":`Kimi Code`,"api.clientConfig.clientGajae":`Gajae Code`,"api.clientConfig.clientDsh":`DeepSeek Harness (DSH)`,"api.clientConfig.clientMcode":`MiniMax Code`,"api.clientConfig.clientZcode":`ZCode`,"api.clientConfig.clientPrime":`Prime Agent`,"api.clientConfig.clientAside":`Aside`,"cws.tabsLabel":`Combo 詳細區段`,"cws.field.nativeAlias":`原生 OpenAI 別名`,"cws.field.nativeAliasHint":`讓此 combo 擁有受支援的未限定原生 OpenAI 模型 ID。帶有帳號或供應商限定的 OpenAI 路由仍保持獨立。`,"cws.field.displayName":`顯示名稱`,"cws.field.displayNameHint":`此 combo 在模型選擇器中的標籤。啟用原生 OpenAI 別名時必填。`,"cws.err.unsupportedNativeAlias":`原生別名必須是目前受支援的裸 OpenAI 模型 ID。`,"cws.err.missingNativeAliasDisplayName":`原生別名必須提供顯示名稱。`,"cws.err.invalidDisplayName":`顯示名稱最多 128 個字元,且不能包含控制字元。`,"claudeDesktop.appliedMarkerUnsaved":`已套用到 Claude Desktop,但套用標記未能儲存 — 在你再次套用之前,下方的已儲存/已套用狀態可能顯示過時資訊。`,"claudeDesktop.status.disabled":`Claude Desktop 整合已關閉。啟用後請完全結束並重新開啟 Desktop。`,"claudeDesktop.enableApply":`啟用並套用`,"lab.title":`相容性實驗室`,"lab.subtitle":`以實驗室投影證據為基礎的唯讀相容性判定矩陣。`,"lab.loadFailed":`無法載入相容性實驗室資料`,"lab.projectionUnavailable":`實驗室投影不可用。請先執行 conformance 或 live 探測。`,"lab.projectionIncompatible":`實驗室投影 schema 不相容。請重新建立投影。`,"lab.statusTitle":`投影狀態`,"lab.matrixTitle":`相容性矩陣`,"lab.verdictsTitle":`判定記錄`,"lab.filter.layer":`證據層`,"lab.filter.verdict":`判定`,"lab.filter.subject":`主體 ID`,"lab.filter.all":`全部`,"lab.col.subject":`主體`,"lab.col.layer":`層級`,"lab.col.suite":`套件`,"lab.col.verdict":`判定`,"lab.col.asOf":`截至`,"lab.col.protocol":`協定符合度`,"lab.col.live":`即時路由相容性`,"lab.col.task":`任務效能`,"lab.empty":`投影中還沒有相容性判定。`,"lab.subjectKind":`類型`,"lab.observationCount":`觀察數`,"lab.eventCount":`事件數`,"lab.verdictCount":`判定數`,"lab.subjectCount":`主體數`,"lab.builtAt":`建立於`,"lab.loading":`載入相容性證據中…`,"lab.loadMore":`載入更多`,"lab.detailTitle":`判定詳細資料`,"lab.detailClose":`關閉`,"lab.detailSubject":`主體`,"lab.detailObservations":`觀察數`,"lab.detailEvents":`貢獻事件`,"lab.detailArtifacts":`產物中繼資料`,"lab.production.title":`觀測到的正式環境流量`,"lab.production.notVerification":`不是實驗室驗證`,"lab.production.attempts":`嘗試`,"lab.production.successes":`成功`,"lab.production.routeErrors":`路由錯誤`,"lab.production.lastObserved":`最近觀測`,"lab.detailLoadFailed":`無法載入判定詳細資料`,"lab.refresh":`重新整理`,"lab.verdict.UNKNOWN":`未知`,"lab.verdict.CLAIMED":`已宣稱`,"lab.verdict.PROBED":`已探測`,"lab.verdict.VERIFIED":`已驗證`,"lab.verdict.DEGRADED":`已降級`,"lab.verdict.BLOCKED":`已封鎖`,"lab.verdict.UNSUPPORTED":`不支援`,"lab.layer.protocol_conformance":`協定符合度`,"lab.layer.live_route_compatibility":`即時路由相容性`,"lab.layer.task_effectiveness":`任務效能`,"dash.visionAdvanced":`進階設定`,"dash.visionMaxDescriptions":`每回合最大描述次數`,"dash.visionMaxDescriptionsInvalid":`請輸入正整數。`,"dash.visionTimeout":`逾時`,"dash.visionTimeoutInvalid":`請輸入 {min} 到 {max} 毫秒之間的整數。`,"dash.visionAdvancedPopover":`進階視覺設定`,"models.newPolicyGlobal":`新模型預設停用`,"models.newPolicyProvider":`新模型策略`,"models.newPolicy_inherit":`繼承`,"models.newPolicy_off":`關閉`,"models.newPolicy_on":`開啟`,"models.newBadge":`新增`,"models.newCount":`{count} 個新增,已關閉`,"models.aliases":`別名`,"models.aliasesTable":`別名表`,"models.aliasPrompt":`供應商別名(留空即清除)`,"models.modelAliasPrompt":`模型別名(留空即清除)`,"models.aliasSaved":`別名已儲存`,"models.aliasConflict":`此別名與現有名稱衝突`,"models.editProviderAlias":`編輯供應商別名`,"models.editModelAlias":`編輯模型別名`,"models.useDefaultAliases":`使用預設別名`,"models.useDefaultAliasesGlobal":`全域使用預設別名`,"models.aliasAuto":`自動`,"models.aliasUser":`使用者`,"models.aliasStale":`過期`,"connection.discovering":`正在探索本機與共享目標…`,"connection.machineUnavailable":`本機機器平面無法使用。共享請求未改用本機資料。`,"connection.disconnect":`中斷 Hub 連線`,"connection.disconnectConfirm":`要中斷此機器與 Hub 的連線,並以獨立模式重新啟動嗎?`,"connection.pairing.title":`將此儀表板連接到 Hub`,"connection.pairing.body":`貼上在 Hub 建立的一次性配對碼。`,"connection.pairing.relayWarning":`此代碼透過固定 Hub 轉送交換,無法重新導向其他主機。`,"connection.pairing.code":`一次性配對碼`,"connection.pairing.submit":`連接`,"connection.pairing.submitting":`連接中…`,"connection.pairing.error":`配對碼遭拒或已過期。輸入內容已保留供檢查。`,"connection.machine.title":`此機器`,"connection.machine.shimHealthy":`Codex shim 狀態正常。`,"connection.machine.shimNeedsAttention":`Codex shim 需要處理。`,"connection.machine.repairShim":`修復 shim`,"connection.machine.removeShim":`移除 shim`,"connection.clients.title":`已連接的用戶端`,"connection.clients.none":`沒有用戶端狀態`,"connection.clients.sync":`立即同步`,"connection.clients.syncing":`同步中…`,"connection.sessionLogout":`登出遠端工作階段`,"connection.sessionLoggingOut":`正在登出遠端工作階段…`,"connection.sessionLogoutFailed":`無法登出遠端工作階段。目前的工作階段已保留。`,"usage.source.connected":`來源:Hub 使用量`,"usage.source.local":`來源:本機 usage.jsonl`,"usage.scope.label":`使用量範圍`,"usage.scope.machine":`此機器`,"usage.scope.hub":`整個 Hub`,"usage.hubOffline":`Hub 使用量無法使用,未以本機使用量替代。`,"integrations.tab.cursor":`Cursor`,"integrations.detail.cursorSeen":`Cursor 最近曾呼叫此代理`,"integrations.detail.cursorNeverSeen":`已安裝 Private Inference;尚未收到請求`,"integrations.detail.cursorAbsent":`找不到 Cursor Private Inference`,"integrations.cursor.title":`Cursor`,"integrations.cursor.intro":`Cursor Private Inference 在本機執行代理程式,並透過 loopback 與 opencodex 通訊。一般版 Cursor 做不到:它的後端會呼叫自訂端點,因此需要公開的 HTTPS 網址。此頁面絕不會寫入 Cursor;請自行把下方的值貼進 Cursor。`,"integrations.cursor.loading":`正在讀取 Cursor 狀態…`,"integrations.cursor.unavailable":`無法從代理讀取 Cursor 狀態。`,"integrations.cursor.detection":`已安裝的版本`,"integrations.cursor.privateInference":`Cursor Private Inference`,"integrations.cursor.regular":`Cursor(一般版)`,"integrations.cursor.detected":`已偵測到`,"integrations.cursor.notFound":`找不到`,"integrations.cursor.regularOnly":`只找到一般版 Cursor。它會把自訂端點導向 Cursor 伺服器,因此沒有公開通道就無法連到 loopback 代理。請參閱指南取得 Private Inference 版本。`,"integrations.cursor.nothingFound":`在常見位置找不到 Cursor。若安裝在其他地方,下方的值仍然適用。`,"integrations.cursor.gateway":`閘道設定值`,"integrations.cursor.gatewayHint":`在 Cursor Private Inference 開啟 Settings > Models > Gateway,貼上這兩個值,然後按 Refresh model list。`,"integrations.cursor.baseUrl":`Base URL`,"integrations.cursor.apiKey":`API 金鑰`,"integrations.cursor.apiKeyCredential":`你的其中一把 opencodex API 金鑰(此綁定需要憑證)`,"integrations.cursor.copy":`複製`,"integrations.cursor.copied":`已複製`,"integrations.cursor.connection":`連線`,"integrations.cursor.seen":`最近一次來自 Cursor 的請求:{time}({ua})`,"integrations.cursor.neverSeen":`代理啟動後尚未收到 Cursor 的請求。儲存閘道後,請在 Cursor 按 Refresh model list。`,"integrations.cursor.models":`Cursor 會顯示的內容`,"integrations.cursor.modelsHint":`Cursor 從自己的模型表決定 Reasoning 階梯,opencodex 只能預測。Context 列出預設與可選的視窗(Cursor 的 Max Mode)。`,"integrations.cursor.ladderFromBundle":`Reasoning 階梯讀取自已安裝的 Cursor Private Inference {version} bundle。階梯由 Cursor 決定,opencodex 只是呈現它的表。`,"integrations.cursor.ladderFromStatic":`Reasoning 階梯是 Cursor 3.18.25 的靜態鏡像(找不到可讀取的 Private Inference bundle)。Context 欄列出預設與可選的視窗。`,"integrations.cursor.unknownVersion":`版本不明`,"integrations.cursor.noControl":`—`,"integrations.cursor.singleWindow":`單一視窗`,"integrations.cursor.noControlTitle":`此 id 不在 Cursor 內建的 effort 表中,因此 Cursor 不會顯示 Reasoning 控制項。`,"integrations.cursor.effortRowsOne":`已發布 1 個 effort 列`,"integrations.cursor.effortRowsMany":`已發布 {n} 個 effort 列`,"integrations.cursor.effortRowsOff":`沒有 effort 列`,"integrations.cursor.tableLessHint":`標為 — 的列在 Cursor 中沒有 Reasoning 控制項。開啟 cursorEffortRows 可為每個 effort 發布一個選擇器項目(id--effort),或在 provider 上設定 modelDefaultReasoningEfforts 作為固定預設值。`,"integrations.cursor.colModel":`模型`,"integrations.cursor.colReasoning":`推理`,"integrations.cursor.colContext":`上下文`,"integrations.cursor.guide":`開啟 Cursor Private Inference 指南`},We={"nav.dashboard":`Дашборд`,"uptime.day":`д`,"uptime.hour":`ч`,"uptime.minute":`мин`,"uptime.second":`с`,"nav.startup":`Безопасность запуска`,"nav.providers":`Провайдеры`,"nav.models":`Модели`,"nav.combos":`Комбо`,"nav.subagents":`Подагенты`,"routing.title":`Интеллект маршрутизации (beta)`,"routing.subtitle":`Политики маршрутизации, пробная оценка и аналитика на основе источников.`,"routing.loadFailed":`Не удалось загрузить данные маршрутизации`,"routing.empty":"Профили маршрутизации не настроены. Добавьте `routingProfiles` в config.json.","routing.revision":`rev`,"routing.detail":`Профиль`,"routing.createProfile":`Создать профиль`,"routing.dryRunError":`Ошибка пробного запуска (HTTP {status})`,"routing.removeConfirm":`Удалить профиль {id}?`,"routing.unknownEvidence.allow":`разрешить`,"routing.unknownEvidence.penalize":`штрафовать`,"routing.unknownEvidence.exclude":`исключить`,"routing.removeCandidate":`Удалить кандидата {provider}/{model}`,"routing.candidates":`Кандидаты`,"routing.require":`Жёсткие требования`,"routing.optimize":`Веса оптимизации`,"routing.limits":`Лимиты`,"routing.unknownEvidence":`Политика неизвестных данных`,"routing.compatibility.title":`Политика совместимости`,"routing.compatibility.enabled":`Требовать доказательства Compatibility Lab`,"routing.compatibility.requiredSuites":`Обязательные наборы`,"routing.compatibility.loadingCatalog":`Загрузка каталога Lab…`,"routing.compatibility.catalogUnavailable":`Каталог Lab недоступен — укажите id наборов вручную в config.json.`,"routing.compatibility.layer.protocol_conformance":`Соответствие протоколу`,"routing.compatibility.layer.live_route_compatibility":`Совместимость живого маршрута`,"routing.compatibility.minStatus":`Минимальный статус совместимости`,"routing.none":`нет`,"routing.unavailable":`–`,"routing.dryRun":`Пробная оценка`,"routing.dryRunContext":`Контекстное окно запроса (токены)`,"routing.dryRunTools":`Запрос требует инструменты`,"routing.dryRunImage":`Запрос требует изображения`,"routing.dryRunStructured":`Запрос требует структурированный вывод`,"routing.dryRunRun":`Оценить кандидатов`,"routing.candidate":`Кандидат`,"routing.eligible":`Допустим`,"routing.exclusions":`Исключения`,"routing.costCap":`Лимит стоимости`,"routing.capOutcome.satisfied":`в пределах лимита`,"routing.capOutcome.exceeded":`сверх лимита`,"routing.capOutcome.unknown-allowed":`неизвестно (разрешено)`,"routing.capOutcome.unknown-excluded":`неизвестно (исключено)`,"routing.exclusion.capability-unsatisfied":`требование не выполнено`,"routing.exclusion.unknown-capability":`неизвестная возможность`,"routing.exclusion.cost-limit":`сверх лимита стоимости`,"routing.exclusion.cost-limit-unknown":`неизвестная стоимость при лимите`,"routing.exclusion.cooldown":`пауза`,"routing.exclusion.unknown-health":`неизвестное состояние`,"routing.exclusion.unknown-quota":`неизвестная квота`,"routing.exclusion.unknown-price":`неизвестная цена`,"routing.exclusion.other":`исключение: {code}`,"routing.score":`Оценка`,"routing.selected":`выбран`,"routing.yes":`да`,"routing.no":`нет`,"routing.analytics":`Аналитика маршрутизации`,"routing.analyticsTotal":`Запросы`,"routing.analyticsSuccessRate":`Успех`,"routing.analyticsFallbackRate":`Фолбэк`,"routing.analyticsP50":`p50`,"routing.analyticsP95":`p95`,"routing.analyticsP99":`p99`,"routing.analyticsCooldown":`Сбои кулдауна`,"routing.analyticsConfidence":`Доверие`,"routing.analyticsTruncated":`усечённая история`,"routing.analyticsRequests":`Запросы`,"routing.analyticsEmpty":`Аналитики пока нет — сначала отправьте несколько запросов.`,"nav.logs":`Логи и отладка`,"nav.usage":`Использование`,"common.github":`GitHub`,"sidebar.star":`Поставить звезду на GitHub`,"sidebar.starred":`Звезда на GitHub поставлена`,"sidebar.starUnauthenticated":`Открыть GitHub, чтобы поставить звезду (gh CLI не выполнил вход)`,"sidebar.starFailed":`Не удалось поставить звезду через gh. Открываем GitHub.`,"sidebar.updateAvailable":`Доступно обновление: {version}`,"sidebar.checkUpdate":`Проверить обновления`,"common.save":`Сохранить`,"common.saving":`Сохранение…`,"common.cancel":`Отмена`,"common.discard":`Отбросить`,"common.delete":`Удалить`,"common.close":`Закрыть`,"common.ok":`ОК`,"common.remove":`Удалить`,"common.loading":`Загрузка…`,"common.retry":`Повторить`,"auth.adminTokenTitle":`Токен администратора OpenCodex (OPENCODEX_ADMIN_AUTH_TOKEN)`,"auth.adminAccountLabel":`Учётная запись`,"auth.adminTokenFieldLabel":`Токен администратора`,"auth.adminTokenRejected":`Токен администратора отклонён. Проверьте его и повторите попытку.`,"auth.adminTokenUnavailable":`Не удалось проверить токен администратора. Повторите попытку.`,"app.logoAria":`Логотип opencodex`,"app.claudeOn":`Claude ВКЛ`,"app.claudeOff":`Claude ВЫКЛ`,"theme.label":`Тема`,"theme.light":`Светлая`,"theme.dark":`Тёмная`,"theme.system":`Системная`,"lang.label":`Язык`,"lang.nativeName":`Русский`,"provider.name.commandCodeAuth":`Command Code - Auth`,"provider.name.commandCodeApi":`Command Code - API`,"provider.name.volcengine":`Volcengine Ark`,"provider.name.volcengineCodingPlan":`Volcengine Ark — тариф Coding`,"provider.name.volcengineAgentPlan":`Volcengine Ark — тариф Agent`,"errorBoundary.title":`Не удалось загрузить страницу`,"errorBoundary.message":`При отображении этого раздела произошла ошибка. Перезагрузите его, чтобы повторить попытку.`,"errorBoundary.details":`Ошибка`,"errorBoundary.reload":`Перезагрузить`,"startup.title":`Безопасность запуска`,"startup.subtitle":`Проверьте, сможет ли Codex подключиться к opencodex после перезагрузки, прежде чем локальный прокси вызовет бесконечное переподключение.`,"startup.refresh":`Обновить`,"startup.backToDashboard":`Назад к панели`,"startup.loading":`Проверка защиты запуска…`,"startup.error":`Не удалось прочитать состояние защиты запуска.`,"startup.staleData":`Последняя проверка не удалась. Значения ниже устарели и не подтверждают защиту.`,"startup.status.native":`Нативная маршрутизация`,"startup.status.protected":`Перезапуск защищён`,"startup.status.atRisk":`Требуется действие`,"startup.summary.native":`Codex не зависит от локального прокси`,"startup.summary.protected":`opencodex будет доступен после перезагрузки`,"startup.summary.atRisk":`После перезагрузки Codex может потерять доступ к моделям`,"startup.riskDetail":`Codex направлен на локальный прокси, но постоянная служба или исправный launcher shim не запустят его снова.`,"startup.riskDetailCustomLocal":`Codex направлен на пользовательский локальный шлюз. opencodex не может управлять или проверять его перезапуск.`,"startup.riskDetailWindowsShim":`Launcher shim защищает поддерживаемые CLI-скрипты, но Codex Desktop и прямой запуск codex.exe в Windows могут обходить его.`,"startup.safeDetail":`Маршрутизация и механизм запуска согласованы. После перезагрузки ручной запуск ocx start не требуется.`,"startup.routing":`Маршрутизация Codex`,"startup.routing.proxy":`Локальный прокси`,"startup.routing.native":`Нативный OpenAI`,"startup.routing.customLocal":`Пользовательский локальный шлюз`,"startup.routing.customRemote":`Пользовательский удалённый шлюз`,"startup.routing.unknown":`Неизвестная или недопустимая маршрутизация`,"startup.restartProtection":`Защита перезапуска`,"startup.preference":`Запуск по требованию`,"startup.enabled":`Включён`,"startup.disabled":`Выключен`,"startup.protection.service":`Фоновая служба`,"startup.protection.shim":`Launcher shim`,"startup.protection.none":`Не установлен`,"startup.details":`Сведения о защите`,"startup.service":`Фоновая служба`,"startup.serviceHint":`Запускается при входе и перезапускает прокси после сбоя.`,"startup.installed":`Установлена`,"startup.notInstalled":`Не установлена`,"startup.unsupported":`Не поддерживается`,"startup.shim":`Codex launcher shim`,"startup.shimHint":`Запускает ocx ensure при запуске поддерживаемого скриптового лаунчера Codex.`,"startup.healthy":`Исправен`,"startup.cliOnly":`Только CLI`,"startup.stale":`Устарел`,"startup.viable":`Готов`,"startup.unhealthy":`Установлен, но неисправен`,"startup.conflict":`Конфликт служб`,"startup.installedDisabled":`Установлен, но отключён`,"startup.install":`Установить`,"startup.installing":`Установка…`,"startup.repair":`Исправить`,"startup.repairing":`Исправление…`,"startup.serviceInstalled":`Фоновая служба успешно установлена.`,"startup.serviceRepaired":`Фоновая служба успешно исправлена.`,"startup.shimInstalled":`Launcher shim Codex успешно установлен.`,"startup.shimRepaired":`Launcher shim Codex успешно исправлен.`,"startup.installFailed":`Не удалось установить:`,"startup.tray.title":`Системный трей Windows`,"startup.tray.hint":`Запускает значок при входе для управления запуском, остановкой, перезапуском, панелью и состоянием прокси.`,"startup.tray.login":`Запускать трей при входе в Windows`,"startup.tray.notProtection":`Трей — это контроллер, а не защита перезапуска. Для автоматического восстановления по-прежнему нужна исправная фоновая служба.`,"startup.tray.running":`Работает`,"startup.tray.stopped":`Установлен, скрыт`,"startup.tray.stale":`Требуется ремонт`,"startup.tray.notInstalled":`Не установлен`,"startup.tray.loading":`Проверка…`,"startup.tray.unavailable":`Статус недоступен`,"startup.tray.install":`Установить и показать трей`,"startup.tray.start":`Показать значок`,"startup.tray.stop":`Закрыть значок`,"startup.tray.uninstall":`Удалить трей входа`,"startup.tray.error":`Действие Windows tray завершилось ошибкой. Подробности: ocx tray status.`,"startup.recovery":`Варианты исправления`,"startup.recoveryHint":`Используйте установку в один клик выше или скопируйте команду для ручного восстановления. Для Codex Desktop и Windows рекомендуется фоновая служба.`,"startup.command.service":`Рекомендуется: постоянная фоновая служба`,"startup.command.shim":`Альтернатива: CLI launcher shim`,"startup.command.native":`Безопасный режим: восстановить нативную маршрутизацию Codex`,"startup.copy":`Копировать`,"startup.copied":`Скопировано`,"startup.recommended":`Рекомендуемое исправление: {cmd}`,"startup.navRisk":`Защита запуска требует внимания`,"startup.codexRuntime.clampHidden":`Некоторые уровни рассуждений скрыты, потому что OpenCodex использует Codex {version}.`,"startup.codexRuntime.clampHiddenWithEfforts":`Некоторые уровни рассуждений скрыты, потому что OpenCodex использует Codex {version} (удалены: {efforts}).`,"startup.codexRuntime.olderBinary":`OpenCodex использует более старый бинарник Codex ({version}). Доступна более новая установка.`,"dash.subtitle":`Актуальное состояние локального прокси opencodex, его провайдеров и моделей, маршрутизируемых в Codex.`,"dash.workspace.overview":`Обзор`,"dash.workspace.sections":`Разделы`,"dash.status":`Статус`,"dash.online":`В сети`,"dash.offline":`Не в сети`,"dash.version":`Версия`,"dash.uptime":`Время работы`,"dash.providers":`Провайдеры`,"dash.tokens30d":`Токены (30 дн.)`,"dash.coverage":`{pct} покрытия`,"dash.mem.title":`Наблюдение за памятью`,"dash.mem.hint":`Диагностика среды выполнения только для чтения. Наблюдаемая память — max(RSS, external, ArrayBuffers), чтобы trimming рабочего набора Windows не скрывал удержанную память.`,"dash.mem.rss":`Резидентная память (RSS)`,"dash.mem.jsHeap":`Куча JS занято`,"dash.mem.jsHeapArena":`арена {total}`,"dash.mem.pressure":`Относительно порога`,"dash.mem.pressureOf":`{pct}% от порога`,"dash.mem.pressureUnknown":`Порог не сообщён`,"dash.mem.jscHeap":`Куча JSC`,"dash.mem.external":`External`,"dash.mem.arrayBuffers":`ArrayBuffers`,"dash.mem.observed":`Наблюдаемая`,"dash.mem.runtime":`Счётчики среды`,"dash.mem.growth":`Изменение наблюдаемой / час`,"dash.mem.perHour":`/ч`,"dash.mem.store":`Хранилище продолжений`,"dash.mem.storeHint":`Кэш прокси previous_response_id. Рост общего числа байт при растущей куче указывает на удержание диалогов, а не на аллокатор среды.`,"dash.mem.storeEntries":`Записи`,"dash.mem.storeTotal":`Всего`,"dash.mem.storeLargest":`Наибольшая`,"dash.mem.storeOldest":`Старейшая`,"dash.mem.threshold":`Порог предупреждения`,"dash.mem.lastWarn":`Последнее предупреждение`,"dash.mem.never":`Никогда`,"dash.mem.details":`Подробности`,"dash.mem.unavailable":`Диагностика памяти недоступна (старая версия прокси).`,"dash.mem.inFlight":`Активные запросы`,"dash.mem.restart":`Дождаться и перезапустить`,"dash.mem.restartConfirm":`Дождаться завершения {count} активных запросов, затем перезапустить (до {seconds} с; оставшиеся при таймауте прервутся).`,"dash.mem.draining":`Ожидание {count} запрос(ов)… перезапуск после завершения`,"dash.mem.reconnecting":`Прокси перезапускается… ожидание подключения`,"dash.mem.restartFailed":`Не удалось дождаться и перезапустить. Проверьте, что прокси запущен.`,"dash.mem.restartNoSupervisor":`Защита перезапуска не обнаружена. После перезапуска прокси может остаться выключенным, пока вы не запустите его снова.`,"dash.activeProviders":`Активные провайдеры`,"dash.noProviders":`Провайдеры не настроены. Выполните {cmd}.`,"dash.col.name":`Название`,"dash.col.adapter":`Адаптер`,"dash.col.baseUrl":`Базовый URL`,"dash.col.model":`Модель`,"dash.modelsNoResults":`Нет моделей, соответствующих поиску.`,"dash.availableModels":`Доступные модели`,"dash.noModels":`Модели не найдены. Проверьте API-ключи провайдеров.`,"dash.cannotConnect":`Не удаётся подключиться к прокси. Он запущен?`,"dash.runStart":`Выполните {cmd}, чтобы запустить прокси.`,"dash.stop":`Остановить прокси`,"dash.stopConfirm":`Остановить прокси и восстановить нативный Codex?`,"dash.stopFailed":`Не удалось остановить прокси (HTTP {status}).`,"dash.maSwitchFailed":`Не удалось переключить режим (HTTP {status}).`,"dash.maNetworkError":`Ошибка сети — прокси запущен?`,"dash.stopping":`Остановка…`,"dash.actions":`Прокси`,"dash.codexRestart":`Обновить список моделей Codex`,"dash.codexRestarting":`Останавливается…`,"dash.codexRestartConfirm":`Остановить app-server'ы Codex, чтобы они перечитали список моделей? Текущий ход Codex будет прерван, и Codex не перезапустится сам — откройте его заново.`,"dash.codexRestartDone":`Остановлено app-server Codex: {count}. Откройте Codex заново, чтобы загрузить актуальный список моделей.`,"dash.codexRestartNothing":`Ни один app-server Codex не запущен. При следующем запуске будет прочитан актуальный список моделей.`,"dash.codexRestartUnknown":`Не удалось получить список процессов, поэтому ничего не остановлено.`,"dash.codexRestartPartial":`app-server не завершились: {count}. Остановите их вручную, если список моделей остаётся устаревшим.`,"dash.codexRestartFailed":`Не удалось обновить список моделей Codex (HTTP {status}).`,"dash.codexRestartUnreachable":`Не удалось связаться с прокси.`,"dash.codexRestartMalformed":`Прокси вернул неожиданный ответ.`,"dash.codexRestartTimeout":`Прокси не ответил вовремя. Возможно, он всё ещё останавливает app-server'ы.`,"models.staleBanner":`Codex показывает список моделей старее этого каталога. Перезапустите Codex, чтобы перечитать его.`,"dash.codexAutoStart":`Запускать opencodex вместе с Codex`,"dash.codexAutoStartHint":`Разрешает установленному launcher shim выполнять ocx ensure. Эта настройка не устанавливает защиту перезапуска; проверьте фактическое состояние в разделе безопасности запуска.`,"dash.searchModel":`Модель сайдкара поиска`,"dash.searchModelHint":`Модель, используемая для web_search на маршрутизируемых моделях, отличных от OpenAI. Требуется вход в аккаунт ChatGPT.`,"dash.searchReasoning":`Уровень рассуждений для поиска`,"dash.visionModel":`Модель сайдкара для изображений`,"dash.visionModelHint":`Модель, которая описывает изображения для маршрутизируемых моделей, работающих только с текстом. Требуется вход в аккаунт ChatGPT.`,"dash.webSearchSidecar":`Сайдкар веб-поиска`,"dash.webSearchSidecarHint":`Выберите бэкенд и модель, используемые для веб-поиска на маршрутизируемых моделях.`,"dash.webSearchStream":`Стримить ответы вживую`,"dash.webSearchStreamHint":`Транслировать начальный текст и рассуждения вживую, пока модель не решит вызвать инструмент; остальное буферизуется для перехвата поиска. Текст до поиска может частично повторяться.`,"dash.visionSidecar":`Сайдкар для изображений`,"dash.visionSidecarHint":`Выберите бэкенд и модель, которые описывают изображения для маршрутизируемых моделей, работающих только с текстом.`,"dash.visionOff":`Выкл`,"dash.shadowCallIntercept":`Перехват теневых вызовов`,"dash.shadowCallInterceptHint":`Перехватывает фоновые служебные вызовы Codex App ({models}: генерация заголовков, сообщений коммитов) и перенаправляет их на выбранную вами модель.`,"dash.shadowCallWarning":`⚠ Когда функция включена, ВСЕ запросы к {models} будут заменены выбранной моделью.`,"dash.shadowCallOriginal":`Оригинал`,"dash.shadowCallModel":`Модель-замена`,"dash.shadowCallTooltip":`Codex App в фоновом режиме вызывает служебную модель для генерации заголовков тредов, сообщений коммитов и оркестрации навыков. Эта модель менялась между версиями клиента, поэтому opencodex перехватывает весь набор: {models}. Включите функцию, чтобы перенаправлять такие вызовы на выбранную вами модель.`,"models.shadowCallIntercept":`Перехват теневых вызовов`,"models.shadowCallInterceptHint":`Перехватывает фоновые служебные вызовы Codex App ({models}: заголовки, сообщения коммитов) и перенаправляет их на выбранную вами модель.`,"dash.sidecarBackend":`Бэкенд`,"dash.sidecarModel":`Модель`,"dash.backendAuto":`Авто`,"dash.backendOpenAI":`OpenAI`,"dash.backendAnthropic":`Anthropic`,"dash.sidecarSaved":`Настройки сайдкара сохранены. Вступят в силу со следующего запроса.`,"dash.sidecarSaveFailed":`Не удалось сохранить настройки сайдкара.`,"dash.injectionLabel":`Делегирование подагентам`,"dash.injectionHint":`Выберите модель, которой Codex будет передавать работу подагентов. Где применяется этот выбор, решают два переключателя ниже.`,"dash.syncCodexSubagentDefaults":`Сохранить и как значение по умолчанию в Codex`,"dash.syncCodexSubagentDefaultsHint":`Если включено, выбранная выше модель записывается в собственную конфигурацию Codex, и новые задачи тоже начинаются с неё. Если выключено, выбор запоминается только здесь. Применится при следующей синхронизации или перезапуске, а ваши настройки [agents] останутся нетронутыми.`,"dash.multiAgentGuidance":`Подсказывать, как делить работу`,"dash.multiAgentGuidanceHint":`Отправляет Codex короткую записку о том, как передавать работу подагентам. На v2 она называет доступные модели и предпочтительную; на v1 работает только при усилии рассуждения max или ultra. Если выключено, записка не добавляется.`,"dash.injectionNone":`Нет`,"dash.injectionEffortLabel":`Уровень рассуждений`,"dash.injectionEffortNone":`По умолчанию для модели`,"dash.effortCapLabel":`Лимит рассуждений V2 ultra`,"dash.subagentEffortCapLabel":`Лимит рассуждений подагентов V2`,"dash.effortCapHelp":`Ограничивает уровень рассуждений для ходов V2 в режиме ultra. Когда лимит задан, входящие запросы с максимальным уровнем рассуждений (из режима ultra) снижаются до выбранного уровня. Лимит для подагентов действует только на порождённые дочерние агенты. Лимиты только понижают уровень рассуждений и никогда не повышают его. Если модель не поддерживает заданный лимитом уровень, он снижается до ближайшего поддерживаемого.`,"dash.effortCapNone":`Без лимита`,"dash.maintenance":`Обслуживание`,"dash.maintenanceHint":`Обновите каталог моделей Codex или установите более новую версию opencodex.`,"dash.syncModels":`Синхронизировать модели`,"dash.syncing":`Синхронизация…`,"dash.syncOk":`Синхронизация завершена. Добавлено моделей: {count}.`,"dash.syncStaleHint":`Если Codex всё ещё показывает старый список, перезапустите долгоживущий app-server ({cmd}).`,"dash.syncFailed":`Ошибка синхронизации: {error}`,"dash.projectConfigTitle":`Конфигурация Codex в проекте обходит OpenCodex`,"dash.projectConfigHint":`Эти локальные настройки репозитория переопределяют прокси OpenCodex (например, направляют запросы напрямую в OpenCode Go). Удалите их, чтобы в этом проекте действовала маршрутизация из ~/.codex/config.toml.`,"dash.checkUpdate":`Проверить обновления`,"dash.updateTitle":`Обновление opencodex`,"dash.updateDesc":`Проверьте npm для выбранного канала, затем решите, перезапускать ли прокси после установки.`,"dash.updateChannel":`Канал`,"dash.updateChecking":`Проверка обновлений…`,"dash.updateInstalled":`Установлена`,"dash.updateLatest":`Последняя`,"dash.updateAvailable":`Доступно обновление`,"dash.updateCurrent":`Актуальная версия`,"dash.updateCommand":`Команда`,"dash.updateSource":`Это рабочая копия из исходного кода. Обновите её в терминале с помощью показанной команды.`,"dash.updateUnavailable":`Не удалось получить сведения о последней версии из npm. Попробуйте позже.`,"dash.updateRetry":`Повторить`,"dash.updateRecheck":`Проверить снова`,"dash.updateCannotAuto":`Обновление в один клик недоступно ({reason}).`,"dash.updateReason.source_checkout":`установка из исходного кода`,"dash.updateReason.latest_unavailable":`реестр npm недоступен`,"dash.updateReason.already_latest":`уже установлена последняя версия`,"dash.updateReason.unknown":`обновление недоступно`,"dash.updateRestart":`Перезапустить после обновления`,"dash.updateRestartHint":`Рекомендуется. Текущий GUI продолжает работать на старом коде, пока прокси не перезапустится.`,"dash.runUpdate":`Обновить`,"dash.updateReconnecting":`Ожидание перезапущенного прокси…`,"dash.updateStatus.running":`Обновление opencodex.`,"dash.updateStatus.restarting":`Обновление установлено. Перезапуск прокси.`,"dash.updateStatus.succeeded":`Обновление завершено.`,"dash.updateVersionTransition":`{currentVersion} -> {latestVersion}.`,"dash.updateStatus.failed":`Обновление не удалось.`,"prov.subtitle":`Настройте вышестоящих провайдеров, которых opencodex маршрутизирует в Codex. Войдите в аккаунт, добавьте провайдера или отредактируйте конфигурацию вручную.`,"prov.add":`Добавить провайдера`,"prov.editJson":`Редактировать JSON`,"prov.accountLogin":`Вход в аккаунт`,"prov.noOauth":`Нет доступных OAuth-провайдеров.`,"prov.loggedIn":`вход выполнен`,"prov.notLoggedIn":`вход не выполнен`,"prov.logout":`Выйти`,"prov.login":`Войти`,"prov.loginWith":`Войти через {provider}`,"prov.waitingBrowser":`Ожидание браузера…`,"prov.didntOpen":`Не открылось? Нажмите здесь`,"prov.copyLink":`Копировать ссылку`,"prov.dontOpenBrowser":`Не открывать браузер на машине с прокси`,"prov.dontOpenBrowserHint":`Полезно для другого профиля браузера или когда панель управления не на машине с прокси.`,"prov.linkCopied":`Скопировано`,"prov.linkCopyUnavailable":`Буфер обмена недоступен`,"prov.deviceCode":`Код устройства`,"prov.copyCode":`Копировать код`,"prov.codeCopied":`Код скопирован`,"prov.editAlias":`Изменить псевдоним`,"prov.aliasPrompt":`Отображаемое имя (оставьте пустым для удаления)`,"prov.aliasSaved":`Псевдоним сохранен`,"prov.aliasSaveFailed":`Не удалось сохранить псевдоним`,"prov.accountId":`ID`,"prov.pasteRedirect":`Вставьте URL перенаправления или код`,"prov.pasteRedirectHint":`Если браузер показывает ошибку localhost, скопируйте полный URL из его адресной строки и вставьте сюда (или вставьте код авторизации).`,"prov.pasteSubmit":`Отправить`,"prov.pasteSubmitting":`Отправка…`,"prov.pasteOk":`Код отправлен — завершаем вход…`,"prov.pasteFail":`Не удалось отправить код: {error}`,"prov.port":`Порт`,"prov.default":`По умолчанию`,"prov.loadingConfig":`Загрузка…`,"prov.saved":`Сохранено! Перезапустите прокси, чтобы применить изменения.`,"prov.loadConfigFail":`Не удалось загрузить конфигурацию`,"prov.invalidJson":`Некорректный JSON`,"prov.saveFailed":`Не удалось сохранить`,"prov.loginFailStart":`Не удалось начать вход в {provider}`,"prov.loginError":`Ошибка входа в {provider}: {error}`,"prov.loginRequestFail":`Не удалось выполнить запрос на вход в {provider}`,"prov.loginCancelled":`Вход в {provider} отменён`,"prov.loginTimeout":`Время ожидания входа в {provider} истекло — браузер был закрыт или вход не был завершён. Попробуйте ещё раз.`,"prov.loginOk":`Выполнен вход в {provider}. Выполните {cmd} (или изменения применятся на лету), чтобы его модели появились в списке.`,"prov.loginSameAccount":`Это всё ещё тот же аккаунт {provider} — переключите аккаунт в браузере и снова нажмите «Добавить аккаунт».`,"oauthTos.highTitle":`{provider}: риск OAuth по подписке`,"oauthTos.elevatedTitle":`{provider}: неофициальный OAuth-мост`,"oauthTos.anthropicBody":`Прямое повторное использование OAuth-токенов подписки Claude через сторонний прокси, такой как OpenCodex, не является поддерживаемой интеграцией Anthropic и может привести к ограничению доступа. Поддерживаемые интеграции Agent SDK, использующие подписки Claude, — это отдельный механизм.`,"oauthTos.highBody":`OpenCodex подключает {provider} через сторонний механизм OAuth. Неподдерживаемое использование может привести к ограничению или приостановке доступа.`,"oauthTos.elevatedBody":`OpenCodex подключает {provider} через неофициальный механизм OAuth. По возможности используйте официальный клиент; нетипичный или автоматизированный трафик может быть расценён как злоупотребление, и доступ может быть ограничен или приостановлен.`,"oauthTos.saferPath":`Более безопасный вариант: вместо этого настройте API-ключ в OpenCodex.`,"oauthTos.acknowledge":`Я понимаю риск и всё равно хочу продолжить с OAuth.`,"oauthTos.continue":`Продолжить с OAuth`,"prov.logoutOk":`Выполнен выход из {provider}.`,"prov.logoutFail":`Не удалось выйти из {provider}. Состояние аккаунта не изменилось.`,"prov.removed":`Провайдер "{name}" удалён.`,"prov.removedDefault":`Провайдер "{name}" удалён. Провайдером по умолчанию теперь является "{defaultProvider}".`,"prov.removeFail":`Не удалось удалить "{name}".`,"prov.removeLastProvider":`Нельзя удалить этого провайдера, если ни один другой включённый провайдер не может стать провайдером по умолчанию.`,"prov.removeHasDependentCombos":`Сначала удалите или обновите зависимые комбо: {combos}.`,"prov.setDefault":`Сделать основным`,"prov.setDefaultSuccess":`"{name}" теперь провайдер по умолчанию.`,"prov.setDefaultFail":`Не удалось сделать "{name}" провайдером по умолчанию.`,"prov.defaultDisabled":`Сначала включите этого провайдера, затем сделайте его основным.`,"prov.updateFail":`Не удалось обновить этого провайдера.`,"prov.networkError":`Ошибка сети. Проверьте, что прокси запущен, и повторите попытку.`,"prov.added":`Провайдер "{name}" добавлен. Уже активен — выполните {cmd} (или перезапустите), чтобы его модели появились в селекторе моделей Codex.`,"prov.removeConfirm":`Удалить провайдера "{name}"? Его модели исчезнут из селектора моделей Codex.`,"prov.hasApiKey":`API-ключ настроен`,"prov.hasHeaders":`настроены пользовательские заголовки`,"prov.accounts":`Аккаунты ({n})`,"prov.accountsAria":`Показать или скрыть аккаунты {name}`,"prov.accountActive":`Активен`,"prov.accountReauth":`Повторный вход`,"prov.reauthenticate":`Переавторизоваться`,"prov.reauthAccountMissing":`Выбранный аккаунт не найден после входа`,"prov.reauthIdentityMismatch":`Аккаунт, в который выполнен вход, не совпадает с выбранным`,"prov.accountAdd":`Добавить аккаунт`,"prov.accountNoLabel":`аккаунт {id}`,"prov.accountSwitchTitle":`Использовать этот аккаунт`,"prov.accountSwitched":`Переключено на {email}.`,"prov.accountSwitchFail":`Не удалось переключить аккаунт`,"prov.accountRemoved":`Аккаунт {email} удалён.`,"prov.accountRemoveFail":`Не удалось удалить {email}. Аккаунт не изменён.`,"prov.accountRemoveAria":`Удалить {email}`,"prov.accountRemoveConfirm":`Удалить аккаунт {email}? Данные его входа будут удалены из этого прокси.`,"prov.keyAdd":`Добавить API-ключ`,"prov.keyAdded":`API-ключ добавлен для {name}.`,"prov.keyAddFail":`Не удалось добавить API-ключ`,"prov.keyPlaceholder":`Вставьте API-ключ`,"prov.keySwitchTitle":`Использовать этот ключ`,"prov.keySwitched":`Переключено на ключ {key}.`,"prov.keySwitchFail":`Не удалось переключить ключ`,"prov.keyRemoved":`Ключ {key} удалён.`,"prov.keyRemoveAria":`Удалить ключ {key}`,"prov.keyRemoveConfirm":`Удалить API-ключ {key}? Он будет удалён из конфигурации этого прокси.`,"prov.activeBadge":`Активен`,"prov.disabledBadge":`Отключён`,"prov.defaultBadge":`По умолчанию`,"prov.enable":`Включить`,"prov.disable":`Отключить`,"prov.enabled":`Провайдер "{name}" включён. Его модели снова могут появляться в Codex.`,"prov.disabled":`Провайдер "{name}" отключён. Настройки сохранены, но его модели скрыты.`,"prov.enableFail":`Не удалось включить "{name}".`,"prov.disableFail":`Не удалось отключить "{name}".`,"prov.enableAria":`Включить провайдера {name}`,"prov.disableAria":`Отключить провайдера {name}`,"prov.defaultCannotDisable":`Провайдера по умолчанию нельзя отключить`,"prov.openaiAccountMode":`Режим аккаунта Codex`,"prov.openaiModePool":`Пул`,"prov.openaiModeDirect":`Прямой`,"prov.openaiPoolDesc":`По умолчанию. Ротация основного входа и добавленных аккаунтов с учётом привязки, квот, периодов ожидания и отказоустойчивого переключения (failover).`,"prov.openaiDirectDesc":`Используется только текущий/основной вход Codex. Сохранённые аккаунты пула не читаются и не ротируются.`,"prov.openaiModeSaved":`Режим аккаунта OpenAI изменён на {mode}.`,"prov.openaiModeSaveFailed":`Не удалось изменить режим аккаунта OpenAI.`,"prov.openaiApiDesc":`Используется API-ключ OpenAI; учётные данные аккаунта Codex никогда не используются.`,"prov.manageCodexAccounts":`Управление аккаунтами Codex`,"prov.openaiApiMissing":`Требуется API-ключ`,"prov.openaiApiSetup":`Настроить API-ключ`,"models.tab.catalog":`Модели`,"models.tab.combos":`Комбо`,"models.tab.compatibility":`Совместимость`,"models.tab.routing":`Маршрутизация (beta)`,"models.tabsLabel":`Поверхности моделей`,"models.subtitle.combos":`Упорядоченные группы моделей, отвечающие под одним id. Связывайте цели через failover или распределяйте нагрузку стратегией балансировки.`,"models.subtitle.compatibility":`Матрица совместимости только для чтения из проекции лаборатории.`,"models.subtitle.routing":`Профили политик, оценка в режиме dry-run и аналитика маршрутизации с подтверждением источников.`,"models.subtitle":`Управляйте тем, какие модели видит Codex — нативные GPT (сквозной проброс) и модели маршрутизируемых провайдеров, сгруппированные по провайдеру (нажмите на заголовок, чтобы свернуть группу). Скрытые модели исчезают из каталога и селектора, но остаются вызываемыми по точному id. Изменения применяются на следующем ходе Codex — opencodex сбрасывает 5-минутный кэш моделей Codex, поэтому перезапуск не требуется.`,"models.nativeGroupLabel":`Нативные OpenAI`,"models.nativeHint":"Модели сквозного проброса используют режим аккаунта (пул или прямое подключение), выбранный на странице «Провайдеры». Отключение модели скрывает её из селектора Codex (запись в каталоге сохраняется, поэтому при повторном включении она восстанавливается в точности). Добавление модели здесь регистрирует маршрутизируемый селектор `openai/`, а не новый «голый» passthrough-идентификатор.","models.active":`{active}/{total} видимо`,"models.workspace.providers":`Провайдеры`,"models.workspace.allProviders":`Все провайдеры`,"models.workspace.mainAria":`Сведения о моделях`,"models.allOn":`Все вкл.`,"models.allOff":`Все выкл.`,"models.presetLabel":`Модели`,"models.presetMode_preset":`Пресет`,"models.presetMode_all":`Все`,"models.presetMode_custom":`Свои`,"models.presetSummary":`Показано {count} из {total} — базовый пресет v{version}`,"models.presetUpdateAvailable":`Доступен пресет v{version}`,"models.presetAppliedToast":`{provider}: пресет применён — выбрано моделей: {count}`,"models.presetClearedToast":`{provider}: показаны все модели`,"models.presetEmpty":`{provider}: пресет не совпал ни с одной моделью — выбор не изменён`,"models.presetConfirmReplace":`Заменить ваш выбор пресетом из {count} моделей?`,"models.cap350k":`Лимит 350k`,"models.capApplied":`Лимит контекста применён — вступит в силу на следующем ходе Codex.`,"models.capSaveFailed":`Не удалось сохранить лимит контекста`,"models.contextCapped":`Лимит 350k`,"models.contextCapLabel":`Окно по умолчанию / лимит`,"models.v2Label":`Подагент`,"models.shadowCallOriginal":`⚠ {models} →`,"models.v2DocsLink":`Что такое v1 / v2?`,"models.v2Mode_v1":`v1`,"models.v2Mode_default":`base`,"models.v2Mode_v2":`v2`,"models.v2ModeDesc_v1":`Все модели → поверхность v1`,"models.v2ModeDesc_default":`Вышестоящие значения по умолчанию (sol/terra=v2, luna=v1)`,"models.v2ModeDesc_v2":`Все модели → поверхность v2`,"models.keepNativeOnV1":`Оставить ChatGPT на v1`,"models.keepNativeOnV1Hint":`Нативные родители ChatGPT шифруют дочерние задачи v2 — Grok и Claude их не читают. Оставьте включённым, если Sol/Terra должны порождать routed-модели. Routed-родители остаются на v2.`,"models.v2Help":`Управляет мультиагентной поверхностью для всех моделей. + +v1: Классический однопоточный агент. Каждая модель использует поверхность взаимодействия v1. +base: Вышестоящие значения по умолчанию — sol/terra используют v2, luna использует v1, остальные следуют функциональному флагу codex. +v2: Многопоточный агент со spawn_agent. Каждая модель использует поверхность взаимодействия v2. + +На v2 «Оставить ChatGPT на v1» оставляет Sol/Terra на v1, чтобы они могли порождать Grok или Claude. ChatGPT шифрует дочерние задачи v2 — routed-модели их не читают. Routed-родители остаются на v2. + +Изменения применяются к новым сессиям.`,"dash.multiAgent":`Подагент`,"models.v2Conflict":`Задан [agents] max_threads — codex откажется запускаться; удалите его из config.toml`,"models.v2Applied":`Режим подагента обновлён — применяется к новым сессиям (перезапустите приложение Codex, чтобы обновить селектор моделей)`,"models.v2ThreadsLabel":`Макс. потоков`,"models.v2ThreadsDefault":`по умолчанию (4)`,"models.v2ThreadsApplied":`Лимит потоков обновлён — применяется к новым сессиям`,"models.v2ThreadsInvalid":`Лимит потоков должен быть целым числом >= 1`,"models.v2ThreadsApply":`Применить`,"models.capValue":`По умолч. {value}`,"models.contextSettings":`Пользовательские окна`,"models.contextSettingsTitle":`Пользовательские окна — {provider}`,"models.contextDefault":`Значение провайдера`,"models.contextModel":`Модель`,"models.contextModelOverride":`Переопределение модели`,"models.contextHint":`Если окно уже известно, запишите здесь реальное окно Codex. При отсутствии метаданных используется это значение; большее заявленное окно только ограничивается, меньшее сохраняется. Пустое поле берёт «Окно по умолчанию / лимит» провайдера или 128k, если лимит выключен.`,"models.contextAutomatic":`Автоматическое определение`,"models.contextSaved":`Контекстные окна обновлены — изменения вступят в силу на следующем ходе Codex.`,"models.contextUnchanged":`Нет изменений контекстных окон для сохранения.`,"models.contextSaveFailed":`Не удалось сохранить контекстные окна`,"models.contextInvalid":`Контекстные окна должны быть положительными целыми числами`,"models.contextCappedValue":`Лимит {value}`,"models.setAll":`Применить ко всем`,"models.setAllHint":`Включает окно по умолчанию {value} для всех маршрутизируемых провайдеров. Если релей не отдаёт context_window / context_length, это значение становится реальным окном Codex. Чтобы задать одну модель вручную, используйте «Пользовательские окна» в той же строке. Нативные провайдеры не затрагиваются.`,"models.collapseAll":`Свернуть все`,"models.expandAll":`Развернуть все`,"models.orderHint":`Порядок в селекторе: модели, выбранные на странице «Подагенты» (в заданном порядке) → остальные маршрутизируемые модели по алфавиту — сначала по провайдеру, затем по ID модели → нативные модели. Переключатели видимости лишь фильтруют модели и не меняют этот порядок.`,"models.custom":`Другое…`,"models.customApply":`Применить`,"models.customPlaceholder":`Токены (напр. 420000)`,"models.customAdd":`Добавить пользовательскую модель`,"models.customAddTitle":`Добавить пользовательскую модель — {provider}`,"models.customEditTitle":`Изменить пользовательскую модель — {provider}`,"models.customAdded":`Пользовательская модель добавлена`,"models.customUpdated":`Пользовательская модель обновлена`,"models.customDeleted":`Пользовательская модель удалена`,"models.customSaveFailed":`Не удалось сохранить пользовательскую модель`,"models.customSaving":`Сохранение…`,"models.customAddBtn":`Добавить`,"models.customEditBtn":`Обновить`,"models.customEdit":`Изменить`,"models.customDelete":`Удалить`,"models.customDeleteConfirm":`Удалить модель {name}?`,"models.customBadge":`Пользовательская`,"models.customSummary":`Пользовательских: {count}`,"models.customFieldModelId":`ID модели (slug эндпоинта)`,"models.customFieldModelIdPlaceholder":`например, qwen4-max-preview`,"models.customFieldDisplayName":`Отображаемое имя (необязательно)`,"models.customFieldDisplayNamePlaceholder":`например, Qwen 4 Max Preview`,"models.customFieldContext":`Контекстное окно`,"models.customFieldModalities":`Входные модальности`,"models.customFieldReasoning":`Уровень рассуждений`,"models.customFieldReasoningOverride":`Переопределить уровень рассуждений`,"models.reasoningEffort.none":`Нет`,"models.reasoningEffort.minimal":`Минимальный`,"models.reasoningEffort.low":`Низкий`,"models.reasoningEffort.medium":`Средний`,"models.reasoningEffort.high":`Высокий`,"models.reasoningEffort.xhigh":`Очень высокий`,"models.reasoningEffort.max":`Максимальный`,"models.tipProvider":`Провайдер`,"models.tipContext":`Контекст`,"models.tipModalities":`Модальности`,"models.tipStatus":`Статус`,"models.tipActive":`Активна`,"models.tipDisabled":`Отключена`,"models.applied":`Применено — вступит в силу на следующем ходе Codex.`,"models.saveFailed":`Не удалось сохранить`,"models.networkError":`Ошибка сети — запущен ли прокси?`,"models.loadFail":`Не удалось загрузить модели — запущен ли прокси?`,"models.noRouted":`Нет маршрутизируемых моделей`,"models.noRoutedHint":`Сначала войдите в провайдера или добавьте нового.`,"models.emptyDiscovery":`Модели не обнаружены. Проверьте адрес провайдера или добавьте статическую/пользовательскую модель.`,"models.emptyDiscoveryDisabled":`Автообнаружение моделей выключено, статические модели не настроены.`,"models.discoveryFailedBadge":`Ошибка обнаружения`,"models.discoveryFailedHttp":`Не удалось обнаружить модели (HTTP {status}).`,"models.discoveryFailedBlocked":`Обнаружение моделей заблокировано политикой назначения.`,"models.discoveryFailedInvalidResponse":`Обнаружение моделей вернуло недопустимый ответ.`,"models.discoveryFailedNetwork":`Обнаружение моделей не удалось из-за сетевой ошибки.`,"models.discoveryFailedProvider":`Провайдер сообщил об ошибке обнаружения моделей.`,"models.discoveryFailedGeneric":`Не удалось обнаружить модели.`,"models.openProviderSettings":`Открыть настройки провайдера`,"models.loading":`Загрузка…`,"models.search":`Поиск моделей…`,"models.showMore":`Показать ещё {n}`,"models.allowlistLabel":`Только выбранные`,"models.allowlistHint":`В каталог попадают только отмеченные модели (пусто = все). Полезно для провайдеров, предоставляющих тысячи моделей.`,"models.selectedCount":`Выбрано: {n}`,"sub.subtitle":`{cmd} в Codex объявляет как переопределения только первые 5 моделей (по приоритету). Выберите здесь до 5 моделей — нативные gpt или маршрутизируемые — и opencodex задаст им приоритет в каталоге так, чтобы именно они шли первыми. Любую другую модель по-прежнему можно вызвать по её точному имени; эта настройка управляет только тем, что отображается.`,"sub.featured":`Избранные`,"sub.advanced":`Дополнительно`,"sub.orderHintAria":`Как используется этот порядок`,"sub.orderHint":`Показанный здесь порядок задаёт позиции 1–5 в верхней части селектора моделей Codex и кандидатов в модели по умолчанию для {cmd}.`,"sub.noneSelected":`Ничего не выбрано — выберите из списка ниже.`,"sub.models":`Модели`,"sub.search":`Поиск моделей (нативные gpt + маршрутизируемые)…`,"sub.noModels":`Нет моделей — сначала войдите в провайдера или добавьте нового.`,"sub.saved":`Сохранено {n} моделей. Начните новую сессию Codex (или выполните {cmd}), чтобы увидеть их как переопределения spawn_agent.`,"sub.saveFailed":`Не удалось сохранить`,"sub.networkError":`Ошибка сети — запущен ли прокси?`,"sub.loadFail":`Не удалось загрузить модели — запущен ли прокси?`,"sub.loading":`Загрузка…`,"sub.moveUp":`Переместить {m} вверх`,"sub.moveDown":`Переместить {m} вниз`,"sub.removeAria":`Убрать {m}`,"sub.workspace.addToFeatured":`Добавить {m} в избранные`,"sub.workspace.allModels":`Все модели`,"sub.workspace.featuredFull":`Список избранных заполнен (макс. 5)`,"sub.workspace.mainAria":`Сведения о модели субагента`,"sub.workspace.notFeatured":`Не в избранных`,"sub.workspace.priority":`Приоритет`,"sub.workspace.removeFromFeatured":`Убрать {m} из избранных`,"sub.workspace.selectModel":`Выберите модель`,"sub.workspace.selectModelDesc":`Выберите модель из списка, чтобы увидеть детали и добавить её в избранные для spawn_agent.`,"sub.workspace.selector":`Публичный селектор`,"sub.ultraMode":`Ультра-режим`,"sub.ultraModeHint":`Включает политику упреждающего делегирования мультиагентов для всех моделей и уровней reasoning effort (сам reasoning effort не меняется). Записывает features.multi_agent_v2.multi_agent_mode_hint_text в config.toml.`,"sub.ultraModeV2Required":`Требуется мультиагентная поверхность v2 — сначала включите multi_agent_v2 и выберите v2 в переключателе режима субагентов.`,"sub.ultraModeText":`Текст делегирования ультра-режима`,"sub.ultraModePreset":`Восстановить пресет`,"sub.ultraModeLoadFail":`Не удалось загрузить настройки ультра-режима — работает ли прокси?`,"sub.ultraModeSaveFail":`Не удалось сохранить настройки ультра-режима`,"sub.ultraModeSaved":`Ультра-режим сохранён. Применяется к новым сеансам Codex.`,"logs.title":`Журнал запросов`,"logs.tabLogs":`Логи`,"logs.tabDebug":`Отладка`,"logs.subtitle":`Недавние запросы через локальный прокси opencodex, новые сверху.`,"logs.autoRefresh":`Автообновление`,"logs.noRequests":`Запросов пока нет.`,"logs.loadError":`Не удалось загрузить журнал запросов.`,"logs.filter.surface.label":`Источник`,"logs.filter.surface.all":`Все`,"logs.filter.surface.claude":`Claude`,"logs.filter.surface.codex":`Codex`,"logs.filter.surface.grok":`Grok`,"logs.filter.interceptedHelpersOnly":`Только перехваченные помощники`,"logs.badge.interceptedHelper":`I · {model}`,"logs.badge.interceptedHelperTitle":`Перехваченный запрос помощника`,"logs.filter.conversation.label":`Диалог`,"logs.filter.conversation.placeholder":`Вставьте ID диалога`,"logs.filter.conversation.clear":`Сбросить`,"logs.filter.model.label":`Модель`,"logs.filter.model.placeholder":`Фильтр по модели или провайдеру`,"logs.filter.conversation.apply":`Фильтровать логи`,"logs.conversation.totals":`{requests} запросов · {tokens} токенов · {cost}`,"logs.conversation.scope":`Итоги только по загруженному кольцу Logs.`,"logs.conversation.excluded":`(из ~$ исключены {unpriced} без цены, {unmetered} без учёта)`,"logs.cost.approximate":`{amount}`,"logs.cost.lowerBound":`≥{amount}`,"logs.cost.unavailable":`недоступно`,"logs.detail.conversation":`Диалог`,"logs.badge.claude":`Claude`,"logs.badge.grok":`Grok`,"logs.col.time":`Время`,"logs.col.request":`Запрос`,"logs.col.model":`Модель`,"logs.col.effort":`Уровень`,"logs.col.provider":`Провайдер`,"logs.col.status":`Статус`,"logs.col.tokens":`Токены`,"logs.col.tokPerSec":`tok/s`,"logs.col.estimatedCost":`~$`,"logs.metric.tokPerSecTitle":`Выходные токены в секунду за полную длительность запроса`,"logs.metric.estimatedCostTitle":`Эквивалент стоимости по прайс-листу API, а не фактическое списание; если цену не удалось сопоставить, значение недоступно`,"usage.cost.total":`Эквивалент стоимости по прайс-листу API (за этот период)`,"usage.cost.disclaimer":`Не является счётом. Расходы могут покрываться подпиской или кредитами провайдера.`,"usage.cost.unpricedNote":`Исключено {count} запросов (нет цены или данных использования)`,"logs.detail.section.basic":`Основная информация`,"logs.detail.route.section":`Решение о маршруте`,"logs.detail.route.kind":`Тип маршрута`,"logs.detail.route.profile":`Профиль`,"logs.detail.route.selected":`Выбрано`,"logs.detail.route.candidates":`Кандидаты`,"logs.detail.route.unknown":`Для этого запроса трасса маршрута не записана (строка до трассировки).`,"logs.detail.section.performance":`Производительность`,"logs.detail.section.cost":`Эквивалент стоимости по прайс-листу API`,"logs.detail.section.attempts":`Попытки комбо`,"logs.detail.section.usage":`Сырые данные использования`,"logs.detail.ttft":`TTFT`,"logs.detail.costTotal":`Эквивалент по прайс-листу`,"logs.detail.totalTokens":`Всего токенов`,"logs.detail.matchedKey":`Совпавший ключ цены`,"logs.detail.priceSource":`Источник цены`,"logs.detail.unavailableReason":`Причина недоступности`,"logs.detail.copyRequestId":`Копировать ID запроса`,"logs.detail.copied":`Скопировано`,"logs.detail.source.jawcode":`каталог jawcode`,"logs.detail.source.expected":`Оверлей ожидаемых цен`,"logs.detail.source.user":`Ценовой оверлей провайдера`,"logs.detail.verification.verified":`Подтверждено`,"logs.detail.verification.derived":`Выведено из базовой модели`,"logs.detail.attempt.target":`Провайдер / модель`,"logs.detail.attempt.reason":`Результат / причина`,"logs.detail.attempt.completed":`Завершено`,"logs.detail.attempt.e2eNote":`Общий tok/s — сквозной показатель; для каждой попытки используется её собственная длительность.`,"logs.detail.attempt.recovery.transient5xx":`Временная ошибка 5xx`,"logs.detail.attempt.recovery.connectionReset":`Соединение сброшено`,"logs.detail.attempt.recovery.oauth401":`Повторная авторизация OAuth`,"logs.detail.attempt.recovery.key429":`Ключ ограничен (429)`,"logs.detail.attempt.recovery.rateLimit429":`Ограничение частоты запросов (429)`,"logs.detail.attempt.recovery.anthropicOauth429":`Anthropic OAuth ограничен (429)`,"logs.detail.attempt.recovery.image413":`Слишком большой размер изображения (413)`,"logs.detail.attempt.recovery.emptyCompletion":`Повтор пустого завершения`,"logs.detail.attempt.recovery.unknown":`Неизвестная причина восстановления`,"logs.detail.reason.usage_missing":`Данные об использовании не были сообщены.`,"logs.detail.reason.usage_unsupported":`Этот провайдер не сообщает данные об использовании.`,"logs.detail.reason.output_missing":`Положительное число выходных токенов не было сообщено.`,"logs.detail.reason.invalid_duration":`Длительность запроса некорректна.`,"logs.detail.reason.price_unmatched":`Подходящая цена не найдена.`,"logs.detail.reason.invalid_cache_breakdown":`Детализация кэш-токенов противоречит общему числу входных токенов.`,"logs.detail.reason.invalid_usage":`В данных использования есть некорректное значение токенов.`,"logs.detail.reason.combo_attempt_unavailable":`Не удалось рассчитать стоимость как минимум одной попытки комбо.`,"logs.detail.estimate.usage_estimated":`Данные об использовании от провайдера — оценочные.`,"logs.detail.estimate.cache_detail_missing":`Детализация кэша недоступна; входные токены оценены по верхней границе.`,"logs.detail.estimate.expected_price_overlay":`Использована подтверждённая ожидаемая цена из прайс-листа.`,"logs.detail.estimate.provider_cost_overlay":`Использован ценовой оверлей провайдера.`,"logs.detail.estimate.priority_lower_bound":`Подтверждённая цена Priority недоступна; показанная оценка является известной нижней границей.`,"logs.col.error":`Ошибка`,"logs.col.upstreamReason":`Причина от провайдера`,"logs.col.duration":`Длительность`,"logs.modelTooltip.model":`модель`,"logs.modelTooltip.resolvedModel":`разрешённая модель`,"logs.modelTooltip.requestedTier":`запрошенный уровень`,"logs.modelTooltip.configuredTier":`настроенный уровень`,"logs.modelTooltip.responseTier":`уровень ответа`,"logs.modelTooltip.supportsTier":`поддержка уровня`,"logs.tokens.reported":`сообщено`,"logs.tokens.unreported":`не сообщено`,"logs.tokens.unsupported":`не поддерживается`,"logs.tokens.estimated":`оценка`,"logs.tokens.input":`вход`,"logs.tokens.output":`выход`,"logs.tokens.cacheRead":`чтение кэша (c)`,"logs.tokens.cacheWrite":`запись кэша (w)`,"logs.tokens.reasoning":`рассуждения`,"logs.tokens.noCache":`нет данных кэша`,"logs.tokens.contextTotal":`активный контекст`,"logs.tokens.noCacheNote":`этот провайдер не сообщает данные о кэш-токенах`,"logs.tokens.noCacheCursor":`детализация кэша Cursor не сообщается`,"logs.tokens.noCacheCursorNote":`Cursor не передает число токенов чтения/записи кэша; это неизвестно, а не подтвержденный промах кэша`,"logs.tokens.estimatedNote":`оценка (провайдер не сообщает точные данные использования)`,"logs.details":`Детали`,"logs.detailTitle":`Детали запроса`,"logs.detailRaw":`Сырая запись лога`,"debug.title":`Отладка`,"debug.subtitle":`Включаемая по желанию диагностика транспорта провайдеров и извлечения данных использования. Ошибки запросов и 502 остаются на вкладке «Логи».`,"debug.debug":`Отладка провайдера`,"debug.usage":`Извлечение данных использования`,"debug.injection":`Лог инъекций`,"debug.claude":`Входящие Claude`,"debug.claudeInbound.title":`Входящие запросы Claude`,"debug.claudeInbound.sub":`Что фактически отправляет Claude Code/Desktop (thinking, effort, метаданные) — текст промптов не сохраняется.`,"debug.claudeInbound.empty":`Запросы пока не зафиксированы. Отправьте сообщение из Claude, пока эта опция включена.`,"debug.claudeInbound.time":`Время`,"debug.claudeInbound.endpoint":`Конечная точка`,"debug.claudeInbound.model":`Модель`,"debug.claudeInbound.none":`нет`,"debug.reset":`Сбросить временные переопределения`,"debug.refresh":`Обновить`,"debug.follow":`Следить`,"debug.streamProvider":`Провайдер`,"debug.streamUsage":`Использование`,"debug.streamInjection":`Инъекции`,"debug.loading":`Загрузка настроек отладки…`,"debug.loadFailed":`Не удалось загрузить настройки отладки.`,"debug.emptyTitle":`Отладочное логирование выключено`,"debug.empty":`Включите «Отладка провайдера» или «Извлечение данных использования» в карточке выше. Строки появятся здесь после отправки запроса через прокси.`,"debug.noLinesTitle":`Ожидание строк`,"debug.noLines.provider":`Отладка провайдера включена, но записываются только аномалии транспорта (потерянные или повреждённые фреймы, а также события подключения и повторов Cursor). Успешный запрос через провайдера вроде Anthropic может не дать ни одной строки.`,"debug.noLines.usage":`Извлечение данных использования включено, но пока ничего не зафиксировано. Отправьте чат или запрос через Codex — и записи появятся здесь.`,"debug.noLines.injection":`Лог инъекций включён, но пока ничего не зафиксировано. Он записывает инъекции мультиагентных инструкций и решения об ограничении уровня рассуждений на ходах совместной работы (collab) и подагентов.`,"usage.title":`Использование`,"usage.subtitle":`Локальный учёт токенов вашего прокси. Отсутствующие данные никогда не показываются как ноль.`,"usage.loading":`Загрузка данных об использовании…`,"usage.empty":`Данных об использовании пока нет. Отправьте запрос через прокси, чтобы увидеть здесь активность.`,"usage.loadError":`Не удалось загрузить данные об использовании.`,"usage.range.all":`Все`,"usage.range.available":`Доступная история`,"usage.historyTruncated":`Итоги охватывают только доступную историю, поскольку старые данные не загружены.`,"usage.historyTruncatedWindow":`У загруженных записей время начала запроса находится в диапазоне от {start} до {end}. Более ранние записи файла пропущены из-за лимита чтения, поэтому выбранный период может быть неполным.`,"usage.range.30d":`30 дн.`,"usage.range.7d":`7 дн.`,"usage.card.requests":`Запросы`,"usage.card.measured":`Измерено`,"usage.card.reported":`Сообщено`,"usage.card.totalTokens":`Всего токенов`,"usage.card.cachedTokens":`Чтения из кэша`,"usage.card.cachedTokensHint":`Токены промпта, отданные из кэша провайдера (чтения). Записи в кэш показаны ниже, если они есть.`,"usage.card.cacheWriteTokens":`записи в кэш`,"usage.card.coverage":`Покрытие`,"usage.card.activeDays":`Активные дни`,"usage.section.heatmap":`Активность по дням`,"usage.section.overview":`Обзор`,"usage.section.models":`Модели`,"usage.section.providers":`Провайдеры`,"usage.section.coverage":`Детализация покрытия`,"usage.workspace.report":`Отчёт об использовании`,"usage.workspace.sections":`Разделы использования`,"usage.coverage.measured":`Измерено`,"usage.coverage.reported":`Сообщено провайдером`,"usage.coverage.estimated":`Оценено`,"usage.coverage.note":`Измеренные записи включают количество токенов, сообщённое провайдером, и оценочные значения. Запросы без отчёта и неподдерживаемые запросы учитываются, но никогда не показываются как ноль токенов.`,"usage.search.models":`Поиск моделей…`,"usage.col.requests":`Запросы`,"usage.col.measured":`Измерено`,"usage.col.reported":`Сообщено`,"usage.col.tokens":`Токены`,"usage.col.share":`Доля`,"usage.heatmap.less":`Меньше`,"usage.heatmap.more":`Больше`,"usage.dayMon":`Пн`,"usage.dayWed":`Ср`,"usage.dayFri":`Пт`,"usage.heatmap.tooltipTokens":`{tokens} токенов`,"usage.heatmap.tooltipRequests":`{requests} запросов`,"nav.storage":`Хранилище`,"storage.title":`Хранилище`,"storage.subtitle":`Смотрите, что занимает CODEX_HOME. Очистка не затрагивает активные сессии.`,"storage.loading":`Сканирование хранилища…`,"storage.empty":`CODEX_HOME пуст или отсутствует — показывать нечего.`,"storage.error":`Не удалось просканировать хранилище. Убедитесь, что CODEX_HOME указывает на корректный каталог.`,"storage.refresh":`Пересканировать`,"storage.rescanned":`Сканирование завершено.`,"storage.card.total":`Общий размер`,"storage.card.files":`Файлы`,"storage.card.home":`CODEX_HOME`,"storage.snapshot.lastScan":`Последний скан`,"storage.snapshot.scanning":`Сканирование…`,"storage.snapshot.unavailable":`Сканирования ещё не было.`,"storage.cleanupCard.title":`Освободить место`,"storage.cleanupCard.tabs":`Параметры очистки`,"storage.cleanupCard.tab.policy":`Политика`,"storage.cleanupCard.tab.quarantine":`Карантин`,"storage.cleanup.noArchives":`Нет архивных сессий для очистки.`,"storage.section.buckets":`Категории`,"storage.section.largest":`Крупнейшие файлы`,"storage.workspace.overview":`Обзор`,"storage.workspace.selectBucket":`Выберите сегмент в списке, чтобы увидеть разбивку.`,"storage.col.bucket":`Категория`,"storage.col.size":`Размер`,"storage.col.files":`Файлы`,"storage.col.oldest":`Старейший`,"storage.col.newest":`Новейший`,"storage.col.rows":`Строки БД`,"storage.rows.unknown":`неизвестно (заблокировано)`,"storage.bucket.sessions":`Активные сессии`,"storage.bucket.archived_sessions":`Архивные сессии`,"storage.bucket.logs_db":`База данных логов`,"storage.bucket.state_db":`База данных состояния`,"storage.bucket.attachments":`Вложения`,"storage.bucket.deletion_manifests":`Манифесты удаления`,"storage.bucket.other":`Прочее`,"storage.cleanup.title":`Очистка архива`,"storage.cleanup.help":`Удаляет самые старые архивные сессии по проценту. Активные сессии не затрагиваются. По умолчанию — карантин: файлы перемещаются в CODEX_HOME/.trash.`,"storage.cleanup.slider":`Доля самых старых архивов`,"storage.cleanup.percent":`{percent}%`,"storage.cleanup.preset":`{percent}`,"storage.cleanup.preview":`Предпросмотр`,"storage.cleanup.confirmTitle":`Подтвердить очистку архива`,"storage.cleanup.confirmBody":`Будет обработано {count} архивных файл(ов) (~{size}), самые старые {percent}%.`,"storage.cleanup.moreFiles":`…и ещё {n}`,"storage.cleanup.permanent":`Удалить навсегда (без карантина)`,"storage.cleanup.permanentWarn":`Безвозвратное удаление нельзя отменить.`,"storage.cleanup.quarantineNote":`Файлы перемещаются в .trash под CODEX_HOME. Восстановить можно на вкладке «Карантин».`,"storage.cleanup.cancel":`Отмена`,"storage.cleanup.confirmQuarantine":`В карантин`,"storage.cleanup.confirmPermanent":`Удалить навсегда`,"storage.cleanup.doneQuarantine":`В карантин: {count} файл(ов) ({size}).`,"storage.cleanup.donePermanent":`Удалено навсегда: {count} файл(ов) ({size}).`,"storage.cleanup.previewFailed":`Не удалось выполнить предпросмотр.`,"storage.cleanup.cleanupFailed":`Не удалось выполнить очистку.`,"storage.cleanup.err.codex_busy":`Codex использует state.sqlite — закройте Codex и повторите попытку.`,"storage.cleanup.err.stale_preview":`Архивы изменились после предпросмотра — выполните предпросмотр снова.`,"storage.cleanup.err.restore_pending_overlap":`Выбранные архивы пересекаются с незавершённым восстановлением из корзины — завершите или повторите восстановление.`,"storage.cleanup.err.referenced_history":`Выбранные архивы всё ещё ссылаются из forked или paginated history.`,"storage.cleanup.err.invalid_digest":`Digest предпросмотра отсутствует или недействителен.`,"storage.cleanup.err.invalid_mode":`Режим должен быть quarantine или permanent.`,"storage.cleanup.err.fs_failed":`Ошибка файловой очистки. Часть изменений могла уже примениться — проверьте CODEX_HOME/.trash и указанный путь восстановления.`,"storage.cleanup.err.fs_failed_trash":`Ошибка файловой очистки. Часть изменений могла уже примениться — проверьте {trashDir} и manifest.json на восстанавливаемые файлы.`,"storage.cleanup.err.db_reconcile_failed":`Не удалось обновить базу состояния Codex.`,"storage.cleanup.err.cleanup_failed":`Не удалось выполнить очистку.`,"storage.trash.title":`Карантин`,"storage.trash.help":`Архивные сессии в CODEX_HOME/.trash. Восстановление возвращает JSONL и строки потоков.`,"storage.trash.empty":`Нет записей в карантине.`,"storage.trash.loading":`Загрузка карантина…`,"storage.trash.col.when":`В карантине с`,"storage.trash.col.files":`Файлы`,"storage.trash.col.size":`Размер`,"storage.trash.col.mode":`Режим`,"storage.trash.col.id":`Запись`,"storage.trash.restore":`Восстановить`,"storage.trash.confirmTitle":`Восстановить запись карантина?`,"storage.trash.confirmBody":`Вернуть {count} файл(ов) (~{size}) из {id} в архивные сессии.`,"storage.trash.cancel":`Отмена`,"storage.trash.confirmRestore":`Восстановить`,"storage.trash.done":`Восстановлено {count} файл(ов) ({size}).`,"storage.trash.restoreFailed":`Не удалось восстановить.`,"storage.trash.listFailed":`Не удалось получить список карантина.`,"storage.trash.mode.quarantine":`карантин`,"storage.trash.mode.permanent":`permanent (незавершён)`,"storage.trash.err.codex_busy":`Codex использует state.sqlite — закройте Codex и повторите попытку.`,"storage.trash.err.invalid_trash":`Идентификатор записи корзины отсутствует или недействителен.`,"storage.trash.err.missing_trash":`Запись корзины не найдена.`,"storage.trash.err.dest_exists":`Цель восстановления уже существует — удалите или переименуйте архивный файл и повторите.`,"storage.trash.err.fs_failed":`Ошибка восстановления файлов. Часть файлов могла уже восстановиться — проверьте archived_sessions и .trash.`,"storage.trash.err.storage_mutation_busy":`Выполняется другая очистка или восстановление — повторите позже.`,"storage.trash.err.db_reconcile_failed":`Не удалось восстановить строки базы состояния Codex.`,"storage.trash.err.restore_failed":`Не удалось восстановить.`,"storage.trash.err.restore_worker_timeout":`Восстановление заняло слишком много времени (более 10 минут) и было остановлено.`,"storage.trash.err.restore_worker_aborted":`Восстановление отменено при завершении работы.`,"storage.trash.err.restore_worker_failed":`Worker восстановления завершился с ошибкой или аварийно.`,"storage.policy.title":`Политика автоочистки`,"storage.policy.help":`Необязательная пакетная очистка, когда архивные сессии превышают порог. По умолчанию выкл. — никогда не включается сама.`,"storage.policy.loading":`Загрузка политики…`,"storage.policy.loadFailed":`Не удалось загрузить политику очистки.`,"storage.policy.saveFailed":`Не удалось сохранить политику очистки.`,"storage.policy.runFailed":`Не удалось выполнить политику.`,"storage.policy.alreadyRunning":`Выполнение политики очистки уже выполняется.`,"storage.policy.invalid":`Недопустимые значения политики.`,"storage.policy.enabled":`Включить автоочистку`,"storage.policy.enabledHint":`По умолчанию выкл. При включении работает только по выбранному расписанию (или «Запустить сейчас»).`,"storage.policy.threshold":`Когда размер архива больше (ГиБ)`,"storage.policy.trigger":`Триггер`,"storage.policy.target":`Цель очистки`,"storage.policy.targetPercent":`Удалить самые старые архивы (%)`,"storage.policy.targetReduce":`Уменьшить архив до (ГиБ)`,"storage.policy.thresholdInc":`Увеличить порог`,"storage.policy.thresholdDec":`Уменьшить порог`,"storage.policy.percentInc":`Увеличить процент`,"storage.policy.percentDec":`Уменьшить процент`,"storage.policy.reduceInc":`Увеличить целевой размер`,"storage.policy.reduceDec":`Уменьшить целевой размер`,"storage.policy.schedule":`Расписание`,"storage.policy.schedule.manual":`Только вручную`,"storage.policy.schedule.startup":`При запуске прокси`,"storage.policy.schedule.daily":`Ежедневно`,"storage.policy.schedule.weekly":`Еженедельно`,"storage.policy.mode":`Режим удаления`,"storage.policy.mode.quarantine":`Карантин (по умолчанию)`,"storage.policy.mode.permanent":`Удалить навсегда`,"storage.policy.permanentWarn":`Постоянный режим нельзя отменить. Предпочитайте карантин, если не уверены.`,"storage.policy.lastRun":`Последний запуск`,"storage.policy.lastRunDetail":`Удалено {count} · освобождено {size}`,"storage.policy.nextRun":`Следующий запуск`,"storage.policy.never":`Никогда`,"storage.policy.save":`Сохранить`,"storage.policy.runNow":`Запустить сейчас`,"storage.policy.running":`Выполняется…`,"storage.policy.saved":`Политика сохранена.`,"storage.policy.skippedDisabled":`Политика отключена — сначала включите её.`,"storage.policy.skippedUnder":`Размер архива ниже порога — делать нечего.`,"storage.policy.skippedEmpty":`Нет архивных кандидатов под цель.`,"storage.policy.doneQuarantine":`Политика отправила в карантин {count} файл(ов) ({size}).`,"storage.policy.donePermanent":`Политика навсегда удалила {count} файл(ов) ({size}).`,"storage.policy.metadataSaveWarning":`Выполнение политики завершено, но не удалось сохранить метаданные расписания.`,"modal.addNamed":`Добавить: {label}`,"modal.add":`Добавить провайдера`,"modal.search":`Поиск провайдеров…`,"modal.logInWith":`Войти через {label}`,"modal.waitingBrowser":`Ожидание браузера…`,"modal.providerName":`Название провайдера`,"modal.adapter":`Адаптер`,"modal.baseUrl":`Базовый URL`,"modal.endpoint":`Конечная точка`,"modal.endpoint.tokenPlan":`Пакет токенов`,"modal.endpoint.payAsYouGo":`Оплата по факту`,"modal.endpoint.custom":`Своя`,"modal.defaultModel":`Модель по умолчанию (необязательно)`,"modal.allowPrivateNetwork":`Разрешить локальную/частную сеть`,"modal.allowPrivateNetworkHint":`Включайте только для провайдеров, намеренно развёрнутых у себя. Конечные точки метаданных остаются заблокированными.`,"modal.nameRequired":`Укажите название провайдера`,"modal.baseUrlRequired":`Укажите базовый URL`,"modal.networkError":`Ошибка сети — запущен ли прокси?`,"modal.loginFailStart":`Не удалось начать вход`,"modal.waitingLogin":`Ожидание входа в браузере…`,"modal.loggingIn":`Выполняется вход…`,"modal.loginTimeout":`Время входа истекло — попробуйте ещё раз.`,"modal.back":`Назад`,"modal.badge.oauth":`OAuth`,"modal.customProvider":`Свой провайдер`,"modal.failedStatus":`Ошибка ({status})`,"modal.loginError":`Ошибка входа: {error}`,"modal.badge.codexLogin":`Вход через Codex`,"modal.badge.local":`Локальный`,"modal.badge.apiKey":`API-ключ`,"modal.badge.direct":`Прямой`,"modal.badge.pool":`Пул`,"modal.badge.free":`Бесплатно`,"modal.invalidPreset":`Этот встроенный пресет провайдера неполный. Перезапустите прокси и попробуйте ещё раз.`,"modal.freeTierTitle":`Бесплатный тариф`,"modal.freeTierDefault":`API-ключ не нужен. Работает из коробки.`,"modal.tab.accounts":`Аккаунты`,"modal.tab.free":`Бесплатные`,"modal.tab.paid":`Платные`,"modal.accountsHint":`Здесь можно войти в аккаунты ChatGPT/Codex и OAuth-провайдеров, а также в аккаунты с API-ключами. Провайдер OpenAI уже встроен — просто войдите, а не добавляйте его заново.`,"modal.accountsCodexAuthLink":`Аутентификация Codex`,"modal.notListed":`Нет нужного провайдера? Добавьте свой`,"modal.catalogLoading":`Загрузка каталога…`,"modal.accountLogin":`Войти`,"modal.accountLogout":`Выйти`,"modal.accountAdd":`Добавить аккаунт`,"modal.accountManage":`Управление`,"modal.accountCodexPool":`Пул аккаунтов ChatGPT`,"modal.accountLoggedIn":`Вход выполнен`,"modal.accountLoggedOut":`Вход не выполнен`,"quota.fiveHourLimit":`5-часовой лимит`,"quota.ageMinutes":`{n} мин`,"quota.ageHours":`{n} ч`,"quota.ageDays":`{n} дн`,"quota.observedAgo":`Получено {age} назад`,"quota.observedHint":`Meta сообщает об использовании только во время потокового ответа, поэтому это последнее полученное значение, а не текущее.`,"quota.weeklyLimit":`Недельный лимит`,"quota.monthlyLimit":`30-дневный лимит`,"quota.cursorFirstParty":`Собственные модели`,"quota.cursorApiUsage":`Использование API`,"quota.totalSubscriptionCredits":`Всего кредитов подписки`,"quota.creditsBalance":`Остаток кредитов`,"quota.creditsPeriodEnds":`Расчётный период заканчивается {date}`,"quota.usedPercent":`Использовано {pct}%`,"quota.limitReached":`Лимит исчерпан`,"quota.resetsToday":`Сброс сегодня в {time}`,"quota.resetsTomorrow":`Сброс завтра в {time}`,"quota.resetsAt":`Сброс: {when}`,"quota.resetsRelativeMinutes":`Сброс через {n} мин`,"quota.resetsRelativeHours":`Сброс через {n} ч`,"pws.status.ready":`Готов`,"pws.status.needsSetup":`Требуется настройка`,"pws.status.needsAttention":`Требует внимания`,"pws.auth.chatgptPassthrough":`Сквозной режим ChatGPT`,"pws.auth.noKey":`Ключ не нужен`,"pws.freeTitle":`Бесплатный тариф (ключ всё же может потребоваться)`,"pws.localTitle":`Локальная среда выполнения`,"pws.modelCountOne":`1 модель`,"pws.modelCount":`{count} моделей`,"pws.rail.suffixDefault":` · по умолчанию`,"pws.rail.suffixLocal":` · локальный`,"pws.rail.suffixFree":` · бесплатный`,"pws.rail.selectAria":`Выбрать {name} — {status}{suffix}`,"pws.searchPlaceholder":`Поиск провайдеров…`,"pws.filterAria":`Фильтр провайдеров`,"pws.providerFiltersAria":`Фильтры провайдеров`,"pws.filters":`Фильтры`,"pws.filterStatus":`Статус`,"pws.pricing":`Тариф`,"pws.paid":`Платные`,"pws.filterType":`Тип`,"pws.type.cloud":`Облачные`,"pws.type.local":`Локальные`,"pws.type.selfHosted":`Свой хостинг`,"pws.type.login":`Вход`,"pws.sort":`Сортировка`,"pws.sortProvidersAria":`Сортировка провайдеров`,"pws.sort.az":`A–Z`,"pws.sort.za":`Z–A`,"pws.sort.freePaid":`Сначала бесплатные`,"pws.sort.paidFree":`Сначала платные`,"pws.sort.accountsFirst":`Сначала с аккаунтами`,"pws.resetAll":`Сбросить всё`,"pws.providerList":`Список провайдеров`,"pws.providersAria":`Провайдеры`,"pws.groupReady":`Готовы ({count})`,"pws.groupNeedsSetup":`Требуется настройка ({count})`,"pws.groupDisabled":`Отключены ({count})`,"pws.noSearchResults":`По вашему запросу провайдеры не найдены.`,"pws.noMatchFilters":`Нет провайдеров, соответствующих фильтрам.`,"pws.noProvidersConfigured":`Провайдеры не настроены.`,"pws.workspaceMainAria":`Сведения о провайдере`,"pws.detailComingSoon":`Подробный вид скоро появится — для управления этим провайдером используйте классический вид.`,"pws.selectPrompt":`Выберите провайдера из списка.`,"pws.connectFirst":`Подключите первого провайдера`,"pws.empty.browseFree":`Посмотреть бесплатных провайдеров`,"pws.empty.browseFreeDesc":`Начните без подписки`,"pws.empty.connectAccount":`Подключить аккаунт`,"pws.empty.connectAccountDesc":`Войдите через ChatGPT или аккаунт провайдера`,"pws.empty.addEndpoint":`Добавить конечную точку`,"pws.empty.addEndpointDesc":`Свой базовый URL и API-ключ`,"pws.tab.overview":`Обзор`,"pws.tab.models":`Модели`,"pws.tab.usage":`Использование`,"pws.tab.accounts":`Аккаунты`,"pws.tab.settings":`Настройки`,"pws.connection":`Подключение`,"pws.status.connected":`Подключено`,"pws.attentionTitle":`Требует внимания`,"pws.attention.reauth":`Активному аккаунту требуется повторная аутентификация`,"pws.attention.reauthForward":`Активному аккаунту Codex требуется повторная аутентификация — откройте вкладку «Аккаунты», чтобы исправить`,"pws.attention.missingCredentials":`Отсутствуют учётные данные`,"pws.cell.auth":`Аутентификация`,"pws.cell.note":`Заметка`,"pws.cell.defaultModel":`Модель по умолчанию`,"pws.statsAria":`Статистика провайдера`,"pws.statsTitle":`Статистика`,"pws.stats.totalRequests":`Запросы (30 дн.)`,"pws.stats.totalTokens":`Токены (30 дн.)`,"pws.stats.quotaUpdated":`Квота обновлена`,"pws.stats.quotaTracked":`Лимиты запросов отслеживаются на вкладке «Использование».`,"pws.stats.source":`Источник`,"pws.usageLast30d":`Использование (последние 30 дней)`,"pws.estimatedCost":`Ориентировочная стоимость`,"pws.costDisclaimer":`Оценка на основе публичных цен API, не фактический счёт.`,"pws.modelBreakdown":`Разбивка по моделям`,"pws.col.model":`Модель`,"pws.col.cost":`Ориент. стоимость`,"pws.col.tokens":`Токены`,"pws.col.requests":`Запр.`,"pws.col.share":`Доля`,"pws.tokenInput":`Вход`,"pws.tokenOutput":`Выход`,"pws.metricRequests":`запросов`,"pws.metricTokens":`токенов`,"pws.usageUnavailable":`Использование пока не зафиксировано.`,"pws.rateLimits":`Лимиты запросов`,"pws.quotaUnavailable":`Нет данных о квоте для этого провайдера.`,"pws.accountQuotaUnavailable":`Данные о лимитах временно недоступны; при наличии показываются последние известные значения.`,"pws.selected":`Выбрана`,"pws.copyModelId":`Копировать ID`,"pws.modelCopied":`Скопировано!`,"pws.modelsAvailable":`Доступно: {count}`,"pws.modelSearchPlaceholder":`Фильтр моделей…`,"pws.modelsLoading":`Загрузка моделей…`,"pws.modelsLoadFailed":`Не удалось загрузить модели.`,"pws.modelsNeedsReauth":`Для автоматического обнаружения моделей необходимо повторно войти в аккаунт. Пока отображаются настроенные модели.`,"pws.modelsConfiguredFallback":`Отображаются настроенные модели (автоматическое обнаружение недоступно).`,"pws.modelsTruncated":`Показаны первые {shown} моделей из {total}. Примените фильтр, чтобы сузить список.`,"pws.retry":`Повторить`,"pws.noModels":`Для этого провайдера модели не обнаружены.`,"pws.noModelMatch":`Нет моделей, соответствующих фильтру.`,"pws.adapterBaseRequired":`Укажите адаптер и базовый URL.`,"pws.addAccount":`Добавить аккаунт`,"pws.addKey":`Добавить API-ключ`,"pws.apiKeys":`API-ключи`,"pws.authMode":`Режим аутентификации`,"pws.availableAccounts":`Доступные аккаунты`,"pws.accountOrdinal":`Аккаунт {count}`,"pws.accountsLoading":`Загрузка аккаунтов…`,"pws.accountsLoadFailed":`Не удалось загрузить аккаунты.`,"pws.retryAccounts":`Повторить`,"pws.noAccounts":`Аккаунты пока не подключены.`,"pws.cockpitImportDescription":`Импортируйте JSON-экспорт Cockpit Tools Antigravity с этого устройства. Содержимое файла не показывается.`,"pws.cockpitImportFileLabel":`JSON-экспорт Cockpit Tools Antigravity`,"pws.cockpitImportChooseFile":`Выбрать JSON-файл`,"pws.cockpitImporting":`Импорт…`,"pws.cockpitImportInvalid":`Выбранный файл не является корректным JSON-экспортом или слишком велик.`,"pws.cockpitImportFailed":`Не удалось завершить импорт аккаунтов.`,"pws.cockpitImportComplete":`Импорт завершён: импортировано — {imported}, обновлено — {updated}, ошибок — {failed}, неподдерживаемых — {unsupported}.`,"pws.accountSwitching":`Переключение…`,"pws.accountCurrent":`Текущий аккаунт`,"pws.defaultModelNone":`Нет (использовать значение провайдера)`,"pws.discardSettings":`Не сохранять`,"pws.jsonEditorDesc":`Редактируйте исходную JSON-конфигурацию провайдера. Изменения сохраняются сразу.`,"pws.jsonEditorTitle":`Редактор JSON — {name}`,"pws.jsonRestore":`Восстановить`,"pws.jsonSave":`Сохранить`,"pws.loggedInTitle":`Вход выполнен`,"pws.notLoggedInTitle":`Вход не выполнен`,"pws.note":`Заметка`,"pws.allowPrivateNetwork":`Разрешить локальную/частную сеть`,"pws.liveModels":`Обнаруживать модели провайдера`,"pws.liveModelsDesc":`Загружать актуальный каталог моделей провайдера. Выключите, чтобы использовать только настроенные статические модели.`,"pws.xaiResponsesOptIn":`Использовать Responses API для Grok 4.5 и 4.6`,"pws.xaiResponsesOptInDesc":`Направляет обе модели через openai-responses. Другие модели Grok и поведение tier не меняются.`,"pws.xaiResponsesOptInMixed":`Включено частично.`,"pws.cursorTransport":`Транспорт Cursor`,"pws.cursorTransportHttp2":`HTTP/2 (по умолчанию)`,"pws.cursorTransportHttp1":`HTTP/1.1 (совместимость с прокси)`,"pws.cursorTransportDesc":`Используйте HTTP/1.1, если прокси нестабильно передаёт поток Cursor по HTTP/2.`,"pws.optionalPlaceholder":`Необязательно`,"pws.providerId":`ID провайдера`,"pws.reauth":`Нужна переавторизация`,"pws.reauthenticate":`Переавторизоваться`,"pws.copyDoctor":`Скопировать ocx doctor`,"pws.doctorCopied":`Скопировано`,"pws.healthCooldownHint":`Дождитесь окончания паузы. Пока не проверяйте эту учётную запись.`,"pws.doctorCopyUnavailable":`Буфер обмена недоступен`,"pws.healthLabel.rateLimited":`Ограничение частоты`,"pws.healthLabel.quotaLimited":`Ограничение квоты`,"pws.healthLabel.reauthRequired":`Требуется повторная аутентификация`,"pws.healthLabel.refreshFailed":`Ошибка обновления`,"pws.healthLabel.metadataMismatch":`Несоответствие метаданных`,"pws.healthLabel.credentialConflict":`Конфликт учётных данных`,"pws.healthSummary.rateLimited":`{provider} {account}: ограничение частоты до {until}. Маршрутизация этой учётной записи приостановлена до этого времени.`,"pws.healthSummary.quotaLimited":`{provider} {account}: квота ограничена до {until}. Маршрутизация этой учётной записи приостановлена до этого времени.`,"pws.healthSummary.reauthRequired":`{provider} {account}: требуется повторная аутентификация.`,"pws.healthSummary.credentialConflict":`{provider} {account}: конфликт учётных данных.`,"pws.healthSummary.metadataMismatch":`{provider} {account}: несоответствие метаданных.`,"pws.healthSummary.staleCredentials":`{provider} {account}: неполные учётные данные.`,"pws.removeConfirm":`Удалить`,"pws.removeConfirmBody":`Удалить провайдера "{name}"? Это действие нельзя отменить.`,"pws.removeDefaultConfirmBody":`Удалить провайдера по умолчанию "{name}"? "{defaultProvider}" станет провайдером по умолчанию. Это действие нельзя отменить.`,"pws.removeConfirmTitle":`Удалить провайдера`,"pws.saveSettings":`Сохранить`,"pws.pacingTitle":`Интервал запросов`,"pws.pacingDesc":`Равномерно задерживает начало исходящих запросов к провайдеру. Потоковые ответы могут пересекаться.`,"pws.pacingEnabled":`Включено`,"pws.pacingRpm":`Запросов в минуту`,"pws.pacingRpmUnit":`RPM`,"pws.pacingDelay":`Минимальный интервал (мс)`,"pws.pacingSlowerWins":`Действует более медленный лимит провайдера. Правила моделей могут только увеличить задержку.`,"pws.pacingQueued":`в очереди`,"pws.pacingNextSlot":`до следующего слота`,"pws.pacingLastModel":`последняя модель`,"pws.pacingNone":`Нет`,"pws.pacingModelOverrides":`Правила моделей`,"pws.pacingModel":`Модель`,"pws.pacingAdd":`Добавить правило`,"pws.pacingRemove":`Удалить`,"pws.pacingRemoveModel":`Удалить интервал запросов для {model}`,"pws.pacingRuleRequired":`Сначала задайте лимит провайдера или правило модели.`,"pws.saving":`Сохранение…`,"pws.settingsSaved":`Настройки сохранены.`,"pws.accountModeSaved":`Режим аккаунта сохранён.`,"pws.accountModeFailed":`Не удалось переключить режим аккаунта.`,"pws.accountModeConfirm":`Переключить режим аккаунта OpenAI? Текущие беседы будут перенаправлены на другой набор аккаунтов, а использование квоты будет учитываться в новом режиме.`,"pws.settingsUnsavedBar":`Есть несохранённые изменения.`,"pws.unsavedLeaveBody":`Есть несохранённые изменения. Сохранить их перед переходом?`,"pws.unsavedLeaveTitle":`Несохранённые изменения`,"pws.attentionRequired":`Требуется внимание`,"pws.attentionAria":`{name}: {reason}`,"pws.missingCredentials":`Отсутствуют учётные данные`,"pws.editJsonDesc":`Редактировать конфигурацию прокси в формате JSON`,"pws.updatesUnavailable":`Обновление провайдера недоступно.`,"pws.dashboard.title":`Обзор провайдеров`,"pws.dashboard.subtitle":`Управляйте всеми провайдерами моделей в одном месте.`,"pws.dashboard.rateLimits":`Лимиты запросов`,"pws.capacity.estimate":`Оценка пула по настроенным весам`,"pws.capacity.currentAccount":`Текущая активная учётная запись`,"pws.capacity.nextRecovery":`Следующее восстановление ёмкости`,"pws.capacity.recoveryShare":`+{percent}% ёмкости пула`,"pws.capacity.incomplete":`Неполное покрытие: исключено аккаунтов: {excluded}`,"pws.capacity.uncalibratedPlan":`Аккаунтов с некалиброванным планом: {count}. Они учитываются с базовым весом места, поэтому оценка может быть заниженной`,"pws.capacity.partial":`Частичное покрытие окон: для {count} аккаунтов доступны не все показанные окна лимитов`,"pws.capacity.windowPartial":`Частично`,"pws.capacity.windowPartialA11y":`{window}: неполное покрытие аккаунтов`,"pws.dashboard.recentlyUsed":`Недавно использованные`,"pws.dashboard.requests":`{count} запросов`,"pws.dashboard.checkedAgo":`Проверено {time}`,"pws.dashboard.noQuota":`Нет данных о квоте`,"pws.dashboard.noUsage":`Данных об использовании пока нет`,"pws.dashboard.noRateLimits":`Данных о лимитах пока нет`,"pws.allProviders":`Обзор провайдеров`,"pws.enabledLabel":`Включён`,"pws.testConnection":`Проверить подключение`,"pws.testing":`Проверка…`,"pws.connectionOk":`Подключение успешно`,"pws.connectionFailed":`Ошибка подключения`,"pws.connectionNotApplicable":`Не применимо — этот провайдер использует статический каталог моделей.`,"pws.editSettings":`Изменить настройки`,"pws.viewUsage":`Подробнее об использовании`,"pws.allSystemsOk":`Все системы работают штатно`,"pws.apiKeyConfigured":`API-ключ настроен`,"pws.addApiKey":`Добавить API-ключ`,"pws.loggedInAs":`Выполнен вход как {email}`,"pws.notLoggedIn":`Вход не выполнен`,"pws.passthrough":`Сквозной режим Codex`,"pws.notes":`Заметки`,"pws.notePlaceholder":`Добавьте заметку об этом провайдере...`,"pws.noteSaved":`Заметка сохранена`,"pws.authSummary":`Аутентификация`,"time.justNow":`Только что`,"time.notChecked":`Не проверялось`,"time.minutesAgo":`{n} мин назад`,"time.hoursAgo":`{n} ч назад`,"time.daysAgo":`{n} дн. назад`,"modal.noMatch":`Ничего не найдено.`,"modal.oauthDefaultNote":`Войдите со своим аккаунтом — API-ключ не нужен.`,"modal.oauthComingSoon":`Вход через OAuth для {label} появится в следующем обновлении. Пока используйте API-ключ.`,"modal.oauthComingSoonShort":`Вход через OAuth для этого провайдера появится в следующем обновлении — пока используйте API-ключ.`,"modal.useApiKeyInstead":`Использовать API-ключ`,"modal.setupGuide":`Инструкция по настройке`,"modal.setupStep1Prefix":`Откройте`,"modal.setupDashboardLink":`панель управления {label}`,"modal.setupStep1Suffix":`и скопируйте свой API-ключ`,"modal.setupStep2":`Вставьте его в поле «API-ключ» ниже`,"modal.setupStep3":`Нажмите «Добавить провайдера» — модели будут обнаружены автоматически`,"modal.namePlaceholder":`напр. openrouter`,"modal.duplicateWarn":`Провайдер "{name}" уже существует и будет перезаписан.`,"modal.forwardHintPrefix":`Ключ не нужен — прокси передаёт ваши учётные данные`,"modal.forwardCredentials":`codex login`,"modal.forwardHintSuffix":`этому провайдеру.`,"modal.localHint":`API-ключ не сохраняется. Будет добавлен статический публичный каталог моделей Cursor для Codex, но живой транспорт Cursor и нативное выполнение файловых и shell-операций остаются отключёнными до прохождения аудита.`,"modal.getApiKey":`Получить API-ключ {label}`,"modal.apiKey":`API-ключ`,"modal.apiKeyTransport":`Заголовок API-ключа`,"modal.apiKeyTransportNative":`x-api-key (нативный Anthropic)`,"modal.apiKeyTransportBearer":`Authorization: Bearer`,"modal.apiKeyPlaceholder":`sk-… (или $ENV_VAR)`,"modal.defaultModelPlaceholder":`напр. gpt-5.5`,"modal.baseUrlPlaceholder":`https://...`,"modal.baseUrlPlaceholderError":`Базовый URL содержит незаменённый {placeholder}. Замените его реальным значением.`,"modal.baseUrlPlaceholderHint":`Перед добавлением замените {placeholder} в базовом URL на реальный ID аккаунта.`,"modal.adding":`Добавление…`,"modal.useOauthLogin":`← Войти через OAuth`,"nav.codexAuth":`Аутентификация Codex`,"nav.codexSet":`Настройки Codex`,"codexSet.tab.multiauth":`Мультиаутентификация`,"codexSet.tab.prompt":`Промпт`,"codexSet.prompt.title":`Слои промпта`,"codexSet.prompt.timing":`Применяется к новым сессиям. Запущенные сессии сохраняют текущие настройки промпта.`,"codexSet.prompt.staleRevision":`Конфигурация изменилась в другом месте. Список перезагружен.`,"codexSet.prompt.writeFailed":`Не удалось сохранить изменение.`,"codexSet.prompt.loadFailed":`Не удалось загрузить слои промпта.`,"codexSet.prompt.repair":`Восстановить`,"codexSet.prompt.repairFailed":`Не удалось выполнить восстановление.`,"codexSet.drift.journalPresent":`Предыдущая запись не завершилась. Восстановление произойдёт автоматически при следующей записи.`,"codexSet.drift.projectionStale":`Сохранённые слои и значение в config.toml расходятся. Восстановление перезапишет значение из ваших слоёв.`,"codexSet.drift.storeMissing":`Файл слоёв отсутствует, но инструкции в config.toml остались. Восстановление сначала создаст резервную копию и сохранит текст одним слоем.`,"codexSet.drift.ownedMalformed":`Сгенерированная строка в config.toml была изменена вручную, поэтому перезаписывать её небезопасно.`,"codexSet.custom.adoptUnsupported":`Значение в {path}, строка {line}, не является однострочной строкой и не может быть импортировано. Перенесите его вручную, чтобы управлять им здесь.`,"codexSet.prompt.unreadable":`Файл конфигурации Codex существует, но его не удалось прочитать, поэтому изменения отклонены.`,"codexSet.layer.permissions":`Разрешения`,"codexSet.layer.collaboration":`Режим совместной работы`,"codexSet.layer.environment":`Контекст окружения`,"codexSet.layer.apps":`Приложения`,"codexSet.layer.skills":`Навыки`,"codexSet.prompt.extensionsUnknown":`Расширения могут добавлять собственные слои. Codex не раскрывает их, поэтому показать их здесь нельзя.`,"codexSet.group.transition":`Уведомления о переходе`,"codexSet.group.transitionDesc":`Они сообщают об изменении, а не описывают состояние, поэтому появляются только при переходе сессии в реальное время или смене модели.`,"codexSet.custom.slotNote":`Пользовательские слои объединяются в один раздел в этом порядке.`,"codexSet.row.alwaysOn":`Всегда включён`,"codexSet.row.onChange":`При изменении`,"codexSet.row.featureGated":`Настраивается в [features]`,"codexSet.row.openFeatures":`Открыть настройки`,"codexSet.dialog.setValue":`{value} (по умолчанию {fallback})`,"codexSet.dialog.copyKey":`Скопировать ключ`,"codexSet.dialog.unknownLayer":`В этой сборке нет описания этого слоя. Он получен из более новой среды выполнения Codex, чем панель.`,"codexSet.custom.heading":`Пользовательские слои`,"codexSet.custom.add":`+ Добавить слой`,"codexSet.custom.newTitle":`Новый слой`,"codexSet.custom.editTitle":`Изменить слой`,"codexSet.custom.titleLabel":`Название`,"codexSet.custom.bodyLabel":`Инструкции`,"codexSet.custom.bodySize":`{bytes} из {max} байт`,"codexSet.custom.normalized":`Табуляции заменены четырьмя пробелами, а окончания строк — на LF.`,"codexSet.custom.titleRequired":`Введите название.`,"codexSet.custom.titleTooLong":`В названии {count} символов; предел — {max}.`,"codexSet.custom.titleMultiline":`Название должно занимать одну строку.`,"codexSet.custom.bodyTooLarge":`Размер этого слоя — {bytes} байт; предел — {max}.`,"codexSet.custom.composedTooLarge":`Общий размер включённых слоёв составит {bytes} байт и превысит предел.`,"codexSet.custom.invalidCharacter":`Управляющий символ в позиции {position} невозможно сохранить.`,"codexSet.custom.discardPrompt":`Отменить изменения?`,"codexSet.custom.keepEditing":`Продолжить редактирование`,"codexSet.custom.delete":`Удалить {title}`,"codexSet.custom.deleteConfirm":`Удалить этот слой? Это действие нельзя отменить.`,"codexSet.custom.layerGone":`Этот слой был удалён в другом месте, поэтому редактор закрыт.`,"codexSet.custom.deleteConfirmNamed":`Удалить “{title}”? Это действие нельзя отменить.`,"codexSet.custom.moveUp":`Переместить {title} вверх`,"codexSet.custom.prevLayer":`Предыдущий слой`,"codexSet.custom.nextLayer":`Следующий слой`,"codexSet.custom.navPosition":`{position} / {total}`,"codexSet.custom.moveDown":`Переместить {title} вниз`,"codexSet.custom.limitReached":`Можно сохранить не более {max} пользовательских слоёв.`,"codexSet.custom.notOwned":`developer_instructions записан вне opencodex, поэтому его нельзя изменить здесь. Импортируйте его, чтобы управлять им как слоем.`,"codexSet.custom.adopt":`Импортировать существующие инструкции`,"codexSet.custom.adoptConfirm":`Импортировать как слой`,"codexSet.custom.adoptRefused":`Не удалось импортировать существующее значение.`,"codexSet.custom.baseReplaced":`Для model_instructions_file задан путь {path}, поэтому базовый промпт заменён за пределами opencodex.`,"codexSet.lint.identity":`Здесь заявлена личность, отличная от той, которую задаёт Codex.`,"codexSet.lint.foreignTool":`Инструменты предоставляет реестр; упоминание инструмента здесь не создаёт его.`,"codexSet.lint.placeholder":`Инструкции не обрабатываются шаблонизатором, поэтому этот текст будет отправлен буквально.`,"codexSet.lint.applyPatch":`apply_patch определяется реестром инструментов, а не инструкциями.`,"codexSet.lint.approvalVocab":`Codex добавляет собственную терминологию подтверждений; это может ей противоречить.`,"codexSet.lint.environment":`Данные среды создаются позже и могут этому противоречить.`,"codexSet.lint.size":`Размер этого слоя превышает 8 KB. Его можно сохранить, но он расходует токены при каждом запросе.`,"codexSet.preset.blank":`Пустой слой`,"codexSet.preset.concise.name":`Краткий ответ`,"codexSet.preset.concise.description":`Короткие ответы без вступлений и лишнего форматирования.`,"codexSet.preset.concise.provenance":`Адаптировано на основе указаний Claude Code о краткости. Формулировки наши, это не копия.`,"codexSet.preset.planFirst.name":`План перед правками`,"codexSet.preset.planFirst.description":`Сначала изложить план, затем внести изменения.`,"codexSet.preset.planFirst.provenance":`Адаптировано на основе подхода Claude Code к планированию. Формулировки наши, это не копия.`,"codexSet.preset.explainWhy.name":`Объяснять причины`,"codexSet.preset.explainWhy.description":`Объяснять не только что, но и почему.`,"codexSet.preset.explainWhy.provenance":`Адаптировано на основе стиля подтверждений Grok Build. Формулировки наши, это не копия.`,"codexSet.preset.testFirst.name":`Сначала тест`,"codexSet.preset.testFirst.description":`Перед исправлением написать тест, который завершается с ошибкой.`,"codexSet.preset.testFirst.provenance":`Адаптировано на основе распространённой практики работы агентов. Формулировки наши, это не копия.`,"codexSet.preset.korean.name":`Ответы на корейском`,"codexSet.preset.korean.description":`Отвечать на корейском независимо от языка запроса.`,"codexSet.preset.korean.provenance":`Написано для opencodex на основе частого пользовательского запроса. Формулировки наши, это не копия.`,"codexSet.dialog.class":`Тип`,"codexSet.dialog.key":`Ключ конфигурации`,"codexSet.dialog.fileValue":`Значение в этом файле`,"codexSet.dialog.absentDefault":`не задано (по умолчанию: {value})`,"codexSet.dialog.noRenderedText":`Codex не раскрывает собранный текст встроенного слоя, поэтому здесь приведены описание слоя и его ключ, а не содержимое.`,"codexSet.dialog.sourceText":`Текст, отправляемый модели`,"codexSet.dialog.sourceBytes":`{bytes} байт`,"codexSet.dialog.notRendered":`На прочитанном нами шаге этот слой ничего не отправил. Разделы пересылаются только при изменении, поэтому в одной выборке слой может отсутствовать.`,"codexSet.dialog.emptySource":`Файл {path} существует, но пуст, поэтому этот слой ничего не отправляет.`,"codexSet.dialog.notExposed":`Базовый промпт передаётся вне списка сообщений, который Codex может напечатать, поэтому показать его здесь нельзя. Заменить его можно через model_instructions_file.`,"codexSet.dialog.textUnavailable":`На этой машине не удалось прочитать промпт Codex, поэтому текст недоступен.`,"codexSet.class.base":`Базовые инструкции`,"codexSet.class.config-toggle":`Переключается здесь`,"codexSet.class.feature-gated":`Управляется флагом функции`,"codexSet.class.runtime-conditional":`Зависит от среды выполнения`,"codexSet.class.extension-unknown":`Слой расширения`,"codexSet.layer.base-instructions":`Базовые инструкции`,"codexSet.layer.model-switch":`Уведомление о смене модели`,"codexSet.layer.personality":`Стиль общения`,"codexSet.layer.context-window-guidance":`Рекомендации по контекстному окну`,"codexSet.layer.realtime":`Реальное время`,"codexSet.layer.agents-md":`AGENTS.md`,"codexSet.layer.environments-instructions":`Среды выполнения`,"codexSet.layer.plugins":`Плагины`,"codexSet.layer.tools":`Инструменты`,"codexSet.layer.multi-agent-mode":`Мультиагентный режим`,"codexSet.layer.git-attribution":`Атрибуция коммитов`,"codexSet.about.base-instructions":`Собственные инструкции Codex. Они передаются вместе с запросом, и отключить их нельзя.`,"codexSet.about.model-switch":`Добавляется при смене модели во время диалога.`,"codexSet.about.personality":`Указания по тону и стилю, управляемые флагом функции.`,"codexSet.about.context-window-guidance":`Рекомендации по оставшемуся бюджету контекста, управляемые флагом функции.`,"codexSet.about.realtime":`Добавляется для сеансов реального времени.`,"codexSet.about.agents-md":`Файлы AGENTS.md вашего проекта. Эта страница лишь показывает слой и никогда не изменяет документацию проекта.`,"codexSet.about.permissions":`Описывает действующие настройки песочницы и подтверждений.`,"codexSet.about.collaboration":`Описывает активный режим совместной работы.`,"codexSet.about.environment":`Рабочий каталог, платформа и другие сведения об окружении.`,"codexSet.about.environments-instructions":`Указания для сред отложенного выполнения, управляемые флагом функции.`,"codexSet.about.apps":`Правила использования подключённых приложений.`,"codexSet.about.plugins":`Добавляется, если выбран плагин или какой-либо плагин объявляет возможность.`,"codexSet.about.tools":`Описания отложенных инструментов, управляемые флагом функции.`,"codexSet.about.skills":`Список доступных навыков.`,"codexSet.about.multi-agent-mode":`Инструкции для подагентов, управляемые флагом функции.`,"codexSet.about.git-attribution":`Просит модель добавлять трейлер Co-authored-by: Codex в коммиты, которые она пишет, и строку Generated with Codex. в пул-реквесты, которые она открывает. Codex берёт это из вашей учётной записи, поэтому настройки нет ни здесь, ни в [features]. Если в учётной записи атрибуция отключена, Codex отправляет обратную инструкцию, а не молчит.`,"codexSet.condition.model-switch":`Добавляется только после смены модели во время сеанса.`,"codexSet.condition.realtime":`Добавляется только в сеансе реального времени.`,"codexSet.condition.agents-md":`Добавляется, если для рабочего каталога найден документ проекта.`,"codexSet.condition.plugins":`Добавляется, если выбран плагин или какой-либо плагин объявляет возможность.`,"codexSet.condition.git-attribution":`Определяется политикой атрибуции вашей учётной записи.`,"codexSet.base.title":`Базовый промпт`,"codexSet.base.prev":`Предыдущий вариант`,"codexSet.base.next":`Следующий вариант`,"codexSet.base.position":`{position} / {total}`,"codexSet.base.swipeHint":`Проведите в сторону, используйте клавиши-стрелки или кнопки со стрелками. Применяется к новым сессиям.`,"codexSet.base.defaultTitle":`Собственный базовый промпт Codex`,"codexSet.base.defaultBody":`Вариант по умолчанию здесь не хранится, поэтому его нечего править или удалять: выбор просто убирает model_instructions_file из конфигурации, и Codex берёт свой штатный промпт.`,"codexSet.base.variantTitle":`Название`,"codexSet.base.variantBody":`Промпт`,"codexSet.base.replacesWarning":`Это ЗАМЕНЯЕТ собственный базовый промпт Codex, а не дополняет его. Короткий текст здесь означает короткие инструкции для модели.`,"codexSet.base.use":`Использовать этот`,"codexSet.base.inUse":`Используется`,"codexSet.base.externalBlocked":`model_instructions_file уже указывает на {path}, и это значение записал не opencodex. Уберите его сами, прежде чем выбирать здесь.`,"nav.api":`API`,"nav.integrations":`Интеграции`,"nav.openMenu":`Открыть меню`,"nav.closeMenu":`Закрыть меню`,"integrations.subtitle":`Подключайте клиенты к opencodex, управляйте учётными данными и восстанавливайте конфигурацию клиентов.`,"integrations.tabsLabel":`Разделы интеграций`,"integrations.tab.overview":`Обзор`,"integrations.tab.keys":`Ключи API`,"integrations.tab.codex":`Codex`,"integrations.tab.claude":`Claude`,"integrations.tab.grok":`Grok Build`,"integrations.tab.opencode":`OpenCode`,"integrations.tab.pi":`Pi`,"integrations.tab.omp":`OMP`,"integrations.tab.hermes":`Hermes`,"integrations.tab.openclaw":`OpenClaw`,"integrations.tab.kimi":`Kimi Code`,"integrations.tab.gajae":`Gajae Code`,"integrations.tab.dsh":`DSH`,"integrations.tab.mcode":`MiniMax Code`,"integrations.tab.zcode":`ZCode`,"integrations.tab.prime":`Prime Agent`,"integrations.tab.aside":`Aside`,"integrations.codex.title":`Codex CLI`,"integrations.codex.body":`Подключением Codex управляет прокси-сервис. При запуске opencodex оно применяется, а при остановке сервиса восстанавливается нативная маршрутизация.`,"integrations.codex.openService":`Открыть управление сервисом`,"integrations.state.notInstalled":`Не установлен`,"integrations.state.unknown":`Проверка…`,"integrations.detail.codexRouted":`Запросы Codex идут через этот прокси`,"integrations.detail.codexAbsent":`Codex пока не идёт через этот прокси`,"integrations.detail.keyCount":`Выпущено ключей: {count}`,"integrations.detail.keyNone":`Ключи не выпущены`,"integrations.detail.keyChecking":`Проверка…`,"integrations.detail.keyUnavailable":`Статус ключей недоступен`,"integrations.detail.claudeOff":`Подключение выключено`,"integrations.detail.desktopCurrent":`Desktop работает с этим профилем`,"integrations.detail.desktopStale":`Файл профиля изменился после применения`,"integrations.detail.desktopNotServed":`Профиль есть, но Desktop использует другой`,"integrations.detail.desktopAbsent":`Профиль не применён`,"integrations.detail.desktopDesiredOff":`Интеграция Claude Desktop отключена`,"integrations.detail.desktopDesiredOffCleanupPending":`Claude Desktop всё ещё использует шлюз; очистка не завершена`,"integrations.detail.desktopDesiredOnNotApplied":`Интеграция включена, но Desktop не использует профиль шлюза`,"integrations.detail.desktopSelectedElsewhere":`Desktop использует другой профиль`,"integrations.detail.desktopProfileDrift":`Выбранный профиль Desktop был изменён`,"integrations.detail.desktopObservedUnsafe":`Выбранный профиль Desktop нельзя безопасно изменить`,"integrations.detail.desktopNotInstalled":`Библиотека конфигурации Claude Desktop не установлена`,"integrations.dialog.desktop.title":`Отключить интеграцию Claude Desktop?`,"integrations.dialog.desktop.changes":`Если {path} содержит профиль шлюза, управляемый opencodex, Desktop сначала выберет новый стандартный профиль без учётных данных, а затем удалит старый профиль и резервную копию.`,"integrations.dialog.desktop.breakage":`Claude Desktop вернётся к обычному Claude вместо моделей, маршрутизируемых через opencodex.`,"integrations.dialog.desktop.undo":`При повторном включении профиль opencodex будет создан заново из сохранённых назначений моделей.`,"integrations.dialog.desktop.restart":`Claude Desktop читает эту конфигурацию только при запуске. Полностью закройте и снова откройте Desktop, чтобы изменение вступило в силу.`,"integrations.dialog.desktop.confirm":`Отключить`,"integrations.native.error.desktopUnsafeMetadata":`Не удалось безопасно прочитать метаданные Claude Desktop в {path}, поэтому библиотека не изменялась.`,"integrations.native.error.desktopCleanupIncomplete":`Claude Desktop указывает на стандартный режим, но старые файлы учётных данных opencodex остались в: {paths}.`,"integrations.native.msg.desktopDisabled":`Интеграция Claude Desktop отключена.`,"integrations.native.msg.desktopEnabled":`Интеграция Claude Desktop включена.`,"integrations.detail.grokModels":`Подключено моделей: {count}`,"integrations.detail.grokAbsent":`В конфигурации нет блока opencodex`,"integrations.dialog.grok.title":`Отключить интеграцию Grok Build?`,"integrations.dialog.grok.changes":`Из {path} будет удалён только блок, отмеченный opencodex. Содержимое, добавленное вручную вне блока, останется без изменений.`,"integrations.dialog.grok.breakage":`После отключения псевдонимы моделей opencodex исчезнут из Grok Build. Модели, использовавшиеся с учётной записью xAI, останутся доступны.`,"integrations.dialog.grok.undo":`Если opencodex запущен на loopback-адресе, при повторном включении будет записан новый блок из списка доступных на тот момент моделей.`,"integrations.dialog.grok.confirm":`Отключить`,"integrations.native.msg.nonLoopbackRemoved":`Grok Build можно регистрировать автоматически, только когда opencodex запущен на loopback-адресе. Предыдущий блок, указывавший на loopback-адрес, удалён.`,"integrations.native.msg.nonLoopbackRemovedNoop":`Grok Build можно регистрировать автоматически, только когда opencodex запущен на loopback-адресе. Предыдущего блока для удаления не было.`,"integrations.native.msg.nonLoopbackSuperseded":`Grok Build можно регистрировать автоматически, только когда opencodex запущен на loopback-адресе. Тем временем другая программа записала в конфигурацию новый блок, поэтому текущий блок в файле создан не этим запросом.`,"integrations.native.error.orphanedMarker":`В {path} есть начальная метка opencodex, но нет конечной. Файл не изменён, потому что невозможно надёжно определить конец блока.`,"integrations.native.error.homeMismatch":`Домашний каталог установленного сервиса не совпадает с текущим, поэтому файл не изменён.`,"integrations.native.error.notInstalled":`Grok Build не установлен, поэтому изменять нечего.`,"integrations.native.error.configBusy":`Конфигурация сохраняется в другом месте, поэтому изменить её не удалось. Повторите попытку чуть позже.`,"integrations.state.absent":`Не применено`,"integrations.state.current":`Применено`,"integrations.state.stale":`Требуется обновление`,"integrations.state.conflict":`Конфликт`,"integrations.state.unsafe":`Невозможно проверить`,"integrations.summary.detected":`Клиентов найдено`,"integrations.summary.applied":`Настроено клиентов`,"integrations.summary.stale":`Требуется обновление`,"integrations.summary.lastChange":`Последнее изменение`,"integrations.summary.disableAll":`Отключить все…`,"integrations.onboarding":`При применении сначала сохраняется резервная копия, а затем записывается один блок провайдера opencodex. При отключении удаляется только этот блок, а сохранённый снимок можно восстановить.`,"integrations.empty.title":`Установленные клиенты не обнаружены`,"integrations.empty.body":`Установите поддерживаемый клиент, затем вернитесь сюда и примените opencodex.`,"integrations.action.apply":`Применить`,"integrations.action.disable":`Отключить`,"integrations.action.refresh":`Обновить`,"integrations.action.settings":`Настройки`,"integrations.action.manageKeys":`Управлять ключами`,"integrations.action.restore":`Восстановить…`,"integrations.action.undo":`Отменить`,"integrations.action.restorePoint":`Восстановить эту точку…`,"integrations.action.snapshotExpired":`Резервная копия устарела`,"integrations.rollback.title":`Центр восстановления`,"integrations.rollback.empty":`Истории применений пока нет`,"integrations.rollback.emptyBody":`Перед каждой успешной записью сначала сохраняется снимок исходного состояния.`,"integrations.catalog.title":`Клиенты`,"integrations.rollback.older":`Более ранние операции`,"integrations.rollback.showMore":`Показать ещё {n}`,"integrations.rollback.failed":`Не удалось загрузить историю откатов.`,"integrations.restore.title":`Восстановить этот снимок?`,"integrations.restore.body":`Сначала будет создана резервная копия текущего файла, затем выбранный снимок заменит его.`,"integrations.restore.driftTitle":`Обнаружены более новые изменения`,"integrations.restore.driftBody":`Изменения, сделанные после этого снимка, будут сохранены в резервной копии, а затем файл будет заменён.`,"integrations.restore.confirm":`Восстановить`,"integrations.restore.confirmDrift":`Сохранить новые изменения и восстановить`,"integrations.restore.pending":`Восстановление…`,"integrations.restore.manual":`Автоматическое восстановление не удалось: {reason}. Восстановите вручную из {path}.`,"integrations.error.load":`Не удалось загрузить состояние интеграции.`,"integrations.error.stale":`Последнее обновление не удалось. Значения ниже могут быть устаревшими.`,"integrations.error.busy":`Другое изменение для этого клиента ещё выполняется. Повторите попытку чуть позже.`,"integrations.error.conflict":`После записи opencodex конфигурация изменилась. Ничего не было удалено.`,"integrations.error.unsafe":`Конфигурацию нельзя изменить безопасно.`,"integrations.error.generic":`Изменить интеграцию не удалось. Предыдущее состояние сохранено.`,"integrations.error.nonLoopback":`{client} может обращаться только к прокси на localhost: в его конфигурации негде разместить заголовок, который требуется при удалённой привязке, поэтому ручная настройка тоже не поможет. Обеспечьте доступ через loopback — туннелем или локальным форвардером.`,"integrations.status.installed":`Установлен`,"integrations.status.notInstalled":`Не установлен`,"integrations.status.appliedAt":`Применено`,"integrations.status.backup":`Резервная копия`,"integrations.status.lastRestore":`Последнее восстановление`,"integrations.status.unknown":`Неизвестно`,"integrations.bulk.title":`Отключить применённые интеграции клиентов?`,"integrations.bulk.body":`Будет удалён только блок, принадлежащий opencodex. Для каждого клиента предварительно сохраняется снимок исходного состояния.`,"integrations.bulk.partial":`Не удалось отключить некоторые клиенты: {clients}`,"integrations.bulk.success":`Применённые интеграции клиентов отключены.`,"integrations.retention.degraded":`Очистка резервных копий отстаёт; старые копии могут всё ещё находиться на диске.`,"integrations.error.residual":`Файл может остаться в промежуточном состоянии: {message} Восстановите его из {path}.`,"integrations.error.recover":`{message} Резервная копия находится в {path}.`,"integrations.kind.apply":`Применено`,"integrations.kind.disable":`Отключено`,"integrations.kind.refresh":`Обновлено`,"integrations.kind.restore":`Восстановлено`,"integrations.kind.overwrite":`Перезаписано`,"integrations.dialog.overwrite.title":`Заменить блок в этом файле настроек?`,"integrations.dialog.overwrite.changesUnowned":`В {path} место, нужное opencodex, занято блоком, который писали не мы. Применение заменит его блоком, который пишет opencodex.`,"integrations.dialog.overwrite.changesForeign":`Ваша правка внутри блока opencodex в {path} будет отброшена и заменена блоком, который пишет opencodex.`,"integrations.dialog.overwrite.breakage":`То, что настраивал прежний блок, перестанет действовать. Остальная часть файла не меняется.`,"integrations.dialog.overwrite.undo":`Снимок сохраняется заранее, поэтому операция попадёт в список откатов ниже и её можно отменить.`,"integrations.dialog.overwrite.confirm":`Заменить`,"integrations.action.overwrite":`Заменить`,"integrations.semantics.opencode":`Действует только при прямом запуске с диска; внедрение окружения через ocx opencode имеет приоритет.`,"integrations.semantics.pi":`Применяется к новым сеансам.`,"integrations.semantics.omp":`Перезапустите OMP, чтобы загрузить каталог.`,"integrations.semantics.hermes":`Применяется к новым сеансам.`,"integrations.semantics.openclaw":`Немедленно применяется к работающему шлюзу.`,"integrations.semantics.kimi":`Чтобы применить, перезапустите клиент или выполните /reload (v2 отслеживает файл).`,"integrations.semantics.gajae":`Применяется в новом сеансе или при открытии /model.`,"integrations.semantics.dsh":`OpenCodex управляет только llm-pi-ai.providers.opencodex в $DSH_HOME/settings.yaml. DSH применяет этот провайдер горячей перезагрузкой; модель по умолчанию и deepseek-official остаются без изменений. Сейчас поддерживается только loopback; реальные учётные данные не записываются.`,"integrations.semantics.mcode":`Управляет только custom_provider.opencodex. Модель по умолчанию и вход MiniMax не меняются.`,"integrations.semantics.zcode":`Управляет только provider.opencodex в ~/.zcode/v2/config.json. Вход Z.ai и другие провайдеры не меняются. Перезапустите ZCode после изменений.`,"integrations.semantics.prime":`Управляет только providers.opencodex в models.json Prime Agent — ~/.prime/agent, если PRIME_AGENT_CODING_AGENT_DIR не переопределяет путь. Другие провайдеры и переопределения моделей не меняются. Применяется к новым сессиям.`,"integrations.semantics.aside":`Управляет только providers.opencodex в models.json Aside для выполнившего вход аккаунта (~/.aside/u/<аккаунт>). Другие провайдеры не меняются. Aside перезаписывает этот файл во время работы, поэтому после применения полностью закройте и снова откройте его.`,"codexAuth.mainAccount":`Основной аккаунт`,"codexAuth.logLabel":`Метка журнала`,"codexAuth.codexApp":`Codex App`,"codexAuth.moreActions":`Показать дополнительные действия`,"codexAuth.copyId":`Скопировать ID аккаунта`,"codexAuth.appLogin":`Вход через приложение`,"codexAuth.accountPool":`Пул аккаунтов`,"codexAuth.accountModeTitle":`Режим аккаунта OpenAI`,"codexAuth.accountModePool":`Режим пула`,"codexAuth.accountModePoolDesc":`Основной вход и подходящие добавленные аккаунты работают здесь в ротации.`,"codexAuth.accountModeDirect":`Прямой режим`,"codexAuth.accountModeDirectDesc":`Запросы используют только основной вход; добавленные аккаунты сохраняются для режима пула.`,"codexAuth.openaiMissing":`Встроенный провайдер OpenAI не настроен.`,"codexAuth.openaiDisabled":`Встроенный провайдер OpenAI отключён.`,"codexAuth.openaiUnavailableDesc":`Ваши аккаунты OpenAI по-прежнему доступны. Включите провайдера для маршрутизации запросов Codex.`,"codexAuth.enableOpenai":`Включить OpenAI`,"codexAuth.enablingOpenai":`Включение...`,"codexAuth.enableOpenaiFailed":`Не удалось включить провайдер OpenAI.`,"codexAuth.openaiPresetLoadFailed":`Не удалось загрузить пресет провайдера OpenAI.`,"codexAuth.openaiPresetUnavailable":`Пресет провайдера OpenAI недоступен.`,"codexAuth.openProviders":`Открыть провайдеров`,"codexAuth.add":`Добавить`,"codexAuth.sparkQuota":`Квота Codex Spark`,"codexAuth.sparkQuotaHint":`Показывать недельное окно GPT-5.3-Codex-Spark на карточках аккаунтов. По умолчанию скрыто: оно относится лишь к одной модели.`,"codexAuth.sparkQuotaShown":`Квота Codex Spark показана`,"codexAuth.sparkQuotaHidden":`Квота Codex Spark скрыта`,"codexAuth.sparkQuotaFailed":`Не удалось изменить настройку квоты Codex Spark`,"codexAuth.refreshQuota":`Обновить квоты`,"codexAuth.refreshingQuota":`Обновление...`,"codexAuth.quotaRefreshed":`Квоты обновлены`,"codexAuth.quotaRefreshFailed":`Не удалось обновить квоты`,"codexAuth.pauseExhausted":`Приостановить исчерпанные`,"codexAuth.pausingExhausted":`Проверка квот...`,"codexAuth.pauseExhaustedSucceeded":`Приостановлено аккаунтов на лимите: {count}`,"codexAuth.pauseExhaustedNone":`Нет аккаунтов с подтверждённым использованием 100%.`,"codexAuth.pauseExhaustedFailed":`Не удалось проверить и приостановить исчерпанные аккаунты.`,"codexAuth.noPool":`В пул ещё не добавлено ни одного аккаунта.`,"codexAuth.pause":`Приостановить`,"codexAuth.resume":`Возобновить`,"codexAuth.paused":`ПРИОСТАНОВЛЕН`,"codexAuth.pauseSucceeded":`Аккаунт {email} приостановлен`,"codexAuth.resumeSucceeded":`Аккаунт {email} снова доступен в пуле`,"codexAuth.pauseFailed":`Не удалось приостановить {email}. Изменений нет.`,"codexAuth.resumeFailed":`Не удалось возобновить {email}. Изменений нет.`,"codexAuth.pausedHint":`До возобновления исключён из автоматического переключения, повторов, восстановления после задержки и ручного выбора.`,"codexAuth.pinned":`ЗАКРЕПЛЁН`,"codexAuth.pinnedHint":`Этот аккаунт выбран вручную, поэтому более высокий порядок выбора не обойдёт его. Закрепление действует, пока этот аккаунт не будет исчерпан, пока вы не выберете другой или пока вы не измените порядок выбора любого аккаунта.`,"codexAuth.fiveHour":`5 ч`,"codexAuth.weekly":`Неделя`,"codexAuth.monthly":`30 дн.`,"codexAuth.resets":`сброс`,"codexAuth.today":`сегодня`,"codexAuth.current":`ТЕКУЩИЙ`,"codexAuth.nextSession":`ВЫБРАН`,"codexAuth.poolPrepared":`ГОТОВ ДЛЯ ПУЛА`,"codexAuth.preparePoolTitle":`Подготовить этот аккаунт для режима пула?`,"codexAuth.preparePoolDesc":`Прямые запросы продолжат использовать основной вход. Когда режим пула будет включён, этот аккаунт станет подготовленным выбором пула.`,"codexAuth.prepareForPool":`Подготовить для пула`,"codexAuth.poolPreparedToast":`{email} подготовлен для режима пула`,"codexAuth.switchTitle":`Сменить активный аккаунт?`,"codexAuth.switchDesc":`Применяется сразу. Существующие привязанные к аккаунту потоки и уже выполняющиеся запросы сохраняют прежний аккаунт; новые или непривязанные запросы используют порядковый уровень выбранного аккаунта. Аккаунты с тем же порядком выбора продолжают чередоваться.`,"codexAuth.cacheWarning":`Кэш промптов сбрасывается при смене аккаунта. Новая сессия начнётся с пустым кэшем.`,"codexAuth.setAsNext":`Использовать этот аккаунт следующим`,"codexAuth.cancel":`Отмена`,"codexAuth.switchBack":`Вернуться на основной аккаунт?`,"codexAuth.switchBackDesc":`Применяется сразу. Существующие привязанные к аккаунту потоки и уже выполняющиеся запросы сохраняют прежний аккаунт; новые или непривязанные запросы используют порядковый уровень аккаунта входа через приложение. Аккаунты с тем же порядком выбора продолжают чередоваться.`,"codexAuth.autoSwitch":`Проактивное переключение по использованию`,"codexAuth.autoSwitchQuotaDesc":`Квота: при использовании {threshold}% или выше следующий запрос может перейти на подходящий аккаунт с меньшим использованием, включая уже привязанную задачу; Go/Free используют только 30 дней.`,"codexAuth.autoSwitchQuotaOffDesc":`Проактивное переключение по использованию выключено. Назначение новых/непривязанных задач и восстановление после сбоев остаются активными.`,"codexAuth.autoSwitchRoundRobinDesc":`Round-robin не использует этот порог и продолжает ротировать новые/непривязанные задачи.`,"codexAuth.autoSwitchFillFirstDesc":`Fill-first: {threshold}% — порог исчерпания для новых/непривязанных задач; здоровые привязанные задачи сохраняют аккаунт.`,"codexAuth.autoSwitchFillFirstOffDesc":`У fill-first нет порога использования для новых/непривязанных задач; cooldown, повторная аутентификация и восстановление после сбоев всё ещё могут менять маршрутизацию.`,"codexAuth.failureRecoveryNote":`Восстановление после сбоев выполняется отдельно: отказ 429/402 до вывода, cooldown, повторная аутентификация, исключение или настроенный failover могут выбрать другой подходящий аккаунт.`,"codexAuth.autoSwitchThreshold":`Порог использования`,"codexAuth.autoSwitchThresholdAria":`Порог использования в процентах`,"codexAuth.autoSwitchThresholdInc":`Увеличить порог использования`,"codexAuth.autoSwitchThresholdDec":`Уменьшить порог использования`,"codexAuth.autoSwitchLoadFailed":`Не удалось загрузить настройку переключения по использованию.`,"codexAuth.autoSwitchThresholdInvalid":`Введите целое число от 1 до 100`,"codexAuth.autoSwitchUpdated":`Проактивное переключение по использованию обновлено`,"codexAuth.autoSwitchUpdateFailed":`Не удалось подтвердить обновление переключения по использованию. Показано последнее подтверждённое значение.`,"codexAuth.requestUserInput":`Запрашивать ввод в режиме Default`,"codexAuth.requestUserInputDesc":`Позволяет Codex ставить сеанс Default на паузу и задавать вопросы через инструмент request_user_input.`,"codexAuth.requestUserInputUpdated":`Флаг обновлён - применяется к новым сеансам.`,"codexAuth.requestUserInputUpdatedRestart":`Флаг обновлён - применяется к новым сеансам. Перезапустите приложение Codex.`,"codexAuth.requestUserInputUpdateFailed":`Не удалось обновить флаг. Ничего не изменено.`,"codexAuth.requestUserInputLoadFailed":`Не удалось прочитать флаг из config.toml.`,"codexAuth.accountPickerTitle":`Выбирать конкретный аккаунт Codex в списке моделей`,"codexAuth.accountPickerOffDesc":`После включения обычные пункты GPT в списке моделей заменяются отдельным пунктом для каждого селектора аккаунта, поэтому можно явно выбрать аккаунт для разговора без выхода из системы. Отключение не удаляет аккаунты.`,"codexAuth.accountPickerOnDesc":`Каждый селектор — публичная метка одного сохранённого аккаунта. Выбор закрепляет разговор за этим аккаунтом: без ротации и перехода на другой аккаунт, а активный аккаунт Pool не меняется.`,"codexAuth.accountPickerCompatibility":`Для встроенного входа Codex App используется отдельный селектор; в созданных map он обычно называется main, а при коллизии получает безопасный суффикс вроде main-2. Добавленные аккаунты получают стабильные метки, не раскрывающие личные данные, а пользовательские имена селекторов сохраняются. Существующие разговоры и сохранённые варианты моделей продолжают маршрутизироваться. Отключение скрывает созданные пункты, но сохраняет селекторы и точные маршруты. Обычные идентификаторы моделей GPT продолжают работать в режиме Pool или Direct.`,"codexAuth.accountPickerUpdated":`Выбор целевого аккаунта обновлён.`,"codexAuth.accountPickerUpdateFailed":`Не удалось обновить выбор целевого аккаунта. Показана последняя подтверждённая настройка.`,"codexAuth.accountPickerLoadFailed":`Не удалось загрузить настройку выбора аккаунта.`,"codexAuth.accountPickerRefreshFailed":`Не удалось обновить эту настройку. По-прежнему показано последнее подтверждённое значение.`,"codexAuth.advancedSettings":`Дополнительные настройки`,"codexAuth.advancedSettingsAria":`Показать или скрыть дополнительные настройки Codex Auth`,"codexAuth.catalogRefreshPending":`Изменение сохранено, но обновление каталога моделей Codex ещё не завершено. Выполните ocx sync, чтобы повторить попытку.`,"anthropicPool.title":`Пул аккаунтов Claude (экспериментально)`,"anthropicPool.enabledDesc":`При 429 аккаунт охлаждается и выполняется переключение. Новые сессии предпочитают использование ниже {threshold}% ({window}).`,"anthropicPool.enabledNoProactiveDesc":`При 429 аккаунт охлаждается и выполняется переключение. При пороге 0 упреждающее переключение по использованию отключено, но выбор новых сессий и восстановление после 429 по-прежнему используют окно {window}.`,"anthropicPool.disabledDesc":`Используется только активный аккаунт Claude. Включайте только если принимаете экспериментальную маршрутизацию.`,"anthropicPool.experimentalWarning":`Экспериментально и недостаточно проверено. Anthropic может ограничить аккаунты, похожие на автоматическую ротацию. Одна организация может делить квоту — пул таких аккаунтов не поможет. Оставляйте выключенным, если не понимаете риск.`,"anthropicPool.needTwoAccounts":`Перед включением пула добавьте минимум два OAuth-аккаунта Claude.`,"anthropicPool.threshold":`Порог использования для новых сессий`,"anthropicPool.thresholdAria":`Порог использования для новых сессий в процентах`,"anthropicPool.thresholdHelp":`0 отключает выбор по квоте (только аффинити + активный аккаунт). По умолчанию 80.`,"anthropicPool.thresholdInvalid":`Введите целое число от 0 до 100`,"anthropicPool.loadFailed":`Не удалось загрузить настройки пула Claude.`,"anthropicPool.saveFailed":`Не удалось сохранить настройки пула Claude.`,"anthropicPool.on":`Вкл`,"anthropicPool.off":`Выкл`,"accountPool.strategy":`Стратегия ротации`,"accountPool.strategyDesc":`Как OpenCodex назначает аккаунт новой/непривязанной задаче.`,"accountPool.strategyQuota":`Квота`,"accountPool.strategyRoundRobin":`Round-robin`,"accountPool.strategyFillFirst":`Fill-first`,"accountPool.strategyHintQuota":`Quota может перепривязать существующую задачу при следующем запросе после превышения порога использования.`,"accountPool.strategyHintRoundRobin":`Round-robin ротирует только задачи без действующей привязки; порог использования не меняет обычную ротацию.`,"accountPool.strategyHintFillFirst":`Fill-first использует порог как точку исчерпания для непривязанных задач; здоровые привязанные задачи сохраняют affinity.`,"accountPool.unboundDefinition":`Новая/непривязанная задача — запрос без текущей привязки к аккаунту; видимая существующая задача может стать непривязанной после сброса прокси или affinity.`,"accountPool.stickyLimit":`Назначений новых/непривязанных задач до ротации`,"accountPool.stickyLimitAria":`Назначений новых/непривязанных задач до ротации`,"accountPool.stickyLimitInc":`Увеличить sticky-лимит`,"accountPool.stickyLimitDec":`Уменьшить sticky-лимит`,"accountPool.stickyLimitHelp":`Назначить выбранному аккаунту столько новых/непривязанных задач перед переходом дальше; счётчик растёт при привязке задачи, а не после успеха upstream.`,"accountPool.stickyLimitInvalid":`Введите целое число от 1 до 100`,"accountPool.strategyLoadFailed":`Не удалось загрузить стратегию ротации.`,"accountPool.strategyUpdateFailed":`Не удалось сохранить стратегию ротации.`,"accountPool.quotaWindow":`Окно квоты`,"accountPool.quotaWindowDesc":`Какая кешированная полоса используется для выбора новых сессий по квоте, проверки порога Fill-first и допустимых замен после 429.`,"accountPool.quotaWindowFiveHour":`Полоса 5 часов`,"accountPool.quotaWindowWeekly":`Недельная полоса`,"accountPool.quotaWindowMaxUtilization":`Наибольшая полоса`,"accountPool.quotaWindowHint":`Недельная полоса пропускает аккаунты с исчерпанной полосой 5 часов, пока остаётся другой допустимый аккаунт, но возвращается к ним, если других нет. При равенстве выбирается меньшее использование за 5 часов; недельные полосы известны только после опроса на странице провайдеров.`,"accountPool.quotaWindowInert":`Полосу использования оценивает только «Квота» или Fill-first с порогом выше 0, поэтому для текущей стратегии ротации эта настройка ничего не меняет.`,"accountPool.priority":`Порядок выбора`,"accountPool.priorityAria":`Порядок выбора для этого аккаунта`,"accountPool.priorityHint":`Большие числа используются раньше. Пул переходит к меньшему числу только тогда, когда все аккаунты выше исчерпаны или недоступны.`,"accountPool.priorityFirst":`Первым`,"accountPool.priorityEarlier":`Раньше`,"accountPool.priorityNormal":`По умолчанию`,"accountPool.priorityLater":`Позже`,"accountPool.priorityLast":`Последним`,"accountPool.priorityOption":`{name} ({value})`,"accountPool.priorityCustom":`Своё значение`,"accountPool.priorityUpdated":`Порядок выбора для {email} обновлён`,"accountPool.priorityUpdateFailed":`Не удалось сохранить порядок выбора для {email}. Показано последнее подтверждённое значение.`,"codexAuth.switched":`{email} выбран для следующего запроса`,"codexAuth.loadFailed":`Не удалось загрузить настройки аккаунтов Codex.`,"codexAuth.switchFailed":`Не удалось переключить аккаунт. Ваш предыдущий выбор не изменён.`,"codexAuth.removeConfirm":`Удалить {id}?`,"codexAuth.removeFailed":`Не удалось удалить аккаунт. Ничего не изменено.`,"codexAuth.addTitle":`Добавить аккаунт Codex`,"codexAuth.addIdLabel":`ID аккаунта (slug)`,"codexAuth.addIdPlaceholder":`codex-work, codex-alt, team...`,"codexAuth.resetCreditsAria":`Кредитов сброса: {count}`,"codexAuth.addJsonLabel":`Содержимое auth.json`,"codexAuth.addHelp":`Скопируйте из ~/.codex/auth.json на другой машине или используйте codex-auth export.`,"codexAuth.importBtn":`Импортировать`,"codexAuth.importInvalidJson":`Некорректный JSON`,"codexAuth.importMissingTokens":`В JSON отсутствует access_token или refresh_token`,"codexAuth.importMissingId":`Укажите ID аккаунта`,"codexAuth.accountAdded":`Аккаунт добавлен в пул`,"codexAuth.addPickDesc":`Войдите в другой аккаунт ChatGPT, чтобы добавить его в пул.`,"codexAuth.oauthLogin":`Вход через OAuth`,"codexAuth.oauthDesc":`Открывает вход ChatGPT в браузере`,"codexAuth.deviceLogin":`Вход по коду устройства`,"codexAuth.deviceDesc":`Для headless или удалённого прокси: введите короткий код на другом устройстве`,"codexAuth.importAuthJson":`Импорт auth.json`,"codexAuth.importAuthJsonDesc":`Из другой установки Codex или через codex-auth export`,"codexAuth.back":`Назад`,"codexAuth.oauthAlreadyInProgress":`Вход уже выполняется. Завершите его в браузере.`,"codexAuth.oauthWaiting":`Ожидание завершения входа ChatGPT в браузере...`,"codexAuth.oauthSubmittingCode":`Отправка кода…`,"codexAuth.oauthCodeSubmitted":`Код отправлен — ждём завершения входа…`,"codexAuth.oauthStatusRetrying":`При проверке статуса входа возникла сетевая ошибка или ошибка прокси — повторяем…`,"codexAuth.oauthCancelled":`Вход отменён.`,"codexAuth.loginFailed":`Не удалось войти`,"codexAuth.needsReauth":`Повторный вход`,"codexAuth.reauthenticate":`Переавторизоваться`,"codexAuth.tokenExpired":`Токен истёк — переавторизуйте этот аккаунт`,"codexAuth.mainTokenExpired":`Токен истёк — повторите вход через приложение Codex`,"codexAuth.emailCollision":`Этот аккаунт совпадает с вашим основным входом Codex. Используйте другой аккаунт.`,"codexAuth.resetCreditsTitle":`Кредиты сброса`,"codexAuth.resetCreditsAvailable":`У вас доступно кредитов сброса: {count}.`,"codexAuth.resetCreditsDesc":`Каждый кредит мгновенно сбрасывает ваши текущие часовые и недельные лимиты использования.`,"codexAuth.noResetCredits":`У вас нет кредитов сброса.`,"codexAuth.earnCreditsHint":`Кредиты начисляются ежемесячно и по реферальной программе.`,"codexAuth.creditsExpireNote":`Кредиты истекают через 30 дней после начисления.`,"codexAuth.useOneCredit":`Использовать 1 кредит`,"codexAuth.confirmResetTitle":`Использовать кредит сброса?`,"codexAuth.confirmResetDesc":`Текущие лимиты запросов будут мгновенно сброшены. У вас осталось кредитов: {count}.`,"codexAuth.irreversible":`Это действие нельзя отменить.`,"codexAuth.useCredit":`Использовать кредит`,"codexAuth.redeeming":`Сброс...`,"codexAuth.resetSuccess":`Лимиты запросов сброшены! Осталось кредитов: {remaining}.`,"codexAuth.resetSuccessGeneric":`Лимиты запросов сброшены!`,"codexAuth.resetAlreadyRedeemed":`Этот кредит уже был использован. Количество кредитов не изменилось.`,"codexAuth.resetNothingToReset":`Сейчас ни одно окно лимитов не требует сброса.`,"codexAuth.resetNoCredit":`Нет доступных кредитов сброса.`,"codexAuth.resetError":`Не удалось использовать кредит сброса. Попробуйте ещё раз.`,"codexAuth.fifoNote":`Первым используется самый старый кредит.`,"codexAuth.confirmWhichCredit":`Будет использован кредит от {date}.`,"codexAuth.creditNext":`Следующий к использованию`,"codexAuth.creditLabel":`Кредит №{n}`,"codexAuth.creditNextBadge":`СЛЕД.`,"codexAuth.creditGranted":`Начислен {date}`,"codexAuth.creditExpires":`Истекает {date} (осталось {days} дн.)`,"api.title":`Доступ по API`,"api.subtitle":`Сгенерированные API-ключи дают внешним приложениям доступ к прокси opencodex. Аутентификация — через заголовок {authHeader}; какие заголовки принимает каждый эндпоинт, смотрите в таблице ниже.`,"api.endpointNote":`Используйте базовый URL с OpenAI-совместимыми клиентами. Responses и Chat Completions доступны под /v1.`,"api.baseUrl":`Базовый URL`,"api.responsesEndpoint":`Responses API`,"api.chatCompletionsEndpoint":`Chat Completions API`,"api.messagesEndpoint":`Messages API`,"api.modelsEndpoint":`Models API`,"api.endpointsTitle":`Конечные точки`,"api.authTitle":`Аутентификация`,"api.authBaseUrlNote":`Настройте клиентов с базовым URL, затем выберите нужный протокольный endpoint ниже.`,"api.authLoopback":`Loopback-привязки (127.0.0.1 или ::1) обходят аутентификацию. Для удалённых привязок нужен сгенерированный ocx_-ключ или OPENCODEX_API_AUTH_TOKEN.`,"api.modelsTitle":`Каталог внешних моделей`,"api.modelsCount":`{count} доступно`,"api.modelsSearch":`Поиск моделей`,"api.modelsSubtitle":`Используйте эти точные ID моделей с /v1/models и выбранным входящим протоколом.`,"api.modelsLoading":`Загрузка моделей…`,"api.modelsLoadFailed":`Не удалось загрузить каталог внешних моделей.`,"api.modelsEmpty":`Пока нет внешне доступных моделей.`,"api.modelsNoMatch":`Нет моделей, соответствующих «{query}».`,"api.colModel":`Модель`,"api.colSource":`Источник`,"api.colProtocols":`Протоколы`,"api.copyModelId":`Копировать ID`,"api.modelCopied":`Скопировано`,"api.testModel":`Тест`,"api.testingModel":`Тестирование…`,"api.testSucceeded":`OK`,"api.testFailed":`Ошибка`,"api.protocolResponses":`Responses`,"api.protocolChatCompletions":`Chat Completions`,"api.protocolMessages":`Messages`,"api.sourceNative":`Пул ChatGPT`,"api.sourceCombo":`Combo-маршрут`,"api.sourceCustom":`Пользовательская`,"api.usageResponsesTitle":`Пример Responses`,"api.usageChatTitle":`Пример Chat Completions`,"api.usageMessagesTitle":`Пример Messages`,"api.newKeyTitle":`Создан новый ключ`,"api.newKeyNote":`Скопируйте ключ сейчас — он больше не будет показан.`,"api.copy":`Копировать`,"api.copied":`Скопировано`,"api.dismiss":`Закрыть`,"api.generateTitle":`Сгенерировать ключ`,"api.keyNamePlaceholder":`Имя ключа (необязательно)`,"api.generate":`Сгенерировать`,"api.generating":`Создание…`,"api.activeKeys":`Активные ключи ({count})`,"api.activeKeysLoading":`Активные ключи`,"api.noKeys":`API-ключей пока нет. Сгенерируйте ключ выше.`,"api.workspace.sections":`Разделы API`,"api.section.keys":`Ключи`,"api.section.connect":`Подключение`,"api.section.endpoints":`Эндпоинты`,"api.section.models":`Модели`,"api.section.examples":`Примеры`,"api.workspace.details":`Сведения об API-ключе`,"api.workspace.keyDetails":`Сведения о ключе`,"api.workspace.keyPrefix":`Префикс ключа`,"api.workspace.deleteKey":`Удалить ключ`,"api.workspace.deleteConfirm":`Удалить этот ключ? Это действие нельзя отменить.`,"api.workspace.usageExamples":`Примеры использования`,"api.copyUrlHint":`Нажмите, чтобы скопировать URL`,"api.urlCopied":`URL скопирован`,"api.copyExampleHint":`Нажмите, чтобы скопировать пример`,"api.exampleCopied":`Пример скопирован`,"api.colName":`Имя`,"api.colKey":`Ключ`,"api.colCreated":`Создан`,"api.confirm":`Подтвердить`,"api.deleteAria":`Удалить API-ключ`,"api.usageSampleInput":`Привет, мир!`,"api.clientConfig.title":`Конфигурация клиента`,"api.clientConfig.rowsLabel":`Подключение клиента`,"api.clientConfig.details":`Подробнее`,"api.clientConfig.detailsAria":`Подробности конфигурации {client}`,"api.clientConfig.copyAria":`Скопировать конфигурацию {client}`,"api.clientConfig.downloadAria":`Скачать конфигурацию {client}`,"api.clientConfig.rowMeta":`{destination} · моделей: {count}`,"api.clientConfig.rowError":`Не удалось собрать конфигурацию {client}.`,"api.clientConfig.copiedAnnounceClient":`Конфигурация {client} скопирована в буфер обмена.`,"api.clientConfig.clientOpencode":`OpenCode`,"api.clientConfig.clientPi":`Pi`,"api.clientConfig.clientOmp":`OMP`,"api.clientConfig.clientHermes":`Hermes`,"api.clientConfig.clientOpenclaw":`OpenClaw`,"api.clientConfig.clientKimi":`Kimi Code`,"api.clientConfig.clientGajae":`Gajae Code`,"api.clientConfig.clientDsh":`DeepSeek Harness (DSH)`,"api.clientConfig.clientMcode":`MiniMax Code`,"api.clientConfig.clientZcode":`ZCode`,"api.clientConfig.clientPrime":`Prime Agent`,"api.clientConfig.clientAside":`Aside`,"api.clientConfig.copy":`Копировать конфигурацию`,"api.clientConfig.download":`Скачать`,"api.clientConfig.loading":`Формируется конфигурация клиента…`,"api.clientConfig.jsonLabel":`Конфигурация {client}`,"api.clientConfig.destination":`Целевой файл`,"api.clientConfig.envHint":`Задайте ключ перед запуском`,"api.clientConfig.mergeWarning":`Объедините это с целевым файлом. Замена удалит ваши другие провайдеры и настройки MCP.`,"api.clientConfig.modelCount":`Экспортировано моделей: {count}`,"api.clientConfig.missingLimits":`У {count} из {total} моделей нет лимита контекста; клиент применит свои значения по умолчанию.`,"api.clientConfig.noKeyYet":`Для {env} пока нет ключа. Создайте ключ выше, прежде чем использовать конфигурацию вне loopback.`,"api.clientConfig.loadFailed":`Не удалось прочитать список моделей, поэтому конфигурация клиента не создана.`,"api.clientConfig.copiedAnnounce":`Конфигурация клиента скопирована в буфер обмена.`,"api.clientConfig.copyFailed":`Не удалось скопировать конфигурацию клиента.`,"api.clientConfig.downloadedAnnounce":`Файл {filename} скачан. Пока ничего не изменилось — объедините его с {destination} самостоятельно.`,"api.clientConfig.whereDisclosure":`Куда положить этот файл`,"api.clientConfig.whereBody":`Путь выше — глобальное расположение. Файл конфигурации проекта в рабочем каталоге имеет приоритет, а ключ читается из переменной окружения, указанной в конфигурации, и никогда не хранится в этом файле.`,"api.keysLoadFailed":`Не удалось загрузить API-ключи.`,"api.createFailed":`Не удалось создать API-ключ.`,"api.deleteFailed":`Не удалось удалить API-ключ.`,"api.auth.endpoint":`Эндпоинт`,"api.auth.required":`Обязателен`,"api.auth.accepted":`Принимается`,"api.auth.rejected":`Не принимается`,"api.auth.testProtocol":`Проверить {protocol}`,"api.auth.testNeedsFreshKey":`Чтобы выполнить проверку с аутентификацией, создайте ключ и оставьте его одноразовое значение на экране.`,"api.key.name":`Имя ключа`,"api.key.rename":`Переименовать`,"api.key.saveName":`Сохранить имя`,"api.key.renaming":`Сохранение…`,"api.key.renameFailed":`Не удалось переименовать ключ. Введённое имя сохранено.`,"api.key.deleting":`Удаление…`,"api.rotation.title":`Ротация ключа`,"api.rotation.description":`Выпускает новый ключ, сохраняя текущий на короткий переходный период.`,"api.rotation.start":`Начать ротацию`,"api.rotation.starting":`Запуск…`,"api.rotation.pending":`Ротация ожидает завершения. Обновите и проверьте клиент перед подтверждением.`,"api.rotation.expires":`Переходный период завершится:`,"api.rotation.secretOnce":`Новый ключ показывается один раз. Скопируйте его перед закрытием.`,"api.rotation.commit":`Завершить ротацию`,"api.rotation.abort":`Отменить ротацию`,"api.rotation.failed":`Операция не завершилась. Обновите данные перед повторной попыткой.`,"api.rotation.startFailed":`Не удалось начать ротацию ключа.`,"api.key.copyFailed":`Не удалось скопировать ключ. Выделите и скопируйте его вручную, прежде чем закрыть панель.`,"api.attribution.title":`Использование по ключам`,"api.attribution.requests7d":`Запросы за 7 дней`,"api.attribution.totalRequests":`Всего учтённых запросов`,"api.attribution.totalRequestsAvailable":`Запросы в доступной истории`,"api.attribution.sinceAvailable":`Доступная атрибуция с`,"api.attribution.lastUsed":`Последнее использование`,"api.attribution.since":`Учёт ведётся с`,"api.attribution.neverUsed":`Не использовался с начала учёта`,"api.attribution.unavailable":`Нет данных`,"api.attribution.unavailableDetail":`Использование ещё не учтено. Запросы до начала учёта нельзя отнести к ключам задним числом.`,"api.attribution.ambiguous":`Два ключа используют один и тот же ID, поэтому нельзя определить, чьё это использование. Задайте каждому ключу уникальный ID в файле конфигурации.`,"api.attribution.railAmbiguous":`дубль ID`,"claude.subtitle":`Используйте GPT, Gemini и другие модели внутри Claude Code.`,"claude.pageTitle":`Claude Code`,"claude.workspace.settings":`Настройки`,"claude.enabledLabel":`Подключение Claude`,"claude.enabledHint":`Если выключено, Claude Code не сможет использовать этот прокси.`,"claude.authMode":`Режим аутентификации`,"claude.authModeHint":`«Подписка» требует аккаунт Claude, «Прокси» работает без аккаунта Anthropic`,"claude.authModeSubscription":`Подписка (аккаунт Claude)`,"claude.authModeProxy":`Прокси (аккаунт не нужен)`,"claude.authModeAuto":`Авто (определять вход в Claude)`,"claude.effectiveMode.label":`Применится при следующем запуске`,"claude.effectiveMode.manual":`Вручную: {mode}`,"claude.effectiveMode.autoPresent":`Авто: подписка — вход в Claude найден через {source}`,"claude.effectiveMode.autoAbsent":`Авто: режим прокси — вход в Claude не найден`,"claude.effectiveMode.autoUnknown":`Авто: подписка — не удалось проверить вход`,"claude.effectiveMode.admissionKey":`API-ключ этого прокси всё равно отправляется.`,"claude.authSource.claude-json-oauth":`аккаунт Claude`,"claude.authSource.claude-credentials-file":`файл учётных данных`,"claude.authSource.macos-keychain":`связку ключей macOS`,"claude.authSource.exported-env":`переменную окружения`,"claude.authSource.unknown":`обнаруженные учётные данные`,"claude.systemEnv":`Автоподключение`,"claude.systemEnvDesc":`Если включено, запуск claude в любом терминале автоматически идёт через прокси.`,"claude.systemEnvUnsupported":`Автоподключение доступно только в macOS. В этой системе запускайте Claude с помощью {cmd}.`,"claude.systemEnvWarn":`⚠ Чтобы изменение вступило в силу, необходимо полностью закрыть и заново запустить приложение терминала. Не рекомендуется.`,"claude.fastMode":`Быстрый режим (OpenAI)`,"claude.fastModeDesc":`Управляет service_tier для моделей OpenAI. ВКЛ = priority (быстрее). ВЫКЛ = default. Авто = сквозная передача (решает клиент).`,"claude.fastAuto":`Авто`,"claude.fastOn":`ВКЛ`,"claude.fastOff":`ВЫКЛ`,"claude.autoContext":`Автоматически использовать большой контекст`,"claude.autoContextDesc":`Определяет, как широко применяется пометка 1M. ВКЛ: строку с большим контекстом получает каждая модель, окно которой вмещает порог сжатия. ВЫКЛ: её получают только модели с настоящим контекстом 1M.`,"claude.autoContextInert":`Неактивно, поскольку в файле конфигурации задано устаревшее значение размера контекста (maxContextTokens). Удалите его там, чтобы снова включить эту настройку.`,"claude.autoCompactWindow":`Порог автосуммаризации`,"claude.autoCompactDefault":`{value} (по умолчанию)`,"claude.autoCompactWindowDesc":`Когда чат достигает этого порога, старые сообщения суммаризируются. Порог никогда не превышает собственный лимит модели, поэтому модели с контекстом 200k не затрагиваются.`,"claude.autoCompactWindowWarn":`Изменение этого значения может сломать модели GPT — если задать порог выше реального лимита модели, чаты будут выдавать ошибку ещё до срабатывания суммаризации.`,"claude.injectAgents":`Авторегистрация подагентов`,"claude.injectAgentsDesc":`Регистрирует модели, выбранные на вкладке «Подагенты» (плюс текущую модель по умолчанию), как доступных для вызова агентов Claude Code (ocx-*). Применяется со следующей сессии.`,"claude.webSearchSidecar":`Переопределение сайдкара веб-поиска`,"claude.webSearchSidecarHint":`Переопределяет основной сайдкар веб-поиска для запросов Claude Code.`,"claude.visionSidecar":`Переопределение сайдкара для изображений`,"claude.visionSidecarHint":`Переопределяет основной сайдкар для изображений в запросах Claude Code.`,"claude.useMainSetting":`Использовать основную настройку`,"claude.sidecarModelPlaceholder":`Модель из основной настройки`,"claude.quickstart":`Начало работы`,"claude.quickstartHint":`{cmd} запускает Claude Code через прокси. Ваш вход в claude.ai остаётся активным.`,"claude.manualEnv":`Ручная настройка (для продвинутых)`,"claude.smallFastModel":`Фоновая вспомогательная модель`,"claude.smallFastModelHint":`Модель, которую Claude Code использует для фоновых задач вроде суммаризации чатов и определения тем. Её также использует алиас подагента haiku. Пусто = значение Claude по умолчанию (Haiku).`,"claude.smallFastModelAccurateHint":`Модель, которую Claude Code использует для фоновых задач, например суммаризации чатов и определения тем. Её также использует алиас подагента haiku.`,"claude.smallFastModelUnsetOption":`Разрешить Claude Code выбрать нативную модель`,"claude.smallFastModelNativeWarning":`Если модель не выбрана, OpenCodex не задаёт переопределения вспомогательной модели. Claude Code может использовать нативную модель Sonnet, что может привести к расходам у вашего нативного провайдера.`,"claude.slotUnset":`Модель Claude по умолчанию`,"claude.modelMap":`Перехват моделей`,"claude.modelMapHint":`Перехватывает запросы к определённой модели и перенаправляет их на выбранную вами. По умолчанию список пуст — пока вы не добавите правило, ничего не происходит.`,"claude.mapFrom":`Исходная модель (напр. claude-sonnet-4-5)`,"claude.mapTo":`Заменить на (напр. gemini/gemini-3-pro)`,"claude.addMapping":`Добавить правило`,"claude.removeMapping":`Удалить правило`,"claude.aliases":`Доступные модели`,"claude.aliasesHint":`Модели, которые появляются в меню /model в Claude Code.`,"claude.aliasProviderOther":`Другое`,"claude.loading":`Загрузка…`,"claude.loadFail":`Не удалось загрузить настройки Claude`,"claude.saved":`Сохранено.`,"claude.saveFailed":`Не удалось сохранить`,"claude.networkError":`Ошибка сети — запущен ли прокси?`,"claude.toggleAria":`Переключить подключение Claude`,"claude.none":`Нет`,"claude.tabsLabel":`Клиент Claude`,"claude.tabCode":`Code`,"claude.tabDesktop":`Desktop`,"claudeDesktop.title":`Claude Desktop`,"claudeDesktop.subtitle":`Маршрутизируйте каждое семейство моделей Claude через доступную модель на порту {port}.`,"claudeDesktop.importJson":`Импорт JSON`,"claudeDesktop.exportJson":`Экспорт JSON`,"claudeDesktop.loading":`Загрузка профиля Claude Desktop…`,"claudeDesktop.loadFail":`Не удалось загрузить профиль Claude Desktop.`,"claudeDesktop.retry":`Повторить`,"claudeDesktop.saveFailed":`Не удалось сохранить профиль Claude Desktop.`,"claudeDesktop.applyFailed":`Профиль сохранён, но применить его не удалось.`,"claudeDesktop.updateFailed":`Не удалось обновить Claude Desktop.`,"claudeDesktop.savedApplied":`Профиль сохранён и применён к Claude Desktop.`,"claudeDesktop.appliedMarkerUnsaved":`Применено к Claude Desktop, но отметка о применении не сохранена — состояние ниже может показывать устаревшие данные до повторного применения.`,"claudeDesktop.savedAppliedAnnounce":`Профиль Claude Desktop сохранён и применён.`,"claudeDesktop.saved":`Профиль сохранён.`,"claudeDesktop.savedAnnounce":`Профиль Claude Desktop сохранён.`,"claudeDesktop.exported":`Профиль экспортирован в JSON.`,"claudeDesktop.importExpected":`Ожидается профиль Claude Desktop версии 1.`,"claudeDesktop.importReady":`JSON импортирован. Проверьте черновик, затем сохраните и примените.`,"claudeDesktop.importedAnnounce":`JSON профиля импортирован. Несохранённые изменения готовы к проверке.`,"claudeDesktop.importInvalid":`Выбранный файл не является допустимым профилем.`,"claudeDesktop.importFailed":`Импорт не удался. {error}`,"claudeDesktop.moved":`{route} перемещён в {family}.`,"claudeDesktop.unsaved":`Несохранённые изменения`,"claudeDesktop.upToDate":`Профиль актуален`,"claudeDesktop.saving":`Сохранение…`,"claudeDesktop.applying":`Применение…`,"claudeDesktop.saveApply":`Сохранить и применить`,"claudeDesktop.emptyTitle":`Нет доступных моделей`,"claudeDesktop.emptyHint":`Добавьте или включите провайдера, затем вернитесь для назначения маршрутов Claude Desktop.`,"claudeDesktop.assignmentsLabel":`Назначения семейств моделей Claude`,"claudeDesktop.family.opus":`Opus`,"claudeDesktop.family.fable":`Fable`,"claudeDesktop.family.sonnet":`Sonnet`,"claudeDesktop.family.haiku":`Haiku`,"claudeDesktop.modelCountOne":`{count} модель`,"claudeDesktop.modelCountMany":`{count} моделей`,"claudeDesktop.chooseDefault":`Выберите модель по умолчанию`,"claudeDesktop.temporaryDefault":`Временная модель по умолчанию`,"claudeDesktop.laneEmpty":`Перетащите модель сюда или используйте её элемент «Переместить».`,"claudeDesktop.laneNoMatch":`В этом семействе нет моделей, соответствующих запросу.`,"nav.grok":`Grok`,"grok.title":`Grok Build`,"grok.subtitle":`Модели, зарегистрированные opencodex в вашей конфигурации Grok.`,"grok.loading":`Загрузка состояния Grok…`,"grok.loadFail":`Не удалось прочитать конфигурацию Grok.`,"grok.notConfiguredTitle":`Grok Build не подключён`,"grok.notConfiguredHint":`Установите Grok и перезапустите прокси — opencodex запишет управляемый блок в:`,"grok.endpoint":`Точка входа`,"grok.colModel":`Модель`,"grok.colAlias":`Псевдоним Grok`,"grok.colContext":`Контекст`,"grok.groupNative":`Нативные модели`,"grok.groupRouted":`Маршрутизируемые модели`,"grok.enabledCount":`Зарегистрировано {on} из {total}`,"grok.saved":`Выбор сохранён.`,"grok.savedApplied":`Выбор сохранён и записан в конфиг Grok.`,"grok.saveFailed":`Не удалось сохранить выбор Grok.`,"grok.applyFailed":`Выбор сохранён, но конфиг Grok обновить не удалось.`,"grok.applySkipped":`Выбор сохранён. Конфиг Grok не изменён.`,"grok.saveApply":`Сохранить и применить`,"grok.saving":`Сохранение…`,"grok.applying":`Применение…`,"grok.unsaved":`Несохранённые изменения`,"grok.upToDate":`Выбор актуален`,"grok.toggleModel":`Зарегистрировать {id} в Grok`,"claudeDesktop.available":`Доступно`,"claudeDesktop.defaultBadge":`По умолчанию`,"claudeDesktop.supports1m":`1M`,"claudeDesktop.unavailable":`Недоступно`,"claudeDesktop.contextM":`Контекст {n}M`,"claudeDesktop.contextK":`Контекст {n}k`,"claudeDesktop.contextUnknown":`контекст неизвестен`,"claudeDesktop.alias":`Псевдоним`,"claudeDesktop.useAsDefault":`Сделать по умолчанию для {family}`,"claudeDesktop.moveTo":`Переместить в`,"claudeDesktop.move":`Переместить`,"claudeDesktop.status.applied":`Применено к Desktop`,"claudeDesktop.status.stale":`Конфигурация устарела — примените заново`,"claudeDesktop.status.notApplied":`Не применено`,"claudeDesktop.status.notActiveProfile":`Desktop использует другой профиль — примените заново`,"claudeDesktop.status.disabled":`Интеграция Claude Desktop отключена. После включения полностью закройте и снова откройте Desktop.`,"claudeDesktop.enableApply":`Включить и применить`,"claudeDesktop.health.lastRequest":`Последний запрос`,"claudeDesktop.health.stats":`{count} запр. / {errors} ошиб.`,"claudeDesktop.effort.supported":`effort`,"claudeDesktop.effort.displayOnly":`effort (только отображение)`,"cws.loading":`Загрузка комбо…`,"cws.loadFailed":`Не удалось загрузить комбо.`,"cws.saveFailed":`Не удалось сохранить комбо.`,"cws.removeFailed":`Не удалось удалить комбо.`,"cws.saved":`Комбо сохранено.`,"cws.created":`Создано: {model}.`,"cws.removed":`Удалено: combo/{id}.`,"cws.renamed":`Переименовано: {from} → {to}.`,"cws.add":`Добавить комбо`,"cws.addTitle":`Добавить комбо`,"cws.addSubtitle":`Создайте виртуальную модель для нескольких провайдеров и выберите точное имя, которое будут запрашивать клиенты.`,"cws.create":`Создать комбо`,"cws.railAria":`Список комбо`,"cws.searchPlaceholder":`Поиск комбо или целей…`,"cws.noSearchResults":`Нет комбо, соответствующих запросу.`,"cws.group.failover":`Failover`,"cws.group.roundRobin":`Round-robin`,"cws.group.other":`Другие стратегии`,"cws.targetCount":`{count} целей`,"cws.targetCountOne":`1 цель`,"cws.overviewTitle":`Комбо`,"cws.overviewBlurb":`Виртуальные модели, маршрутизирующие между целями провайдер/модель через failover, round-robin, взвешенный случайный выбор, наименее используемую цель или ближайший сброс квоты.`,"cws.count.total":`Всего`,"cws.count.failover":`Failover`,"cws.count.roundRobin":`Round-robin`,"cws.count.other":`Другие`,"cws.howTitle":`Как это работает`,"cws.howBody":`Запросите у Codex публичное имя модели комбо. Если оно не задано, используется combo/. OpenCodex выбирает цель и переключается на следующую только при сбоях вышестоящего провайдера, допускающих повтор. Если доступных целей не осталось, запрос завершается ошибкой, а не переходит на глобальный провайдер по умолчанию.`,"cws.attentionTitle":`Требует внимания`,"cws.attention.empty":`Цели не настроены`,"cws.attention.few":`Только одна цель — failover некуда переключаться`,"cws.attention.catalogOmitted":`Отсутствует в каталоге моделей — возможности участников неполны или несовместимы (нет context window / метаданных, или пустое пересечение modalities). Маршрутизация по alias всё ещё работает`,"cws.attention.allTargetsExhausted":`Квота исчерпана у всех включённых целей`,"cws.emptyTitle":`Создайте первое комбо`,"cws.empty.createDesc":`Задайте имя виртуальной модели и объедините в цепочку два и более бэкенда.`,"cws.backToAll":`Назад ко всем комбо`,"cws.allCombos":`Все комбо`,"cws.copyModel":`Копировать id`,"cws.copied":`Скопировано`,"cws.tabsLabel":`Разделы деталей комбо`,"cws.tab.config":`Конфигурация`,"cws.tab.about":`О комбо`,"cws.strategy":`Стратегия`,"cws.strategy.failover":`Failover`,"cws.strategy.roundRobin":`Round-robin`,"cws.strategy.random":`Случайный`,"cws.strategy.leastUsed":`Наименее используемый`,"cws.strategy.resetWindow":`Окно сброса`,"cws.strategy.failoverHint":`Цели перебираются по порядку. Если первая завершается ошибкой, допускающей повтор (лимит запросов, сбой, ограничение подписки), происходит переключение на следующую.`,"cws.strategy.roundRobinHint":`Детерминированное распределение трафика по весам. Выбранная цель удерживается на серию успешных запросов, затем селектор переходит к следующей.`,"cws.strategy.randomHint":`Для каждого запроса выбирается одна подходящая цель с вероятностью, пропорциональной весу. Между запросами привязки нет.`,"cws.strategy.leastUsedHint":`Каждый запрос направляется к подходящей цели с наименьшим числом успешных запросов. Счётчики обнуляются при перезапуске прокси.`,"cws.strategy.resetWindowHint":`Предпочитается подходящая цель, чьё окно квот сбрасывается раньше всех. Без данных о квотах действует порядок из конфигурации.`,"cws.field.id":`Id комбо`,"cws.field.idHint":`Клиенты будут запрашивать {model}`,"cws.field.idInternalHint":`Внутренний id комбо. Его можно изменить после создания.`,"cws.field.idHintEdit":`При переименовании комбо будет перенесено на новый id. Клиенты запрашивают {model}.`,"cws.field.alias":`Публичное имя модели`,"cws.field.aliasPlaceholder":`deepseek-v4-flash или vendor/model`,"cws.field.aliasHint":`Необязательно. Используйте имя без префикса, собственный префикс вроде vendor/model или оставьте поле пустым для combo/.`,"cws.field.nativeAlias":`Нативный псевдоним OpenAI`,"cws.field.nativeAliasHint":`Разрешает комбо занимать поддерживаемый неквалифицированный ID нативной модели OpenAI. Маршруты OpenAI с аккаунтом или провайдером остаются отдельными.`,"cws.field.displayName":`Отображаемое имя`,"cws.field.displayNameHint":`Подпись в списке моделей. Обязательна для нативного псевдонима OpenAI.`,"cws.field.stickyLimit":`Успешных запросов до ротации`,"cws.field.stickyLimitHint":`Выбранная цель удерживается на указанное число успешных запросов, прежде чем взвешенный селектор перейдёт к следующей.`,"cws.field.defaultEffort":`Рассуждения по умолчанию`,"cws.field.defaultEffortNone":`Нет (по умолчанию для цели)`,"cws.field.defaultEffortHint":`Используется, только если клиент не указал уровень рассуждений. Варианты — пересечение заявленных уровней выбранных целей.`,"cws.capability.imageInputUnavailable":`Доступно, когда все выбранные цели поддерживают ввод изображений.`,"cws.capability.imageInputHint":`Включено по умолчанию, если все цели поддерживают изображения. Выключите, чтобы принимать только текст.`,"cws.capability.imageInput":`Изображения / мультимодальность`,"cws.capability.adaptiveEffort":`Адаптивная шкала рассуждений`,"cws.capability.adaptiveEffortHint":`Выкл.: цель без настройки рассуждений скрывает выбор уровня для всей комбинации. Вкл.: такие цели остаются доступными, а в выборе сохраняются уровни, общие для остальных целей.`,"cws.capabilities":`Возможности`,"cws.field.defaultEffortUnsupported":`Этот уровень не входит в общую лестницу целей — при запросе он будет проигнорирован или снижен.`,"cws.field.defaultEffortUnsupportedOption":`нет в пересечении`,"cws.targets":`Цели`,"cws.targets.failoverHint":`Порядок важен — первая цель основная.`,"cws.targets.roundRobinHint":`Веса задают детерминированный относительный выбор; при равных весах порядок определяет очерёдность в кольце ротации.`,"cws.targets.randomHint":`Веса задают вероятности каждого выбора; порядок не важен.`,"cws.targets.leastUsedHint":`Порядок разрешает только равенство между одинаково используемыми целями.`,"cws.targets.resetWindowHint":`Порядок применяется, когда данных о квотах нет или они равны.`,"cws.target.provider":`Провайдер`,"cws.target.model":`Модель`,"cws.target.weight":`Вес`,"cws.target.pickProvider":`Выберите провайдера…`,"cws.target.pickProviderFirst":`Сначала выберите провайдера…`,"cws.target.pickModel":`Выберите модель…`,"cws.target.noModels":`Нет моделей для этого провайдера`,"cws.target.modelPlaceholder":`id модели`,"cws.target.add":`Добавить цель`,"cws.target.drag":`Перетащите, чтобы изменить порядок`,"cws.target.moveUp":`Переместить вверх`,"cws.target.moveDown":`Переместить вниз`,"cws.quota.available":`Доступно`,"cws.quota.exhausted":`Квота исчерпана`,"cws.quota.unknown":`Квота неизвестна`,"cws.quota.allExhausted":`Квота исчерпана у всех включённых целей. Выберите другую цель или дождитесь восстановления квоты.`,"cws.aboutTitle":`Поведение во время работы`,"cws.aboutBody":`После сбоя цель на короткое время выводится из ротации с учётом заголовка Retry-After. При ошибках валидации или превышения контекста переключение не выполняется. Каждая цель адаптирует уровень рассуждений к своим возможностям; полностью исчерпанное комбо завершает запрос ошибкой. Разделы «Логи» и «Использование» сохраняют упорядоченные физические попытки и расход по каждой попытке.`,"cws.removeConfirmTitle":`Удалить {model}?`,"cws.removeConfirmDesc":`Виртуальная модель будет удалена из конфигурации и каталога Codex. Провайдеры при этом не удаляются.`,"cws.unsavedTitle":`Несохранённые изменения`,"cws.unsavedDesc":`Отбросить изменения этого комбо и продолжить?`,"cws.keepEditing":`Продолжить редактирование`,"cws.err.missingId":`Необходимо указать id комбо.`,"cws.err.invalidId":`Id должен начинаться с буквы или цифры и содержать только буквы, цифры, точки, подчёркивания и дефисы (не более 64 символов).`,"cws.err.duplicateId":`Комбо с таким id уже существует.`,"cws.err.invalidAlias":`Алиас должен содержать только буквы, цифры, точки, подчёркивания и дефисы, максимум с одним сегментом "/".`,"cws.err.aliasReservedNamespace":`Алиас не должен использовать зарезервированное пространство имён "combo/".`,"cws.err.aliasNativeFamily":`Алиасы без префикса из нативного семейства OpenAI (gpt-*, o1-*, o3-*, o4-*, codex-*) недопустимы.`,"cws.err.unsupportedNativeAlias":`Нативный алиас должен быть одним из поддерживаемых сейчас неквалифицированных id моделей OpenAI.`,"cws.err.missingNativeAliasDisplayName":`Для нативного алиаса требуется отображаемое имя.`,"cws.err.invalidDisplayName":`Отображаемое имя должно содержать не более 128 символов и не иметь управляющих символов.`,"cws.err.duplicateAlias":`Другое комбо уже использует этот алиас.`,"cws.err.noTargets":`Добавьте хотя бы одну цель.`,"cws.err.incompleteTarget":`Для каждой цели нужно указать провайдера и модель.`,"cws.target.disabled":`{name} (отключён)`,"cws.err.reservedNamespace":`Прежде чем создавать комбо, необходимо переименовать физического провайдера с именем «combo».`,"cws.err.providerCollision":`Id комбо конфликтует с именем настроенного провайдера.`,"cws.err.unknownProvider":`Каждая цель должна использовать настроенного провайдера.`,"cws.err.duplicateTarget":`Одна и та же цель провайдер/модель может встречаться только один раз.`,"cws.err.invalidStickyLimit":`Число успешных запросов до ротации должно быть целым от 1 до 100.`,"cws.err.invalidWeight":`Каждый вес round-robin должен быть целым числом от 1 до 10000.`,"cws.err.noEnabledTarget":`Хотя бы одна цель должна использовать включённого провайдера.`,"dash.injectionManage":`Открыть настройки`,"sub.settings":`Настройки`,"sub.sections":`Разделы подагентов`,"sub.delegation.model":`Модель, которую вызывать первой`,"sub.delegation.modelHint":`Модель, к которой Codex обращается первой, когда передаёт работу. Список выше — кого он вообще может вызвать, а здесь выбирается первый в очереди.`,"dash.syncModelsHint":`Перезаписывает каталог моделей Codex по подключённым провайдерам.`,"dash.syncRun":`Синхронизировать`,"lab.title":`Compatibility Lab`,"lab.subtitle":`Read-only compatibility verdict matrix from lab projection evidence.`,"lab.loadFailed":`Could not load compatibility lab data`,"lab.projectionUnavailable":`Lab projection is not available. Run conformance or live probes first.`,"lab.projectionIncompatible":`Lab projection schema is incompatible. Rebuild the projection.`,"lab.statusTitle":`Projection status`,"lab.matrixTitle":`Compatibility matrix`,"lab.verdictsTitle":`Verdict records`,"lab.filter.layer":`Evidence layer`,"lab.filter.verdict":`Verdict`,"lab.filter.subject":`Subject ID`,"lab.filter.all":`All`,"lab.col.subject":`Subject`,"lab.col.layer":`Layer`,"lab.col.suite":`Suite`,"lab.col.verdict":`Verdict`,"lab.col.asOf":`As of`,"lab.col.protocol":`Protocol conformance`,"lab.col.live":`Live route compatibility`,"lab.col.task":`Task effectiveness`,"lab.empty":`No compatibility verdicts in the projection yet.`,"lab.subjectKind":`Kind`,"lab.observationCount":`Observations`,"lab.eventCount":`Events`,"lab.verdictCount":`Verdicts`,"lab.subjectCount":`Subjects`,"lab.builtAt":`Built`,"lab.loading":`Loading compatibility evidence…`,"lab.loadMore":`Load more`,"lab.detailTitle":`Verdict detail`,"lab.detailClose":`Close`,"lab.detailSubject":`Subject`,"lab.detailObservations":`Observations`,"lab.detailEvents":`Contributing events`,"lab.detailArtifacts":`Artifact metadata`,"lab.production.title":`Наблюдаемый производственный трафик`,"lab.production.notVerification":`Не является проверкой Lab`,"lab.production.attempts":`Попытки`,"lab.production.successes":`Успешные попытки`,"lab.production.routeErrors":`Ошибки маршрута`,"lab.production.lastObserved":`Последнее наблюдение`,"lab.detailLoadFailed":`Could not load verdict detail`,"lab.refresh":`Refresh`,"lab.verdict.UNKNOWN":`Unknown`,"lab.verdict.CLAIMED":`Claimed`,"lab.verdict.PROBED":`Probed`,"lab.verdict.VERIFIED":`Verified`,"lab.verdict.DEGRADED":`Degraded`,"lab.verdict.BLOCKED":`Blocked`,"lab.verdict.UNSUPPORTED":`Unsupported`,"lab.layer.protocol_conformance":`Protocol conformance`,"lab.layer.live_route_compatibility":`Live route compatibility`,"lab.layer.task_effectiveness":`Task effectiveness`,"dash.visionAdvanced":`Дополнительные настройки`,"dash.visionMaxDescriptions":`Максимум описаний за ход`,"dash.visionMaxDescriptionsInvalid":`Введите положительное целое число.`,"dash.visionTimeout":`Таймаут`,"dash.visionTimeoutInvalid":`Введите целое число от {min} до {max} миллисекунд.`,"dash.visionAdvancedPopover":`Дополнительные настройки изображений`,"models.newPolicyGlobal":`Добавлять новые модели выключенными`,"models.newPolicyProvider":`Политика новых моделей`,"models.newPolicy_inherit":`Наследовать`,"models.newPolicy_off":`Выкл.`,"models.newPolicy_on":`Вкл.`,"models.newBadge":`НОВАЯ`,"models.newCount":`Новых: {count}, выкл.`,"models.aliases":`Псевдонимы`,"models.aliasesTable":`Таблица псевдонимов`,"models.aliasPrompt":`Псевдоним провайдера (оставьте пустым, чтобы очистить)`,"models.modelAliasPrompt":`Псевдоним модели (оставьте пустым, чтобы очистить)`,"models.aliasSaved":`Псевдоним сохранён`,"models.aliasConflict":`Этот псевдоним конфликтует с существующим именем`,"models.editProviderAlias":`Изменить псевдоним провайдера`,"models.editModelAlias":`Изменить псевдоним модели`,"models.useDefaultAliases":`Использовать псевдонимы по умолчанию`,"models.useDefaultAliasesGlobal":`Использовать псевдонимы по умолчанию везде`,"models.aliasAuto":`авто`,"models.aliasUser":`пользователь`,"models.aliasStale":`устарел`,"connection.discovering":`Discovering local and shared targets…`,"connection.machineUnavailable":`The local machine plane is unavailable. Shared requests were not redirected locally.`,"connection.disconnect":`Disconnect from hub`,"connection.disconnectConfirm":`Disconnect this machine from the hub and restart it in standalone mode?`,"connection.pairing.title":`Connect this dashboard to the hub`,"connection.pairing.body":`Paste the one-time pairing code created on the hub.`,"connection.pairing.relayWarning":`This code is exchanged through the fixed hub relay. The relay cannot be redirected to another host.`,"connection.pairing.code":`One-time pairing code`,"connection.pairing.submit":`Connect`,"connection.pairing.submitting":`Connecting…`,"connection.pairing.error":`The pairing code was refused or expired. The code was left in place so you can check it.`,"connection.machine.title":`This machine`,"connection.machine.shimHealthy":`Codex shim is healthy.`,"connection.machine.shimNeedsAttention":`Codex shim needs attention.`,"connection.machine.repairShim":`Repair shim`,"connection.machine.removeShim":`Remove shim`,"connection.clients.title":`Connected clients`,"connection.clients.none":`No client status available`,"connection.clients.sync":`Sync now`,"connection.clients.syncing":`Syncing…`,"connection.sessionLogout":`Выйти из удалённой сессии`,"connection.sessionLoggingOut":`Выход из удалённой сессии…`,"connection.sessionLogoutFailed":`Не удалось выйти из удалённой сессии. Текущая сессия сохранена.`,"usage.source.connected":`Source: hub usage`,"usage.source.local":`Source: local usage.jsonl`,"usage.scope.label":`Usage scope`,"usage.scope.machine":`This machine`,"usage.scope.hub":`Hub-wide`,"usage.hubOffline":`Hub usage is unavailable. Local usage was not substituted.`,"integrations.tab.cursor":`Cursor`,"integrations.detail.cursorSeen":`Cursor недавно обращался к этому прокси`,"integrations.detail.cursorNeverSeen":`Cursor Private Inference установлен; запросов пока не было`,"integrations.detail.cursorAbsent":`Cursor Private Inference не найден`,"integrations.cursor.title":`Cursor`,"integrations.cursor.intro":`Cursor Private Inference запускает своего агента локально и обращается к opencodex через loopback. Обычный Cursor так не может: его серверная часть обращается к пользовательскому эндпоинту, для чего нужен публичный HTTPS-адрес. Эта страница ничего не записывает в Cursor; самостоятельно вставьте указанные ниже значения в Cursor.`,"integrations.cursor.loading":`Получение статуса Cursor…`,"integrations.cursor.unavailable":`Не удалось получить от прокси статус Cursor.`,"integrations.cursor.detection":`Установленные сборки`,"integrations.cursor.privateInference":`Cursor Private Inference`,"integrations.cursor.regular":`Cursor (обычная версия)`,"integrations.cursor.detected":`Обнаружено`,"integrations.cursor.notFound":`Не найдено`,"integrations.cursor.regularOnly":`Найден только обычный Cursor. Пользовательские эндпоинты он направляет через серверы Cursor, поэтому loopback-прокси недоступен без публичного туннеля. Сведения о сборке Cursor Private Inference см. в руководстве.`,"integrations.cursor.nothingFound":`Установка Cursor в обычных расположениях не обнаружена. Если Cursor установлен в другом месте, приведённые ниже значения всё равно подходят.`,"integrations.cursor.gateway":`Параметры шлюза`,"integrations.cursor.gatewayHint":`В Cursor Private Inference откройте Settings > Models > Gateway, вставьте эти два значения, затем нажмите Refresh model list.`,"integrations.cursor.baseUrl":`Базовый URL`,"integrations.cursor.apiKey":`API-ключ`,"integrations.cursor.apiKeyCredential":`Один из ваших API-ключей opencodex (для этой привязки требуются учётные данные)`,"integrations.cursor.copy":`Скопировать`,"integrations.cursor.copied":`Скопировано`,"integrations.cursor.connection":`Подключение`,"integrations.cursor.seen":`Последний запрос от Cursor: {time} ({ua})`,"integrations.cursor.neverSeen":`Запросов от Cursor не было с момента запуска прокси. После сохранения параметров шлюза нажмите Refresh model list в Cursor.`,"integrations.cursor.models":`Что будет отображаться в Cursor`,"integrations.cursor.modelsHint":`Cursor выбирает шкалу уровней рассуждений из собственной таблицы моделей, поэтому opencodex может только предсказать её. В столбце «Контекст» указаны окно по умолчанию и дополнительное окно, доступное при включении Max Mode в Cursor.`,"integrations.cursor.ladderFromBundle":`Уровни рассуждения прочитаны из установленного бандла Cursor Private Inference {version}. Их определяет Cursor; opencodex лишь показывает его таблицу.`,"integrations.cursor.ladderFromStatic":`Уровни рассуждения — статическая копия Cursor 3.18.25 (читаемый бандл Private Inference не найден). Столбец «Контекст» показывает окно по умолчанию и опциональное окно.`,"integrations.cursor.unknownVersion":`версия неизвестна`,"integrations.cursor.noControl":`—`,"integrations.cursor.singleWindow":`одно окно`,"integrations.cursor.noControlTitle":`Этого id нет во встроенной таблице усилий Cursor, поэтому Cursor не показывает управление рассуждением.`,"integrations.cursor.effortRowsOne":`опубликована 1 строка усилия`,"integrations.cursor.effortRowsMany":`опубликовано строк усилия: {n}`,"integrations.cursor.effortRowsOff":`строк усилия нет`,"integrations.cursor.tableLessHint":`Строки с — не получают управление рассуждением в Cursor. Включите cursorEffortRows, чтобы публиковать по одной записи выбора на каждое усилие (id--effort), или задайте modelDefaultReasoningEfforts у провайдера для фиксированного значения.`,"integrations.cursor.colModel":`Модель`,"integrations.cursor.colReasoning":`Рассуждения`,"integrations.cursor.colContext":`Контекст`,"integrations.cursor.guide":`Открыть руководство по Cursor Private Inference`},Ge={"nav.dashboard":`ダッシュボード`,"uptime.day":`日`,"uptime.hour":`時間`,"uptime.minute":`分`,"uptime.second":`秒`,"nav.startup":`起動安全性`,"nav.providers":`プロバイダー`,"nav.models":`モデル`,"nav.combos":`コンボ`,"nav.subagents":`サブエージェント`,"routing.title":`ルーティングインテリジェンス (beta)`,"routing.subtitle":`ポリシープロファイル、ドライラン評価、ソース連携のルーティング分析。`,"routing.loadFailed":`ルーティングデータを読み込めませんでした`,"routing.empty":"ルーティングプロファイルが設定されていません。config.json に `routingProfiles` を追加してください。","routing.revision":`rev`,"routing.detail":`プロファイル`,"routing.createProfile":`プロファイルを作成`,"routing.dryRunError":`ドライラン失敗 (HTTP {status})`,"routing.removeConfirm":`プロファイル {id} を削除しますか?`,"routing.unknownEvidence.allow":`許可`,"routing.unknownEvidence.penalize":`ペナルティ`,"routing.unknownEvidence.exclude":`除外`,"routing.removeCandidate":`候補 {provider}/{model} を削除`,"routing.candidates":`候補`,"routing.require":`必須要件`,"routing.optimize":`最適化ウェイト`,"routing.limits":`制限`,"routing.unknownEvidence":`不明なエビデンスのポリシー`,"routing.compatibility.title":`互換性ポリシー`,"routing.compatibility.enabled":`Compatibility Lab エビデンスを必須にする`,"routing.compatibility.requiredSuites":`必須スイート`,"routing.compatibility.loadingCatalog":`Lab カタログを読み込み中…`,"routing.compatibility.catalogUnavailable":`Lab カタログを利用できません — config.json でスイート ID を手動入力してください。`,"routing.compatibility.layer.protocol_conformance":`プロトコル適合`,"routing.compatibility.layer.live_route_compatibility":`ライブルート互換性`,"routing.compatibility.minStatus":`最低互換性ステータス`,"routing.none":`なし`,"routing.unavailable":`–`,"routing.dryRun":`ドライラン評価`,"routing.dryRunContext":`リクエストのコンテキストウィンドウ(トークン)`,"routing.dryRunTools":`リクエストにツールが必要`,"routing.dryRunImage":`リクエストに画像入力が必要`,"routing.dryRunStructured":`リクエストに構造化出力が必要`,"routing.dryRunRun":`候補を評価`,"routing.candidate":`候補`,"routing.eligible":`対象`,"routing.exclusions":`除外`,"routing.costCap":`コスト上限`,"routing.capOutcome.satisfied":`上限内`,"routing.capOutcome.exceeded":`上限超過`,"routing.capOutcome.unknown-allowed":`不明(許可)`,"routing.capOutcome.unknown-excluded":`不明(除外)`,"routing.exclusion.capability-unsatisfied":`能力要件未達`,"routing.exclusion.unknown-capability":`能力不明`,"routing.exclusion.cost-limit":`コスト上限超過`,"routing.exclusion.cost-limit-unknown":`上限下でコスト不明`,"routing.exclusion.cooldown":`クールダウン`,"routing.exclusion.unknown-health":`健全性不明`,"routing.exclusion.unknown-quota":`割当不明`,"routing.exclusion.unknown-price":`価格不明`,"routing.exclusion.other":`除外: {code}`,"routing.score":`スコア`,"routing.selected":`選択済み`,"routing.yes":`はい`,"routing.no":`いいえ`,"routing.analytics":`ルーティング分析`,"routing.analyticsTotal":`リクエスト`,"routing.analyticsSuccessRate":`成功率`,"routing.analyticsFallbackRate":`フォールバック`,"routing.analyticsP50":`p50`,"routing.analyticsP95":`p95`,"routing.analyticsP99":`p99`,"routing.analyticsCooldown":`クールダウン失敗`,"routing.analyticsConfidence":`信頼度`,"routing.analyticsTruncated":`切り捨て履歴`,"routing.analyticsRequests":`リクエスト`,"routing.analyticsEmpty":`分析はまだありません。まずリクエストを送信してください。`,"nav.logs":`ログ & デバッグ`,"nav.usage":`使用量`,"common.github":`GitHub`,"sidebar.star":`GitHub でスターを付ける`,"sidebar.starred":`GitHub でスター済み`,"sidebar.starUnauthenticated":`GitHub を開いてスターを付ける (gh CLI が未ログイン)`,"sidebar.starFailed":`gh でスターを付けられませんでした。代わりに GitHub を開きます。`,"sidebar.updateAvailable":`更新あり: {version}`,"sidebar.checkUpdate":`更新を確認`,"common.save":`保存`,"common.saving":`保存中…`,"common.cancel":`キャンセル`,"common.discard":`破棄`,"common.delete":`削除`,"common.close":`閉じる`,"common.ok":`OK`,"common.remove":`削除`,"common.loading":`読み込み中…`,"common.retry":`再試行`,"auth.adminTokenTitle":`OpenCodex 管理者トークン (OPENCODEX_ADMIN_AUTH_TOKEN)`,"auth.adminAccountLabel":`アカウント`,"auth.adminTokenFieldLabel":`管理者トークン`,"auth.adminTokenRejected":`管理者トークンが拒否されました。確認してもう一度お試しください。`,"auth.adminTokenUnavailable":`管理者トークンを確認できませんでした。もう一度お試しください。`,"app.logoAria":`opencodex ロゴ`,"app.claudeOn":`Claude オン`,"app.claudeOff":`Claude オフ`,"theme.label":`テーマ`,"theme.light":`ライト`,"theme.dark":`ダーク`,"theme.system":`システム`,"lang.label":`言語`,"lang.nativeName":`日本語`,"provider.name.commandCodeAuth":`Command Code - Auth`,"provider.name.commandCodeApi":`Command Code - API`,"provider.name.volcengine":`Volcengine Ark`,"provider.name.volcengineCodingPlan":`Volcengine Ark コーディングプラン`,"provider.name.volcengineAgentPlan":`Volcengine Ark エージェントプラン`,"errorBoundary.title":`ページを読み込めませんでした`,"errorBoundary.message":`このセクションの表示中にエラーが発生しました。再読み込みしてもう一度お試しください。`,"errorBoundary.details":`エラー`,"errorBoundary.reload":`再読み込み`,"startup.title":`起動安全性`,"startup.subtitle":`再起動後にローカルプロキシへの接続が再接続ループになる前に、Codex が opencodex へ到達できるか確認します。`,"startup.refresh":`更新`,"startup.backToDashboard":`ダッシュボードに戻る`,"startup.loading":`起動保護を確認中…`,"startup.error":`起動保護を読み取れませんでした。`,"startup.staleData":`最新の確認に失敗しました。以下は古い値であり、保護の証明にはなりません。`,"startup.status.native":`ネイティブルーティング`,"startup.status.protected":`再起動保護済み`,"startup.status.atRisk":`対応が必要`,"startup.summary.native":`Codex はローカルプロキシに依存していません`,"startup.summary.protected":`再起動後も opencodex を利用できます`,"startup.summary.atRisk":`再起動後に Codex がモデルへ接続できなくなる可能性があります`,"startup.riskDetail":`Codex はローカルプロキシを参照していますが、再起動する永続サービスまたは正常な launcher shim がありません。`,"startup.riskDetailCustomLocal":`Codex はカスタムローカルゲートウェイを参照しています。opencodex はその再起動ライフサイクルを管理・検証できません。`,"startup.riskDetailWindowsShim":`Launcher shim は対応する CLI スクリプトのみを保護し、Windows の Codex Desktop と codex.exe の直接起動はこれを迂回できます。`,"startup.safeDetail":`現在のルーティングと起動方式は整合しています。再起動後に ocx start を手動実行する必要はありません。`,"startup.routing":`Codex ルーティング`,"startup.routing.proxy":`ローカルプロキシ`,"startup.routing.native":`OpenAI ネイティブ`,"startup.routing.customLocal":`カスタムローカルゲートウェイ`,"startup.routing.customRemote":`カスタム遠隔ゲートウェイ`,"startup.routing.unknown":`不明または無効なルーティング`,"startup.restartProtection":`再起動保護`,"startup.preference":`オンデマンド起動`,"startup.enabled":`有効`,"startup.disabled":`無効`,"startup.protection.service":`バックグラウンドサービス`,"startup.protection.shim":`Launcher shim`,"startup.protection.none":`未インストール`,"startup.details":`保護の詳細`,"startup.service":`バックグラウンドサービス`,"startup.serviceHint":`ログイン時に起動し、クラッシュ後にプロキシを再起動します。`,"startup.installed":`インストール済み`,"startup.notInstalled":`未インストール`,"startup.unsupported":`未対応`,"startup.shim":`Codex launcher shim`,"startup.shimHint":`対応する Codex スクリプトランチャーの起動時に ocx ensure を実行します。`,"startup.healthy":`正常`,"startup.cliOnly":`CLI のみ`,"startup.stale":`要更新`,"startup.viable":`利用可能`,"startup.unhealthy":`インストール済み・異常`,"startup.conflict":`サービス競合`,"startup.installedDisabled":`インストール済み・無効`,"startup.install":`インストール`,"startup.installing":`インストール中…`,"startup.repair":`修復`,"startup.repairing":`修復中…`,"startup.serviceInstalled":`バックグラウンドサービスをインストールしました。`,"startup.serviceRepaired":`バックグラウンドサービスを修復しました。`,"startup.shimInstalled":`Codex ランチャー shim をインストールしました。`,"startup.shimRepaired":`Codex ランチャー shim を修復しました。`,"startup.installFailed":`インストールに失敗しました:`,"startup.tray.title":`Windows システムトレイ`,"startup.tray.hint":`ログイン時にトレイを起動し、プロキシの開始・停止・再起動・ダッシュボード・状態をクリックで操作します。`,"startup.tray.login":`Windows ログイン時にトレイを開始`,"startup.tray.notProtection":`トレイは操作画面であり再起動保護ではありません。無人復旧には正常なバックグラウンドサービスが必要です。`,"startup.tray.running":`実行中`,"startup.tray.stopped":`インストール済み・非表示`,"startup.tray.stale":`修復が必要`,"startup.tray.notInstalled":`未インストール`,"startup.tray.loading":`確認中…`,"startup.tray.unavailable":`状態を確認できません`,"startup.tray.install":`トレイをインストールして表示`,"startup.tray.start":`トレイアイコンを表示`,"startup.tray.stop":`トレイアイコンを終了`,"startup.tray.uninstall":`ログイントレイを削除`,"startup.tray.error":`Windows トレイ操作に失敗しました。ocx tray status で詳細を確認してください。`,"startup.recovery":`修復方法`,"startup.recoveryHint":`上のワンクリックインストールを使うか、手動修復用のコマンドをコピーできます。Codex Desktop と Windows 実行ファイルにはバックグラウンドサービスを推奨します。`,"startup.command.service":`推奨: 永続バックグラウンドサービス`,"startup.command.shim":`代替: CLI launcher shim`,"startup.command.native":`安全策: Codex ネイティブルーティングを復元`,"startup.copy":`コピー`,"startup.copied":`コピー済み`,"startup.recommended":`推奨修復: {cmd}`,"startup.navRisk":`起動保護に対応が必要です`,"startup.codexRuntime.clampHidden":`OpenCodex が Codex {version} を使用したため、一部の reasoning effort オプションが非表示になりました。`,"startup.codexRuntime.clampHiddenWithEfforts":`OpenCodex が Codex {version} を使用したため、一部の reasoning effort オプションが非表示になりました(削除: {efforts})。`,"startup.codexRuntime.olderBinary":`OpenCodex は古い Codex バイナリ({version})を使用しています。より新しいインストールが利用可能です。`,"dash.subtitle":`ローカル opencodex プロキシ、そのプロバイダー、Codex にルーティングされるモデルのライブ状態です。`,"dash.workspace.overview":`概要`,"dash.workspace.sections":`セクション`,"dash.status":`状態`,"dash.online":`オンライン`,"dash.offline":`オフライン`,"dash.version":`バージョン`,"dash.uptime":`稼働時間`,"dash.providers":`プロバイダー`,"dash.tokens30d":`トークン (30日)`,"dash.coverage":`{pct} カバレッジ`,"dash.mem.title":`メモリ可観測性`,"dash.mem.hint":`読み取り専用のランタイム診断。観測メモリは max(RSS, external, ArrayBuffers) で、Windows の working set trimming がコミット済み保持を隠さないようにします。`,"dash.mem.rss":`常駐メモリ (RSS)`,"dash.mem.jsHeap":`JS ヒープ使用量`,"dash.mem.jsHeapArena":`アリーナ {total}`,"dash.mem.pressure":`警告しきい値に対して`,"dash.mem.pressureOf":`しきい値の {pct}%`,"dash.mem.pressureUnknown":`しきい値の情報なし`,"dash.mem.jscHeap":`JSC ヒープ`,"dash.mem.external":`External`,"dash.mem.arrayBuffers":`ArrayBuffers`,"dash.mem.observed":`観測値`,"dash.mem.runtime":`ランタイムカウンター`,"dash.mem.growth":`1時間あたりの観測変化`,"dash.mem.perHour":`/時間`,"dash.mem.store":`継続ストア`,"dash.mem.storeHint":`プロキシの previous_response_id キャッシュ。ヒープ増加中に合計バイトが増える場合、ランタイムアロケータではなく会話保持を示します。`,"dash.mem.storeEntries":`エントリ`,"dash.mem.storeTotal":`合計`,"dash.mem.storeLargest":`最大`,"dash.mem.storeOldest":`最古`,"dash.mem.threshold":`警告しきい値`,"dash.mem.lastWarn":`最終警告`,"dash.mem.never":`なし`,"dash.mem.details":`詳細`,"dash.mem.unavailable":`メモリ診断は利用できません(旧バージョンのプロキシ)。`,"dash.mem.inFlight":`処理中のリクエスト`,"dash.mem.restart":`完了後に再起動`,"dash.mem.restartConfirm":`処理中のリクエスト {count} 件の完了を待ってから再起動します(最大 {seconds} 秒。タイムアウト時は残りを打ち切ります)。`,"dash.mem.draining":`リクエスト {count} 件の完了を待機中… 完了後に再起動`,"dash.mem.reconnecting":`プロキシを再起動中… 再接続を待機`,"dash.mem.restartFailed":`完了後の再起動に失敗しました。プロキシが起動しているか確認してください。`,"dash.mem.restartNoSupervisor":`再起動保護がありません。再起動後、プロキシが自動で戻らない可能性があります。`,"dash.activeProviders":`アクティブなプロバイダー`,"dash.noProviders":`プロバイダーが設定されていません。{cmd} を実行してください。`,"dash.col.name":`名前`,"dash.col.adapter":`アダプター`,"dash.col.baseUrl":`ベース URL`,"dash.col.model":`モデル`,"dash.modelsNoResults":`検索に一致するモデルはありません。`,"dash.availableModels":`利用可能なモデル`,"dash.noModels":`モデルが見つかりません。プロバイダーの API キーを確認してください。`,"dash.cannotConnect":`プロキシに接続できません。起動していますか?`,"dash.runStart":`{cmd} を実行してプロキシを起動してください。`,"dash.stop":`プロキシを停止`,"dash.stopConfirm":`プロキシを停止してネイティブの Codex に戻しますか?`,"dash.stopFailed":`プロキシを停止できませんでした (HTTP {status})。`,"dash.maSwitchFailed":`モードの切り替えに失敗しました (HTTP {status})。`,"dash.maNetworkError":`ネットワークエラー — プロキシは起動していますか?`,"dash.stopping":`停止中…`,"dash.actions":`プロキシ`,"dash.codexRestart":`Codex のモデル一覧を再読み込み`,"dash.codexRestarting":`停止中…`,"dash.codexRestartConfirm":`Codex app-server を停止してモデル一覧を読み直させますか? 進行中の Codex の処理は中断され、Codex は自動では再起動しないので後で開き直してください。`,"dash.codexRestartDone":`Codex app-server を {count} 個停止しました。Codex を開き直すと最新のモデル一覧が読み込まれます。`,"dash.codexRestartNothing":`実行中の Codex app-server はありません。次回起動時に最新のモデル一覧を読み込みます。`,"dash.codexRestartUnknown":`プロセスを列挙できなかったため、何も停止しませんでした。`,"dash.codexRestartPartial":`app-server が {count} 個終了しませんでした。モデル一覧が古いままなら手動で停止してください。`,"dash.codexRestartFailed":`Codex のモデル一覧を再読み込みできませんでした (HTTP {status})。`,"dash.codexRestartUnreachable":`プロキシに接続できませんでした。`,"dash.codexRestartMalformed":`プロキシが予期しない応答を返しました。`,"dash.codexRestartTimeout":`プロキシから時間内に応答がありませんでした。app-server の停止が続いている可能性があります。`,"models.staleBanner":`Codex はこのカタログより古いモデル一覧を表示しています。Codex を再起動すると読み直されます。`,"dash.codexAutoStart":`Codex と一緒に opencodex を起動`,"dash.codexAutoStartHint":`インストール済み launcher shim に ocx ensure の実行を許可します。この設定だけでは再起動保護はインストールされません。起動安全性で実際の状態を確認してください。`,"dash.searchModel":`検索サイドカーモデル`,"dash.searchModelHint":`非 OpenAI ルーティングモデルで web_search に使うモデル。ChatGPT ログインが必要です。`,"dash.searchReasoning":`検索の推論負荷`,"dash.visionModel":`ビジョンサイドカーモデル`,"dash.visionModelHint":`テキスト専用ルーティングモデルで画像を説明するために使うモデル。ChatGPT ログインが必要です。`,"dash.webSearchSidecar":`ウェブ検索サイドカー`,"dash.webSearchSidecarHint":`ルーティングモデルでウェブ検索に使うバックエンドとモデルを選択します。`,"dash.webSearchStream":`回答をライブ配信`,"dash.webSearchStreamHint":`モデルがツール呼び出しを決定するまで、先頭のテキストと推論をライブ配信します。以降は検索インターセプトのためバッファされます。検索前のテキストは一部繰り返される場合があります。`,"dash.visionSidecar":`ビジョンサイドカー`,"dash.visionSidecarHint":`テキスト専用ルーティングモデルで画像を説明するために使うバックエンドとモデルを選択します。`,"dash.visionOff":`オフ`,"dash.shadowCallIntercept":`シャドウコール傍受`,"dash.shadowCallInterceptHint":`Codex App のバックグラウンドヘルパー呼び出し({models}: タイトル生成、コミットメッセージ)を傍受し、選択したモデルにリダイレクトします。`,"dash.shadowCallWarning":`⚠ オンにすると、{models} へのリクエストがすべて選択したモデルに置き換えられます。`,"dash.shadowCallOriginal":`元のモデル`,"dash.shadowCallModel":`差し替えモデル`,"dash.shadowCallTooltip":`Codex App はスレッドタイトル生成、コミットメッセージ生成、スキルオーケストレーションをバックグラウンドで呼び出します。使われるモデルはクライアントのバージョンによって変わるため、opencodex は {models} をまとめて傍受します。これをオンにすると、それらの呼び出しを選択したモデルにリダイレクトします。`,"models.shadowCallIntercept":`シャドウコール傍受`,"models.shadowCallInterceptHint":`Codex App のバックグラウンドヘルパー呼び出し({models}: タイトル、コミットメッセージ)を傍受し、選択したモデルにリダイレクトします。`,"dash.sidecarBackend":`バックエンド`,"dash.sidecarModel":`モデル`,"dash.backendAuto":`自動`,"dash.backendOpenAI":`OpenAI`,"dash.backendAnthropic":`Anthropic`,"dash.sidecarSaved":`サイドカー設定を保存しました。次回リクエスト時に適用されます。`,"dash.sidecarSaveFailed":`サイドカー設定の保存に失敗しました。`,"dash.injectionLabel":`サブエージェント委任`,"dash.injectionHint":`Codex がサブエージェントに作業を渡すときのモデルを選びます。この選択をどこに適用するかは下の 2 つのスイッチが決めます。`,"dash.syncCodexSubagentDefaults":`Codex の既定値としても保存`,"dash.syncCodexSubagentDefaultsHint":`オンにすると、上で選んだモデルが Codex 自身の設定にも保存され、新しいタスクも最初からそのモデルを使います。オフならここだけで記憶します。反映は次回の同期または再起動時で、自分で書いた [agents] 設定はそのまま残ります。`,"dash.multiAgentGuidance":`作業の分け方を伝える`,"dash.multiAgentGuidanceHint":`「作業はこう分けて任せる」という短いメモを Codex に送ります。v2 では使えるモデルと優先モデルを伝え、v1 では推論強度が max か ultra のときだけ働きます。オフならメモは付きません。`,"dash.injectionNone":`なし`,"dash.injectionEffortLabel":`推論負荷`,"dash.injectionEffortNone":`モデル既定`,"dash.effortCapLabel":`V2 ultra 推論上限`,"dash.subagentEffortCapLabel":`V2 サブエージェント推論上限`,"dash.effortCapHelp":`V2 ultra モードのターンの推論負荷を制限します。設定すると、(ultra モードからの)最大負荷リクエストは選択したレベルに制限されます。サブエージェント上限は生成された子エージェントにのみ適用されます。上限は負荷を下げるだけで上げることはありません。モデルが制限レベルをサポートしない場合、最も近いサポートレベルに切り下げられます。`,"dash.effortCapNone":`上限なし`,"dash.maintenance":`メンテナンス`,"dash.maintenanceHint":`Codex のモデルカタログを更新するか、より新しい opencodex リリースをインストールします。`,"dash.syncModels":`モデルを同期`,"dash.syncing":`同期中…`,"dash.syncOk":`同期完了。{count} 個のモデルを追加しました。`,"dash.syncStaleHint":`Codex がまだ古いリストを表示する場合、長時間稼働の app-server を再起動してください({cmd})。`,"dash.syncFailed":`同期失敗: {error}`,"dash.projectConfigTitle":`プロジェクトの Codex 設定が OpenCodex をバイパスします`,"dash.projectConfigHint":`これらのリポジローカル設定は OpenCodex プロキシを上書きします(例: OpenCode Go に直接ルーティング)。~/.codex/config.toml のルーティングがそのプロジェクトで適用されるように削除してください。`,"dash.checkUpdate":`更新を確認`,"dash.updateTitle":`opencodex を更新`,"dash.updateDesc":`選択したチャンネルの npm を確認し、インストール後にプロキシを再起動するか選択します。`,"dash.updateChannel":`チャンネル`,"dash.updateChecking":`更新を確認中…`,"dash.updateInstalled":`インストール済み`,"dash.updateLatest":`最新`,"dash.updateAvailable":`更新があります`,"dash.updateCurrent":`最新です`,"dash.updateCommand":`コマンド`,"dash.updateSource":`これはソースチェックアウトです。表示されたコマンドでターミナルから更新してください。`,"dash.updateUnavailable":`npm から最新バージョンを読み取れませんでした。後でもう一度お試しください。`,"dash.updateRetry":`再試行`,"dash.updateRecheck":`再確認`,"dash.updateCannotAuto":`ワンクリック更新は利用できません({reason})。`,"dash.updateReason.source_checkout":`ソースチェックアウト`,"dash.updateReason.latest_unavailable":`npm レジストリに到達できません`,"dash.updateReason.already_latest":`最新です`,"dash.updateReason.unknown":`更新は利用できません`,"dash.updateRestart":`更新後に再起動`,"dash.updateRestartHint":`推奨。プロキシが再起動されるまで現在の GUI は古いコードを実行し続けます。`,"dash.runUpdate":`更新`,"dash.updateReconnecting":`再起動したプロキシを待機中…`,"dash.updateStatus.running":`opencodex を更新しています。`,"dash.updateStatus.restarting":`更新をインストールしました。プロキシを再起動中。`,"dash.updateStatus.succeeded":`更新が完了しました。`,"dash.updateVersionTransition":`{currentVersion} -> {latestVersion}.`,"dash.updateStatus.failed":`更新に失敗しました。`,"prov.subtitle":`opencodex が Codex にルーティングする上流プロバイダーを設定します。アカウントでログインするか、プロバイダーを追加、または生の設定を編集します。`,"prov.add":`プロバイダーを追加`,"prov.editJson":`JSON を編集`,"prov.accountLogin":`アカウントログイン`,"prov.noOauth":`利用可能な OAuth プロバイダーがありません。`,"prov.loggedIn":`ログイン済み`,"prov.notLoggedIn":`未ログイン`,"prov.logout":`ログアウト`,"prov.login":`ログイン`,"prov.loginWith":`{provider} でログイン`,"prov.waitingBrowser":`ブラウザを待機中…`,"prov.didntOpen":`開きませんか? ここをクリック`,"prov.copyLink":`リンクをコピー`,"prov.dontOpenBrowser":`プロキシのマシンでブラウザーを開かない`,"prov.dontOpenBrowserHint":`別のブラウザープロファイルでログインする場合や、ダッシュボードがプロキシと別のマシンにある場合に使います。`,"prov.linkCopied":`コピーしました`,"prov.linkCopyUnavailable":`クリップボードを使用できません`,"prov.deviceCode":`デバイスコード`,"prov.copyCode":`コードをコピー`,"prov.codeCopied":`コードをコピーしました`,"prov.pasteRedirect":`リダイレクト URL またはコードを貼り付け`,"prov.pasteRedirectHint":`ブラウザに localhost エラーが表示された場合、アドレスバーから URL 全体をコピーしてここに貼り付けてください(または認可コードを貼り付け)。`,"prov.pasteSubmit":`送信`,"prov.pasteSubmitting":`送信中…`,"prov.pasteOk":`コードを送信しました — ログインを完了しています…`,"prov.pasteFail":`コードを送信できませんでした: {error}`,"prov.port":`ポート`,"prov.default":`デフォルト`,"prov.loadingConfig":`読み込み中…`,"prov.saved":`保存しました! 適用にはプロキシを再起動してください。`,"prov.loadConfigFail":`設定の読み込みに失敗しました`,"prov.invalidJson":`無効な JSON です`,"prov.saveFailed":`保存に失敗しました`,"prov.loginFailStart":`{provider} ログインを開始できませんでした`,"prov.loginError":`{provider} ログインエラー: {error}`,"prov.loginRequestFail":`{provider} ログインリクエストに失敗しました`,"prov.loginCancelled":`{provider} ログインはキャンセルされました`,"prov.loginTimeout":`{provider} ログインがタイムアウトしました — ブラウザが閉じたか完了しませんでした。もう一度お試しください。`,"prov.loginOk":`{provider} にログインしました。{cmd} を実行(またはライブで適用)してモデルを一覧表示します。`,"prov.loginSameAccount":`同じ {provider} アカウントのままです。ブラウザでアカウントを切り替えてから、もう一度アカウント追加を試してください。`,"oauthTos.highTitle":`{provider}: サブスクリプション OAuth リスク`,"oauthTos.elevatedTitle":`{provider}: 非公式 OAuth ブリッジ`,"oauthTos.anthropicBody":`OpenCodex のような第三者プロキシ経由で Claude サブスクリプションの OAuth トークンを直接再利用することは、Anthropic がサポートする統合ではなく、アクセス制限につながる可能性があります。Claude サブスクリプションを使用するサポートされた Agent SDK 統合は別物です。`,"oauthTos.highBody":`OpenCodex は第三者 OAuth パス経由で {provider} に接続します。サポート外の利用はアクセス制限や停止につながる可能性があります。`,"oauthTos.elevatedBody":`OpenCodex は非公式 OAuth パス経由で {provider} に接続します。可能な場合は公式クライアントを使用してください。異常または自動化されたトラフィックは悪用とみなされ、アクセスが制限または停止される可能性があります。`,"oauthTos.saferPath":`より安全な選択肢: 代わりに OpenCodex で API キーを設定してください。`,"oauthTos.acknowledge":`リスクを理解した上で、OAuth を続行します。`,"oauthTos.continue":`OAuth で続行`,"prov.logoutOk":`{provider} からログアウトしました。`,"prov.logoutFail":`{provider} からログアウトできませんでした。アカウント状態は変更されていません。`,"prov.removed":`"{name}" を削除しました。`,"prov.removedDefault":`"{name}" を削除しました。既定のプロバイダーは "{defaultProvider}" になりました。`,"prov.removeFail":`"{name}" の削除に失敗しました。`,"prov.removeLastProvider":`このプロバイダーは、他に有効なプロバイダーを既定にできない場合は削除できません。`,"prov.removeHasDependentCombos":`先に依存するコンボを削除または更新してください: {combos}。`,"prov.setDefault":`既定に設定`,"prov.setDefaultSuccess":`"{name}" を既定のプロバイダーに設定しました。`,"prov.setDefaultFail":`"{name}" を既定のプロバイダーに設定できませんでした。`,"prov.defaultDisabled":`既定に設定する前に、このプロバイダーを有効にしてください。`,"prov.updateFail":`このプロバイダーを更新できませんでした。`,"prov.networkError":`ネットワークエラーです。プロキシが実行中であることを確認して、もう一度試してください。`,"prov.added":`"{name}" を追加しました。即時反映 — {cmd} を実行(または再起動)して Codex のピッカーにモデルを一覧表示します。`,"prov.removeConfirm":`プロバイダー "{name}" を削除しますか? そのモデルは Codex のピッカーから消えます。`,"prov.hasApiKey":`API キー設定済み`,"prov.hasHeaders":`カスタムヘッダー設定済み`,"prov.accounts":`アカウント ({n})`,"prov.accountsAria":`{name} のアカウントを切り替え`,"prov.accountActive":`アクティブ`,"prov.accountReauth":`再ログイン`,"prov.reauthenticate":`再認証`,"prov.reauthAccountMissing":`ログイン後に選択されたアカウントが見つかりませんでした`,"prov.reauthIdentityMismatch":`サインインしたアカウントが選択したアカウントと一致しませんでした`,"prov.accountAdd":`アカウントを追加`,"prov.accountNoLabel":`アカウント {id}`,"prov.accountSwitchTitle":`このアカウントを使用`,"prov.accountSwitched":`{email} に切り替えました。`,"prov.accountSwitchFail":`アカウントの切り替えに失敗しました`,"prov.accountRemoved":`{email} を削除しました。`,"prov.accountRemoveFail":`{email} を削除できませんでした。アカウントは変更されていません。`,"prov.accountRemoveAria":`{email} を削除`,"prov.accountRemoveConfirm":`アカウント {email} を削除しますか? そのログインはこのプロキシから削除されます。`,"prov.keyAdd":`API キーを追加`,"prov.keyAdded":`{name} に API キーを追加しました。`,"prov.keyAddFail":`API キーの追加に失敗しました`,"prov.keyPlaceholder":`API キーを貼り付け`,"prov.keySwitchTitle":`このキーを使用`,"prov.keySwitched":`キー {key} に切り替えました。`,"prov.keySwitchFail":`キーの切り替えに失敗しました`,"prov.keyRemoved":`キー {key} を削除しました。`,"prov.keyRemoveAria":`キー {key} を削除`,"prov.keyRemoveConfirm":`API キー {key} を削除しますか? このプロキシの設定から削除されます。`,"prov.activeBadge":`アクティブ`,"prov.disabledBadge":`無効`,"prov.defaultBadge":`デフォルト`,"prov.enable":`有効化`,"prov.disable":`無効化`,"prov.enabled":`"{name}" を有効にしました。そのモデルは再び Codex に表示できます。`,"prov.disabled":`"{name}" を無効にしました。設定は保持されますが、モデルは非表示になります。`,"prov.enableFail":`"{name}" の有効化に失敗しました。`,"prov.disableFail":`"{name}" の無効化に失敗しました。`,"prov.enableAria":`プロバイダー {name} を有効化`,"prov.disableAria":`プロバイダー {name} を無効化`,"prov.defaultCannotDisable":`デフォルトプロバイダーは無効化できません`,"prov.openaiAccountMode":`Codex アカウントモード`,"prov.openaiModePool":`プール`,"prov.openaiModeDirect":`ダイレクト`,"prov.openaiPoolDesc":`デフォルト。アフィニティ、クォータ、クールダウン、フェイルオーバーを使ってメインログインと追加アカウントをローテーションします。`,"prov.openaiDirectDesc":`現在/メインの Codex ログインのみを使用します。保存されたプールアカウントは読み込まれずローテーションもされません。`,"prov.openaiModeSaved":`OpenAI アカウントモードを {mode} に変更しました。`,"prov.openaiModeSaveFailed":`OpenAI アカウントモードを変更できませんでした。`,"prov.openaiApiDesc":`OpenAI API キーを使用し、Codex アカウントの資格情報は使用しません。`,"prov.manageCodexAccounts":`Codex アカウントを管理`,"prov.openaiApiMissing":`API キーが必要です`,"prov.openaiApiSetup":`API キーを設定`,"models.tab.catalog":`モデル`,"models.tab.combos":`コンボ`,"models.tab.compatibility":`互換性`,"models.tab.routing":`ルーティング (beta)`,"models.tabsLabel":`モデルサーフェス`,"models.subtitle.combos":`複数のモデルを 1 つの id にまとめ、順に応答させます。failover でターゲットを連鎖させるか、分散戦略で負荷を分散します。`,"models.subtitle.compatibility":`ラボ投影証拠の読み取り専用互換性判定マトリクス。`,"models.subtitle.routing":`ポリシープロファイル、dry-run 評価、そして根拠の残るルーティング分析です。`,"models.subtitle":`Codex に表示するモデルを切り替えます — ネイティブ GPT パススルーとルーティングプロバイダー、プロバイダー別(ヘッダーをクリックで折りたたみ)。非表示モデルはカタログとピッカーから外れますが、正確な id での直接呼び出しは可能です。変更は次回の Codex ターンで適用 — opencodex は Codex の 5 分間モデルキャッシュを無効化するので再起動は不要です。`,"models.nativeGroupLabel":`OpenAI ネイティブ`,"models.nativeHint":"パススルーモデルはプロバイダーで選択したプールまたはダイレクトアカウントオプションを使用します。一つオフにすると Codex ピッカーから隠します(カタログエントリは保持されるので、再有効化で正確に復元されます)。 ここでモデルを追加すると、bare passthrough id ではなくルーティングされた `openai/` セレクタとして登録されます。","models.active":`{active}/{total} 表示中`,"models.workspace.providers":`プロバイダー`,"models.workspace.allProviders":`すべてのプロバイダー`,"models.workspace.mainAria":`モデルの詳細`,"models.allOn":`すべてオン`,"models.allOff":`すべてオフ`,"models.presetLabel":`モデル`,"models.presetMode_preset":`プリセット`,"models.presetMode_all":`すべて`,"models.presetMode_custom":`カスタム`,"models.presetSummary":`{total} 件中 {count} 件を表示 — コアプリセット v{version}`,"models.presetUpdateAvailable":`プリセット v{version} が利用可能`,"models.presetAppliedToast":`{provider}: プリセットを適用 — {count} 件を選択`,"models.presetClearedToast":`{provider}: すべてのモデルを表示`,"models.presetEmpty":`{provider}: プリセットに一致するモデルがないため選択は変更していません`,"models.presetConfirmReplace":`選択中の一覧を {count} 件のプリセットで置き換えますか?`,"models.cap350k":`350k 上限`,"models.capApplied":`コンテキスト上限を適用しました — 次回の Codex ターンで有効になります。`,"models.capSaveFailed":`コンテキスト上限の保存に失敗しました`,"models.contextCapped":`350k 上限`,"models.contextCapLabel":`デフォルトウィンドウ / 上限`,"models.v2Label":`サブエージェント`,"models.shadowCallOriginal":`⚠ {models} →`,"models.v2DocsLink":`v1 / v2 とは?`,"models.v2Mode_v1":`v1`,"models.v2Mode_default":`ベース`,"models.v2Mode_v2":`v2`,"models.v2ModeDesc_v1":`すべてのモデル → v1 サーフェス`,"models.v2ModeDesc_default":`上流のデフォルト(sol/terra=v2、luna=v1)`,"models.v2ModeDesc_v2":`すべてのモデル → v2 サーフェス`,"models.keepNativeOnV1":`ChatGPT を v1 のまま`,"models.keepNativeOnV1Hint":`ChatGPT ネイティブの親は v2 子タスクを暗号化するため、Grok や Claude は読めません。Sol/Terra から routed モデルを spawn するならオンのまま。routed 親は v2 のままです。`,"models.v2Help":`すべてのモデルのマルチエージェントサーフェスを制御します。 + +v1: クラシックな単一スレッドエージェント。すべてのモデルが v1 コラボサーフェスを使います。 +ベース: 上流のデフォルト — sol/terra は v2、luna は v1、それ以外は codex のフィーチャーフラグに従います。 +v2: spawn_agent を備えたマルチスレッドエージェント。すべてのモデルが v2 コラボサーフェスを使います。 + +v2 では「ChatGPT を v1 のまま」にすると Sol/Terra が v1 に留まり、Grok や Claude を spawn できます。ChatGPT は v2 子タスクを暗号化するため routed モデルは読めません。routed 親は v2 のままです。 + +変更は新規セッションに適用されます。`,"dash.multiAgent":`サブエージェント`,"models.v2Conflict":`[agents] max_threads が設定されています — codex は起動を拒否します; config.toml から削除してください`,"models.v2Applied":`サブエージェントモードを更新しました — 新規セッションに適用(ピッカーを更新するには Codex アプリを再起動)`,"models.v2ThreadsLabel":`最大スレッド数`,"models.v2ThreadsDefault":`デフォルト (4)`,"models.v2ThreadsApplied":`スレッド上限を更新しました — 新規セッションに適用`,"models.v2ThreadsInvalid":`スレッド上限は 1 以上の整数にしてください`,"models.v2ThreadsApply":`適用`,"models.capValue":`デフォルト {value}`,"models.contextSettings":`カスタムウィンドウ`,"models.contextSettingsTitle":`カスタムウィンドウ — {provider}`,"models.contextDefault":`プロバイダーのデフォルト`,"models.contextModel":`モデル`,"models.contextModelOverride":`モデル別の上書き`,"models.contextHint":`すでに分かっている場合は、ここに Codex の実際のコンテキストウィンドウを書きます。上流の値がないときはこの値を使い、より大きな報告値だけ下げ、より小さな上流のコンテキストウィンドウはそのまま残します。空欄ならプロバイダーの「デフォルトウィンドウ / 上限」を使い、その上限がオフなら 128k です。`,"models.contextAutomatic":`自動検出`,"models.contextSaved":`コンテキストウィンドウを更新しました — 次回の Codex ターンから有効です。`,"models.contextUnchanged":`保存するコンテキストウィンドウの変更はありません。`,"models.contextSaveFailed":`コンテキストウィンドウを保存できませんでした`,"models.contextInvalid":`コンテキストウィンドウは正の整数で指定してください`,"models.contextCappedValue":`{value} 上限`,"models.setAll":`すべて設定`,"models.setAllHint":`すべてのルーティング済みプロバイダーに {value} のデフォルトウィンドウをオンにします。中継が context_window / context_length を返さない場合、この値が実際の Codex ウィンドウになります。1 モデルだけ手で書くときは同じ行の「カスタムウィンドウ」を使います。ネイティブプロバイダーには影響しません。`,"models.collapseAll":`すべて折りたたむ`,"models.expandAll":`すべて展開`,"models.orderHint":`ピッカーの順序: サブエージェントの選択(選択順) → 残りのルーティングモデルはプロバイダー別、次にモデル ID 別のアルファベット順 → ネイティブモデル。表示切り替えはモデルをフィルタするだけで、この順序は変更しません。`,"models.custom":`カスタム…`,"models.customApply":`適用`,"models.customPlaceholder":`トークン (例: 420000)`,"models.applied":`適用しました — 次回の Codex ターンで有効になります。`,"models.saveFailed":`保存に失敗しました`,"models.networkError":`ネットワークエラー — プロキシは起動していますか?`,"models.loadFail":`モデルの読み込みに失敗しました — プロキシは起動していますか?`,"models.noRouted":`ルーティングモデルがありません`,"models.noRoutedHint":`まずプロバイダーにログインするか追加してください。`,"models.emptyDiscovery":`モデルが見つかりませんでした。プロバイダーのエンドポイントを確認するか、静的/カスタムモデルを追加してください。`,"models.emptyDiscoveryDisabled":`ライブモデル検出がオフで、静的モデルも設定されていません。`,"models.discoveryFailedBadge":`検出に失敗`,"models.discoveryFailedHttp":`モデル検出に失敗しました(HTTP {status})。`,"models.discoveryFailedBlocked":`モデル検出は宛先ポリシーによりブロックされました。`,"models.discoveryFailedInvalidResponse":`モデル検出が無効な応答を返しました。`,"models.discoveryFailedNetwork":`ネットワークエラーによりモデル検出に失敗しました。`,"models.discoveryFailedProvider":`プロバイダーがモデル検出エラーを報告しました。`,"models.discoveryFailedGeneric":`モデル検出に失敗しました。`,"models.openProviderSettings":`プロバイダー設定を開く`,"models.loading":`読み込み中…`,"models.search":`モデルを検索…`,"models.showMore":`さらに {n} 件表示`,"models.allowlistLabel":`選択のみ`,"models.allowlistHint":`チェックしたモデルのみカタログに送信します(空 = すべて)。数千のモデルを公開するプロバイダーで有用です。`,"models.selectedCount":`{n} 件選択`,"sub.subtitle":`Codex の {cmd} は最初の 5 モデル(優先度順)のみをオーバーライドとして通知します。ここで最大 5 つを選んでください — ネイティブ gpt またはルーティング — opencodex がカタログ優先度を設定し、これらが先頭に来るようにします。他のモデルも正確な名前で呼び出し可能です; これは表示のみを制御します。`,"sub.featured":`おすすめ`,"sub.advanced":`詳細設定`,"sub.orderHintAria":`この順序の使われ方`,"sub.orderHint":`ここでの表示順が Codex モデルピッカーの上位 1〜5 番目の位置と {cmd} のデフォルトモデル候補を決定します。`,"sub.noneSelected":`未選択 — 以下のリストから選んでください。`,"sub.models":`モデル`,"sub.search":`モデルを検索(ネイティブ gpt + ルーティング)…`,"sub.noModels":`モデルがありません — まずプロバイダーにログインするか追加してください。`,"sub.saved":`{n} 件のモデルを保存しました。新規 Codex セッションを開始(または {cmd} を実行)して spawn_agent オーバーライドとして確認してください。`,"sub.saveFailed":`保存に失敗しました`,"sub.networkError":`ネットワークエラー — プロキシは起動していますか?`,"sub.loadFail":`モデルの読み込みに失敗しました — プロキシは起動していますか?`,"sub.loading":`読み込み中…`,"sub.moveUp":`{m} を上へ移動`,"sub.moveDown":`{m} を下へ移動`,"sub.removeAria":`{m} を削除`,"sub.workspace.addToFeatured":`{m} をおすすめに追加`,"sub.workspace.allModels":`すべてのモデル`,"sub.workspace.featuredFull":`おすすめリストがいっぱいです(最大 5)`,"sub.workspace.mainAria":`サブエージェントのモデル詳細`,"sub.workspace.notFeatured":`おすすめ未設定`,"sub.workspace.priority":`優先度`,"sub.workspace.removeFromFeatured":`{m} をおすすめから削除`,"sub.workspace.selectModel":`モデルを選択`,"sub.workspace.selectModelDesc":`一覧からモデルを選んで詳細を確認し、spawn_agent のおすすめに設定します。`,"sub.workspace.selector":`公開セレクター`,"sub.ultraMode":`ウルトラモード`,"sub.ultraModeHint":`すべてのモデルと reasoning effort で Proactive マルチエージェント委任ポリシーを有効にします(reasoning effort 自体は変更しません)。config.toml に features.multi_agent_v2.multi_agent_mode_hint_text を書き込みます。`,"sub.ultraModeV2Required":`v2 マルチエージェントサーフェスが必要です — 先に multi_agent_v2 を有効にし、サブエージェントモードで v2 を選択してください。`,"sub.ultraModeText":`ウルトラモード委任テキスト`,"sub.ultraModePreset":`プリセットを復元`,"sub.ultraModeLoadFail":`ウルトラモード設定を読み込めませんでした — プロキシは実行中ですか?`,"sub.ultraModeSaveFail":`ウルトラモード設定の保存に失敗しました`,"sub.ultraModeSaved":`ウルトラモードを保存しました。新しい Codex セッションから適用されます。`,"logs.title":`リクエストログ`,"logs.tabLogs":`ログ`,"logs.tabDebug":`デバッグ`,"logs.subtitle":`ローカル opencodex プロキシを経由した最近のリクエスト(新しい順)。`,"logs.autoRefresh":`自動更新`,"logs.noRequests":`まだリクエストがありません。`,"logs.loadError":`リクエストログを読み込めませんでした。`,"logs.filter.surface.label":`サーフェス`,"logs.filter.surface.all":`すべて`,"logs.filter.surface.claude":`Claude`,"logs.filter.surface.codex":`Codex`,"logs.filter.surface.grok":`Grok`,"logs.filter.interceptedHelpersOnly":`インターセプトされたヘルパーのみ`,"logs.badge.interceptedHelper":`I · {model}`,"logs.badge.interceptedHelperTitle":`インターセプトされたヘルパー要求`,"logs.filter.conversation.label":`会話`,"logs.filter.conversation.placeholder":`会話 ID を貼り付け`,"logs.filter.conversation.clear":`クリア`,"logs.filter.model.label":`モデル`,"logs.filter.model.placeholder":`モデルまたはプロバイダーで絞り込む`,"logs.filter.conversation.apply":`ログを絞り込み`,"logs.conversation.totals":`{requests} 件 · {tokens} トークン · {cost}`,"logs.conversation.scope":`合計は現在読み込まれている Logs リングのみです。`,"logs.conversation.excluded":`(~$ から価格なし {unpriced} / 未計測 {unmetered} を除外)`,"logs.cost.approximate":`{amount}`,"logs.cost.lowerBound":`≥{amount}`,"logs.cost.unavailable":`利用不可`,"logs.detail.conversation":`会話`,"logs.badge.claude":`Claude`,"logs.badge.grok":`Grok`,"logs.col.time":`時刻`,"logs.col.request":`リクエスト`,"logs.col.model":`モデル`,"logs.col.effort":`負荷`,"logs.col.provider":`プロバイダー`,"logs.col.status":`状態`,"logs.col.tokens":`トークン`,"logs.col.tokPerSec":`tok/s`,"logs.col.estimatedCost":`~$`,"logs.metric.tokPerSecTitle":`リクエスト全体の所要時間あたりの出力トークン数`,"logs.metric.estimatedCostTitle":`API 定価相当額(実際の請求ではありません); 未対応の価格は利用できません`,"usage.cost.total":`API 定価相当額(この期間)`,"usage.cost.disclaimer":`請求明細ではありません。サブスクリプション利用量やプロバイダークレジットが代わりに適用される場合があります。`,"usage.cost.unpricedNote":`{count} 件のリクエストを除外(価格または使用量なし)`,"logs.detail.section.basic":`基本情報`,"logs.detail.route.section":`ルート決定`,"logs.detail.route.kind":`ルート種別`,"logs.detail.route.profile":`プロファイル`,"logs.detail.route.selected":`選択済み`,"logs.detail.route.candidates":`候補`,"logs.detail.route.unknown":`このリクエストにはルートトレースが記録されていません(トレース前の行)。`,"logs.detail.section.performance":`パフォーマンス`,"logs.detail.section.cost":`API 定価相当額`,"logs.detail.section.attempts":`コンボの試行`,"logs.detail.section.usage":`生の使用量`,"logs.detail.ttft":`TTFT`,"logs.detail.costTotal":`定価相当額`,"logs.detail.totalTokens":`合計トークン`,"logs.detail.matchedKey":`一致した価格キー`,"logs.detail.priceSource":`価格ソース`,"logs.detail.unavailableReason":`利用不可の理由`,"logs.detail.copyRequestId":`リクエスト ID をコピー`,"logs.detail.copied":`コピーしました`,"logs.detail.source.jawcode":`jawcode カタログ`,"logs.detail.source.expected":`予想価格オーバーレイ`,"logs.detail.source.user":`プロバイダー設定の価格オーバーレイ`,"logs.detail.verification.verified":`検証済み`,"logs.detail.verification.derived":`ベースモデルから派生`,"logs.detail.attempt.target":`プロバイダー / モデル`,"logs.detail.attempt.reason":`結果 / 理由`,"logs.detail.attempt.completed":`完了`,"logs.detail.attempt.e2eNote":`トップレベルの tok/s はエンドツーエンドです; 各試行は自身の所要時間を使います。`,"logs.detail.attempt.recovery.transient5xx":`一時的な5xxエラー`,"logs.detail.attempt.recovery.connectionReset":`接続がリセットされました`,"logs.detail.attempt.recovery.oauth401":`OAuth 再認証`,"logs.detail.attempt.recovery.key429":`キーがレート制限 (429)`,"logs.detail.attempt.recovery.rateLimit429":`レート制限 (429)`,"logs.detail.attempt.recovery.anthropicOauth429":`Anthropic OAuth レート制限 (429)`,"logs.detail.attempt.recovery.image413":`画像ペイロードが大きすぎます (413)`,"logs.detail.attempt.recovery.emptyCompletion":`空の完了を再試行`,"logs.detail.attempt.recovery.unknown":`不明なリカバリ理由`,"logs.detail.reason.usage_missing":`使用量が報告されませんでした。`,"logs.detail.reason.usage_unsupported":`このプロバイダーは使用量を報告しません。`,"logs.detail.reason.output_missing":`正の出力トークン数が報告されませんでした。`,"logs.detail.reason.invalid_duration":`リクエストの所要時間が有効ではありません。`,"logs.detail.reason.price_unmatched":`一致する価格が見つかりませんでした。`,"logs.detail.reason.invalid_cache_breakdown":`キャッシュトークンの詳細が合計入力トークンと矛盾しています。`,"logs.detail.reason.invalid_usage":`使用量に無効なトークン値が含まれています。`,"logs.detail.reason.combo_attempt_unavailable":`少なくとも 1 つのコンボ試行に価格を設定できませんでした。`,"logs.detail.estimate.usage_estimated":`プロバイダーの使用量は推定です。`,"logs.detail.estimate.cache_detail_missing":`キャッシュの詳細が利用できませんでした; 入力は上限の推定です。`,"logs.detail.estimate.expected_price_overlay":`検証済みの予想定価が使用されました。`,"logs.detail.estimate.provider_cost_overlay":`プロバイダー設定の価格オーバーレイが使用されました。`,"logs.detail.estimate.priority_lower_bound":`確認済みの Priority 価格を利用できないため、表示される見積もりは既知の下限です。`,"logs.col.error":`エラー`,"logs.col.upstreamReason":`上流の理由`,"logs.col.duration":`所要時間`,"logs.modelTooltip.model":`モデル`,"logs.modelTooltip.resolvedModel":`解決後モデル`,"logs.modelTooltip.requestedTier":`要求ティア`,"logs.modelTooltip.configuredTier":`設定ティア`,"logs.modelTooltip.responseTier":`応答ティア`,"logs.modelTooltip.supportsTier":`ティア対応`,"logs.tokens.reported":`報告済み`,"logs.tokens.unreported":`未報告`,"logs.tokens.unsupported":`非対応`,"logs.tokens.estimated":`推定`,"logs.tokens.input":`入力`,"logs.tokens.output":`出力`,"logs.tokens.cacheRead":`キャッシュ読み取り (c)`,"logs.tokens.cacheWrite":`キャッシュ書き込み (w)`,"logs.tokens.reasoning":`推論`,"logs.tokens.noCache":`キャッシュデータなし`,"logs.tokens.contextTotal":`アクティブコンテキスト`,"logs.tokens.noCacheNote":`このプロバイダーはキャッシュトークンを報告しません`,"logs.tokens.noCacheCursor":`Cursor のキャッシュ詳細は未報告`,"logs.tokens.noCacheCursorNote":`Cursor はキャッシュ read/write トークン数を公開しません。これは不明という意味で、キャッシュミスの確定ではありません`,"logs.tokens.estimatedNote":`推定(プロバイダーは正確な使用量を報告しません)`,"logs.details":`詳細`,"logs.detailTitle":`リクエストの詳細`,"logs.detailRaw":`生のログエントリ`,"debug.title":`デバッグ`,"debug.subtitle":`オプトインのプロバイダートランスポートおよび使用量抽出診断です。リクエストエラーと 502 はログタブに残ります。`,"debug.debug":`プロバイダーデバッグ`,"debug.usage":`使用量抽出`,"debug.injection":`インジェクションログ`,"debug.claude":`Claude インバウンド`,"debug.claudeInbound.title":`Claude インバウンドリクエスト`,"debug.claudeInbound.sub":`Claude Code/Desktop が実際に送信する内容(thinking、effort、metadata) — プロンプトテキストは保存されません。`,"debug.claudeInbound.empty":`まだキャプチャされたリクエストはありません。これがオンの状態で Claude からメッセージを送信してください。`,"debug.claudeInbound.time":`時刻`,"debug.claudeInbound.endpoint":`エンドポイント`,"debug.claudeInbound.model":`モデル`,"debug.claudeInbound.none":`なし`,"debug.reset":`ランタイムオーバーライドをクリア`,"debug.refresh":`更新`,"debug.follow":`追従`,"debug.streamProvider":`プロバイダー`,"debug.streamUsage":`使用量`,"debug.streamInjection":`インジェクション`,"debug.loading":`デバッグ設定を読み込み中…`,"debug.loadFailed":`デバッグ設定を読み込めませんでした。`,"debug.emptyTitle":`デバッグログはオフです`,"debug.empty":`上のカードでプロバイダーデバッグまたは使用量抽出をオンにしてください。プロキシ経由でリクエストを送信すると、ここに行が表示されます。`,"debug.noLinesTitle":`行を待機中`,"debug.noLines.provider":`プロバイダーデバッグはオンですが、トランスポートの異常(欠落または不正なフレーム、Cursor のダイヤル/再試行イベント)のみを記録します。Anthropic のようなプロバイダーでの正常なリクエストは行を生成しないことがあります。`,"debug.noLines.usage":`使用量抽出はオンですが、まだ何もキャプチャされていません。Codex 経由でチャット/リクエストを送信するとここに表示されます。`,"debug.noLines.injection":`インジェクションログはオンですが、まだ何もキャプチャされていません。コラボおよびサブエージェントのターンでのマルチエージェントガイダンスインジェクションと負荷上限の決定を記録します。`,"usage.title":`使用量`,"usage.subtitle":`プロキシからのローカルトークン会計です。欠損した使用量はゼロとして表示されることはありません。`,"usage.loading":`使用量データを読み込み中…`,"usage.empty":`まだ使用量が記録されていません。プロキシ経由でリクエストを送信するとここにアクティビティが表示されます。`,"usage.loadError":`使用量データを読み込めませんでした。`,"usage.range.all":`すべて`,"usage.range.available":`利用可能な履歴`,"usage.historyTruncated":`古い利用履歴が読み込まれていないため、合計は利用可能な履歴のみを対象とします。`,"usage.historyTruncatedWindow":`読み込まれた行のリクエスト開始時刻は {start} から {end} の範囲です。読み取り上限によりファイル前方の記録が除外されているため、選択した期間は不完全な場合があります。`,"usage.range.30d":`30日`,"usage.range.7d":`7日`,"usage.card.requests":`リクエスト`,"usage.card.measured":`計測`,"usage.card.reported":`報告`,"usage.card.totalTokens":`合計トークン`,"usage.card.cachedTokens":`キャッシュ読み取り`,"usage.card.cachedTokensHint":`プロバイダーキャッシュから提供されたプロンプトトークン(読み取り)。キャッシュ書き込みは存在する場合、下に表示されます。`,"usage.card.cacheWriteTokens":`キャッシュ書き込み`,"usage.card.coverage":`カバレッジ`,"usage.card.activeDays":`アクティブ日数`,"usage.section.heatmap":`日のアクティビティ`,"usage.section.overview":`概要`,"usage.section.models":`モデル`,"usage.section.providers":`プロバイダー`,"usage.section.coverage":`カバレッジ内訳`,"usage.workspace.report":`使用量レポート`,"usage.workspace.sections":`使用量セクション`,"usage.coverage.measured":`計測`,"usage.coverage.reported":`プロバイダー報告`,"usage.coverage.estimated":`推定`,"usage.coverage.note":`計測エントリにはプロバイダー報告および推定のトークン数が含まれます。未報告および非対応のリクエストは追跡されますが、ゼロトークンに水増しされることはありません。`,"usage.search.models":`モデルを検索…`,"usage.col.requests":`リクエスト`,"usage.col.measured":`計測`,"usage.col.reported":`報告`,"usage.col.tokens":`トークン`,"usage.col.share":`割合`,"usage.heatmap.less":`少ない`,"usage.heatmap.more":`多い`,"usage.dayMon":`月`,"usage.dayWed":`水`,"usage.dayFri":`金`,"usage.heatmap.tooltipTokens":`{tokens} トークン`,"usage.heatmap.tooltipRequests":`{requests} リクエスト`,"nav.storage":`ストレージ`,"storage.title":`ストレージ`,"storage.subtitle":`CODEX_HOME の使用状況を確認。クリーンアップはアクティブセッションに触れません。`,"storage.loading":`ストレージをスキャン中…`,"storage.empty":`CODEX_HOME が空か存在しません — 報告するものはありません。`,"storage.error":`ストレージのスキャンに失敗しました。CODEX_HOME が有効なディレクトリを指しているか確認してください。`,"storage.refresh":`再スキャン`,"storage.rescanned":`スキャンが完了しました。`,"storage.card.total":`合計サイズ`,"storage.card.files":`ファイル`,"storage.card.home":`CODEX_HOME`,"storage.snapshot.lastScan":`最終スキャン`,"storage.snapshot.scanning":`スキャン中…`,"storage.snapshot.unavailable":`まだスキャンがありません。`,"storage.cleanupCard.title":`容量を空ける`,"storage.cleanupCard.tabs":`クリーンアップオプション`,"storage.cleanupCard.tab.policy":`ポリシー`,"storage.cleanupCard.tab.quarantine":`隔離`,"storage.cleanup.noArchives":`クリーンアップ対象のアーカイブセッションはありません。`,"storage.section.buckets":`バケット`,"storage.section.largest":`最大ファイル`,"storage.workspace.overview":`概要`,"storage.workspace.selectBucket":`一覧からバケットを選ぶと内訳が表示されます。`,"storage.col.bucket":`バケット`,"storage.col.size":`サイズ`,"storage.col.files":`ファイル`,"storage.col.oldest":`最古`,"storage.col.newest":`最新`,"storage.col.rows":`DB 行`,"storage.rows.unknown":`不明(ロック中)`,"storage.bucket.sessions":`アクティブセッション`,"storage.bucket.archived_sessions":`アーカイブ済みセッション`,"storage.bucket.logs_db":`ログデータベース`,"storage.bucket.state_db":`状態データベース`,"storage.bucket.attachments":`添付`,"storage.bucket.deletion_manifests":`削除マニフェスト`,"storage.bucket.other":`その他`,"storage.cleanup.title":`アーカイブのクリーンアップ`,"storage.cleanup.help":`古いアーカイブセッションを割合で削除します。アクティブセッションには触れません。既定は隔離で、ファイルは CODEX_HOME/.trash へ移動します。`,"storage.cleanup.slider":`古いアーカイブの割合`,"storage.cleanup.percent":`{percent}%`,"storage.cleanup.preset":`{percent}`,"storage.cleanup.preview":`プレビュー`,"storage.cleanup.confirmTitle":`アーカイブクリーンアップの確認`,"storage.cleanup.confirmBody":`アーカイブ {count} 件(約 {size})、古い {percent}% を処理します。`,"storage.cleanup.moreFiles":`…ほか {n} 件`,"storage.cleanup.permanent":`完全に削除する(隔離しない)`,"storage.cleanup.permanentWarn":`完全削除は元に戻せません。`,"storage.cleanup.quarantineNote":`ファイルは CODEX_HOME 下の .trash へ移動します。隔離タブから復元できます。`,"storage.cleanup.cancel":`キャンセル`,"storage.cleanup.confirmQuarantine":`隔離する`,"storage.cleanup.confirmPermanent":`完全に削除`,"storage.cleanup.doneQuarantine":`{count} 件を隔離しました({size})。`,"storage.cleanup.donePermanent":`{count} 件を完全削除しました({size})。`,"storage.cleanup.previewFailed":`プレビューに失敗しました。`,"storage.cleanup.cleanupFailed":`クリーンアップに失敗しました。`,"storage.cleanup.err.codex_busy":`Codex が state.sqlite を使用中です — Codex を終了して再試行してください。`,"storage.cleanup.err.stale_preview":`プレビュー以降にアーカイブが変わりました — プレビューをやり直してください。`,"storage.cleanup.err.restore_pending_overlap":`選択したアーカイブは未完了の隔離復元と重なっています — 復元を完了するか再試行してください。`,"storage.cleanup.err.referenced_history":`選択したアーカイブはフォークまたはページング履歴から参照されています。`,"storage.cleanup.err.invalid_digest":`プレビューのダイジェストが無い、または無効です。`,"storage.cleanup.err.invalid_mode":`モードは quarantine または permanent である必要があります。`,"storage.cleanup.err.fs_failed":`ファイルシステムのクリーンアップに失敗しました。一部の変更は既に適用されている可能性があります — CODEX_HOME/.trash と表示されたリカバリパスを確認してください。`,"storage.cleanup.err.fs_failed_trash":`ファイルシステムのクリーンアップに失敗しました。一部の変更は既に適用されている可能性があります — {trashDir} と manifest.json で復旧可能なファイルを確認してください。`,"storage.cleanup.err.db_reconcile_failed":`Codex の状態データベースを更新できませんでした。`,"storage.cleanup.err.cleanup_failed":`クリーンアップに失敗しました。`,"storage.trash.title":`隔離`,"storage.trash.help":`CODEX_HOME/.trash へ移したアーカイブセッションです。復元すると JSONL とスレッド行が戻ります。`,"storage.trash.empty":`隔離エントリはありません。`,"storage.trash.loading":`隔離を読み込み中…`,"storage.trash.col.when":`隔離日時`,"storage.trash.col.files":`ファイル`,"storage.trash.col.size":`サイズ`,"storage.trash.col.mode":`モード`,"storage.trash.col.id":`エントリ`,"storage.trash.restore":`復元`,"storage.trash.confirmTitle":`隔離エントリを復元しますか?`,"storage.trash.confirmBody":`{id} から {count} 件(約 {size})をアーカイブセッションへ戻します。`,"storage.trash.cancel":`キャンセル`,"storage.trash.confirmRestore":`復元`,"storage.trash.done":`{count} 件を復元しました({size})。`,"storage.trash.restoreFailed":`復元に失敗しました。`,"storage.trash.listFailed":`隔離一覧を取得できませんでした。`,"storage.trash.mode.quarantine":`隔離`,"storage.trash.mode.permanent":`完全削除(未完了)`,"storage.trash.err.codex_busy":`Codex が state.sqlite を使用中です — Codex を終了して再試行してください。`,"storage.trash.err.invalid_trash":`隔離エントリ ID が無い、または無効です。`,"storage.trash.err.missing_trash":`隔離エントリが見つかりません。`,"storage.trash.err.dest_exists":`復元先が既に存在します — アーカイブファイルを削除または改名して再試行してください。`,"storage.trash.err.fs_failed":`ファイルシステムの復元に失敗しました。一部は既に復元されている可能性があります — archived_sessions と .trash を確認してください。`,"storage.trash.err.storage_mutation_busy":`別のストレージクリーンアップまたは復元が進行中です — しばらくして再試行してください。`,"storage.trash.err.db_reconcile_failed":`Codex の状態データベース行を復元できませんでした。`,"storage.trash.err.restore_failed":`復元に失敗しました。`,"storage.trash.err.restore_worker_timeout":`復元が長時間(10 分超)かかったため停止しました。`,"storage.trash.err.restore_worker_aborted":`シャットダウン中に復元がキャンセルされました。`,"storage.trash.err.restore_worker_failed":`復元ワーカーがクラッシュまたは予期しないエラーで失敗しました。`,"storage.policy.title":`自動クリーンアップ方針`,"storage.policy.help":`アーカイブがしきい値を超えたときの任意の一括クリーンアップ。既定はオフ — 自動では有効になりません。`,"storage.policy.loading":`方針を読み込み中…`,"storage.policy.loadFailed":`クリーンアップ方針を読み込めませんでした。`,"storage.policy.saveFailed":`クリーンアップ方針を保存できませんでした。`,"storage.policy.runFailed":`方針の実行に失敗しました。`,"storage.policy.alreadyRunning":`クリーンアップ方針の実行が既に進行中です。`,"storage.policy.invalid":`方針の値が無効です。`,"storage.policy.enabled":`自動クリーンアップを有効化`,"storage.policy.enabledHint":`既定はオフです。有効にすると選択したスケジュール(または今すぐ実行)でのみ動きます。`,"storage.policy.threshold":`アーカイブサイズが超えたら(GiB)`,"storage.policy.trigger":`トリガー`,"storage.policy.target":`クリーンアップ目標`,"storage.policy.targetPercent":`古いアーカイブを削除(%)`,"storage.policy.targetReduce":`アーカイブを次のサイズまで縮小(GiB)`,"storage.policy.thresholdInc":`しきい値を上げる`,"storage.policy.thresholdDec":`しきい値を下げる`,"storage.policy.percentInc":`パーセントを上げる`,"storage.policy.percentDec":`パーセントを下げる`,"storage.policy.reduceInc":`削減目標を上げる`,"storage.policy.reduceDec":`削減目標を下げる`,"storage.policy.schedule":`スケジュール`,"storage.policy.schedule.manual":`手動のみ`,"storage.policy.schedule.startup":`プロキシ起動時`,"storage.policy.schedule.daily":`毎日`,"storage.policy.schedule.weekly":`毎週`,"storage.policy.mode":`削除モード`,"storage.policy.mode.quarantine":`隔離(既定)`,"storage.policy.mode.permanent":`完全削除`,"storage.policy.permanentWarn":`完全削除モードは元に戻せません。確信がなければ隔離を使ってください。`,"storage.policy.lastRun":`前回の実行`,"storage.policy.lastRunDetail":`{count} 件削除 · {size} 解放`,"storage.policy.nextRun":`次回の実行`,"storage.policy.never":`なし`,"storage.policy.save":`保存`,"storage.policy.runNow":`今すぐ実行`,"storage.policy.running":`実行中…`,"storage.policy.saved":`方針を保存しました。`,"storage.policy.skippedDisabled":`方針が無効です — 先に有効化してください。`,"storage.policy.skippedUnder":`アーカイブサイズがしきい値未満です — 作業はありません。`,"storage.policy.skippedEmpty":`目標に合うアーカイブ候補がありません。`,"storage.policy.doneQuarantine":`方針が {count} 件を隔離しました({size})。`,"storage.policy.donePermanent":`方針が {count} 件を完全削除しました({size})。`,"storage.policy.metadataSaveWarning":`方針の実行は完了しましたが、スケジュールのメタデータを保存できませんでした。`,"modal.addNamed":`追加: {label}`,"modal.add":`プロバイダーを追加`,"modal.search":`プロバイダーを検索…`,"modal.logInWith":`{label} でログイン`,"modal.waitingBrowser":`ブラウザを待機中…`,"modal.providerName":`プロバイダー名`,"modal.adapter":`アダプター`,"modal.baseUrl":`ベース URL`,"modal.endpoint":`エンドポイント`,"modal.endpoint.tokenPlan":`トークンプラン`,"modal.endpoint.payAsYouGo":`従量課金`,"modal.endpoint.custom":`カスタム`,"modal.defaultModel":`デフォルトモデル(任意)`,"modal.allowPrivateNetwork":`ローカル/プライベートネットワークを許可`,"modal.allowPrivateNetworkHint":`意図的にセルフホストしたプロバイダーに対してのみ有効化してください。メタデータエンドポイントはブロックされたままです。`,"modal.nameRequired":`プロバイダー名は必須です`,"modal.baseUrlRequired":`ベース URL は必須です`,"modal.networkError":`ネットワークエラー — プロキシは起動していますか?`,"modal.loginFailStart":`ログインを開始できませんでした`,"modal.waitingLogin":`ブラウザログインを待機中…`,"modal.loggingIn":`ログイン中…`,"modal.loginTimeout":`ログインがタイムアウトしました — もう一度お試しください。`,"modal.back":`戻る`,"modal.badge.oauth":`OAuth`,"modal.customProvider":`カスタムプロバイダー`,"modal.failedStatus":`失敗 ({status})`,"modal.loginError":`ログインエラー: {error}`,"modal.badge.codexLogin":`Codex ログイン`,"modal.badge.local":`ローカル`,"modal.badge.apiKey":`API キー`,"modal.badge.direct":`ダイレクト`,"modal.badge.pool":`プール`,"modal.badge.free":`無料`,"modal.invalidPreset":`この組み込みプロバイダープリセットは不完全です。プロキシを再起動してもう一度お試しください。`,"modal.freeTierTitle":`無料枠`,"modal.freeTierDefault":`API キー不要です。そのまま利用できます。`,"modal.tab.accounts":`アカウント`,"modal.tab.free":`無料`,"modal.tab.paid":`有料`,"modal.accountsHint":`ChatGPT/Codex、OAuth プロバイダー、API キーアカウントにここからサインインします。OpenAI は組み込み済み — 再度追加せずログインしてください。`,"modal.accountsCodexAuthLink":`Codex 認証`,"modal.notListed":`プロバイダーが載っていませんか? カスタムを追加`,"modal.catalogLoading":`カタログを読み込み中…`,"modal.accountLogin":`ログイン`,"modal.accountLogout":`ログアウト`,"modal.accountAdd":`アカウントを追加`,"modal.accountManage":`管理`,"modal.accountCodexPool":`ChatGPT アカウントプール`,"modal.accountLoggedIn":`ログイン済み`,"modal.accountLoggedOut":`未ログイン`,"quota.fiveHourLimit":`5 時間上限`,"quota.ageMinutes":`{n}分`,"quota.ageHours":`{n}時間`,"quota.ageDays":`{n}日`,"quota.observedAgo":`{age}前に取得`,"quota.observedHint":`Meta はストリーミング応答中にのみ使用量を報告します。リアルタイムの値ではなく、最後に取得した値です。`,"quota.weeklyLimit":`週間上限`,"quota.monthlyLimit":`30 日上限`,"quota.cursorFirstParty":`ファーストパーティモデル`,"quota.cursorApiUsage":`API 使用量`,"quota.totalSubscriptionCredits":`サブスクリプションクレジット合計`,"quota.creditsBalance":`クレジット残高`,"quota.creditsPeriodEnds":`請求期間終了日: {date}`,"quota.usedPercent":`{pct}% 使用`,"quota.limitReached":`上限に達しました`,"quota.resetsToday":`今日 {time} にリセット`,"quota.resetsTomorrow":`明日 {time} にリセット`,"quota.resetsAt":`{when} にリセット`,"quota.resetsRelativeMinutes":`{n} 分後にリセット`,"quota.resetsRelativeHours":`{n} 時間後にリセット`,"pws.status.ready":`準備完了`,"pws.status.needsSetup":`セットアップが必要`,"pws.status.needsAttention":`要対応`,"pws.auth.chatgptPassthrough":`ChatGPT パススルー`,"pws.auth.noKey":`キー不要`,"pws.freeTitle":`無料料金(キーが必要な場合もあります)`,"pws.localTitle":`ローカルランタイム`,"pws.modelCountOne":`1 モデル`,"pws.modelCount":`{count} モデル`,"pws.rail.suffixDefault":` · デフォルト`,"pws.rail.suffixLocal":` · ローカル`,"pws.rail.suffixFree":` · 無料`,"pws.rail.selectAria":`{name} を選択 — {status}{suffix}`,"pws.searchPlaceholder":`プロバイダーを検索…`,"pws.filterAria":`プロバイダーを絞り込み`,"pws.providerFiltersAria":`プロバイダーフィルタ`,"pws.filters":`フィルタ`,"pws.filterStatus":`状態`,"pws.pricing":`料金`,"pws.paid":`有料`,"pws.filterType":`タイプ`,"pws.type.cloud":`クラウド`,"pws.type.local":`ローカル`,"pws.type.selfHosted":`セルフホスト`,"pws.type.login":`ログイン`,"pws.sort":`並べ替え`,"pws.sortProvidersAria":`プロバイダーを並べ替え`,"pws.sort.az":`A–Z`,"pws.sort.za":`Z–A`,"pws.sort.freePaid":`無料優先`,"pws.sort.paidFree":`有料優先`,"pws.sort.accountsFirst":`アカウント優先`,"pws.resetAll":`すべてリセット`,"pws.providerList":`プロバイダー一覧`,"pws.providersAria":`プロバイダー`,"pws.groupReady":`準備完了 ({count})`,"pws.groupNeedsSetup":`セットアップが必要 ({count})`,"pws.groupDisabled":`無効 ({count})`,"pws.noSearchResults":`検索に一致するプロバイダーがありません。`,"pws.noMatchFilters":`フィルタに一致するプロバイダーがありません。`,"pws.noProvidersConfigured":`プロバイダーが設定されていません。`,"pws.workspaceMainAria":`プロバイダーの詳細`,"pws.detailComingSoon":`詳細ビューは近日対応 — このプロバイダーの管理にはクラシックビューを使用してください。`,"pws.selectPrompt":`リストからプロバイダーを選択してください。`,"pws.connectFirst":`最初のプロバイダーを接続`,"pws.empty.browseFree":`無料プロバイダーを見る`,"pws.empty.browseFreeDesc":`サブスクリプションなしで始める`,"pws.empty.connectAccount":`アカウントを接続`,"pws.empty.connectAccountDesc":`ChatGPT やプロバイダーのログインを使用`,"pws.empty.addEndpoint":`エンドポイントを追加`,"pws.empty.addEndpointDesc":`カスタムベース URL と API キー`,"pws.tab.overview":`概要`,"pws.tab.models":`モデル`,"pws.tab.usage":`使用量`,"pws.tab.accounts":`アカウント`,"pws.tab.settings":`設定`,"pws.connection":`接続`,"pws.status.connected":`接続済み`,"pws.attentionTitle":`要対応`,"pws.attention.reauth":`アクティブアカウントの再認証が必要です`,"pws.attention.reauthForward":`アクティブな Codex アカウントの再認証が必要です — アカウントタブを開いて修正してください`,"pws.attention.missingCredentials":`資格情報が不足しています`,"pws.cell.auth":`認証`,"pws.cell.note":`メモ`,"pws.cell.defaultModel":`デフォルトモデル`,"pws.statsAria":`プロバイダー統計`,"pws.statsTitle":`統計`,"pws.stats.totalRequests":`リクエスト (30日)`,"pws.stats.totalTokens":`トークン (30日)`,"pws.stats.quotaUpdated":`クォータを更新しました`,"pws.stats.quotaTracked":`レート制限は使用量タブで追跡されます。`,"pws.stats.source":`ソース`,"pws.usageLast30d":`使用量 (過去30日)`,"pws.metricRequests":`リクエスト`,"pws.metricTokens":`トークン`,"pws.usageUnavailable":`まだ使用量が記録されていません。`,"pws.rateLimits":`レート制限`,"pws.quotaUnavailable":`このプロバイダーのクォータデータがありません。`,"pws.accountQuotaUnavailable":`レート制限データを一時的に取得できません。前回の値がある場合はそれを表示します。`,"pws.selected":`選択中`,"pws.copyModelId":`ID をコピー`,"pws.modelCopied":`コピーしました!`,"pws.modelsAvailable":`{count} 件利用可能`,"pws.modelSearchPlaceholder":`モデルを絞り込み…`,"pws.modelsLoading":`モデルを読み込み中…`,"pws.modelsLoadFailed":`モデルを読み込めませんでした。`,"pws.modelsNeedsReauth":`ライブモデル検出が動作するには再ログインが必要です。今は設定済みモデルを表示しています。`,"pws.modelsConfiguredFallback":`設定済みモデルを表示中(ライブ検出は利用不可)。`,"pws.modelsTruncated":`最初の {shown} / {total} モデルを表示中。リストを絞り込んでください。`,"pws.retry":`再試行`,"pws.noModels":`このプロバイダーで検出されたモデルはありません。`,"pws.noModelMatch":`フィルタに一致するモデルがありません。`,"pws.adapterBaseRequired":`アダプターとベース URL は必須です。`,"pws.addAccount":`アカウントを追加`,"pws.addKey":`API キーを追加`,"pws.apiKeys":`API キー`,"pws.authMode":`認証モード`,"pws.availableAccounts":`利用可能なアカウント`,"pws.accountOrdinal":`アカウント {count}`,"pws.accountsLoading":`アカウントを読み込み中…`,"pws.accountsLoadFailed":`アカウントを読み込めませんでした。`,"pws.retryAccounts":`再試行`,"pws.noAccounts":`まだアカウントが接続されていません。`,"pws.cockpitImportDescription":`このデバイスから Cockpit Tools Antigravity JSON エクスポートをインポートします。ファイル内容は表示されません。`,"pws.cockpitImportFileLabel":`Cockpit Tools Antigravity JSON エクスポート`,"pws.cockpitImportChooseFile":`JSON ファイルを選択`,"pws.cockpitImporting":`インポート中…`,"pws.cockpitImportInvalid":`選択したファイルは有効な JSON エクスポートではないか、大きすぎます。`,"pws.cockpitImportFailed":`アカウントのインポートを完了できませんでした。`,"pws.cockpitImportComplete":`インポート完了: インポート {imported}、更新 {updated}、失敗 {failed}、未対応 {unsupported}。`,"pws.accountSwitching":`切り替え中…`,"pws.accountCurrent":`現在のアカウント`,"pws.defaultModelNone":`なし(プロバイダーのデフォルトを使用)`,"pws.discardSettings":`破棄`,"pws.jsonEditorDesc":`生のプロバイダー JSON 設定を編集します。変更はすぐに保存されます。`,"pws.jsonEditorTitle":`JSON エディタ — {name}`,"pws.jsonRestore":`復元`,"pws.jsonSave":`保存`,"pws.loggedInTitle":`ログイン済み`,"pws.notLoggedInTitle":`未ログイン`,"pws.note":`メモ`,"pws.allowPrivateNetwork":`ローカル/プライベートネットワークを許可`,"pws.liveModels":`プロバイダーからモデルを検出`,"pws.liveModelsDesc":`プロバイダーのライブモデルカタログを取得します。オフにすると設定済みの静的モデルのみを使用します。`,"pws.xaiResponsesOptIn":`Grok 4.5 と 4.6 で Responses API を使用`,"pws.xaiResponsesOptInDesc":`両モデルを openai-responses 経由でルーティングします。他の Grok モデルと tier 動作は変わりません。`,"pws.xaiResponsesOptInMixed":`一部のみ有効です。`,"pws.cursorTransport":`Cursor トランスポート`,"pws.cursorTransportHttp2":`HTTP/2(デフォルト)`,"pws.cursorTransportHttp1":`HTTP/1.1(プロキシ互換)`,"pws.cursorTransportDesc":`プロキシが Cursor の HTTP/2 ストリームを安定して転送できない場合は HTTP/1.1 を使用します。`,"pws.optionalPlaceholder":`任意`,"pws.providerId":`プロバイダー ID`,"pws.reauth":`再認証が必要`,"pws.reauthenticate":`再認証`,"pws.copyDoctor":`ocx doctor をコピー`,"pws.doctorCopied":`コピー済み`,"pws.healthCooldownHint":`クールダウンが終わるまで待ってください。まだこのアカウントをプローブしないでください。`,"pws.doctorCopyUnavailable":`クリップボードを利用できません`,"pws.healthLabel.rateLimited":`レート制限中`,"pws.healthLabel.quotaLimited":`クォータ制限中`,"pws.healthLabel.reauthRequired":`再認証が必要です`,"pws.healthLabel.refreshFailed":`更新に失敗しました`,"pws.healthLabel.metadataMismatch":`メタデータの不一致`,"pws.healthLabel.credentialConflict":`資格情報の競合`,"pws.healthSummary.rateLimited":`{provider} {account}: {until} までレート制限中です。それまでこのアカウントのルーティングは停止します。`,"pws.healthSummary.quotaLimited":`{provider} {account}: {until} までクォータ制限中です。それまでこのアカウントのルーティングは停止します。`,"pws.healthSummary.reauthRequired":`{provider} {account}: 再認証が必要です。`,"pws.healthSummary.credentialConflict":`{provider} {account}: 資格情報の競合があります。`,"pws.healthSummary.metadataMismatch":`{provider} {account}: メタデータが一致しません。`,"pws.healthSummary.staleCredentials":`{provider} {account}: 資格情報が不完全です。`,"pws.removeConfirm":`削除`,"pws.removeConfirmBody":`プロバイダー "{name}" を削除しますか? これは元に戻せません。`,"pws.removeDefaultConfirmBody":`既定のプロバイダー "{name}" を削除しますか? "{defaultProvider}" が既定のプロバイダーになります。この操作は元に戻せません。`,"pws.removeConfirmTitle":`プロバイダーを削除`,"pws.saveSettings":`保存`,"pws.pacingTitle":`リクエスト間隔調整`,"pws.pacingDesc":`このプロバイダーへの送信開始を均等に遅延します。ストリーミング応答は重複できます。`,"pws.pacingEnabled":`有効`,"pws.pacingRpm":`1分あたりのリクエスト数`,"pws.pacingRpmUnit":`RPM`,"pws.pacingDelay":`最小間隔 (ms)`,"pws.pacingSlowerWins":`より遅いプロバイダー制限が優先され、モデル設定は遅延を増やす場合のみ適用されます。`,"pws.pacingQueued":`待機中`,"pws.pacingNextSlot":`次のスロットまで`,"pws.pacingLastModel":`最後のモデル`,"pws.pacingNone":`なし`,"pws.pacingModelOverrides":`モデル別設定`,"pws.pacingModel":`モデル`,"pws.pacingAdd":`設定を追加`,"pws.pacingRemove":`削除`,"pws.pacingRemoveModel":`{model} のリクエスト間隔設定を削除`,"pws.pacingRuleRequired":`プロバイダー制限またはモデル別設定を追加してから有効にしてください。`,"pws.saving":`保存中…`,"pws.settingsSaved":`設定を保存しました。`,"pws.accountModeSaved":`アカウントモードを保存しました。`,"pws.accountModeFailed":`アカウントモードを切り替えられませんでした。`,"pws.accountModeConfirm":`OpenAI のアカウントモードを切り替えますか?実行中の会話はもう一方のモードのアカウントセットに再割り当てされ、クォータ使用量は新しいモードで計上されます。`,"pws.settingsUnsavedBar":`未保存の変更があります。`,"pws.unsavedLeaveBody":`未保存の変更があります。保存してから移動しますか?`,"pws.unsavedLeaveTitle":`未保存の変更`,"pws.attentionRequired":`要対応`,"pws.attentionAria":`{name}: {reason}`,"pws.missingCredentials":`資格情報が不足しています`,"pws.editJsonDesc":`生のプロキシ設定を JSON として編集`,"pws.updatesUnavailable":`プロバイダーの更新は利用できません。`,"pws.dashboard.title":`プロバイダー概要`,"pws.dashboard.subtitle":`すべてのモデルプロバイダーを一か所で管理します。`,"pws.dashboard.rateLimits":`レート制限`,"pws.capacity.estimate":`設定済み重みによるプール推定`,"pws.capacity.currentAccount":`現在の有効アカウント`,"pws.capacity.nextRecovery":`次の容量回復`,"pws.capacity.recoveryShare":`+{percent}% のプール容量`,"pws.capacity.incomplete":`対象範囲が不完全です: {excluded} 件を除外`,"pws.capacity.uncalibratedPlan":`未校正プランの {count} 件は基準シート重みで計上されるため、この推定値は控えめになる場合があります`,"pws.capacity.partial":`一部の期間の対象範囲が不完全です: {count} 件のアカウントでは表示中のすべての制限期間を取得できません`,"pws.capacity.windowPartial":`一部のみ`,"pws.capacity.windowPartialA11y":`{window}: アカウントの対象範囲が不完全です`,"pws.dashboard.recentlyUsed":`最近の使用`,"pws.dashboard.requests":`{count} リクエスト`,"pws.dashboard.checkedAgo":`{time} に確認`,"pws.dashboard.noQuota":`クォータデータなし`,"pws.dashboard.noUsage":`まだ使用量データはありません`,"pws.dashboard.noRateLimits":`まだレート制限データはありません`,"pws.allProviders":`プロバイダー概要`,"pws.enabledLabel":`有効`,"pws.testConnection":`接続テスト`,"pws.testing":`テスト中…`,"pws.connectionOk":`接続 OK`,"pws.connectionFailed":`接続失敗`,"pws.connectionNotApplicable":`対象外 — このプロバイダーは静的モデルカタログを使用します。`,"pws.editSettings":`設定を編集`,"pws.viewUsage":`詳細な使用量を表示`,"pws.allSystemsOk":`すべてのシステムが稼働中`,"pws.apiKeyConfigured":`API キー設定済み`,"pws.addApiKey":`API キーを追加`,"pws.loggedInAs":`{email} としてログイン中`,"pws.notLoggedIn":`未ログイン`,"pws.passthrough":`Codex パススルー`,"pws.notes":`メモ`,"pws.notePlaceholder":`このプロバイダーについてのメモを追加...`,"pws.noteSaved":`メモを保存しました`,"pws.authSummary":`認証`,"time.justNow":`たった今`,"time.notChecked":`未確認`,"time.minutesAgo":`{n}分前`,"time.hoursAgo":`{n}時間前`,"time.daysAgo":`{n}日前`,"modal.noMatch":`一致なし。`,"modal.oauthDefaultNote":`アカウントでログイン — API キー不要です。`,"modal.oauthComingSoon":`{label} の OAuth ログインは次回の更新で対応予定です。今は API キーをお使いください。`,"modal.oauthComingSoonShort":`このプロバイダーの OAuth ログインは次回の更新で対応予定です — 今は API キーをお使いください。`,"modal.useApiKeyInstead":`代わりに API キーを使用`,"modal.setupGuide":`セットアップガイド`,"modal.setupStep1Prefix":`にアクセスし`,"modal.setupDashboardLink":`{label} ダッシュボード`,"modal.setupStep1Suffix":`から API キーをコピー`,"modal.setupStep2":`下の API キー欄に貼り付け`,"modal.setupStep3":`プロバイダーを追加をクリック — モデルは自動検出されます`,"modal.namePlaceholder":`例: openrouter`,"modal.duplicateWarn":`プロバイダー "{name}" は既存で、上書きされます。`,"modal.forwardHintPrefix":`キー不要 — プロキシはあなたの`,"modal.forwardCredentials":`codex ログイン`,"modal.forwardHintSuffix":`資格情報をこのプロバイダーに転送します。`,"modal.localHint":`API キーは保存されません。Cursor の静的な公開モデルカタログを Codex に追加しますが、ライブの Cursor トランスポートとネイティブのファイル/シェル実行は監査されるまで無効のままです。`,"modal.getApiKey":`{label} の API キーを取得`,"modal.apiKey":`API キー`,"modal.apiKeyTransport":`API キーヘッダー`,"modal.apiKeyTransportNative":`x-api-key (Anthropic 標準)`,"modal.apiKeyTransportBearer":`Authorization: Bearer`,"modal.apiKeyPlaceholder":`sk-… (または $ENV_VAR)`,"modal.defaultModelPlaceholder":`例: gpt-5.5`,"modal.baseUrlPlaceholder":`https://...`,"modal.baseUrlPlaceholderError":`ベース URL に未解決の {placeholder} が含まれています。実際の値に置き換えてください。`,"modal.baseUrlPlaceholderHint":`プロバイダーを追加する前に、ベース URL の {placeholder} を実際の Account ID に置き換えてください。`,"modal.adding":`追加中…`,"modal.useOauthLogin":`← OAuth ログインを使用`,"nav.codexAuth":`Codex 認証`,"nav.codexSet":`Codex 設定`,"codexSet.tab.multiauth":`マルチ認証`,"codexSet.tab.prompt":`プロンプト`,"codexSet.prompt.title":`プロンプトレイヤー`,"codexSet.prompt.timing":`新しく開始したセッションから適用されます。実行中のセッションは現在の設定を保持します。`,"codexSet.prompt.staleRevision":`他の場所で設定が変更されたため、一覧を再読み込みしました。`,"codexSet.prompt.writeFailed":`変更を保存できませんでした。`,"codexSet.prompt.loadFailed":`プロンプトレイヤーを読み込めませんでした。`,"codexSet.prompt.repair":`修復`,"codexSet.prompt.repairFailed":`修復を完了できませんでした。`,"codexSet.drift.journalPresent":`前回の書き込みが完了していません。次の書き込み時に自動で復旧します。`,"codexSet.drift.projectionStale":`保存済みのレイヤーと config.toml の値が一致しません。修復するとレイヤーの内容で値を書き直します。`,"codexSet.drift.storeMissing":`レイヤーファイルがないのに config.toml に指示が残っています。修復するとバックアップを作成し、その内容を1つのレイヤーとして保持します。`,"codexSet.drift.ownedMalformed":`config.toml に生成された行が手動で変更されているため、安全に書き直せません。`,"codexSet.custom.adoptUnsupported":`{path} の {line} 行目の値が単一行の文字列ではないため、インポートできません。ここで管理するには手動で移動してください。`,"codexSet.prompt.unreadable":`Codex の設定ファイルは存在しますが読み取れないため、変更を拒否しました。`,"codexSet.layer.permissions":`権限`,"codexSet.layer.collaboration":`コラボレーションモード`,"codexSet.layer.environment":`環境コンテキスト`,"codexSet.layer.apps":`アプリ`,"codexSet.layer.skills":`スキル`,"codexSet.prompt.extensionsUnknown":`拡張機能は独自のレイヤーを追加できます。Codex が公開していないため、ここには表示できません。`,"codexSet.group.transition":`遷移通知`,"codexSet.group.transitionDesc":`状態を説明するのではなく変化を知らせる項目のため、セッションがリアルタイムに切り替わるかモデルが変わったときだけ現れます。`,"codexSet.custom.slotNote":`カスタムレイヤーはこの順序で連結され、1つのセクションになります。`,"codexSet.row.alwaysOn":`常に有効`,"codexSet.row.onChange":`変更時に送信`,"codexSet.row.featureGated":`[features] で設定`,"codexSet.row.openFeatures":`設定を開く`,"codexSet.dialog.setValue":`{value}(デフォルト {fallback})`,"codexSet.dialog.copyKey":`キーをコピー`,"codexSet.dialog.unknownLayer":`このビルドにはこのレイヤーの説明がありません。ダッシュボードより新しい Codex ランタイムのレイヤーです。`,"codexSet.custom.heading":`カスタムレイヤー`,"codexSet.custom.add":`+ レイヤーを追加`,"codexSet.custom.newTitle":`新しいレイヤー`,"codexSet.custom.editTitle":`レイヤーを編集`,"codexSet.custom.titleLabel":`タイトル`,"codexSet.custom.bodyLabel":`指示`,"codexSet.custom.bodySize":`{max} バイト中 {bytes} バイト`,"codexSet.custom.normalized":`タブを半角スペース4個に、改行コードを LF に変換しました。`,"codexSet.custom.titleRequired":`タイトルを入力してください。`,"codexSet.custom.titleTooLong":`タイトルは {count} 文字です。上限は {max} 文字です。`,"codexSet.custom.titleMultiline":`タイトルは1行で入力してください。`,"codexSet.custom.bodyTooLarge":`このレイヤーは {bytes} バイトです。上限は {max} バイトです。`,"codexSet.custom.composedTooLarge":`有効なレイヤーを合わせると {bytes} バイトとなり、上限を超えます。`,"codexSet.custom.invalidCharacter":`位置 {position} の制御文字は保存できません。`,"codexSet.custom.discardPrompt":`変更を破棄しますか?`,"codexSet.custom.keepEditing":`編集を続ける`,"codexSet.custom.delete":`{title} を削除`,"codexSet.custom.deleteConfirm":`このレイヤーを削除しますか?元に戻せません。`,"codexSet.custom.layerGone":`他の場所でそのレイヤーが削除されたため、エディターを閉じました。`,"codexSet.custom.deleteConfirmNamed":`“{title}” を削除しますか?元に戻せません。`,"codexSet.custom.moveUp":`{title} を上へ移動`,"codexSet.custom.prevLayer":`前のレイヤー`,"codexSet.custom.nextLayer":`次のレイヤー`,"codexSet.custom.navPosition":`{position} / {total}`,"codexSet.custom.moveDown":`{title} を下へ移動`,"codexSet.custom.limitReached":`カスタムレイヤーは最大 {max} 個まで保存できます。`,"codexSet.custom.notOwned":`developer_instructions は opencodex の外部で作成されたため、ここでは編集できません。レイヤーとして管理するにはインポートしてください。`,"codexSet.custom.adopt":`既存の指示をインポート`,"codexSet.custom.adoptConfirm":`レイヤーとしてインポート`,"codexSet.custom.adoptRefused":`既存の値をインポートできませんでした。`,"codexSet.custom.baseReplaced":`model_instructions_file が {path} に設定されているため、opencodex の外部で基本プロンプトが置き換えられています。`,"codexSet.lint.identity":`Codex が設定するものとは異なるアイデンティティを名乗っています。`,"codexSet.lint.foreignTool":`ツールはレジストリから提供されます。ここに名前を書いてもツールは作成されません。`,"codexSet.lint.placeholder":`指示にはテンプレートエンジンが適用されないため、このまま送信されます。`,"codexSet.lint.applyPatch":`apply_patch は指示ではなく、ツールレジストリで定義されます。`,"codexSet.lint.approvalVocab":`Codex は独自の承認用語を挿入するため、これと矛盾する可能性があります。`,"codexSet.lint.environment":`環境情報は後で生成されるため、これと矛盾する可能性があります。`,"codexSet.lint.size":`このレイヤーは 8 KB を超えています。保存はできますが、リクエストのたびにトークンを消費します。`,"codexSet.preset.blank":`空のレイヤー`,"codexSet.preset.concise.name":`簡潔な出力`,"codexSet.preset.concise.description":`短く答え、前置きと不要な書式を省きます。`,"codexSet.preset.concise.provenance":`Claude Code の簡潔さに関する指示をもとに翻案しました。独自の文言であり、複製ではありません。`,"codexSet.preset.planFirst.name":`編集前に計画`,"codexSet.preset.planFirst.description":`計画を示してから変更します。`,"codexSet.preset.planFirst.provenance":`Claude Code の計画重視の方針をもとに翻案しました。独自の文言であり、複製ではありません。`,"codexSet.preset.explainWhy.name":`理由を説明`,"codexSet.preset.explainWhy.description":`何をするかだけでなく、理由も説明します。`,"codexSet.preset.explainWhy.provenance":`Grok Build の確認スタイルをもとに翻案しました。独自の文言であり、複製ではありません。`,"codexSet.preset.testFirst.name":`テストを先に`,"codexSet.preset.testFirst.description":`修正前に、失敗するテストを作成します。`,"codexSet.preset.testFirst.provenance":`一般的なエージェントの実践をもとに翻案しました。独自の文言であり、複製ではありません。`,"codexSet.preset.korean.name":`韓国語で回答`,"codexSet.preset.korean.description":`リクエストの言語にかかわらず、韓国語で回答します。`,"codexSet.preset.korean.provenance":`よく要望される項目をもとに opencodex 向けに作成しました。独自の文言であり、複製ではありません。`,"codexSet.dialog.class":`種類`,"codexSet.dialog.key":`設定キー`,"codexSet.dialog.fileValue":`このファイルの値`,"codexSet.dialog.absentDefault":`未設定(デフォルト: {value})`,"codexSet.dialog.noRenderedText":`Codex は組み立て済みの組み込みレイヤー本文を公開していません。そのため、このダイアログには内容ではなくレイヤーの説明とキーを表示します。`,"codexSet.dialog.sourceText":`モデルに送られる原文`,"codexSet.dialog.sourceBytes":`{bytes} バイト`,"codexSet.dialog.notRendered":`確認したターンでは、このレイヤーは何も送信していません。各セクションは内容が変わったときだけ再送されるため、一度の確認では欠けて見えることがあります。`,"codexSet.dialog.emptySource":`{path} のファイルは存在しますが空のため、このレイヤーは何も送信しません。`,"codexSet.dialog.notExposed":`基本プロンプトは Codex が出力するメッセージ一覧の外を通るため、ここには表示できません。model_instructions_file で置き換えることは可能です。`,"codexSet.dialog.textUnavailable":`このマシンでは Codex のプロンプトを読み取れず、原文を表示できません。`,"codexSet.class.base":`基本指示`,"codexSet.class.config-toggle":`ここで切り替え可能`,"codexSet.class.feature-gated":`機能フラグ制御`,"codexSet.class.runtime-conditional":`ランタイム条件付き`,"codexSet.class.extension-unknown":`拡張レイヤー`,"codexSet.layer.base-instructions":`基本指示`,"codexSet.layer.model-switch":`モデル切り替え通知`,"codexSet.layer.personality":`パーソナリティ`,"codexSet.layer.context-window-guidance":`コンテキストウィンドウのガイダンス`,"codexSet.layer.realtime":`リアルタイム`,"codexSet.layer.agents-md":`AGENTS.md`,"codexSet.layer.environments-instructions":`実行環境`,"codexSet.layer.plugins":`プラグイン`,"codexSet.layer.tools":`ツール`,"codexSet.layer.multi-agent-mode":`マルチエージェントモード`,"codexSet.layer.git-attribution":`コミットの帰属表示`,"codexSet.about.base-instructions":`Codex 自体の指示です。リクエストに含まれ、無効にはできません。`,"codexSet.about.model-switch":`会話の途中でセッションのモデルが変わると追加されます。`,"codexSet.about.personality":`機能フラグで制御されるトーンと語調のガイダンスです。`,"codexSet.about.context-window-guidance":`機能フラグで制御される残りのコンテキスト予算のガイダンスです。`,"codexSet.about.realtime":`リアルタイムセッションに追加されます。`,"codexSet.about.agents-md":`プロジェクトの AGENTS.md ファイルです。このページはレイヤーを表示するだけで、プロジェクト文書は編集しません。`,"codexSet.about.permissions":`現在適用されているサンドボックスと承認設定を説明します。`,"codexSet.about.collaboration":`有効なコラボレーションモードを説明します。`,"codexSet.about.environment":`作業ディレクトリ、プラットフォーム、その他の環境情報です。`,"codexSet.about.environments-instructions":`機能フラグで制御される遅延実行環境向けのガイダンスです。`,"codexSet.about.apps":`接続済みアプリの使用方法です。`,"codexSet.about.plugins":`プラグインが選択されているか、プラグインが機能を提供すると追加されます。`,"codexSet.about.tools":`機能フラグで制御される遅延読み込みツールの説明です。`,"codexSet.about.skills":`利用可能なスキルの一覧です。`,"codexSet.about.multi-agent-mode":`機能フラグで制御されるサブエージェント向けの指示です。`,"codexSet.about.git-attribution":`モデルが書いたコミットに Co-authored-by: Codex トレーラーを、開いたプルリクエストに Generated with Codex. の行を追加させます。Codex がアカウントから取得するため、ここでも [features] でも変更できません。アカウント側で無効にすると、何も送らないのではなく逆の指示を送ります。`,"codexSet.condition.model-switch":`セッション中にモデルが変わった後のみ含まれます。`,"codexSet.condition.realtime":`リアルタイムセッションのみ含まれます。`,"codexSet.condition.agents-md":`作業ディレクトリ用のプロジェクト文書が見つかると含まれます。`,"codexSet.condition.plugins":`プラグインが選択されているか、プラグインが機能を提供すると含まれます。`,"codexSet.condition.git-attribution":`アカウントの帰属表示ポリシーで決まります。`,"codexSet.base.title":`ベースプロンプト`,"codexSet.base.prev":`前のオプション`,"codexSet.base.next":`次のオプション`,"codexSet.base.position":`{position} / {total}`,"codexSet.base.swipeHint":`左右にスワイプするか、矢印キーか矢印ボタンでオプションを切り替えます。新しく開始したセッションに適用されます。`,"codexSet.base.defaultTitle":`Codex 自身のベースプロンプト`,"codexSet.base.defaultBody":`デフォルトはここに保存されないため、編集も削除もできません。選ぶと設定から model_instructions_file が削除され、Codex は同梱のプロンプトを使います。`,"codexSet.base.variantTitle":`名前`,"codexSet.base.variantBody":`プロンプト`,"codexSet.base.replacesWarning":`Codex 自身のベースプロンプトに追記するのではなく、丸ごと置き換えます。ここに短く書けば、モデルへの指示もその分だけになります。`,"codexSet.base.use":`これを使う`,"codexSet.base.inUse":`使用中`,"codexSet.base.externalBlocked":`model_instructions_file はすでに {path} を指しており、opencodex が書いた値ではありません。自分で消してから選んでください。`,"nav.api":`API`,"nav.integrations":`連携`,"nav.openMenu":`メニューを開く`,"nav.closeMenu":`メニューを閉じる`,"integrations.subtitle":`クライアントを opencodex に接続し、認証情報の管理とクライアント設定の復元を行います。`,"integrations.tabsLabel":`連携画面`,"integrations.tab.overview":`概要`,"integrations.tab.keys":`API キー`,"integrations.tab.codex":`Codex`,"integrations.tab.claude":`Claude`,"integrations.tab.grok":`Grok Build`,"integrations.tab.opencode":`OpenCode`,"integrations.tab.pi":`Pi`,"integrations.tab.omp":`OMP`,"integrations.tab.hermes":`Hermes`,"integrations.tab.openclaw":`OpenClaw`,"integrations.tab.kimi":`Kimi Code`,"integrations.tab.gajae":`Gajae Code`,"integrations.tab.dsh":`DSH`,"integrations.tab.mcode":`MiniMax Code`,"integrations.tab.zcode":`ZCode`,"integrations.tab.prime":`Prime Agent`,"integrations.tab.aside":`Aside`,"integrations.codex.title":`Codex CLI`,"integrations.codex.body":`Codex の接続はプロキシサービスが管理します。opencodex を起動すると適用され、サービスを停止するとネイティブのルーティングに戻ります。`,"integrations.codex.openService":`サービス制御を開く`,"integrations.state.notInstalled":`未インストール`,"integrations.state.unknown":`確認中`,"integrations.detail.codexRouted":`Codex のリクエストはこのプロキシを経由します`,"integrations.detail.codexAbsent":`Codex はまだこのプロキシを経由していません`,"integrations.detail.keyCount":`キー {count} 個を発行済み`,"integrations.detail.keyNone":`発行済みのキーはありません`,"integrations.detail.keyChecking":`確認中…`,"integrations.detail.keyUnavailable":`キーの状態を取得できません`,"integrations.detail.claudeOff":`接続がオフです`,"integrations.detail.desktopCurrent":`Desktop はこのプロファイルで動作しています`,"integrations.detail.desktopStale":`適用後にプロファイルが変更されました`,"integrations.detail.desktopNotServed":`プロファイルはありますが Desktop は別のものを使用中です`,"integrations.detail.desktopAbsent":`適用されたプロファイルはありません`,"integrations.detail.desktopDesiredOff":`Claude Desktop 連携はオフです`,"integrations.detail.desktopDesiredOffCleanupPending":`Claude Desktop はまだゲートウェイを使用しています。クリーンアップ待ちです`,"integrations.detail.desktopDesiredOnNotApplied":`連携はオンですが、Desktop はゲートウェイプロファイルを使用していません`,"integrations.detail.desktopSelectedElsewhere":`Desktop は別のプロファイルを使用しています`,"integrations.detail.desktopProfileDrift":`選択された Desktop プロファイルが変更されました`,"integrations.detail.desktopObservedUnsafe":`選択された Desktop プロファイルは安全に変更できません`,"integrations.detail.desktopNotInstalled":`Claude Desktop の設定ライブラリがインストールされていません`,"integrations.dialog.desktop.title":`Claude Desktop 連携を無効にしますか?`,"integrations.dialog.desktop.changes":`{path} に opencodex 管理のゲートウェイプロファイルがある場合、Desktop は先に認証情報のない標準プロファイルを選択し、その後で古いプロファイルとバックアップを削除します。`,"integrations.dialog.desktop.breakage":`Claude Desktop は opencodex 経由のモデルではなく、標準の Claude に戻ります。`,"integrations.dialog.desktop.undo":`再度有効にすると、保存済みのモデル割り当てから opencodex プロファイルを再生成します。`,"integrations.dialog.desktop.restart":`Claude Desktop は起動時にのみこの設定を読み取ります。変更を反映するには完全に終了して再起動してください。`,"integrations.dialog.desktop.confirm":`無効にする`,"integrations.native.error.desktopUnsafeMetadata":`{path} の Claude Desktop メタデータを安全に読み取れなかったため、ライブラリは変更されませんでした。`,"integrations.native.error.desktopCleanupIncomplete":`Claude Desktop は標準モードを指していますが、古い opencodex 認証情報ファイルが残っています: {paths}。`,"integrations.native.msg.desktopDisabled":`Claude Desktop 連携を無効にしました。`,"integrations.native.msg.desktopEnabled":`Claude Desktop 連携を有効にしました。`,"integrations.detail.grokModels":`モデル {count} 個を接続済み`,"integrations.detail.grokAbsent":`設定に opencodex ブロックがありません`,"integrations.dialog.grok.title":`Grok Build 連携を解除しますか?`,"integrations.dialog.grok.changes":`{path} から、opencodex が印を付けたブロックだけを削除します。ブロック外に直接書いた内容はそのまま残します。`,"integrations.dialog.grok.breakage":`解除すると、Grok Build から opencodex のモデルエイリアスが消えます。xAI アカウントで使用していたモデルはそのままです。`,"integrations.dialog.grok.undo":`opencodex が loopback アドレスで実行中なら、再び有効にしたとき、現在利用できるモデル一覧からブロックを新しく書き込みます。`,"integrations.dialog.grok.confirm":`解除`,"integrations.native.msg.nonLoopbackRemoved":`Grok Build を自動登録できるのは、opencodex が loopback アドレスで実行中の場合だけです。loopback アドレスを指していた以前のブロックを削除しました。`,"integrations.native.msg.nonLoopbackRemovedNoop":`Grok Build を自動登録できるのは、opencodex が loopback アドレスで実行中の場合だけです。削除する以前のブロックはありませんでした。`,"integrations.native.msg.nonLoopbackSuperseded":`Grok Build を自動登録できるのは、opencodex が loopback アドレスで実行中の場合だけです。その間に別の場所から設定へ新しいブロックが書かれたため、現在ファイルにあるブロックはこのリクエストが作成したものではありません。`,"integrations.native.error.orphanedMarker":`{path} に opencodex の開始マーカーはありますが、終了マーカーがありません。ブロックの終端を特定できないため、ファイルは変更していません。`,"integrations.native.error.homeMismatch":`インストール済みサービスのホームと現在のホームが一致しないため、ファイルは変更していません。`,"integrations.native.error.notInstalled":`Grok Build がインストールされていないため、変更できません。`,"integrations.native.error.configBusy":`別の場所で設定を保存中のため、変更できませんでした。しばらくしてから再試行してください。`,"integrations.state.absent":`未適用`,"integrations.state.current":`適用済み`,"integrations.state.stale":`更新が必要`,"integrations.state.conflict":`競合`,"integrations.state.unsafe":`確認不可`,"integrations.summary.detected":`検出されたクライアント`,"integrations.summary.applied":`設定済みクライアント`,"integrations.summary.stale":`更新が必要`,"integrations.summary.lastChange":`最終変更`,"integrations.summary.disableAll":`すべて無効にする…`,"integrations.onboarding":`適用時は、先にバックアップを保存してから opencodex プロバイダーブロックを 1 つだけ書き込みます。無効化時はそのブロックだけを削除し、保存されたスナップショットから復元できます。`,"integrations.empty.title":`インストール済みのクライアントが検出されませんでした`,"integrations.empty.body":`対応クライアントをインストールしてから、ここに戻って opencodex を適用してください。`,"integrations.action.apply":`適用`,"integrations.action.disable":`無効にする`,"integrations.action.refresh":`更新`,"integrations.action.settings":`設定`,"integrations.action.manageKeys":`キーを管理`,"integrations.action.restore":`復元…`,"integrations.action.undo":`元に戻す`,"integrations.action.restorePoint":`この時点に復元…`,"integrations.action.snapshotExpired":`バックアップ期限切れ`,"integrations.rollback.title":`復元センター`,"integrations.rollback.empty":`適用履歴はまだありません`,"integrations.rollback.emptyBody":`書き込みが成功するたびに、変更前のスナップショットが先に保存されます。`,"integrations.catalog.title":`クライアント`,"integrations.rollback.older":`以前の操作`,"integrations.rollback.showMore":`さらに {n} 件表示`,"integrations.rollback.failed":`ロールバック履歴を読み込めませんでした。`,"integrations.restore.title":`このスナップショットを復元しますか?`,"integrations.restore.body":`現在のファイルを先にバックアップしてから、選択したスナップショットで置き換えます。`,"integrations.restore.driftTitle":`新しい編集が検出されました`,"integrations.restore.driftBody":`このスナップショット以降の変更をバックアップしてから、ファイルを置き換えます。`,"integrations.restore.confirm":`復元`,"integrations.restore.confirmDrift":`新しい編集をバックアップして復元`,"integrations.restore.pending":`復元中…`,"integrations.restore.manual":`自動復元に失敗しました: {reason}。{path} から手動で復元してください。`,"integrations.error.load":`連携状態を読み込めませんでした。`,"integrations.error.stale":`最新の更新に失敗しました。以下の値は古い可能性があります。`,"integrations.error.busy":`このクライアントに対する別の変更が実行中です。しばらくしてから再試行してください。`,"integrations.error.conflict":`opencodex の書き込み後に設定が変更されました。何も削除していません。`,"integrations.error.unsafe":`設定を安全に変更できません。`,"integrations.error.generic":`連携の変更に失敗しました。以前の状態は保持されています。`,"integrations.error.nonLoopback":`{client} は localhost のプロキシにしか接続できません。リモートバインドに必要な認証ヘッダーを置く場所が設定ファイルになく、手動で書いても同じです。トンネルやローカルフォワーダーで loopback 経路を用意してください。`,"integrations.status.installed":`インストール済み`,"integrations.status.notInstalled":`未インストール`,"integrations.status.appliedAt":`適用`,"integrations.status.backup":`バックアップ`,"integrations.status.lastRestore":`最終復元`,"integrations.status.unknown":`不明`,"integrations.bulk.title":`適用済みのクライアント連携を無効にしますか?`,"integrations.bulk.body":`opencodex が所有するブロックだけを削除します。各クライアントについて、変更前のスナップショットを保存します。`,"integrations.bulk.partial":`一部のクライアントを無効にできませんでした: {clients}`,"integrations.bulk.success":`適用済みのクライアント連携を無効にしました。`,"integrations.retention.degraded":`バックアップの整理が遅れています。古いバックアップがディスクに残っている可能性があります。`,"integrations.error.residual":`ファイルが中間状態のままの可能性があります: {message} {path} から復元してください。`,"integrations.error.recover":`{message} バックアップは {path} にあります。`,"integrations.kind.apply":`適用`,"integrations.kind.disable":`解除`,"integrations.kind.refresh":`更新`,"integrations.kind.restore":`復元`,"integrations.kind.overwrite":`上書き`,"integrations.dialog.overwrite.title":`この設定ファイルのブロックを置き換えますか?`,"integrations.dialog.overwrite.changesUnowned":`{path} で opencodex が必要とする位置を、opencodex が書いたのではないブロックが占めています。適用すると opencodex が書くブロックに置き換わります。`,"integrations.dialog.overwrite.changesForeign":`{path} の opencodex ブロックに加えた変更は破棄され、opencodex が書くブロックに置き換わります。`,"integrations.dialog.overwrite.breakage":`そのブロックが設定していた内容は効かなくなります。ファイルの他の部分はそのままです。`,"integrations.dialog.overwrite.undo":`先にスナップショットを保存するので、下のロールバック一覧に残り、元に戻せます。`,"integrations.dialog.overwrite.confirm":`置き換える`,"integrations.action.overwrite":`置き換える`,"integrations.semantics.opencode":`ディスクから直接起動した場合にのみ適用されます。ocx opencode の環境注入が優先されます。`,"integrations.semantics.pi":`新しいセッションから適用されます。`,"integrations.semantics.omp":`カタログを読み込むには OMP を再起動してください。`,"integrations.semantics.hermes":`新しいセッションから適用されます。`,"integrations.semantics.openclaw":`実行中のゲートウェイにすぐ適用されます。`,"integrations.semantics.kimi":`再起動するか /reload を実行すると適用されます(v2 はファイルを監視します)。`,"integrations.semantics.gajae":`新しいセッション、または /model を開いたときに適用されます。`,"integrations.semantics.dsh":`OpenCodex が管理するのは $DSH_HOME/settings.yaml 内の llm-pi-ai.providers.opencodex だけです。DSH はこのプロバイダーをホットリロードし、既定のモデルと deepseek-official は変更しません。現在はループバック専用で、実際の認証情報は書き込みません。`,"integrations.semantics.mcode":`custom_provider.opencodex のみを管理します。既定モデルと MiniMax ログインは変更しません。`,"integrations.semantics.zcode":`~/.zcode/v2/config.json の provider.opencodex のみを管理します。Z.ai ログインと他のプロバイダーは変更しません。変更後は ZCode を再起動してください。`,"integrations.semantics.prime":`Prime Agent の models.json 内の providers.opencodex のみを管理します。場所は ~/.prime/agent ですが、PRIME_AGENT_CODING_AGENT_DIR が設定されている場合はそちらが優先されます。他のプロバイダーとモデルオーバーライドは変更しません。新しいセッションから適用されます。`,"integrations.semantics.aside":`サインイン中のアカウントの Aside models.json 内の providers.opencodex のみを管理します。場所は ~/.aside/u/<アカウント> です。他のプロバイダーは変更しません。Aside は実行中にこのファイルを書き換えるため、適用後は Aside を完全に終了して再度開いてください。`,"codexAuth.mainAccount":`メインアカウント`,"codexAuth.logLabel":`ログラベル`,"codexAuth.codexApp":`Codex App`,"codexAuth.moreActions":`その他の操作を表示`,"codexAuth.copyId":`アカウント ID をコピー`,"codexAuth.appLogin":`アプリログイン`,"codexAuth.accountPool":`アカウントプール`,"codexAuth.accountModeTitle":`OpenAI アカウントモード`,"codexAuth.accountModePool":`プールモード`,"codexAuth.accountModePoolDesc":`メインログインと対象の追加アカウントがここでローテーションします。`,"codexAuth.accountModeDirect":`ダイレクトモード`,"codexAuth.accountModeDirectDesc":`リクエストはメインログインのみを使用します; 追加アカウントはプールモード用に保持されます。`,"codexAuth.openaiMissing":`組み込みの OpenAI プロバイダーが設定されていません。`,"codexAuth.openaiDisabled":`組み込みの OpenAI プロバイダーが無効です。`,"codexAuth.openaiUnavailableDesc":`OpenAI アカウントは引き続き利用できます。Codex リクエストをルーティングするにはプロバイダーを有効にしてください。`,"codexAuth.enableOpenai":`OpenAI を有効にする`,"codexAuth.enablingOpenai":`有効化中...`,"codexAuth.enableOpenaiFailed":`OpenAI プロバイダーを有効にできませんでした。`,"codexAuth.openaiPresetLoadFailed":`OpenAI プロバイダーのプリセットを読み込めませんでした。`,"codexAuth.openaiPresetUnavailable":`OpenAI プロバイダーのプリセットを利用できません。`,"codexAuth.openProviders":`プロバイダーを開く`,"codexAuth.add":`追加`,"codexAuth.sparkQuota":`Codex Spark 使用量`,"codexAuth.sparkQuotaHint":`アカウントカードに GPT-5.3-Codex-Spark の週次枠を表示します。対象が 1 モデルのみのため既定は非表示です。`,"codexAuth.sparkQuotaShown":`Codex Spark 使用量を表示しました`,"codexAuth.sparkQuotaHidden":`Codex Spark 使用量を非表示にしました`,"codexAuth.sparkQuotaFailed":`Codex Spark 使用量の設定を変更できませんでした`,"codexAuth.refreshQuota":`クォータを更新`,"codexAuth.refreshingQuota":`更新中...`,"codexAuth.quotaRefreshed":`クォータを更新しました`,"codexAuth.quotaRefreshFailed":`クォータの更新に失敗しました`,"codexAuth.pauseExhausted":`上限到達を一括停止`,"codexAuth.pausingExhausted":`クォータを確認中...`,"codexAuth.pauseExhaustedSucceeded":`上限に達したアカウントを停止しました: {count}`,"codexAuth.pauseExhaustedNone":`使用率 100% が確認されたアカウントはありません。`,"codexAuth.pauseExhaustedFailed":`上限到達アカウントの確認と停止に失敗しました。`,"codexAuth.noPool":`まだプールアカウントは追加されていません。`,"codexAuth.pause":`一時停止`,"codexAuth.resume":`再開`,"codexAuth.paused":`一時停止中`,"codexAuth.pauseSucceeded":`{email} を一時停止しました`,"codexAuth.resumeSucceeded":`{email} をアカウントプールに戻しました`,"codexAuth.pauseFailed":`{email} を一時停止できませんでした。変更はありません。`,"codexAuth.resumeFailed":`{email} を再開できませんでした。変更はありません。`,"codexAuth.pausedHint":`再開するまで、自動切り替え、再試行、クールダウン復旧、手動選択の対象外です。`,"codexAuth.pinned":`固定中`,"codexAuth.pinnedHint":`手動で選択したアカウントなので、これより高い選択順序が先に使われることはありません。固定はこのアカウントを使い切るか、別のアカウントを選ぶか、いずれかの選択順序を変更するまで続きます。`,"codexAuth.fiveHour":`5時間`,"codexAuth.weekly":`週`,"codexAuth.monthly":`30日`,"codexAuth.resets":`リセット`,"codexAuth.today":`今日`,"codexAuth.current":`現在`,"codexAuth.nextSession":`選択済み`,"codexAuth.poolPrepared":`プール準備済み`,"codexAuth.preparePoolTitle":`このアカウントをプールモード用に準備しますか?`,"codexAuth.preparePoolDesc":`ダイレクトリクエストはメインログインを使い続けます。このアカウントはプールモードが有効化された際の準備済みプール選択になります。`,"codexAuth.prepareForPool":`プール用に準備`,"codexAuth.poolPreparedToast":`{email} はプールモード用に準備されました`,"codexAuth.switchTitle":`アクティブアカウントを切り替えますか?`,"codexAuth.switchDesc":`すぐに反映されます。アカウントに紐付いた既存スレッドと処理中のリクエストは現在のアカウントを維持し、新規または未紐付けのリクエストは選択したアカウントの順序ティアを使います。同じ選択順序のアカウントは引き続き交代で使われます。`,"codexAuth.cacheWarning":`アカウント切り替えでプロンプトキャッシュはリセットされます。新規セッションは空のキャッシュで開始します。`,"codexAuth.setAsNext":`このアカウントを次に使う`,"codexAuth.cancel":`キャンセル`,"codexAuth.switchBack":`メインに戻しますか?`,"codexAuth.switchBackDesc":`すぐに反映されます。アカウントに紐付いた既存スレッドと処理中のリクエストは現在のアカウントを維持し、新規または未紐付けのリクエストはアプリログインアカウントの順序ティアを使います。同じ選択順序のアカウントは引き続き交代で使われます。`,"codexAuth.autoSwitch":`使用量ベースのプロアクティブ切り替え`,"codexAuth.autoSwitchQuotaDesc":`クォータ: 使用率が {threshold}% 以上になると、既に紐付いたタスクを含む次のリクエストが、使用率の低い適格アカウントへ移る場合があります。Go/Free は 30 日枠のみを使用します。`,"codexAuth.autoSwitchQuotaOffDesc":`使用量ベースのプロアクティブ切り替えはオフです。新規/未紐付けタスクの割り当てと障害回復は引き続き適用されます。`,"codexAuth.autoSwitchRoundRobinDesc":`ラウンドロビン割り当てはこのしきい値を使用せず、新規/未紐付けタスクを引き続きローテーションします。`,"codexAuth.autoSwitchFillFirstDesc":`フィルファースト: {threshold}% は新規/未紐付けタスクの使い切り基準です。正常な紐付け済みタスクはアカウントを維持します。`,"codexAuth.autoSwitchFillFirstOffDesc":`フィルファーストには新規/未紐付けタスクの使用量基準がありません。クールダウン、再認証、障害回復では引き続きルーティングが変わる場合があります。`,"codexAuth.failureRecoveryNote":`障害回復は別です。出力前の 429/402 拒否、クールダウン、再認証、除外、または設定済みの一時障害フェイルオーバーにより、別の適格アカウントが選ばれる場合があります。`,"codexAuth.autoSwitchThreshold":`使用量しきい値`,"codexAuth.autoSwitchThresholdAria":`使用量しきい値(パーセント)`,"codexAuth.autoSwitchThresholdInc":`使用量しきい値を上げる`,"codexAuth.autoSwitchThresholdDec":`使用量しきい値を下げる`,"codexAuth.autoSwitchLoadFailed":`使用量ベースの切り替え設定を読み込めませんでした。`,"codexAuth.autoSwitchThresholdInvalid":`1 から 100 までの整数を入力してください`,"codexAuth.autoSwitchUpdated":`使用量ベースのプロアクティブ切り替え設定を更新しました`,"codexAuth.autoSwitchUpdateFailed":`使用量ベースの切り替え更新を確認できませんでした。最後に確認された値を表示しています。`,"codexAuth.requestUserInput":`Default モードで入力を求める`,"codexAuth.requestUserInputDesc":`Default モードのセッションで Codex が一時停止し、request_user_input ツールで質問できるようにします。`,"codexAuth.requestUserInputUpdated":`機能フラグを更新しました - 新しいセッションから適用されます。`,"codexAuth.requestUserInputUpdatedRestart":`機能フラグを更新しました - 新しいセッションから適用されます。Codex アプリを再起動してください。`,"codexAuth.requestUserInputUpdateFailed":`機能フラグを更新できませんでした。変更はありません。`,"codexAuth.requestUserInputLoadFailed":`config.toml から機能フラグを読み込めませんでした。`,"codexAuth.accountPickerTitle":`モデルピッカーで使用する Codex アカウントを指定`,"codexAuth.accountPickerOffDesc":`有効にすると、通常の GPT ピッカー項目がアカウントセレクターごとの項目に置き換わり、ログアウトせずに会話で使うアカウントを明示的に選べます。無効にしてもアカウントは削除されません。`,"codexAuth.accountPickerOnDesc":`各セレクターは保存済みアカウント 1 つに対応する公開ラベルです。選択した会話はそのアカウントに固定され、Pool のローテーションやフォールバックは行われず、現在の Pool アカウントも変更されません。`,"codexAuth.accountPickerCompatibility":`組み込みの Codex App ログインには専用セレクターがあります。生成されたマップでは通常 main と呼ばれ、必要に応じて main-2 のような衝突を避けるサフィックスが使われます。追加アカウントには安定したプライバシー保護ラベルが割り当てられ、カスタムセレクター名は保持されます。既存の会話と保存済みのモデル選択は引き続きルーティングされます。無効にすると生成された項目だけが非表示になり、セレクターと完全一致ルートは保持されます。通常の GPT モデル ID は従来どおり Pool または Direct で動作します。`,"codexAuth.accountPickerUpdated":`アカウント指定を更新しました。`,"codexAuth.accountPickerUpdateFailed":`アカウント指定を更新できませんでした。最後に確認された設定を表示しています。`,"codexAuth.accountPickerLoadFailed":`アカウント指定の設定を読み込めませんでした。`,"codexAuth.accountPickerRefreshFailed":`この設定を更新できませんでした。最後に確認された値を引き続き表示しています。`,"codexAuth.advancedSettings":`詳細設定`,"codexAuth.advancedSettingsAria":`高度な Codex 認証設定を表示または非表示`,"codexAuth.catalogRefreshPending":`変更は保存されましたが、Codex モデルカタログの更新が保留中です。ocx sync を実行して再試行してください。`,"anthropicPool.title":`Claude アカウントプール(実験的)`,"anthropicPool.enabledDesc":`429 時にアカウントをクールダウンしてフェイルオーバーします。新規セッションは{window}の使用率が {threshold}% 未満のアカウントを優先します。`,"anthropicPool.enabledNoProactiveDesc":`429 時にアカウントをクールダウンしてフェイルオーバーします。しきい値 0 では使用量に基づく事前切り替えは無効ですが、新規セッション選択と 429 復旧では引き続き {window} ウィンドウを使用します。`,"anthropicPool.disabledDesc":`アクティブな Claude アカウントのみを使用します。実験的ルーティングを受け入れる場合のみ有効にしてください。`,"anthropicPool.experimentalWarning":`実験的で十分に検証されていません。自動的な複数アカウント回転に見える行為は Anthropic により制限される可能性があります。同一組織はクォータを共有することがあり、その場合プールしても効果がありません。リスクを理解していない場合はオフのままにしてください。`,"anthropicPool.needTwoAccounts":`プールを有効にする前に、Claude OAuth アカウントを 2 つ以上追加してください。`,"anthropicPool.threshold":`新規セッションの使用率しきい値`,"anthropicPool.thresholdAria":`新規セッションの使用率しきい値(パーセント)`,"anthropicPool.thresholdHelp":`0 はクォータに基づく選択を無効にします(アフィニティ + アクティブアカウントのみ)。デフォルト 80。`,"anthropicPool.thresholdInvalid":`0 から 100 までの整数を入力してください`,"anthropicPool.loadFailed":`Claude プール設定を読み込めませんでした。`,"anthropicPool.saveFailed":`Claude プール設定を保存できませんでした。`,"anthropicPool.on":`オン`,"anthropicPool.off":`オフ`,"accountPool.strategy":`ローテーション戦略`,"accountPool.strategyDesc":`OpenCodex が新規/未紐付けタスクへアカウントを割り当てる方法です。`,"accountPool.strategyQuota":`クォータ`,"accountPool.strategyRoundRobin":`ラウンドロビン`,"accountPool.strategyFillFirst":`フィルファースト`,"accountPool.strategyHintQuota":`クォータ戦略は使用量しきい値を超えると、既存タスクの次のリクエストも別アカウントへ再紐付けできます。`,"accountPool.strategyHintRoundRobin":`ラウンドロビンは有効な紐付けがないタスクだけをローテーションし、使用量しきい値は通常のローテーションを変えません。`,"accountPool.strategyHintFillFirst":`フィルファーストはしきい値を未紐付けタスクの使い切り基準として使用し、正常な紐付け済みタスクは親和性を維持します。`,"accountPool.unboundDefinition":`新規/未紐付けタスクとは、現在のアカウント紐付けがないリクエストです。既存の表示中タスクも、プロキシまたは親和性のリセット後は未紐付けになる場合があります。`,"accountPool.stickyLimit":`ローテーション前の新規/未紐付け割り当て数`,"accountPool.stickyLimitAria":`ローテーション前の新規/未紐付け割り当て数`,"accountPool.stickyLimitInc":`スティッキー上限を上げる`,"accountPool.stickyLimitDec":`スティッキー上限を下げる`,"accountPool.stickyLimitHelp":`次へ進む前に、この回数の新規/未紐付けタスクを選択アカウントへ割り当てます。カウンターは上流の成功後ではなく、タスクを紐付けた時点で増えます。`,"accountPool.stickyLimitInvalid":`1 から 100 までの整数を入力してください`,"accountPool.strategyLoadFailed":`ローテーション戦略を読み込めませんでした。`,"accountPool.strategyUpdateFailed":`ローテーション戦略を保存できませんでした。`,"accountPool.quotaWindow":`クォータ集計ウィンドウ`,"accountPool.quotaWindowDesc":`クォータに基づく新規セッション選択、フィルファーストのしきい値判定、対象となる 429 代替先で使うキャッシュ済み使用量バーを指定します。`,"accountPool.quotaWindowFiveHour":`5 時間バー`,"accountPool.quotaWindowWeekly":`週間バー`,"accountPool.quotaWindowMaxUtilization":`高い方のバー`,"accountPool.quotaWindowHint":`週間バーでは、他に対象アカウントが残る間だけ 5 時間バーを使い切ったアカウントをスキップし、残らない場合はそれらへフォールバックします。同点では 5 時間使用量が少ない方を優先します。アカウントごとの週間バーはプロバイダーページで取得した後にのみ判明します。`,"accountPool.quotaWindowInert":`使用量バーを評価するのはクォータ、またはしきい値が 0 を超えるフィルファーストだけです。現在のローテーション戦略では、この設定は何も変えません。`,"accountPool.priority":`選択順序`,"accountPool.priorityAria":`このアカウントの選択順序`,"accountPool.priorityHint":`数値が大きいほど先に使われます。上位のアカウントがすべて使い切られるか利用できなくなったときにのみ、より小さい数値へ移ります。`,"accountPool.priorityFirst":`最初`,"accountPool.priorityEarlier":`早め`,"accountPool.priorityNormal":`標準`,"accountPool.priorityLater":`遅め`,"accountPool.priorityLast":`最後`,"accountPool.priorityOption":`{name}({value})`,"accountPool.priorityCustom":`カスタム`,"accountPool.priorityUpdated":`{email} の選択順序を更新しました`,"accountPool.priorityUpdateFailed":`{email} の選択順序を保存できませんでした。最後に確認された値を表示しています。`,"codexAuth.switched":`次のリクエストでは {email} を使用します`,"codexAuth.loadFailed":`Codex アカウント設定を読み込めませんでした。`,"codexAuth.switchFailed":`アカウントを切り替えられませんでした。以前の選択はそのままです。`,"codexAuth.removeConfirm":`{id} を削除しますか?`,"codexAuth.removeFailed":`アカウントを削除できませんでした。何も変更されていません。`,"codexAuth.addTitle":`Codex アカウントを追加`,"codexAuth.addIdLabel":`アカウント ID (スラッグ)`,"codexAuth.addIdPlaceholder":`codex-work、codex-alt、team...`,"codexAuth.resetCreditsAria":`{count} 個のリセットクレジット`,"codexAuth.addJsonLabel":`auth.json の内容`,"codexAuth.addHelp":`別のマシンの ~/.codex/auth.json からコピー、または codex-auth export を使用。`,"codexAuth.importBtn":`インポート`,"codexAuth.importInvalidJson":`無効な JSON です`,"codexAuth.importMissingTokens":`JSON に access_token または refresh_token がありません`,"codexAuth.importMissingId":`アカウント ID は必須です`,"codexAuth.accountAdded":`アカウントをプールに追加しました`,"codexAuth.addPickDesc":`別の ChatGPT アカウントでログインしてプールに追加します。`,"codexAuth.oauthLogin":`OAuth ログイン`,"codexAuth.oauthDesc":`ブラウザで ChatGPT ログインを開きます`,"codexAuth.deviceLogin":`デバイスコードでログイン`,"codexAuth.deviceDesc":`ヘッドレスやリモートのプロキシ向け。別の端末で短いコードを入力します`,"codexAuth.importAuthJson":`auth.json をインポート`,"codexAuth.importAuthJsonDesc":`別の Codex インストールまたは codex-auth export から`,"codexAuth.back":`戻る`,"codexAuth.oauthAlreadyInProgress":`ログインは既に進行中です。ブラウザで完了してください。`,"codexAuth.oauthWaiting":`ブラウザで ChatGPT ログインが完了するのを待機中...`,"codexAuth.oauthSubmittingCode":`コードを送信中…`,"codexAuth.oauthCodeSubmitted":`コードを送信しました — ログイン完了を待っています…`,"codexAuth.oauthStatusRetrying":`ログイン状態の確認中にネットワークまたはプロキシ エラーが発生しました — 再試行中…`,"codexAuth.oauthCancelled":`ログインはキャンセルされました。`,"codexAuth.loginFailed":`ログインに失敗しました`,"codexAuth.needsReauth":`再ログイン`,"codexAuth.reauthenticate":`再認証`,"codexAuth.tokenExpired":`トークンが期限切れ — このアカウントを再認証してください`,"codexAuth.mainTokenExpired":`トークンが期限切れ — Codex アプリログインから再度サインインしてください`,"codexAuth.emailCollision":`このアカウントはメインの Codex ログインと一致します。別のアカウントを使用してください。`,"codexAuth.resetCreditsTitle":`リセットクレジット`,"codexAuth.resetCreditsAvailable":`{count} 個のリセットクレジットが利用可能です。`,"codexAuth.resetCreditsDesc":`各クレジットは現在の時間別・週間使用量上限を即座にリセットします。`,"codexAuth.noResetCredits":`リセットクレジットはありません。`,"codexAuth.earnCreditsHint":`クレジットは毎月および紹介プログラム経由で獲得できます。`,"codexAuth.creditsExpireNote":`クレジットは獲得から 30 日で失効します。`,"codexAuth.useOneCredit":`1 クレジットを使用`,"codexAuth.confirmResetTitle":`リセットクレジットを使用しますか?`,"codexAuth.confirmResetDesc":`現在のレート制限を即座にリセットします。残り {count} クレジットです。`,"codexAuth.irreversible":`この操作は元に戻せません。`,"codexAuth.useCredit":`クレジットを使用`,"codexAuth.redeeming":`リセット中...`,"codexAuth.resetSuccess":`レート制限をリセットしました! 残り {remaining} クレジット。`,"codexAuth.resetSuccessGeneric":`レート制限をリセットしました!`,"codexAuth.resetAlreadyRedeemed":`このクレジットは既に引き換え済みです。クレジットは変わりません。`,"codexAuth.resetNothingToReset":`今リセットが必要なレート制限枠はありません。`,"codexAuth.resetNoCredit":`利用可能なリセットクレジットはありません。`,"codexAuth.resetError":`リセットクレジットの引き換えに失敗しました。もう一度お試しください。`,"codexAuth.fifoNote":`最も古いクレジットが先に使用されます。`,"codexAuth.confirmWhichCredit":`{date} のクレジットが使用されます。`,"codexAuth.creditNext":`次に使用`,"codexAuth.creditLabel":`クレジット #{n}`,"codexAuth.creditNextBadge":`次`,"codexAuth.creditGranted":`付与 {date}`,"codexAuth.creditExpires":`失効 {date} (残り {days}日)`,"api.title":`API アクセス`,"api.subtitle":`生成した API キーで外部アプリから opencodex プロキシに接続します。認証は {authHeader} ヘッダーで行い、エンドポイントごとに受け付けるヘッダーは下の表のとおりです。`,"api.endpointNote":`ベース URL を OpenAI 互換クライアントで使ってください。Responses と Chat Completions は /v1 配下で公開されます。`,"api.endpointsTitle":`エンドポイント`,"api.baseUrl":`ベース URL`,"api.responsesEndpoint":`Responses API`,"api.chatCompletionsEndpoint":`Chat Completions API`,"api.messagesEndpoint":`Messages API`,"api.modelsEndpoint":`Models API`,"api.authTitle":`認証`,"api.authBaseUrlNote":`クライアントにはベース URL を設定し、下のプロトコル別エンドポイントを選んでください。`,"api.authLoopback":`ループバック (127.0.0.1 または ::1) は認証を省略します。リモートでは生成した ocx_ キーまたは OPENCODEX_API_AUTH_TOKEN が必要です。`,"api.modelsTitle":`外部モデルカタログ`,"api.modelsCount":`{count} 件が利用可能`,"api.modelsSearch":`モデルを検索`,"api.modelsSubtitle":`これらの ID を /v1/models と選択したプロトコルで使用してください。`,"api.modelsLoading":`モデルを読み込み中…`,"api.modelsLoadFailed":`外部モデルカタログを読み込めませんでした。`,"api.modelsEmpty":`外部から呼び出せるモデルはまだありません。`,"api.modelsNoMatch":`「{query}」に一致するモデルはありません。`,"api.colModel":`モデル`,"api.colSource":`ソース`,"api.colProtocols":`プロトコル`,"api.copyModelId":`ID をコピー`,"api.modelCopied":`コピーしました`,"api.testModel":`テスト`,"api.testingModel":`テスト中…`,"api.testSucceeded":`OK`,"api.testFailed":`失敗`,"api.protocolResponses":`Responses`,"api.protocolChatCompletions":`Chat Completions`,"api.protocolMessages":`Messages`,"api.sourceNative":`ChatGPT プール`,"api.sourceCombo":`コンボ`,"api.sourceCustom":`カスタム`,"api.usageResponsesTitle":`Responses の例`,"api.usageChatTitle":`Chat Completions の例`,"api.usageMessagesTitle":`Messages の例`,"api.newKeyTitle":`新しいキーを作成しました`,"api.newKeyNote":`今すぐこのキーをコピーしてください — 再表示されません。`,"api.copy":`コピー`,"api.copied":`コピーしました`,"api.dismiss":`閉じる`,"api.generateTitle":`キーを生成`,"api.keyNamePlaceholder":`キー名(任意)`,"api.generate":`生成`,"api.generating":`作成中…`,"api.activeKeys":`アクティブなキー ({count})`,"api.activeKeysLoading":`有効なキー`,"api.noKeys":`まだ API キーがありません。上で生成してください。`,"api.workspace.sections":`API セクション`,"api.section.keys":`キー`,"api.section.connect":`接続`,"api.section.endpoints":`エンドポイント`,"api.section.models":`モデル`,"api.section.examples":`例`,"api.workspace.details":`APIキーの詳細`,"api.workspace.keyDetails":`キーの詳細`,"api.workspace.keyPrefix":`キーのプレフィックス`,"api.workspace.deleteKey":`キーを削除`,"api.workspace.deleteConfirm":`このキーを削除しますか?この操作は元に戻せません。`,"api.workspace.usageExamples":`使用例`,"api.copyUrlHint":`クリックして URL をコピー`,"api.urlCopied":`URL をコピーしました`,"api.copyExampleHint":`クリックして例をコピー`,"api.exampleCopied":`例をコピーしました`,"api.colName":`名前`,"api.colKey":`キー`,"api.colCreated":`作成日`,"api.confirm":`確認`,"api.deleteAria":`API キーを削除`,"api.usageSampleInput":`こんにちは、世界!`,"api.clientConfig.title":`クライアント設定`,"api.clientConfig.rowsLabel":`クライアントを接続`,"api.clientConfig.details":`詳細`,"api.clientConfig.detailsAria":`{client} 設定の詳細`,"api.clientConfig.copyAria":`{client} 設定をコピー`,"api.clientConfig.downloadAria":`{client} 設定をダウンロード`,"api.clientConfig.rowMeta":`{destination} · モデル {count} 件`,"api.clientConfig.rowError":`{client} の設定を生成できませんでした。`,"api.clientConfig.copiedAnnounceClient":`{client} の設定をクリップボードにコピーしました。`,"api.clientConfig.clientOpencode":`OpenCode`,"api.clientConfig.clientPi":`Pi`,"api.clientConfig.clientOmp":`OMP`,"api.clientConfig.clientHermes":`Hermes`,"api.clientConfig.clientOpenclaw":`OpenClaw`,"api.clientConfig.clientKimi":`Kimi Code`,"api.clientConfig.clientGajae":`Gajae Code`,"api.clientConfig.clientDsh":`DeepSeek Harness (DSH)`,"api.clientConfig.clientMcode":`MiniMax Code`,"api.clientConfig.clientZcode":`ZCode`,"api.clientConfig.clientPrime":`Prime Agent`,"api.clientConfig.clientAside":`Aside`,"api.clientConfig.copy":`設定をコピー`,"api.clientConfig.download":`ダウンロード`,"api.clientConfig.loading":`クライアント設定を生成中…`,"api.clientConfig.jsonLabel":`{client} 設定`,"api.clientConfig.destination":`配置先ファイル`,"api.clientConfig.envHint":`起動前にキーを設定`,"api.clientConfig.mergeWarning":`配置先ファイルにマージしてください。置き換えると既存のプロバイダーや MCP 設定が失われます。`,"api.clientConfig.modelCount":`{count} 件のモデルを書き出しました`,"api.clientConfig.missingLimits":`{total} 件中 {count} 件のモデルにコンテキスト上限がないため、クライアント側の既定値が使われます。`,"api.clientConfig.noKeyYet":`{env} に対応するキーがまだありません。ループバック外で使う前に上でキーを発行してください。`,"api.clientConfig.loadFailed":`モデル一覧を読み取れなかったため、クライアント設定を生成できませんでした。`,"api.clientConfig.copiedAnnounce":`クライアント設定をクリップボードにコピーしました。`,"api.clientConfig.copyFailed":`クライアント設定をコピーできませんでした。`,"api.clientConfig.downloadedAnnounce":`{filename} をダウンロードしました。まだ何も変わっていません。{destination} に自分でマージしてください。`,"api.clientConfig.whereDisclosure":`このファイルの置き場所`,"api.clientConfig.whereBody":`上のパスはグローバル設定の場所です。作業ディレクトリのプロジェクト設定ファイルが優先され、キーは設定に書かれた環境変数から読み込まれ、このファイルには保存されません。`,"api.keysLoadFailed":`APIキーを読み込めませんでした。`,"api.createFailed":`APIキーを作成できませんでした。`,"api.deleteFailed":`APIキーを削除できませんでした。`,"api.auth.endpoint":`エンドポイント`,"api.auth.required":`必須`,"api.auth.accepted":`利用可`,"api.auth.rejected":`不可`,"api.auth.testProtocol":`{protocol} をテスト`,"api.auth.testNeedsFreshKey":`認証付きテストを実行するには、キーを新しく作成し、一度だけ表示される値を画面に残したままにしてください。`,"api.key.name":`キー名`,"api.key.rename":`名前を変更`,"api.key.saveName":`名前を保存`,"api.key.renaming":`保存中…`,"api.key.renameFailed":`名前を変更できませんでした。入力内容はそのまま残しています。`,"api.key.deleting":`削除中…`,"api.rotation.title":`キーのローテーション`,"api.rotation.description":`短い移行期間だけ現在のキーを有効にしたまま、置き換え用キーを発行します。`,"api.rotation.start":`ローテーションを開始`,"api.rotation.starting":`開始中…`,"api.rotation.pending":`ローテーションは保留中です。確定前にクライアントを更新して動作を確認してください。`,"api.rotation.expires":`移行期間の終了:`,"api.rotation.secretOnce":`置き換え用キー — 表示は一度だけです。閉じる前にコピーしてください。`,"api.rotation.commit":`ローテーションを確定`,"api.rotation.abort":`ローテーションを中止`,"api.rotation.failed":`操作を完了できませんでした。更新してから再試行してください。`,"api.rotation.startFailed":`キーのローテーションを開始できませんでした。`,"api.key.copyFailed":`キーをコピーできませんでした。このパネルを閉じる前に手動で選択してコピーしてください。`,"api.attribution.title":`キー別の使用状況`,"api.attribution.requests7d":`直近 7 日のリクエスト`,"api.attribution.totalRequests":`集計済みリクエスト総数`,"api.attribution.totalRequestsAvailable":`利用可能な履歴のリクエスト`,"api.attribution.sinceAvailable":`利用可能な集計開始日`,"api.attribution.lastUsed":`最終使用`,"api.attribution.since":`集計開始`,"api.attribution.neverUsed":`集計開始以降は未使用`,"api.attribution.unavailable":`使用状況なし`,"api.attribution.unavailableDetail":`まだ集計された使用状況がありません。集計開始前のリクエストは遡って割り当てられません。`,"api.attribution.ambiguous":`2 つのキーが同じ ID を共有しているため、どちらの使用状況か判別できません。設定ファイルでキーごとに一意の ID を指定してください。`,"api.attribution.railAmbiguous":`ID 重複`,"claude.subtitle":`Claude Code 内で GPT、Gemini などのモデルを使用します。`,"claude.pageTitle":`Claude Code`,"claude.workspace.settings":`設定`,"claude.enabledLabel":`Claude 接続`,"claude.enabledHint":`オフにすると Claude Code はこのプロキシを使用できません。`,"claude.authMode":`認証モード`,"claude.authModeHint":`サブスクリプションは Claude アカウントが必要、プロキシは Anthropic アカウント不要で動作します`,"claude.authModeSubscription":`サブスクリプション(Claude アカウント)`,"claude.authModeProxy":`プロキシ(アカウント不要)`,"claude.authModeAuto":`自動 (Claude 認証を検出)`,"claude.effectiveMode.label":`次回起動時に適用`,"claude.effectiveMode.manual":`手動: {mode}`,"claude.effectiveMode.autoPresent":`自動: サブスクリプション — {source} で Claude 認証を検出`,"claude.effectiveMode.autoAbsent":`自動: プロキシモード — Claude 認証が見つかりません`,"claude.effectiveMode.autoUnknown":`自動: サブスクリプション — 認証を確認できませんでした`,"claude.effectiveMode.admissionKey":`このプロキシの API キーは引き続き送信されます。`,"claude.authSource.claude-json-oauth":`Claude アカウント`,"claude.authSource.claude-credentials-file":`認証情報ファイル`,"claude.authSource.macos-keychain":`macOS キーチェーン`,"claude.authSource.exported-env":`環境変数`,"claude.authSource.unknown":`検出された認証情報`,"claude.systemEnv":`自動接続`,"claude.systemEnvDesc":`オンにすると、任意のターミナルで claude を実行すると自動的にプロキシ経由になります。`,"claude.systemEnvUnsupported":`自動接続は macOS でのみ利用できます。このシステムでは {cmd} で Claude を起動してください。`,"claude.systemEnvWarn":`⚠ これを有効化するにはターミナルアプリを完全に終了して再起動する必要があります。推奨されません。`,"claude.fastMode":`高速モード(OpenAI)`,"claude.fastModeDesc":`OpenAI モデルの service_tier を制御します。オン = 優先(高速)。オフ = デフォルト。自動 = パススルー(クライアントが決定)。`,"claude.fastAuto":`自動`,"claude.fastOn":`オン`,"claude.fastOff":`オフ`,"claude.autoContext":`大きなコンテキストを自動で使用`,"claude.autoContextDesc":`1M マーキングがどこまで及ぶかを制御します。オン: 圧縮しきい値を収められるウィンドウを持つモデルに大型コンテキスト行を付けます。オフ: 真の 1M モデルのみに付けます。`,"claude.autoContextInert":`設定ファイルにレガシーのコンテキストサイズ値(maxContextTokens)が存在するため無効です。再び有効化するにはそれを削除してください。`,"claude.autoCompactWindow":`自動要約ポイント`,"claude.autoCompactDefault":`{value}(デフォルト)`,"claude.autoCompactWindowDesc":`チャットがこのポイントに達すると古いメッセージが要約されます。各モデル自身の上限を超えることはないので、200k モデルは影響を受けません。`,"claude.autoCompactWindowWarn":`これを変更すると GPT モデルが壊れる可能性があります — モデルの実際の上限より高く設定すると、要約が働く前にチャットがエラーになります。`,"claude.injectAgents":`サブエージェントを自動登録`,"claude.injectAgentsDesc":`サブエージェントタブで選んだモデル(と現在のデフォルトモデル)をディスパッチ可能な Claude Code エージェント(ocx-*)として登録します。次回セッションから適用されます。`,"claude.webSearchSidecar":`ウェブ検索サイドカーの上書き`,"claude.webSearchSidecarHint":`Claude Code リクエストのメインウェブ検索サイドカーを上書きします。`,"claude.visionSidecar":`ビジョンサイドカーの上書き`,"claude.visionSidecarHint":`Claude Code リクエストのメインビジョンサイドカーを上書きします。`,"claude.useMainSetting":`メイン設定を使用`,"claude.sidecarModelPlaceholder":`メイン設定のモデル`,"claude.quickstart":`はじめる`,"claude.quickstartHint":`{cmd} はプロキシ経由で Claude Code を開きます。あなたの claude.ai ログインはそのまま有効です。`,"claude.manualEnv":`手動セットアップ(高度)`,"claude.smallFastModel":`バックグラウンドヘルパーモデル`,"claude.smallFastModelHint":`チャットの要約やトピック検出のようなバックグラウンド作業に Claude Code が使うモデルです。haiku サブエージェントエイリアスもこれを使います。空 = Claude デフォルト(Haiku)。`,"claude.smallFastModelAccurateHint":`チャットの要約やトピック検出など、Claude Code がバックグラウンド処理に使うモデルです。サブエージェントの haiku エイリアスもこのモデルを使います。`,"claude.smallFastModelUnsetOption":`Claude Code に選択させる(ネイティブモデル)`,"claude.smallFastModelNativeWarning":`未設定の場合、OpenCodex はヘルパーモデルの上書きを設定しません。Claude Code がネイティブの Sonnet モデルを使用し、ネイティブプロバイダーで料金が発生する可能性があります。`,"claude.slotUnset":`Claude デフォルトを使用`,"claude.modelMap":`モデルの傍受`,"claude.modelMapHint":`特定モデルへのリクエストを傍受し、選んだモデルに再ルーティングします。デフォルトは空 — ルールを追加するまで何も起きません。`,"claude.mapFrom":`元のモデル(例: claude-sonnet-4-5)`,"claude.mapTo":`差し替え先(例: gemini/gemini-3-pro)`,"claude.addMapping":`ルールを追加`,"claude.removeMapping":`ルールを削除`,"claude.aliases":`利用可能なモデル`,"claude.aliasesHint":`Claude Code の /model メニューに表示されるモデル。`,"claude.aliasProviderOther":`その他`,"claude.loading":`読み込み中…`,"claude.loadFail":`Claude 設定の読み込みに失敗しました`,"claude.saved":`保存しました。`,"claude.saveFailed":`保存に失敗しました`,"claude.networkError":`ネットワークエラー — プロキシは起動していますか?`,"claude.toggleAria":`Claude 接続を切り替え`,"claude.none":`なし`,"claude.tabsLabel":`Claude クライアント`,"claude.tabCode":`Code`,"claude.tabDesktop":`Desktop`,"claudeDesktop.title":`Claude Desktop`,"claudeDesktop.subtitle":`各 Claude モデルファミリーをポート {port} の利用可能なモデルへルーティングします。`,"claudeDesktop.importJson":`JSON をインポート`,"claudeDesktop.exportJson":`JSON をエクスポート`,"claudeDesktop.loading":`Claude Desktop プロファイルを読み込み中…`,"claudeDesktop.loadFail":`Claude Desktop プロファイルの読み込みに失敗しました。`,"claudeDesktop.retry":`再試行`,"claudeDesktop.saveFailed":`Claude Desktop プロファイルの保存に失敗しました。`,"claudeDesktop.applyFailed":`プロファイルは保存されましたが、適用できませんでした。`,"claudeDesktop.updateFailed":`Claude Desktop の更新に失敗しました。`,"claudeDesktop.savedApplied":`プロファイルを保存し、Claude Desktop に適用しました。`,"claudeDesktop.appliedMarkerUnsaved":`Claude Desktop への適用は完了しましたが、適用マーカーを保存できませんでした。再度適用するまで、下の保存済み/適用済み表示が実際と異なる場合があります。`,"claudeDesktop.savedAppliedAnnounce":`Claude Desktop プロファイルを保存して適用しました。`,"claudeDesktop.saved":`プロファイルを保存しました。`,"claudeDesktop.savedAnnounce":`Claude Desktop プロファイルを保存しました。`,"claudeDesktop.exported":`プロファイルを JSON としてエクスポートしました。`,"claudeDesktop.importExpected":`バージョン 1 の Claude Desktop プロファイルが必要です。`,"claudeDesktop.importReady":`JSON をインポートしました。ドラフトを確認して保存・適用してください。`,"claudeDesktop.importedAnnounce":`プロファイル JSON をインポートしました。未保存の変更を確認できます。`,"claudeDesktop.importInvalid":`選択されたファイルは有効なプロファイルではありません。`,"claudeDesktop.importFailed":`インポートに失敗しました。{error}`,"claudeDesktop.moved":`{route} を {family} に移動しました。`,"claudeDesktop.unsaved":`未保存の変更`,"claudeDesktop.upToDate":`プロファイルは最新です`,"claudeDesktop.saving":`保存中…`,"claudeDesktop.applying":`適用中…`,"claudeDesktop.saveApply":`保存して適用`,"claudeDesktop.emptyTitle":`利用可能なモデルがありません`,"claudeDesktop.emptyHint":`プロバイダーを追加または有効化してから、Claude Desktop ルートを割り当ててください。`,"claudeDesktop.assignmentsLabel":`Claude モデルファミリーの割り当て`,"claudeDesktop.family.opus":`Opus`,"claudeDesktop.family.fable":`Fable`,"claudeDesktop.family.sonnet":`Sonnet`,"claudeDesktop.family.haiku":`Haiku`,"claudeDesktop.modelCountOne":`{count} モデル`,"claudeDesktop.modelCountMany":`{count} モデル`,"claudeDesktop.chooseDefault":`デフォルトを選択`,"claudeDesktop.temporaryDefault":`一時的なデフォルト`,"claudeDesktop.laneEmpty":`ここにモデルをドロップするか、移動コントロールを使用してください。`,"claudeDesktop.laneNoMatch":`検索に一致するモデルはこのファミリーにありません。`,"nav.grok":`Grok`,"grok.title":`Grok Build`,"grok.subtitle":`opencodex が Grok 設定に登録したモデルです。`,"grok.loading":`Grok の状態を読み込み中…`,"grok.loadFail":`Grok 設定を読み取れませんでした。`,"grok.notConfiguredTitle":`Grok Build が未設定です`,"grok.notConfiguredHint":`Grok をインストールしてプロキシを再起動すると、opencodex が管理ブロックを次の場所に書き込みます:`,"grok.endpoint":`エンドポイント`,"grok.colModel":`モデル`,"grok.colAlias":`Grok エイリアス`,"grok.colContext":`コンテキスト`,"grok.groupNative":`ネイティブモデル`,"grok.groupRouted":`ルーティングモデル`,"grok.enabledCount":`{total} 件中 {on} 件を登録`,"grok.saved":`選択を保存しました。`,"grok.savedApplied":`選択を保存し、Grok 設定に反映しました。`,"grok.saveFailed":`Grok の選択を保存できませんでした。`,"grok.applyFailed":`選択は保存しましたが、Grok 設定を更新できませんでした。`,"grok.applySkipped":`選択は保存しましたが、Grok 設定は変更されませんでした。`,"grok.saveApply":`保存して適用`,"grok.saving":`保存中…`,"grok.applying":`適用中…`,"grok.unsaved":`未保存の変更`,"grok.upToDate":`選択は最新です`,"grok.toggleModel":`{id} を Grok に登録`,"claudeDesktop.available":`利用可能`,"claudeDesktop.defaultBadge":`既定`,"claudeDesktop.supports1m":`1M`,"claudeDesktop.unavailable":`利用不可`,"claudeDesktop.contextM":`{n}M コンテキスト`,"claudeDesktop.contextK":`{n}k コンテキスト`,"claudeDesktop.contextUnknown":`コンテキスト不明`,"claudeDesktop.alias":`エイリアス`,"claudeDesktop.useAsDefault":`{family} のデフォルトに設定`,"claudeDesktop.moveTo":`移動先`,"claudeDesktop.move":`移動`,"claudeDesktop.status.applied":`Desktop に適用済み`,"claudeDesktop.status.stale":`設定が古くなっています — 再適用してください`,"claudeDesktop.status.notApplied":`未適用`,"claudeDesktop.status.notActiveProfile":`Desktop は別のプロファイルを使用中 — 再適用してください`,"claudeDesktop.status.disabled":`Claude Desktop 連携はオフです。有効にした後、Desktop を完全に終了して再起動してください。`,"claudeDesktop.enableApply":`有効にして適用`,"claudeDesktop.health.lastRequest":`最終リクエスト`,"claudeDesktop.health.stats":`{count} リクエスト / {errors} エラー`,"claudeDesktop.effort.supported":`effort`,"claudeDesktop.effort.displayOnly":`effort (表示のみ)`,"cws.loading":`コンボを読み込み中…`,"cws.loadFailed":`コンボを読み込めませんでした。`,"cws.saveFailed":`コンボを保存できませんでした。`,"cws.removeFailed":`コンボを削除できませんでした。`,"cws.saved":`コンボを保存しました。`,"cws.created":`{model} を作成しました。`,"cws.removed":`combo/{id} を削除しました。`,"cws.add":`コンボを追加`,"cws.addTitle":`コンボを追加`,"cws.addSubtitle":`プロバイダー全体にファンアウトする仮想モデルを作成します。クライアントは combo/ をリクエストします。`,"cws.create":`コンボを作成`,"cws.railAria":`コンボ一覧`,"cws.searchPlaceholder":`コンボやターゲットを検索…`,"cws.noSearchResults":`検索に一致するコンボがありません。`,"cws.group.failover":`フェイルオーバー`,"cws.group.roundRobin":`ラウンドロビン`,"cws.group.other":`その他の戦略`,"cws.targetCount":`{count} ターゲット`,"cws.targetCountOne":`1 ターゲット`,"cws.overviewTitle":`コンボ`,"cws.overviewBlurb":`プロバイダー/モデルターゲット間を、フェイルオーバー、ラウンドロビン、重み付きランダム、最少使用、最短クォータリセットで振り分ける仮想モデル。`,"cws.count.total":`合計`,"cws.count.failover":`フェイルオーバー`,"cws.count.roundRobin":`ラウンドロビン`,"cws.count.other":`その他`,"cws.howTitle":`仕組み`,"cws.howBody":`Codex に combo/ を要求します。OpenCodex はターゲットを選び、再試行可能な上流の失敗時のみホップします。利用可能なターゲットが残っていない場合、グローバルなデフォルトプロバイダーを使わずにフェイルクローズします。`,"cws.attentionTitle":`要対応`,"cws.attention.empty":`ターゲットが設定されていません`,"cws.attention.few":`ターゲットが 1 つだけ — フェイルオーバーのホップ先がありません`,"cws.attention.catalogOmitted":`モデルカタログにありません — メンバー能力が不完全または非互換です(context window / メタデータ不足、または modality 交差が空)。エイリアス指定のルーティングは動作します`,"cws.attention.allTargetsExhausted":`有効なすべてのターゲットがクォータを使い切っています`,"cws.emptyTitle":`最初のコンボを作成`,"cws.empty.createDesc":`仮想モデルに名前を付け、2 つ以上のバックエンドをつなぎます。`,"cws.backToAll":`すべてのコンボに戻る`,"cws.allCombos":`すべてのコンボ`,"cws.copyModel":`ID をコピー`,"cws.copied":`コピーしました`,"cws.renamed":`{from} を {to} に変更しました。`,"cws.tabsLabel":`コンボ詳細セクション`,"cws.tab.config":`設定`,"cws.tab.about":`概要`,"cws.strategy":`ストラテジー`,"cws.strategy.failover":`フェイルオーバー`,"cws.strategy.roundRobin":`ラウンドロビン`,"cws.strategy.random":`ランダム`,"cws.strategy.leastUsed":`最少使用`,"cws.strategy.resetWindow":`リセットウィンドウ`,"cws.strategy.failoverHint":`ターゲットを順に試します。最初が再試行可能なエラー(レート制限、障害、サブスクリプションゲート)で失敗した場合、次へホップします。`,"cws.strategy.roundRobinHint":`重みで決定論的にトラフィックを分散します。選んだターゲットを成功リクエストのバッチ分保持し、次へ進みます。`,"cws.strategy.randomHint":`リクエストごとに適格なターゲットを 1 つ抽選します。確率は重みに比例し、リクエスト間でスティッキネスはありません。`,"cws.strategy.leastUsedHint":`各リクエストを、成功回数が最も少ない適格なターゲットへ振ります。カウントはプロキシの再起動でリセットされます。`,"cws.strategy.resetWindowHint":`クォータのウィンドウが最も早くリセットされる適格なターゲットを優先します。クォータデータがない場合は設定順に従います。`,"cws.field.id":`コンボ ID`,"cws.field.idHint":`クライアントは {model} をリクエストします`,"cws.field.idInternalHint":`コンボの内部 ID。作成後も変更できます。`,"cws.field.idHintEdit":`ID を変更するとコンボの名前が変更されます。クライアントは {model} をリクエストします。`,"cws.field.alias":`公開モデル名`,"cws.field.aliasPlaceholder":`deepseek-v4-flash または vendor/model`,"cws.field.aliasHint":`任意。プレフィックスなしの名前、vendor/model のようなカスタムプレフィックスを指定するか、空欄のままにすると combo/ を使用します。`,"cws.field.nativeAlias":`ネイティブ OpenAI エイリアス`,"cws.field.nativeAliasHint":`このコンボがサポート対象の修飾なし OpenAI ネイティブモデル ID を所有します。アカウント修飾・プロバイダー修飾ルートは別のままです。`,"cws.field.displayName":`表示名`,"cws.field.displayNameHint":`モデルピッカーに表示するラベルです。ネイティブ OpenAI エイリアスでは必須です。`,"cws.field.stickyLimit":`ローテーション前の固定成功数`,"cws.field.stickyLimitHint":`重み付きセレクタが進む前に、選んだターゲットをこの回数の成功リクエスト分保持します。`,"cws.field.defaultEffort":`デフォルトの推論`,"cws.field.defaultEffortNone":`なし(ターゲットのデフォルト)`,"cws.field.defaultEffortHint":`クライアントが推論負荷を省略した場合のみ使用されます。選択肢は選択ターゲットが広告する負荷の交差です。`,"cws.capability.imageInputUnavailable":`選択した全ターゲットが画像入力に対応すると有効になります。`,"cws.capability.imageInputHint":`全ターゲットが画像対応なら既定でオン。オフにするとテキストのみ。`,"cws.capability.imageInput":`画像 / マルチモーダル`,"cws.capability.adaptiveEffort":`適応的な推論レベル`,"cws.capability.adaptiveEffortHint":`オフ: 推論レベルを持たない対象があると、コンボ全体のセレクターが消えます。オン: その対象はそのまま使え、セレクターには残りの対象で共通するレベルが表示されます。`,"cws.capabilities":`能力`,"cws.field.defaultEffortUnsupported":`この負荷はターゲット共通の階段にありません — リクエスト時に無視またはスナップされます。`,"cws.field.defaultEffortUnsupportedOption":`交差に含まれない`,"cws.targets":`ターゲット`,"cws.targets.failoverHint":`順序が重要 — 最初がプライマリです。`,"cws.targets.roundRobinHint":`重みが決定論的な相対選択を制御し、順序がローテーションリングの同点を解消します。`,"cws.targets.randomHint":`重みが各抽選の確率を制御します。順序は影響しません。`,"cws.targets.leastUsedHint":`順序は同じ使用回数のターゲット間の同点のみを解消します。`,"cws.targets.resetWindowHint":`順序はクォータデータが欠落または同点のときに適用されます。`,"cws.target.provider":`プロバイダー`,"cws.target.model":`モデル`,"cws.target.weight":`重み`,"cws.target.pickProvider":`プロバイダーを選択…`,"cws.target.pickProviderFirst":`最初にプロバイダーを選択…`,"cws.target.pickModel":`モデルを選択…`,"cws.target.noModels":`このプロバイダーにモデルはありません`,"cws.target.modelPlaceholder":`モデル ID`,"cws.target.add":`ターゲットを追加`,"cws.target.drag":`ドラッグで並べ替え`,"cws.target.moveUp":`上へ移動`,"cws.target.moveDown":`下へ移動`,"cws.quota.available":`利用可能`,"cws.quota.exhausted":`クォータを使い切りました`,"cws.quota.unknown":`クォータ不明`,"cws.quota.allExhausted":`有効なすべてのターゲットがクォータを使い切っています。別のターゲットを選ぶか、クォータの回復を待ってください。`,"cws.aboutTitle":`ランタイム`,"cws.aboutBody":`失敗したターゲットは Retry-After を尊重して短時間クールダウンします。無効またはコンテキストエラーはホップしません。各ターゲットは自身の能力に推論負荷を適応させます; 枯渇したコンボはフェイルクローズします。ログと使用量は順序付きの物理試行と試行ごとの使用量を保持します。`,"cws.removeConfirmTitle":`{model} を削除しますか?`,"cws.removeConfirmDesc":`これで仮想モデルが設定と Codex カタログから削除されます。プロバイダーは削除されません。`,"cws.unsavedTitle":`未保存の変更`,"cws.unsavedDesc":`このコンボへの編集を破棄して続行しますか?`,"cws.keepEditing":`編集を続ける`,"cws.err.missingId":`コンボ ID は必須です。`,"cws.err.invalidId":`ID は英字または数字で始まり、英数字、ドット、アンダースコア、ハイフンのみ使用できます(最大 64)。`,"cws.err.duplicateId":`この ID のコンボはすでに存在します。`,"cws.err.invalidAlias":`エイリアスには英字、数字、ドット、アンダースコア、ハイフンを使用でき、スラッシュ区切りは 1 つまでです。`,"cws.err.aliasReservedNamespace":`エイリアスに予約済みの "combo/" 名前空間は使用できません。`,"cws.err.aliasNativeFamily":`OpenAI ネイティブファミリー(gpt-*、o1-*、o3-*、o4-*、codex-*)のプレフィックスなしエイリアスは使用できません。`,"cws.err.unsupportedNativeAlias":`ネイティブエイリアスには、現在サポートされている OpenAI の bare model id を指定してください。`,"cws.err.missingNativeAliasDisplayName":`ネイティブエイリアスには表示名が必要です。`,"cws.err.invalidDisplayName":`表示名は 128 文字以内で、制御文字を含めることはできません。`,"cws.err.duplicateAlias":`別のコンボがすでにこのエイリアスを使用しています。`,"cws.err.noTargets":`少なくとも 1 つのターゲットを追加してください。`,"cws.err.incompleteTarget":`各ターゲットにはプロバイダーとモデルが必要です。`,"cws.target.disabled":`{name}(無効)`,"cws.err.reservedNamespace":`combo という物理プロバイダーは、コンボ作成前に名前を変更する必要があります。`,"cws.err.providerCollision":`コンボ ID が設定されたプロバイダー名と衝突しています。`,"cws.err.unknownProvider":`各ターゲットは設定済みプロバイダーを使用する必要があります。`,"cws.err.duplicateTarget":`同じプロバイダー/モデルターゲットは一度しか使用できません。`,"cws.err.invalidStickyLimit":`固定成功数は 1 から 100 の整数にしてください。`,"cws.err.invalidWeight":`各ラウンドロビン重みは 1 から 10000 の整数にしてください。`,"cws.err.noEnabledTarget":`少なくとも 1 つのターゲットは有効なプロバイダーを使用する必要があります。`,"prov.editAlias":`Edit alias`,"prov.aliasPrompt":`Display name (leave empty to clear)`,"prov.aliasSaved":`Alias saved`,"prov.aliasSaveFailed":`Could not save alias`,"prov.accountId":`ID`,"models.customAdd":`Add custom model`,"models.customAddTitle":`Add custom model — {provider}`,"models.customEditTitle":`Edit custom model — {provider}`,"models.customAdded":`Custom model added`,"models.customUpdated":`Custom model updated`,"models.customDeleted":`Custom model deleted`,"models.customSaveFailed":`Failed to save custom model`,"models.customSaving":`Saving…`,"models.customAddBtn":`Add`,"models.customEditBtn":`Update`,"models.customEdit":`Edit`,"models.customDelete":`Delete`,"models.customDeleteConfirm":`Delete the {name} model?`,"models.customBadge":`Custom`,"models.customSummary":`{count} custom`,"models.customFieldModelId":`Model ID (endpoint slug)`,"models.customFieldModelIdPlaceholder":`e.g. qwen4-max-preview`,"models.customFieldDisplayName":`Display name (optional)`,"models.customFieldDisplayNamePlaceholder":`e.g. Qwen 4 Max Preview`,"models.customFieldContext":`Context window`,"models.customFieldModalities":`Input modalities`,"models.customFieldReasoning":`推論努力`,"models.customFieldReasoningOverride":`推論努力を上書き`,"models.reasoningEffort.none":`なし`,"models.reasoningEffort.minimal":`最小`,"models.reasoningEffort.low":`低`,"models.reasoningEffort.medium":`中`,"models.reasoningEffort.high":`高`,"models.reasoningEffort.xhigh":`非常に高`,"models.reasoningEffort.max":`最大`,"models.tipProvider":`Provider`,"models.tipContext":`Context`,"models.tipModalities":`Modalities`,"models.tipStatus":`Status`,"models.tipActive":`Active`,"models.tipDisabled":`Disabled`,"pws.estimatedCost":`Estimated cost`,"pws.costDisclaimer":`API list-price estimate, not an actual charge.`,"pws.modelBreakdown":`Model breakdown`,"pws.col.model":`Model`,"pws.col.cost":`Est. cost`,"pws.col.tokens":`Tokens`,"pws.col.requests":`Req.`,"pws.col.share":`Share`,"pws.tokenInput":`Input`,"pws.tokenOutput":`Output`,"dash.injectionManage":`設定を開く`,"sub.settings":`設定`,"sub.sections":`サブエージェントのセクション`,"sub.delegation.model":`最初に呼ぶモデル`,"sub.delegation.modelHint":`Codex が作業を任せるとき、最初に呼ぶモデルです。上のおすすめが呼べる候補で、ここで選んだものがその中の第一候補になります。`,"dash.syncModelsHint":`接続済みのプロバイダーをもとに Codex のモデルカタログを書き直します。`,"dash.syncRun":`今すぐ同期`,"lab.title":`Compatibility Lab`,"lab.subtitle":`Read-only compatibility verdict matrix from lab projection evidence.`,"lab.loadFailed":`Could not load compatibility lab data`,"lab.projectionUnavailable":`Lab projection is not available. Run conformance or live probes first.`,"lab.projectionIncompatible":`Lab projection schema is incompatible. Rebuild the projection.`,"lab.statusTitle":`Projection status`,"lab.matrixTitle":`Compatibility matrix`,"lab.verdictsTitle":`Verdict records`,"lab.filter.layer":`Evidence layer`,"lab.filter.verdict":`Verdict`,"lab.filter.subject":`Subject ID`,"lab.filter.all":`All`,"lab.col.subject":`Subject`,"lab.col.layer":`Layer`,"lab.col.suite":`Suite`,"lab.col.verdict":`Verdict`,"lab.col.asOf":`As of`,"lab.col.protocol":`Protocol conformance`,"lab.col.live":`Live route compatibility`,"lab.col.task":`Task effectiveness`,"lab.empty":`No compatibility verdicts in the projection yet.`,"lab.subjectKind":`Kind`,"lab.observationCount":`Observations`,"lab.eventCount":`Events`,"lab.verdictCount":`Verdicts`,"lab.subjectCount":`Subjects`,"lab.builtAt":`Built`,"lab.loading":`Loading compatibility evidence…`,"lab.loadMore":`Load more`,"lab.detailTitle":`Verdict detail`,"lab.detailClose":`Close`,"lab.detailSubject":`Subject`,"lab.detailObservations":`Observations`,"lab.detailEvents":`Contributing events`,"lab.detailArtifacts":`Artifact metadata`,"lab.production.title":`観測された本番トラフィック`,"lab.production.notVerification":`ラボ検証ではありません`,"lab.production.attempts":`試行`,"lab.production.successes":`成功`,"lab.production.routeErrors":`ルートエラー`,"lab.production.lastObserved":`最終観測`,"lab.detailLoadFailed":`Could not load verdict detail`,"lab.refresh":`Refresh`,"lab.verdict.UNKNOWN":`Unknown`,"lab.verdict.CLAIMED":`Claimed`,"lab.verdict.PROBED":`Probed`,"lab.verdict.VERIFIED":`Verified`,"lab.verdict.DEGRADED":`Degraded`,"lab.verdict.BLOCKED":`Blocked`,"lab.verdict.UNSUPPORTED":`Unsupported`,"lab.layer.protocol_conformance":`Protocol conformance`,"lab.layer.live_route_compatibility":`Live route compatibility`,"lab.layer.task_effectiveness":`Task effectiveness`,"dash.visionAdvanced":`詳細設定`,"dash.visionMaxDescriptions":`1 ターンあたりの最大説明数`,"dash.visionMaxDescriptionsInvalid":`正の整数を入力してください。`,"dash.visionTimeout":`タイムアウト`,"dash.visionTimeoutInvalid":`{min} から {max} ミリ秒の整数を入力してください。`,"dash.visionAdvancedPopover":`詳細なビジョン設定`,"models.newPolicyGlobal":`新しいモデルを無効で追加`,"models.newPolicyProvider":`新しいモデルのポリシー`,"models.newPolicy_inherit":`継承`,"models.newPolicy_off":`オフ`,"models.newPolicy_on":`オン`,"models.newBadge":`新着`,"models.newCount":`新着 {count} 件、オフ`,"models.aliases":`エイリアス`,"models.aliasesTable":`エイリアス一覧`,"models.aliasPrompt":`プロバイダーのエイリアス(空にすると解除)`,"models.modelAliasPrompt":`モデルのエイリアス(空にすると解除)`,"models.aliasSaved":`エイリアスを保存しました`,"models.aliasConflict":`このエイリアスは既存の名前と競合します`,"models.editProviderAlias":`プロバイダーのエイリアスを編集`,"models.editModelAlias":`モデルのエイリアスを編集`,"models.useDefaultAliases":`既定のエイリアスを使う`,"models.useDefaultAliasesGlobal":`既定のエイリアスを全体で使う`,"models.aliasAuto":`自動`,"models.aliasUser":`ユーザー`,"models.aliasStale":`古い`,"connection.discovering":`Discovering local and shared targets…`,"connection.machineUnavailable":`The local machine plane is unavailable. Shared requests were not redirected locally.`,"connection.disconnect":`Disconnect from hub`,"connection.disconnectConfirm":`Disconnect this machine from the hub and restart it in standalone mode?`,"connection.pairing.title":`Connect this dashboard to the hub`,"connection.pairing.body":`Paste the one-time pairing code created on the hub.`,"connection.pairing.relayWarning":`This code is exchanged through the fixed hub relay. The relay cannot be redirected to another host.`,"connection.pairing.code":`One-time pairing code`,"connection.pairing.submit":`Connect`,"connection.pairing.submitting":`Connecting…`,"connection.pairing.error":`The pairing code was refused or expired. The code was left in place so you can check it.`,"connection.machine.title":`This machine`,"connection.machine.shimHealthy":`Codex shim is healthy.`,"connection.machine.shimNeedsAttention":`Codex shim needs attention.`,"connection.machine.repairShim":`Repair shim`,"connection.machine.removeShim":`Remove shim`,"connection.clients.title":`Connected clients`,"connection.clients.none":`No client status available`,"connection.clients.sync":`Sync now`,"connection.clients.syncing":`Syncing…`,"connection.sessionLogout":`リモートセッションからログアウト`,"connection.sessionLoggingOut":`リモートセッションからログアウト中…`,"connection.sessionLogoutFailed":`リモートセッションからログアウトできませんでした。現在のセッションは維持されています。`,"usage.source.connected":`Source: hub usage`,"usage.source.local":`Source: local usage.jsonl`,"usage.scope.label":`Usage scope`,"usage.scope.machine":`This machine`,"usage.scope.hub":`Hub-wide`,"usage.hubOffline":`Hub usage is unavailable. Local usage was not substituted.`,"integrations.tab.cursor":`Cursor`,"integrations.detail.cursorSeen":`Cursor から最近このプロキシへのリクエストがありました`,"integrations.detail.cursorNeverSeen":`Private Inference はインストール済みですが、まだリクエストはありません`,"integrations.detail.cursorAbsent":`Cursor Private Inference が見つかりません`,"integrations.cursor.title":`Cursor`,"integrations.cursor.intro":`Cursor Private Inference はエージェントをローカルで実行し、loopback 経由で opencodex と通信します。通常版の Cursor では利用できません。バックエンドがカスタムエンドポイントを呼び出すため、公開 HTTPS URL が必要です。このページから Cursor への書き込みは行いません。以下の値を自分で Cursor に貼り付けてください。`,"integrations.cursor.loading":`Cursor の状態を読み込み中…`,"integrations.cursor.unavailable":`プロキシから Cursor の状態を読み取れませんでした。`,"integrations.cursor.detection":`インストール済みのビルド`,"integrations.cursor.privateInference":`Cursor Private Inference`,"integrations.cursor.regular":`Cursor(通常版)`,"integrations.cursor.detected":`検出済み`,"integrations.cursor.notFound":`見つかりません`,"integrations.cursor.regularOnly":`通常版の Cursor のみが見つかりました。カスタムエンドポイントは Cursor のサーバー経由でルーティングされるため、公開トンネルがなければ loopback プロキシには接続できません。Private Inference ビルドについてはガイドを参照してください。`,"integrations.cursor.nothingFound":`通常の場所に Cursor のインストールが見つかりませんでした。別の場所にインストールされている場合でも、以下の値を使用できます。`,"integrations.cursor.gateway":`ゲートウェイの値`,"integrations.cursor.gatewayHint":`Cursor Private Inference で Settings > Models > Gateway を開き、この 2 つの値を貼り付けてから、Refresh model list を押してください。`,"integrations.cursor.baseUrl":`Base URL`,"integrations.cursor.apiKey":`API Key`,"integrations.cursor.apiKeyCredential":`opencodex API キーのいずれか(このバインドには認証情報が必要です)`,"integrations.cursor.copy":`コピー`,"integrations.cursor.copied":`コピーしました`,"integrations.cursor.connection":`接続`,"integrations.cursor.seen":`Cursor からの最終リクエスト: {time} ({ua})`,"integrations.cursor.neverSeen":`プロキシの起動後、Cursor からのリクエストはありません。ゲートウェイを保存したら、Cursor で Refresh model list を押してください。`,"integrations.cursor.models":`Cursor に表示される内容`,"integrations.cursor.modelsHint":`Cursor は独自のモデルテーブルから推論レベルの段階を決めるため、opencodex が示せるのは予測のみです。コンテキスト欄にはデフォルトとオプトインのウィンドウ(Cursor の Max Mode)を表示します。`,"integrations.cursor.ladderFromBundle":`推論レベルの段階は、インストール済みの Cursor Private Inference {version} バンドルから読み取りました。決めるのは Cursor で、opencodex はその表を表示するだけです。`,"integrations.cursor.ladderFromStatic":`推論レベルの段階は Cursor 3.18.25 の静的ミラーです(読み取れる Private Inference のバンドルが見つかりません)。コンテキスト欄はデフォルトとオプトインのウィンドウを示します。`,"integrations.cursor.unknownVersion":`バージョン不明`,"integrations.cursor.noControl":`—`,"integrations.cursor.singleWindow":`単一ウィンドウ`,"integrations.cursor.noControlTitle":`この ID は Cursor 内蔵の effort 表にないため、Cursor は推論コントロールを表示しません。`,"integrations.cursor.effortRowsOne":`effort 行を 1 件公開`,"integrations.cursor.effortRowsMany":`effort 行を {n} 件公開`,"integrations.cursor.effortRowsOff":`effort 行なし`,"integrations.cursor.tableLessHint":`— の行は Cursor で推論コントロールが使えません。cursorEffortRows を有効にすると effort ごとにピッカー項目(id--effort)を公開できます。固定の既定値はプロバイダーの modelDefaultReasoningEfforts で設定します。`,"integrations.cursor.colModel":`モデル`,"integrations.cursor.colReasoning":`推論`,"integrations.cursor.colContext":`コンテキスト`,"integrations.cursor.guide":`Cursor Private Inference のガイドを開く`},Ke={"nav.dashboard":`Gösterge Paneli`,"uptime.day":` gün`,"uptime.hour":` saat`,"uptime.minute":` dk`,"uptime.second":` sn`,"nav.startup":`Başlatma Güvenliği`,"nav.providers":`Sağlayıcılar`,"nav.models":`Modeller`,"nav.combos":`Kombolar`,"nav.subagents":`Alt Ajanlar`,"nav.logs":`Günlükler & Hata Ayıklama`,"nav.usage":`Kullanım`,"common.github":`GitHub`,"sidebar.star":`GitHub'da Yıldız Ver`,"sidebar.starred":`GitHub'da Yıldız Verildi`,"sidebar.starUnauthenticated":`Yıldız vermek için GitHub'ı açın (gh CLI oturum açmamış)`,"sidebar.starFailed":`gh üzerinden yıldız verilemedi. Bunun yerine GitHub açılıyor.`,"sidebar.updateAvailable":`Güncelleme mevcut: {version}`,"sidebar.checkUpdate":`Güncellemeleri kontrol et`,"common.save":`Kaydet`,"common.saving":`Kaydediliyor…`,"common.cancel":`İptal`,"common.discard":`Vazgeç`,"common.delete":`Sil`,"common.close":`Kapat`,"common.ok":`Tamam`,"common.remove":`Kaldır`,"common.loading":`Yükleniyor…`,"common.retry":`Tekrar Dene`,"auth.adminTokenTitle":`OpenCodex yönetici jetonu (OPENCODEX_ADMIN_AUTH_TOKEN)`,"auth.adminAccountLabel":`Hesap`,"auth.adminTokenFieldLabel":`Yönetici jetonu`,"auth.adminTokenRejected":`Bu yönetici jetonu reddedildi. Kontrol edip tekrar deneyin.`,"auth.adminTokenUnavailable":`Yönetici jetonu doğrulanamadı. Tekrar deneyin.`,"app.logoAria":`opencodex logosu`,"app.claudeOn":`Claude AÇIK`,"app.claudeOff":`Claude KAPALI`,"theme.label":`Tema`,"theme.light":`Açık`,"theme.dark":`Koyu`,"theme.system":`Sistem`,"lang.label":`Dil`,"lang.nativeName":`Türkçe`,"provider.name.commandCodeAuth":`Command Code - Auth`,"provider.name.commandCodeApi":`Command Code - API`,"provider.name.volcengine":`Volcengine Ark`,"provider.name.volcengineCodingPlan":`Volcengine Ark Coding Plan`,"provider.name.volcengineAgentPlan":`Volcengine Ark Agent Plan`,"errorBoundary.title":`Sayfa yüklenemedi`,"errorBoundary.message":`Bu bölümde bir işleme hatası oluştu. Yeniden denemek için sayfayı yenileyin.`,"errorBoundary.details":`Hata`,"errorBoundary.reload":`Yeniden Yükle`,"routing.title":`Yönlendirme Zekası (beta)`,"routing.subtitle":`Politika profilleri, simülasyon değerlendirmesi ve kaynak destekli yönlendirme analitiği.`,"routing.loadFailed":`Yönlendirme verileri yüklenemedi`,"routing.empty":"Yapılandırılmış yönlendirme profili yok. config.json dosyasına `routingProfiles` ekleyin.","routing.revision":`revizyon`,"routing.detail":`Profil`,"routing.createProfile":`Profil oluştur`,"routing.dryRunError":`Simülasyon başarısız oldu (HTTP {status})`,"routing.removeConfirm":`{id} profili kaldırılsın mı?`,"routing.unknownEvidence.allow":`izin ver`,"routing.unknownEvidence.penalize":`cezalandır`,"routing.unknownEvidence.exclude":`hariç tut`,"routing.removeCandidate":`{provider}/{model} adayı kaldırılsın mı`,"routing.candidates":`Adaylar`,"routing.require":`Katı gereksinimler`,"routing.optimize":`Optimizasyon ağırlıkları`,"routing.limits":`Limitler`,"routing.unknownEvidence":`Bilinmeyen kanıt politikası`,"routing.compatibility.title":`Uyumluluk politikası`,"routing.compatibility.enabled":`Compatibility Lab kanıtı gerekli`,"routing.compatibility.requiredSuites":`Gerekli test süitleri`,"routing.compatibility.loadingCatalog":`Lab kataloğu yükleniyor…`,"routing.compatibility.catalogUnavailable":`Lab kataloğu kullanılamıyor — test süiti kimliklerini config.json içinde elle girin.`,"routing.compatibility.layer.protocol_conformance":`Protokol uyumu`,"routing.compatibility.layer.live_route_compatibility":`Canlı rota uyumluluğu`,"routing.compatibility.minStatus":`Minimum uyumluluk durumu`,"routing.none":`yok`,"routing.unavailable":`–`,"routing.dryRun":`Simülasyon değerlendirmesi`,"routing.dryRunContext":`İstek bağlam penceresi (jetonlar)`,"routing.dryRunTools":`İstek araç gerektiriyor`,"routing.dryRunImage":`İstek görsel girdisi gerektiriyor`,"routing.dryRunStructured":`İstek yapılandırılmış çıktı gerektiriyor`,"routing.dryRunRun":`Adayları değerlendir`,"routing.candidate":`Aday`,"routing.eligible":`Uygun`,"routing.exclusions":`Hariç Tutulanlar`,"routing.costCap":`Maliyet tavanı`,"routing.capOutcome.satisfied":`limit içinde`,"routing.capOutcome.exceeded":`limit aşıldı`,"routing.capOutcome.unknown-allowed":`bilinmiyor (izinli)`,"routing.capOutcome.unknown-excluded":`bilinmiyor (hariç tutuldu)`,"routing.exclusion.capability-unsatisfied":`yetenek karşılanmadı`,"routing.exclusion.unknown-capability":`bilinmeyen yetenek`,"routing.exclusion.cost-limit":`maliyet tavanı aşıldı`,"routing.exclusion.cost-limit-unknown":`maliyet tavanı doğrulanamadı`,"routing.exclusion.cooldown":`soğuma süresi`,"routing.exclusion.unknown-health":`bilinmeyen sağlık`,"routing.exclusion.unknown-quota":`bilinmeyen kota`,"routing.exclusion.unknown-price":`bilinmeyen fiyat`,"routing.exclusion.other":`hariç tutma: {code}`,"routing.score":`Puan`,"routing.selected":`seçildi`,"routing.yes":`evet`,"routing.no":`hayır`,"routing.analytics":`Yönlendirme analitiği`,"routing.analyticsTotal":`İstekler`,"routing.analyticsSuccessRate":`Başarı`,"routing.analyticsFallbackRate":`Yedekleme`,"routing.analyticsP50":`p50`,"routing.analyticsP95":`p95`,"routing.analyticsP99":`p99`,"routing.analyticsCooldown":`Soğuma süresi hataları`,"routing.analyticsConfidence":`Güvenilirlik`,"routing.analyticsTruncated":`kısaltılmış geçmiş`,"routing.analyticsRequests":`İstekler`,"routing.analyticsEmpty":`Henüz analitik verisi yok — önce birkaç istek gönderin.`,"startup.title":`Başlatma güvenliği`,"startup.subtitle":`Yerel proxy yönlendirmesi bir yeniden bağlanma döngüsüne girmeden önce Codex'in yeniden başlatmanın ardından opencodex'e erişebildiğini doğrulayın.`,"startup.refresh":`Yenile`,"startup.backToDashboard":`Gösterge Paneline Dön`,"startup.loading":`Başlatma koruması kontrol ediliyor…`,"startup.error":`Başlatma koruması okunamadı.`,"startup.staleData":`Son başlatma kontrolü başarısız oldu. Aşağıdaki değerler güncel değildir ve koruma kanıtı olarak kabul edilmemelidir.`,"startup.status.native":`Yerel yönlendirme`,"startup.status.protected":`Yeniden başlatma korumalı`,"startup.status.atRisk":`Eylem gerekiyor`,"startup.summary.native":`Codex yerel proxy'ye bağımlı değildir`,"startup.summary.protected":`opencodex yeniden başlatmanın ardından kullanılabilir olacaktır`,"startup.summary.atRisk":`Codex yeniden başlatmanın ardından model erişimini kaybedebilir`,"startup.riskDetail":`Codex yerel proxy'ye sabitlenmiş, ancak kalıcı bir servis veya sağlıklı başlatıcı shim bunu tekrar başlatmayacak.`,"startup.riskDetailCustomLocal":`Codex özel bir yerel ağ geçidine işaret ediyor. opencodex bu ağ geçidinin yeniden başlatma yaşam döngüsünü yönetemez veya doğrulayamaz.`,"startup.riskDetailWindowsShim":`Başlatıcı shim desteklenen CLI betiklerini korur, ancak Codex Desktop ve doğrudan codex.exe başlatmaları Windows'ta bunu atlayabilir.`,"startup.safeDetail":"Mevcut yönlendirme ve başlatma mekanizması tutarlıdır. Yeniden başlatmanın ardından manuel `ocx start` komutuna gerek yoktur.","startup.routing":`Codex yönlendirmesi`,"startup.routing.proxy":`Yerel proxy`,"startup.routing.native":`Yerel OpenAI`,"startup.routing.customLocal":`Özel yerel ağ geçidi`,"startup.routing.customRemote":`Özel uzak ağ geçidi`,"startup.routing.unknown":`Bilinmeyen veya geçersiz yönlendirme`,"startup.restartProtection":`Yeniden başlatma koruması`,"startup.preference":`İsteğe bağlı başlatma`,"startup.enabled":`Etkin`,"startup.disabled":`Devre dışı`,"startup.protection.service":`Arka plan servisi`,"startup.protection.shim":`Başlatıcı shim`,"startup.protection.none":`Yüklü değil`,"startup.details":`Koruma detayları`,"startup.service":`Arka plan servisi`,"startup.serviceHint":`Oturum açmada başlar ve çökmenin ardından proxy'yi yeniden başlatır.`,"startup.installed":`Yüklü`,"startup.notInstalled":`Yüklü değil`,"startup.unsupported":`Desteklenmiyor`,"startup.shim":`Codex başlatıcı shim`,"startup.shimHint":"Desteklenen bir Codex betik başlatıcısı başladığında `ocx ensure` çalıştırır.","startup.healthy":`Sağlıklı`,"startup.cliOnly":`Yalnızca CLI`,"startup.stale":`Eski`,"startup.viable":`Hazır`,"startup.unhealthy":`Yüklü ama sağlıksız`,"startup.conflict":`Servis çakışması`,"startup.installedDisabled":`Yüklü ama devre dışı`,"startup.install":`Yükle`,"startup.installing":`Yükleniyor…`,"startup.repair":`Onar`,"startup.repairing":`Onarılıyor…`,"startup.serviceInstalled":`Arka plan servisi başarıyla yüklendi.`,"startup.serviceRepaired":`Arka plan servisi başarıyla onarıldı.`,"startup.shimInstalled":`Codex başlatıcı shim başarıyla yüklendi.`,"startup.shimRepaired":`Codex başlatıcı shim başarıyla onarıldı.`,"startup.installFailed":`Yükleme başarısız oldu:`,"startup.tray.title":`Windows sistem tepsisi`,"startup.tray.hint":`Tek tıkla proxy başlatma, durdurma, yeniden başlatma, panel ve durum kontrolleri için oturum tepsisi simgesi yükleyin.`,"startup.tray.login":`Windows oturum açılışında tepsiyi başlat`,"startup.tray.notProtection":`Tepsi bir denetleyicidir, yeniden başlatma koruması değildir. İnsansız proxy kurtarma için hâlâ geçerli bir arka plan servisi gereklidir.`,"startup.tray.running":`Çalışıyor`,"startup.tray.stopped":`Yüklü, gizli`,"startup.tray.stale":`Onarım gerekiyor`,"startup.tray.notInstalled":`Yüklü değil`,"startup.tray.loading":`Kontrol ediliyor…`,"startup.tray.unavailable":`Durum kullanılamıyor`,"startup.tray.install":`Yükle ve tepsiyi göster`,"startup.tray.start":`Tepsi simgesini göster`,"startup.tray.stop":`Tepsi simgesinden çık`,"startup.tray.uninstall":`Oturum tepsisini kaldır`,"startup.tray.error":"Windows tepsi eylemi başarısız oldu. Detaylar için `ocx tray status` kontrol edin.","startup.recovery":`Onarım seçenekleri`,"startup.recoveryHint":`Yukarıdaki tek tıkla yükleyicileri kullanın veya manuel onarım için komutu kopyalayın. Codex Desktop ve Windows yürütülebilir dosyaları için arka plan servisi önerilir.`,"startup.command.service":`Önerilen: kalıcı arka plan servisi`,"startup.command.shim":`Alternatif: CLI başlatıcı shim`,"startup.command.native":`Güvenli mod: yerel Codex yönlendirmesini geri yükle`,"startup.copy":`Kopyala`,"startup.copied":`Kopyalandı`,"startup.recommended":`Önerilen onarım: {cmd}`,"startup.navRisk":`Başlatma koruması dikkat gerektiriyor`,"startup.codexRuntime.clampHidden":`OpenCodex, Codex {version} kullandığı için bazı akıl yürütme çabası seçenekleri gizlendi.`,"startup.codexRuntime.clampHiddenWithEfforts":`OpenCodex, Codex {version} kullandığı için bazı akıl yürütme çabası seçenekleri gizlendi (kaldırılanlar: {efforts}).`,"startup.codexRuntime.olderBinary":`OpenCodex eski bir Codex ikili dosyası ({version}) kullanıyor. Daha yeni bir kurulum mevcut.`,"dash.subtitle":`Yerel opencodex proxy'sinin, sağlayıcılarının ve Codex'e yönlendirilen modellerin canlı durumu.`,"dash.workspace.overview":`Genel Bakış`,"dash.workspace.sections":`Bölümler`,"dash.status":`Durum`,"dash.online":`Çevrimiçi`,"dash.offline":`Çevrimdışı`,"dash.version":`Sürüm`,"dash.uptime":`Çalışma Süresi`,"dash.providers":`Sağlayıcılar`,"dash.tokens30d":`Jetonlar (30 gün)`,"dash.coverage":`%{pct} kapsam`,"dash.mem.title":`Bellek izlenebilirliği`,"dash.mem.hint":`Salt okunur çalışma zamanı tanılamaları. Gözlemlenen bellek max(RSS, harici, ArrayBuffers) değeridir.`,"dash.mem.rss":`Yerleşik küme (RSS)`,"dash.mem.jsHeap":`Kullanımdaki JS yığını`,"dash.mem.jsHeapArena":`arena {total}`,"dash.mem.pressure":`Uyarı eşiğine göre`,"dash.mem.pressureOf":`Eşiğin %{pct}'si`,"dash.mem.pressureUnknown":`Bildirilen eşik yok`,"dash.mem.jscHeap":`JSC yığını`,"dash.mem.external":`Harici`,"dash.mem.arrayBuffers":`ArrayBuffers`,"dash.mem.observed":`Gözlemlenen`,"dash.mem.runtime":`Çalışma zamanı sayaçları`,"dash.mem.growth":`Saatlik gözlemlenen kayma`,"dash.mem.perHour":`/saat`,"dash.mem.store":`Devamlılık deposu`,"dash.mem.storeHint":`Proxy previous_response_id önbelleği.`,"dash.mem.storeEntries":`Girdiler`,"dash.mem.storeTotal":`Toplam`,"dash.mem.storeLargest":`En büyük`,"dash.mem.storeOldest":`En eski`,"dash.mem.threshold":`Uyarı eşiği`,"dash.mem.lastWarn":`Son uyarı`,"dash.mem.never":`Hiçbir zaman`,"dash.mem.details":`Detaylar`,"dash.mem.unavailable":`Bellek tanılaması kullanılamıyor (eski proxy).`,"dash.mem.inFlight":`İşlemdeki istekler`,"dash.mem.restart":`Boşalt & yeniden başlat`,"dash.mem.restartConfirm":`{count} işlemdeki isteğin tamamlanmasını bekleyin, ardından yeniden başlatın ({seconds} saniyeye kadar).`,"dash.mem.draining":`{count} istek boşaltılıyor… tamamlandığında yeniden başlatılacak`,"dash.mem.reconnecting":`Proxy yeniden başlatılıyor… yeniden bağlanması bekleniyor`,"dash.mem.restartFailed":`Boşaltma ve yeniden başlatma başarısız oldu. Proxy'nin çalıştığını kontrol edin.`,"dash.mem.restartNoSupervisor":`Yeniden başlatma koruması algılanmadı.`,"dash.activeProviders":`Aktif sağlayıcılar`,"dash.noProviders":`Yapılandırılmış sağlayıcı yok. {cmd} komutunu çalıştırın.`,"dash.col.name":`İsim`,"dash.col.adapter":`Adaptör`,"dash.col.baseUrl":`Taban URL`,"dash.col.model":`Model`,"dash.modelsNoResults":`Aramanızla eşleşen model bulunamadı.`,"dash.availableModels":`Kullanılabilir modeller`,"dash.noModels":`Model bulunamadı. Sağlayıcı API anahtarlarını kontrol edin.`,"dash.cannotConnect":`Proxy'ye bağlanılamıyor. Çalışıyor mu?`,"dash.runStart":`Proxy'yi başlatmak için {cmd} çalıştırın.`,"dash.stop":`Proxy'yi Durdur`,"dash.stopConfirm":`Proxy durdurulsun ve yerel Codex geri yüklensin mi?`,"dash.stopFailed":`Proxy durdurulamadı (HTTP {status}).`,"dash.maSwitchFailed":`Mod değiştirme başarısız oldu (HTTP {status}).`,"dash.maNetworkError":`Ağ hatası — proxy çalışıyor mu?`,"dash.stopping":`Durduruluyor…`,"dash.actions":`Proxy`,"dash.codexRestart":`Codex model listesini yenile`,"dash.codexRestarting":`Durduruluyor…`,"dash.codexRestartConfirm":`Model listesini yeniden okumaları için Codex app-server'ları durdurulsun mu? Süren bir Codex işlemi kesilir ve Codex kendiliğinden yeniden başlamaz — sonrasında yeniden açın.`,"dash.codexRestartDone":`{count} Codex app-server durduruldu. Güncel model listesi için Codex'i yeniden açın.`,"dash.codexRestartNothing":`Çalışan Codex app-server yok. Sonraki açılışta güncel model listesi okunur.`,"dash.codexRestartUnknown":`Süreçler listelenemedi, bu yüzden hiçbir şey durdurulmadı.`,"dash.codexRestartPartial":`{count} app-server kapanmadı. Model listesi eski kalırsa bunları elle durdurun.`,"dash.codexRestartFailed":`Codex model listesi yenilenemedi (HTTP {status}).`,"dash.codexRestartUnreachable":`Proxy'ye ulaşılamadı.`,"dash.codexRestartMalformed":`Proxy beklenmeyen bir yanıt döndürdü.`,"dash.codexRestartTimeout":`Proxy zamanında yanıt vermedi. App-server'ları durdurmaya devam ediyor olabilir.`,"models.staleBanner":`Codex, bu katalogdan daha eski bir model listesi gösteriyor. Yeniden okumak için Codex'i yeniden başlatın.`,"dash.codexAutoStart":`opencodex'i Codex ile başlat`,"dash.codexAutoStartHint":`Yüklü bir shim'in ocx ensure çalıştırmasına izin verir. Arka plan servisi veya yeniden başlatma koruması kurmaz; sistem durumu için Başlatma Güvenliği'ne bakın.`,"dash.searchModel":`Arama yan araç modeli`,"dash.searchModelHint":`OpenAI dışı yönlendirilen modellerde web_search için kullanılan model. ChatGPT girişi gerektirir.`,"dash.searchReasoning":`Arama akıl yürütme çabası`,"dash.visionModel":`Görsel yan araç modeli`,"dash.visionModelHint":`Salt metin yönlendirilen modeller için görselleri tanımlamakta kullanılan model. ChatGPT girişi gerektirir.`,"dash.webSearchSidecar":`Web arama yan aracı (sidecar)`,"dash.webSearchSidecarHint":`Yönlendirilen modellerde web araması için kullanılan arka ucu ve modeli seçin.`,"dash.webSearchStream":`Yanıtları canlı akıt`,"dash.webSearchStreamHint":`Model bir araç çağrısına karar verene kadar baştaki metni ve akıl yürütmeyi canlı akıtır; kalanı arama yakalama için arabelleğe alınır. Aramadan önce yazılan metin kısmen tekrarlanabilir.`,"dash.visionSidecar":`Görsel yan aracı (sidecar)`,"dash.visionSidecarHint":`Salt metin modeller için görselleri tanımlamakta kullanılan arka ucu ve modeli seçin.`,"dash.visionOff":`Kapalı`,"dash.shadowCallIntercept":`Gölge Çağrı Yakalama`,"dash.shadowCallInterceptHint":`Codex App'in arka plan yardımcı çağrılarını ({models}) başlık oluşturma ve commit mesajları için yakalar ve seçtiğiniz modele yönlendirir.`,"dash.shadowCallWarning":`⚠ Etkinleştirildiğinde, {models} için olan TÜM istekler seçilen modelle değiştirilecektir.`,"dash.shadowCallOriginal":`Orijinal`,"dash.shadowCallModel":`Yedek model`,"dash.shadowCallTooltip":`Codex App arka planda başlık ve commit mesajı oluşturmak için yardımcı çağrılar yapar (orijinal {models}). Etkinleştirerek bunları seçtiğiniz modele yönlendirebilirsiniz.`,"models.shadowCallIntercept":`Gölge Çağrı Yakalama`,"models.shadowCallInterceptHint":`Codex App'in başlıklar ve commit mesajları için yaptığı arka plan çağrılarını ({models}) yakalar ve seçtiğiniz modele yönlendirir.`,"dash.sidecarBackend":`Arka uç`,"dash.sidecarModel":`Model`,"dash.backendAuto":`Otomatik`,"dash.backendOpenAI":`OpenAI`,"dash.backendAnthropic":`Anthropic`,"dash.sidecarSaved":`Yan araç ayarları kaydedildi. Sonraki istekte uygulanacak.`,"dash.sidecarSaveFailed":`Yan araç ayarları kaydedilemedi.`,"dash.injectionLabel":`Alt ajan devri`,"dash.injectionHint":`Codex'in alt ajan işlerini devredeceği modeli seçin.`,"dash.injectionManage":`Ayarları aç`,"dash.syncCodexSubagentDefaults":`Ayrıca Codex varsayılanı olarak kaydet`,"dash.syncCodexSubagentDefaultsHint":`Açık olduğunda, yukarıdaki seçim Codex'in kendi yapılandırmasına yazılır.`,"dash.multiAgentGuidance":`Codex'e işi nasıl böleceğini söyle`,"dash.multiAgentGuidanceHint":`Codex'e işleri alt ajanlara nasıl devredeceğini söyleyen kısa bir not gönderir.`,"dash.injectionNone":`Yok`,"dash.injectionEffortLabel":`Akıl yürütme çabası`,"dash.injectionEffortNone":`Model varsayılanı`,"dash.effortCapLabel":`V2 ultra çaba limiti`,"dash.subagentEffortCapLabel":`V2 alt ajan çaba limiti`,"dash.effortCapHelp":`V2 ultra modu turları için akıl yürütme çabasını sınırlar. Ayarlandığında, gelen maksimum çaba istekleri (ultra modundan) seçilen seviye ile sınırlandırılır. Alt ajan limiti yalnızca türetilen çocuk ajanları etkiler. Limitler çabayı yalnızca düşürür, asla yükseltmez. Bir model sınırlandırılan seviyeyi desteklemiyorsa, desteklenen en yakın alt seviyeye iner.`,"dash.effortCapNone":`Limit yok`,"dash.maintenance":`Bakım`,"dash.maintenanceHint":`Codex'in model kataloğunu yenileyin veya daha yeni bir opencodex sürümü yükleyin.`,"dash.syncModels":`Modelleri senkronize et`,"dash.syncModelsHint":`Bağladığınız sağlayıcılardan Codex'in model kataloğunu yeniden yazın.`,"dash.syncRun":`Şimdi senkronize et`,"dash.syncing":`Senkronize ediliyor…`,"dash.syncOk":`Senkronizasyon tamamlandı. {count} model eklendi.`,"dash.syncStaleHint":`Codex hâlâ eski bir liste gösteriyorsa uygulama sunucusunu yeniden başlatın ({cmd}).`,"dash.syncFailed":`Senkronizasyon başarısız oldu: {error}`,"dash.projectConfigTitle":`Proje Codex konfigürasyonu OpenCodex'i atlıyor`,"dash.projectConfigHint":`Bu depoya özel ayarlar OpenCodex proxy'sini geçersiz kılar.`,"dash.checkUpdate":`Güncellemeyi kontrol et`,"dash.updateTitle":`opencodex'i güncelle`,"dash.updateDesc":`Seçilen kanal için npm'i kontrol edin.`,"dash.updateChannel":`Kanal`,"dash.updateChecking":`Güncellemeler kontrol ediliyor…`,"dash.updateInstalled":`Yüklü`,"dash.updateLatest":`En son`,"dash.updateAvailable":`Güncelleme mevcut`,"dash.updateCurrent":`Güncel`,"dash.updateCommand":`Komut`,"dash.updateSource":`Bu bir kaynak kod kopyasıdır. Terminalden güncelleyin.`,"dash.updateUnavailable":`npm'den en son sürüm okunamadı.`,"dash.updateRetry":`Tekrar dene`,"dash.updateRecheck":`Yeniden kontrol et`,"dash.updateCannotAuto":`Tek tıkla güncelleme kullanılamıyor ({reason}).`,"dash.updateReason.source_checkout":`kaynak kod kopyası`,"dash.updateReason.latest_unavailable":`npm sunucusuna ulaşılamıyor`,"dash.updateReason.already_latest":`zaten en son sürümde`,"dash.updateReason.unknown":`güncelleme kullanılamıyor`,"dash.updateRestart":`Güncellemeden sonra yeniden başlat`,"dash.updateRestartHint":`Önerilir. Proxy yeniden başlayana kadar mevcut GUI eski kodu çalıştırmaya devam eder.`,"dash.runUpdate":`Güncelle`,"dash.updateReconnecting":`Yeniden başlatılan proxy bekleniyor…`,"dash.updateStatus.running":`opencodex güncelleniyor.`,"dash.updateStatus.restarting":`Güncelleme yüklendi. Proxy yeniden başlatılıyor.`,"dash.updateStatus.succeeded":`Güncelleme tamamlandı.`,"dash.updateVersionTransition":`{currentVersion} -> {latestVersion}.`,"dash.updateStatus.failed":`Güncelleme başarısız oldu.`,"prov.subtitle":`opencodex'in Codex'e yönlendirdiği sağlayıcıları yapılandırın.`,"prov.add":`Sağlayıcı Ekle`,"prov.editJson":`JSON Düzenle`,"prov.accountLogin":`Hesap girişi`,"prov.noOauth":`Kullanılabilir OAuth sağlayıcısı yok.`,"prov.loggedIn":`giriş yapıldı`,"prov.notLoggedIn":`giriş yapılmadı`,"prov.logout":`Çıkış Yap`,"prov.login":`Giriş Yap`,"prov.loginWith":`{provider} ile Giriş Yap`,"prov.waitingBrowser":`Tarayıcı bekleniyor…`,"prov.didntOpen":`Açılmadı mı? Buraya tıklayın`,"prov.copyLink":`Bağlantıyı kopyala`,"prov.dontOpenBrowser":`Proxy makinesinde tarayıcı açma`,"prov.dontOpenBrowserHint":`Farklı bir tarayıcı profili için ya da pano proxy'nin makinesinde değilken kullanışlıdır.`,"prov.linkCopied":`Kopyalandı`,"prov.linkCopyUnavailable":`Pano kullanılamıyor`,"prov.deviceCode":`Cihaz kodu`,"prov.copyCode":`Kodu kopyala`,"prov.codeCopied":`Kod kopyalandı`,"prov.editAlias":`Takma adı düzenle`,"prov.aliasPrompt":`Görüntülenen ad (temizlemek için boş bırakın)`,"prov.aliasSaved":`Takma ad kaydedildi`,"prov.aliasSaveFailed":`Takma ad kaydedilemedi`,"prov.accountId":`ID`,"prov.pasteRedirect":`Yönlendirme URL'sini veya kodu yapıştırın`,"prov.pasteRedirectHint":`Tarayıcı localhost hatası gösterirse adres çubuğundaki URL'yi kopyalayıp buraya yapıştırın.`,"prov.pasteSubmit":`Gönder`,"prov.pasteSubmitting":`Gönderiliyor…`,"prov.pasteOk":`Kod gönderildi — giriş tamamlanıyor…`,"prov.pasteFail":`Kod gönderilemedi: {error}`,"prov.port":`Port`,"prov.default":`Varsayılan`,"prov.loadingConfig":`Yükleniyor…`,"prov.saved":`Kaydedildi! Uygulamak için proxy'yi yeniden başlatın.`,"prov.loadConfigFail":`Konfigürasyon yüklenemedi`,"prov.invalidJson":`Geçersiz JSON`,"prov.saveFailed":`Kaydetme başarısız`,"prov.loginFailStart":`{provider} girişi başlatılamadı`,"prov.loginError":`{provider} giriş hatası: {error}`,"prov.loginRequestFail":`{provider} giriş isteği başarısız oldu`,"prov.loginCancelled":`{provider} girişi iptal edildi`,"prov.loginTimeout":`{provider} girişi zaman aşımına uğradı.`,"prov.loginSameAccount":`Hâlâ aynı {provider} hesabı — tarayıcıda hesap değiştirin, ardından tekrar Hesap Ekle'yi deneyin.`,"prov.loginOk":`{provider} hesabına giriş yapıldı. Modellerini listelemek için {cmd} çalıştırın (veya canlı olarak uygulanır).`,"prov.added":`"{name}" eklendi. Modellerini listelemek için {cmd} çalıştırın (veya canlı olarak uygulanır).`,"oauthTos.highTitle":`{provider}: abonelik OAuth riski`,"oauthTos.elevatedTitle":`{provider}: gayri resmi OAuth köprüsü`,"oauthTos.anthropicBody":`Claude abonelik OAuth jetonlarının OpenCodex gibi üçüncü taraf bir proxy üzerinden doğrudan yeniden kullanılması desteklenen bir Anthropic entegrasyonu değildir ve erişim kısıtlamalarına yol açabilir. Claude aboneliklerini kullanan desteklenen Agent SDK entegrasyonları ayrıdır.`,"oauthTos.highBody":`OpenCodex, {provider} bağlantısını üçüncü taraf bir OAuth yolu üzerinden kurar. Desteklenmeyen kullanım, erişim kısıtlamalarına veya hesabın askıya alınmasına yol açabilir.`,"oauthTos.elevatedBody":`OpenCodex, {provider} bağlantısını resmi olmayan bir OAuth yolu üzerinden kurar. Mümkün olduğunda resmi istemciyi kullanın; sıra dışı veya otomatik trafik kötüye kullanım olarak değerlendirilebilir ve erişim kısıtlanabilir veya askıya alınabilir.`,"oauthTos.saferPath":`Daha güvenli seçenek: OpenCodex'te bir API anahtarı yapılandırın.`,"oauthTos.acknowledge":`Riski anlıyorum ve OAuth ile devam etmek istiyorum.`,"oauthTos.continue":`OAuth ile devam et`,"prov.logoutOk":`{provider} çıkışı yapıldı.`,"prov.logoutFail":`{provider} çıkışı yapılamadı.`,"prov.removed":`"{name}" kaldırıldı.`,"prov.removedDefault":`"{name}" kaldırıldı. Varsayılan sağlayıcı artık "{defaultProvider}".`,"prov.removeFail":`"{name}" kaldırılamadı.`,"prov.removeLastProvider":`Başka etkin sağlayıcı yokken bu sağlayıcıyı kaldıramazsınız.`,"prov.removeHasDependentCombos":`Önce bağımlı komboları kaldırın veya güncelleyin: {combos}.`,"prov.setDefault":`Varsayılan olarak ayarla`,"prov.setDefaultSuccess":`"{name}" artık varsayılan sağlayıcı.`,"prov.setDefaultFail":`"{name}" varsayılan yapılamadı.`,"prov.defaultDisabled":`Varsayılan yapmadan önce bu sağlayıcıyı etkinleştirin.`,"prov.updateFail":`Sağlayıcı güncellenemedi.`,"prov.networkError":`Ağ hatası. Proxy'nin çalıştığını kontrol edin.`,"prov.removeConfirm":`"{name}" sağlayıcısı kaldırılsın mı?`,"prov.hasApiKey":`API anahtarı yapılandırıldı`,"prov.hasHeaders":`özel başlıklar yapılandırıldı`,"prov.accounts":`Hesaplar ({n})`,"prov.accountsAria":`{name} hesaplarını aç/kapat`,"prov.accountActive":`Aktif`,"prov.accountReauth":`Tekrar giriş yap`,"prov.reauthenticate":`Yeniden doğrula`,"prov.reauthAccountMissing":`Seçilen hesap bulunamadı`,"prov.reauthIdentityMismatch":`Giriş yapılan hesap seçilen hesapla eşleşmedi`,"prov.accountAdd":`Hesap ekle`,"prov.accountNoLabel":`hesap {id}`,"prov.accountSwitchTitle":`Bu hesabı kullan`,"prov.accountSwitched":`{email} hesabına geçildi.`,"prov.accountSwitchFail":`Hesap değiştirilemedi`,"prov.accountRemoved":`{email} kaldırıldı.`,"prov.accountRemoveFail":`{email} kaldırılamadı.`,"prov.accountRemoveAria":`{email} hesabını kaldır`,"prov.accountRemoveConfirm":`{email} hesabı kaldırılsın mı?`,"prov.keyAdd":`API anahtarı ekle`,"prov.keyAdded":`{name} için API anahtarı eklendi.`,"prov.keyAddFail":`API anahtarı eklenemedi`,"prov.keyPlaceholder":`API anahtarını yapıştırın`,"prov.keySwitchTitle":`Bu anahtarı kullan`,"prov.keySwitched":`{key} anahtarına geçildi.`,"prov.keySwitchFail":`Anahtar değiştirilemedi`,"prov.keyRemoved":`{key} anahtarı kaldırıldı.`,"prov.keyRemoveAria":`{key} anahtarını kaldır`,"prov.keyRemoveConfirm":`{key} API anahtarı silinsin mi?`,"prov.activeBadge":`Aktif`,"prov.disabledBadge":`Devre Dışı`,"prov.defaultBadge":`Varsayılan`,"prov.enable":`Etkinleştir`,"prov.disable":`Devre Dışı Bırak`,"prov.enabled":`"{name}" etkinleştirildi.`,"prov.disabled":`"{name}" devre dışı bırakıldı.`,"prov.enableFail":`"{name}" etkinleştirilemedi.`,"prov.disableFail":`"{name}" devre dışı bırakılamadı.`,"prov.enableAria":`{name} sağlayıcısını etkinleştir`,"prov.disableAria":`{name} sağlayıcısını devre dışı bırak`,"prov.defaultCannotDisable":`Varsayılan sağlayıcı devre dışı bırakılamaz`,"prov.openaiAccountMode":`Codex hesap modu`,"prov.openaiModePool":`Havuz (Pool)`,"prov.openaiModeDirect":`Doğrudan (Direct)`,"prov.openaiPoolDesc":`Varsayılan. Ana girişi ve eklenen hesapları havuzda döndürür.`,"prov.openaiDirectDesc":`Yalnızca ana Codex girişini kullanır.`,"prov.openaiModeSaved":`OpenAI hesap modu {mode} olarak değiştirildi.`,"prov.openaiModeSaveFailed":`OpenAI hesap modu değiştirilemedi.`,"prov.openaiApiDesc":`OpenAI API anahtarı kullanır.`,"prov.manageCodexAccounts":`Codex hesaplarını yönet`,"prov.openaiApiMissing":`API anahtarı gerekli`,"prov.openaiApiSetup":`API anahtarı ayarla`,"models.tab.catalog":`Modeller`,"models.tab.combos":`Kombolar`,"models.tab.compatibility":`Uyumluluk`,"models.tab.routing":`Yönlendirme (beta)`,"models.tabsLabel":`Model yüzeyleri`,"models.subtitle.combos":`Tek bir kimlik olarak yanıt veren sıralı model grupları. Hedefleri failover ile zincirleyin veya yükü dengeleme stratejisiyle dağıtın.`,"models.subtitle.compatibility":`Lab projeksiyon kanıtından salt okunur uyumluluk matrisi.`,"models.subtitle.routing":`Politika profilleri ve simülasyon değerlendirmesi.`,"models.subtitle":`Codex'in göreceği modelleri açıp kapatın.`,"models.nativeGroupLabel":`Yerel OpenAI`,"models.nativeHint":"Doğrudan geçiş modelleri Sağlayıcılar bölümünde seçilen Havuz veya Doğrudan seçeneğini kullanır. Buradan model eklemek, yeni bir düz passthrough kimliği değil, yönlendirilmiş bir `openai/` seçici kaydeder.","models.active":`{active}/{total} görünür`,"models.workspace.providers":`Sağlayıcılar`,"models.workspace.allProviders":`Tüm sağlayıcılar`,"models.workspace.mainAria":`Model detayları`,"models.allOn":`Tümünü aç`,"models.allOff":`Tümünü kapat`,"models.presetLabel":`Modeller`,"models.presetMode_preset":`Ön ayar`,"models.presetMode_all":`Tümü`,"models.presetMode_custom":`Özel`,"models.presetSummary":`{total} modelden {count} tanesi gösteriliyor — çekirdek ön ayar v{version}`,"models.presetUpdateAvailable":`Ön ayar v{version} mevcut`,"models.presetAppliedToast":`{provider}: ön ayar uygulandı — {count} model seçildi`,"models.presetClearedToast":`{provider}: tüm modeller gösteriliyor`,"models.presetEmpty":`{provider}: ön ayar hiçbir modelle eşleşmedi — seçim değişmedi`,"models.presetConfirmReplace":`Seçiminiz {count} modellik ön ayarla değiştirilsin mi?`,"models.cap350k":`350k Sınırı`,"models.capApplied":`Bağlam sınırı uygulandı.`,"models.capSaveFailed":`Bağlam sınırı kaydedilemedi`,"models.contextCapped":`350k sınırı`,"models.contextCapLabel":`Varsayılan pencere / sınır`,"models.v2Label":`Alt Ajan`,"models.shadowCallOriginal":`⚠ {models} →`,"models.v2DocsLink":`v1 / v2 nedir?`,"models.v2Mode_v1":`v1`,"models.v2Mode_default":`taban`,"models.v2Mode_v2":`v2`,"models.v2ModeDesc_v1":`Tüm modeller → v1 yüzeyi`,"models.v2ModeDesc_default":`Yukarı akış varsayılanları`,"models.v2ModeDesc_v2":`Tüm modeller → v2 yüzeyi`,"models.keepNativeOnV1":`ChatGPT v1'de kalsın`,"models.keepNativeOnV1Hint":`ChatGPT yerel ebeveynleri v2 çocuk görevlerini şifreler; Grok ve Claude okuyamaz. Sol/Terra yönlendirilmiş modelleri spawn edecekse açık bırakın. Yönlendirilmiş ebeveynler v2'de kalır.`,"models.v2Help":`v1 alt ajanları birincil modelle sınırlandırır; taban standart ajan sınırlarını devralır; v2 tam çoklu ajan orkestrasyonunu etkinleştirir. + +v2'de ChatGPT v1'de kalsın, Sol/Terra'yı v1'de bırakır; böylece Grok veya Claude spawn edebilirler. ChatGPT v2 çocuk görevlerini şifreler; yönlendirilmiş modeller okuyamaz. Yönlendirilmiş ebeveynler v2'de kalır. + +Değişiklikler yeni oturumlara uygulanır.`,"dash.multiAgent":`Alt Ajan`,"models.v2Conflict":`[agents] max_threads ayarlanmış — config.toml dosyasından kaldırın`,"models.v2Applied":`Çoklu ajan modu güncellendi. Yeni oturumlar bu modu kullanacaktır; model seçiciyi yenilemek için Codex uygulamasını yeniden başlatın.`,"models.v2ThreadsLabel":`Maksimum iş parçacığı`,"models.v2ThreadsDefault":`varsayılan (4)`,"models.v2ThreadsApplied":`İş parçacığı limiti güncellendi`,"models.v2ThreadsInvalid":`İş parçacığı limiti >= 1 tamsayı olmalıdır`,"models.v2ThreadsApply":`Uygula`,"models.capValue":`Varsayılan {value}`,"models.contextSettings":`Özel pencereler`,"models.contextSettingsTitle":`Özel pencereler — {provider}`,"models.contextDefault":`Sağlayıcı varsayılanı`,"models.contextModel":`Model`,"models.contextModelOverride":`Model geçersiz kılma`,"models.contextHint":`Pencereyi biliyorsanız gerçek Codex penceresini buraya yazın. Üst akış değer yoksa bu kullanılır; daha büyük bildirilen pencere düşürülür, daha küçük olan korunur. Boş bırakırsanız sağlayıcının «Varsayılan pencere / sınır» değeri kullanılır; o sınır kapalıysa 128k olur.`,"models.contextAutomatic":`Otomatik keşif`,"models.contextSaved":`Bağlam pencereleri güncellendi.`,"models.contextUnchanged":`Kaydedilecek bağlam penceresi değişikliği yok.`,"models.contextSaveFailed":`Bağlam pencereleri kaydedilemedi`,"models.contextInvalid":`Bağlam pencereleri pozitif tam sayılar olmalıdır`,"models.contextCappedValue":`{value} sınırı`,"models.setAll":`Tümünü ayarla`,"models.setAllHint":`Her yönlendirilen sağlayıcıda {value} varsayılan pencereyi açar. Röle context_window / context_length vermezse bu değer gerçek Codex penceresi olur. Tek bir modeli elle yazmak için aynı satırdaki «Özel pencereler»i kullanın.`,"models.collapseAll":`Tümünü daralt`,"models.expandAll":`Tümünü genişlet`,"models.orderHint":`Seçici sırası: Alt ajan seçimleri → kalan modeller.`,"models.custom":`Özel…`,"models.customApply":`Uygula`,"models.customPlaceholder":`Jetonlar (örn. 420000)`,"models.customAdd":`Özel model ekle`,"models.customAddTitle":`Özel model ekle — {provider}`,"models.customEditTitle":`Özel modeli düzenle — {provider}`,"models.customAdded":`Özel model eklendi`,"models.customUpdated":`Özel model güncellendi`,"models.customDeleted":`Özel model silindi`,"models.customSaveFailed":`Özel model kaydedilemedi`,"models.customSaving":`Kaydediliyor…`,"models.customAddBtn":`Ekle`,"models.customEditBtn":`Güncelle`,"models.customEdit":`Düzenle`,"models.customDelete":`Sil`,"models.customDeleteConfirm":`{name} modeli silinsin mi?`,"models.customBadge":`Özel`,"models.customSummary":`{count} özel`,"models.customFieldModelId":`Model ID`,"models.customFieldModelIdPlaceholder":`örn. qwen4-max-preview`,"models.customFieldDisplayName":`Görüntülenen ad (isteğe bağlı)`,"models.customFieldDisplayNamePlaceholder":`örn. Qwen 4 Max Preview`,"models.customFieldContext":`Bağlam penceresi`,"models.customFieldModalities":`Girdi türleri`,"models.customFieldReasoning":`Akıl yürütme çabası`,"models.customFieldReasoningOverride":`Akıl yürütme çabasını geçersiz kıl`,"models.reasoningEffort.none":`Yok`,"models.reasoningEffort.minimal":`Minimal`,"models.reasoningEffort.low":`Düşük`,"models.reasoningEffort.medium":`Orta`,"models.reasoningEffort.high":`Yüksek`,"models.reasoningEffort.xhigh":`Çok yüksek`,"models.reasoningEffort.max":`Maksimum`,"models.tipProvider":`Sağlayıcı`,"models.tipContext":`Bağlam`,"models.tipModalities":`Girdi Türleri`,"models.tipStatus":`Durum`,"models.tipActive":`Aktif`,"models.tipDisabled":`Devre Dışı`,"models.applied":`Uygulandı.`,"models.saveFailed":`Kaydetme başarısız`,"models.networkError":`Ağ hatası — proxy çalışıyor mu?`,"models.loadFail":`Modeller yüklenemedi — proxy çalışıyor mu?`,"models.noRouted":`Yönlendirilen model yok`,"models.noRoutedHint":`Önce bir sağlayıcıya giriş yapın veya ekleyin.`,"models.emptyDiscovery":`Keşfedilen model yok.`,"models.emptyDiscoveryDisabled":`Canlı model keşfi kapalı.`,"models.discoveryFailedBadge":`Keşif başarısız`,"models.discoveryFailedHttp":`Model keşfi başarısız oldu (HTTP {status}).`,"models.discoveryFailedBlocked":`Model keşfi engellendi.`,"models.discoveryFailedInvalidResponse":`Model keşfi geçersiz bir yanıt döndürdü.`,"models.discoveryFailedNetwork":`Model keşfi ağ hatası nedeniyle başarısız oldu.`,"models.discoveryFailedProvider":`Sağlayıcı bir model keşfi hatası bildirdi.`,"models.discoveryFailedGeneric":`Model keşfi başarısız oldu.`,"models.openProviderSettings":`Sağlayıcı ayarlarını aç`,"models.loading":`Yükleniyor…`,"models.search":`Modellerde ara…`,"models.showMore":`{n} tane daha göster`,"models.allowlistLabel":`Sadece seçilenler`,"models.allowlistHint":`Sadece işaretli modeller kataloğa gönderilir.`,"models.selectedCount":`{n} seçildi`,"sub.subtitle":`Codex'in {cmd} komutu, geçersiz kılma olarak yalnızca ilk 5 modeli (önceliğe göre) sunar. Buradan 5 taneye kadar seçin (yerel gpt veya yönlendirilen) ve opencodex bunların katalog önceliğini tam olarak bunların liderlik edeceği şekilde ayarlar. Diğer herhangi bir model tam adıyla çağrılabilir kalır; bu yalnızca neyin gösterileceğini kontrol eder.`,"sub.featured":`Öne Çıkarılanlar`,"sub.advanced":`Gelişmiş`,"sub.orderHintAria":`Bu sıra nasıl kullanılır`,"sub.orderHint":`Burada gösterilen sıralama, Codex model seçicisinin üst kısmındaki 1-5 pozisyonlarını ve {cmd} için varsayılan model adaylarını belirler.`,"sub.noneSelected":`Hiçbiri seçilmedi — aşağıdaki listeden seçin.`,"sub.models":`Modeller`,"sub.search":`Modellerde ara…`,"sub.settings":`Ayarlar`,"sub.sections":`Alt ajan bölümleri`,"sub.delegation.model":`İlk çağrılacak model`,"sub.delegation.modelHint":`Codex'in iş devrederken ilk ulaştığı model.`,"sub.noModels":`Model yok — önce bir sağlayıcı ekleyin.`,"sub.saved":`{n} model kaydedildi. Bunları spawn_agent geçersiz kılmaları olarak görmek için yeni bir Codex oturumu başlatın (veya {cmd} çalıştırın).`,"sub.saveFailed":`Kaydetme başarısız`,"sub.networkError":`Ağ hatası — proxy çalışıyor mu?`,"sub.loadFail":`Modeller yüklenemedi`,"sub.loading":`Yükleniyor…`,"sub.moveUp":`{m} modelini yukarı taşı`,"sub.moveDown":`{m} modelini aşağı taşı`,"sub.removeAria":`{m} modelini kaldır`,"sub.workspace.addToFeatured":`{m} modelini öne çıkarılanlara ekle`,"sub.workspace.allModels":`Tüm modeller`,"sub.workspace.featuredFull":`Öne çıkarılanlar listesi dolu (maksimum 5)`,"sub.workspace.mainAria":`Alt ajan model detayları`,"sub.workspace.notFeatured":`Öne çıkarılmadı`,"sub.workspace.priority":`Öncelik`,"sub.workspace.removeFromFeatured":`{m} modelini öne çıkarılanlardan kaldır`,"sub.workspace.selectModel":`Bir model seçin`,"sub.workspace.selectModelDesc":`Detayları görmek için listeden bir model seçin.`,"sub.workspace.selector":`Genel seçici`,"sub.ultraMode":`Ultra modu`,"sub.ultraModeHint":`Tüm modeller ve reasoning effort için Proactive çoklu ajan delegasyon politikasını etkinleştirir (reasoning effort değerini değiştirmez). config.toml dosyasına features.multi_agent_v2.multi_agent_mode_hint_text yazar.`,"sub.ultraModeV2Required":`v2 çoklu ajan yüzeyi gerekir — önce multi_agent_v2'yi etkinleştirin ve alt ajan modu denetiminde v2'yi seçin.`,"sub.ultraModeText":`Ultra modu delegasyon metni`,"sub.ultraModePreset":`Ön ayarı geri yükle`,"sub.ultraModeLoadFail":`Ultra modu ayarları yüklenemedi — proxy çalışıyor mu?`,"sub.ultraModeSaveFail":`Ultra modu ayarları kaydedilemedi`,"sub.ultraModeSaved":`Ultra modu kaydedildi. Yeni Codex oturumlarına uygulanır.`,"logs.title":`İstek Günlükleri`,"logs.tabLogs":`Günlükler`,"logs.tabDebug":`Hata Ayıklama`,"logs.subtitle":`Proxy üzerinden yönlendirilen son istekler.`,"logs.autoRefresh":`Otomatik yenile`,"logs.noRequests":`Henüz istek yok.`,"logs.loadError":`İstek günlükleri yüklenemedi.`,"logs.filter.surface.label":`Yüzey`,"logs.filter.surface.all":`Tümü`,"logs.filter.surface.claude":`Claude`,"logs.filter.surface.codex":`Codex`,"logs.filter.surface.grok":`Grok`,"logs.filter.interceptedHelpersOnly":`Yalnizca yakalanan yardimcilar`,"logs.badge.interceptedHelper":`I · {model}`,"logs.badge.interceptedHelperTitle":`Yakalanan yardimci istegi`,"logs.filter.conversation.label":`Sohbet`,"logs.filter.conversation.placeholder":`Sohbet ID'sini yapıştırın`,"logs.filter.conversation.clear":`Temizle`,"logs.filter.model.label":`Model`,"logs.filter.model.placeholder":`Modele veya sağlayıcıya göre filtrele`,"logs.filter.conversation.apply":`Günlükleri filtrele`,"logs.conversation.totals":`{requests} istek · {tokens} jeton · {cost}`,"logs.conversation.scope":`Toplamlar yalnızca yüklü günlükleri kapsar.`,"logs.conversation.excluded":`({unpriced} fiyatlandırılmamış, {unmetered} ölçülmemiş hariç)`,"logs.cost.approximate":`{amount}`,"logs.cost.lowerBound":`≥{amount}`,"logs.cost.unavailable":`kullanılamıyor`,"logs.detail.conversation":`Sohbet`,"logs.badge.claude":`Claude`,"logs.badge.grok":`Grok`,"logs.col.time":`Zaman`,"logs.col.request":`İstek`,"logs.col.model":`Model`,"logs.col.effort":`Çaba`,"logs.col.provider":`Sağlayıcı`,"logs.col.status":`Durum`,"logs.col.tokens":`Jetonlar`,"logs.col.tokPerSec":`jeton/sn`,"logs.col.estimatedCost":`~$`,"logs.metric.tokPerSecTitle":`Çıktı jetonu / saniye`,"logs.metric.estimatedCostTitle":`Tahmini API liste fiyatı`,"usage.cost.total":`API liste fiyatı eşdeğeri`,"usage.cost.disclaimer":`Fatura makbuzu değildir.`,"usage.cost.unpricedNote":`{count} istek hariç tutuldu`,"logs.detail.section.basic":`Temel bilgiler`,"logs.detail.route.section":`Yönlendirme kararı`,"logs.detail.route.kind":`Yönlendirme türü`,"logs.detail.route.profile":`Profil`,"logs.detail.route.selected":`Seçilen`,"logs.detail.route.candidates":`Adaylar`,"logs.detail.route.unknown":`Kayıtlı yönlendirme izi yok.`,"logs.detail.section.performance":`Performans`,"logs.detail.section.cost":`Tahmini maliyet`,"logs.detail.section.attempts":`Kombo denemeleri`,"logs.detail.section.usage":`Ham kullanım`,"logs.detail.ttft":`TTFT`,"logs.detail.costTotal":`Liste fiyatı eşdeğeri`,"logs.detail.totalTokens":`Toplam jeton`,"logs.detail.matchedKey":`Eşleşen anahtar`,"logs.detail.priceSource":`Fiyat kaynağı`,"logs.detail.unavailableReason":`Kullanılamama nedeni`,"logs.detail.copyRequestId":`İstek ID kopyala`,"logs.detail.copied":`Kopyalandı`,"logs.detail.source.jawcode":`katalog`,"logs.detail.source.expected":`Beklenen fiyat`,"logs.detail.source.user":`Kullanıcı tarafından yapılandırılan sağlayıcı fiyat katmanı`,"logs.detail.verification.verified":`Doğrulandı`,"logs.detail.verification.derived":`Taban modelden türetildi`,"logs.detail.attempt.target":`Sağlayıcı / model`,"logs.detail.attempt.reason":`Sonuç / neden`,"logs.detail.attempt.completed":`Tamamlandı`,"logs.detail.attempt.e2eNote":`Süreç bilgisi`,"logs.detail.attempt.recovery.transient5xx":`Geçici 5xx`,"logs.detail.attempt.recovery.connectionReset":`Bağlantı sıfırlandı`,"logs.detail.attempt.recovery.oauth401":`OAuth yeniden doğrulaması`,"logs.detail.attempt.recovery.key429":`Anahtar oranı kısıtlandı (429)`,"logs.detail.attempt.recovery.rateLimit429":`Oran kısıtlandı (429)`,"logs.detail.attempt.recovery.anthropicOauth429":`Anthropic OAuth kısıtlandı (429)`,"logs.detail.attempt.recovery.image413":`Görsel boyutu çok büyük (413)`,"logs.detail.attempt.recovery.emptyCompletion":`Boş tamamlama yeniden denemesi`,"logs.detail.attempt.recovery.unknown":`Bilinmeyen kurtarma nedeni`,"logs.detail.reason.usage_missing":`Kullanım bildirilmedi.`,"logs.detail.reason.usage_unsupported":`Bu sağlayıcı kullanım bildirmeyebilir.`,"logs.detail.reason.output_missing":`Çıktı jeton sayısı bildirilmedi.`,"logs.detail.reason.invalid_duration":`İstek süresi geçersiz.`,"logs.detail.reason.price_unmatched":`Eşleşen fiyat bulunamadı.`,"logs.detail.reason.invalid_cache_breakdown":`Önbellek detayları çakışıyor.`,"logs.detail.reason.invalid_usage":`Kullanım geçersiz bir jeton değeri içeriyor.`,"logs.detail.reason.combo_attempt_unavailable":`Kombo denemesi fiyatlandırılamadı.`,"logs.detail.estimate.usage_estimated":`Sağlayıcı kullanımı tahminidir.`,"logs.detail.estimate.cache_detail_missing":`Önbellek detayları eksik.`,"logs.detail.estimate.expected_price_overlay":`Doğrulanmış liste fiyatı kullanıldı.`,"logs.detail.estimate.provider_cost_overlay":`Kullanıcı tarafından yapılandırılan bir sağlayıcı fiyat katmanı kullanıldı.`,"logs.detail.estimate.priority_lower_bound":`Doğrulanan Priority fiyatı kullanılamıyor; gösterilen tahmin bilinen bir alt sınırdır.`,"logs.col.error":`Hata`,"logs.col.upstreamReason":`Yukarı akış nedeni`,"logs.col.duration":`Süre`,"logs.modelTooltip.model":`model`,"logs.modelTooltip.resolvedModel":`çözümlenen model`,"logs.modelTooltip.requestedTier":`istenen katman`,"logs.modelTooltip.configuredTier":`yapılandırılan katman`,"logs.modelTooltip.responseTier":`yanıt katmanı`,"logs.modelTooltip.supportsTier":`katman desteği`,"logs.tokens.reported":`bildirilen`,"logs.tokens.unreported":`bildirilmeyen`,"logs.tokens.unsupported":`desteklenmeyen`,"logs.tokens.estimated":`tahmini`,"logs.tokens.input":`girdi`,"logs.tokens.output":`çıktı`,"logs.tokens.cacheRead":`önbellek okuma`,"logs.tokens.cacheWrite":`önbellek yazma`,"logs.tokens.reasoning":`akıl yürütme`,"logs.tokens.noCache":`önbellek verisi yok`,"logs.tokens.contextTotal":`aktif bağlam`,"logs.tokens.noCacheNote":`bu sağlayıcı önbellek jetonlarını bildirmiyor`,"logs.tokens.noCacheCursor":`Cursor önbellek detayı bildirilmedi`,"logs.tokens.noCacheCursorNote":`Cursor önbellek jeton sayılarını sunmaz`,"logs.tokens.estimatedNote":`tahmini kullanım`,"logs.details":`Detaylar`,"logs.detailTitle":`İstek detayları`,"logs.detailRaw":`Ham günlük kaydı`,"debug.title":`Hata Ayıklama`,"debug.subtitle":`Sağlayıcı taşıma ve kullanım çıkarma tanılamaları.`,"debug.debug":`Sağlayıcı hata ayıklama`,"debug.usage":`Kullanım çıkarma`,"debug.injection":`Enjeksiyon günlüğü`,"debug.claude":`Claude gelen istekler`,"debug.claudeInbound.title":`Claude gelen istekleri`,"debug.claudeInbound.sub":`Claude Code/Desktop tarafından gönderilen istekler.`,"debug.claudeInbound.empty":`Henüz istek yakalanmadı.`,"debug.claudeInbound.time":`Zaman`,"debug.claudeInbound.endpoint":`Uç nokta`,"debug.claudeInbound.model":`Model`,"debug.claudeInbound.none":`yok`,"debug.reset":`Sıfırla`,"debug.refresh":`Yenile`,"debug.follow":`Takip Et`,"debug.streamProvider":`Sağlayıcı`,"debug.streamUsage":`Kullanım`,"debug.streamInjection":`Enjeksiyon`,"debug.loading":`Ayarlar yükleniyor…`,"debug.loadFailed":`Ayarlar yüklenemedi.`,"debug.emptyTitle":`Hata ayıklama günlüğü kapalı`,"debug.empty":`Hata ayıklama seçeneğini açın.`,"debug.noLinesTitle":`Satırlar bekleniyor`,"debug.noLines.provider":`Sağlayıcı hata ayıklama açık.`,"debug.noLines.usage":`Kullanım çıkarma açık.`,"debug.noLines.injection":`Enjeksiyon günlüğü açık.`,"usage.title":`Kullanım`,"usage.subtitle":`Proxy'nizden yerel jeton muhasebesi.`,"usage.loading":`Kullanım verileri yükleniyor…`,"usage.empty":`Henüz kullanım kaydedilmedi.`,"usage.loadError":`Kullanım verileri yüklenemedi.`,"usage.range.all":`Tümü`,"usage.range.available":`Mevcut geçmiş`,"usage.historyTruncated":`Toplamlar yalnızca mevcut geçmişi kapsar.`,"usage.historyTruncatedWindow":`Yüklenen satırların istek başlangıç zamanları {start} ile {end} arasındadır. Dosyanın önceki kayıtları okuma sınırı nedeniyle atlandı, bu yüzden seçilen aralık eksik olabilir.`,"usage.range.30d":`30 gün`,"usage.range.7d":`7 gün`,"usage.card.requests":`İstekler`,"usage.card.measured":`Ölçülen`,"usage.card.reported":`Bildirilen`,"usage.card.totalTokens":`Toplam jeton`,"usage.card.cachedTokens":`Önbellek okuma`,"usage.card.cachedTokensHint":`Sağlayıcı önbelleğinden sunulan jetonlar.`,"usage.card.cacheWriteTokens":`önbellek yazma`,"usage.card.coverage":`Kapsam`,"usage.card.activeDays":`Aktif günler`,"usage.section.heatmap":`Günlük aktivite`,"usage.section.overview":`Genel bakış`,"usage.section.models":`Modeller`,"usage.section.providers":`Sağlayıcılar`,"usage.section.coverage":`Kapsam dağılımı`,"usage.workspace.report":`Kullanım raporu`,"usage.workspace.sections":`Kullanım bölümleri`,"usage.coverage.measured":`Ölçülen`,"usage.coverage.reported":`Bildirilen`,"usage.coverage.estimated":`Tahmin edilen`,"usage.coverage.note":`Ölçülen girdiler bildirilen ve tahmin edilen jeton sayılarını içerir.`,"usage.search.models":`Modellerde ara…`,"usage.col.requests":`İstekler`,"usage.col.measured":`Ölçülen`,"usage.col.reported":`Bildirilen`,"usage.col.tokens":`Jetonlar`,"usage.col.share":`Pay`,"usage.heatmap.less":`Daha az`,"usage.heatmap.more":`Daha fazla`,"usage.dayMon":`Pzt`,"usage.dayWed":`Çar`,"usage.dayFri":`Cum`,"usage.heatmap.tooltipTokens":`{tokens} jeton`,"usage.heatmap.tooltipRequests":`{requests} istek`,"nav.storage":`Depolama`,"storage.title":`Depolama`,"storage.subtitle":`CODEX_HOME dizinini nelerin kullandığını görün.`,"storage.loading":`Depolama taranıyor…`,"storage.empty":`CODEX_HOME boş veya eksik.`,"storage.error":`Depolama taraması başarısız oldu.`,"storage.refresh":`Yeniden tara`,"storage.rescanned":`Tarama tamamlandı.`,"storage.card.total":`Toplam boyut`,"storage.card.files":`Dosyalar`,"storage.card.home":`CODEX_HOME`,"storage.snapshot.lastScan":`Son tarama`,"storage.snapshot.scanning":`Taranıyor…`,"storage.snapshot.unavailable":`Henüz tarama yok.`,"storage.cleanupCard.title":`Alan açın`,"storage.cleanupCard.tabs":`Temizleme seçenekleri`,"storage.cleanupCard.tab.policy":`Politika`,"storage.cleanupCard.tab.quarantine":`Karantina`,"storage.cleanup.noArchives":`Temizlenecek arşivlenmiş oturum yok.`,"storage.section.buckets":`Kovalar`,"storage.section.largest":`En büyük dosyalar`,"storage.workspace.overview":`Genel Bakış`,"storage.workspace.selectBucket":`Ayrıntıları görmek için listeden bir kova seçin.`,"storage.col.bucket":`Kova`,"storage.col.size":`Boyut`,"storage.col.files":`Dosyalar`,"storage.col.oldest":`En eski`,"storage.col.newest":`En yeni`,"storage.col.rows":`DB satırları`,"storage.rows.unknown":`bilinmiyor (kilitli)`,"storage.bucket.sessions":`Aktif oturumlar`,"storage.bucket.archived_sessions":`Arşivlenmiş oturumlar`,"storage.bucket.logs_db":`Günlük veritabanı`,"storage.bucket.state_db":`Durum veritabanı`,"storage.bucket.attachments":`Eklentiler`,"storage.bucket.deletion_manifests":`Silme bildirimleri`,"storage.bucket.other":`Diğer`,"storage.cleanup.title":`Arşiv temizleme`,"storage.cleanup.help":`En eski arşivlenmiş oturumları yüzdeye göre kaldırın.`,"storage.cleanup.slider":`En eski arşivlenen yüzde`,"storage.cleanup.percent":`%{percent}`,"storage.cleanup.preset":`{percent}`,"storage.cleanup.preview":`Önizleme`,"storage.cleanup.confirmTitle":`Arşiv temizliğini onayla`,"storage.cleanup.confirmBody":`Toplam ~{size} tutan {count} arşivlenmiş oturum dosyası silinsin mi (%{percent} eşiği)?`,"storage.cleanup.moreFiles":`…ve {n} tane daha`,"storage.cleanup.permanent":`Kalıcı olarak sil (karantinayı atla)`,"storage.cleanup.permanentWarn":`Kalıcı silme geri alınamaz.`,"storage.cleanup.quarantineNote":`Dosyalar CODEX_HOME/.trash altına taşınır.`,"storage.cleanup.cancel":`İptal`,"storage.cleanup.confirmQuarantine":`Karantinaya Al`,"storage.cleanup.confirmPermanent":`Kalıcı Olarak Sil`,"storage.cleanup.doneQuarantine":`{count} dosya karantinaya alındı ({size}).`,"storage.cleanup.donePermanent":`{count} dosya kalıcı olarak silindi ({size}).`,"storage.cleanup.previewFailed":`Önizleme başarısız oldu.`,"storage.cleanup.cleanupFailed":`Temizleme başarısız oldu.`,"storage.cleanup.err.codex_busy":`Codex state.sqlite dosyasını kullanıyor.`,"storage.cleanup.err.stale_preview":`Önizlemeden sonra arşivlenmiş dosyalar değişti.`,"storage.cleanup.err.restore_pending_overlap":`Seçilen arşivler tamamlanmamış bir geri yükleme ile çakışıyor.`,"storage.cleanup.err.referenced_history":`Seçilen arşivler hâlâ geçmiş tarafından referans gösteriliyor.`,"storage.cleanup.err.invalid_digest":`Önizleme özeti eksik veya geçersiz.`,"storage.cleanup.err.invalid_mode":`Temizleme modu karantina veya kalıcı olmalıdır.`,"storage.cleanup.err.fs_failed":`Dosya sistemi temizliği başarısız oldu. Bazı dosyalar başarısızlıktan önce taşınmış olabilir; CODEX_HOME/.trash veya hedef dizini inceleyin.`,"storage.cleanup.err.fs_failed_trash":`{trashDir} için çöp kutusu işlemi başarısız oldu.`,"storage.cleanup.err.db_reconcile_failed":`Codex durum veritabanı güncellenemedi.`,"storage.cleanup.err.cleanup_failed":`Temizleme başarısız oldu.`,"storage.trash.title":`Karantina`,"storage.trash.help":`CODEX_HOME/.trash altına taşınan arşivlenmiş oturumlar.`,"storage.trash.empty":`Karantinaya alınmış öğe yok.`,"storage.trash.loading":`Karantina yükleniyor…`,"storage.trash.col.when":`Karantinaya Alındı`,"storage.trash.col.files":`Dosyalar`,"storage.trash.col.size":`Boyut`,"storage.trash.col.mode":`Mod`,"storage.trash.col.id":`Girdi`,"storage.trash.restore":`Geri Yükle`,"storage.trash.confirmTitle":`Karantina girdisi geri yüklensin mi?`,"storage.trash.confirmBody":`{id} kimliğinden {count} dosya (~{size}) geri yüklensin mi?`,"storage.trash.cancel":`İptal`,"storage.trash.confirmRestore":`Geri Yükle`,"storage.trash.done":`{count} dosya geri yüklendi ({size}).`,"storage.trash.restoreFailed":`Geri yükleme başarısız oldu.`,"storage.trash.listFailed":`Karantina girdileri listelenemedi.`,"storage.trash.mode.quarantine":`karantina`,"storage.trash.mode.permanent":`kalıcı (tamamlanmamış)`,"storage.trash.err.codex_busy":`Codex state.sqlite dosyasını kullanıyor.`,"storage.trash.err.invalid_trash":`Çöp girdisi ID'si eksik veya geçersiz.`,"storage.trash.err.missing_trash":`Çöp girdisi bulunamadı.`,"storage.trash.err.dest_exists":`Geri yükleme hedefi zaten mevcut.`,"storage.trash.err.fs_failed":`Dosya sistemi geri yüklemesi başarısız oldu.`,"storage.trash.err.db_reconcile_failed":`Veritabanı satırları geri yüklenemedi.`,"storage.trash.err.storage_mutation_busy":`Başka bir depolama eylemi devam ediyor.`,"storage.trash.err.restore_failed":`Geri yükleme başarısız oldu.`,"storage.trash.err.restore_worker_timeout":`Geri yükleme çok uzun sürdü.`,"storage.trash.err.restore_worker_aborted":`Geri yükleme iptal edildi.`,"storage.trash.err.restore_worker_failed":`Geri yükleme işleyicisi çöktü.`,"storage.policy.title":`Otomatik temizleme politikası`,"storage.policy.help":`İsteğe bağlı toplu temizleme politikası.`,"storage.policy.loading":`Politika yükleniyor…`,"storage.policy.loadFailed":`Temizleme politikası yüklenemedi.`,"storage.policy.saveFailed":`Temizleme politikası kaydedilemedi.`,"storage.policy.runFailed":`Politika çalıştırması başarısız oldu.`,"storage.policy.alreadyRunning":`Bir politika çalıştırması zaten devam ediyor.`,"storage.policy.invalid":`Geçersiz politika değerleri.`,"storage.policy.enabled":`Otomatik temizlemeyi etkinleştir`,"storage.policy.enabledHint":`Varsayılan olarak kapalıdır.`,"storage.policy.threshold":`Arşivlenen boyut aşıldığında (GiB)`,"storage.policy.trigger":`Tetikleyici`,"storage.policy.target":`Temizleme hedefi`,"storage.policy.targetPercent":`En eski arşivlenenleri kaldır (%)`,"storage.policy.targetReduce":`Arşiv boyutunu düşür (GiB)`,"storage.policy.thresholdInc":`Eşiği artır`,"storage.policy.thresholdDec":`Eşiği azalt`,"storage.policy.percentInc":`Yüzdeyi artır`,"storage.policy.percentDec":`Yüzdeyi azalt`,"storage.policy.reduceInc":`Hedef boyutu artır`,"storage.policy.reduceDec":`Hedef boyutu azalt`,"storage.policy.schedule":`Zamanlama`,"storage.policy.schedule.manual":`Yalnızca manuel`,"storage.policy.schedule.startup":`Proxy başlangıcında`,"storage.policy.schedule.daily":`Günlük`,"storage.policy.schedule.weekly":`Haftalık`,"storage.policy.mode":`Silme modu`,"storage.policy.mode.quarantine":`Karantina (varsayılan)`,"storage.policy.mode.permanent":`Kalıcı silme`,"storage.policy.permanentWarn":`Kalıcı mod geri alınamaz.`,"storage.policy.lastRun":`Son çalıştırma`,"storage.policy.lastRunDetail":`{count} kaldırıldı · {size} alan açıldı`,"storage.policy.nextRun":`Sonraki çalıştırma`,"storage.policy.never":`Hiçbir zaman`,"storage.policy.save":`Kaydet`,"storage.policy.runNow":`Şimdi çalıştır`,"storage.policy.running":`Çalıştırılıyor…`,"storage.policy.saved":`Politika kaydedildi.`,"storage.policy.skippedDisabled":`Politika devre dışı.`,"storage.policy.skippedUnder":`Arşiv boyutu eşiğin altında.`,"storage.policy.skippedEmpty":`Hedefle eşleşen aday yok.`,"storage.policy.doneQuarantine":`Politika {count} dosyayı karantinaya aldı ({size}).`,"storage.policy.donePermanent":`Politika {count} dosyayı kalıcı olarak sildi ({size}).`,"storage.policy.metadataSaveWarning":`Politika çalışması tamamlandı ancak zamanlama meta verileri kaydedilemedi.`,"modal.addNamed":`Ekle: {label}`,"modal.add":`Sağlayıcı ekle`,"modal.search":`Sağlayıcılarda ara…`,"modal.logInWith":`{label} ile giriş yap`,"modal.waitingBrowser":`Tarayıcı bekleniyor…`,"modal.providerName":`Sağlayıcı adı`,"modal.adapter":`Adaptör`,"modal.baseUrl":`Taban URL`,"modal.endpoint":`Uç nokta`,"modal.endpoint.tokenPlan":`Jeton planı`,"modal.endpoint.payAsYouGo":`Kullandıkça öde`,"modal.endpoint.custom":`Özel`,"modal.defaultModel":`Varsayılan model (isteğe bağlı)`,"modal.allowPrivateNetwork":`Yerel/özel ağa izin ver`,"modal.allowPrivateNetworkHint":`Yalnızca yerel barındırılan sağlayıcılar için etkinleştirin.`,"modal.nameRequired":`Sağlayıcı adı gereklidir`,"modal.baseUrlRequired":`Taban URL gereklidir`,"modal.networkError":`Ağ hatası — proxy çalışıyor mu?`,"modal.loginFailStart":`Giriş başlatılamadı`,"modal.waitingLogin":`Tarayıcı girişi bekleniyor…`,"modal.loggingIn":`Giriş yapılıyor…`,"modal.loginTimeout":`Giriş zaman aşılanına uğradı.`,"modal.back":`Geri`,"modal.badge.oauth":`OAuth`,"modal.customProvider":`Özel sağlayıcı`,"modal.failedStatus":`Başarısız ({status})`,"modal.loginError":`Giriş hatası: {error}`,"modal.badge.codexLogin":`Codex girişi`,"modal.badge.local":`Yerel`,"modal.badge.apiKey":`API anahtarı`,"modal.badge.direct":`Doğrudan`,"modal.badge.pool":`Havuz`,"modal.badge.free":`Ücretsiz`,"modal.invalidPreset":`Bu yerleşik sağlayıcı ayarı eksik.`,"modal.freeTierTitle":`Ücretsiz katman`,"modal.freeTierDefault":`API anahtarı gerekmez. Doğrudan çalışır.`,"modal.tab.accounts":`Hesaplar`,"modal.tab.free":`Ücretsiz`,"modal.tab.paid":`Ücretli`,"modal.accountsHint":`ChatGPT/Codex ve OAuth hesaplarına buradan giriş yapın.`,"modal.accountsCodexAuthLink":`Codex Kimlik Doğrulaması`,"modal.notListed":`Sağlayıcı listede yok mu? Özel sağlayıcı ekleyin`,"modal.catalogLoading":`Katalog yükleniyor…`,"modal.accountLogin":`Giriş yap`,"modal.accountLogout":`Çıkış yap`,"modal.accountAdd":`Hesap ekle`,"modal.accountManage":`Yönet`,"modal.accountCodexPool":`ChatGPT hesap havuzu`,"modal.accountLoggedIn":`Giriş yapıldı`,"modal.accountLoggedOut":`Giriş yapılmadı`,"quota.fiveHourLimit":`5 saatlik limit`,"quota.ageMinutes":`{n} dk`,"quota.ageHours":`{n} sa`,"quota.ageDays":`{n} g`,"quota.observedAgo":`{age} önce alındı`,"quota.observedHint":`Meta kullanımı yalnızca akış yanıtı sırasında bildirir; bu canlı bir ölçüm değil, en son alınan değerdir.`,"quota.weeklyLimit":`Haftalık limit`,"quota.monthlyLimit":`30 günlük limit`,"quota.cursorFirstParty":`Birinci taraf modeller`,"quota.cursorApiUsage":`API kullanımı`,"quota.totalSubscriptionCredits":`Toplam abonelik kredileri`,"quota.creditsBalance":`Kredi bakiyesi`,"quota.creditsPeriodEnds":`Faturalandırma dönemi {date} tarihinde sona erer`,"quota.usedPercent":`%{pct} kullanıldı`,"quota.limitReached":`Limite ulaşıldı`,"quota.resetsToday":`Bugün {time} saatinde sıfırlanır`,"quota.resetsTomorrow":`Yarın {time} saatinde sıfırlanır`,"quota.resetsAt":`{when} sıfırlanır`,"quota.resetsRelativeMinutes":`{n} dakika içinde sıfırlanır`,"quota.resetsRelativeHours":`{n} saat içinde sıfırlanır`,"pws.status.ready":`Hazır`,"pws.status.needsSetup":`Kurulum gerekiyor`,"pws.status.needsAttention":`Dikkat gerekiyor`,"pws.auth.chatgptPassthrough":`ChatGPT doğrudan geçiş`,"pws.auth.noKey":`Anahtar gerekmiyor`,"pws.freeTitle":`Ücretsiz fiyatlandırma`,"pws.localTitle":`Yerel çalışma zamanı`,"pws.modelCountOne":`1 model`,"pws.modelCount":`{count} model`,"pws.rail.suffixDefault":` · varsayılan`,"pws.rail.suffixLocal":` · yerel`,"pws.rail.suffixFree":` · ücretsiz`,"pws.rail.selectAria":`{name} seç — {status}{suffix}`,"pws.searchPlaceholder":`Sağlayıcılarda ara…`,"pws.filterAria":`Sağlayıcıları filtrele`,"pws.providerFiltersAria":`Sağlayıcı filtreleri`,"pws.filters":`Filtreler`,"pws.filterStatus":`Durum`,"pws.pricing":`Fiyatlandırma`,"pws.paid":`Ücretli`,"pws.filterType":`Tür`,"pws.type.cloud":`Bulut`,"pws.type.local":`Yerel`,"pws.type.selfHosted":`Kendi barındırdığı`,"pws.type.login":`Giriş yap`,"pws.sort":`Sırala`,"pws.sortProvidersAria":`Sağlayıcıları sırala`,"pws.sort.az":`A–Z`,"pws.sort.za":`Z–A`,"pws.sort.freePaid":`Önce ücretsizler`,"pws.sort.paidFree":`Önce ücretliler`,"pws.sort.accountsFirst":`Önce hesaplar`,"pws.resetAll":`Tümünü sıfırla`,"pws.providerList":`Sağlayıcı listesi`,"pws.providersAria":`Sağlayıcılar`,"pws.groupReady":`Hazır ({count})`,"pws.groupNeedsSetup":`Kurulum gerekiyor ({count})`,"pws.groupDisabled":`Devre dışı ({count})`,"pws.noSearchResults":`Aramanızla eşleşen sağlayıcı yok.`,"pws.noMatchFilters":`Filtrelerle eşleşen sağlayıcı yok.`,"pws.noProvidersConfigured":`Yapılandırılmış sağlayıcı yok.`,"pws.workspaceMainAria":`Sağlayıcı detayları`,"pws.detailComingSoon":`Detay görünümü yakında geliyor.`,"pws.selectPrompt":`Listeden bir sağlayıcı seçin.`,"pws.connectFirst":`İlk sağlayıcınızı bağlayın`,"pws.empty.browseFree":`Ücretsiz sağlayıcılara göz atın`,"pws.empty.browseFreeDesc":`Abonelik olmadan başlayın`,"pws.empty.connectAccount":`Bir hesap bağlayın`,"pws.empty.connectAccountDesc":`ChatGPT veya sağlayıcı girişinizi kullanın`,"pws.empty.addEndpoint":`Bir uç nokta ekleyin`,"pws.empty.addEndpointDesc":`Özel taban URL ve API anahtarı`,"pws.tab.overview":`Genel Bakış`,"pws.tab.models":`Modeller`,"pws.tab.usage":`Kullanım`,"pws.tab.accounts":`Hesaplar`,"pws.tab.settings":`Ayarlar`,"pws.connection":`Bağlantı`,"pws.status.connected":`Bağlandı`,"pws.attentionTitle":`Dikkat gerekiyor`,"pws.attention.reauth":`Aktif hesap yeniden doğrulama gerektiriyor`,"pws.attention.reauthForward":`Aktif Codex hesabı yeniden doğrulama gerektiriyor`,"pws.attention.missingCredentials":`Kimlik bilgileri eksik`,"pws.cell.auth":`Kimlik Doğrulama`,"pws.cell.note":`Not`,"pws.cell.defaultModel":`Varsayılan model`,"pws.statsAria":`Sağlayıcı istatistikleri`,"pws.statsTitle":`İstatistikler`,"pws.stats.totalRequests":`İstekler (30 gün)`,"pws.stats.totalTokens":`Jetonlar (30 gün)`,"pws.stats.quotaUpdated":`Kota güncellendi`,"pws.stats.quotaTracked":`Oran limitleri Kullanım sekmesinde takip edilir.`,"pws.stats.source":`Kaynak`,"pws.usageLast30d":`Kullanım (son 30 gün)`,"pws.estimatedCost":`Tahmini maliyet`,"pws.costDisclaimer":`API liste fiyatı tahminidir.`,"pws.modelBreakdown":`Model dağılımı`,"pws.col.model":`Model`,"pws.col.cost":`Tahm. maliyet`,"pws.col.tokens":`Jetonlar`,"pws.col.requests":`İst.`,"pws.col.share":`Pay`,"pws.tokenInput":`Girdi`,"pws.tokenOutput":`Çıktı`,"pws.metricRequests":`istek`,"pws.metricTokens":`jeton`,"pws.usageUnavailable":`Henüz kullanım kaydedilmedi.`,"pws.rateLimits":`Oran limitleri`,"pws.quotaUnavailable":`Bu sağlayıcı için kota verisi yok.`,"pws.accountQuotaUnavailable":`Oran limiti verisi geçici olarak kullanılamıyor.`,"pws.selected":`Seçilen`,"pws.copyModelId":`ID Kopyala`,"pws.modelCopied":`Kopyalandı!`,"pws.modelsAvailable":`{count} kullanılabilir`,"pws.modelSearchPlaceholder":`Modelleri filtrele…`,"pws.modelsLoading":`Modeller yükleniyor…`,"pws.modelsLoadFailed":`Modeller yüklenemedi.`,"pws.modelsNeedsReauth":`Hesabın yeniden giriş yapması gerekiyor.`,"pws.modelsConfiguredFallback":`Yapılandırılmış modeller gösteriliyor.`,"pws.modelsTruncated":`{total} modelden ilk {shown} tanesi gösteriliyor.`,"pws.retry":`Tekrar Dene`,"pws.noModels":`Bu sağlayıcı için keşfedilen model yok.`,"pws.noModelMatch":`Filtreyle eşleşen model yok.`,"pws.adapterBaseRequired":`Adaptör ve taban URL gereklidir.`,"pws.addAccount":`Hesap ekle`,"pws.addKey":`API anahtarı ekle`,"pws.apiKeys":`API Anahtarları`,"pws.authMode":`Kimlik doğrulama modu`,"pws.availableAccounts":`Mevcut hesaplar`,"pws.accountOrdinal":`Hesap {count}`,"pws.accountsLoading":`Hesaplar yükleniyor…`,"pws.accountsLoadFailed":`Hesaplar yüklenemedi.`,"pws.retryAccounts":`Tekrar Dene`,"pws.noAccounts":`Henüz bağlı hesap yok.`,"pws.cockpitImportDescription":`Bu cihazdan bir Cockpit Tools Antigravity JSON dışa aktarımını içe aktarın. Dosya içeriği gösterilmez.`,"pws.cockpitImportFileLabel":`Cockpit Tools Antigravity JSON dışa aktarımı`,"pws.cockpitImportChooseFile":`JSON dosyası seç`,"pws.cockpitImporting":`İçe aktarılıyor…`,"pws.cockpitImportInvalid":`Seçilen dosya geçerli bir JSON dışa aktarımı değil veya çok büyük.`,"pws.cockpitImportFailed":`Hesap içe aktarımı tamamlanamadı.`,"pws.cockpitImportComplete":`İçe aktarma tamamlandı: {imported} içe aktarıldı, {updated} güncellendi, {failed} başarısız, {unsupported} desteklenmiyor.`,"pws.accountSwitching":`Değiştiriliyor…`,"pws.accountCurrent":`Mevcut hesap`,"pws.defaultModelNone":`Yok (sağlayıcı varsayılanını kullan)`,"pws.discardSettings":`Vazgeç`,"pws.jsonEditorDesc":`Ham sağlayıcı JSON konfigürasyonunu düzenleyin.`,"pws.jsonEditorTitle":`JSON düzenleyici — {name}`,"pws.jsonRestore":`Geri Yükle`,"pws.jsonSave":`Kaydet`,"pws.loggedInTitle":`Giriş yapıldı`,"pws.notLoggedInTitle":`Giriş yapılmadı`,"pws.note":`Not`,"pws.allowPrivateNetwork":`Yerel/özel ağa izin ver`,"pws.liveModels":`Sağlayıcıdan canlı model keşfet`,"pws.liveModelsDesc":`Sağlayıcının canlı model kataloğunu çekin.`,"pws.xaiResponsesOptIn":`Grok 4.5 ve 4.6 için Responses API kullan`,"pws.xaiResponsesOptInDesc":`İki modeli de openai-responses üzerinden yönlendirir. Diğer Grok modelleri ve katman davranışı değişmez.`,"pws.xaiResponsesOptInMixed":`Kısmen etkin.`,"pws.cursorTransport":`Cursor aktarımı`,"pws.cursorTransportHttp2":`HTTP/2 (varsayılan)`,"pws.cursorTransportHttp1":`HTTP/1.1 (proxy uyumluluğu)`,"pws.cursorTransportDesc":`Proxy'niz Cursor'ın HTTP/2 akışını güvenilir biçimde taşıyamıyorsa HTTP/1.1 kullanın.`,"pws.optionalPlaceholder":`İsteğe bağlı`,"pws.providerId":`Sağlayıcı ID`,"pws.reauth":`Yeniden doğrulama gerekiyor`,"pws.reauthenticate":`Yeniden doğrula`,"pws.copyDoctor":`ocx doctor kopyala`,"pws.doctorCopied":`Kopyalandı`,"pws.doctorCopyUnavailable":`Pano kullanılamıyor`,"pws.healthCooldownHint":`Soğuma süresi bitene kadar bekleyin.`,"pws.healthLabel.rateLimited":`Oran kısıtlandı`,"pws.healthLabel.quotaLimited":`Kota kısıtlandı`,"pws.healthLabel.reauthRequired":`Yeniden doğrulama gerekli`,"pws.healthLabel.refreshFailed":`Yenileme başarısız oldu`,"pws.healthLabel.metadataMismatch":`Meta veri uyuşmazlığı`,"pws.healthLabel.credentialConflict":`Kimlik bilgisi çakışması`,"pws.healthSummary.rateLimited":`{provider} {account}: {until} tarihine kadar oran kısıtlandı.`,"pws.healthSummary.quotaLimited":`{provider} {account}: {until} tarihine kadar kota kısıtlandı.`,"pws.healthSummary.reauthRequired":`{provider} {account}: yeniden doğrulama gerekli.`,"pws.healthSummary.credentialConflict":`{provider} {account}: kimlik bilgisi çakışması.`,"pws.healthSummary.metadataMismatch":`{provider} {account}: meta veri uyuşmazlığı.`,"pws.healthSummary.staleCredentials":`{provider} {account}: eksik kimlik bilgileri.`,"pws.removeConfirm":`Kaldır`,"pws.removeConfirmBody":`"{name}" sağlayıcısı kaldırılsın mı?`,"pws.removeDefaultConfirmBody":`Varsayılan sağlayıcı "{name}" kaldırılsın mı? "{defaultProvider}" varsayılan sağlayıcı olacaktır. Bu işlem geri alınamaz.`,"pws.removeConfirmTitle":`Sağlayıcıyı kaldır`,"pws.saveSettings":`Kaydet`,"pws.pacingTitle":`İstek aralığı`,"pws.pacingDesc":`Bu sağlayıcıya giden istek başlangıçlarını eşit aralıklarla geciktirir. Akış yanıtları çakışabilir.`,"pws.pacingEnabled":`Etkin`,"pws.pacingRpm":`Dakikadaki istek`,"pws.pacingRpmUnit":`RPM`,"pws.pacingDelay":`En kısa aralık (ms)`,"pws.pacingSlowerWins":`Daha yavaş sağlayıcı sınırı geçerlidir. Model kuralları yalnızca ek gecikme getirir.`,"pws.pacingQueued":`kuyrukta`,"pws.pacingNextSlot":`sonraki aralığa`,"pws.pacingLastModel":`son model`,"pws.pacingNone":`Yok`,"pws.pacingModelOverrides":`Model kuralları`,"pws.pacingModel":`Model`,"pws.pacingAdd":`Kural ekle`,"pws.pacingRemove":`Kaldır`,"pws.pacingRemoveModel":`{model} için istek aralığı kuralını kaldır`,"pws.pacingRuleRequired":`Önce bir sağlayıcı sınırı veya model kuralı belirleyin.`,"pws.saving":`Kaydediliyor…`,"pws.settingsSaved":`Ayarlar kaydedildi.`,"pws.accountModeSaved":`Hesap modu kaydedildi.`,"pws.accountModeFailed":`Hesap modu değiştirilemedi.`,"pws.accountModeConfirm":`OpenAI hesap modu değiştirilsin mi? Aktif oturumlar hedef hesap havuzuna yönlendirilecek ve kota yeni mod altında birikecektir.`,"pws.settingsUnsavedBar":`Kaydedilmemiş değişiklikleriniz var.`,"pws.unsavedLeaveBody":`Kaydedilmemiş değişiklikleriniz var. Ayrılmadan önce kaydetmek ister misiniz?`,"pws.unsavedLeaveTitle":`Kaydedilmemiş değişiklikler`,"pws.attentionRequired":`Dikkat gerekiyor`,"pws.attentionAria":`{name}: {reason}`,"pws.missingCredentials":`Kimlik bilgileri eksik`,"pws.editJsonDesc":`Proxy konfigürasyonunu JSON olarak düzenleyin`,"pws.updatesUnavailable":`Sağlayıcı güncellemeleri mevcut değil.`,"pws.dashboard.title":`Sağlayıcılara genel bakış`,"pws.dashboard.subtitle":`Tüm model sağlayıcılarınızı tek bir yerden yönetin.`,"pws.dashboard.rateLimits":`ORAN LİMİTLERİ`,"pws.capacity.estimate":`Havuz kapasite tahmini`,"pws.capacity.currentAccount":`Mevcut geçerli hesap`,"pws.capacity.nextRecovery":`Sonraki kapasite yenilenmesi`,"pws.capacity.recoveryShare":`+%{percent} havuz kapasitesi`,"pws.capacity.incomplete":`Kısmi pencere kapsamı ({excluded} hariç tutuldu)`,"pws.capacity.uncalibratedPlan":`Kalibre edilmemiş plandaki {count} hesap temel koltuk ağırlığıyla sayılır; bu tahmin ihtiyatlı olabilir`,"pws.capacity.partial":`Kısmi ({count} hesap kota metriği bildiriyor)`,"pws.capacity.windowPartial":`Kısmi`,"pws.capacity.windowPartialA11y":`{window}: eksik hesap kapsamı`,"pws.dashboard.recentlyUsed":`SON KULLANILANLAR`,"pws.dashboard.requests":`{count} istek`,"pws.dashboard.checkedAgo":`{time} önce kontrol edildi`,"pws.dashboard.noQuota":`Kota verisi yok`,"pws.dashboard.noUsage":`Henüz kullanım verisi yok`,"pws.dashboard.noRateLimits":`Henüz oran limiti verisi yok`,"pws.allProviders":`Sağlayıcı Genel Bakışı`,"pws.enabledLabel":`Etkin`,"pws.testConnection":`Bağlantıyı test et`,"pws.testing":`Test ediliyor…`,"pws.connectionOk":`Bağlantı Başarılı`,"pws.connectionFailed":`Bağlantı Başarısız`,"pws.connectionNotApplicable":`Uygulanamaz — bu sağlayıcı statik katalog kullanıyor.`,"pws.editSettings":`Ayarları düzenle`,"pws.viewUsage":`Detaylı kullanımı gör`,"pws.allSystemsOk":`Tüm sistemler çalışır durumda`,"pws.apiKeyConfigured":`API anahtarı yapılandırıldı`,"pws.addApiKey":`API anahtarı ekle`,"pws.loggedInAs":`{email} olarak giriş yapıldı`,"pws.notLoggedIn":`Giriş yapılmadı`,"pws.passthrough":`Codex doğrudan geçiş`,"pws.notes":`NOTLAR`,"pws.notePlaceholder":`Bu sağlayıcı hakkında bir not ekleyin...`,"pws.noteSaved":`Not kaydedildi`,"pws.authSummary":`KİMLİK DOĞRULAMA`,"time.justNow":`Az önce`,"time.notChecked":`Kontrol edilmedi`,"time.minutesAgo":`{n}dk önce`,"time.hoursAgo":`{n}sa önce`,"time.daysAgo":`{n}gün önce`,"modal.noMatch":`Eşleşme yok.`,"modal.oauthDefaultNote":`Hesabınızla giriş yapın — API anahtarı gerekmez.`,"modal.oauthComingSoon":`{label} için OAuth girişi gelecektir. Şimdilik API anahtarı kullanın.`,"modal.oauthComingSoonShort":`Bu sağlayıcı için OAuth girişi gelecektir — şimdilik API anahtarı kullanın.`,"modal.useApiKeyInstead":`Bunun yerine API anahtarı kullanın`,"modal.setupGuide":`Kurulum rehberi`,"modal.setupStep1Prefix":`Adresine gidin:`,"modal.setupDashboardLink":`{label} paneli`,"modal.setupStep1Suffix":`ve API anahtarınızı kopyalayın`,"modal.setupStep2":`Aşağıdaki API anahtarı alanına yapıştırın`,"modal.setupStep3":`Sağlayıcı ekle'ye tıklayın — modeller otomatik keşfedilir`,"modal.namePlaceholder":`örn. openrouter`,"modal.duplicateWarn":`"{name}" sağlayıcısı mevcut ve üzerine yazılacak.`,"modal.forwardHintPrefix":`Anahtar gerekmez — proxy kimlik bilgilerinizi iletir`,"modal.forwardCredentials":`codex girişi`,"modal.forwardHintSuffix":`kimlik bilgilerini bu sağlayıcıya iletir.`,"modal.localHint":`Yerel mod, Cursor'ın statik kamuya açık kataloğunu kaydeder. Canlı taşıma, dosya ve kabuk (shell) araçları denetlenene kadar kapalı kalır.`,"modal.getApiKey":`{label} API anahtarınızı alın`,"modal.apiKey":`API anahtarı`,"modal.apiKeyTransport":`API anahtar başlığı`,"modal.apiKeyTransportNative":`x-api-key (Anthropic yerel)`,"modal.apiKeyTransportBearer":`Authorization: Bearer`,"modal.apiKeyPlaceholder":`sk-… (veya $ENV_VAR)`,"modal.defaultModelPlaceholder":`örn. gpt-5.5`,"modal.baseUrlPlaceholder":`https://...`,"modal.baseUrlPlaceholderError":`Taban URL çözümlenmemiş bir {placeholder} içeriyor.`,"modal.baseUrlPlaceholderHint":`Eklemeden önce {placeholder} değerini gerçek Hesap ID'nizle değiştirin.`,"modal.adding":`Ekleniyor…`,"modal.useOauthLogin":`← OAuth girişini kullan`,"nav.codexAuth":`Codex Kimlik Doğrulama`,"nav.codexSet":`Codex Ayarları`,"codexSet.tab.multiauth":`Çoklu kimlik doğrulama`,"codexSet.tab.prompt":`İstem`,"codexSet.prompt.title":`İstem katmanları`,"codexSet.prompt.timing":`Yeni başlatılan oturumlara uygulanır. Çalışan oturumlar mevcut istem ayarlarını korur.`,"codexSet.prompt.staleRevision":`Yapılandırma başka bir yerde değişti. Liste yeniden yüklendi.`,"codexSet.prompt.writeFailed":`Değişiklik kaydedilemedi.`,"codexSet.prompt.loadFailed":`İstem katmanları yüklenemedi.`,"codexSet.prompt.repair":`Onar`,"codexSet.prompt.repairFailed":`Onarım tamamlanamadı.`,"codexSet.drift.journalPresent":`Önceki yazma işlemi tamamlanmadı. Kurtarma bir sonraki yazma sırasında otomatik olarak çalışır.`,"codexSet.drift.projectionStale":`Kaydedilen katmanlar ile config.toml içindeki değer uyuşmuyor. Onarım, değeri katmanlarınıza göre yeniden yazar.`,"codexSet.drift.storeMissing":`Katman dosyası yok ancak config.toml içinde talimatlar duruyor. Onarım önce yedek alır ve metni tek bir katman olarak saklar.`,"codexSet.drift.ownedMalformed":`config.toml içinde oluşturulan satır elle değiştirilmiş, bu yüzden yeniden yazmak artık güvenli değil.`,"codexSet.custom.adoptUnsupported":`{path} dosyasının {line}. satırındaki değer tek satırlık bir dizge olmadığından içe aktarılamaz. Burada yönetmek için elle taşıyın.`,"codexSet.prompt.unreadable":`Codex yapılandırma dosyası var ancak okunamadı, bu yüzden değişiklikler reddedildi.`,"codexSet.layer.permissions":`İzinler`,"codexSet.layer.collaboration":`İş birliği modu`,"codexSet.layer.environment":`Ortam bağlamı`,"codexSet.layer.apps":`Uygulamalar`,"codexSet.layer.skills":`Beceriler`,"codexSet.prompt.extensionsUnknown":`Uzantılar kendi katmanlarını ekleyebilir. Codex bunları göstermediği için burada listelenemez.`,"codexSet.group.transition":`Geçiş bildirimleri`,"codexSet.group.transitionDesc":`Durumu anlatmak yerine bir değişikliği bildirirler; bu yüzden yalnızca oturum gerçek zamanlıya geçtiğinde veya model değiştiğinde görünürler.`,"codexSet.custom.slotNote":`Özel katmanlar bu sırayla birleştirilip tek bir bölüm olur.`,"codexSet.row.alwaysOn":`Her zaman açık`,"codexSet.row.onChange":`Değişimde gönderilir`,"codexSet.row.featureGated":`[features] altında yapılandırılır`,"codexSet.row.openFeatures":`Ayarları aç`,"codexSet.dialog.setValue":`{value} (varsayılan {fallback})`,"codexSet.dialog.copyKey":`Anahtarı kopyala`,"codexSet.dialog.unknownLayer":`Bu derlemede bu katman için açıklama yok. Panelden daha yeni bir Codex çalışma zamanından geliyor.`,"codexSet.custom.heading":`Özel katmanlar`,"codexSet.custom.add":`+ Katman ekle`,"codexSet.custom.newTitle":`Yeni katman`,"codexSet.custom.editTitle":`Katmanı düzenle`,"codexSet.custom.titleLabel":`Başlık`,"codexSet.custom.bodyLabel":`Talimatlar`,"codexSet.custom.bodySize":`{max} baytın {bytes} baytı`,"codexSet.custom.normalized":`Sekmeler dört boşluğa, satır sonları LF biçimine dönüştürüldü.`,"codexSet.custom.titleRequired":`Bir başlık girin.`,"codexSet.custom.titleTooLong":`Başlık {count} karakter; sınır {max}.`,"codexSet.custom.titleMultiline":`Başlık tek satır olmalıdır.`,"codexSet.custom.bodyTooLarge":`Bu katman {bytes} bayt; sınır {max}.`,"codexSet.custom.composedTooLarge":`Etkin katmanların toplamı {bytes} bayt olacak ve sınırı aşacak.`,"codexSet.custom.invalidCharacter":`{position} konumundaki denetim karakteri kaydedilemez.`,"codexSet.custom.discardPrompt":`Değişiklikler silinsin mi?`,"codexSet.custom.keepEditing":`Düzenlemeye devam et`,"codexSet.custom.delete":`{title} katmanını sil`,"codexSet.custom.deleteConfirm":`Bu katman silinsin mi? Bu işlem geri alınamaz.`,"codexSet.custom.layerGone":`Bu katman başka bir yerde silindiği için düzenleyici kapatıldı.`,"codexSet.custom.deleteConfirmNamed":`“{title}” silinsin mi? Bu işlem geri alınamaz.`,"codexSet.custom.moveUp":`{title} katmanını yukarı taşı`,"codexSet.custom.prevLayer":`Önceki katman`,"codexSet.custom.nextLayer":`Sonraki katman`,"codexSet.custom.navPosition":`{position} / {total}`,"codexSet.custom.moveDown":`{title} katmanını aşağı taşı`,"codexSet.custom.limitReached":`En fazla {max} özel katman saklayabilirsiniz.`,"codexSet.custom.notOwned":`developer_instructions opencodex dışında yazıldığı için buradan düzenlenemez. Katman olarak yönetmek için içe aktarın.`,"codexSet.custom.adopt":`Mevcut talimatları içe aktar`,"codexSet.custom.adoptConfirm":`Katman olarak içe aktar`,"codexSet.custom.adoptRefused":`Mevcut değer içe aktarılamadı.`,"codexSet.custom.baseReplaced":`model_instructions_file {path} olarak ayarlandığından, opencodex dışındaki bir öğe temel istemi değiştirmiş.`,"codexSet.lint.identity":`Bu, Codex tarafından belirlenenden farklı bir kimlik iddia ediyor.`,"codexSet.lint.foreignTool":`Araçlar kayıt defterinden gelir; burada bir aracın adını belirtmek onu oluşturmaz.`,"codexSet.lint.placeholder":`Talimatlar bir şablon motorundan geçmez, bu nedenle bu metin olduğu gibi gönderilir.`,"codexSet.lint.applyPatch":`apply_patch talimatlar tarafından değil, araç kayıt defteri tarafından tanımlanır.`,"codexSet.lint.approvalVocab":`Codex kendi onay terimlerini ekler; bu metin onlarla çelişebilir.`,"codexSet.lint.environment":`Ortam bilgileri daha sonra oluşturulur ve bu metinle çelişebilir.`,"codexSet.lint.size":`Bu katman 8 KB sınırını aşıyor. Yine de kaydedilir, ancak her istekte belirteç harcar.`,"codexSet.preset.blank":`Boş katman`,"codexSet.preset.concise.name":`Kısa çıktı`,"codexSet.preset.concise.description":`Kısa yanıtlar, giriş yok, en az biçimlendirme.`,"codexSet.preset.concise.provenance":`Claude Code'un kısa yanıt talimatlarından uyarlandı. İfade bize aittir, kopya değildir.`,"codexSet.preset.planFirst.name":`Düzenlemeden önce planla`,"codexSet.preset.planFirst.description":`Önce planı belirt, ardından değişikliği yap.`,"codexSet.preset.planFirst.provenance":`Claude Code'un planlama yaklaşımından uyarlandı. İfade bize aittir, kopya değildir.`,"codexSet.preset.explainWhy.name":`Gerekçeyi açıkla`,"codexSet.preset.explainWhy.description":`Yalnızca ne olduğunu değil, nedenini de belirt.`,"codexSet.preset.explainWhy.provenance":`Grok Build'in onaylama tarzından uyarlandı. İfade bize aittir, kopya değildir.`,"codexSet.preset.testFirst.name":`Önce test`,"codexSet.preset.testFirst.description":`Düzeltmeden önce başarısız olan testi yaz.`,"codexSet.preset.testFirst.provenance":`Yaygın ajan uygulamalarından uyarlandı. İfade bize aittir, kopya değildir.`,"codexSet.preset.korean.name":`Korece yanıtlar`,"codexSet.preset.korean.description":`İstek hangi dilde olursa olsun Korece yanıt ver.`,"codexSet.preset.korean.provenance":`Sık istenen bir ihtiyaçtan yola çıkarak opencodex için yazıldı. İfade bize aittir, kopya değildir.`,"codexSet.dialog.class":`Tür`,"codexSet.dialog.key":`Yapılandırma anahtarı`,"codexSet.dialog.fileValue":`Bu dosyadaki değer`,"codexSet.dialog.absentDefault":`ayarlanmamış (varsayılan: {value})`,"codexSet.dialog.noRenderedText":`Codex yerleşik bir katmanın birleştirilmiş metnini göstermez. Bu nedenle bu iletişim kutusu içeriği göstermek yerine katmanı açıklar ve anahtarını belirtir.`,"codexSet.dialog.sourceText":`Modele gönderilen metin`,"codexSet.dialog.sourceBytes":`{bytes} bayt`,"codexSet.dialog.notRendered":`Okuduğumuz turda bu katman hiçbir şey göndermedi. Bölümler yalnızca değiştiklerinde yeniden gönderilir, bu yüzden tek bir örnekte görünmeyebilir.`,"codexSet.dialog.emptySource":`{path} konumundaki dosya var ancak boş, bu yüzden bu katman hiçbir şey göndermiyor.`,"codexSet.dialog.notExposed":`Temel istem, Codex'in yazdırabildiği mesaj listesinin dışından geçtiği için burada gösterilemez. model_instructions_file ile değiştirilebilir.`,"codexSet.dialog.textUnavailable":`Bu makinede Codex istemi okunamadı, bu yüzden metin kullanılamıyor.`,"codexSet.class.base":`Temel talimatlar`,"codexSet.class.config-toggle":`Buradan değiştirilebilir`,"codexSet.class.feature-gated":`Özellik bayraklı`,"codexSet.class.runtime-conditional":`Çalışma zamanına bağlı`,"codexSet.class.extension-unknown":`Uzantı katmanı`,"codexSet.layer.base-instructions":`Temel talimatlar`,"codexSet.layer.model-switch":`Model değişikliği bildirimi`,"codexSet.layer.personality":`Kişilik`,"codexSet.layer.context-window-guidance":`Bağlam penceresi yönlendirmesi`,"codexSet.layer.realtime":`Gerçek zamanlı`,"codexSet.layer.agents-md":`AGENTS.md`,"codexSet.layer.environments-instructions":`Yürütme ortamları`,"codexSet.layer.plugins":`Eklentiler`,"codexSet.layer.tools":`Araçlar`,"codexSet.layer.multi-agent-mode":`Çoklu ajan modu`,"codexSet.layer.git-attribution":`Commit atıfları`,"codexSet.about.base-instructions":`Codex'in kendi talimatlarıdır. İstekle birlikte gönderilir ve kapatılamaz.`,"codexSet.about.model-switch":`Oturum sırasında model değiştiğinde eklenir.`,"codexSet.about.personality":`Bir özellik bayrağının yönettiği ton ve anlatım yönlendirmesi.`,"codexSet.about.context-window-guidance":`Bir özellik bayrağının yönettiği kalan bağlam bütçesi önerileri.`,"codexSet.about.realtime":`Gerçek zamanlı oturumlarda eklenir.`,"codexSet.about.agents-md":`Projenizin AGENTS.md dosyalarıdır. Bu sayfa katmanı gösterir, proje belgelerinizi değiştirmez.`,"codexSet.about.permissions":`Geçerli korumalı alan ve onay ayarlarını açıklar.`,"codexSet.about.collaboration":`Etkin iş birliği modunu açıklar.`,"codexSet.about.environment":`Çalışma dizini, platform ve diğer ortam bilgileri.`,"codexSet.about.environments-instructions":`Bir özellik bayrağının yönettiği ertelenmiş yürütme ortamı talimatları.`,"codexSet.about.apps":`Bağlı uygulamaların nasıl kullanılacağını açıklar.`,"codexSet.about.plugins":`Bir eklenti seçildiğinde veya herhangi bir eklenti bir yetenek bildirdiğinde eklenir.`,"codexSet.about.tools":`Bir özellik bayrağının yönettiği ertelenmiş araç açıklamaları.`,"codexSet.about.skills":`Kullanılabilir becerilerin listesi.`,"codexSet.about.multi-agent-mode":`Bir özellik bayrağının yönettiği alt ajan talimatları.`,"codexSet.about.git-attribution":`Modelin yazdığı commit’lere Co-authored-by: Codex trailer’ını, açtığı pull request’lere de Generated with Codex. satırını eklemesini söyler. Codex bunu hesabınızdan okur; ne burada ne de [features] altında değiştirilebilir. Hesabınız kapattığında Codex hiçbir şey göndermek yerine tersi yönde talimat gönderir.`,"codexSet.condition.model-switch":`Yalnızca oturum sırasında model değiştikten sonra eklenir.`,"codexSet.condition.realtime":`Yalnızca gerçek zamanlı oturumlarda eklenir.`,"codexSet.condition.agents-md":`Çalışma dizini için bir proje belgesi bulunduğunda eklenir.`,"codexSet.condition.plugins":`Bir eklenti seçildiğinde veya herhangi bir eklenti bir yetenek bildirdiğinde eklenir.`,"codexSet.condition.git-attribution":`Hesabınızın atıf politikası belirler.`,"codexSet.base.title":`Temel istem`,"codexSet.base.prev":`Önceki seçenek`,"codexSet.base.next":`Sonraki seçenek`,"codexSet.base.position":`{position} / {total}`,"codexSet.base.swipeHint":`Yana kaydırın, yön tuşlarını ya da ok düğmelerini kullanarak seçenekler arasında geçin. Yeni başlatılan oturumlarda geçerlidir.`,"codexSet.base.defaultTitle":`Codex’in kendi temel istemi`,"codexSet.base.defaultBody":`Varsayılan burada saklanmaz, dolayısıyla düzenlenecek veya silinecek bir şey yoktur: seçmek yapılandırmadan model_instructions_file satırını kaldırır ve Codex kendi istemini kullanır.`,"codexSet.base.variantTitle":`Ad`,"codexSet.base.variantBody":`İstem`,"codexSet.base.replacesWarning":`Bu, Codex’in temel istemine eklemek yerine onun YERİNE geçer. Buraya kısa yazarsanız modelin talimatları da o kadar kısa olur.`,"codexSet.base.use":`Bunu kullan`,"codexSet.base.inUse":`Kullanımda`,"codexSet.base.externalBlocked":`model_instructions_file zaten {path} yolunu gösteriyor ve bunu opencodex yazmadı. Buradan seçim yapmadan önce kendiniz temizleyin.`,"nav.api":`API`,"nav.integrations":`Entegrasyonlar`,"nav.openMenu":`Menüyü aç`,"nav.closeMenu":`Menüyü kapat`,"integrations.subtitle":`İstemcileri opencodex'e bağlayın, kimlik bilgilerini yönetin.`,"integrations.tabsLabel":`Entegrasyon yüzeyleri`,"integrations.tab.overview":`Genel Bakış`,"integrations.tab.keys":`API Anahtarları`,"integrations.tab.codex":`Codex`,"integrations.tab.claude":`Claude`,"integrations.tab.grok":`Grok Build`,"integrations.tab.opencode":`OpenCode`,"integrations.tab.pi":`Pi`,"integrations.tab.omp":`OMP`,"integrations.tab.hermes":`Hermes`,"integrations.tab.openclaw":`OpenClaw`,"integrations.tab.kimi":`Kimi Code`,"integrations.tab.gajae":`Gajae Code`,"integrations.tab.dsh":`DSH`,"integrations.tab.mcode":`MiniMax Code`,"integrations.tab.zcode":`ZCode`,"integrations.tab.prime":`Prime Agent`,"integrations.tab.aside":`Aside`,"integrations.codex.title":`Codex CLI`,"integrations.codex.body":`Codex bağlantısı proxy servisine aittir.`,"integrations.codex.openService":`Servis kontrollerini aç`,"integrations.state.notInstalled":`Yüklü değil`,"integrations.state.unknown":`Kontrol ediliyor…`,"integrations.detail.codexRouted":`Codex istekleri bu proxy üzerinden geçer`,"integrations.detail.codexAbsent":`Codex henüz bu proxy üzerinden yönlendirilmedi`,"integrations.detail.keyCount":`{count} anahtar oluşturuldu`,"integrations.detail.keyNone":`Oluşturulmuş anahtar yok`,"integrations.detail.keyChecking":`Kontrol ediliyor…`,"integrations.detail.keyUnavailable":`Anahtar durumu kullanılamıyor`,"integrations.detail.claudeOff":`Bağlantı kapalı`,"integrations.detail.desktopCurrent":`Desktop bu profili çalıştırıyor`,"integrations.detail.desktopStale":`Profil dosyası değiştirildi`,"integrations.detail.desktopNotServed":`Profil mevcut ancak Desktop başkasını kullanıyor`,"integrations.detail.desktopAbsent":`Uygulanan profil yok`,"integrations.detail.desktopDesiredOff":`Claude Desktop entegrasyonu kapalı`,"integrations.detail.desktopDesiredOffCleanupPending":`Claude Desktop hâlâ ağ geçidini kullanıyor; temizlik bekleniyor`,"integrations.detail.desktopDesiredOnNotApplied":`Entegrasyon açık ancak Desktop kullanmıyor`,"integrations.detail.desktopSelectedElsewhere":`Desktop başka bir profil kullanıyor`,"integrations.detail.desktopProfileDrift":`Seçilen Desktop profili değişti`,"integrations.detail.desktopObservedUnsafe":`Desktop profili güvenle değiştirilemiyor`,"integrations.detail.desktopNotInstalled":`Claude Desktop kütüphanesi yüklü değil`,"integrations.detail.grokModels":`{count} model bağlandı`,"integrations.detail.grokAbsent":`Konfigürasyonda opencodex bloğu yok`,"integrations.dialog.grok.title":`Grok Build entegrasyonu devre dışı bırakılsın mı?`,"integrations.dialog.grok.changes":`Yalnızca {path} dosyasında opencodex tarafından işaretlenen blok kaldırılacaktır. Blok dışında yazılan içerik değişmeden kalır.`,"integrations.dialog.grok.breakage":`Devre dışı bırakmak, opencodex model takma adlarını Grok Build'den kaldırır. xAI hesabınızla kullanılan modeller kullanılabilir kalır.`,"integrations.dialog.grok.undo":`opencodex bir geri döngü (loopback) adresinde çalışıyorsa, bunu tekrar açmak şu anda mevcut olan modellerden yeni bir blok yazar.`,"integrations.dialog.grok.confirm":`Devre Dışı Bırak`,"integrations.dialog.desktop.title":`Claude Desktop entegrasyonu devre dışı bırakılsın mı?`,"integrations.dialog.desktop.changes":`Eğer {path} opencodex tarafından yönetilen bir ağ geçidi profili içeriyorsa, Desktop önce kimlik bilgisiz yeni bir standart profil seçer, ardından eski profili ve yedeği kaldırır.`,"integrations.dialog.desktop.breakage":`Claude Desktop, opencodex üzerinden yönlendirilen modeller yerine standart Claude'a geri dönecektir.`,"integrations.dialog.desktop.undo":`Bunu tekrar açmak, kaydedilmiş model atamalarınızdan opencodex profilini yeniden oluşturur.`,"integrations.dialog.desktop.restart":`Claude Desktop bu yapılandırmayı yalnızca açılışta okur. Değişikliğin yürürlüğe girmesi için uygulamayı tamamen kapatıp yeniden açın.`,"integrations.dialog.desktop.confirm":`Devre Dışı Bırak`,"integrations.native.msg.nonLoopbackRemoved":`Grok Build yalnızca opencodex bir geri döngü (loopback) adresinde çalışırken otomatik olarak kaydolabilir. Loopback'i işaret eden önceki blok kaldırıldı.`,"integrations.native.msg.nonLoopbackRemovedNoop":`Grok Build yalnızca opencodex bir geri döngü (loopback) adresinde çalışırken otomatik olarak kaydolabilir. Kaldırılacak önceki bir blok yoktu.`,"integrations.native.msg.nonLoopbackSuperseded":`Grok Build yalnızca opencodex bir geri döngü adresi üzerinde çalışırken otomatik kaydedilebilir. Bu sırada başka bir işlem yeni bir blok yazdı, bu nedenle şu an dosyadaki blok bu istek tarafından oluşturulmadı.`,"integrations.native.error.orphanedMarker":`{path} bir opencodex başlangıç işaretine sahip ancak bitiş işareti yok. opencodex bloğunun nerede bittiğini belirleyemediği için dosya değiştirilmeden bırakıldı.`,"integrations.native.error.homeMismatch":`Yüklü servis dizini mevcut ev dizini ile uyuşmuyor, bu nedenle dosya değiştirilmeden bırakıldı.`,"integrations.native.error.notInstalled":`Grok Build yüklü değil, bu nedenle değiştirilecek bir şey yok.`,"integrations.native.error.configBusy":`Yapılandırma başka bir yerde kaydediliyor ve değiştirilemedi. Kısa süre sonra tekrar deneyin.`,"integrations.native.error.desktopUnsafeMetadata":`{path} konumundaki Claude Desktop meta verileri güvenle okunamadı, bu nedenle kütüphanesi değiştirilmedi.`,"integrations.native.error.desktopCleanupIncomplete":`Claude Desktop standart moda yönlendirildi, ancak eski opencodex kimlik bilgisi dosyaları şu konumda kalmaya devam ediyor: {paths}.`,"integrations.native.msg.desktopDisabled":`Claude Desktop entegrasyonu devre dışı bırakıldı.`,"integrations.native.msg.desktopEnabled":`Claude Desktop entegrasyonu etkinleştirildi.`,"integrations.state.absent":`Uygulanmadı`,"integrations.state.current":`Uygulandı`,"integrations.state.stale":`Güncelleme gerekiyor`,"integrations.state.conflict":`Çakışma`,"integrations.state.unsafe":`Doğrulanamadı`,"integrations.summary.detected":`Algılanan istemciler`,"integrations.summary.applied":`Yapılandırılan istemciler`,"integrations.summary.stale":`Güncelleme gerekiyor`,"integrations.summary.lastChange":`Son değişiklik`,"integrations.summary.disableAll":`Tümünü devre dışı bırak…`,"integrations.onboarding":`Uygulama işlemi bir yedek aldıktan sonra yazar.`,"integrations.empty.title":`Yüklü istemci algılanmadı`,"integrations.empty.body":`Desteklenen bir istemci yükleyin.`,"integrations.action.apply":`Uygula`,"integrations.action.disable":`Devre Dışı Bırak`,"integrations.action.refresh":`Güncelle`,"integrations.action.settings":`Ayarlar`,"integrations.action.manageKeys":`Anahtarları yönet`,"integrations.action.restore":`Geri Yükle…`,"integrations.action.undo":`Geri Al`,"integrations.action.restorePoint":`Bu noktayı geri yükle…`,"integrations.action.snapshotExpired":`Yedek süresi doldu`,"integrations.rollback.title":`Geri alma merkezi`,"integrations.rollback.empty":`Henüz uygulama geçmişi yok`,"integrations.rollback.emptyBody":`Her başarılı yazma bir anlık görüntü saklar.`,"integrations.catalog.title":`İstemciler`,"integrations.rollback.older":`Önceki işlemler`,"integrations.rollback.showMore":`{n} tane daha göster`,"integrations.rollback.failed":`Geri alma geçmişi yüklenemedi.`,"integrations.restore.title":`Bu anlık görüntü geri yüklensin mi?`,"integrations.restore.body":`Mevcut dosya önce yedeklenir.`,"integrations.restore.driftTitle":`Daha yeni düzenlemeler algılandı`,"integrations.restore.driftBody":`Bu anlık görüntüden sonra yapılan değişiklikler yedeklenecektir.`,"integrations.restore.confirm":`Geri Yükle`,"integrations.restore.confirmDrift":`Yedekle ve geri yükle`,"integrations.restore.pending":`Geri yükleniyor…`,"integrations.restore.manual":`{reason}: {path} dosyasını el ile geri yükleyin`,"integrations.error.load":`Entegrasyon durumu yüklenemedi.`,"integrations.error.stale":`Son yenileme başarısız oldu.`,"integrations.error.busy":`İşlem devam ediyor.`,"integrations.error.conflict":`Konfigürasyon yazıldıktan sonra değişti.`,"integrations.error.unsafe":`Konfigürasyon güvenle değiştirilemiyor.`,"integrations.error.generic":`Entegrasyon değişikliği başarısız oldu.`,"integrations.error.nonLoopback":`{client} istemcisi geri döngü (loopback) dışında bir adreste. Yerel entegrasyonlar yalnızca loopback (127.0.0.1 veya ::1) üzerinden düzenlenebilir. Bir API anahtarı ayarlayın veya yerel erişim kullanın.`,"integrations.status.installed":`Yüklü`,"integrations.status.notInstalled":`Yüklü değil`,"integrations.status.appliedAt":`Uygulandı`,"integrations.status.backup":`Yedek`,"integrations.status.lastRestore":`Son geri yükleme`,"integrations.status.unknown":`Bilinmiyor`,"integrations.bulk.title":`Uygulanan istemci entegrasyonları devre dışı bırakılsın mı?`,"integrations.bulk.body":`Yalnızca opencodex bloğu kaldırılır.`,"integrations.bulk.partial":`Bazı istemciler devre dışı bırakılamadı: {clients}`,"integrations.bulk.success":`Uygulanan istemci entegrasyonları devre dışı bırakıldı.`,"integrations.retention.degraded":`Yedek temizliği geride kaldı.`,"integrations.error.residual":`{path} konumunda {message}`,"integrations.error.recover":`{path} kurtarılırken {message}`,"integrations.kind.apply":`Uygulandı`,"integrations.kind.disable":`Devre Dışı Bırakıldı`,"integrations.kind.refresh":`Güncellendi`,"integrations.kind.restore":`Geri Yüklendi`,"integrations.kind.overwrite":`Üzerine yazıldı`,"integrations.dialog.overwrite.title":`Bu yapılandırmadaki blok değiştirilsin mi?`,"integrations.dialog.overwrite.changesUnowned":`{path} içinde opencodex'in ihtiyaç duyduğu yeri, bizim yazmadığımız bir blok tutuyor. Uygulamak onu opencodex'in yazacağı blokla değiştirir.`,"integrations.dialog.overwrite.changesForeign":`{path} içindeki opencodex bloğuna yaptığınız değişiklik atılacak ve opencodex'in yazacağı blokla değiştirilecek.`,"integrations.dialog.overwrite.breakage":`Diğer bloğun yaptığı ayarlar artık geçerli olmaz. Dosyanın kalanına dokunulmaz.`,"integrations.dialog.overwrite.undo":`Önce bir anlık görüntü kaydedilir; bu işlem aşağıdaki geri alma listesinde görünür ve geri alınabilir.`,"integrations.dialog.overwrite.confirm":`Değiştir`,"integrations.action.overwrite":`Değiştir`,"integrations.semantics.opencode":`Doğrudan disk başlatmaları.`,"integrations.semantics.pi":`Yeni oturumlara uygulanır.`,"integrations.semantics.hermes":`Yeni oturumlara uygulanır.`,"integrations.semantics.openclaw":`Anında uygulanır.`,"integrations.semantics.kimi":`Uygulamak için yeniden başlatın.`,"integrations.semantics.gajae":`Yeni oturuma uygulanır.`,"integrations.semantics.dsh":`OpenCodex yalnızca $DSH_HOME/settings.yaml içindeki llm-pi-ai.providers.opencodex bölümünü yönetir. DSH bu sağlayıcıyı çalışırken yeniden yükler; varsayılan modeliniz ve deepseek-official değişmez. Şimdilik yalnızca geri döngü desteklenir; gerçek kimlik bilgisi yazılmaz.`,"integrations.semantics.mcode":`Yalnızca custom_provider.opencodex bölümünü yönetir. Varsayılan model ve MiniMax oturumu değişmez.`,"integrations.semantics.zcode":`Yalnızca ~/.zcode/v2/config.json içindeki provider.opencodex bölümünü yönetir. Z.ai oturumu ve diğer sağlayıcılar değişmez. Değişikliklerden sonra ZCode'u yeniden başlatın.`,"integrations.semantics.prime":`Yalnızca Prime Agent'ın models.json dosyasındaki providers.opencodex bölümünü yönetir — PRIME_AGENT_CODING_AGENT_DIR ayarlı değilse ~/.prime/agent. Diğer sağlayıcılar ve model geçersiz kılmaları değişmez. Yeni oturumlarda geçerli olur.`,"integrations.semantics.aside":`Yalnızca oturum açmış hesabın Aside models.json dosyasındaki providers.opencodex bölümünü yönetir (~/.aside/u/). Diğer sağlayıcılar değişmez. Aside çalışırken bu dosyayı yeniden yazar; bu nedenle uyguladıktan sonra Aside'ı tamamen kapatıp yeniden açın.`,"integrations.semantics.omp":`Kataloğu yüklemek için OMP'yi yeniden başlatın.`,"codexAuth.mainAccount":`Ana Hesap`,"codexAuth.logLabel":`Günlük etiketi`,"codexAuth.codexApp":`Codex Uygulaması`,"codexAuth.moreActions":`Daha fazla işlem göster`,"codexAuth.copyId":`Hesap kimliğini kopyala`,"codexAuth.appLogin":`Uygulama girişi`,"codexAuth.accountPool":`Hesap Havuzu`,"codexAuth.accountModeTitle":`OpenAI hesap modu`,"codexAuth.accountModePool":`Havuz modu`,"codexAuth.accountModePoolDesc":`Ana giriş ve eklenen hesaplar burada döner.`,"codexAuth.accountModeDirect":`Doğrudan mod`,"codexAuth.accountModeDirectDesc":`İstekler yalnızca ana girişi kullanır.`,"codexAuth.accountPickerTitle":`Model seçiciden belirli bir Codex hesabını hedefleyin`,"codexAuth.accountPickerOffDesc":`Etkinleştirildiğinde, sıradan GPT seçici satırlarının yerini her hesap seçicisi için bir giriş alır, böylece çıkış yapmadan bir konuşma için tam hesabı seçebilirsiniz. Kapatılması hiçbir hesabı kaldırmaz.`,"codexAuth.accountPickerOnDesc":`Her seçici, kayıtlı bir hesap için genel bir etikettir. Bunu seçmek, konuşmayı eşlenen hesaba kilitler: asla dönmez veya yedek hesaba geçmez ve aktif Havuz hesabını değiştirmez.`,"codexAuth.accountPickerCompatibility":`Yerleşik Codex Uygulama girişinin kendi seçicisi vardır; oluşturulan haritalar normalde bunu main olarak adlandırır ve gerektiğinde main-2 gibi çakışmasız bir son ek kullanır. Eklenen hesaplar kararlı ve gizlilik açısından güvenli etiketler alırken, özel seçici etiketleri değişmeden kalır. Mevcut konuşmalar ve kaydedilmiş model seçimleri yönlendirilmeye devam eder. Bunu kapatmak oluşturulan girişleri gizler ancak seçicileri ve tam rotaları korur. Yalın GPT model kimlikleri Havuz veya Doğrudan davranışlarını sürdürür.`,"codexAuth.accountPickerUpdated":`Hesap hedefleme güncellendi.`,"codexAuth.accountPickerUpdateFailed":`Hesap hedefleme güncellenemedi. Son doğrulanan ayar gösteriliyor.`,"codexAuth.accountPickerLoadFailed":`Hesap hedefleme ayarı yüklenemedi.`,"codexAuth.accountPickerRefreshFailed":`Bu ayar yenilenemedi. Son doğrulanan değer hâlâ gösteriliyor.`,"codexAuth.advancedSettings":`Gelişmiş ayarlar`,"codexAuth.advancedSettingsAria":`Gelişmiş Codex Auth ayarlarını göster veya gizle`,"codexAuth.catalogRefreshPending":`Değişiklik kaydedildi ancak Codex model kataloğunun yenilenmesi bekleniyor. Yeniden denemek için ocx sync çalıştırın.`,"codexAuth.openaiMissing":`Yerleşik OpenAI sağlayıcısı yapılandırılmamış.`,"codexAuth.openaiDisabled":`Yerleşik OpenAI sağlayıcısı devre dışı.`,"codexAuth.openaiUnavailableDesc":`OpenAI hesaplarınız kullanılabilir.`,"codexAuth.enableOpenai":`OpenAI'yi Etkinleştir`,"codexAuth.enablingOpenai":`Etkinleştiriliyor...`,"codexAuth.enableOpenaiFailed":`OpenAI sağlayıcısı etkinleştirilemedi.`,"codexAuth.openaiPresetLoadFailed":`Ayar yüklenemedi.`,"codexAuth.openaiPresetUnavailable":`Ayar kullanılamıyor.`,"codexAuth.openProviders":`Sağlayıcıları Aç`,"codexAuth.add":`Ekle`,"codexAuth.sparkQuota":`Codex Spark kotası`,"codexAuth.sparkQuotaHint":`Hesap kartlarında GPT-5.3-Codex-Spark haftalık penceresini gösterir. Yalnızca tek bir modeli kapsadığı için varsayılan olarak gizlidir.`,"codexAuth.sparkQuotaShown":`Codex Spark kotası gösteriliyor`,"codexAuth.sparkQuotaHidden":`Codex Spark kotası gizlendi`,"codexAuth.sparkQuotaFailed":`Codex Spark kotası ayarı değiştirilemedi`,"codexAuth.refreshQuota":`Kotaları yenile`,"codexAuth.refreshingQuota":`Yenileniyor...`,"codexAuth.quotaRefreshed":`Kotalar yenilendi`,"codexAuth.quotaRefreshFailed":`Kotalar yenilenemedi`,"codexAuth.pauseExhausted":`Tükenenleri duraklat`,"codexAuth.pausingExhausted":`Kotalar kontrol ediliyor...`,"codexAuth.pauseExhaustedSucceeded":`Limitteki hesaplar duraklatıldı: {count}`,"codexAuth.pauseExhaustedNone":`%100 kullanımı doğrulanmış hesap yok.`,"codexAuth.pauseExhaustedFailed":`Tükenen hesaplar kontrol edilemedi.`,"codexAuth.noPool":`Henüz havuz hesabı eklenmedi.`,"codexAuth.pause":`Duraklat`,"codexAuth.resume":`Devam Ettir`,"codexAuth.paused":`DURAKLATILDI`,"codexAuth.pauseSucceeded":`{email} duraklatıldı`,"codexAuth.resumeSucceeded":`{email} tekrar havuza alındı`,"codexAuth.pauseFailed":`{email} duraklatılamadı.`,"codexAuth.resumeFailed":`{email} devam ettirilemedi.`,"codexAuth.pausedHint":`Devam ettirilene kadar otomatik seçimden hariç tutulur.`,"codexAuth.pinned":`SABİTLENDİ`,"codexAuth.pinnedHint":`Bu hesabı elle seçtiniz.`,"codexAuth.fiveHour":`5saat`,"codexAuth.weekly":`Hafta`,"codexAuth.monthly":`30gün`,"codexAuth.resets":`sıfırlanma`,"codexAuth.today":`Bugün`,"codexAuth.current":`MEVCUT`,"codexAuth.nextSession":`SEÇİLEN`,"codexAuth.poolPrepared":`HAVUZ İÇİN HAZIRLANDI`,"codexAuth.preparePoolTitle":`Bu hesap Havuz modu için hazırlansın mı?`,"codexAuth.preparePoolDesc":`Doğrudan istekler ana girişi kullanmaya devam eder.`,"codexAuth.prepareForPool":`Havuz İçin Hazırla`,"codexAuth.poolPreparedToast":`{email} Havuz modu için hazırlandı`,"codexAuth.switchTitle":`Aktif hesap değiştirilsin mi?`,"codexAuth.switchDesc":`Anında yürürlüğe girer. Mevcut hesaba bağlı iş parçacıkları ve işlenmekte olan istekler yakalanan hesaplarını korur; yeni veya bağımsız istekler seçilen hesabın sıra kademesini kullanır ve aynı seçim sırasındaki hesaplar sırayla görev almaya devam eder.`,"codexAuth.cacheWarning":`Hesap değiştirildiğinde önbellek sıfırlanır.`,"codexAuth.setAsNext":`Sonraki istekte bu hesabı kullan`,"codexAuth.cancel":`İptal`,"codexAuth.switchBack":`Ana hesaba geri dönülsün mü?`,"codexAuth.switchBackDesc":`Anında yürürlüğe girer. Mevcut hesaba bağlı iş parçacıkları ve işlenmekte olan istekler yakalanan hesaplarını korur; yeni veya bağımsız istekler Uygulama giriş hesabınızın sıra kademesini kullanır ve aynı seçim sırasındaki hesaplar sırayla görev almaya devam eder.`,"codexAuth.autoSwitch":`Kullanıma dayalı proaktif geçiş`,"codexAuth.autoSwitchQuotaDesc":`Kota: %{threshold} veya üzeri kullanımda sonraki istek daha az kullanılan bir hesaba geçebilir.`,"codexAuth.autoSwitchQuotaOffDesc":`Kullanıma dayalı proaktif geçiş kapalı.`,"codexAuth.autoSwitchRoundRobinDesc":`Round-robin bu eşiği kullanmaz.`,"codexAuth.autoSwitchFillFirstDesc":`Kullanım %{threshold} eşiğini aşana kadar hesabı doldurun, ardından sonraki kullanılabilir hesaba geçin.`,"codexAuth.autoSwitchFillFirstOffDesc":`İlk doldurma modunda kullanım boşaltma noktası yok.`,"codexAuth.failureRecoveryNote":`Hata kurtarma ayrıdır.`,"codexAuth.autoSwitchThreshold":`Kullanım eşiği`,"codexAuth.autoSwitchThresholdAria":`Kullanım eşiği, yüzde`,"codexAuth.autoSwitchThresholdInc":`Kullanım eşiğini artır`,"codexAuth.autoSwitchThresholdDec":`Kullanım eşiğini azalt`,"codexAuth.autoSwitchLoadFailed":`Ayarlar yüklenemedi.`,"codexAuth.autoSwitchThresholdInvalid":`1 ile 100 arasında bir tam sayı girin`,"codexAuth.autoSwitchUpdated":`Proaktif geçiş güncellendi`,"codexAuth.autoSwitchUpdateFailed":`Geçiş güncellemesi doğrulanamadı.`,"codexAuth.requestUserInput":`Varsayılan modda kullanıcı girdisi iste`,"codexAuth.requestUserInputDesc":`Codex'in soru sormasına izin verir.`,"codexAuth.requestUserInputUpdated":`Özellik güncellendi.`,"codexAuth.requestUserInputUpdatedRestart":`Özellik güncellendi. Codex uygulamasını yeniden başlatın.`,"codexAuth.requestUserInputUpdateFailed":`Özellik güncellenemedi.`,"codexAuth.requestUserInputLoadFailed":`Özellik okunamadı.`,"anthropicPool.title":`Claude hesap havuzu (deneysel)`,"anthropicPool.enabledDesc":`429 alındığında hesabı bekletir ve başka bir hesaba geçer. Yeni oturumlar {window} değerine göre %{threshold} altında kullanıma sahip hesapları tercih eder.`,"anthropicPool.enabledNoProactiveDesc":`429 alındığında hesabı bekletir ve başka bir hesaba geçer. Eşik 0 iken kullanıma dayalı öngörülü geçiş kapalıdır, ancak yeni oturum seçimi ve 429 kurtarma hâlâ {window} penceresini kullanır.`,"anthropicPool.disabledDesc":`Yalnızca aktif Claude hesabını kullanır.`,"anthropicPool.experimentalWarning":`Deneysel: Claude OAuth hesaplarını döndürmek desteklenmeyen bir kullanım yoludur ve Anthropic hesap kısıtlamalarına veya hesabın askıya alınmasına yol açabilir. Aynı kuruluşu paylaşan hesaplar oran limitlerini paylaşır ve döndürmeden ek kapasite kazanmaz. Riskleri anlamıyorsanız kapalı tutun.`,"anthropicPool.needTwoAccounts":`Havuzu etkinleştirmeden önce en az iki Claude OAuth hesabı ekleyin.`,"anthropicPool.threshold":`Yeni oturum kullanım eşiği`,"anthropicPool.thresholdAria":`Yeni oturum kullanım eşiği, yüzde`,"anthropicPool.thresholdHelp":`0 kota bazlı seçimi devre dışı bırakır. Varsayılan 80.`,"anthropicPool.thresholdInvalid":`0 ile 100 arasında bir tam sayı girin`,"anthropicPool.loadFailed":`Claude havuz ayarları yüklenemedi.`,"anthropicPool.saveFailed":`Claude havuz ayarları kaydedilemedi.`,"anthropicPool.on":`Açık`,"anthropicPool.off":`Kapalı`,"accountPool.strategy":`Rotasyon stratejisi`,"accountPool.strategyDesc":`OpenCodex'in yeni bir göreve nasıl hesap atayacağı.`,"accountPool.strategyQuota":`Kota`,"accountPool.strategyRoundRobin":`Round-robin`,"accountPool.strategyFillFirst":`İlk doldurma`,"accountPool.strategyHintQuota":`Kota kullanımı eşik aşıldığında hesabı değiştirebilir.`,"accountPool.strategyHintRoundRobin":`Round-robin yalnızca canlı bir hesap bağı olmayan yeni/bağımsız görevleri döndürür; mevcut görevler bağlı kalabilir ve kullanım eşiği normal rotasyonu değiştirmez.`,"accountPool.strategyHintFillFirst":`İlk doldurma eşiği boşaltma noktası olarak kullanır.`,"accountPool.unboundDefinition":`Bağlı olmayan yeni görev.`,"accountPool.stickyLimit":`Döndürmeden önceki sabit atamalar`,"accountPool.stickyLimitAria":`Döndürmeden önceki sabit atamalar`,"accountPool.stickyLimitInc":`Sabit limiti artır`,"accountPool.stickyLimitDec":`Sabit limiti azalt`,"accountPool.stickyLimitHelp":`Seçilen hesabı bu kadar yeni atama boyunca tutun.`,"accountPool.stickyLimitInvalid":`1 ile 100 arasında bir tam sayı girin`,"accountPool.strategyLoadFailed":`Strateji yüklenemedi.`,"accountPool.strategyUpdateFailed":`Strateji kaydedilemedi.`,"accountPool.quotaWindow":`Kota penceresi`,"accountPool.quotaWindowDesc":`Kotaya dayalı yeni oturum seçimi, İlk doldurma eşik kontrolleri ve uygun 429 yedekleri için hangi önbelleğe alınmış kullanım çubuğunun kullanılacağını belirler.`,"accountPool.quotaWindowFiveHour":`5 saatlik çubuk`,"accountPool.quotaWindowWeekly":`Haftalık çubuk`,"accountPool.quotaWindowMaxUtilization":`Daha yüksek çubuk`,"accountPool.quotaWindowHint":`Haftalık çubuk, başka uygun hesap kaldığı sürece 5 saatlik çubuğu tükenmiş hesapları atlar; hiçbiri kalmazsa bu hesaplara geri döner. Eşitlikte 5 saatlik kullanımı daha düşük olan seçilir; hesap başına haftalık çubuklar ancak Sağlayıcılar sayfası sorguladıktan sonra bilinir.`,"accountPool.quotaWindowInert":`Kullanım çubuğunu yalnızca Kota ya da eşiği 0'ın üzerinde olan İlk doldurma puanlar; bu yüzden geçerli rotasyon stratejisi için bu ayar hiçbir şeyi değiştirmez.`,"accountPool.priority":`Seçim sırası`,"accountPool.priorityAria":`Bu hesap için seçim sırası`,"accountPool.priorityHint":`Yüksek sayılar önce kullanılır.`,"accountPool.priorityFirst":`İlk`,"accountPool.priorityEarlier":`Daha önce`,"accountPool.priorityNormal":`Normal`,"accountPool.priorityLater":`Daha sonra`,"accountPool.priorityLast":`Son`,"accountPool.priorityOption":`{name} ({value})`,"accountPool.priorityCustom":`Özel`,"accountPool.priorityUpdated":`{email} için seçim sırası güncellendi`,"accountPool.priorityUpdateFailed":`{email} için öncelik güncellenemedi`,"codexAuth.switched":`Sonraki istek için {email} seçildi`,"codexAuth.loadFailed":`Codex hesap ayarları yüklenemedi.`,"codexAuth.switchFailed":`Hesap değiştirilemedi.`,"codexAuth.removeConfirm":`{id} kaldırılsın mı?`,"codexAuth.removeFailed":`Hesap kaldırılamadı.`,"codexAuth.addTitle":`Codex Hesabı Ekle`,"codexAuth.addIdLabel":`Hesap ID`,"codexAuth.addIdPlaceholder":`codex-is, codex-yedek...`,"codexAuth.resetCreditsAria":`{count} sıfırlama kredisi`,"codexAuth.addJsonLabel":`auth.json içeriği`,"codexAuth.addHelp":`Başka bir makineden kopyalayın.`,"codexAuth.importBtn":`İçe Aktar`,"codexAuth.importInvalidJson":`Geçersiz JSON`,"codexAuth.importMissingTokens":`Eksik jetonlar`,"codexAuth.importMissingId":`Hesap ID gereklidir`,"codexAuth.accountAdded":`Hesap havuza eklendi`,"codexAuth.addPickDesc":`Havuza eklemek için başka bir ChatGPT hesabı ile giriş yapın.`,"codexAuth.oauthLogin":`OAuth Girişi`,"codexAuth.oauthDesc":`Tarayıcıda ChatGPT girişini açar`,"codexAuth.deviceLogin":`Cihaz koduyla giriş`,"codexAuth.deviceDesc":`Başsız veya uzak proxy için: kısa kodu başka bir cihazda girin`,"codexAuth.importAuthJson":`auth.json İçe Aktar`,"codexAuth.importAuthJsonDesc":`Başka bir kurulumdan veya dışa aktarımdan`,"codexAuth.back":`Geri`,"codexAuth.oauthAlreadyInProgress":`Giriş zaten devam ediyor.`,"codexAuth.oauthWaiting":`Tarayıcıda girişin tamamlanması bekleniyor...`,"codexAuth.oauthSubmittingCode":`Kod gönderiliyor…`,"codexAuth.oauthCodeSubmitted":`Kod gönderildi — giriş bitmesi bekleniyor…`,"codexAuth.oauthStatusRetrying":`Durum kontrol edilirken hata — tekrar deneniyor…`,"codexAuth.oauthCancelled":`Giriş iptal edildi.`,"codexAuth.loginFailed":`Giriş başarısız oldu`,"codexAuth.needsReauth":`Tekrar Giriş Yap`,"codexAuth.reauthenticate":`Yeniden Doğrula`,"codexAuth.tokenExpired":`Jeton süresi doldu — hesabı yeniden doğrulayın`,"codexAuth.mainTokenExpired":`Jeton süresi doldu — tekrar giriş yapın`,"codexAuth.emailCollision":`Bu hesap ana girişinizle eşleşiyor.`,"codexAuth.resetCreditsTitle":`Kredileri Sıfırla`,"codexAuth.resetCreditsAvailable":`{count} sıfırlama krediniz var.`,"codexAuth.resetCreditsDesc":`Her kredi oran limitlerinizi anında sıfırlar.`,"codexAuth.noResetCredits":`Sıfırlama krediniz yok.`,"codexAuth.earnCreditsHint":`Krediler aylık ve tavsiye programı ile kazanılır.`,"codexAuth.creditsExpireNote":`Kredilerin süresi 30 gün içinde dolmaktadır.`,"codexAuth.useOneCredit":`1 Kredi Kullan`,"codexAuth.confirmResetTitle":`Sıfırlama Kredisi Kullanılsın mı?`,"codexAuth.confirmResetDesc":`Bu işlem oran limitlerinizi anında sıfırlayacaktır. {count} krediniz kaldı.`,"codexAuth.irreversible":`Bu işlem geri alınamaz.`,"codexAuth.useCredit":`Kredi Kullan`,"codexAuth.redeeming":`Sıfırlanıyor...`,"codexAuth.resetSuccess":`Oran limitleri sıfırlandı! Kalan kredi: {remaining}.`,"codexAuth.resetSuccessGeneric":`Oran limitleri sıfırlandı!`,"codexAuth.resetAlreadyRedeemed":`Bu kredi zaten kullanıldı.`,"codexAuth.resetNothingToReset":`Şu anda sıfırlanması gereken oran limiti yok.`,"codexAuth.resetNoCredit":`Kullanılabilir sıfırlama kredisi yok.`,"codexAuth.resetError":`Sıfırlama kredisi kullanılamadı.`,"codexAuth.fifoNote":`En eski kredi ilk önce kullanılır.`,"codexAuth.confirmWhichCredit":`{date} tarihli kredi kullanılacak.`,"codexAuth.creditNext":`Sonraki kullanılacak`,"codexAuth.creditLabel":`Kredi #{n}`,"codexAuth.creditNextBadge":`SONRAKİ`,"codexAuth.creditGranted":`Verildiği tarih {date}`,"codexAuth.creditExpires":`Son kullanma {date} ({days}gün kaldı)`,"api.title":`API Erişimi`,"api.subtitle":`Harici uygulamalardan opencodex proxy'sine erişmek için üretilen API anahtarlarını kullanın. Anahtarlar {authHeader} başlığı üzerinden kimlik doğrulaması yapar; her bir uç noktanın neleri kabul ettiğini görmek için aşağıdaki tabloya bakın.`,"api.baseUrl":`Taban URL`,"api.responsesEndpoint":`Responses API`,"api.chatCompletionsEndpoint":`Chat Completions API`,"api.messagesEndpoint":`Messages API`,"api.modelsEndpoint":`Models API`,"api.endpointNote":`OpenAI uyumlu istemcilerle taban URL'yi kullanın.`,"api.endpointsTitle":`Uç noktalar`,"api.authTitle":`Kimlik Doğrulama`,"api.authLoopback":`Geri döngü (loopback) bağlantıları (127.0.0.1 / ::1) kimlik doğrulamasını atlar. Harici/ağ istemcileri x-opencodex-api-key veya Authorization başlığında bir ocx_ API anahtarı ya da OPENCODEX_API_AUTH_TOKEN göndermelidir.`,"api.authBaseUrlNote":`İstemcileri taban URL ile yapılandırın.`,"api.newKeyTitle":`Yeni anahtar oluşturuldu`,"api.newKeyNote":`Bu anahtarı şimdi kopyalayın — tekrar gösterilmeyecektir.`,"api.copy":`Kopyala`,"api.copied":`Kopyalandı`,"api.dismiss":`Kapat`,"api.generateTitle":`Anahtar oluştur`,"api.keyNamePlaceholder":`Anahtar adı (isteğe bağlı)`,"api.generate":`Oluştur`,"api.generating":`Oluşturuluyor…`,"api.activeKeys":`Aktif anahtarlar ({count})`,"api.activeKeysLoading":`Aktif anahtarlar`,"api.noKeys":`Henüz API anahtarı yok. Yukarıdan bir tane oluşturun.`,"api.workspace.sections":`API bölümleri`,"api.section.keys":`Anahtarlar`,"api.section.connect":`Bağlan`,"api.section.endpoints":`Uç noktalar`,"api.section.models":`Modeller`,"api.section.examples":`Örnekler`,"api.workspace.details":`API anahtar detayları`,"api.workspace.keyDetails":`Anahtar detayları`,"api.workspace.keyPrefix":`Anahtar ön eki`,"api.workspace.deleteKey":`Anahtarı sil`,"api.workspace.deleteConfirm":`Bu API anahtarı silinsin mi? Bunu kullanan istemciler erişimi anında kaybedecektir. Bu işlem geri alınamaz.`,"api.workspace.usageExamples":`Kullanım örnekleri`,"api.copyUrlHint":`URL kopyalamak için tıklayın`,"api.urlCopied":`URL kopyalandı`,"api.copyExampleHint":`Örnek kopyalamak için tıklayın`,"api.exampleCopied":`Örnek kopyalandı`,"api.colName":`İsim`,"api.colKey":`Anahtar`,"api.colCreated":`Oluşturuldu`,"api.confirm":`Onayla`,"api.deleteAria":`API anahtarını sil`,"api.modelsTitle":`Harici model kataloğu`,"api.modelsCount":`{count} çağrılabilir`,"api.modelsLoading":`Modeller yükleniyor…`,"api.modelsSearch":`Modellerde ara`,"api.modelsSubtitle":`Uç noktalarınızla bu tam model ID'lerini kullanın.`,"api.modelsEmpty":`Henüz harici olarak çağrılabilir model yok.`,"api.modelsNoMatch":`“{query}” ile eşleşen model yok.`,"api.modelsLoadFailed":`Harici model kataloğu yüklenemedi.`,"api.colModel":`Model`,"api.colSource":`Kaynak`,"api.colProtocols":`Protokoller`,"api.sourceNative":`ChatGPT havuzu`,"api.sourceCombo":`Kombo rotası`,"api.sourceCustom":`Özel`,"api.protocolResponses":`Responses`,"api.protocolChatCompletions":`Chat Completions`,"api.protocolMessages":`Messages`,"api.copyModelId":`ID Kopyala`,"api.modelCopied":`Kopyalandı`,"api.testModel":`Test Et`,"api.testingModel":`Test ediliyor…`,"api.testSucceeded":`Tamam`,"api.testFailed":`Başarısız`,"api.usageChatTitle":`Chat Completions örneği`,"api.usageResponsesTitle":`Responses örneği`,"api.usageMessagesTitle":`Messages örneği`,"api.usageSampleInput":`Merhaba dünya!`,"api.clientConfig.title":`İstemci konfigürasyonu`,"api.clientConfig.rowsLabel":`Bir istemci bağlayın`,"api.clientConfig.details":`Detaylar`,"api.clientConfig.detailsAria":`{client} konfigürasyon detayları`,"api.clientConfig.copyAria":`{client} konfigürasyon JSON kopyala`,"api.clientConfig.downloadAria":`{client} konfigürasyonu indir`,"api.clientConfig.rowMeta":`{destination} · {count} model`,"api.clientConfig.rowError":`{client} konfigürasyonu oluşturulamadı.`,"api.clientConfig.copiedAnnounceClient":`{client} konfigürasyon JSON panoya kopyalandı.`,"api.clientConfig.clientOpencode":`OpenCode`,"api.clientConfig.clientPi":`Pi`,"api.clientConfig.clientHermes":`Hermes`,"api.clientConfig.clientOpenclaw":`OpenClaw`,"api.clientConfig.clientKimi":`Kimi Code`,"api.clientConfig.clientGajae":`Gajae Code`,"api.clientConfig.clientDsh":`DeepSeek Harness (DSH)`,"api.clientConfig.clientMcode":`MiniMax Code`,"api.clientConfig.clientZcode":`ZCode`,"api.clientConfig.clientPrime":`Prime Agent`,"api.clientConfig.clientAside":`Aside`,"api.clientConfig.copy":`JSON Kopyala`,"api.clientConfig.download":`İndir`,"api.clientConfig.loading":`İstemci konfigürasyonu oluşturuluyor…`,"api.clientConfig.jsonLabel":`{client} konfigürasyon JSON`,"api.clientConfig.destination":`Hedef dosya`,"api.clientConfig.envHint":`Başlatmadan önce anahtarı ayarlayın`,"api.clientConfig.mergeWarning":`Bunu hedef dosyaya birleştirin. Dosyanın üzerine yazıp değiştirmek, diğer sağlayıcılarınızı ve MCP ayarlarınızı silecektir.`,"api.clientConfig.modelCount":`{count} model dışa aktarıldı`,"api.clientConfig.missingLimits":`{total} modelden {count} tanesi bağlam sınırı olmadan gönderildi.`,"api.clientConfig.noKeyYet":`{env} arkasında henüz anahtar yok.`,"api.clientConfig.loadFailed":`Model listesi okunamadı.`,"api.clientConfig.copiedAnnounce":`İstemci konfigürasyonu JSON panoya kopyalandı.`,"api.clientConfig.copyFailed":`Kopyalanamadı.`,"api.clientConfig.downloadedAnnounce":`{filename} indirildi. Henüz hiçbir şey değişmedi — kendiniz {destination} konumuna birleştirin.`,"api.clientConfig.whereDisclosure":`Bu dosya nereye gidiyor`,"api.clientConfig.whereBody":`Yukarıdaki hedef genel (global) yoldur. Çalışma dizinindeki projeye özel bir konfigürasyon dosyası buna öncelik eder ve istemci anahtarı bu dosyadan değil, konfigürasyonda belirtilen çevre değişkeninden okur.`,"api.clientConfig.clientOmp":`OMP`,"api.keysLoadFailed":`API anahtarları yüklenemedi.`,"api.createFailed":`API anahtarı oluşturulamadı.`,"api.deleteFailed":`API anahtarı silinemedi.`,"api.auth.endpoint":`Uç nokta`,"api.auth.required":`Gerekli`,"api.auth.accepted":`Kabul edildi`,"api.auth.rejected":`Kabul edilmedi`,"api.auth.testProtocol":`{protocol} Test Et`,"api.auth.testNeedsFreshKey":`Test için bir anahtar oluşturun.`,"api.key.name":`Anahtar adı`,"api.key.rename":`Yeniden adlandır`,"api.key.saveName":`İsmi kaydet`,"api.key.renaming":`Kaydediliyor…`,"api.key.renameFailed":`Yeniden adlandırılamadı.`,"api.key.deleting":`Siliniyor…`,"api.rotation.title":`Anahtar döndürme`,"api.rotation.description":`Mevcut anahtarı kısa bir geçiş süresince geçerli tutarak yeni anahtar oluşturur.`,"api.rotation.start":`Döndürmeyi başlat`,"api.rotation.starting":`Başlatılıyor…`,"api.rotation.pending":`Döndürme bekliyor. Onaylamadan önce istemciyi güncelleyip doğrulayın.`,"api.rotation.expires":`Geçiş süresi sonu:`,"api.rotation.secretOnce":`Yeni anahtar yalnızca bir kez gösterilir. Kapatmadan önce kopyalayın.`,"api.rotation.commit":`Döndürmeyi onayla`,"api.rotation.abort":`Döndürmeyi iptal et`,"api.rotation.failed":`İşlem tamamlanmadı. Yeniden denemeden önce yenileyin.`,"api.rotation.startFailed":`Anahtar döndürme başlatılamadı.`,"api.key.copyFailed":`Otomatik kopyalanamadı. Kapatmadan önce anahtarı manuel olarak seçip kopyalayın — tekrar gösterilmeyecektir.`,"api.attribution.title":`Atfedilen kullanım`,"api.attribution.requests7d":`Son 7 gün istekleri`,"api.attribution.totalRequests":`Toplam atfedilen istekler`,"api.attribution.totalRequestsAvailable":`Mevcut geçmişteki istekler`,"api.attribution.sinceAvailable":`Mevcut atıf başlangıcı`,"api.attribution.lastUsed":`Son kullanım`,"api.attribution.since":`Atıf başlangıcı`,"api.attribution.neverUsed":`Henüz kullanılmadı`,"api.attribution.unavailable":`Kullanım mevcut değil`,"api.attribution.unavailableDetail":`Henüz kullanım atfedilmedi.`,"api.attribution.ambiguous":`İki anahtar aynı ID'yi paylaşıyor.`,"api.attribution.railAmbiguous":`mükerrer ID`,"claude.subtitle":`Claude Code içinde GPT, Gemini ve diğer modelleri kullanın.`,"claude.pageTitle":`Claude Code`,"claude.workspace.settings":`Ayarlar`,"claude.enabledLabel":`Claude bağlantısı`,"claude.enabledHint":`Kapalı olduğunda Claude Code bu proxy'yi kullanamaz.`,"claude.authMode":`Kimlik Doğrulama Modu`,"claude.authModeHint":`Abonelik Claude hesabı gerektirir, Proxy modunda gerekmez`,"claude.authModeSubscription":`Abonelik (Claude hesabı)`,"claude.authModeProxy":`Proxy (hesap gerekmez)`,"claude.authModeAuto":`Otomatik (Claude doğrulamasını algıla)`,"claude.effectiveMode.label":`Sonraki başlatmada geçerli`,"claude.effectiveMode.manual":`Manuel: {mode}`,"claude.effectiveMode.autoPresent":`Otomatik: abonelik — {source} üzerinden Claude kimliği bulundu`,"claude.effectiveMode.autoAbsent":`Otomatik: proxy modu — Claude kimliği bulunamadı`,"claude.effectiveMode.autoUnknown":`Otomatik: abonelik — doğrulanamadı`,"claude.effectiveMode.admissionKey":`Bu proxy'nin API anahtarı hâlâ gönderiliyor.`,"claude.authSource.claude-json-oauth":`Claude hesabı`,"claude.authSource.claude-credentials-file":`kimlik bilgileri dosyası`,"claude.authSource.macos-keychain":`macOS Keychain`,"claude.authSource.exported-env":`ortam değişkeni`,"claude.authSource.unknown":`algılanan bir kimlik bilgisi`,"claude.systemEnv":`Otomatik bağlan`,"claude.systemEnvDesc":`Açık olduğunda terminalde claude çalıştırmak proxy üzerinden geçer.`,"claude.systemEnvUnsupported":`Otomatik bağlanma yalnızca macOS üzerinde kullanılabilirdir. Bu sistemde Claude'u {cmd} ile başlatın.`,"claude.systemEnvWarn":`⚠ Terminal uygulamasını tamamen kapatıp yeniden açmalısınız.`,"claude.fastMode":`Hızlı Mod (OpenAI)`,"claude.fastModeDesc":`OpenAI modelleri için service_tier ayarını kontrol eder.`,"claude.fastAuto":`Otomatik`,"claude.fastOn":`AÇIK`,"claude.fastOff":`KAPALI`,"claude.autoContext":`Otomatik büyük bağlam kullan`,"claude.autoContextDesc":`1M işaretinin ne kadar ileri gideceğini kontrol eder. AÇIK: penceresi sıkıştırma eşiğini barındırabilen her model 1M aralığı kazanır. KAPALI: yalnızca gerçek 1M modelleri alır.`,"claude.autoContextInert":`Pasif durumdadır çünkü konfigürasyon dosyasında eski bir bağlam boyutu değeri (maxContextTokens) var. Yeniden etkinleştirmek için oradan kaldırın.`,"claude.autoCompactWindow":`Otomatik özetleme noktası`,"claude.autoCompactDefault":`{value} (varsayılan)`,"claude.autoCompactWindowDesc":`Sohbet bu noktaya ulaştığında eski mesajlar özetlenir. Her modelin kendi sınırını asla aşmaz, bu nedenle 200k modeller etkilenmez.`,"claude.autoCompactWindowWarn":`Bunu değiştirmek GPT modellerini bozabilir — bir modelin gerçek sınırından daha yüksek ayarlanırsa, sohbetler özet devreye girmeden önce hata verecektir.`,"claude.injectAgents":`Alt ajanları otomatik kaydet`,"claude.injectAgentsDesc":`Alt Ajanlar sekmesinde seçilen modelleri (ve mevcut varsayılan modeli) çağrılabilir Claude Code ajanları (ocx-*) olarak kaydeder. Sonraki oturumdan itibaren uygulanır.`,"claude.webSearchSidecar":`Web arama yan araç geçersiz kılması`,"claude.webSearchSidecarHint":`Claude Code istekleri için ana web arama sidecar'ını geçersiz kılın.`,"claude.visionSidecar":`Görsel yan araç geçersiz kılması`,"claude.visionSidecarHint":`Claude Code istekleri için ana görsel sidecar'ını geçersiz kılın.`,"claude.useMainSetting":`Ana ayarı kullan`,"claude.sidecarModelPlaceholder":`Ana ayar modeli`,"claude.quickstart":`Başlarken`,"claude.quickstartHint":`{cmd} Claude Code'u proxy üzerinden açar. claude.ai girişiniz aktif kalır.`,"claude.manualEnv":`Manuel kurulum (gelişmiş)`,"claude.smallFastModel":`Arka plan yardımcı modeli`,"claude.smallFastModelHint":`Claude Code'un sohbet özetleri ve konu tespiti gibi arka plan işleri için kullandığı model. haiku alt ajan takma adı da bunu kullanır. Boş = Claude varsayılanı (Haiku).`,"claude.smallFastModelAccurateHint":`Claude Code'un sohbet özetleri ve konu tespiti gibi arka plan işleri için kullandığı model. haiku alt ajan takma adı da bunu kullanır.`,"claude.smallFastModelUnsetOption":`Bırakın Claude Code seçsin (yerel model)`,"claude.smallFastModelNativeWarning":`Ayarlanmadığında, OpenCodex yardımcı model geçersiz kılmalarını boş bırakır. Claude Code kendi yerel Sonnet modelini kullanabilir ve bu durum yerel sağlayıcınızdan ücret alınmasına yol açabilir.`,"claude.slotUnset":`Claude varsayılanını kullan`,"claude.modelMap":`Model yakalama`,"claude.modelMapHint":`Belirli bir model isteklerini yakalar ve seçtiğiniz modele yönlendirir.`,"claude.mapFrom":`Orijinal model (örn. claude-sonnet-4-5)`,"claude.mapTo":`Değiştirilecek model (örn. gemini/gemini-3-pro)`,"claude.addMapping":`Kural ekle`,"claude.removeMapping":`Kuralı kaldır`,"claude.aliases":`Kullanılabilir modeller`,"claude.aliasesHint":`Claude Code'un /model menüsünde görünen modeller.`,"claude.aliasProviderOther":`Diğer`,"claude.loading":`Yükleniyor…`,"claude.loadFail":`Claude ayarları yüklenemedi`,"claude.saved":`Kaydedildi.`,"claude.saveFailed":`Kaydetme başarısız`,"claude.networkError":`Ağ hatası — proxy çalışıyor mu?`,"claude.toggleAria":`Claude bağlantısını değiştir`,"claude.none":`Yok`,"cws.loading":`Kombolar yükleniyor…`,"cws.loadFailed":`Kombolar yüklenemedi.`,"cws.saveFailed":`Kombo kaydedilemedi.`,"cws.removeFailed":`Kombo kaldırılamadı.`,"cws.saved":`Kombo kaydedildi.`,"cws.created":`{model} oluşturuldu.`,"cws.removed":`combo/{id} kaldırıldı.`,"cws.renamed":`{from}, {to} olarak yeniden adlandırıldı.`,"cws.add":`Kombo ekle`,"cws.addTitle":`Kombo ekle`,"cws.addSubtitle":`Sağlayıcılar arasında sanal bir model oluşturun.`,"cws.create":`Kombo oluştur`,"cws.railAria":`Kombo listesi`,"cws.searchPlaceholder":`Kombolarda veya hedeflerde ara…`,"cws.noSearchResults":`Aramanızla eşleşen kombo yok.`,"cws.group.failover":`Yedekli (Failover)`,"cws.group.roundRobin":`Round-robin`,"cws.group.other":`Diğer stratejiler`,"cws.targetCount":`{count} hedef`,"cws.targetCountOne":`1 hedef`,"cws.overviewTitle":`Kombolar`,"cws.overviewBlurb":`Sağlayıcı/model hedefleri arasında failover, round-robin, ağırlıklı rastgele, en az kullanılan veya en yakın kota sıfırlamasıyla yönlendiren sanal modeller.`,"cws.count.total":`Toplam`,"cws.count.failover":`Failover`,"cws.count.roundRobin":`Round-robin`,"cws.count.other":`Diğer`,"cws.howTitle":`Nasıl çalışır`,"cws.howBody":`Codex'ten kombonun kamuya açık model adını isteyin. Bir ad olmadan varsayılan combo/ şeklindedir. OpenCodex bir hedef seçer ve yalnızca yeniden denenebilir yukarı akış hatalarında atlar. Hiçbir hedef kullanılabilir kalmazsa, istek küresel varsayılan sağlayıcıyı kullanmak yerine kapalı olarak başarısız olur (fail closed).`,"cws.attentionTitle":`Dikkat gerekiyor`,"cws.attention.empty":`Yapılandırılmış hedef yok`,"cws.attention.few":`Yalnızca bir hedef — geçiş yapılacak yer yok`,"cws.attention.catalogOmitted":`Model kataloğunda eksik — üye yetenekleri eksik veya uyumsuz (eksik bağlam penceresi / meta veri veya boş modalite kesişimi). Takma ada göre yönlendirme hâlâ çalışır`,"cws.attention.allTargetsExhausted":`Etkin hedeflerin tüm kotaları tükendi`,"cws.emptyTitle":`İlk kombonuzu oluşturun`,"cws.empty.createDesc":`Sanal bir model adlandırın ve iki veya daha fazla arka ucu bağlayın.`,"cws.backToAll":`Tüm kombolara dön`,"cws.allCombos":`Tüm kombolar`,"cws.copyModel":`ID kopyala`,"cws.copied":`Kopyalandı`,"cws.tabsLabel":`Kombo detay bölümleri`,"cws.tab.config":`Konfigürasyon`,"cws.tab.about":`Hakkında`,"cws.strategy":`Strateji`,"cws.strategy.failover":`Yedekli (Failover)`,"cws.strategy.roundRobin":`Round-robin`,"cws.strategy.random":`Rastgele`,"cws.strategy.leastUsed":`En az kullanılan`,"cws.strategy.resetWindow":`Sıfırlama penceresi`,"cws.strategy.failoverHint":`Hedefleri sırayla deneyin. İlk hedef yeniden denenebilir bir hatayla (oran sınırı, kesinti, abonelik engeli) başarısız olursa sonraki hedefe atlayın.`,"cws.strategy.roundRobinHint":`Trafiği ağırlığa göre kararlı bir şekilde dengeleyin. Seçilen her hedefi bir dizi başarılı istek boyunca tutun, ardından ilerleyin.`,"cws.strategy.randomHint":`Her istek için ağırlığa orantılı olasılıkla bir uygun hedef çekilir. İstekler arasında yapışkanlık yoktur.`,"cws.strategy.leastUsedHint":`Her isteği, kayıtlı başarısı en az olan uygun hedefe yönlendirir. Sayaçlar proxy ile yeniden başlar.`,"cws.strategy.resetWindowHint":`Kota penceresi en yakında sıfırlanacak uygun hedefi tercih eder. Kota verisi yoksa yapılandırma sırasına döner.`,"cws.field.id":`Kombo ID`,"cws.field.idHint":`İstemciler {model} isteyecek`,"cws.field.idInternalHint":`Dahili kombo ID. Oluşturduktan sonra değiştirebilirsiniz.`,"cws.field.idHintEdit":`Yeniden adlandırmak komboyu yeni bir ID'ye taşır. İstemciler {model} ister.`,"cws.field.alias":`Genel model adı`,"cws.field.aliasPlaceholder":`deepseek-v4-flash veya üretici/model`,"cws.field.aliasHint":`İsteğe bağlı. Ön eki olmayan yalın bir ad, üretici/model gibi özel bir ön ek kullanın veya combo/ kullanmak için boş bırakın.`,"cws.field.nativeAlias":`Yerel OpenAI takma adı`,"cws.field.nativeAliasHint":`Bu kombonun desteklenen nitelemesiz yerel bir OpenAI model kimliğine sahip olmasına izin verin. Hesap ve sağlayıcı nitelikli OpenAI rotaları ayrı kalır.`,"cws.field.displayName":`Görünen ad`,"cws.field.displayNameHint":`Bu kombo için seçici etiketi. Yerel OpenAI takma adı etkinleştirildiğinde gereklidir.`,"cws.field.stickyLimit":`Döndürmeden önceki sabit başarılar`,"cws.field.stickyLimitHint":`Ağırlıklı seçici ilerlemeden önce seçilen hedefi bu kadar başarılı istek boyunca tutun.`,"cws.field.defaultEffort":`Varsayılan akıl yürütme`,"cws.field.defaultEffortNone":`Yok (hedef varsayılanı)`,"cws.field.defaultEffortHint":`Yalnızca istemci akıl yürütme çabasını belirtmediğinde (atladığında) kullanılır. Seçenekler, seçilen hedeflerin duyurulan çabalarının kesişimidir; katalog çaba meta verisi olmayan hedefler hiçbir seçenek sunmaz.`,"cws.capability.imageInputUnavailable":`Seçilen tüm hedefler görsel girişini destekleyene kadar kullanılamaz.`,"cws.capability.imageInputHint":`Tüm hedefler görselleri desteklediğinde varsayılan olarak açıktır. Yalnızca metin kabul etmek için kapatın.`,"cws.capability.imageInput":`Görsel / çok modlu`,"cws.capability.adaptiveEffort":`Uyarlanabilir akıl yürütme düzeyi`,"cws.capability.adaptiveEffortHint":`Kapalı: akıl yürütme denetimi olmayan bir hedef, tüm kombinasyonun seçicisini gizler. Açık: bu hedefler kullanılabilir kalır ve seçici, kalan hedeflerin ortak düzeylerini gösterir.`,"cws.capabilities":`Yetenekler`,"cws.field.defaultEffortUnsupported":`Bu çaba hedeflerin ortak merdiveninde yok — istek anında yok sayılacak veya uydurulacaktır.`,"cws.field.defaultEffortUnsupportedOption":`kesişimde değil`,"cws.targets":`Hedefler`,"cws.targets.failoverHint":`Sıralama önemlidir — birincil olan ilktir.`,"cws.targets.roundRobinHint":`Ağırlıklar kararlı bağıntılı seçimi kontrol eder; sıralama rotasyon halkasındaki eşitlikleri bozar.`,"cws.targets.randomHint":`Ağırlıklar her çekilişin olasılığını kontrol eder; sıralamanın önemi yoktur.`,"cws.targets.leastUsedHint":`Sıralama yalnızca eşit kullanımlı hedefler arasındaki eşitliği bozar.`,"cws.targets.resetWindowHint":`Kota verisi eksik veya eşitse sıralama uygulanır.`,"cws.target.provider":`Sağlayıcı`,"cws.target.model":`Model`,"cws.target.weight":`Ağırlık`,"cws.target.pickProvider":`Sağlayıcı seçin…`,"cws.target.pickProviderFirst":`Önce bir sağlayıcı seçin…`,"cws.target.pickModel":`Model seçin…`,"cws.target.noModels":`Bu sağlayıcı için model yok`,"cws.target.modelPlaceholder":`model ID`,"cws.target.add":`Hedef ekle`,"cws.target.drag":`Yeniden sıralamak için sürükleyin`,"cws.target.moveUp":`Yukarı taşı`,"cws.target.moveDown":`Aşağı taşı`,"cws.quota.available":`Kullanılabilir`,"cws.quota.exhausted":`Kota tükendi`,"cws.quota.unknown":`Kota bilinmiyor`,"cws.quota.allExhausted":`Etkin hedeflerin tüm kotaları tükendi. Başka bir hedef seçin veya kotanın yenilenmesini bekleyin.`,"cws.aboutTitle":`Çalışma zamanı`,"cws.aboutBody":`Başarısız hedefler Retry-After süresine uyarak kısa süreliğine soğumaya alınır. Geçersiz istek veya bağlam (context) hataları diğer hedefe atlamaz. Her hedef çabayı kendi yeteneklerine uyarlar; tükenen kombolar kapalı olarak başarısız olur (fail closed). Günlükler ve Kullanım bölümü sıralı fiziksel denemeleri ve deneme başına kullanımı saklar.`,"cws.removeConfirmTitle":`{model} kaldırılsın mı?`,"cws.removeConfirmDesc":`Bu işlem sanal modeli konfigürasyondan ve Codex kataloğundan kaldırır. Hiçbir sağlayıcıyı silmez.`,"cws.unsavedTitle":`Kaydedilmemiş değişiklikler`,"cws.unsavedDesc":`Düzenlemelerden vazgeçip devam edilsin mi?`,"cws.keepEditing":`Düzenlemeye devam et`,"cws.err.missingId":`Kombo ID gereklidir.`,"cws.err.invalidId":`ID harf veya sayı ile başlamalıdır.`,"cws.err.duplicateId":`Bu ID ile bir kombo zaten var.`,"cws.err.invalidAlias":`Takma ad geçerli karakterler içermelidir.`,"cws.err.aliasReservedNamespace":`Takma ad ayrılmış namespace kullanamaz.`,"cws.err.aliasNativeFamily":`Yerel OpenAI isimlerine izin verilmez.`,"cws.err.unsupportedNativeAlias":`Yerel takma ad, şu anda desteklenen yalın bir OpenAI model kimliği olmalıdır.`,"cws.err.missingNativeAliasDisplayName":`Yerel takma adlar için bir görünen ad gereklidir.`,"cws.err.invalidDisplayName":`Görünen ad en fazla 128 karakter olmalı ve kontrol karakteri içermemelidir.`,"cws.err.duplicateAlias":`Başka bir kombo bu takma adı zaten kullanıyor.`,"cws.err.noTargets":`En az bir hedef ekleyin.`,"cws.err.incompleteTarget":`Her hedefin bir sağlayıcısı ve modeli olmalıdır.`,"cws.target.disabled":`{name} (devre dışı)`,"cws.err.reservedNamespace":`Önce fiziksel sağlayıcı yeniden adlandırılmalıdır.`,"cws.err.providerCollision":`Kombo ID'si bir sağlayıcı adı ile çakışıyor.`,"cws.err.unknownProvider":`Her hedef yapılandırılmış bir sağlayıcı kullanmalıdır.`,"cws.err.duplicateTarget":`Aynı hedef yalnızca bir kez görünebilir.`,"cws.err.invalidStickyLimit":`Limit 1 ile 100 arasında bir tam sayı olmalıdır.`,"cws.err.invalidWeight":`Ağırlık 1 ile 10000 arasında olmalıdır.`,"cws.err.noEnabledTarget":`En az bir hedef etkin bir sağlayıcı kullanmalıdır.`,"claude.tabsLabel":`Claude istemcisi`,"claude.tabCode":`Code`,"claude.tabDesktop":`Desktop`,"claudeDesktop.title":`Claude Desktop`,"claudeDesktop.subtitle":`Claude model ailelerini {port} portundaki bir modele yönlendirin.`,"claudeDesktop.importJson":`JSON İçe Aktar`,"claudeDesktop.exportJson":`JSON Dışa Aktar`,"claudeDesktop.loading":`Claude Desktop profili yükleniyor…`,"claudeDesktop.loadFail":`Claude Desktop profili yüklenemedi.`,"claudeDesktop.retry":`Tekrar Dene`,"claudeDesktop.saveFailed":`Claude Desktop profili kaydedilemedi.`,"claudeDesktop.applyFailed":`Profil kaydedildi ancak uygulanamadı.`,"claudeDesktop.updateFailed":`Güncelleme başarısız oldu.`,"claudeDesktop.savedApplied":`Profil kaydedildi ve uygulandı.`,"claudeDesktop.appliedMarkerUnsaved":`Profil uygulandı ancak işaretçi kaydedilmedi.`,"claudeDesktop.savedAppliedAnnounce":`Profil kaydedildi ve uygulandı.`,"claudeDesktop.saved":`Profil kaydedildi.`,"claudeDesktop.savedAnnounce":`Profil kaydedildi.`,"claudeDesktop.exported":`Profil JSON olarak dışa aktarıldı.`,"claudeDesktop.importExpected":`Sürüm 1 profili bekleniyordu.`,"claudeDesktop.importReady":`JSON içe aktarıldı.`,"claudeDesktop.importedAnnounce":`Profil JSON içe aktarıldı.`,"claudeDesktop.importInvalid":`Seçilen dosya geçerli bir profil değil.`,"claudeDesktop.importFailed":`İçe aktarma başarısız oldu. {error}`,"claudeDesktop.moved":`{route}, {family} ailesine taşındı.`,"claudeDesktop.unsaved":`Kaydedilmemiş değişiklikler`,"claudeDesktop.upToDate":`Profil güncel`,"claudeDesktop.saving":`Kaydediliyor…`,"claudeDesktop.applying":`Uygulanıyor…`,"claudeDesktop.saveApply":`Kaydet & uygula`,"claudeDesktop.emptyTitle":`Kullanılabilir model yok`,"claudeDesktop.emptyHint":`Bir sağlayıcı ekleyin veya etkinleştirin.`,"claudeDesktop.assignmentsLabel":`Claude model ailesi atamaları`,"claudeDesktop.family.opus":`Opus`,"claudeDesktop.family.fable":`Fable`,"claudeDesktop.family.sonnet":`Sonnet`,"claudeDesktop.family.haiku":`Haiku`,"claudeDesktop.modelCountOne":`{count} model`,"claudeDesktop.modelCountMany":`{count} model`,"claudeDesktop.chooseDefault":`Bir varsayılan seçin`,"claudeDesktop.temporaryDefault":`Geçici varsayılan`,"claudeDesktop.laneEmpty":`Buraya bir model sürükleyin.`,"claudeDesktop.laneNoMatch":`Bu ailede aramanızla eşleşen model yok.`,"nav.grok":`Grok`,"grok.title":`Grok Build`,"grok.subtitle":`opencodex'in Grok konfigürasyonunuza kaydettiği modeller.`,"grok.loading":`Grok durumu yükleniyor…`,"grok.loadFail":`Grok konfigürasyonu okunamadı.`,"grok.notConfiguredTitle":`Grok Build henüz bağlanmadı`,"grok.notConfiguredHint":`Proxy'yi Grok yüklüyken başlatın.`,"grok.endpoint":`Uç nokta`,"grok.colModel":`Model`,"grok.colAlias":`Grok takma adı`,"grok.colContext":`Bağlam`,"grok.groupNative":`Yerel modeller`,"grok.groupRouted":`Yönlendirilen modeller`,"grok.enabledCount":`{total} modelden {on} tanesi kayıtlı`,"grok.saved":`Seçim kaydedildi.`,"grok.savedApplied":`Seçim kaydedildi ve Grok konfigürasyonuna yazıldı.`,"grok.saveFailed":`Grok seçimi kaydedilemedi.`,"grok.applyFailed":`Seçim kaydedildi ancak Grok güncellenemedi.`,"grok.applySkipped":`Seçim kaydedildi.`,"grok.saveApply":`Kaydet & uygula`,"grok.saving":`Kaydediliyor…`,"grok.applying":`Uygulanıyor…`,"grok.unsaved":`Kaydedilmemiş değişiklikler`,"grok.upToDate":`Seçim güncel`,"grok.toggleModel":`{id} modelini Grok ile kaydet`,"claudeDesktop.available":`Kullanılabilir`,"claudeDesktop.defaultBadge":`Varsayılan`,"claudeDesktop.supports1m":`1M`,"claudeDesktop.unavailable":`Kullanılamıyor`,"claudeDesktop.contextM":`{n}M bağlam`,"claudeDesktop.contextK":`{n}k bağlam`,"claudeDesktop.contextUnknown":`bağlam bilinmiyor`,"claudeDesktop.alias":`Takma Ad`,"claudeDesktop.useAsDefault":`{family} varsayılanı olarak kullan`,"claudeDesktop.moveTo":`Şuraya taşı`,"claudeDesktop.move":`Taşı`,"claudeDesktop.status.applied":`Desktop'a Uygulandı`,"claudeDesktop.status.stale":`Konfigürasyon eski — tekrar uygulayın`,"claudeDesktop.status.notApplied":`Uygulanmadı`,"claudeDesktop.status.notActiveProfile":`Desktop başka bir profil kullanıyor`,"claudeDesktop.status.disabled":`Claude Desktop entegrasyonu kapalı.`,"claudeDesktop.enableApply":`Etkinleştir ve uygula`,"claudeDesktop.health.lastRequest":`Son istek`,"claudeDesktop.health.stats":`{count} istek / {errors} hata`,"claudeDesktop.effort.supported":`çaba`,"claudeDesktop.effort.displayOnly":`çaba (yalnızca ekran)`,"lab.title":`Compatibility Lab`,"lab.subtitle":`Read-only compatibility verdict matrix from lab projection evidence.`,"lab.loadFailed":`Could not load compatibility lab data`,"lab.projectionUnavailable":`Lab projection is not available. Run conformance or live probes first.`,"lab.projectionIncompatible":`Lab projection schema is incompatible. Rebuild the projection.`,"lab.statusTitle":`Projection status`,"lab.matrixTitle":`Compatibility matrix`,"lab.verdictsTitle":`Verdict records`,"lab.filter.layer":`Evidence layer`,"lab.filter.verdict":`Verdict`,"lab.filter.subject":`Subject ID`,"lab.filter.all":`All`,"lab.col.subject":`Subject`,"lab.col.layer":`Layer`,"lab.col.suite":`Suite`,"lab.col.verdict":`Verdict`,"lab.col.asOf":`As of`,"lab.col.protocol":`Protocol conformance`,"lab.col.live":`Live route compatibility`,"lab.col.task":`Task effectiveness`,"lab.empty":`No compatibility verdicts in the projection yet.`,"lab.subjectKind":`Kind`,"lab.observationCount":`Observations`,"lab.eventCount":`Events`,"lab.verdictCount":`Verdicts`,"lab.subjectCount":`Subjects`,"lab.builtAt":`Built`,"lab.loading":`Loading compatibility evidence…`,"lab.loadMore":`Load more`,"lab.detailTitle":`Verdict detail`,"lab.detailClose":`Close`,"lab.detailSubject":`Subject`,"lab.detailObservations":`Observations`,"lab.detailEvents":`Contributing events`,"lab.detailArtifacts":`Artifact metadata`,"lab.production.title":`Gözlemlenen üretim trafiği`,"lab.production.notVerification":`Lab doğrulaması değildir`,"lab.production.attempts":`Denemeler`,"lab.production.successes":`Başarılı denemeler`,"lab.production.routeErrors":`Rota hataları`,"lab.production.lastObserved":`Son gözlem`,"lab.detailLoadFailed":`Could not load verdict detail`,"lab.refresh":`Refresh`,"lab.verdict.UNKNOWN":`Unknown`,"lab.verdict.CLAIMED":`Claimed`,"lab.verdict.PROBED":`Probed`,"lab.verdict.VERIFIED":`Verified`,"lab.verdict.DEGRADED":`Degraded`,"lab.verdict.BLOCKED":`Blocked`,"lab.verdict.UNSUPPORTED":`Unsupported`,"lab.layer.protocol_conformance":`Protocol conformance`,"lab.layer.live_route_compatibility":`Live route compatibility`,"lab.layer.task_effectiveness":`Task effectiveness`,"dash.visionAdvanced":`Gelişmiş ayarlar`,"dash.visionMaxDescriptions":`Tur başına en fazla açıklama`,"dash.visionMaxDescriptionsInvalid":`Pozitif bir tam sayı girin.`,"dash.visionTimeout":`Zaman aşımı`,"dash.visionTimeoutInvalid":`{min} ile {max} milisaniye arasında bir tam sayı girin.`,"dash.visionAdvancedPopover":`Gelişmiş görsel ayarları`,"models.newPolicyGlobal":`Yeni modeller devre dışı başlasın`,"models.newPolicyProvider":`Yeni model ilkesi`,"models.newPolicy_inherit":`Devral`,"models.newPolicy_off":`Kapalı`,"models.newPolicy_on":`Açık`,"models.newBadge":`YENİ`,"models.newCount":`{count} yeni, kapalı`,"models.aliases":`Takma adlar`,"models.aliasesTable":`Takma ad tablosu`,"models.aliasPrompt":`Sağlayıcı takma adı (temizlemek için boş bırakın)`,"models.modelAliasPrompt":`Model takma adı (temizlemek için boş bırakın)`,"models.aliasSaved":`Takma ad kaydedildi`,"models.aliasConflict":`Bu takma ad mevcut bir adla çakışıyor`,"models.editProviderAlias":`Sağlayıcı takma adını düzenle`,"models.editModelAlias":`Model takma adını düzenle`,"models.useDefaultAliases":`Varsayılan takma adları kullan`,"models.useDefaultAliasesGlobal":`Varsayılan takma adları her yerde kullan`,"models.aliasAuto":`otomatik`,"models.aliasUser":`kullanıcı`,"models.aliasStale":`eski`,"connection.discovering":`Discovering local and shared targets…`,"connection.machineUnavailable":`The local machine plane is unavailable. Shared requests were not redirected locally.`,"connection.disconnect":`Disconnect from hub`,"connection.disconnectConfirm":`Disconnect this machine from the hub and restart it in standalone mode?`,"connection.pairing.title":`Connect this dashboard to the hub`,"connection.pairing.body":`Paste the one-time pairing code created on the hub.`,"connection.pairing.relayWarning":`This code is exchanged through the fixed hub relay. The relay cannot be redirected to another host.`,"connection.pairing.code":`One-time pairing code`,"connection.pairing.submit":`Connect`,"connection.pairing.submitting":`Connecting…`,"connection.pairing.error":`The pairing code was refused or expired. The code was left in place so you can check it.`,"connection.machine.title":`This machine`,"connection.machine.shimHealthy":`Codex shim is healthy.`,"connection.machine.shimNeedsAttention":`Codex shim needs attention.`,"connection.machine.repairShim":`Repair shim`,"connection.machine.removeShim":`Remove shim`,"connection.clients.title":`Connected clients`,"connection.clients.none":`No client status available`,"connection.clients.sync":`Sync now`,"connection.clients.syncing":`Syncing…`,"connection.sessionLogout":`Uzak oturumdan çık`,"connection.sessionLoggingOut":`Uzak oturumdan çıkılıyor…`,"connection.sessionLogoutFailed":`Uzak oturumdan çıkılamadı. Mevcut oturum korundu.`,"usage.source.connected":`Source: hub usage`,"usage.source.local":`Source: local usage.jsonl`,"usage.scope.label":`Usage scope`,"usage.scope.machine":`This machine`,"usage.scope.hub":`Hub-wide`,"usage.hubOffline":`Hub usage is unavailable. Local usage was not substituted.`,"integrations.tab.cursor":`Cursor`,"integrations.detail.cursorSeen":`Cursor kısa süre önce bu proxy'ye istek gönderdi`,"integrations.detail.cursorNeverSeen":`Private Inference yüklü; henüz istek alınmadı`,"integrations.detail.cursorAbsent":`Cursor Private Inference bulunamadı`,"integrations.cursor.title":`Cursor`,"integrations.cursor.intro":`Cursor Private Inference, aracısını yerel olarak çalıştırır ve geri döngü üzerinden opencodex ile iletişim kurar. Normal Cursor bunu yapamaz: arka ucu özel uç noktayı çağırır ve herkese açık bir HTTPS URL'sine ihtiyaç duyar. Bu sayfa Cursor'a hiçbir zaman yazmaz; aşağıdaki değerleri Cursor'a kendiniz yapıştırın.`,"integrations.cursor.loading":`Cursor durumu okunuyor…`,"integrations.cursor.unavailable":`Cursor durumu proxy'den okunamadı.`,"integrations.cursor.detection":`Yüklü derlemeler`,"integrations.cursor.privateInference":`Cursor Private Inference`,"integrations.cursor.regular":`Cursor (normal)`,"integrations.cursor.detected":`Algılandı`,"integrations.cursor.notFound":`Bulunamadı`,"integrations.cursor.regularOnly":`Yalnızca normal Cursor bulundu. Özel uç noktaları Cursor sunucuları üzerinden yönlendirdiği için geri döngü proxy'sine herkese açık bir tünel olmadan erişilemez. Private Inference derlemesi için kılavuza bakın.`,"integrations.cursor.nothingFound":`Olağan konumlarda Cursor kurulumu bulunamadı. Başka bir yere yüklenmişse aşağıdaki değerler yine de geçerlidir.`,"integrations.cursor.gateway":`Ağ geçidi değerleri`,"integrations.cursor.gatewayHint":`Cursor Private Inference'da Settings > Models > Gateway bölümünü açın, bu iki değeri yapıştırın ve ardından Refresh model list düğmesine basın.`,"integrations.cursor.baseUrl":`Base URL`,"integrations.cursor.apiKey":`API Key`,"integrations.cursor.apiKeyCredential":`opencodex API anahtarlarınızdan biri (bu bağlantı için kimlik bilgisi gerekir)`,"integrations.cursor.copy":`Kopyala`,"integrations.cursor.copied":`Kopyalandı`,"integrations.cursor.connection":`Bağlantı`,"integrations.cursor.seen":`Cursor'dan gelen son istek: {time} ({ua})`,"integrations.cursor.neverSeen":`Proxy başlatıldığından beri Cursor'dan istek alınmadı. Ağ geçidini kaydettikten sonra Cursor'da Refresh model list düğmesine basın.`,"integrations.cursor.models":`Cursor'da gösterilecekler`,"integrations.cursor.modelsHint":`Cursor, akıl yürütme kademesini kendi model tablosundan seçtiği için opencodex bunu yalnızca tahmin edebilir. Bağlam sütunu varsayılan pencereyi ve isteğe bağlı pencereyi (Cursor'ın Max Mode'u) listeler.`,"integrations.cursor.ladderFromBundle":`Akıl yürütme kademeleri yüklü Cursor Private Inference {version} paketinden okundu. Bunlara Cursor karar verir; opencodex yalnızca tablosunu gösterir.`,"integrations.cursor.ladderFromStatic":`Akıl yürütme kademeleri Cursor 3.18.25'in statik bir kopyasıdır (okunabilir bir Private Inference paketi bulunamadı). Bağlam sütunu varsayılan ve isteğe bağlı pencereyi gösterir.`,"integrations.cursor.unknownVersion":`bilinmeyen sürüm`,"integrations.cursor.noControl":`—`,"integrations.cursor.singleWindow":`tek pencere`,"integrations.cursor.noControlTitle":`Bu kimlik Cursor'ın yerleşik çaba tablosunda yok, bu yüzden Cursor akıl yürütme denetimi göstermez.`,"integrations.cursor.effortRowsOne":`1 çaba satırı yayımlandı`,"integrations.cursor.effortRowsMany":`{n} çaba satırı yayımlandı`,"integrations.cursor.effortRowsOff":`çaba satırı yok`,"integrations.cursor.tableLessHint":`— ile işaretli satırlar Cursor'da akıl yürütme denetimi almaz. Her çaba için bir seçici girdisi (id--effort) yayımlamak üzere cursorEffortRows'u açın veya sabit bir varsayılan için sağlayıcıda modelDefaultReasoningEfforts ayarlayın.`,"integrations.cursor.colModel":`Model`,"integrations.cursor.colReasoning":`Akıl yürütme`,"integrations.cursor.colContext":`Bağlam`,"integrations.cursor.guide":`Cursor Private Inference kılavuzunu aç`},qe={en:{"lab.title":`Compatibility Lab`,"lab.subtitle":`Read-only compatibility verdict matrix from lab projection evidence.`,"lab.loadFailed":`Could not load compatibility lab data`,"lab.projectionUnavailable":`Lab projection is not available. Run conformance or live probes first.`,"lab.projectionIncompatible":`Lab projection schema is incompatible. Rebuild the projection.`,"lab.statusTitle":`Projection status`,"lab.matrixTitle":`Compatibility matrix`,"lab.verdictsTitle":`Verdict records`,"lab.filter.layer":`Evidence layer`,"lab.filter.verdict":`Verdict`,"lab.filter.subject":`Subject ID`,"lab.filter.all":`All`,"lab.col.subject":`Subject`,"lab.col.layer":`Layer`,"lab.col.suite":`Suite`,"lab.col.verdict":`Verdict`,"lab.col.asOf":`As of`,"lab.col.protocol":`Protocol conformance`,"lab.col.live":`Live route compatibility`,"lab.col.task":`Task effectiveness`,"lab.empty":`No compatibility verdicts in the projection yet.`,"lab.subjectKind":`Kind`,"lab.observationCount":`Observations`,"lab.eventCount":`Events`,"lab.verdictCount":`Verdicts`,"lab.subjectCount":`Subjects`,"lab.builtAt":`Built`,"lab.loading":`Loading compatibility evidence…`,"lab.loadMore":`Load more`,"lab.detailTitle":`Verdict detail`,"lab.detailClose":`Close`,"lab.detailSubject":`Subject`,"lab.detailObservations":`Observations`,"lab.detailEvents":`Evidence events`,"lab.detailArtifacts":`Artifact metadata`,"lab.detailLoadFailed":`Could not load verdict detail`,"lab.refresh":`Refresh`,"lab.verdict.UNKNOWN":`Unknown`,"lab.verdict.CLAIMED":`Claimed`,"lab.verdict.PROBED":`Probed`,"lab.verdict.VERIFIED":`Verified`,"lab.verdict.DEGRADED":`Degraded`,"lab.verdict.BLOCKED":`Blocked`,"lab.verdict.UNSUPPORTED":`Unsupported`,"lab.layer.protocol_conformance":`Protocol conformance`,"lab.layer.live_route_compatibility":`Live route compatibility`,"lab.layer.task_effectiveness":`Task effectiveness`},de:{"lab.title":`Kompatibilitäts-Labor`,"lab.subtitle":`Schreibgeschützte Kompatibilitätsmatrix aus den Evidenzen der Lab-Projektion.`,"lab.loadFailed":`Kompatibilitätsdaten konnten nicht geladen werden`,"lab.projectionUnavailable":`Die Lab-Projektion ist nicht verfügbar. Führe zuerst Konformitäts- oder Live-Probes aus.`,"lab.projectionIncompatible":`Das Schema der Lab-Projektion ist inkompatibel. Baue die Projektion neu auf.`,"lab.statusTitle":`Projektionsstatus`,"lab.matrixTitle":`Kompatibilitätsmatrix`,"lab.verdictsTitle":`Urteilsdatensätze`,"lab.filter.layer":`Evidenzschicht`,"lab.filter.verdict":`Urteil`,"lab.filter.subject":`Subjekt-ID`,"lab.filter.all":`Alle`,"lab.col.subject":`Subjekt`,"lab.col.layer":`Schicht`,"lab.col.suite":`Suite`,"lab.col.verdict":`Urteil`,"lab.col.asOf":`Stand`,"lab.col.protocol":`Protokollkonformität`,"lab.col.live":`Live-Route-Kompatibilität`,"lab.col.task":`Aufgabenwirksamkeit`,"lab.empty":`In der Projektion gibt es noch keine Kompatibilitätsurteile.`,"lab.subjectKind":`Art`,"lab.observationCount":`Beobachtungen`,"lab.eventCount":`Ereignisse`,"lab.verdictCount":`Urteile`,"lab.subjectCount":`Subjekte`,"lab.builtAt":`Erstellt`,"lab.loading":`Kompatibilitätsevidenz wird geladen…`,"lab.loadMore":`Mehr laden`,"lab.detailTitle":`Urteilsdetails`,"lab.detailClose":`Schließen`,"lab.detailSubject":`Subjekt`,"lab.detailObservations":`Beobachtungen`,"lab.detailEvents":`Evidenzereignisse`,"lab.detailArtifacts":`Artefakt-Metadaten`,"lab.detailLoadFailed":`Urteilsdetails konnten nicht geladen werden`,"lab.refresh":`Aktualisieren`,"lab.verdict.UNKNOWN":`Unbekannt`,"lab.verdict.CLAIMED":`Behauptet`,"lab.verdict.PROBED":`Geprüft`,"lab.verdict.VERIFIED":`Verifiziert`,"lab.verdict.DEGRADED":`Eingeschränkt`,"lab.verdict.BLOCKED":`Blockiert`,"lab.verdict.UNSUPPORTED":`Nicht unterstützt`,"lab.layer.protocol_conformance":`Protokollkonformität`,"lab.layer.live_route_compatibility":`Live-Route-Kompatibilität`,"lab.layer.task_effectiveness":`Aufgabenwirksamkeit`},fr:{"lab.title":`Laboratoire de compatibilité`,"lab.subtitle":`Matrice en lecture seule des verdicts de compatibilité fondée sur les preuves de la projection du laboratoire.`,"lab.loadFailed":`Impossible de charger les données du laboratoire de compatibilité`,"lab.projectionUnavailable":`La projection du laboratoire n’est pas disponible. Exécutez d’abord les sondes de conformité ou en conditions réelles.`,"lab.projectionIncompatible":`Le schéma de la projection du laboratoire est incompatible. Reconstruisez la projection.`,"lab.statusTitle":`État de la projection`,"lab.matrixTitle":`Matrice de compatibilité`,"lab.verdictsTitle":`Enregistrements des verdicts`,"lab.filter.layer":`Couche de preuves`,"lab.filter.verdict":`Verdict`,"lab.filter.subject":`ID du sujet`,"lab.filter.all":`Tous`,"lab.col.subject":`Sujet`,"lab.col.layer":`Couche`,"lab.col.suite":`Suite`,"lab.col.verdict":`Verdict`,"lab.col.asOf":`Établi le`,"lab.col.protocol":`Conformité au protocole`,"lab.col.live":`Compatibilité du routage en conditions réelles`,"lab.col.task":`Efficacité des tâches`,"lab.empty":`La projection ne contient encore aucun verdict de compatibilité.`,"lab.subjectKind":`Type`,"lab.observationCount":`Observations`,"lab.eventCount":`Événements`,"lab.verdictCount":`Verdicts`,"lab.subjectCount":`Sujets`,"lab.builtAt":`Générée le`,"lab.loading":`Chargement des preuves de compatibilité…`,"lab.loadMore":`Charger plus`,"lab.detailTitle":`Détails du verdict`,"lab.detailClose":`Fermer`,"lab.detailSubject":`Sujet`,"lab.detailObservations":`Observations`,"lab.detailEvents":`Événements probants`,"lab.detailArtifacts":`Métadonnées des artefacts`,"lab.detailLoadFailed":`Impossible de charger les détails du verdict`,"lab.refresh":`Actualiser`,"lab.verdict.UNKNOWN":`Inconnu`,"lab.verdict.CLAIMED":`Déclaré`,"lab.verdict.PROBED":`Sondé`,"lab.verdict.VERIFIED":`Vérifié`,"lab.verdict.DEGRADED":`Dégradé`,"lab.verdict.BLOCKED":`Bloqué`,"lab.verdict.UNSUPPORTED":`Non pris en charge`,"lab.layer.protocol_conformance":`Conformité au protocole`,"lab.layer.live_route_compatibility":`Compatibilité du routage en conditions réelles`,"lab.layer.task_effectiveness":`Efficacité des tâches`},ko:{"lab.title":`호환성 랩`,"lab.subtitle":`랩 프로젝션 증거를 기반으로 한 읽기 전용 호환성 판정 매트릭스입니다.`,"lab.loadFailed":`호환성 랩 데이터를 불러오지 못했습니다`,"lab.projectionUnavailable":`랩 프로젝션을 사용할 수 없습니다. 먼저 적합성 또는 라이브 프로브를 실행하세요.`,"lab.projectionIncompatible":`랩 프로젝션 스키마가 호환되지 않습니다. 프로젝션을 다시 빌드하세요.`,"lab.statusTitle":`프로젝션 상태`,"lab.matrixTitle":`호환성 매트릭스`,"lab.verdictsTitle":`판정 레코드`,"lab.filter.layer":`증거 레이어`,"lab.filter.verdict":`판정`,"lab.filter.subject":`대상 ID`,"lab.filter.all":`전체`,"lab.col.subject":`대상`,"lab.col.layer":`레이어`,"lab.col.suite":`스위트`,"lab.col.verdict":`판정`,"lab.col.asOf":`기준 시각`,"lab.col.protocol":`프로토콜 적합성`,"lab.col.live":`라이브 경로 호환성`,"lab.col.task":`작업 효과성`,"lab.empty":`프로젝션에 아직 호환성 판정이 없습니다.`,"lab.subjectKind":`종류`,"lab.observationCount":`관측`,"lab.eventCount":`이벤트`,"lab.verdictCount":`판정`,"lab.subjectCount":`대상`,"lab.builtAt":`빌드 시각`,"lab.loading":`호환성 증거를 불러오는 중…`,"lab.loadMore":`더 불러오기`,"lab.detailTitle":`판정 상세`,"lab.detailClose":`닫기`,"lab.detailSubject":`대상`,"lab.detailObservations":`관측`,"lab.detailEvents":`증거 이벤트`,"lab.detailArtifacts":`아티팩트 메타데이터`,"lab.detailLoadFailed":`판정 상세를 불러오지 못했습니다`,"lab.refresh":`새로고침`,"lab.verdict.UNKNOWN":`알 수 없음`,"lab.verdict.CLAIMED":`주장됨`,"lab.verdict.PROBED":`프로브됨`,"lab.verdict.VERIFIED":`검증됨`,"lab.verdict.DEGRADED":`저하됨`,"lab.verdict.BLOCKED":`차단됨`,"lab.verdict.UNSUPPORTED":`지원되지 않음`,"lab.layer.protocol_conformance":`프로토콜 적합성`,"lab.layer.live_route_compatibility":`라이브 경로 호환성`,"lab.layer.task_effectiveness":`작업 효과성`},zh:{"lab.title":`兼容性实验室`,"lab.subtitle":`基于实验室投影证据的只读兼容性判定矩阵。`,"lab.loadFailed":`无法加载兼容性实验室数据`,"lab.projectionUnavailable":`实验室投影不可用。请先运行一致性探测或实时探测。`,"lab.projectionIncompatible":`实验室投影架构不兼容。请重新构建投影。`,"lab.statusTitle":`投影状态`,"lab.matrixTitle":`兼容性矩阵`,"lab.verdictsTitle":`判定记录`,"lab.filter.layer":`证据层`,"lab.filter.verdict":`判定`,"lab.filter.subject":`主体 ID`,"lab.filter.all":`全部`,"lab.col.subject":`主体`,"lab.col.layer":`层`,"lab.col.suite":`测试套件`,"lab.col.verdict":`判定`,"lab.col.asOf":`截至`,"lab.col.protocol":`协议一致性`,"lab.col.live":`实时路由兼容性`,"lab.col.task":`任务有效性`,"lab.empty":`投影中还没有兼容性判定。`,"lab.subjectKind":`类型`,"lab.observationCount":`观测`,"lab.eventCount":`事件`,"lab.verdictCount":`判定`,"lab.subjectCount":`主体`,"lab.builtAt":`构建时间`,"lab.loading":`正在加载兼容性证据…`,"lab.loadMore":`加载更多`,"lab.detailTitle":`判定详情`,"lab.detailClose":`关闭`,"lab.detailSubject":`主体`,"lab.detailObservations":`观测`,"lab.detailEvents":`证据事件`,"lab.detailArtifacts":`制品元数据`,"lab.detailLoadFailed":`无法加载判定详情`,"lab.refresh":`刷新`,"lab.verdict.UNKNOWN":`未知`,"lab.verdict.CLAIMED":`已声明`,"lab.verdict.PROBED":`已探测`,"lab.verdict.VERIFIED":`已验证`,"lab.verdict.DEGRADED":`降级`,"lab.verdict.BLOCKED":`已阻止`,"lab.verdict.UNSUPPORTED":`不支持`,"lab.layer.protocol_conformance":`协议一致性`,"lab.layer.live_route_compatibility":`实时路由兼容性`,"lab.layer.task_effectiveness":`任务有效性`},"zh-TW":{"lab.title":`相容性實驗室`,"lab.subtitle":`基於實驗室投影證據的唯讀相容性判定矩陣。`,"lab.loadFailed":`無法載入相容性實驗室資料`,"lab.projectionUnavailable":`實驗室投影不可用。請先執行一致性探測或即時探測。`,"lab.projectionIncompatible":`實驗室投影架構不相容。請重新建置投影。`,"lab.statusTitle":`投影狀態`,"lab.matrixTitle":`相容性矩陣`,"lab.verdictsTitle":`判定記錄`,"lab.filter.layer":`證據層`,"lab.filter.verdict":`判定`,"lab.filter.subject":`主體 ID`,"lab.filter.all":`全部`,"lab.col.subject":`主體`,"lab.col.layer":`層`,"lab.col.suite":`測試套件`,"lab.col.verdict":`判定`,"lab.col.asOf":`截至`,"lab.col.protocol":`協定一致性`,"lab.col.live":`即時路由相容性`,"lab.col.task":`任務有效性`,"lab.empty":`投影中還沒有相容性判定。`,"lab.subjectKind":`類型`,"lab.observationCount":`觀測`,"lab.eventCount":`事件`,"lab.verdictCount":`判定`,"lab.subjectCount":`主體`,"lab.builtAt":`建置時間`,"lab.loading":`正在載入相容性證據…`,"lab.loadMore":`載入更多`,"lab.detailTitle":`判定詳情`,"lab.detailClose":`關閉`,"lab.detailSubject":`主體`,"lab.detailObservations":`觀測`,"lab.detailEvents":`證據事件`,"lab.detailArtifacts":`產物中繼資料`,"lab.detailLoadFailed":`無法載入判定詳情`,"lab.refresh":`重新整理`,"lab.verdict.UNKNOWN":`未知`,"lab.verdict.CLAIMED":`已聲明`,"lab.verdict.PROBED":`已探測`,"lab.verdict.VERIFIED":`已驗證`,"lab.verdict.DEGRADED":`降級`,"lab.verdict.BLOCKED":`已封鎖`,"lab.verdict.UNSUPPORTED":`不支援`,"lab.layer.protocol_conformance":`協定一致性`,"lab.layer.live_route_compatibility":`即時路由相容性`,"lab.layer.task_effectiveness":`任務有效性`},ru:{"lab.title":`Лаборатория совместимости`,"lab.subtitle":`Матрица вердиктов совместимости только для чтения на основе данных проекции лаборатории.`,"lab.loadFailed":`Не удалось загрузить данные лаборатории совместимости`,"lab.projectionUnavailable":`Проекция лаборатории недоступна. Сначала выполните проверки соответствия или live-проверки.`,"lab.projectionIncompatible":`Схема проекции лаборатории несовместима. Перестройте проекцию.`,"lab.statusTitle":`Состояние проекции`,"lab.matrixTitle":`Матрица совместимости`,"lab.verdictsTitle":`Записи вердиктов`,"lab.filter.layer":`Слой доказательств`,"lab.filter.verdict":`Вердикт`,"lab.filter.subject":`ID субъекта`,"lab.filter.all":`Все`,"lab.col.subject":`Субъект`,"lab.col.layer":`Слой`,"lab.col.suite":`Набор`,"lab.col.verdict":`Вердикт`,"lab.col.asOf":`По состоянию на`,"lab.col.protocol":`Соответствие протоколу`,"lab.col.live":`Совместимость live-маршрута`,"lab.col.task":`Эффективность задач`,"lab.empty":`В проекции пока нет вердиктов совместимости.`,"lab.subjectKind":`Тип`,"lab.observationCount":`Наблюдения`,"lab.eventCount":`События`,"lab.verdictCount":`Вердикты`,"lab.subjectCount":`Субъекты`,"lab.builtAt":`Собрано`,"lab.loading":`Загрузка доказательств совместимости…`,"lab.loadMore":`Загрузить ещё`,"lab.detailTitle":`Детали вердикта`,"lab.detailClose":`Закрыть`,"lab.detailSubject":`Субъект`,"lab.detailObservations":`Наблюдения`,"lab.detailEvents":`События доказательств`,"lab.detailArtifacts":`Метаданные артефактов`,"lab.detailLoadFailed":`Не удалось загрузить детали вердикта`,"lab.refresh":`Обновить`,"lab.verdict.UNKNOWN":`Неизвестно`,"lab.verdict.CLAIMED":`Заявлено`,"lab.verdict.PROBED":`Проверено пробой`,"lab.verdict.VERIFIED":`Подтверждено`,"lab.verdict.DEGRADED":`Ограничено`,"lab.verdict.BLOCKED":`Заблокировано`,"lab.verdict.UNSUPPORTED":`Не поддерживается`,"lab.layer.protocol_conformance":`Соответствие протоколу`,"lab.layer.live_route_compatibility":`Совместимость live-маршрута`,"lab.layer.task_effectiveness":`Эффективность задач`},ja:{"lab.title":`互換性ラボ`,"lab.subtitle":`ラボ投影の証拠に基づく読み取り専用の互換性判定マトリクスです。`,"lab.loadFailed":`互換性ラボのデータを読み込めませんでした`,"lab.projectionUnavailable":`ラボ投影を利用できません。先に適合性プローブまたはライブプローブを実行してください。`,"lab.projectionIncompatible":`ラボ投影のスキーマに互換性がありません。投影を再構築してください。`,"lab.statusTitle":`投影ステータス`,"lab.matrixTitle":`互換性マトリクス`,"lab.verdictsTitle":`判定レコード`,"lab.filter.layer":`証拠レイヤー`,"lab.filter.verdict":`判定`,"lab.filter.subject":`サブジェクト ID`,"lab.filter.all":`すべて`,"lab.col.subject":`サブジェクト`,"lab.col.layer":`レイヤー`,"lab.col.suite":`スイート`,"lab.col.verdict":`判定`,"lab.col.asOf":`時点`,"lab.col.protocol":`プロトコル適合性`,"lab.col.live":`ライブ経路互換性`,"lab.col.task":`タスク有効性`,"lab.empty":`投影にはまだ互換性判定がありません。`,"lab.subjectKind":`種類`,"lab.observationCount":`観測`,"lab.eventCount":`イベント`,"lab.verdictCount":`判定`,"lab.subjectCount":`サブジェクト`,"lab.builtAt":`構築日時`,"lab.loading":`互換性の証拠を読み込み中…`,"lab.loadMore":`さらに読み込む`,"lab.detailTitle":`判定の詳細`,"lab.detailClose":`閉じる`,"lab.detailSubject":`サブジェクト`,"lab.detailObservations":`観測`,"lab.detailEvents":`証拠イベント`,"lab.detailArtifacts":`アーティファクトのメタデータ`,"lab.detailLoadFailed":`判定の詳細を読み込めませんでした`,"lab.refresh":`更新`,"lab.verdict.UNKNOWN":`不明`,"lab.verdict.CLAIMED":`申告済み`,"lab.verdict.PROBED":`プローブ済み`,"lab.verdict.VERIFIED":`検証済み`,"lab.verdict.DEGRADED":`低下`,"lab.verdict.BLOCKED":`ブロック`,"lab.verdict.UNSUPPORTED":`未対応`,"lab.layer.protocol_conformance":`プロトコル適合性`,"lab.layer.live_route_compatibility":`ライブ経路互換性`,"lab.layer.task_effectiveness":`タスク有効性`},tr:{"lab.title":`Uyumluluk Laboratuvarı`,"lab.subtitle":`Laboratuvar projeksiyonu kanıtlarından oluşturulan salt okunur uyumluluk karar matrisi.`,"lab.loadFailed":`Uyumluluk laboratuvarı verileri yüklenemedi`,"lab.projectionUnavailable":`Laboratuvar projeksiyonu kullanılamıyor. Önce uygunluk veya canlı probları çalıştırın.`,"lab.projectionIncompatible":`Laboratuvar projeksiyonu şeması uyumsuz. Projeksiyonu yeniden oluşturun.`,"lab.statusTitle":`Projeksiyon durumu`,"lab.matrixTitle":`Uyumluluk matrisi`,"lab.verdictsTitle":`Karar kayıtları`,"lab.filter.layer":`Kanıt katmanı`,"lab.filter.verdict":`Karar`,"lab.filter.subject":`Özne kimliği`,"lab.filter.all":`Tümü`,"lab.col.subject":`Özne`,"lab.col.layer":`Katman`,"lab.col.suite":`Paket`,"lab.col.verdict":`Karar`,"lab.col.asOf":`Tarih`,"lab.col.protocol":`Protokol uygunluğu`,"lab.col.live":`Canlı rota uyumluluğu`,"lab.col.task":`Görev etkinliği`,"lab.empty":`Projeksiyonda henüz uyumluluk kararı yok.`,"lab.subjectKind":`Tür`,"lab.observationCount":`Gözlemler`,"lab.eventCount":`Olaylar`,"lab.verdictCount":`Kararlar`,"lab.subjectCount":`Özneler`,"lab.builtAt":`Oluşturulma`,"lab.loading":`Uyumluluk kanıtları yükleniyor…`,"lab.loadMore":`Daha fazla yükle`,"lab.detailTitle":`Karar ayrıntısı`,"lab.detailClose":`Kapat`,"lab.detailSubject":`Özne`,"lab.detailObservations":`Gözlemler`,"lab.detailEvents":`Kanıt olayları`,"lab.detailArtifacts":`Artefakt meta verileri`,"lab.detailLoadFailed":`Karar ayrıntısı yüklenemedi`,"lab.refresh":`Yenile`,"lab.verdict.UNKNOWN":`Bilinmiyor`,"lab.verdict.CLAIMED":`İddia edildi`,"lab.verdict.PROBED":`Problandı`,"lab.verdict.VERIFIED":`Doğrulandı`,"lab.verdict.DEGRADED":`Kısıtlı`,"lab.verdict.BLOCKED":`Engellendi`,"lab.verdict.UNSUPPORTED":`Desteklenmiyor`,"lab.layer.protocol_conformance":`Protokol uygunluğu`,"lab.layer.live_route_compatibility":`Canlı rota uyumluluğu`,"lab.layer.task_effectiveness":`Görev etkinliği`}},Je={en:{subjectKindUnknown:`Unknown`,"artifact.present":`Present`,"artifact.corrupt":`Corrupt`,"artifact.purged_unavailable":`Purged / unavailable`,selectVerdict:`View verdict for {subject}`,"community.title":`Community evidence`,"community.notLocalVerdict":`Untrusted read-only context. Not included in this local verdict.`,"community.bundles":`Bundles`,"community.activeRecords":`Active records`,"community.revokedRecords":`Revoked records`},de:{subjectKindUnknown:`Unbekannt`,"artifact.present":`Vorhanden`,"artifact.corrupt":`Beschädigt`,"artifact.purged_unavailable":`Gelöscht / nicht verfügbar`,selectVerdict:`Urteil für {subject} anzeigen`,"community.title":`Community-Evidenz`,"community.notLocalVerdict":`Nicht vertrauenswürdiger Nur-Lese-Kontext. Nicht Teil dieses lokalen Urteils.`,"community.bundles":`Pakete`,"community.activeRecords":`Aktive Einträge`,"community.revokedRecords":`Widerrufene Einträge`},fr:{subjectKindUnknown:`Inconnu`,"artifact.present":`Présent`,"artifact.corrupt":`Corrompu`,"artifact.purged_unavailable":`Purgé / indisponible`,selectVerdict:`Afficher le verdict pour {subject}`,"community.title":`Données de la communauté`,"community.notLocalVerdict":`Contexte non fiable en lecture seule. Non inclus dans ce verdict local.`,"community.bundles":`Lots`,"community.activeRecords":`Enregistrements actifs`,"community.revokedRecords":`Enregistrements révoqués`},ko:{subjectKindUnknown:`알 수 없음`,"artifact.present":`있음`,"artifact.corrupt":`손상됨`,"artifact.purged_unavailable":`삭제됨 / 사용할 수 없음`,selectVerdict:`{subject}의 판정 보기`,"community.title":`커뮤니티 증거`,"community.notLocalVerdict":`신뢰되지 않는 읽기 전용 컨텍스트입니다. 이 로컬 판정에는 포함되지 않습니다.`,"community.bundles":`번들`,"community.activeRecords":`활성 레코드`,"community.revokedRecords":`폐기된 레코드`},zh:{subjectKindUnknown:`未知`,"artifact.present":`存在`,"artifact.corrupt":`已损坏`,"artifact.purged_unavailable":`已清除 / 不可用`,selectVerdict:`查看 {subject} 的判定`,"community.title":`社区证据`,"community.notLocalVerdict":`不受信任的只读上下文。不计入此本地判定。`,"community.bundles":`证据包`,"community.activeRecords":`有效记录`,"community.revokedRecords":`已撤销记录`},"zh-TW":{subjectKindUnknown:`未知`,"artifact.present":`存在`,"artifact.corrupt":`已損壞`,"artifact.purged_unavailable":`已清除 / 不可用`,selectVerdict:`查看 {subject} 的判定`,"community.title":`社群證據`,"community.notLocalVerdict":`不受信任的唯讀脈絡。不計入此本地判定。`,"community.bundles":`證據包`,"community.activeRecords":`有效記錄`,"community.revokedRecords":`已撤銷記錄`},ru:{subjectKindUnknown:`Неизвестно`,"artifact.present":`Доступен`,"artifact.corrupt":`Повреждён`,"artifact.purged_unavailable":`Удалён / недоступен`,selectVerdict:`Открыть вердикт для {subject}`,"community.title":`Данные сообщества`,"community.notLocalVerdict":`Недоверенный контекст только для чтения. Не входит в этот локальный вердикт.`,"community.bundles":`Пакеты`,"community.activeRecords":`Активные записи`,"community.revokedRecords":`Отозванные записи`},ja:{subjectKindUnknown:`不明`,"artifact.present":`存在`,"artifact.corrupt":`破損`,"artifact.purged_unavailable":`削除済み / 利用不可`,selectVerdict:`{subject} の判定を表示`,"community.title":`コミュニティ証拠`,"community.notLocalVerdict":`信頼されていない読み取り専用コンテキストです。このローカル判定には含まれません。`,"community.bundles":`バンドル`,"community.activeRecords":`有効なレコード`,"community.revokedRecords":`取り消されたレコード`},tr:{subjectKindUnknown:`Bilinmiyor`,"artifact.present":`Mevcut`,"artifact.corrupt":`Bozuk`,"artifact.purged_unavailable":`Temizlenmiş / kullanılamıyor`,selectVerdict:`{subject} için kararı görüntüle`,"community.title":`Topluluk kanıtı`,"community.notLocalVerdict":`Güvenilmeyen salt okunur bağlam. Bu yerel karara dahil değildir.`,"community.bundles":`Paketler`,"community.activeRecords":`Etkin kayıtlar`,"community.revokedRecords":`Geri çekilen kayıtlar`}};function Ye(e,t,n){let r=Je[e][t];if(n)for(let[e,t]of Object.entries(n))r=r.split(`{${e}}`).join(String(t));return r}function Xe(e,t){return{...t,...qe[e]}}var Ze={en:Xe(`en`,Re),de:Xe(`de`,ze),fr:Xe(`fr`,Be),ko:Xe(`ko`,Ve),zh:Xe(`zh`,He),"zh-TW":Xe(`zh-TW`,Ue),ru:Xe(`ru`,We),ja:Xe(`ja`,Ge),tr:Xe(`tr`,Ke)};function Qe(e){return Ze[e][`lang.nativeName`]}function $e(e,t){return Ze[e][t]}var et=[{code:`en`,htmlLang:`en`},{code:`de`,htmlLang:`de`},{code:`fr`,htmlLang:`fr`},{code:`ko`,htmlLang:`ko`},{code:`zh`,htmlLang:`zh-CN`},{code:`zh-TW`,htmlLang:`zh-TW`},{code:`ru`,htmlLang:`ru`},{code:`ja`,htmlLang:`ja`},{code:`tr`,htmlLang:`tr`}],tt=`ocx-lang`,nt=null;function rt(){try{let e=localStorage.getItem(tt);if(e===`en`||e===`de`||e===`fr`||e===`ko`||e===`zh`||e===`zh-TW`||e===`ru`||e===`ja`||e===`tr`)return e}catch{}let e=typeof navigator<`u`&&navigator?.language?navigator.language.toLowerCase():`en`;return e.startsWith(`de`)?`de`:e.startsWith(`fr`)?`fr`:e.startsWith(`ko`)?`ko`:e.startsWith(`zh`)?e.includes(`tw`)||e.includes(`hk`)||e.includes(`mo`)||e.includes(`hant`)?`zh-TW`:`zh`:e.startsWith(`ru`)?`ru`:e.startsWith(`ja`)?`ja`:e.startsWith(`tr`)?`tr`:`en`}function it(){return nt??rt()}function at(e){nt=e}var ot=(0,_.createContext)(null);function st(e,t){if(!t)return e;let n=e;for(let e of Object.keys(t))n=n.split(`{${e}}`).join(String(t[e]));return n}function ct(){let e=(0,_.useContext)(ot);if(!e)throw Error(`useI18n must be used within LanguageProvider`);return e}function Q(){return ct().t}function lt({children:e}){let[t,n]=(0,_.useState)(()=>{let e=rt();return at(e),e}),r=(0,_.useCallback)(e=>{at(e),n(e)},[]);(0,_.useEffect)(()=>{let e=et.find(e=>e.code===t)??et[0];document.documentElement.lang=e.htmlLang;try{localStorage.setItem(`ocx-lang`,t)}catch{}},[t]);let i=(0,_.useCallback)((e,n)=>st(Ze[t][e]??Re[e]??e,n),[t]),a=(0,_.useMemo)(()=>({locale:t,setLocale:r,t:i}),[t,i]);return(0,J.jsx)(ot.Provider,{value:a,children:e})}function ut({k:e,cmd:t,vars:n}){let{t:r}=ct(),[i,a=``]=r(e,n).split(`{cmd}`);return(0,J.jsxs)(J.Fragment,{children:[i,(0,J.jsx)(`code`,{className:`chip`,children:t}),a]})}function dt(e){return e.replace(/^#\/?/,``)}function ft(e,t=window){let n=dt(e);if(dt(t.location.hash)===n)return;let r=`${t.location.pathname}${t.location.search}#${n}`;t.history.replaceState(t.history.state,``,r)}function pt(e,t=window){let n=dt(e);dt(t.location.hash)!==n&&(t.location.hash=n)}var mt=m(),ht=4,gt=8,_t=8,vt=280,yt=120,bt=160,xt=12;function St(){return typeof window<`u`?window.innerHeight:800}function Ct(){return typeof window<`u`?window.innerWidth:1024}function wt(e,{align:t,placement:n=`below`,menuHeight:r=vt}={}){let i=Math.min(Math.max(r,yt),vt),a=St(),o=Ct();if(n===`right`){let t=a-e.top-_t,n=e.top-_t,r=i+gt>t&&n>t,s=Math.max(_t,Math.min(e.right+xt,o-bt-_t));return r?{position:`fixed`,left:s,bottom:a-e.top+gt,minWidth:bt,maxHeight:Math.max(yt,Math.min(vt,e.top-_t-ht))}:{position:`fixed`,top:e.top,left:s,minWidth:bt,maxHeight:Math.max(yt,Math.min(vt,a-e.top-_t))}}let s=t??(e.right>o/2?`right`:`left`),c=Math.max(e.width,0),l=a-e.bottom-_t,u=e.top-_t;if(i+ht>l&&u>l){let t={position:`fixed`,bottom:a-e.top+gt,minWidth:c,maxHeight:Math.max(0,Math.min(vt,u-ht))};return s===`right`?t.right=Math.max(_t,o-e.right):t.left=Math.max(_t,Math.min(e.left,o-_t-c)),t}let d={position:`fixed`,top:e.bottom+ht,minWidth:c,maxHeight:Math.max(0,Math.min(vt,l-ht))};return s===`right`?d.right=Math.max(_t,o-e.right):d.left=Math.max(_t,Math.min(e.left,o-_t-c)),d}function Tt({on:e,mixed:t=!1,onClick:n,disabled:r,label:i,showLabel:a=!1,title:o}){let s=a&&!!i;return(0,J.jsxs)(`button`,{type:`button`,className:`switch${e?` on`:``}${t?` mixed`:``}${s?` switch-labeled`:``}`,onClick:n,disabled:r,"aria-pressed":t?`mixed`:e,"aria-label":s?void 0:i??(e?`enabled`:`disabled`),title:o,children:[(0,J.jsx)(`span`,{className:`knob`}),s?(0,J.jsx)(`span`,{className:`switch-labeled-text text-label muted`,children:i}):null]})}function $({tone:e,children:t}){return(0,J.jsxs)(`div`,{className:`notice ${e===`ok`?`notice-ok`:e===`warn`?`notice-warn`:`notice-err`}`,role:`status`,children:[e===`ok`?(0,J.jsx)(ue,{}):(0,J.jsx)(_e,{}),(0,J.jsx)(`span`,{children:t})]})}function Et({tone:e,children:t,onDismiss:n,dismissLabel:r}){return(0,mt.createPortal)((0,J.jsx)(`div`,{className:`toast-notice-host`,role:`presentation`,children:(0,J.jsxs)(`div`,{className:`toast-notice notice ${e===`ok`?`notice-ok`:e===`warn`?`notice-warn`:`notice-err`}`,role:`status`,"aria-live":`polite`,children:[e===`ok`?(0,J.jsx)(ue,{}):(0,J.jsx)(_e,{}),(0,J.jsx)(`span`,{className:`toast-notice-copy`,children:t}),n&&(0,J.jsx)(`button`,{type:`button`,className:`toast-notice-dismiss`,onClick:n,"aria-label":r,children:`×`})]})}),document.body)}function Dt({value:e,options:t,onChange:n,disabled:r,id:i,label:a,describedBy:o,title:s,style:c,align:l,placement:u,dropdownStyle:d,portal:f=!0}){let p=(0,_.useId)(),[m,h]=(0,_.useState)(!1),[g,v]=(0,_.useState)(null),[y,b]=(0,_.useState)(),x=(0,_.useRef)(null),S=(0,_.useRef)(null),C=(0,_.useRef)(null),w=(0,_.useCallback)(e=>`${p}-${e}`,[p]),T=t.find(t=>t.value===e),E=t.length===0?0:Math.max(0,t.findIndex(t=>t.value===e)),D=!m||t.length===0?E:Math.min(g??E,t.length-1),O=(0,_.useCallback)((e=!1)=>{h(!1),v(null),e&&S.current?.focus()},[]),k=(0,_.useCallback)(e=>{if(r||t.length===0)return;let n=Math.max(0,Math.min(t.length-1,e));v(n),h(!0)},[r,t.length]),A=(0,_.useCallback)(e=>{if(!f)return;let t=S.current;t&&b(wt(t.getBoundingClientRect(),{align:l,placement:u,menuHeight:e}))},[l,u,f]);(0,_.useEffect)(()=>{if(!m)return;let e=e=>{let t=e.target;x.current?.contains(t)||C.current?.contains(t)||O()};return document.addEventListener(`mousedown`,e),()=>document.removeEventListener(`mousedown`,e)},[O,m]),(0,_.useLayoutEffect)(()=>{if(!m||!f)return;A();let e=()=>A(C.current?.offsetHeight);return window.addEventListener(`resize`,e),window.addEventListener(`scroll`,e,!0),()=>{window.removeEventListener(`resize`,e),window.removeEventListener(`scroll`,e,!0)}},[m,t.length,f,A]),(0,_.useLayoutEffect)(()=>{if(!m||!f||!C.current||!S.current)return;let e=C.current.offsetHeight;if(!e)return;let t=wt(S.current.getBoundingClientRect(),{align:l,placement:u,menuHeight:e});b(e=>e?.top===t.top&&e?.bottom===t.bottom&&e?.maxHeight===t.maxHeight?e:t)},[l,m,t.length,u,f]),(0,_.useLayoutEffect)(()=>{!m||!C.current||C.current.querySelector(`[id="${w(D)}"]`)?.scrollIntoView({block:`nearest`})},[D,m,w]);let j=e=>{if(r)return;let i=t[e];i&&(n(i.value),O(!0))},M=e=>{if(!r)switch(e.key){case`ArrowDown`:e.preventDefault(),k(m?Math.min(t.length-1,D+1):E);break;case`ArrowUp`:e.preventDefault(),k(m?Math.max(0,D-1):E);break;case`Home`:e.preventDefault(),k(0);break;case`End`:e.preventDefault(),k(t.length-1);break;case`Enter`:case` `:e.preventDefault(),m?j(D):k(E);break;case`Escape`:m&&(e.preventDefault(),O(!0));break;case`Tab`:if(m){let e=t[D];e&&n(e.value),h(!1)}}},N=m&&t[D]?w(D):void 0,P=m&&!r?(0,J.jsx)(`div`,{ref:C,id:p,className:`select-dropdown${f?` select-dropdown-portal`:``}${!f&&l===`right`?` select-dropdown-right`:``}${!f&&u===`right`?` select-dropdown-beside`:``}`,role:`listbox`,"aria-label":a,style:f?{...y,zIndex:60,...d}:d,children:t.map((t,n)=>(0,J.jsx)(`button`,{id:w(n),type:`button`,role:`option`,tabIndex:-1,disabled:r,"aria-selected":t.value===e,className:`select-option${t.value===e?` active`:``}${n===D?` select-option-active`:``}`,onMouseEnter:()=>v(n),onClick:()=>j(n),children:t.label},t.value))}):null;return(0,J.jsxs)(`div`,{ref:x,className:`custom-select`,style:{position:`relative`,display:`inline-block`,...c},children:[(0,J.jsxs)(`button`,{ref:S,id:i,type:`button`,role:`combobox`,title:s,"aria-describedby":o,className:`select-trigger`,onClick:()=>{r||(m?O():k(E))},onKeyDown:M,disabled:r,"aria-haspopup":`listbox`,"aria-expanded":m,"aria-controls":m?p:void 0,"aria-activedescendant":N,"aria-label":a,children:[(0,J.jsx)(`span`,{children:T?.label??e}),(0,J.jsx)(Se,{style:{width:12,height:12,color:`var(--muted)`,transform:m?`rotate(90deg)`:`none`,transition:`transform .12s`}})]}),f?P&&(0,mt.createPortal)(P,document.body):P]})}function Ot({icon:e,title:t,children:n,className:r,style:i}){return(0,J.jsxs)(`div`,{className:r?`empty ${r}`:`empty`,style:i,children:[e,(0,J.jsx)(`div`,{className:`title`,children:t}),n&&(0,J.jsx)(`div`,{className:`text-control`,children:n})]})}function kt({content:e,children:t,side:n=`top`,maxWidth:r=280}){let[i,a]=(0,_.useState)(!1),o=(0,_.useId)(),s=(0,_.useRef)(null),c=()=>{s.current!==null&&window.clearTimeout(s.current),s.current=window.setTimeout(()=>a(!0),150)},l=()=>{s.current!==null&&(window.clearTimeout(s.current),s.current=null),a(!1)};return(0,_.useEffect)(()=>()=>{s.current!==null&&window.clearTimeout(s.current)},[]),(0,J.jsxs)(`button`,{type:`button`,className:`ocx-tooltip`,onMouseEnter:c,onMouseLeave:l,onFocus:c,onBlur:l,onKeyDown:e=>{e.key===`Escape`&&l()},"aria-describedby":i?o:void 0,style:{display:`inline`,border:0,background:`transparent`,padding:0,margin:0,color:`inherit`,font:`inherit`,cursor:`inherit`},children:[t,i&&(0,J.jsx)(`span`,{id:o,className:`ocx-tooltip-bubble ocx-tooltip-bubble--${n}`,role:`tooltip`,style:{maxWidth:r},children:e})]})}var At=45e3,jt=2147483647;async function Mt(e){if(e.status!==204){if(typeof e.text==`function`){let t=await e.text();return t.trim()?JSON.parse(t):void 0}return await e.json()}}function Nt(e,t){return typeof e.error==`string`&&e.error?e.error:typeof e.message==`string`&&e.message?e.message:t}async function Pt(e,t=`HTTP ${e.status}`){if(!e.ok){let n=t;try{n=Nt(await e.json(),t)}catch{}throw Error(n)}return Mt(e)}async function Ft(e){if(!e.ok)return null;try{return await Mt(e)}catch{return null}}var It=[`gpt-5.6-luna`];function Lt(e){let t=Array.isArray(e)?e.filter(e=>typeof e==`string`&&e.trim()!==``).map(e=>e.trim()):[];return t.length>0?t:It}function Rt(e){return Lt(e).join(`, `)}function zt(e){return Lt(e).map(e=>e.replace(/^gpt-/,``)).join(`, `)}var Bt=`dashboard/update`;function Vt(){let e=window.location.hash.replace(/^#\/?/,``);return e===`dashboard/providers`?`providers`:e===`dashboard/models`?`models`:`overview`}function Ht(){return window.location.hash.replace(/^#\/?/,``)===Bt}function Ut(e){return e===`overview`?`dashboard`:`dashboard/${e}`}async function Wt(e,t){let n=await Pt(e,t);if(n===void 0)throw Error(t??`empty response`);return n}var Gt=[`low`,`medium`,`high`,`xhigh`];function Kt(e){return e?.includes(`-preview.`)?`preview`:`latest`}function qt(e,t){switch(e){case`source_checkout`:return t(`dash.updateReason.source_checkout`);case`latest_unavailable`:return t(`dash.updateReason.latest_unavailable`);case`already_latest`:return t(`dash.updateReason.already_latest`);default:return t(`dash.updateReason.unknown`)}}function Jt(e,t){switch(e){case`running`:return t(`dash.updateStatus.running`);case`restarting`:return t(`dash.updateStatus.restarting`);case`succeeded`:return t(`dash.updateStatus.succeeded`);case`failed`:return t(`dash.updateStatus.failed`)}}function Yt(e,t){let n={...e};return t?.model!==void 0&&(n.model=t.model),t?.backend===null?delete n.backend:t?.backend!==void 0&&(n.backend=t.backend),t?.reasoning!==void 0&&(n.reasoning=t.reasoning),t?.streamRoutedModelOutput!==void 0&&(n.streamRoutedModelOutput=t.streamRoutedModelOutput),t?.enabled!==void 0&&(n.enabled=t.enabled),t?.maxDescriptionsPerTurn!==void 0&&(n.maxDescriptionsPerTurn=t.maxDescriptionsPerTurn),t?.timeoutMs!==void 0&&(n.timeoutMs=t.timeoutMs),n}function Xt(e){return{vision:{reasoning:e}}}function Zt(e){return{vision:{enabled:e}}}function Qt(e){return{vision:{maxDescriptionsPerTurn:e}}}function $t(e){return{vision:{timeoutMs:e}}}var en=At,tn=jt,nn=1;function rn(e){let t=e.trim();if(!/^[0-9]+$/.test(t))return;let n=Number(t);if(!(!Number.isSafeInteger(n)||n<=0))return n}function an(e){let t=rn(e);if(!(t===void 0||ttn))return t}var on=[`low`,`medium`,`high`,`xhigh`,`max`];function sn(e,t){let n=e.find(e=>e.id===t)?.reasoningEfforts;if(!n||n.length===0)return[...on];let r=on.filter(e=>n.includes(e));return r.length>0?r:[...on]}function cn(e,t){let n=ln(e,t);return e.includes(n)?e:[n,...e]}function ln(e,t){if(e.length===0||e.includes(t))return t;let n=on.indexOf(t),r=e[0],i=on.indexOf(r);for(let t of e){let e=on.indexOf(t);e<=n&&e>=i&&(r=t,i=e)}return r}function un(e){let t=[];for(let n of e)(n.provider===`openai`||n.provider===`anthropic`)&&t.push({value:n.id,label:`${n.provider}/${n.id}`});return t}function dn(e,t,n,r){if(e===void 0){let e=un(t);return n&&!e.some(e=>e.value===n)&&e.unshift({value:n,label:n,...r?{backend:r}:{},model:n}),e}let i=e.map(e=>({value:e.value,label:e.label,backend:e.backend,model:e.model}));return n&&!i.some(e=>e.value===n)&&i.unshift({value:n,label:n,...r?{backend:r}:{},model:n}),i}function fn(e,t,n,r){let i=e?e.map(e=>({value:e.value,label:e.label,backend:e.backend})):un(t);return n&&!i.some(e=>e.value===n)&&i.unshift({value:n,label:n,...r?{backend:r}:{}}),i}function pn(e,t,n){let r=Lt(n),i=r.flatMap(t=>{let n=e.find(e=>e.namespaced.startsWith(t))??e.find(e=>e.id.startsWith(t));return n?[{provider:n.provider,modelId:t}]:[]}),a=e.filter(e=>i.some(t=>e.provider===t.provider&&e.id.startsWith(t.modelId))),o=new Set([...r.flatMap(e=>[e,`openai/${e}`]),...a.flatMap(e=>[e.namespaced,`${e.provider}/${e.id}`])]),s=[{value:``,label:`—`},...e.filter(e=>!o.has(e.namespaced)).map(e=>({value:e.namespaced,label:e.namespaced}))];return t&&!o.has(t)&&!s.some(e=>e.value===t)&&s.push({value:t,label:t}),s}function mn(e,t){return e.find(e=>e.id===t)?.provider===`anthropic`?`anthropic`:`openai`}function hn(e,t,n){let r=t.find(e=>e.value===n);return{backend:r?.backend??mn(e,n),model:r?.model??n}}function gn(e,t,n){return t.find(e=>e.value===n)?.backend||(n.includes(`/`)?`routed`:mn(e,n))}var _n=!1;typeof window<`u`&&typeof window.addEventListener==`function`&&(window.addEventListener(`keydown`,()=>{_n=!0},{capture:!0,passive:!0}),window.addEventListener(`pointerdown`,()=>{_n=!1},{capture:!0,passive:!0}));function vn(e){if(e){if(_n){e.focus({preventScroll:!0});return}try{e.focus({preventScroll:!0,focusVisible:!1})}catch{e.focus({preventScroll:!0})}}}function yn(e,t){let n=(0,_.useRef)(null);return(0,_.useEffect)(()=>{let r=n.current;if(r){if(e){r.open||r.showModal();return}r.open&&r.close(),vn(t.current)}},[e,t]),(0,_.useEffect)(()=>()=>{let e=n.current;e?.open&&e.close(),vn(t.current)},[t]),n}function bn(e){let{t,updateOpen:n,closeUpdateDialog:r,updateDialogRef:i,updateChannel:a,changeUpdateChannel:o,updateLoading:s,updateError:c,updateCheck:l,fetchUpdateCheck:u,updateRestart:d,setUpdateRestart:f,runUpdate:p,maHelpOpen:m,setMaHelpOpen:h,maHelpDialogRef:g,effortCapHelpOpen:_,setEffortCapHelpOpen:v,effortCapHelpDialogRef:y,shadowCallHelpOpen:b,setShadowCallHelpOpen:x,shadowCallHelpDialogRef:S,shadowCall:C}=e;return(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)(`dialog`,{ref:i,id:`dashboard-update-dialog`,className:`modal-overlay`,style:{display:n?`flex`:`none`,border:`none`,margin:0,maxWidth:`none`,maxHeight:`none`,width:`100%`,height:`100%`},"aria-labelledby":`update-title`,onCancel:e=>{e.preventDefault(),r()},children:(0,J.jsxs)(`div`,{className:`modal-card`,children:[(0,J.jsxs)(`div`,{className:`modal-head`,children:[(0,J.jsx)(`h3`,{id:`update-title`,children:t(`dash.updateTitle`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-icon`,onClick:r,"aria-label":t(`common.cancel`),children:(0,J.jsx)(de,{})})]}),(0,J.jsx)(`div`,{className:`modal-desc`,children:t(`dash.updateDesc`)}),(0,J.jsxs)(`div`,{className:`update-row`,children:[(0,J.jsx)(`label`,{className:`field-label`,htmlFor:`update-channel`,children:t(`dash.updateChannel`)}),(0,J.jsx)(Dt,{value:a,options:[{value:`latest`,label:`latest`},{value:`preview`,label:`preview`}],onChange:e=>o(e),disabled:s,label:t(`dash.updateChannel`),portal:!1})]}),s&&(0,J.jsx)(Ot,{className:`update-empty`,icon:(0,J.jsx)(`span`,{className:`spin`}),title:t(`dash.updateChecking`)}),c&&(0,J.jsxs)(`div`,{className:`notice notice-err`,role:`status`,children:[(0,J.jsx)(_e,{}),(0,J.jsx)(`span`,{children:c})]}),l&&!s&&(0,J.jsxs)(`div`,{className:`update-box`,children:[(0,J.jsxs)(`div`,{className:`spread`,children:[(0,J.jsxs)(`div`,{children:[(0,J.jsx)(`div`,{className:`muted text-label`,children:t(`dash.updateInstalled`)}),(0,J.jsx)(`div`,{className:`mono`,children:l.currentVersion})]}),(0,J.jsxs)(`div`,{children:[(0,J.jsx)(`div`,{className:`muted text-label`,children:t(`dash.updateLatest`)}),(0,J.jsx)(`div`,{className:`mono`,children:l.latestVersion??`—`})]}),(0,J.jsx)(`span`,{className:`badge ${l.updateAvailable?`badge-green`:`badge-muted`}`,children:l.updateAvailable?t(`dash.updateAvailable`):t(`dash.updateCurrent`)})]}),(0,J.jsxs)(`div`,{className:`muted update-command`,children:[t(`dash.updateCommand`),` `,(0,J.jsx)(`code`,{className:`chip`,children:l.command})]}),l.reason===`source_checkout`&&(0,J.jsxs)(`div`,{className:`notice-warn`,role:`status`,children:[(0,J.jsx)(_e,{}),` `,t(`dash.updateSource`)]}),l.reason===`latest_unavailable`&&(0,J.jsxs)(`div`,{className:`notice-warn`,role:`status`,children:[(0,J.jsx)(_e,{}),` `,t(`dash.updateUnavailable`),(0,J.jsxs)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,disabled:s,onClick:()=>{u(a,!0)},style:{marginLeft:12},children:[(0,J.jsx)(pe,{}),` `,t(`dash.updateRetry`)]})]}),!l.canUpdate&&l.reason!==`latest_unavailable`&&l.reason!==`source_checkout`&&(0,J.jsxs)(`div`,{className:`update-recheck`,children:[(0,J.jsx)(`span`,{className:`muted update-recheck-reason`,children:t(`dash.updateCannotAuto`,{reason:qt(l.reason,t)})}),(0,J.jsxs)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,disabled:s,onClick:()=>{u(a,!0)},children:[(0,J.jsx)(pe,{}),` `,t(s?`dash.updateChecking`:`dash.updateRecheck`)]})]}),l.canUpdate&&(0,J.jsxs)(`div`,{className:`spread update-restart`,children:[(0,J.jsxs)(`div`,{children:[(0,J.jsx)(`div`,{className:`font-semibold`,children:t(`dash.updateRestart`)}),(0,J.jsx)(`div`,{className:`muted text-label`,children:t(`dash.updateRestartHint`)})]}),(0,J.jsx)(`button`,{type:`button`,className:`switch ${d?`on`:``}`,onClick:()=>f(e=>!e),"aria-label":t(`dash.updateRestart`),"aria-pressed":d,children:(0,J.jsx)(`span`,{className:`knob`})})]})]}),(0,J.jsxs)(`div`,{className:`modal-actions`,children:[(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost`,onClick:r,children:t(`common.cancel`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-primary`,onClick:p,disabled:!l?.canUpdate||s,children:t(`dash.runUpdate`)})]})]})}),(0,J.jsxs)(`dialog`,{ref:g,id:`multi-agent-help-dialog`,className:`modal-overlay`,style:{display:m?`flex`:`none`,border:`none`,margin:0,maxWidth:`none`,maxHeight:`none`,width:`100%`,height:`100%`},"aria-labelledby":`multi-agent-help-title`,onCancel:e=>{e.preventDefault(),h(!1)},children:[(0,J.jsx)(`button`,{type:`button`,className:`modal-backdrop-dismiss`,"aria-label":t(`common.close`),tabIndex:-1,onClick:()=>h(!1)}),(0,J.jsxs)(`div`,{className:`modal-card`,onClick:e=>e.stopPropagation(),children:[(0,J.jsxs)(`div`,{className:`modal-head`,children:[(0,J.jsx)(`h3`,{id:`multi-agent-help-title`,children:t(`dash.multiAgent`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-icon`,onClick:()=>h(!1),"aria-label":t(`common.close`),children:(0,J.jsx)(de,{})})]}),(0,J.jsx)(`div`,{className:`modal-desc leading-relaxed`,style:{whiteSpace:`pre-line`},children:t(`models.v2Help`)}),(0,J.jsx)(`div`,{style:{marginTop:12},children:(0,J.jsx)(`a`,{className:`text-control`,href:`https://opencodex.me/guides/sub-agent-surface/`,target:`_blank`,rel:`noreferrer`,style:{color:`var(--accent)`},children:t(`models.v2DocsLink`)})}),(0,J.jsx)(`div`,{className:`modal-actions`,children:(0,J.jsx)(`button`,{type:`button`,className:`btn btn-primary`,onClick:()=>h(!1),children:t(`common.ok`)})})]})]}),(0,J.jsxs)(`dialog`,{ref:y,id:`effort-cap-help-dialog`,className:`modal-overlay`,style:{display:_?`flex`:`none`,border:`none`,margin:0,maxWidth:`none`,maxHeight:`none`,width:`100%`,height:`100%`},"aria-labelledby":`effort-cap-help-title`,onCancel:e=>{e.preventDefault(),v(!1)},children:[(0,J.jsx)(`button`,{type:`button`,className:`modal-backdrop-dismiss`,"aria-label":t(`common.close`),tabIndex:-1,onClick:()=>v(!1)}),(0,J.jsxs)(`div`,{className:`modal-card`,onClick:e=>e.stopPropagation(),children:[(0,J.jsxs)(`div`,{className:`modal-head`,children:[(0,J.jsx)(`h3`,{id:`effort-cap-help-title`,children:t(`dash.effortCapLabel`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-icon`,onClick:()=>v(!1),"aria-label":t(`common.close`),children:(0,J.jsx)(de,{})})]}),(0,J.jsx)(`div`,{className:`modal-desc leading-relaxed`,style:{whiteSpace:`pre-line`},children:t(`dash.effortCapHelp`)}),(0,J.jsx)(`div`,{className:`modal-actions`,children:(0,J.jsx)(`button`,{type:`button`,className:`btn btn-primary`,onClick:()=>v(!1),children:t(`common.ok`)})})]})]}),(0,J.jsxs)(`dialog`,{ref:S,id:`shadow-call-help-dialog`,className:`modal-overlay`,style:{display:b?`flex`:`none`,border:`none`,margin:0,maxWidth:`none`,maxHeight:`none`,width:`100%`,height:`100%`},"aria-labelledby":`shadow-call-help-title`,onCancel:e=>{e.preventDefault(),x(!1)},children:[(0,J.jsx)(`button`,{type:`button`,className:`modal-backdrop-dismiss`,"aria-label":t(`common.close`),tabIndex:-1,onClick:()=>x(!1)}),(0,J.jsxs)(`div`,{className:`modal-card`,onClick:e=>e.stopPropagation(),children:[(0,J.jsxs)(`div`,{className:`modal-head`,children:[(0,J.jsx)(`h3`,{id:`shadow-call-help-title`,children:t(`dash.shadowCallIntercept`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-icon`,onClick:()=>x(!1),"aria-label":t(`common.close`),children:(0,J.jsx)(de,{})})]}),(0,J.jsx)(`div`,{className:`modal-desc leading-relaxed`,style:{whiteSpace:`pre-line`},children:t(`dash.shadowCallTooltip`,{models:Rt(C?.sourceModels)})}),(0,J.jsx)(`div`,{className:`modal-actions`,children:(0,J.jsx)(`button`,{type:`button`,className:`btn btn-primary`,onClick:()=>x(!1),children:t(`common.ok`)})})]})]})]})}var xn={anthropic:`claude-color.svg`,"anthropic-apikey":`claude-color.svg`,"azure-openai":`openai.svg`,chatgpt:`openai.svg`,"cloudflare-ai-gateway":`cloudflare-ai-gateway-color.svg`,"cloudflare-workers-ai":`cloudflare-ai-gateway-color.svg`,cline:`cline-color.svg`,"cline-pass":`cline-color.svg`,"command-code":`commandcode-color.svg`,commandcode:`commandcode-color.svg`,cursor:`cursor-color.svg`,deepseek:`deepseek-color.svg`,firepass:`firepass-color.svg`,fireworks:`fireworks-color.svg`,github:`github-copilot-color.svg`,"github-copilot":`copilot-color.svg`,"gitlab-duo":`gitlab-duo-color.svg`,google:`gemini-color.svg`,"google-antigravity":`antigravity-color.svg`,"google-vertex":`gemini-color.svg`,groq:`groq-color.svg`,huggingface:`huggingface-color.svg`,kimi:`kimi-color.svg`,"kimi-code":`kimi-color.svg`,kiro:`kiro-color.svg`,"lm-studio":`lm-studio-color.svg`,"meta-model":`meta.svg`,"meta-muse":`meta.svg`,mistral:`mistral-color.svg`,minimax:`minimax.svg`,"minimax-cn":`minimax.svg`,moonshot:`moonshot-color.svg`,nvidia:`nvidia-color.svg`,ollama:`ollama-color.svg`,"ollama-cloud":`ollama-color.svg`,openai:`openai.svg`,"openai-apikey":`openai.svg`,"opencode-free":`opencode.svg`,"opencode-go":`opencode.svg`,"opencode-zen":`opencode.svg`,openrouter:`openrouter-color.svg`,qianfan:`qianfan-color.svg`,alibaba:`alibaba-color.svg`,"alibaba-token-plan":`alibaba-color.svg`,"alibaba-token-plan-intl":`alibaba-color.svg`,baseten:`baseten.svg`,bizrouter:`bizrouter.svg`,cerebras:`cerebras.svg`,deepinfra:`deepinfra.svg`,digitalocean:`digitalocean.svg`,featherless:`featherless.svg`,hyperbolic:`hyperbolic.svg`,kilo:`kilo.svg`,nanogpt:`nanogpt.svg`,nebius:`nebius.svg`,neuralwatt:`neuralwatt.svg`,nous:`nous.svg`,novita:`novita.svg`,orcarouter:`orcarouter.svg`,parallel:`parallel.svg`,sambanova:`sambanova.svg`,scaleway:`scaleway.svg`,siliconflow:`siliconflow.svg`,synthetic:`synthetic.svg`,together:`together.svg`,umans:`umans.svg`,venice:`venice.svg`,vultr:`vultr.svg`,litellm:`litellm.svg`,zenmux:`zenmux.svg`,zai:`zai.svg`,"zhipu-bigmodel":`zai.svg`,"zhipu-bigmodel-coding":`zai.svg`,"qwen-cloud":`qwen-portal-color.svg`,"vercel-ai-gateway":`vercel-ai-gateway-color.svg`,vllm:`vllm-color.svg`,xai:`grok.svg`,"mimo-free":`xiaomi-color.svg`,mimo:`xiaomi-color.svg`,xiaomi:`xiaomi-color.svg`,"xiaomi-mimo":`xiaomi-color.svg`},Sn={anthropic:`Anthropic Claude`,"anthropic-apikey":`Anthropic Claude`,chatgpt:`ChatGPT`,openai:`OpenAI (Codex login)`,"openai-apikey":`OpenAI API`,"azure-openai":`Azure OpenAI`,"cloudflare-ai-gateway":`Cloudflare AI Gateway`,"cloudflare-workers-ai":`Cloudflare Workers AI`,cline:`Cline`,"cline-pass":`ClinePass`,nvidia:`NVIDIA NIM`,ollama:`Ollama`,"ollama-cloud":`Ollama Cloud`,xai:`xAI Grok`,"mimo-free":`MiMo Free`,xiaomi:`Xiaomi`,cursor:`Cursor`,deepseek:`DeepSeek`,github:`GitHub`,"github-copilot":`GitHub Copilot`,"gitlab-duo":`GitLab Duo`,openrouter:`OpenRouter`,"opencode-go":`OpenCode Go`,"opencode-free":`OpenCode Free`,"opencode-zen":`OpenCode Zen`,mistral:`Mistral`,groq:`Groq`,"meta-model":`Meta Model API`,"meta-muse":`Muse Code`,alibaba:`Alibaba Coding Plan`,"alibaba-token-plan":`Alibaba Token Plan`,"alibaba-token-plan-intl":`Alibaba Token Plan (Intl)`,kimi:`Kimi`,"kimi-code":`Kimi`,moonshot:`Moonshot`,google:`Google`,"google-vertex":`Google Vertex`,"lm-studio":`LM Studio`,huggingface:`Hugging Face`,"qwen-cloud":`Qwen Cloud`,siliconflow:`SiliconFlow`,"tencent-coding-plan":`Tencent Cloud Coding Plan`,"vercel-ai-gateway":`Vercel AI Gateway`,vllm:`vLLM`,litellm:`LiteLLM`},Cn={"command-code":`provider.name.commandCodeAuth`,commandcode:`provider.name.commandCodeApi`,volcengine:`provider.name.volcengine`,"volcengine-coding-plan":`provider.name.volcengineCodingPlan`,"volcengine-agent-plan":`provider.name.volcengineAgentPlan`},wn=new Set([...Object.keys(Sn),...Object.keys(Cn)]);function Tn(e){let t=e.toLowerCase();return Object.hasOwn(xn,t)?xn[t]:void 0}function En(e,t){let n=Tn(e);return n?`/provider-icons/${n}`:void 0}var Dn=new Set([`cerebras.svg`,`deepinfra.svg`,`neuralwatt.svg`,`nous.svg`,`novita.svg`,`siliconflow.svg`,`synthetic.svg`,`zenmux.svg`,`grok.svg`,`kimi-color.svg`,`ollama-color.svg`,`opencode.svg`,`vercel-ai-gateway-color.svg`]),On=new Set([`baseten.svg`,`kilo.svg`,`sambanova.svg`,`venice.svg`,`zai.svg`]),kn=new Set([`bizrouter.svg`,`featherless.svg`,`hyperbolic.svg`,`nebius.svg`,`parallel.svg`,`umans.svg`]);function An(e){if(!e)return`image`;let t=e.split(`/`).pop()??``;return Dn.has(t)?`mask`:On.has(t)?`plate`:kn.has(t)?`dark-plate`:`image`}function jn(e,t){let n=e.toLowerCase(),r=Object.hasOwn(Cn,n)?Cn[n]:void 0;return r?t(r):(Object.hasOwn(Sn,n)?Sn[n]:void 0)||(e===n&&/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(e)?e.split(`-`).map(e=>e&&e[0].toUpperCase()+e.slice(1)).join(` `):e)}function Mn(e){return wn.has(e.toLowerCase())}function Nn(e){return e===`command-code`?`commandcode-auth`:e===`commandcode`?`commandcode-api`:e}function Pn(e,t){let n=e.indexOf(`/`);if(n<=0)return e;let r=e.slice(0,n),i=e.slice(n+1);if(r===`command-code`||r===`commandcode`){let e=i.match(/^([a-z0-9]+)-([a-z0-9]+(?:-[a-z0-9]+)+)$/i);return e&&i.startsWith(`${e[1]}-${e[1]}-`)&&(i=i.slice(e[1].length+1)),`${r===`command-code`?`commandcode-auth`:`commandcode-api`}/${i}`}return e}function Fn({t:e,models:t,modelsLoading:n,modelQuery:r,setModelQuery:i,filteredGroups:a,expandedProviders:o,setExpandedProviders:s}){return(0,J.jsxs)(J.Fragment,{children:[(0,J.jsxs)(`div`,{className:`h-section`,children:[e(`dash.availableModels`),` `,(0,J.jsx)(`span`,{className:`count`,children:t.length}),n&&(0,J.jsx)(`span`,{className:`spin`,style:{marginLeft:4}})]}),t.length===0&&!n?(0,J.jsx)(Ot,{title:e(`dash.noModels`)}):(0,J.jsxs)(J.Fragment,{children:[(0,J.jsxs)(`div`,{className:`pws-search-wrap`,children:[(0,J.jsx)(ve,{className:`pws-search-icon`,width:14,height:14,"aria-hidden":`true`}),(0,J.jsx)(`input`,{type:`search`,className:`input pws-search-input`,placeholder:e(`models.search`),value:r,onChange:e=>i(e.target.value),"aria-label":e(`models.search`)})]}),a.length===0?(0,J.jsx)(`p`,{className:`muted text-control`,style:{margin:`4px 0`},children:e(`dash.modelsNoResults`)}):(0,J.jsx)(`div`,{className:`dash-model-acc`,children:a.map(([t,n])=>{let i=r.trim().toLowerCase()!==``||o.has(t);return(0,J.jsxs)(`div`,{className:`dash-model-group`,children:[(0,J.jsxs)(`button`,{type:`button`,className:`dash-model-head`,onClick:()=>s(e=>{let n=new Set(e);return n.has(t)?n.delete(t):n.add(t),n}),"aria-expanded":i,children:[(0,J.jsx)(Se,{width:12,height:12,style:{transform:i?`rotate(90deg)`:`none`,transition:`transform .12s`,color:`var(--muted)`},"aria-hidden":`true`}),(0,J.jsx)(`span`,{className:`font-semibold`,children:jn(t,e)}),(0,J.jsx)(`span`,{className:`count`,children:n.length})]}),i&&(0,J.jsx)(`div`,{className:`dash-model-chips`,children:n.map(e=>(0,J.jsx)(`code`,{className:`dash-model-chip`,children:e.id},`${e.provider}/${e.id}`))})]},t)})})]})]})}var In={ko:[{v:0x2386f26fc10000,s:`경`},{v:0xe8d4a51000,s:`조`},{v:1e8,s:`억`},{v:1e4,s:`만`}],zh:[{v:0x2386f26fc10000,s:`京`},{v:0xe8d4a51000,s:`兆`},{v:1e8,s:`亿`},{v:1e4,s:`万`}],"zh-TW":[{v:0x2386f26fc10000,s:`京`},{v:0xe8d4a51000,s:`兆`},{v:1e8,s:`億`},{v:1e4,s:`萬`}]};function Ln(e){return e.replace(/\.0+$/,``).replace(/(\.\d*?)0+$/,`$1`)}function Rn(e,t){let n=In[t];if(n){for(let t of n)if(e>=t.v)return`${Ln((e/t.v).toFixed(1))}${t.s}`;return String(e)}return e<1e4?String(e):e<1e6?`${Ln((e/1e3).toFixed(1))}K`:e<1e9?`${Ln((e/1e6).toFixed(1))}M`:e<0xe8d4a51000?`${Ln((e/1e9).toFixed(1))}B`:`${Ln((e/0xe8d4a51000).toFixed(1))}T`}function zn(e,t){let n=Math.max(0,Math.floor(e)),r=$e(t,`uptime.day`),i=$e(t,`uptime.hour`),a=$e(t,`uptime.minute`),o=$e(t,`uptime.second`);if(n<300)return`${n}${o}`;let s=Math.floor(n/60);if(s<60)return`${s}${a}`;let c=Math.floor(s/60);if(c<24){let e=s%60;return e>0?`${c}${i} ${e}${a}`:`${c}${i}`}let l=Math.floor(c/24),u=c%24;return u>0?`${l}${r} ${u}${i}`:`${l}${r}`}function Bn({locale:e,health:t,providers:n,usage30d:r,usageLoading:i,healthLoading:a,startupHealth:o,projectConfigWarnings:s,maMode:c,maBusy:l,maHelpTriggerRef:u,maHelpOpen:d,setMaHelpOpen:f,switchMaMode:p,maError:m}){let h=Q(),g=t?.status===`ok`;return(0,J.jsxs)(J.Fragment,{children:[(0,J.jsxs)(`div`,{className:`dash-overview-head`,children:[(0,J.jsxs)(`div`,{className:`stat-row`,children:[(0,J.jsxs)(`div`,{className:`stat`,children:[(0,J.jsxs)(`div`,{className:`label`,style:{display:`flex`,alignItems:`center`,gap:6},children:[h(`dash.multiAgent`),(0,J.jsx)(`button`,{ref:u,type:`button`,className:`btn btn-ghost btn-sm`,style:{width:24,height:24,minWidth:24,flex:`0 0 24px`,padding:0,borderRadius:`var(--radius-pill)`,color:`var(--muted)`},onClick:()=>f(!0),"aria-label":h(`dash.multiAgent`),"aria-haspopup":`dialog`,"aria-controls":`multi-agent-help-dialog`,"aria-expanded":d,children:(0,J.jsx)(Z,{width:14,height:14,"aria-hidden":`true`})})]}),(0,J.jsx)(`div`,{className:`value`,style:{display:`flex`,alignItems:`center`,justifyContent:`center`},children:(0,J.jsx)(`div`,{role:`radiogroup`,"aria-label":h(`dash.multiAgent`),style:{display:`inline-flex`,borderRadius:`var(--radius-pill)`,background:`var(--surface-soft, var(--raised))`,padding:3,gap:2},children:[`v1`,`default`,`v2`].map(e=>(0,J.jsx)(`button`,{type:`button`,role:`radio`,"aria-checked":c===e,className:`btn btn-sm text-caption${c===e?` btn-primary`:` btn-ghost`}`,style:{borderRadius:`var(--radius-pill)`,minWidth:36,padding:`5px 10px`,border:`none`,background:c===e?void 0:`transparent`,color:c===e?void 0:`var(--muted)`},disabled:l,onClick:()=>void p(e),children:h(`models.v2Mode_${e}`)},e))})}),m&&(0,J.jsx)(`div`,{role:`alert`,className:`text-caption`,style:{color:`var(--red)`,marginTop:4,textAlign:`center`,maxWidth:280,wordBreak:`break-word`},children:m})]}),(0,J.jsxs)(`div`,{className:`stat`,"aria-busy":a||void 0,children:[(0,J.jsx)(`div`,{className:`label`,children:h(`dash.status`)}),(0,J.jsxs)(`div`,{className:`value`,style:{display:`flex`,alignItems:`center`,gap:9,color:g?`var(--green)`:`var(--red)`},children:[(0,J.jsx)(`span`,{className:`dot ${g?`dot-green`:`dot-red`}`}),h(g?`dash.online`:`dash.offline`)]})]}),(0,J.jsxs)(`div`,{className:`stat`,"aria-busy":a||void 0,children:[(0,J.jsx)(`div`,{className:`label`,children:h(`dash.version`)}),(0,J.jsx)(`div`,{className:`value mono`,children:t?.version??`—`})]}),(0,J.jsxs)(`div`,{className:`stat`,"aria-busy":a||void 0,children:[(0,J.jsx)(`div`,{className:`label`,children:h(`dash.uptime`)}),(0,J.jsx)(`div`,{className:`value mono`,children:t?zn(t.uptime,e):`—`})]}),(0,J.jsxs)(`div`,{className:`stat`,"aria-busy":a||void 0,children:[(0,J.jsx)(`div`,{className:`label`,children:h(`dash.providers`)}),(0,J.jsx)(`div`,{className:`value`,children:n.length})]}),(0,J.jsxs)(`div`,{className:`stat`,"aria-busy":i||void 0,children:[(0,J.jsx)(`div`,{className:`label`,children:h(`dash.tokens30d`)}),(0,J.jsx)(`div`,{className:`value mono`,children:r&&r.summary.requests>0?Rn(r.summary.totalTokens,e):`—`}),(0,J.jsx)(`div`,{className:`muted text-label dash-stat-coverage`,children:r&&r.summary.requests>0?h(`dash.coverage`).replace(`{pct}`,`${Math.round(r.summary.coverageRatio*100)}%`):`\xA0`})]})]}),(0,J.jsx)(`div`,{className:`startup-health-slot`,"aria-live":`polite`,children:o?(0,J.jsxs)(`button`,{type:`button`,className:`startup-health-bar`,onClick:()=>pt(`startup`),children:[(0,J.jsx)(`span`,{className:`dot ${o===`error`?`dot-red`:o===`at-risk`?`dot-amber`:`dot-green`}`,"aria-hidden":`true`}),(0,J.jsx)(`span`,{className:`startup-health-bar__summary`,children:h(o===`error`?`startup.error`:o===`at-risk`?`startup.summary.atRisk`:o===`protected`?`startup.summary.protected`:`startup.summary.native`)})]}):(0,J.jsxs)(`div`,{className:`startup-health-bar startup-health-bar--pending`,"aria-hidden":`true`,children:[(0,J.jsx)(`span`,{className:`dot dot-amber`}),(0,J.jsx)(`span`,{className:`startup-health-bar__summary`,children:`\xA0`})]})})]}),s.length>0&&(0,J.jsxs)(`div`,{className:`notice notice-err maintenance-notice`,role:`alert`,children:[(0,J.jsx)(_e,{}),(0,J.jsxs)(`div`,{children:[(0,J.jsx)(`div`,{className:`font-semibold`,children:h(`dash.projectConfigTitle`)}),(0,J.jsx)(`div`,{className:`muted text-control`,style:{marginTop:4},children:h(`dash.projectConfigHint`)}),(0,J.jsx)(`ul`,{className:`text-control`,style:{margin:`10px 0 0`,paddingLeft:18},children:s.map(e=>(0,J.jsxs)(`li`,{style:{marginBottom:8},children:[(0,J.jsx)(`code`,{children:e.path}),` — `,e.issues.join(`, `),(0,J.jsx)(`div`,{className:`muted`,style:{marginTop:2},children:e.bypass})]},e.path))})]})]})]})}function Vn(e){let t=new AbortController;if(typeof AbortSignal<`u`&&typeof AbortSignal.any==`function`&&typeof AbortSignal.timeout==`function`)return{controller:t,signal:AbortSignal.any([t.signal,AbortSignal.timeout(e)]),clear:()=>void 0};let n=setTimeout(()=>t.abort(),e);return{controller:t,signal:t.signal,clear:()=>clearTimeout(n)}}function Hn(){return typeof document<`u`&&document.visibilityState===`hidden`}function Un(e,t){return typeof window<`u`&&typeof window.setInterval==`function`?window.setInterval(e,t):setInterval(e,t)}function Wn(e){if(typeof window<`u`&&typeof window.clearInterval==`function`){window.clearInterval(e);return}clearInterval(e)}function Gn(e,t,n){let r=n?.pauseWhenHidden!==!1,i=null,a=!1,o=()=>{try{e()}catch(e){console.error(`[visibility-poll]`,e)}},s=()=>{i!==null||a||(i=Un(o,t))},c=()=>{i!==null&&(Wn(i),i=null)},l=()=>{if(r){if(Hn()){c();return}o(),s()}};return r&&Hn()||s(),r&&typeof document<`u`&&document.addEventListener(`visibilitychange`,l),n?.immediate&&o(),()=>{a=!0,c(),r&&typeof document<`u`&&document.removeEventListener(`visibilitychange`,l)}}var Kn=new Map;function qn(e,t){let n=`${e}:${t}`,r=Kn.get(n);return r||(r=new Intl.NumberFormat(e,{minimumFractionDigits:t,maximumFractionDigits:t}),Kn.set(n,r)),r}var Jn=new Map;function Yn(e){let t=Jn.get(e);return t||(t=new Intl.NumberFormat(e),Jn.set(e,t)),t}function Xn(e,t){if(!Number.isFinite(e)||e<=0)return`0 B`;let n=[`B`,`KiB`,`MiB`,`GiB`,`TiB`],r=Math.min(Math.floor(Math.log(e)/Math.log(1024)),n.length-1),i=e/1024**r;return`${qn(t,r===0?0:1).format(i)} ${n[r]}`}function Zn(e,t){return!Number.isFinite(e)||e<0?`—`:zn(e/1e3,t)}function Qn(e){return typeof e.observedBytes==`number`?e.observedBytes:Math.max(e.rss,e.external??0,e.arrayBuffers??0)}function $n(e){if(e.observedMetric)return e.observedMetric;if(e.watchdog?.observedMetric)return e.watchdog.observedMetric;let t=[{metric:`rss`,bytes:e.rss},{metric:`external`,bytes:e.external??0},{metric:`arrayBuffers`,bytes:e.arrayBuffers??0}];return t.reduce((e,t)=>t.bytes>e.bytes?t:e,t[0]).metric}function er(e){if(e.length<2)return null;let t=e[0],n=e[e.length-1],r=n.at-t.at;return r<=0?null:(Qn(n)-Qn(t))/r*36e5}function tr({label:e,value:t,sub:n,tone:r}){return(0,J.jsxs)(`div`,{className:`stat`,children:[(0,J.jsx)(`div`,{className:`label`,children:e}),(0,J.jsx)(`div`,{className:`value mono${r?` value--${r}`:``}`,children:t}),n&&(0,J.jsx)(`div`,{className:`stat-sub mono`,children:n})]})}function nr({observedBytes:e,thresholdBytes:t,metric:n,locale:r,t:i}){let a=e!==null&&t!==null&&t>0?e/t:null,o=a===null?`unknown`:a>=1?`over`:a>=.75?`warn`:`ok`,s=a===null?null:Math.round(a*100);return(0,J.jsxs)(`div`,{className:`mem-pressure mem-pressure--${o}`,children:[(0,J.jsxs)(`div`,{className:`mem-pressure-head`,children:[(0,J.jsxs)(`span`,{className:`mem-pressure-label`,children:[i(`dash.mem.pressure`),n?(0,J.jsx)(`span`,{className:`mem-pressure-metric mono`,children:n}):null]}),(0,J.jsxs)(`span`,{className:`mem-pressure-figure mono`,children:[e===null?`—`:Xn(e,r),t!==null&&(0,J.jsxs)(`span`,{className:`mem-pressure-limit`,children:[` / `,Xn(t,r)]})]})]}),(0,J.jsx)(`div`,{className:`mem-pressure-track`,role:`presentation`,children:(0,J.jsx)(`span`,{className:`mem-pressure-fill`,style:{"--mem-scale":String(a===null?0:Math.min(1,Math.max(.01,a)))}})}),(0,J.jsx)(`div`,{className:`mem-pressure-foot`,children:s===null?i(`dash.mem.pressureUnknown`):i(`dash.mem.pressureOf`,{pct:s})})]})}var rr=60,ir=1500,ar=12e4;function or({apiBase:e}){let{locale:t,t:n}=ct(),[r,i]=(0,_.useState)(null),[a,o]=(0,_.useState)(!1),[s,c]=(0,_.useState)(`idle`),[l,u]=(0,_.useState)(null),[d,f]=(0,_.useState)(!1),[p,m]=(0,_.useState)(!1),[h,g]=(0,_.useState)(null);(0,_.useEffect)(()=>{let t=!1;return(async()=>{try{let n=await fetch(`${e}/api/startup-health`);if(!n.ok||t)return;let r=await n.json();t||f(r.protection===`none`)}catch{}})(),()=>{t=!0}},[e]),(0,_.useEffect)(()=>{let t=!1,n=!1,r=null,a=async()=>{if(n)return;n=!0;let a=Vn(1e4);r=a;try{let n=await fetch(`${e}/api/system/memory`,{signal:a.signal});if(!n.ok)throw Error(`memory unavailable`);let r=await n.json();if(t)return;i(r),o(!1),m(typeof r.activeTurnCount==`number`),r.isDraining&&s===`idle`&&c(`draining`),(s===`draining`||s===`reconnecting`)&&h!=null&&typeof r.pid==`number`&&r.pid!==h&&!r.isDraining&&(c(`idle`),g(null),u(null))}catch{if(t)return;s===`draining`||s===`reconnecting`?c(`reconnecting`):o(!0)}finally{a.clear(),r===a&&(r=null),n=!1}};a();let l=Gn(()=>void a(),5e3);return()=>{t=!0,r?.controller.abort(),r?.clear(),l()}},[e,s,h]),(0,_.useEffect)(()=>{if(s!==`reconnecting`)return;let t=!1,r=!1,i=null,a=Date.now(),o=()=>{if(r||t)return;r=!0;let o=Vn(5e3);i=o,fetch(`${e}/api/system/health`,{cache:`no-store`,signal:o.signal}).then(async e=>{if(t)return;if(!e.ok){Date.now()-a>=ar&&(c(`error`),u(n(`dash.mem.restartFailed`)));return}let r=h==null;if(h!=null)try{let t=await e.json();r=typeof t.pid==`number`&&t.pid!==h}catch{r=!0}if(!t){if(r){c(`idle`),g(null),u(null);return}Date.now()-a>=ar&&(c(`error`),u(n(`dash.mem.restartFailed`)))}}).catch(()=>{t||Date.now()-a>=ar&&(c(`error`),u(n(`dash.mem.restartFailed`)))}).finally(()=>{o.clear(),i===o&&(i=null),r=!1})};o();let l=setInterval(o,ir);return()=>{t=!0,i?.controller.abort(),i?.clear(),clearInterval(l)}},[e,s,h,n]);let v=()=>{let t=r?.activeTurnCount??0,i=[n(`dash.mem.restartConfirm`,{count:t,seconds:rr})];d&&i.push(n(`dash.mem.restartNoSupervisor`)),window.confirm(i.join(` + +`))&&(async()=>{u(null),g(typeof r?.pid==`number`?r.pid:null),c(`draining`);try{if(!(await fetch(`${e}/api/system/restart`,{method:`POST`})).ok)throw Error(`restart_failed`)}catch{c(`error`),g(null),u(n(`dash.mem.restartFailed`))}})()};if(a&&!r&&s===`idle`)return(0,J.jsxs)(`div`,{className:`panel`,style:{marginBottom:24},children:[(0,J.jsxs)(`div`,{className:`font-semibold`,style:{display:`flex`,alignItems:`center`,gap:8},children:[(0,J.jsx)(ce,{width:16,height:16,"aria-hidden":`true`}),n(`dash.mem.title`)]}),(0,J.jsx)(`div`,{className:`muted text-control`,style:{marginTop:8},children:n(`dash.mem.unavailable`)})]});let y=r?.watchdog?er(r.watchdog.samples):null,b=r?r.observedBytes??r.watchdog?.observedBytes??Qn(r):null,x=r?$n(r):null,S=(()=>{let e=r?.watchdog?.warnThresholdBytes;if(y===null||y<=0||b===null||!e)return;let t=e-b;if(t<=0)return`danger`;let n=t/y;if(n<=1)return`danger`;if(n<=8)return`warn`})(),C=r?.responseState,w=r?.activeTurnCount,T=s===`draining`||s===`reconnecting`;return(0,J.jsxs)(`div`,{className:`panel`,style:{marginBottom:24},children:[(0,J.jsxs)(`div`,{className:`mem-head`,children:[(0,J.jsxs)(`div`,{className:`font-semibold mem-head-title`,children:[(0,J.jsx)(ce,{width:16,height:16,"aria-hidden":`true`}),n(`dash.mem.title`)]}),p&&(0,J.jsxs)(`div`,{className:`mem-head-actions`,children:[(0,J.jsxs)(`span`,{className:`mem-inflight`,children:[(0,J.jsx)(`span`,{className:`mem-inflight-label`,children:n(`dash.mem.inFlight`)}),(0,J.jsx)(`span`,{className:`mem-inflight-value mono`,children:typeof w==`number`?Yn(t).format(w):`—`})]}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,disabled:T,onClick:v,children:n(`dash.mem.restart`)})]})]}),(0,J.jsx)(nr,{observedBytes:b,thresholdBytes:r?.watchdog?.warnThresholdBytes??null,metric:x,locale:t,t:n}),(0,J.jsxs)(`div`,{className:`stat-row mem-stats`,children:[(0,J.jsx)(tr,{label:n(`dash.mem.rss`),value:r?Xn(r.rss,t):`—`}),(0,J.jsx)(tr,{label:n(`dash.mem.jsHeap`),value:r?Xn(r.heapUsed,t):`—`,sub:r?n(`dash.mem.jsHeapArena`,{total:Xn(r.heapTotal,t)}):void 0}),(0,J.jsx)(tr,{label:n(`dash.mem.jscHeap`),value:r?.jscHeap?Xn(r.jscHeap.heapSize,t):`—`}),(0,J.jsx)(tr,{label:n(`dash.mem.growth`),value:y===null?`—`:`${y>=0?`+`:`−`}${Xn(Math.abs(y),t)}${n(`dash.mem.perHour`)}`,tone:S})]}),(0,J.jsxs)(`details`,{style:{marginTop:10},children:[(0,J.jsx)(`summary`,{className:`muted text-label`,style:{cursor:`pointer`,padding:`2px 2px`},children:n(`dash.mem.details`)}),(0,J.jsx)(`div`,{className:`muted text-control`,style:{margin:`8px 0 0`},children:n(`dash.mem.hint`)}),(0,J.jsx)(`div`,{className:`muted text-label`,style:{margin:`14px 0 6px`},children:n(`dash.mem.runtime`)}),(0,J.jsxs)(`div`,{className:`stat-row`,children:[(0,J.jsx)(tr,{label:n(`dash.mem.observed`),value:b===null?`—`:`${Xn(b,t)} (${x})`}),(0,J.jsx)(tr,{label:n(`dash.mem.external`),value:r?.external===void 0?`—`:Xn(r.external,t)}),(0,J.jsx)(tr,{label:n(`dash.mem.arrayBuffers`),value:r?.arrayBuffers===void 0?`—`:Xn(r.arrayBuffers,t)})]}),(0,J.jsx)(`div`,{className:`muted text-label`,style:{margin:`14px 0 6px`},children:n(`dash.mem.store`)}),(0,J.jsx)(`div`,{className:`muted text-control`,style:{marginBottom:10},children:n(`dash.mem.storeHint`)}),(0,J.jsxs)(`div`,{className:`stat-row`,children:[(0,J.jsx)(tr,{label:n(`dash.mem.storeEntries`),value:C?Yn(t).format(C.count):`—`}),(0,J.jsx)(tr,{label:n(`dash.mem.storeTotal`),value:C?Xn(C.totalBytes,t):`—`}),(0,J.jsx)(tr,{label:n(`dash.mem.storeLargest`),value:C?Xn(C.largestBytes,t):`—`}),(0,J.jsx)(tr,{label:n(`dash.mem.storeOldest`),value:C?C.count===0?`—`:Zn(C.oldestAgeMs,t):`—`})]}),r?.watchdog&&(0,J.jsxs)(`div`,{className:`stat-row`,style:{marginTop:16},children:[(0,J.jsx)(tr,{label:n(`dash.mem.threshold`),value:Xn(r.watchdog.warnThresholdBytes,t)}),(0,J.jsx)(tr,{label:n(`dash.mem.lastWarn`),value:r.watchdog.lastWarnAt?new Date(r.watchdog.lastWarnAt).toLocaleString(t):n(`dash.mem.never`)})]})]}),p&&(0,J.jsxs)(`div`,{className:`mem-status`,"aria-live":`polite`,children:[s===`draining`&&(0,J.jsx)(`span`,{className:`muted text-control`,children:n(`dash.mem.draining`,{count:typeof w==`number`?w:0})}),s===`reconnecting`&&(0,J.jsx)(`span`,{className:`muted text-control`,children:n(`dash.mem.reconnecting`)}),s===`error`&&l&&(0,J.jsx)(`span`,{className:`text-control`,style:{color:`var(--danger, #c44)`},children:l}),d&&s===`idle`&&(0,J.jsx)(`span`,{className:`muted text-control`,children:n(`dash.mem.restartNoSupervisor`)})]})]})}function sr({apiBase:e,d:t}){let{t:n,maMode:r,maModeResolved:i,effortCapHelpTriggerRef:a,effortCapHelpOpen:o,setEffortCapHelpOpen:s,effortCap:c,subagentEffortCap:l,effortCapSaving:u,setEffortCap:d,setSubagentEffortCap:f,setEffortCapSaving:p}=t;return!i||r===`v1`?null:(0,J.jsx)(`div`,{className:`panel`,children:(0,J.jsxs)(`div`,{className:`injection-head`,children:[(0,J.jsxs)(`span`,{className:`injection-label`,style:{display:`inline-flex`,alignItems:`center`,gap:6},children:[n(`dash.effortCapLabel`),(0,J.jsx)(`button`,{ref:a,type:`button`,className:`btn btn-ghost btn-sm`,style:{width:22,height:22,minWidth:22,padding:0,borderRadius:`var(--radius-pill)`,color:`var(--muted)`},onClick:()=>s(e=>!e),"aria-label":n(`dash.effortCapLabel`),"aria-expanded":o,"aria-haspopup":`dialog`,"aria-controls":`effort-cap-help-dialog`,children:(0,J.jsx)(Z,{width:13,height:13,"aria-hidden":`true`})})]}),(0,J.jsx)(Dt,{value:c,options:[{value:``,label:n(`dash.effortCapNone`)},...Gt.map(e=>({value:e,label:e}))],onChange:async t=>{if(!u){p(!0);try{let n=await Wt(await fetch(`${e}/api/effort-caps`,{method:`PUT`,headers:{"Content-Type":`application/json`},body:JSON.stringify({effortCap:t||null})}));d(n.effortCap??``),f(n.subagentEffortCap??``)}catch{}finally{p(!1)}}},disabled:u,label:n(`dash.effortCapLabel`),align:`right`}),(0,J.jsx)(Dt,{value:l,options:[{value:``,label:n(`dash.effortCapNone`)},...Gt.map(e=>({value:e,label:e}))],onChange:async t=>{if(!u){p(!0);try{let n=await Wt(await fetch(`${e}/api/effort-caps`,{method:`PUT`,headers:{"Content-Type":`application/json`},body:JSON.stringify({subagentEffortCap:t||null})}));d(n.effortCap??``),f(n.subagentEffortCap??``)}catch{}finally{p(!1)}}},disabled:u,label:n(`dash.subagentEffortCapLabel`),align:`right`})]})})}function cr({d:e}){let{t,injectionModel:n,injectionEffort:r,injectionEfforts:i,injectionAvailable:a,injectionSaving:o,saveInjection:s}=e;return(0,J.jsxs)(`div`,{className:`panel dash-delegation-summary`,children:[(0,J.jsx)(`div`,{className:`font-semibold`,children:t(`dash.injectionLabel`)}),(0,J.jsxs)(`div`,{className:`dash-delegation-controls`,children:[(0,J.jsx)(Dt,{value:n,options:[{value:``,label:t(`dash.injectionNone`)},...a.map(e=>({value:e.namespaced,label:Pn(`${e.provider}/${e.model}`,t)}))],onChange:e=>{s({model:e||null,effort:r||null})},disabled:o,label:t(`dash.injectionLabel`),align:`right`}),n&&i.length>0&&(0,J.jsx)(Dt,{value:r,options:[{value:``,label:t(`dash.injectionEffortNone`)},...i.map(e=>({value:e,label:e}))],onChange:e=>{s({model:n||null,effort:e||null})},disabled:o,label:t(`dash.injectionEffortLabel`),align:`right`}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:()=>pt(`#subagents`),children:t(`dash.injectionManage`)})]})]})}function lr({d:e}){let{t,runSync:n,syncing:r,updateTriggerRef:i,openUpdateDialog:a,updateLoading:o,updateOpen:s,syncResult:c,syncError:l,updateJob:u,reconnecting:d,clearSyncFeedback:f}=e,p=!!c&&(!!c.warning||!!c.nativeSubagentDefaultsWarning||!!c.staleAppServerHint),[m,h]=(0,_.useState)(!1),g=(0,_.useRef)(null);(0,_.useEffect)(()=>(g.current&&=(clearTimeout(g.current),null),(c||l)&&!p&&(g.current=setTimeout(()=>{g.current=null,h(!0),f()},l?8e3:6e3)),()=>{g.current&&clearTimeout(g.current)}),[c,l,p,f]);let v=()=>{h(!1),n()},y=()=>{h(!0),f()};return(0,J.jsxs)(J.Fragment,{children:[(0,J.jsxs)(`div`,{className:`panel maintenance-panel`,children:[(0,J.jsxs)(`div`,{className:`dash-sync-summary`,children:[(0,J.jsxs)(`div`,{className:`dash-sync-copy`,children:[(0,J.jsx)(`div`,{className:`font-semibold`,children:t(`dash.syncModels`)}),(0,J.jsx)(`div`,{className:`muted text-control dash-sync-hint`,children:t(`dash.syncModelsHint`)})]}),(0,J.jsxs)(`div`,{className:`maintenance-actions`,children:[(0,J.jsxs)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:v,disabled:r,children:[(0,J.jsx)(pe,{className:r?`spin-icon`:void 0}),` `,t(r?`dash.syncing`:`dash.syncRun`)]}),(0,J.jsx)(`button`,{ref:i,type:`button`,className:`maintenance-update-anchor`,onClick:a,disabled:o,"aria-haspopup":`dialog`,"aria-controls":`dashboard-update-dialog`,"aria-expanded":s,"aria-label":t(`dash.checkUpdate`),tabIndex:-1})]})]}),u&&(0,J.jsxs)(`div`,{className:`notice ${u.status===`failed`?`notice-err`:`notice-ok`} maintenance-notice`,role:`status`,children:[u.status===`failed`?(0,J.jsx)(_e,{}):(0,J.jsx)(pe,{}),(0,J.jsxs)(`span`,{children:[Jt(u.status,t),u.latestVersion?` ${t(`dash.updateVersionTransition`,{currentVersion:u.currentVersion,latestVersion:u.latestVersion})}`:``,d?` ${t(`dash.updateReconnecting`)}`:``,u.error?` ${u.error}`:``]})]})]}),!m&&c&&(0,J.jsxs)(`div`,{className:`action-toast notice ${p?`notice-warn`:`notice-ok`}`,role:`status`,"aria-live":`polite`,children:[p?(0,J.jsx)(_e,{}):(0,J.jsx)(ue,{}),(0,J.jsxs)(`span`,{children:[t(`dash.syncOk`,{count:c.added}),c.warning?` ${c.warning}`:``,c.nativeSubagentDefaultsWarning?` ${c.nativeSubagentDefaultsWarning}`:``,c.staleAppServerHint?(0,J.jsxs)(J.Fragment,{children:[` `,(0,J.jsx)(ut,{k:`dash.syncStaleHint`,cmd:`ocx sync --restart-codex`})]}):null]}),(0,J.jsx)(`button`,{type:`button`,className:`action-toast-dismiss`,onClick:y,"aria-label":t(`api.dismiss`),children:(0,J.jsx)(de,{width:13,height:13,"aria-hidden":`true`})})]}),!m&&l&&(0,J.jsxs)(`div`,{className:`action-toast notice notice-err`,role:`status`,"aria-live":`polite`,children:[(0,J.jsx)(_e,{}),(0,J.jsx)(`span`,{children:t(`dash.syncFailed`,{error:l})}),(0,J.jsx)(`button`,{type:`button`,className:`action-toast-dismiss`,onClick:y,"aria-label":t(`api.dismiss`),children:(0,J.jsx)(de,{width:13,height:13,"aria-hidden":`true`})})]})]})}function ur({t:e,open:t,triggerRef:n,onClose:r,maxValue:i,maxInvalid:a,timeoutValue:o,timeoutInvalid:s,disabled:c,setMaxDraft:l,setMaxInvalid:u,setTimeoutDraft:d,setTimeoutInvalid:f,commitMaxDescriptions:p,commitTimeout:m}){let h=(0,_.useRef)(null),g=(0,_.useRef)(null),[v,y]=(0,_.useState)(),b=(0,_.useCallback)(()=>{n.current&&y(wt(n.current.getBoundingClientRect(),{align:`right`,placement:`below`,menuHeight:h.current?.offsetHeight??180}))},[n]);return(0,_.useLayoutEffect)(()=>{if(!t)return;b();let e=()=>b();return window.addEventListener(`resize`,e),window.addEventListener(`scroll`,e,!0),()=>{window.removeEventListener(`resize`,e),window.removeEventListener(`scroll`,e,!0)}},[t,b,a,s]),(0,_.useEffect)(()=>{if(!t)return;let e=e=>{let t=e.target;if(h.current?.contains(t)||n.current?.contains(t))return;let i=document.activeElement;i&&h.current?.contains(i)&&i.blur(),r()};return document.addEventListener(`mousedown`,e),()=>document.removeEventListener(`mousedown`,e)},[t,r,n]),(0,_.useEffect)(()=>{if(!t)return;let e=e=>{e.key===`Escape`&&(e.preventDefault(),r(),n.current?.focus())};return document.addEventListener(`keydown`,e),()=>document.removeEventListener(`keydown`,e)},[t,r,n]),(0,_.useEffect)(()=>{t&&g.current?.focus()},[t]),t?(0,J.jsxs)(`div`,{ref:h,id:`dash-vision-advanced-popover`,className:`dash-vision-advanced-popover`,role:`dialog`,"aria-modal":`false`,"aria-label":e(`dash.visionAdvancedPopover`),style:{...v,zIndex:60},children:[(0,J.jsx)(`div`,{className:`dash-vision-advanced-popover-title`,children:e(`dash.visionAdvancedPopover`)}),(0,J.jsxs)(`label`,{className:`dash-vision-number`,children:[(0,J.jsx)(`span`,{className:`muted setting-hint`,id:`dash-vision-max-label`,children:e(`dash.visionMaxDescriptions`)}),(0,J.jsx)(`span`,{className:`codex-auto-switch-input-wrap`,children:(0,J.jsx)(`input`,{ref:g,className:`input mono codex-auto-switch-input`,type:`number`,min:1,step:1,inputMode:`numeric`,value:i,disabled:c,"aria-invalid":a||void 0,"aria-label":e(`dash.visionMaxDescriptions`),"aria-describedby":a?`dash-vision-max-error dash-vision-max-label`:`dash-vision-max-label`,onChange:e=>{u(!1),l(e.target.value)},onBlur:e=>p(e.currentTarget.value),onKeyDown:e=>{e.nativeEvent.isComposing||c||(e.key===`Enter`?(e.preventDefault(),p(e.currentTarget.value)):e.key===`Escape`&&(e.preventDefault(),l(null),u(!1)))}})}),a&&(0,J.jsx)(`span`,{id:`dash-vision-max-error`,className:`muted setting-hint`,role:`alert`,children:e(`dash.visionMaxDescriptionsInvalid`)})]}),(0,J.jsxs)(`label`,{className:`dash-vision-number`,children:[(0,J.jsx)(`span`,{className:`muted setting-hint`,id:`dash-vision-timeout-label`,children:e(`dash.visionTimeout`)}),(0,J.jsxs)(`span`,{className:`codex-auto-switch-input-wrap`,children:[(0,J.jsx)(`input`,{className:`input mono codex-auto-switch-input`,type:`number`,min:nn,max:tn,step:1e3,inputMode:`numeric`,value:o,disabled:c,"aria-invalid":s||void 0,"aria-label":e(`dash.visionTimeout`),"aria-describedby":s?`dash-vision-timeout-error dash-vision-timeout-label`:`dash-vision-timeout-label`,onChange:e=>{f(!1),d(e.target.value)},onBlur:e=>m(e.currentTarget.value),onKeyDown:e=>{e.nativeEvent.isComposing||c||(e.key===`Enter`?(e.preventDefault(),m(e.currentTarget.value)):e.key===`Escape`&&(e.preventDefault(),d(null),f(!1)))}}),(0,J.jsx)(`span`,{className:`codex-auto-switch-unit`,"aria-hidden":`true`,children:`ms`})]}),s&&(0,J.jsx)(`span`,{id:`dash-vision-timeout-error`,className:`muted setting-hint`,role:`alert`,children:e(`dash.visionTimeoutInvalid`,{min:nn,max:tn})})]})]}):null}function dr({d:e}){let{t,settings:n,settingsSaving:r,toggleCodexAutoStart:i,sidecar:a,sidecarSaving:o,sidecarModels:s,visionModels:c,models:l,saveSidecar:u,shadowCall:d,shadowCallSaving:f,shadowCallHelpTriggerRef:p,shadowCallHelpOpen:m,setShadowCallHelpOpen:h,saveShadowCall:g}=e,v=a?.vision.enabled!==!1,y=v?a?.vision.model??`gpt-5.4-mini`:``,b=a?.vision.reasoning??`low`,x=sn(l,y),S=ln(x,b),C=String(a?.vision.maxDescriptionsPerTurn??8),w=String(a?.vision.timeoutMs??en),[T,E]=(0,_.useState)(null),[D,O]=(0,_.useState)(null),[k,A]=(0,_.useState)(!1),[j,M]=(0,_.useState)(!1),[N,P]=(0,_.useState)(!1),F=(0,_.useRef)(null),I=T??C,L=D??w,R=(e=I)=>{let t=rn(e);if(t===void 0){E(e),A(!0);return}A(!1),E(null),t!==(a?.vision.maxDescriptionsPerTurn??8)&&u(Qt(t))},z=(e=L)=>{let t=an(e);if(t===void 0){O(e),M(!0);return}M(!1),O(null),t!==(a?.vision.timeoutMs??en)&&u($t(t))};return(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)(`div`,{className:`panel`,children:(0,J.jsxs)(`div`,{className:`spread`,children:[(0,J.jsxs)(`div`,{style:{flex:1,minWidth:0},children:[(0,J.jsx)(`div`,{className:`font-semibold`,children:t(`dash.codexAutoStart`)}),(0,J.jsx)(`div`,{className:`muted setting-hint`,children:t(`dash.codexAutoStartHint`)})]}),(0,J.jsx)(`button`,{type:`button`,className:`switch ${n?.codexAutoStart??!0?`on`:``}`,onClick:i,disabled:!n||r,"aria-label":t(`dash.codexAutoStart`),"aria-pressed":n?.codexAutoStart??!0,children:(0,J.jsx)(`span`,{className:`knob`})})]})}),(0,J.jsxs)(`div`,{className:`dash-sidecar-grid`,children:[(0,J.jsxs)(`div`,{className:`panel dash-delegation-summary dash-sidecar-row-card`,"aria-busy":!a||void 0,children:[(0,J.jsxs)(`div`,{className:`dash-sidecar-copy`,children:[(0,J.jsx)(`div`,{className:`font-semibold`,children:t(`dash.webSearchSidecar`)}),(0,J.jsx)(`div`,{className:`muted setting-hint`,children:t(`dash.webSearchSidecarHint`)})]}),(0,J.jsxs)(`div`,{className:`dash-delegation-controls`,children:[(0,J.jsx)(`div`,{className:`dash-sidecar-select-row`,children:(0,J.jsx)(Dt,{value:a?.webSearch.model??`gpt-5.6-luna`,options:s,onChange:e=>{u({webSearch:hn(l,s,e)})},disabled:!a||o,label:t(`dash.sidecarModel`),align:`right`})}),(0,J.jsxs)(`div`,{className:`dash-sidecar-trailing-row`,title:t(`dash.webSearchStreamHint`),children:[(0,J.jsx)(`span`,{className:`muted setting-hint dash-sidecar-toggle-label`,children:t(`dash.webSearchStream`)}),(0,J.jsx)(`button`,{type:`button`,className:`switch ${a?.webSearch.streamRoutedModelOutput?`on`:``}`,onClick:()=>{u({webSearch:{streamRoutedModelOutput:!a?.webSearch.streamRoutedModelOutput}})},disabled:!a||o,"aria-label":t(`dash.webSearchStream`),"aria-pressed":a?.webSearch.streamRoutedModelOutput===!0,children:(0,J.jsx)(`span`,{className:`knob`})})]})]})]}),(0,J.jsxs)(`div`,{className:`panel dash-delegation-summary dash-sidecar-row-card dash-vision-sidecar-card`,"aria-busy":!a||void 0,children:[(0,J.jsxs)(`div`,{className:`dash-sidecar-copy`,children:[(0,J.jsx)(`div`,{className:`font-semibold`,children:t(`dash.visionSidecar`)}),(0,J.jsx)(`div`,{className:`muted setting-hint`,children:t(`dash.visionSidecarHint`)})]}),(0,J.jsxs)(`div`,{className:`dash-delegation-controls`,children:[(0,J.jsxs)(`div`,{className:`dash-sidecar-select-row`,children:[(0,J.jsx)(Dt,{value:y,options:[{value:``,label:t(`dash.visionOff`)},...c],onChange:e=>{if(e===``){u(Zt(!1));return}let t=ln(sn(l,e),S),n={vision:{model:e,backend:gn(l,c,e),reasoning:t}};v||(n.vision={...n.vision,enabled:!0}),u(n)},disabled:!a||o,label:t(`dash.sidecarModel`)}),(0,J.jsx)(Dt,{value:S,options:cn(x,S).map(e=>({value:e,label:e})),onChange:e=>{u(Xt(e))},disabled:!v||!a||o,align:`right`,label:`${t(`dash.visionSidecar`)} — ${t(`dash.injectionEffortLabel`)}`})]}),(0,J.jsx)(`div`,{className:`dash-sidecar-trailing-row`,children:(0,J.jsxs)(`button`,{type:`button`,ref:F,className:`dash-vision-advanced-trigger`,onClick:()=>P(e=>!e),disabled:!v||!a||o,"aria-expanded":N,"aria-haspopup":`dialog`,"aria-controls":`dash-vision-advanced-popover`,children:[(0,J.jsx)(`span`,{children:t(`dash.visionAdvanced`)}),(0,J.jsx)(Se,{width:12,height:12,"aria-hidden":`true`,style:{transform:N?`rotate(90deg)`:`none`,transition:`transform .12s`}})]})})]}),(0,mt.createPortal)((0,J.jsx)(ur,{t,open:N,triggerRef:F,onClose:()=>P(!1),maxValue:I,maxInvalid:k,timeoutValue:L,timeoutInvalid:j,disabled:!v||!a||o,setMaxDraft:E,setMaxInvalid:A,setTimeoutDraft:O,setTimeoutInvalid:M,commitMaxDescriptions:R,commitTimeout:z}),document.body)]})]}),(0,J.jsx)(`div`,{className:`panel`,"aria-busy":!d||void 0,children:(0,J.jsxs)(`div`,{className:`spread`,style:{alignItems:`center`},children:[(0,J.jsxs)(`div`,{style:{display:`flex`,alignItems:`center`,gap:8},children:[(0,J.jsx)(`span`,{className:`font-semibold`,children:t(`dash.shadowCallIntercept`)}),(0,J.jsx)(`button`,{ref:p,type:`button`,className:`btn btn-ghost btn-sm`,style:{width:22,height:22,minWidth:22,padding:0,borderRadius:`var(--radius-pill)`,color:`var(--muted)`},onClick:()=>h(e=>!e),"aria-label":t(`dash.shadowCallIntercept`),"aria-expanded":m,"aria-haspopup":`dialog`,"aria-controls":`shadow-call-help-dialog`,children:(0,J.jsx)(Z,{width:13,height:13,"aria-hidden":`true`})}),(0,J.jsx)(`code`,{className:`muted text-caption`,children:`⚠ ${zt(d?.sourceModels)}`})]}),(0,J.jsxs)(`div`,{className:`setting-controls`,style:{display:`flex`,gap:8,alignItems:`center`},children:[(0,J.jsx)(`button`,{type:`button`,className:`switch ${d?.enabled?`on`:``}`,onClick:()=>g({enabled:!d?.enabled}),disabled:!d||f,"aria-label":t(`dash.shadowCallIntercept`),"aria-pressed":d?.enabled??!1,children:(0,J.jsx)(`span`,{className:`knob`})}),(0,J.jsx)(Dt,{value:d?.model??``,options:pn(l,d?.model,d?.sourceModels).map(e=>e.value===``?e:{...e,label:Pn(e.value,t)}),onChange:e=>{g({model:e})},disabled:!d||f||!d?.enabled,label:t(`dash.shadowCallModel`),align:`right`})]})]})})]})}function fr(e){return(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)(sr,{apiBase:e.apiBase,d:e}),(0,J.jsxs)(`div`,{className:`dash-overview-tools`,children:[(0,J.jsx)(cr,{apiBase:e.apiBase,d:e}),(0,J.jsx)(lr,{d:e})]}),(0,J.jsx)(dr,{d:e}),(0,J.jsx)(or,{apiBase:e.apiBase})]})}function pr(e){return(0,J.jsxs)(`div`,{className:`dash-overview-stack`,children:[(0,J.jsx)(Bn,{...e}),(0,J.jsx)(fr,{...e})]})}function mr({t:e,providers:t}){return(0,J.jsxs)(J.Fragment,{children:[(0,J.jsxs)(`div`,{className:`h-section`,children:[e(`dash.activeProviders`),` `,(0,J.jsx)(`span`,{className:`count`,children:t.length})]}),t.length===0?(0,J.jsx)(Ot,{title:(0,J.jsx)(ut,{k:`dash.noProviders`,cmd:`ocx init`})}):(0,J.jsx)(`div`,{className:`tbl-wrap`,children:(0,J.jsxs)(`table`,{className:`tbl`,children:[(0,J.jsx)(`thead`,{children:(0,J.jsxs)(`tr`,{children:[(0,J.jsx)(`th`,{children:e(`dash.col.name`)}),(0,J.jsx)(`th`,{children:e(`dash.col.adapter`)}),(0,J.jsx)(`th`,{children:e(`dash.col.baseUrl`)}),(0,J.jsx)(`th`,{children:e(`dash.col.model`)})]})}),(0,J.jsx)(`tbody`,{children:t.map(t=>(0,J.jsxs)(`tr`,{children:[(0,J.jsx)(`td`,{className:`font-semibold`,children:jn(t.name,e)}),(0,J.jsx)(`td`,{children:(0,J.jsx)(`span`,{className:`chip`,children:t.adapter})}),(0,J.jsx)(`td`,{className:`muted mono text-label`,children:t.baseUrl}),(0,J.jsx)(`td`,{className:`muted`,children:t.defaultModel??`—`})]},t.name))})]})})]})}var hr=`__ocxCachedAt`;function gr(e){try{let t=sessionStorage.getItem(e);if(!t)return null;let n=JSON.parse(t);return _r(n)?n.data:n}catch{return null}}function _r(e){return typeof e==`object`&&!!e&&typeof e[hr]==`number`&&`data`in e}function vr(e){try{let t=sessionStorage.getItem(e);if(!t)return null;let n=JSON.parse(t);return _r(n)?{data:n.data,cachedAt:n[hr]}:{data:n,cachedAt:null}}catch{return null}}function yr(e,t){try{sessionStorage.setItem(e,JSON.stringify({[hr]:Date.now(),data:t}))}catch{}}function br(e,t){try{sessionStorage.setItem(e,JSON.stringify(t))}catch{}}function xr(e){return e.routingKind===`custom-local`?`startup.riskDetailCustomLocal`:e.shimCoverage===`cli-only`?`startup.riskDetailWindowsShim`:`startup.riskDetail`}function Sr(e,t){return!t.mutationInFlight&&e.request===t.request&&e.mutation===t.mutation}function Cr(e,t){return{request:++e.current,mutation:t.current}}var wr=2e3;function Tr(e){let t=e.status;return t!==`native`&&t!==`protected`&&t!==`at-risk`?null:t}function Er(e){return e?e.stale&&e.status!==`error`:!1}function Dr(e,t){return!t||e!==null&&e!==`error`?e:t.status}var Or=3e4;function kr(e){return{multiAgentGuidanceEnabled:e.multiAgentGuidanceEnabled!==!1,syncCodexSubagentDefaults:e.syncCodexSubagentDefaults===!0,injectionModel:e.model??``,injectionEffort:e.effort??``}}function Ar(e,t){return t.aborted?!0:e instanceof Error&&e.name===`AbortError`}async function jr(e,t){try{let n=await fetch(`${e}/api/startup-health`,{signal:t});if(!n.ok)throw Error(`startup health unavailable`);let r=await n.json(),i=Tr(r);if(!i)throw Error(`invalid startup health response`);return{status:i,stale:r.diagnosticStale===!0}}catch(e){if(Ar(e,t))throw e;return{status:`error`,stale:!1}}}async function Mr(e,t){try{return(await Ft(await fetch(`${e}/api/diagnostics/project-config`,{signal:t})))?.grouped??[]}catch{return[]}}async function Nr(e,t){return Wt(await fetch(`${e}/api/models`,{signal:t}))}async function Pr(e,t){return Wt(await fetch(`${e}/api/usage?range=30d`,{signal:t}))}async function Fr(e,t,n){let{request:r,mutation:i}=Cr(n.shadowCallRequestEpochRef,n.shadowCallMutationEpochRef),[a,o]=await Promise.all([fetch(`${e}/api/sidecar-settings`,{signal:t}),fetch(`${e}/api/shadow-call-settings`,{signal:t})]),s=await Wt(a),c;try{if(o.ok){let e=await o.json();Sr({request:r,mutation:i},{request:n.shadowCallRequestEpochRef.current,mutation:n.shadowCallMutationEpochRef.current,mutationInFlight:n.shadowCallMutationInFlightRef.current})&&(c=e)}else Sr({request:r,mutation:i},{request:n.shadowCallRequestEpochRef.current,mutation:n.shadowCallMutationEpochRef.current,mutationInFlight:n.shadowCallMutationInFlightRef.current})&&(c=null)}catch{Sr({request:r,mutation:i},{request:n.shadowCallRequestEpochRef.current,mutation:n.shadowCallMutationEpochRef.current,mutationInFlight:n.shadowCallMutationInFlightRef.current})&&(c=null)}return{sidecar:s,shadowCall:c}}async function Ir(e,t,n){let{request:r,mutation:i}=Cr(n.settingsRequestEpochRef,n.settingsMutationEpochRef),a=await Wt(await fetch(`${e}/api/settings`,{signal:t})),o,s;return Sr({request:r,mutation:i},{request:n.settingsRequestEpochRef.current,mutation:n.settingsMutationEpochRef.current,mutationInFlight:n.settingsMutationInFlightRef.current})&&(o=a,s=a.startupHealth),{settings:o,startupHealthSeed:s}}async function Lr(e,t){try{let n=await fetch(`${e}/api/v2`,{signal:t});if(!n.ok)return{maMode:`default`};let r=await n.json();return r.multiAgentMode===`v1`||r.multiAgentMode===`v2`?{maMode:r.multiAgentMode}:{maMode:`default`}}catch(e){if(Ar(e,t))throw e;return{maMode:`default`}}}async function Rr(e,t){try{let[n,r]=await Promise.all([fetch(`${e}/api/system/health`,{signal:t}),fetch(`${e}/api/providers`,{signal:t})]);return{health:await Wt(n),providers:await Wt(r),error:!1}}catch{return{health:null,providers:[],error:!0}}}async function zr(e,t){let[n,r]=await Promise.all([fetch(`${e}/api/injection-model`,{signal:t}).catch(()=>null),fetch(`${e}/api/effort-caps`,{signal:t}).catch(()=>null)]),i;try{if(n?.ok){let e=await n.json();i={...kr(e),injectionEfforts:e.efforts??[],injectionAvailable:e.available??[]}}}catch{}let a;try{if(r?.ok){let e=await r.json();a={effortCap:e.effortCap??``,subagentEffortCap:e.subagentEffortCap??``}}}catch{}return{injection:i,effortCaps:a}}function Br(e,t=`all`){return t===`codex`?[`usage-summary-30d`,e,`codex`].join(`:`):[`usage-summary-30d`,e,`all`].join(`:`)}var Vr=`ocx.dash.controls.v1:`,Hr=`ocx.dash.overview.v1:`,Ur=`ocx.dash.usage30d.v1:`,Wr=`ocx.dash.startup.v1:`,Gr=`ocx.dash.maMode.v1:`;function Kr(e){let t=new Map;for(let n of e){let e=t.get(n.provider);e?e.push(n):t.set(n.provider,[n])}return[...t.entries()].sort(([e],[t])=>e.localeCompare(t))}function qr(e){return`${Vr}${e}`}function Jr(e){let{locale:t,t:n}=ct(),[r,i]=(0,_.useState)(Vt),[a,o]=(0,_.useState)(``),[s,c]=(0,_.useState)(new Set),l=(0,_.useMemo)(()=>gr(qr(e)),[e]),u=(0,_.useMemo)(()=>gr(`${Hr}${e}`),[e]),d=(0,_.useMemo)(()=>gr(`${Ur}${e}`),[e]),f=(0,_.useMemo)(()=>{let t=gr(`${Wr}${e}`);return t===`error`?null:t},[e]),p=(0,_.useMemo)(()=>gr(`${Gr}${e}`),[e]),[m,h]=(0,_.useState)(()=>u?.health??null),[g,v]=(0,_.useState)(()=>f),[y,b]=(0,_.useState)(()=>u?.providers??[]),[x,S]=(0,_.useState)([]),[C,w]=(0,_.useState)(()=>l?.settings??null),[T,E]=(0,_.useState)(()=>l?.sidecar??null),[D,O]=(0,_.useState)(()=>l?.shadowCall??null),[k,A]=(0,_.useState)(()=>d),[j,M]=(0,_.useState)(!1),[N,P]=(0,_.useState)(!1),[F,I]=(0,_.useState)(!1),[L,R]=(0,_.useState)(!1),[z,B]=(0,_.useState)(!1),[V,H]=(0,_.useState)(()=>p??`default`),[U,W]=(0,_.useState)(!1),[ee,K]=(0,_.useState)(null),[q,J]=(0,_.useState)(!1),[Y,te]=(0,_.useState)(!1),[ne,re]=(0,_.useState)(!1),[ie,ae]=(0,_.useState)(``),[oe,se]=(0,_.useState)(``),[ce,le]=(0,_.useState)([]),[ue,de]=(0,_.useState)([]),[fe,pe]=(0,_.useState)(!1),[X,me]=(0,_.useState)(!0),[he,ge]=(0,_.useState)(!1),[_e,Z]=(0,_.useState)(``),[ve,ye]=(0,_.useState)(``),[be,xe]=(0,_.useState)(!1),[Se,Ce]=(0,_.useState)(null),[we,Te]=(0,_.useState)(null),[Ee,De]=(0,_.useState)([]),[Oe,ke]=(0,_.useState)(!1),[Ae,je]=(0,_.useState)(`latest`),[Me,Ne]=(0,_.useState)(!0),[Pe,Fe]=(0,_.useState)(!1),Ie=(0,_.useRef)(0),Le=(0,_.useRef)(null),Re=(0,_.useRef)(0),ze=(0,_.useRef)(0),Be=(0,_.useRef)(0),Ve=(0,_.useRef)(!1),He=(0,_.useRef)(0),Ue=(0,_.useRef)(0),We=(0,_.useRef)(!1),[Ge,Ke]=(0,_.useState)(null),[qe,Je]=(0,_.useState)(null),[Ye,Xe]=(0,_.useState)(null),[Ze,Qe]=(0,_.useState)(!1),[$e,et]=(0,_.useState)(!1),tt=(0,_.useRef)(null),nt=(0,_.useRef)(null),rt=(0,_.useRef)(null),it=(0,_.useRef)(null),at=yn(Y,tt),ot=yn(Oe,nt),st=yn(q,rt),Q=yn(ne,it);(0,_.useEffect)(()=>{let e=()=>i(Vt());return window.addEventListener(`hashchange`,e),()=>window.removeEventListener(`hashchange`,e)},[]),(0,_.useEffect)(()=>()=>{Re.current+=1,Le.current!==null&&(window.clearTimeout(Le.current),Le.current=null)},[]);let lt=(0,_.useRef)(f),ut=(0,_.useRef)(0),dt=(0,_.useRef)({settingsRequestEpochRef:ze,settingsMutationEpochRef:Be,settingsMutationInFlightRef:Ve,shadowCallRequestEpochRef:He,shadowCallMutationEpochRef:Ue,shadowCallMutationInFlightRef:We}).current,pt=G(`dashboard-startup-health:${e}`,[e],t=>jr(e,t),{pollMs:3e4}),mt=Er(pt.data),ht=pt.refresh;(0,_.useEffect)(()=>{if(!mt)return;let e=window.setTimeout(()=>{ht()},wr);return()=>window.clearTimeout(e)},[mt,ht]);let gt=G(`dashboard-overview:${e}`,[e],t=>Rr(e,t),{pollMs:5e3}),_t=m!==null||gt.data!==void 0,vt=G(`dashboard-ma-mode:${e}`,[e],t=>Lr(e,t),{pollMs:5e3}),yt=G(`dashboard-sidecars:${e}`,[e],async t=>{let n=ut.current;return{...await Fr(e,t,dt),startupHealthGeneration:n}},{pollMs:5e3}),bt=G(`dashboard-settings:${e}`,[e],async t=>{let n=ut.current;return{...await Ir(e,t,dt),startupHealthGeneration:n}},{pollMs:5e3}),xt=G(`dashboard-multi-agent:${e}`,[e],t=>zr(e,t),{pollMs:5e3,enabled:_t}),St=G(Br(e),[e],t=>Pr(e,t),{enabled:_t,pollMs:6e4,deadlineMs:6e4}),Ct=G(`dashboard-diagnostics:${e}`,[e],t=>Mr(e,t),{pollMs:Or,enabled:_t}),wt=G(`dashboard-models:${e}`,[e,$e],t=>Nr(e,t),{enabled:_t&&!$e});(0,_.useEffect)(()=>{if(pt.data!==void 0){let t=pt.data;ut.current+=1,v(t.status),lt.current=t.status,t.status!==`error`&&!t.stale&&br(`${Wr}${e}`,t.status)}},[pt.data,e]),(0,_.useEffect)(()=>{let t=gt.data;t&&(t.health&&(h(t.health),b(t.providers),br(`${Hr}${e}`,{health:t.health,providers:t.providers})),et(t.error))},[gt.data,e]),(0,_.useEffect)(()=>{vt.data!==void 0&&(H(vt.data.maMode),br(`${Gr}${e}`,vt.data.maMode))},[vt.data,e]);let Tt=vt.data!==void 0||p!==null;(0,_.useEffect)(()=>{let e=xt.data;e&&(e.injection&&(me(e.injection.multiAgentGuidanceEnabled),ge(e.injection.syncCodexSubagentDefaults),ae(e.injection.injectionModel),se(e.injection.injectionEffort),le(e.injection.injectionEfforts),de(e.injection.injectionAvailable)),e.effortCaps&&(Z(e.effortCaps.effortCap),ye(e.effortCaps.subagentEffortCap)))},[xt.data]),(0,_.useEffect)(()=>{let t=yt.data;if(!t)return;E(t.sidecar),t.shadowCall!==void 0&&O(t.shadowCall);let n=gr(qr(e))??{};br(qr(e),{...n,sidecar:t.sidecar,...t.shadowCall===void 0?{}:{shadowCall:t.shadowCall}})},[yt.data,e]),(0,_.useEffect)(()=>{let t=bt.data;if(t){if(t.settings!==void 0&&w(t.settings),t.startupHealthSeed!==void 0&&t.startupHealthGeneration===ut.current){let n=Dr(lt.current,t.startupHealthSeed);v(n),lt.current=n,n&&br(`${Wr}${e}`,n)}if(t.settings!==void 0){let n=gr(qr(e))??{};br(qr(e),{...n,settings:t.settings})}}},[bt.data,e]),(0,_.useEffect)(()=>{St.data!==void 0&&(A(St.data),br(`${Ur}${e}`,St.data))},[St.data,e]),(0,_.useEffect)(()=>{Ct.data&&De(Ct.data)},[Ct.data]),(0,_.useEffect)(()=>{wt.data&&S(wt.data),I(wt.loading)},[wt.data,wt.loading]),(0,_.useEffect)(()=>()=>{ze.current+=1,He.current+=1},[]);let $=G(Ye?.id&&Ye.restart?`update-job:${e}:${Ye.id}`:`update-job:idle:${e}`,[e,Ye?.id,Ye?.restart,Ye?.latestVersion],async t=>{if(!Ye?.id||!Ye.restart)return{reconnecting:!1};let n=Ye.latestVersion;try{let r=await Wt(await fetch(`${e}/api/update/status?jobId=${encodeURIComponent(Ye.id)}`,{signal:t}));if(r.job){if(r.job.status===`failed`)return{job:r.job,reconnecting:!1};if(n)try{if((await Wt(await fetch(`${e}/healthz`,{cache:`no-store`,signal:t}))).version===n)return{job:r.job,reconnecting:!1,reload:!0}}catch{return{job:r.job,reconnecting:!0}}return{job:r.job,reconnecting:!1}}}catch{return{reconnecting:!0}}return{reconnecting:!1}},{pollMs:1500,enabled:!!(Ye?.id&&Ye.restart),pauseWhenHidden:!1});(0,_.useEffect)(()=>{let e=$.data;e&&(`job`in e&&e.job&&Xe(e.job),Qe(e.reconnecting),`reload`in e&&e.reload&&window.location.reload())},[$.data]);let Et=(0,_.useMemo)(()=>Kr(x),[x]),Dt=(0,_.useMemo)(()=>{let e=a.trim().toLowerCase();if(!e)return Et;let t=[];for(let[n,r]of Et){let i=r.filter(t=>t.id.toLowerCase().includes(e)||n.toLowerCase().includes(e));i.length>0&&t.push([n,i])}return t},[Et,a]),Ot=(0,_.useMemo)(()=>{let e=T?.webSearch.backend;return dn(T?.webSearchModels,x,T?.webSearch.model,e===`routed`?void 0:e)},[x,T?.webSearchModels,T?.webSearch]),kt=(0,_.useMemo)(()=>fn(T?.visionModels,x,T?.vision?.model,T?.vision?.backend),[T?.visionModels,x,T?.vision]),At=async t=>{if(!T||j)return;let n=T,r={webSearch:Yt(T.webSearch,t.webSearch),vision:Yt(T.vision,t.vision),...T.visionModels?{visionModels:T.visionModels}:{},...T.webSearchModels?{webSearchModels:T.webSearchModels}:{}};M(!0),E(r);try{let n=await Wt(await fetch(`${e}/api/sidecar-settings`,{method:`PUT`,headers:{"Content-Type":`application/json`},body:JSON.stringify(t)}),`save failed`);E({webSearch:n.webSearch,vision:n.vision,...n.visionModels?{visionModels:n.visionModels}:{},...n.webSearchModels?{webSearchModels:n.webSearchModels}:{}});let r=gr(qr(e))??{};br(qr(e),{...r,sidecar:{webSearch:n.webSearch,vision:n.vision,...n.visionModels?{visionModels:n.visionModels}:{},...n.webSearchModels?{webSearchModels:n.webSearchModels}:{}}})}catch{E(n)}finally{M(!1)}};async function jt(t){if(!D||N)return;let n=D,r={...D,...t};P(!0),We.current=!0,O(r);try{if(!(await fetch(`${e}/api/shadow-call-settings`,{method:`PUT`,headers:{"Content-Type":`application/json`},body:JSON.stringify(t)})).ok)throw Error(`shadow-call save failed`);Ue.current+=1}catch{O(n)}finally{We.current=!1,P(!1)}}let Mt=async t=>{if(!(U||V===t)){W(!0),K(null);try{let r=await fetch(`${e}/api/v2`,{method:`PUT`,headers:{"Content-Type":`application/json`},body:JSON.stringify({multiAgentMode:t})});if(r.ok)H(t),br(`${Gr}${e}`,t);else{let e=n(`dash.maSwitchFailed`,{status:String(r.status)});try{let t=await r.json();e=typeof t.error==`string`&&t.error||typeof t.message==`string`&&t.message||e}catch{}K(e)}}catch(e){K(e instanceof Error?e.message:n(`dash.maNetworkError`))}finally{W(!1)}}},Nt=async t=>{if(!fe){pe(!0);try{if(!(await fetch(`${e}/api/injection-model`,{method:`PUT`,headers:{"Content-Type":`application/json`},body:JSON.stringify(t)})).ok)throw Error(`injection save failed`);let n=await Wt(await fetch(`${e}/api/injection-model`)),r=kr(n);me(r.multiAgentGuidanceEnabled),ge(r.syncCodexSubagentDefaults),ae(r.injectionModel),se(r.injectionEffort),Array.isArray(n.efforts)&&le(n.efforts),Array.isArray(n.available)&&de(n.available)}catch{}finally{pe(!1)}}},Pt=async()=>{if(!C||L)return;let t=!C.codexAutoStart;R(!0),Ve.current=!0,w({...C,codexAutoStart:t});try{let n=await Wt(await fetch(`${e}/api/settings`,{method:`PUT`,headers:{"Content-Type":`application/json`},body:JSON.stringify({codexAutoStart:t})}),`save failed`);Be.current+=1,w(e=>e&&{...e,codexAutoStart:n.codexAutoStart,startupHealth:n.startupHealth??e.startupHealth})}catch{w(e=>e&&{...e,codexAutoStart:!t}),et(!0)}finally{Ve.current=!1,R(!1)}},Ft=(0,_.useCallback)(()=>{Ce(null),Te(null)},[]),It=async()=>{if(!z){B(!0),Ce(null),Te(null);try{let t=await Wt(await fetch(`${e}/api/sync`,{method:`POST`}),`sync failed`);Ce(t),t.projectConfigGrouped&&De(t.projectConfigGrouped)}catch(e){Te(e instanceof Error?e.message:String(e))}finally{B(!1)}}},Lt=async(t,n=!1)=>{n&&(Ie.current=0),Le.current!==null&&(window.clearTimeout(Le.current),Le.current=null);let r=++Re.current;Fe(!0),Je(null),Ke(null);try{let n=await Wt(await fetch(`${e}/api/update/check?tag=${t}`),`update check failed`);if(r!==Re.current)return;if(Ke(n),n.reason===`latest_unavailable`&&Ie.current<2){let e=++Ie.current;Le.current=window.setTimeout(()=>{r===Re.current&&(Le.current=null,Lt(t))},800*e);return}n.reason!==`latest_unavailable`&&(Ie.current=0),Fe(!1)}catch(e){if(r!==Re.current)return;Je(e instanceof Error?e.message:String(e)),Fe(!1)}},Rt=()=>{Re.current+=1,Le.current!==null&&(window.clearTimeout(Le.current),Le.current=null),Fe(!1),ke(!1)},zt=()=>{let e=Kt(m?.version);je(e),Ne(!0),ke(!0),Lt(e,!0)},Bt=e=>{je(e),Lt(e,!0)},Ut=(0,_.useRef)(zt);return(0,_.useEffect)(()=>{Ut.current=zt}),(0,_.useEffect)(()=>{let e=()=>{Ht()&&(ft(`dashboard`),Ut.current())},t=Ht()?window.setTimeout(e,0):null;return window.addEventListener(`hashchange`,e),()=>{t!==null&&window.clearTimeout(t),window.removeEventListener(`hashchange`,e)}},[]),{apiBase:e,locale:t,t:n,selectedSection:r,setSelectedSection:i,modelQuery:a,setModelQuery:o,expandedProviders:s,setExpandedProviders:c,health:m,startupHealth:g,providers:y,models:x,settings:C,sidecar:T,shadowCall:D,usage30d:k,usageLoading:St.loading&&!k,healthLoading:gt.loading&&!m,sidecarSaving:j,shadowCallSaving:N,modelsLoading:F,settingsSaving:L,syncing:z,maMode:V,maModeResolved:Tt,maBusy:U,setMaHelpOpen:J,maHelpOpen:q,maError:ee,effortCapHelpOpen:Y,setEffortCapHelpOpen:te,shadowCallHelpOpen:ne,setShadowCallHelpOpen:re,injectionModel:ie,injectionEffort:oe,injectionEfforts:ce,injectionAvailable:ue,injectionSaving:fe,multiAgentGuidanceEnabled:X,syncCodexSubagentDefaults:he,saveInjection:Nt,effortCap:_e,subagentEffortCap:ve,effortCapSaving:be,setEffortCap:Z,setSubagentEffortCap:ye,setEffortCapSaving:xe,syncResult:Se,syncError:we,projectConfigWarnings:Ee,updateOpen:Oe,updateChannel:Ae,setUpdateRestart:Ne,updateRestart:Me,updateLoading:Pe,updateCheck:Ge,updateError:qe,updateJob:Ye,reconnecting:Ze,error:$e,effortCapHelpTriggerRef:tt,updateTriggerRef:nt,maHelpTriggerRef:rt,shadowCallHelpTriggerRef:it,effortCapHelpDialogRef:at,updateDialogRef:ot,maHelpDialogRef:st,shadowCallHelpDialogRef:Q,filteredGroups:Dt,sidecarModels:Ot,visionModels:kt,saveSidecar:At,saveShadowCall:jt,switchMaMode:Mt,toggleCodexAutoStart:Pt,runSync:It,clearSyncFeedback:Ft,fetchUpdateCheck:Lt,closeUpdateDialog:Rt,openUpdateDialog:zt,changeUpdateChannel:Bt,runUpdate:async()=>{if(Ge?.canUpdate){Je(null);try{let t=await Wt(await fetch(`${e}/api/update/run`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({tag:Ae,restart:Me})}),`update failed to start`);if(!t.job)throw Error(`update failed to start`);Xe(t.job),Qe(!1),Rt()}catch(e){Je(e instanceof Error?e.message:String(e))}}}}}function Yr(e){pt(Ut(e))}function Xr({apiBase:e}){let t=Jr(e),{t:n,error:r,selectedSection:i,providers:a,models:o,modelsLoading:s,modelQuery:c,setModelQuery:l,filteredGroups:u,expandedProviders:d,setExpandedProviders:f}=t;if(r)return(0,J.jsx)(Ot,{style:{marginTop:40},icon:(0,J.jsx)(_e,{}),title:(0,J.jsx)(`span`,{style:{color:`var(--red)`},children:n(`dash.cannotConnect`)}),children:(0,J.jsx)(ut,{k:`dash.runStart`,cmd:`ocx start`})});let p=(0,J.jsx)(pr,{...t}),m=(0,J.jsx)(mr,{t:n,providers:a}),h=(0,J.jsx)(Fn,{t:n,models:o,modelsLoading:s,modelQuery:c,setModelQuery:l,filteredGroups:u,expandedProviders:d,setExpandedProviders:f}),g=(0,J.jsx)(bn,{...t}),_=[{id:`overview`,label:n(`dash.workspace.overview`),body:p},{id:`providers`,label:n(`dash.activeProviders`),body:m},{id:`models`,label:n(`dash.availableModels`),body:h}],v=_.find(e=>e.id===i)??_[0],y=Yr,b=e=>{let t=_.findIndex(e=>e.id===i),n=-1;if(e.key===`ArrowRight`?n=(t+1)%_.length:e.key===`ArrowLeft`?n=(t-1+_.length)%_.length:e.key===`Home`?n=0:e.key===`End`&&(n=_.length-1),n<0)return;e.preventDefault();let r=_[n];y(r.id),document.getElementById(`dashboard-tab-${r.id}`)?.focus()};return(0,J.jsxs)(`div`,{className:`dashboard-workspace-shell`,children:[(0,J.jsx)(`div`,{className:`page-head`,children:(0,J.jsx)(`h2`,{children:n(`nav.dashboard`)})}),(0,J.jsx)(`p`,{className:`page-sub`,children:n(`dash.subtitle`)}),(0,J.jsx)(`div`,{className:`page-tabs`,role:`tablist`,"aria-label":n(`dash.workspace.sections`),children:_.map(e=>(0,J.jsx)(`button`,{type:`button`,role:`tab`,id:`dashboard-tab-${e.id}`,"aria-selected":i===e.id,"aria-controls":`dashboard-panel-${e.id}`,tabIndex:i===e.id?0:-1,className:`page-tab${i===e.id?` page-tab--active`:``}`,onClick:()=>y(e.id),onKeyDown:b,children:e.label},e.id))}),(0,J.jsx)(`section`,{className:`dashboard-workspace-main`,role:`tabpanel`,id:`dashboard-panel-${v.id}`,"aria-labelledby":`dashboard-tab-${v.id}`,tabIndex:0,children:v.body}),g]})}var Zr=`https://chatgpt.com/backend-api/codex`,Qr=`openai`;function $r(e){try{let t=new URL(e.trim());if(t.username||t.password||t.search||t.hash)return;let n=t.pathname.replace(/\/+$/,``);return`${t.origin}${n}`}catch{return}}var ei=new URL(Zr).protocol;function ti(e,...t){return`${ei}//${e}/${t.join(`/`)}`}var ni={"cline-pass":{adapter:`openai-chat`,baseUrl:ti(`api.cline.bot`,`api`,`v1`)},"mimo-free":{adapter:`mimo-free`,baseUrl:ti(`api.xiaomimimo.com`,`api`,`free-ai`,`openai`,`chat`)}};function ri(e,t){let n=ni[e];return!n||t.adapter!==n.adapter||$r(t.baseUrl)!==$r(n.baseUrl)}function ii(e){try{let t=new URL(e).hostname.replace(/^\[|\]$/g,``).toLowerCase();return t===`localhost`||t===`127.0.0.1`||t===`::1`}catch{return!1}}function ai(e){return e.keyOptional===!0||e.authMode===`oauth`||e.authMode===`forward`||e.authMode===`local`||ii(e.baseUrl)||e.hasApiKey===!0}function oi(e){return e.adapter===`openai-responses`&&e.authMode===`forward`&&$r(e.baseUrl)===Zr}function si(e,t){return e===Qr&&oi(t)}function ci(e){return e.freeTier===!0||e.keyOptional===!0||e.authMode===`local`||ii(e.baseUrl)}function li(e,t){return si(e,t)?`accounts`:ci(t)?`free`:`paid`}function ui(e,t){let n=[...e],r=(e,t)=>e.name.localeCompare(t.name,void 0,{sensitivity:`base`}),i=e=>e.tier??li(e.name,e);switch(t){case`az`:return n.sort(r);case`za`:return n.sort((e,t)=>r(t,e));case`free-paid`:return n.sort((e,t)=>(i(e)===`free`?0:1)-(i(t)===`free`?0:1)||r(e,t));case`paid-free`:return n.sort((e,t)=>(i(e)===`free`)-+(i(t)===`free`)||r(e,t));case`accounts-first`:return n.sort((e,t)=>{let n=e=>{let t=i(e);return t===`accounts`?0:t===`free`?1:2};return n(e)-n(t)||r(e,t)});default:return n}}function di(e){let t=[],n=[],r=[];for(let[i,a]of Object.entries(e)){if(a.disabled){r.push({name:i,...a});continue}ai(a)?t.push({name:i,...a,tier:li(i,a)}):n.push({name:i,...a})}return{ready:t,needsSetup:n,disabled:r}}function fi(e,t){let n=new Set(Object.entries(t).filter(([,e])=>e).map(([e])=>e));if(n.size===0)return e;let r=e=>e.map(e=>n.has(e.name)?{...e,activeNeedsReauth:!0}:e);return{ready:r(e.ready),needsSetup:r(e.needsSetup),disabled:e.disabled}}function pi(e){return e.disabled?`disabled`:`activeNeedsReauth`in e&&e.activeNeedsReauth?`needs-setup`:ai(e)?`ready`:`needs-setup`}function mi(e){let t=e.openai,n=e.chatgpt;if(!t||!n||!si(`openai`,t)||!oi(n))return e;let r={...e};return delete r.chatgpt,r}function hi(e){return e.authMode===`local`||ii(e.baseUrl)}var gi=[`ollama`,`vllm`,`lm-studio`,`lmstudio`,`litellm`,`localai`];function _i(e){let t=(e.authMode??``).toLowerCase();if(t===`oauth`||t===`forward`)return`login`;if(hi(e))return`local`;let n=`${e.name??``} ${e.adapter} ${e.baseUrl}`.toLowerCase();return gi.some(e=>n.includes(e))?`selfHosted`:`cloud`}function vi(e){if(!e||typeof e!=`object`)return{};let t=e.available;if(!t||typeof t!=`object`||Array.isArray(t))return{};let n={};for(let[e,r]of Object.entries(t))Array.isArray(r)&&(n[e]=r.filter(e=>typeof e==`string`));return n}function yi(e){if(!e||typeof e!=`object`)return{};let t=e.liveModelCounts;if(!t||typeof t!=`object`||Array.isArray(t))return{};let n={};for(let[e,r]of Object.entries(t))typeof r!=`number`||!Number.isFinite(r)||r<0||(n[e]=Math.floor(r));return n}function bi(e){if(!e||typeof e!=`object`)return{};let t=e.selected;if(!t||typeof t!=`object`||Array.isArray(t))return{};let n={};for(let[e,r]of Object.entries(t))Array.isArray(r)&&(n[e]=r.filter(e=>typeof e==`string`));return n}function xi(e){let t={};for(let[n,r]of Object.entries(vi(e)))t[n]=r.length;return t}function Si(e){return Object.entries(e).filter(e=>typeof e[1].requests==`number`&&e[1].requests>0).map(([e,t])=>({name:e,...t,requests:t.requests})).sort((e,t)=>t.requests-e.requests||e.name.localeCompare(t.name))}var Ci={justNow:`Just now`,notChecked:`Not checked`,minutesAgo:e=>`${e}m ago`,hoursAgo:e=>`${e}h ago`,daysAgo:e=>`${e}d ago`};function wi(e,t,n){let r=typeof t==`object`&&t?t:Ci,i=typeof t==`number`?t:n??Date.now();if(e===void 0||!Number.isFinite(e))return r.notChecked;let a=Math.max(0,i-e),o=Math.floor(a/6e4);if(o<1)return r.justNow;if(o<60)return r.minutesAgo(o);let s=Math.floor(o/60);return s<24?r.hoursAgo(s):r.daysAgo(Math.floor(s/24))}function Ti(e){return{justNow:e(`time.justNow`),notChecked:e(`time.notChecked`),minutesAgo:t=>e(`time.minutesAgo`,{n:t}),hoursAgo:t=>e(`time.hoursAgo`,{n:t}),daysAgo:t=>e(`time.daysAgo`,{n:t})}}function Ei(e,t){let n=[];for(let r of e.ready)r.activeNeedsReauth&&n.push({name:r.name,reason:t[r.name]??`Active account needs re-authentication`});for(let r of e.needsSetup){let e=r.activeNeedsReauth?t[r.name]??`Active account needs re-authentication`:t[r.name]??`Missing credentials`;n.push({name:r.name,reason:e})}for(let r of e.disabled){let e=t[r.name];e&&n.push({name:r.name,reason:e})}return n}function Di(e){return e===`Active account needs re-authentication`?`reauth`:e===`Missing credentials`?`missing`:`custom`}function Oi(e,t=`en`){if(e===void 0)return`—`;if(t.toLowerCase().slice(0,2)===`de`){let t=e=>e.replace(/\.0+$/,``).replace(`.`,`,`);return e>=1e9?`${t((e/1e9).toFixed(2))} Mrd.`:e>=1e6?`${t((e/1e6).toFixed(1))} Mio.`:e>=1e3?`${t((e/1e3).toFixed(1))} Tsd.`:String(e)}return e>=1e9?`${(e/1e9).toFixed(2).replace(/\.?0+$/,``)}B`:e>=1e6?`${(e/1e6).toFixed(1)}M`:e>=1e3?`${(e/1e3).toFixed(1)}k`:String(e)}function ki(e,t=`en`){return Oi(e,t)}function Ai(e,t=`en`){return e==null||!Number.isFinite(e)||e<0?`—`:`~$${new Intl.NumberFormat(t,{minimumFractionDigits:4,maximumFractionDigits:4}).format(e)}`}function ji(e,t){let n=pi(e);return n===`disabled`?t(`prov.disabledBadge`):n===`ready`?t(`pws.status.ready`):`activeNeedsReauth`in e&&e.activeNeedsReauth?t(`pws.status.needsAttention`):t(`pws.status.needsSetup`)}function Mi(e,t){switch(e.authMode){case`oauth`:return t(`modal.badge.oauth`);case`forward`:return t(`pws.auth.chatgptPassthrough`);case`local`:return t(`modal.badge.local`);case`key`:return t(`modal.badge.apiKey`);default:return e.authMode??(e.keyOptional?t(`pws.auth.noKey`):t(`modal.badge.apiKey`))}}function Ni(e){let t=pi(e);return t===`disabled`?`providers-workspace-rail-status providers-workspace-rail-status--inactive`:t===`ready`?`providers-workspace-rail-status providers-workspace-rail-status--active`:`providers-workspace-rail-status providers-workspace-rail-status--warning`}function Pi({name:e,adapter:t,baseUrl:n,cls:r}){let i=Q(),a=En(e,{adapter:t,baseUrl:n}),o=An(a),s=o===`plate`?`${r} provider-icon--plate`:o===`dark-plate`?`${r} provider-icon--plate-dark`:r;return(0,J.jsx)(`span`,{className:s,children:a&&o===`mask`?(0,J.jsx)(`span`,{className:`provider-icon-mask`,style:{maskImage:`url(${a})`,WebkitMaskImage:`url(${a})`},"aria-hidden":`true`}):a?(0,J.jsx)(`img`,{src:a,alt:``,"aria-hidden":`true`}):(0,J.jsx)(Fi,{name:e,label:jn(e,i)})})}function Fi({name:e,label:t}){let n=[...e].reduce((e,t)=>e+t.charCodeAt(0),0)%360,r=(t.trim()[0]??e[0]??`?`).toUpperCase();return(0,J.jsx)(`span`,{className:`provider-icon-fallback`,style:{background:`hsl(${n} 55% 90%)`,color:`hsl(${n} 65% 32%)`},"aria-hidden":`true`,children:r})}function Ii({item:e,selected:t,tabbable:n,modelCount:r,isDefault:i,showConfigId:a,onClick:o,onFocus:s}){let c=Q(),l=ci(e),u=hi(e),d=ji(e,c),f=jn(e.name,c),p=a?`${f} (${e.name})`:f,m=`${i?c(`pws.rail.suffixDefault`):``}${u?c(`pws.rail.suffixLocal`):l?c(`pws.rail.suffixFree`):``}`,h=r!==void 0&&r>0?r===1?c(`pws.modelCountOne`):c(`pws.modelCount`,{count:r}):``,g=[a?e.name:``,h].filter(Boolean).join(` · `);return(0,J.jsxs)(`button`,{type:`button`,className:`providers-workspace-rail-row${t?` providers-workspace-rail-row--selected`:``}`,onClick:o,role:`option`,"aria-selected":t,tabIndex:n?0:-1,"aria-label":c(`pws.rail.selectAria`,{name:p,status:d,suffix:m}),title:p,onFocus:s,children:[(0,J.jsx)(Pi,{name:e.name,adapter:e.adapter,baseUrl:e.baseUrl,cls:`providers-workspace-rail-icon`}),(0,J.jsxs)(`span`,{className:`providers-workspace-rail-copy`,children:[(0,J.jsxs)(`span`,{className:`providers-workspace-rail-primary`,children:[(0,J.jsx)(`span`,{className:`providers-workspace-rail-name-label`,title:f,children:f}),u?(0,J.jsx)(`span`,{className:`pwi-rail-badge pwi-rail-badge--local`,title:c(`pws.localTitle`),children:c(`modal.badge.local`)}):l?(0,J.jsx)(`span`,{className:`pwi-rail-badge pwi-rail-badge--free`,title:c(`pws.freeTitle`),children:c(`modal.badge.free`)}):null]}),(0,J.jsx)(`span`,{className:`providers-workspace-rail-secondary`,title:g||void 0,children:g||`\xA0`})]}),(0,J.jsxs)(`span`,{className:`providers-workspace-rail-trail`,children:[i&&(0,J.jsx)(`span`,{className:`pwi-default-star`,title:c(`prov.defaultBadge`),"aria-label":c(`prov.defaultBadge`),children:(0,J.jsx)(Ie,{width:17,height:17,"aria-hidden":`true`})}),(0,J.jsx)(`span`,{className:Ni(e),title:d,"aria-hidden":`true`})]})]})}var Li=e=>typeof e==`number`&&Number.isFinite(e)?e:void 0,Ri=e=>{let t=Li(e);if(t===void 0)return;let n=t>1e10?t:t*1e3;return Number.isFinite(new Date(n).getTime())?t:void 0};function zi(e,t){if(!e||typeof e!=`object`||Array.isArray(e))return null;let n=e,r=Array.isArray(n.customWindows)?n.customWindows.flatMap(e=>{if(!e||typeof e!=`object`)return[];let t=e;return typeof t.label!=`string`||Li(t.percent)===void 0?[]:[{label:t.label,percent:t.percent,...Li(t.resetAt)===void 0?{}:{resetAt:t.resetAt}}]}):[],i=n.creditsUsd&&typeof n.creditsUsd==`object`&&!Array.isArray(n.creditsUsd)?n.creditsUsd:null,a=Li(i?.used),o=Li(i?.limit),s=Li(i?.remaining),c=Li(i?.percent),l=Ri(i?.expiresAt),u=a!==void 0&&o!==void 0&&s!==void 0&&c!==void 0?{used:a,limit:o,remaining:s,percent:c,...l===void 0?{}:{expiresAt:l},...typeof i?.unlimited==`boolean`?{unlimited:i.unlimited}:{}}:void 0,d={...Li(n.fiveHourPercent)===void 0?{}:{fiveHourPercent:n.fiveHourPercent},...Li(n.fiveHourResetAt)===void 0?{}:{fiveHourResetAt:n.fiveHourResetAt},...Li(n.weeklyPercent)===void 0?{}:{weeklyPercent:n.weeklyPercent},...Li(n.weeklyResetAt)===void 0?{}:{weeklyResetAt:n.weeklyResetAt},...Li(n.monthlyPercent)===void 0?{}:{monthlyPercent:n.monthlyPercent},...Li(n.monthlyResetAt)===void 0?{}:{monthlyResetAt:n.monthlyResetAt},...r.length>0?{customWindows:r}:{},...u?{creditsUsd:u}:{},updatedAt:Li(n.updatedAt)??t??Date.now()};return d.fiveHourPercent!==void 0||d.weeklyPercent!==void 0||d.monthlyPercent!==void 0||(d.customWindows?.length??0)>0||d.creditsUsd!==void 0?d:null}function Bi(e){return zi(e?.quota,e?.updatedAt)}function Vi(e){if(!e||typeof e!=`object`||Array.isArray(e))return;let t=e,n=Li(t.usedPercent);if(n!==void 0)return{usedPercent:n,...typeof t.incomplete==`boolean`?{incomplete:t.incomplete}:{},...Li(t.excludedAccounts)===void 0?{}:{excludedAccounts:t.excludedAccounts},...Li(t.nextRecoveryAt)===void 0?{}:{nextRecoveryAt:t.nextRecoveryAt},...Li(t.nextRecoveryPercent)===void 0?{}:{nextRecoveryPercent:t.nextRecoveryPercent}}}function Hi(e){let t=e?.aggregation;if(!t||typeof t!=`object`||Array.isArray(t))return null;let n=t;if(n.kind!==`capacity-weighted-v1`||n.scope!==`routable-known`)return null;let r=Li(n.excludedAccounts),i=Li(n.unknownPlanAccounts);if(r===void 0||i===void 0||typeof n.incomplete!=`boolean`)return null;let a=n.currentAccount&&typeof n.currentAccount==`object`&&!Array.isArray(n.currentAccount)?n.currentAccount:null,o=Array.isArray(n.customWindows)?n.customWindows.flatMap(e=>{if(!e||typeof e!=`object`||Array.isArray(e))return[];let t=e,n=Vi(t);return typeof t.label==`string`&&n?[{label:t.label,...n}]:[]}):[],s=Vi(n.fiveHour),c=Vi(n.weekly),l=Vi(n.monthly),u=!!s||!!c||!!l||o.length>0;return{presentation:n.presentation===`aggregate`||n.presentation===`effective-account-fallback`||n.presentation===`coverage-only`?n.presentation:u?`aggregate`:`coverage-only`,incomplete:n.incomplete,excludedAccounts:r,unknownPlanAccounts:i,partialWindowAccounts:Li(n.partialWindowAccounts)??0,...s?{fiveHour:s}:{},...c?{weekly:c}:{},...l?{monthly:l}:{},...o.length>0?{customWindows:o}:{},...a?{currentAccount:{...typeof a.plan==`string`||a.plan===null?{plan:a.plan}:{},quota:zi(a.quota)}}:{}}}function Ui(e){if(!e?.trim())return``;let[t,n]=e.split(`:`,2);return n?`${t} · ${n.replace(/-/g,` `)}`:e}function Wi(e,t,n,r,i,a){let o=r&&r.length>0?r:t?[t]:[],s=[...new Set([...a?e:o,...i])],c=n.trim().toLowerCase();return c?s.filter(e=>e.toLowerCase().includes(c)):s}function Gi(e){let t=e?.trim().toLowerCase();return t===`go`||t===`free`}function Ki(e,t){if(!e)return null;let n=e.shortPercent===void 0&&e.shortResetAt===void 0?e:{...e,fiveHourPercent:e.fiveHourPercent??e.shortPercent,fiveHourResetAt:e.fiveHourResetAt??e.shortResetAt};return Gi(t)?{...n.monthlyPercent===void 0?{}:{monthlyPercent:n.monthlyPercent},...n.monthlyResetAt===void 0?{}:{monthlyResetAt:n.monthlyResetAt},...n.creditsUsd===void 0?{}:{creditsUsd:n.creditsUsd},...n.resetCredits===void 0?{}:{resetCredits:n.resetCredits},updatedAt:n.updatedAt}:n}function qi(e){return e===`5h`?0:e===`First-party models`?2:e===`API usage`?3:5}function Ji(e,t){switch(e){case`First-party models`:return t(`quota.cursorFirstParty`);case`API usage`:return t(`quota.cursorApiUsage`);case`Total subscription credits`:return t(`quota.totalSubscriptionCredits`);default:return e}}function Yi(e,t,n){let r=Ki(e,t);if(!r)return[];let i=[];typeof r.fiveHourPercent==`number`&&i.push({rank:0,row:{windowKey:`fiveHour`,label:n(`codexAuth.fiveHour`),limitLabel:n(`quota.fiveHourLimit`),percent:r.fiveHourPercent,resetAt:r.fiveHourResetAt}}),typeof r.weeklyPercent==`number`&&i.push({rank:1,row:{windowKey:`weekly`,label:n(`codexAuth.weekly`),limitLabel:n(`quota.weeklyLimit`),percent:r.weeklyPercent,resetAt:r.weeklyResetAt}}),typeof r.monthlyPercent==`number`&&i.push({rank:4,row:{windowKey:`monthly`,label:n(`codexAuth.monthly`),limitLabel:n(`quota.monthlyLimit`),percent:r.monthlyPercent,resetAt:r.monthlyResetAt}});for(let e of r.customWindows??[]){let t=Ji(e.label,n);i.push({rank:qi(e.label),row:{customLabel:e.label,label:t,limitLabel:t,percent:e.percent,resetAt:e.resetAt}})}return i.sort((e,t)=>e.rank-t.rank).map(e=>e.row)}function Xi(e){if(!e)return-1;let t=[e.fiveHourPercent,e.weeklyPercent,e.monthlyPercent].filter(e=>typeof e==`number`);for(let n of e.customWindows??[])typeof n.percent==`number`&&t.push(n.percent);return t.length?Math.max(...t):-1}function Zi(e){switch(e){case`en`:return`en-GB`;case`de`:return`de-DE`;case`fr`:return`fr-FR`;case`ko`:return`ko-KR`;case`zh`:return`zh-CN`;case`zh-TW`:return`zh-TW`;case`ru`:return`ru-RU`;case`ja`:return`ja-JP`;case`tr`:return`tr-TR`;default:return e}}function Qi(e){return e>=99.5}function $i(e,t){return t>0&&e>=t}function ea(e,t){return $i(e,t)||Qi(e)?`bar-warn`:`bar-green`}function ta(e){let t=Math.max(0,Math.min(100,e));return t<=0?0:Math.max(4,Math.round(t))}function na(e){return{"--bar-scale":String(ta(e)/100)}}function ra(e,t,n=Date.now()){let r=n-e;return!Number.isFinite(r)||r<6e4?null:r<36e5?t(`quota.ageMinutes`).replace(`{n}`,String(Math.floor(r/6e4))):r<864e5?t(`quota.ageHours`).replace(`{n}`,String(Math.floor(r/36e5))):t(`quota.ageDays`).replace(`{n}`,String(Math.floor(r/864e5)))}function ia({quota:e,plan:t,threshold:n,t:r,className:i,layout:a=`compact`,pending:o=!1,incompleteWindowKeys:s,incompleteCustomWindowLabels:c,observedAt:l}){let{locale:u}=ct(),d=Yi(e,t,r),f=l===void 0?null:ra(l,r),p=f===null?null:(0,J.jsx)(`p`,{className:`quota-observed muted`,title:r(`quota.observedHint`),children:r(`quota.observedAgo`).replace(`{age}`,f)});return d.length===0?o?a===`stacked`?(0,J.jsxs)(`div`,{className:`quota-stacked quota-stacked--pending${i?` ${i}`:``}`,"aria-busy":`true`,role:`status`,children:[Array.from({length:2},(e,t)=>(0,J.jsxs)(`div`,{className:`quota-stacked-row quota-stacked-row--skeleton`,"aria-hidden":`true`,children:[(0,J.jsxs)(`div`,{className:`quota-stacked-head`,children:[(0,J.jsx)(`span`,{className:`quota-skel quota-skel--label`,style:{width:72}}),(0,J.jsx)(`span`,{className:`quota-skel quota-skel--time`,style:{width:64}})]}),(0,J.jsxs)(`div`,{className:`quota-stacked-bar-row`,children:[(0,J.jsx)(`span`,{className:`quota-skel quota-skel--bar`,style:{height:6,flex:1}}),(0,J.jsx)(`span`,{className:`quota-skel quota-skel--val`,style:{width:36}})]})]},t)),(0,J.jsx)(`span`,{className:`sr-only`,children:r(`common.loading`)})]}):(0,J.jsxs)(`div`,{className:`codex-account-quota-slot quota-compact quota-compact--pending${i?` ${i}`:``}`,"aria-busy":`true`,role:`status`,children:[(0,J.jsxs)(`div`,{className:`quota-row quota-row--skeleton`,"aria-hidden":`true`,children:[(0,J.jsx)(`span`,{className:`quota-skel quota-skel--label`}),(0,J.jsx)(`span`,{className:`quota-skel quota-skel--reset`}),(0,J.jsx)(`span`,{className:`quota-skel quota-skel--day`}),(0,J.jsx)(`span`,{className:`quota-skel quota-skel--time`}),(0,J.jsx)(`span`,{className:`quota-skel quota-skel--bar`}),(0,J.jsx)(`span`,{className:`quota-skel quota-skel--val`})]}),(0,J.jsx)(`span`,{className:`sr-only`,children:r(`common.loading`)})]}):null:a===`stacked`?(0,J.jsxs)(`div`,{className:`quota-stacked${i?` ${i}`:``}`,children:[p,d.map(e=>(0,J.jsx)(oa,{row:e,threshold:n,t:r,locale:u,incomplete:e.windowKey?s?.has(e.windowKey)===!0:e.customLabel!==void 0&&c?.has(e.customLabel)===!0},e.limitLabel))]}):(0,J.jsxs)(`div`,{className:`codex-account-quota-slot quota-compact${i?` ${i}`:``}`,children:[p,d.map(e=>(0,J.jsx)(aa,{label:e.label,percent:e.percent,resetAt:e.resetAt,threshold:n,t:r,locale:u},e.label))]})}function aa({label:e,percent:t,resetAt:n,threshold:r,t:i,locale:a}){let o=Qi(t),s=$i(t,r),c=ea(t,r),l=ca(n,i,a),u=l.day||l.time?`${i(`codexAuth.resets`)} ${l.day} ${l.time}`.replace(/\s+/g,` `).trim():void 0,d=l.day!==``||l.time!==``;return(0,J.jsxs)(`div`,{className:`quota-row${s?` quota-row--warn`:``}${o?` quota-row--exhausted`:``}`,children:[(0,J.jsx)(`span`,{className:`quota-label`,title:u,children:e}),(0,J.jsx)(`span`,{className:`quota-reset-label`,children:d?i(`codexAuth.resets`):``}),(0,J.jsx)(`span`,{className:`quota-reset-day`,children:l.day}),(0,J.jsx)(`span`,{className:`quota-reset-time`,children:l.time}),(0,J.jsx)(`div`,{className:`bar`,title:u,children:(0,J.jsx)(`div`,{className:`bar-fill ${c}`,style:na(t)})}),(0,J.jsxs)(`span`,{className:`quota-val${s?` quota-val--warn`:``}`,title:o?i(`quota.limitReached`):u,"aria-label":u,children:[s&&(0,J.jsx)(_e,{width:12,height:12,"aria-hidden":`true`}),Math.round(t),`%`,o?` · ${i(`quota.limitReached`)}`:``]})]})}function oa({row:e,threshold:t,t:n,locale:r,incomplete:i}){let a=Qi(e.percent),o=$i(e.percent,t),s=ea(e.percent,t),c=la(e.resetAt,n,r);return(0,J.jsxs)(`div`,{className:`quota-stacked-row${o?` quota-stacked-row--warn`:``}${a?` quota-stacked-row--exhausted`:``}`,children:[(0,J.jsxs)(`div`,{className:`quota-stacked-head`,children:[(0,J.jsxs)(`span`,{className:`quota-stacked-limit-group`,children:[(0,J.jsx)(`span`,{className:`quota-stacked-limit`,children:e.limitLabel}),i&&(0,J.jsx)(`span`,{className:`quota-window-partial`,role:`note`,"aria-label":n(`pws.capacity.windowPartialA11y`,{window:e.limitLabel}),title:n(`pws.capacity.windowPartialA11y`,{window:e.limitLabel}),children:n(`pws.capacity.windowPartial`)})]}),(0,J.jsx)(`span`,{className:`quota-stacked-reset muted`,children:c})]}),(0,J.jsxs)(`div`,{className:`quota-stacked-bar-row`,children:[(0,J.jsx)(`div`,{className:`bar quota-stacked-bar`,children:(0,J.jsx)(`div`,{className:`bar-fill ${s}`,style:na(e.percent)})}),(0,J.jsx)(`span`,{className:`quota-stacked-used${o?` quota-stacked-used--warn`:``}`,children:n(`quota.usedPercent`,{pct:Math.round(e.percent)})})]}),a&&(0,J.jsxs)(`div`,{className:`quota-stacked-limit-reached`,role:`status`,children:[(0,J.jsx)(_e,{width:12,height:12,"aria-hidden":`true`}),n(`quota.limitReached`)]})]})}function sa(e){if(typeof e!=`number`||!Number.isFinite(e))return null;let t=e<1e10?e*1e3:e,n=new Date(t);return Number.isFinite(n.getTime())?{date:n,ms:t}:null}function ca(e,t,n){let r=sa(e);if(!r)return{day:``,time:``};let{date:i}=r,a=new Date,o=Zi(n),s=new Intl.DateTimeFormat(o,{hour:`2-digit`,minute:`2-digit`,hour12:!1}).format(i);return i.getFullYear()===a.getFullYear()&&i.getMonth()===a.getMonth()&&i.getDate()===a.getDate()?{day:t(`codexAuth.today`),time:s}:{day:new Intl.DateTimeFormat(o,{day:`numeric`,month:`short`}).format(i),time:s}}function la(e,t,n=`en`,r=Date.now()){let i=sa(e);if(!i)return``;let{date:a,ms:o}=i,s=Zi(n),c=new Intl.DateTimeFormat(s,{hour:`2-digit`,minute:`2-digit`,hour12:!1}).format(a),l=new Date(r),u=new Date(l.getFullYear(),l.getMonth(),l.getDate()).getTime(),d=new Date(a.getFullYear(),a.getMonth(),a.getDate()).getTime(),f=Math.round((d-u)/864e5);if(f===1)return t(`quota.resetsTomorrow`,{time:c});let p=a.getFullYear()!==l.getFullYear(),m=new Intl.DateTimeFormat(s,{day:`numeric`,month:`short`,...p?{year:`numeric`}:{}}).format(a);if(o<=r)return t(`quota.resetsAt`,{date:m,time:c,when:`${m}, ${c}`});let h=Math.round((o-r)/6e4);if(h<60)return t(`quota.resetsRelativeMinutes`,{n:Math.max(1,h)});let g=Math.round(h/60);return g<12&&f===0?t(`quota.resetsRelativeHours`,{n:Math.max(1,g)}):f===0?t(`quota.resetsToday`,{time:c}):t(`quota.resetsAt`,{date:m,time:c,when:`${m}, ${c}`})}function ua(e){switch(e){case`en`:return`en-GB`;case`de`:return`de-DE`;case`fr`:return`fr-FR`;case`ko`:return`ko-KR`;case`zh`:return`zh-CN`;case`zh-TW`:return`zh-TW`;case`ru`:return`ru-RU`;case`ja`:return`ja-JP`;case`tr`:return`tr-TR`;default:return e}}function da(e){let t=new Date(e>1e10?e:e*1e3);return Number.isFinite(t.getTime())?t:null}function fa({report:e,pending:t}){let n=Q(),{locale:r}=ct(),i=Hi(e),a=Bi(e),o=a?.creditsUsd,s=i?.presentation===`aggregate`,c=new Set,l=new Set;if(s&&i){i.fiveHour?.incomplete&&c.add(`fiveHour`),i.weekly?.incomplete&&c.add(`weekly`),i.monthly?.incomplete&&c.add(`monthly`);for(let e of i.customWindows??[])e.incomplete&&l.add(e.label)}let u=s&&i?[...i.fiveHour?[{key:0,label:n(`codexAuth.fiveHour`),window:i.fiveHour}]:[],...i.weekly?[{key:1,label:n(`codexAuth.weekly`),window:i.weekly}]:[],...i.monthly?[{key:2,label:n(`codexAuth.monthly`),window:i.monthly}]:[],...(i.customWindows??[]).map((e,t)=>({key:t+3,label:e.label,window:e}))]:[],d=e=>new Intl.NumberFormat(r,{maximumFractionDigits:1}).format(e),f=e=>{let t=da(e);return t===null?null:new Intl.DateTimeFormat(r,{dateStyle:`medium`,timeStyle:`short`}).format(t)},p=ua(r),m=e=>new Intl.NumberFormat(p,{style:`currency`,currency:`USD`}).format(e),h=o?.expiresAt===void 0?null:(e=>{let t=da(e);return t===null?null:new Intl.DateTimeFormat(p,{dateStyle:`medium`}).format(t)})(o.expiresAt);return(0,J.jsxs)(J.Fragment,{children:[s&&(0,J.jsx)(`div`,{className:`pws-capacity-label`,children:n(`pws.capacity.estimate`)}),(a||t)&&(0,J.jsx)(ia,{quota:a,threshold:80,t:n,layout:`stacked`,pending:t,incompleteWindowKeys:s?c:void 0,incompleteCustomWindowLabels:s?l:void 0}),(o||i)&&(0,J.jsxs)(`div`,{className:`pws-capacity-details`,children:[o&&(0,J.jsxs)(`div`,{className:`pws-capacity-recovery`,children:[(0,J.jsx)(`span`,{children:n(`quota.creditsBalance`)}),(0,J.jsx)(`strong`,{children:m(o.remaining)})]}),h!==null&&(0,J.jsx)(`div`,{className:`pws-capacity-recovery`,children:(0,J.jsx)(`span`,{children:n(`quota.creditsPeriodEnds`,{date:h})})}),u.flatMap(({key:e,label:t,window:r})=>{let i=r.nextRecoveryAt===void 0?null:f(r.nextRecoveryAt);return i!==null&&r.nextRecoveryPercent!==void 0?[(0,J.jsxs)(`div`,{className:`pws-capacity-recovery`,children:[(0,J.jsxs)(`span`,{children:[n(`pws.capacity.nextRecovery`),` · `,t,` · `,i]}),(0,J.jsx)(`strong`,{children:n(`pws.capacity.recoveryShare`,{percent:d(r.nextRecoveryPercent)})})]},e)]:[]}),s&&i&&i.currentAccount?.quota&&(0,J.jsxs)(`div`,{className:`pws-capacity-current`,children:[(0,J.jsxs)(`span`,{className:`pws-capacity-label`,children:[n(`pws.capacity.currentAccount`),i.currentAccount.plan?` · ${i.currentAccount.plan}`:``]}),(0,J.jsx)(ia,{quota:i.currentAccount.quota,threshold:80,t:n,layout:`stacked`})]}),i&&i.incomplete&&i.excludedAccounts>0&&(0,J.jsx)(`div`,{className:`pws-capacity-incomplete`,children:n(`pws.capacity.incomplete`,{excluded:i.excludedAccounts})}),i&&i.unknownPlanAccounts>0&&(0,J.jsx)(`div`,{className:`pws-capacity-incomplete`,children:n(`pws.capacity.uncalibratedPlan`,{count:i.unknownPlanAccounts})}),i&&i.partialWindowAccounts>0&&(0,J.jsx)(`div`,{className:`pws-capacity-incomplete`,children:n(`pws.capacity.partial`,{count:i.partialWindowAccounts})})]})]})}function pa({sections:e,quotaReports:t,usageTotals:n,usageLoading:r=!1,quotasLoading:i=!1,onSelectProvider:a,onEditConfig:o}){let s=Q(),{locale:c}=ct(),l=Ti(s),u=(0,_.useMemo)(()=>[...e.ready,...e.needsSetup,...e.disabled],[e]),d=(0,_.useMemo)(()=>new Set(u.map(e=>e.name)),[u]),f=(0,_.useMemo)(()=>Ei(e,{}),[e]),p=f.length,m=(0,_.useMemo)(()=>e.ready.filter(e=>e.activeNeedsReauth).length,[e]),h=e.ready.length-m,g=e.needsSetup.length+m,v=(0,_.useMemo)(()=>{let e=[];for(let n of u){let r=t[n.name],i=r?Bi(r):null,a=r?Hi(r):null;r&&(i||a?.presentation===`coverage-only`)&&e.push({item:n,report:r,urgency:i?Xi(i):-1})}return e.sort((e,t)=>t.urgency-e.urgency||e.item.name.localeCompare(t.item.name))},[u,t]),y=(0,_.useMemo)(()=>{let e={};for(let[t,r]of Object.entries(n))d.has(t)&&(e[t]=r);return Si(e).slice(0,4)},[n,d]),b=e=>{let t=Di(e);return t===`reauth`?s(`pws.attention.reauth`):t===`missing`?s(`pws.attention.missingCredentials`):e};return(0,J.jsxs)(`div`,{className:`pws-dashboard`,children:[(0,J.jsxs)(`div`,{className:`pws-dashboard-header`,children:[(0,J.jsx)(`div`,{className:`pws-dashboard-header-text`,children:(0,J.jsx)(`h2`,{className:`pws-dashboard-title`,children:s(`pws.dashboard.title`)})}),o&&(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:o,children:s(`prov.editJson`)})]}),(0,J.jsxs)(`div`,{className:`pws-dashboard-summary`,children:[(0,J.jsx)(ma,{count:h,label:s(`pws.status.ready`),tone:`ok`}),(0,J.jsx)(ma,{count:g,label:s(m>0?`pws.status.needsAttention`:`pws.status.needsSetup`),tone:`warn`}),(0,J.jsx)(ma,{count:e.disabled.length,label:s(`prov.disabledBadge`),tone:`muted`})]}),p>0&&(0,J.jsxs)(`section`,{className:`pws-dashboard-section pws-dashboard-attention`,"aria-label":s(`pws.attentionTitle`),children:[(0,J.jsxs)(`h3`,{className:`pws-dashboard-section-title`,children:[(0,J.jsx)(_e,{style:{width:14,height:14},"aria-hidden":`true`}),s(`pws.attentionTitle`)]}),(0,J.jsx)(`div`,{className:`pws-dashboard-rows`,children:f.map(e=>(0,J.jsxs)(`button`,{type:`button`,className:`pws-dashboard-row pws-dashboard-row--attention`,onClick:()=>a(e.name),children:[(0,J.jsx)(Pi,{name:e.name,adapter:``,baseUrl:``,cls:`pws-dashboard-row-icon`}),(0,J.jsxs)(`div`,{className:`pws-dashboard-row-info`,children:[(0,J.jsx)(`span`,{className:`pws-dashboard-row-name`,children:jn(e.name,s)}),(0,J.jsx)(`span`,{className:`pws-dashboard-row-meta muted`,children:b(e.reason)})]}),(0,J.jsx)(Se,{className:`pws-dashboard-row-chevron`,"aria-hidden":`true`})]},`${e.name}:${e.reason}`))})]}),(0,J.jsxs)(`div`,{className:`pws-dashboard-columns`,children:[(0,J.jsxs)(`section`,{className:`pws-dashboard-section pws-dashboard-section--rate-limits`,"aria-label":s(`pws.dashboard.rateLimits`),"aria-busy":i||void 0,children:[(0,J.jsx)(`h3`,{className:`pws-dashboard-section-title`,children:s(`pws.dashboard.rateLimits`)}),v.length>0?(0,J.jsx)(`div`,{className:`pws-dashboard-rows`,children:v.map(({item:e,report:t})=>(0,J.jsxs)(`button`,{type:`button`,className:`pws-dashboard-row`,onClick:()=>a(e.name),children:[(0,J.jsx)(Pi,{name:e.name,adapter:e.adapter,baseUrl:e.baseUrl,cls:`pws-dashboard-row-icon`}),(0,J.jsxs)(`div`,{className:`pws-dashboard-row-info`,children:[(0,J.jsx)(`span`,{className:`pws-dashboard-row-name`,children:jn(e.name,s)}),(0,J.jsx)(`span`,{className:`pws-dashboard-row-meta muted`,children:s(`pws.dashboard.checkedAgo`,{time:wi(t.updatedAt,l)})})]}),(0,J.jsx)(Se,{className:`pws-dashboard-row-chevron`,"aria-hidden":`true`}),(0,J.jsx)(`div`,{className:`pws-dashboard-row-bars`,children:(0,J.jsx)(fa,{report:t,pending:i&&!t.quota})})]},e.name))}):i?(0,J.jsx)(`div`,{className:`pws-dashboard-rows pws-dashboard-rows--pending`,"aria-hidden":`true`,children:Array.from({length:3},(e,t)=>(0,J.jsxs)(`div`,{className:`pws-dashboard-row pws-dashboard-row--skeleton`,children:[(0,J.jsx)(`span`,{className:`pws-dashboard-row-icon pws-skel`}),(0,J.jsxs)(`div`,{className:`pws-dashboard-row-info`,children:[(0,J.jsx)(`span`,{className:`pws-skel pws-skel--name`}),(0,J.jsx)(`span`,{className:`pws-skel pws-skel--meta`})]}),(0,J.jsx)(`div`,{className:`pws-dashboard-row-bars`,children:(0,J.jsx)(ia,{quota:null,threshold:80,t:s,layout:`stacked`,pending:!0})})]},t))}):(0,J.jsx)(`p`,{className:`muted pws-dashboard-empty`,children:s(`pws.dashboard.noRateLimits`)})]}),(0,J.jsx)(`section`,{className:`pws-dashboard-section pws-dashboard-section--recent`,"aria-label":s(`pws.dashboard.recentlyUsed`),"aria-busy":r||void 0,children:(0,J.jsxs)(`details`,{className:`pws-dashboard-recent-details`,children:[(0,J.jsx)(`summary`,{className:`pws-dashboard-section-title`,children:s(`pws.dashboard.recentlyUsed`)}),y.length>0?(0,J.jsx)(`div`,{className:`pws-dashboard-rows`,children:y.map(e=>(0,J.jsxs)(`button`,{type:`button`,className:`pws-dashboard-row`,onClick:()=>a(e.name),children:[(0,J.jsx)(Pi,{name:e.name,adapter:``,baseUrl:``,cls:`pws-dashboard-row-icon`}),(0,J.jsx)(`span`,{className:`pws-dashboard-row-name`,children:jn(e.name,s)}),(0,J.jsx)(`span`,{className:`pws-dashboard-row-count muted`,children:s(`pws.dashboard.requests`,{count:Oi(e.requests,c)})}),(0,J.jsx)(Se,{className:`pws-dashboard-row-chevron`,"aria-hidden":`true`})]},e.name))}):r?(0,J.jsx)(`div`,{className:`pws-dashboard-rows pws-dashboard-rows--pending`,"aria-hidden":`true`,children:Array.from({length:3},(e,t)=>(0,J.jsxs)(`div`,{className:`pws-dashboard-row pws-dashboard-row--skeleton`,children:[(0,J.jsx)(`span`,{className:`pws-dashboard-row-icon pws-skel`}),(0,J.jsx)(`span`,{className:`pws-skel pws-skel--name`}),(0,J.jsx)(`span`,{className:`pws-skel pws-skel--count`})]},t))}):(0,J.jsx)(`p`,{className:`muted pws-dashboard-empty`,children:s(`pws.dashboard.noUsage`)})]})})]})]})}function ma({count:e,label:t,tone:n}){return(0,J.jsxs)(`div`,{className:`pws-dashboard-card pws-dashboard-card--${n}`,children:[(0,J.jsx)(`span`,{className:`pws-dashboard-card-count`,children:e}),(0,J.jsx)(`span`,{className:`pws-dashboard-card-label`,children:t})]})}function ha({editor:e,providerName:t,saving:n,onSave:r,message:i}){let a=Q(),o=(0,_.useRef)(null);return(0,_.useEffect)(()=>{e.open&&o.current?.focus()},[e.open]),e.open?(0,J.jsxs)(`div`,{className:`pwi-json-panel`,children:[(0,J.jsxs)(`div`,{className:`pwi-json-panel-header`,children:[(0,J.jsx)(`span`,{className:`pwi-json-panel-title`,children:a(`pws.jsonEditorTitle`,{name:t})}),(0,J.jsxs)(`div`,{className:`pwi-json-panel-actions`,children:[e.onRestore&&e.isDirty&&(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:e.onRestore,children:a(`pws.jsonRestore`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:e.onClose,children:a(`common.cancel`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-primary btn-sm`,onClick:r,disabled:n||!e.isDirty,children:a(n?`pws.saving`:`pws.jsonSave`)})]})]}),(0,J.jsx)(`p`,{className:`pwi-json-panel-desc muted`,children:a(`pws.jsonEditorDesc`)}),(0,J.jsx)(`textarea`,{ref:o,className:`input pwi-json-textarea`,value:e.draft,onChange:t=>e.onDraftChange(t.target.value),spellCheck:!1,rows:20,"aria-label":a(`pws.jsonEditorDesc`)}),i&&(0,J.jsx)(`div`,{className:i.ok?`pwi-settings-msg pwi-settings-msg--ok`:`pwi-settings-msg pwi-settings-msg--err`,children:i.text})]}):null}var ga=[{id:`az`,labelKey:`pws.sort.az`},{id:`za`,labelKey:`pws.sort.za`},{id:`free-paid`,labelKey:`pws.sort.freePaid`},{id:`paid-free`,labelKey:`pws.sort.paidFree`},{id:`accounts-first`,labelKey:`pws.sort.accountsFirst`}],_a=18e5;function va(e,t){if(!e||typeof e!=`object`||Array.isArray(e))return null;let n=e;return typeof n.updatedAt!=`number`||!Number.isFinite(n.updatedAt)||t-n.updatedAt>=_a||!(`quota`in n)||n.label!==void 0&&typeof n.label!=`string`||n.source!==void 0&&typeof n.source!=`string`?null:{...typeof n.label==`string`?{label:n.label}:{},...typeof n.source==`string`?{source:n.source}:{},updatedAt:n.updatedAt,quota:n.quota,...n.aggregation===void 0?{}:{aggregation:n.aggregation}}}function ya(e,t=Date.now()){if(!e||typeof e!=`object`||Array.isArray(e))return null;let n={};for(let[r,i]of Object.entries(e)){let e=va(i,t);r.trim()&&e&&(n[r]=e)}return n}function ba(e){return ya(gr(e))}function xa(e,t=Date.now()){if(!Array.isArray(e))return{};let n={};for(let r of e){if(!r||typeof r!=`object`||Array.isArray(r))continue;let e=r.provider,i=va(r,t);typeof e==`string`&&e.trim()&&i&&(n[e]=i)}return n}function Sa({providers:e,apiBase:t,defaultProvider:n,selectedName:r,onSelect:i,onRemoveProvider:a,onAddProvider:o,onEditConfig:s,jsonEditor:c,jsonSaving:l=!1,modelsRefreshToken:u=0,activeAccountNeedsReauth:d,quotaRefreshEpoch:f=0,quotaForceRefresh:p=!1,detail:m}){let h=Q(),[g,v]=(0,_.useState)(``),[y,b]=(0,_.useState)({ready:!0,needsSetup:!0,disabled:!0}),[x,S]=(0,_.useState)({free:!0,paid:!0}),[C,w]=(0,_.useState)({cloud:!0,local:!0,selfHosted:!0,login:!0}),[T,E]=(0,_.useState)(`az`),[D,O]=(0,_.useState)(!1),[k,A]=(0,_.useState)(null),[j,M]=(0,_.useState)({}),[N,P]=(0,_.useState)({}),[F,I]=(0,_.useState)({}),[L,R]=(0,_.useState)({}),[z,B]=(0,_.useState)(!1),[V,H]=(0,_.useState)(!1),U=`ocx.providers.quotas.v1:${t}`,W=`ocx.providers.usage.v1:${t}`,[ee,K]=(0,_.useState)(()=>gr(W)?.totals??{}),[q,Y]=(0,_.useState)(()=>gr(W)?.models??{}),[te,ne]=(0,_.useState)(()=>ba(U)??{}),[re,ie]=(0,_.useState)(()=>!gr(W)),[ae,oe]=(0,_.useState)(()=>{let e=ba(U);return!e||Object.keys(e).length===0}),[se,ce]=(0,_.useState)(0),le=(0,_.useRef)(null),ue=G(Br(t),[t],async e=>{let n=await fetch(t+`/api/usage?range=30d`,{signal:e});if(!n.ok)throw Error(String(n.status));return await n.json()},{deadlineMs:6e4}),de=(0,_.useMemo)(()=>fi(di(mi(e)),d??{}),[e,d]),fe=(0,_.useCallback)(()=>{ce(e=>e+1)},[]);(0,_.useEffect)(()=>{let e=!1,n=window.setTimeout(()=>{B(!0),(async()=>{try{let n=await Pt(await fetch(`${t}/api/selected-models`));if(e)return;M(xi(n)),P(vi(n)),I(yi(n)),R(bi(n)),H(!1)}catch{if(e)return;H(!0)}finally{e||B(!1)}})()},0);return()=>{e=!0,window.clearTimeout(n)}},[t,u,se]),(0,_.useEffect)(()=>{let e=!1,t=window.setTimeout(()=>{let t=ue.data;if(e)return;if(!t){ue.loading&&ie(!gr(W));return}let n={};for(let e of t.providers??[])n[e.provider]={requests:e.requests,totalTokens:e.totalTokens};K(n);let r={};for(let e of t.models??[]){let t=e.provider;r[t]||(r[t]=[]),r[t].push({model:e.model,...e.resolvedModel?{resolvedModel:e.resolvedModel}:{},requests:e.requests,totalTokens:e.totalTokens,inputTokens:e.inputTokens,outputTokens:e.outputTokens,shareRatio:e.shareRatio,...e.estimatedCostUsd===void 0?{}:{estimatedCostUsd:e.estimatedCostUsd}})}Y(r),br(W,{totals:n,models:r}),ie(!1)},0);return()=>{e=!0,window.clearTimeout(t)}},[t,W,ue.data,ue.loading]),(0,_.useEffect)(()=>{let e=!1,n=window.setTimeout(()=>{let n=ba(U);(!n||Object.keys(n).length===0)&&oe(!0),fetch(`${t}/api/provider-quotas${p?`?refresh=1`:``}`).then(e=>Ft(e)).then(t=>{if(e||!t)return;let n=xa(t.reports);ne(n),br(U,n)}).catch(()=>{e||ne(e=>{let t=ya(e)??{};return br(U,t),t})}).finally(()=>{e||oe(!1)})},0);return()=>{e=!0,window.clearTimeout(n)}},[t,f,p,U]),(0,_.useEffect)(()=>{if(!D)return;let e=e=>{le.current&&!le.current.contains(e.target)&&O(!1)},t=e=>{e.key===`Escape`&&O(!1)};return document.addEventListener(`mousedown`,e),window.addEventListener(`keydown`,t),()=>{document.removeEventListener(`mousedown`,e),window.removeEventListener(`keydown`,t)}},[D]);let pe=(0,_.useMemo)(()=>[...de.ready,...de.needsSetup,...de.disabled],[de]),X=(0,_.useMemo)(()=>pe.filter(ci).length,[pe]),me=pe.length-X,ge=(0,_.useMemo)(()=>{let e={cloud:0,local:0,selfHosted:0,login:0};for(let t of pe)e[_i(t)]+=1;return e},[pe]),_e=(0,_.useMemo)(()=>{let e=g.trim().toLowerCase(),t=t=>ui(t.filter(t=>{if(e&&!t.name.toLowerCase().includes(e)&&!t.adapter.toLowerCase().includes(e))return!1;let n=ci(t);return!(n&&!x.free||!n&&!x.paid||!C[_i(t)])}),T);return{ready:y.ready?t(de.ready):[],needsSetup:y.needsSetup?t(de.needsSetup):[],disabled:y.disabled?t(de.disabled):[]}},[de,g,y,x,C,T]),Z=!y.ready||!y.needsSetup||!y.disabled||!x.free||!x.paid||!C.cloud||!C.local||!C.selfHosted||!C.login||T!==`az`,ye=()=>{b({ready:!0,needsSetup:!0,disabled:!0}),S({free:!0,paid:!0}),w({cloud:!0,local:!0,selfHosted:!0,login:!0}),E(`az`)},be=(0,_.useMemo)(()=>r?pe.find(e=>e.name===r)??null:null,[r,pe]),xe=(0,_.useMemo)(()=>{let e=new Map;for(let t of pe){let n=jn(t.name,h);e.set(n,(e.get(n)??0)+1)}let t=new Set;for(let[n,r]of e.entries())r>1&&t.add(n);return t},[pe,h]);if(pe.length===0)return(0,J.jsx)(Ca,{onAddProvider:o});let Se=[{key:`ready`,label:h(`pws.status.ready`),count:de.ready.length},{key:`needsSetup`,label:h(`pws.status.needsSetup`),count:de.needsSetup.length},{key:`disabled`,label:h(`prov.disabledBadge`),count:de.disabled.length}],Ce=[{id:`ready`,label:h(`pws.status.ready`),count:_e.ready.length,ariaLabel:h(`pws.groupReady`,{count:_e.ready.length}),items:_e.ready},{id:`needs-setup`,label:h(`pws.status.needsSetup`),count:_e.needsSetup.length,ariaLabel:h(`pws.groupNeedsSetup`,{count:_e.needsSetup.length}),items:_e.needsSetup},{id:`disabled`,label:h(`prov.disabledBadge`),count:_e.disabled.length,ariaLabel:h(`pws.groupDisabled`,{count:_e.disabled.length}),items:_e.disabled}],we=Ce.flatMap(e=>e.items.map(e=>e.name)),Te=k&&we.includes(k)?k:r&&we.includes(r)?r:we[0]??null;return(0,J.jsx)(`div`,{className:`pws-shell-container`,children:(0,J.jsxs)(`div`,{className:`pws-root`,children:[(0,J.jsxs)(`aside`,{className:`pws-rail`,"aria-label":h(`pws.providerList`),children:[(0,J.jsxs)(`div`,{className:`pws-search-row`,children:[(0,J.jsxs)(`div`,{className:`pws-search-wrap`,children:[(0,J.jsx)(ve,{className:`pws-search-icon`,width:14,height:14,"aria-hidden":`true`}),(0,J.jsx)(`input`,{type:`search`,className:`input pws-search-input`,placeholder:h(`pws.searchPlaceholder`),value:g,onChange:e=>v(e.target.value),"aria-label":h(`pws.searchPlaceholder`)})]}),(0,J.jsxs)(`div`,{className:`pws-filter-wrap`,ref:le,children:[(0,J.jsxs)(`button`,{type:`button`,className:`pws-filter-btn${Z||D?` pws-filter-btn--active`:``}`,onClick:()=>O(e=>!e),"aria-label":h(`pws.filterAria`),"aria-expanded":D,"aria-controls":`pws-provider-filters`,children:[(0,J.jsx)(Le,{width:18,height:18,"aria-hidden":`true`}),Z&&(0,J.jsx)(`span`,{className:`pws-filter-dot`,"aria-hidden":`true`})]}),D&&(0,J.jsxs)(`div`,{id:`pws-provider-filters`,className:`pws-filter-menu`,role:`group`,"aria-label":h(`pws.providerFiltersAria`),children:[(0,J.jsx)(`div`,{className:`pws-filter-title`,children:h(`pws.filters`)}),(0,J.jsx)(`div`,{className:`pws-filter-head`,children:h(`pws.filterStatus`)}),Se.map(({key:e,label:t,count:n})=>(0,J.jsxs)(`label`,{className:`pws-filter-option`,children:[(0,J.jsx)(`input`,{type:`checkbox`,checked:y[e],onChange:()=>b(t=>({...t,[e]:!t[e]}))}),(0,J.jsx)(`span`,{className:`pws-filter-label`,children:t}),(0,J.jsx)(`span`,{className:`pws-filter-count`,children:n})]},e)),(0,J.jsx)(`div`,{className:`pws-filter-head`,children:h(`pws.pricing`)}),(0,J.jsxs)(`label`,{className:`pws-filter-option`,children:[(0,J.jsx)(`input`,{type:`checkbox`,checked:x.free,onChange:()=>S(e=>({...e,free:!e.free}))}),(0,J.jsx)(`span`,{className:`pws-filter-label`,children:h(`modal.badge.free`)}),(0,J.jsx)(`span`,{className:`pws-filter-count`,children:X})]}),(0,J.jsxs)(`label`,{className:`pws-filter-option`,children:[(0,J.jsx)(`input`,{type:`checkbox`,checked:x.paid,onChange:()=>S(e=>({...e,paid:!e.paid}))}),(0,J.jsx)(`span`,{className:`pws-filter-label`,children:h(`pws.paid`)}),(0,J.jsx)(`span`,{className:`pws-filter-count`,children:me})]}),(0,J.jsx)(`div`,{className:`pws-filter-head`,children:h(`pws.filterType`)}),[{key:`cloud`,label:h(`pws.type.cloud`),count:ge.cloud},{key:`local`,label:h(`pws.type.local`),count:ge.local},{key:`selfHosted`,label:h(`pws.type.selfHosted`),count:ge.selfHosted},{key:`login`,label:h(`pws.type.login`),count:ge.login}].map(({key:e,label:t,count:n})=>(0,J.jsxs)(`label`,{className:`pws-filter-option`,children:[(0,J.jsx)(`input`,{type:`checkbox`,checked:C[e],onChange:()=>w(t=>({...t,[e]:!t[e]}))}),(0,J.jsx)(`span`,{className:`pws-filter-label`,children:t}),(0,J.jsx)(`span`,{className:`pws-filter-count`,children:n})]},e)),(0,J.jsx)(`div`,{className:`pws-filter-head`,children:h(`pws.sort`)}),(0,J.jsx)(`div`,{className:`pws-sort-grid`,role:`group`,"aria-label":h(`pws.sortProvidersAria`),children:ga.map(e=>(0,J.jsx)(`button`,{type:`button`,className:`pws-sort-btn${T===e.id?` pws-sort-btn--active`:``}`,onClick:()=>E(e.id),"aria-pressed":T===e.id,children:h(e.labelKey)},e.id))}),(0,J.jsx)(`div`,{className:`pws-filter-footer`,children:(0,J.jsx)(`button`,{type:`button`,className:`link-btn`,onClick:ye,disabled:!Z,children:h(`pws.resetAll`)})})]})]})]}),(0,J.jsxs)(`div`,{className:`pws-rail-list`,role:`listbox`,"aria-label":h(`pws.providersAria`),onKeyDown:e=>{let t=Array.from(e.currentTarget.querySelectorAll(`[role="option"]`));if(t.length===0)return;let n=document.activeElement,r=t.findIndex(e=>e===n||e.contains(n));if(e.key===`ArrowDown`||e.key===`ArrowUp`){e.preventDefault();let n=e.key===`ArrowDown`?1:-1;t[r<0?n>0?0:t.length-1:(r+n+t.length)%t.length]?.focus();return}if(e.key===`Home`){e.preventDefault(),t[0]?.focus();return}e.key===`End`&&(e.preventDefault(),t[t.length-1]?.focus())},children:[Object.values(_e).every(e=>e.length===0)&&(0,J.jsx)(`span`,{className:`muted pws-rail-empty`,role:`status`,children:h(g?`pws.noSearchResults`:Z?`pws.noMatchFilters`:`pws.noProvidersConfigured`)}),Ce.map(({id:e,label:t,count:o,ariaLabel:s,items:c})=>c.length===0?null:(0,J.jsxs)(`div`,{className:`pws-rail-group`,role:`group`,"aria-label":s,children:[(0,J.jsxs)(`div`,{className:`pws-rail-group-head`,"aria-hidden":`true`,children:[(0,J.jsx)(`span`,{className:`pws-rail-group-label`,children:t}),(0,J.jsx)(`span`,{className:`pws-rail-group-count`,children:o})]}),c.map(e=>(0,J.jsxs)(`div`,{className:`pws-rail-row-wrap`,children:[(0,J.jsx)(Ii,{item:e,selected:r===e.name,tabbable:Te===e.name,modelCount:j[e.name],isDefault:n===e.name,showConfigId:xe.has(jn(e.name,h)),onClick:()=>i(e.name),onFocus:()=>A(e.name)}),a&&(0,J.jsx)(`button`,{type:`button`,className:`pws-rail-row-remove`,tabIndex:-1,"aria-hidden":`true`,onClick:t=>{t.stopPropagation(),a(e.name)},title:h(`pws.removeConfirmTitle`),children:(0,J.jsx)(he,{width:14,height:14})})]},e.name))]},e))]})]}),(0,J.jsx)(`main`,{className:`pws-main`,"aria-label":h(`pws.workspaceMainAria`),children:c?.open?(0,J.jsx)(ha,{editor:c,providerName:h(`nav.providers`),saving:l,onSave:()=>{c.onSave()}}):be?m?.(be,{usageTotals:ee[be.name],modelUsage:q[be.name],quotaReport:te[be.name],availableModels:N[be.name]??[],hasLiveModels:(F[be.name]??0)>0,selectedModels:L[be.name]??[],modelsLoading:z,modelsLoadFailed:V,onRetryModels:fe})??(0,J.jsxs)(`div`,{className:`pws-detail-placeholder`,children:[(0,J.jsx)(`h3`,{children:jn(be.name,h)}),(0,J.jsx)(`p`,{className:`muted`,children:h(`pws.detailComingSoon`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:()=>i(null),children:h(`modal.back`)})]}):(0,J.jsx)(pa,{sections:de,quotaReports:te,usageTotals:ee,usageLoading:re,quotasLoading:ae,onSelectProvider:e=>i(e),onEditConfig:s})})]})})}function Ca({onAddProvider:e}){let t=Q();return(0,J.jsx)(`div`,{className:`pws-empty-root`,children:(0,J.jsxs)(`div`,{className:`pws-empty-hero`,children:[(0,J.jsx)(`div`,{"aria-hidden":`true`,children:(0,J.jsx)(re,{style:{width:64,height:64}})}),(0,J.jsx)(`h2`,{children:t(`pws.connectFirst`)}),(0,J.jsxs)(`div`,{className:`pws-empty-tiles`,children:[(0,J.jsxs)(`button`,{type:`button`,className:`pws-empty-tile`,onClick:()=>e({tier:`free`}),children:[(0,J.jsx)(`span`,{"aria-hidden":`true`,children:(0,J.jsx)(Ne,{width:18,height:18})}),(0,J.jsx)(`span`,{className:`pws-empty-tile-label`,children:t(`pws.empty.browseFree`)}),(0,J.jsx)(`span`,{className:`pws-empty-tile-desc muted`,children:t(`pws.empty.browseFreeDesc`)})]}),(0,J.jsxs)(`button`,{type:`button`,className:`pws-empty-tile`,onClick:()=>e({tier:`accounts`}),children:[(0,J.jsx)(`span`,{"aria-hidden":`true`,children:(0,J.jsx)(De,{width:18,height:18})}),(0,J.jsx)(`span`,{className:`pws-empty-tile-label`,children:t(`pws.empty.connectAccount`)}),(0,J.jsx)(`span`,{className:`pws-empty-tile-desc muted`,children:t(`pws.empty.connectAccountDesc`)})]}),(0,J.jsxs)(`button`,{type:`button`,className:`pws-empty-tile`,onClick:()=>e({custom:!0}),children:[(0,J.jsx)(`span`,{"aria-hidden":`true`,children:(0,J.jsx)(Ee,{width:18,height:18})}),(0,J.jsx)(`span`,{className:`pws-empty-tile-label`,children:t(`pws.empty.addEndpoint`)}),(0,J.jsx)(`span`,{className:`pws-empty-tile-desc muted`,children:t(`pws.empty.addEndpointDesc`)})]})]})]})})}function wa(e){if(si(e.name,e))return`codex-accounts`;let t=(e.authMode??``).toLowerCase();if(t===`forward`||t===`local`||hi(e))return null;if(t===`oauth`)return`oauth-accounts`;let n=e.hasApiKey===!0;return!(t===`key`||n||t===``)||e.keyOptional===!0&&!n?null:`api-keys`}function Ta(e,t,n){let r=t.alias?.trim();if(r)return r;let i=t.email?.trim();if(i)return i;let a=e.findIndex(e=>e.id===t.id);return n(`pws.accountOrdinal`,{count:String(a>=0?a+1:1)})}function Ea({item:e,usageTotals:t,quotaReport:n,oauthEmail:r,oauth:i,apiBase:a,connectionIdentity:o,onEditSettings:s,onViewUsage:c,onUpdateProvider:l,onReauthenticate:u,onCancelLogin:d,reauthBusy:f=!1}){let p=Q(),{locale:m}=ct(),h=Ti(p),g=pi(e),v=!!e.activeNeedsReauth,y=p(g===`ready`?`pws.status.connected`:g===`needs-setup`?v?`pws.status.needsAttention`:`pws.status.needsSetup`:`prov.disabledBadge`),b=t?.requests,x=t?.totalTokens,S=Bi(n),C=JSON.stringify([a??null,e.name,e.adapter,e.baseUrl,e.authMode??null,e.apiKeyTransport??null,e.liveModels??null,e.disabled===!0,e.hasApiKey===!0,e.hasHeaders===!0,e.allowPrivateNetwork===!0,e.keyOptional===!0,e.activeNeedsReauth===!0,o??null]),[w,T]=(0,_.useState)(null),E=(0,_.useRef)(null),D=w?.key===C&&w.testing,O=w?.key===C?w.result:null;(0,_.useEffect)(()=>()=>{E.current?.key===C&&(E.current.controller.abort(),E.current=null)},[C]);let k=(0,_.useCallback)(async()=>{if(!a)return;E.current?.controller.abort();let t=new AbortController;E.current={key:C,controller:t},T({key:C,testing:!0,result:null});try{let n=await Pt(await fetch(`${a}/api/providers/test?name=${encodeURIComponent(e.name)}`,{method:`POST`,signal:t.signal}),p(`pws.connectionFailed`));if(!n)throw Error(p(`pws.connectionFailed`));t.signal.aborted||T({key:C,testing:!1,result:n})}catch(e){t.signal.aborted||T({key:C,testing:!1,result:{applicable:!0,ok:!1,error:e instanceof Error?e.message:p(`pws.connectionFailed`)}})}finally{E.current?.controller===t&&(E.current=null)}},[a,C,e.name,p]),A=O?.applicable===!1?`not-applicable`:O?.ok===!0?`ok`:`failed`,j=O?.applicable===!1?p(`pws.connectionNotApplicable`):O?.ok===!0?O.message||p(`pws.connectionOk`):O?.error||p(`pws.connectionFailed`);return(0,J.jsxs)(`div`,{className:`pws-overview-layout`,children:[(0,J.jsxs)(`div`,{className:`pws-overview-main`,children:[(0,J.jsxs)(`section`,{className:`pws-section`,"aria-label":p(`pws.connection`),children:[(0,J.jsx)(`h3`,{className:`pws-section-title`,children:p(`pws.connection`)}),(0,J.jsxs)(`dl`,{className:`pws-kv`,children:[(0,J.jsxs)(`div`,{className:`pws-kv-row`,children:[(0,J.jsx)(`dt`,{children:p(`dash.status`)}),(0,J.jsxs)(`dd`,{className:g===`ready`?`pws-status-ok`:`pws-status-warn`,children:[g===`ready`?(0,J.jsx)(ue,{style:{width:13,height:13},"aria-hidden":`true`}):(0,J.jsx)(_e,{style:{width:13,height:13},"aria-hidden":`true`}),y]})]}),(0,J.jsxs)(`div`,{className:`pws-kv-row`,children:[(0,J.jsx)(`dt`,{children:p(`modal.baseUrl`)}),(0,J.jsx)(`dd`,{children:(0,J.jsx)(`code`,{children:e.baseUrl?.trim()?e.baseUrl:`—`})})]}),(0,J.jsxs)(`div`,{className:`pws-kv-row`,children:[(0,J.jsx)(`dt`,{children:p(`pws.cell.auth`)}),(0,J.jsx)(`dd`,{children:r?`${Mi(e,p)} · ${r}`:Mi(e,p)})]}),(0,J.jsxs)(`div`,{className:`pws-kv-row`,children:[(0,J.jsx)(`dt`,{children:p(`modal.defaultModel`)}),(0,J.jsx)(`dd`,{children:e.defaultModel??(0,J.jsx)(`span`,{className:`muted`,children:`—`})})]}),e.note&&(0,J.jsxs)(`div`,{className:`pws-kv-row`,children:[(0,J.jsx)(`dt`,{children:p(`pws.cell.note`)}),(0,J.jsx)(`dd`,{className:`muted`,children:e.note})]})]}),a&&(0,J.jsxs)(`div`,{className:`row`,style:{marginTop:12,alignItems:`center`},children:[(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,disabled:D,onClick:()=>void k(),children:p(D?`pws.testing`:`pws.testConnection`)}),O&&(0,J.jsx)(`span`,{role:`status`,className:A===`ok`?`pws-status-ok`:A===`failed`?`pws-status-warn`:`muted`,"data-connection-test-state":A,children:j})]}),s&&(0,J.jsx)(`button`,{type:`button`,className:`link-btn pws-edit-settings-link`,onClick:s,children:p(`pws.editSettings`)})]}),n&&(0,J.jsxs)(`section`,{className:`pws-section`,"aria-label":p(`pws.rateLimits`),children:[(0,J.jsx)(`h3`,{className:`pws-section-title`,children:p(`pws.rateLimits`)}),(0,J.jsx)(fa,{report:n,pending:!1})]}),(0,J.jsxs)(`section`,{className:`pws-section`,"aria-label":p(`pws.authSummary`),children:[(0,J.jsx)(`h3`,{className:`pws-section-title`,children:p(`pws.authSummary`)}),v?(0,J.jsxs)(`div`,{className:`pws-auth-summary pws-auth-summary--warn`,role:`status`,children:[(0,J.jsx)(_e,{style:{width:14,height:14},"aria-hidden":`true`}),(0,J.jsxs)(`div`,{className:`pws-auth-summary-body`,children:[(0,J.jsxs)(`span`,{children:[(0,J.jsx)(`strong`,{children:p(`pws.status.needsAttention`)}),` — `,e.authMode===`forward`?p(`pws.attention.reauthForward`):p(`pws.attention.reauth`)]}),u&&(0,J.jsx)(`button`,{type:`button`,className:`btn btn-primary btn-sm`,disabled:f,onClick:()=>u(),children:p(f?`prov.waitingBrowser`:`pws.reauthenticate`)}),f&&d&&(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:()=>d(),children:p(`common.cancel`)})]})]}):(0,J.jsxs)(`div`,{className:`pws-auth-summary`,children:[(0,J.jsx)(`span`,{className:`pws-auth-dot`}),(0,J.jsx)(`span`,{children:e.authMode===`forward`?p(`pws.passthrough`):e.authMode===`oauth`?r?p(`pws.loggedInAs`,{email:r}):i?.loggedIn?p(`pws.loggedInTitle`):p(`pws.notLoggedIn`):e.hasApiKey?p(`pws.apiKeyConfigured`):Mi(e,p)})]})]})]}),(0,J.jsxs)(`aside`,{className:`pws-overview-sidebar`,children:[(0,J.jsxs)(`section`,{className:`pws-section`,"aria-label":p(`pws.statsAria`),children:[(0,J.jsx)(`h3`,{className:`pws-section-title`,children:p(`pws.statsTitle`)}),(0,J.jsxs)(`dl`,{className:`pws-kv`,children:[typeof b==`number`&&(0,J.jsxs)(`div`,{className:`pws-kv-row`,children:[(0,J.jsx)(`dt`,{children:p(`pws.stats.totalRequests`)}),(0,J.jsx)(`dd`,{className:`pws-kv-mono`,children:Oi(b,m)})]}),typeof x==`number`&&(0,J.jsxs)(`div`,{className:`pws-kv-row`,children:[(0,J.jsx)(`dt`,{children:p(`pws.stats.totalTokens`)}),(0,J.jsx)(`dd`,{className:`pws-kv-mono`,children:ki(x,m)})]}),n&&(0,J.jsxs)(`div`,{className:`pws-kv-row`,children:[(0,J.jsx)(`dt`,{children:p(`pws.stats.quotaUpdated`)}),(0,J.jsx)(`dd`,{className:`pws-kv-mono`,title:n.source?Ui(n.source):void 0,children:wi(n.updatedAt,h)})]}),typeof b!=`number`&&typeof x!=`number`&&!n&&(0,J.jsx)(`div`,{className:`muted`,children:p(`pws.usageUnavailable`)})]}),c&&(0,J.jsxs)(`button`,{type:`button`,className:`link-btn pws-view-usage-link`,onClick:c,children:[p(`pws.viewUsage`),` →`]}),S&&(0,J.jsx)(`div`,{className:`muted pws-stats-note`,children:p(`pws.stats.quotaTracked`)})]}),(0,J.jsx)(Da,{item:e,onUpdateProvider:l})]})]})}function Da({item:e,onUpdateProvider:t}){let n=Q(),[r,i]=(0,_.useState)(!1),[a,o]=(0,_.useState)(``),[s,c]=(0,_.useState)(!1),[l,u]=(0,_.useState)(``),d=(0,_.useRef)(null);(0,_.useEffect)(()=>{r&&d.current?.focus()},[r]);let f=(0,_.useCallback)(async()=>{if(s||!t)return;let r=a.trim();if(r===(e.note??``)){i(!1),u(``);return}c(!0);try{let a=await t(e.name,{note:r||void 0});if(!a.ok){u(a.error||n(`prov.saveFailed`));return}u(``),i(!1)}finally{c(!1)}},[a,e.name,e.note,t,s,n]);return r?(0,J.jsxs)(`section`,{className:`pws-section pws-notes-section`,"aria-label":n(`pws.notes`),children:[(0,J.jsx)(`h3`,{className:`pws-section-title`,children:n(`pws.notes`)}),(0,J.jsx)(`textarea`,{ref:d,className:`pws-notes-textarea`,value:a,onChange:e=>o(e.target.value),onBlur:()=>void f(),onKeyDown:t=>{t.key===`Escape`&&(o(e.note??``),u(``),i(!1))},placeholder:n(`pws.notePlaceholder`),rows:3,disabled:s}),l?(0,J.jsx)(`p`,{className:`pws-inline-error`,role:`alert`,children:l}):null]}):(0,J.jsxs)(`section`,{className:`pws-section pws-notes-section`,"aria-label":n(`pws.notes`),children:[(0,J.jsx)(`h3`,{className:`pws-section-title`,children:n(`pws.notes`)}),(0,J.jsx)(`button`,{type:`button`,className:`pws-notes-display`,onClick:()=>{t&&(o(e.note??``),u(``),i(!0))},disabled:!t,children:e.note||(0,J.jsx)(`span`,{className:`muted`,children:n(`pws.notePlaceholder`)})})]})}function Oa(e){return e.includes(`/`)?e.replaceAll(`/`,`-`):e}function ka(e,t){let n=Oa(e);for(let r of t)if(r!==e&&Oa(r)===n)return!0;return!1}function Aa({item:e,apiBase:t,availableModels:n,hasLiveModels:r,selectedModels:i,modelsLoading:a=!1,modelsLoadFailed:o=!1,needsReauth:s=!1,onRetryModels:c,onOpenAccounts:l}){let u=Q(),[d,f]=(0,_.useState)(``),[p,m]=(0,_.useState)(``),[h,g]=(0,_.useState)(!1),[v,y]=(0,_.useState)(``),[b,x]=(0,_.useState)(``),[S,C]=(0,_.useState)([]),[w,T]=(0,_.useState)(!1),[E,D]=(0,_.useState)(!1),[O,k]=(0,_.useState)(0),[A,j]=(0,_.useState)(null),M=(0,_.useRef)(null),N=(0,_.useMemo)(()=>new Set(i),[i]),P=(0,_.useMemo)(()=>e.models??[],[e.models]),F=p.trim(),I=[...n,...S,...P,...e.defaultModel?[e.defaultModel]:[]],L=!w||!F||n.includes(F)||S.includes(F)||P.includes(F)||e.defaultModel===F||ka(F,I),R=(0,_.useMemo)(()=>Wi(n,e.defaultModel,d,P,S,r),[n,e.defaultModel,d,P,S,r]);(0,_.useEffect)(()=>{let n=!0;return(async()=>{try{let r=await fetch(`${t}/api/custom-models`);if(!r.ok)throw Error();let i=await r.json();if(!Array.isArray(i))throw Error(`Invalid custom model list`);if(!n)return;C(i.flatMap(t=>{if(!t||typeof t!=`object`)return[];let n=t;return n.provider===e.name&&typeof n.modelId==`string`?[n.modelId]:[]})),D(!1),y(``),T(!0)}catch{if(!n)return;C([]),T(!1),D(!0),y(u(`models.networkError`))}})(),()=>{n=!1}},[t,e.name,u,O]);let z=()=>{T(!1),D(!1),y(``),k(e=>e+1)};(0,_.useEffect)(()=>()=>{M.current!=null&&window.clearTimeout(M.current)},[]);let B=async e=>{try{await navigator.clipboard.writeText(e),j(e),M.current!=null&&window.clearTimeout(M.current),M.current=window.setTimeout(()=>{j(t=>t===e?null:t),M.current=null},1200)}catch{}},V=async()=>{if(!(L||h)){g(!0),y(``),x(``);try{(await fetch(`${t}/api/custom-models`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({provider:e.name,modelId:F})})).ok?(C(e=>e.includes(F)?e:[...e,F]),m(``),x(u(`models.customAdded`)),c?.()):y(u(`models.customSaveFailed`))}catch{y(u(`models.networkError`))}finally{g(!1)}}},H=n.length===0&&P.length===0&&S.length===0&&!e.defaultModel,U=n.length===0&&P.length>0,W=R.length>300,ee=W?R.slice(0,300):R;return(0,J.jsxs)(`div`,{className:`pws-section`,children:[(0,J.jsxs)(`div`,{className:`pws-section-head`,children:[(0,J.jsx)(`h3`,{className:`pws-section-title`,children:u(`pws.tab.models`)}),R.length>0&&(0,J.jsx)(`span`,{className:`muted`,children:u(`pws.modelsAvailable`,{count:R.length})})]}),s&&(0,J.jsxs)(`div`,{className:`pws-inline-error`,role:`status`,children:[(0,J.jsx)(`span`,{children:u(`pws.modelsNeedsReauth`)}),l&&(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:l,children:u(`pws.tab.accounts`)})]}),U&&!s&&(0,J.jsx)(`p`,{className:`muted text-label`,style:{marginBottom:10},children:u(`pws.modelsConfiguredFallback`)}),(0,J.jsx)(`label`,{className:`text-label pws-custom-model-label`,htmlFor:`pws-custom-model-${e.name}`,children:u(`models.customAdd`)}),(0,J.jsxs)(`div`,{className:`row pws-custom-model-row`,children:[(0,J.jsx)(`input`,{id:`pws-custom-model-${e.name}`,className:`input`,value:p,onChange:e=>m(e.target.value),onKeyDown:e=>{e.key===`Enter`&&V()},placeholder:u(`models.customFieldModelIdPlaceholder`),"aria-label":u(`models.customAdd`),disabled:h}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-primary btn-sm`,onClick:()=>{V()},disabled:h||L,children:u(h?`models.customSaving`:`models.customAddBtn`)})]}),b&&(0,J.jsx)(`p`,{className:`muted text-label`,role:`status`,children:b}),v&&(0,J.jsxs)(`p`,{className:`pws-inline-error`,role:`alert`,children:[v,E&&(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:z,style:{marginLeft:8},children:u(`common.retry`)})]}),!H&&(0,J.jsx)(`input`,{type:`search`,className:`input pws-model-search`,placeholder:u(`pws.modelSearchPlaceholder`),value:d,onChange:e=>f(e.target.value),"aria-label":u(`pws.modelSearchPlaceholder`)}),a&&H?(0,J.jsx)(`p`,{className:`muted`,role:`status`,children:u(`pws.modelsLoading`)}):o&&H?(0,J.jsxs)(`div`,{role:`alert`,className:`pws-inline-error`,children:[(0,J.jsx)(`span`,{children:u(`pws.modelsLoadFailed`)}),c&&(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:c,children:u(`pws.retry`)})]}):H?(0,J.jsx)(`p`,{className:`muted`,children:u(`pws.noModels`)}):R.length===0?(0,J.jsx)(`p`,{className:`muted`,role:`status`,children:u(`pws.noModelMatch`)}):(0,J.jsx)(`ul`,{className:`pws-model-list`,children:ee.map(t=>{let n=t===e.defaultModel,r=N.has(t);return(0,J.jsxs)(`li`,{className:`pws-model-chip`,children:[(0,J.jsx)(`button`,{type:`button`,className:`pws-model-chip-main`,onClick:()=>{B(t)},title:t,"aria-label":u(A===t?`pws.modelCopied`:`pws.copyModelId`),children:(0,J.jsx)(`span`,{className:`pws-model-id`,children:t})}),n?(0,J.jsx)(`span`,{className:`badge badge-muted pws-model-flag`,children:u(`prov.defaultBadge`)}):null,r?(0,J.jsx)(`span`,{className:`badge badge-accent pws-model-flag`,children:u(`pws.selected`)}):null]},t)})}),W&&(0,J.jsx)(`p`,{className:`muted text-label`,style:{marginTop:10},children:u(`pws.modelsTruncated`,{shown:`300`,total:String(R.length)})})]})}function ja({item:e,usageTotals:t,quotaReport:n,modelUsage:r}){let i=Q(),{locale:a}=ct(),o=Ti(i),s=t?.requests!==void 0,c=Bi(n),[l,u]=(0,_.useState)(null),d=(0,_.useMemo)(()=>r?.length?r.toSorted((e,t)=>t.totalTokens-e.totalTokens):[],[r]),f=(0,_.useMemo)(()=>{if(!d.length)return;let e=0,t=!1;for(let n of d)n.estimatedCostUsd!==void 0&&(e+=n.estimatedCostUsd,t=!0);return t?e:void 0},[d]);return(0,J.jsxs)(`div`,{className:`pws-section`,children:[(0,J.jsxs)(`div`,{className:`pws-usage-block`,children:[(0,J.jsx)(`h3`,{className:`pws-section-title`,children:i(`pws.usageLast30d`)}),s?(0,J.jsxs)(J.Fragment,{children:[(0,J.jsxs)(`div`,{className:`pws-usage-metrics pws-usage-metrics-3`,role:`group`,"aria-label":i(`pws.usageLast30d`),children:[(0,J.jsxs)(`div`,{className:`pws-usage-metric`,children:[(0,J.jsx)(`span`,{className:`pws-usage-metric-value mono`,children:Ai(f,a)}),(0,J.jsx)(`span`,{className:`muted pws-usage-metric-label`,children:i(`pws.estimatedCost`)})]}),(0,J.jsxs)(`div`,{className:`pws-usage-metric`,children:[(0,J.jsx)(`span`,{className:`pws-usage-metric-value`,children:Oi(t?.requests,a)}),(0,J.jsx)(`span`,{className:`muted pws-usage-metric-label`,children:i(`pws.metricRequests`)})]}),(0,J.jsxs)(`div`,{className:`pws-usage-metric`,children:[(0,J.jsx)(`span`,{className:`pws-usage-metric-value`,children:ki(t?.totalTokens,a)}),(0,J.jsx)(`span`,{className:`muted pws-usage-metric-label`,children:i(`pws.metricTokens`)})]})]}),(0,J.jsx)(`p`,{className:`muted pws-cost-disclaimer`,children:i(`pws.costDisclaimer`)})]}):(0,J.jsx)(`p`,{className:`muted`,children:i(`pws.usageUnavailable`)})]}),d.length>0&&(0,J.jsxs)(`div`,{className:`pws-usage-block`,children:[(0,J.jsx)(`h3`,{className:`pws-section-title`,children:i(`pws.modelBreakdown`)}),(0,J.jsx)(`div`,{className:`tbl-wrap`,children:(0,J.jsxs)(`table`,{className:`pws-model-table`,children:[(0,J.jsx)(`thead`,{children:(0,J.jsxs)(`tr`,{children:[(0,J.jsx)(`th`,{children:i(`pws.col.model`)}),(0,J.jsx)(`th`,{className:`num`,children:i(`pws.col.cost`)}),(0,J.jsx)(`th`,{className:`num`,children:i(`pws.col.tokens`)}),(0,J.jsx)(`th`,{className:`num`,children:i(`pws.col.requests`)}),(0,J.jsx)(`th`,{children:i(`pws.col.share`)})]})}),(0,J.jsx)(`tbody`,{children:d.map(e=>{let t=e.model,n=l===t;return(0,J.jsxs)(_.Fragment,{children:[(0,J.jsxs)(`tr`,{className:`pws-model-row`,children:[(0,J.jsx)(`td`,{className:`mono`,children:(0,J.jsx)(`button`,{type:`button`,className:`pws-model-expand`,"aria-expanded":n,onClick:()=>u(n?null:t),children:e.model})}),(0,J.jsx)(`td`,{className:`num mono`,children:Ai(e.estimatedCostUsd,a)}),(0,J.jsx)(`td`,{className:`num mono`,children:ki(e.totalTokens,a)}),(0,J.jsx)(`td`,{className:`num`,children:e.requests}),(0,J.jsx)(`td`,{children:(0,J.jsx)(`div`,{className:`pws-share-bar`,children:(0,J.jsx)(`div`,{className:`pws-share-bar-fill`,style:{width:`${Math.round(e.shareRatio*100)}%`}})})})]}),n&&(0,J.jsx)(`tr`,{className:`pws-model-detail`,children:(0,J.jsx)(`td`,{colSpan:5,children:(0,J.jsxs)(`div`,{className:`pws-model-detail-grid`,children:[(0,J.jsxs)(`div`,{children:[(0,J.jsx)(`span`,{className:`muted`,children:i(`pws.tokenInput`)}),(0,J.jsxs)(`span`,{className:`mono`,children:[` `,ki(e.inputTokens,a)]})]}),(0,J.jsxs)(`div`,{children:[(0,J.jsx)(`span`,{className:`muted`,children:i(`pws.tokenOutput`)}),(0,J.jsxs)(`span`,{className:`mono`,children:[` `,ki(e.outputTokens,a)]})]})]})})})]},t)})})]})})]}),(0,J.jsxs)(`div`,{className:`pws-usage-block`,children:[(0,J.jsx)(`h3`,{className:`pws-section-title`,children:i(`pws.rateLimits`)}),c?(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)(ia,{quota:c,plan:null,threshold:80,t:i,layout:`stacked`}),(0,J.jsxs)(`dl`,{className:`pws-kv pws-usage-meta`,children:[n?.source?.trim()&&(0,J.jsxs)(`div`,{className:`pws-kv-row`,children:[(0,J.jsx)(`dt`,{children:i(`pws.stats.source`)}),(0,J.jsx)(`dd`,{children:Ui(n.source)})]}),(0,J.jsxs)(`div`,{className:`pws-kv-row`,children:[(0,J.jsx)(`dt`,{children:i(`pws.stats.quotaUpdated`)}),(0,J.jsx)(`dd`,{children:wi(n?.updatedAt,o)})]})]})]}):(0,J.jsx)(`p`,{className:`muted`,children:i(`pws.quotaUnavailable`)})]})]})}function Ma(e){if(!e)return null;let t=e.trim();return t?t.length<=4?`account-…`:`account-…${t.slice(-4)}`:null}function Na(e){return Ma(e)??`account-…`}function Pa(e){return e===`healthy`?`ok`:e===`cooldown`?`muted`:e===`reauth_required`||e===`warning`?`warn`:`muted`}function Fa(e){let t=Pa(e);return t===`ok`?`badge badge-green`:t===`warn`?`badge badge-amber`:`badge badge-muted`}function Ia(e){return e===`reauth_required`}function La(e){return!!e?.needsReauth||Ia(e?.health?.status)}function Ra(e){return e===`cooldown`}function za(e){return e===`warning`||e===`reauth_required`}function Ba(e){if(!e||e.status===`healthy`)return null;if(e.status===`cooldown`)return e.reason===`rate_limit`?`pws.healthLabel.rateLimited`:`pws.healthLabel.quotaLimited`;if(e.status===`reauth_required`)return e.reason===`refresh_failed`?`pws.healthLabel.refreshFailed`:`pws.healthLabel.reauthRequired`;switch(e.reason){case`refresh_conflict`:return`pws.healthLabel.credentialConflict`;case`metadata_mismatch`:return`pws.healthLabel.metadataMismatch`;case`stale_credentials`:return`pws.healthLabel.refreshFailed`;default:return`pws.healthLabel.reauthRequired`}}function Va(e,t){let n=Ba(t);return n?e(n):null}function Ha(e,t,n,r){if(!r||r.status===`healthy`)return null;let i=n===`__main__`?e(`codexAuth.mainAccount`):Na(n);if(r.status===`cooldown`){let n=r.until?new Date(r.until).toLocaleString():``;return e(r.reason===`rate_limit`?`pws.healthSummary.rateLimited`:`pws.healthSummary.quotaLimited`,{provider:t,account:i,until:n})}return r.status===`reauth_required`?e(`pws.healthSummary.reauthRequired`,{provider:t,account:i}):r.reason===`refresh_conflict`?e(`pws.healthSummary.credentialConflict`,{provider:t,account:i}):r.reason===`metadata_mismatch`?e(`pws.healthSummary.metadataMismatch`,{provider:t,account:i}):e(`pws.healthSummary.staleCredentials`,{provider:t,account:i})}async function Ua(e){let t=navigator.clipboard?.writeText?.bind(navigator.clipboard);if(t)try{return await t(e),!0}catch{}return Wa(e)}function Wa(e){if(typeof document>`u`||typeof document.execCommand!=`function`)return!1;let t=document.createElement(`textarea`);t.value=e,t.setAttribute(`readonly`,``),t.setAttribute(`aria-hidden`,`true`),t.style.position=`fixed`,t.style.top=`0`,t.style.opacity=`0`,document.body.appendChild(t);try{return t.select(),document.execCommand(`copy`)}catch{return!1}finally{t.remove()}}function Ga(e,t){return e(t?t===`copied`?`pws.doctorCopied`:`pws.doctorCopyUnavailable`:`pws.copyDoctor`)}var Ka=e=>({step:e?`oauth-waiting`:`pick`,id:``,error:``,authUrl:``,deviceCode:``,instructions:``,manualCode:``,manualCodeState:`idle`,statusNotice:``,statusTone:`ok`,flowId:null});function qa(e,t){switch(t.type){case`set-step`:return{...e,step:t.step};case`set-id`:return{...e,id:t.id};case`set-error`:return{...e,error:t.error};case`set-auth-url`:return{...e,authUrl:t.authUrl};case`set-login-hint`:return{...e,authUrl:t.authUrl,deviceCode:t.deviceCode??``,instructions:t.instructions??``};case`set-manual-code`:return{...e,manualCode:t.manualCode};case`set-manual-code-state`:return{...e,manualCodeState:t.manualCodeState};case`set-status-notice`:return{...e,statusNotice:t.statusNotice,statusTone:t.statusTone??e.statusTone};case`set-flow-id`:return{...e,flowId:t.flowId};case`clear-manual-code`:return{...e,manualCode:``,manualCodeState:`idle`,statusNotice:``,statusTone:`ok`};case`reset-oauth-start`:return{...e,error:``,statusNotice:``,statusTone:`ok`,flowId:null};case`oauth-code-submitted`:return{...e,error:``,manualCode:``,manualCodeState:`waiting`,statusTone:`ok`,statusNotice:``};default:return e}}function Ja({id:e,error:t,onIdChange:n,onStartOAuth:r,onStartDeviceOAuth:i,onClose:a}){let o=Q();return(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)(`h3`,{style:{marginBottom:4},children:o(`codexAuth.addTitle`)}),(0,J.jsx)(`p`,{className:`modal-desc`,children:o(`codexAuth.addPickDesc`)}),(0,J.jsx)(`label`,{className:`field-label`,htmlFor:`codex-account-id-input`,children:o(`codexAuth.addIdLabel`)}),(0,J.jsx)(`input`,{id:`codex-account-id-input`,className:`input`,placeholder:o(`codexAuth.addIdPlaceholder`),value:e,onChange:e=>n(e.target.value),style:{marginBottom:12}}),(0,J.jsx)(`button`,{type:`button`,className:`list-row`,onClick:r,style:{marginBottom:8},children:(0,J.jsxs)(`div`,{style:{display:`flex`,alignItems:`center`,gap:10},children:[(0,J.jsx)(Ne,{width:20}),(0,J.jsxs)(`div`,{children:[(0,J.jsx)(`div`,{className:`title`,children:o(`codexAuth.oauthLogin`)}),(0,J.jsx)(`div`,{className:`sub`,children:o(`codexAuth.oauthDesc`)})]})]})}),(0,J.jsx)(`button`,{type:`button`,className:`list-row`,onClick:i,style:{marginBottom:8},children:(0,J.jsxs)(`div`,{style:{display:`flex`,alignItems:`center`,gap:10},children:[(0,J.jsx)(ke,{width:20}),(0,J.jsxs)(`div`,{children:[(0,J.jsx)(`div`,{className:`title`,children:o(`codexAuth.deviceLogin`)}),(0,J.jsx)(`div`,{className:`sub`,children:o(`codexAuth.deviceDesc`)})]})]})}),t&&(0,J.jsx)(`div`,{className:`notice notice-err`,style:{marginTop:8},children:t}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost`,onClick:a,style:{width:`100%`},children:o(`codexAuth.cancel`)})]})}var Ya=2500;function Xa(){let[e,t]=(0,_.useState)(null),n=(0,_.useRef)(null),r=(0,_.useRef)(0),i=(0,_.useCallback)(()=>{n.current&&=(clearTimeout(n.current),null)},[]);return(0,_.useEffect)(()=>i,[i]),{outcomeFor:(0,_.useCallback)(t=>e&&Object.is(e.scope,t)?e.outcome:null,[e]),copy:(0,_.useCallback)((e,a)=>{let o=++r.current;Ua(e).then(e=>{r.current===o&&(i(),t({scope:a,outcome:e?`copied`:`unavailable`}),n.current=setTimeout(()=>{n.current=null,r.current===o&&t(null)},Ya))})},[i])}}function Za({url:e}){let t=Q(),{outcomeFor:n,copy:r}=Xa();if(!e)return null;let i=n(e),a=t(i===`copied`?`prov.linkCopied`:i===`unavailable`?`prov.linkCopyUnavailable`:`prov.copyLink`);return(0,J.jsxs)(`div`,{className:`login-url-block`,children:[(0,J.jsx)(`code`,{className:`login-url-block-text`,children:e}),(0,J.jsxs)(`div`,{className:`login-url-block-actions`,children:[(0,J.jsxs)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:()=>r(e,e),children:[(0,J.jsx)(ke,{style:{width:13,height:13},"aria-hidden":`true`}),(0,J.jsx)(`span`,{"aria-live":`polite`,children:a})]}),(0,J.jsxs)(`a`,{href:e,target:`_blank`,rel:`noreferrer`,className:`login-url-block-open`,children:[(0,J.jsx)(Te,{style:{width:13,height:13},"aria-hidden":`true`}),` `,t(`prov.didntOpen`)]})]})]})}function Qa({hint:e,paste:t}){let n=Q(),r=Xa(),i=e.deviceCode??``,a=e.url??``;if(!i&&!a&&!e.instructions&&!t)return null;let o=r.outcomeFor(i),s=n(o===`copied`?`prov.codeCopied`:o===`unavailable`?`prov.linkCopyUnavailable`:`prov.copyCode`);return(0,J.jsxs)(`div`,{className:`login-hint`,children:[i&&(0,J.jsxs)(`div`,{className:`login-hint-device pwi-device-code-wrap`,children:[(0,J.jsx)(`span`,{className:`text-label`,children:n(`prov.deviceCode`)}),(0,J.jsx)(`code`,{className:`login-hint-device-code pwi-device-code`,children:i}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-primary btn-sm`,onClick:()=>r.copy(i,i),children:(0,J.jsx)(`span`,{"aria-live":`polite`,children:s})})]}),(0,J.jsx)(Za,{url:a}),e.instructions&&(0,J.jsx)(`div`,{className:`muted text-label`,children:e.instructions}),t&&(0,J.jsxs)(`div`,{className:`login-hint-paste`,children:[(0,J.jsx)(`div`,{className:`muted text-label`,children:n(`prov.pasteRedirectHint`)}),(0,J.jsxs)(`div`,{className:`login-hint-paste-row`,children:[(0,J.jsx)(`input`,{type:`text`,autoComplete:`off`,spellCheck:!1,value:t.value,onChange:e=>t.onChange(e.target.value),onKeyDown:e=>{e.key===`Enter`&&(e.preventDefault(),t.onSubmit())},placeholder:n(`prov.pasteRedirect`),"aria-label":n(`prov.pasteRedirect`),disabled:t.busy,className:`input text-label login-hint-paste-input`}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost`,disabled:t.busy||t.disabled===!0||!t.value.trim(),onClick:t.onSubmit,children:t.busy?t.submittingLabel??n(`prov.pasteSubmitting`):n(`prov.pasteSubmit`)})]}),t.message&&(0,J.jsx)(`div`,{className:`text-label`,"aria-live":`polite`,style:{color:t.ok?`var(--accent-hover)`:`var(--amber)`},children:t.message})]})]})}function $a({reauthAccountId:e,authUrl:t,deviceCode:n,instructions:r,manualCode:i,manualCodeBusy:a,manualCodeWaiting:o,statusNotice:s,statusTone:c,flowId:l,error:u,onSwitchToDevice:d,onManualCodeChange:f,onSubmitManualCode:p,onClose:m}){let h=Q();return(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)(`h3`,{style:{marginBottom:4},children:h(e?`codexAuth.reauthenticate`:`codexAuth.oauthLogin`)}),(0,J.jsx)(`p`,{className:`modal-desc`,children:h(`codexAuth.oauthWaiting`)}),(0,J.jsx)(Qa,{hint:{url:t,deviceCode:n,instructions:r},paste:{value:i,busy:a,disabled:a||o||!i.trim()||!l,submittingLabel:h(`codexAuth.oauthSubmittingCode`),message:``,ok:!0,onChange:f,onSubmit:p}}),s&&(0,J.jsx)(`div`,{className:c===`warn`?`notice-warn`:`notice notice-ok`,role:`status`,"aria-live":`polite`,style:{marginTop:12},children:s}),u&&(0,J.jsx)(`div`,{className:`notice notice-err`,style:{marginTop:12},children:u}),d&&!n&&(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:d,style:{width:`100%`,marginTop:8},children:h(`codexAuth.deviceLogin`)}),(0,J.jsx)(`div`,{style:{textAlign:`center`,padding:`24px 0`},children:(0,J.jsx)(`span`,{className:`spin`,style:{width:24,height:24}})}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost`,onClick:m,style:{width:`100%`},children:h(`codexAuth.cancel`)})]})}var eo=`ocx.oauth.openBrowser`;function to(){try{let e=window.localStorage.getItem(eo);return e===`0`?!1:e===`1`||void 0}catch{return}}function no(){let e=to();return e===void 0?{}:{openBrowser:e}}function ro(e){try{window.localStorage.setItem(eo,e?`1`:`0`)}catch{}}function io(e){if(typeof e!=`object`||!e||Array.isArray(e))return{catalogRefreshPending:!1};let t=Object.getOwnPropertyDescriptor(e,`catalogRefreshPending`);return{catalogRefreshPending:t!==void 0&&`value`in t&&t.value===!0}}var ao=3e5,oo=96e4;function so({apiBase:e,reauthAccountId:t,ui:n,dispatch:r,t:i}){let a=(0,_.useRef)(!0),o=(0,_.useRef)(0),s=(0,_.useRef)(!1),c=(0,_.useRef)(null),l=(0,_.useRef)(null),u=(0,_.useRef)(null),d=(0,_.useRef)(null),f=(0,_.useRef)(n.manualCodeState),p=(0,_.useRef)(null),m=(0,_.useRef)(null),h=(0,_.useRef)(()=>{}),g=(0,_.useRef)(()=>{}),v=n.manualCodeState===`submitting`,y=n.manualCodeState===`waiting`;(0,_.useEffect)(()=>{f.current=n.manualCodeState},[n.manualCodeState]),(0,_.useEffect)(()=>{d.current=n.flowId},[n.flowId]);let b=(0,_.useCallback)(()=>{c.current&&=(c.current(),null),l.current&&=(clearTimeout(l.current),null),u.current?.abort(),u.current=null,s.current=!1},[]),x=(0,_.useCallback)(()=>{r({type:`clear-manual-code`}),o.current=0},[r]),S=(0,_.useCallback)(async()=>{x();let t=d.current;d.current=null,r({type:`set-flow-id`,flowId:null}),r({type:`set-login-hint`,authUrl:``}),b(),p.current?.abort(),p.current=null,t&&await fetch(`${e}/api/codex-auth/login/cancel`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({flowId:t})}).catch(()=>{})},[e,x,r,b]);(0,_.useEffect)(()=>(a.current=!0,()=>{x(),a.current=!1,m.current=null,p.current?.abort(),p.current=null;let t=d.current;d.current=null,r({type:`set-flow-id`,flowId:null}),b(),t&&fetch(`${e}/api/codex-auth/login/cancel`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({flowId:t})}).catch(()=>{})}),[e,x,r,b]);let C=(0,_.useCallback)((e,t)=>{h.current=e,g.current=t},[]),w=(0,_.useCallback)(()=>{n.step===`oauth-waiting`&&S(),g.current()},[n.step,S]),T=(0,_.useCallback)(async(n,m)=>{x(),d.current=null,r({type:`set-flow-id`,flowId:null});let _=new AbortController;p.current?.abort(),p.current=_,r({type:`reset-oauth-start`}),o.current=0;try{let p=t??n?.trim()??``,v=m?.device===!0,y=()=>fetch(`${e}/api/codex-auth/login`,{signal:_.signal,method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({...no(),...v?{device:!0}:{},...t?{id:t,reauth:!0}:p?{id:p}:{}})}),C=await y();if(!a.current)return;if(C.status===409){if(await fetch(`${e}/api/codex-auth/login/cancel`,{method:`POST`,headers:{"Content-Type":`application/json`},body:`{}`}),!a.current||_.signal.aborted)return;if(C=await y(),C.status===409){r({type:`set-error`,error:i(`codexAuth.oauthAlreadyInProgress`)});return}}let w=await Pt(C,i(`modal.networkError`));if(!a.current||!w)return;if(w.url){d.current=w.flowId??null,r({type:`set-flow-id`,flowId:w.flowId??null}),r({type:`set-login-hint`,authUrl:w.url,deviceCode:w.deviceCode,instructions:w.instructions}),r({type:`set-step`,step:`oauth-waiting`}),b();let n=w.flowId??``,m=t?`&reauth=1`:``,_=n?`${e}/api/codex-auth/login-status?flowId=${encodeURIComponent(n)}${p?`&accountId=${encodeURIComponent(p)}`:``}${m}`:`${e}/api/codex-auth/login-status`,y=new AbortController;u.current=y,c.current=Gn(async()=>{if(s.current||y.signal.aborted)return;s.current=!0;let e=AbortSignal.any([y.signal,AbortSignal.timeout(1e4)]);try{let n=await Ft(await fetch(_,{signal:e}));if(!a.current||y.signal.aborted)return;if(!n){o.current+=1,o.current>=3&&r({type:`set-status-notice`,statusNotice:i(`codexAuth.oauthStatusRetrying`),statusTone:`warn`});return}if(o.current=0,f.current===`waiting`?r({type:`set-status-notice`,statusNotice:i(`codexAuth.oauthCodeSubmitted`),statusTone:`ok`}):r({type:`set-status-notice`,statusNotice:``,statusTone:`ok`}),n.status===`done`){if(b(),x(),d.current=null,r({type:`set-flow-id`,flowId:null}),!a.current)return;h.current(io(n)),g.current()}else(n.status===`error`||n.status===`expired`)&&(b(),x(),d.current=null,r({type:`set-flow-id`,flowId:null}),a.current&&(t||r({type:`set-step`,step:`pick`}),r({type:`set-error`,error:n.error??i(`codexAuth.loginFailed`)})))}catch(e){if(!a.current||y.signal.aborted||e instanceof Error&&e.name===`AbortError`)return;o.current+=1,o.current>=3&&r({type:`set-status-notice`,statusNotice:i(`codexAuth.oauthStatusRetrying`),statusTone:`warn`})}finally{s.current=!1}},2e3),l.current=setTimeout(()=>{c.current&&(x(),S(),a.current&&(t||r({type:`set-step`,step:`pick`}),r({type:`set-error`,error:i(`modal.loginTimeout`)})))},v?oo:ao)}w.error&&!w.url&&r({type:`set-error`,error:w.error})}catch(e){a.current&&!(e instanceof Error&&e.name===`AbortError`)&&r({type:`set-error`,error:e instanceof Error?e.message:String(e)})}},[e,S,x,r,t,b,i]);return(0,_.useEffect)(()=>{if(!t){m.current=null;return}m.current!==t&&(m.current=t,T())},[t,T]),{manualCodeBusy:v,manualCodeWaiting:y,bindCallbacks:C,closeModal:w,startOAuth:T,submitManualCode:(0,_.useCallback)(async()=>{let t=d.current,s=n.manualCode.trim();if(!(!t||!s||v||y)){r({type:`set-manual-code-state`,manualCodeState:`submitting`}),r({type:`set-status-notice`,statusNotice:``,statusTone:`ok`});try{let n=await fetch(`${e}/api/codex-auth/login/code`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({flowId:t,input:s})});if(!a.current)return;if(!n.ok){r({type:`set-error`,error:i(`prov.pasteFail`,{error:(await n.json().catch(()=>({}))).error??n.statusText})}),r({type:`set-manual-code-state`,manualCodeState:`idle`});return}r({type:`oauth-code-submitted`}),r({type:`set-status-notice`,statusNotice:i(`codexAuth.oauthCodeSubmitted`),statusTone:`ok`}),o.current=0}catch{a.current&&(r({type:`set-error`,error:i(`modal.networkError`)}),r({type:`set-manual-code-state`,manualCodeState:`idle`}))}}},[e,r,v,y,i,n.manualCode])}}function co({apiBase:e,onClose:t,onAdded:n,reauthAccountId:r}){let i=Q(),[a,o]=(0,_.useReducer)(qa,r,Ka),s=(0,_.useRef)(null),c=(0,_.useRef)(null),{manualCodeBusy:l,manualCodeWaiting:u,bindCallbacks:d,closeModal:f,startOAuth:p,submitManualCode:m}=so({apiBase:e,reauthAccountId:r,ui:a,dispatch:o,t:i});(0,_.useEffect)(()=>{d(n,t)},[d,n,t]),(0,_.useEffect)(()=>{s.current=document.activeElement;let e=c.current;e&&!e.open&&e.showModal();let t=e?.querySelector(`input:not([disabled]), button:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex='-1'])`);return t&&t.focus(),()=>{s.current?.focus()}},[]);let h=(0,_.useCallback)(e=>{e.preventDefault(),f()},[f]),g=i(r?`codexAuth.reauthenticate`:`codexAuth.addTitle`);return(0,J.jsx)(`dialog`,{ref:c,"aria-label":g,className:`modal-overlay`,onCancel:h,children:(0,J.jsxs)(`div`,{className:`modal-card`,style:{maxWidth:440},children:[a.step===`pick`&&(0,J.jsx)(Ja,{id:a.id,error:a.error,onIdChange:e=>o({type:`set-id`,id:e}),onStartOAuth:()=>{p(a.id)},onStartDeviceOAuth:()=>{p(a.id,{device:!0})},onClose:f}),a.step===`oauth-waiting`&&(0,J.jsx)($a,{reauthAccountId:r,authUrl:a.authUrl,deviceCode:a.deviceCode,instructions:a.instructions,manualCode:a.manualCode,manualCodeBusy:l,manualCodeWaiting:u,statusNotice:a.statusNotice,statusTone:a.statusTone,flowId:a.flowId,error:a.error,onSwitchToDevice:()=>{p(a.id,{device:!0})},onManualCodeChange:e=>o({type:`set-manual-code`,manualCode:e}),onSubmitManualCode:()=>{m()},onClose:f})]})})}var lo=[2,1,0,-1,-2],uo=new Map([[2,`accountPool.priorityFirst`],[1,`accountPool.priorityEarlier`],[0,`accountPool.priorityNormal`],[-1,`accountPool.priorityLater`],[-2,`accountPool.priorityLast`]]);function fo(e){return typeof e==`number`&&Number.isInteger(e)&&e>=-100&&e<=100?e:0}function po(e){let t=fo(e);return t>0?`+${t}`:String(t)}function mo(e){return uo.has(e)}function ho(e){return uo.get(e)??null}function go(e,t){let n=fo(t);return e(`accountPool.priorityOption`,{name:e(ho(n)??`accountPool.priorityCustom`),value:po(n)})}var _o=1e4;function vo(e){return typeof e==`number`&&Number.isInteger(e)&&e>=0&&e<=100?e:80}function yo(e){let t=e.trim();if(!/^\d+$/.test(t))return null;let n=Number(t);return n>=1&&n<=100?n:null}function bo(e,t){return e>0?0:Number.isInteger(t)&&t>=1&&t<=100?t:80}function xo(e,t,n,r){return n===r?e||t?`defer`:`apply`:`ignore`}function So(e){return e&&typeof e==`object`&&e&&`autoSwitchThreshold`in e?e.autoSwitchThreshold:e}function Co(e,t,n){if(e<=0){let t=bo(e,n);return{threshold:t,lastEnabled:t}}return{threshold:0,lastEnabled:yo(t)??bo(0,n)}}async function wo(e,t,n=(e,t)=>fetch(e,t),r=_o){if(!Number.isInteger(t)||t<0||t>100)return!1;try{return(await n(`${e}/api/codex-auth/auto-switch`,{method:`PUT`,headers:{"Content-Type":`application/json`},body:JSON.stringify({threshold:t}),signal:AbortSignal.timeout(r)})).ok}catch{return!1}}var To=3e4,Eo=new Map;function Do(e,t=!0){let n=Eo.get(e),[r,i]=(0,_.useState)(()=>n?.accounts??[]),a=G(Br(e,`codex`),[e],async t=>{let n=await fetch(`${e}/api/usage?range=30d&surface=codex`,{signal:t});if(!n.ok)throw Error(`account usage load failed`);return n.json()},{enabled:t}),[o,s]=(0,_.useState)(()=>n?.activeId??null),[c,l]=(0,_.useState)(()=>n==null?`loading`:`ready`),[u,d]=(0,_.useState)(null),[f,p]=(0,_.useState)(null),[m,h]=(0,_.useState)(null),[g,v]=(0,_.useState)(!1),[y,b]=(0,_.useState)(null),[x,S]=(0,_.useState)(0),[C,w]=(0,_.useState)(()=>n!=null),[T,E]=(0,_.useState)(0),D=(0,_.useRef)(null);D.current===null&&(D.current=new Set);let O=(0,_.useRef)(null),k=(0,_.useRef)(0),A=(0,_.useRef)(null),j=(0,_.useRef)(null);j.current===null&&(j.current=new Set);let M=(0,_.useRef)(null),N=(0,_.useRef)(null),P=(0,_.useRef)(!!n?.accounts.length),F=(0,_.useRef)(n!=null),I=(0,_.useRef)(null),L=(0,_.useRef)(null),R=(0,_.useCallback)(e=>(j.current.add(e),()=>{j.current.delete(e)}),[]),z=(0,_.useCallback)(()=>So(M.current?.value),[]),B=(0,_.useCallback)(()=>M.current?.value,[]),V=(0,_.useCallback)(async(t=!1)=>{let n=++k.current,r=Vn(2e4);S(e=>e+1);try{let a=[...j.current],o=new Map;for(let e of a)o.set(e,e.beginActiveRead());!t&&!F.current&&l(`loading`);let c=null,u,d=(async()=>{try{let a=await fetch(`${e}/api/codex-auth/accounts${t?`?refresh=1`:``}`,{signal:r.signal});if(!a.ok)throw Error(`account load failed`);let o=await a.json();return k.current===n&&(c=(o.accounts??[]).map(e=>{let t=e.isMain?`main`:e.logLabel;return{...e,...t?{logLabel:t}:{},priority:fo(e.priority)}}),i(c),P.current=c.length>0,F.current=!0,l(`ready`)),!0}catch{return!1}})(),f=(async()=>{try{let t=await fetch(`${e}/api/codex-auth/active`,{signal:r.signal});if(!t.ok)throw Error(`active account load failed`);let i=await t.json();if(k.current===n){let e=i.activeCodexAccountId??null,t=A.current;t&&e!==t.id||(A.current=null,u=e,s(e)),M.current={value:i},b(typeof i.pinnedAccountId==`string`?i.pinnedAccountId:null);for(let e of a)e.acceptActiveRead(i,o.get(e))}return!0}catch{if(k.current===n)for(let e of a)e.rejectActiveRead();return!1}})(),[p,m]=await Promise.all([d,f]);if(k.current!==n)return!1;if(p){l(`ready`),F.current=!0;let t=Eo.get(e);return Eo.set(e,{accounts:c??t?.accounts??[],activeId:u===void 0?t?.activeId??null:u}),m}return F.current||l(`error`),!1}finally{r.clear(),S(e=>Math.max(0,e-1)),w(!0)}},[e]);(0,_.useEffect)(()=>{t&&O.current!==e&&(O.current=e,Promise.resolve().then(()=>{V()}))},[e,t,V]);let H=r.some(e=>e.hasCredential&&!e.quota);(0,_.useEffect)(()=>{if(!t||!H||T>0)return;let e=[350,900,2e3].map(e=>window.setTimeout(()=>{V(!1)},e));return()=>{for(let t of e)window.clearTimeout(t)}},[t,H,T,V]),(0,_.useEffect)(()=>{if(!(!t||T>0))return Gn(()=>{V()},To)},[t,V,T]);let U=(0,_.useCallback)(()=>{let e={};return D.current.add(e),E(D.current.size),e},[]),W=(0,_.useCallback)(e=>{D.current.delete(e)&&E(D.current.size)},[]),ee=(0,_.useCallback)(async t=>{if(N.current||L.current)return{ok:!1,reason:`busy`};N.current=t??`__main__`,d(t??`__main__`);try{let n=await fetch(`${e}/api/codex-auth/active`,{method:`PUT`,headers:{"Content-Type":`application/json`},body:JSON.stringify({accountId:t})});if(!n.ok)throw Error(`account switch failed`);let r=(await n.json().catch(()=>({}))).activeCodexAccountId??t;return A.current={id:r??null},s(r??null),b(r??`__main__`),V(),{ok:!0,activeId:r??null}}catch{return{ok:!1,reason:`request`}}finally{N.current=null,d(null)}},[e,V]),K=(0,_.useCallback)(async(t,n)=>{try{return(await fetch(`${e}/api/codex-auth/accounts/alias`,{method:`PUT`,headers:{"Content-Type":`application/json`},body:JSON.stringify({id:t,alias:n.trim()})})).ok?(await V(),{ok:!0}):{ok:!1,reason:`request`}}catch{return{ok:!1,reason:`request`}}},[e,V]),q=(0,_.useCallback)(async(t,n)=>{if(I.current)return{ok:!1,reason:`busy`};I.current={accountId:t},p(t);try{let r=await fetch(`${e}/api/codex-auth/accounts/pause`,{method:`PUT`,headers:{"Content-Type":`application/json`},body:JSON.stringify({id:t,paused:n})});if(!r.ok)return{ok:!1,reason:`request`};let a=await r.json().catch(()=>({})),o=a&&typeof a==`object`?a:{};if(i(e=>e.map(e=>e.id===t||t===`__main__`&&e.isMain?{...e,paused:n}:e)),Object.prototype.hasOwnProperty.call(o,`activeCodexAccountId`)){let e=o.activeCodexAccountId??null;A.current={id:e},s(e)}return n&&b(e=>e===t?null:e),V(),{ok:!0}}catch{return{ok:!1,reason:`request`}}finally{I.current=null,p(null)}},[e,V]),J=(0,_.useCallback)(async(t,n)=>{if(L.current||N.current)return{ok:!1,reason:`busy`};L.current={accountId:t},h(t);try{let r=await fetch(`${e}/api/codex-auth/accounts/priority`,{method:`PUT`,headers:{"Content-Type":`application/json`},body:JSON.stringify({id:t,priority:n})});if(!r.ok)return{ok:!1,reason:`request`};let a=await r.json().catch(()=>({})),o=fo((a&&typeof a==`object`?a:{}).priority??n);return i(e=>e.map(e=>e.id===t||t===`__main__`&&e.isMain?{...e,priority:o}:e)),b(null),A.current=null,V(),{ok:!0}}catch{return{ok:!1,reason:`request`}}finally{L.current=null,h(null)}},[e,V]),Y=(0,_.useCallback)(async()=>{if(I.current)return{ok:!1,reason:`busy`};I.current=`bulk`,v(!0);try{let t=await fetch(`${e}/api/codex-auth/accounts/pause-exhausted`,{method:`PUT`});if(!t.ok)return{ok:!1,reason:`request`};let n=await t.json().catch(()=>({})),r=n&&typeof n==`object`?n:{},a=new Set(r.pausedAccountIds??[]);if(i(e=>e.map(e=>a.has(e.id)||a.has(`__main__`)&&e.isMain?{...e,paused:!0}:e)),Object.prototype.hasOwnProperty.call(r,`activeCodexAccountId`)){let e=r.activeCodexAccountId??null;A.current={id:e},s(e)}return b(e=>e!==null&&a.has(e)?null:e),V(),{ok:!0,pausedCount:r.pausedCount??a.size}}catch{return{ok:!1,reason:`request`}}finally{I.current=null,v(!1)}},[e,V]),te=(0,_.useCallback)(async t=>{try{let n=await fetch(`${e}/api/codex-auth/accounts?id=${encodeURIComponent(t)}`,{method:`DELETE`});if(!n.ok)return{ok:!1,reason:`request`};let r=io(await n.json().catch(()=>({})));return await V(),{ok:!0,...r}}catch{return{ok:!1,reason:`request`}}},[e,V]),ne=(0,_.useCallback)(async()=>await V()?{ok:!0}:{ok:!1,reason:`reload`},[V]),re=o&&o!==`__main__`?r.find(e=>e.id===o):null,ie=r.find(e=>e.isMain),ae=re??ie,oe=!ae?.paused&&La(ae);return{accounts:(0,_.useMemo)(()=>{let e=new Map((a.data?.accounts??[]).map(e=>[e.accountLogLabel,e]));return r.map(t=>{let n=t.isMain?`main`:t.logLabel,r=n?e.get(n):void 0;return r?{...t,usage30d:r}:t})},[r,a.data]),activeId:o,loadState:c,refreshing:x>0,initialLoading:!C,switchingId:u,pauseUpdatingId:f,priorityUpdatingId:m,pausingExhausted:g,activeNeedsReauth:oe,activePinnedId:y,load:V,switchAccount:ee,setAccountPaused:q,setAccountPriority:J,pauseExhaustedAccounts:Y,saveAlias:K,removeAccount:te,syncAfterAccountAdded:ne,pauseRefresh:U,resumeRefresh:W,subscribeLoadObserver:R,readLastThreshold:z,readLastActive:B}}function Oo(e,t,n,r,i=1){let a=e.trim(),o=a===``?NaN:Number(a),s=Math.min(r,Math.max(n,(Number.isFinite(o)?o:n)+t));return String(i<1?Math.round(s*10)/10:Math.round(s))}function ko({disabled:e=!1,onIncrement:t,onDecrement:n,incrementLabel:r,decrementLabel:i}){return(0,J.jsxs)(`div`,{className:`ocx-stepper`,role:`group`,children:[(0,J.jsx)(`button`,{type:`button`,className:`ocx-stepper__btn`,disabled:e,"aria-label":r,onMouseDown:e=>e.preventDefault(),onClick:t,children:(0,J.jsx)(ye,{width:10,height:10,"aria-hidden":`true`})}),(0,J.jsx)(`button`,{type:`button`,className:`ocx-stepper__btn`,disabled:e,"aria-label":i,onMouseDown:e=>e.preventDefault(),onClick:n,children:(0,J.jsx)(be,{width:10,height:10,"aria-hidden":`true`})})]})}var Ao={quota:{on:`codexAuth.autoSwitchQuotaDesc`,off:`codexAuth.autoSwitchQuotaOffDesc`},"round-robin":{on:`codexAuth.autoSwitchRoundRobinDesc`,off:`codexAuth.autoSwitchRoundRobinDesc`},"fill-first":{on:`codexAuth.autoSwitchFillFirstDesc`,off:`codexAuth.autoSwitchFillFirstOffDesc`}};function jo({threshold:e,draft:t,strategy:n=`quota`,hydrated:r=!0,saving:i,loadError:a,feedback:o,onDraftChange:s,onEditingChange:c,onCommit:l,onCancel:u,onToggle:d,onRetry:f}){let p=Q(),m=(0,_.useRef)(!1),h=e>0,g=Ao[n][h?`on`:`off`],v=i||!r,y=i?p(`common.saving`):o?.message??``,b=i?`pending`:o?.tone,x=y?`codex-auto-switch-desc codex-auto-switch-feedback`:`codex-auto-switch-desc`;return(0,J.jsxs)(`div`,{className:`card card-row codex-auto-switch-card`,style:{marginTop:16},"aria-busy":i||!r&&!a||void 0,children:[(0,J.jsxs)(`div`,{className:`codex-auto-switch-copy`,children:[(0,J.jsx)(`strong`,{children:p(`codexAuth.autoSwitch`)}),(0,J.jsx)(`div`,{id:`codex-auto-switch-desc`,className:`card-sub`,role:a?`alert`:void 0,children:a?p(`codexAuth.autoSwitchLoadFailed`):p(g,{threshold:e})}),(0,J.jsx)(`div`,{className:`card-sub`,children:p(`codexAuth.failureRecoveryNote`)}),(0,J.jsx)(`div`,{className:`card-sub`,children:p(`codexAuth.cacheWarning`)})]}),(0,J.jsxs)(`div`,{className:`codex-auto-switch-controls`,onBlur:e=>{if(!e.currentTarget.contains(e.relatedTarget)){if(c(!1),m.current){m.current=!1;return}h&&!v&&l()}},children:[a&&(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:f,children:p(`pws.retryAccounts`)}),h&&(0,J.jsxs)(`label`,{className:`codex-auto-switch-threshold`,children:[(0,J.jsx)(`span`,{className:`field-label`,children:p(`codexAuth.autoSwitchThreshold`)}),(0,J.jsxs)(`span`,{className:`codex-auto-switch-input-wrap`,children:[(0,J.jsx)(`input`,{className:`input mono codex-auto-switch-input`,type:`number`,min:1,max:100,step:1,inputMode:`numeric`,value:t,readOnly:v,"aria-disabled":v,"aria-label":p(`codexAuth.autoSwitchThresholdAria`),"aria-describedby":x,onChange:e=>s(e.target.value),onFocus:()=>{v||c(!0)},onKeyDown:e=>{e.nativeEvent.isComposing||v||(e.key===`Enter`?(e.preventDefault(),l()):e.key===`Escape`&&(e.preventDefault(),u()))}}),(0,J.jsx)(`span`,{className:`codex-auto-switch-unit`,"aria-hidden":`true`,children:`%`}),(0,J.jsx)(ko,{disabled:v,incrementLabel:p(`codexAuth.autoSwitchThresholdInc`),decrementLabel:p(`codexAuth.autoSwitchThresholdDec`),onIncrement:()=>{c(!0),s(Oo(t,1,1,100))},onDecrement:()=>{c(!0),s(Oo(t,-1,1,100))}})]})]}),(0,J.jsx)(`span`,{className:`codex-auto-switch-toggle-slot`,children:(0,J.jsx)(`button`,{type:`button`,className:`toggle ${h?`on`:``}`,onPointerDownCapture:()=>{m.current=!0},onPointerUp:()=>{m.current=!1},onPointerCancel:()=>{m.current=!1},onClick:()=>{m.current=!1,d()},disabled:v,"aria-pressed":h,"aria-label":p(`codexAuth.autoSwitch`),"aria-describedby":x,title:p(`codexAuth.autoSwitch`),children:(0,J.jsx)(`span`,{className:`toggle-knob`})})})]}),y&&(0,J.jsx)(`div`,{id:`codex-auto-switch-feedback`,className:`codex-auto-switch-feedback${b===`err`?` is-error`:``}`,role:b===`err`?`alert`:`status`,"aria-atomic":`true`,children:y})]})}var Mo=[`quota`,`round-robin`,`fill-first`],No=[`five-hour`,`weekly`,`max-utilization`],Po=`quota`,Fo=`five-hour`,Io=new Set(Mo),Lo=new Set(No);function Ro(e){return typeof e==`string`&&Io.has(e)?e:Po}function zo(e){return typeof e==`string`&&Lo.has(e)?e:Fo}function Bo(e){return typeof e==`number`&&Number.isInteger(e)&&e>=1&&e<=100?e:1}function Vo(e){let t=e.trim();if(!/^\d+$/.test(t))return null;let n=Number(t);return n>=1&&n<=100?n:null}async function Ho(e,t,n=(e,t)=>fetch(e,t)){if(t.strategy===void 0&&t.stickyLimit===void 0)return{ok:!1};try{let r=await n(`${e}/api/codex-auth/pool-strategy`,{method:`PUT`,headers:{"Content-Type":`application/json`},body:JSON.stringify({...t.strategy===void 0?{}:{strategy:t.strategy},...t.stickyLimit===void 0?{}:{stickyLimit:t.stickyLimit}})});if(!r.ok)return{ok:!1};let i=await r.json();return{ok:!0,strategy:Ro(i.accountPoolStrategy??t.strategy),stickyLimit:Bo(i.accountPoolStickyLimit??t.stickyLimit)}}catch{return{ok:!1}}}var Uo={quota:`accountPool.strategyQuota`,"round-robin":`accountPool.strategyRoundRobin`,"fill-first":`accountPool.strategyFillFirst`},Wo={quota:`accountPool.strategyHintQuota`,"round-robin":`accountPool.strategyHintRoundRobin`,"fill-first":`accountPool.strategyHintFillFirst`};function Go({strategy:e,stickyDraft:t,disabled:n=!1,strategySelectId:r=`account-pool-strategy`,stickyInputId:i=`account-pool-sticky-limit`,onStrategyChange:a,onStickyDraftChange:o,onStickyCommit:s}){let c=Q(),l=Mo.map(e=>({value:e,label:c(Uo[e])}));return(0,J.jsxs)(`div`,{className:`account-pool-strategy-controls`,children:[(0,J.jsxs)(`div`,{className:`setting-row`,children:[(0,J.jsxs)(`div`,{className:`setting-label`,children:[(0,J.jsx)(`span`,{className:`title`,id:`${r}-label`,children:c(`accountPool.strategy`)}),(0,J.jsx)(`span`,{className:`desc`,children:c(`accountPool.strategyDesc`)}),(0,J.jsx)(`span`,{className:`desc`,children:c(Wo[e])}),(0,J.jsx)(`span`,{className:`desc`,children:c(`accountPool.unboundDefinition`)})]}),(0,J.jsx)(`div`,{className:`setting-controls`,children:(0,J.jsx)(Dt,{id:r,value:e,options:l,disabled:n,label:c(`accountPool.strategy`),onChange:e=>a(e)})})]}),e===`round-robin`&&(0,J.jsxs)(`div`,{className:`setting-row`,children:[(0,J.jsxs)(`label`,{className:`setting-label`,htmlFor:i,children:[(0,J.jsx)(`span`,{className:`title`,children:c(`accountPool.stickyLimit`)}),(0,J.jsx)(`span`,{className:`desc`,children:c(`accountPool.stickyLimitHelp`)})]}),(0,J.jsx)(`div`,{className:`setting-controls`,children:(0,J.jsxs)(`span`,{className:`codex-auto-switch-input-wrap`,children:[(0,J.jsx)(`input`,{id:i,className:`input mono codex-auto-switch-input`,type:`number`,min:1,max:100,step:1,inputMode:`numeric`,value:t,disabled:n,"aria-label":c(`accountPool.stickyLimitAria`),onChange:e=>o(e.target.value),onBlur:()=>s(),onKeyDown:e=>{e.nativeEvent.isComposing||n||e.key===`Enter`&&(e.preventDefault(),s())}}),(0,J.jsx)(ko,{disabled:n,incrementLabel:c(`accountPool.stickyLimitInc`),decrementLabel:c(`accountPool.stickyLimitDec`),onIncrement:()=>{let e=Oo(t,1,1,100);o(e),s(e)},onDecrement:()=>{let e=Oo(t,-1,1,100);o(e),s(e)}})]})})]})]})}function Ko(e){if(!e||typeof e!=`object`)return null;let t=e;return!(`accountPoolStrategy`in t)&&!(`accountPoolStickyLimit`in t)?null:{strategy:Ro(t.accountPoolStrategy),stickyLimit:Bo(t.accountPoolStickyLimit)}}function qo({apiBase:e,subscribeLoadObserver:t,readLastActive:n,onStrategyResolved:r}){let i=Q(),[a,o]=(0,_.useState)(Po),[s,c]=(0,_.useState)(1),[l,u]=(0,_.useState)(`1`),[d,f]=(0,_.useState)(!1),p=(0,_.useRef)(!1),[m,h]=(0,_.useState)(!1),g=(0,_.useRef)(!1),v=(0,_.useRef)(!1),y=(0,_.useRef)(0),[b,x]=(0,_.useState)(!1),[S,C]=(0,_.useState)(null),w=(0,_.useCallback)(e=>{let t=Ro(e.accountPoolStrategy),n=Bo(e.accountPoolStickyLimit);o(t),r?.(t),c(n),u(String(n)),p.current=!0,f(!0),x(!1),C(null)},[r]),T=(0,_.useCallback)(e=>{let t=Ko(e);t&&w({accountPoolStrategy:t.strategy,accountPoolStickyLimit:t.stickyLimit})},[w]),E=(0,_.useCallback)(async()=>{try{let t=await fetch(`${e}/api/codex-auth/active`);if(!t.ok)throw Error(`load`);let n=await t.json();if(g.current){v.current=!0;return}w(n)}catch{g.current||x(!0)}},[e,w]),D=(0,_.useCallback)(()=>{v.current&&(v.current=!1,queueMicrotask(()=>{if(g.current){v.current=!0;return}E()}))},[E]);(0,_.useEffect)(()=>{if(!t)return;let e=t({beginActiveRead:()=>y.current,acceptActiveRead:(e,t)=>{if(t===y.current){if(g.current){v.current=!0;return}T(e)}},rejectActiveRead:()=>{p.current||x(!0)}});return!g.current&&n&&T(n()),e},[t,T,n]),(0,_.useEffect)(()=>{!n||t||g.current||T(n())},[n,T,t]),(0,_.useEffect)(()=>{t||E()},[E,t]);let O=(0,_.useCallback)(async t=>{if(g.current)return;let n=a,l=s;t.strategy!==void 0&&(o(t.strategy),r?.(t.strategy)),t.stickyLimit!==void 0&&(c(t.stickyLimit),u(String(t.stickyLimit))),g.current=!0,h(!0),C(null),y.current+=1;let d=await Ho(e,t);y.current+=1,d.ok?(o(d.strategy),r?.(d.strategy),c(d.stickyLimit),u(String(d.stickyLimit)),p.current=!0,f(!0)):(C(i(`accountPool.strategyUpdateFailed`)),o(n),r?.(n),c(l),u(String(l))),g.current=!1,h(!1),D()},[e,r,D,s,a,i]),k=m||b||!d;return(0,J.jsxs)(`div`,{className:`card account-pool-strategy-card`,"aria-busy":m||!d&&!b,children:[b&&(0,J.jsx)(`div`,{className:`card-sub`,role:`alert`,children:i(`accountPool.strategyLoadFailed`)}),b&&(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm account-pool-strategy-card__retry`,onClick:()=>{E()},children:i(`common.retry`)}),!b&&(0,J.jsx)(Go,{strategy:a,stickyDraft:l,disabled:k,strategySelectId:`codex-pool-strategy`,stickyInputId:`codex-pool-sticky-limit`,onStrategyChange:e=>{k||e===a||O({strategy:e})},onStickyDraftChange:u,onStickyCommit:e=>{if(k)return;let t=Vo(e??l);if(t===null){u(String(s)),C(i(`accountPool.stickyLimitInvalid`));return}if(t===s){u(String(t));return}O({stickyLimit:t})}}),S&&(0,J.jsx)(`div`,{role:`alert`,className:`card-sub account-pool-strategy-card__error`,children:S})]})}function Jo({t:e,open:t,onToggle:n,children:r}){return(0,J.jsxs)(`section`,{className:`codex-auth-advanced`,children:[(0,J.jsxs)(`button`,{type:`button`,className:`codex-auth-advanced__toggle`,"aria-expanded":t,"aria-controls":`codex-auth-advanced-boxes`,onClick:n,children:[(0,J.jsx)(`span`,{children:e(`codexAuth.advancedSettings`)}),(0,J.jsx)(Se,{width:12,height:12,"aria-hidden":`true`,className:t?`codex-auth-advanced__chevron is-open`:`codex-auth-advanced__chevron`})]}),t&&(0,J.jsx)(`div`,{id:`codex-auth-advanced-boxes`,className:`codex-auth-advanced__boxes`,children:r})]})}function Yo(e,t){let[n,r]=(0,_.useState)(80),[i,a]=(0,_.useState)(`80`),[o,s]=(0,_.useState)(!1),[c,l]=(0,_.useState)(!1),[u,d]=(0,_.useState)(!1),[f,p]=(0,_.useState)(null),m=(0,_.useRef)(80),h=(0,_.useRef)(!1),g=(0,_.useRef)(80),v=(0,_.useRef)(!1),y=(0,_.useRef)(!1),b=(0,_.useRef)(!1),x=(0,_.useRef)(0),S=(0,_.useRef)(null),C=(0,_.useRef)(null),w=(0,_.useCallback)(e=>{m.current=e,h.current=!0,s(!0),r(e),e>0&&(g.current=e),a(String(e>0?e:g.current))},[]),T=(0,_.useCallback)(e=>{if(v.current||y.current){S.current=e;return}S.current=null,w(e)},[w]),E=(0,_.useCallback)(()=>{let e=S.current;return e!==null&&(S.current=null,w(e),!0)},[w]),D=(0,_.useCallback)(()=>{C.current!==null&&(window.clearTimeout(C.current),C.current=null),p(null)},[]),O=(0,_.useCallback)((e,t)=>{C.current!==null&&window.clearTimeout(C.current),p({tone:t?`err`:`ok`,message:e}),C.current=window.setTimeout(()=>{p(null),C.current=null},5e3)},[]);(0,_.useEffect)(()=>()=>{C.current!==null&&window.clearTimeout(C.current)},[]);let k=(0,_.useCallback)(()=>(h.current||l(!1),x.current),[]),A=(0,_.useCallback)((e,t)=>{l(!1);let n=So(e),r=xo(v.current,y.current,t,x.current);r===`defer`?S.current=vo(n):r===`apply`&&T(vo(n))},[T]),j=(0,_.useCallback)(e=>{h.current||v.current||y.current||(l(!1),w(vo(So(e))))},[w]),M=(0,_.useCallback)(()=>{h.current||l(!0)},[]),N=(0,_.useCallback)(async(n,r,i=!0)=>{if(y.current)return!1;y.current=!0,v.current=!1,D(),d(!0),x.current+=1;try{let a=await wo(e,n);return x.current+=1,a?(S.current=null,w(n),i&&O(t.updated,!1)):(E()||w(r),O(t.updateFailed,!0)),a}finally{y.current=!1,d(!1)}},[e,w,D,t.updateFailed,t.updated,E,O]),P=(0,_.useCallback)(()=>{v.current=!1;let e=m.current;E()||a(String(e>0?e:g.current)),O(t.invalid,!0)},[t.invalid,E,O]),F=(0,_.useCallback)(()=>{v.current=!1,b.current=!0,D();let e=m.current;E()||a(String(e>0?e:g.current))},[D,E]),I=(0,_.useCallback)(async()=>{if(b.current)return b.current=!1,!0;if(!h.current||y.current)return!1;let e=m.current;v.current=!1;let t=yo(i);return t===null?(P(),!1):t===e?(E()||a(String(t)),!0):N(t,e)},[i,E,P,N]),L=(0,_.useCallback)(async()=>{if(!h.current||y.current)return!1;let e=m.current;v.current=!1;let t=Co(e,i,g.current),n=await N(t.threshold,e);return n?(g.current=t.lastEnabled,t.threshold===0&&a(String(t.lastEnabled)),n):!1},[i,N]);return{threshold:n,draft:i,hydrated:o,saving:u,loadError:c,feedback:f,beginServerRead:k,acceptServerRead:A,hydrateServerValue:j,rejectServerRead:M,setDraft:(0,_.useCallback)(e=>{h.current&&(v.current=!0,b.current=!1,D(),a(e))},[D]),setEditing:(0,_.useCallback)(e=>{v.current=e},[]),commit:I,cancel:F,toggle:L,retry:(0,_.useCallback)(()=>{l(!1),D()},[D])}}function Xo({value:e,disabled:t=!1,selectId:n,onChange:r}){let i=Q(),a=fo(e),o=i(`accountPool.priorityHint`),s=`${n}-hint`,c=lo.map(e=>({value:String(e),label:go(i,e)})),l=mo(a)?c:[{value:String(a),label:go(i,a)},...c];return(0,J.jsxs)(`div`,{className:`codex-account-priority`,children:[(0,J.jsx)(`label`,{className:`codex-account-priority-label`,htmlFor:n,children:i(`accountPool.priority`)}),(0,J.jsx)(Dt,{id:n,value:String(a),options:l,disabled:t,label:i(`accountPool.priorityAria`),describedBy:s,title:o,onChange:e=>r(fo(Number.parseInt(e,10)))}),(0,J.jsx)(`span`,{id:s,className:`sr-only`,children:o})]})}function Zo({value:e}){let t=Q(),n=fo(e);return n===0?null:(0,J.jsx)(`span`,{className:`badge badge-muted`,children:go(t,n)})}function Qo(e,t){return`${e??``}\0${JSON.stringify(t)}`}var $o={month:`short`,day:`numeric`,year:`numeric`},es={month:`short`,day:`numeric`,year:`numeric`,hour:`2-digit`,minute:`2-digit`},ts=new Map;function ns(e,t){let n=Qo(e,t),r=ts.get(n);return r||(r=new Intl.DateTimeFormat(e,t),ts.set(n,r)),r}function rs(e,t){let n=new Date(e);return Number.isNaN(n.getTime())?`—`:ns(t,$o).format(n)}function is(e,t){let n=new Date(e);return Number.isNaN(n.getTime())?`—`:ns(t,es).format(n)}var as=new Intl.NumberFormat(`en-US`,{style:`currency`,currency:`USD`,currencyDisplay:`narrowSymbol`,minimumFractionDigits:4,maximumFractionDigits:4});function os(e,t){return!Number.isFinite(e)||e<0?`—`:`~${as.format(e)}`}function ss(e,t){return rs(e,t)}function cs(e,t){return is(e,t)}function ls(e){return Math.max(0,Math.ceil((new Date(e).getTime()-Date.now())/864e5))}function us({index:e,grantedAt:t,expiresAt:n,isNext:r,locale:i,t:a}){let o=ls(n),s=o<=7;return(0,J.jsxs)(`div`,{className:`credit-item${r?` credit-next`:``}`,children:[(0,J.jsxs)(`div`,{className:`credit-item-head`,children:[(0,J.jsx)(Oe,{width:13}),(0,J.jsx)(`span`,{className:`credit-item-label`,children:r?a(`codexAuth.creditNext`):a(`codexAuth.creditLabel`,{n:String(e+1)})}),r&&(0,J.jsx)(`span`,{className:`badge badge-amber text-micro`,style:{padding:`1px 6px`},children:a(`codexAuth.creditNextBadge`)})]}),(0,J.jsxs)(`div`,{className:`credit-item-dates`,children:[(0,J.jsx)(`span`,{children:a(`codexAuth.creditGranted`,{date:ss(t,i)})}),(0,J.jsx)(`span`,{className:s?`credit-urgent`:``,children:a(`codexAuth.creditExpires`,{date:cs(n,i),days:String(o)})})]})]})}function ds({account:e,onClick:t,t:n}){let r=e.quota?.resetCredits;return e.quota==null?(0,J.jsxs)(`span`,{className:`badge badge-muted codex-ticket-badge-slot`,"aria-hidden":`true`,children:[(0,J.jsx)(Oe,{width:12}),`0`]}):r===void 0?null:(0,J.jsxs)(`button`,{type:`button`,className:`badge ${typeof r==`number`&&r>0?`badge-amber`:`badge-muted`} badge-clickable`,onClick:e=>{e.stopPropagation(),t()},"aria-label":n(`codexAuth.resetCreditsAria`,{count:String(r)}),children:[(0,J.jsx)(Oe,{width:12}),r]})}function fs({t:e,paused:t,saving:n}){let r=e(`codexAuth.pause`),i=e(`codexAuth.resume`),a=e(`common.saving`),o=n?a:t?i:r,s=Math.max(r.length,i.length,a.length);return(0,J.jsx)(`span`,{className:`codex-auth-pause-label`,style:{minWidth:`${s}ch`},children:o})}function ps({pool:e,activeId:t,accountModeState:n,switchActionLabel:r,threshold:i,onOpenReset:a,onSwitch:o,onTogglePause:s,pauseUpdatingId:c,pauseBusy:l,onPriorityChange:u,priorityUpdatingId:d,switchingId:f,pinnedId:p=null,onReauth:m,onEditAlias:h,onRemove:g,onCopyDoctor:v,doctorCopyOutcomeFor:y}){let b=Q(),x=e=>!e.paused&&t===e.id,S=Xa(),[C,w]=(0,_.useState)(new Set);return(0,J.jsx)(J.Fragment,{children:e.map(e=>{let t=e.health?.status,_=!!e.needsReauth||Ia(t),T=Ra(t),E=Va(b,e.health),D=Ha(b,`codex`,e.id,e.health);return(0,J.jsxs)(`div`,{className:`card ${x(e)?`card-active`:``}`,style:{marginBottom:8},children:[(0,J.jsxs)(`div`,{className:`card-head`,children:[(0,J.jsx)(`span`,{className:`dot ${_?`dot-amber`:x(e)?`dot-blue`:`dot-muted`}`}),(0,J.jsx)(`strong`,{children:e.alias??e.email}),(0,J.jsxs)(`span`,{className:`card-badges`,children:[e.plan&&(0,J.jsx)(`span`,{className:`badge badge-green`,children:e.plan}),e.paused&&(0,J.jsx)(`span`,{className:`badge badge-muted`,title:b(`codexAuth.pausedHint`),children:b(`codexAuth.paused`)}),(0,J.jsx)(Zo,{value:e.priority}),e.id===p&&!e.paused&&(0,J.jsx)(`span`,{className:`badge badge-muted`,children:b(`codexAuth.pinned`)}),(0,J.jsx)(ds,{t:b,account:e,onClick:()=>a(e)}),E&&(0,J.jsx)(`span`,{className:Fa(t),children:E}),_&&!E&&(0,J.jsx)(`span`,{className:`badge badge-amber`,children:b(`codexAuth.needsReauth`)}),x(e)&&!_&&!T&&(0,J.jsx)(`span`,{className:`badge badge-primary`,children:b(n===`direct`?`codexAuth.poolPrepared`:`codexAuth.nextSession`)})]}),!e.paused&&(!x(e)||p!==e.id)&&!_&&!T&&(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm codex-account-switch`,onClick:()=>o(e),children:r}),_&&(0,J.jsx)(`button`,{type:`button`,className:`btn btn-primary btn-sm`,onClick:()=>m(e.id),children:b(`codexAuth.reauthenticate`)}),v&&za(t)&&(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm codex-auth-action-btn`,onClick:()=>v(e.id),children:(0,J.jsx)(`span`,{"aria-live":`polite`,children:Ga(b,y?.(e.id))})}),(0,J.jsxs)(`button`,{type:`button`,className:`btn btn-sm btn-ghost codex-auth-action-btn`,onClick:()=>s(e),disabled:l,title:e.paused?b(`codexAuth.pausedHint`):void 0,"aria-label":e.paused?`${b(`codexAuth.resume`)}. ${b(`codexAuth.pausedHint`)}`:b(`codexAuth.pause`),children:[e.paused?(0,J.jsx)(me,{width:14}):(0,J.jsx)(X,{width:14}),(0,J.jsx)(fs,{t:b,paused:e.paused,saving:c===e.id})]}),(0,J.jsxs)(`details`,{className:`codex-account-more card-right`,open:C.has(e.id),onToggle:t=>{let n=t.currentTarget.open;w(t=>{let r=new Set(t);return n?r.add(e.id):r.delete(e.id),r})},children:[(0,J.jsx)(`summary`,{className:`btn btn-ghost btn-sm`,"aria-label":`${b(`codexAuth.moreActions`)} — ${e.email}`,title:b(`codexAuth.moreActions`),children:`⋯`}),(0,J.jsxs)(`div`,{className:`codex-account-more-body`,children:[(0,J.jsxs)(`span`,{className:`mono text-caption muted`,children:[b(`prov.accountId`),`: `,Na(e.id)]}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:()=>S.copy(e.id,e.id),children:S.outcomeFor(e.id)===`copied`?b(`startup.copied`):b(`codexAuth.copyId`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:()=>void h(e),children:b(`prov.editAlias`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn-icon btn-icon-danger`,"aria-label":`${b(`common.remove`)} — ${e.email}`,title:`${b(`common.remove`)} — ${e.email}`,onClick:t=>{t.stopPropagation(),g(e.id)},children:(0,J.jsx)(de,{width:14})})]})]})]}),(0,J.jsxs)(`div`,{className:`codex-account-identity`,children:[(0,J.jsxs)(`div`,{className:`codex-account-identity-copy`,children:[e.email,e.plan?` · ${e.plan}`:``]}),(fo(e.priority)!==0||C.has(e.id))&&(0,J.jsx)(Xo,{value:e.priority,selectId:`codex-account-priority-${e.id}`,disabled:d!==null||f!==null,onChange:t=>u(e,t)})]}),D&&(0,J.jsx)(`div`,{className:`card-sub faint`,children:D}),T&&(0,J.jsx)(`div`,{className:`card-sub faint`,children:b(`pws.healthCooldownHint`)}),_?(0,J.jsx)(`div`,{className:`card-sub faint`,children:b(`codexAuth.tokenExpired`)}):!T&&(0,J.jsx)(ia,{quota:e.quota,plan:e.plan,threshold:i,t:b,pending:e.quota==null})]},e.id)})})}function ms({onReauth:e}){let t=Q();return(0,J.jsxs)(`div`,{className:`notice-warn`,style:{marginBottom:12,display:`flex`,alignItems:`center`,justifyContent:`space-between`,gap:12,flexWrap:`wrap`},children:[(0,J.jsxs)(`span`,{children:[(0,J.jsx)(_e,{width:14}),` `,t(`codexAuth.tokenExpired`)]}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-primary btn-sm`,onClick:e,children:t(`codexAuth.reauthenticate`)})]})}function hs({confirm:e,mainEmail:t,accountModeState:n,switchingId:r,orderBusy:i=!1,onCancel:a,onConfirm:o}){let s=Q(),c=(0,_.useRef)(null);(0,_.useEffect)(()=>{let e=c.current;e&&!e.open&&e.showModal()},[]);let l=(0,_.useCallback)(e=>{e.preventDefault(),a()},[a]);return(0,J.jsxs)(`dialog`,{ref:c,className:`modal-overlay`,"aria-labelledby":`codex-switch-title`,onCancel:l,children:[(0,J.jsx)(`button`,{type:`button`,className:`modal-backdrop-dismiss`,"aria-label":s(`common.close`),tabIndex:-1,onClick:a}),(0,J.jsxs)(`div`,{className:`modal-card`,onClick:e=>e.stopPropagation(),role:`document`,children:[(0,J.jsx)(`h3`,{id:`codex-switch-title`,children:n===`direct`?s(`codexAuth.preparePoolTitle`):e.id===`__main__`?s(`codexAuth.switchBack`):s(`codexAuth.switchTitle`)}),(0,J.jsx)(`p`,{className:`modal-desc`,children:n===`direct`?s(`codexAuth.preparePoolDesc`):e.id===`__main__`?s(`codexAuth.switchBackDesc`):s(`codexAuth.switchDesc`)}),(0,J.jsxs)(`div`,{className:`card`,style:{margin:`12px 0`},children:[(0,J.jsx)(`strong`,{children:e.id===`__main__`?t||s(`codexAuth.codexApp`):e.email}),e.plan&&(0,J.jsx)(`span`,{className:`badge badge-green`,style:{marginLeft:8},children:e.plan})]}),e.id!==`__main__`&&(0,J.jsxs)(`div`,{className:`notice-warn`,children:[(0,J.jsx)(_e,{width:14}),` `,s(`codexAuth.cacheWarning`)]}),(0,J.jsxs)(`div`,{className:`modal-actions`,children:[(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost`,onClick:a,children:s(`codexAuth.cancel`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-primary`,disabled:!!r||i,onClick:o,children:s(r?`pws.accountSwitching`:n===`direct`?`codexAuth.prepareForPool`:`codexAuth.setAsNext`)})]})]})]})}function gs({resetPopup:e,resetConfirm:t,creditDetails:n,creditDetailsLoading:r,redeeming:i,onClose:a,onShowConfirm:o,onCancelConfirm:s,onRedeem:c}){let{locale:l,t:u}=ct(),d=(0,_.useRef)(null);(0,_.useEffect)(()=>{let e=d.current;e&&!e.open&&e.showModal()},[]);let f=(0,_.useCallback)(e=>{e.preventDefault(),a()},[a]);return(0,J.jsxs)(`dialog`,{ref:d,className:`modal-overlay`,"aria-labelledby":`codex-reset-title`,onCancel:f,children:[(0,J.jsx)(`button`,{type:`button`,className:`modal-backdrop-dismiss`,"aria-label":u(`common.close`),tabIndex:-1,onClick:a}),(0,J.jsx)(`div`,{className:`modal-card`,onClick:e=>e.stopPropagation(),role:`document`,children:t?(0,J.jsxs)(J.Fragment,{children:[(0,J.jsxs)(`div`,{style:{textAlign:`center`,padding:`12px 0`},children:[(0,J.jsx)(`div`,{className:`confirm-icon`,children:(0,J.jsx)(_e,{width:22})}),(0,J.jsx)(`h3`,{id:`codex-reset-title`,children:u(`codexAuth.confirmResetTitle`)}),(0,J.jsx)(`p`,{className:`modal-desc`,children:u(`codexAuth.confirmResetDesc`,{count:String(e.quota?.resetCredits??0)})}),n&&n[0]&&(0,J.jsx)(`p`,{className:`faint text-label`,children:u(`codexAuth.confirmWhichCredit`,{date:ss(n[0].granted_at,l)})}),(0,J.jsx)(`p`,{className:`faint text-label`,children:u(`codexAuth.irreversible`)})]}),(0,J.jsxs)(`div`,{className:`modal-actions`,children:[(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost`,onClick:s,children:u(`codexAuth.cancel`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-primary`,onClick:c,disabled:i,children:u(i?`codexAuth.redeeming`:`codexAuth.useCredit`)})]})]}):(0,J.jsxs)(J.Fragment,{children:[(0,J.jsxs)(`h3`,{id:`codex-reset-title`,children:[(0,J.jsx)(Oe,{width:16}),` `,u(`codexAuth.resetCreditsTitle`)]}),(0,J.jsxs)(`div`,{className:`card-sub`,children:[e.email,e.plan?` · ${e.plan}`:``]}),(0,J.jsx)(`div`,{style:{margin:`16px 0`},children:(e.quota?.resetCredits??0)>0?(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)(`p`,{style:{marginBottom:12},children:u(`codexAuth.resetCreditsAvailable`,{count:String(e.quota?.resetCredits??0)})}),r&&(0,J.jsx)(`p`,{className:`faint text-label`,children:u(`common.loading`)}),n&&n.length>0&&(0,J.jsx)(`div`,{className:`credit-list`,children:n.map((e,t)=>(0,J.jsx)(us,{index:t,grantedAt:e.granted_at,expiresAt:e.expires_at,isNext:t===0,locale:l,t:u},`${e.granted_at}:${e.expires_at}`))}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-primary`,style:{marginTop:12,width:`100%`},onClick:o,disabled:i,children:u(`codexAuth.useOneCredit`)}),(0,J.jsx)(`p`,{className:`card-sub text-caption`,style:{marginTop:8,textAlign:`center`},children:u(`codexAuth.fifoNote`)})]}):(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)(`p`,{className:`faint`,children:u(`codexAuth.noResetCredits`)}),(0,J.jsx)(`p`,{className:`modal-desc`,children:u(`codexAuth.earnCreditsHint`)})]})})]})})]})}function _s({t:e,main:t,isMainActive:n,accountModeState:r,threshold:i,switchActionLabel:a,onSwitch:o,onTogglePause:s,pauseUpdatingId:c,pauseBusy:l,onPriorityChange:u,priorityUpdatingId:d,switchingId:f,pinnedId:p=null,onOpenReset:m,onCopyDoctor:h,doctorCopyOutcomeFor:g}){let _=e(`codexAuth.codexApp`),v=t?.id??`__main__`,y={id:`__main__`,email:t?.email||_,plan:t?.plan,isMain:!0,paused:t?.paused??!1,priority:t?.priority??0,hasCredential:!0,quota:t?.quota??null},b=!!t?.needsReauth||Ia(t?.health?.status),x=Ra(t?.health?.status),S=Va(e,t?.health),C=t?Ha(e,`codex`,v,t.health):null;return(0,J.jsxs)(`div`,{className:`card ${n?`card-active`:``}`,style:{marginBottom:12},children:[(0,J.jsxs)(`div`,{className:`card-head`,children:[(0,J.jsx)(`span`,{className:`dot ${b?`dot-amber`:`dot-green`}`}),(0,J.jsx)(`strong`,{children:e(`codexAuth.mainAccount`)}),(0,J.jsxs)(`span`,{className:`card-badges`,children:[t?.plan&&(0,J.jsx)(`span`,{className:`badge badge-green`,children:t.plan}),t?.paused&&(0,J.jsx)(`span`,{className:`badge badge-muted`,title:e(`codexAuth.pausedHint`),children:e(`codexAuth.paused`)}),(0,J.jsx)(Zo,{value:y.priority}),p===`__main__`&&!t?.paused&&(0,J.jsx)(`span`,{className:`badge badge-muted`,children:e(`codexAuth.pinned`)}),t&&(0,J.jsx)(ds,{t:e,account:{...t,id:`__main__`},onClick:()=>m({...t,id:`__main__`})}),S&&(0,J.jsx)(`span`,{className:Fa(t?.health?.status),children:S}),b&&!S&&(0,J.jsx)(`span`,{className:`badge badge-amber`,children:e(`codexAuth.needsReauth`)}),!t?.paused&&(0,J.jsx)(`span`,{className:`badge ${n?`badge-primary`:`badge-muted`}`,children:e(n?r===`direct`?`codexAuth.poolPrepared`:`codexAuth.nextSession`:`codexAuth.current`)})]}),!t?.paused&&(!n||p!==`__main__`)&&!b&&!x&&(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm codex-account-switch`,onClick:()=>o(y),children:a}),h&&za(t?.health?.status)&&(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm codex-auth-action-btn`,onClick:()=>h(v),children:(0,J.jsx)(`span`,{"aria-live":`polite`,children:Ga(e,g?.(v))})}),t&&(0,J.jsxs)(`button`,{type:`button`,className:`btn btn-sm btn-ghost codex-auth-action-btn`,onClick:()=>s(y),disabled:l,title:t.paused?e(`codexAuth.pausedHint`):void 0,"aria-label":t.paused?`${e(`codexAuth.resume`)}. ${e(`codexAuth.pausedHint`)}`:e(`codexAuth.pause`),children:[t.paused?(0,J.jsx)(me,{width:14}):(0,J.jsx)(X,{width:14}),(0,J.jsx)(fs,{t:e,paused:!!t.paused,saving:c===`__main__`})]}),(0,J.jsxs)(`span`,{className:`card-right`,children:[(0,J.jsx)(De,{width:14}),` `,e(`codexAuth.appLogin`)]})]}),(0,J.jsxs)(`div`,{className:`codex-account-identity`,children:[(0,J.jsxs)(`div`,{className:`codex-account-identity-copy`,children:[t?.email||e(`codexAuth.appLogin`),t?.plan?` · ${t.plan}`:``]}),t&&(0,J.jsx)(Xo,{value:y.priority,selectId:`codex-account-priority-${y.id}`,disabled:d!==null||f!==null,onChange:e=>u(y,e)})]}),C&&(0,J.jsx)(`div`,{className:`card-sub faint`,children:C}),x&&(0,J.jsx)(`div`,{className:`card-sub faint`,children:e(`pws.healthCooldownHint`)}),b?(0,J.jsx)(`div`,{className:`card-sub faint`,children:e(`codexAuth.mainTokenExpired`)}):!x&&(0,J.jsx)(ia,{quota:t?.quota??null,plan:t?.plan,threshold:i,t:e,pending:t!=null&&t.quota==null})]})}function vs({t:e,embedded:t,refreshingQuota:n,pausingExhausted:r,pauseBusy:i,actionFeedback:a,actionFeedbackTone:o,onRefresh:s,onPauseExhausted:c,sparkVisible:l,sparkBusy:u,onToggleSpark:d}){return(0,J.jsxs)(`div`,{className:t?`row`:`page-head codex-auth-page-head`,style:t?{justifyContent:`flex-end`,marginBottom:8}:void 0,children:[!t&&(0,J.jsx)(`h2`,{className:`page-title`,children:e(`nav.codexAuth`)}),(0,J.jsxs)(`div`,{className:t?`row`:`codex-auth-page-head__actions`,children:[(0,J.jsx)(`span`,{className:`codex-auth-page-head__feedback${o===`ok`?` is-ok`:``}${o===`warn`?` is-warn`:``}${o===`err`?` is-err`:``}`,role:`status`,"aria-live":`polite`,children:a??``}),l!==void 0&&d&&(0,J.jsxs)(`span`,{className:`codex-auth-spark-toggle`,children:[(0,J.jsx)(`span`,{className:`codex-auth-spark-toggle__label`,children:e(`codexAuth.sparkQuota`)}),(0,J.jsx)(`button`,{type:`button`,className:`toggle ${l?`on`:``}`,onClick:d,disabled:!!u,"aria-pressed":l,"aria-label":e(`codexAuth.sparkQuota`),title:e(`codexAuth.sparkQuotaHint`),children:(0,J.jsx)(`span`,{className:`toggle-knob`})})]}),(0,J.jsxs)(`button`,{type:`button`,className:`btn btn-sm btn-ghost codex-auth-action-btn`,onClick:c,disabled:n||r||!!i,children:[(0,J.jsx)(X,{width:14}),` `,e(r?`codexAuth.pausingExhausted`:`codexAuth.pauseExhausted`)]}),(0,J.jsxs)(`button`,{type:`button`,className:`btn btn-sm btn-ghost codex-auth-action-btn`,onClick:s,disabled:n||r||!!i,children:[(0,J.jsx)(pe,{width:14}),` `,e(n?`codexAuth.refreshingQuota`:`codexAuth.refreshQuota`)]})]})]})}function ys({t:e,loadState:t,accountsCount:n,onRetry:r}){return t===`loading`&&n===0?(0,J.jsxs)(`div`,{className:`codex-auth-load-skeleton`,role:`status`,"aria-live":`polite`,"aria-busy":`true`,children:[(0,J.jsxs)(`div`,{className:`card codex-auth-load-skeleton__main`,style:{marginBottom:12},"aria-hidden":`true`,children:[(0,J.jsxs)(`div`,{className:`card-head`,children:[(0,J.jsx)(`span`,{className:`dot dot-muted`}),(0,J.jsx)(`strong`,{children:e(`codexAuth.mainAccount`)}),(0,J.jsxs)(`span`,{className:`card-badges`,children:[(0,J.jsxs)(`span`,{className:`badge badge-muted codex-ticket-badge-slot`,"aria-hidden":`true`,children:[(0,J.jsx)(Oe,{width:12}),`0`]}),(0,J.jsx)(`span`,{className:`badge badge-primary`,children:e(`codexAuth.nextSession`)})]}),(0,J.jsxs)(`button`,{type:`button`,className:`btn btn-sm btn-ghost`,tabIndex:-1,disabled:!0,children:[(0,J.jsx)(X,{width:14}),` `,e(`codexAuth.pause`)]}),(0,J.jsxs)(`span`,{className:`card-right`,children:[(0,J.jsx)(De,{width:14}),` `,e(`codexAuth.appLogin`)]})]}),(0,J.jsxs)(`div`,{className:`card-sub`,children:[(0,J.jsx)(`span`,{className:`codex-auth-load-skeleton__strut`,children:e(`codexAuth.appLogin`)}),(0,J.jsx)(`span`,{className:`codex-auth-load-skeleton__line codex-auth-load-skeleton__line--sub`})]}),(0,J.jsx)(ia,{quota:null,threshold:0,t:e,pending:!0})]}),(0,J.jsxs)(`div`,{className:`section-sep`,"aria-hidden":`true`,children:[(0,J.jsx)(`span`,{className:`section-label`,children:e(`codexAuth.accountPool`)}),(0,J.jsx)(`div`,{className:`sep-line`}),(0,J.jsxs)(`button`,{type:`button`,className:`btn btn-sm btn-ghost`,tabIndex:-1,disabled:!0,children:[(0,J.jsx)(fe,{width:14}),` `,e(`codexAuth.add`)]})]}),(0,J.jsx)(`div`,{className:`empty codex-auth-pool-empty codex-auth-load-skeleton__empty`,"aria-hidden":`true`,children:(0,J.jsx)(`div`,{className:`title`,children:e(`codexAuth.noPool`)})}),(0,J.jsx)(`span`,{className:`sr-only`,children:e(`pws.accountsLoading`)})]}):t===`error`?(0,J.jsxs)(`div`,{className:`pwi-auth-state pwi-auth-state--error`,role:`alert`,children:[(0,J.jsx)(`span`,{children:e(`codexAuth.loadFailed`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:r,children:e(`pws.retryAccounts`)})]}):null}function bs(e,t){return t===void 0?e(`codexAuth.resetSuccessGeneric`):e(`codexAuth.resetSuccess`,{remaining:String(t)})}async function xs(e,t,n,r){try{let i=await Ft(await fetch(`${e}/api/codex-auth/reset-credits/consume`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({accountId:t})}));return i?i.code===`reset`||i.code===`already_redeemed`?(await r(!0),{ok:!0,close:!0,toast:bs(n,typeof i.remaining==`number`&&Number.isFinite(i.remaining)?Math.max(0,i.remaining):void 0)}):{ok:!1,close:!0,toast:n(i.code===`nothing_to_reset`?`codexAuth.resetNothingToReset`:i.code===`no_credit`?`codexAuth.resetNoCredit`:`codexAuth.resetError`)}:{ok:!1,toast:n(`codexAuth.resetError`)}}catch{return{ok:!1,toast:n(`codexAuth.resetError`)}}}var Ss=`ocx doctor`;function Cs({apiBase:e,accountModeState:t=null,banner:n=null,embedded:r=!1,onActiveNeedsReauthChange:i,controller:a,advancedExtras:o=null}){let s=Q(),c=Yo(e,{updated:s(`codexAuth.autoSwitchUpdated`),updateFailed:s(`codexAuth.autoSwitchUpdateFailed`),invalid:s(`codexAuth.autoSwitchThresholdInvalid`)}),[l,u]=(0,_.useState)(null),{beginServerRead:d,acceptServerRead:f,rejectServerRead:p,hydrateServerValue:m}=c,h=Do(e,!a),g=a??h,{accounts:v,activeId:y,loadState:b,switchingId:x,pauseUpdatingId:S,priorityUpdatingId:C,pausingExhausted:w,activePinnedId:T,load:E}=g,[D,O]=(0,_.useState)(null),[k,A]=(0,_.useState)(!1),[j,M]=(0,_.useState)(!1),[N,P]=(0,_.useState)(null),[F,I]=(0,_.useState)(null),[L,R]=(0,_.useState)(null),z=(0,_.useRef)(null),[B,V]=(0,_.useState)(!1),[H,U]=(0,_.useState)(void 0),[W,ee]=(0,_.useState)(!1),[G,K]=(0,_.useState)(null),[q,Y]=(0,_.useState)(!1),[te,ne]=(0,_.useState)(!1),[re,ie]=(0,_.useState)(null),[ae,oe]=(0,_.useState)(!1),se=Xa(),ce=(0,_.useCallback)((e,t=`ok`)=>{z.current&&clearTimeout(z.current),I(e),R(t),z.current=setTimeout(()=>{I(null),R(null),z.current=null},5e3)},[]);(0,_.useEffect)(()=>()=>{z.current&&clearTimeout(z.current)},[]);let le=(0,_.useCallback)(e=>{se.copy(Ss,e)},[se]),{subscribeLoadObserver:ue,readLastThreshold:de}=g;(0,_.useEffect)(()=>ue({beginActiveRead:d,acceptActiveRead:f,rejectActiveRead:p}),[ue,d,f,p]),(0,_.useEffect)(()=>{let e=de();e!==void 0&&m(e)},[de,m]),(0,_.useEffect)(()=>{if(!k)return;let e=g.pauseRefresh();return()=>g.resumeRefresh(e)},[g,k]);let pe=y&&y!==`__main__`?v.find(e=>e.id===y):null,X=!pe?.paused&&La(pe);(0,_.useEffect)(()=>{i?.(X)},[X,i]);let me=(0,_.useCallback)(e=>{P(e),A(!0)},[]),he=(0,_.useCallback)(()=>{A(!1),P(null)},[]),ge=(0,_.useCallback)(e=>{g.syncAfterAccountAdded(),ce(s(e.catalogRefreshPending?`codexAuth.catalogRefreshPending`:`codexAuth.accountAdded`),e.catalogRefreshPending?`warn`:`ok`),he()},[he,g,ce,s]),_e=async e=>{let n=await g.switchAccount(e);if(!n.ok){if(n.reason===`busy`)return;ce(s(`codexAuth.switchFailed`),`err`);return}O(null);let r=n.activeId,i=r&&r!==`__main__`?v.find(e=>e.id===r)?.email??s(`pws.accountOrdinal`,{count:`1`}):s(`codexAuth.mainAccount`);ce(s(t===`direct`?`codexAuth.poolPreparedToast`:`codexAuth.switched`,{email:i}))},Z=async e=>{let t=window.prompt(s(`prov.aliasPrompt`),e.alias??``);if(t===null)return;let n=await g.saveAlias(e.id,t);ce(s(n.ok?`prov.aliasSaved`:`prov.aliasSaveFailed`),n.ok?`ok`:`err`)},ve=async e=>{let t=!e.paused,n=await g.setAccountPaused(e.id,t);!n.ok&&n.reason===`busy`||(O(t=>t?.id===e.id?null:t),ce(s(n.ok?t?`codexAuth.pauseSucceeded`:`codexAuth.resumeSucceeded`:t?`codexAuth.pauseFailed`:`codexAuth.resumeFailed`,{email:e.alias??e.email}),n.ok?`ok`:`err`))},ye=async(e,t)=>{if(t===e.priority)return;let n=await g.setAccountPriority(e.id,t);!n.ok&&n.reason===`busy`||ce(s(n.ok?`accountPool.priorityUpdated`:`accountPool.priorityUpdateFailed`,{email:e.alias??e.email}),n.ok?`ok`:`err`)},be=async e=>{let t=v.find(t=>t.id===e)?.email??s(`pws.accountOrdinal`,{count:`1`});if(!window.confirm(s(`codexAuth.removeConfirm`,{id:t})))return;let n=await g.removeAccount(e);n.ok?n.catalogRefreshPending&&ce(s(`codexAuth.catalogRefreshPending`),`warn`):ce(s(`codexAuth.removeFailed`),`err`)},xe=async()=>{V(!0);try{let e=await E(!0);ce(s(e?`codexAuth.quotaRefreshed`:`codexAuth.quotaRefreshFailed`),e?`ok`:`err`)}finally{V(!1)}};(0,_.useEffect)(()=>{let t=new AbortController;return fetch(`${e}/api/settings`,{signal:t.signal}).then(e=>e.ok?e.json():null).then(e=>{t.signal.aborted||typeof e?.showCodexSparkQuota!=`boolean`||U(e.showCodexSparkQuota)}).catch(()=>{}),()=>{t.abort()}},[e]);let Se=async()=>{if(W||H===void 0)return;let t=!H;ee(!0),U(t);try{let n=await fetch(`${e}/api/settings`,{method:`PUT`,headers:{"content-type":`application/json`},body:JSON.stringify({showCodexSparkQuota:t})});if(!n.ok)throw Error(`save`);let r=await n.json(),i=typeof r.showCodexSparkQuota==`boolean`?r.showCodexSparkQuota:t;U(i),ce(s(i?`codexAuth.sparkQuotaShown`:`codexAuth.sparkQuotaHidden`),`ok`),await E(!0)}catch{U(!t),ce(s(`codexAuth.sparkQuotaFailed`),`err`)}finally{ee(!1)}},Ce=async()=>{let e=await g.pauseExhaustedAccounts();!e.ok&&e.reason===`busy`||ce(e.ok?e.pausedCount>0?s(`codexAuth.pauseExhaustedSucceeded`,{count:String(e.pausedCount)}):s(`codexAuth.pauseExhaustedNone`):s(`codexAuth.pauseExhaustedFailed`),e.ok?`ok`:`err`)},we=async t=>{K(t),Y(!1),ie(null),oe(!0);try{let n=await Ft(await fetch(`${e}/api/codex-auth/reset-credits?accountId=${encodeURIComponent(t.id)}`));if(n){let e=(n.credits??[]).sort((e,t)=>new Date(e.granted_at).getTime()-new Date(t.granted_at).getTime());ie(e)}}catch{}finally{oe(!1)}},Te=async t=>{ne(!0);try{let n=await xs(e,t,s,E);n.close&&(K(null),Y(!1)),n.toast&&ce(n.toast,n.ok?`ok`:`err`)}finally{ne(!1)}},Ee=v.find(e=>e.isMain),De=v.filter(e=>!e.isMain),Oe=!Ee?.paused&&(!y||y===`__main__`),ke=s(t===`direct`?`codexAuth.prepareForPool`:`codexAuth.setAsNext`),Ae=S!==null||w,je=c.threshold??0,Me=!r;return(0,J.jsxs)(`div`,{children:[(0,J.jsx)(vs,{t:s,embedded:r,refreshingQuota:B,actionFeedback:F,actionFeedbackTone:L,pausingExhausted:w,pauseBusy:Ae,onRefresh:()=>{xe()},onPauseExhausted:()=>{Ce()},sparkVisible:H,sparkBusy:W,onToggleSpark:()=>{Se()}}),n,(0,J.jsx)(ys,{t:s,loadState:b,accountsCount:v.length,onRetry:()=>{E()}}),(b!==`loading`||v.length!==0)&&(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)(_s,{t:s,main:Ee,isMainActive:Oe,accountModeState:t,threshold:je,switchActionLabel:ke,onSwitch:O,onTogglePause:ve,pauseUpdatingId:S,pauseBusy:Ae,onPriorityChange:(e,t)=>{ye(e,t)},priorityUpdatingId:C,switchingId:x,pinnedId:T,onOpenReset:we,onCopyDoctor:Me?le:void 0,doctorCopyOutcomeFor:Me?se.outcomeFor:void 0}),(0,J.jsxs)(`div`,{className:`section-sep`,children:[(0,J.jsx)(`span`,{className:`section-label`,children:s(`codexAuth.accountPool`)}),(0,J.jsx)(`div`,{className:`sep-line`}),(0,J.jsxs)(`button`,{type:`button`,className:`btn btn-sm btn-ghost`,onClick:()=>A(!0),children:[(0,J.jsx)(fe,{width:14}),` `,s(`codexAuth.add`)]})]}),X&&pe&&(0,J.jsx)(ms,{onReauth:()=>me(pe.id)}),De.length===0&&(0,J.jsx)(Ot,{title:s(`codexAuth.noPool`)}),(0,J.jsx)(ps,{pool:De,activeId:y,accountModeState:t,switchActionLabel:ke,threshold:je,onOpenReset:we,onSwitch:O,onTogglePause:ve,pauseUpdatingId:S,pauseBusy:Ae,onPriorityChange:(e,t)=>{ye(e,t)},priorityUpdatingId:C,switchingId:x,pinnedId:T,onReauth:me,onEditAlias:Z,onRemove:be,onCopyDoctor:Me?le:void 0,doctorCopyOutcomeFor:Me?se.outcomeFor:void 0})]}),(0,J.jsx)(qo,{apiBase:e,subscribeLoadObserver:g.subscribeLoadObserver,readLastActive:g.readLastActive,onStrategyResolved:u}),(0,J.jsxs)(Jo,{t:s,open:j,onToggle:()=>M(e=>!e),children:[l!==null&&(0,J.jsx)(jo,{threshold:c.threshold,draft:c.draft,strategy:l,hydrated:c.hydrated,saving:c.saving,loadError:c.loadError,feedback:c.feedback,onDraftChange:c.setDraft,onEditingChange:c.setEditing,onCommit:c.commit,onCancel:c.cancel,onToggle:c.toggle,onRetry:()=>{c.retry(),E()}}),o]}),D&&(0,J.jsx)(hs,{confirm:D,mainEmail:Ee?.email,accountModeState:t,switchingId:x,orderBusy:C!==null,onCancel:()=>O(null),onConfirm:()=>{_e(D.id===`__main__`?`__main__`:D.id)}}),G&&(0,J.jsx)(gs,{resetPopup:G,resetConfirm:q,creditDetails:re,creditDetailsLoading:ae,redeeming:te,onClose:()=>{K(null),Y(!1),ie(null)},onShowConfirm:()=>Y(!0),onCancelConfirm:()=>Y(!1),onRedeem:()=>{Te(G.id)}}),k&&(0,J.jsx)(co,{apiBase:e,reauthAccountId:N??void 0,onClose:he,onAdded:ge})]})}var ws={"five-hour":`accountPool.quotaWindowFiveHour`,weekly:`accountPool.quotaWindowWeekly`,"max-utilization":`accountPool.quotaWindowMaxUtilization`};function Ts({apiBase:e,accountCount:t}){let n=Q(),[r,i]=(0,_.useState)(null),[a,o]=(0,_.useState)(`80`),[s,c]=(0,_.useState)(`1`),[l,u]=(0,_.useState)(!1),[d,f]=(0,_.useState)(null),[p,m]=(0,_.useState)(!1);(0,_.useEffect)(()=>{let t=!1,n=new AbortController;return Promise.resolve().then(()=>fetch(`${e}/api/oauth/accounts/pool?provider=anthropic`,{signal:n.signal})).then(e=>{if(!e.ok)throw Error(`load`);return e.json()}).then(e=>{if(t)return;let n=typeof e.autoSwitchThreshold==`number`?e.autoSwitchThreshold:80,r=Bo(e.stickyLimit);i({enabled:e.enabled===!0,threshold:n,strategy:Ro(e.strategy),stickyLimit:r,quotaWindow:zo(e.quotaWindow)}),o(String(n)),c(String(r)),m(!1)}).catch(()=>{t||n.signal.aborted||m(!0)}),()=>{t=!0,n.abort()}},[e]);let h=(0,_.useCallback)(async t=>{let a=r;i({enabled:t.enabled,threshold:t.threshold,strategy:t.strategy,stickyLimit:t.stickyLimit,quotaWindow:t.quotaWindow}),u(!0),f(null);try{let n=await fetch(`${e}/api/oauth/accounts/pool`,{method:`PUT`,headers:{"content-type":`application/json`},body:JSON.stringify({provider:`anthropic`,enabled:t.enabled,autoSwitchThreshold:t.threshold,strategy:t.strategy,stickyLimit:t.stickyLimit,quotaWindow:t.quotaWindow})});if(!n.ok)throw Error(`save`);let r=await n.json().catch(()=>null),a=Ro(r?.strategy??t.strategy),s=Bo(r?.stickyLimit??t.stickyLimit),l=zo(r?.quotaWindow??t.quotaWindow);i({enabled:t.enabled,threshold:t.threshold,strategy:a,stickyLimit:s,quotaWindow:l}),o(String(t.threshold)),c(String(s))}catch{f(n(`anthropicPool.saveFailed`)),a&&(i(a),o(String(a.threshold)),c(String(a.stickyLimit)))}finally{u(!1)}},[e,r,n]),g=r?.enabled===!0,v=r?.threshold??80,y=r?.strategy??`quota`,b=r?.stickyLimit??1,x=r?.quotaWindow??`five-hour`,S=y===`round-robin`,C=r===null&&!p,w=C||l||p||!g&&t<2;return(0,J.jsxs)(`div`,{className:`card anthropic-pool-card`,"aria-busy":C||l,children:[(0,J.jsxs)(`div`,{className:`card-row`,style:{alignItems:`flex-start`,gap:12},children:[(0,J.jsxs)(`div`,{style:{flex:1},children:[(0,J.jsx)(`strong`,{children:n(`anthropicPool.title`)}),(0,J.jsx)(`div`,{className:`card-sub`,style:{marginTop:4},children:p?n(`anthropicPool.loadFailed`):C?n(`common.loading`):g?v===0?n(`anthropicPool.enabledNoProactiveDesc`,{window:n(ws[x])}):n(`anthropicPool.enabledDesc`,{threshold:v,window:n(ws[x])}):n(`anthropicPool.disabledDesc`)})]}),(0,J.jsx)(`button`,{type:`button`,className:`toggle ${g?`on`:``}`,disabled:w,"aria-pressed":g,"aria-label":n(`anthropicPool.title`),title:n(g?`anthropicPool.on`:`anthropicPool.off`),onClick:()=>{h({enabled:!g,threshold:v,strategy:y,stickyLimit:b,quotaWindow:x})},children:(0,J.jsx)(`span`,{className:`toggle-knob`})})]}),(0,J.jsx)(`div`,{role:`alert`,className:`card-sub anthropic-pool-card__notice`,children:n(`anthropicPool.experimentalWarning`)}),t<2&&(0,J.jsx)(`div`,{className:`card-sub`,style:{marginTop:8},children:n(`anthropicPool.needTwoAccounts`)}),g&&r&&(0,J.jsxs)(J.Fragment,{children:[(0,J.jsxs)(`label`,{className:`field anthropic-pool-card__field`,children:[(0,J.jsx)(`span`,{className:`field-label`,children:n(`anthropicPool.threshold`)}),(0,J.jsx)(`input`,{className:`input mono`,type:`number`,min:0,max:100,step:1,value:a,disabled:l,"aria-label":n(`anthropicPool.thresholdAria`),onChange:e=>o(e.target.value),onBlur:()=>{let e=Number(a);if(!Number.isInteger(e)||e<0||e>100){o(String(v)),f(n(`anthropicPool.thresholdInvalid`));return}e!==v&&h({enabled:!0,threshold:e,strategy:y,stickyLimit:b,quotaWindow:x})}}),(0,J.jsx)(`div`,{className:`card-sub`,style:{marginTop:4},children:n(`anthropicPool.thresholdHelp`)})]}),(0,J.jsx)(Go,{strategy:y,stickyDraft:s,disabled:l,strategySelectId:`anthropic-pool-strategy`,stickyInputId:`anthropic-pool-sticky-limit`,onStrategyChange:e=>{e!==y&&h({enabled:!0,threshold:v,strategy:e,stickyLimit:b,quotaWindow:x})},onStickyDraftChange:c,onStickyCommit:e=>{let t=Vo(e??s);if(t===null){c(String(b)),f(n(`accountPool.stickyLimitInvalid`));return}if(t===b){c(String(t));return}h({enabled:!0,threshold:v,strategy:y,stickyLimit:t,quotaWindow:x})}}),(0,J.jsxs)(`div`,{className:`field anthropic-pool-card__field anthropic-pool-card__field--quota-window`,children:[(0,J.jsx)(`span`,{className:`field-label`,children:n(`accountPool.quotaWindow`)}),(0,J.jsx)(Dt,{id:`anthropic-pool-quota-window`,value:x,options:No.map(e=>({value:e,label:n(ws[e])})),disabled:l||S,label:n(`accountPool.quotaWindow`),onChange:e=>{let t=zo(e);t!==x&&h({enabled:!0,threshold:v,strategy:y,stickyLimit:b,quotaWindow:t})}}),(0,J.jsx)(`div`,{className:`card-sub`,style:{marginTop:4},children:n(`accountPool.quotaWindowDesc`)}),(0,J.jsx)(`div`,{className:`card-sub`,style:{marginTop:4},children:n(S?`accountPool.quotaWindowInert`:`accountPool.quotaWindowHint`)})]})]}),d&&(0,J.jsx)(`div`,{role:`alert`,className:`card-sub`,style:{marginTop:8,color:`var(--danger, #c44)`},children:d})]})}function Es({serverDefault:e=!0}){let t=Q(),[n,r]=(0,_.useState)(to);return(0,J.jsxs)(`label`,{className:`open-browser-pref`,children:[(0,J.jsx)(`input`,{type:`checkbox`,checked:!(n??e),onChange:e=>{let t=!e.target.checked;r(t),ro(t)}}),(0,J.jsxs)(`span`,{className:`open-browser-pref-copy`,children:[(0,J.jsx)(`span`,{className:`text-label`,children:t(`prov.dontOpenBrowser`)}),(0,J.jsx)(`span`,{className:`muted text-label`,children:t(`prov.dontOpenBrowserHint`)})]})]})}var Ds=4e3,Os=262144,ks=[],As=[];function js({initialState:e,onUpdateProvider:t}){let n=Q(),[r,i]=(0,_.useState)(e),[a,o]=(0,_.useState)(e),[s,c]=(0,_.useState)(!1),[l,u]=(0,_.useState)(``);e!==a&&(o(e),i(e));let d=r===`mixed`,f=async()=>{if(!t||s)return;let e=r!==!0;c(!0),u(``);try{let r=await t(`xai`,{xaiResponsesOptIn:e});if(!r.ok){u(r.error??n(`prov.updateFail`));return}i(r.xaiResponsesOptInState??e)}catch{u(n(`prov.networkError`))}finally{c(!1)}};return(0,J.jsxs)(`div`,{className:`pwi-auth-optin-row`,children:[(0,J.jsxs)(`div`,{className:`pwi-auth-optin-copy`,children:[(0,J.jsx)(`span`,{className:`pwi-auth-optin-label`,children:n(`pws.xaiResponsesOptIn`)}),(0,J.jsxs)(`span`,{className:`pwi-auth-row-secondary`,children:[n(`pws.xaiResponsesOptInDesc`),d&&(0,J.jsxs)(`span`,{className:`pwi-auth-optin-mixed`,children:[` `,n(`pws.xaiResponsesOptInMixed`)]})]}),l&&(0,J.jsx)(`span`,{className:`pwi-auth-optin-error`,role:`alert`,children:l})]}),(0,J.jsx)(Tt,{on:r===!0,mixed:d,onClick:()=>{f()},disabled:!t||s,label:n(`pws.xaiResponsesOptIn`)})]})}var Ms=new Set([`totalCount`,`importedCount`,`updatedCount`,`failedCount`,`unsupportedCount`,`results`]),Ns=new Set([`imported`,`updated`,`failed`,`unsupported`]),Ps={imported:new Set([`imported`]),updated:new Set([`updated`]),failed:new Set([`invalid_record`,`credential_rejected`,`identity_mismatch`,`missing_project`,`persist_failed`]),unsupported:new Set([`unsupported_provider`,`unsupported_format`])};function Fs(e){if(!e||typeof e!=`object`||Array.isArray(e))return!1;let t=Object.getPrototypeOf(e);return t===Object.prototype||t===null}function Is(e){return typeof e==`number`&&Number.isSafeInteger(e)&&e>=0}function Ls(e){if(!Fs(e)||Object.keys(e).some(e=>!Ms.has(e)))return null;let{totalCount:t,importedCount:n,updatedCount:r,failedCount:i,unsupportedCount:a,results:o}=e;if(!Is(t)||!Is(n)||!Is(r)||!Is(i)||!Is(a)||!Array.isArray(o)||o.length!==t||n+r+i+a!==t)return null;let s={imported:0,updated:0,failed:0,unsupported:0};for(let[e,t]of o.entries()){if(!Fs(t)||Object.keys(t).some(e=>![`index`,`status`,`code`].includes(e)))return null;let n=String(t.status),r=String(t.code);if(t.index!==e||!Ns.has(n)||!Ps[n]?.has(r))return null;s[n]+=1}return s.imported!==n||s.updated!==r||s.failed!==i||s.unsupported!==a?null:{importedCount:n,updatedCount:r,failedCount:i,unsupportedCount:a}}function Rs({item:e,apiBase:t,oauth:n,accounts:r=ks,keys:i=As,accountLoadState:a=`ready`,switchingAccountId:o=null,busy:s=!1,loginHint:c,authHandlers:l,onCodexActiveNeedsReauthChange:u,codexController:d,onUpdateProvider:f}){let p=Q(),[m,h]=(0,_.useState)(!1),[g,v]=(0,_.useState)(``),[y,b]=(0,_.useState)(!1),[x,S]=(0,_.useState)(!1),[C,w]=(0,_.useState)(`idle`),[T,E]=(0,_.useState)(null),[D,O]=(0,_.useState)(!1),k=(0,_.useRef)(null),[A,j]=(0,_.useState)(``),[M,N]=(0,_.useState)(!1),[P,F]=(0,_.useState)(``),[I,L]=(0,_.useState)(!0);(0,_.useEffect)(()=>{if(r.length===0){O(!1);return}if(!r.some(e=>e.quota==null&&!e.quotaUnavailable)){O(!1);return}O(!0);let e=window.setTimeout(()=>O(!1),Ds);return()=>window.clearTimeout(e)},[r]);let R=wa({...e,hasApiKey:e.hasApiKey||i.length>0}),z=R===`oauth-accounts`,B=R===`api-keys`;if(R===`codex-accounts`)return(0,J.jsxs)(`section`,{className:`pwi-section pwi-auth-section`,"aria-label":p(`pws.availableAccounts`),children:[(0,J.jsx)(`h3`,{className:`pwi-section-title`,children:p(`pws.availableAccounts`)}),(0,J.jsx)(`div`,{className:`pwi-auth-body`,children:(0,J.jsx)(Cs,{apiBase:t,embedded:!0,controller:d,onActiveNeedsReauthChange:u})})]});if(!R||!l)return null;let V=c?.provider===e.name?c:null,H=async()=>{let n=A.trim();if(!(!n||M)){N(!0),F(``);try{let r=await fetch(`${t}/api/oauth/login/code`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({provider:e.name,input:n})});if(!r.ok){let e=await r.json().catch(()=>({}));L(!1),F(p(`prov.pasteFail`,{error:e.error||r.statusText}));return}j(``),L(!0),F(p(`prov.pasteOk`))}catch{L(!1),F(p(`modal.networkError`))}finally{N(!1)}}},U=r.length>0||n?.loggedIn===!0,W=r.find(e=>e.active&&e.needsReauth),ee=!!W,G=async()=>{let t=g.trim();if(t){b(!0);try{await l.onAddApiKey(e.name,t)&&(v(``),h(!1))}finally{b(!1)}}},K=async n=>{if(!(!n||x)){S(!0),w(`idle`),E(null);try{if(!n.name.toLowerCase().endsWith(`.json`)||n.size>Os){w(`invalid`);return}let r;try{r=JSON.parse(await n.text())}catch{w(`invalid`);return}let i=await fetch(`${t}/api/oauth/accounts/import`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({provider:`google-antigravity`,format:`cockpit-tools`,document:r})});if(!i.ok){w(`failed`);return}let a=Ls(await i.json().catch(()=>null));if(!a){w(`failed`);return}E(a),w(`complete`);try{await l.onRetryAccounts?.(e.name)}catch{}}catch{w(`failed`)}finally{k.current&&(k.current.value=``),S(!1)}}};return(0,J.jsxs)(`section`,{className:`pwi-section pwi-auth-section`,"aria-label":p(z?`pws.availableAccounts`:`pws.apiKeys`),children:[(0,J.jsx)(`h3`,{className:`pwi-section-title`,children:p(z?`pws.availableAccounts`:`pws.apiKeys`)}),(0,J.jsxs)(`div`,{className:`pwi-auth-body`,children:[e.name===`xai`&&(0,J.jsx)(js,{initialState:e.xaiResponsesOptInState??!1,onUpdateProvider:f}),z&&(0,J.jsxs)(J.Fragment,{children:[e.name===`anthropic`&&(0,J.jsx)(Ts,{apiBase:t,accountCount:r.length}),e.name===`google-antigravity`&&(0,J.jsxs)(`div`,{className:`pwi-auth-add-key`,children:[(0,J.jsxs)(`div`,{children:[(0,J.jsx)(`div`,{id:`cockpit-import-description`,className:`pwi-auth-row-secondary`,children:p(`pws.cockpitImportDescription`)}),(0,J.jsx)(`label`,{className:`sr-only`,htmlFor:`cockpit-import-file`,children:p(`pws.cockpitImportFileLabel`)}),(0,J.jsx)(`input`,{ref:k,id:`cockpit-import-file`,type:`file`,accept:`application/json,.json`,className:`sr-only`,"aria-describedby":`cockpit-import-description cockpit-import-status`,disabled:x,onChange:e=>{K(e.currentTarget.files?.[0])}})]}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,disabled:x,onClick:()=>k.current?.click(),children:p(x?`pws.cockpitImporting`:`pws.cockpitImportChooseFile`)}),(0,J.jsxs)(`div`,{id:`cockpit-import-status`,role:`status`,"aria-live":`polite`,children:[C===`invalid`&&p(`pws.cockpitImportInvalid`),C===`failed`&&p(`pws.cockpitImportFailed`),C===`complete`&&T&&p(`pws.cockpitImportComplete`,{imported:T.importedCount,updated:T.updatedCount,failed:T.failedCount,unsupported:T.unsupportedCount})]})]}),(0,J.jsxs)(`div`,{className:`pwi-auth-status-row`,children:[(0,J.jsx)(`span`,{className:`pwi-auth-dot ${ee?`pwi-auth-dot--warn`:U?`pwi-auth-dot--ok`:`pwi-auth-dot--off`}`,"aria-hidden":`true`}),(0,J.jsx)(`span`,{className:`pwi-auth-status-text`,children:U?r.length>0?p(`pws.loggedInTitle`):n?.email??p(`pws.loggedInTitle`):n?.error||p(`pws.notLoggedInTitle`)}),(0,J.jsxs)(`span`,{className:`pwi-auth-actions`,children:[W&&(0,J.jsx)(`button`,{type:`button`,className:`btn btn-primary btn-sm`,disabled:s,onClick:()=>void l.onReauth(e.name,W.id),children:p(`pws.reauthenticate`)}),U?(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:()=>void l.onLogout(e.name),children:p(`prov.logout`)}):(0,J.jsxs)(`button`,{type:`button`,className:`btn btn-primary btn-sm`,disabled:s,onClick:()=>void l.onLogin(e.name,!1),children:[s?(0,J.jsx)(`span`,{className:`pwi-spin-inline`,"aria-hidden":`true`}):(0,J.jsx)(De,{style:{width:13,height:13},"aria-hidden":`true`}),p(s?`prov.waitingBrowser`:`prov.login`)]})]})]}),!s&&(0,J.jsx)(Es,{}),s&&V&&(0,J.jsxs)(`div`,{className:`pwi-auth-wait`,children:[(0,J.jsx)(`span`,{className:`pwi-spin-inline`,"aria-hidden":`true`}),(0,J.jsxs)(`div`,{className:`pwi-auth-wait-copy`,children:[(0,J.jsx)(`div`,{className:`pwi-auth-wait-title`,children:p(`prov.waitingBrowser`)}),(0,J.jsx)(Qa,{hint:{url:V.url,deviceCode:V.deviceCode,instructions:V.instructions},paste:{value:A,busy:M,message:P,ok:I,onChange:j,onSubmit:()=>{H()}}}),l.onCancelLogin&&(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:()=>void l.onCancelLogin?.(e.name),children:p(`common.cancel`)})]})]}),a===`loading`&&r.length===0&&(0,J.jsxs)(`div`,{className:`pwi-auth-state`,role:`status`,children:[(0,J.jsx)(`span`,{className:`pwi-spin-inline`,"aria-hidden":`true`}),p(`pws.accountsLoading`)]}),a===`error`&&(0,J.jsxs)(`div`,{className:`pwi-auth-state pwi-auth-state--error`,role:`alert`,children:[(0,J.jsx)(`span`,{children:p(`pws.accountsLoadFailed`)}),l.onRetryAccounts&&(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:()=>void l.onRetryAccounts?.(e.name),children:p(`pws.retryAccounts`)})]}),r.length>0&&(0,J.jsx)(`ul`,{className:`pwi-auth-list`,children:r.map(t=>{let n=Ta(r,t,p),i=o===t.id,a=t.health?.status,c=!!t.needsReauth||Ia(a),u=Ra(a),d=Na(t.id),f=Va(p,t.health),m=Ha(p,e.name,t.id,t.health);return(0,J.jsxs)(`li`,{className:`pwi-auth-acct${t.active?` pwi-auth-acct--active`:``}`,children:[(0,J.jsxs)(`div`,{className:`pwi-auth-row${t.active?` pwi-auth-row--active`:``}`,children:[(0,J.jsxs)(`button`,{type:`button`,className:`pwi-auth-row-main`,onClick:()=>{!t.active&&!c&&!u&&!o&&l.onSwitchAccount(e.name,t)},"aria-current":t.active?`true`:void 0,"aria-label":`${n}${t.active?` — ${p(`pws.accountCurrent`)}`:``}`,disabled:!!(c||u||o&&!i),children:[(0,J.jsx)(`span`,{className:`pwi-auth-dot ${c?`pwi-auth-dot--warn`:t.active?`pwi-auth-dot--ok`:`pwi-auth-dot--off`}`,"aria-hidden":`true`}),(0,J.jsxs)(`span`,{className:`pwi-auth-row-copy`,children:[(0,J.jsx)(`span`,{className:`pwi-auth-row-label`,children:n}),(0,J.jsx)(`span`,{className:`pwi-auth-row-secondary`,children:[t.email,`${p(`prov.accountId`)}: ${d}`].filter(Boolean).join(` · `)}),m&&(0,J.jsx)(`span`,{className:`pwi-auth-row-secondary faint`,children:m}),u&&(0,J.jsx)(`span`,{className:`pwi-auth-row-secondary faint`,children:p(`pws.healthCooldownHint`)})]}),f&&(0,J.jsx)(`span`,{className:Fa(a),children:f}),c&&!f&&(0,J.jsx)(`span`,{className:`badge badge-amber`,children:p(`pws.reauth`)}),t.active&&(0,J.jsx)(`span`,{className:`badge badge-primary`,children:p(`prov.accountActive`)}),i&&(0,J.jsx)(`span`,{className:`badge badge-muted`,children:p(`pws.accountSwitching`)})]}),c&&(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,disabled:s||!!o,onClick:()=>void l.onReauth(e.name,t.id),children:p(`pws.reauthenticate`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:()=>void l.onEditAlias(e.name,`oauth`,t.id,t.alias),children:p(`prov.editAlias`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm pwi-auth-row-remove`,"aria-label":`${p(`common.remove`)} — ${n}`,title:`${p(`common.remove`)} — ${n}`,disabled:!!o,onClick:()=>void l.onRemoveAccount(e.name,t),children:(0,J.jsx)(he,{style:{width:13,height:13},"aria-hidden":`true`})})]}),(t.quota!=null||t.quotaUnavailable||D&&t.quota==null)&&(0,J.jsx)(`div`,{className:`pwi-auth-acct-quota`,children:t.quotaUnavailable?(0,J.jsx)(`p`,{className:`muted pwi-auth-acct-quota-stale`,children:p(`pws.accountQuotaUnavailable`)}):(0,J.jsx)(ia,{quota:t.quota??null,plan:null,threshold:80,t:p,layout:`stacked`,pending:t.quota==null,...e.name===`meta-muse`&&t.quota?{observedAt:t.quota.updatedAt}:{}})})]},t.id)})}),a===`ready`&&U&&r.length===0&&(0,J.jsx)(`div`,{className:`pwi-auth-state pwi-auth-state--empty`,children:p(`pws.noAccounts`)}),U&&(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,style:{marginTop:8},onClick:()=>void l.onLogin(e.name,!0),disabled:s||!!o,children:p(`pws.addAccount`)})]}),B&&(0,J.jsxs)(J.Fragment,{children:[i.length>0&&(0,J.jsx)(`ul`,{className:`pwi-auth-list`,children:i.map(t=>(0,J.jsxs)(`li`,{className:`pwi-auth-row${t.active?` pwi-auth-row--active`:``}`,children:[(0,J.jsxs)(`button`,{type:`button`,className:`pwi-auth-row-main`,onClick:()=>void l.onSwitchApiKey(e.name,t),disabled:t.active,children:[(0,J.jsx)(`span`,{className:`pwi-auth-dot ${t.active?`pwi-auth-dot--ok`:`pwi-auth-dot--off`}`,"aria-hidden":`true`}),(0,J.jsxs)(`span`,{className:`pwi-auth-row-copy`,children:[(0,J.jsx)(`span`,{className:`pwi-auth-row-label`,children:t.label??t.masked}),t.label&&(0,J.jsxs)(`code`,{className:`pwi-auth-row-secondary`,children:[t.masked,` · `,p(`prov.accountId`),`: `,t.id]})]}),t.active&&(0,J.jsx)(`span`,{className:`badge badge-primary`,children:p(`prov.accountActive`)})]}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:()=>void l.onEditAlias(e.name,`api-key`,t.id,t.label),children:p(`prov.editAlias`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm pwi-auth-row-remove`,"aria-label":`${p(`common.remove`)} — ${t.label??t.masked}`,title:`${p(`common.remove`)} — ${t.label??t.masked}`,onClick:()=>void l.onRemoveApiKey(e.name,t),children:(0,J.jsx)(he,{style:{width:13,height:13},"aria-hidden":`true`})})]},t.id))}),m?(0,J.jsxs)(`div`,{className:`pwi-auth-add-key`,children:[(0,J.jsx)(`input`,{className:`input`,type:`password`,value:g,onChange:e=>v(e.target.value),placeholder:p(`modal.apiKeyPlaceholder`),autoComplete:`off`,disabled:y}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-primary btn-sm`,onClick:()=>void G(),disabled:y||!g.trim(),children:p(y?`pws.saving`:`pws.addKey`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:()=>{h(!1),v(``)},children:p(`common.cancel`)})]}):(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,style:{marginTop:8},onClick:()=>h(!0),children:p(`pws.addKey`)})]})]})]})}function zs(e,t,n){let r=e?.find(e=>e.id===t);if(!r)return n;if(r.baseUrl)return r.baseUrl;let i=new Set((e??[]).map(e=>e.baseUrl?.trim().replace(/\/+$/,``)).filter(e=>!!e)),a=n.trim().replace(/\/+$/,``);return i.has(a)?``:n}function Bs(e,t){if(!e?.length)return`custom`;let n=t.trim().replace(/\/+$/,``);for(let t of e)if(t.baseUrl&&t.baseUrl.trim().replace(/\/+$/,``)===n)return t.id;return e.some(e=>e.id===`custom`)?`custom`:e[0].id}function Vs(e,t,n){let r=e?.find(e=>e.id===t);return r?.baseUrl?r.baseUrl.trim():n.trim()}var Hs=`https://chatgpt.com/backend-api/codex`;function Us(e){try{let t=new URL(e.trim());if(t.username||t.password||t.search||t.hash)return;let n=t.pathname.replace(/\/+$/,``);return`${t.origin}${n}`}catch{return}}function Ws(e){return[`openai`,...Object.entries(e).filter(([,e])=>e.authMode===`forward`).map(([e])=>e).filter(e=>e!==`openai`).sort((e,t)=>e.localeCompare(t))]}function Gs(e){return e?e.adapter!==`openai-responses`||e.authMode!==`forward`||typeof e.baseUrl!=`string`||Us(e.baseUrl)!==Hs?`invalid`:e.disabled===!0?`disabled`:`ready`:`absent`}function Ks(e){return e.id===`openai`}function qs(e){return e.id===`openai`?e.codexAccountMode===`direct`?`prov.openaiDirectDesc`:`prov.openaiPoolDesc`:null}function Js(e){let t={adapter:e.adapter.trim(),baseUrl:e.baseUrl.trim()};return e.responsesPath?.trim()&&(t.responsesPath=e.responsesPath.trim()),(e.authMode===`key`||e.authMode===`forward`)&&(t.authMode=e.authMode),e.authMode===`key`&&e.apiKey.trim()&&(t.apiKey=e.apiKey.trim()),e.adapter.trim()===`anthropic`&&e.authMode===`key`&&e.apiKeyTransport===`bearer`&&(t.apiKeyTransport=`bearer`),e.defaultModel.trim()&&(t.defaultModel=e.defaultModel.trim()),e.allowPrivateNetwork&&(t.allowPrivateNetwork=!0),t}function Ys(e,t){return Ks(e)?Xs(e):{name:t.name.trim(),provider:Js(t)}}function Xs(e){if(!e.provider)throw Error(`Missing canonical provider seed for ${e.id}`);return{name:e.id,provider:structuredClone(e.provider)}}var Zs=class extends Error{i18nKey;constructor(e){super(e),this.name=`OpenAiEnableError`,this.i18nKey=e}};async function Qs(e,t,n=fetch){if(t===`disabled`){if((await n(`${e}/api/providers?name=openai`,{method:`PATCH`,headers:{"Content-Type":`application/json`},body:JSON.stringify({disabled:!1})})).ok)return;throw new Zs(`codexAuth.enableOpenaiFailed`)}let r=await n(`${e}/api/provider-presets`);if(!r.ok)throw new Zs(`codexAuth.openaiPresetLoadFailed`);let i=(await r.json()).providers?.find(e=>e.id===`openai`);if(!i?.provider)throw new Zs(`codexAuth.openaiPresetUnavailable`);if(!(await n(`${e}/api/providers`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify(Xs(i))})).ok)throw new Zs(`codexAuth.enableOpenaiFailed`)}var $s=[`openai-responses`,`openai-chat`,`anthropic`,`google`,`azure-openai`,`cursor`],ec=[];function tc(e){return e===`http1.1`||e===`h1`?`http1.1`:`http2`}function nc(e){return e===void 0?``:String(e)}function rc(e){if(!e.trim())return;let t=Number(e);return Number.isFinite(t)&&t>=1/60?t:void 0}function ic(e){if(!e.trim())return;let t=Number(e);return Number.isSafeInteger(t)&&t>0?t:void 0}function ac(e){let t=Object.entries(e?.models??{}).sort(([e],[t])=>e.localeCompare(t)).map(([e,t])=>[e,t.requestsPerMinute??null,t.minIntervalMs??null]);return JSON.stringify([e?.enabled===!0,e?.requestsPerMinute??null,e?.minIntervalMs??null,t])}function oc({item:e,availableModels:t=ec,apiBase:n,onUpdateProvider:r,onDirtyChange:i,onRegisterSave:a}){let o=Q(),s=String(e.authMode??(e.keyOptional?`local`:`key`)),c=ri(e.name,e),l=c?e.liveModels!==!1:!1,u=tc(e.upstreamHttpVersion),[d,f]=(0,_.useState)(e.adapter),[p,m]=(0,_.useState)(e.baseUrl),[h,g]=(0,_.useState)(e.defaultModel??``),[v,y]=(0,_.useState)(s),[b,x]=(0,_.useState)(e.apiKeyTransport??`x-api-key`),[S,C]=(0,_.useState)(e.note??``),[w,T]=(0,_.useState)(e.allowPrivateNetwork??!1),[E,D]=(0,_.useState)(l),[O,k]=(0,_.useState)(u),[A,j]=(0,_.useState)(!1),[M,N]=(0,_.useState)(null),[P,F]=(0,_.useState)(e.codexAccountMode??`pool`),[I,L]=(0,_.useState)(!1),[R,z]=(0,_.useState)(null),[B,V]=(0,_.useState)(),[H,U]=(0,_.useState)(n?`loading`:`idle`),[W,ee]=(0,_.useState)(()=>`custom`),[G,K]=(0,_.useState)(e.requestPacing?.enabled===!0),[q,Y]=(0,_.useState)(()=>nc(e.requestPacing?.requestsPerMinute)),[te,ne]=(0,_.useState)(()=>nc(e.requestPacing?.minIntervalMs)),[re,ie]=(0,_.useState)(()=>({...e.requestPacing?.models??{}})),[ae,oe]=(0,_.useState)(``),[se,ce]=(0,_.useState)(``),[le,ue]=(0,_.useState)(``),[de,fe]=(0,_.useState)(null);(0,_.useEffect)(()=>{f(e.adapter),m(e.baseUrl),g(e.defaultModel??``),y(String(e.authMode??(e.keyOptional?`local`:`key`))),x(e.apiKeyTransport??`x-api-key`),C(e.note??``),T(e.allowPrivateNetwork??!1),D(l),k(u),K(e.requestPacing?.enabled===!0),Y(nc(e.requestPacing?.requestsPerMinute)),ne(nc(e.requestPacing?.minIntervalMs)),ie({...e.requestPacing?.models??{}}),N(null),z(null),queueMicrotask(()=>ee(Bs(B,e.baseUrl)))},[e.adapter,e.baseUrl,e.defaultModel,e.authMode,e.apiKeyTransport,e.keyOptional,e.note,e.allowPrivateNetwork,l,u,e.requestPacing,B]),(0,_.useEffect)(()=>{F(e.codexAccountMode??`pool`)},[e.codexAccountMode]),(0,_.useEffect)(()=>{if(!n)return;let t=!1,r=e.name,i=e.baseUrl;return fetch(`${n}/api/provider-presets`).then(e=>Ft(e)).then(e=>{if(t)return;if(!e){V(void 0),U(`error`);return}let n=(e.providers??[]).find(e=>e.id===r)?.baseUrlChoices;V(n),U(`ready`),ee(Bs(n,i))}).catch(()=>{t||(V(void 0),U(`error`))}),()=>{t=!0}},[n,e.name]),(0,_.useEffect)(()=>{if(!n)return;let t=!0,r=!1,i=()=>{if(r)return;r=!0;let i=Vn(1e4);fetch(`${n}/api/provider-request-pacing?name=${encodeURIComponent(e.name)}`,{signal:i.signal}).then(e=>Ft(e)).then(e=>{t&&e&&fe(e)}).catch(()=>void 0).finally(()=>{i.clear(),r=!1})};i();let a=Gn(i,2e3);return()=>{t=!1,a()}},[n,e.name]);let pe=(0,_.useMemo)(()=>({enabled:G,...rc(q)===void 0?{}:{requestsPerMinute:rc(q)},...ic(te)===void 0?{}:{minIntervalMs:ic(te)},...Object.keys(re).length>0?{models:re}:{}}),[te,G,re,q]),X=d.trim()!==e.adapter||p.trim()!==e.baseUrl||h.trim()!==(e.defaultModel??``)||v!==String(e.authMode??(e.keyOptional?`local`:`key`))||d.trim()===`anthropic`&&v===`key`&&b!==(e.apiKeyTransport??`x-api-key`)||S.trim()!==(e.note??``)||w!==(e.allowPrivateNetwork??!1)||E!==l||d.trim()===`cursor`&&O!==u,me=ac(pe)!==ac(e.requestPacing),he=X||me;(0,_.useEffect)(()=>(i?.(he),()=>i?.(!1)),[he,i]);let ge=(0,_.useMemo)(()=>{let n=new Set(t);return h.trim()&&n.add(h.trim()),e.defaultModel&&n.add(e.defaultModel),[...n].sort((e,t)=>e.localeCompare(t))},[t,h,e.defaultModel]),_e=(0,_.useMemo)(()=>{let e=[...$s];return d&&!e.includes(d)&&e.unshift(d),e},[d]),Z=Mn(e.name),ve=H===`ready`&&!!(B&&B.length>0),ye=d.trim()===`anthropic`&&v===`key`,be=e.name===`openai`?Gs(e):`invalid`,xe=be===`ready`||be===`disabled`,Se=Z&&H!==`error`,Ce=async()=>{if(!r)return N({ok:!1,text:o(`pws.updatesUnavailable`)}),!1;if(I)return!1;let t=ve?Vs(B,W,p):p.trim();if(!d.trim()||!t)return N({ok:!1,text:o(`pws.adapterBaseRequired`)}),!1;j(!0),N(null);try{if(G&&!pe.requestsPerMinute&&!pe.minIntervalMs&&!pe.models)return N({ok:!1,text:o(`pws.pacingRuleRequired`)}),!1;let n=me&&!X,i=n?{requestPacing:pe}:{adapter:d.trim(),baseUrl:t,defaultModel:h.trim(),authMode:v,note:S.trim(),allowPrivateNetwork:w,...me?{requestPacing:pe}:{}};n||(c&&E!==(e.liveModels!==!1)&&(i.liveModels=E),d.trim()===`cursor`&&O!==u&&(i.upstreamHttpVersion=O===`http1.1`?`http1.1`:null),ye?i.apiKeyTransport=b:e.apiKeyTransport!==void 0&&(i.apiKeyTransport=``));let a=await r(e.name,i);return N(a.ok?{ok:!0,text:o(`pws.settingsSaved`)}:{ok:!1,text:a.error||o(`prov.saveFailed`)}),a.ok}finally{j(!1)}},we=(0,_.useRef)(Ce);(0,_.useEffect)(()=>{we.current=Ce}),(0,_.useEffect)(()=>{if(a)return a(()=>we.current()),()=>a(null)},[a]);let Te=async e=>{if(!(I||A||e===P)){if(!r){z({ok:!1,text:o(`pws.updatesUnavailable`)});return}L(!0),z(null);try{let t=await r(`openai`,{codexAccountMode:e});t.ok?(F(e),z({ok:!0,text:o(`pws.accountModeSaved`)})):z({ok:!1,text:t.error||o(`pws.accountModeFailed`)})}catch{z({ok:!1,text:o(`pws.accountModeFailed`)})}finally{L(!1)}}},Ee=()=>{f(e.adapter),m(e.baseUrl),g(e.defaultModel??``),y(s),x(e.apiKeyTransport??`x-api-key`),C(e.note??``),T(e.allowPrivateNetwork??!1),D(l),k(u),N(null),K(e.requestPacing?.enabled===!0),Y(nc(e.requestPacing?.requestsPerMinute)),ne(nc(e.requestPacing?.minIntervalMs)),ie({...e.requestPacing?.models??{}}),ee(Bs(B,e.baseUrl))},Oe=(e,t)=>{switch(e){case`token-plan`:return o(`modal.endpoint.tokenPlan`);case`payg`:return o(`modal.endpoint.payAsYouGo`);case`custom`:return o(`modal.endpoint.custom`);default:return t}};return(0,J.jsxs)(`div`,{className:`pwi-settings-form`,children:[(0,J.jsxs)(`label`,{className:`pwi-settings-field`,children:[(0,J.jsxs)(`span`,{className:`pwi-settings-label`,children:[(0,J.jsx)(De,{style:{width:12,height:12}}),` `,o(`pws.providerId`)]}),(0,J.jsx)(`input`,{className:`input`,value:e.name,readOnly:!0,disabled:!0})]}),(0,J.jsxs)(`label`,{className:`pwi-settings-field`,children:[(0,J.jsx)(`span`,{className:`pwi-settings-label`,children:o(`modal.adapter`)}),Z?(0,J.jsx)(`input`,{className:`input`,value:d,readOnly:!0,disabled:!0}):(0,J.jsx)(`select`,{className:`input`,value:d,onChange:e=>f(e.target.value),children:_e.map(e=>(0,J.jsx)(`option`,{value:e,children:e},e))})]}),ve?(0,J.jsxs)(J.Fragment,{children:[(0,J.jsxs)(`label`,{className:`pwi-settings-field`,children:[(0,J.jsx)(`span`,{className:`pwi-settings-label`,children:o(`modal.endpoint`)}),(0,J.jsx)(`select`,{className:`input`,value:W,onChange:e=>{let t=e.target.value;ee(t),m(zs(B,t,p))},children:B.map(e=>(0,J.jsx)(`option`,{value:e.id,children:Oe(e.id,e.label)},e.id))})]}),W===`custom`&&(0,J.jsxs)(`label`,{className:`pwi-settings-field`,children:[(0,J.jsx)(`span`,{className:`pwi-settings-label`,children:o(`modal.baseUrl`)}),(0,J.jsx)(`input`,{className:`input`,value:p,onChange:e=>m(e.target.value),placeholder:o(`modal.baseUrlPlaceholder`)})]})]}):(0,J.jsxs)(`label`,{className:`pwi-settings-field`,children:[(0,J.jsx)(`span`,{className:`pwi-settings-label`,children:o(`modal.baseUrl`)}),(0,J.jsx)(`input`,{className:`input`,value:p,onChange:e=>m(e.target.value),readOnly:Se,disabled:Se})]}),d.trim()===`cursor`&&(0,J.jsxs)(`label`,{className:`pwi-settings-field`,children:[(0,J.jsx)(`span`,{className:`pwi-settings-label`,children:o(`pws.cursorTransport`)}),(0,J.jsxs)(`select`,{className:`input`,value:O,onChange:e=>k(e.target.value),children:[(0,J.jsx)(`option`,{value:`http2`,children:o(`pws.cursorTransportHttp2`)}),(0,J.jsx)(`option`,{value:`http1.1`,children:o(`pws.cursorTransportHttp1`)})]}),(0,J.jsx)(`span`,{className:`pwi-settings-hint`,children:o(`pws.cursorTransportDesc`)})]}),(0,J.jsxs)(`label`,{className:`pwi-settings-field`,children:[(0,J.jsx)(`span`,{className:`pwi-settings-label`,children:o(`pws.cell.defaultModel`)}),ge.length>0?(0,J.jsxs)(`select`,{className:`input`,value:h,onChange:e=>g(e.target.value),children:[(0,J.jsx)(`option`,{value:``,children:o(`pws.defaultModelNone`)}),ge.map(e=>(0,J.jsx)(`option`,{value:e,children:e},e))]}):(0,J.jsx)(`input`,{className:`input`,value:h,onChange:e=>g(e.target.value),placeholder:o(`pws.optionalPlaceholder`)})]}),(0,J.jsxs)(`label`,{className:`pwi-settings-field`,children:[(0,J.jsx)(`span`,{className:`pwi-settings-label`,children:o(`pws.authMode`)}),Z?(0,J.jsx)(`input`,{className:`input`,value:Mi(e,o),readOnly:!0,disabled:!0}):(0,J.jsxs)(`select`,{className:`input`,value:v,onChange:e=>y(e.target.value),children:[(0,J.jsx)(`option`,{value:`key`,children:o(`modal.badge.apiKey`)}),(0,J.jsx)(`option`,{value:`forward`,children:o(`pws.auth.chatgptPassthrough`)}),(0,J.jsx)(`option`,{value:`oauth`,children:o(`modal.badge.oauth`)}),(0,J.jsx)(`option`,{value:`local`,children:o(`modal.badge.local`)})]})]}),xe&&(0,J.jsxs)(`label`,{className:`pwi-settings-field`,children:[(0,J.jsx)(`span`,{className:`pwi-settings-label`,children:o(`codexAuth.accountModeTitle`)}),(0,J.jsxs)(`select`,{className:`input`,value:P,disabled:I||A,onChange:e=>{let t=e.target.value;if(t!==P){if(!window.confirm(o(`pws.accountModeConfirm`))){e.target.value=P;return}Te(t)}},children:[(0,J.jsx)(`option`,{value:`pool`,children:o(`codexAuth.accountModePool`)}),(0,J.jsx)(`option`,{value:`direct`,children:o(`codexAuth.accountModeDirect`)})]}),(0,J.jsx)(`span`,{className:`pwi-settings-hint`,children:o(P===`direct`?`codexAuth.accountModeDirectDesc`:`codexAuth.accountModePoolDesc`)}),I&&(0,J.jsx)(`span`,{className:`muted text-label`,children:o(`pws.accountSwitching`)}),R&&(0,J.jsx)(`span`,{role:R.ok?`status`:`alert`,className:R.ok?`pwi-settings-mode-msg pwi-settings-mode-msg--ok`:`pwi-settings-mode-msg pwi-settings-mode-msg--err`,children:R.text})]}),ye&&(0,J.jsxs)(`label`,{className:`pwi-settings-field`,children:[(0,J.jsx)(`span`,{className:`pwi-settings-label`,children:o(`modal.apiKeyTransport`)}),(0,J.jsxs)(`select`,{className:`input`,value:b,onChange:e=>x(e.target.value),children:[(0,J.jsx)(`option`,{value:`x-api-key`,children:o(`modal.apiKeyTransportNative`)}),(0,J.jsx)(`option`,{value:`bearer`,children:o(`modal.apiKeyTransportBearer`)})]})]}),(0,J.jsxs)(`label`,{className:`pwi-settings-field`,children:[(0,J.jsx)(`span`,{className:`pwi-settings-label`,children:o(`pws.note`)}),(0,J.jsx)(`textarea`,{className:`input pwi-settings-textarea`,value:S,onChange:e=>C(e.target.value),rows:2})]}),(0,J.jsxs)(`label`,{className:`pwi-settings-field`,style:{flexDirection:`row`,alignItems:`center`,gap:8},children:[(0,J.jsx)(`input`,{type:`checkbox`,checked:w,onChange:e=>T(e.target.checked)}),(0,J.jsx)(`span`,{className:`pwi-settings-label`,children:o(`pws.allowPrivateNetwork`)})]}),(0,J.jsxs)(`label`,{className:`pwi-settings-field`,style:{flexDirection:`row`,alignItems:`flex-start`,gap:8},children:[(0,J.jsx)(`input`,{type:`checkbox`,checked:E,disabled:!c,onChange:e=>D(e.target.checked)}),(0,J.jsxs)(`span`,{children:[(0,J.jsx)(`span`,{className:`pwi-settings-label`,children:o(`pws.liveModels`)}),(0,J.jsx)(`span`,{className:`muted text-label`,style:{display:`block`,marginTop:2},children:o(`pws.liveModelsDesc`)})]})]}),(0,J.jsxs)(`section`,{className:`pwi-pacing-card`,"aria-labelledby":`pwi-pacing-title`,children:[(0,J.jsxs)(`div`,{className:`pwi-pacing-head`,children:[(0,J.jsxs)(`div`,{children:[(0,J.jsx)(`h3`,{id:`pwi-pacing-title`,children:o(`pws.pacingTitle`)}),(0,J.jsx)(`p`,{children:o(`pws.pacingDesc`)})]}),(0,J.jsxs)(`label`,{className:`pwi-pacing-toggle`,children:[(0,J.jsx)(`input`,{type:`checkbox`,checked:G,onChange:e=>K(e.target.checked)}),` `,o(`pws.pacingEnabled`)]})]}),(0,J.jsxs)(`div`,{className:`pwi-pacing-grid`,children:[(0,J.jsxs)(`label`,{className:`pwi-settings-field`,children:[(0,J.jsx)(`span`,{className:`pwi-settings-label`,children:o(`pws.pacingRpm`)}),(0,J.jsx)(`input`,{className:`input`,type:`number`,min:`0.016667`,step:`any`,value:q,onChange:e=>Y(e.target.value),placeholder:`38`})]}),(0,J.jsxs)(`label`,{className:`pwi-settings-field`,children:[(0,J.jsx)(`span`,{className:`pwi-settings-label`,children:o(`pws.pacingDelay`)}),(0,J.jsx)(`input`,{className:`input`,type:`number`,min:`1`,step:`1`,value:te,onChange:e=>ne(e.target.value),placeholder:`1600`})]})]}),(0,J.jsx)(`p`,{className:`pwi-settings-hint`,children:o(`pws.pacingSlowerWins`)}),(0,J.jsxs)(`div`,{className:`pwi-pacing-status`,"aria-live":`polite`,children:[(0,J.jsxs)(`span`,{children:[(0,J.jsx)(`strong`,{children:de?.queued??0}),` `,o(`pws.pacingQueued`)]}),(0,J.jsxs)(`span`,{children:[(0,J.jsxs)(`strong`,{children:[de?.nextSlotInMs??0,` ms`]}),` `,o(`pws.pacingNextSlot`)]}),(0,J.jsxs)(`span`,{children:[(0,J.jsx)(`strong`,{children:de?.lastModelId??o(`pws.pacingNone`)}),` `,o(`pws.pacingLastModel`)]})]}),(0,J.jsx)(`h4`,{children:o(`pws.pacingModelOverrides`)}),(0,J.jsxs)(`div`,{className:`pwi-pacing-grid pwi-pacing-grid--model`,children:[(0,J.jsxs)(`label`,{className:`pwi-settings-field`,children:[(0,J.jsx)(`span`,{className:`pwi-settings-label`,children:o(`pws.pacingModel`)}),(0,J.jsx)(`input`,{className:`input`,list:`pacing-models-${e.name}`,value:ae,onChange:e=>oe(e.target.value)}),(0,J.jsx)(`datalist`,{id:`pacing-models-${e.name}`,children:t.map(e=>(0,J.jsx)(`option`,{value:e},e))})]}),(0,J.jsxs)(`label`,{className:`pwi-settings-field`,children:[(0,J.jsx)(`span`,{className:`pwi-settings-label`,children:o(`pws.pacingRpm`)}),(0,J.jsx)(`input`,{className:`input`,type:`number`,min:`0.016667`,step:`any`,value:se,onChange:e=>ce(e.target.value)})]}),(0,J.jsxs)(`label`,{className:`pwi-settings-field`,children:[(0,J.jsx)(`span`,{className:`pwi-settings-label`,children:o(`pws.pacingDelay`)}),(0,J.jsx)(`input`,{className:`input`,type:`number`,min:`1`,step:`1`,value:le,onChange:e=>ue(e.target.value)})]}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:()=>{let e=ae.trim(),t=rc(se),n=ic(le);!e||t===void 0&&n===void 0||(ie(r=>({...r,[e]:{...t===void 0?{}:{requestsPerMinute:t},...n===void 0?{}:{minIntervalMs:n}}})),oe(``),ce(``),ue(``))},children:o(`pws.pacingAdd`)})]}),Object.entries(re).length>0&&(0,J.jsx)(`div`,{className:`pwi-pacing-overrides`,children:Object.entries(re).map(([e,t])=>(0,J.jsxs)(`div`,{className:`pwi-pacing-row`,children:[(0,J.jsx)(`code`,{children:e}),(0,J.jsxs)(`span`,{children:[t.requestsPerMinute===void 0?``:`${t.requestsPerMinute} ${o(`pws.pacingRpmUnit`)}`,t.requestsPerMinute!==void 0&&t.minIntervalMs!==void 0?` · `:``,t.minIntervalMs===void 0?``:`${t.minIntervalMs} ms`]}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:()=>ie(t=>Object.fromEntries(Object.entries(t).filter(([t])=>t!==e))),"aria-label":o(`pws.pacingRemoveModel`,{model:e}),children:o(`pws.pacingRemove`)})]},e))})]}),he&&(0,J.jsxs)(`div`,{className:`pwi-settings-sticky-bar`,children:[(0,J.jsx)(`span`,{className:`muted`,children:o(`pws.settingsUnsavedBar`)}),(0,J.jsxs)(`div`,{className:`pwi-settings-sticky-bar-actions`,children:[(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:Ee,disabled:A,children:o(`pws.discardSettings`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-primary btn-sm`,onClick:()=>void Ce(),disabled:A||I,children:o(A?`pws.saving`:`pws.saveSettings`)})]})]}),M&&(0,J.jsx)(`div`,{role:M.ok?`status`:`alert`,className:M.ok?`pwi-settings-msg pwi-settings-msg--ok`:`pwi-settings-msg pwi-settings-msg--err`,children:M.text})]})}function sc({providerName:e,defaultProviderName:t,onConfirm:n,onCancel:r}){let i=Q();return(0,J.jsx)(`div`,{className:`dialog-backdrop`,onClick:r,children:(0,J.jsxs)(`div`,{className:`dialog`,role:`alertdialog`,"aria-label":i(`pws.removeConfirmTitle`),onClick:e=>e.stopPropagation(),children:[(0,J.jsx)(`h3`,{children:i(`pws.removeConfirmTitle`)}),(0,J.jsx)(`p`,{children:t?i(`pws.removeDefaultConfirmBody`,{name:e,defaultProvider:t}):i(`pws.removeConfirmBody`,{name:e})}),(0,J.jsxs)(`div`,{className:`dialog-actions`,children:[(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost`,onClick:r,children:i(`common.cancel`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-danger`,onClick:n,children:i(`pws.removeConfirm`)})]})]})})}function cc({onSave:e,onDiscard:t,onCancel:n,saving:r=!1}){let i=Q();return(0,J.jsx)(`div`,{className:`dialog-backdrop`,onClick:n,children:(0,J.jsxs)(`div`,{className:`dialog`,role:`alertdialog`,"aria-label":i(`pws.unsavedLeaveTitle`),onClick:e=>e.stopPropagation(),children:[(0,J.jsx)(`h3`,{children:i(`pws.unsavedLeaveTitle`)}),(0,J.jsx)(`p`,{children:i(`pws.unsavedLeaveBody`)}),(0,J.jsxs)(`div`,{className:`dialog-actions`,children:[(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost`,onClick:n,children:i(`common.cancel`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost`,onClick:t,children:i(`pws.discardSettings`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-primary`,onClick:e,disabled:r,children:i(r?`pws.saving`:`pws.saveSettings`)})]})]})})}function lc({item:e,usageTotals:t,modelUsage:n,quotaReport:r,availableModels:i,hasLiveModels:a,selectedModels:o,modelsLoading:s,modelsLoadFailed:c,onRetryModels:l,oauthEmail:u,onDeselect:d,apiBase:f,oauth:p,accounts:m,accountLoadState:h,accountsFocusToken:g=0,accountsFocusProvider:v=null,switchingAccountId:y,keys:b,busyProvider:x,loginHint:S,authHandlers:C,onCodexActiveNeedsReauthChange:w,codexController:T,onUpdateProvider:E,isDefault:D,onRemoveProvider:O,onSetDisabled:k,onSetDefault:A}){let j=Q(),[M,N]=(0,_.useState)(`overview`),[P,F]=(0,_.useState)(!1),[I,L]=(0,_.useState)(null),[R,z]=(0,_.useState)(!1),B=(0,_.useRef)(null),[V,H]=(0,_.useState)(0),U=(0,_.useCallback)(e=>{B.current=e},[]),W=e.disabled===!0,ee=(0,_.useMemo)(()=>ci(e),[e]),G=(0,_.useMemo)(()=>hi(e),[e]),K=(0,_.useMemo)(()=>wa(e),[e]),q=v===e.name?g:0,Y=JSON.stringify([T?.activeId??``,m?.find(e=>e.active)?.id??``,b?.find(e=>e.active)?.id??``,p?.loggedIn===void 0?``:String(p.loggedIn),p?.needsReauth===void 0?``:String(p.needsReauth),u??``]),te=(0,_.useMemo)(()=>[{id:`overview`,label:j(`pws.tab.overview`)},{id:`models`,label:j(`pws.tab.models`)},{id:`usage`,label:j(`pws.tab.usage`)},...K?[{id:`accounts`,label:j(K===`api-keys`?`pws.apiKeys`:`pws.tab.accounts`)}]:[],{id:`settings`,label:j(`pws.tab.settings`)}],[K,j]),ne=(0,_.useCallback)(e=>{if(P&&M===`settings`&&e!==`settings`){L(e);return}N(e)},[M,P]);q!==V&&!(q&&!K)&&(H(q),q&&K&&(P&&M===`settings`?L(`accounts`):N(`accounts`)));let re=(0,_.useCallback)(()=>{if(P&&M===`settings`){L(`deselect`);return}d()},[P,M,d]),ie=(0,_.useCallback)((e,t)=>{let n;if(e.key===`ArrowRight`)n=(t+1)%te.length;else if(e.key===`ArrowLeft`)n=(t-1+te.length)%te.length;else if(e.key===`Home`)n=0;else if(e.key===`End`)n=te.length-1;else return;e.preventDefault(),ne(te[n].id),e.currentTarget.parentElement?.querySelectorAll(`[role="tab"]`)[n]?.focus()},[ne,te]),ae=`pws-tab-${M}`,oe=`pws-panel-${M}`;return(0,J.jsxs)(`div`,{className:`pws-detail`,children:[(0,J.jsx)(`div`,{className:`pws-detail-head`,children:(0,J.jsxs)(`button`,{type:`button`,className:`pws-detail-back-link`,onClick:re,children:[(0,J.jsx)(Se,{className:`pws-detail-back-chevron`,"aria-hidden":`true`}),j(`pws.allProviders`)]})}),(0,J.jsxs)(`div`,{className:`pws-detail-head-main`,children:[(0,J.jsx)(Pi,{name:e.name,adapter:e.adapter,baseUrl:e.baseUrl,cls:`pws-detail-icon`}),(0,J.jsx)(`div`,{className:`pws-detail-title-wrap`,children:(0,J.jsxs)(`h2`,{className:`pws-detail-title`,children:[jn(e.name,j),G&&(0,J.jsx)(`span`,{className:`pwi-rail-badge pwi-rail-badge--local`,children:j(`modal.badge.local`)}),!G&&ee&&(0,J.jsx)(`span`,{className:`pwi-rail-badge pwi-rail-badge--free`,children:j(`modal.badge.free`)})]})}),(0,J.jsxs)(`div`,{className:`pws-detail-actions`,children:[!D&&!W&&A&&(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:()=>A(e.name),children:j(`prov.setDefault`)}),O&&(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm btn-icon-only`,onClick:()=>O(e.name),"aria-label":j(`pws.removeConfirmTitle`),title:j(`pws.removeConfirmTitle`),children:(0,J.jsx)(he,{style:{width:15,height:15},"aria-hidden":`true`})}),k&&(0,J.jsxs)(`div`,{className:`pws-detail-toggle`,children:[(0,J.jsx)(`span`,{className:`pws-detail-toggle-label`,children:j(`pws.enabledLabel`)}),(0,J.jsx)(Tt,{on:!W,onClick:()=>k(e.name,!W),disabled:D,label:j(`pws.enabledLabel`)})]})]})]}),(0,J.jsx)(`div`,{className:`pws-detail-tabs`,role:`tablist`,children:te.map((e,t)=>(0,J.jsx)(`button`,{type:`button`,role:`tab`,id:`pws-tab-${e.id}`,"aria-controls":`pws-panel-${e.id}`,"aria-selected":M===e.id,tabIndex:M===e.id?0:-1,className:`pws-detail-tab${M===e.id?` pws-detail-tab--active`:``}`,onClick:()=>ne(e.id),onKeyDown:e=>ie(e,t),children:e.label},e.id))}),(0,J.jsxs)(`div`,{className:`pws-detail-panel`,role:`tabpanel`,id:oe,"aria-labelledby":ae,tabIndex:0,children:[M===`overview`&&(0,J.jsx)(Ea,{item:e,apiBase:f,connectionIdentity:Y,usageTotals:t,quotaReport:r,oauthEmail:u,oauth:p,onEditSettings:()=>ne(`settings`),onViewUsage:()=>ne(`usage`),onUpdateProvider:E,reauthBusy:x===e.name,onCancelLogin:C?.onCancelLogin?()=>void C.onCancelLogin?.(e.name):void 0,onReauthenticate:e.activeNeedsReauth?()=>{if(e.authMode===`oauth`){let t=m??[],n=t.find(e=>e.active&&e.needsReauth)??t.find(e=>e.needsReauth);C?.onReauth(e.name,n?.id);return}ne(`accounts`)}:void 0}),M===`models`&&(0,J.jsx)(Aa,{item:e,apiBase:f,availableModels:i,hasLiveModels:a,selectedModels:o,modelsLoading:s,modelsLoadFailed:c,needsReauth:(m??[]).some(e=>e.active&&e.needsReauth)||p?.needsReauth===!0,onRetryModels:l,onOpenAccounts:K?()=>ne(`accounts`):void 0},e.name),M===`usage`&&(0,J.jsx)(ja,{item:e,usageTotals:t,quotaReport:r,modelUsage:n}),M===`accounts`&&(0,J.jsx)(Rs,{item:e,apiBase:f,oauth:p,accounts:m,keys:b,accountLoadState:h,switchingAccountId:y,busy:x===e.name,loginHint:S,authHandlers:C,onUpdateProvider:E,onCodexActiveNeedsReauthChange:w,codexController:T}),M===`settings`&&(0,J.jsx)(oc,{item:e,apiBase:f,availableModels:i,onUpdateProvider:E,onDirtyChange:F,onRegisterSave:U},e.name)]}),I&&(0,J.jsx)(cc,{saving:R,onCancel:()=>{R||L(null)},onDiscard:()=>{if(R)return;let e=I;L(null),F(!1),e===`deselect`?d():N(e)},onSave:()=>{(async()=>{if(!R){z(!0);try{if(!(await B.current?.()??!1))return;let e=I;L(null),F(!1),e===`deselect`?d():e&&N(e)}finally{z(!1)}}})()}})]})}var uc=new Set([`anthropic`,`google-antigravity`,`meta-muse`]),dc=new Set([`github-copilot`,`cursor`]);function fc(e){let t=e.trim().toLowerCase();return uc.has(t)?`high`:dc.has(t)?`elevated`:null}function pc(e){switch(e){case`high`:return`oauthTos.highTitle`;case`elevated`:return`oauthTos.elevatedTitle`;default:return e}}function mc(e){switch(e){case`high`:return`oauthTos.highBody`;case`elevated`:return`oauthTos.elevatedBody`;default:return e}}function hc(e,t=!1){let n={};for(let[t,r]of Object.entries(e))La(r.accounts.find(e=>e.active)??r.accounts.find(e=>e.id===r.activeAccountId))&&(n[t]=!0);return t&&(n.openai=!0),n}function gc(e){let{apiBase:t,t:n,config:r,aliveRef:i,notify:a,fetchConfig:o,fetchOauth:s,fetchProviderQuotas:c,codexActiveNeedsReauth:l}=e,[u,d]=(0,_.useState)({}),[f,p]=(0,_.useState)({}),[m,h]=(0,_.useState)(null),[g,v]=(0,_.useState)({}),[y,b]=(0,_.useState)({}),[x,S]=(0,_.useState)(null),[C,w]=(0,_.useState)(``),T=(0,_.useRef)({}),E=(0,_.useRef)(null),D=(0,_.useRef)(null),O=(0,_.useRef)(null),k=(0,_.useCallback)(async e=>{let n=[...new Set(e)];return p(e=>{let t={...e};for(let e of n)t[e]=`loading`;return t}),(await Promise.all(n.map(async e=>{let n=(T.current[e]??0)+1;T.current[e]=n;try{let r=await fetch(`${t}/api/oauth/accounts?provider=${encodeURIComponent(e)}`);if(!r.ok)throw Error(String(r.status));let a=await r.json();return!i.current||T.current[e]!==n||(d(t=>({...t,[e]:{activeAccountId:a.activeAccountId??null,accounts:a.accounts??[]}})),p(t=>({...t,[e]:`ready`})),(async()=>{try{let r=await fetch(`${t}/api/oauth/accounts?provider=${encodeURIComponent(e)}"a=1`);if(!r.ok)return;let o=await r.json();if(!i.current||T.current[e]!==n)return;d(t=>({...t,[e]:{activeAccountId:o.activeAccountId??a.activeAccountId??null,accounts:o.accounts??a.accounts??[]}}))}catch{}})(),!0)}catch{return!i.current||T.current[e]!==n||(p(t=>({...t,[e]:`error`})),!1)}}))).every(Boolean)},[i,t]),A=(0,_.useCallback)(async e=>{let n=await Promise.all(e.map(async e=>[e,(await fetch(`${t}/api/providers/keys?name=${encodeURIComponent(e)}`).then(async e=>{if(!e.ok)throw Error(String(e.status));return e.json()}).catch(()=>null))?.keys??[]]));b(Object.fromEntries(n))},[t]),j=async(e,r)=>{if(r.active||r.needsReauth||O.current)return;let o={provider:e,accountId:r.id};O.current=o,h(o);let l=Ta(u[e]?.accounts??[r],r,n);try{if(!(await fetch(`${t}/api/oauth/accounts/active`,{method:`PUT`,headers:{"Content-Type":`application/json`},body:JSON.stringify({provider:e,accountId:r.id})})).ok){a(n(`prov.accountSwitchFail`),!1);return}let i=await k([e]);if(await Promise.all([s(),c(!0)]),!i){a(n(`pws.accountsLoadFailed`),!1);return}a(n(`prov.accountSwitched`,{email:l}),!0)}catch{a(n(`prov.accountSwitchFail`),!1)}finally{O.current?.provider===o.provider&&O.current.accountId===o.accountId&&(O.current=null,i.current&&h(null))}},M=async(e,r)=>{if(r.active)return;let i=await fetch(`${t}/api/providers/keys/active`,{method:`PUT`,headers:{"Content-Type":`application/json`},body:JSON.stringify({name:e,id:r.id})});if(i.ok)a(n(`prov.keySwitched`,{key:r.label??r.masked}),!0),A(Object.keys(y)),c(!0);else{let e=await i.json().catch(()=>({}));a(e.error||n(`prov.keySwitchFail`),!1)}},N=async(e,r)=>{window.confirm(n(`prov.keyRemoveConfirm`,{key:r.label??r.masked}))&&(await fetch(`${t}/api/providers/keys?name=${encodeURIComponent(e)}&id=${encodeURIComponent(r.id)}`,{method:`DELETE`})).ok&&(a(n(`prov.keyRemoved`,{key:r.label??r.masked}),!0),A(Object.keys(y)),o(),c(!0))},P=async(e,r)=>{let i=r.trim();if(!i)return!1;try{let r=await fetch(`${t}/api/providers/keys`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({name:e,key:i})});if(!r.ok){let e=await r.json().catch(()=>({}));return a(e.error||n(`prov.keyAddFail`),!1),!1}return a(n(`prov.keyAdded`,{name:e}),!0),S(null),await Promise.all([A(Object.keys(y).includes(e)?Object.keys(y):[...Object.keys(y),e]),o(),c(!0)]),!0}catch{return a(n(`prov.keyAddFail`),!1),!1}},F=async e=>{await P(e,C)&&w(``)},I=async(e,r,i,o)=>{let s=window.prompt(n(`prov.aliasPrompt`),o??``);if(s===null)return;let c=s.trim(),l=await fetch(r===`oauth`?`${t}/api/oauth/accounts/alias`:`${t}/api/providers/keys/alias`,{method:`PUT`,headers:{"Content-Type":`application/json`},body:JSON.stringify(r===`oauth`?{provider:e,accountId:i,alias:c}:{name:e,id:i,alias:c})});if(!l.ok){let e=await l.json().catch(()=>({}));a(e.error||n(`prov.aliasSaveFailed`),!1);return}r===`oauth`?await k([e]):await A(Object.keys(y).includes(e)?Object.keys(y):[...Object.keys(y),e]),a(n(`prov.aliasSaved`),!0)},L=async(e,r)=>{let i=Ta(u[e]?.accounts??[r],r,n);if(window.confirm(n(`prov.accountRemoveConfirm`,{email:i})))try{if(!(await fetch(`${t}/api/oauth/accounts?provider=${encodeURIComponent(e)}&id=${encodeURIComponent(r.id)}`,{method:`DELETE`})).ok){a(n(`prov.accountRemoveFail`,{email:i}),!1);return}a(n(`prov.accountRemoved`,{email:i}),!0),await k([e]),await Promise.all([s(),c(!0)])}catch{a(n(`prov.accountRemoveFail`,{email:i}),!1)}},R=(0,_.useMemo)(()=>r?Object.entries(r.providers).filter(([,e])=>e.authMode===`oauth`).map(([e])=>e):[],[r]);(0,_.useEffect)(()=>{if(R.length===0)return;let e=R.join(`,`);E.current!==e&&(E.current=e,Promise.resolve().then(()=>{k(R)}))},[k,R]);let z=(0,_.useMemo)(()=>r?Object.entries(r.providers).filter(([,e])=>e.hasApiKey&&e.authMode!==`oauth`&&e.authMode!==`forward`).map(([e])=>e):[],[r]);return(0,_.useEffect)(()=>{if(z.length===0)return;let e=z.join(`,`);D.current!==e&&(D.current=e,Promise.resolve().then(()=>{A(z)}))},[A,z]),{accountSets:u,accountLoadStates:f,switchingAccount:m,openAccounts:g,keyPools:y,addingKeyFor:x,newKeyValue:C,setAccountSets:d,setAccountLoadStates:p,setSwitchingAccount:h,setOpenAccounts:v,setKeyPools:b,setAddingKeyFor:S,setNewKeyValue:w,fetchAccountSets:k,fetchKeyPools:A,switchAccount:j,switchApiKey:M,removeApiKey:N,addApiKeyValue:P,addApiKey:F,editCredentialAlias:I,removeAccount:L,oauthCardProviders:R,keyCardProviders:z,activeAccountNeedsReauth:(0,_.useMemo)(()=>hc(u,l),[u,l])}}var _c=new Set([`hasApiKey`,`hasHeaders`,`xaiResponsesOptInState`]);function vc(e){return{defaultProvider:e.defaultProvider,providers:Object.fromEntries(Object.entries(e.providers).map(([e,t])=>{let n={};for(let[e,r]of Object.entries(t))_c.has(e)||(n[e]=structuredClone(r));return[e,n]}))}}function yc(e){let{apiBase:t,config:n,notify:r,fetchConfig:i,fetchProviderQuotas:a,onSaved:o,t:s}=e,[c,l]=(0,_.useState)(!1),[u,d]=(0,_.useState)(``),[f,p]=(0,_.useState)(!1),[m,h]=(0,_.useState)(``),[g,v]=(0,_.useState)(!1),[y,b]=(0,_.useState)(!1),x=(0,_.useRef)(!1);(0,_.useEffect)(()=>{n&&!x.current&&d(JSON.stringify(vc(n),null,2))},[n]);let S=(0,_.useCallback)(async()=>{v(!0);let e;try{e=JSON.parse(u)}catch{return r(s(`prov.invalidJson`),!1),v(!1),!1}try{let n=JSON.parse(m),c=await fetch(`${t}/api/providers`,{method:`PUT`,headers:{"Content-Type":`application/json`},body:JSON.stringify({baseline:n,next:e})});if(!c.ok){let e=await c.json().catch(()=>({}));return r(e.error||s(`prov.saveFailed`),!1),!1}return r(s(`prov.saved`),!0),l(!1),p(!1),x.current=!1,b(!1),h(JSON.stringify(e,null,2)),i(),a(!0),o(),!0}catch{return r(s(`prov.saveFailed`),!1),!1}finally{v(!1)}},[t,u,i,a,m,r,o,s]),C=(0,_.useCallback)(()=>{let e=n?JSON.stringify(vc(n),null,2):u;h(e),d(e),b(!1),p(!0),x.current=!0},[n,u]),w=(0,_.useCallback)(()=>{b(!1),p(!1),x.current=!1;let e=n?JSON.stringify(vc(n),null,2):m;h(e),d(e)},[n,m]);return{editing:c,setEditing:l,draft:u,setDraft:d,jsonEditorOpen:f,jsonBaseline:m,jsonSaving:g,jsonLeaveOpen:y,jsonEditorOpenRef:x,saveConfig:S,openJsonEditor:C,discardJsonEditor:w,requestCloseJsonEditor:(0,_.useCallback)(()=>{if(f&&u!==m){b(!0);return}w()},[w,u,m,f]),restoreJsonEditor:(0,_.useCallback)(()=>{d(m)},[m]),jsonIsDirty:f&&u!==m,setJsonLeaveOpen:b}}var bc={xai:`xAI (Grok)`,anthropic:`Anthropic (Claude)`,kimi:`Kimi (Moonshot)`,"meta-muse":`Meta Muse Code (CLI)`,"google-antigravity":`Google Antigravity`,"github-copilot":`GitHub Copilot`,cursor:`Cursor`},xc=e=>bc[e]??e;function Sc({apiBase:e,t,aliveRef:n,accountSets:r,setAccountSets:i,setBusy:a,setStatus:o,setLoginInfo:s,setOauthStatus:c,notify:l,fetchConfig:u,fetchOauth:d,fetchAccountSets:f,fetchProviderQuotas:p,bumpModelsRefresh:m,onLoginSettled:h}){let g=(0,_.useRef)(null);g.current===null&&(g.current=new Map);let v=(0,_.useCallback)(e=>{let t=(g.current.get(e)??0)+1;return g.current.set(e,t),t},[]);return{cancelLoginOAuth:(0,_.useCallback)(async r=>{let i=v(r);try{await fetch(`${e}/api/oauth/login/cancel`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({provider:r})})}catch{}n.current&&(g.current.get(r)===i&&(a(e=>e===r?null:e),s(e=>e?.provider===r?null:e)),l(t(`prov.loginCancelled`,{provider:xc(r)}),!1))},[n,e,v,l,a,s,t]),loginOAuth:async(d,_=!1,y)=>{let b=v(d),x=y?.trim()||void 0;a(d),o(``),s(null);try{let a=await fetch(`${e}/api/oauth/login`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({provider:d,...no(),..._||x?{addAccount:!0}:{},...x?{accountId:x,reauth:!0}:{}})});if(g.current.get(d)!==b||!n.current)return;if(!a.ok){l((await a.json().catch(()=>({}))).error||t(`prov.loginFailStart`,{provider:xc(d)}),!1);return}let o=await a.json();(o.url||o.instructions||o.deviceCode)&&s({provider:d,url:o.url,instructions:o.instructions,deviceCode:o.deviceCode});let v=r[d]?.accounts.length??0,y=!1;for(let a=0;a<150&&n.current&&g.current.get(d)===b;a++){if(await new Promise(e=>setTimeout(e,2e3)),g.current.get(d)!==b||!n.current)return;let a=await fetch(`${e}/api/oauth/status?provider=${d}`).catch(()=>null),o=a?await Ft(a)??null:null;if(!o)continue;if(o.error){c(e=>({...e,[d]:o})),l(/cancel/i.test(o.error)?t(`prov.loginCancelled`,{provider:xc(d)}):t(`prov.loginError`,{provider:xc(d),error:o.error}),!1),s(null),y=!0;break}let S=o.accounts?.length??0;if(_||x?S>v||o.done===!0:o.loggedIn||o.done===!0){c(e=>({...e,[d]:o}));let e=x?o.accounts?.find(e=>e.id===x):o.accounts?.find(e=>e.active)??o.accounts?.find(e=>e.id===o.activeAccountId);if(x&&!e){l(t(`prov.loginError`,{provider:xc(d),error:t(`prov.reauthAccountMissing`)}),!1),s(null),y=!0;break}if(e?.needsReauth){l(t(`prov.loginError`,{provider:xc(d),error:t(`prov.reauthIdentityMismatch`)}),!1),s(null),y=!0;break}if(o.accounts){let e=o.accounts.find(e=>e.active)?.id??null;i(t=>({...t,[d]:{activeAccountId:o.activeAccountId??e,accounts:o.accounts}}))}s(null),h?.(d);let a=Object.keys(r);if(await f(new Set(a).has(d)?a:[...a,d]),!n.current||g.current.get(d)!==b)return;_&&!x&&S<=v?l(t(`prov.loginSameAccount`,{provider:xc(d)}),!1):l(t(`prov.loginOk`,{provider:xc(d),cmd:`ocx sync`}),!0),u(),p(!0),m(),y=!0;break}}!y&&g.current.get(d)===b&&n.current&&(await fetch(`${e}/api/oauth/login/cancel`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({provider:d})}).catch(()=>{}),l(t(`prov.loginTimeout`,{provider:xc(d)}),!1),s(null))}catch{g.current.get(d)===b&&l(t(`prov.loginRequestFail`,{provider:xc(d)}),!1)}finally{n.current&&g.current.get(d)===b&&a(null)}},logoutOAuth:async n=>{v(n),a(e=>e===n?null:e),s(e=>e?.provider===n?null:e);try{if(!(await fetch(`${e}/api/oauth/logout?provider=${encodeURIComponent(n)}`,{method:`POST`})).ok){l(t(`prov.logoutFail`,{provider:xc(n)}),!1);return}await Promise.all([f([n]),d(),u(),p(!0)]),m(),l(t(`prov.logoutOk`,{provider:xc(n)}),!0)}catch{l(t(`prov.logoutFail`,{provider:xc(n)}),!1)}}}}async function Cc(e,t){try{let t=await e.json();if(typeof t.error==`string`&&t.error.trim())return t.error.trim()}catch{}return t}function wc(e,t,n){switch(e.code){case`last_provider`:return t(`prov.removeLastProvider`);case`provider_has_dependent_combos`:return t(`prov.removeHasDependentCombos`,{combos:(Array.isArray(e.combos)?e.combos.filter(e=>typeof e==`string`).join(`, `):``)||`—`});case`default_provider_disabled`:return t(`prov.defaultDisabled`);default:return typeof e.error==`string`&&e.error.trim()?e.error.trim():n}}function Tc({apiBase:e,t,removeBusyRef:n,workspaceSelected:r,setWorkspaceSelected:i,setRemoveConfirmName:a,notify:o,fetchConfig:s,fetchOauth:c,fetchProviderQuotas:l,refreshCodexAccount:u}){let d=(0,_.useCallback)(async e=>{a(e)},[a]),f=(0,_.useCallback)(async u=>{let d=u;if(!d||n.current)return;n.current=!0,a(null);let f=t(`prov.removeFail`,{name:d});try{let n=await fetch(`${e}/api/providers?name=${encodeURIComponent(d)}`,{method:`DELETE`});if(n.ok){let e=await n.json().catch(()=>({})),a=typeof e.defaultProvider==`string`?e.defaultProvider:null;o(a?t(`prov.removedDefault`,{name:d,defaultProvider:a}):t(`prov.removed`,{name:d}),!0),r===d&&i(null),s(),c(),l(!0)}else o(wc(await n.json().catch(()=>({})),t,f),!1)}catch{o(f,!1)}finally{n.current=!1}},[e,s,c,l,o,n,a,i,t,r]),p=(0,_.useCallback)(async(n,r)=>{let i=await fetch(`${e}/api/providers?name=${encodeURIComponent(n)}`,{method:`PATCH`,headers:{"Content-Type":`application/json`},body:JSON.stringify({disabled:r})});if(!i.ok){o(await Cc(i,t(r?`prov.disableFail`:`prov.enableFail`,{name:n})),!1);return}o(t(r?`prov.disabled`:`prov.enabled`,{name:n}),!0),s(),c(),l(!0)},[e,s,c,l,o,t]),m=(0,_.useCallback)(async(n,r)=>{try{let i=await fetch(`${e}/api/providers?name=${encodeURIComponent(n)}`,{method:`PATCH`,headers:{"Content-Type":`application/json`},body:JSON.stringify(r)});if(!i.ok)return{ok:!1,error:await Cc(i,t(`prov.updateFail`))};let a=await i.json().catch(()=>({}));if(await s(),Object.hasOwn(r,`codexAccountMode`)){let e=[l(!0)];u&&e.push(Promise.resolve(u())),await Promise.all(e)}let o=a.xaiResponsesOptInState;return{ok:!0,...o===!0||o===!1||o===`mixed`?{xaiResponsesOptInState:o}:{}}}catch{return{ok:!1,error:t(`prov.networkError`)}}},[e,s,l,u,t]);return{removeProvider:d,confirmRemoveProvider:f,setProviderDisabled:p,setDefaultProvider:(0,_.useCallback)(async n=>{try{let r=await fetch(`${e}/api/providers?name=${encodeURIComponent(n)}`,{method:`PATCH`,headers:{"Content-Type":`application/json`},body:JSON.stringify({setDefault:!0})});return r.ok?(o(t(`prov.setDefaultSuccess`,{name:n}),!0),await s(),!0):(o(wc(await r.json().catch(()=>({})),t,t(`prov.setDefaultFail`,{name:n})),!1),!1)}catch{return o(t(`prov.setDefaultFail`,{name:n}),!1),!1}},[e,s,o,t]),updateProvider:m}}function Ec({apiBase:e,t,setConfig:n,setOauthProviders:r,setOauthStatus:i,notify:a,invalidateProviderQuotas:o,configCacheKey:s}){return{fetchConfig:(0,_.useCallback)(async()=>{try{let t=await Pt(await fetch(`${e}/api/config`));n(t??null),s&&t&&br(s,t)}catch{a(t(`prov.loadConfigFail`),!1)}},[e,s,a,n,t]),fetchOauth:(0,_.useCallback)(async()=>{try{let t=(await Pt(await fetch(`${e}/api/oauth/providers`)))?.providers??[];r(t);let n=await Promise.all(t.map(async t=>{let n=await fetch(`${e}/api/oauth/status?provider=${encodeURIComponent(t)}`).catch(()=>null);return[t,n?await Ft(n)??{loggedIn:!1}:{loggedIn:!1}]}));i(Object.fromEntries(n))}catch{}},[e,r,i]),fetchProviderQuotas:(0,_.useCallback)(async(e=!1)=>{o(e)},[o])}}function Dc({providerId:e,providerLabel:t,onCancel:n,onContinue:r}){let i=Q(),a=(0,_.useId)(),o=(0,_.useId)(),s=(0,_.useRef)(null),c=(0,_.useRef)(!1),[l,u]=(0,_.useState)(!1),[d,f]=(0,_.useState)(!1),p=fc(e);(0,_.useEffect)(()=>{let e=s.current;e&&!e.open&&e.showModal()},[]);let m=(0,_.useCallback)(e=>{e.preventDefault(),n()},[n]);if(!p)return null;let h=e.trim().toLowerCase(),g=h===`anthropic`?`oauthTos.anthropicBody`:mc(p),v=h===`anthropic`||h===`google-antigravity`;return(0,J.jsxs)(`dialog`,{ref:s,"aria-labelledby":a,"aria-describedby":o,className:`modal-overlay`,onCancel:m,children:[(0,J.jsx)(`button`,{type:`button`,className:`modal-backdrop-dismiss`,"aria-label":i(`common.close`),tabIndex:-1,onClick:n}),(0,J.jsxs)(`div`,{className:`modal-card`,onClick:e=>e.stopPropagation(),style:{maxWidth:460},children:[(0,J.jsx)(`h3`,{id:a,children:i(pc(p),{provider:t})}),(0,J.jsxs)(`div`,{id:o,className:`notice-warn`,style:{marginTop:12,display:`flex`,gap:8,alignItems:`flex-start`},children:[(0,J.jsx)(_e,{width:16,height:16,style:{flexShrink:0,marginTop:2},"aria-hidden":`true`}),(0,J.jsx)(`p`,{className:`modal-desc`,style:{margin:0},children:i(g,{provider:t})})]}),v&&(0,J.jsx)(`p`,{className:`muted text-label`,style:{marginTop:12},children:i(`oauthTos.saferPath`)}),(0,J.jsxs)(`label`,{className:`oauth-tos-ack`,style:{display:`flex`,gap:8,alignItems:`flex-start`,marginTop:14},children:[(0,J.jsx)(`input`,{type:`checkbox`,checked:l,onChange:e=>u(e.target.checked),style:{marginTop:3},"aria-required":`true`}),(0,J.jsx)(`span`,{className:`text-label`,children:i(`oauthTos.acknowledge`)})]}),(0,J.jsxs)(`div`,{className:`modal-actions`,children:[(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost`,onClick:n,children:i(`common.cancel`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-primary`,disabled:!l||d,onClick:()=>{!l||c.current||(c.current=!0,f(!0),r())},children:i(`oauthTos.continue`)})]})]})]})}function Oc(e){return{adapter:e.adapter,baseUrl:e.baseUrl,authMode:e.auth,freeTier:!!e.freeTier,keyOptional:!!e.keyOptional}}function kc(e){return li(e.id,Oc(e))}function Ac(e){let t={accounts:[],free:[],paid:[]};for(let n of e)t[kc(n)].push(n);return t}function jc(e,t){let n=t.trim().toLowerCase();return n?e.filter(e=>e.label.toLowerCase().includes(n)||e.id.toLowerCase().includes(n)):e}function Mc(e,t,n){return!n||e.kind!==`oauth`||t!==e.id?!1:n.provider===e.id}var Nc={},Pc=[],Fc={};function Ic({presets:e,usageRank:t=Nc,presetsLoading:n=!1,initialTier:r=`free`,onSelectPreset:i,onSelectCustom:a,accountRows:o=Pc,accountStatus:s=Fc,busyProvider:c=null,loginHint:l=null,paste:u,onLogin:d,onCancelLogin:f,onLogout:p,onManage:m}){let h=Q(),[g,v]=(0,_.useState)(r),[y,b]=(0,_.useState)(``),x=(0,_.useMemo)(()=>e.filter(e=>e.id!==`custom`),[e]),S=(0,_.useMemo)(()=>{let e=Object.keys(t).length>0;return x.toSorted((n,r)=>{if(e){let e=t[n.id]??0,i=t[r.id]??0;if(i!==e)return i-e}return n.label.localeCompare(r.label,void 0,{sensitivity:`base`})||n.id.localeCompare(r.id)})},[x,t]),C=(0,_.useMemo)(()=>Ac(S),[S])[g],w=(0,_.useMemo)(()=>jc(C,y),[C,y]),T=e=>{let t=e.codexAccountMode===`direct`?(0,J.jsx)(`span`,{className:`badge badge-green`,children:h(`modal.badge.direct`)}):e.codexAccountMode===`pool`?(0,J.jsx)(`span`,{className:`badge badge-accent`,children:h(`modal.badge.pool`)}):e.auth===`oauth`?(0,J.jsx)(`span`,{className:`badge badge-accent`,children:h(`modal.badge.oauth`)}):e.auth===`forward`?(0,J.jsx)(`span`,{className:`badge badge-green`,children:h(`modal.badge.codexLogin`)}):e.auth===`local`?(0,J.jsx)(`span`,{className:`badge badge-amber`,children:h(`modal.badge.local`)}):e.keyOptional?null:(0,J.jsx)(`span`,{className:`badge badge-muted`,children:h(`modal.badge.apiKey`)}),n=(e.freeTier||e.keyOptional)&&e.auth===`key`?(0,J.jsx)(`span`,{className:`badge badge-green`,children:h(`modal.badge.free`)}):null;return(0,J.jsxs)(J.Fragment,{children:[n,t]})};return(0,J.jsxs)(`div`,{className:`provider-catalog`,children:[(0,J.jsx)(`div`,{className:`provider-catalog-tabs`,role:`tablist`,children:[`accounts`,`free`,`paid`].map(e=>(0,J.jsx)(`button`,{type:`button`,role:`tab`,"aria-selected":g===e,className:`provider-catalog-tab${g===e?` active`:``}`,onClick:()=>{v(e),b(``)},children:h(e===`accounts`?`modal.tab.accounts`:e===`free`?`modal.tab.free`:`modal.tab.paid`)},e))}),g===`accounts`&&(0,J.jsx)(`div`,{className:`provider-catalog-accounts-hint muted text-label`,children:h(`modal.accountsHint`)}),(0,J.jsx)(`input`,{className:`input provider-catalog-search`,value:y,onChange:e=>b(e.target.value),placeholder:h(`modal.search`)}),(0,J.jsxs)(`div`,{className:`provider-catalog-rows`,children:[n&&w.length===0&&(0,J.jsx)(`div`,{className:`muted text-control provider-catalog-empty`,children:h(`modal.catalogLoading`)}),g!==`accounts`&&w.map(e=>(0,J.jsxs)(`button`,{type:`button`,className:`list-row`,onClick:()=>i(e),children:[(0,J.jsx)(Pi,{name:e.id,adapter:e.adapter,cls:`provider-icon provider-icon-sm`}),(0,J.jsxs)(`div`,{children:[(0,J.jsx)(`div`,{className:`title`,children:e.label}),(0,J.jsxs)(`div`,{className:`sub`,children:[(0,J.jsx)(`code`,{className:`chip`,children:e.adapter}),e.note?` · ${e.note}`:``]})]}),(0,J.jsx)(`div`,{className:`provider-catalog-badges`,children:T(e)})]},e.id)),g!==`accounts`&&!n&&w.length===0&&(0,J.jsx)(`div`,{className:`muted text-control provider-catalog-empty`,children:h(`modal.noMatch`)}),g===`accounts`&&o.map(e=>{let t=s[e.id],n=c===e.id,r=!!t?.loggedIn,i=r?t?.email??e.statusLabel??h(`modal.accountLoggedIn`):t?.error??e.statusLabel??h(`modal.accountLoggedOut`),a=Mc(e,c,l);return(0,J.jsxs)(`div`,{className:`list-row provider-catalog-account-row${a?` provider-catalog-account-row--waiting`:``}`,children:[(0,J.jsxs)(`div`,{className:`provider-catalog-account-row-head`,children:[(0,J.jsx)(Pi,{name:e.id,cls:`provider-icon provider-icon-sm`}),(0,J.jsxs)(`div`,{children:[(0,J.jsx)(`div`,{className:`title`,children:e.label}),(0,J.jsx)(`div`,{className:`sub`,children:i})]}),(0,J.jsx)(`div`,{className:`provider-catalog-badges`,children:e.kind===`key`?null:e.kind===`codex`?(0,J.jsxs)(J.Fragment,{children:[r&&(0,J.jsx)(`a`,{className:`btn btn-ghost`,href:e.href??`#codex-set`,children:h(`modal.accountManage`)}),d&&(0,J.jsx)(`button`,{type:`button`,className:r?`btn btn-ghost`:`btn btn-primary`,disabled:n,onClick:()=>{n||d(e.id)},children:h(n?`codexAuth.enablingOpenai`:r?`modal.accountAdd`:`modal.accountLogin`)})]}):r?(0,J.jsxs)(J.Fragment,{children:[m&&(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost`,onClick:()=>m(e.id),children:h(`modal.accountManage`)}),d&&(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost`,disabled:n,onClick:()=>{n||d(e.id,!0)},children:h(n?`prov.waitingBrowser`:`modal.accountAdd`)}),n&&f&&(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost`,onClick:()=>f(e.id),children:h(`common.cancel`)}),p&&!n&&(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost`,onClick:()=>p(e.id),children:h(`modal.accountLogout`)})]}):n?f&&(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost`,onClick:()=>f(e.id),children:h(`common.cancel`)}):d&&(0,J.jsx)(`button`,{type:`button`,className:`btn btn-primary`,onClick:()=>d(e.id),children:h(`modal.accountLogin`)})})]}),a&&l&&(0,J.jsx)(Qa,{hint:{url:l.url,deviceCode:l.deviceCode,instructions:l.instructions},...u?{paste:{value:u.value,busy:u.busy,message:u.message,ok:u.ok,onChange:u.onChange,onSubmit:()=>u.onSubmit(e.id)}}:{}})]},e.id)}),g===`accounts`&&o.length===0&&!n&&(0,J.jsx)(`div`,{className:`muted text-control provider-catalog-empty`,children:h(`modal.noMatch`)})]}),(0,J.jsxs)(`div`,{className:`provider-catalog-footer`,children:[(0,J.jsx)(`div`,{style:{flex:1}}),g!==`accounts`&&(0,J.jsx)(`button`,{type:`button`,className:`link-btn`,onClick:a,children:h(`modal.notListed`)})]})]})}function Lc({preset:e,oauthSupported:t,oauthBusy:n,oauthMsg:r,oauthMsgTone:i,oauthUrl:a,oauthDeviceCode:o,oauthInstructions:s,manualCode:c,manualCodeBusy:l,manualCodeMsg:u,manualCodeOk:d,onRequestLogin:f,onUseApiKeyInstead:p,onManualCodeChange:m,onSubmitManualCode:h,onBack:g}){let _=Q();return(0,J.jsxs)(`div`,{style:{display:`flex`,flexDirection:`column`,gap:14},children:[(0,J.jsx)(`div`,{className:`muted text-control`,children:e.note??_(`modal.oauthDefaultNote`)}),t.includes(e.oauthProvider??``)?(0,J.jsxs)(J.Fragment,{children:[(0,J.jsxs)(`button`,{type:`button`,className:`btn btn-primary`,onClick:()=>f(e.oauthProvider),disabled:n,style:{width:`100%`,padding:`12px 16px`},children:[(0,J.jsx)(De,{}),n?_(`modal.waitingBrowser`):_(`modal.logInWith`,{label:e.label})]}),!n&&(0,J.jsx)(Es,{})]}):(0,J.jsx)(`div`,{className:`text-control`,style:{color:`var(--amber)`,background:`var(--amber-soft)`,border:`1px solid var(--amber)`,borderRadius:`var(--radius-sm)`,padding:`10px 12px`},children:_(`modal.oauthComingSoon`,{label:e.label})}),r&&(0,J.jsx)(`div`,{className:`text-label`,style:{color:i===`warn`?`var(--amber)`:`var(--accent-hover)`},children:r}),n&&(0,J.jsx)(Qa,{hint:{url:a,deviceCode:o,instructions:s},paste:{value:c,busy:l,disabled:!e.oauthProvider,message:u,ok:d,onChange:m,onSubmit:()=>{e.oauthProvider&&h(e.oauthProvider)}}}),(0,J.jsxs)(`div`,{style:{display:`flex`,gap:8,alignItems:`center`,marginTop:2},children:[(0,J.jsx)(`button`,{type:`button`,className:`link-btn`,onClick:p,children:_(`modal.useApiKeyInstead`)}),(0,J.jsx)(`div`,{style:{flex:1}}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost`,onClick:g,children:_(`modal.back`)})]})]})}function Rc({label:e,children:t}){return(0,J.jsxs)(`label`,{style:{display:`block`},children:[(0,J.jsx)(`span`,{className:`field-label`,children:e}),t]})}function zc({preset:e,form:t,endpointChoice:n,error:r,saving:i,dup:a,isCustom:o,isLocal:s,isReservedForward:c,presetDescription:l,onFormChange:u,onEndpointChoiceChange:d,onSubmit:f,onUseOauthLogin:p,onBack:m}){let h=Q();return(0,J.jsxs)(`div`,{style:{display:`flex`,flexDirection:`column`,gap:10},children:[!c&&!o&&!s&&!e.keyOptional&&e.note&&(0,J.jsxs)(`details`,{className:`setup-guide`,children:[(0,J.jsx)(`summary`,{children:h(`modal.setupGuide`)}),(0,J.jsxs)(`ol`,{className:`text-label leading-relaxed`,style:{margin:`8px 0 0`,paddingLeft:18,color:`var(--muted)`},children:[(0,J.jsxs)(`li`,{children:[h(`modal.setupStep1Prefix`),` `,(0,J.jsx)(`a`,{href:e.dashboardUrl,target:`_blank`,rel:`noreferrer`,children:h(`modal.setupDashboardLink`,{label:e.label})}),` `,h(`modal.setupStep1Suffix`)]}),(0,J.jsx)(`li`,{children:h(`modal.setupStep2`)}),(0,J.jsx)(`li`,{children:h(`modal.setupStep3`)})]}),e.note&&(0,J.jsx)(`div`,{className:`text-label`,style:{color:`var(--muted)`,marginTop:6,fontStyle:`italic`},children:e.note}),/\{[^}]*\}/.test(t.baseUrl)&&(0,J.jsx)(`div`,{className:`text-label`,style:{color:`var(--amber)`,marginTop:6},children:h(`modal.baseUrlPlaceholderHint`)})]}),(0,J.jsx)(Rc,{label:h(`modal.providerName`),children:(0,J.jsx)(`input`,{className:`input`,value:t.name,readOnly:c,onChange:e=>u({...t,name:e.target.value}),placeholder:h(`modal.namePlaceholder`)})}),a&&(0,J.jsx)(`div`,{className:`text-label`,style:{color:`var(--amber)`},children:h(`modal.duplicateWarn`,{name:t.name.trim()})}),!c&&(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)(Rc,{label:h(`modal.adapter`),children:(0,J.jsx)(`select`,{className:`input`,value:t.adapter,onChange:e=>u({...t,adapter:e.target.value}),children:[`openai-responses`,`openai-chat`,`anthropic`,`google`,`azure-openai`,`cursor`].map(e=>(0,J.jsx)(`option`,{value:e,children:e},e))})}),e.baseUrlChoices&&e.baseUrlChoices.length>0?(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)(Rc,{label:h(`modal.endpoint`),children:(0,J.jsx)(`select`,{className:`input`,value:n,onChange:n=>{let r=n.target.value;d(r),u({...t,baseUrl:zs(e.baseUrlChoices,r,t.baseUrl)})},children:e.baseUrlChoices.map(e=>(0,J.jsx)(`option`,{value:e.id,children:e.id===`token-plan`?h(`modal.endpoint.tokenPlan`):e.id===`payg`?h(`modal.endpoint.payAsYouGo`):e.id===`custom`?h(`modal.endpoint.custom`):e.label},e.id))})}),n===`custom`&&(0,J.jsx)(Rc,{label:h(`modal.baseUrl`),children:(0,J.jsx)(`input`,{className:`input`,value:t.baseUrl,onChange:e=>u({...t,baseUrl:e.target.value}),placeholder:h(`modal.baseUrlPlaceholder`)})})]}):(0,J.jsx)(Rc,{label:h(`modal.baseUrl`),children:(0,J.jsx)(`input`,{className:`input`,value:t.baseUrl,onChange:e=>u({...t,baseUrl:e.target.value}),placeholder:h(`modal.baseUrlPlaceholder`)})}),!c&&(0,J.jsxs)(`label`,{className:`modal-field`,style:{flexDirection:`row`,alignItems:`center`,gap:8},children:[(0,J.jsx)(`input`,{type:`checkbox`,checked:t?.allowPrivateNetwork??!1,onChange:e=>u({...t,allowPrivateNetwork:e.target.checked})}),(0,J.jsx)(`span`,{className:`muted text-control`,children:h(`modal.allowPrivateNetwork`)})]}),!c&&(t?.allowPrivateNetwork??!1)&&(0,J.jsx)(`p`,{className:`muted text-hint`,children:h(`modal.allowPrivateNetworkHint`)})]}),t.authMode===`forward`?(0,J.jsx)(`div`,{className:`text-label`,style:{color:`var(--green)`,background:`var(--green-soft)`,border:`1px solid var(--green)`,borderRadius:`var(--radius-sm)`,padding:`8px 10px`},children:l(e)}):t.authMode===`local`?(0,J.jsx)(`div`,{className:`text-label leading-relaxed`,style:{color:`var(--amber)`,background:`var(--amber-soft)`,border:`1px solid var(--amber)`,borderRadius:`var(--radius-sm)`,padding:`8px 10px`},children:h(`modal.localHint`)}):e.keyOptional?(0,J.jsxs)(`div`,{className:`text-label leading-relaxed`,style:{color:`var(--green)`,background:`var(--green-soft)`,border:`1px solid var(--green)`,borderRadius:`var(--radius-sm)`,padding:`10px 12px`},children:[(0,J.jsx)(`strong`,{children:h(`modal.freeTierTitle`)}),` — `,e.note??h(`modal.freeTierDefault`)]}):(0,J.jsxs)(J.Fragment,{children:[e.dashboardUrl&&(0,J.jsxs)(`a`,{className:`text-label`,href:e.dashboardUrl,target:`_blank`,rel:`noreferrer`,style:{display:`inline-flex`,alignItems:`center`,gap:5},children:[(0,J.jsx)(Ee,{style:{width:14,height:14}}),h(`modal.getApiKey`,{label:e.label}),(0,J.jsx)(Te,{style:{width:13,height:13}})]}),(0,J.jsx)(Rc,{label:h(`modal.apiKey`),children:(0,J.jsx)(`input`,{className:`input`,type:`password`,value:t.apiKey,onChange:e=>u({...t,apiKey:e.target.value}),placeholder:h(`modal.apiKeyPlaceholder`)})}),t.adapter===`anthropic`&&t.authMode===`key`&&(0,J.jsx)(Rc,{label:h(`modal.apiKeyTransport`),children:(0,J.jsxs)(`select`,{className:`input`,value:t.apiKeyTransport??`x-api-key`,onChange:e=>u({...t,apiKeyTransport:e.target.value===`bearer`?`bearer`:void 0}),children:[(0,J.jsx)(`option`,{value:`x-api-key`,children:h(`modal.apiKeyTransportNative`)}),(0,J.jsx)(`option`,{value:`bearer`,children:h(`modal.apiKeyTransportBearer`)})]})})]}),!c&&(0,J.jsx)(Rc,{label:h(`modal.defaultModel`),children:(0,J.jsx)(`input`,{className:`input`,value:t.defaultModel,onChange:e=>u({...t,defaultModel:e.target.value}),placeholder:h(`modal.defaultModelPlaceholder`)})}),r&&(0,J.jsx)(`div`,{className:`text-control`,role:`alert`,style:{color:`var(--red)`},children:r}),(0,J.jsxs)(`div`,{style:{display:`flex`,gap:8,marginTop:4,alignItems:`center`},children:[(0,J.jsx)(`button`,{type:`button`,className:`btn btn-primary`,onClick:f,disabled:i,children:h(i?`modal.adding`:`modal.add`)}),e.auth===`oauth`&&(0,J.jsx)(`button`,{type:`button`,className:`link-btn`,onClick:p,children:h(`modal.useOauthLogin`)}),(0,J.jsx)(`div`,{style:{flex:1}}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost`,onClick:m,children:h(`modal.back`)})]})]})}var Bc=2e3;function Vc({apiBase:e,t,aliveRef:n,onAdded:r}){return{loginOAuth:(0,_.useCallback)(async(i,a)=>{let{setOauthBusy:o,setOauthMsg:s,setOauthMsgTone:c,setOauthUrl:l,setManualCode:u,setManualCodeMsg:d,setManualCodeOk:f}=a;o(!0),s(``),c(`ok`),l(``,i),u(``),d(``),f(!0);try{let a=await fetch(`${e}/api/oauth/login`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({provider:i,...no()})});if(!n.current)return;if(!a.ok){let e=await a.json().catch(()=>({}));c(`warn`),s(e.error===`unknown oauth provider`?t(`modal.oauthComingSoonShort`):e.error||t(`modal.loginFailStart`));return}let o=await a.json();l(o.url??``,i,o.deviceCode,o.instructions),o.url||o.deviceCode?s(t(`modal.waitingLogin`)):s(o.instructions||t(`modal.loggingIn`));for(let a=0;a<100;a++){if(await new Promise(e=>setTimeout(e,Bc)),!n.current)return;let a=await fetch(`${e}/api/oauth/status?provider=${i}`).catch(()=>null),o=a?await Ft(a):null;if(!n.current)return;if(o?.error){c(`warn`),s(t(`modal.loginError`,{error:o.error}));return}if(o?.loggedIn){r(i);return}}c(`warn`),s(t(`modal.loginTimeout`))}catch{n.current&&(c(`warn`),s(t(`modal.networkError`)))}finally{n.current&&o(!1)}},[n,e,r,t]),submitManualCode:(0,_.useCallback)(async(r,i,a,o)=>{let s=i.trim();if(!s||a)return;let{setManualCodeBusy:c,setManualCode:l,setManualCodeOk:u,setManualCodeMsg:d}=o;c(!0),d(``);try{let i=await fetch(`${e}/api/oauth/login/code`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({provider:r,input:s})});if(!n.current)return;if(!i.ok){let e=await i.json().catch(()=>({}));u(!1),d(t(`prov.pasteFail`,{error:e.error||i.statusText}));return}l(``),u(!0),d(t(`prov.pasteOk`))}catch{n.current&&(u(!1),d(t(`modal.networkError`)))}finally{n.current&&c(!1)}},[n,e,t])}}function Hc(e,t){return{preset:e?{id:`custom`,label:t,adapter:`openai-chat`,baseUrl:``,auth:`key`}:null,form:e?{name:``,adapter:`openai-chat`,baseUrl:``,authMode:`key`,apiKey:``,apiKeyTransport:void 0,defaultModel:``,allowPrivateNetwork:!1}:null,saving:!1,error:``,oauthBusy:!1,oauthMsg:``,oauthMsgTone:`ok`,oauthUrl:``,oauthDeviceCode:``,oauthInstructions:``,oauthUrlProvider:null,manualCode:``,manualCodeBusy:!1,manualCodeMsg:``,manualCodeOk:!0,endpointChoice:`custom`,oauthTosPending:null}}function Uc(e,t){switch(t.type){case`choose-preset`:return{...e,preset:t.preset,form:t.form,endpointChoice:t.endpointChoice,error:``,oauthMsg:``,oauthMsgTone:`ok`,oauthUrl:``,oauthDeviceCode:``,oauthInstructions:``,oauthUrlProvider:null,oauthBusy:!1,manualCode:``,manualCodeMsg:``,manualCodeOk:!0};case`back`:return{...e,preset:null,form:null,endpointChoice:`custom`,error:``,oauthMsg:``,oauthMsgTone:`ok`,oauthUrl:``,oauthDeviceCode:``,oauthInstructions:``,oauthUrlProvider:null,oauthBusy:!1,manualCode:``,manualCodeMsg:``,manualCodeOk:!0};case`set-form`:return{...e,form:t.form};case`set-endpoint-choice`:return{...e,endpointChoice:t.choice};case`set-saving`:return{...e,saving:t.saving};case`set-error`:return{...e,error:t.error};case`set-oauth-busy`:return{...e,oauthBusy:t.busy};case`set-oauth-msg`:return{...e,oauthMsg:t.msg,oauthMsgTone:t.tone??e.oauthMsgTone};case`set-oauth-tone`:return{...e,oauthMsgTone:t.tone};case`set-oauth-url`:return e.preset?.oauthProvider===t.providerId?{...e,oauthUrl:t.url,oauthDeviceCode:t.deviceCode??``,oauthInstructions:t.instructions??``,oauthUrlProvider:t.providerId}:e;case`set-manual-code`:return{...e,manualCode:t.code};case`set-manual-code-busy`:return{...e,manualCodeBusy:t.busy};case`set-manual-code-msg`:return{...e,manualCodeMsg:t.msg,manualCodeOk:t.ok??e.manualCodeOk};case`set-oauth-tos-pending`:return{...e,oauthTosPending:t.providerId};case`use-oauth-login`:return{...e,form:t.form,error:``,oauthUrl:``,oauthDeviceCode:``,oauthInstructions:``,oauthUrlProvider:null};case`use-api-key-instead`:return{...e,form:t.form,oauthMsg:``,oauthMsgTone:`ok`,oauthUrl:``,oauthDeviceCode:``,oauthInstructions:``,oauthUrlProvider:null,oauthBusy:!1,manualCode:``,manualCodeMsg:``};default:return e}}function Wc({apiBase:e,existingNames:t,onClose:n,onAdded:r,initialTier:i,initialCustom:a=!1,accountRows:o,accountStatus:s,accountBusy:c,accountLoginHint:l=null,onAccountLogin:u,onAccountCancelLogin:d,onAccountLogout:f,onAccountManage:p,onOpen:m}){let h=Q(),g=(0,_.useMemo)(()=>[{id:`custom`,label:h(`modal.customProvider`),adapter:`openai-chat`,baseUrl:``,auth:`key`}],[h]),[v,y]=(0,_.useReducer)(Uc,a,e=>Hc(e,h(`modal.customProvider`))),b=(0,_.useRef)(!0),x=(0,_.useRef)(null),S=(0,_.useRef)(null),C=G(`add-provider-oauth:${e}`,[e],async t=>{let n=await fetch(`${e}/api/oauth/providers`,{signal:t});return n.ok?(await n.json()).providers??[]:[]}),w=G(`add-provider-presets:${e}`,[e],async t=>{let n=await fetch(`${e}/api/provider-presets`,{signal:t});if(!n.ok)throw Error(String(n.status));let r=await n.json();return Array.isArray(r.providers)&&r.providers.length>0?r.providers:null}),T=G(Br(e),[e],async t=>{let n=await fetch(`${e}/api/usage?range=30d`,{signal:t});if(!n.ok)throw Error(String(n.status));return await n.json()},{deadlineMs:6e4}),E=C.data??[],D=w.data??g,O=w.loading,k=Object.fromEntries((T.data?.providers??[]).map(e=>[e.provider,e.requests])),{preset:A,form:j,saving:M,error:N,oauthBusy:P,oauthMsg:F,oauthMsgTone:I,oauthUrl:L,oauthUrlProvider:R,oauthDeviceCode:z,oauthInstructions:B,manualCode:V,manualCodeBusy:H,manualCodeMsg:U,manualCodeOk:W,endpointChoice:ee,oauthTosPending:K}=v;(0,_.useEffect)(()=>{b.current=!0,x.current=document.activeElement,m?.();let e=S.current;if(e){let t=e.querySelector(`input:not([disabled]), button:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex='-1'])`);t&&t.focus()}return()=>{b.current=!1,x.current?.focus()}},[]),(0,_.useEffect)(()=>{let e=e=>{e.key===`Escape`&&!K&&n()};return window.addEventListener(`keydown`,e),()=>window.removeEventListener(`keydown`,e)},[n,K]);let q=e=>{let t=qs(e);return t?h(t):e.note},Y=e=>{let t=Bs(e.baseUrlChoices,e.baseUrl);y({type:`choose-preset`,preset:e,endpointChoice:t,form:{name:e.id===`custom`?``:e.id,adapter:e.adapter,baseUrl:e.baseUrlChoices?.length?zs(e.baseUrlChoices,t,e.baseUrl):e.baseUrl,responsesPath:e.responsesPath,authMode:e.auth,apiKey:``,apiKeyTransport:void 0,defaultModel:e.defaultModel??``,allowPrivateNetwork:!1}})},te=async()=>{if(!j)return;let t=A?Ks(A):!1,n=A?.baseUrlChoices?.length?Vs(A.baseUrlChoices,ee,j.baseUrl):j.baseUrl.trim();if(!t&&!j.name.trim()){y({type:`set-error`,error:h(`modal.nameRequired`)});return}if(!t&&!n){y({type:`set-error`,error:h(`modal.baseUrlRequired`)});return}if(!t&&/\{[^}]*\}/.test(n)){y({type:`set-error`,error:h(`modal.baseUrlPlaceholderError`)});return}let i={...j,baseUrl:n},a;try{a=Ys(A??{id:`custom`},i)}catch{y({type:`set-error`,error:h(`modal.invalidPreset`)});return}y({type:`set-saving`,saving:!0}),y({type:`set-error`,error:``});try{let t=await fetch(`${e}/api/providers`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify(a)});if(!t.ok){let e=await t.json().catch(()=>({}));y({type:`set-error`,error:e.error||h(`modal.failedStatus`,{status:t.status})});return}r(a.name)}catch{y({type:`set-error`,error:h(`modal.networkError`)})}finally{y({type:`set-saving`,saving:!1})}},{loginOAuth:ne,submitManualCode:re}=Vc({apiBase:e,t:h,aliveRef:b,onAdded:r}),ie={setOauthBusy:e=>y({type:`set-oauth-busy`,busy:e}),setOauthMsg:e=>y({type:`set-oauth-msg`,msg:e}),setOauthMsgTone:e=>y({type:`set-oauth-tone`,tone:e}),setOauthUrl:(e,t,n,r)=>y({type:`set-oauth-url`,url:e,providerId:t,deviceCode:n,instructions:r}),setManualCode:e=>y({type:`set-manual-code`,code:e}),setManualCodeMsg:e=>y({type:`set-manual-code-msg`,msg:e}),setManualCodeOk:e=>y({type:`set-manual-code-msg`,msg:U,ok:e})},ae=j?t.includes(j.name.trim())&&j.name.trim()!==``:!1,oe=e=>{if(!P){if(fc(e)){y({type:`set-oauth-tos-pending`,providerId:e});return}ne(e,ie)}},se=e=>{re(e,V,H,{setManualCodeBusy:e=>y({type:`set-manual-code-busy`,busy:e}),setManualCode:e=>y({type:`set-manual-code`,code:e}),setManualCodeOk:e=>y({type:`set-manual-code-msg`,msg:U,ok:e}),setManualCodeMsg:e=>y({type:`set-manual-code-msg`,msg:e})})},ce=A?.id===`custom`,le=j?.authMode===`local`,ue=A?Ks(A):!1;return(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)(`div`,{role:`dialog`,"aria-modal":`true`,"aria-label":h(`modal.add`),className:`modal-overlay`,children:(0,J.jsxs)(`div`,{ref:S,className:`modal-card`,children:[(0,J.jsxs)(`div`,{className:`modal-head`,children:[(0,J.jsx)(`h3`,{children:A?h(`modal.addNamed`,{label:A.label}):h(`modal.add`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-icon`,"aria-label":h(`common.close`),onClick:n,children:(0,J.jsx)(de,{})})]}),A?j&&(A.auth===`oauth`&&j.authMode===`oauth`?(0,J.jsx)(Lc,{preset:A,oauthSupported:E,oauthBusy:P,oauthMsg:F,oauthMsgTone:I,oauthUrl:R===A.oauthProvider?L:``,oauthDeviceCode:R===A.oauthProvider?z:``,oauthInstructions:R===A.oauthProvider?B:``,manualCode:V,manualCodeBusy:H,manualCodeMsg:U,manualCodeOk:W,onRequestLogin:oe,onUseApiKeyInstead:()=>{y({type:`use-api-key-instead`,form:{...j,authMode:`key`}})},onManualCodeChange:e=>y({type:`set-manual-code`,code:e}),onSubmitManualCode:e=>{se(e)},onBack:()=>y({type:`back`})}):(0,J.jsx)(zc,{preset:A,form:j,endpointChoice:ee,error:N,saving:M,dup:ae,isCustom:ce,isLocal:le,isReservedForward:ue,presetDescription:q,onFormChange:e=>y({type:`set-form`,form:e}),onEndpointChoiceChange:e=>y({type:`set-endpoint-choice`,choice:e}),onSubmit:()=>{te()},onUseOauthLogin:()=>y({type:`use-oauth-login`,form:{...j,authMode:`oauth`}}),onBack:()=>y({type:`back`})})):(0,J.jsx)(Ic,{presets:D,usageRank:k,presetsLoading:O,initialTier:i,onSelectPreset:e=>Y(e),onSelectCustom:()=>Y(g[0]),accountRows:o,accountStatus:s,busyProvider:c,onLogin:u,onCancelLogin:d,onLogout:f,onManage:p,loginHint:l,paste:{value:V,busy:H,message:U,ok:W,onChange:e=>y({type:`set-manual-code`,code:e}),onSubmit:e=>{se(e)}}})]})}),K&&(0,J.jsx)(Dc,{providerId:K,providerLabel:A?.label??K,onCancel:()=>y({type:`set-oauth-tos-pending`,providerId:null}),onContinue:()=>{let e=K;e&&(y({type:`set-oauth-tos-pending`,providerId:null}),ne(e,ie))}},K)]})}function Gc({apiBase:e,config:t,adding:n,addIntent:r,busy:i,addModalAccountRows:a,accountLoginStatus:o,accountLoginHint:s,removeConfirmName:c,removeDefaultProvider:l,codexLoginOpen:u,jsonLeaveOpen:d,jsonSaving:f,oauthTosPending:p,onCloseAdd:m,onAdded:h,onAccountLogin:g,onAccountCancelLogin:_,onAccountLogout:v,onAccountManage:y,onOpenAdd:b,onCloseCodexLogin:x,onCodexAdded:S,onCancelRemove:C,onConfirmRemove:w,onCancelJsonLeave:T,onDiscardJson:E,onSaveJson:D,onCancelOauthTos:O,onContinueOauthTos:k}){return(0,J.jsxs)(J.Fragment,{children:[n&&(0,J.jsx)(Wc,{apiBase:e,existingNames:Object.keys(t.providers),initialTier:r?.tier,initialCustom:r?.custom,onClose:m,onAdded:h,accountRows:a,accountStatus:o,accountBusy:i,accountLoginHint:s??null,onAccountLogin:g,onAccountCancelLogin:_,onAccountLogout:v,onAccountManage:y,onOpen:b}),u&&(0,J.jsx)(co,{apiBase:e,onClose:x,onAdded:S}),c&&(0,J.jsx)(sc,{providerName:c,defaultProviderName:l,onCancel:C,onConfirm:w}),d&&T&&E&&D&&(0,J.jsx)(cc,{saving:f??!1,onCancel:T,onDiscard:E,onSave:D}),p&&(0,J.jsx)(Dc,{providerId:p.provider,providerLabel:xc(p.provider),onCancel:O,onContinue:k},`${p.provider}:${p.addAccount?`add`:`login`}`)]})}function Kc(e,t,n){return[...Ws(e.providers).map(e=>({id:e,label:jn(e,n),kind:`codex`,href:`#codex-set`})),...t.toSorted((e,t)=>e.localeCompare(t)).map(e=>({id:e,label:xc(e),kind:`oauth`}))]}function qc(e,t){let n={...t},r=t.openai;if(r)for(let[t,i]of Object.entries(e.providers))i.authMode===`forward`&&(n[t]=r);return n}function Jc({apiBase:e}){let t=Q(),n=`ocx.providers.config.v1:${e}`,[r,i]=(0,_.useState)(()=>gr(n)),[a,o]=(0,_.useState)(!1),[s,c]=(0,_.useState)(``),[l,u]=(0,_.useState)(!1),[d,f]=(0,_.useState)(`err`),[p,m]=(0,_.useState)(0),[h,g]=(0,_.useState)([]),[v,y]=(0,_.useState)({}),[b,x]=(0,_.useState)(null),[S,C]=(0,_.useState)(null),[w,T]=(0,_.useState)(null),[E,D]=(0,_.useState)(null),[O,k]=(0,_.useState)(null),[A,j]=(0,_.useState)(!1),[M,N]=(0,_.useState)(0),[P,F]=(0,_.useState)(null),[I,L]=(0,_.useState)({token:0,provider:null}),R=(0,_.useRef)(!0),z=(0,_.useRef)(null),B=(0,_.useRef)(!1),V=(0,_.useCallback)((e,t=!0)=>{c(e),u(t),f(t?`ok`:`err`),m(e=>e+1)},[]),H=(0,_.useCallback)(()=>{c(``),u(!1),f(`err`)},[]),U=(0,_.useCallback)(e=>{if(e.catalogRefreshPending){c(t(`codexAuth.catalogRefreshPending`)),u(!1),f(`warn`),m(e=>e+1);return}V(t(`codexAuth.accountAdded`),!0)},[V,t]);(0,_.useEffect)(()=>(R.current=!0,()=>{R.current=!1}),[]),(0,_.useEffect)(()=>{if(!s||!l)return;let e=window.setTimeout(H,4500);return()=>window.clearTimeout(e)},[s,l,p,H]);let W=(0,_.useCallback)(e=>{o(!1),D(null),T(e),L(t=>({token:t.token+1,provider:e}))},[]);G(`add-provider-presets:${e}`,[e],async t=>{let n=await fetch(`${e}/api/provider-presets`,{signal:t});if(!n.ok)throw Error(String(n.status));let r=await n.json();return Array.isArray(r.providers)&&r.providers.length>0?r.providers:null}),G(Br(e),[e],async t=>{let n=await fetch(`${e}/api/usage?range=30d`,{signal:t});if(!n.ok)throw Error(String(n.status));return await n.json()},{deadlineMs:6e4});let[ee,K]=(0,_.useState)({epoch:0,force:!1}),{fetchConfig:q,fetchOauth:Y,fetchProviderQuotas:te}=Ec({apiBase:e,t,setConfig:i,setOauthProviders:g,setOauthStatus:y,notify:V,invalidateProviderQuotas:(0,_.useCallback)((e=!1)=>{K(t=>({epoch:t.epoch+1,force:e}))},[]),configCacheKey:n}),ne=Do(e),re=ne.activeNeedsReauth,ie=(0,_.useMemo)(()=>{let e=ne.accounts;if(e.length===0&&ne.loadState===`loading`)return v;let t=e.find(e=>e.isMain)??e[0],n=!!t&&!!t.email&&t.email!==`Codex App login`,r=e.some(e=>!e.isMain&&(e.hasCredential||e.email)),i=n||r,a=n?t?.email:e.find(e=>!e.isMain&&e.email)?.email??void 0;return{...v,openai:{loggedIn:i,...a?{email:a}:{},...re?{needsReauth:!0}:{}}}},[v,ne.accounts,ne.loadState,re]),{accountSets:ae,setAccountSets:oe,accountLoadStates:se,switchingAccount:ce,keyPools:le,fetchAccountSets:ue,switchAccount:de,switchApiKey:pe,removeApiKey:X,addApiKeyValue:me,editCredentialAlias:he,removeAccount:ge,activeAccountNeedsReauth:_e}=gc({apiBase:e,t,config:r,oauthStatus:ie,aliveRef:R,notify:V,fetchConfig:q,fetchOauth:Y,fetchProviderQuotas:te,codexActiveNeedsReauth:re}),{draft:Z,setDraft:ve,jsonEditorOpen:ye,jsonSaving:be,jsonLeaveOpen:xe,saveConfig:Se,openJsonEditor:Ce,discardJsonEditor:we,requestCloseJsonEditor:Te,restoreJsonEditor:Ee,jsonIsDirty:De,setJsonLeaveOpen:Oe}=yc({apiBase:e,config:r,notify:V,fetchConfig:q,fetchProviderQuotas:te,onSaved:()=>N(e=>e+1),t});(0,_.useEffect)(()=>{z.current!==e&&(z.current=e,Promise.resolve().then(()=>{q(),Y()}))},[e,q,Y]);let ke=()=>N(e=>e+1),{cancelLoginOAuth:Ae,loginOAuth:je,logoutOAuth:Me}=Sc({apiBase:e,t,aliveRef:R,accountSets:ae,setAccountSets:oe,setBusy:x,setStatus:c,setLoginInfo:C,setOauthStatus:y,notify:V,fetchConfig:q,fetchOauth:Y,fetchAccountSets:ue,fetchProviderQuotas:te,bumpModelsRefresh:ke,onLoginSettled:W}),{removeProvider:Ne,confirmRemoveProvider:Pe,setProviderDisabled:Fe,setDefaultProvider:Ie,updateProvider:Le}=Tc({apiBase:e,t,removeBusyRef:B,workspaceSelected:w,setWorkspaceSelected:T,setRemoveConfirmName:k,notify:V,fetchConfig:q,fetchOauth:Y,fetchProviderQuotas:te,refreshCodexAccount:()=>ne.load(!0)}),Re=(e,t=!1,n)=>{if(b!==e){if(fc(e)){F({provider:e,addAccount:t,...n?{accountId:n}:{}});return}je(e,t,n)}};if(!r)return(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)(`div`,{className:`page-head`,children:(0,J.jsx)(`h2`,{children:t(`nav.providers`)})}),s?(0,J.jsx)(Et,{tone:d,onDismiss:H,dismissLabel:t(`common.close`),children:s}):(0,J.jsxs)(`div`,{className:`providers-workspace providers-workspace--boot`,"aria-busy":`true`,children:[(0,J.jsx)(`div`,{className:`providers-workspace-rail providers-workspace-rail--boot`,"aria-hidden":`true`}),(0,J.jsx)(`div`,{className:`providers-workspace-main`,children:(0,J.jsxs)(`p`,{className:`muted`,children:[(0,J.jsx)(`span`,{className:`spin`,"aria-hidden":`true`}),` `,t(`prov.loadingConfig`)]})})]})]});let ze=Kc(r,h,t),Be=qc(r,ie),Ve=e=>r.providers[e]?.authMode===`forward`;return(0,J.jsxs)(J.Fragment,{children:[(0,J.jsxs)(`div`,{className:`page-head`,children:[(0,J.jsx)(`h2`,{children:t(`nav.providers`)}),(0,J.jsx)(`div`,{className:`row`,children:(0,J.jsxs)(`button`,{type:`button`,className:`btn btn-primary`,onClick:()=>o(!0),children:[(0,J.jsx)(fe,{}),t(`prov.add`)]})})]}),s&&(0,J.jsx)(Et,{tone:d,onDismiss:H,dismissLabel:t(`common.close`),children:s}),(0,J.jsx)(Sa,{onRemoveProvider:Ne,providers:r.providers,apiBase:e,defaultProvider:r.defaultProvider,selectedName:w,onSelect:T,onAddProvider:e=>{D(e??null),o(!0)},onEditConfig:Ce,jsonEditor:{open:ye,draft:Z,isDirty:De,onDraftChange:ve,onSave:()=>Se(),onClose:Te,onRestore:Ee},jsonSaving:be,modelsRefreshToken:M,activeAccountNeedsReauth:_e,quotaRefreshEpoch:ee.epoch,quotaForceRefresh:ee.force,detail:(t,n)=>{let i=Be[t.name]??v[t.name];return(0,J.jsx)(lc,{item:t,usageTotals:n.usageTotals,modelUsage:n.modelUsage,quotaReport:n.quotaReport,availableModels:n.availableModels,hasLiveModels:n.hasLiveModels,selectedModels:n.selectedModels,modelsLoading:n.modelsLoading,modelsLoadFailed:n.modelsLoadFailed,onRetryModels:n.onRetryModels,oauthEmail:i?.email,onDeselect:()=>T(null),apiBase:e,oauth:i,accounts:ae[t.name]?.accounts??[],keys:le[t.name]??[],accountLoadState:se[t.name]??(t.authMode===`oauth`?`idle`:`ready`),accountsFocusToken:I.token,accountsFocusProvider:I.provider,switchingAccountId:ce?.provider===t.name?ce.accountId:null,busyProvider:b,loginHint:S,authHandlers:{onLogin:Re,onCancelLogin:Ae,onLogout:Me,onReauth:(e,t)=>Re(e,!0,t),onSwitchAccount:de,onRemoveAccount:ge,onRetryAccounts:async e=>{await ue([e])},onAddApiKey:me,onSwitchApiKey:pe,onRemoveApiKey:X,onEditAlias:he},isDefault:t.name===r.defaultProvider,onRemoveProvider:Ne,onSetDisabled:Fe,onSetDefault:e=>{Ie(e)},onUpdateProvider:Le,codexController:ne},t.name)}}),(0,J.jsx)(Gc,{apiBase:e,config:r,adding:a,addIntent:E,busy:b,addModalAccountRows:ze,accountLoginStatus:Be,accountLoginHint:S,removeConfirmName:O,removeDefaultProvider:O===r.defaultProvider?Object.entries(r.providers).find(([e,t])=>e!==O&&t.disabled!==!0)?.[0]??null:null,codexLoginOpen:A,jsonLeaveOpen:xe,jsonSaving:be,oauthTosPending:P,onCloseAdd:()=>{b&&Ae(b),o(!1),D(null)},onAdded:e=>{o(!1),D(null),V(t(`prov.added`,{name:e,cmd:`ocx sync`}),!0),q(),Y(),te(!0),ke()},onAccountLogin:async(n,i=!1)=>{if(n===`openai`){if(b===`openai`)return;let n=r.providers.openai,i=Gs(n);if(i===`invalid`){V(t(`codexAuth.openaiMissing`),!1);return}if(i===`absent`||i===`disabled`){x(`openai`);try{await Qs(e,i),await q()}catch(e){e instanceof Zs?V(t(e.i18nKey),!1):V(e instanceof Error?e.message:t(`prov.saveFailed`),!1);return}finally{R.current&&x(e=>e===`openai`?null:e)}}j(!0);return}if(Ve(n)){j(!0);return}(r.providers[n]?.authMode===`oauth`||h.includes(n))&&Re(n,i)},onAccountCancelLogin:e=>{Ae(e)},onAccountLogout:e=>{Me(e)},onAccountManage:e=>{W(e)},onOpenAdd:Y,onCloseCodexLogin:()=>j(!1),onCodexAdded:e=>{j(!1),U(e),q(),Y(),te(!0),ke()},onCancelRemove:()=>k(null),onConfirmRemove:()=>{Pe(O)},onCancelJsonLeave:()=>{be||Oe(!1)},onDiscardJson:we,onSaveJson:()=>{Se()},onCancelOauthTos:()=>F(null),onContinueOauthTos:()=>{let e=P;e&&(F(null),je(e.provider,e.addAccount,e.accountId))}})]})}function Yc(e){let{t}=ct();return e.state===`stale`?(0,J.jsxs)(`div`,{className:`codex-stale-banner`,role:`status`,children:[(0,J.jsx)(`span`,{className:`codex-stale-banner-text`,children:t(`models.staleBanner`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-sm`,disabled:e.controller.restarting,onClick:()=>{e.controller.restart()},children:e.controller.restarting?t(`dash.codexRestarting`):t(`dash.codexRestart`)})]}):null}var Xc=[`fresh`,`stale`,`not_running`,`unknown`],Zc=[`stopped`,`nothing_running`,`enumeration_unavailable`,`partially_stopped`];function Qc(e){return Array.isArray(e)&&e.every(e=>typeof e==`number`&&Number.isSafeInteger(e)&&e>0)}function $c(e){if(typeof e!=`object`||!e)return!1;let t=e;if(!(typeof t.success==`boolean`&&typeof t.synced==`boolean`&&typeof t.stateBefore==`string`&&Xc.includes(t.stateBefore)&&typeof t.code==`string`&&Zc.includes(t.code)&&Qc(t.requested)&&Qc(t.stopped)&&Qc(t.surviving)&&Qc(t.failed)))return!1;let n=t.success,r=t.code,i=t.surviving,a=t.failed,o=t.stopped;return!(n!==(r!==`partially_stopped`)||n&&(i.length>0||a.length>0)||!n&&i.length===0&&a.length===0||(r===`nothing_running`||r===`enumeration_unavailable`)&&o.length>0)}function el(e){if(typeof e!=`object`||!e)return!1;let t=e;return typeof t.state==`string`&&Xc.includes(t.state)&&typeof t.runningCount==`number`&&Number.isSafeInteger(t.runningCount)&&t.runningCount>=0}var tl={state:null,runningCount:0};async function nl(e,t={}){let n=t.fetchFn??fetch;try{let r=await n(`${e}/api/system/codex-app-server`,{signal:t.signal});if(!r.ok)return tl;let i=await r.json().catch(()=>null);return el(i)?{state:i.state,runningCount:i.runningCount}:tl}catch{return tl}}var rl=3e4;function il(e){return(e instanceof DOMException||e instanceof Error)&&(e.name===`AbortError`||e.name===`TimeoutError`)}async function al(e,t={}){let{fetchFn:n=fetch,timeoutMs:r=rl,formatFailure:i=e=>`Failed to restart Codex (HTTP ${e}).`,formatUnreachable:a=()=>`Could not reach the proxy.`,formatMalformed:o=()=>`The proxy returned an unexpected response.`,formatTimeout:s=()=>`The proxy did not answer in time. It may still be working.`}=t,c;try{c=await n(`${e}/api/system/codex-restart`,{method:`POST`,signal:AbortSignal.timeout(r)})}catch(e){return{ok:!1,message:il(e)?s():a()}}if(!c.ok)return{ok:!1,message:i(c.status)};let l;try{l=await c.json()}catch(e){return{ok:!1,message:il(e)?s():o()}}return $c(l)?{ok:!0,result:l}:{ok:!1,message:o()}}function ol(e){return e===`stopped`||e===`nothing_running`}function sl(e,t={}){let{t:n}=ct(),[r,i]=(0,_.useState)(!1),a=(0,_.useRef)(!0),o=(0,_.useRef)(t.onSettled);return(0,_.useEffect)(()=>{o.current=t.onSettled},[t.onSettled]),(0,_.useEffect)(()=>(a.current=!0,()=>{a.current=!1}),[]),{restarting:r,restart:(0,_.useCallback)(async()=>{if(!confirm(n(`dash.codexRestartConfirm`)))return null;i(!0);let t=await al(e,{formatFailure:e=>n(`dash.codexRestartFailed`,{status:String(e)}),formatUnreachable:()=>n(`dash.codexRestartUnreachable`),formatTimeout:()=>n(`dash.codexRestartTimeout`),formatMalformed:()=>n(`dash.codexRestartMalformed`)});if(a.current&&i(!1),!t.ok||!t.result)return alert(t.message),null;let r=t.result;return r.code===`stopped`?alert(n(`dash.codexRestartDone`,{count:String(r.stopped.length)})):r.code===`nothing_running`?alert(n(`dash.codexRestartNothing`)):r.code===`enumeration_unavailable`?alert(n(`dash.codexRestartUnknown`)):alert(n(`dash.codexRestartPartial`,{count:String(r.surviving.length)})),a.current&&ol(r.code)&&o.current?.(r.code),r.code},[e,n])}}var cl={"gpt-5.6-sol":Ae,"gpt-5.6-terra":Ne,"gpt-5.6-luna":je,"gpt-daybreak-blue-latest":Ae,"daybreak-blue-latest":Ae,"daybreak-red-latest":De},ll={width:14,height:14,flexShrink:0,verticalAlign:`text-bottom`};function ul(e){return e.slice(e.lastIndexOf(`/`)+1)}function dl(e){return cl[e]??cl[ul(e)]??null}function fl(e){let t=dl(e);return t?(0,_.createElement)(`span`,{className:`model-label`},(0,_.createElement)(t,{style:ll,"aria-hidden":!0}),e):e}function pl(e,t,n){if(!n)return{kind:`disabled`,data:void 0,error:void 0,showSkeleton:!1,refreshing:!1,showError:!1};let r=e.data!==void 0,i=!e.lastAttemptOk&&e.error!==void 0;return e.refreshing?r?{kind:`loading-with-stale-data`,data:e.data,error:e.error,showSkeleton:!1,refreshing:!0,showError:i}:{kind:i?`retrying-cold`:`cold`,data:void 0,error:i?e.error:void 0,showSkeleton:!0,refreshing:!0,showError:!1}:i?r?{kind:`failed-with-stale`,data:e.data,error:e.error,showSkeleton:!1,refreshing:!1,showError:!0}:{kind:`failed-cold`,data:void 0,error:e.error,showSkeleton:!1,refreshing:!1,showError:!0}:r?{kind:t(e.data)?`ready-empty`:`ready-populated`,data:e.data,error:void 0,showSkeleton:!1,refreshing:!1,showError:!1}:{kind:`cold`,data:void 0,error:void 0,showSkeleton:!0,refreshing:!1,showError:!1}}function ml(e,t,n,r){let{isEmpty:i,sessionCacheKey:a,...o}=r,s=(0,_.useMemo)(()=>a?vr(a):null,[a]),c=G(e,t,(0,_.useCallback)(async e=>{let t=await n(e);return a&&yr(a,t),t},[n,a]),{...o,...a?{initialData:o.initialData??s?.data,initialDataCachedAt:o.initialDataCachedAt??s?.cachedAt??null}:{}});return{...c,state:pl(c,i,r.enabled!==!1)}}function hl({className:e,style:t}){return(0,J.jsx)(`span`,{"aria-hidden":`true`,className:e?`data-surface-skeleton__block ${e}`:`data-surface-skeleton__block`,style:t})}function gl({label:e,rows:t=3,className:n}){let r=Math.max(1,Math.floor(t));return(0,J.jsxs)(`div`,{className:n?`data-surface-skeleton ${n}`:`data-surface-skeleton`,role:`status`,"aria-live":`polite`,"aria-atomic":`true`,"aria-busy":`true`,children:[(0,J.jsx)(`span`,{className:`sr-only`,children:e}),Array.from({length:r},(e,t)=>(0,J.jsx)(`div`,{className:`data-surface-skeleton__row`,"aria-hidden":`true`,children:(0,J.jsx)(hl,{})},t))]})}function _l({children:e,busy:t=!0,live:n=!0,className:r}){return(0,J.jsxs)(`div`,{className:r?`data-surface-status ${r}`:`data-surface-status`,role:n?`status`:void 0,"aria-live":n?`polite`:void 0,"aria-atomic":n?`true`:void 0,"aria-busy":t||void 0,children:[t&&(0,J.jsx)(`span`,{className:`spin`,"aria-hidden":`true`}),(0,J.jsx)(`span`,{children:e})]})}var vl=class extends _.Component{state={error:null};static getDerivedStateFromError(e){return{error:e instanceof Error?e:Error(String(e))}}reload=()=>{this.setState({error:null})};render(){return this.state.error?(0,J.jsxs)(`section`,{className:`card`,role:`alert`,style:{maxWidth:720,padding:`var(--space-6)`},children:[(0,J.jsxs)(`h2`,{style:{margin:`0 0 var(--space-2)`,fontSize:`var(--text-title)`},children:[this.props.pageName,`: `,this.props.title]}),(0,J.jsx)(`p`,{className:`muted`,style:{margin:`0 0 var(--space-4)`},children:this.props.message}),(0,J.jsxs)(`p`,{style:{margin:`0 0 var(--space-5)`,overflowWrap:`anywhere`},children:[(0,J.jsxs)(`strong`,{children:[this.props.detailsLabel,`:`]}),` `,this.state.error.message]}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-primary`,onClick:this.reload,children:this.props.reloadLabel})]}):this.props.children}},yl=`gpt-daybreak-blue-latest`,bl=`gpt-6-astra`,xl=Object.freeze({[yl]:`gpt-5.6-sol`});Object.freeze(Object.keys(xl)),Object.freeze({[yl]:{displayName:`Daybreak Blue`,description:`Frontier general-purpose model with safeguards for defensive cybersecurity work.`}});var Sl=new Set([`gpt-5.5`,`gpt-5.4`,`gpt-5.4-mini`,`gpt-5.3-codex-spark`,`gpt-5.6-sol`,`gpt-5.6-terra`,`gpt-5.6-luna`,yl,bl]),Cl=[`low`,`medium`,`high`,`xhigh`,`max`,`ultra`],wl=[`failover`,`round-robin`,`random`,`least-used`,`reset-window`],Tl={failover:`cws.strategy.failover`,"round-robin":`cws.strategy.roundRobin`,random:`cws.strategy.random`,"least-used":`cws.strategy.leastUsed`,"reset-window":`cws.strategy.resetWindow`},El={failover:`cws.strategy.failoverHint`,"round-robin":`cws.strategy.roundRobinHint`,random:`cws.strategy.randomHint`,"least-used":`cws.strategy.leastUsedHint`,"reset-window":`cws.strategy.resetWindowHint`},Dl={failover:`cws.targets.failoverHint`,"round-robin":`cws.targets.roundRobinHint`,random:`cws.targets.randomHint`,"least-used":`cws.targets.leastUsedHint`,"reset-window":`cws.targets.resetWindowHint`},Ol=new Set(wl);function kl(e,t,n=`strict`){let r=e.filter(e=>e.provider.trim()&&e.model.trim());if(r.length===0)return[...Cl];let i=new Set(Cl),a=null;for(let e of r){let r=`${e.provider.trim()}/${e.model.trim()}`,o=t.get(r);if(o===void 0||n===`adaptive`&&o.length===0)continue;let s=o.filter(e=>i.has(e));if(a===null)a=s;else{let e=new Set(s);a=a.filter(t=>e.has(t))}}if(a===null)return[...Cl];let o=new Set(a);return Cl.filter(e=>o.has(e))}var Al=0;function jl(e={}){return{provider:e.provider??``,model:e.model??``,...e.weight===void 0?{}:{weight:e.weight},clientKey:e.clientKey??`ct-${++Al}`}}function Ml(e){return e===`disabled`?`disabled`:`auto`}function Nl(e){return e===`adaptive`?`adaptive`:`strict`}var Pl=/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$/,Fl=/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}(\/[a-zA-Z0-9][a-zA-Z0-9._-]{0,63})?$/,Il=/^(?:gpt-|o1-|o3-|o4-|codex-)/;function Ll(e){return Pl.test(e.trim())}function Rl(e){return`combo/${e.trim()}`}function zl(e,t){return(typeof t==`string`?t.trim():``)||Rl(e)}function Bl(e,t){let n=t.trim(),r=e.nativeAlias&&(!n||n.includes(`/`)||!Il.test(n));return{...e,alias:n?t:null,model:zl(e.id,t),...r?{nativeAlias:!1,displayName:null}:{}}}function Vl(e){return typeof e==`string`&&e.trim()?e.trim():null}function Hl(e){return typeof e==`string`&&Ol.has(e)?e:`failover`}function Ul(e){return typeof e==`number`&&Number.isInteger(e)&&e>=1&&e<=100?e:1}function Wl(e){return typeof e==`string`&&Cl.includes(e)?e:null}function Gl(e){return typeof e==`number`&&Number.isInteger(e)&&e>=1&&e<=1e4?e:void 0}function Kl(e){if(!e||typeof e!=`object`)return[];let t=e.combos;if(!Array.isArray(t))return[];let n=[];for(let e of t){if(!e||typeof e!=`object`)continue;let t=e,r=typeof t.id==`string`?t.id.trim():``;if(!r)continue;let i=Array.isArray(t.targets)?t.targets:[],a=[];for(let e of i){if(!e||typeof e!=`object`)continue;let t=e,n=typeof t.provider==`string`?t.provider.trim():``,r=typeof t.model==`string`?t.model.trim():``;if(!n||!r)continue;let i=Gl(t.weight);a.push(jl(i===void 0?{provider:n,model:r}:{provider:n,model:r,weight:i}))}n.push({id:r,model:typeof t.model==`string`&&t.model.trim()?t.model.trim():zl(r,Vl(t.alias)),alias:Vl(t.alias),nativeAlias:t.nativeAlias===!0,displayName:Vl(t.displayName),strategy:Hl(t.strategy),stickyLimit:Ul(t.stickyLimit),defaultEffort:Wl(t.defaultEffort),imageInput:Ml(t.imageInput),reasoningEffortMode:Nl(t.reasoningEffortMode),targets:a})}return n.sort((e,t)=>e.id.localeCompare(t.id,void 0,{sensitivity:`base`}))}function ql(e){let t=[],n=[],r=[];for(let i of e)i.strategy===`failover`?t.push(i):i.strategy===`round-robin`?n.push(i):r.push(i);return{failover:t,roundRobin:n,other:r}}function Jl(e,t){let n=t.trim().toLowerCase();return n?e.filter(e=>e.id.toLowerCase().includes(n)||e.model.toLowerCase().includes(n)?!0:e.targets.some(e=>e.provider.toLowerCase().includes(n)||e.model.toLowerCase().includes(n))):e}function Yl(e){return e&&typeof e==`object`&&!Array.isArray(e)?e:null}function Xl(e){return typeof e==`number`&&Number.isFinite(e)?e:null}function Zl(e,t){let n=Xl(e);return n!==null&&t-n<18e5}function Ql(e){let t=Xl(e);return t!==null&&Number.isInteger(t)&&t>=0?t:null}function $l(e,t){let n=Yl(e),r=Xl(n?.usedPercent);return!!n&&r!==null&&r>=0&&Ql(n.includedAccounts)!==null&&(Ql(n.includedAccounts)??0)>0&&Ql(n.excludedAccounts)===0&&n.incomplete===!1&&Zl(n.updatedAt,t)}function eu(e,t){let n=Yl(e);if(!n||n.kind!==`capacity-weighted-v1`||n.scope!==`routable-known`||n.presentation!==`aggregate`||n.incomplete!==!1)return!1;for(let e of[`includedAccounts`,`excludedAccounts`,`unknownPlanAccounts`,`missingQuotaAccounts`,`pausedAccounts`,`reauthAccounts`,`staleQuotaAccounts`,`partialWindowAccounts`])if(Ql(n[e])===null)return!1;if((Ql(n.includedAccounts)??0)===0)return!1;for(let e of[`excludedAccounts`,`unknownPlanAccounts`,`missingQuotaAccounts`,`pausedAccounts`,`reauthAccounts`,`staleQuotaAccounts`,`partialWindowAccounts`])if(n[e]!==0)return!1;let r=!1;for(let e of[`fiveHour`,`weekly`,`monthly`])if(Object.hasOwn(n,e)){if(!$l(n[e],t))return!1;r=!0}if(Object.hasOwn(n,`customWindows`)){if(!Array.isArray(n.customWindows))return!1;for(let e of n.customWindows){let n=Yl(e);if(!n||typeof n.label!=`string`||!n.label.trim()||!$l(n,t))return!1;r=!0}}return r}function tu(e,t){if(!Zl(e.updatedAt,t))return`unknown`;let n=Yl(e.quota);if(!n||!Zl(n.updatedAt,t)||e.aggregation!==void 0&&!eu(e.aggregation,t))return`unknown`;let r=!1,i=!1;for(let e of[`fiveHourPercent`,`weeklyPercent`,`monthlyPercent`]){if(!Object.hasOwn(n,e))continue;let t=Xl(n[e]);if(t===null||t<0)return`unknown`;r=!0,t>=100&&(i=!0)}for(let e of[`fiveHourResetAt`,`weeklyResetAt`,`monthlyResetAt`])if(Object.hasOwn(n,e)&&Xl(n[e])===null)return`unknown`;if(Object.hasOwn(n,`customWindows`)){if(!Array.isArray(n.customWindows))return`unknown`;for(let e of n.customWindows){let t=Yl(e),n=Xl(t?.percent);if(!t||typeof t.label!=`string`||!t.label.trim()||n===null||n<0||Object.hasOwn(t,`resetAt`)&&Xl(t.resetAt)===null)return`unknown`;r=!0,n>=100&&(i=!0)}}if(Object.hasOwn(n,`creditsUsd`)){let e=Yl(n.creditsUsd);if(!e)return`unknown`;let t=Xl(e.used),a=Xl(e.limit),o=Xl(e.remaining),s=Xl(e.percent);if(t===null||t<0||a===null||a<0||o===null||s===null||s<0||e.unlimited!==void 0&&typeof e.unlimited!=`boolean`||Object.hasOwn(e,`expiresAt`)&&Xl(e.expiresAt)===null)return`unknown`;r=!0,e.unlimited!==!0&&o<=0&&(i=!0)}return r?i?`exhausted`:`available`:`unknown`}function nu(e,t=Date.now()){if(!Array.isArray(e))return{};let n={};for(let r of e){let e=Yl(r),i=typeof e?.provider==`string`?e.provider.trim():``;if(!e||!i)continue;let a=tu(e,t);n[i]=Object.hasOwn(n,i)&&n[i]!==a?`unknown`:a}return n}function ru(e,t,n){let r=e.flatMap(e=>{let t=e.provider.trim();return!t||!e.model.trim()||!Object.hasOwn(n,t)||n[t]?.disabled===!0?[]:[t]});if(r.length===0)return`unknown`;let i=!1;for(let e of r){let n=t[e]??`unknown`;if(n===`available`)return`available`;n===`unknown`&&(i=!0)}return i?`unknown`:`exhausted`}function iu(e,t={}){let n=[],r=t.cataloguedComboIds;for(let i of e)i.targets.length===0?n.push({id:i.id,model:i.model,reason:`empty-targets`}):i.targets.length<2&&n.push({id:i.id,model:i.model,reason:`few-targets`}),r&&i.targets.length>0&&!r.has(i.id)&&n.push({id:i.id,model:i.model,reason:`catalog-omitted`}),t.providerQuotaStates&&t.providers&&ru(i.targets,t.providerQuotaStates,t.providers)===`exhausted`&&n.push({id:i.id,model:i.model,reason:`all-targets-exhausted`});return n}function au(e,t){return e.id!==t.id||e.alias!==t.alias||e.nativeAlias!==t.nativeAlias||e.displayName!==t.displayName||e.strategy!==t.strategy||e.stickyLimit!==t.stickyLimit||e.defaultEffort!==t.defaultEffort||(e.imageInput??`auto`)!==(t.imageInput??`auto`)||(e.reasoningEffortMode??`strict`)!==(t.reasoningEffortMode??`strict`)||e.targets.length!==t.targets.length?!1:e.targets.every((e,n)=>{let r=t.targets[n];return e.provider===r.provider&&e.model===r.model&&(e.weight??1)===(r.weight??1)})}function ou(e,t={}){let n=e.strategy===`round-robin`||e.strategy===`random`;return{id:e.id.trim(),...t.renameFrom?{renameFrom:t.renameFrom}:{},combo:{targets:e.targets.map(e=>n?{provider:e.provider.trim(),model:e.model.trim(),weight:e.weight??1}:{provider:e.provider.trim(),model:e.model.trim()}),strategy:e.strategy,defaultEffort:e.defaultEffort,...e.imageInput===`disabled`?{imageInput:`disabled`}:{},...e.reasoningEffortMode===`adaptive`?{reasoningEffortMode:`adaptive`}:{},...e.strategy===`round-robin`?{stickyLimit:e.stickyLimit}:{},...e.alias&&e.alias.trim()?{alias:e.alias.trim()}:{},...e.nativeAlias?{nativeAlias:!0}:{},...e.displayName&&e.displayName.trim()?{displayName:e.displayName.trim()}:{}}}}function su(e,t){let n=e.id.trim();if(!n)return`missingId`;if(!Ll(n))return`invalidId`;if(t.existingIds.includes(n))return`duplicateId`;if(Object.hasOwn(t.providers,`combo`))return`reservedNamespace`;if(Object.hasOwn(t.providers,n))return`providerCollision`;let r=e.alias?.trim()??``,i=e.displayName?.trim()??``;if(r){if(!Fl.test(r))return`invalidAlias`;if(r===`combo`||r.startsWith(`combo/`))return`aliasReservedNamespace`;if(!r.includes(`/`)&&Il.test(r)&&!e.nativeAlias)return`aliasNativeFamily`;if((t.existingAliases??[]).includes(r))return`duplicateAlias`}let a=[...e.displayName??``].some(e=>{let t=e.codePointAt(0)??0;return t<=31||t===127});if(e.displayName!==null&&(i.length>128||a))return`invalidDisplayName`;if(e.nativeAlias&&!Sl.has(r))return`unsupportedNativeAlias`;if(e.nativeAlias&&!i)return`missingNativeAliasDisplayName`;if(e.targets.length<1)return`noTargets`;for(let n of e.targets){if(!n.provider.trim()||!n.model.trim())return`incompleteTarget`;if(!Object.hasOwn(t.providers,n.provider.trim()))return`unknownProvider`}let o=new Set;for(let t of e.targets){let e=`${t.provider.trim()}/${t.model.trim()}`;if(o.has(e))return`duplicateTarget`;o.add(e)}if(e.strategy===`round-robin`&&(!Number.isInteger(e.stickyLimit)||e.stickyLimit<1||e.stickyLimit>100))return`invalidStickyLimit`;if(e.strategy===`round-robin`||e.strategy===`random`)for(let t of e.targets){let e=t.weight??1;if(!Number.isInteger(e)||e<1||e>1e4)return`invalidWeight`}return e.targets.some(e=>t.providers[e.provider.trim()]?.disabled!==!0)?null:`noEnabledTarget`}function cu(e=``){return{id:e,model:e?Rl(e):`combo/`,alias:null,nativeAlias:!1,displayName:null,strategy:`failover`,stickyLimit:1,defaultEffort:null,imageInput:`auto`,reasoningEffortMode:`strict`,targets:[jl()]}}function lu(e,t){return e.length!==0&&e.every(e=>{let n=e.provider.trim(),r=e.model.trim();return!n||!r?!1:!!t.find(e=>e.provider===n&&e.id===r)?.inputModalities?.includes(`image`)})}function uu(e){return e.filter(e=>!e.disabled&&!e.hiddenFromPicker).sort((e,t)=>e.name.localeCompare(t.name))}function du(e,t,n){if(e===``)return;let r=Number(e);if(Number.isFinite(r))return Math.min(n,Math.max(t,r))}function fu(e){if(!e)return!1;let t=e.name.toLowerCase();if(t!==`openai`&&t!==`chatgpt`||(e.authMode??``).toLowerCase()!==`forward`||(e.adapter??``).toLowerCase()!==`openai-responses`)return!1;let n=(e.baseUrl??``).replace(/\/+$/,``);return!n||n.includes(`chatgpt.com/backend-api/codex`)}function pu(e,t,n){let r=new Set([t]),i=n.find(e=>e.name===t);(t.toLowerCase()===`chatgpt`||fu(i))&&r.add(`openai`);let a=[],o=new Set;for(let t of e)!r.has(t.provider)||!t.id||o.has(t.id)||(o.add(t.id),a.push(t.id));return a.toSorted((e,t)=>e.localeCompare(t))}function mu({value:e,onChange:t,disabled:n}){let r=Q();return(0,J.jsx)(`div`,{className:`cwi-strategy-seg`,role:`radiogroup`,"aria-label":r(`cws.strategy`),children:wl.map(i=>(0,J.jsx)(`button`,{type:`button`,role:`radio`,"aria-checked":e===i,className:`btn btn-sm${e===i?` btn-primary`:` btn-ghost`}`,disabled:n,onClick:()=>t(i),children:r(Tl[i])},i))})}function hu({id:e,value:t,onChange:n,disabled:r,allowedEfforts:i}){let a=Q(),o=i??Cl,s=t!==null&&!o.includes(t);return(0,J.jsxs)(J.Fragment,{children:[(0,J.jsxs)(`select`,{id:e,className:`input`,value:t??``,disabled:r,"aria-label":a(`cws.field.defaultEffort`),onChange:e=>n(e.target.value===``?null:e.target.value),children:[(0,J.jsx)(`option`,{value:``,children:a(`cws.field.defaultEffortNone`)}),s&&t?(0,J.jsxs)(`option`,{value:t,children:[t,` (`,a(`cws.field.defaultEffortUnsupportedOption`),`)`]}):null,o.map(e=>(0,J.jsx)(`option`,{value:e,children:e},e))]}),s?(0,J.jsx)(`p`,{className:`muted`,style:{fontSize:12,margin:`4px 0 0`,color:`var(--danger, #b42318)`},children:a(`cws.field.defaultEffortUnsupported`)}):null]})}function gu({targets:e,models:t,imageInput:n,reasoningEffortMode:r,disabled:i,onChange:a}){let o=Q(),s=lu(e,t),c=s&&n!==`disabled`;return(0,J.jsxs)(`section`,{className:`cwi-capabilities`,"aria-label":o(`cws.capabilities`),children:[(0,J.jsx)(`span`,{className:`field-label`,children:o(`cws.capabilities`)}),(0,J.jsxs)(`div`,{className:`cwi-capability-row`,children:[(0,J.jsxs)(`div`,{children:[(0,J.jsx)(`span`,{className:`cwi-capability-label`,children:o(`cws.capability.imageInput`)}),(0,J.jsx)(`p`,{className:`muted cwi-capability-hint`,children:o(s?`cws.capability.imageInputHint`:`cws.capability.imageInputUnavailable`)})]}),(0,J.jsx)(Tt,{on:c,onClick:()=>{s&&a({imageInput:n===`auto`?`disabled`:`auto`})},disabled:i||!s,label:o(`cws.capability.imageInput`)})]}),(0,J.jsxs)(`div`,{className:`cwi-capability-row`,children:[(0,J.jsxs)(`div`,{children:[(0,J.jsx)(`span`,{className:`cwi-capability-label`,children:o(`cws.capability.adaptiveEffort`)}),(0,J.jsx)(`p`,{className:`muted cwi-capability-hint`,children:o(`cws.capability.adaptiveEffortHint`)})]}),(0,J.jsx)(Tt,{on:r===`adaptive`,onClick:()=>{a({reasoningEffortMode:r===`adaptive`?`strict`:`adaptive`})},disabled:i,label:o(`cws.capability.adaptiveEffort`)})]})]})}function _u({targets:e,strategy:t,providers:n,models:r,providerQuotaStates:i,onChange:a}){let o=Q(),s=uu(n),[c,l]=(0,_.useState)(null),[u,d]=(0,_.useState)(null),f=(t,n)=>{a(e.map((e,r)=>r===t?{...e,...n}:e))},p=(t,n)=>{if(t===n||t<0||n<0||t>=e.length||n>=e.length)return;let r=[...e],[i]=r.splice(t,1);r.splice(n,0,i),a(r)};return(0,J.jsxs)(`div`,{className:`cwi-target-list`,children:[e.map((m,h)=>{let g=n.find(e=>e.name===m.provider),_=g&&!s.some(e=>e.name===m.provider)?[...s,g]:s,v=pu(r,m.provider,n),y=m.model&&!v.includes(m.model)?[m.model,...v]:v,b=!m.provider,x=c===h,S=u===h&&c!==null&&c!==h,C=i[m.provider.trim()]??`unknown`;return(0,J.jsxs)(`div`,{className:[`cwi-target-row`,t===`failover`?`cwi-target-row--failover`:``,x?`cwi-target-row--dragging`:``,S?`cwi-target-row--drop`:``].filter(Boolean).join(` `),onDragOver:e=>{c!==null&&(e.preventDefault(),e.dataTransfer.dropEffect=`move`,u!==h&&d(h))},onDrop:e=>{e.preventDefault(),c!==null&&p(c,h),l(null),d(null)},onDragEnd:()=>{l(null),d(null)},children:[(0,J.jsx)(`button`,{type:`button`,className:`cwi-target-grip`,draggable:!0,"aria-label":o(`cws.target.drag`),title:o(`cws.target.drag`),onDragStart:e=>{l(h),e.dataTransfer.effectAllowed=`move`,e.dataTransfer.setData(`text/plain`,String(h))},children:(0,J.jsx)(Fe,{width:14,height:14,"aria-hidden":`true`})}),(0,J.jsxs)(`div`,{className:`cwi-target-reorder`,children:[(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,disabled:h===0,"aria-label":o(`cws.target.moveUp`),onClick:()=>p(h,h-1),children:(0,J.jsx)(ye,{width:14,height:14,"aria-hidden":`true`})}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,disabled:h===e.length-1,"aria-label":o(`cws.target.moveDown`),onClick:()=>p(h,h+1),children:(0,J.jsx)(be,{width:14,height:14,"aria-hidden":`true`})})]}),(0,J.jsxs)(`select`,{className:`input`,value:m.provider,"aria-label":o(`cws.target.provider`),onChange:e=>{let t=e.target.value,i=pu(r,t,n)[0]??``;f(h,{provider:t,model:i})},children:[(0,J.jsx)(`option`,{value:``,children:o(`cws.target.pickProvider`)}),_.map(e=>(0,J.jsx)(`option`,{value:e.name,children:e.disabled?o(`cws.target.disabled`,{name:jn(e.name,o)}):jn(e.name,o)},e.name))]}),(0,J.jsxs)(`select`,{className:`input`,value:m.model,disabled:b,"aria-label":o(`cws.target.model`),onChange:e=>f(h,{model:e.target.value}),children:[(0,J.jsx)(`option`,{value:``,children:b?o(`cws.target.pickProviderFirst`):y.length===0?o(`cws.target.noModels`):o(`cws.target.pickModel`)}),y.map(e=>(0,J.jsx)(`option`,{value:e,children:e},e))]}),(t===`round-robin`||t===`random`)&&(0,J.jsx)(`input`,{className:`input mono`,type:`number`,min:1,max:1e4,value:m.weight??1,"aria-label":o(`cws.target.weight`),onChange:e=>{let t=du(e.target.value,1,1e4);t!==void 0&&f(h,{weight:t})}}),(0,J.jsx)(`span`,{className:`cwi-quota-badge cwi-quota-badge--${C}`,"aria-label":o(`cws.quota.${C}`),children:o(`cws.quota.${C}`)}),(0,J.jsx)(`div`,{className:`cwi-target-actions`,children:(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,disabled:e.length<=1,onClick:()=>a(e.filter((e,t)=>t!==h)),"aria-label":o(`common.remove`),children:(0,J.jsx)(he,{width:14,height:14})})})]},m.clientKey??`${m.provider}:${m.model}`)}),(0,J.jsxs)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,style:{alignSelf:`flex-start`},onClick:()=>a([...e,jl()]),children:[(0,J.jsx)(fe,{width:14,height:14}),` `,o(`cws.target.add`)]})]})}function vu({existingIds:e,existingAliases:t,providerMap:n,providerQuotaStates:r,providers:i,models:a,onClose:o,onSubmit:s}){let c=Q(),l=(0,_.useRef)(null),[u,d]=(0,_.useState)(()=>cu()),[f,p]=(0,_.useState)(!1),[m,h]=(0,_.useState)(``),g=(0,_.useMemo)(()=>{let e=new Map;for(let t of a)e.set(`${t.provider}/${t.id}`,t.reasoningEfforts);return e},[a]),v=(0,_.useMemo)(()=>kl(u.targets,g,u.reasoningEffortMode??`strict`),[u.targets,g,u.reasoningEffortMode]),y=ru(u.targets,r,n)===`exhausted`;(0,_.useEffect)(()=>{let e=l.current;e&&!e.open&&e.showModal()},[]);let b=(0,_.useCallback)(()=>{f||o()},[f,o]),x=(0,_.useCallback)(e=>{e.preventDefault(),b()},[b]),S=async()=>{let r=su(u,{existingIds:e,existingAliases:t,isCreate:!0,providers:n});if(r){h(c(`cws.err.${r}`));return}p(!0),h(``);let i=u.id.trim(),a=u.alias?.trim()||null;try{let e=await s({...u,id:i,alias:a,model:zl(i,a)});if(!e.ok){h(e.error||c(`cws.saveFailed`));return}}finally{p(!1)}};return(0,J.jsxs)(`dialog`,{ref:l,className:`modal-overlay`,"aria-labelledby":`cwi-add-title`,onCancel:x,children:[(0,J.jsx)(`button`,{type:`button`,className:`modal-backdrop-dismiss`,"aria-label":c(`common.close`),tabIndex:-1,onClick:b}),(0,J.jsxs)(`div`,{className:`modal-card`,style:{width:`min(560px, 94vw)`},onClick:e=>e.stopPropagation(),children:[(0,J.jsxs)(`div`,{className:`row`,style:{justifyContent:`space-between`,marginBottom:8},children:[(0,J.jsx)(`h3`,{id:`cwi-add-title`,style:{margin:0},children:c(`cws.addTitle`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:b,disabled:f,"aria-label":c(`common.close`),children:(0,J.jsx)(de,{width:16,height:16})})]}),(0,J.jsx)(`p`,{className:`muted`,style:{marginTop:0,maxWidth:`62ch`,overflowWrap:`anywhere`},children:c(`cws.addSubtitle`)}),m&&(0,J.jsx)($,{tone:`err`,children:m}),y&&(0,J.jsx)(`div`,{className:`cwi-quota-banner`,role:`status`,"aria-live":`polite`,children:c(`cws.quota.allExhausted`)}),(0,J.jsxs)(`div`,{className:`cwi-modal-form`,children:[(0,J.jsxs)(`div`,{className:`cwi-field`,children:[(0,J.jsx)(`label`,{htmlFor:`cwi-new-id`,children:c(`cws.field.id`)}),(0,J.jsx)(`input`,{id:`cwi-new-id`,className:`input mono`,value:u.id,disabled:f,onChange:e=>d(t=>({...t,id:e.target.value,model:zl(e.target.value,t.alias)}))}),(0,J.jsx)(`p`,{className:`muted`,style:{fontSize:12,margin:`8px 0 0`},children:c(`cws.field.idInternalHint`)})]}),(0,J.jsxs)(`div`,{className:`cwi-field`,children:[(0,J.jsx)(`label`,{htmlFor:`cwi-new-alias`,children:c(`cws.field.alias`)}),(0,J.jsx)(`input`,{id:`cwi-new-alias`,className:`input mono`,value:u.alias??``,placeholder:c(`cws.field.aliasPlaceholder`),disabled:f,onChange:e=>d(t=>({...t,alias:e.target.value.trim()?e.target.value:null,model:zl(t.id,e.target.value)}))}),(0,J.jsx)(`p`,{className:`muted`,style:{fontSize:12,margin:`8px 0 0`},children:c(`cws.field.aliasHint`)}),(0,J.jsx)(`p`,{className:`muted`,style:{fontSize:12,margin:`8px 0 0`},children:c(`cws.field.idHint`,{model:u.id.trim()?zl(u.id,u.alias):`…`})})]}),(0,J.jsxs)(`div`,{className:`cwi-field`,children:[(0,J.jsx)(`span`,{className:`field-label`,children:c(`cws.strategy`)}),(0,J.jsx)(mu,{value:u.strategy,disabled:f,onChange:e=>d(t=>({...t,strategy:e}))}),(0,J.jsx)(`p`,{className:`muted`,style:{fontSize:12,margin:`8px 0 0`},children:c(El[u.strategy])})]}),(0,J.jsxs)(`div`,{className:`cwi-field`,children:[(0,J.jsx)(`label`,{htmlFor:`cwi-new-effort`,children:c(`cws.field.defaultEffort`)}),(0,J.jsx)(hu,{id:`cwi-new-effort`,value:u.defaultEffort,disabled:f,allowedEfforts:v,onChange:e=>d(t=>({...t,defaultEffort:e}))}),(0,J.jsx)(`p`,{className:`muted`,style:{fontSize:12,margin:`8px 0 0`},children:c(`cws.field.defaultEffortHint`)})]}),u.strategy===`round-robin`&&(0,J.jsxs)(`div`,{className:`cwi-field`,children:[(0,J.jsx)(`label`,{htmlFor:`cwi-new-sticky`,children:c(`cws.field.stickyLimit`)}),(0,J.jsx)(`input`,{id:`cwi-new-sticky`,className:`input mono`,type:`number`,min:1,max:100,value:u.stickyLimit,disabled:f,onChange:e=>{let t=du(e.target.value,1,100);t!==void 0&&d(e=>({...e,stickyLimit:t}))}}),(0,J.jsx)(`p`,{className:`muted`,style:{fontSize:12,margin:`8px 0 0`},children:c(`cws.field.stickyLimitHint`)})]}),(0,J.jsxs)(`div`,{className:`cwi-field`,children:[(0,J.jsx)(`span`,{className:`field-label`,children:c(`cws.targets`)}),(0,J.jsx)(_u,{targets:u.targets,strategy:u.strategy,providers:i,models:a,providerQuotaStates:r,onChange:e=>d(t=>({...t,targets:e}))}),(0,J.jsx)(`p`,{className:`muted`,style:{fontSize:12,margin:`8px 0 0`},children:c(Dl[u.strategy])})]}),(0,J.jsx)(gu,{targets:u.targets,models:a,imageInput:u.imageInput??`auto`,reasoningEffortMode:u.reasoningEffortMode??`strict`,disabled:f,onChange:e=>d(t=>({...t,...e}))})]}),(0,J.jsxs)(`div`,{className:`cwi-modal-actions`,children:[(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost`,onClick:b,disabled:f,children:c(`common.cancel`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-primary`,onClick:()=>{S()},disabled:f||y,children:c(f?`common.saving`:`cws.create`)})]})]})]})}var yu=[`config`,`about`],bu=e=>`cws-detail-tab-${e}`,xu=e=>`cws-detail-panel-${e}`;function Su({baseline:e,isCreate:t=!1,otherIds:n,otherAliases:r,providerMap:i,providerQuotaStates:a,providers:o,models:s,onBack:c,onSaved:l,onRequestRemove:u,onSave:d,onDirtyChange:f}){let p=Q(),[m,h]=(0,_.useState)(`config`),g=(0,_.useCallback)((e,t)=>{let n;if(e.key===`ArrowRight`)n=(t+1)%yu.length;else if(e.key===`ArrowLeft`)n=(t-1+yu.length)%yu.length;else if(e.key===`Home`)n=0;else if(e.key===`End`)n=yu.length-1;else return;e.preventDefault(),h(yu[n]),e.currentTarget.parentElement?.querySelectorAll(`[role="tab"]`)[n]?.focus()},[]),[v,y]=(0,_.useState)(e),[b,x]=(0,_.useState)(!1),[S,C]=(0,_.useState)(null),[w,T]=(0,_.useState)(!1),E=!au(v,e),D=ru(v.targets,a,i)===`exhausted`,O=`${e.id}:${e.alias??``}:${e.nativeAlias}:${e.displayName??``}:${e.strategy}:${e.stickyLimit}:${e.defaultEffort}:${e.imageInput??`auto`}:${e.reasoningEffortMode??`strict`}:${e.targets.map(e=>`${e.provider}/${e.model}:${e.weight??1}`).join(`,`)}`,k=(0,_.useMemo)(()=>{let e=new Map;for(let t of s)e.set(`${t.provider}/${t.id}`,t.reasoningEfforts);return e},[s]),A=(0,_.useMemo)(()=>kl(v.targets,k,v.reasoningEffortMode??`strict`),[v.targets,k,v.reasoningEffortMode]),j=(0,_.useCallback)(t=>{let n=t(v);y(n),f(!au(n,e))},[v,e,f]);(0,_.useEffect)(()=>{let t=window.setTimeout(()=>{y(e),C(null),h(`config`),f(!1)},0);return()=>window.clearTimeout(t)},[O]);let M=async()=>{try{await navigator.clipboard.writeText(e.model),T(!0),window.setTimeout(()=>T(!1),1200)}catch{}},N=async()=>{let a=su(v,{existingIds:n,existingAliases:r,isCreate:t,providers:i});if(a){C({ok:!1,text:p(`cws.err.${a}`)});return}x(!0);let o=v.id.trim(),s=v.alias?.trim()||null,c=v.displayName?.trim()||null,u={...v,id:o,alias:s,displayName:c,model:zl(o,s)},f=!t&&o!==e.id?e.id:void 0;try{let e=await d(u,t,f);if(!e.ok){C({ok:!1,text:e.error||p(`cws.saveFailed`)});return}C({ok:!0,text:t?p(`cws.created`,{model:u.model}):p(`cws.saved`)}),l(u)}finally{x(!1)}},P=t?v.id.trim()?zl(v.id,v.alias):p(`cws.addTitle`):e.model;return(0,J.jsxs)(`div`,{className:`combos-workspace-detail`,children:[(0,J.jsxs)(`div`,{className:`combos-workspace-detail-head`,children:[c&&(0,J.jsxs)(`button`,{type:`button`,className:`btn btn-ghost btn-sm pwi-back-overview`,onClick:c,"aria-label":p(`cws.backToAll`),children:[(0,J.jsx)(Se,{style:{width:14,height:14,transform:`rotate(180deg)`},"aria-hidden":`true`}),p(`cws.allCombos`)]}),(0,J.jsx)(`h2`,{className:`combos-workspace-detail-title`,children:P}),!t&&(0,J.jsx)(`button`,{type:`button`,className:`chip cwi-copy-chip`,onClick:()=>{M()},title:p(`cws.copyModel`),children:p(w?`cws.copied`:`cws.copyModel`)}),(0,J.jsxs)(`div`,{className:`combos-workspace-detail-actions`,children:[!t&&u&&(0,J.jsxs)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:u,children:[(0,J.jsx)(he,{width:14,height:14}),` `,p(`common.remove`)]}),(0,J.jsx)(`button`,{id:t?`cwi-edit-create`:`cwi-edit-save`,type:`button`,className:`btn btn-primary btn-sm`,disabled:!t&&!E||b||D,onClick:()=>{N()},children:p(b?`common.saving`:t?`cws.create`:`common.save`)})]})]}),S&&(0,J.jsx)($,{tone:S.ok?`ok`:`err`,children:S.text}),D&&(0,J.jsx)(`div`,{className:`cwi-quota-banner`,role:`status`,"aria-live":`polite`,children:p(`cws.quota.allExhausted`)}),(0,J.jsx)(`div`,{className:`segmented combos-workspace-segmented`,role:`tablist`,"aria-label":p(`cws.tabsLabel`),children:yu.map((e,t)=>(0,J.jsx)(`button`,{type:`button`,role:`tab`,id:bu(e),"aria-selected":m===e,"aria-controls":xu(e),tabIndex:m===e?0:-1,className:`btn btn-sm ${m===e?`btn-primary`:`btn-ghost`}`,onClick:()=>h(e),onKeyDown:e=>g(e,t),children:p(e===`config`?`cws.tab.config`:`cws.tab.about`)},e))}),(0,J.jsx)(`div`,{className:`combos-workspace-tab-content`,role:`tabpanel`,id:xu(`config`),"aria-labelledby":bu(`config`),hidden:m!==`config`,children:(0,J.jsxs)(`div`,{className:`cwi-form-grid`,children:[(0,J.jsxs)(`div`,{className:`cwi-field`,children:[(0,J.jsx)(`label`,{htmlFor:`cwi-edit-id`,children:p(`cws.field.id`)}),(0,J.jsx)(`input`,{id:`cwi-edit-id`,className:`input mono`,value:v.id,disabled:b,onChange:e=>j(t=>({...t,id:e.target.value,model:zl(e.target.value,t.alias)}))}),(0,J.jsx)(`p`,{className:`muted`,style:{fontSize:12,margin:`8px 0 0`},children:t?p(`cws.field.idInternalHint`):p(`cws.field.idHintEdit`,{model:zl(v.id,v.alias)})})]}),(0,J.jsxs)(`div`,{className:`cwi-field`,children:[(0,J.jsx)(`label`,{htmlFor:`cwi-edit-alias`,children:p(`cws.field.alias`)}),(0,J.jsx)(`input`,{id:`cwi-edit-alias`,className:`input mono`,value:v.alias??``,placeholder:Rl(v.id.trim()||`…`),disabled:b,onChange:e=>j(t=>Bl(t,e.target.value))}),(0,J.jsx)(`p`,{className:`muted`,style:{fontSize:12,margin:`8px 0 0`},children:p(`cws.field.aliasHint`)})]}),(0,J.jsxs)(`div`,{className:`cwi-field`,children:[(0,J.jsxs)(`label`,{htmlFor:`cwi-edit-native-alias`,children:[(0,J.jsx)(`input`,{id:`cwi-edit-native-alias`,type:`checkbox`,checked:v.nativeAlias,disabled:b,onChange:e=>j(t=>({...t,nativeAlias:e.target.checked}))}),` `,p(`cws.field.nativeAlias`)]}),(0,J.jsx)(`p`,{className:`muted`,style:{fontSize:12,margin:`8px 0 0`},children:p(`cws.field.nativeAliasHint`)})]}),(0,J.jsxs)(`div`,{className:`cwi-field`,children:[(0,J.jsx)(`label`,{htmlFor:`cwi-edit-display-name`,children:p(`cws.field.displayName`)}),(0,J.jsx)(`input`,{id:`cwi-edit-display-name`,className:`input`,value:v.displayName??``,maxLength:128,disabled:b,onChange:e=>j(t=>({...t,displayName:e.target.value||null}))}),(0,J.jsx)(`p`,{className:`muted`,style:{fontSize:12,margin:`8px 0 0`},children:p(`cws.field.displayNameHint`)})]}),(0,J.jsxs)(`div`,{className:`cwi-field`,children:[(0,J.jsx)(`span`,{className:`field-label`,children:p(`cws.strategy`)}),(0,J.jsx)(mu,{value:v.strategy,disabled:b,onChange:e=>j(t=>({...t,strategy:e}))}),(0,J.jsx)(`p`,{className:`muted`,style:{fontSize:12,margin:`8px 0 0`},children:p(El[v.strategy])})]}),(0,J.jsxs)(`div`,{className:`cwi-field`,children:[(0,J.jsx)(`label`,{htmlFor:`cwi-effort`,children:p(`cws.field.defaultEffort`)}),(0,J.jsx)(hu,{id:`cwi-effort`,value:v.defaultEffort,disabled:b,allowedEfforts:A,onChange:e=>j(t=>({...t,defaultEffort:e}))}),(0,J.jsx)(`p`,{className:`muted`,style:{fontSize:12,margin:`8px 0 0`},children:p(`cws.field.defaultEffortHint`)})]}),v.strategy===`round-robin`&&(0,J.jsxs)(`div`,{className:`cwi-field`,children:[(0,J.jsx)(`label`,{htmlFor:`cwi-sticky`,children:p(`cws.field.stickyLimit`)}),(0,J.jsx)(`input`,{id:`cwi-sticky`,className:`input mono`,type:`number`,min:1,max:100,value:v.stickyLimit,disabled:b,onChange:e=>{let t=du(e.target.value,1,100);t!==void 0&&j(e=>({...e,stickyLimit:t}))}})]}),(0,J.jsxs)(`div`,{className:`cwi-field`,children:[(0,J.jsx)(`span`,{className:`field-label`,children:p(`cws.targets`)}),(0,J.jsx)(_u,{targets:v.targets,strategy:v.strategy,providers:o,models:s,providerQuotaStates:a,onChange:e=>j(t=>({...t,targets:e}))}),(0,J.jsx)(`p`,{className:`muted`,style:{fontSize:12,margin:`8px 0 0`},children:p(Dl[v.strategy])})]}),(0,J.jsx)(gu,{targets:v.targets,models:s,imageInput:v.imageInput??`auto`,reasoningEffortMode:v.reasoningEffortMode??`strict`,disabled:b,onChange:e=>j(t=>({...t,...e}))})]})}),(0,J.jsx)(`div`,{className:`combos-workspace-tab-content`,role:`tabpanel`,id:xu(`about`),"aria-labelledby":bu(`about`),hidden:m!==`about`,tabIndex:0,children:(0,J.jsxs)(`section`,{className:`pwi-section`,children:[(0,J.jsx)(`h3`,{className:`pwi-section-title`,children:p(`cws.aboutTitle`)}),(0,J.jsx)(`p`,{className:`muted`,style:{margin:0,maxWidth:`70ch`,overflowWrap:`anywhere`},children:p(`cws.aboutBody`)})]})})]})}function Cu({model:e,onCancel:t,onConfirm:n}){let r=Q(),i=(0,_.useRef)(null);(0,_.useEffect)(()=>{let e=i.current;e&&!e.open&&e.showModal()},[]);let a=(0,_.useCallback)(e=>{e.preventDefault(),t()},[t]);return(0,J.jsxs)(`dialog`,{ref:i,className:`modal-overlay`,"aria-labelledby":`cwi-remove-title`,onCancel:a,children:[(0,J.jsx)(`button`,{type:`button`,className:`modal-backdrop-dismiss`,"aria-label":r(`common.close`),tabIndex:-1,onClick:t}),(0,J.jsxs)(`div`,{className:`modal-card pwi-remove-confirm-card`,onClick:e=>e.stopPropagation(),children:[(0,J.jsx)(`h3`,{id:`cwi-remove-title`,className:`pwi-remove-confirm-title`,children:r(`cws.removeConfirmTitle`,{model:e})}),(0,J.jsx)(`p`,{className:`muted pwi-remove-confirm-desc`,children:r(`cws.removeConfirmDesc`)}),(0,J.jsxs)(`div`,{className:`pwi-remove-confirm-actions`,children:[(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost`,onClick:t,children:r(`common.cancel`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn pwi-remove-confirm-danger`,onClick:n,children:r(`common.remove`)})]})]})]})}function wu({onKeep:e,onDiscard:t}){let n=Q(),r=(0,_.useRef)(null);(0,_.useEffect)(()=>{let e=r.current;e&&!e.open&&e.showModal()},[]);let i=(0,_.useCallback)(t=>{t.preventDefault(),e()},[e]);return(0,J.jsxs)(`dialog`,{ref:r,className:`modal-overlay`,"aria-labelledby":`cwi-unsaved-title`,onCancel:i,children:[(0,J.jsx)(`button`,{type:`button`,className:`modal-backdrop-dismiss`,"aria-label":n(`common.close`),tabIndex:-1,onClick:e}),(0,J.jsxs)(`div`,{className:`modal-card pwi-json-unsaved-card`,onClick:e=>e.stopPropagation(),children:[(0,J.jsx)(`h3`,{id:`cwi-unsaved-title`,className:`pwi-json-unsaved-title`,children:n(`cws.unsavedTitle`)}),(0,J.jsx)(`p`,{className:`muted pwi-json-unsaved-desc`,children:n(`cws.unsavedDesc`)}),(0,J.jsxs)(`div`,{className:`pwi-json-unsaved-actions`,children:[(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost`,"data-testid":`cwi-unsaved-keep`,onClick:e,children:n(`cws.keepEditing`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-danger`,"data-testid":`cwi-unsaved-discard`,onClick:t,children:n(`common.discard`)})]})]})]})}function Tu(e,t){return t(e===`empty-targets`?`cws.attention.empty`:e===`catalog-omitted`?`cws.attention.catalogOmitted`:e===`all-targets-exhausted`?`cws.attention.allTargetsExhausted`:`cws.attention.few`)}function Eu({combos:e,cataloguedComboIds:t,providerMap:n,providerQuotaStates:r,onSelect:i,onAdd:a}){let o=Q(),s=ql(e),c=iu(e,{cataloguedComboIds:t,providers:n,providerQuotaStates:r});return(0,J.jsxs)(`div`,{className:`combos-workspace-overview`,children:[(0,J.jsxs)(`div`,{className:`combos-workspace-overview-head`,children:[(0,J.jsx)(`h2`,{className:`combos-workspace-overview-title`,children:o(`cws.overviewTitle`)}),(0,J.jsxs)(`button`,{type:`button`,className:`btn btn-primary btn-sm`,onClick:a,children:[(0,J.jsx)(fe,{width:14,height:14}),` `,o(`cws.add`)]})]}),(0,J.jsx)(`p`,{className:`muted`,style:{marginTop:0,maxWidth:`62ch`},children:o(`cws.overviewBlurb`)}),(0,J.jsxs)(`div`,{className:`cwi-count-strip`,children:[(0,J.jsxs)(`div`,{className:`cwi-count-pill`,children:[(0,J.jsx)(`strong`,{children:e.length}),(0,J.jsx)(`span`,{children:o(`cws.count.total`)})]}),(0,J.jsxs)(`div`,{className:`cwi-count-pill`,children:[(0,J.jsx)(`strong`,{children:s.failover.length}),(0,J.jsx)(`span`,{children:o(`cws.count.failover`)})]}),(0,J.jsxs)(`div`,{className:`cwi-count-pill`,children:[(0,J.jsx)(`strong`,{children:s.roundRobin.length}),(0,J.jsx)(`span`,{children:o(`cws.count.roundRobin`)})]}),(0,J.jsxs)(`div`,{className:`cwi-count-pill`,children:[(0,J.jsx)(`strong`,{children:s.other.length}),(0,J.jsx)(`span`,{children:o(`cws.count.other`)})]})]}),(0,J.jsxs)(`section`,{className:`pwi-section`,"aria-label":o(`cws.howTitle`),children:[(0,J.jsx)(`h3`,{className:`pwi-section-title`,children:o(`cws.howTitle`)}),(0,J.jsx)(`p`,{className:`muted`,style:{margin:0},children:o(`cws.howBody`)})]}),c.length>0&&(0,J.jsxs)(`section`,{className:`pwi-section`,"aria-label":o(`cws.attentionTitle`),children:[(0,J.jsx)(`h3`,{className:`pwi-section-title`,children:o(`cws.attentionTitle`)}),(0,J.jsx)(`div`,{className:`cwi-attention-list`,children:c.map(e=>(0,J.jsxs)(`button`,{type:`button`,className:`cwi-attention-row`,onClick:()=>i(e.id),children:[(0,J.jsx)(_e,{width:14,height:14,"aria-hidden":`true`}),(0,J.jsx)(`code`,{className:`chip`,children:e.model}),(0,J.jsx)(`span`,{className:`muted`,children:Tu(e.reason,o)}),(0,J.jsx)(Se,{width:14,height:14,style:{marginLeft:`auto`},"aria-hidden":`true`})]},`${e.id}:${e.reason}`))})]})]})}function Du({combos:e,providerQuotaStates:t,providers:n,models:r,cataloguedComboIds:i,loading:a,onRefresh:o,onSave:s,onRemove:c,onAdd:l,adding:u,onCloseAdd:d,onCreated:f}){let p=Q(),m=(0,_.useMemo)(()=>Object.fromEntries(n.map(e=>[e.name,{disabled:e.disabled}])),[n]),[h,g]=(0,_.useState)(``),[v,y]=(0,_.useState)(null),[b,x]=(0,_.useState)(void 0),[S,C]=(0,_.useState)(null),[w,T]=(0,_.useState)(null),E=(0,_.useMemo)(()=>cu(),[]),D=(0,_.useMemo)(()=>Jl(e,h),[e,h]),O=(0,_.useMemo)(()=>ql(D),[D]),k=(0,_.useMemo)(()=>e.flatMap(e=>e.alias?[e.alias]:[]),[e]),A=v&&e.some(e=>e.id===v)?v:null,j=e.find(e=>e.id===A)??null,M=j&&w?.id===j.id?w:j,[N,P]=(0,_.useState)(!1),F=[],I=[];if(M)for(let t of e)t.id!==M.id&&(F.push(t.id),t.alias&&I.push(t.alias));let L=(0,_.useCallback)(e=>{if(e!==A){if(!N){y(e),T(null);return}x(e)}},[A,N]),R=()=>{b!==void 0&&(y(b),T(null),P(!1),x(void 0))},z=()=>x(void 0),B=b!==void 0&&N,V=!a&&e.length===0;return(0,J.jsxs)(`div`,{className:`combos-workspace-root`,children:[(0,J.jsxs)(`aside`,{className:`combos-workspace-rail`,"aria-label":p(`cws.railAria`),children:[(0,J.jsxs)(`div`,{className:`combos-workspace-rail-header`,children:[(0,J.jsxs)(`div`,{children:[(0,J.jsx)(`div`,{className:`combos-workspace-rail-title`,children:p(`nav.combos`)}),(0,J.jsx)(`div`,{className:`combos-workspace-rail-count`,children:e.length})]}),(0,J.jsxs)(`button`,{type:`button`,className:`btn btn-primary btn-sm`,onClick:()=>{if(V){document.getElementById(`cwi-edit-id`)?.focus();return}l()},"aria-label":p(`cws.add`),children:[(0,J.jsx)(fe,{width:14,height:14}),` `,p(`cws.add`)]})]}),e.length>0&&(0,J.jsx)(`div`,{className:`cwi-search-row`,children:(0,J.jsxs)(`div`,{className:`cwi-search-wrap`,children:[(0,J.jsx)(ve,{className:`cwi-search-icon`,"aria-hidden":`true`}),(0,J.jsx)(`input`,{className:`input cwi-search-input`,value:h,onChange:e=>g(e.target.value),placeholder:p(`cws.searchPlaceholder`),"aria-label":p(`cws.searchPlaceholder`)})]})}),(0,J.jsx)(`div`,{className:`combos-workspace-rail-list`,children:D.length===0&&e.length>0?(0,J.jsx)(`p`,{className:`muted`,style:{padding:`16px`},children:p(`cws.noSearchResults`)}):(0,J.jsx)(J.Fragment,{children:[[`failover`,O.failover,`cws.group.failover`],[`round-robin`,O.roundRobin,`cws.group.roundRobin`],[`other`,O.other,`cws.group.other`]].map(([e,t,n])=>t.length>0?(0,J.jsxs)(`div`,{className:`combos-workspace-rail-group`,children:[(0,J.jsxs)(`div`,{className:`combos-workspace-rail-group-head`,children:[(0,J.jsx)(`span`,{className:`pwi-dot`,"aria-hidden":`true`}),p(n),(0,J.jsx)(`span`,{className:`combos-workspace-rail-count`,children:t.length})]}),t.map(e=>(0,J.jsxs)(`button`,{type:`button`,className:`combos-workspace-rail-row${A===e.id?` combos-workspace-rail-row--selected`:``}`,onClick:()=>L(e.id),"aria-current":A===e.id?`true`:void 0,children:[(0,J.jsx)(`span`,{className:`combos-workspace-rail-icon`,"aria-hidden":`true`,children:(0,J.jsx)(Pe,{width:16,height:16})}),(0,J.jsx)(`span`,{className:`combos-workspace-rail-name`,children:e.model}),(0,J.jsx)(`span`,{className:`combos-workspace-rail-meta`,children:e.targets.length===1?p(`cws.targetCountOne`):p(`cws.targetCount`,{count:e.targets.length})}),(0,J.jsx)(Se,{className:`combos-workspace-rail-chevron`,"aria-hidden":`true`})]},e.id))]},e):null)})})]}),(0,J.jsx)(`div`,{className:`combos-workspace-main`,children:M?(0,J.jsx)(Su,{baseline:M,otherIds:F,otherAliases:I,providerMap:m,providerQuotaStates:t,providers:n,models:r,onBack:()=>L(null),onSaved:e=>{P(!1),e.id===M.id?T(e):(y(e.id),T(null)),o()},onRequestRemove:()=>C(M.id),onSave:s,onDirtyChange:P},M.id):V?(0,J.jsx)(Su,{baseline:E,isCreate:!0,otherIds:[],otherAliases:[],providerMap:m,providerQuotaStates:t,providers:n,models:r,onSaved:e=>{P(!1),y(e.id),T(e),f(e.id)},onSave:s,onDirtyChange:P},`first-combo`):(0,J.jsx)(Eu,{combos:e,cataloguedComboIds:i,providerMap:m,providerQuotaStates:t,onSelect:e=>L(e),onAdd:l})}),u&&!V&&(0,J.jsx)(vu,{existingIds:e.map(e=>e.id),existingAliases:k,providerMap:m,providerQuotaStates:t,providers:n,models:r,onClose:d,onSubmit:async e=>{let t=await s(e,!0);return t.ok&&(d(),f(e.id),y(e.id),T(null)),t}}),S&&(0,J.jsx)(Cu,{model:e.find(e=>e.id===S)?.model??Rl(S),onCancel:()=>C(null),onConfirm:()=>{(async()=>{let e=await c(S);C(null),e.ok&&(A===S&&(y(null),T(null)),o())})()}}),B&&(0,J.jsx)(wu,{onKeep:z,onDiscard:R})]})}function Ou(e){if(!e||typeof e!=`object`||Array.isArray(e))return;let t=e.error;return typeof t==`string`&&t.trim()?t:void 0}function ku(e){return!!e&&typeof e==`object`&&!Array.isArray(e)&&e.success===!0}function Au(e){return vr(e)?.data??null}function ju(e){return vr(e)?.cachedAt??null}function Mu({apiBase:e,active:t=!0,onCountChange:n}){let r=Q(),i=`ocx.combos.workspace.v1:${e}`,a=(0,_.useMemo)(()=>Au(i),[i]),[o,s]=(0,_.useState)(a??null),[c,l]=(0,_.useState)(``),[u,d]=(0,_.useState)(!1),[f,p]=(0,_.useState)(!1),m=(e,t)=>{l(e),d(t)};(0,_.useEffect)(()=>{if(!c||!u)return;let e=window.setTimeout(()=>{l(``),d(!1)},5e3);return()=>window.clearTimeout(e)},[c,u]);let h=(0,_.useCallback)(async t=>{let[n,r,a]=await Promise.all([fetch(`${e}/api/combos`,{signal:t}),fetch(`${e}/api/config`,{signal:t}),fetch(`${e}/api/models`,{signal:t})]);if(!n.ok||!r.ok||!a.ok)throw Error(`combo workspace load failed`);let o=await n.json(),c=await r.json(),l=await a.json(),u=Array.isArray(l)?l:Array.isArray(l?.models)?l.models:[],d=Kl(o),f=c.providers??{},p=mi(f),m=Object.entries(f).map(([e,t])=>({name:e,disabled:!!t.disabled,hiddenFromPicker:!Object.hasOwn(p,e),authMode:t.authMode,adapter:t.adapter,baseUrl:t.baseUrl})),h=[],g=new Set;for(let e of u){if(!e||typeof e!=`object`)continue;let t=e;if(typeof t.provider!=`string`||typeof t.id!=`string`)continue;let n=t.provider.trim(),r=t.id.trim();if(!n||!r)continue;if(n===`combo`){g.add(r);continue}if(t.disabled===!0)continue;let i=Array.isArray(t.reasoningEfforts)?t.reasoningEfforts.filter(e=>typeof e==`string`):void 0,a=Array.isArray(t.inputModalities)?t.inputModalities.filter(e=>typeof e==`string`).map(e=>e.trim()).filter(Boolean):void 0;h.push({provider:n,id:r,namespaced:typeof t.namespaced==`string`?t.namespaced:void 0,...i?{reasoningEfforts:i}:{},...a&&a.length>0?{inputModalities:a}:{}})}for(let[e,t]of Object.entries(f)){let n=typeof t.defaultModel==`string`?t.defaultModel.trim():``;!n||t.disabled||h.some(t=>t.provider===e&&t.id===n)||h.push({provider:e,id:n,namespaced:`${e}/${n}`})}let _={combos:d,providers:m,models:h,cataloguedComboIds:[...g]};return yr(i,_),s(_),_},[e,i]),g=ml(i,[e],h,{isEmpty:()=>!1,initialData:a??void 0,initialDataCachedAt:ju(i),staleAfterMs:6e4,enabled:t}),{state:v}=g,y=(0,_.useCallback)(async t=>{let n=await fetch(`${e}/api/provider-quotas`,{signal:t});if(!n.ok)throw Error(`combo quota load failed`);let r=await n.json();return r&&typeof r==`object`&&!Array.isArray(r)?r:{}},[e]),b=ml(`ocx.combos.provider-quotas.v1:${e}`,[e],y,{isEmpty:()=>!1,pollMs:6e4,pauseWhenHidden:!0,enabled:t}),x=(0,_.useMemo)(()=>b.lastAttemptOk?nu(b.data?.reports):{},[b.data,b.lastAttemptOk]),S=v.data??o??void 0,C=S?.combos??[];(0,_.useEffect)(()=>{S&&n?.(C.length)},[C.length,S,n]);let w=S?.providers??[],T=S?.models??[],E=new Set(S?.cataloguedComboIds??[]),D=async(t,n,i)=>{try{let a=await fetch(`${e}/api/combos`,{method:`PUT`,headers:{"content-type":`application/json`},body:JSON.stringify(ou(t,i?{renameFrom:i}:{}))}),o=a.ok?await a.json():await a.json().catch(()=>null),s=Ou(o);if(!a.ok||s||!ku(o)){let e=s||r(`cws.saveFailed`);return m(e,!1),{ok:!1,error:e}}return g.refresh(),m(i?r(`cws.renamed`,{from:Rl(i),to:t.model}):n?r(`cws.created`,{model:t.model}):r(`cws.saved`),!0),{ok:!0}}catch{let e=r(`cws.saveFailed`);return m(e,!1),{ok:!1,error:e}}},O=async t=>{try{let n=await fetch(`${e}/api/combos?id=${encodeURIComponent(t)}`,{method:`DELETE`}),i=n.ok?await n.json():await n.json().catch(()=>null),a=Ou(i);if(!n.ok||a||!ku(i)){let e=a||r(`cws.removeFailed`);return m(e,!1),{ok:!1,error:e}}return g.refresh(),m(r(`cws.removed`,{id:t}),!0),{ok:!0}}catch{let e=r(`cws.removeFailed`);return m(e,!1),{ok:!1,error:e}}};if(v.showSkeleton&&!S)return(0,J.jsx)(gl,{label:r(`cws.loading`),rows:5});if(v.kind===`failed-cold`&&!S){let e=v.error instanceof Error?v.error.message:r(`cws.loadFailed`);return(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)($,{tone:`err`,children:e}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:()=>g.refresh(),children:r(`common.retry`)})]})}return(0,J.jsxs)(`div`,{className:`combos-workspace-shell`,children:[c&&(0,J.jsx)(`div`,{className:`combos-workspace-shell-banner`,children:(0,J.jsx)($,{tone:u?`ok`:`err`,children:c})}),v.showError&&(0,J.jsx)(`div`,{className:`combos-workspace-shell-banner`,children:(0,J.jsx)($,{tone:`err`,children:r(`cws.loadFailed`)})}),(0,J.jsxs)(`div`,{className:`combos-workspace-shell-body`,"aria-busy":v.refreshing,children:[(0,J.jsx)(`span`,{className:`sr-only`,role:`status`,"aria-live":`polite`,"aria-atomic":`true`,children:v.refreshing?r(`common.loading`):``}),(0,J.jsx)(Du,{combos:C,providerQuotaStates:x,providers:w,models:T,cataloguedComboIds:E,loading:!1,onRefresh:()=>g.refresh(),onSave:D,onRemove:O,onAdd:()=>p(!0),adding:f,onCloseAdd:()=>p(!1),onCreated:()=>g.refresh()})]})]})}var Nu=0;function Pu(){return Nu+=1,`candidate-${Nu}`}function Fu(e,t){return{provider:e,model:t,key:Pu()}}function Iu(e){if(!Array.isArray(e))return[];let t=[];for(let n of e){if(!n||typeof n!=`object`||Array.isArray(n))continue;let e=typeof n.suiteId==`string`?n.suiteId.trim():``,r=n.evidenceLayer;!e||r!==`protocol_conformance`&&r!==`live_route_compatibility`||t.push({suiteId:e,evidenceLayer:r})}return t}function Lu(e){if(typeof e!=`object`||!e||Array.isArray(e))return;let t=e,n={requiredSuites:Iu(t.requiredSuites)};return(t.minStatus===`PROBED`||t.minStatus===`VERIFIED`)&&(n.minStatus=t.minStatus),typeof t.maxEvidenceAgeMs==`number`&&Number.isFinite(t.maxEvidenceAgeMs)&&t.maxEvidenceAgeMs>=0&&(n.maxEvidenceAgeMs=t.maxEvidenceAgeMs),(t.unknownEvidence===`allow`||t.unknownEvidence===`penalize`||t.unknownEvidence===`exclude`)&&(n.unknownEvidence=t.unknownEvidence),(t.degradedEvidence===`allow`||t.degradedEvidence===`penalize`||t.degradedEvidence===`exclude`)&&(n.degradedEvidence=t.degradedEvidence),n}var Ru={latency:`0.55`,health:`0.25`,cost:`0.1`,quota:`0.1`},zu={capability:`exclude`,health:`penalize`,quota:`penalize`,cost:`penalize`};function Bu(e){return e===!0?`true`:e===!1?`false`:``}function Vu(e){return e===void 0?``:String(e)}function Hu(e=``,t=``){return{id:``,alias:``,candidates:[Fu(e,t)],require:{minContextWindow:``,minQuotaHeadroom:``,tools:``,imageInput:``,structuredOutput:``,reasoningEffort:``,serviceTier:``,localOnly:``,remoteAllowed:``,encryptedCodexTasks:``},optimize:{...Ru},limits:{maxEstimatedCostUsd:``,onUnknownCost:`allow`},unknownEvidence:{...zu},compatibility:{enabled:!1,requiredSuites:[],minStatus:``,maxEvidenceAgeMs:``,unknownEvidence:`exclude`,degradedEvidence:`penalize`}}}function Uu(e){let t=Lu(e.compatibility);return{id:e.id,alias:e.alias??``,candidates:e.candidates.map(e=>({...e,key:Pu()})),require:{minContextWindow:Vu(e.require.minContextWindow),minQuotaHeadroom:Vu(e.require.minQuotaHeadroom),tools:Bu(e.require.tools),imageInput:Bu(e.require.imageInput),structuredOutput:Bu(e.require.structuredOutput),reasoningEffort:e.require.reasoningEffort??``,serviceTier:e.require.serviceTier??``,localOnly:Bu(e.require.localOnly),remoteAllowed:Bu(e.require.remoteAllowed),encryptedCodexTasks:Bu(e.require.encryptedCodexTasks)},optimize:{latency:String(e.optimize.latency),health:String(e.optimize.health),cost:String(e.optimize.cost),quota:String(e.optimize.quota)},limits:{maxEstimatedCostUsd:Vu(e.limits.maxEstimatedCostUsd),onUnknownCost:e.limits.onUnknownCost===`exclude`?`exclude`:`allow`},unknownEvidence:{...e.unknownEvidence},compatibility:{enabled:!!t,requiredSuites:t?.requiredSuites??[],minStatus:t?.minStatus??``,maxEvidenceAgeMs:Vu(t?.maxEvidenceAgeMs),unknownEvidence:t?.unknownEvidence??`exclude`,degradedEvidence:t?.degradedEvidence??`penalize`}}}function Wu(e){let t=e.trim();return t?Number(t):void 0}function Gu(e){if(e===`true`)return!0;if(e===`false`)return!1}function Ku(e){return Object.fromEntries(Object.entries(e).filter(([,e])=>e!==void 0))}function qu(e,t,n){let r=Ku({minContextWindow:Wu(e.require.minContextWindow),minQuotaHeadroom:Wu(e.require.minQuotaHeadroom),tools:Gu(e.require.tools),imageInput:Gu(e.require.imageInput),structuredOutput:Gu(e.require.structuredOutput),reasoningEffort:e.require.reasoningEffort.trim()||void 0,serviceTier:e.require.serviceTier.trim()||void 0,localOnly:Gu(e.require.localOnly),remoteAllowed:Gu(e.require.remoteAllowed),encryptedCodexTasks:Gu(e.require.encryptedCodexTasks)}),i=Wu(e.limits.maxEstimatedCostUsd),a=e.compatibility.enabled?Ku({requiredSuites:e.compatibility.requiredSuites,minStatus:e.compatibility.minStatus||void 0,maxEvidenceAgeMs:Wu(e.compatibility.maxEvidenceAgeMs),unknownEvidence:e.compatibility.unknownEvidence,degradedEvidence:e.compatibility.degradedEvidence}):void 0,o=Ku({maxEstimatedCostUsd:i,onUnknownCost:e.limits.onUnknownCost===`exclude`?`exclude`:void 0});return{mode:t,id:e.id.trim(),...t===`update`&&n?{expectedRevision:n}:{},profile:{...e.alias.trim()?{alias:e.alias.trim()}:{},candidates:e.candidates.map(e=>({provider:e.provider.trim(),model:e.model.trim()})),...Object.keys(r).length>0?{require:r}:{},optimize:{latency:Number(e.optimize.latency),health:Number(e.optimize.health),cost:Number(e.optimize.cost),quota:Number(e.optimize.quota)},...Object.keys(o).length>0?{limits:o}:{},unknownEvidence:{...e.unknownEvidence},...a&&Object.keys(a).length>0?{compatibility:a}:{}}}}function Ju(e){if(!e||typeof e!=`object`||Array.isArray(e))return;let t=e.error;if(typeof t==`string`&&t.trim())return t;if(t&&typeof t==`object`&&!Array.isArray(t)){let e=t.message;if(typeof e==`string`&&e.trim())return e}}function Yu(e){return!!e&&typeof e==`object`&&!Array.isArray(e)&&e.success===!0}function Xu(e,t){return e.filter(e=>e.provider===t)}var Zu={en:{maxEvidenceAgeMs:`Maximum evidence age (ms)`,unknownEvidence:`Unknown evidence`,degradedEvidence:`Degraded evidence`},de:{maxEvidenceAgeMs:`Maximales Evidenzalter (ms)`,unknownEvidence:`Unbekannte Evidenz`,degradedEvidence:`Eingeschränkte Evidenz`},fr:{maxEvidenceAgeMs:`Âge maximal des preuves (ms)`,unknownEvidence:`Preuves inconnues`,degradedEvidence:`Preuves dégradées`},ko:{maxEvidenceAgeMs:`최대 증거 유효 기간 (ms)`,unknownEvidence:`알 수 없는 증거`,degradedEvidence:`저하된 증거`},zh:{maxEvidenceAgeMs:`证据最大有效期(毫秒)`,unknownEvidence:`未知证据`,degradedEvidence:`降级证据`},"zh-TW":{maxEvidenceAgeMs:`證據最大有效期限(毫秒)`,unknownEvidence:`未知證據`,degradedEvidence:`降級證據`},ru:{maxEvidenceAgeMs:`Максимальный возраст доказательств (мс)`,unknownEvidence:`Неизвестные доказательства`,degradedEvidence:`Ухудшенные доказательства`},ja:{maxEvidenceAgeMs:`エビデンスの最大有効期間 (ms)`,unknownEvidence:`不明なエビデンス`,degradedEvidence:`低下したエビデンス`},tr:{maxEvidenceAgeMs:`Maksimum kanıt yaşı (ms)`,unknownEvidence:`Bilinmeyen kanıt`,degradedEvidence:`Bozulmuş kanıt`}},Qu=[`tools`,`imageInput`,`structuredOutput`,`localOnly`,`remoteAllowed`,`encryptedCodexTasks`],$u=[`reasoningEffort`,`serviceTier`],ed={minContextWindow:{min:1,max:void 0,step:1},minQuotaHeadroom:{min:0,max:1,step:`any`}},td=Object.keys(ed),nd=[`latency`,`health`,`cost`,`quota`],rd=[`capability`,`health`,`quota`,`cost`],id=[`allow`,`penalize`,`exclude`],ad=[`allow`,`exclude`];function od(e){return`${e.evidenceLayer}:${e.suiteId}`}function sd(e){return!!e&&typeof e==`object`&&!Array.isArray(e)}function cd(e){let t=new Set,n=[];for(let r of e){if(!sd(r))continue;let e=typeof r.suiteId==`string`?r.suiteId.trim():``,i=r.evidenceLayer;if(!e||i!==`protocol_conformance`&&i!==`live_route_compatibility`)continue;let a={suiteId:e,evidenceLayer:i},o=od(a);t.has(o)||(t.add(o),n.push({...a,key:o}))}return n.sort((e,t)=>{let n=e.evidenceLayer.localeCompare(t.evidenceLayer);return n===0?e.suiteId.localeCompare(t.suiteId):n})}function ld(e,t){return e.some(e=>e.suiteId===t.suiteId&&e.evidenceLayer===t.evidenceLayer)}function ud(e,t){return e===void 0?t:`${Math.round(e)}ms`}function dd(e,t){return e==null?t:`${Math.round(e*100)}%`}function fd(e,t,n){switch(e){case`satisfied`:return t(`routing.capOutcome.satisfied`);case`exceeded`:return t(`routing.capOutcome.exceeded`);case`unknown-allowed`:return t(`routing.capOutcome.unknown-allowed`);case`unknown-excluded`:return t(`routing.capOutcome.unknown-excluded`);default:return n}}function pd(e,t){switch(e){case`capability-unsatisfied`:return t(`routing.exclusion.capability-unsatisfied`);case`unknown-capability`:return t(`routing.exclusion.unknown-capability`);case`cost-limit`:return t(`routing.exclusion.cost-limit`);case`cost-limit-unknown`:return t(`routing.exclusion.cost-limit-unknown`);case`cooldown`:return t(`routing.exclusion.cooldown`);case`unknown-health`:return t(`routing.exclusion.unknown-health`);case`unknown-quota`:return t(`routing.exclusion.unknown-quota`);case`unknown-price`:return t(`routing.exclusion.unknown-price`);default:return t(`routing.exclusion.other`,{code:e})}}function md(e){if(!e||typeof e!=`object`||Array.isArray(e))return[];let t=e.profiles;return Array.isArray(t)?t.filter(e=>sd(e)?typeof e.id==`string`&&typeof e.model==`string`&&typeof e.revision==`string`&&Array.isArray(e.candidates)&&sd(e.require)&&sd(e.optimize)&&sd(e.limits)&&sd(e.unknownEvidence):!1).map(e=>{let t=Lu(`compatibility`in e?e.compatibility:void 0),n={...e};return delete n.compatibility,{...n,alias:e.alias??null,...t?{compatibility:t}:{}}}):[]}function hd(e){let t=Array.isArray(e)?e:e&&typeof e==`object`&&Array.isArray(e.models)?e.models:[],n=new Set,r=[];for(let e of t){if(!e||typeof e!=`object`||Array.isArray(e))continue;let t=typeof e.provider==`string`?e.provider.trim():``,i=typeof e.id==`string`?e.id.trim():``;if(!t||!i||t===`combo`||t===`policy`||e.disabled===!0)continue;let a=JSON.stringify([t,i]);n.has(a)||(n.add(a),r.push({provider:t,id:i}))}return r}function gd(e,t,n){let r=n??t;if(r){let t=e.find(e=>e.id===r);if(t)return t}return e[0]??null}function _d({apiBase:e,active:t=!0,onCountChange:n}){let{locale:r,t:i}=ct(),a=Zu[r],o=i(`routing.unavailable`),[s,c]=(0,_.useState)([]),[l,u]=(0,_.useState)(null),[d,f]=(0,_.useState)([]),[p,m]=(0,_.useState)({}),[h,g]=(0,_.useState)([]),[v,y]=(0,_.useState)(``),[b,x]=(0,_.useState)(null),[S,C]=(0,_.useState)(null),[w,T]=(0,_.useState)(null),[E,D]=(0,_.useState)(!1),[O,k]=(0,_.useState)(``),[A,j]=(0,_.useState)(!1),[M,N]=(0,_.useState)(!1),[P,F]=(0,_.useState)(!1),[I,L]=(0,_.useState)(null),[R,z]=(0,_.useState)(``),[B,V]=(0,_.useState)(!1),[H,U]=(0,_.useState)([]),[W,ee]=(0,_.useState)(!1),G=(0,_.useRef)(null),K=(0,_.useRef)(0),q=(0,_.useRef)(null),Y=(0,_.useRef)(!0),te=(0,_.useCallback)(()=>{Y.current=!1,q.current?.abort(),K.current++},[]),ne=(0,_.useRef)(0),re=(0,_.useCallback)((e,t)=>{T({message:e,ok:t})},[]);(0,_.useEffect)(()=>{if(!w?.ok)return;let e=window.setTimeout(()=>T(null),5e3);return()=>window.clearTimeout(e)},[w]);let ie=(0,_.useCallback)(()=>{ne.current+=1,L(null),z(``),V(!1)},[]),ae=(0,_.useCallback)(e=>{G.current=e,x(e),C(e?Uu(e):null),T(null),ie()},[ie]),oe=(0,_.useCallback)(async t=>{if(!Y.current)return;q.current?.abort();let n=new AbortController;q.current=n;let{signal:r}=n,i=++K.current;y(``);try{let[n,a,o,s,l]=await Promise.all([fetch(`${e}/api/routing-profiles`,{signal:r}),fetch(`${e}/api/routing-analytics`,{signal:r}),fetch(`${e}/api/config`,{signal:r}),fetch(`${e}/api/models`,{signal:r}),fetch(`${e}/api/lab/catalog`,{signal:r})]);if(!n.ok)throw Error(`load-${n.status}`);let[d,p,h,_,v]=await Promise.all([n.json(),a.ok?a.json():Promise.resolve(null),o.ok?o.json():Promise.resolve({}),s.ok?s.json():Promise.resolve([]),l.ok?l.json().catch(()=>null):Promise.resolve(null)]);if(i!==K.current)return;let y=md(d),b=G.current,S=gd(y,b?.id??null,t),w=h.providers??{},T=Object.entries(w).filter(([,e])=>e.disabled!==!0).map(([e])=>e).sort((e,t)=>et)),E=Object.fromEntries(Object.entries(w).filter(([,e])=>e.disabled!==!0&&typeof e.defaultModel==`string`).map(([e,t])=>[e,t.defaultModel.trim()]));G.current=S,c(y),x(S),C(S?Uu(S):null),u(p),f(T),m(E),g(hd(_)),v&&Array.isArray(v.scenarios)?(U(cd(v.scenarios)),ee(!1)):(U([]),ee(!0)),(!b||!S||b.id!==S.id||b.revision!==S.revision)&&ie()}catch(e){if(i!==K.current||r.aborted)return;y(e instanceof Error?e.message:String(e))}finally{q.current===n&&(q.current=null)}},[e,ie]);(0,_.useEffect)(()=>{if(!t){te();return}Y.current=!0;let e=window.setTimeout(()=>void oe(),0);return()=>{window.clearTimeout(e),te()}},[t,te,oe]),(0,_.useEffect)(()=>{n?.(s.length)},[n,s.length]);let se=d[0]??``,ce=p[se]??Xu(h,se)[0]?.id??``,le=()=>{G.current=null,x(null),C(Hu(se,ce)),T(null),ie()},ue=()=>{if(b){C(Uu(b)),T(null);return}ae(s[0]??null)},de=async()=>{if(!(!S||E)){D(!0),T(null);try{let t=qu(S,b?`update`:`create`,b?.revision),n=await fetch(`${e}/api/routing-profiles`,{method:`PUT`,headers:{"content-type":`application/json`},body:JSON.stringify(t)}),r=await Ft(n);if(!n.ok){let e=await n.json().catch(()=>null);re(Ju(e)??i(`routing.loadFailed`),!1);return}if(!Yu(r)){re(Ju(r)??i(`routing.loadFailed`),!1);return}await oe(t.id),re(i(`common.ok`),!0)}catch(e){re(e instanceof Error?e.message:i(`routing.loadFailed`),!1)}finally{D(!1)}}},fe=async()=>{if(!(!b||E)&&window.confirm(i(`routing.removeConfirm`,{id:b.id}))){D(!0),T(null);try{let t=await fetch(`${e}/api/routing-profiles?id=${encodeURIComponent(b.id)}`,{method:`DELETE`}),n=await Ft(t);if(!t.ok){let e=await t.json().catch(()=>null);re(Ju(e)??i(`routing.loadFailed`),!1);return}if(!Yu(n)){re(Ju(n)??i(`routing.loadFailed`),!1);return}G.current=null,await oe(),re(i(`common.ok`),!0)}catch(e){re(e instanceof Error?e.message:i(`routing.loadFailed`),!1)}finally{D(!1)}}},pe=(e,t,n)=>{C(r=>{if(!r)return r;let i=r.candidates.map((r,i)=>i===e?t===`provider`?{...r,provider:n,model:p[n]??Xu(h,n)[0]?.id??``}:{...r,model:n}:r);return{...r,candidates:i}})},X=()=>{C(e=>e&&{...e,candidates:[...e.candidates,Fu(se,ce)]})},me=e=>{C(t=>t&&{...t,candidates:t.candidates.filter((t,n)=>n!==e)})},he=async()=>{if(!b)return;let t=++ne.current;V(!0),L(null),z(``);try{let n={},r=O.trim()?Number(O.trim()):NaN;Number.isFinite(r)&&r>0&&(n.contextWindow=r),A&&(n.toolsRequired=!0),M&&(n.imageInputRequired=!0),P&&(n.structuredOutputRequired=!0);let a=await fetch(`${e}/api/routing-profiles/dry-run`,{method:`POST`,headers:{"content-type":`application/json`},body:JSON.stringify({profile:b.id,evidence:n})});if(t!==ne.current)return;if(!a.ok){let e=await a.json().catch(()=>null);if(t!==ne.current)return;z(Ju(e)??i(`routing.dryRunError`,{status:a.status}));return}let o=await a.json();if(t!==ne.current)return;L(o)}catch(e){if(t!==ne.current)return;z(e instanceof Error?e.message:String(e))}finally{t===ne.current&&V(!1)}},ge=S?.candidates.map(e=>Xu(h,e.provider))??[];return(0,J.jsxs)(`div`,{className:`page`,"data-page":`routing`,children:[(0,J.jsxs)(`div`,{className:`row`,style:{display:`flex`,gap:8,justifyContent:`flex-end`,marginBottom:12},children:[(0,J.jsxs)(`button`,{type:`button`,className:`btn btn-primary btn-sm`,onClick:le,children:[(0,J.jsx)(`span`,{"aria-hidden":`true`,children:`+`}),` `,i(`routing.createProfile`)]}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:()=>void oe(),children:i(`common.retry`)})]}),v?(0,J.jsxs)($,{tone:`err`,children:[i(`routing.loadFailed`),`: `,v]}):null,w?(0,J.jsx)($,{tone:w.ok?`ok`:`err`,children:w.message}):null,s.length>0?(0,J.jsx)(`div`,{className:`panel`,style:{display:`flex`,flexDirection:`column`,gap:12},children:s.map(e=>(0,J.jsx)(`button`,{type:`button`,className:`model-card`,style:{textAlign:`left`,cursor:`pointer`},onClick:()=>ae(e),"aria-pressed":b?.id===e.id,children:(0,J.jsxs)(`div`,{className:`card-badges`,children:[(0,J.jsx)(`strong`,{children:e.id}),(0,J.jsx)(`span`,{className:`badge badge-muted`,children:e.model}),(0,J.jsxs)(`span`,{className:`badge badge-muted`,children:[i(`routing.revision`),`: `,e.revision]})]})},e.id))}):null,S?(0,J.jsxs)(`form`,{className:`panel`,style:{marginTop:14,display:`flex`,flexDirection:`column`,gap:16},onSubmit:e=>{e.preventDefault(),de()},children:[(0,J.jsxs)(`div`,{className:`page-head`,children:[(0,J.jsxs)(`h3`,{children:[i(`routing.detail`),`: `,b?.model??(0,J.jsx)(`code`,{children:`policy/…`})]}),b?(0,J.jsxs)(`span`,{className:`badge badge-muted`,children:[i(`routing.revision`),`: `,b.revision]}):null]}),(0,J.jsxs)(`div`,{className:`model-grid`,children:[(0,J.jsxs)(`label`,{className:`field-label`,children:[(0,J.jsx)(`code`,{children:`id`}),(0,J.jsx)(`input`,{className:`input`,required:!0,disabled:b!==null,value:S.id,onChange:e=>C(t=>t&&{...t,id:e.target.value})})]}),(0,J.jsxs)(`label`,{className:`field-label`,children:[(0,J.jsx)(`code`,{children:`alias`}),(0,J.jsx)(`input`,{className:`input`,value:S.alias,onChange:e=>C(t=>t&&{...t,alias:e.target.value})})]})]}),(0,J.jsxs)(`fieldset`,{style:{border:0,padding:0,margin:0},children:[(0,J.jsx)(`legend`,{className:`field-label`,children:i(`routing.candidates`)}),(0,J.jsxs)(`div`,{style:{display:`flex`,flexDirection:`column`,gap:10},children:[S.candidates.map((e,t)=>{let n=[...new Set([e.provider,...d])].filter(Boolean),r=`routing-model-options-${t}`;return(0,J.jsxs)(`div`,{className:`model-card`,children:[(0,J.jsxs)(`div`,{className:`model-grid`,children:[(0,J.jsxs)(`label`,{className:`field-label`,children:[(0,J.jsx)(`code`,{children:`provider`}),(0,J.jsxs)(`select`,{className:`input`,required:!0,value:e.provider,onChange:e=>pe(t,`provider`,e.target.value),children:[(0,J.jsx)(`option`,{value:``,disabled:!0,children:i(`routing.none`)}),n.map(e=>(0,J.jsx)(`option`,{value:e,children:e},e))]})]}),(0,J.jsxs)(`label`,{className:`field-label`,children:[(0,J.jsx)(`code`,{children:`model`}),(0,J.jsx)(`input`,{className:`input`,required:!0,list:r,value:e.model,onChange:e=>pe(t,`model`,e.target.value)}),(0,J.jsx)(`datalist`,{id:r,children:ge[t]?.map(e=>(0,J.jsx)(`option`,{value:e.id},e.id))})]})]}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,disabled:S.candidates.length===1,onClick:()=>me(t),"aria-label":i(`routing.removeCandidate`,{provider:e.provider,model:e.model}),children:i(`common.remove`)})]},e.key)}),(0,J.jsxs)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:X,children:[(0,J.jsx)(`span`,{"aria-hidden":`true`,children:`+`}),` `,i(`routing.candidate`)]})]})]}),(0,J.jsxs)(`fieldset`,{style:{border:0,padding:0,margin:0},children:[(0,J.jsx)(`legend`,{className:`field-label`,children:i(`routing.require`)}),(0,J.jsxs)(`div`,{className:`model-grid`,children:[td.map(e=>(0,J.jsxs)(`label`,{className:`field-label`,children:[(0,J.jsx)(`code`,{children:e}),(0,J.jsx)(`input`,{className:`input`,type:`number`,min:ed[e].min,max:ed[e].max,step:ed[e].step,value:S.require[e],onChange:t=>C(n=>n&&{...n,require:{...n.require,[e]:t.target.value}})})]},e)),$u.map(e=>(0,J.jsxs)(`label`,{className:`field-label`,children:[(0,J.jsx)(`code`,{children:e}),(0,J.jsx)(`input`,{className:`input`,value:S.require[e],onChange:t=>C(n=>n&&{...n,require:{...n.require,[e]:t.target.value}})})]},e)),Qu.map(e=>(0,J.jsxs)(`label`,{className:`field-label`,children:[(0,J.jsx)(`code`,{children:e}),(0,J.jsxs)(`select`,{className:`input`,value:S.require[e],onChange:t=>C(n=>n&&{...n,require:{...n.require,[e]:t.target.value}}),children:[(0,J.jsx)(`option`,{value:``,children:i(`routing.none`)}),(0,J.jsx)(`option`,{value:`true`,children:i(`routing.yes`)}),(0,J.jsx)(`option`,{value:`false`,children:i(`routing.no`)})]})]},e))]})]}),(0,J.jsxs)(`fieldset`,{style:{border:0,padding:0,margin:0},children:[(0,J.jsx)(`legend`,{className:`field-label`,children:i(`routing.optimize`)}),(0,J.jsx)(`div`,{className:`model-grid`,children:nd.map(e=>(0,J.jsxs)(`label`,{className:`field-label`,children:[(0,J.jsx)(`code`,{children:e}),(0,J.jsx)(`input`,{className:`input`,type:`number`,min:0,step:`any`,required:!0,value:S.optimize[e],onChange:t=>C(n=>n&&{...n,optimize:{...n.optimize,[e]:t.target.value}})})]},e))})]}),(0,J.jsxs)(`fieldset`,{style:{border:0,padding:0,margin:0},children:[(0,J.jsx)(`legend`,{className:`field-label`,children:i(`routing.limits`)}),(0,J.jsxs)(`label`,{className:`field-label`,children:[(0,J.jsx)(`code`,{children:`maxEstimatedCostUsd`}),(0,J.jsx)(`input`,{className:`input`,type:`number`,min:0,step:`any`,value:S.limits.maxEstimatedCostUsd,onChange:e=>C(t=>t&&{...t,limits:{...t.limits,maxEstimatedCostUsd:e.target.value}})})]}),(0,J.jsxs)(`label`,{className:`field-label`,children:[(0,J.jsx)(`code`,{children:`onUnknownCost`}),(0,J.jsx)(`select`,{className:`input`,value:S.limits.onUnknownCost,onChange:e=>C(t=>t&&{...t,limits:{...t.limits,onUnknownCost:e.target.value}}),children:ad.map(e=>(0,J.jsx)(`option`,{value:e,children:i(`routing.unknownEvidence.${e}`)},e))})]})]}),(0,J.jsxs)(`fieldset`,{style:{border:0,padding:0,margin:0},children:[(0,J.jsx)(`legend`,{className:`field-label`,children:i(`routing.unknownEvidence`)}),(0,J.jsx)(`div`,{className:`model-grid`,children:rd.map(e=>(0,J.jsxs)(`label`,{className:`field-label`,children:[(0,J.jsx)(`code`,{children:e}),(0,J.jsx)(`select`,{className:`input`,value:S.unknownEvidence[e],onChange:t=>C(n=>n&&{...n,unknownEvidence:{...n.unknownEvidence,[e]:t.target.value}}),children:id.map(e=>(0,J.jsx)(`option`,{value:e,children:i(`routing.unknownEvidence.${e}`)},e))})]},e))})]}),(0,J.jsxs)(`fieldset`,{style:{border:0,padding:0,margin:0},children:[(0,J.jsx)(`legend`,{className:`field-label`,children:i(`routing.compatibility.title`)}),(0,J.jsxs)(`label`,{className:`checkbox`,children:[(0,J.jsx)(`input`,{type:`checkbox`,checked:S.compatibility.enabled,onChange:e=>C(t=>t&&{...t,compatibility:{...t.compatibility,enabled:e.target.checked}})}),i(`routing.compatibility.enabled`)]}),S.compatibility.enabled?(0,J.jsxs)(`div`,{className:`model-grid`,style:{marginTop:10},children:[(0,J.jsxs)(`div`,{className:`field-label`,style:{gridColumn:`1 / -1`},children:[i(`routing.compatibility.requiredSuites`),W?(0,J.jsx)(`div`,{style:{marginTop:6},children:(0,J.jsx)($,{tone:`warn`,children:i(`routing.compatibility.catalogUnavailable`)})}):null,H.length>0?(0,J.jsx)(`div`,{style:{display:`flex`,flexDirection:`column`,gap:4,marginTop:6},children:H.map(e=>(0,J.jsxs)(`label`,{className:`checkbox`,children:[(0,J.jsx)(`input`,{type:`checkbox`,checked:ld(S.compatibility.requiredSuites,e),onChange:t=>C(n=>{if(!n)return n;let r=t.target.checked?[...n.compatibility.requiredSuites,{suiteId:e.suiteId,evidenceLayer:e.evidenceLayer}]:n.compatibility.requiredSuites.filter(t=>t.suiteId!==e.suiteId||t.evidenceLayer!==e.evidenceLayer);return{...n,compatibility:{...n.compatibility,requiredSuites:r}}})}),(0,J.jsxs)(`span`,{children:[e.suiteId,` `,(0,J.jsxs)(`span`,{className:`muted`,children:[`(`,i(`routing.compatibility.layer.${e.evidenceLayer}`),`)`]})]})]},e.key))}):null]}),(0,J.jsxs)(`label`,{className:`field-label`,children:[i(`routing.compatibility.minStatus`),(0,J.jsxs)(`select`,{className:`input`,value:S.compatibility.minStatus,onChange:e=>C(t=>t&&{...t,compatibility:{...t.compatibility,minStatus:e.target.value}}),children:[(0,J.jsx)(`option`,{value:``,children:i(`routing.none`)}),(0,J.jsx)(`option`,{value:`PROBED`,children:i(`lab.verdict.PROBED`)}),(0,J.jsx)(`option`,{value:`VERIFIED`,children:i(`lab.verdict.VERIFIED`)})]})]}),(0,J.jsxs)(`label`,{className:`field-label`,children:[a.maxEvidenceAgeMs,(0,J.jsx)(`input`,{className:`input`,type:`number`,min:0,value:S.compatibility.maxEvidenceAgeMs,onChange:e=>C(t=>t&&{...t,compatibility:{...t.compatibility,maxEvidenceAgeMs:e.target.value}})})]}),(0,J.jsxs)(`label`,{className:`field-label`,children:[a.unknownEvidence,(0,J.jsx)(`select`,{className:`input`,value:S.compatibility.unknownEvidence,onChange:e=>C(t=>t&&{...t,compatibility:{...t.compatibility,unknownEvidence:e.target.value}}),children:id.map(e=>(0,J.jsx)(`option`,{value:e,children:i(`routing.unknownEvidence.${e}`)},e))})]}),(0,J.jsxs)(`label`,{className:`field-label`,children:[a.degradedEvidence,(0,J.jsx)(`select`,{className:`input`,value:S.compatibility.degradedEvidence,onChange:e=>C(t=>t&&{...t,compatibility:{...t.compatibility,degradedEvidence:e.target.value}}),children:id.map(e=>(0,J.jsx)(`option`,{value:e,children:i(`routing.unknownEvidence.${e}`)},e))})]})]}):null]}),(0,J.jsxs)(`div`,{style:{display:`flex`,gap:8,flexWrap:`wrap`},children:[(0,J.jsx)(`button`,{type:`submit`,className:`btn btn-primary`,disabled:E,children:i(E?`common.saving`:`common.save`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost`,disabled:E,onClick:ue,children:i(`common.cancel`)}),b?(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost`,disabled:E,onClick:()=>void fe(),children:i(`common.remove`)}):null]})]}):null,b&&(0,J.jsxs)(`div`,{className:`panel`,style:{marginTop:14,display:`flex`,flexDirection:`column`,gap:10},children:[(0,J.jsx)(`h3`,{children:i(`routing.dryRun`)}),(0,J.jsxs)(`label`,{className:`field-label`,htmlFor:`routing-context`,children:[i(`routing.dryRunContext`),(0,J.jsx)(`input`,{id:`routing-context`,className:`input`,type:`number`,min:1,value:O,onChange:e=>{k(e.target.value),ie()}})]}),(0,J.jsxs)(`label`,{className:`checkbox`,children:[(0,J.jsx)(`input`,{type:`checkbox`,checked:A,onChange:e=>{j(e.target.checked),ie()}}),i(`routing.dryRunTools`)]}),(0,J.jsxs)(`label`,{className:`checkbox`,children:[(0,J.jsx)(`input`,{type:`checkbox`,checked:M,onChange:e=>{N(e.target.checked),ie()}}),i(`routing.dryRunImage`)]}),(0,J.jsxs)(`label`,{className:`checkbox`,children:[(0,J.jsx)(`input`,{type:`checkbox`,checked:P,onChange:e=>{F(e.target.checked),ie()}}),i(`routing.dryRunStructured`)]}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-primary`,disabled:!b||B,onClick:()=>void he(),children:i(`routing.dryRunRun`)}),R?(0,J.jsx)($,{tone:`err`,children:R}):null,I?(0,J.jsxs)(`table`,{className:`tbl`,children:[(0,J.jsx)(`thead`,{children:(0,J.jsxs)(`tr`,{children:[(0,J.jsx)(`th`,{children:i(`routing.candidate`)}),(0,J.jsx)(`th`,{children:i(`routing.eligible`)}),(0,J.jsx)(`th`,{children:i(`routing.exclusions`)}),(0,J.jsx)(`th`,{children:i(`routing.costCap`)}),(0,J.jsx)(`th`,{children:i(`routing.score`)})]})}),(0,J.jsx)(`tbody`,{children:I.candidates.map((e,t)=>(0,J.jsxs)(`tr`,{children:[(0,J.jsxs)(`td`,{children:[e.provider,`/`,e.model,t===I.selectedIndex?` ✓ (${i(`routing.selected`)})`:``]}),(0,J.jsx)(`td`,{children:e.eligible?i(`routing.yes`):i(`routing.no`)}),(0,J.jsx)(`td`,{children:e.exclusions.map(e=>pd(e.code,i)).join(`, `)||i(`routing.none`)}),(0,J.jsx)(`td`,{children:fd(e.cost?.capOutcome,i,o)}),(0,J.jsx)(`td`,{children:e.score?e.score.total.toFixed(3):o})]},`${e.provider}/${e.model}`))})]}):null]}),s.length>0&&(0,J.jsxs)(`div`,{className:`panel`,style:{marginTop:14,display:`flex`,flexDirection:`column`,gap:10},children:[(0,J.jsx)(`h3`,{children:i(`routing.analytics`)}),l?(0,J.jsxs)(J.Fragment,{children:[(0,J.jsxs)(`div`,{className:`card-badges`,children:[(0,J.jsxs)(`span`,{className:`badge badge-muted`,children:[i(`routing.analyticsTotal`),`: `,l.totalRequests]}),(0,J.jsxs)(`span`,{className:`badge badge-muted`,children:[i(`routing.analyticsSuccessRate`),`: `,dd(l.successRate,o)]}),(0,J.jsxs)(`span`,{className:`badge badge-muted`,children:[i(`routing.analyticsFallbackRate`),`: `,dd(l.fallbackRate,o)]}),(0,J.jsxs)(`span`,{className:`badge badge-muted`,children:[i(`routing.analyticsP50`),`: `,ud(l.durationMs.p50,o)]}),(0,J.jsxs)(`span`,{className:`badge badge-muted`,children:[i(`routing.analyticsP95`),`: `,ud(l.durationMs.p95,o)]}),(0,J.jsxs)(`span`,{className:`badge badge-muted`,children:[i(`routing.analyticsP99`),`: `,ud(l.durationMs.p99,o)]}),(0,J.jsxs)(`span`,{className:`badge badge-muted`,children:[i(`routing.analyticsCooldown`),`: `,l.cooldownTriggeringFailures]}),(0,J.jsxs)(`span`,{className:`badge badge-muted`,children:[i(`routing.analyticsConfidence`),`: `,l.confidence??o]}),l.historyTruncated?(0,J.jsx)(`span`,{className:`badge badge-muted`,children:i(`routing.analyticsTruncated`)}):null]}),(0,J.jsxs)(`table`,{className:`tbl`,children:[(0,J.jsx)(`thead`,{children:(0,J.jsxs)(`tr`,{children:[(0,J.jsx)(`th`,{children:i(`routing.candidate`)}),(0,J.jsx)(`th`,{children:i(`routing.analyticsRequests`)}),(0,J.jsx)(`th`,{children:i(`routing.analyticsSuccessRate`)}),(0,J.jsx)(`th`,{children:i(`routing.analyticsP50`)})]})}),(0,J.jsx)(`tbody`,{children:l.breakdown.map(e=>(0,J.jsxs)(`tr`,{children:[(0,J.jsxs)(`td`,{children:[e.provider,`/`,e.model]}),(0,J.jsx)(`td`,{children:e.requests}),(0,J.jsx)(`td`,{children:dd(e.successRate,o)}),(0,J.jsx)(`td`,{children:ud(e.p50DurationMs,o)})]},`${e.provider}/${e.model}`))})]})]}):(0,J.jsx)(`p`,{className:`muted`,children:i(`routing.analyticsEmpty`)})]})]})}var vd=[`protocol_conformance`,`live_route_compatibility`,`task_effectiveness`],yd=[`UNKNOWN`,`CLAIMED`,`PROBED`,`VERIFIED`,`DEGRADED`,`BLOCKED`,`UNSUPPORTED`],bd=[`present`,`corrupt`,`purged_unavailable`];function xd(e){return!!e&&typeof e==`object`&&!Array.isArray(e)}function Sd(e){return Array.isArray(e)&&e.every(e=>typeof e==`string`)}function Cd(e){return e===null||typeof e==`string`}function wd(e){return e===void 0||typeof e==`number`&&Number.isFinite(e)}function Td(e){if(!xd(e)||typeof e.projectionAvailable!=`boolean`||e.projectionIncompatible!==void 0&&typeof e.projectionIncompatible!=`boolean`)return null;for(let t of[`sqliteSchemaVersion`,`builtAtMs`,`eventCount`,`subjectCount`,`observationCount`,`claimCount`,`verdictCount`,`artifactCount`,`corruptionCount`])if(!wd(e[t]))return null;return e.projectionSpecVersion!==void 0&&typeof e.projectionSpecVersion!=`string`?null:e}function Ed(e){return!xd(e)||typeof e.projectionKey!=`string`||e.projectionKey.length===0||typeof e.subjectId!=`string`||e.subjectId.length===0||typeof e.evidenceLayer!=`string`||!vd.includes(e.evidenceLayer)||typeof e.suiteId!=`string`||e.suiteId.length===0||typeof e.suiteVersion!=`string`||typeof e.suiteManifestDigest!=`string`||typeof e.projectionSpecVersion!=`string`||typeof e.verdict!=`string`||!yd.includes(e.verdict)||typeof e.asOf!=`number`||!Number.isFinite(e.asOf)||!Sd(e.scenarioManifestDigests)||!Cd(e.claimSourceDigest)||!Sd(e.contributingEventIds)||!Sd(e.contradictingEventIds)||!Sd(e.notes)?null:e}function Dd(e){return!xd(e)||!Array.isArray(e.verdicts)?{verdicts:[],hasMore:!1}:{verdicts:e.verdicts.map(Ed).filter(e=>e!==null),hasMore:e.hasMore===!0,nextCursor:typeof e.nextCursor==`string`?e.nextCursor:void 0}}function Od(e){return!xd(e)||!Array.isArray(e.subjects)?{subjects:[],hasMore:!1}:{subjects:e.subjects.filter(e=>xd(e)&&typeof e.subjectId==`string`&&typeof e.subjectKind==`string`),hasMore:e.hasMore===!0,nextCursor:typeof e.nextCursor==`string`?e.nextCursor:void 0}}function kd(e){return!xd(e)||!xd(e.subject)||typeof e.subject.subjectKind!=`string`?null:e.subject}function Ad(e){return!xd(e)||typeof e.eventId!=`string`||typeof e.subjectId!=`string`||typeof e.evidenceLayer!=`string`||!vd.includes(e.evidenceLayer)||typeof e.suiteId!=`string`||typeof e.suiteVersion!=`string`||typeof e.suiteManifestDigest!=`string`||typeof e.scenarioId!=`string`||typeof e.scenarioVersion!=`string`||typeof e.scenarioManifestDigest!=`string`||typeof e.outcome!=`string`||typeof e.completedAt!=`number`||!Number.isFinite(e.completedAt)||typeof e.executionMode!=`string`||typeof e.excluded!=`boolean`||!Cd(e.exclusionReason)?null:e}function jd(e){return!xd(e)||!Array.isArray(e.observations)?{observations:[],hasMore:!1}:{observations:e.observations.map(Ad).filter(e=>e!==null),hasMore:e.hasMore===!0,nextCursor:typeof e.nextCursor==`string`?e.nextCursor:void 0}}function Md(e){if(!xd(e)||!xd(e.event))return null;let t={...e.event};return delete t.payload_json,typeof t.eventKind!=`string`||typeof t.eventId!=`string`||typeof t.recordedAt!=`number`||!Number.isFinite(t.recordedAt)||typeof t.producer!=`string`||typeof t.producerVersion!=`string`||typeof t.excluded!=`boolean`||!Cd(t.exclusionReason)?null:t}function Nd(e){if(!xd(e)||!xd(e.artifact))return null;let t=e.artifact;return typeof t.digest!=`string`||typeof t.status!=`string`||!bd.includes(t.status)||!Cd(t.artifactClass)||!Cd(t.mediaType)||t.byteCount!==null&&(typeof t.byteCount!=`number`||!Number.isFinite(t.byteCount))||!Cd(t.lastError)?null:t}function Pd(e){let t={},n=e.subjectQuery.trim();return e.layer&&(t.layer=e.layer),e.verdict&&(t.verdict=e.verdict),n&&(t.subjectId=n),e.suiteId.trim()&&(t.suiteId=e.suiteId.trim()),t}function Fd(){return{protocol_conformance:[],live_route_compatibility:[],task_effectiveness:[]}}function Id(e,t){let n=new Map(t.map(e=>[e.subjectId,e.subjectKind])),r=new Map;for(let t of e){let e=r.get(t.subjectId);e||(e={subjectId:t.subjectId,subjectKind:n.get(t.subjectId)??``,byLayer:Fd()},r.set(t.subjectId,e)),e.byLayer[t.evidenceLayer].push(t)}return[...r.values()].sort((e,t)=>e.subjectId.localeCompare(t.subjectId))}function Ld(e){return e.length<=16?e:`${e.slice(0,8)}.${e.slice(-6)}`}function Rd(e,t){return!Number.isFinite(e)||e<=0?`-`:new Date(e).toLocaleString(t)}function zd(e){let t=new Set;e.suiteManifestDigest&&t.add(e.suiteManifestDigest);for(let n of e.scenarioManifestDigests)n&&t.add(n);return e.claimSourceDigest&&t.add(e.claimSourceDigest),[...t]}var Bd=50,Vd=200,Hd=6,Ud=200;async function Wd(e,t,n){return Pt(await fetch(`${e}${t}`,{signal:n}))}function Gd(e,t){let n=new URLSearchParams({limit:String(Bd)});for(let[t,r]of Object.entries(e))r&&n.set(t,r);return t&&n.set(`cursor`,t),n.toString()}var Kd=class extends Error{};function qd(){return new Kd}function Jd(e,t,n){if(e.hasMore!==void 0&&typeof e.hasMore!=`boolean`||e.nextCursor!==void 0&&typeof e.nextCursor!=`string`||t&&!n)throw qd()}function Yd(e){if(!xd(e)||!Array.isArray(e.verdicts))throw qd();let t=Dd(e);if(t.verdicts.length!==e.verdicts.length)throw qd();return Jd(e,t.hasMore,t.nextCursor),t}function Xd(e){if(!xd(e)||!Array.isArray(e.subjects))throw qd();let t=Od(e);if(t.subjects.length!==e.subjects.length)throw qd();return Jd(e,t.hasMore,t.nextCursor),t}function Zd(e){if(!xd(e)||!Array.isArray(e.observations))throw qd();let t=jd(e);if(t.observations.length!==e.observations.length)throw qd();return Jd(e,t.hasMore,t.nextCursor),t}async function Qd(e,t){let n=Td(await Wd(e,`/api/lab/status`,t));if(!n)throw qd();return n}async function $d(e,t,n,r){return Yd(await Wd(e,`/api/lab/verdicts?${Gd({layer:t.layer,verdict:t.verdict,subjectId:t.subjectId,suiteId:t.suiteId},n)}`,r))}async function ef(e,t,n){return Xd(await Wd(e,`/api/lab/subjects?${Gd({},t)}`,n))}async function tf(e){let t=[],n=new Set,r;for(let i=0;i{let r=await ef(e,n,t);return{rows:r.subjects,hasMore:r.hasMore,nextCursor:r.nextCursor}})}async function rf(e,t,n){let r=kd(await Wd(e,`/api/lab/subjects/${encodeURIComponent(t)}`,n));if(!r)throw qd();return r}async function af(e,t,n,r){return Zd(await Wd(e,`/api/lab/observations?${Gd({subjectId:t.subjectId,layer:t.layer,suiteId:t.suiteId},n)}`,r))}async function of(e,t,n){return tf(async r=>{let i=await af(e,t,r,n);return{rows:i.observations,hasMore:i.hasMore,nextCursor:i.nextCursor}})}async function sf(e,t,n){let r=Md(await Wd(e,`/api/lab/events/${encodeURIComponent(t)}`,n));if(!r)throw qd();return r}async function cf(e,t,n){let r=Nd(await Wd(e,`/api/lab/artifacts/${encodeURIComponent(t)}`,n));if(!r)throw qd();return r}function lf(e){if(!xd(e)||e.verificationStatus!==`not_verification`||!xd(e.summary))throw qd();let t=e.summary;if(t.verificationStatus!==`not_verification`||typeof t.subjectId!=`string`||typeof t.recentProductionAttempts!=`number`||typeof t.recentSuccessfulAttempts!=`number`||typeof t.recentRouteErrorSignals!=`number`||t.lastObservedProductionAttempt!==void 0&&typeof t.lastObservedProductionAttempt!=`number`)throw qd();return{verificationStatus:`not_verification`,summary:t}}async function uf(e,t,n){return lf(await Wd(e,`/api/lab/production-signals?${Gd({subjectId:t})}`,n))}function df(e,t){let n=new Set(t);return Object.keys(e).every(e=>n.has(e))}function ff(e){return typeof e==`string`&&/^[0-9a-f]{64}$/.test(e)}function pf(e){return typeof e==`number`&&Number.isSafeInteger(e)&&e>=0}function mf(e){if(!xd(e)||!df(e,[`evidence`,`trustClass`,`locallyVerified`])||e.trustClass!==`community_untrusted_v1`||e.locallyVerified!==!1||!Array.isArray(e.evidence)||e.evidence.length>4096)return null;let t=[];for(let n of e.evidence){if(!xd(n)||!df(n,[`trustClass`,`status`,`bundleId`,`publisherKeyId`,`activeRecordCount`,`revokedRecordCount`])||n.trustClass!==`community_untrusted_v1`||n.status!==`cryptographically_valid`||!ff(n.bundleId)||!ff(n.publisherKeyId)||!pf(n.activeRecordCount)||!pf(n.revokedRecordCount))return null;t.push({trustClass:`community_untrusted_v1`,status:`cryptographically_valid`,bundleId:n.bundleId,publisherKeyId:n.publisherKeyId,activeRecordCount:n.activeRecordCount,revokedRecordCount:n.revokedRecordCount})}return{evidence:t,trustClass:`community_untrusted_v1`,locallyVerified:!1}}async function hf(e,t){let n=mf(await Wd(e,`/api/lab/public/community`,t));if(!n)throw qd();return n}async function gf(e,t,n){let[r,i]=await Promise.all([Qd(e,n),hf(e,n).catch(e=>{if(n.aborted)throw e;return null})]);if(!r.projectionAvailable)return{status:r,verdicts:[],subjects:[],subjectsTruncated:!1,hasMore:!1,community:i};let[a,o]=await Promise.all([$d(e,t,void 0,n),nf(e,n)]);return{status:r,verdicts:a.verdicts,subjects:o.rows,subjectsTruncated:o.truncated,hasMore:a.hasMore,nextCursor:a.nextCursor,community:i}}async function _f(e,t,n,r){return $d(e,t,n,r)}async function vf(e,t,n,r){let i=e.slice(0,Ud),a=[],o=0,s=async()=>{for(;;){if(n.aborted)throw new DOMException(`Aborted`,`AbortError`);let e=o++;if(e>=i.length)return;try{a.push(await r(i[e]))}catch(e){if(n.aborted)throw e}}},c=Math.min(t,i.length);return await Promise.all(Array.from({length:c},()=>s())),a}async function yf(e,t,n){let r=[...new Set([...t.contributingEventIds,...t.contradictingEventIds])],i=zd(t),a={subjectId:t.subjectId,layer:t.evidenceLayer,suiteId:t.suiteId},[o,s,c,l,u]=await Promise.all([rf(e,t.subjectId,n),of(e,a,n),vf(r,Hd,n,t=>sf(e,t,n)),vf(i,Hd,n,t=>cf(e,t,n)),uf(e,t.subjectId,n).catch(e=>{if(n.aborted)throw e;return null})]);return{subject:o,observations:s.rows,observationsTruncated:s.truncated,events:c,artifacts:l,production:u}}var bf={protocol_conformance:`lab.layer.protocol_conformance`,live_route_compatibility:`lab.layer.live_route_compatibility`,task_effectiveness:`lab.layer.task_effectiveness`},xf={protocol_conformance:`lab.col.protocol`,live_route_compatibility:`lab.col.live`,task_effectiveness:`lab.col.task`},Sf={UNKNOWN:`lab.verdict.UNKNOWN`,CLAIMED:`lab.verdict.CLAIMED`,PROBED:`lab.verdict.PROBED`,VERIFIED:`lab.verdict.VERIFIED`,DEGRADED:`lab.verdict.DEGRADED`,BLOCKED:`lab.verdict.BLOCKED`,UNSUPPORTED:`lab.verdict.UNSUPPORTED`},Cf={present:`artifact.present`,corrupt:`artifact.corrupt`,purged_unavailable:`artifact.purged_unavailable`};function wf(e,t){if(!(e instanceof Error))return t;let n=e.message;return n===`Failed to fetch`||n.includes(`NetworkError`)||n.includes(`network error`)?t:n||t}function Tf({verdict:e,caption:t,label:n,selected:r,onSelect:i}){let a=`lab-verdict-badge${r?` lab-verdict-badge--selected`:``}`;return i?(0,J.jsxs)(`button`,{type:`button`,className:a,"data-verdict":e,title:t,onClick:i,children:[(0,J.jsx)(`span`,{children:n}),(0,J.jsx)(`span`,{className:`suite`,children:t})]}):(0,J.jsxs)(`span`,{className:a,"data-verdict":e,title:t,children:[(0,J.jsx)(`span`,{children:n}),(0,J.jsx)(`span`,{className:`suite`,children:t})]})}function Ef({rows:e,t,selectedKey:n,onSelect:r}){return e.length===0?(0,J.jsx)(`span`,{className:`muted`,children:`-`}):(0,J.jsx)(`div`,{className:`lab-verdict-stack`,children:e.map(e=>(0,J.jsx)(Tf,{verdict:e.verdict,caption:e.suiteId,label:t(Sf[e.verdict]),selected:n===e.projectionKey,onSelect:()=>r(e)},e.projectionKey))})}function Df({data:e,t,locale:n}){let{status:r}=e,i=[{label:t(`lab.subjectCount`),value:String(r.subjectCount??0)},{label:t(`lab.verdictCount`),value:String(r.verdictCount??0)},{label:t(`lab.observationCount`),value:String(r.observationCount??0)},{label:t(`lab.eventCount`),value:String(r.eventCount??0)}];return r.builtAtMs&&i.push({label:t(`lab.builtAt`),value:Rd(r.builtAtMs,n)}),(0,J.jsx)(`div`,{className:`lab-status-grid`,"aria-label":t(`lab.statusTitle`),children:i.map(e=>(0,J.jsxs)(`div`,{className:`lab-status-card`,children:[(0,J.jsx)(`span`,{className:`label`,children:e.label}),(0,J.jsx)(`span`,{className:`value`,children:e.value})]},e.label))})}function Of({community:e,locale:t}){if(!e||e.evidence.length===0)return null;let n=e.evidence.reduce((e,t)=>e+t.activeRecordCount,0),r=e.evidence.reduce((e,t)=>e+t.revokedRecordCount,0);return(0,J.jsxs)(`section`,{className:`lab-matrix-block`,"data-testid":`lab-community-evidence`,children:[(0,J.jsx)(`h3`,{className:`lab-matrix-title`,children:Ye(t,`community.title`)}),(0,J.jsx)(`p`,{className:`muted`,children:Ye(t,`community.notLocalVerdict`)}),(0,J.jsxs)(`dl`,{className:`lab-detail-meta`,children:[(0,J.jsxs)(`div`,{children:[(0,J.jsx)(`dt`,{children:Ye(t,`community.bundles`)}),(0,J.jsx)(`dd`,{children:e.evidence.length})]}),(0,J.jsxs)(`div`,{children:[(0,J.jsx)(`dt`,{children:Ye(t,`community.activeRecords`)}),(0,J.jsx)(`dd`,{children:n})]}),(0,J.jsxs)(`div`,{children:[(0,J.jsx)(`dt`,{children:Ye(t,`community.revokedRecords`)}),(0,J.jsx)(`dd`,{children:r})]})]})]})}function kf({verdict:e,detail:t,loading:n,error:r,t:i,locale:a,onClose:o}){let s=new Set([...e.contributingEventIds,...e.contradictingEventIds]).size;return(0,J.jsxs)(`aside`,{className:`lab-detail-pane`,"aria-label":i(`lab.detailTitle`),children:[(0,J.jsxs)(`div`,{className:`lab-detail-head`,children:[(0,J.jsx)(`h3`,{children:Ld(e.subjectId)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:o,children:i(`lab.detailClose`)})]}),(0,J.jsxs)(`dl`,{className:`lab-detail-meta`,children:[(0,J.jsxs)(`div`,{children:[(0,J.jsx)(`dt`,{children:i(`lab.col.layer`)}),(0,J.jsx)(`dd`,{children:i(bf[e.evidenceLayer])})]}),(0,J.jsxs)(`div`,{children:[(0,J.jsx)(`dt`,{children:i(`lab.col.suite`)}),(0,J.jsx)(`dd`,{className:`mono`,children:e.suiteId})]}),(0,J.jsxs)(`div`,{children:[(0,J.jsx)(`dt`,{children:i(`lab.col.verdict`)}),(0,J.jsx)(`dd`,{children:i(Sf[e.verdict])})]}),(0,J.jsxs)(`div`,{children:[(0,J.jsx)(`dt`,{children:i(`lab.col.asOf`)}),(0,J.jsx)(`dd`,{children:Rd(e.asOf,a)})]})]}),n&&(0,J.jsx)(_l,{busy:!0,children:i(`common.loading`)}),r&&(0,J.jsx)($,{tone:`err`,children:r}),t&&(0,J.jsxs)(J.Fragment,{children:[(0,J.jsxs)(`section`,{className:`lab-detail-section`,children:[(0,J.jsx)(`h4`,{children:i(`lab.detailSubject`)}),(0,J.jsx)(`p`,{className:`mono`,children:t.subject.subjectKind})]}),t.production&&(0,J.jsxs)(`section`,{className:`lab-detail-section`,"data-testid":`lab-production-signals`,children:[(0,J.jsx)(`h4`,{children:i(`lab.production.title`)}),(0,J.jsx)(`p`,{className:`muted`,children:i(`lab.production.notVerification`)}),(0,J.jsxs)(`dl`,{className:`lab-detail-meta`,children:[(0,J.jsxs)(`div`,{children:[(0,J.jsx)(`dt`,{children:i(`lab.production.attempts`)}),(0,J.jsx)(`dd`,{children:t.production.summary.recentProductionAttempts})]}),(0,J.jsxs)(`div`,{children:[(0,J.jsx)(`dt`,{children:i(`lab.production.successes`)}),(0,J.jsx)(`dd`,{children:t.production.summary.recentSuccessfulAttempts})]}),(0,J.jsxs)(`div`,{children:[(0,J.jsx)(`dt`,{children:i(`lab.production.routeErrors`)}),(0,J.jsx)(`dd`,{children:t.production.summary.recentRouteErrorSignals})]}),t.production.summary.lastObservedProductionAttempt!==void 0&&(0,J.jsxs)(`div`,{children:[(0,J.jsx)(`dt`,{children:i(`lab.production.lastObserved`)}),(0,J.jsx)(`dd`,{children:Rd(t.production.summary.lastObservedProductionAttempt,a)})]})]})]}),t.observations.length>0&&(0,J.jsxs)(`section`,{className:`lab-detail-section`,children:[(0,J.jsx)(`h4`,{children:i(`lab.detailObservations`)}),(0,J.jsx)(`ul`,{className:`lab-detail-list`,children:t.observations.map(e=>(0,J.jsxs)(`li`,{children:[(0,J.jsx)(`span`,{className:`mono`,children:e.scenarioId}),(0,J.jsx)(`span`,{children:e.outcome}),(0,J.jsx)(`span`,{className:`muted`,children:Rd(e.completedAt,a)})]},e.eventId))})]}),(t.events.length>0||s>0)&&(0,J.jsxs)(`section`,{className:`lab-detail-section`,children:[(0,J.jsxs)(`h4`,{children:[i(`lab.detailEvents`),t.events.length(0,J.jsxs)(`li`,{children:[(0,J.jsx)(`span`,{className:`mono`,children:e.eventId}),(0,J.jsx)(`span`,{children:e.eventKind})]},e.eventId))})]}),t.artifacts.length>0&&(0,J.jsxs)(`section`,{className:`lab-detail-section`,children:[(0,J.jsx)(`h4`,{children:i(`lab.detailArtifacts`)}),(0,J.jsx)(`ul`,{className:`lab-detail-list`,children:t.artifacts.map(e=>(0,J.jsxs)(`li`,{children:[(0,J.jsx)(`span`,{className:`mono`,title:e.digest,children:Ld(e.digest)}),(0,J.jsx)(`span`,{children:e.artifactClass??`-`}),(0,J.jsx)(`span`,{children:Ye(a,Cf[e.status])})]},e.digest))})]})]})]})}function Af({apiBase:e,active:t=!0,onCountChange:n}){let{t:r,locale:i}=ct(),[a,o]=(0,_.useState)({layer:``,verdict:``,subjectQuery:``,suiteId:``}),[s,c]=(0,_.useState)(null),[l,u]=(0,_.useState)(null),[d,f]=(0,_.useState)(!1),[p,m]=(0,_.useState)(null),[h,g]=(0,_.useState)(null),[v,y]=(0,_.useState)(!1),[b,x]=(0,_.useState)(null),S=(0,_.useRef)(null),C=(0,_.useRef)(null),w=(0,_.useRef)(null),T=(0,_.useMemo)(()=>Pd(a),[a]),E=JSON.stringify(T),D=(0,_.useCallback)(t=>gf(e,T,t),[e,T]),O=ml(`lab-matrix:${e}:${E}`,[e,E],D,{isEmpty:e=>e.verdicts.length===0,pollMs:6e4,enabled:t,pauseWhenHidden:!0}),k=(0,_.useCallback)(()=>{S.current?.abort(),S.current=null,c(null),u(null),f(!1)},[]),A=(0,_.useCallback)(()=>{w.current=null,C.current?.abort(),C.current=null,m(null),g(null),x(null),y(!1)},[]);(0,_.useEffect)(()=>{S.current?.abort()},[O.data]),(0,_.useEffect)(()=>()=>{S.current?.abort(),C.current?.abort()},[]);let j=(0,_.useCallback)(e=>{A(),k(),o(e)},[A,k]),M=s!==null&&s.baseData===O.data&&s.queryKey===E?s:null,N=l!==null&&l.baseData===O.data&&l.queryKey===E?l.message:null,P=(0,_.useMemo)(()=>{if(!t||!O.data?.status.projectionAvailable)return null;let e=O.data.status.verdictCount;return typeof e==`number`?e:O.data.verdicts.length+(M?.verdicts.length??0)},[t,O.data,M]);(0,_.useEffect)(()=>{n?.(P)},[n,P]);let F=(0,_.useMemo)(()=>O.data?[...O.data.verdicts,...M?.verdicts??[]]:[],[O.data,M]),I=(0,_.useMemo)(()=>O.data?Id(F,O.data.subjects):[],[F,O.data]),L=(0,_.useCallback)(async()=>{let t=M?.nextCursor??O.data?.nextCursor,n=O.data;if(!t||!n||d)return;S.current?.abort();let i=new AbortController;S.current=i;let a=E;u(null),f(!0);try{let r=await _f(e,T,t,i.signal);if(i.signal.aborted)return;c(e=>{let t=e?.baseData===n&&e.queryKey===a?e.verdicts:[];return{baseData:n,queryKey:a,verdicts:[...t,...r.verdicts],nextCursor:r.nextCursor,hasMore:r.hasMore}})}catch(e){i.signal.aborted||u({baseData:n,queryKey:a,message:wf(e,r(`lab.loadFailed`))})}finally{S.current===i&&(S.current=null,f(!1))}},[e,d,T,E,O.data,r,M]),R=(0,_.useCallback)(async n=>{if(!t)return;C.current?.abort();let i=new AbortController;C.current=i,w.current=n.projectionKey,m(n),g(null),x(null),y(!0);try{let t=await yf(e,n,i.signal);!i.signal.aborted&&w.current===n.projectionKey&&g(t)}catch(e){!i.signal.aborted&&w.current===n.projectionKey&&x(wf(e,r(`lab.detailLoadFailed`)))}finally{C.current===i&&(C.current=null,!i.signal.aborted&&w.current===n.projectionKey&&y(!1))}},[t,e,r]),z=O.refresh,B=(0,_.useCallback)(()=>{A(),k(),z({forceLoading:!0})},[A,z,k]),V=t?p:null,H=M?M.hasMore:O.data?.hasMore??!1,U=[{value:``,label:r(`lab.filter.all`)},...vd.map(e=>({value:e,label:r(bf[e])}))],W=[{value:``,label:r(`lab.filter.all`)},...yd.map(e=>({value:e,label:r(Sf[e])}))],ee=[{value:``,label:r(`lab.filter.all`)},...(O.data?.subjects??[]).map(e=>({value:e.subjectId,label:`${Ld(e.subjectId)} · ${e.subjectKind}`}))];if(O.state.showSkeleton)return(0,J.jsx)(gl,{label:r(`lab.loading`),rows:5});let G=O.state.showError?wf(O.error,r(`lab.loadFailed`)):null,K=O.data?.status,q=K&&!K.projectionAvailable,Y=K?.projectionIncompatible===!0;return(0,J.jsxs)(`div`,{className:`lab-page`,children:[(0,J.jsx)(`div`,{className:`lab-toolbar`,children:(0,J.jsxs)(`button`,{type:`button`,className:`btn btn-ghost`,onClick:B,disabled:O.refreshing,children:[(0,J.jsx)(pe,{}),r(`lab.refresh`)]})}),O.refreshing&&!O.state.showSkeleton&&(0,J.jsx)(_l,{busy:!0,live:!G,children:r(`common.loading`)}),G&&(0,J.jsx)($,{tone:`err`,children:G}),Y&&(0,J.jsx)($,{tone:`err`,children:r(`lab.projectionIncompatible`)}),q&&!Y&&(0,J.jsx)(Ot,{title:r(`lab.projectionUnavailable`)}),O.data&&(0,J.jsx)(Of,{community:O.data.community,locale:i}),O.data&&K?.projectionAvailable&&!Y&&(0,J.jsxs)(`div`,{className:`lab-layout`,children:[(0,J.jsxs)(`div`,{className:`lab-main`,children:[(0,J.jsx)(Df,{data:O.data,t:r,locale:i}),(0,J.jsxs)(`div`,{className:`lab-filters`,children:[(0,J.jsxs)(`div`,{className:`lab-filter-field`,children:[(0,J.jsx)(`label`,{htmlFor:`lab-filter-layer`,children:r(`lab.filter.layer`)}),(0,J.jsx)(Dt,{id:`lab-filter-layer`,value:a.layer,options:U,onChange:e=>j(t=>({...t,layer:e})),label:r(`lab.filter.layer`),portal:!1})]}),(0,J.jsxs)(`div`,{className:`lab-filter-field`,children:[(0,J.jsx)(`label`,{htmlFor:`lab-filter-verdict`,children:r(`lab.filter.verdict`)}),(0,J.jsx)(Dt,{id:`lab-filter-verdict`,value:a.verdict,options:W,onChange:e=>j(t=>({...t,verdict:e})),label:r(`lab.filter.verdict`),portal:!1})]}),(0,J.jsxs)(`div`,{className:`lab-filter-field`,children:[(0,J.jsx)(`label`,{htmlFor:`lab-filter-subject`,children:r(`lab.filter.subject`)}),(0,J.jsx)(Dt,{id:`lab-filter-subject`,value:a.subjectQuery,options:ee,onChange:e=>j(t=>({...t,subjectQuery:e})),label:r(`lab.filter.subject`),portal:!1})]})]}),I.length===0?(0,J.jsx)(Ot,{title:r(`lab.empty`)}):(0,J.jsxs)(J.Fragment,{children:[(0,J.jsxs)(`div`,{className:`lab-matrix-block`,children:[(0,J.jsx)(`h3`,{className:`lab-matrix-title`,children:r(`lab.matrixTitle`)}),(0,J.jsx)(`div`,{className:`lab-matrix-scroll`,children:(0,J.jsxs)(`table`,{className:`lab-matrix`,children:[(0,J.jsx)(`thead`,{children:(0,J.jsxs)(`tr`,{children:[(0,J.jsx)(`th`,{children:r(`lab.col.subject`)}),(0,J.jsx)(`th`,{children:r(`lab.subjectKind`)}),vd.map(e=>(0,J.jsx)(`th`,{children:r(xf[e])},e))]})}),(0,J.jsx)(`tbody`,{children:I.map(e=>(0,J.jsxs)(`tr`,{children:[(0,J.jsx)(`td`,{className:`subject`,title:e.subjectId,children:Ld(e.subjectId)}),(0,J.jsx)(`td`,{className:`kind`,children:e.subjectKind||Ye(i,`subjectKindUnknown`)}),vd.map(t=>(0,J.jsx)(`td`,{children:(0,J.jsx)(Ef,{rows:e.byLayer[t],t:r,selectedKey:V?.projectionKey??null,onSelect:e=>{R(e)}})},t))]},e.subjectId))})]})})]}),(0,J.jsxs)(`div`,{className:`lab-matrix-block`,children:[(0,J.jsx)(`h3`,{className:`lab-matrix-title`,children:r(`lab.verdictsTitle`)}),(0,J.jsx)(`div`,{className:`lab-matrix-scroll`,children:(0,J.jsxs)(`table`,{className:`lab-detail-table`,children:[(0,J.jsx)(`thead`,{children:(0,J.jsxs)(`tr`,{children:[(0,J.jsx)(`th`,{children:r(`lab.col.subject`)}),(0,J.jsx)(`th`,{children:r(`lab.col.layer`)}),(0,J.jsx)(`th`,{children:r(`lab.col.suite`)}),(0,J.jsx)(`th`,{children:r(`lab.col.verdict`)}),(0,J.jsx)(`th`,{children:r(`lab.col.asOf`)})]})}),(0,J.jsx)(`tbody`,{children:F.map(e=>{let t=V?.projectionKey===e.projectionKey;return(0,J.jsxs)(`tr`,{className:t?`selected`:``,children:[(0,J.jsx)(`td`,{className:`mono`,title:e.subjectId,children:(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,"data-verdict-detail":e.projectionKey,"aria-pressed":t,"aria-label":Ye(i,`selectVerdict`,{subject:Ld(e.subjectId)}),onClick:()=>{R(e)},children:Ld(e.subjectId)})}),(0,J.jsx)(`td`,{children:r(bf[e.evidenceLayer])}),(0,J.jsx)(`td`,{className:`mono`,children:e.suiteId}),(0,J.jsx)(`td`,{children:(0,J.jsx)(Tf,{verdict:e.verdict,caption:e.suiteId,label:r(Sf[e.verdict])})}),(0,J.jsx)(`td`,{children:Rd(e.asOf,i)})]},e.projectionKey)})})]})})]}),N&&(0,J.jsx)($,{tone:`err`,children:N}),H&&(0,J.jsx)(`div`,{className:`lab-load-more`,children:(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost`,disabled:d,onClick:()=>{L()},children:r(d?`common.loading`:`lab.loadMore`)})})]})]}),V&&(0,J.jsx)(kf,{verdict:V,detail:h,loading:v,error:b,t:r,locale:i,onClose:A})]})]})}var jf=[`catalog`,`combos`,`routing`,`compatibility`];function Mf(e){return e===`catalog`?`models`:`models/${e}`}function Nf(e=window.location.hash){let t=dt(e);return t===`models/combos`||t===`combos`||t.startsWith(`combos/`)?`combos`:t===`models/routing`||t===`routing`||t.startsWith(`routing/`)?`routing`:t===`models/compatibility`||t===`lab`||t.startsWith(`lab/`)?`compatibility`:`catalog`}function Pf(e){pt(Mf(e))}function Ff(e){return`models-tab-${e}`}function If(e){return`models-panel-${e}`}var Lf={catalog:`models.tab.catalog`,combos:`models.tab.combos`,routing:`models.tab.routing`,compatibility:`models.tab.compatibility`};function Rf({tab:e,onSelect:t,meta:n}){let r=Q(),i=(0,_.useRef)(null);i.current===null&&(i.current=new Map);let a=e=>{t(e),window.requestAnimationFrame(()=>{i.current.get(e)?.focus({preventScroll:!0})})},o=t=>{let n=jf.indexOf(e),r=null;t.key===`ArrowLeft`?r=(n-1+jf.length)%jf.length:t.key===`ArrowRight`?r=(n+1)%jf.length:t.key===`Home`?r=0:t.key===`End`&&(r=jf.length-1),r!==null&&(t.preventDefault(),a(jf[r]))};return(0,J.jsx)(`div`,{className:`page-tabs`,role:`tablist`,"aria-label":r(`models.tabsLabel`),children:jf.map(t=>{let s=t===e,c=n?.[t];return(0,J.jsxs)(`button`,{ref:e=>{e?i.current.set(t,e):i.current.delete(t)},type:`button`,role:`tab`,id:Ff(t),"aria-selected":s,"aria-controls":If(t),tabIndex:s?0:-1,className:`page-tab${s?` page-tab--active`:``}`,onClick:()=>a(t),onKeyDown:o,children:[r(Lf[t]),c?(0,J.jsx)(`span`,{className:`section-tab-meta`,children:c}):null]},t)})})}function zf(e,t){let n=new Map;for(let t of e){let e=n.get(t.provider);e?e.push(t):n.set(t.provider,[t])}let r=new Map(t.map(e=>[e.name,e]));for(let e of t){if(e.disabled===!0){n.delete(e.name);continue}e.authMode!==`forward`&&(n.has(e.name)||n.set(e.name,[]))}return[...n.entries()].map(([e,t])=>{let n=r.get(e);return{provider:e,rows:t,native:t.length>0&&t.every(e=>e.native===!0),nativeProviderGroup:t.some(e=>e.native===!0),liveModels:n?.liveModels!==!1,configuredModels:n?.models??[],contextWindow:n?.contextWindow,modelContextWindows:n?.modelContextWindows,discovery:n?.discovery,entitlement:n?.entitlement}}).sort((e,t)=>e.nativeProviderGroup===t.nativeProviderGroup?e.provider.localeCompare(t.provider):e.nativeProviderGroup?-1:1)}function Bf(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function Vf(e){if(!Array.isArray(e)||e.some(e=>typeof e!=`string`))throw Error(`invalid model list`);return[...new Set(e)]}function Hf(e){if(!Bf(e))throw Error(`invalid selected models response`);let t=e.selected;if(!Bf(t))throw Error(`invalid selected models response`);return Object.fromEntries(Object.entries(t).map(([e,t])=>[e,Vf(t)]))}async function Uf(e,t=fetch,n){let r=await t(`${e}/api/selected-models`,n?{signal:n}:void 0);if(!r.ok)throw Error(`selected models HTTP ${r.status}`);return Hf(await r.json())}function Wf(e,t,n,r=!1){if(r)return!0;let i=e[t];return!i||i.length===0||i.includes(n)}function Gf(e,t,n,r,i){return Wf(e,t,n,r)&&!i}async function Kf(e,t,n,r,i,a=fetch){return a(`${e}/api/model-visibility`,{method:`PUT`,headers:{"Content-Type":`application/json`},body:JSON.stringify({scope:t,provider:n,targets:r,enabled:i})})}function qf(e,t){return e===t}function Jf(e,t){switch(t.reason){case`http`:return e(`models.discoveryFailedHttp`,{status:t.httpStatus});case`blocked`:return e(`models.discoveryFailedBlocked`);case`invalid_response`:return e(`models.discoveryFailedInvalidResponse`);case`network`:return e(`models.discoveryFailedNetwork`);case`provider`:return e(`models.discoveryFailedProvider`);default:return e(`models.discoveryFailedGeneric`)}}var Yf=[`none`,`minimal`,`low`,`medium`,`high`,`xhigh`,`max`],Xf=Array.from({length:18},(e,t)=>1e5+t*5e4),Zf=new Set(Xf),Qf=272e3,$f=922e3,ep=[Qf,372e3,$f],tp=new Set(ep),np=`custom`,rp=[4,8,16,32,64,128,256,500,1e3],ip=new Set(rp),ap=`ocx-models-collapsed:v2`;function op(e){return!Number.isFinite(e)||e<=0?String(e):e%1e3==0?e>=1e6?Number((e/1e6).toFixed(2))+`M`:`${e/1e3}k`:e.toLocaleString()}function sp(e){let t=new Set;for(let n of e)n.disabled&&t.add(n.namespaced);return t}function cp(e,t,n,r){let i=[];for(let a of e){let e=t.has(a.id)||t.has(a.namespaced);Gf(n,a.provider,a.id,a.native===!0,e)&&i.push({value:a.namespaced,label:r?Pn(a.namespaced,r):a.namespaced})}return i}function lp(e=localStorage){try{let t=e.getItem(ap);if(t===null)return null;let n=JSON.parse(t);return Array.isArray(n)?new Set(n.filter(e=>typeof e==`string`)):null}catch{return null}}function up(e,t=localStorage){try{t.setItem(ap,JSON.stringify([...e]))}catch{}}function dp({liveModels:e,discovery:t,showFailureBadge:n=!0}){let r=Q(),i=e&&t?.status===`failed`?t:void 0;return(0,J.jsxs)(`div`,{className:`row muted text-label leading-body`,role:`status`,style:{alignItems:`flex-start`,gap:8,padding:`6px 0`},children:[(0,J.jsx)(Z,{width:15,height:15,"aria-hidden":`true`,style:{flexShrink:0,marginTop:2}}),(0,J.jsxs)(`span`,{children:[i&&n&&(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)(`span`,{className:`badge badge-amber`,children:r(`models.discoveryFailedBadge`)}),` `]}),i?`${Jf(r,i)} `:`${r(e?`models.emptyDiscovery`:`models.emptyDiscoveryDisabled`)} `,(0,J.jsx)(`button`,{type:`button`,className:`link-btn`,onClick:()=>pt(`providers`),children:r(`models.openProviderSettings`)})]})]})}var fp={catalog:`models.subtitle`,combos:`models.subtitle.combos`,routing:`models.subtitle.routing`,compatibility:`models.subtitle.compatibility`};function pp(e){let t=e.replace(/[_,\s]/g,``);if(!t)return null;let n=Number(t);return Number.isSafeInteger(n)&&n>0?n:void 0}function mp({apiBase:e,restartEpoch:t=0}){let[n,r]=(0,_.useState)(null),i=(0,_.useRef)(!0);(0,_.useEffect)(()=>(i.current=!0,()=>{i.current=!1}),[]);let a=(0,_.useCallback)(t=>{nl(e,{signal:t}).then(e=>{t?.aborted||!i.current||r(e.state)})},[e]),{restarting:o,restart:s}=sl(e,{onSettled:()=>a()});(0,_.useEffect)(()=>{let e=new AbortController;return a(e.signal),()=>e.abort()},[a,t]);let[c,l]=(0,_.useState)(Nf),[u,d]=(0,_.useState)(()=>new Set([Nf()])),f=(0,_.useCallback)(e=>{l(e),d(t=>t.has(e)?t:new Set([...t,e]))},[]);(0,_.useEffect)(()=>{let e=()=>f(Nf());return window.addEventListener(`hashchange`,e),window.addEventListener(`popstate`,e),()=>{window.removeEventListener(`hashchange`,e),window.removeEventListener(`popstate`,e)}},[f]);let p=(0,_.useCallback)(e=>{Pf(e),f(e)},[f]),m=c===`catalog`,[h,g]=(0,_.useState)(null),[v,y]=(0,_.useState)(null),[b,x]=(0,_.useState)(null),S=Q(),C=`ocx.models.catalog.v1:${e}`,w=(0,_.useMemo)(()=>gr(C),[C]),[T,E]=(0,_.useState)(()=>w?.models??[]),[D,O]=(0,_.useState)(()=>w?.providers??[]),[k,A]=(0,_.useState)(()=>new Set(w?.disabled??[])),[j,M]=(0,_.useState)(()=>w?.selectedModels??null),[N,P]=(0,_.useState)({}),[F,I]=(0,_.useState)({}),[L,R]=(0,_.useState)(()=>w?.contextCaps??{}),[z,B]=(0,_.useState)(()=>w?.contextCapValue??35e4),[V,H]=(0,_.useState)(``),[U,W]=(0,_.useState)(!1),[ee,G]=(0,_.useState)({}),[q,Y]=(0,_.useState)({}),te=lp(),[ne,ie]=(0,_.useState)(()=>te??new Set),ae=(0,_.useRef)(te===null),[oe,se]=(0,_.useState)(``),[ce,le]=(0,_.useState)(!1),[de,fe]=(0,_.useState)(0),X=(e,t)=>{le(e),se(t),fe(e=>e+1)};(0,_.useEffect)(()=>{if(!oe)return;let e=setTimeout(()=>se(``),ce?6e3:8e3);return()=>clearTimeout(e)},[oe,ce,de]);let[me,he]=(0,_.useState)(!1),ve=(0,_.useRef)(!1),ye=(0,_.useRef)(0),be=(0,_.useRef)(!1),[xe,Ce]=(0,_.useState)(null),[we,Te]=(0,_.useState)({}),[Ee,De]=(0,_.useState)(null),[Oe,ke]=(0,_.useState)({providers:{},models:{},defaults:{global:!1,providers:{}}}),[Ae,je]=(0,_.useState)(!1),[Me,Ne]=(0,_.useState)(null),[Pe,Fe]=(0,_.useState)(!0),[Ie,Le]=(0,_.useState)(!1),[Re,ze]=(0,_.useState)(``),Be=(0,_.useRef)(!1),[Ve,He]=(0,_.useState)(``),[Ue,We]=(0,_.useState)(!1),[Ge,Ke]=(0,_.useState)(!1),[qe,Je]=(0,_.useState)(!1),Ye=(0,_.useCallback)(async t=>{let n=await Ft(await fetch(`${e}/api/aliases`,{signal:t}));n&&!t?.aborted&&ke(n)},[e]);(0,_.useEffect)(()=>{let e=new AbortController;return Ye(e.signal),()=>e.abort()},[Ye]);let Xe=async t=>{let n=window.prompt(S(`models.aliasPrompt`),Oe.providers[t]??``);if(n!==null){if(!(await fetch(`${e}/api/providers/${encodeURIComponent(t)}/alias`,{method:`PUT`,headers:{"content-type":`application/json`},body:JSON.stringify({alias:n.trim()||null})})).ok){X(!1,S(`models.aliasConflict`));return}await Ye(),X(!0,S(`models.aliasSaved`))}},Ze=async(t,n)=>{let r=Oe.models[t]?.[n]?.alias??``,i=window.prompt(S(`models.modelAliasPrompt`),r);if(i===null)return;let a=i.trim()?{set:{[n]:i.trim()}}:{remove:[n]};if(!(await fetch(`${e}/api/providers/${encodeURIComponent(t)}/model-aliases`,{method:`PUT`,headers:{"content-type":`application/json`},body:JSON.stringify(a)})).ok){X(!1,S(`models.aliasConflict`));return}await Ye(),X(!0,S(`models.aliasSaved`))},Qe=async(t,n)=>{(await fetch(`${e}/api/default-aliases`,{method:`PUT`,headers:{"content-type":`application/json`},body:JSON.stringify({enabled:t,...n?{provider:n}:{}})})).ok&&await Ye()},[$e,et]=(0,_.useState)(`add`),[tt,nt]=(0,_.useState)(``),[rt,it]=(0,_.useState)(``),[at,ot]=(0,_.useState)(``),[st,ct]=(0,_.useState)(``),[lt,ut]=(0,_.useState)(``),[dt,ft]=(0,_.useState)(!1),[pt,mt]=(0,_.useState)([`text`]),[ht,gt]=(0,_.useState)(!1),[_t,vt]=(0,_.useState)([]),yt=(0,_.useRef)(!1),[bt,xt]=(0,_.useState)(!1),[St,Ct]=(0,_.useState)(``),[wt,Et]=(0,_.useState)(null),[At,jt]=(0,_.useState)([]),[Mt,Nt]=(0,_.useState)(``),[It,Lt]=(0,_.useState)(``),[Bt,Vt]=(0,_.useState)({}),[Ht,Ut]=(0,_.useState)({contextWindow:null,modelContextWindows:{}}),[Wt,Gt]=(0,_.useState)(new Set),[Kt,qt]=(0,_.useState)(!1),[Jt,Yt]=(0,_.useState)(!1),[Xt,Zt]=(0,_.useState)(``),[Qt,$t]=(0,_.useState)(null),en=(0,_.useRef)(null),[tn,nn]=(0,_.useState)(null),[rn,an]=(0,_.useState)(!1),[on,sn]=(0,_.useState)(null);(0,_.useEffect)(()=>()=>{en.current&&clearTimeout(en.current)},[]);let cn=(0,_.useMemo)(()=>cp(T,k,j??{},S),[T,k,j,S]),ln=(0,_.useMemo)(()=>{let e=new Set(cn.map(e=>e.value));return pn(T.filter(t=>e.has(t.namespaced)),tn?.model,tn?.sourceModels)},[T,tn?.model,tn?.sourceModels,cn]),un=(0,_.useCallback)(async()=>{let t=Vn(15e3);try{let n=await Ft(await fetch(`${e}/api/shadow-call-settings`,{signal:t.signal}));n&&nn(n)}catch{}finally{t.clear()}},[e]),dn=(0,_.useCallback)(async()=>{if(Be.current)return;let t=Vn(15e3);try{let n=await fetch(`${e}/api/v2`,{signal:t.signal});if(!(n.headers.get(`content-type`)??``).includes(`application/json`)){Ce(null);return}let r=await Ft(n);if(!r||typeof r.enabled!=`boolean`){Ce(null);return}Ce({enabled:r.enabled,agentsMaxThreadsConflict:r.agentsMaxThreadsConflict===!0,maxConcurrentThreadsPerSession:typeof r.maxConcurrentThreadsPerSession==`number`?r.maxConcurrentThreadsPerSession:null,multiAgentMode:r.multiAgentMode===`v1`||r.multiAgentMode===`v2`?r.multiAgentMode:`default`,keepNativeChatGptOnV1:r.keepNativeChatGptOnV1===!0})}catch{Ce(null)}finally{t.clear(),Fe(!1)}},[e]),fn=(0,_.useCallback)(async t=>{let[n,r,i,a]=await Promise.all([fetch(`${e}/api/models`,{signal:t}),fetch(`${e}/api/provider-context-caps`,{signal:t}),fetch(`${e}/api/providers`,{signal:t}),Uf(e,fetch,t)]),[o,s,c]=await Promise.all([Pt(n),Pt(r),Pt(i)]);if(o===void 0||s===void 0||c===void 0)throw Error(`models payload missing`);if(t.aborted)throw Error(`models request aborted`);let l=sp(o),u=typeof s.value==`number`&&Number.isFinite(s.value)&&s.value>0?s.value:typeof s.cap==`number`&&Number.isFinite(s.cap)&&s.cap>0?s.cap:void 0,d=u===void 0?35e4:u,f={models:o,providers:c,selectedModels:a,disabled:[...l],contextCaps:s.caps??{},contextCapValue:d};return br(C,f),f},[e,C]),mn=(0,_.useCallback)(e=>{let t=zf(e.models,e.providers);sn(e=>e!==null&&!t.some(t=>t.provider===e)?null:e),E(e.models),O(e.providers),A(new Set(e.disabled)),M(e.selectedModels),B(e.contextCapValue),R(e.contextCaps)},[]),hn=ml(C,[e],async e=>{let t=await fn(e);if(e.aborted)throw Error(`models request aborted`);return mn(t),t},{isEmpty:()=>!1,pollMs:1e4,initialData:w??void 0,enabled:m,deadlineMs:6e4}),gn=hn.state,_n=(0,_.useCallback)(async(e=!1)=>{if(be.current&&!e)return!1;be.current=!0;let t=++ye.current;try{let e=await fn(new AbortController().signal);return qf(t,ye.current)?(mn(e),K(C,e),!0):!1}catch{return!1}finally{qf(t,ye.current)&&(be.current=!1)}},[mn,C,fn]);(0,_.useEffect)(()=>{if(!m)return;let e=window.setTimeout(()=>{un(),dn(),Wn(),Kn()},0),t=Gn(()=>{Be.current||dn()},1e4);return()=>{window.clearTimeout(e),t()}},[m,un,dn]);let vn=(0,_.useMemo)(()=>zf(T,D),[T,D]),yn=T.length>0||gn.data!==void 0,bn=e=>{let t=[...new Set([...e.rows.map(e=>e.id),...e.configuredModels,...Object.keys(e.modelContextWindows??{})])].sort(),n=t[0]??``;Et(e.provider),jt(t),Nt(n);let r=e.contextWindow?String(e.contextWindow):``,i=Object.fromEntries(Object.entries(e.modelContextWindows??{}).map(([e,t])=>[e,String(t)]));Lt(r),Vt(i),Ut({contextWindow:e.contextWindow??null,modelContextWindows:Object.fromEntries(Object.entries(e.modelContextWindows??{}).map(([e,t])=>[e,t]))}),Gt(new Set),qt(!1),Zt(``)},xn=e=>{Nt(e)},Sn=async()=>{if(!wt)return;let t=pp(It);if(!vn.find(e=>e.provider===wt)){Zt(S(`models.contextSaveFailed`));return}if(Kt&&t===void 0){Zt(S(`models.contextInvalid`));return}let n={};for(let e of Wt){let t=pp(Bt[e]??``);if(t===void 0){Zt(S(`models.contextInvalid`));return}t!==(Ht.modelContextWindows[e]??null)&&(n[e]=t)}let r=Kt&&t!==Ht.contextWindow;if(!r&&Object.keys(n).length===0){Et(null),X(!0,S(`models.contextUnchanged`));return}Yt(!0),Zt(``);try{let i={};r&&(i.contextWindow=t),Object.keys(n).length>0&&(i.modelContextWindows=n),await Pt(await fetch(`${e}/api/providers?name=${encodeURIComponent(wt)}`,{method:`PATCH`,headers:{"Content-Type":`application/json`},body:JSON.stringify(i)}),S(`models.contextSaveFailed`))}catch(e){Zt(e instanceof Error?e.message:S(`models.contextSaveFailed`));return}finally{Yt(!1)}Et(null),X(!0,S(`models.contextSaved`)),await _n(!0)};(0,_.useEffect)(()=>{if(!ae.current||vn.length===0)return;ae.current=!1;let e=new Set(vn.map(e=>e.provider));ie(e),up(e)},[vn]);let Cn=(0,_.useMemo)(()=>j?T.filter(e=>Gf(j,e.provider,e.id,e.native===!0,k.has(e.namespaced))).length:0,[k,T,j]),wn=(0,_.useMemo)(()=>({catalog:yn?S(`models.active`,{active:Cn,total:T.length}):void 0,combos:h===null?void 0:String(h),routing:v===null?void 0:String(v),compatibility:b===null?void 0:String(b)}),[yn,h,b,Cn,T.length,v,S]),Tn=async(t,n,r,i)=>{++ye.current,he(!0),ve.current=!0,se(``);let a=null;try{(await Kf(e,t,n,r,i)).ok||(a=`models.saveFailed`)}catch{a=`models.networkError`}finally{let e=await _n(!0);a?(le(!1),se(S(a))):e&&(le(!0),se(S(`models.applied`))),he(!1),ve.current=!1}},En=async(t,n=!1)=>{he(!0),ve.current=!0,se(``);let r=L[t]===void 0;try{let i=await fetch(`${e}/api/provider-context-caps`,{method:`PUT`,headers:{"Content-Type":`application/json`},body:JSON.stringify(r&&n?{provider:t,enabled:r,value:$f}:{provider:t,enabled:r})});try{let e=await Pt(i,S(`models.capSaveFailed`));R(e?.caps??{}),le(!0),se(S(`models.capApplied`)),await _n(!0)}catch(e){le(!1),se(e instanceof Error?e.message:S(`models.capSaveFailed`))}}catch{le(!1),se(S(`models.networkError`))}finally{he(!1),ve.current=!1}},Dn=e=>{ie(t=>{let n=new Set(t);return n.has(e)?n.delete(e):n.add(e),up(n),n})},On=e=>{ie(()=>{let t=e?new Set(vn.map(e=>e.provider)):new Set;return up(t),t})},kn=async t=>{he(!0),ve.current=!0,se(``);try{let n=await fetch(`${e}/api/provider-context-caps`,{method:`PUT`,headers:{"Content-Type":`application/json`},body:JSON.stringify(t)});try{let e=await Pt(n,S(`models.capSaveFailed`));typeof e?.value==`number`&&Number.isFinite(e.value)&&e.value>0&&B(e.value),R(e?.caps??{}),le(!0),se(S(`models.capApplied`)),await _n(!0)}catch(e){le(!1),se(e instanceof Error?e.message:S(`models.capSaveFailed`))}}catch{le(!1),se(S(`models.networkError`))}finally{he(!1),ve.current=!1}},An=(0,_.useMemo)(()=>{let e=vn.filter(e=>!e.native);return e.length>0&&e.every(e=>L[e.provider]===z)&&Object.keys(L).every(e=>L[e]===z)},[vn,L,z]),Mn=e=>{!Number.isSafeInteger(e)||e<=0||kn(An?{value:e,setAll:!0}:{value:e})},Fn=(e,t)=>{if(t===`custom`){G(t=>({...t,[e]:!0})),Y(t=>({...t,[e]:String(L[e]??z)}));return}G(t=>({...t,[e]:!1}));let n=Number(t);Number.isSafeInteger(n)&&n>0&&n!==L[e]&&kn({provider:e,enabled:!0,value:n})},In=e=>{let t=Number((q[e]??``).replace(/[_,\s]/g,``));if(!Number.isSafeInteger(t)||t<=0){X(!1,S(`models.capSaveFailed`));return}G(t=>({...t,[e]:!1})),kn({provider:e,enabled:!0,value:t})},Ln=e=>{if(e===`custom`){W(!0),H(String(z));return}W(!1);let t=Number(e);Number.isSafeInteger(t)&&t>0&&t!==z&&Mn(t)},Rn=()=>{let e=Number(V.replace(/[_,\s]/g,``));if(!Number.isSafeInteger(e)||e<=0){X(!1,S(`models.capSaveFailed`));return}W(!1),Mn(e)},zn=()=>{kn({setAll:!An})},Bn=async t=>{if(!(!tn||rn)){an(!0),nn({...tn,...t});try{await fetch(`${e}/api/shadow-call-settings`,{method:`PUT`,headers:{"Content-Type":`application/json`},body:JSON.stringify(t)})}finally{an(!1)}}},Hn=async t=>{if(!(!xe||Be.current)){Le(!0),Be.current=!0,ze(``),se(``);try{let n=await fetch(`${e}/api/v2`,{method:`PUT`,headers:{"Content-Type":`application/json`},body:JSON.stringify(t)});try{let e=await Pt(n,S(`models.saveFailed`));if(!e||typeof e.enabled!=`boolean`){le(!1),se(S(`models.saveFailed`));return}Ce({enabled:e.enabled,agentsMaxThreadsConflict:e.agentsMaxThreadsConflict===!0,maxConcurrentThreadsPerSession:typeof e.maxConcurrentThreadsPerSession==`number`?e.maxConcurrentThreadsPerSession:null,multiAgentMode:e.multiAgentMode===`v1`||e.multiAgentMode===`v2`?e.multiAgentMode:`default`,keepNativeChatGptOnV1:e.keepNativeChatGptOnV1===!0}),le(!0),se(S(`models.v2Applied`)),ze((e.warnings??[]).join(` `))}catch(e){le(!1),se(e instanceof Error?e.message:S(`models.saveFailed`))}}catch{le(!1),se(S(`models.networkError`))}finally{Le(!1),Be.current=!1}}},Un=async e=>{!xe||xe.multiAgentMode===e||await Hn({multiAgentMode:e})},Wn=async()=>{try{let t=Vn(15e3),n=await Ft(await fetch(`${e}/api/model-presets`,{signal:t.signal}));Te(n?.providers??{})}catch{Te({})}},Kn=async()=>{try{let t=await fetch(`${e}/api/model-discovery`);De(await Ft(t)??null)}catch{De(null)}},qn=async(t,n)=>{await Ft(await fetch(`${e}/api/model-discovery`,{method:`PUT`,headers:{"content-type":`application/json`},body:JSON.stringify({policy:t,provider:n??null})})),await Promise.all([Kn(),_n()])},Jn=async(t,n)=>{if(!Me){Ne(t);try{let r=Vn(3e4),i=await Ft(await fetch(`${e}/api/model-presets`,{method:`PUT`,headers:{"content-type":`application/json`},body:JSON.stringify({provider:t,mode:n}),signal:r.signal}))??{};i.fallback===`preset-empty`?X(!1,S(`models.presetEmpty`,{provider:t})):X(!0,n===`all`?S(`models.presetClearedToast`,{provider:t}):S(`models.presetAppliedToast`,{provider:t,count:String(i.selected?.length??0)})),await Promise.all([Wn(),_n()])}catch(e){X(!1,e instanceof Error?e.message:String(e))}finally{Ne(null)}}},Yn=async e=>{!xe||xe.keepNativeChatGptOnV1===e||await Hn({keepNativeChatGptOnV1:e})},Xn=async t=>{if(!(!xe||Be.current)){if(!Number.isInteger(t)||t<1){X(!1,S(`models.v2ThreadsInvalid`));return}if(xe.maxConcurrentThreadsPerSession!==t){Le(!0),Be.current=!0,ze(``),se(``);try{let n=await fetch(`${e}/api/v2`,{method:`PUT`,headers:{"Content-Type":`application/json`},body:JSON.stringify({maxConcurrentThreadsPerSession:t})});try{let e=await Pt(n,S(`models.saveFailed`));if(!e||typeof e.enabled!=`boolean`){le(!1),se(S(`models.saveFailed`));return}Ce({enabled:e.enabled,agentsMaxThreadsConflict:e.agentsMaxThreadsConflict===!0,maxConcurrentThreadsPerSession:typeof e.maxConcurrentThreadsPerSession==`number`?e.maxConcurrentThreadsPerSession:null,multiAgentMode:e.multiAgentMode===`v1`||e.multiAgentMode===`v2`?e.multiAgentMode:`default`,keepNativeChatGptOnV1:e.keepNativeChatGptOnV1===!0}),le(!0),se(S(`models.v2ThreadsApplied`)),We(!1)}catch(e){le(!1),se(e instanceof Error?e.message:S(`models.saveFailed`))}}catch{le(!1),se(S(`models.networkError`))}finally{Le(!1),Be.current=!1}}}},Zn=e=>{if(e===`custom`){We(!0),He(String(xe?.maxConcurrentThreadsPerSession??``));return}We(!1),Xn(Number(e))},Qn=(e,t)=>{en.current&&clearTimeout(en.current),en.current=setTimeout(()=>{$t({namespaced:e,rect:t.getBoundingClientRect()})},300)},$n=(e,t)=>{en.current&&clearTimeout(en.current),$t({namespaced:e,rect:t.getBoundingClientRect()})},er=()=>{en.current&&clearTimeout(en.current),en.current=setTimeout(()=>$t(null),120)},tr=()=>{en.current&&clearTimeout(en.current)},nr=async(t,n,r,i,a,o)=>{xt(!0),Ct(``);try{let s=await fetch(`${e}/api/custom-models`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({provider:t,modelId:n,displayName:r,contextWindow:i,inputModalities:a,reasoningEfforts:o})});try{await Pt(s,S(`models.customSaveFailed`)),Je(!1),X(!0,S(`models.customAdded`)),await _n(!0)}catch(e){Ct(e instanceof Error?e.message:S(`models.customSaveFailed`))}}catch{Ct(S(`models.networkError`))}finally{xt(!1)}},rr=async(t,n)=>{xt(!0),Ct(``);try{let r=await fetch(`${e}/api/custom-models/${encodeURIComponent(t)}`,{method:`PUT`,headers:{"Content-Type":`application/json`},body:JSON.stringify(n)});try{await Pt(r,S(`models.customSaveFailed`)),Je(!1),X(!0,S(`models.customUpdated`)),await _n(!0)}catch(e){Ct(e instanceof Error?e.message:S(`models.customSaveFailed`))}}catch{Ct(S(`models.networkError`))}finally{xt(!1)}},ir=async t=>{try{(await fetch(`${e}/api/custom-models/${encodeURIComponent(t)}`,{method:`DELETE`})).ok?(X(!0,S(`models.customDeleted`)),await _n(!0)):X(!1,S(`models.customSaveFailed`))}catch{X(!1,S(`models.networkError`))}},ar=gn.data??w,or=gn.kind===`failed-cold`?gn.error instanceof Error?gn.error.message:S(`models.loadFail`):null,sr=gn.showSkeleton&&!ar,cr=j??{},lr=e=>{let{provider:t,rows:n,nativeProviderGroup:r,liveModels:i,discovery:a}=e,o=ne.has(t),s=e=>Gf(cr,t,e.id,e.native===!0,k.has(e.namespaced)),c=n.filter(s).length,l=Ee?.recentArrivals[t]??[],u=new Set(l.map(e=>e.id)),d=L[t]!==void 0,f=L[t]??z,p=n.reduce((e,t)=>{let n=typeof t.contextWindow==`number`&&t.contextWindow>0?t.contextWindow:void 0;return n===void 0?e:e===void 0||n>e?n:e},void 0),m=d?f:r?Qf:p??f,h=e.nativeProviderGroup?ep:Xf,g=e.nativeProviderGroup?tp:Zf,_=i&&a?.status===`failed`?a:void 0,v=(N[t]??``).trim().toLowerCase(),y=v?n.filter(e=>e.id.toLowerCase().includes(v)):n,b=y.toSorted((e,t)=>Number(!s(e))-Number(!s(t))),x=F[t]??60,C=b.slice(0,x),w=y.length-C.length,T=n.length>0,E=!T||n.every(s),D=!T||n.every(e=>!s(e)),O=e=>{T&&Tn(`provider`,t,n.map(e=>({id:e.id,native:e.native===!0})),e)};return(0,J.jsxs)(`div`,{className:`card models-provider-card`,children:[(0,J.jsxs)(`div`,{className:`row group-head models-provider-head${o?``:` open`}`,children:[(0,J.jsxs)(`button`,{type:`button`,className:`row models-provider-toggle`,onClick:()=>Dn(t),"aria-expanded":!o,style:{flex:`1 1 auto`,border:0,background:`transparent`,padding:0,color:`inherit`,cursor:`pointer`,textAlign:`left`},children:[(0,J.jsx)(Se,{style:{width:14,height:14,color:`var(--muted)`,transform:o?`none`:`rotate(90deg)`,transition:`transform .12s`}}),(0,J.jsx)(`span`,{className:`text-body font-semibold`,style:{whiteSpace:`nowrap`},children:Nn(t)}),Oe.providers[t]&&(0,J.jsx)(`span`,{className:`models-chip mono text-caption`,children:Oe.providers[t]}),r&&(0,J.jsx)(`span`,{className:`models-chip muted mono text-caption`,children:S(`models.nativeGroupLabel`)}),_&&(0,J.jsx)(`span`,{className:`badge badge-amber`,role:`status`,title:Jf(S,_),children:S(`models.discoveryFailedBadge`)}),(0,J.jsx)(`span`,{className:`muted mono text-label`,children:S(`models.active`,{active:c,total:n.length})}),l.length>0&&(0,J.jsx)(`span`,{className:`models-chip mono text-caption`,children:S(`models.newCount`,{count:l.length})})]}),(0,J.jsxs)(`div`,{className:`row models-provider-actions`,children:[(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm models-alias-edit`,"aria-label":S(`models.editProviderAlias`),title:S(`models.editProviderAlias`),onClick:()=>void Xe(t),children:(0,J.jsx)(ge,{style:{width:14,height:14}})}),(0,J.jsx)(Tt,{on:Oe.defaults.providers[t]??Oe.defaults.global,onClick:()=>void Qe(!(Oe.defaults.providers[t]??Oe.defaults.global),t),label:S(`models.useDefaultAliases`),showLabel:!0}),(0,J.jsxs)(`button`,{type:`button`,className:`btn btn-ghost btn-sm text-caption`,onClick:e=>{e.stopPropagation(),et(`add`),nt(t),it(``),ot(``),ct(``),ut(``),ft(!1),mt([`text`]),gt(!1),vt([]),yt.current=!1,Ct(``),Je(!0)},"aria-haspopup":`dialog`,children:[(0,J.jsx)(`span`,{"aria-hidden":`true`,children:`+`}),` `,S(`models.customAdd`)]}),(()=>{let e=we[t];if(!e)return null;let n=Me===t,r=e.mode===`custom`&&e.appliedVersion!==void 0&&e.appliedVersion(0,J.jsx)(`button`,{type:`button`,role:`radio`,"aria-checked":e.mode===r,className:`btn btn-sm${e.mode===r?` btn-primary`:` btn-ghost`}`,style:{background:e.mode===r?void 0:`transparent`,color:e.mode===r?void 0:`var(--muted)`},disabled:me||n,onClick:n=>{n.stopPropagation(),!(r===`preset`&&e.mode===`custom`&&!confirm(S(`models.presetConfirmReplace`,{count:String(e.presetCount)})))&&Jn(t,r)},children:S(`models.presetMode_${r}`)},r)),e.mode===`custom`&&(0,J.jsx)(`button`,{type:`button`,role:`radio`,"aria-checked":!0,className:`btn btn-sm btn-primary`,disabled:!0,children:S(`models.presetMode_custom`)})]}),e.mode===`preset`&&(0,J.jsx)(`span`,{className:`muted mono text-label`,children:S(`models.presetSummary`,{count:String(e.presetCount),total:String(e.totalCount),version:String(e.availableVersion)})}),r&&(0,J.jsx)(`span`,{className:`badge badge-amber`,role:`status`,children:S(`models.presetUpdateAvailable`,{version:String(e.availableVersion)})})]})})(),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm text-caption`,disabled:me||E,onClick:()=>O(!0),children:S(`models.allOn`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm text-caption`,disabled:me||D,onClick:()=>O(!1),children:S(`models.allOff`)}),(0,J.jsxs)(`div`,{className:`models-cap-cluster`,children:[(0,J.jsx)(Tt,{on:d,onClick:()=>En(t,r),disabled:me,label:S(`models.contextCapLabel`),showLabel:!0}),(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)(Dt,{value:ee[t]?np:String(m),options:[...!g.has(m)&&!ee[t]?[{value:String(m),label:op(m)}]:[],...h.map(e=>({value:String(e),label:op(e)})),{value:np,label:S(`models.custom`)}],onChange:e=>Fn(t,e),disabled:me||!d,label:S(`models.capValue`,{value:op(m)}),title:S(`models.contextCapLabel`)}),d&&ee[t]&&(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)(`input`,{className:`input`,style:{width:120},inputMode:`numeric`,placeholder:S(`models.customPlaceholder`),value:q[t]??``,onChange:e=>Y(n=>({...n,[t]:e.target.value})),onKeyDown:e=>{e.key===`Enter`&&In(t)},disabled:me,"aria-label":S(`models.customPlaceholder`)}),(0,J.jsx)(`button`,{type:`button`,onClick:()=>In(t),disabled:me,className:`btn btn-ghost btn-sm`,children:S(`models.customApply`)})]})]}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm text-caption`,onClick:()=>bn(e),"aria-haspopup":`dialog`,children:S(`models.contextSettings`)})]})]})]}),!o&&(0,J.jsxs)(`div`,{className:`models-provider-body`,children:[r&&(0,J.jsx)(`p`,{className:`muted text-label models-provider-hint`,children:S(`models.nativeHint`)}),!r&&Ee&&(0,J.jsxs)(`div`,{className:`row models-provider-hint`,children:[(0,J.jsx)(`span`,{className:`muted text-label`,children:S(`models.newPolicyProvider`)}),(0,J.jsx)(`div`,{className:`segmented models-segmented`,role:`radiogroup`,"aria-label":S(`models.newPolicyProvider`),children:[`off`,`on`].map(e=>(0,J.jsx)(`button`,{type:`button`,role:`radio`,"aria-checked":(Ee.providers[t]??`inherit`)===e,className:`btn btn-sm${(Ee.providers[t]??`inherit`)===e?` btn-primary`:` btn-ghost`}`,onClick:()=>void qn(e,t),children:S(`models.newPolicy_${e}`)},e))})]}),n.length===0&&(0,J.jsx)(dp,{liveModels:i,discovery:a,showFailureBadge:!1}),n.length>30&&(0,J.jsx)(`input`,{className:`input`,placeholder:S(`models.search`),value:N[t]??``,onChange:e=>P(n=>({...n,[t]:e.target.value})),"aria-label":S(`models.search`)}),C.map(e=>{let n=!s(e);return(0,J.jsxs)(`div`,{className:`model-row-wrap`,onMouseEnter:t=>Qn(e.namespaced,t.currentTarget),onMouseLeave:er,onFocus:t=>$n(e.namespaced,t.currentTarget),onBlur:e=>{e.currentTarget.contains(e.relatedTarget)||$t(null)},children:[(0,J.jsxs)(`div`,{className:`row models-model-row`,children:[(0,J.jsx)(Tt,{on:!n,onClick:()=>void Tn(`models`,t,[{id:e.id,native:e.native===!0}],n),disabled:me,label:e.native?e.id:e.namespaced}),Oe.models[t]?.[e.id]&&(0,J.jsx)(`strong`,{className:`mono text-control`,children:Oe.models[t][e.id].alias}),(0,J.jsx)(`code`,{className:`mono text-control`,style:{color:n?`var(--faint)`:`var(--text)`,textDecoration:n?`line-through`:`none`},children:e.native?fl(e.id):Pn(e.namespaced,S)}),Oe.models[t]?.[e.id]?.source===`builtin`&&(0,J.jsx)(`span`,{className:`models-chip muted text-caption`,children:S(`models.aliasAuto`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,"aria-label":S(`models.editModelAlias`),title:S(`models.editModelAlias`),onClick:()=>void Ze(t,e.id),children:(0,J.jsx)(ge,{style:{width:13,height:13}})}),e.custom&&(0,J.jsx)(`span`,{className:`models-chip muted mono text-caption`,children:S(`models.customBadge`)}),!e.custom&&u.has(e.id)&&(0,J.jsx)(`span`,{className:`badge badge-amber`,children:S(`models.newBadge`)}),e.contextCapped&&(0,J.jsx)(`span`,{className:`models-chip muted mono text-caption`,children:S(`models.contextCappedValue`,{value:op(e.contextCap??z)})})]}),Qt?.namespaced===e.namespaced&&(()=>{let t=Qt.rect,r=t.bottom+4,i=r+360>window.innerHeight;return(0,J.jsxs)(`div`,{className:`model-tip${e.custom?` has-actions`:``}${i?` flip-up`:``}`,role:`tooltip`,style:{position:`fixed`,left:t.left+24,...i?{bottom:window.innerHeight-t.top+4}:{top:r}},onMouseEnter:tr,onMouseLeave:er,children:[(0,J.jsx)(`div`,{className:`model-tip-id`,children:e.native?e.id:e.namespaced}),e.displayName&&(0,J.jsx)(`div`,{className:`model-tip-display`,children:e.displayName}),e.custom&&(0,J.jsx)(`span`,{className:`models-chip models-chip--tip muted mono text-caption`,children:S(`models.customBadge`)}),(0,J.jsxs)(`div`,{className:`model-tip-grid`,children:[(0,J.jsx)(`span`,{className:`model-tip-key`,children:S(`models.tipProvider`)}),(0,J.jsx)(`span`,{className:`model-tip-val`,children:jn(e.provider,S)}),(e.contextWindow||e.contextCap)&&(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)(`span`,{className:`model-tip-key`,children:S(`models.tipContext`)}),(0,J.jsx)(`span`,{className:`model-tip-val`,children:op(e.contextWindow??e.contextCap??0)})]}),e.inputModalities&&e.inputModalities.length>0&&(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)(`span`,{className:`model-tip-key`,children:S(`models.tipModalities`)}),(0,J.jsx)(`span`,{className:`model-tip-val`,children:e.inputModalities.join(`, `)})]}),(0,J.jsx)(`span`,{className:`model-tip-key`,children:S(`models.tipStatus`)}),(0,J.jsx)(`span`,{className:`model-tip-val`,children:S(n?`models.tipDisabled`:`models.tipActive`)})]}),e.custom&&e.customId&&(0,J.jsxs)(`div`,{className:`model-tip-actions`,children:[(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm text-caption`,onClick:()=>{et(`edit`),nt(e.provider),it(e.customId),ot(e.id),ct(e.displayName??``),ut(e.contextWindow?String(e.contextWindow):``),ft(!1),mt(e.inputModalities??[`text`]),gt(Array.isArray(e.reasoningEfforts)),vt(e.reasoningEfforts??[]),yt.current=Array.isArray(e.reasoningEfforts),Ct(``),Je(!0),$t(null)},children:S(`models.customEdit`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm text-caption`,style:{color:`var(--red)`},onClick:()=>{window.confirm(S(`models.customDeleteConfirm`,{name:e.displayName??e.id}))&&ir(e.customId),$t(null)},children:S(`models.customDelete`)})]})]})})()]},e.namespaced)}),w>0&&(0,J.jsx)(`button`,{type:`button`,onClick:()=>I(e=>({...e,[t]:x+60})),className:`btn btn-ghost btn-sm models-show-more`,children:S(`models.showMore`,{n:w})})]})]},t)},ur=on?vn.filter(e=>e.provider===on):vn,dr=(0,J.jsxs)(J.Fragment,{children:[(0,J.jsxs)(`div`,{className:`models-control-top-row`,children:[Ee&&(0,J.jsxs)(`div`,{className:`models-shadow-row row muted text-control`,children:[(0,J.jsx)(`span`,{className:`models-shadow-label`,children:S(`models.newPolicyGlobal`)}),(0,J.jsx)(Tt,{on:Ee.policy===`off`,onClick:()=>void qn(Ee.policy===`off`?`on`:`off`),label:S(`models.newPolicyGlobal`)})]}),(0,J.jsxs)(`div`,{className:`row`,children:[(0,J.jsx)(Tt,{on:Oe.defaults.global,onClick:()=>void Qe(!Oe.defaults.global),label:S(`models.useDefaultAliasesGlobal`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:()=>je(e=>!e),children:S(`models.aliases`)})]}),(0,J.jsxs)(`div`,{className:`models-shadow-row row muted text-control`,"aria-busy":!tn||void 0,children:[(0,J.jsxs)(`span`,{className:`models-shadow-label`,children:[S(`models.shadowCallIntercept`),` `,(0,J.jsx)(kt,{content:S(`models.shadowCallInterceptHint`,{models:Rt(tn?.sourceModels)}),side:`top`,maxWidth:320,children:(0,J.jsx)(`span`,{style:{cursor:`help`},"aria-label":S(`models.shadowCallInterceptHint`,{models:Rt(tn?.sourceModels)}),children:`ⓘ`})})]}),(0,J.jsx)(`code`,{className:`text-caption models-shadow-warning`,style:{opacity:.6},children:S(`models.shadowCallOriginal`,{models:zt(tn?.sourceModels)})}),(0,J.jsx)(Tt,{on:tn?.enabled??!1,onClick:()=>void Bn({enabled:!tn?.enabled}),disabled:!tn||rn,label:S(`models.shadowCallIntercept`)}),(0,J.jsx)(`div`,{className:`models-shadow-model-slot`,children:(0,J.jsx)(Dt,{value:tn?.model??``,options:ln,onChange:e=>{nn(t=>t&&{...t,model:e}),Bn({model:e})},disabled:!tn||rn||!tn.enabled,label:S(`models.shadowCallIntercept`)})})]}),(Pe||xe)&&(0,J.jsxs)(`div`,{className:`models-v2-mode-row row`,children:[(0,J.jsx)(`span`,{className:`muted text-control`,children:S(`models.v2Label`)}),(0,J.jsx)(`div`,{className:`segmented models-segmented`,role:`radiogroup`,"aria-label":S(`models.v2Label`),children:[`v1`,`default`,`v2`].map(e=>(0,J.jsx)(`button`,{type:`button`,role:`radio`,"aria-checked":(xe?.multiAgentMode??`default`)===e,className:`btn btn-sm${(xe?.multiAgentMode??`default`)===e?` btn-primary`:` btn-ghost`}`,style:{background:(xe?.multiAgentMode??`default`)===e?void 0:`transparent`,color:(xe?.multiAgentMode??`default`)===e?void 0:`var(--muted)`},disabled:!xe||Ie,onClick:()=>void Un(e),children:S(`models.v2Mode_${e}`)},e))}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,style:{width:24,height:24,minWidth:24,flex:`0 0 24px`,padding:0,borderRadius:`var(--radius-pill)`,color:`var(--muted)`},disabled:!xe,onClick:()=>Ke(!0),"aria-label":S(`models.v2Label`),"aria-haspopup":`dialog`,children:(0,J.jsx)(Z,{width:14,height:14,"aria-hidden":`true`})})]}),xe&&xe.multiAgentMode===`v2`&&(0,J.jsx)(`div`,{className:`models-v2-keep-native-row`,children:(0,J.jsxs)(`div`,{className:`models-v2-keep-native`,children:[(0,J.jsx)(`span`,{className:`models-v2-keep-native-label text-caption`,children:S(`models.keepNativeOnV1`)}),(0,J.jsx)(Tt,{on:xe.keepNativeChatGptOnV1===!0,onClick:()=>void Yn(!xe.keepNativeChatGptOnV1),disabled:Ie,label:S(`models.keepNativeOnV1`)}),(0,J.jsx)(kt,{content:S(`models.keepNativeOnV1Hint`),side:`top`,maxWidth:360,children:(0,J.jsx)(`span`,{className:`models-v2-keep-native-info`,"aria-label":S(`models.keepNativeOnV1Hint`),children:(0,J.jsx)(Z,{width:13,height:13,"aria-hidden":`true`})})})]})})]}),xe&&(xe.enabled||xe.agentsMaxThreadsConflict||Re)&&(0,J.jsxs)(`div`,{className:`models-v2-detail-row row`,children:[xe.enabled&&(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)(`span`,{className:`muted text-control`,children:S(`models.v2ThreadsLabel`)}),(0,J.jsx)(Dt,{value:Ue?`custom`:xe.maxConcurrentThreadsPerSession!==null&&xe.maxConcurrentThreadsPerSession!==void 0?ip.has(xe.maxConcurrentThreadsPerSession)?String(xe.maxConcurrentThreadsPerSession):`custom`:``,options:[...xe.maxConcurrentThreadsPerSession===null||xe.maxConcurrentThreadsPerSession===void 0?[{value:``,label:S(`models.v2ThreadsDefault`)}]:[],...xe.maxConcurrentThreadsPerSession!==null&&xe.maxConcurrentThreadsPerSession!==void 0&&!ip.has(xe.maxConcurrentThreadsPerSession)&&!Ue?[{value:`custom`,label:String(xe.maxConcurrentThreadsPerSession)}]:[],...rp.map(e=>({value:String(e),label:String(e)})),{value:`custom`,label:S(`models.custom`)}],onChange:e=>Zn(e),disabled:Ie,label:S(`models.v2ThreadsLabel`)}),Ue&&(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)(`input`,{className:`input`,style:{width:100},inputMode:`numeric`,value:Ve,onChange:e=>He(e.target.value),onKeyDown:e=>{e.key===`Enter`&&Xn(Number(Ve.replace(/[_,\s]/g,``)))},disabled:Ie,"aria-label":S(`models.v2ThreadsLabel`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-sm`,disabled:Ie,onClick:()=>{Xn(Number(Ve.replace(/[_,\s]/g,``)))},children:S(`models.v2ThreadsApply`)})]})]}),xe.enabled&&xe.agentsMaxThreadsConflict&&(0,J.jsx)(`span`,{className:`mono text-label`,style:{color:`var(--err, #e5484d)`},children:S(`models.v2Conflict`)}),Re&&(0,J.jsx)(`span`,{className:`muted text-label`,children:Re})]}),(0,J.jsxs)(`div`,{className:`row models-cap-row`,children:[(0,J.jsx)(`span`,{className:`muted text-control`,children:S(`models.contextCapLabel`)}),(0,J.jsx)(Dt,{value:U?np:String(z),options:[...!Zf.has(z)&&!U?[{value:String(z),label:op(z)}]:[],...Xf.map(e=>({value:String(e),label:op(e)})),{value:np,label:S(`models.custom`)}],onChange:e=>Ln(e),disabled:me,label:S(`models.contextCapLabel`)}),U&&(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)(`input`,{className:`input`,style:{width:160},inputMode:`numeric`,placeholder:S(`models.customPlaceholder`),value:V,onChange:e=>H(e.target.value),onKeyDown:e=>{e.key===`Enter`&&Rn()},disabled:me,"aria-label":S(`models.customPlaceholder`)}),(0,J.jsx)(`button`,{type:`button`,onClick:Rn,disabled:me,className:`btn btn-ghost btn-sm`,children:S(`models.customApply`)})]}),(0,J.jsx)(Tt,{on:An,onClick:zn,disabled:me,label:S(`models.setAll`)}),(0,J.jsx)(`span`,{className:`muted text-label leading-body`,children:S(`models.setAllHint`,{value:op(z)})})]}),(()=>{let e=T.filter(e=>e.custom).length;return e===0?null:(0,J.jsx)(`div`,{className:`row muted text-label models-custom-summary`,children:(0,J.jsx)(`span`,{className:`models-chip mono text-caption`,children:S(`models.customSummary`,{count:e})})})})(),(0,J.jsxs)(`div`,{className:`row muted text-label leading-body models-order-hint`,children:[(0,J.jsx)(Z,{width:15,height:15,"aria-hidden":`true`}),(0,J.jsx)(`span`,{children:S(`models.orderHint`)})]})]}),fr=(0,J.jsxs)(`div`,{className:`row models-collapse-controls`,children:[(0,J.jsxs)(`button`,{type:`button`,className:`btn btn-ghost btn-sm text-caption`,onClick:()=>On(!0),disabled:me,children:[(0,J.jsx)(Se,{width:12,height:12,"aria-hidden":`true`}),` `,S(`models.collapseAll`)]}),(0,J.jsxs)(`button`,{type:`button`,className:`btn btn-ghost btn-sm text-caption`,onClick:()=>On(!1),disabled:me,children:[(0,J.jsx)(Se,{width:12,height:12,"aria-hidden":`true`,style:{transform:`rotate(90deg)`}}),` `,S(`models.expandAll`)]})]}),pr=(0,J.jsx)(J.Fragment,{children:vn.length===0&&(0,J.jsx)(Ot,{icon:(0,J.jsx)(re,{}),title:S(`models.noRouted`),children:S(`models.noRoutedHint`)})}),mr=(0,J.jsxs)(J.Fragment,{children:[Ge&&(0,J.jsx)(`div`,{className:`modal-overlay`,role:`dialog`,"aria-modal":`true`,"aria-label":S(`models.v2Label`),onClick:()=>Ke(!1),onKeyDown:e=>{e.key===`Escape`&&Ke(!1)},children:(0,J.jsxs)(`div`,{className:`modal-card`,onClick:e=>e.stopPropagation(),children:[(0,J.jsxs)(`div`,{className:`modal-head`,children:[(0,J.jsx)(`h3`,{children:S(`models.v2Label`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:()=>Ke(!1),"aria-label":S(`common.close`),children:`×`})]}),(0,J.jsx)(`div`,{className:`modal-desc leading-relaxed`,style:{whiteSpace:`pre-line`},children:S(`models.v2Help`)}),(0,J.jsx)(`div`,{className:`models-help-link`,children:(0,J.jsx)(`a`,{className:`text-control`,href:`https://opencodex.me/guides/sub-agent-surface/`,target:`_blank`,rel:`noreferrer`,style:{color:`var(--accent)`},children:S(`models.v2DocsLink`)})}),(0,J.jsx)(`div`,{className:`modal-actions`,children:(0,J.jsx)(`button`,{type:`button`,className:`btn btn-primary`,onClick:()=>Ke(!1),children:S(`common.ok`)})})]})}),wt&&(0,J.jsx)(`div`,{className:`modal-overlay`,role:`dialog`,"aria-modal":`true`,"aria-label":S(`models.contextSettings`),onClick:()=>{Jt||Et(null)},onKeyDown:e=>{e.key===`Escape`&&!Jt&&Et(null)},children:(0,J.jsxs)(`div`,{className:`modal-card`,onClick:e=>e.stopPropagation(),children:[(0,J.jsxs)(`div`,{className:`modal-head`,children:[(0,J.jsx)(`h3`,{children:S(`models.contextSettingsTitle`,{provider:jn(wt,S)})}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:()=>Et(null),disabled:Jt,"aria-label":S(`common.close`),children:`×`})]}),Xt&&(0,J.jsx)($,{tone:`err`,children:Xt}),(0,J.jsx)(`p`,{className:`modal-desc leading-relaxed`,children:S(`models.contextHint`)}),(0,J.jsxs)(`div`,{className:`models-context-fields`,children:[(0,J.jsxs)(`label`,{className:`text-label models-field`,children:[S(`models.contextDefault`),(0,J.jsx)(`input`,{className:`input`,inputMode:`numeric`,value:It,onChange:e=>{Lt(e.target.value),qt(!0)},disabled:Jt,placeholder:S(`models.contextAutomatic`),autoFocus:!0})]}),At.length>0&&(0,J.jsxs)(J.Fragment,{children:[(0,J.jsxs)(`div`,{className:`text-label models-field`,children:[S(`models.contextModel`),(0,J.jsx)(Dt,{value:Mt,options:At.map(e=>({value:e,label:e})),onChange:xn,disabled:Jt,label:S(`models.contextModel`)})]}),(0,J.jsxs)(`label`,{className:`text-label models-field`,children:[S(`models.contextModelOverride`),(0,J.jsx)(`input`,{className:`input`,inputMode:`numeric`,value:Bt[Mt]??``,onChange:e=>{Vt(t=>({...t,[Mt]:e.target.value})),Gt(e=>new Set(e).add(Mt))},disabled:Jt,placeholder:S(`models.contextAutomatic`)})]})]})]}),(0,J.jsxs)(`div`,{className:`modal-actions`,children:[(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost`,onClick:()=>Et(null),disabled:Jt,children:S(`common.cancel`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-primary`,onClick:()=>void Sn(),disabled:Jt,children:S(Jt?`models.customSaving`:`models.customApply`)})]})]})}),qe&&(0,J.jsx)(`div`,{className:`modal-overlay`,role:`dialog`,"aria-modal":`true`,"aria-label":S(`models.customAdd`),onClick:()=>{bt||Je(!1)},onKeyDown:e=>{e.key===`Escape`&&!bt&&Je(!1)},children:(0,J.jsxs)(`div`,{className:`modal-card`,onClick:e=>e.stopPropagation(),children:[(0,J.jsxs)(`div`,{className:`modal-head`,children:[(0,J.jsx)(`h3`,{children:S($e===`add`?`models.customAddTitle`:`models.customEditTitle`,{provider:jn(tt,S)})}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:()=>Je(!1),disabled:bt,"aria-label":S(`common.close`),children:`×`})]}),St&&(0,J.jsx)($,{tone:`err`,children:St}),(0,J.jsxs)(`div`,{className:`models-field-stack`,children:[(0,J.jsxs)(`label`,{className:`text-label models-field`,children:[S(`models.customFieldModelId`),(0,J.jsx)(`input`,{className:`input`,value:at,onChange:e=>ot(e.target.value),disabled:bt,placeholder:S(`models.customFieldModelIdPlaceholder`),autoFocus:!0})]}),(0,J.jsxs)(`label`,{className:`text-label models-field`,children:[S(`models.customFieldDisplayName`),(0,J.jsx)(`input`,{className:`input`,value:st,onChange:e=>ct(e.target.value),disabled:bt,placeholder:S(`models.customFieldDisplayNamePlaceholder`)})]}),(0,J.jsxs)(`label`,{className:`text-label models-field`,children:[S(`models.customFieldContext`),(0,J.jsxs)(`div`,{className:`row models-field-row`,children:[(0,J.jsx)(Dt,{value:dt?`custom`:lt,options:[{value:``,label:`—`},{value:`100000`,label:`100k`},{value:`128000`,label:`128k`},{value:`200000`,label:`200k`},{value:`256000`,label:`256k`},{value:`352000`,label:`352k`},{value:`500000`,label:`500k`},{value:`1000000`,label:`1M`},{value:`custom`,label:S(`models.custom`)}],onChange:e=>{if(e===`custom`){ft(!0);return}ft(!1),ut(e)},disabled:bt,label:S(`models.customFieldContext`)}),dt&&(0,J.jsx)(`input`,{className:`input`,style:{width:120},inputMode:`numeric`,value:lt,onChange:e=>ut(e.target.value),disabled:bt,placeholder:S(`models.customPlaceholder`),"aria-label":S(`models.customFieldContext`)})]})]}),(0,J.jsxs)(`div`,{className:`text-label models-field`,children:[S(`models.customFieldModalities`),(0,J.jsx)(`div`,{className:`row models-field-row`,children:[`text`,`image`,`audio`].map(e=>(0,J.jsxs)(`label`,{className:`row models-modality-option`,children:[(0,J.jsx)(`input`,{type:`checkbox`,checked:pt.includes(e),onChange:t=>{mt(n=>t.target.checked?[...n,e]:n.filter(t=>t!==e))},disabled:bt}),(0,J.jsx)(`span`,{className:`text-control`,children:e})]},e))})]}),(0,J.jsxs)(`div`,{className:`text-label models-field`,children:[S(`models.customFieldReasoning`),(0,J.jsx)(`div`,{className:`row models-field-row`,children:(0,J.jsxs)(`label`,{className:`row models-modality-option`,children:[(0,J.jsx)(`input`,{type:`checkbox`,checked:ht,onChange:e=>{if(gt(e.target.checked),e.target.checked&&!yt.current){yt.current=!0;let e=T.find(e=>e.provider===tt&&e.id===at),t=Array.isArray(e?.reasoningEfforts)?e.reasoningEfforts:void 0;vt(t??[...Yf])}},disabled:bt}),(0,J.jsx)(`span`,{className:`text-control`,children:S(`models.customFieldReasoningOverride`)})]})}),ht&&(0,J.jsx)(`div`,{className:`row models-field-row`,style:{flexWrap:`wrap`},children:Yf.map(e=>(0,J.jsxs)(`label`,{className:`row models-modality-option`,children:[(0,J.jsx)(`input`,{type:`checkbox`,checked:_t.includes(e),onChange:t=>{vt(n=>t.target.checked?[...n,e]:n.filter(t=>t!==e))},disabled:bt}),(0,J.jsx)(`span`,{className:`text-control`,children:S(`models.reasoningEffort.${e}`)})]},e))})]})]}),(0,J.jsxs)(`div`,{className:`modal-actions`,children:[(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost`,onClick:()=>Je(!1),disabled:bt,children:S(`common.cancel`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-primary`,disabled:bt||!at.trim(),onClick:()=>{let e=at.trim(),t=st.trim(),n=lt?Number(lt.replace(/[_,\s]/g,``)):void 0,r=n&&n>0?Math.floor(n):void 0;if($e===`add`){let n=ht?_t:void 0;nr(tt,e,t||void 0,r,pt.length>0?pt:void 0,n)}else rr(rt,{modelId:e,displayName:t,contextWindow:r??null,inputModalities:pt,reasoningEfforts:ht?_t:null})},children:S(bt?`models.customSaving`:$e===`add`?`models.customAddBtn`:`models.customEditBtn`)})]})]})})]}),hr=(0,J.jsxs)(`div`,{className:`models-workspace-shell`,children:[oe&&(0,J.jsxs)(`div`,{className:`action-toast notice ${ce?`notice-ok`:`notice-err`}`,role:`status`,"aria-live":`polite`,children:[ce?(0,J.jsx)(ue,{}):(0,J.jsx)(_e,{}),(0,J.jsx)(`span`,{children:oe})]}),gn.showError&&(0,J.jsx)($,{tone:`err`,children:S(`models.loadFail`)}),(0,J.jsxs)(`div`,{className:`models-workspace-root`,"aria-busy":gn.refreshing||void 0,children:[(0,J.jsxs)(`aside`,{className:`models-workspace-rail`,"aria-label":S(`nav.models`),children:[(0,J.jsxs)(`div`,{className:`models-workspace-rail-header`,children:[(0,J.jsx)(`span`,{className:`models-workspace-rail-title`,children:S(`models.workspace.providers`)}),(0,J.jsx)(`span`,{className:`models-workspace-rail-count`,children:vn.length})]}),(0,J.jsxs)(`div`,{className:`models-workspace-rail-list`,children:[(0,J.jsxs)(`button`,{type:`button`,className:`models-workspace-rail-row${on===null?` models-workspace-rail-row--selected`:``}`,onClick:()=>sn(null),"aria-current":on===null?`true`:void 0,children:[(0,J.jsx)(`span`,{className:`models-workspace-rail-name`,children:S(`models.workspace.allProviders`)}),(0,J.jsx)(`span`,{className:`models-workspace-rail-meta`,children:S(`models.active`,{active:Cn,total:T.length})})]}),vn.map(e=>{let{provider:t,rows:n}=e,r=n.filter(e=>Gf(cr,t,e.id,e.native===!0,k.has(e.namespaced))).length;return(0,J.jsxs)(`button`,{type:`button`,className:`models-workspace-rail-row${on===t?` models-workspace-rail-row--selected`:``}`,onClick:()=>sn(t),"aria-current":on===t?`true`:void 0,children:[(0,J.jsx)(`span`,{className:`models-workspace-rail-name`,children:jn(t,S)}),(0,J.jsx)(`span`,{className:`models-workspace-rail-meta`,children:S(`models.active`,{active:r,total:n.length})})]},t)})]})]}),(0,J.jsxs)(`section`,{className:`models-workspace-main`,"aria-label":S(`models.workspace.mainAria`),children:[dr,fr,Ae&&(0,J.jsxs)(`div`,{className:`card`,"aria-label":S(`models.aliasesTable`),children:[(0,J.jsx)(`div`,{className:`row group-head`,children:(0,J.jsx)(`strong`,{children:S(`models.aliases`)})}),Object.entries(Oe.models).flatMap(([e,t])=>Object.entries(t).map(([t,n])=>(0,J.jsxs)(`div`,{className:`row models-model-row`,children:[(0,J.jsxs)(`code`,{className:`mono text-caption`,style:{flex:1},children:[e,`/`,t]}),(0,J.jsx)(`strong`,{className:`mono text-control`,children:n.alias}),(0,J.jsx)(`span`,{className:`models-chip muted text-caption`,children:n.source===`builtin`?S(`models.aliasAuto`):S(`models.aliasUser`)}),n.stale&&(0,J.jsx)(`span`,{className:`badge badge-amber`,children:S(`models.aliasStale`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,"aria-label":S(`models.editModelAlias`),onClick:()=>void Ze(e,t),children:(0,J.jsx)(ge,{style:{width:13,height:13}})})]},`${e}/${t}`)))]}),(0,J.jsx)(`div`,{className:`models-provider-list`,children:ur.map(e=>lr(e))}),vn.length===0&&pr]})]}),mr]});return(0,J.jsxs)(J.Fragment,{children:[(0,J.jsxs)(`div`,{className:`page-head`,children:[(0,J.jsx)(`h2`,{children:S(`nav.models`)}),(0,J.jsx)(`div`,{className:`page-head-actions`,children:(0,J.jsx)(`button`,{type:`button`,className:`sidebar-orb`,onClick:()=>{s()},disabled:o,"aria-label":S(o?`dash.codexRestarting`:`dash.codexRestart`),title:S(o?`dash.codexRestarting`:`dash.codexRestart`),children:(0,J.jsx)(pe,{})})})]}),(0,J.jsx)(Yc,{state:n,controller:{restarting:o,restart:s}}),(0,J.jsx)(Rf,{tab:c,onSelect:p,meta:wn}),(0,J.jsx)(`p`,{className:`page-sub`,children:S(fp[c])}),(0,J.jsx)(`div`,{className:`models-tab-panel`,role:`tabpanel`,id:If(`catalog`),"aria-labelledby":Ff(`catalog`),hidden:c!==`catalog`,children:(0,J.jsx)(vl,{pageName:S(`models.tab.catalog`),title:S(`errorBoundary.title`),message:S(`errorBoundary.message`),detailsLabel:S(`errorBoundary.details`),reloadLabel:S(`errorBoundary.reload`),children:sr?(0,J.jsx)(gl,{label:S(`models.loading`),rows:5}):or===null?hr:(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)($,{tone:`err`,children:or}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:()=>hn.refresh(),children:S(`common.retry`)})]})})}),(0,J.jsx)(`div`,{className:`models-tab-panel models-tab-panel--fill`,role:`tabpanel`,id:If(`combos`),"aria-labelledby":Ff(`combos`),hidden:c!==`combos`,children:u.has(`combos`)&&(0,J.jsx)(vl,{pageName:S(`models.tab.combos`),title:S(`errorBoundary.title`),message:S(`errorBoundary.message`),detailsLabel:S(`errorBoundary.details`),reloadLabel:S(`errorBoundary.reload`),children:(0,J.jsx)(Mu,{apiBase:e,active:c===`combos`,onCountChange:g})})}),(0,J.jsx)(`div`,{className:`models-tab-panel`,role:`tabpanel`,id:If(`routing`),"aria-labelledby":Ff(`routing`),hidden:c!==`routing`,children:u.has(`routing`)&&(0,J.jsx)(vl,{pageName:S(`models.tab.routing`),title:S(`errorBoundary.title`),message:S(`errorBoundary.message`),detailsLabel:S(`errorBoundary.details`),reloadLabel:S(`errorBoundary.reload`),children:(0,J.jsx)(_d,{apiBase:e,active:c===`routing`,onCountChange:y})})}),(0,J.jsx)(`div`,{className:`models-tab-panel`,role:`tabpanel`,id:If(`compatibility`),"aria-labelledby":Ff(`compatibility`),hidden:c!==`compatibility`,children:u.has(`compatibility`)&&(0,J.jsx)(vl,{pageName:S(`models.tab.compatibility`),title:S(`errorBoundary.title`),message:S(`errorBoundary.message`),detailsLabel:S(`errorBoundary.details`),reloadLabel:S(`errorBoundary.reload`),children:(0,J.jsx)(Af,{apiBase:e,active:c===`compatibility`,onCountChange:x})})})]})}var hp=`section`;function gp(e,t){return[e,hp,t].join(`-`)}function _p(e){return[e,hp,``].join(`-`)}var vp=1200;function yp({scope:e,items:t,ariaLabel:n}){let[r,i]=(0,_.useState)(t[0]?.id??``),a=(0,_.useRef)(null),o=(0,_.useRef)(null),s=(0,_.useCallback)(()=>{a.current=null,o.current!==null&&(clearTimeout(o.current),o.current=null)},[]),c=(0,_.useCallback)(()=>{s();let n=null,r=1/0;for(let i of t){let t=document.getElementById(gp(e,i.id));if(!t)continue;let a=Math.abs(t.getBoundingClientRect().top-72);a()=>s(),[s]),(0,_.useEffect)(()=>{if(typeof IntersectionObserver>`u`)return;let n=t.map(t=>document.getElementById(gp(e,t.id))).filter(e=>e!==null);if(n.length===0)return;let r=new IntersectionObserver(t=>{let n=a.current;if(n){let r=document.getElementById(gp(e,n));t.some(e=>e.isIntersecting&&e.target===r)&&(s(),i(n));return}let r=t.filter(e=>e.isIntersecting).sort((e,t)=>e.boundingClientRect.top-t.boundingClientRect.top)[0];if(!r)return;let o=r.target.id.slice(_p(e).length);i(e=>e===o?e:o)},{rootMargin:`-72px 0px -60% 0px`,threshold:0});for(let e of n)r.observe(e);return()=>r.disconnect()},[s,t,e]);let l=t=>{let n=document.getElementById(gp(e,t));n&&(a.current=t,o.current!==null&&clearTimeout(o.current),o.current=setTimeout(c,vp),i(t),n.scrollIntoView({behavior:`smooth`,block:`start`}))};return(0,J.jsx)(`div`,{className:`page-tabs section-tabs`,role:`tablist`,"aria-label":n,children:t.map(t=>(0,J.jsxs)(`button`,{type:`button`,role:`tab`,"aria-selected":r===t.id,"aria-controls":gp(e,t.id),tabIndex:r===t.id?0:-1,className:`page-tab${r===t.id?` page-tab--active`:``}`,onClick:()=>l(t.id),children:[t.label,t.meta?(0,J.jsx)(`span`,{className:`section-tab-meta`,children:t.meta}):null]},t.id))})}function bp({model:e,effort:t,efforts:n,available:r,guidanceEnabled:i,syncCodexDefaults:a,saving:o,onSave:s,ultraMode:c,ultraSaving:l,onUltraModeSave:u,ultraLoadFailed:d,onUltraModeRetry:f}){let p=Q(),m=(c.hintText??``).trim().length>0;return(0,J.jsxs)(`div`,{className:`swi-delegation`,children:[d&&(0,J.jsxs)(`div`,{className:`swi-delegation-row`,children:[(0,J.jsxs)(`div`,{className:`setting-copy`,children:[(0,J.jsx)(`div`,{className:`font-semibold`,children:p(`sub.ultraMode`)}),(0,J.jsx)(`div`,{className:`muted setting-hint`,children:p(`sub.ultraModeLoadFail`)})]}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:f,children:p(`common.retry`)})]}),(0,J.jsxs)(`div`,{className:`swi-delegation-row`,children:[(0,J.jsxs)(`div`,{className:`setting-copy`,children:[(0,J.jsx)(`div`,{className:`font-semibold`,children:p(`sub.delegation.model`)}),(0,J.jsx)(`div`,{className:`muted setting-hint`,children:p(`sub.delegation.modelHint`)})]}),(0,J.jsxs)(`div`,{className:`swi-delegation-controls`,children:[(0,J.jsx)(Dt,{value:e,options:[{value:``,label:p(`dash.injectionNone`)},...r.map(e=>({value:e.namespaced,label:Pn(`${e.provider}/${e.model}`,p)}))],onChange:e=>s({model:e||null,effort:t||null}),disabled:o,label:p(`dash.injectionLabel`),align:`right`}),e&&n.length>0&&(0,J.jsx)(Dt,{value:t,options:[{value:``,label:p(`dash.injectionEffortNone`)},...n.map(e=>({value:e,label:e}))],onChange:t=>s({model:e||null,effort:t||null}),disabled:o,label:p(`dash.injectionEffortLabel`),align:`right`})]})]}),(0,J.jsxs)(`div`,{className:`swi-delegation-row`,children:[(0,J.jsxs)(`div`,{className:`setting-copy`,children:[(0,J.jsx)(`div`,{className:`font-semibold`,children:p(`dash.syncCodexSubagentDefaults`)}),(0,J.jsx)(`div`,{className:`muted setting-hint`,children:p(`dash.syncCodexSubagentDefaultsHint`)})]}),(0,J.jsx)(`button`,{type:`button`,className:`switch ${a?`on`:``}`,onClick:()=>s({syncCodexSubagentDefaults:!a}),disabled:o||!e,"aria-label":p(`dash.syncCodexSubagentDefaults`),"aria-pressed":a,children:(0,J.jsx)(`span`,{className:`knob`})})]}),(0,J.jsxs)(`details`,{className:`swi-advanced`,children:[(0,J.jsx)(`summary`,{className:`muted text-label`,children:p(`sub.advanced`)}),(0,J.jsxs)(`div`,{className:`swi-delegation-row`,children:[(0,J.jsxs)(`div`,{className:`setting-copy`,children:[(0,J.jsxs)(`div`,{className:`font-semibold`,style:{display:`inline-flex`,alignItems:`center`,gap:6},children:[p(`models.v2Label`),(0,J.jsxs)(kt,{content:p(`models.v2Help`),side:`top`,maxWidth:380,children:[(0,J.jsx)(Z,{width:13,height:13,"aria-hidden":`true`}),(0,J.jsx)(`span`,{className:`sr-only`,children:p(`models.v2Label`)})]})]}),(0,J.jsx)(`div`,{className:`muted setting-hint`,children:(0,J.jsx)(`a`,{className:`text-control`,href:`https://opencodex.me/guides/sub-agent-surface/`,target:`_blank`,rel:`noreferrer`,style:{color:`var(--accent)`},children:p(`models.v2DocsLink`)})})]}),(0,J.jsx)(`div`,{className:`swi-delegation-controls`,children:(0,J.jsx)(`div`,{className:`segmented models-segmented`,role:`radiogroup`,"aria-label":p(`models.v2Label`),children:[`v1`,`default`,`v2`].map(e=>(0,J.jsx)(`button`,{type:`button`,role:`radio`,"aria-checked":c.multiAgentMode===e,className:`btn btn-sm${c.multiAgentMode===e?` btn-primary`:` btn-ghost`}`,style:{background:c.multiAgentMode===e?void 0:`transparent`,color:c.multiAgentMode===e?void 0:`var(--muted)`},disabled:l||d,onClick:()=>{c.multiAgentMode!==e&&u({multiAgentMode:e})},children:p(`models.v2Mode_${e}`)},e))})})]}),(0,J.jsxs)(`div`,{className:`swi-delegation-row`,children:[(0,J.jsxs)(`div`,{className:`setting-copy`,children:[(0,J.jsx)(`div`,{className:`font-semibold`,children:p(`dash.multiAgentGuidance`)}),(0,J.jsx)(`div`,{className:`muted setting-hint`,children:p(`dash.multiAgentGuidanceHint`)})]}),(0,J.jsx)(`button`,{type:`button`,className:`switch ${i?`on`:``}`,onClick:()=>s({multiAgentGuidanceEnabled:!i}),disabled:o,"aria-label":p(`dash.multiAgentGuidance`),"aria-pressed":i,children:(0,J.jsx)(`span`,{className:`knob`})})]}),(0,J.jsxs)(`div`,{className:`swi-delegation-row`,children:[(0,J.jsxs)(`div`,{className:`setting-copy`,children:[(0,J.jsx)(`div`,{className:`font-semibold`,children:p(`sub.ultraMode`)}),(0,J.jsx)(`div`,{className:`muted setting-hint`,children:p(`sub.ultraModeHint`)})]}),(0,J.jsx)(`button`,{type:`button`,className:`switch ${m?`on`:``}`,onClick:()=>u({multiAgentModeHintText:m?null:Sp}),disabled:o||l||!m&&!c.multiAgentV2Enabled,"aria-label":p(`sub.ultraMode`),"aria-pressed":m,children:(0,J.jsx)(`span`,{className:`knob`})}),!c.multiAgentV2Enabled&&(0,J.jsx)(`div`,{className:`muted setting-hint`,children:p(`sub.ultraModeV2Required`)})]}),m&&(0,J.jsx)(`div`,{className:`swi-delegation-row swi-ultra-mode-editor`,children:(0,J.jsx)(xp,{initialHint:c.hintText??``,disabled:o||l,onSave:u,preset:`Proactive multi-agent delegation is active. Any earlier instruction requiring an explicit user request before spawning sub-agents no longer applies. Delegate independent sub-tasks to sub-agents whenever parallel work would materially improve speed or quality — do not serialize work that can run concurrently. Each sub-agent runs in its own context and can use all available tools; prefer spawning specialists over doing everything yourself. This mode remains active until a later multi-agent mode developer message changes it.`,labels:{text:p(`sub.ultraModeText`),preset:p(`sub.ultraModePreset`),save:p(`common.save`)}},c.hintText)})]})]})}function xp({initialHint:e,disabled:t,onSave:n,preset:r,labels:i}){let[a,o]=(0,_.useState)(e);return(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)(`textarea`,{className:`input swi-ultra-mode-textarea`,value:a,onChange:e=>o(e.target.value),disabled:t,rows:4,"aria-label":i.text}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:()=>o(r),disabled:t,children:i.preset}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-primary btn-sm`,onClick:()=>{a.trim().length!==0&&n({multiAgentModeHintText:a})},disabled:t||a.trim().length===0,children:i.save})]})}var Sp=`Proactive multi-agent delegation is active. Any earlier instruction requiring an explicit user request before spawning sub-agents no longer applies. Delegate independent sub-tasks to sub-agents whenever parallel work would materially improve speed or quality — do not serialize work that can run concurrently. Each sub-agent runs in its own context and can use all available tools; prefer spawning specialists over doing everything yourself. This mode remains active until a later multi-agent mode developer message changes it.`;function Cp({available:e,chosen:t,busy:n=!1,onToggle:r,onMove:i,onSave:a,delegation:o}){let s=Q(),[c,l]=(0,_.useState)(``),u=(0,_.useMemo)(()=>new Set(t),[t]),d=t.length>=5,f=(0,_.useMemo)(()=>{let t=c.trim().toLowerCase();return e.filter(e=>!t||e.toLowerCase().includes(t))},[e,c]),p=(0,_.useMemo)(()=>[{id:`featured`,label:s(`sub.featured`),meta:`${t.length}/5`},{id:`models`,label:s(`sub.models`),meta:String(f.length)},{id:`settings`,label:s(`sub.settings`)}],[s,t.length,f.length]);return(0,J.jsxs)(`div`,{className:`subagents-workspace-shell`,children:[(0,J.jsx)(yp,{scope:`subagents`,items:p,ariaLabel:s(`sub.sections`)}),(0,J.jsxs)(`div`,{className:`subagents-workspace-root`,children:[(0,J.jsxs)(`section`,{id:gp(`subagents`,`featured`),className:`subagents-workspace-section`,"aria-label":s(`sub.featured`),children:[(0,J.jsxs)(`div`,{className:`swi-featured-head`,children:[(0,J.jsx)(`h2`,{className:`swi-featured-title`,children:s(`sub.featured`)}),(0,J.jsxs)(`span`,{className:`swi-featured-count`,children:[t.length,`/`,5]}),(0,J.jsxs)(kt,{content:(0,J.jsx)(ut,{k:`sub.orderHint`,cmd:`spawn_agent`}),side:`bottom`,maxWidth:380,children:[(0,J.jsx)(Z,{width:14,height:14,"aria-hidden":`true`}),(0,J.jsx)(`span`,{className:`sr-only`,children:s(`sub.orderHintAria`)})]})]}),t.length===0?(0,J.jsx)(`div`,{className:`swi-featured-empty`,children:s(`sub.noneSelected`)}):(0,J.jsx)(`div`,{className:`swi-featured-list`,children:t.map((e,a)=>(0,J.jsxs)(`div`,{className:`swi-featured-row`,children:[(0,J.jsx)(`span`,{className:`swi-featured-pos`,children:a+1}),(0,J.jsx)(`span`,{className:`swi-featured-name`,children:fl(e)}),(0,J.jsxs)(`span`,{className:`swi-featured-actions`,children:[(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-icon btn-sm`,onClick:()=>i(a,-1),disabled:n||a===0,"aria-label":s(`sub.moveUp`,{m:e}),children:(0,J.jsx)(ye,{})}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-icon btn-sm`,onClick:()=>i(a,1),disabled:n||a===t.length-1,"aria-label":s(`sub.moveDown`,{m:e}),children:(0,J.jsx)(be,{})}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-icon btn-sm`,onClick:()=>r(e),disabled:n,"aria-label":s(`sub.removeAria`,{m:e}),style:{color:`var(--red)`},children:(0,J.jsx)(de,{})})]})]},e))}),(0,J.jsx)(`div`,{className:`swi-save-row`,children:(0,J.jsx)(`button`,{type:`button`,className:`btn btn-primary`,onClick:a,disabled:n,children:s(`common.save`)})})]}),(0,J.jsxs)(`section`,{id:gp(`subagents`,`models`),className:`subagents-workspace-section`,"aria-label":s(`sub.models`),children:[(0,J.jsxs)(`div`,{className:`swi-featured-head`,children:[(0,J.jsx)(`h2`,{className:`swi-featured-title`,children:s(`sub.models`)}),(0,J.jsx)(`span`,{className:`swi-featured-count`,children:f.length})]}),(0,J.jsxs)(`div`,{className:`swi-picker-box`,children:[(0,J.jsx)(`div`,{className:`subagents-workspace-rail-search`,children:(0,J.jsx)(`input`,{className:`input`,value:c,onChange:e=>l(e.target.value),placeholder:s(`sub.search`),"aria-label":s(`sub.search`)})}),(0,J.jsx)(`div`,{className:`subagents-workspace-rail-list`,children:f.length===0?(0,J.jsx)(`span`,{className:`subagents-workspace-rail-empty`,children:s(`sub.noModels`)}):f.map(e=>{let i=u.has(e),a=i?t.indexOf(e)+1:null,o=!i&&(d||n);return(0,J.jsxs)(`div`,{className:`subagents-workspace-rail-row${i?` subagents-workspace-rail-row--selected`:``}`,children:[(0,J.jsxs)(`span`,{className:`subagents-workspace-rail-row-main`,children:[(0,J.jsx)(`span`,{className:`swi-rail-priority`,children:a??``}),(0,J.jsx)(ie,{className:`swi-rail-icon`,"aria-hidden":`true`}),(0,J.jsx)(`span`,{className:`subagents-workspace-rail-name`,children:fl(e)})]}),(0,J.jsx)(`button`,{type:`button`,className:`subagents-workspace-rail-toggle${i?` subagents-workspace-rail-toggle--on`:``}${o?` subagents-workspace-rail-toggle--disabled`:``}`,onClick:()=>{o||r(e)},disabled:o,"aria-pressed":i,"aria-label":s(i?`sub.workspace.removeFromFeatured`:`sub.workspace.addToFeatured`,{m:e}),title:i?s(`sub.workspace.removeFromFeatured`,{m:e}):d?s(`sub.workspace.featuredFull`):s(`sub.workspace.addToFeatured`,{m:e}),children:i?(0,J.jsx)(ue,{style:{width:14,height:14}}):(0,J.jsx)(fe,{style:{width:14,height:14}})})]},e)})})]})]}),(0,J.jsxs)(`section`,{id:gp(`subagents`,`settings`),className:`subagents-workspace-section`,"aria-label":s(`sub.settings`),children:[(0,J.jsx)(`div`,{className:`swi-featured-head`,children:(0,J.jsx)(`h2`,{className:`swi-featured-title`,children:s(`sub.settings`)})}),(0,J.jsx)(bp,{model:o.model,effort:o.effort,efforts:o.efforts,available:o.available,guidanceEnabled:o.guidanceEnabled,syncCodexDefaults:o.syncCodexDefaults,saving:o.saving,onSave:o.onSave,ultraMode:o.ultraMode,ultraSaving:o.ultraSaving,onUltraModeSave:o.onUltraModeSave,ultraLoadFailed:o.ultraLoadFailed,onUltraModeRetry:o.onUltraModeRetry})]})]})]})}function wp(e){let[t,n]=(0,_.useState)(!1),[r,i]=(0,_.useState)(!1),[a,o]=(0,_.useState)(``),[s,c]=(0,_.useState)(``),[l,u]=(0,_.useState)([]),[d,f]=(0,_.useState)([]),[p,m]=(0,_.useState)(!0),[h,g]=(0,_.useState)(!1),v=(0,_.useCallback)(e=>{let t=kr(e);m(t.multiAgentGuidanceEnabled),g(t.syncCodexSubagentDefaults),o(t.injectionModel),c(t.injectionEffort),Array.isArray(e.efforts)&&u(e.efforts),Array.isArray(e.available)&&f(e.available)},[]);return(0,_.useEffect)(()=>{let t=!1;return(async()=>{try{let n=await Wt(await fetch(`${e}/api/injection-model`));if(t)return;v(n)}catch{}finally{t||n(!0)}})(),()=>{t=!0}},[e,v]),{loaded:t,saving:r,model:a,effort:s,efforts:l,available:d,guidanceEnabled:p,syncCodexDefaults:h,save:(0,_.useCallback)(async t=>{if(!r){i(!0);try{if(!(await fetch(`${e}/api/injection-model`,{method:`PUT`,headers:{"Content-Type":`application/json`},body:JSON.stringify(t)})).ok)throw Error(`injection save failed`);let n=await fetch(`${e}/api/injection-model`);v(await Wt(n))}catch{}finally{i(!1)}}},[e,v,r])}}function Tp(e){return gr(e)}function Ep({apiBase:e}){let t=Q(),n=`ocx.subagents.v1:${e}`,r=Tp(n),[i,a]=(0,_.useState)(()=>r?.chosen??[]),[o,s]=(0,_.useState)(``),[c,l]=(0,_.useState)(!1),[u,d]=(0,_.useState)(!1),f=(0,_.useRef)(!1),p=wp(e),[m,h]=(0,_.useState)({enabled:!1,hintText:null,multiAgentV2Enabled:!1,multiAgentMode:`default`}),[g,v]=(0,_.useState)(!1),[y,b]=(0,_.useState)(!1),x=(0,_.useRef)(0),S=(0,_.useRef)(e);(0,_.useEffect)(()=>{S.current=e,x.current++},[e]);let C=(0,_.useCallback)(async n=>{if(S.current!==e)return!1;let r=++x.current,i=await Pt(await fetch(`${e}/api/v2`,{signal:n}),t(`sub.ultraModeLoadFail`));return!i||n?.aborted||r!==x.current||S.current!==e?!1:(b(!1),h({enabled:i.enabled??!1,hintText:i.multiAgentModeHintText??null,multiAgentV2Enabled:i.enabled===!0&&i.multiAgentMode===`v2`,multiAgentMode:i.multiAgentMode??`default`}),!0)},[e,t]);(0,_.useEffect)(()=>{let e=new AbortController;return(async()=>{await C(e.signal)})().catch(()=>{e.signal.aborted||(l(!1),b(!0),s(t(`sub.ultraModeLoadFail`)))}),()=>{e.abort()}},[C,t]);let w=async n=>{if(g)return;let r=e;v(!0),s(``);try{if(await Pt(await fetch(`${e}/api/v2`,{method:`PUT`,headers:{"Content-Type":`application/json`},body:JSON.stringify(n)}),t(`sub.ultraModeSaveFail`)),S.current!==r||!await C())return;l(!0),s(t(`sub.ultraModeSaved`))}catch(e){if(S.current!==r)return;l(!1),s(e instanceof Error&&e.message?e.message:t(`sub.networkError`))}finally{v(!1)}},T=(0,_.useCallback)(async()=>{try{if(!await C())return;l(!1),s(e=>e===t(`sub.ultraModeLoadFail`)?``:e)}catch{l(!1),b(!0),s(t(`sub.ultraModeLoadFail`))}},[C,t]),E=(0,_.useCallback)(async r=>{let i=await Pt(await fetch(`${e}/api/subagent-models`,{signal:r}),t(`sub.loadFail`));if(!i)throw Error(t(`sub.loadFail`));let o=i.available??[],s=new Set(o),c={available:o,chosen:(i.chosen??[]).filter(e=>s.has(e))};return a(c.chosen),br(n,c),c},[e,n,t]),D=ml(n,[e],E,{isEmpty:()=>!1,initialData:r??void 0}),{state:O}=D,k=D.refresh,A=O.data??r,j=A?.available??[],M=e=>{u||(s(``),a(t=>t.includes(e)?t.filter(t=>t!==e):t.length>=5?t:[...t,e]))},N=(e,t)=>{u||a(n=>{let r=[...n],i=e+t;return i<0||i>=r.length?n:([r[e],r[i]]=[r[i],r[e]],r)})},P=async()=>{if(!(u||f.current)){f.current=!0,d(!0),s(``);try{let r=await Pt(await fetch(`${e}/api/subagent-models`,{method:`PUT`,headers:{"Content-Type":`application/json`},body:JSON.stringify({models:i})}),t(`sub.saveFailed`)),o=r?.applied??i;r?.applied&&a(r.applied),br(n,{available:j,chosen:o}),l(!0),s(t(`sub.saved`,{n:o.length,cmd:`ocx sync`}))}catch(e){l(!1),s(e instanceof Error&&e.message?e.message:t(`sub.networkError`))}finally{f.current=!1,d(!1)}}};if(O.showSkeleton&&!A)return(0,J.jsx)(gl,{label:t(`sub.loading`),rows:4});if(O.kind===`failed-cold`){let e=O.error instanceof Error?O.error.message:t(`sub.loadFail`);return(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)($,{tone:`err`,children:e}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:()=>k(),children:t(`common.retry`)})]})}return(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)(`div`,{className:`page-head`,children:(0,J.jsx)(`h2`,{children:t(`nav.subagents`)})}),o&&(0,J.jsx)($,{tone:c?`ok`:`err`,children:o}),O.showError&&(0,J.jsx)($,{tone:`err`,children:t(`sub.loadFail`)}),(0,J.jsx)(Cp,{available:j,chosen:i,busy:u,onToggle:M,onMove:N,onSave:()=>{P()},delegation:{model:p.model,effort:p.effort,efforts:p.efforts,available:p.available,guidanceEnabled:p.guidanceEnabled,syncCodexDefaults:p.syncCodexDefaults,saving:p.saving,onSave:e=>{p.save(e)},ultraMode:m,ultraSaving:g,onUltraModeSave:e=>{w(e)},ultraLoadFailed:y,onUltraModeRetry:()=>{T()}}})]})}function Dp(e,t,n){let r=Array(e);return new Proxy(r,{get(r,i,a){if(typeof i==`string`){let a=i.charCodeAt(0);if(a>=48&&a<=57){let a=+i;if(Number.isInteger(a)&&a>=0&&ar[t]!==e)?(r=o,i=t(...o),n?.onChange&&!(a&&n.skipInitialOnChange)&&n.onChange(i),a=!1,i):i}return o.updateDeps=e=>{r=e},o}function kp(e,t){if(e===void 0)throw Error(`Unexpected undefined${t?`: ${t}`:``}`);return e}var Ap=(e,t)=>Math.abs(e-t)<1.01,jp=(e,t,n)=>{let r;return function(...i){e.clearTimeout(r),r=e.setTimeout(()=>t.apply(this,i),n)}},Mp,Np=()=>{if(Mp!==void 0)return Mp;if(typeof navigator>`u`)return Mp=!1;if(/iP(hone|od|ad)/.test(navigator.userAgent))return Mp=!0;let e=navigator.maxTouchPoints;return Mp=navigator.platform===`MacIntel`&&e!==void 0&&e>0},Pp=e=>{let{offsetWidth:t,offsetHeight:n}=e;return{width:t,height:n}},Fp=e=>e,Ip=e=>{let t=Math.max(e.startIndex-e.overscan,0),n=Math.min(e.endIndex+e.overscan,e.count-1)-t+1,r=Array(n);for(let e=0;e{let n=e.scrollElement;if(!n)return;let r=e.targetWindow;if(!r)return;let i=e=>{let{width:n,height:r}=e;t({width:Math.round(n),height:Math.round(r)})};if(i(Pp(n)),!r.ResizeObserver)return()=>{};let a=new r.ResizeObserver(t=>{let r=()=>{let e=t[0];if(e?.borderBoxSize){let t=e.borderBoxSize[0];if(t){i({width:t.inlineSize,height:t.blockSize});return}}i(Pp(n))};e.options.useAnimationFrameWithResizeObserver?requestAnimationFrame(r):r()});return a.observe(n,{box:`border-box`}),()=>{a.unobserve(n)}},Rp={passive:!0},zp=typeof window>`u`||`onscrollend`in window,Bp=(e,t,n)=>{let r=e.scrollElement;if(!r)return;let i=e.targetWindow;if(!i)return;let a=e.options.useScrollendEvent&&zp,o=0,s=a?null:jp(i,()=>t(o,!1),e.options.isScrollingResetDelay),c=e=>()=>{o=n(r),s?.(),t(o,e)},l=c(!0),u=c(!1);return r.addEventListener(`scroll`,l,Rp),a&&r.addEventListener(`scrollend`,u,Rp),()=>{r.removeEventListener(`scroll`,l),a&&r.removeEventListener(`scrollend`,u)}},Vp=(e,t)=>Bp(e,t,t=>{let{horizontal:n,isRtl:r}=e.options;return n?t.scrollLeft*(r&&-1||1):t.scrollTop}),Hp=(e,t,n)=>{if(n.options.useCachedMeasurements){let t=n.indexFromElement(e),r=n.options.getItemKey(t);return n.itemSizeCache.get(r)??n.options.estimateSize(t)}if(t?.borderBoxSize){let e=t.borderBoxSize[0];if(e)return Math.round(e[n.options.horizontal?`inlineSize`:`blockSize`])}if(!t){let t=n.indexFromElement(e),r=n.options.getItemKey(t),i=n.itemSizeCache.get(r);if(i!==void 0)return i}return e[n.options.horizontal?`offsetWidth`:`offsetHeight`]},Up=(e,{adjustments:t=0,behavior:n},r)=>{var i,a;(a=(i=r.scrollElement)?.scrollTo)==null||a.call(i,{[r.options.horizontal?`left`:`top`]:e+t,behavior:n})},Wp=class{constructor(e){this.unsubs=[],this.scrollElement=null,this.targetWindow=null,this.isScrolling=!1,this.scrollState=null,this.measurementsCache=[],this._flatMeasurements=null,this.itemSizeCache=new Map,this.itemSizeCacheVersion=0,this.laneAssignments=new Map,this.pendingMin=null,this.prevLanes=void 0,this.lanesChangedFlag=!1,this.lanesSettling=!1,this.pendingScrollAnchor=null,this.scrollRect=null,this.scrollOffset=null,this.scrollDirection=null,this.scrollAdjustments=0,this._iosDeferredAdjustment=0,this._iosTouching=!1,this._iosJustTouchEnded=!1,this._iosTouchEndTimerId=null,this._intendedScrollOffset=null,this.elementsCache=new Map,this.now=()=>{var e;return((e=this.targetWindow?.performance)?.now)?.call(e)??Date.now()},this.observer=(()=>{let e=null,t=()=>e||(!this.targetWindow||!this.targetWindow.ResizeObserver?null:e=new this.targetWindow.ResizeObserver(e=>{e.forEach(e=>{let t=()=>{let t=e.target,n=this.indexFromElement(t);if(!t.isConnected){this.observer.unobserve(t);for(let[e,n]of this.elementsCache)if(n===t){this.elementsCache.delete(e);break}return}this.shouldMeasureDuringScroll(n)&&this.resizeItem(n,this.options.measureElement(t,e,this))};this.options.useAnimationFrameWithResizeObserver?requestAnimationFrame(t):t()})}));return{disconnect:()=>{var n;(n=t())==null||n.disconnect(),e=null},observe:e=>t()?.observe(e,{box:`border-box`}),unobserve:e=>t()?.unobserve(e)}})(),this.range=null,this.setOptions=e=>{let t={debug:!1,initialOffset:0,overscan:1,paddingStart:0,paddingEnd:0,scrollPaddingStart:0,scrollPaddingEnd:0,horizontal:!1,getItemKey:Fp,rangeExtractor:Ip,onChange:()=>{},measureElement:Hp,initialRect:{width:0,height:0},scrollMargin:0,gap:0,indexAttribute:`data-index`,initialMeasurementsCache:[],lanes:1,anchorTo:`start`,followOnAppend:!1,scrollEndThreshold:1,isScrollingResetDelay:150,enabled:!0,isRtl:!1,useScrollendEvent:!1,useAnimationFrameWithResizeObserver:!1,laneAssignmentMode:`estimate`,useCachedMeasurements:!1};for(let n in e){let r=e[n];r!==void 0&&(t[n]=r)}let n=this.options,r=null,i=null,a=!1;if(n!==void 0&&n.enabled&&t.enabled&&t.anchorTo===`end`&&this.scrollElement!==null){let e=n.count,o=t.count,s=this.getMeasurements(),c=e>0?s[0]?.key??n.getItemKey(0):null,l=e>0?s[e-1]?.key??n.getItemKey(e-1):null;if(o!==e||e>0&&o>0&&(t.getItemKey(0)!==c||t.getItemKey(o-1)!==l)){a=!0;let c=e>0?this.getVirtualItemForOffset(this.getScrollOffset())??s[0]:null;c&&(r=[c.key,this.getScrollOffset()-c.start]);let u=t.followOnAppend===!0?`auto`:t.followOnAppend||null;u&&o>e&&this.isAtEnd(n.scrollEndThreshold)&&(e===0||t.getItemKey(o-1)!==l)&&(i=u)}}this.options=t,a&&(this.pendingMin=0,this.itemSizeCacheVersion++);let o=!1,s=0;if(r&&this.scrollOffset!==null){let[e,t]=r,n=this.getMeasurements(),{count:i,getItemKey:a}=this.options,c=0;for(;c{var t,n;(n=(t=this.options).onChange)==null||n.call(t,this,e)},this.maybeNotify=Op(()=>(this.calculateRange(),[this.isScrolling,this.range?this.range.startIndex:null,this.range?this.range.endIndex:null]),e=>{this.notify(e)},{key:!1,debug:()=>this.options.debug,initialDeps:[this.isScrolling,this.range?this.range.startIndex:null,this.range?this.range.endIndex:null]}),this.cleanup=()=>{this.unsubs.filter(Boolean).forEach(e=>e()),this.unsubs=[],this.observer.disconnect(),this.rafId!=null&&this.targetWindow&&(this.targetWindow.cancelAnimationFrame(this.rafId),this.rafId=null),this.scrollState=null,this._iosDeferredAdjustment=0,this._iosTouching=!1,this._iosJustTouchEnded=!1,this.scrollElement=null,this.targetWindow=null},this._didMount=()=>()=>{this.cleanup()},this._willUpdate=()=>{let e=this.options.enabled?this.options.getScrollElement():null;if(this.scrollElement!==e){if(this.cleanup(),!e){this.maybeNotify();return}if(this.scrollElement=e,this.targetWindow=this.scrollElement&&`ownerDocument`in this.scrollElement?this.scrollElement.ownerDocument.defaultView:this.scrollElement?.window??null,this.elementsCache.forEach(e=>{this.observer.observe(e)}),this.unsubs.push(this.options.observeElementRect(this,e=>{this.scrollRect=e,this.maybeNotify()})),this.unsubs.push(this.options.observeElementOffset(this,(e,t)=>{if(t&&this._intendedScrollOffset===null&&e===this.scrollOffset)return;this._intendedScrollOffset!==null&&Math.abs(e-this._intendedScrollOffset)<1.5&&(e=this._intendedScrollOffset),this._intendedScrollOffset=null,this.scrollAdjustments=0;let n=this.getScrollOffset();this.scrollDirection=t?n===e?this.scrollDirection:n{this._iosTouching=!0,this._iosJustTouchEnded=!1,this._iosTouchEndTimerId!==null&&this.targetWindow!=null&&(this.targetWindow.clearTimeout(this._iosTouchEndTimerId),this._iosTouchEndTimerId=null)},n=()=>{this._iosTouching=!1,!(!Np()||this.targetWindow==null)&&(this._iosJustTouchEnded=!0,this._iosTouchEndTimerId=this.targetWindow.setTimeout(()=>{this._iosJustTouchEnded=!1,this._iosTouchEndTimerId=null,this._flushIosDeferredIfReady()},150))};e.addEventListener(`touchstart`,t,Rp),e.addEventListener(`touchend`,n,Rp),this.unsubs.push(()=>{e.removeEventListener(`touchstart`,t),e.removeEventListener(`touchend`,n),this._iosTouchEndTimerId!==null&&this.targetWindow!=null&&(this.targetWindow.clearTimeout(this._iosTouchEndTimerId),this._iosTouchEndTimerId=null)})}this._scrollToOffset(this.getScrollOffset(),{adjustments:void 0,behavior:void 0})}let t=this.pendingScrollAnchor;if(this.pendingScrollAnchor=null,t&&this.scrollElement&&this.options.enabled){let[e,n,r,i]=t;e!==null&&!r&&(Np()&&(this.isScrolling||this._iosTouching||this._iosJustTouchEnded)?i!==0&&(this._iosDeferredAdjustment+=i):this._scrollToOffset(this.getScrollOffset(),{adjustments:void 0,behavior:void 0})),r&&this.scrollToEnd({behavior:r})}},this._flushIosDeferredIfReady=()=>{if(this._iosDeferredAdjustment===0||this.isScrolling||this._iosTouching||this._iosJustTouchEnded)return;let e=this.getScrollOffset(),t=this.getMaxScrollOffset();if(e<0||e>t)return;if(this._iosDeferredAdjustment<0&&e>=t-1){this._iosDeferredAdjustment=0;return}let n=this._iosDeferredAdjustment;this._iosDeferredAdjustment=0,this._scrollToOffset(e,{adjustments:this.scrollAdjustments+=n,behavior:void 0})},this.rafId=null,this.getSize=()=>this.options.enabled?(this.scrollRect=this.scrollRect??this.options.initialRect,this.scrollRect[this.options.horizontal?`width`:`height`]):(this.scrollRect=null,0),this.getScrollOffset=()=>this.options.enabled?(this.scrollOffset=this.scrollOffset??(typeof this.options.initialOffset==`function`?this.options.initialOffset():this.options.initialOffset),this.scrollOffset):(this.scrollOffset=null,0),this.getMeasurementOptions=Op(()=>[this.options.count,this.options.paddingStart,this.options.scrollMargin,this.options.getItemKey,this.options.enabled,this.options.lanes,this.options.laneAssignmentMode,this.options.gap],(e,t,n,r,i,a,o,s)=>(this.prevLanes!==void 0&&this.prevLanes!==a&&(this.lanesChangedFlag=!0),this.prevLanes=a,this.pendingMin=null,{count:e,paddingStart:t,scrollMargin:n,getItemKey:r,enabled:i,lanes:a,laneAssignmentMode:o,gap:s}),{key:!1}),this.getMeasurements=Op(()=>[this.getMeasurementOptions(),this.itemSizeCacheVersion],({count:e,paddingStart:t,scrollMargin:n,getItemKey:r,enabled:i,lanes:a,laneAssignmentMode:o,gap:s},c)=>{let l=this.itemSizeCache;if(!i)return this.measurementsCache=[],this.itemSizeCache.clear(),this.laneAssignments.clear(),[];if(this.laneAssignments.size>e)for(let t of this.laneAssignments.keys())t>=e&&this.laneAssignments.delete(t);this.lanesChangedFlag&&(this.lanesChangedFlag=!1,this.lanesSettling=!0,this.measurementsCache=[],this.itemSizeCache.clear(),this.laneAssignments.clear(),this.pendingMin=null),this.measurementsCache.length===0&&!this.lanesSettling&&(this.measurementsCache=this.options.initialMeasurementsCache,this.measurementsCache.forEach(e=>{this.itemSizeCache.set(e.key,e.size)}));let u=this.lanesSettling?0:this.pendingMin??0;if(this.pendingMin=null,this.lanesSettling&&this.measurementsCache.length===e&&(this.lanesSettling=!1),a===1){let i=e*2,a=this._flatMeasurements;if(!a||a.length0&&e.set(a.subarray(0,u*2)),a=e,this._flatMeasurements=a}let o;if(u===0)o=t+n;else{let e=u-1;o=a[e*2]+a[e*2+1]+s}for(let t=u;t1){u=c;let e=f[u],r=e===void 0?void 0:d[e];h=r?r.end+s:t+n}else if(m===a){let e=0,t=p[0],n=f[0];for(let r=1;rthis.options.debug}),this.calculateRange=Op(()=>[this.getMeasurements(),this.getSize(),this.getScrollOffset(),this.options.lanes],(e,t,n,r)=>e.length===0||t===0?(this.range=null,null):(this.range=qp(e,t,n,r,r===1&&this._flatMeasurements!=null?this._flatMeasurements:null),this.range),{key:!1,debug:()=>this.options.debug}),this.getVirtualIndexes=Op(()=>{let e=null,t=null,n=this.calculateRange();return n&&(e=n.startIndex,t=n.endIndex),this.maybeNotify.updateDeps([this.isScrolling,e,t]),[this.options.rangeExtractor,this.options.overscan,this.options.count,e,t]},(e,t,n,r,i)=>r===null||i===null?[]:e({startIndex:r,endIndex:i,overscan:t,count:n}),{key:!1,debug:()=>this.options.debug}),this.indexFromElement=e=>{let t=this.options.indexAttribute,n=e.getAttribute(t);return n?parseInt(n,10):(console.warn(`Missing attribute name '${t}={index}' on measured element.`),-1)},this.shouldMeasureDuringScroll=e=>{if(!this.scrollState||this.scrollState.behavior!==`smooth`)return!0;let t=this.scrollState.index??this.getVirtualItemForOffset(this.scrollState.lastTargetOffset)?.index;if(t!==void 0&&this.range){let n=Math.max(this.options.overscan,Math.ceil((this.range.endIndex-this.range.startIndex)/2)),r=Math.max(0,t-n),i=Math.min(this.options.count-1,t+n);return e>=r&&e<=i}return!0},this.measureElement=e=>{if(!e){this.elementsCache.forEach((e,t)=>{e.isConnected||(this.observer.unobserve(e),this.elementsCache.delete(t))});return}let t=this.indexFromElement(e),n=this.options.getItemKey(t),r=this.elementsCache.get(n);r!==e&&(r&&this.observer.unobserve(r),this.observer.observe(e),this.elementsCache.set(n,e)),(!this.isScrolling||this.scrollState)&&this.shouldMeasureDuringScroll(t)&&this.resizeItem(t,this.options.measureElement(e,void 0,this))},this.resizeItem=(e,t)=>{if(e<0||e>=this.options.count)return;let n,r,i,a=this._flatMeasurements;if(this.options.lanes===1&&a!==null)i=this.options.getItemKey(e),r=a[e*2],n=a[e*2+1];else{let t=this.measurementsCache[e];if(!t)return;i=t.key,r=t.start,n=t.size}let o=this.itemSizeCache.get(i)??n,s=t-o;if(s!==0){let a=this.options.anchorTo===`end`&&this.scrollState?.behavior!==`smooth`&&this.getVirtualDistanceFromEnd()<=this.options.scrollEndThreshold,c=a?this.getTotalSize():0,l=this.getScrollOffset()+this.scrollAdjustments,u=this.itemSizeCache.has(i)?r+o<=l&&this.scrollDirection!==`backward`:r[this.getVirtualIndexes(),this.getMeasurements()],(e,t)=>{let n=[];for(let r=0,i=e.length;rthis.options.debug}),this.getVirtualItemForOffset=e=>{let t=this.getMeasurements();if(t.length===0)return;let n=this._flatMeasurements,r=this.options.lanes===1&&n!=null;return kp(t[Gp(0,t.length-1,r?e=>n[e*2]:e=>kp(t[e]).start,e)])},this.getMaxScrollOffset=()=>{if(!this.scrollElement)return 0;if(`scrollHeight`in this.scrollElement)return this.options.horizontal?this.scrollElement.scrollWidth-this.scrollElement.clientWidth:this.scrollElement.scrollHeight-this.scrollElement.clientHeight;{let e=this.scrollElement.document.documentElement;return this.options.horizontal?e.scrollWidth-this.scrollElement.innerWidth:e.scrollHeight-this.scrollElement.innerHeight}},this.getVirtualDistanceFromEnd=()=>Math.max(this.getTotalSize()-this.getSize()-this.getScrollOffset(),0),this.getDistanceFromEnd=()=>Math.max(this.getMaxScrollOffset()-this.getScrollOffset(),0),this.isAtEnd=(e=this.options.scrollEndThreshold)=>this.getDistanceFromEnd()<=e,this.getOffsetForAlignment=(e,t,n=0)=>{if(!this.scrollElement)return 0;let r=this.getSize(),i=this.getScrollOffset();t===`auto`&&(t=e>=i+r?`end`:`start`),t===`center`?e+=(n-r)/2:t===`end`&&(e-=r);let a=this.getMaxScrollOffset();return Math.max(Math.min(a,e),0)},this.getOffsetForIndex=(e,t=`auto`)=>{e=Math.max(0,Math.min(e,this.options.count-1));let n=this.getSize(),r=this.getScrollOffset(),i=this.measurementsCache[e];if(!i)return;if(t===`auto`){if(i.end>=r+n-this.options.scrollPaddingEnd)t=`end`;else if(i.start<=r+this.options.scrollPaddingStart)t=`start`;else return[r,t]}if(t===`end`&&e===this.options.count-1)return[this.getMaxScrollOffset(),t];let a=t===`end`?i.end+this.options.scrollPaddingEnd:i.start-this.options.scrollPaddingStart;return[this.getOffsetForAlignment(a,t,i.size),t]},this.scrollToOffset=(e,{align:t=`start`,behavior:n=`auto`}={})=>{this._iosDeferredAdjustment=0;let r=this.getOffsetForAlignment(e,t),i=this.now();this.scrollState={index:null,align:t,behavior:n,startedAt:i,lastTargetOffset:r,stableFrames:0},this._scrollToOffset(r,{adjustments:void 0,behavior:n}),this.scheduleScrollReconcile()},this.scrollToIndex=(e,{align:t=`auto`,behavior:n=`auto`}={})=>{this._iosDeferredAdjustment=0,e=Math.max(0,Math.min(e,this.options.count-1));let r=this.getOffsetForIndex(e,t);if(!r)return;let[i,a]=r,o=this.now();this.scrollState={index:e,align:a,behavior:n,startedAt:o,lastTargetOffset:i,stableFrames:0},this._scrollToOffset(i,{adjustments:void 0,behavior:n}),this.scheduleScrollReconcile()},this.scrollBy=(e,{behavior:t=`auto`}={})=>{let n=this.getScrollOffset()+e,r=this.now();this.scrollState={index:null,align:`start`,behavior:t,startedAt:r,lastTargetOffset:n,stableFrames:0},this._scrollToOffset(n,{adjustments:void 0,behavior:t}),this.scheduleScrollReconcile()},this.scrollToEnd=({behavior:e=`auto`}={})=>{if(this.options.count>0){this.scrollToIndex(this.options.count-1,{align:`end`,behavior:e});return}this.scrollToOffset(Math.max(this.getTotalSize()-this.getSize(),0),{behavior:e})},this.getTotalSize=()=>{let e=this.getMeasurements(),t;if(e.length===0)t=this.options.paddingStart;else if(this.options.lanes===1){let n=e.length-1,r=this._flatMeasurements;t=r==null?e[n]?.end??0:r[n*2]+r[n*2+1]}else{let n=Array(this.options.lanes).fill(null),r=e.length-1;for(;r>=0&&n.some(e=>e===null);){let t=e[r];n[t.lane]===null&&(n[t.lane]=t.end),r--}t=Math.max(...n.filter(e=>e!==null))}return Math.max(t-this.options.scrollMargin+this.options.paddingEnd,0)},this.takeSnapshot=()=>{let e=[];if(this.itemSizeCache.size===0)return e;let t=this.getMeasurements();for(let n of t)n&&this.itemSizeCache.has(n.key)&&e.push({index:n.index,key:n.key,start:n.start,size:n.size,end:n.end,lane:n.lane});return e},this._scrollToOffset=(e,{adjustments:t,behavior:n})=>{this._intendedScrollOffset=e+(t??0),this.options.scrollToFn(e,{behavior:n,adjustments:t},this)},this.measure=()=>{this.pendingMin=null,this.itemSizeCache.clear(),this.laneAssignments.clear(),this.itemSizeCacheVersion++,this.notify(!1)},this.setOptions(e)}applyScrollAdjustment(e,t){return e===0?!1:Np()&&(this.isScrolling||this._iosTouching||this._iosJustTouchEnded)?(this._iosDeferredAdjustment+=e,!1):(this._scrollToOffset(this.getScrollOffset(),{adjustments:this.scrollAdjustments+=e,behavior:t}),this.scrollOffset!==null&&(this.scrollOffset+=this.scrollAdjustments,this.scrollOffset<0&&(this.scrollOffset=0),this.scrollAdjustments=0),!0)}scheduleScrollReconcile(){if(!this.targetWindow){this.scrollState=null;return}this.rafId??=this.targetWindow.requestAnimationFrame(()=>{this.rafId=null,this.reconcileScroll()})}reconcileScroll(){if(!this.scrollState||!this.scrollElement)return;if(this.now()-this.scrollState.startedAt>5e3){this.scrollState=null;return}let e=this.scrollState.index==null?void 0:this.getOffsetForIndex(this.scrollState.index,this.scrollState.align),t=e?e[0]:this.scrollState.lastTargetOffset,n=t!==this.scrollState.lastTargetOffset;if(!n&&Ap(t,this.getScrollOffset())){if(this.scrollState.stableFrames++,this.scrollState.stableFrames>=1){this.getScrollOffset()!==t&&this._scrollToOffset(t,{adjustments:void 0,behavior:`auto`}),this.scrollState=null;return}}else if(this.scrollState.stableFrames=0,n){let e=this.getSize()||600,n=Math.abs(t-this.getScrollOffset()),r=this.scrollState.behavior===`smooth`&&n>e;this.scrollState.lastTargetOffset=t,r||(this.scrollState.behavior=`auto`),this._scrollToOffset(t,{adjustments:void 0,behavior:r?`smooth`:`auto`})}this.scheduleScrollReconcile()}},Gp=(e,t,n,r)=>{for(;e<=t;){let i=(e+t)/2|0,a=n(i);if(ar)t=i-1;else return i}return e>0?e-1:0};function Kp(e,t,n){let r=0;for(;r<=t;){let i=(r+t)/2|0,a=e[i*2];if(an)t=i-1;else return i}return r>0?r-1:0}function qp(e,t,n,r,i){let a=e.length-1;if(e.length<=r)return{startIndex:0,endIndex:a};if(r===1&&i!==null){let e=Kp(i,a,n),r=e,o=n+t;for(;re[t].start,n),s=o;if(r===1)for(;s1){let i=Array(r).fill(0);for(;se=0&&c.some(e=>e>=n);){let t=e[o];c[t.lane]=t.start,o--}o=Math.max(0,o-o%r),s=Math.min(a,s+(r-1-s%r))}return{startIndex:o,endIndex:s}}var Jp=typeof document<`u`?_.useLayoutEffect:_.useEffect;function Yp({useFlushSync:e=!0,directDomUpdates:t=!1,directDomUpdatesMode:n=`transform`,...r}){let i=_.useReducer(e=>e+1,0)[1],a=_.useRef({enabled:t,mode:n,container:null,lastSize:null,lastPositions:new WeakMap,prevRange:null});a.current.enabled=t,a.current.mode=n;let o=e=>{let t=a.current;if(!t.enabled||!t.container)return;let n=e.getTotalSize();if(n!==t.lastSize){t.lastSize=n;let r=e.options.horizontal?`width`:`height`;t.container.style[r]=`${n}px`}},s=e=>{let t=a.current;if(!t.enabled||!t.container)return;o(e);let n=!!e.options.horizontal,r=t.mode===`transform`,i=n?`left`:`top`,s=e.options.scrollMargin,c=e.getVirtualItems();for(let a of c){let o=a.start-s,c=e.elementsCache.get(a.key);c&&t.lastPositions.get(c)!==o&&(t.lastPositions.set(c,o),r?c.style.transform=n?`translate3d(${o}px, 0, 0)`:`translate3d(0, ${o}px, 0)`:c.style[i]=`${o}px`)}},c={...r,onChange:(t,n)=>{var o;let c=a.current,l=!0;if(c.enabled){s(t);let e=t.range,n=c.prevRange;l=!n||n.isScrolling!==t.isScrolling||n.startIndex!==e?.startIndex||n.endIndex!==e?.endIndex,l&&(c.prevRange=e?{startIndex:e.startIndex,endIndex:e.endIndex,isScrolling:t.isScrolling}:null)}l&&(e&&n?(0,mt.flushSync)(i):i()),(o=r.onChange)==null||o.call(r,t,n)}},[l]=_.useState(()=>{let e=new Wp(c);return Object.assign(e,{containerRef:t=>{let n=a.current;if(n.container=t,n.lastSize=null,t&&n.enabled){let r=e.getTotalSize();n.lastSize=r;let i=e.options.horizontal?`width`:`height`;t.style[i]=`${r}px`}}})});return l.setOptions(c),Jp(()=>l._didMount(),[]),Jp(()=>(o(l),l._willUpdate())),Jp(()=>{s(l)}),l}function Xp(e){return Yp({observeElementRect:Lp,observeElementOffset:Vp,scrollToFn:Up,...e})}var Zp=32;function Qp(e){return[...new Uint8Array(e)].map(e=>e.toString(16).padStart(2,`0`)).join(``)}function $p(e){for(let t=0;t4096))return Qp(await crypto.subtle.digest(`SHA-256`,new TextEncoder().encode(t))).slice(0,Zp)}function tm(e,t,n){if(!e)return!1;let r=t.trim();return r?e===r||n!==void 0&&e===n:!1}var nm={400:{en:{label:`Bad request`,description:`The proxy could not understand the request. Check the model, message shape, headers, and JSON body before retrying.`},fr:{label:`Requête incorrecte`,description:`Le proxy n’a pas pu comprendre la requête. Vérifiez le modèle, la structure des messages, les en-têtes et le corps JSON avant de réessayer.`},ko:{label:`잘못된 요청`,description:`프록시가 요청을 이해할 수 없습니다. 재시도 전에 모델, 메시지 형식, 헤더, JSON 본문을 확인해야 합니다.`},zh:{label:`错误请求`,description:`代理无法理解该请求。重试前请检查模型、消息结构、标头和 JSON 正文。`},"zh-TW":{label:`錯誤請求`,description:`代理無法理解該請求。重試前請檢查模型、訊息結構、標頭和 JSON 本文。`},de:{label:`Ungültige Anfrage`,description:`Der Proxy konnte die Anfrage nicht verstehen. Prüfe Modell, Nachrichtenformat, Header und JSON-Body vor einem erneuten Versuch.`},ru:{label:`Некорректный запрос`,description:`Прокси не смог интерпретировать запрос. Перед повторной попыткой проверьте модель, формат сообщений, заголовки и тело JSON.`},ja:{label:`不正なリクエスト`,description:`プロキシがリクエストを解釈できませんでした。再試行前にモデル、メッセージ形式、ヘッダー、JSON 本文を確認してください。`},tr:{label:`Hatalı istek`,description:`Proxy isteği anlayamadı. Yeniden denemeden önce modeli, mesaj yapısını, başlıkları ve JSON gövdesini kontrol edin.`}},401:{en:{label:`Unauthorized`,description:`Credentials are missing, expired, or invalid. Re-login or refresh the account/provider credentials used by opencodex.`},fr:{label:`Non autorisé`,description:`Les identifiants sont absents, expirés ou non valides. Reconnectez-vous ou actualisez les identifiants du compte ou du fournisseur utilisés par opencodex.`},ko:{label:`인증 필요`,description:`자격 증명이 없거나 만료되었거나 유효하지 않습니다. opencodex에서 사용하는 계정 또는 제공자 자격 증명을 다시 로그인하거나 갱신해야 합니다.`},zh:{label:`未授权`,description:`凭据缺失、已过期或无效。请重新登录,或刷新 opencodex 使用的账号/提供商凭据。`},"zh-TW":{label:`未授權`,description:`憑證缺失、已過期或無效。請重新登入,或重新整理 opencodex 使用的帳號/供應商憑證。`},de:{label:`Nicht autorisiert`,description:`Anmeldedaten fehlen, sind abgelaufen oder ungültig. Melde dich erneut an oder aktualisiere die von opencodex genutzten Konto-/Anbieter-Zugangsdaten.`},ru:{label:`Не авторизован`,description:`Учётные данные отсутствуют, истекли или недействительны. Войдите заново или обновите учётные данные аккаунта или провайдера, которые использует opencodex.`},ja:{label:`認証が必要`,description:`認証情報が不在・期限切れ・無効です。opencodex が使用するアカウントまたはプロバイダー認証情報を再ログインまたは更新してください。`},tr:{label:`Yetkisiz erişim`,description:`Kimlik bilgileri eksik, süresi dolmuş veya geçersiz. opencodex tarafından kullanılan hesap veya sağlayıcı kimlik bilgilerini yeniden doğrulayın.`}},402:{en:{label:`Payment required`,description:`The upstream provider rejected the request because billing, credits, or plan access is not available. Add credits, update billing, or switch provider.`},fr:{label:`Paiement requis`,description:`Le fournisseur en amont a rejeté la requête, car la facturation, les crédits ou l’accès à l’offre ne sont pas disponibles. Ajoutez des crédits, mettez à jour la facturation ou changez de fournisseur.`},ko:{label:`결제 필요`,description:`청구, 크레딧, 플랜 접근 권한 문제로 업스트림 제공자가 요청을 거부했습니다. 크레딧 추가, 결제 정보 갱신, 제공자 전환이 필요합니다.`},zh:{label:`需要付款`,description:`上游提供商因账单、额度或套餐权限不可用而拒绝了请求。请充值、更新账单信息或切换提供商。`},"zh-TW":{label:`需要付款`,description:`上游供應商因帳單、額度或方案許可權不可用而拒絕了請求。請儲值、更新帳單資訊或切換供應商。`},de:{label:`Zahlung erforderlich`,description:`Der Upstream-Anbieter hat die Anfrage abgelehnt, weil Abrechnung, Guthaben oder Planzugriff nicht verfügbar ist. Guthaben aufladen, Abrechnung aktualisieren oder Anbieter wechseln.`},ru:{label:`Требуется оплата`,description:`Вышестоящий провайдер отклонил запрос из-за проблем с оплатой, кредитами или доступом по тарифному плану. Пополните баланс, обновите платёжные данные или переключитесь на другого провайдера.`},ja:{label:`支払いが必要`,description:`課金、クレジット、プランアクセスが利用できないため上流プロバイダーがリクエストを拒否しました。クレジット追加、支払い情報更新、プロバイダー切替が必要です。`},tr:{label:`Ödeme gerekli`,description:`Yukarı akış sağlayıcısı faturalandırma, kredi veya plan erişimi bulunmadığından isteği reddetti. Kredi ekleyin, ödeme bilgilerini güncelleyin veya sağlayıcı değiştirin.`}},403:{en:{label:`Forbidden`,description:`The account is authenticated but not allowed to use this model or operation. Often a plan/subscription gate (e.g. Ollama Cloud Pro), org policy, or model permission — not necessarily a bad API key.`},fr:{label:`Accès interdit`,description:`Le compte est authentifié, mais n’est pas autorisé à utiliser ce modèle ou cette opération. Il s’agit souvent d’une restriction liée à l’offre ou à l’abonnement (p. ex. Ollama Cloud Pro), à la politique de l’organisation ou aux autorisations du modèle — pas nécessairement d’une clé API incorrecte.`},ko:{label:`권한 없음`,description:`계정 인증은 되었지만 이 모델 또는 작업을 사용할 권한이 없습니다. 플랜/구독 제한(예: Ollama Cloud Pro), 조직 정책, 모델 권한 문제인 경우가 많으며 API 키가 잘못된 것은 아닐 수 있습니다.`},zh:{label:`禁止访问`,description:`账号已认证,但无权使用此模型或操作。常见原因是套餐/订阅限制(例如 Ollama Cloud Pro)、组织策略或模型权限——不一定是 API 密钥无效。`},"zh-TW":{label:`禁止存取`,description:`帳號已認證,但無權使用此模型或操作。常見原因是方案/訂閱限制(例如 Ollama Cloud Pro)、組織策略或模型許可權——不一定是 API 金鑰無效。`},de:{label:`Verboten`,description:`Das Konto ist authentifiziert, darf dieses Modell oder diese Operation aber nicht nutzen. Oft Plan-/Abo-Sperre (z. B. Ollama Cloud Pro), Organisationsrichtlinie oder Modellrecht — nicht zwingend ein ungültiger API-Key.`},ru:{label:`Доступ запрещён`,description:`Аккаунт аутентифицирован, но не имеет права использовать эту модель или операцию. Часто причина — ограничение тарифа или подписки (например, Ollama Cloud Pro), политика организации или права доступа к модели, а не обязательно неверный API-ключ.`},ja:{label:`アクセス禁止`,description:`アカウントは認証済みですがこのモデルや操作の使用が許可されていません。多くはプラン/サブスクリプション制限(例: Ollama Cloud Pro)、組織ポリシー、モデル権限であり、API キーが不正とは限りません。`},tr:{label:`Erişim yasaklandı`,description:`Hesabın kimliği doğrulandı ancak bu modeli veya işlemi kullanma izni yok. Genellikle plan/abonelik sınırı (örn. Ollama Cloud Pro), organizasyon politikası veya model izni kaynaklıdır.`}},404:{en:{label:`Not found`,description:`The requested route, model, account, or upstream resource was not found. Verify the model name and opencodex provider configuration.`},fr:{label:`Introuvable`,description:`La route, le modèle, le compte ou la ressource en amont demandés sont introuvables. Vérifiez le nom du modèle et la configuration du fournisseur opencodex.`},ko:{label:`찾을 수 없음`,description:`요청한 경로, 모델, 계정 또는 업스트림 리소스를 찾을 수 없습니다. 모델 이름과 opencodex 제공자 설정을 확인해야 합니다.`},zh:{label:`未找到`,description:`找不到请求的路由、模型、账号或上游资源。请确认模型名称和 opencodex 提供商配置。`},"zh-TW":{label:`未找到`,description:`找不到請求的路由、模型、帳號或上游資源。請確認模型名稱和 opencodex 供應商配置。`},de:{label:`Nicht gefunden`,description:`Die angeforderte Route, das Modell, das Konto oder die Upstream-Ressource wurde nicht gefunden. Prüfe Modellname und opencodex-Anbieterkonfiguration.`},ru:{label:`Не найдено`,description:`Запрошенный маршрут, модель, аккаунт или вышестоящий ресурс не найден. Проверьте имя модели и конфигурацию провайдера в opencodex.`},ja:{label:`見つかりません`,description:`要求されたルート、モデル、アカウント、上流リソースが見つかりませんでした。モデル名と opencodex プロバイダー設定を確認してください。`},tr:{label:`Bulunamadı`,description:`İstenen rota, model, hesap veya yukarı akış kaynağı bulunamadı. Model adını ve opencodex sağlayıcı yapılandırmasını doğrulayın.`}},408:{en:{label:`Request timeout`,description:`The request took too long before the proxy or upstream provider could complete it. Retry with a smaller request or a different provider.`},fr:{label:`Délai d’attente de la requête dépassé`,description:`La requête a pris trop de temps pour que le proxy ou le fournisseur en amont puisse la traiter. Réessayez avec une requête plus petite ou un autre fournisseur.`},ko:{label:`요청 시간 초과`,description:`프록시 또는 업스트림 제공자가 요청을 완료하기 전에 시간이 초과되었습니다. 더 작은 요청으로 재시도하거나 다른 제공자로 전환해야 합니다.`},zh:{label:`请求超时`,description:`代理或上游提供商未能在限定时间内完成请求。请缩小请求后重试,或切换提供商。`},"zh-TW":{label:`請求逾時`,description:`代理或上游供應商未能在限定時間內完成請求。請縮小請求後重試,或切換供應商。`},de:{label:`Anfrage-Timeout`,description:`Die Anfrage dauerte zu lange, bevor Proxy oder Upstream-Anbieter sie abschließen konnten. Mit kleinerer Anfrage oder anderem Anbieter erneut versuchen.`},ru:{label:`Тайм-аут запроса`,description:`Обработка запроса заняла слишком много времени, и прокси или вышестоящий провайдер не успел её завершить. Повторите попытку с меньшим запросом или через другого провайдера.`},ja:{label:`リクエストタイムアウト`,description:`プロキシまたは上流プロバイダーがリクエストを完了する前に時間切れになりました。より小さいリクエストで再試行するか、別のプロバイダーに切り替えてください。`},tr:{label:`İstek zaman aşımı`,description:`Proxy veya yukarı akış sağlayıcısı isteği tamamlayamadan zaman aşımına uğradı. Daha küçük bir istek veya farklı bir sağlayıcı ile tekrar deneyin.`}},409:{en:{label:`Conflict`,description:`The request conflicts with the current account, session, or provider state. Refresh the session or retry after the active operation finishes.`},fr:{label:`Conflit`,description:`La requête entre en conflit avec l’état actuel du compte, de la session ou du fournisseur. Actualisez la session ou réessayez une fois l’opération en cours terminée.`},ko:{label:`상태 충돌`,description:`요청이 현재 계정, 세션 또는 제공자 상태와 충돌합니다. 세션을 갱신하거나 진행 중인 작업이 끝난 뒤 재시도해야 합니다.`},zh:{label:`状态冲突`,description:`请求与当前账号、会话或提供商状态冲突。请刷新会话,或等待当前操作完成后重试。`},"zh-TW":{label:`狀態衝突`,description:`請求與當前帳號、會話或供應商狀態衝突。請重新整理會話,或等待當前操作完成後重試。`},de:{label:`Konflikt`,description:`Die Anfrage kollidiert mit dem aktuellen Konto-, Sitzungs- oder Anbieterstatus. Sitzung aktualisieren oder nach Abschluss der laufenden Operation erneut versuchen.`},ru:{label:`Конфликт`,description:`Запрос конфликтует с текущим состоянием аккаунта, сессии или провайдера. Обновите сессию или повторите попытку после завершения текущей операции.`},ja:{label:`状態の衝突`,description:`リクエストが現在のアカウント、セッション、プロバイダー状態と衝突しています。セッションを更新するか、進行中の操作が終わった後に再試行してください。`},tr:{label:`Durum çakışması`,description:`İstek mevcut hesap, oturum veya sağlayıcı durumuyla çakışıyor. Oturumu yenileyin veya aktif işlem bittikten sonra tekrar deneyin.`}},413:{en:{label:`Request too large`,description:`The prompt, attachments, or generated payload exceeds a proxy or upstream limit. Reduce tokens, file size, or conversation history.`},fr:{label:`Requête trop volumineuse`,description:`L’invite, les pièces jointes ou la charge utile générée dépassent une limite du proxy ou du fournisseur en amont. Réduisez le nombre de jetons, la taille des fichiers ou l’historique de la conversation.`},ko:{label:`요청 과대`,description:`프롬프트, 첨부 파일 또는 생성 페이로드가 프록시나 업스트림 한도를 초과했습니다. 토큰, 파일 크기, 대화 기록을 줄여야 합니다.`},zh:{label:`请求过大`,description:`提示、附件或生成的负载超过了代理或上游限制。请减少 token、文件大小或对话历史。`},"zh-TW":{label:`請求過大`,description:`提示、附件或生成的負載超過了代理或上游限制。請減少 token、檔案大小或對話歷史。`},de:{label:`Anfrage zu groß`,description:`Prompt, Anhänge oder generierte Nutzlast überschreiten ein Proxy- oder Upstream-Limit. Tokens, Dateigröße oder Verlauf reduzieren.`},ru:{label:`Слишком большой запрос`,description:`Промпт, вложения или сформированная полезная нагрузка превышают лимит прокси или вышестоящего провайдера. Сократите количество токенов, размер файлов или историю диалога.`},ja:{label:`リクエストが大きすぎます`,description:`プロンプト、添付ファイル、生成ペイロードがプロキシまたは上流の制限を超えました。トークン、ファイルサイズ、会話履歴を減らしてください。`},tr:{label:`İstek çok büyük`,description:`İstemi, ekler veya oluşturulan veri proxy ya da yukarı akış sınırını aşıyor. Jeton sayısını, dosya boyutunu veya sohbet geçmişini azaltın.`}},422:{en:{label:`Invalid content`,description:`The provider accepted the request format but rejected its contents. Check model options, tool definitions, message roles, and unsupported fields.`},fr:{label:`Contenu non valide`,description:`Le fournisseur a accepté le format de la requête, mais en a rejeté le contenu. Vérifiez les options du modèle, les définitions des outils, les rôles des messages et les champs non pris en charge.`},ko:{label:`내용 검증 실패`,description:`제공자가 요청 형식은 받았지만 내용을 거부했습니다. 모델 옵션, 도구 정의, 메시지 역할, 지원되지 않는 필드를 확인해야 합니다.`},zh:{label:`内容无效`,description:`提供商接受了请求格式,但拒绝了其中的内容。请检查模型选项、工具定义、消息角色和不支持的字段。`},"zh-TW":{label:`內容無效`,description:`供應商接受了請求格式,但拒絕了其中的內容。請檢查模型選項、工具定義、訊息角色和不支援的欄位。`},de:{label:`Ungültiger Inhalt`,description:`Der Anbieter akzeptierte das Anfrageformat, lehnte den Inhalt aber ab. Prüfe Modelloptionen, Tool-Definitionen, Nachrichtenrollen und nicht unterstützte Felder.`},ru:{label:`Недопустимое содержимое`,description:`Провайдер принял формат запроса, но отклонил его содержимое. Проверьте параметры модели, определения инструментов, роли сообщений и неподдерживаемые поля.`},ja:{label:`内容の検証失敗`,description:`プロバイダーはリクエスト形式を受け付けましたが内容を拒否しました。モデルオプション、ツール定義、メッセージロール、未サポートのフィールドを確認してください。`},tr:{label:`Geçersiz içerik`,description:`Sağlayıcı istek formatını kabul etti ancak içeriğini reddetti. Model seçeneklerini, araç tanımlarını, mesaj rollerini ve desteklenmeyen alanları kontrol edin.`}},424:{en:{label:`Provider dependency failed`,description:`A required upstream dependency failed while opencodex was routing the request. Retry later or switch to another configured provider.`},fr:{label:`Échec d’une dépendance du fournisseur`,description:`Une dépendance en amont requise a échoué pendant le routage de la requête par opencodex. Réessayez plus tard ou sélectionnez un autre fournisseur configuré.`},ko:{label:`제공자 의존성 실패`,description:`opencodex가 요청을 라우팅하는 동안 필요한 업스트림 의존성이 실패했습니다. 나중에 재시도하거나 다른 설정된 제공자로 전환해야 합니다.`},zh:{label:`提供商依赖失败`,description:`opencodex 路由请求时,必需的上游依赖失败。请稍后重试,或切换到另一个已配置的提供商。`},"zh-TW":{label:`供應商依賴失敗`,description:`opencodex 路由請求時,必需的上游依賴失敗。請稍後重試,或切換到另一個已配置的供應商。`},de:{label:`Anbieter-Abhängigkeit fehlgeschlagen`,description:`Eine erforderliche Upstream-Abhängigkeit ist fehlgeschlagen, während opencodex die Anfrage geroutet hat. Später erneut versuchen oder zu einem anderen Anbieter wechseln.`},ru:{label:`Сбой зависимости провайдера`,description:`Необходимая вышестоящая зависимость дала сбой, пока opencodex маршрутизировал запрос. Повторите попытку позже или переключитесь на другого настроенного провайдера.`},ja:{label:`プロバイダー依存の失敗`,description:`opencodex がリクエストをルーティング中に必要な上流依存が失敗しました。後で再試行するか、別の設定済みプロバイダーに切り替えてください。`},tr:{label:`Sağlayıcı bağımlılığı başarısız`,description:`opencodex isteği yönlendirirken gerekli bir yukarı akış bağımlılığı başarısız oldu. Daha sonra tekrar deneyin veya başka bir sağlayıcıya geçin.`}},429:{en:{label:`Rate limited`,description:`The upstream provider rate or quota limit has been reached. Wait for the quota window to reset or switch account/provider.`},fr:{label:`Limite de débit atteinte`,description:`La limite de débit ou de quota du fournisseur en amont a été atteinte. Attendez la réinitialisation de la fenêtre de quota ou changez de compte ou de fournisseur.`},ko:{label:`한도 초과`,description:`업스트림 제공자의 속도 또는 할당량 한도에 도달했습니다. 한도 창이 초기화될 때까지 기다리거나 계정/제공자를 전환해야 합니다.`},zh:{label:`限流`,description:`已达到上游提供商的速率或额度限制。请等待额度窗口重置,或切换账号/提供商。`},"zh-TW":{label:`限流`,description:`已達到上游供應商的速率或額度限制。請等待額度視窗重設,或切換帳號/供應商。`},de:{label:`Ratenlimit erreicht`,description:`Das Raten- oder Kontingentlimit des Upstream-Anbieters ist erreicht. Auf Reset des Kontingentfensters warten oder Konto/Anbieter wechseln.`},ru:{label:`Превышен лимит запросов`,description:`Достигнут лимит скорости или квота вышестоящего провайдера. Дождитесь сброса окна квоты или переключитесь на другой аккаунт или провайдера.`},ja:{label:`レート制限`,description:`上流プロバイダーのレートまたはクォータ制限に達しました。クォータウィンドウがリセットされるまで待つか、アカウント/プロバイダーを切り替えてください。`},tr:{label:`Oran sınırı aşıldı`,description:`Yukarı akış sağlayıcısının hız veya kota sınırına ulaşıldı. Kota penceresinin sıfırlanmasını bekleyin ya da hesap/sağlayıcı değiştirin.`}},499:{en:{label:`Client closed request`,description:`The client disconnected or canceled the request before opencodex finished routing it. Retry if the cancellation was accidental.`},fr:{label:`Requête fermée par le client`,description:`Le client s’est déconnecté ou a annulé la requête avant la fin de son routage par opencodex. Réessayez si l’annulation était involontaire.`},ko:{label:`클라이언트 취소`,description:`opencodex가 라우팅을 끝내기 전에 클라이언트 연결이 끊기거나 요청이 취소되었습니다. 의도한 취소가 아니면 다시 시도해야 합니다.`},zh:{label:`客户端已取消`,description:`opencodex 完成路由前,客户端已断开连接或取消请求。如果不是有意取消,请重试。`},"zh-TW":{label:`客戶端已取消`,description:`opencodex 完成路由前,客戶端已斷開連線或取消請求。如果不是有意取消,請重試。`},de:{label:`Client hat Anfrage geschlossen`,description:`Der Client hat die Verbindung getrennt oder die Anfrage abgebrochen, bevor opencodex das Routing abgeschlossen hat. Bei versehentlichem Abbruch erneut versuchen.`},ru:{label:`Запрос закрыт клиентом`,description:`Клиент отключился или отменил запрос до того, как opencodex завершил его маршрутизацию. Если отмена была случайной, повторите попытку.`},ja:{label:`クライアントがリクエストをクローズ`,description:`opencodex がルーティングを終える前にクライアントが切断またはキャンセルしました。意図しないキャンセルなら再試行してください。`},tr:{label:`İstemci isteği kapattı`,description:`opencodex yönlendirmeyi bitirmeden önce istemci bağlantıyı kesti veya isteği iptal etti. İptal kazara yapıldıysa tekrar deneyin.`}},500:{en:{label:`Proxy error`,description:`opencodex hit an internal error while handling the request. Retry once, then check proxy logs if it repeats.`},fr:{label:`Erreur du proxy`,description:`opencodex a rencontré une erreur interne lors du traitement de la requête. Réessayez une fois, puis consultez les journaux du proxy si l’erreur se reproduit.`},ko:{label:`프록시 오류`,description:`opencodex가 요청을 처리하는 동안 내부 오류가 발생했습니다. 한 번 재시도하고 반복되면 프록시 로그를 확인해야 합니다.`},zh:{label:`代理错误`,description:`opencodex 处理请求时发生内部错误。请先重试一次;如果重复出现,请检查代理日志。`},"zh-TW":{label:`代理錯誤`,description:`opencodex 處理請求時發生內部錯誤。請先重試一次;如果重複出現,請檢查代理日誌。`},de:{label:`Proxy-Fehler`,description:`opencodex ist bei der Anfragebearbeitung auf einen internen Fehler gestoßen. Einmal erneut versuchen, bei Wiederholung Proxy-Logs prüfen.`},ru:{label:`Ошибка прокси`,description:`В opencodex произошла внутренняя ошибка при обработке запроса. Повторите попытку один раз; если ошибка повторяется, проверьте логи прокси.`},ja:{label:`プロキシエラー`,description:`opencodex がリクエスト処理中に内部エラーに遭遇しました。1 回再試行し、繰り返す場合はプロキシログを確認してください。`},tr:{label:`Proxy hatası`,description:`opencodex isteği işlerken dahili bir hatayla karşılaştı. Bir kez tekrar deneyin, tekrarlarsa proxy günlüklerini kontrol edin.`}},502:{en:{label:`Bad upstream response`,description:`The upstream provider returned an invalid or failed response through the proxy. Retry or route the request to another provider.`},fr:{label:`Réponse incorrecte du fournisseur en amont`,description:`Le fournisseur en amont a renvoyé une réponse non valide ou en échec par l’intermédiaire du proxy. Réessayez ou acheminez la requête vers un autre fournisseur.`},ko:{label:`업스트림 응답 오류`,description:`업스트림 제공자가 프록시를 통해 유효하지 않거나 실패한 응답을 반환했습니다. 재시도하거나 다른 제공자로 라우팅해야 합니다.`},zh:{label:`上游响应错误`,description:`上游提供商通过代理返回了无效或失败的响应。请重试,或将请求路由到其他提供商。`},"zh-TW":{label:`上游回應錯誤`,description:`上游供應商透過代理返回了無效或失敗的回應。請重試,或將請求路由到其他供應商。`},de:{label:`Ungültige Upstream-Antwort`,description:`Der Upstream-Anbieter lieferte über den Proxy eine ungültige oder fehlgeschlagene Antwort. Erneut versuchen oder zu einem anderen Anbieter routen.`},ru:{label:`Некорректный ответ провайдера`,description:`Вышестоящий провайдер вернул через прокси недействительный или ошибочный ответ. Повторите попытку или направьте запрос другому провайдеру.`},ja:{label:`上流レスポンス不良`,description:`上流プロバイダーがプロキシ経由で無効または失敗したレスポンスを返しました。再試行するか、リクエストを別のプロバイダーにルーティングしてください。`},tr:{label:`Kötü yukarı akış yanıtı`,description:`Yukarı akış sağlayıcısı proxy üzerinden geçersiz veya başarısız bir yanıt döndürdü. Tekrar deneyin veya isteği başka bir sağlayıcıya yönlendirin.`}},503:{en:{label:`Provider unavailable`,description:`The proxy or upstream provider is temporarily unavailable or overloaded. Wait briefly, then retry or switch provider.`},fr:{label:`Fournisseur indisponible`,description:`Le proxy ou le fournisseur en amont est temporairement indisponible ou surchargé. Patientez un instant, puis réessayez ou changez de fournisseur.`},ko:{label:`제공자 사용 불가`,description:`프록시 또는 업스트림 제공자가 일시적으로 사용할 수 없거나 과부하 상태입니다. 잠시 기다린 뒤 재시도하거나 제공자를 전환해야 합니다.`},zh:{label:`提供商不可用`,description:`代理或上游提供商暂时不可用或过载。请稍后重试,或切换提供商。`},"zh-TW":{label:`供應商不可用`,description:`代理或上游供應商暫時不可用或過載。請稍後重試,或切換供應商。`},de:{label:`Anbieter nicht verfügbar`,description:`Proxy oder Upstream-Anbieter ist vorübergehend nicht verfügbar oder überlastet. Kurz warten, dann erneut versuchen oder Anbieter wechseln.`},ru:{label:`Провайдер недоступен`,description:`Прокси или вышестоящий провайдер временно недоступен или перегружен. Немного подождите, затем повторите попытку или смените провайдера.`},ja:{label:`プロバイダー利用不可`,description:`プロキシまたは上流プロバイダーが一時的に利用不可または過負荷です。少し待ってから再試行するか、プロバイダーを切り替えてください。`},tr:{label:`Sağlayıcı kullanılamıyor`,description:`Proxy veya yukarı akış sağlayıcısı geçici olarak kullanılamıyor veya aşırı yüklü. Kısa bir süre bekleyip tekrar deneyin ya da sağlayıcı değiştirin.`}},504:{en:{label:`Upstream timeout`,description:`The upstream provider did not respond before the proxy timeout. Retry with a smaller request or choose a faster provider.`},fr:{label:`Délai d’attente du fournisseur en amont dépassé`,description:`Le fournisseur en amont n’a pas répondu avant l’expiration du délai du proxy. Réessayez avec une requête plus petite ou choisissez un fournisseur plus rapide.`},ko:{label:`업스트림 시간 초과`,description:`프록시 시간 제한 전에 업스트림 제공자가 응답하지 않았습니다. 더 작은 요청으로 재시도하거나 더 빠른 제공자를 선택해야 합니다.`},zh:{label:`上游超时`,description:`上游提供商未在代理超时前响应。请缩小请求后重试,或选择响应更快的提供商。`},"zh-TW":{label:`上游逾時`,description:`上游供應商未在代理逾時前回應。請縮小請求後重試,或選擇回應更快的供應商。`},de:{label:`Upstream-Timeout`,description:`Der Upstream-Anbieter antwortete nicht vor dem Proxy-Timeout. Mit kleinerer Anfrage erneut versuchen oder schnelleren Anbieter wählen.`},ru:{label:`Тайм-аут вышестоящего провайдера`,description:`Вышестоящий провайдер не ответил до истечения тайм-аута прокси. Повторите попытку с меньшим запросом или выберите более быстрого провайдера.`},ja:{label:`上流タイムアウト`,description:`上流プロバイダーがプロキシタイムアウト前に応答しませんでした。より小さいリクエストで再試行するか、より速いプロバイダーを選んでください。`},tr:{label:`Yukarı akış zaman aşımı`,description:`Yukarı akış sağlayıcısı proxy zaman aşımı süresinden önce yanıt vermedi. Daha küçük bir istekle tekrar deneyin veya daha hızlı bir sağlayıcı seçin.`}},529:{en:{label:`Provider overloaded`,description:`The upstream provider is overloaded or capacity-limited. Wait and retry, or switch to another account/provider.`},fr:{label:`Fournisseur surchargé`,description:`Le fournisseur en amont est surchargé ou sa capacité est limitée. Patientez et réessayez, ou changez de compte ou de fournisseur.`},ko:{label:`제공자 과부하`,description:`업스트림 제공자가 과부하 상태이거나 처리 용량이 제한되었습니다. 기다렸다가 재시도하거나 다른 계정/제공자로 전환해야 합니다.`},zh:{label:`提供商过载`,description:`上游提供商过载或容量受限。请等待后重试,或切换到其他账号/提供商。`},"zh-TW":{label:`供應商過載`,description:`上游供應商過載或容量受限。請等待後重試,或切換到其他帳號/供應商。`},de:{label:`Anbieter überlastet`,description:`Der Upstream-Anbieter ist überlastet oder kapazitätsbegrenzt. Warten und erneut versuchen oder anderes Konto/Anbieter nutzen.`},ru:{label:`Провайдер перегружен`,description:`Вышестоящий провайдер перегружен или ограничен по мощности. Подождите и повторите попытку либо переключитесь на другой аккаунт или провайдера.`},ja:{label:`プロバイダー過負荷`,description:`上流プロバイダーが過負荷または容量制限されています。待ってから再試行するか、別のアカウント/プロバイダーに切り替えてください。`},tr:{label:`Sağlayıcı aşırı yüklü`,description:`Yukarı akış sağlayıcısı aşırı yüklü veya kapasitesi sınırlı. Bekleyip tekrar deneyin veya başka bir hesap/sağlayıcıya geçin.`}}},rm={client:{en:{label:`Request error`,description:`The proxy or upstream provider rejected the request. Check the request shape, credentials, model name, and provider configuration.`},fr:{label:`Erreur de requête`,description:`Le proxy ou le fournisseur en amont a rejeté la requête. Vérifiez sa structure, les identifiants, le nom du modèle et la configuration du fournisseur.`},ko:{label:`요청 오류`,description:`프록시 또는 업스트림 제공자가 요청을 거부했습니다. 요청 형식, 자격 증명, 모델 이름, 제공자 설정을 확인해야 합니다.`},zh:{label:`请求错误`,description:`代理或上游提供商拒绝了该请求。请检查请求结构、凭据、模型名称和提供商配置。`},"zh-TW":{label:`請求錯誤`,description:`代理或上游供應商拒絕了該請求。請檢查請求結構、憑證、模型名稱和供應商配置。`},de:{label:`Anfragefehler`,description:`Der Proxy oder Upstream-Anbieter hat die Anfrage abgelehnt. Prüfe Anfrageformat, Anmeldedaten, Modellname und Anbieterkonfiguration.`},ru:{label:`Ошибка запроса`,description:`Прокси или вышестоящий провайдер отклонил запрос. Проверьте структуру запроса, учётные данные, имя модели и конфигурацию провайдера.`},ja:{label:`リクエストエラー`,description:`プロキシまたは上流プロバイダーがリクエストを拒否しました。リクエスト形式、認証情報、モデル名、プロバイダー設定を確認してください。`},tr:{label:`İstek hatası`,description:`Proxy veya yukarı akış sağlayıcısı isteği reddetti. İstek yapısını, kimlik bilgilerini, model adını ve sağlayıcı yapılandırmasını kontrol edin.`}},server:{en:{label:`Server or upstream error`,description:`opencodex or an upstream provider failed while processing the request. Retry later or route the request to another provider.`},fr:{label:`Erreur du serveur ou du fournisseur en amont`,description:`opencodex ou un fournisseur en amont a échoué lors du traitement de la requête. Réessayez plus tard ou acheminez la requête vers un autre fournisseur.`},ko:{label:`서버 또는 업스트림 오류`,description:`opencodex 또는 업스트림 제공자가 요청 처리 중 실패했습니다. 나중에 재시도하거나 다른 제공자로 라우팅해야 합니다.`},zh:{label:`服务器或上游错误`,description:`opencodex 或上游提供商处理请求时失败。请稍后重试,或将请求路由到其他提供商。`},"zh-TW":{label:`伺服器或上游錯誤`,description:`opencodex 或上游供應商處理請求時失敗。請稍後重試,或將請求路由到其他供應商。`},de:{label:`Server- oder Upstream-Fehler`,description:`opencodex oder ein Upstream-Anbieter ist bei der Anfragebearbeitung fehlgeschlagen. Später erneut versuchen oder zu einem anderen Anbieter routen.`},ru:{label:`Ошибка сервера или провайдера`,description:`opencodex или вышестоящий провайдер завершил обработку запроса с ошибкой. Повторите попытку позже или направьте запрос другому провайдеру.`},ja:{label:`サーバーまたは上流エラー`,description:`opencodex または上流プロバイダーがリクエスト処理中に失敗しました。後で再試行するか、リクエストを別のプロバイダーにルーティングしてください。`},tr:{label:`Sunucu veya yukarı akış hatası`,description:`opencodex veya bir yukarı akış sağlayıcısı isteği işlerken başarısız oldu. Daha sonra tekrar deneyin veya isteği başka bir sağlayıcıya yönlendirin.`}}};function im(e){return e.toLowerCase().startsWith(`fr`)?`fr`:e===`de`||e===`ko`||e===`zh`||e===`zh-TW`||e===`ru`||e===`ja`||e===`tr`?e:`en`}function am(e,t){if(e<400)return null;let n=im(t);return(nm[Math.trunc(e)]??(e<500?rm.client:rm.server))[n]}var om=[`provider`,`usage`,`injection`];function sm(e){return e>0?`[${new Date(e).toLocaleTimeString()}] `:``}function cm(e){return new Date(e).toLocaleTimeString()}function lm(e,t){return t===`provider`?!!e?.enabled:t===`usage`?!!e?.usage:!!e?.injection}function um(e,t){return t===`debug`?e.enabled:t===`usage`?e.usage:t===`injection`?e.injection:e.claude}function dm({entries:e}){let{t}=ct();return(0,J.jsxs)(`div`,{className:`card`,style:{marginBottom:16,padding:`12px 14px`},children:[(0,J.jsx)(`div`,{className:`font-semibold`,style:{marginBottom:4},children:t(`debug.claudeInbound.title`)}),(0,J.jsx)(`div`,{className:`muted text-control`,style:{marginBottom:10},children:t(`debug.claudeInbound.sub`)}),e.length===0?(0,J.jsx)(`div`,{className:`muted text-control`,children:t(`debug.claudeInbound.empty`)}):(0,J.jsx)(`div`,{style:{overflowX:`auto`},children:(0,J.jsxs)(`table`,{className:`table text-label`,children:[(0,J.jsx)(`thead`,{children:(0,J.jsxs)(`tr`,{children:[(0,J.jsx)(`th`,{children:t(`debug.claudeInbound.time`)}),(0,J.jsx)(`th`,{children:t(`debug.claudeInbound.endpoint`)}),(0,J.jsx)(`th`,{children:t(`debug.claudeInbound.model`)}),(0,J.jsx)(`th`,{children:`thinking`}),(0,J.jsx)(`th`,{children:`effort`}),(0,J.jsx)(`th`,{children:`beta`}),(0,J.jsx)(`th`,{children:`metadata`}),(0,J.jsx)(`th`,{children:`system`})]})}),(0,J.jsx)(`tbody`,{children:e.map(e=>(0,J.jsxs)(`tr`,{children:[(0,J.jsx)(`td`,{className:`muted mono`,children:cm(e.at)}),(0,J.jsx)(`td`,{className:`mono`,children:e.endpoint}),(0,J.jsxs)(`td`,{className:`mono`,title:e.resolvedModel,children:[e.model,e.resolvedModel&&e.resolvedModel!==e.model&&(0,J.jsxs)(`span`,{className:`muted`,children:[` → `,e.resolvedModel]})]}),(0,J.jsxs)(`td`,{className:`mono`,children:[e.thinkingType??`-`,e.thinkingBudgetTokens!==void 0&&(0,J.jsxs)(`span`,{className:`muted`,children:[` (`,e.thinkingBudgetTokens,`)`]})]}),(0,J.jsx)(`td`,{className:`mono`,children:e.outputConfigEffort??`-`}),(0,J.jsx)(`td`,{className:`mono`,title:e.anthropicBeta,style:{maxWidth:160,overflow:`hidden`,textOverflow:`ellipsis`,whiteSpace:`nowrap`},children:e.anthropicBeta??`-`}),(0,J.jsx)(`td`,{className:`mono`,title:e.metadataKeys?.join(`, `),children:e.hasMetadataUserId?`user_id ${e.userIdTag??``}`:t(`debug.claudeInbound.none`)}),(0,J.jsx)(`td`,{className:`mono`,children:e.hasSystem?e.systemTag??`yes`:t(`debug.claudeInbound.none`)})]},e.id))})]})})]})}function fm({debug:e,stream:t,streamEnabled:n,entries:r,scrollContainerRef:i,lineVirtualizer:a}){let{t:o}=ct();return e?n?r.length===0?(0,J.jsxs)(`div`,{className:`empty`,children:[(0,J.jsx)(`div`,{className:`font-semibold`,style:{marginBottom:6},children:o(`debug.noLinesTitle`)}),(0,J.jsx)(`div`,{className:`muted text-control`,style:{maxWidth:560,marginInline:`auto`},children:o(`debug.noLines.${t}`)})]}):(0,J.jsx)(`div`,{ref:i,className:`log-detail-json`,style:{maxHeight:`calc(100vh - 280px)`,overflow:`auto`},children:(0,J.jsx)(`div`,{style:{position:`relative`,height:a.getTotalSize(),width:`100%`},children:a.getVirtualItems().map(e=>(0,J.jsx)(`div`,{ref:a.measureElement,"data-index":e.index,style:{position:`absolute`,top:0,left:0,width:`100%`,transform:`translateY(${e.start}px)`},children:`${sm(r[e.index].at)}${r[e.index].line}`},e.key))})}):(0,J.jsxs)(`div`,{className:`empty`,children:[(0,J.jsx)(`div`,{className:`font-semibold`,style:{marginBottom:6},children:o(`debug.emptyTitle`)}),(0,J.jsx)(`div`,{className:`muted text-control`,style:{maxWidth:560,marginInline:`auto`},children:o(`debug.empty`)})]}):null}function pm({debug:e,debugBusy:t,stream:n,onSetFlag:r,onReset:i,onStreamChange:a}){let{t:o}=ct();return(0,J.jsxs)(`div`,{className:`card`,style:{marginBottom:16,padding:`12px 14px`},children:[(0,J.jsxs)(`div`,{style:{display:`flex`,alignItems:`center`,justifyContent:`space-between`,gap:12,flexWrap:`wrap`},children:[(0,J.jsx)(`div`,{style:{display:`flex`,flexWrap:`wrap`,gap:16},children:[`debug`,`usage`,`injection`,`claude`].map(n=>{let i=um(e,n);return(0,J.jsxs)(`div`,{style:{display:`inline-flex`,alignItems:`center`,gap:10,minWidth:220},children:[(0,J.jsx)(Tt,{on:i,disabled:t,label:o(`debug.${n}`),onClick:()=>r(n,!i)}),(0,J.jsx)(`span`,{className:`text-control`,children:o(`debug.${n}`)})]},n)})}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,disabled:t,onClick:i,children:o(`debug.reset`)})]}),(e.enabled||e.usage||e.injection)&&(0,J.jsxs)(`div`,{style:{display:`inline-flex`,gap:6,marginTop:12},children:[e.enabled&&(0,J.jsx)(`button`,{type:`button`,className:`btn btn-sm${n===`provider`?` btn-primary`:` btn-ghost`}`,onClick:()=>a(`provider`),children:o(`debug.streamProvider`)}),e.usage&&(0,J.jsx)(`button`,{type:`button`,className:`btn btn-sm${n===`usage`?` btn-primary`:` btn-ghost`}`,onClick:()=>a(`usage`),children:o(`debug.streamUsage`)}),e.injection&&(0,J.jsx)(`button`,{type:`button`,className:`btn btn-sm${n===`injection`?` btn-primary`:` btn-ghost`}`,onClick:()=>a(`injection`),children:o(`debug.streamInjection`)})]})]})}function mm({embedded:e,refreshing:t,streamEnabled:n,follow:r,onRefresh:i,onFollowChange:a}){let{t:o}=ct();return(0,J.jsxs)(J.Fragment,{children:[(0,J.jsxs)(`div`,{className:e?`row`:`page-head`,style:e?{justifyContent:`flex-end`,marginBottom:4}:void 0,children:[!e&&(0,J.jsx)(`h2`,{children:o(`debug.title`)}),(0,J.jsxs)(`div`,{style:{display:`inline-flex`,alignItems:`center`,gap:12},children:[(0,J.jsxs)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,disabled:t||!n,onClick:i,children:[(0,J.jsx)(pe,{}),` `,o(`debug.refresh`)]}),(0,J.jsxs)(`label`,{className:`muted text-control`,style:{cursor:`pointer`,display:`inline-flex`,alignItems:`center`,gap:6},children:[(0,J.jsx)(`input`,{type:`checkbox`,checked:r,onChange:e=>a(e.target.checked)}),o(`debug.follow`)]})]})]}),(0,J.jsx)(`p`,{className:`page-sub`,children:o(`debug.subtitle`)})]})}function hm(e){return`debug-settings:${e}`}function gm({apiBase:e,embedded:t,active:n=!0}){let{t:r}=ct(),i=`ocx.debug.settings.v1:${e}`,a=gr(i),o=hm(e),[s,c]=(0,_.useState)(!1),[l,u]=(0,_.useState)(`provider`),[d,f]=(0,_.useState)([]),[p,m]=(0,_.useState)(!0),[h,g]=(0,_.useState)(!1),v=(0,_.useRef)(0),y=(0,_.useRef)(0),b=(0,_.useRef)(0),x=(0,_.useRef)(null),S=(0,_.useRef)(null),C=(0,_.useRef)(null),w=ml(o,[e],async t=>{let n=await fetch(`${e}/api/debug`,{signal:t});if(!n.ok)throw Error(String(n.status));let r=await n.json();return br(i,r),r},{pollMs:2e3,enabled:n,isEmpty:()=>!1,initialData:a??void 0}),T=w.state,E=w.data??a??null,D=G(`debug-claude-inbound:${e}`,[e,E?.claude],async t=>{let n=await fetch(`${e}/api/claude/inbound-debug`,{signal:t});if(!n.ok)return[];let r=await n.json();return Array.isArray(r.entries)?r.entries:[]},{pollMs:2e3,enabled:n&&!!E?.claude}).data??[],O=Xp({count:d.length,getScrollElement:()=>S.current,estimateSize:()=>20,overscan:30,getItemKey:e=>d[e].seq}),k=(0,_.useCallback)(e=>lm(E,e),[E]);(0,_.useEffect)(()=>{if(!E||k(l))return;let e=om.find(k);if(!e)return;let t=window.setTimeout(()=>u(e),0);return()=>window.clearTimeout(t)},[E,l,k]);let A=k(l),j=l===`provider`?`${e}/api/debug/logs`:l===`usage`?`${e}/api/debug/usage-logs`:`${e}/api/debug/injection-logs`,M=(0,_.useCallback)(async(e,t)=>{let n=++b.current;if(!A){n===b.current&&(f([]),v.current=0);return}g(!0);try{let r=new URLSearchParams({limit:`500`});!e&&v.current>0&&r.set(`after`,String(v.current));let i=await fetch(`${j}?${r}`,{signal:t});if(!i.ok||t?.aborted||n!==b.current)return;let a=await i.json();if(t?.aborted||n!==b.current||a.length===0)return;f(t=>(e?a:[...t,...a]).slice(-2e3)),v.current=a[a.length-1].seq}catch{}finally{n===b.current&&g(!1)}},[j,A]);(0,_.useEffect)(()=>{if(!n)return;let t=`${e}:${l}:${A}`,r=C.current!==t;if(C.current=t,!r&&d.length>0)return;v.current=0;let i=new AbortController,a=window.setTimeout(()=>{r&&f([]),M(!0,i.signal)},0);return()=>{window.clearTimeout(a),b.current+=1,i.abort()}},[n,e,l,A]);let N=(0,_.useRef)(!1),P=(0,_.useEffectEvent)(()=>{if(N.current)return;N.current=!0;let e=Vn(1e4);M(!1,e.signal).finally(()=>{e.clear(),N.current=!1})});(0,_.useEffect)(()=>{if(!(!n||!p||!A))return Gn(()=>P(),1e3)},[n,p,A]),(0,_.useEffect)(()=>{p&&d.length>0&&O.scrollToIndex(d.length-1,{align:`end`})},[d,p,O]);let F=async t=>{let n=++y.current;c(!0);let r=async()=>{try{let r=await fetch(`${e}/api/debug`,{method:`PUT`,headers:{"content-type":`application/json`},body:JSON.stringify(t)});if(!r.ok)return;let a=await r.json();if(n!==y.current)return;br(i,a),K(o,a)}catch{}},a=(x.current??Promise.resolve()).then(r,r);x.current=a.then(()=>void 0,()=>void 0);try{await a}finally{n===y.current&&c(!1)}},I=async(e,t)=>{await F({[e]:t})},L=async()=>{await F({reset:!0})};return(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)(mm,{embedded:t,refreshing:h,streamEnabled:A,follow:p,onRefresh:()=>void M(!0),onFollowChange:m}),!E&&T.showError?(0,J.jsxs)(`div`,{className:`notice notice-err`,role:`alert`,children:[(0,J.jsx)(`span`,{children:r(`debug.loadFailed`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:()=>w.refresh(),children:r(`common.retry`)})]}):T.showSkeleton&&!E?(0,J.jsx)(gl,{label:r(`debug.loading`),rows:3}):E?(0,J.jsx)(pm,{debug:E,debugBusy:s,stream:l,onSetFlag:(e,t)=>{I(e,t)},onReset:()=>{L()},onStreamChange:u}):null,E&&T.showError&&(0,J.jsx)($,{tone:`err`,children:r(`debug.loadFailed`)}),E?.claude&&(0,J.jsx)(dm,{entries:D}),(0,J.jsx)(fm,{debug:!!E,stream:l,streamEnabled:A,entries:d,scrollContainerRef:S,lineVirtualizer:O})]})}function _m(){return window.location.hash.replace(/^#\/?/,``)===`logs/debug`?`debug`:`logs`}function vm(e){window.location.hash=e===`debug`?`logs/debug`:`logs`}function ym(e){e.key===`ArrowLeft`||e.key===`Home`?(e.preventDefault(),vm(`logs`),document.getElementById(`logs-tab-logs`)?.focus()):(e.key===`ArrowRight`||e.key===`End`)&&(e.preventDefault(),vm(`debug`),document.getElementById(`logs-tab-debug`)?.focus())}function bm(e,t){return[`${t(`logs.modelTooltip.model`)}=${e.model}`,e.resolvedModel?`${t(`logs.modelTooltip.resolvedModel`)}=${e.resolvedModel}`:void 0,e.requestedServiceTier?`${t(`logs.modelTooltip.requestedTier`)}=${e.requestedServiceTier}`:void 0,e.configuredServiceTier?`${t(`logs.modelTooltip.configuredTier`)}=${e.configuredServiceTier}`:void 0,e.responseServiceTier?`${t(`logs.modelTooltip.responseTier`)}=${e.responseServiceTier}`:void 0,e.modelSupportsServiceTier===void 0?void 0:`${t(`logs.modelTooltip.supportsTier`)}=${e.modelSupportsServiceTier}`].filter(Boolean).join(` · `)}function xm(e){return e.requestedSpeedLabel||void 0}var Sm=new Intl.NumberFormat(`en-US`,{style:`currency`,currency:`USD`,currencyDisplay:`narrowSymbol`,minimumFractionDigits:4,maximumFractionDigits:4});function Cm(e,t,n,r=!1){if(!Number.isFinite(e)||e<0)return t(`logs.cost.unavailable`);let i=Sm.format(e);return t(r?`logs.cost.lowerBound`:`logs.cost.approximate`,{amount:i})}function wm(e,t,n){return!e||e.kind===`unavailable`?t(`logs.cost.unavailable`):Cm(e.estimate.cost.total,t,n,e.estimate.priorityLowerBound)}function Tm(e){let t=0,n=!0,r=0,i=0,a=0;for(let o of e){if(o.usageStatus===`unsupported`){a+=1;continue}let e=o.displayMetrics?.cost;if(e?.kind===`value`){let i=e.estimate.cost.total;if(Number.isFinite(i)&&i>=0){t+=i,r+=1,n&&=e.estimate.priorityLowerBound===!0;continue}}i+=1}return{estimatedCostUsd:t,priorityLowerBound:r>0&&n,unpricedRequests:i,unmeteredRequests:a}}function Em(e){return e===`cursor`||e.startsWith(`cursor-`)}function Dm(e){let t=e.usage;if(!t)return{};let n=typeof t.cacheCreationInputTokens==`number`?t.cacheCreationInputTokens:void 0;return{read:typeof t.cacheReadInputTokens==`number`?t.cacheReadInputTokens:typeof t.cachedInputTokens==`number`&&n!==void 0?Math.max(0,t.cachedInputTokens-n):t.cachedInputTokens,write:n}}function Om(e,t){if(!e.usage)return;let n=Dm(e),r=[`${t(`logs.tokens.input`)}=${e.usage.inputTokens}`,`${t(`logs.tokens.output`)}=${e.usage.outputTokens}`];return n.read!==void 0&&r.push(`${t(`logs.tokens.cacheRead`)}=${n.read}`),n.write!==void 0&&r.push(`${t(`logs.tokens.cacheWrite`)}=${n.write}`),typeof e.usage.contextTotalTokens==`number`&&r.push(`${t(`logs.tokens.contextTotal`)}=${e.usage.contextTotalTokens}`),typeof e.usage.reasoningOutputTokens==`number`&&r.push(`${t(`logs.tokens.reasoning`)}=${e.usage.reasoningOutputTokens}`),e.usageStatus===`estimated`&&r.push(t(`logs.tokens.estimatedNote`)),e.usageStatus===`estimated`&&n.read===void 0&&n.write===void 0&&r.push(t(Em(e.provider)?`logs.tokens.noCacheCursorNote`:`logs.tokens.noCacheNote`)),r.join(` · `)}function km(e,t){return t===`all`?!0:t===`claude`?e.surface===`claude`||e.surface===`claude-desktop`:t===`grok`?e.surface===`grok`:e.surface===void 0}function Am(e,t){let n=t.trim().toLowerCase();if(!n)return!0;let r=Array.isArray(e.attempts)?e.attempts.flatMap(e=>e&&typeof e==`object`?[e.provider,e.model]:[]):[];return[e.model,e.resolvedModel,e.provider,...r].some(e=>typeof e==`string`&&e.toLowerCase().includes(n))}function jm(e){return e===void 0||typeof e==`string`}function Mm(e){if(e===void 0)return!0;if(!e||typeof e!=`object`||!jm(e.routeKind)||e.profile!==void 0&&(!e.profile||typeof e.profile!=`object`||!jm(e.profile.id)||!jm(e.profile.revision))||e.selected!==void 0&&(!e.selected||typeof e.selected!=`object`||!jm(e.selected.provider)||!jm(e.selected.model)||!jm(e.selected.reason)))return!1;if(e.candidates===void 0)return!0;if(!Array.isArray(e.candidates))return!1;for(let t of e.candidates){if(!t||typeof t!=`object`||!jm(t.provider)||!jm(t.model)||t.eligible!==void 0&&typeof t.eligible!=`boolean`)return!1;if(t.exclusions!==void 0){if(!Array.isArray(t.exclusions))return!1;for(let e of t.exclusions)if(!e||typeof e!=`object`||!jm(e.code))return!1}}return!0}function Nm(e){if(e.routeDecision===void 0||Mm(e.routeDecision))return e;let t={...e};return delete t.routeDecision,t}function Pm(e){return`ocx.logs.list.v1:${e}`}function Fm(e){if(!Array.isArray(e))return null;for(let t of e)if(!t||typeof t!=`object`||typeof t.timestamp!=`number`||typeof t.model!=`string`||typeof t.provider!=`string`||typeof t.status!=`number`||typeof t.durationMs!=`number`||t.shadowCallRewrittenFrom!==void 0&&typeof t.shadowCallRewrittenFrom!=`string`||!Mm(t.routeDecision))return null;return e}function Im(e){if(!e.usage)return typeof e.totalTokens==`number`?e.totalTokens:void 0;let t=e.usage.inputTokens+e.usage.outputTokens,n=e.usage.totalTokens??e.totalTokens;return typeof n==`number`?Math.max(n,t):t}function Lm(e){let t=Im(e),n=e.usage?.contextTotalTokens;return typeof n==`number`?Math.max(t??0,n)||void 0:t}function Rm(e){let t=e.requestedEffort?.replace(/\s*->\s*/g,` → `),n=e.effectiveEffort;return t?!n||t===n||t.split(` → `).at(-1)===n?t:`${t} → ${n}`:n??`-`}function zm(e){if(!(!e.reasoningWireField||e.reasoningWireValue===void 0))return`${e.reasoningWireField}=${e.reasoningWireValue}`}function Bm(e,t){if(!e||e.kind===`unavailable`||!Number.isFinite(e.value)||e.value<=0)return`—`;let n=e.value>=100?0:1,r=new Intl.NumberFormat(t,{minimumFractionDigits:n,maximumFractionDigits:n}).format(e.value);return`${e.estimated?`~`:``}${r}`}var Vm=2e3,Hm=4,Um=3,Wm={usage_missing:`logs.detail.reason.usage_missing`,usage_unsupported:`logs.detail.reason.usage_unsupported`,output_missing:`logs.detail.reason.output_missing`,invalid_duration:`logs.detail.reason.invalid_duration`,price_unmatched:`logs.detail.reason.price_unmatched`,invalid_cache_breakdown:`logs.detail.reason.invalid_cache_breakdown`,invalid_usage:`logs.detail.reason.invalid_usage`,combo_attempt_unavailable:`logs.detail.reason.combo_attempt_unavailable`},Gm={usage_estimated:`logs.detail.estimate.usage_estimated`,cache_detail_missing:`logs.detail.estimate.cache_detail_missing`,expected_price_overlay:`logs.detail.estimate.expected_price_overlay`,provider_cost_overlay:`logs.detail.estimate.provider_cost_overlay`,priority_lower_bound:`logs.detail.estimate.priority_lower_bound`},Km={"transient-5xx":`logs.detail.attempt.recovery.transient5xx`,"connection-reset":`logs.detail.attempt.recovery.connectionReset`,"oauth-401":`logs.detail.attempt.recovery.oauth401`,"key-429":`logs.detail.attempt.recovery.key429`,"rate-limit-429":`logs.detail.attempt.recovery.rateLimit429`,"anthropic-oauth-429":`logs.detail.attempt.recovery.anthropicOauth429`,"image-413":`logs.detail.attempt.recovery.image413`,"empty-completion":`logs.detail.attempt.recovery.emptyCompletion`};function qm(e){return Wm[e]}function Jm(e){return Gm[e]}function Ym(e){return Km[e]??`logs.detail.attempt.recovery.unknown`}function Xm(e){return e===`verified`?`logs.detail.verification.verified`:`logs.detail.verification.derived`}function Zm(e){return e>=200&&e<300?`var(--green)`:e>=400?`var(--red)`:`var(--amber)`}function Qm(e,t,n){let r=n?{timeZone:n}:void 0;try{return{date:new Date(e).toLocaleDateString(t,r),time:new Date(e).toLocaleTimeString(t,r)}}catch{return{date:new Date(e).toLocaleDateString(t),time:new Date(e).toLocaleTimeString(t)}}}function $m(e,t,n){let{date:r,time:i}=Qm(e,t,n);return`${r} ${i}`}function eh(e){let t=0;for(let n of e){let e=Im(n);e!==void 0&&(t+=e)}return{requests:e.length,totalTokens:t,...Tm(e)}}function th({apiBase:e}){let{t,locale:n}=ct(),r=Pm(e),i=Fm(gr(r)),[a,o]=(0,_.useState)(!0),[s,c]=(0,_.useState)({error:null,count:0}),[l,u]=(0,_.useState)(null),[d,f]=(0,_.useState)(`all`),[p,m]=(0,_.useState)(!1),[h,g]=(0,_.useState)(``),[v,y]=(0,_.useState)(``),[b,x]=(0,_.useState)(),S=(0,_.useRef)(null),C=(0,_.useRef)({key:r,failures:0,nextAttemptAt:0,error:null}),w=et.find(e=>e.code===n)?.htmlLang,[T,E]=(0,_.useState)();(0,_.useEffect)(()=>{let t=new AbortController,n=!1;return fetch(`${e}/api/settings`,{signal:t.signal}).then(e=>e.ok?e.json():null).then(e=>{n||!e||typeof e.timeZone==`string`&&e.timeZone.trim()&&E(e.timeZone.trim())}).catch(()=>{}),()=>{n=!0,t.abort()}},[e]);let[D,O]=(0,_.useState)(_m),[k,A]=(0,_.useState)(()=>_m()===`debug`);(0,_.useEffect)(()=>{let e=()=>O(_m());return window.addEventListener(`hashchange`,e),()=>window.removeEventListener(`hashchange`,e)},[]),(0,_.useEffect)(()=>{D===`debug`&&A(!0)},[D]);let j=vm,M=(0,_.useCallback)(async t=>{let n=C.current;if(n.key!==r&&(n={key:r,failures:0,nextAttemptAt:0,error:null},C.current=n),n.failures>0&&Date.now()e.length===0,enabled:D===`logs`,pollMs:a?Vm:void 0,initialData:i??void 0}),P=N.state,F=P.data??i??[],I=N.refresh,L=(0,_.useCallback)(()=>{C.current={key:r,failures:0,nextAttemptAt:0,error:null},I({forceLoading:!0})},[I,r]),R=!N.refreshing&&P.showError;!N.refreshing&&!P.showError&&P.data!==void 0&&s.count!==0?c({error:null,count:0}):R&&s.error!==P.error&&c(e=>({error:P.error,count:e.count+1}));let z=s.count>=Um||!a&&R,B=l?am(l.status,n):null,V=h.trim();(0,_.useEffect)(()=>{let e=!1;if(!V){x(void 0);return}return em(V).then(t=>{e||x(t)}),()=>{e=!0}},[V]);let H=F.filter(e=>km(e,d)&&(!p||!!e.shadowCallRewrittenFrom)&&Am(e,v)&&(!V||tm(e.conversationId,V,b))),U=V?eh(H):null,W=Xp({count:H.length,getScrollElement:()=>S.current,estimateSize:()=>92,overscan:15,getItemKey:e=>{let t=H[H.length-1-e];return t.requestId??`${t.timestamp}:${t.model}:${t.provider}`}}),ee=W.getVirtualItems(),G=ee.length>0?ee[0].start:0,K=ee.length>0?W.getTotalSize()-ee[ee.length-1].end:0;return(0,J.jsxs)(`div`,{className:`logs-page`,children:[(0,J.jsxs)(`div`,{className:`page-head`,children:[(0,J.jsx)(`h2`,{children:t(`nav.logs`)}),D===`logs`&&(0,J.jsxs)(`label`,{className:`muted text-control logs-auto-refresh`,children:[(0,J.jsx)(`input`,{type:`checkbox`,checked:a,onChange:e=>o(e.target.checked)}),t(`logs.autoRefresh`)]})]}),(0,J.jsxs)(`div`,{className:`page-tabs`,role:`tablist`,"aria-label":t(`nav.logs`),children:[(0,J.jsx)(`button`,{type:`button`,role:`tab`,id:`logs-tab-logs`,"aria-selected":D===`logs`,"aria-controls":`logs-panel-logs`,tabIndex:D===`logs`?0:-1,className:`page-tab${D===`logs`?` page-tab--active`:``}`,onClick:()=>j(`logs`),onKeyDown:ym,children:t(`logs.tabLogs`)}),(0,J.jsx)(`button`,{type:`button`,role:`tab`,id:`logs-tab-debug`,"aria-selected":D===`debug`,"aria-controls":`logs-panel-debug`,tabIndex:D===`debug`?0:-1,className:`page-tab${D===`debug`?` page-tab--active`:``}`,onClick:()=>j(`debug`),onKeyDown:ym,children:t(`logs.tabDebug`)})]}),k&&(0,J.jsx)(`div`,{role:`tabpanel`,id:`logs-panel-debug`,"aria-labelledby":`logs-tab-debug`,hidden:D!==`debug`,children:(0,J.jsx)(gm,{apiBase:e,embedded:!0,active:D===`debug`})}),(0,J.jsxs)(`div`,{role:`tabpanel`,id:`logs-panel-logs`,"aria-labelledby":`logs-tab-logs`,hidden:D!==`logs`,children:[(0,J.jsxs)(`div`,{className:`logs-toolbar`,children:[(0,J.jsx)(`span`,{className:`muted text-control`,children:t(`logs.filter.surface.label`)}),(0,J.jsx)(`div`,{className:`segmented logs-segmented`,role:`radiogroup`,"aria-label":t(`logs.filter.surface.label`),children:[`all`,`claude`,`codex`,`grok`].map(e=>(0,J.jsx)(`button`,{type:`button`,role:`radio`,"aria-checked":d===e,className:`btn btn-sm${d===e?` btn-primary`:` btn-ghost`}`,style:{background:d===e?void 0:`transparent`,color:d===e?void 0:`var(--muted)`},onClick:()=>f(e),children:t(`logs.filter.surface.${e}`)},e))}),(0,J.jsxs)(`label`,{className:`muted text-control logs-filter-field`,children:[(0,J.jsx)(`input`,{type:`checkbox`,checked:p,onChange:e=>m(e.target.checked)}),t(`logs.filter.interceptedHelpersOnly`)]}),(0,J.jsxs)(`label`,{className:`muted text-control logs-filter-field`,children:[t(`logs.filter.conversation.label`),(0,J.jsx)(`input`,{type:`search`,className:`input mono`,value:h,onChange:e=>g(e.target.value),placeholder:t(`logs.filter.conversation.placeholder`),"aria-label":t(`logs.filter.conversation.label`)})]}),(0,J.jsxs)(`label`,{className:`muted text-control logs-filter-field`,children:[t(`logs.filter.model.label`),(0,J.jsx)(`input`,{type:`search`,className:`input mono`,value:v,onChange:e=>y(e.target.value),placeholder:t(`logs.filter.model.placeholder`),"aria-label":t(`logs.filter.model.label`)})]}),V&&(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:()=>g(``),children:t(`logs.filter.conversation.clear`)})]}),U&&(0,J.jsx)(`div`,{className:`logs-conversation-totals`,children:(0,J.jsxs)($,{tone:`ok`,children:[t(`logs.conversation.totals`,{requests:U.requests,tokens:Rn(U.totalTokens,w??n),cost:Cm(U.estimatedCostUsd,t,w,U.priorityLowerBound)}),` `,(0,J.jsxs)(`span`,{className:`muted`,children:[t(`logs.conversation.scope`),U.unpricedRequests+U.unmeteredRequests>0?` ${t(`logs.conversation.excluded`,{unpriced:U.unpricedRequests,unmetered:U.unmeteredRequests})}`:``]})]})}),P.kind===`failed-cold`&&(0,J.jsxs)($,{tone:`err`,children:[P.error instanceof Error?`${t(`logs.loadError`)} ${P.error.message}`:t(`logs.loadError`),` `,(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:L,disabled:P.refreshing,children:t(`common.retry`)})]}),z&&F.length>0&&(0,J.jsxs)($,{tone:`err`,children:[t(`logs.loadError`),` `,(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:L,disabled:N.refreshing,children:t(`common.retry`)})]}),P.kind===`failed-cold`?null:P.showSkeleton&&F.length===0?(0,J.jsx)(gl,{label:t(`common.loading`),rows:6}):H.length===0?(0,J.jsx)(Ot,{title:t(`logs.noRequests`)}):(0,J.jsx)(J.Fragment,{children:(0,J.jsx)(`div`,{ref:S,className:`tbl-wrap logs-table-wrap`,children:(0,J.jsxs)(`table`,{className:`tbl logs-table`,children:[(0,J.jsxs)(`colgroup`,{children:[(0,J.jsx)(`col`,{className:`logs-col-time`}),(0,J.jsx)(`col`,{className:`logs-col-tokens`}),(0,J.jsx)(`col`,{className:`logs-col-rate`}),(0,J.jsx)(`col`,{className:`logs-col-cost`}),(0,J.jsx)(`col`,{className:`logs-col-model`}),(0,J.jsx)(`col`,{className:`logs-col-effort`}),(0,J.jsx)(`col`,{className:`logs-col-provider`}),(0,J.jsx)(`col`,{className:`logs-col-status`}),(0,J.jsx)(`col`,{className:`logs-col-request`}),(0,J.jsx)(`col`,{className:`logs-col-duration`})]}),(0,J.jsx)(`thead`,{children:(0,J.jsxs)(`tr`,{children:[(0,J.jsx)(`th`,{children:t(`logs.col.time`)}),(0,J.jsx)(`th`,{className:`num log-col-tokens`,children:t(`logs.col.tokens`)}),(0,J.jsx)(`th`,{className:`num log-col-rate`,title:t(`logs.metric.tokPerSecTitle`),children:t(`logs.col.tokPerSec`)}),(0,J.jsx)(`th`,{className:`num log-col-cost`,title:t(`logs.metric.estimatedCostTitle`),children:t(`logs.col.estimatedCost`)}),(0,J.jsx)(`th`,{className:`log-col-model`,children:t(`logs.col.model`)}),(0,J.jsx)(`th`,{children:t(`logs.col.effort`)}),(0,J.jsx)(`th`,{children:t(`logs.col.provider`)}),(0,J.jsx)(`th`,{children:t(`logs.col.status`)}),(0,J.jsx)(`th`,{children:t(`logs.col.request`)}),(0,J.jsx)(`th`,{className:`num log-col-duration`,children:t(`logs.col.duration`)})]})}),(0,J.jsxs)(`tbody`,{children:[G>0&&(0,J.jsx)(`tr`,{children:(0,J.jsx)(`td`,{colSpan:10,className:`logs-virtual-spacer`,style:{height:G}})}),ee.map(e=>{let r=H[H.length-1-e.index],i=zm(r),a=Qm(r.timestamp,w,T);return(0,J.jsxs)(`tr`,{"data-index":e.index,ref:W.measureElement,children:[(0,J.jsx)(`td`,{className:`muted mono log-col-time`,children:(0,J.jsxs)(`span`,{className:`logs-stack-start`,children:[(0,J.jsx)(`span`,{children:a.date}),(0,J.jsx)(`span`,{children:a.time})]})}),(0,J.jsx)(`td`,{className:`num mono log-col-tokens`,title:Om(r,t),children:(()=>{let e=Lm(r),{read:i,write:a}=Dm(r);return e===void 0?(0,J.jsx)(`span`,{className:`muted`,children:t(`logs.tokens.${r.usageStatus??`unreported`}`)}):(0,J.jsxs)(`span`,{className:`logs-stack-end`,children:[(0,J.jsxs)(`span`,{children:[r.usageStatus===`estimated`?`~`:``,Rn(e,n)]}),i!==void 0&&i>0&&(0,J.jsxs)(`span`,{className:`muted text-caption leading-tight`,children:[`c `,Rn(i,n)]}),a!==void 0&&a>0&&(0,J.jsxs)(`span`,{className:`muted text-caption leading-tight`,children:[`w `,Rn(a,n)]}),r.usageStatus===`estimated`&&i===void 0&&a===void 0&&(0,J.jsx)(`span`,{className:`muted text-caption leading-tight`,children:t(Em(r.provider)?`logs.tokens.noCacheCursor`:`logs.tokens.noCache`)})]})})()}),(0,J.jsx)(`td`,{className:`num mono log-col-rate`,children:Bm(r.displayMetrics?.tokPerSecond,w)}),(0,J.jsx)(`td`,{className:`num mono log-col-cost`,children:wm(r.displayMetrics?.cost,t,w)}),(0,J.jsx)(`td`,{className:`mono log-col-model`,title:bm(r,t),children:(0,J.jsxs)(`span`,{className:`logs-model-cell`,children:[(0,J.jsx)(`span`,{children:fl(r.resolvedModel??r.model)}),r.shadowCallRewrittenFrom&&(0,J.jsx)(`span`,{className:`badge badge-muted`,style:{whiteSpace:`nowrap`},title:t(`logs.badge.interceptedHelperTitle`),children:t(`logs.badge.interceptedHelper`,{model:r.shadowCallRewrittenFrom})}),(r.surface===`claude`||r.surface===`claude-desktop`)&&(0,J.jsx)(`span`,{className:`badge badge-accent`,children:t(`logs.badge.claude`)}),r.surface===`grok`&&(0,J.jsx)(`span`,{className:`badge badge-accent`,children:t(`logs.badge.grok`)}),xm(r)&&(0,J.jsx)(`span`,{className:`badge badge-amber`,children:xm(r)})]})}),(0,J.jsx)(`td`,{className:`mono log-reasoning-cell`,title:i,children:Rm(r)}),(0,J.jsx)(`td`,{className:`muted`,children:jn(r.provider,t)}),(0,J.jsx)(`td`,{children:(0,J.jsxs)(`span`,{className:`log-status-cell`,children:[(0,J.jsx)(`span`,{className:`mono font-semibold`,style:{color:Zm(r.status)},children:r.status}),(0,J.jsx)(`button`,{type:`button`,className:`log-detail-btn`,onClick:()=>u(r),"aria-label":`${t(`logs.details`)}: ${r.requestId??r.status}`,children:t(`logs.details`)})]})}),(0,J.jsx)(`td`,{className:`muted mono`,children:(0,J.jsx)(`span`,{className:`log-reqid`,title:r.requestId,children:r.requestId??`-`})}),(0,J.jsxs)(`td`,{className:`num log-col-duration`,children:[r.durationMs,`ms`]})]},e.key)}),K>0&&(0,J.jsx)(`tr`,{children:(0,J.jsx)(`td`,{colSpan:10,className:`logs-virtual-spacer`,style:{height:K}})})]})]})})}),l&&(0,J.jsx)(rh,{detail:l,detailInfo:B,localeCode:n,localeTag:w,serverTimeZone:T,t,onClose:()=>u(null),onFilterConversation:e=>{g(e),u(null)}})]})]})}function nh(e){let t=(0,_.useRef)(null);return(0,_.useEffect)(()=>{let n=t.current;n&&(e&&!n.open?n.showModal():!e&&n.open&&n.close())},[e]),t}function rh({detail:e,detailInfo:t,localeCode:n,localeTag:r,serverTimeZone:i,t:a,onClose:o,onFilterConversation:s}){let c=nh(!0),[l,u]=(0,_.useState)(!1),d=Dm(e),f=e.displayMetrics?.cost,p=zm(e),m=async()=>{if(e.requestId)try{await navigator.clipboard.writeText(e.requestId),u(!0),window.setTimeout(()=>u(!1),1200)}catch{}};return(0,J.jsxs)(`dialog`,{ref:c,className:`modal-overlay`,"aria-labelledby":`log-detail-title`,onCancel:e=>{e.preventDefault(),o()},children:[(0,J.jsx)(`button`,{type:`button`,className:`modal-backdrop-dismiss`,"aria-label":a(`common.close`),tabIndex:-1,onClick:o}),(0,J.jsxs)(`div`,{className:`modal-card log-detail-card`,onClick:e=>e.stopPropagation(),role:`document`,children:[(0,J.jsxs)(`div`,{className:`modal-head`,children:[(0,J.jsxs)(`h3`,{id:`log-detail-title`,children:[(0,J.jsx)(`span`,{className:`mono`,style:{color:Zm(e.status)},children:e.status}),t&&(0,J.jsx)(`span`,{className:`logs-detail-info`,children:t.label})]}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:o,"aria-label":a(`common.cancel`),children:(0,J.jsx)(de,{})})]}),t&&(0,J.jsx)(`p`,{className:`modal-desc`,children:t.description}),(0,J.jsxs)(`section`,{className:`log-detail-section`,"aria-labelledby":`log-detail-basic`,children:[(0,J.jsx)(`h4`,{id:`log-detail-basic`,className:`log-detail-section-title`,children:a(`logs.detail.section.basic`)}),(0,J.jsxs)(`div`,{className:`log-detail-grid`,children:[(0,J.jsx)(`span`,{className:`muted`,children:a(`logs.col.time`)}),(0,J.jsx)(`span`,{className:`mono`,children:$m(e.timestamp,r,i)}),(0,J.jsx)(`span`,{className:`muted`,children:a(`logs.col.request`)}),(0,J.jsxs)(`span`,{className:`log-detail-request-row`,children:[(0,J.jsx)(`span`,{className:`mono log-detail-break`,children:e.requestId??`—`}),e.requestId&&(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:()=>void m(),children:a(l?`logs.detail.copied`:`logs.detail.copyRequestId`)})]}),e.conversationId&&(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)(`span`,{className:`muted`,children:a(`logs.detail.conversation`)}),(0,J.jsxs)(`span`,{className:`log-detail-request-row`,children:[(0,J.jsx)(`span`,{className:`mono log-detail-break`,children:e.conversationId}),s&&(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:()=>s(e.conversationId),children:a(`logs.filter.conversation.apply`)})]})]}),(0,J.jsx)(`span`,{className:`muted`,children:a(`logs.col.model`)}),(0,J.jsx)(`span`,{className:`mono`,children:fl(e.resolvedModel??e.model)}),(0,J.jsx)(`span`,{className:`muted`,children:a(`logs.col.provider`)}),(0,J.jsx)(`span`,{children:jn(e.provider,a)}),(e.requestedEffort||e.effectiveEffort)&&(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)(`span`,{className:`muted`,children:a(`logs.col.effort`)}),(0,J.jsxs)(`span`,{className:`mono`,children:[Rm(e),p?` (${p})`:``]})]}),e.errorCode&&(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)(`span`,{className:`muted`,children:a(`logs.col.error`)}),(0,J.jsx)(`span`,{className:`mono`,children:e.errorCode})]}),e.upstreamError&&(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)(`span`,{className:`muted`,children:a(`logs.col.upstreamReason`)}),(0,J.jsx)(`span`,{className:`mono log-detail-break`,children:e.upstreamError})]})]})]}),(0,J.jsxs)(`section`,{className:`log-detail-section`,"aria-labelledby":`log-detail-route`,children:[(0,J.jsx)(`h4`,{id:`log-detail-route`,className:`log-detail-section-title`,children:a(`logs.detail.route.section`)}),e.routeDecision?(0,J.jsxs)(`div`,{className:`log-detail-grid`,children:[(0,J.jsx)(`span`,{className:`muted`,children:a(`logs.detail.route.kind`)}),(0,J.jsx)(`span`,{className:`mono`,children:e.routeDecision.routeKind??`–`}),e.routeDecision.profile?.id&&(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)(`span`,{className:`muted`,children:a(`logs.detail.route.profile`)}),(0,J.jsxs)(`span`,{className:`mono`,children:[e.routeDecision.profile.id,` (`,e.routeDecision.profile.revision,`)`]})]}),e.routeDecision.selected?.provider&&(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)(`span`,{className:`muted`,children:a(`logs.detail.route.selected`)}),(0,J.jsxs)(`span`,{className:`mono`,children:[e.routeDecision.selected.provider,`/`,e.routeDecision.selected.model,e.routeDecision.selected.reason?` — ${e.routeDecision.selected.reason}`:``]})]}),(0,J.jsx)(`span`,{className:`muted`,children:a(`logs.detail.route.candidates`)}),(0,J.jsx)(`span`,{className:`mono`,children:(e.routeDecision.candidates??[]).map(e=>`${typeof e.provider==`string`&&e.provider.length>0?e.provider:`–`}/${typeof e.model==`string`&&e.model.length>0?e.model:`–`}${e.eligible===!0?` ✓`:e.eligible===!1?` ✗`:` ?`}`).join(` `)||`–`})]}):(0,J.jsx)(`p`,{className:`log-detail-notes-line muted`,children:a(`logs.detail.route.unknown`)})]}),(0,J.jsxs)(`section`,{className:`log-detail-section`,"aria-labelledby":`log-detail-performance`,children:[(0,J.jsx)(`h4`,{id:`log-detail-performance`,className:`log-detail-section-title`,children:a(`logs.detail.section.performance`)}),(0,J.jsxs)(`div`,{className:`log-detail-grid`,children:[(0,J.jsx)(`span`,{className:`muted`,children:a(`logs.col.duration`)}),(0,J.jsxs)(`span`,{className:`mono`,children:[e.durationMs,`ms`]}),(0,J.jsx)(`span`,{className:`muted`,children:a(`logs.col.tokPerSec`)}),(0,J.jsx)(`span`,{className:`mono`,children:Bm(e.displayMetrics?.tokPerSecond,r)}),e.firstOutputMs!==void 0&&(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)(`span`,{className:`muted`,children:a(`logs.detail.ttft`)}),(0,J.jsxs)(`span`,{className:`mono`,children:[e.firstOutputMs,`ms`]})]})]}),e.displayMetrics?.tokPerSecond.kind===`unavailable`&&(0,J.jsx)(`p`,{className:`log-detail-notes-line muted`,children:a(qm(e.displayMetrics.tokPerSecond.reason))})]}),(0,J.jsxs)(`section`,{className:`log-detail-section`,"aria-labelledby":`log-detail-cost`,children:[(0,J.jsx)(`h4`,{id:`log-detail-cost`,className:`log-detail-section-title`,children:a(`logs.detail.section.cost`)}),(0,J.jsx)(`p`,{className:`log-detail-notes-line muted`,children:a(`usage.cost.disclaimer`)}),f?.kind===`value`?(0,J.jsxs)(J.Fragment,{children:[(0,J.jsxs)(`div`,{className:`log-detail-grid`,children:[(0,J.jsx)(`span`,{className:`muted`,children:a(`logs.detail.costTotal`)}),(0,J.jsx)(`span`,{className:`mono`,children:Cm(f.estimate.cost.total,a,r,f.estimate.priorityLowerBound)}),(0,J.jsx)(`span`,{className:`muted`,children:a(`logs.tokens.input`)}),(0,J.jsx)(`span`,{className:`mono`,children:Cm(f.estimate.cost.input,a,r,f.estimate.priorityLowerBound)}),(0,J.jsx)(`span`,{className:`muted`,children:a(`logs.tokens.cacheRead`)}),(0,J.jsx)(`span`,{className:`mono`,children:Cm(f.estimate.cost.cacheRead,a,r,f.estimate.priorityLowerBound)}),(0,J.jsx)(`span`,{className:`muted`,children:a(`logs.tokens.cacheWrite`)}),(0,J.jsx)(`span`,{className:`mono`,children:Cm(f.estimate.cost.cacheWrite,a,r,f.estimate.priorityLowerBound)}),(0,J.jsx)(`span`,{className:`muted`,children:a(`logs.tokens.output`)}),(0,J.jsx)(`span`,{className:`mono`,children:Cm(f.estimate.cost.output,a,r,f.estimate.priorityLowerBound)}),f.estimate.price&&(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)(`span`,{className:`muted`,children:a(`logs.detail.matchedKey`)}),(0,J.jsxs)(`span`,{className:`mono log-detail-break`,children:[f.estimate.price.jawcodeProvider??f.estimate.price.provider,`/`,f.estimate.price.modelId]}),(0,J.jsx)(`span`,{className:`muted`,children:a(`logs.detail.priceSource`)}),(0,J.jsxs)(`span`,{children:[a(`logs.detail.source.${f.estimate.price.source}`),` · `,a(Xm(f.estimate.price.status))]})]})]}),f.estimateReasons.length>0&&(0,J.jsx)(`ul`,{className:`log-detail-notes`,children:f.estimateReasons.map(e=>(0,J.jsx)(`li`,{children:a(Jm(e))},e))})]}):(0,J.jsxs)(`div`,{className:`log-detail-grid`,children:[(0,J.jsx)(`span`,{className:`muted`,children:a(`logs.detail.costTotal`)}),(0,J.jsx)(`span`,{className:`mono`,children:a(`logs.cost.unavailable`)}),(0,J.jsx)(`span`,{className:`muted`,children:a(`logs.detail.unavailableReason`)}),(0,J.jsx)(`span`,{children:f?.kind===`unavailable`?a(qm(f.reason)):a(`logs.detail.reason.usage_missing`)})]})]}),e.attempts?.length?(0,J.jsxs)(`section`,{className:`log-detail-section`,"aria-labelledby":`log-detail-attempts`,children:[(0,J.jsx)(`h4`,{id:`log-detail-attempts`,className:`log-detail-section-title`,children:a(`logs.detail.section.attempts`)}),(0,J.jsx)(`p`,{className:`log-detail-notes-line muted`,children:a(`logs.detail.attempt.e2eNote`)}),(0,J.jsx)(`div`,{className:`log-detail-attempts-wrap`,children:(0,J.jsxs)(`table`,{className:`tbl log-detail-attempts`,children:[(0,J.jsx)(`thead`,{children:(0,J.jsxs)(`tr`,{children:[(0,J.jsx)(`th`,{className:`num`,children:`#`}),(0,J.jsx)(`th`,{children:a(`logs.detail.attempt.target`)}),(0,J.jsx)(`th`,{className:`num`,children:a(`logs.col.duration`)}),(0,J.jsx)(`th`,{className:`num`,children:a(`logs.col.tokPerSec`)}),(0,J.jsx)(`th`,{className:`num`,children:a(`logs.col.estimatedCost`)}),(0,J.jsx)(`th`,{children:a(`logs.detail.attempt.reason`)})]})}),(0,J.jsx)(`tbody`,{children:e.attempts.toSorted((e,t)=>e.ordinal-t.ordinal).map(e=>{let t=e.displayMetrics?.cost,n=zm(e),i=t?.kind===`value`?t.estimate.price:void 0,o=e.errorCode??(e.recoveryKinds.length?e.recoveryKinds.map(e=>a(Ym(e))).join(`, `):void 0)??(t?.kind===`unavailable`?a(qm(t.reason)):a(`logs.detail.attempt.completed`));return(0,J.jsxs)(`tr`,{children:[(0,J.jsx)(`td`,{className:`num mono`,children:e.ordinal}),(0,J.jsxs)(`td`,{children:[(0,J.jsx)(`span`,{children:jn(e.provider,a)}),(0,J.jsx)(`br`,{}),(0,J.jsx)(`span`,{className:`mono muted log-detail-break`,children:e.model}),(e.requestedEffort||e.effectiveEffort)&&(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)(`br`,{}),(0,J.jsxs)(`span`,{className:`mono muted text-caption log-detail-break`,children:[Rm(e),n?` (${n})`:``]})]}),i&&(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)(`br`,{}),(0,J.jsxs)(`span`,{className:`muted text-caption log-detail-break`,children:[i.jawcodeProvider??i.provider,`/`,i.modelId,` · `,a(`logs.detail.source.${i.source}`),` · `,a(Xm(i.status))]})]})]}),(0,J.jsxs)(`td`,{className:`num mono`,children:[e.durationMs,`ms`]}),(0,J.jsx)(`td`,{className:`num mono`,children:Bm(e.displayMetrics?.tokPerSecond,r)}),(0,J.jsx)(`td`,{className:`num mono`,children:wm(t,a,r)}),(0,J.jsx)(`td`,{className:`log-detail-break`,children:o})]},`${e.ordinal}-${e.provider}-${e.model}`)})})]})})]}):null,(0,J.jsxs)(`section`,{className:`log-detail-section`,"aria-labelledby":`log-detail-usage`,children:[(0,J.jsx)(`h4`,{id:`log-detail-usage`,className:`log-detail-section-title`,children:a(`logs.detail.section.usage`)}),(0,J.jsxs)(`div`,{className:`log-detail-grid`,children:[(0,J.jsx)(`span`,{className:`muted`,children:a(`logs.tokens.input`)}),(0,J.jsx)(`span`,{className:`mono`,children:e.usage?Rn(e.usage.inputTokens,n):`—`}),(0,J.jsx)(`span`,{className:`muted`,children:a(`logs.tokens.output`)}),(0,J.jsx)(`span`,{className:`mono`,children:e.usage?Rn(e.usage.outputTokens,n):`—`}),(0,J.jsx)(`span`,{className:`muted`,children:a(`logs.tokens.cacheRead`)}),(0,J.jsx)(`span`,{className:`mono`,children:d.read===void 0?`—`:Rn(d.read,n)}),(0,J.jsx)(`span`,{className:`muted`,children:a(`logs.tokens.cacheWrite`)}),(0,J.jsx)(`span`,{className:`mono`,children:d.write===void 0?`—`:Rn(d.write,n)}),(0,J.jsx)(`span`,{className:`muted`,children:a(`logs.tokens.reasoning`)}),(0,J.jsx)(`span`,{className:`mono`,children:e.usage?.reasoningOutputTokens===void 0?`—`:Rn(e.usage.reasoningOutputTokens,n)}),(0,J.jsx)(`span`,{className:`muted`,children:a(`logs.detail.totalTokens`)}),(0,J.jsx)(`span`,{className:`mono`,children:Lm(e)===void 0?`—`:Rn(Lm(e),n)}),e.usage?.contextTotalTokens!==void 0&&(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)(`span`,{className:`muted`,children:a(`logs.tokens.contextTotal`)}),(0,J.jsx)(`span`,{className:`mono`,children:Rn(e.usage.contextTotalTokens,n)})]})]}),e.usageStatus===`estimated`&&(0,J.jsx)(`p`,{className:`log-detail-notes-line muted`,children:a(`logs.tokens.estimatedNote`)})]}),(0,J.jsxs)(`details`,{className:`log-detail-raw`,children:[(0,J.jsx)(`summary`,{children:a(`logs.detailRaw`)}),(0,J.jsx)(`pre`,{className:`log-detail-json`,children:JSON.stringify(e,null,2)})]})]})]})}function ih(e){return`${Math.round(e*100)}%`}function ah(e,t){let n=`${t}/${e}`,r=0;for(let e=0;e>>0;return`hsl(${r%360} 55% 55%)`}function oh(e){let t=new Map(e.map(e=>[e.date,e])),n=[],r=new Date;r.setHours(0,0,0,0),r.setDate(r.getDate()-6);for(let e=0;e<7;e++){let e=`${r.getFullYear()}-${String(r.getMonth()+1).padStart(2,`0`)}-${String(r.getDate()).padStart(2,`0`)}`,i=t.get(e);n.push({date:e,requests:i?.requests??0,measuredRequests:i?.measuredRequests??0,reportedRequests:i?.reportedRequests??0,totalTokens:i?.totalTokens??0,models:i?.models??[]}),r.setDate(r.getDate()+1)}return n}function sh(e){let t=e.filter(e=>e>0).sort((e,t)=>e-t);if(t.length===0)return[0,0,0,0];let n=e=>t[Math.min(t.length-1,Math.floor(e*t.length))];return[n(.25),n(.5),n(.75),n(.95)]}function ch(e,t){return e<=0?0:e<=t[0]?1:e<=t[1]?2:e<=t[2]?3:4}function lh(e){let t=sh(e.map(e=>e.totalTokens)),n=new Map(e.map(e=>[e.date,e])),r=new Date;r.setHours(0,0,0,0);let i=new Date(r);i.setDate(i.getDate()-364),i.setDate(i.getDate()-i.getDay());let a=[],o=[],s=[`Jan`,`Feb`,`Mar`,`Apr`,`May`,`Jun`,`Jul`,`Aug`,`Sep`,`Oct`,`Nov`,`Dec`],c=-4,l=-1,u=[],d=new Date(i);for(;d<=r;){let e=`${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,`0`)}-${String(d.getDate()).padStart(2,`0`)}`,r=d.getMonth();d.getDay()===0&&r!==l&&a.length-c>=4&&(o.push({label:s[r],col:a.length}),c=a.length,l=r);let i=n.get(e);u.push({date:e,requests:i?.requests??0,totalTokens:i?.totalTokens??0,level:i?ch(i.totalTokens,t):0,dayOfWeek:d.getDay()}),d.getDay()===6&&(a.push(u),u=[]),d.setDate(d.getDate()+1)}if(u.length>0){for(;u.length<7;)u.push({date:``,requests:0,totalTokens:0,level:0,dayOfWeek:u.length});a.push(u)}return{weeks:a,months:o,buckets:t}}function uh({surface:e,range:t,onSurface:n,onRange:r,t:i}){return(0,J.jsxs)(`div`,{className:`usage-filters`,children:[(0,J.jsx)(`div`,{className:`usage-segmented`,role:`group`,"aria-label":i(`logs.filter.surface.label`),children:[`all`,`codex`,`claude`,`grok`].map(t=>{let r=i(`logs.filter.surface.${t}`);return(0,J.jsxs)(`button`,{type:`button`,className:`usage-segmented-btn usage-source-btn${e===t?` active`:``}`,"aria-label":r,"aria-pressed":e===t,onClick:()=>n(t),children:[t===`codex`&&(0,J.jsx)(`img`,{className:`usage-source-mark`,src:`/provider-icons/openai.svg`,alt:``,"aria-hidden":`true`}),t===`claude`&&(0,J.jsx)(`img`,{className:`usage-source-mark`,src:`/provider-icons/claude-color.svg`,alt:``,"aria-hidden":`true`}),t===`grok`&&(0,J.jsx)(`img`,{className:`usage-source-mark usage-source-mark--mono`,src:`/provider-icons/grok.svg`,alt:``,"aria-hidden":`true`}),(0,J.jsx)(`span`,{className:t===`all`?`usage-source-label`:`usage-source-label usage-source-label-collapsible`,children:r})]},t)})}),(0,J.jsx)(`div`,{className:`usage-segmented`,role:`group`,"aria-label":i(`usage.title`),children:[`all`,`30d`,`7d`].map(e=>{let n=i(e===`all`?`usage.range.available`:`usage.range.${e}`);return(0,J.jsx)(`button`,{type:`button`,className:`usage-segmented-btn${t===e?` active`:``}`,"aria-label":n,"aria-pressed":t===e,onClick:()=>r(e),children:n},e)})})]})}function dh({summary:e,activeDays:t,locale:n,t:r}){return(0,J.jsxs)(J.Fragment,{children:[(0,J.jsxs)(`div`,{className:`usage-cards usage-cards-3x2`,role:`group`,"aria-label":r(`usage.title`),children:[(0,J.jsxs)(`div`,{className:`stat`,children:[(0,J.jsx)(`div`,{className:`muted`,children:r(`usage.card.requests`)}),(0,J.jsx)(`div`,{className:`stat-value`,children:e.requests})]}),(0,J.jsxs)(`div`,{className:`stat`,children:[(0,J.jsx)(`div`,{className:`muted`,children:r(`usage.card.measured`)}),(0,J.jsx)(`div`,{className:`stat-value`,children:e.measuredRequests})]}),(0,J.jsxs)(`div`,{className:`stat`,children:[(0,J.jsx)(`div`,{className:`muted`,children:r(`usage.card.totalTokens`)}),(0,J.jsx)(`div`,{className:`stat-value`,children:Rn(e.totalTokens,n)})]}),(0,J.jsxs)(`div`,{className:`stat`,title:r(`usage.card.cachedTokensHint`),children:[(0,J.jsx)(`div`,{className:`muted`,children:r(`usage.card.cachedTokens`)}),(0,J.jsx)(`div`,{className:`stat-value`,children:Rn(e.cacheReadInputTokens??e.cachedInputTokens,n)}),(e.cacheCreationInputTokens??0)>0&&(0,J.jsxs)(`div`,{className:`muted text-caption`,children:[r(`usage.card.cacheWriteTokens`),`: `,Rn(e.cacheCreationInputTokens??0,n)]})]}),(0,J.jsxs)(`div`,{className:`stat`,children:[(0,J.jsx)(`div`,{className:`muted`,children:r(`usage.card.coverage`)}),(0,J.jsx)(`div`,{className:`stat-value`,children:ih(e.coverageRatio)})]}),(0,J.jsxs)(`div`,{className:`stat`,children:[(0,J.jsx)(`div`,{className:`muted`,children:r(`usage.card.activeDays`)}),(0,J.jsx)(`div`,{className:`stat-value`,children:t})]})]}),e.estimatedCostUsd!==void 0&&(0,J.jsxs)(`div`,{className:`usage-cost-row`,role:`note`,children:[(0,J.jsx)(`span`,{className:`muted`,children:r(`usage.cost.total`)}),(0,J.jsx)(`span`,{className:`stat-value mono usage-cost-value`,children:os(e.estimatedCostUsd,n)}),(0,J.jsx)(`span`,{className:`muted text-caption`,children:r(`usage.cost.disclaimer`)}),(e.unpricedRequests??0)+(e.unmeteredRequests??0)>0&&(0,J.jsx)(`span`,{className:`muted text-caption`,children:r(`usage.cost.unpricedNote`).replace(`{count}`,String((e.unpricedRequests??0)+(e.unmeteredRequests??0)))})]})]})}function fh({weekBars:e,locale:t,t:n}){let[r,i]=(0,_.useState)(null),a=Math.max(1,...e.map(e=>e.totalTokens));return(0,J.jsx)(`div`,{className:`daybars`,role:`img`,"aria-label":n(`usage.section.heatmap`),children:e.map(e=>{let n=Math.round(e.totalTokens/a*100),o=e.date.slice(5);return(0,J.jsxs)(`div`,{className:`daybar`,onMouseEnter:()=>i(e.date),onMouseLeave:()=>i(t=>t===e.date?null:t),children:[(0,J.jsx)(`div`,{className:`daybar-track`,children:(0,J.jsxs)(`div`,{className:`daybar-stack`,style:{"--daybar-scale":String(Math.max(0,Math.min(1,n/100)))},children:[e.models.map(e=>(0,J.jsx)(`div`,{className:`daybar-seg`,style:{flexGrow:e.totalTokens,background:ah(e.model,e.provider)}},`${e.provider}/${e.model}`)),e.models.length===0&&e.totalTokens>0&&(0,J.jsx)(`div`,{className:`daybar-seg`,style:{flexGrow:1,background:`var(--green)`}})]})}),r===e.date&&e.totalTokens>0&&(0,J.jsxs)(`div`,{className:`daybar-tip`,role:`tooltip`,children:[(0,J.jsx)(`div`,{className:`daybar-tip-date`,children:e.date}),e.models.slice(0,8).map(e=>(0,J.jsxs)(`div`,{className:`daybar-tip-row`,children:[(0,J.jsx)(`span`,{className:`daybar-tip-swatch`,style:{background:ah(e.model,e.provider)}}),(0,J.jsx)(`span`,{className:`daybar-tip-name`,children:fl(e.model)}),(0,J.jsx)(`span`,{className:`daybar-tip-val`,children:Rn(e.totalTokens,t)})]},`${e.provider}/${e.model}`))]}),(0,J.jsx)(`span`,{className:`daybar-count`,children:Rn(e.totalTokens,t)}),(0,J.jsx)(`span`,{className:`daybar-label muted`,children:o})]},e.date)})})}function ph({range:e,heatmap:t,weekBars:n,locale:r,t:i}){let a=(0,_.useRef)(null),[o,s]=(0,_.useState)(null);return(0,_.useEffect)(()=>{let e=a.current;if(!e)return;let t=()=>{e.scrollLeft=e.scrollWidth};t();let n=new ResizeObserver(t);return n.observe(e),()=>n.disconnect()},[t,e]),(0,J.jsxs)(`section`,{className:`panel`,style:{marginTop:16},"aria-labelledby":`usage-heatmap-title`,children:[(0,J.jsx)(`h3`,{id:`usage-heatmap-title`,className:`panel-title`,children:i(`usage.section.heatmap`)}),e===`7d`?(0,J.jsx)(fh,{weekBars:n,locale:r,t:i}):(0,J.jsxs)(`div`,{className:`heatmap`,ref:a,role:`img`,"aria-labelledby":`usage-heatmap-title`,children:[(0,J.jsxs)(`div`,{className:`heatmap-months`,style:{gridTemplateColumns:`28px repeat(${t.weeks.length}, calc(var(--hm-cell) + var(--hm-gap)))`},children:[(0,J.jsx)(`span`,{className:`heatmap-day-spacer`}),t.months.map(e=>(0,J.jsx)(`span`,{className:`heatmap-month`,style:{gridColumn:e.col+2},children:e.label},`${e.label}-${e.col}`))]}),(0,J.jsxs)(`div`,{className:`heatmap-body`,children:[(0,J.jsxs)(`div`,{className:`heatmap-days`,children:[(0,J.jsx)(`span`,{}),(0,J.jsx)(`span`,{children:i(`usage.dayMon`)}),(0,J.jsx)(`span`,{}),(0,J.jsx)(`span`,{children:i(`usage.dayWed`)}),(0,J.jsx)(`span`,{}),(0,J.jsx)(`span`,{children:i(`usage.dayFri`)}),(0,J.jsx)(`span`,{})]}),(0,J.jsx)(`div`,{className:`heatmap-grid`,style:{gridTemplateColumns:`repeat(${t.weeks.length}, var(--hm-cell))`},children:t.weeks.map((e,t)=>(0,J.jsx)(`div`,{className:`heatmap-week`,children:e.map((e,n)=>(0,J.jsx)(`div`,{className:`heatmap-cell heatmap-cell-${e.level}`,onMouseEnter:r=>{if(!e.date)return;let i=r.currentTarget.getBoundingClientRect();s({weekIndex:t,dayIndex:n,x:i.left+i.width/2,y:i.top})},onMouseLeave:()=>s(e=>e?.weekIndex===t&&e.dayIndex===n?null:e)},e.date||`pad-${t}-${n}`))},e[0]?.date||`week-${t}`))})]}),o&&(()=>{let e=t.weeks[o.weekIndex]?.[o.dayIndex];return e?.date?(0,J.jsxs)(`div`,{className:`heatmap-tip`,role:`tooltip`,style:{left:o.x,top:o.y},children:[(0,J.jsx)(`div`,{className:`heatmap-tip-date`,children:e.date}),(0,J.jsx)(`div`,{className:`heatmap-tip-val`,children:i(`usage.heatmap.tooltipTokens`,{tokens:Rn(e.totalTokens,r)})}),(0,J.jsx)(`div`,{className:`heatmap-tip-req muted`,children:i(`usage.heatmap.tooltipRequests`,{requests:e.requests})})]}):null})(),(0,J.jsxs)(`div`,{className:`heatmap-legend muted`,children:[(0,J.jsx)(`span`,{children:i(`usage.heatmap.less`)}),[0,1,2,3,4].map(e=>(0,J.jsx)(`span`,{className:`heatmap-cell heatmap-cell-${e}`},e)),(0,J.jsx)(`span`,{children:i(`usage.heatmap.more`)})]})]})]})}function mh({title:e,titleId:t,children:n}){return(0,J.jsxs)(`section`,{className:`usw-section`,"aria-labelledby":t,children:[(0,J.jsx)(`h3`,{id:t,className:`h-section`,children:e}),n]})}function hh({models:e,modelQuery:t,onModelQuery:n,locale:r,t:i,workspace:a=!1}){let o=i(`usage.search.models`),s=i(`usage.section.models`),c=`usage-models-title`,l=(0,J.jsx)(`input`,{className:`input`,"aria-label":o,placeholder:o,value:t,onChange:e=>n(e.target.value)}),u=(0,J.jsx)(`div`,{className:`tbl-wrap`,children:(0,J.jsxs)(`table`,{className:`tbl`,children:[(0,J.jsx)(`thead`,{children:(0,J.jsxs)(`tr`,{children:[(0,J.jsx)(`th`,{children:i(`logs.col.model`)}),(0,J.jsx)(`th`,{children:i(`logs.col.provider`)}),(0,J.jsx)(`th`,{className:`num`,children:i(`usage.col.requests`)}),(0,J.jsx)(`th`,{className:`num`,children:i(`usage.col.measured`)}),(0,J.jsx)(`th`,{className:`num`,children:i(`usage.col.tokens`)}),(0,J.jsx)(`th`,{children:i(`usage.col.share`)})]})}),(0,J.jsx)(`tbody`,{children:e.map(e=>(0,J.jsxs)(`tr`,{children:[(0,J.jsx)(`td`,{className:`mono`,children:fl(e.model)}),(0,J.jsx)(`td`,{className:`muted`,children:jn(e.provider,i)}),(0,J.jsx)(`td`,{className:`num`,children:e.requests}),(0,J.jsx)(`td`,{className:`num`,children:e.measuredRequests}),(0,J.jsx)(`td`,{className:`num mono`,children:Rn(e.totalTokens,r)}),(0,J.jsx)(`td`,{children:(0,J.jsx)(`div`,{className:`usage-bar`,children:(0,J.jsx)(`div`,{className:`usage-bar-fill`,style:{width:`${Math.round(e.shareRatio*100)}%`}})})})]},`${e.provider}/${e.model}`))})]})});return a?(0,J.jsxs)(mh,{title:s,titleId:c,children:[(0,J.jsx)(`div`,{className:`usw-section-toolbar`,children:l}),u]}):(0,J.jsxs)(`section`,{className:`panel`,style:{marginTop:16},"aria-labelledby":c,children:[(0,J.jsxs)(`div`,{className:`panel-head`,children:[(0,J.jsx)(`h3`,{id:c,className:`panel-title`,children:s}),l]}),u]})}function gh({providers:e,locale:t,t:n,workspace:r=!1}){let i=n(`usage.section.providers`),a=`usage-providers-title`,o=(0,J.jsx)(`div`,{className:`tbl-wrap`,children:(0,J.jsxs)(`table`,{className:`tbl`,children:[(0,J.jsx)(`thead`,{children:(0,J.jsxs)(`tr`,{children:[(0,J.jsx)(`th`,{children:n(`logs.col.provider`)}),(0,J.jsx)(`th`,{className:`num`,children:n(`usage.col.requests`)}),(0,J.jsx)(`th`,{className:`num`,children:n(`usage.col.measured`)}),(0,J.jsx)(`th`,{className:`num`,children:n(`usage.col.tokens`)}),(0,J.jsx)(`th`,{children:n(`usage.col.share`)})]})}),(0,J.jsx)(`tbody`,{children:e.map(e=>(0,J.jsxs)(`tr`,{children:[(0,J.jsx)(`td`,{className:`mono`,children:jn(e.provider,n)}),(0,J.jsx)(`td`,{className:`num`,children:e.requests}),(0,J.jsx)(`td`,{className:`num`,children:e.measuredRequests}),(0,J.jsx)(`td`,{className:`num mono`,children:Rn(e.totalTokens,t)}),(0,J.jsx)(`td`,{children:(0,J.jsx)(`div`,{className:`usage-bar`,children:(0,J.jsx)(`div`,{className:`usage-bar-fill`,style:{width:`${Math.round(e.shareRatio*100)}%`}})})})]},e.provider))})]})});return r?(0,J.jsx)(mh,{title:i,titleId:a,children:o}):(0,J.jsxs)(`section`,{className:`panel`,style:{marginTop:16},"aria-labelledby":a,children:[(0,J.jsx)(`h3`,{id:a,className:`panel-title`,children:i}),o]})}function _h({summary:e,t,workspace:n=!1}){let r=t(`usage.section.coverage`),i=`usage-coverage-title`,a=(0,J.jsxs)(J.Fragment,{children:[(0,J.jsxs)(`div`,{className:`usage-cards usage-cards-3x2`,children:[(0,J.jsxs)(`div`,{className:`stat`,children:[(0,J.jsx)(`div`,{className:`muted`,children:t(`usage.coverage.measured`)}),(0,J.jsx)(`div`,{className:`stat-value`,children:e.measuredRequests})]}),(0,J.jsxs)(`div`,{className:`stat`,children:[(0,J.jsx)(`div`,{className:`muted`,children:t(`usage.coverage.reported`)}),(0,J.jsx)(`div`,{className:`stat-value`,children:e.reportedRequests})]}),(0,J.jsxs)(`div`,{className:`stat`,children:[(0,J.jsx)(`div`,{className:`muted`,children:t(`usage.coverage.estimated`)}),(0,J.jsx)(`div`,{className:`stat-value`,children:e.estimatedRequests})]}),(0,J.jsxs)(`div`,{className:`stat`,children:[(0,J.jsx)(`div`,{className:`muted`,children:t(`logs.tokens.unreported`)}),(0,J.jsx)(`div`,{className:`stat-value`,children:e.unreportedRequests})]}),(0,J.jsxs)(`div`,{className:`stat`,children:[(0,J.jsx)(`div`,{className:`muted`,children:t(`logs.tokens.unsupported`)}),(0,J.jsx)(`div`,{className:`stat-value`,children:e.unsupportedRequests})]})]}),(0,J.jsx)(`p`,{className:`muted text-control`,style:{marginTop:12},children:t(`usage.coverage.note`)})]});return n?(0,J.jsx)(mh,{title:r,titleId:i,children:a}):(0,J.jsxs)(`section`,{className:`panel`,style:{marginTop:16},"aria-labelledby":i,children:[(0,J.jsx)(`h3`,{id:i,className:`panel-title`,children:r}),a]})}function vh({data:e,heatmap:t,weekBars:n,activeDays:r,filteredModels:i,modelQuery:a,onModelQuery:o,sortedProviders:s,range:c,locale:l,t:u}){let d=!!e&&e.summary.requests===0,f=[{id:`overview`,label:u(`usage.section.overview`),meta:e?`${e.summary.requests}`:`—`,body:e?(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)(dh,{summary:e.summary,activeDays:r,locale:l,t:u}),(0,J.jsx)(ph,{range:c,heatmap:t,weekBars:n,locale:l,t:u})]}):null},{id:`models`,label:u(`usage.section.models`),meta:e?`${e.models.length}`:`—`,body:e?(0,J.jsx)(hh,{models:i,modelQuery:a,onModelQuery:o,locale:l,t:u,workspace:!0}):null},{id:`providers`,label:u(`usage.section.providers`),meta:e?`${e.providers.length}`:`—`,body:e?(0,J.jsx)(gh,{providers:s,locale:l,t:u,workspace:!0}):null},{id:`coverage`,label:u(`usage.section.coverage`),meta:e?ih(e.summary.coverageRatio):`—`,body:e?(0,J.jsx)(_h,{summary:e.summary,t:u,workspace:!0}):null}];return(0,J.jsx)(`div`,{className:`usage-workspace-shell`,children:(0,J.jsxs)(`div`,{className:`usage-workspace-root`,children:[(0,J.jsx)(yp,{scope:`usage`,ariaLabel:u(`usage.workspace.sections`),items:f.map(e=>({id:e.id,label:e.label,meta:e.meta}))}),(0,J.jsx)(`section`,{className:`usage-workspace-main`,"aria-label":u(`usage.workspace.report`),children:d?(0,J.jsx)(Ot,{title:u(`usage.empty`)}):f.map(e=>(0,J.jsx)(`div`,{id:gp(`usage`,e.id),className:`usw-body usw-section-block`,children:e.body},e.id))})]})})}var yh=new Map;function bh(e,t,n,r,i,a){return`ocx.usage.v2:${e}:${r?`connected`:`standalone`}:${i}:${a??``}:${t}:${n}`}function xh(e,t,n,r,i,a){let o=bh(e,t,n,r,i,a);return yh.get(o)??gr(o)}function Sh(e,t,n,r,i,a,o){let s=bh(e,t,n,r,i,a);yh.set(s,o),br(s,o)}function Ch({apiBase:e,connected:t=!1,apiKeyId:n}){let{t:r,locale:i}=ct(),[a,o]=(0,_.useState)(`30d`),[s,c]=(0,_.useState)(`all`),[l,u]=(0,_.useState)(`machine`),[d,f]=(0,_.useState)(``),p=(0,_.useCallback)(async r=>{let i=new URLSearchParams({range:a,surface:s});t&&l===`machine`&&n&&i.set(`apiKeyId`,n);let o=await fetch(`${e}/api/usage?${i}`,{signal:r});if(!o.ok)throw Error(`${o.status} ${o.statusText}`.trim());let c=await o.json();return Sh(e,a,s,t,l,n,c),c},[e,n,t,a,l,s]),m=bh(e,a,s,t,l,n),h=xh(e,a,s,t,l,n),g=ml(m,[e,n,t,a,l,s],p,{isEmpty:()=>!1,initialData:h??void 0}),{state:v}=g,y=v.data??h??null,b=(0,_.useMemo)(()=>lh(y?.days??[]),[y?.days]),x=(0,_.useMemo)(()=>oh(y?.days??[]),[y?.days]),S=(0,_.useMemo)(()=>(y?.days??[]).filter(e=>e.requests>0).length,[y?.days]),C=(0,_.useMemo)(()=>{let e=d.trim().toLowerCase(),t=(y?.models??[]).toSorted((e,t)=>t.totalTokens-e.totalTokens);return e?t.filter(t=>t.model.toLowerCase().includes(e)||t.provider.toLowerCase().includes(e)||(t.resolvedModel??``).toLowerCase().includes(e)).slice(0,100):t.slice(0,100)},[y?.models,d]),w=(0,_.useMemo)(()=>(y?.providers??[]).toSorted((e,t)=>t.totalTokens-e.totalTokens),[y?.providers]);return(0,J.jsxs)(J.Fragment,{children:[(0,J.jsxs)(`div`,{className:`page-head usage-head`,children:[(0,J.jsx)(`h2`,{id:`usage-page-title`,children:r(`usage.title`)}),(0,J.jsx)(uh,{surface:s,range:a,onSurface:c,onRange:o,t:r})]}),(0,J.jsx)(`p`,{className:`page-sub`,children:r(`usage.subtitle`)}),t&&(0,J.jsxs)(`div`,{className:`usage-source-row`,children:[(0,J.jsx)(`span`,{children:r(`usage.source.connected`)}),(0,J.jsxs)(`div`,{className:`usage-scope-control`,role:`group`,"aria-label":r(`usage.scope.label`),children:[(0,J.jsx)(`button`,{type:`button`,className:`btn btn-sm${l===`machine`?` btn-primary`:` btn-ghost`}`,"aria-pressed":l===`machine`,onClick:()=>u(`machine`),children:r(`usage.scope.machine`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-sm${l===`hub`?` btn-primary`:` btn-ghost`}`,"aria-pressed":l===`hub`,onClick:()=>u(`hub`),children:r(`usage.scope.hub`)})]})]}),v.showSkeleton&&!y?(0,J.jsx)(gl,{label:r(`usage.loading`),rows:5}):v.kind===`failed-cold`?(0,J.jsxs)($,{tone:`err`,children:[t?r(`usage.hubOffline`):v.error instanceof Error?`${r(`usage.loadError`)} ${v.error.message}`:r(`usage.loadError`),` `,(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:()=>g.refresh(),children:r(`common.retry`)})]}):(0,J.jsxs)(J.Fragment,{children:[v.showError&&(0,J.jsx)($,{tone:`err`,children:r(t?`usage.hubOffline`:`usage.loadError`)}),y?.historyTruncated&&(0,J.jsx)($,{tone:`warn`,children:(()=>{let e=wh(y.snapshotWindowStart),t=wh(y.snapshotWindowEnd);return e!==null&&t!==null?r(`usage.historyTruncatedWindow`,{start:e,end:t}):r(`usage.historyTruncated`)})()}),(0,J.jsx)(vh,{data:y,heatmap:b,weekBars:x,activeDays:S,filteredModels:C,modelQuery:d,onModelQuery:f,sortedProviders:w,range:a,locale:i,t:r})]})]})}function wh(e){if(typeof e!=`number`||!Number.isFinite(e))return null;let t=new Date(e);return Number.isFinite(t.getTime())?t.toLocaleString():null}function Th(e,t){if(e<1024)return`${e} B`;let n=[`KiB`,`MiB`,`GiB`,`TiB`],r=e,i=-1;do r/=1024,i++;while(r>=1024&&i0,[f,p]=(0,_.useState)(!1);return(0,J.jsxs)(`div`,{className:`stw-section`,"data-testid":`codex-log-guard`,children:[(0,J.jsx)(`h3`,{className:`stw-section-title`,children:n(`storage.bucket.logs_db`)}),(0,J.jsxs)(`dl`,{className:`stw-kv`,children:[(0,J.jsxs)(`div`,{className:`stw-kv-row`,children:[(0,J.jsx)(`dt`,{children:n(`dash.status`)}),(0,J.jsxs)(`dd`,{className:`stw-kv-mono`,children:[(0,J.jsx)(`code`,{children:jh(t,e.schema.state)}),c&&e.schema.state===`unsupported`?(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)(`span`,{"aria-hidden":`true`,children:` · `}),(0,J.jsx)(`code`,{children:Dh(t,`inspectionOnly`)})]}):null]})]}),(0,J.jsxs)(`div`,{className:`stw-kv-row`,children:[(0,J.jsx)(`dt`,{children:n(`storage.bucket.logs_db`)}),(0,J.jsx)(`dd`,{className:`stw-kv-mono`,children:Th(e.files.databaseBytes,t)})]}),(0,J.jsxs)(`div`,{className:`stw-kv-row`,children:[(0,J.jsx)(`dt`,{children:(0,J.jsx)(`code`,{children:`WAL`})}),(0,J.jsx)(`dd`,{className:`stw-kv-mono`,children:Th(e.files.walBytes,t)})]}),(0,J.jsxs)(`div`,{className:`stw-kv-row`,children:[(0,J.jsx)(`dt`,{children:(0,J.jsx)(`code`,{children:`SHM`})}),(0,J.jsx)(`dd`,{className:`stw-kv-mono`,children:Th(e.files.shmBytes,t)})]}),s&&(0,J.jsxs)(J.Fragment,{children:[(0,J.jsxs)(`div`,{className:`stw-kv-row`,children:[(0,J.jsx)(`dt`,{children:n(`storage.col.rows`)}),(0,J.jsx)(`dd`,{className:`stw-kv-mono`,children:s.totalRows.toLocaleString(t)})]}),(0,J.jsxs)(`div`,{className:`stw-kv-row`,children:[(0,J.jsx)(`dt`,{children:(0,J.jsx)(`code`,{children:`TRACE`})}),(0,J.jsxs)(`dd`,{className:`stw-kv-mono`,children:[(s.traceShare*100).toFixed(1),`%`]})]}),(0,J.jsxs)(`div`,{className:`stw-kv-row`,children:[(0,J.jsx)(`dt`,{children:(0,J.jsx)(`code`,{children:`freelist`})}),(0,J.jsx)(`dd`,{className:`stw-kv-mono`,children:Th(s.reclaimableBytes,t)})]})]}),!s&&e.metricsSkipped&&(0,J.jsxs)(`div`,{className:`stw-kv-row`,"data-testid":`log-guard-metrics-skipped`,children:[(0,J.jsx)(`dt`,{children:n(`storage.col.rows`)}),(0,J.jsx)(`dd`,{className:`muted`,children:Dh(t,`metricsSkippedLarge`).replace(`{threshold}`,Th(e.metricsSkipped.thresholdBytes,t))})]}),(0,J.jsxs)(`div`,{className:`stw-kv-row`,children:[(0,J.jsx)(`dt`,{children:(0,J.jsx)(`code`,{children:`sqlite_home`})}),(0,J.jsx)(`dd`,{className:`stw-kv-mono`,children:(0,J.jsx)(`code`,{children:e.externalSqliteHome?Dh(t,`externalSqliteHome`):`CODEX_HOME`})})]})]}),l&&(0,J.jsxs)(`div`,{className:`stw-section`,"data-testid":`log-guard-protection`,children:[(0,J.jsx)(`h4`,{className:`stw-section-title`,children:Dh(t,`protection`)}),(0,J.jsxs)(`div`,{className:`stw-kv-row`,children:[(0,J.jsx)(`span`,{className:`muted`,children:(0,J.jsx)(`code`,{children:Mh(t,l.state)})}),(0,J.jsxs)(`span`,{className:`stw-kv-mono`,children:[(0,J.jsx)(`code`,{children:Nh(t,l.desiredMode)}),l.observedMode===l.desiredMode?null:(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)(`span`,{"aria-hidden":`true`,children:` · `}),(0,J.jsx)(`code`,{children:Nh(t,l.observedMode)})]})]})]}),(0,J.jsxs)(`div`,{className:`storage-policy-actions`,children:[(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,"data-testid":`log-guard-protect-compat`,disabled:u,"aria-pressed":l.desiredMode===`compat`,onClick:()=>o({action:`protect`,mode:`compat`}),children:Dh(t,`compat`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,"data-testid":`log-guard-protect-quiet`,disabled:u,"aria-pressed":l.desiredMode===`quiet`,onClick:()=>o({action:`protect`,mode:`quiet`}),children:Dh(t,`quiet`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,"data-testid":`log-guard-unprotect`,disabled:u||l.desiredMode===`off`,onClick:()=>o({action:`unprotect`}),children:Dh(t,`disable`)}),l.state===`drifted`&&(0,J.jsx)(`button`,{type:`button`,className:`btn btn-sm`,"data-testid":`log-guard-repair`,disabled:u,onClick:()=>o({action:`repair`}),children:Dh(t,`repair`)}),r&&(0,J.jsx)(`span`,{className:`muted`,role:`status`,children:kh(t,`applying`)})]}),i&&(0,J.jsx)(`p`,{className:`err`,role:`alert`,children:i})]}),d&&(0,J.jsx)(`div`,{className:`stw-section`,"data-testid":`log-guard-reclaim`,children:(0,J.jsx)(`div`,{className:`storage-policy-actions`,children:f?(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)(`button`,{type:`button`,className:`btn btn-sm`,"data-testid":`log-guard-compact-confirm`,disabled:r,onClick:()=>{p(!1),o({action:`compact`})},children:Dh(t,`confirmCompact`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,disabled:r,onClick:()=>p(!1),children:Dh(t,`cancel`)})]}):(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,"data-testid":`log-guard-compact`,disabled:r,onClick:()=>p(!0),children:Dh(t,`compact`)})})}),a&&(0,J.jsx)(`div`,{className:`stw-section`,children:(0,J.jsx)(`p`,{className:`stw-kv-mono`,role:`status`,"data-testid":`log-guard-compact-result`,children:a})}),s&&s.topTargets.length>0&&(0,J.jsxs)(`div`,{className:`stw-section`,children:[(0,J.jsx)(`h4`,{className:`stw-section-title`,children:(0,J.jsx)(`code`,{children:`target`})}),s.topTargets.slice(0,5).map(e=>(0,J.jsxs)(`div`,{className:`stw-file-row`,children:[(0,J.jsx)(`span`,{className:`stw-file-path`,title:e.target,children:(0,J.jsx)(`code`,{children:e.target})}),(0,J.jsx)(`span`,{className:`stw-file-size`,children:e.rows.toLocaleString(t)})]},e.target))]}),(0,J.jsx)(`p`,{className:`stw-hint`,children:(0,J.jsxs)(`code`,{children:[`immutable=1 · snapshot=`,e.snapshot]})})]})}function Bh({locale:e,t}){return(0,J.jsxs)(`div`,{className:`stw-section`,"data-testid":`codex-log-guard-unavailable`,children:[(0,J.jsx)(`h3`,{className:`stw-section-title`,children:t(`storage.bucket.logs_db`)}),(0,J.jsx)(`p`,{className:`stw-hint`,children:Dh(e,`inspectionUnavailable`)})]})}function Vh({report:e,locale:t,apiBase:n=``,logGuardBusy:r=!1,onLogGuardAction:i}){let a=Q(),[o,s]=(0,_.useState)(null),[c,l]=(0,_.useState)(null),[u,d]=(0,_.useState)(!1),[f,p]=(0,_.useState)(null),[m,h]=(0,_.useState)(null),g=(0,_.useMemo)(()=>e.buckets.toSorted((e,t)=>t.bytes-e.bytes),[e.buckets]),v=g.find(e=>e.key===o)??null,y=c?.generation===e.generatedAt?c.report:e.codexLogs??null,b=f?.generation===e.generatedAt?f.message:null,x=m?.generation===e.generatedAt?m.summary:null,S=r||u,C=(0,_.useMemo)(()=>{let t=[];for(let n of e.buckets)for(let e of n.largest??[])t.push({...e,bucketKey:n.key});return t.sort((e,t)=>t.bytes-e.bytes).slice(0,10)},[e.buckets]),w=(0,_.useMemo)(()=>new Map(e.buckets.map(e=>[e.key,e])),[e.buckets]);return(0,J.jsxs)(`div`,{className:`storage-workspace-root`,children:[(0,J.jsxs)(`aside`,{className:`storage-workspace-rail`,"aria-label":a(`storage.section.buckets`),children:[(0,J.jsxs)(`div`,{className:`storage-workspace-rail-header`,children:[(0,J.jsx)(`span`,{className:`storage-workspace-rail-title`,children:a(`storage.section.buckets`)}),(0,J.jsx)(`span`,{className:`storage-workspace-rail-count`,children:g.length})]}),(0,J.jsx)(`div`,{className:`storage-workspace-rail-list`,children:g.length===0?(0,J.jsx)(`span`,{className:`storage-workspace-rail-empty`,children:a(`storage.empty`)}):g.map(e=>(0,J.jsxs)(`button`,{type:`button`,className:`storage-workspace-rail-row${o===e.key?` storage-workspace-rail-row--selected`:``}`,onClick:()=>s(t=>t===e.key?null:e.key),"aria-current":o===e.key?`true`:void 0,children:[(0,J.jsxs)(`span`,{className:`storage-workspace-rail-primary`,children:[(0,J.jsx)(`span`,{className:`storage-workspace-rail-name`,children:Fh(e,a)}),(0,J.jsx)(`span`,{className:`storage-workspace-rail-size`,children:Th(e.bytes,t)})]}),(0,J.jsxs)(`span`,{className:`storage-workspace-rail-meta`,children:[e.fileCount.toLocaleString(t),` `,a(`storage.col.files`).toLowerCase()]})]},e.key))})]}),(0,J.jsx)(`section`,{className:`storage-workspace-main`,"aria-label":v?Fh(v,a):a(`storage.section.largest`),children:v?(0,J.jsxs)(`div`,{className:`stw-detail`,children:[(0,J.jsx)(`div`,{className:`stw-detail-toolbar`,children:(0,J.jsxs)(`button`,{type:`button`,className:`stw-detail-back`,onClick:()=>s(null),children:[(0,J.jsx)(Se,{className:`stw-detail-back-chevron`,"aria-hidden":`true`}),a(`modal.back`)]})}),(0,J.jsxs)(`div`,{className:`stw-detail-body`,children:[(0,J.jsx)(`h2`,{className:`stw-detail-title`,children:Fh(v,a)}),(0,J.jsxs)(`dl`,{className:`stw-kv`,children:[(0,J.jsxs)(`div`,{className:`stw-kv-row`,children:[(0,J.jsx)(`dt`,{children:a(`storage.col.size`)}),(0,J.jsx)(`dd`,{className:`stw-kv-mono`,children:Th(v.bytes,t)})]}),(0,J.jsxs)(`div`,{className:`stw-kv-row`,children:[(0,J.jsx)(`dt`,{children:a(`storage.col.files`)}),(0,J.jsx)(`dd`,{className:`stw-kv-mono`,children:v.fileCount.toLocaleString(t)})]}),(0,J.jsxs)(`div`,{className:`stw-kv-row`,children:[(0,J.jsx)(`dt`,{children:a(`storage.col.oldest`)}),(0,J.jsx)(`dd`,{children:Ih(v.oldest,t)})]}),(0,J.jsxs)(`div`,{className:`stw-kv-row`,children:[(0,J.jsx)(`dt`,{children:a(`storage.col.newest`)}),(0,J.jsx)(`dd`,{children:Ih(v.newest,t)})]}),(0,J.jsxs)(`div`,{className:`stw-kv-row`,children:[(0,J.jsx)(`dt`,{children:a(`storage.col.rows`)}),(0,J.jsx)(`dd`,{className:`stw-kv-mono`,children:Lh(v,t,a)})]})]}),(v.largest?.length??0)>0&&(0,J.jsxs)(`div`,{className:`stw-section`,children:[(0,J.jsx)(`h3`,{className:`stw-section-title`,children:a(`storage.section.largest`)}),v.largest.map(e=>(0,J.jsxs)(`div`,{className:`stw-file-row`,children:[(0,J.jsx)(`span`,{className:`stw-file-path`,title:e.path,children:e.path}),(0,J.jsx)(`span`,{className:`stw-file-size`,children:Th(e.bytes,t)})]},e.path))]})]})]}):(0,J.jsxs)(`div`,{className:`stw-overview`,children:[(0,J.jsxs)(`div`,{className:`stw-summary`,children:[(0,J.jsxs)(`div`,{className:`stw-summary-card`,children:[(0,J.jsx)(`div`,{className:`stw-summary-label`,children:a(`storage.card.total`)}),(0,J.jsx)(`div`,{className:`stw-summary-value`,children:Th(e.total.bytes,t)})]}),(0,J.jsxs)(`div`,{className:`stw-summary-card`,children:[(0,J.jsx)(`div`,{className:`stw-summary-label`,children:a(`storage.card.files`)}),(0,J.jsx)(`div`,{className:`stw-summary-value`,children:e.total.fileCount.toLocaleString(t)})]}),(0,J.jsxs)(`div`,{className:`stw-summary-card`,children:[(0,J.jsx)(`div`,{className:`stw-summary-label`,children:a(`storage.card.home`)}),(0,J.jsx)(`div`,{className:`stw-summary-value mono stw-home-path`,title:e.codexHome,children:e.codexHome})]})]}),y?(0,J.jsx)(zh,{report:y,locale:t,t:a,busy:S,error:b,compaction:x,onAction:r=>{if(i){i(r);return}if(u)return;let a=e.generatedAt;(async()=>{d(!0),p(null),r.action===`compact`&&h(null);try{let e=r.action===`protect`?`protect`:r.action,i={method:`POST`,...r.action===`protect`?{headers:{"content-type":`application/json`},body:JSON.stringify({mode:r.mode})}:{}},o=await fetch(`${n}/api/storage/codex-logs/${e}`,i);if(!o.ok){let e=await o.json().catch(()=>({}));p({generation:a,message:Rh(t,e.error)});return}if(r.action===`compact`){let e=(await o.json().catch(()=>null))?.report;if(e){let n=Th(e.logicalBytesReclaimed??0,t),r=Th(e.physicalDatabaseBytesReclaimed??0,t),i=e.complete?Dh(t,`compactComplete`):Dh(t,`compactPartial`),o=e.pagesReclaimed??0,s=!e.complete&&e.stopReason?` (${e.stopReason})`:``;h({generation:a,summary:[`${i}${s}`,`${o.toLocaleString(t)} ${Dh(t,`pagesUnit`)}`,`${n} / ${r}`].join(` — `)})}try{let e=await fetch(`${n}/api/storage/codex-logs`);if(e.ok){let t=await e.json();l({generation:a,report:t})}}catch{}return}let s=await o.json();l({generation:a,report:s})}catch{p({generation:a,message:kh(t,`error.generic`)})}finally{d(!1)}})()}}):e.codexLogsError===`inspect_failed`?(0,J.jsx)(Bh,{locale:t,t:a}):null,C.length>0?(0,J.jsxs)(`div`,{className:`stw-section`,children:[(0,J.jsx)(`h3`,{className:`stw-section-title`,children:a(`storage.section.largest`)}),C.map(e=>{let n=w.get(e.bucketKey);return(0,J.jsxs)(`div`,{className:`stw-file-row`,children:[(0,J.jsx)(`span`,{className:`stw-file-path`,title:e.path,children:e.path}),n&&(0,J.jsx)(`span`,{className:`stw-file-bucket`,children:Fh(n,a)}),(0,J.jsx)(`span`,{className:`stw-file-size`,children:Th(e.bytes,t)})]},`${e.bucketKey}:${e.path}`)})]}):(0,J.jsxs)(`p`,{className:`stw-hint`,children:[(0,J.jsx)(le,{style:{width:14,height:14,verticalAlign:`text-bottom`,marginRight:6},"aria-hidden":`true`}),a(`storage.workspace.selectBucket`)]})]})})]})}var Hh=1024**3,Uh=[10,25,50],Wh=(e,t)=>{if(!(e instanceof Error))return t;let n=e.message;return n===`Failed to fetch`||n.includes(`NetworkError`)||n.includes(`network error`)||n.includes(`JSON`)||n.includes(`Unexpected end of`)?t:n||t};function Gh({apiBase:e,locale:t,t:n,onDone:r}){let[i,a]=(0,_.useState)(25),[o,s]=(0,_.useState)(null),[c,l]=(0,_.useState)(!1),[u,d]=(0,_.useState)(!1),[f,p]=(0,_.useState)(!1),[m,h]=(0,_.useState)(null),[g,v]=(0,_.useState)(null),y=(0,_.useRef)(null),b=(0,_.useRef)(null),x=(0,_.useRef)(!1),S=(0,_.useCallback)((e=!1)=>{l(!1),d(!1),e&&s(null)},[]);(0,_.useEffect)(()=>{x.current=f},[f]),(0,_.useEffect)(()=>{if(!c)return;b.current=document.activeElement,y.current?.focus();let e=e=>{e.key===`Escape`&&!x.current&&S()};return window.addEventListener(`keydown`,e),()=>{window.removeEventListener(`keydown`,e),b.current?.focus()}},[c,S]);let C=(e,t,r)=>{switch(e){case`codex_busy`:return n(`storage.cleanup.err.codex_busy`);case`stale_preview`:return n(`storage.cleanup.err.stale_preview`);case`restore_pending_overlap`:return n(`storage.cleanup.err.restore_pending_overlap`);case`referenced_history`:return n(`storage.cleanup.err.referenced_history`);case`invalid_digest`:return n(`storage.cleanup.err.invalid_digest`);case`invalid_mode`:return n(`storage.cleanup.err.invalid_mode`);case`fs_failed`:return r?n(`storage.cleanup.err.fs_failed_trash`,{trashDir:r}):n(`storage.cleanup.err.fs_failed`);case`db_reconcile_failed`:return n(`storage.cleanup.err.db_reconcile_failed`);case`cleanup_failed`:return n(`storage.cleanup.err.cleanup_failed`);default:return t??n(`storage.cleanup.cleanupFailed`)}},w=e=>n(`storage.cleanup.preset`,{percent:new Intl.NumberFormat(t,{style:`percent`,maximumFractionDigits:0}).format(e/100)}),T=async()=>{p(!0),v(null),h(null);try{let t=await fetch(`${e}/api/storage/cleanup/preview`,{method:`POST`,headers:{"content-type":`application/json`},body:JSON.stringify({percent:i})});if(!t.ok){let e=await t.json().catch(()=>({}));throw Error(C(e.error,n(`storage.cleanup.previewFailed`)))}let r=await t.json();s(r),l(!0)}catch(e){v(Wh(e,n(`storage.cleanup.previewFailed`)))}finally{p(!1)}},E=async()=>{if(o){p(!0),v(null);try{let i=await fetch(`${e}/api/storage/cleanup`,{method:`POST`,headers:{"content-type":`application/json`},body:JSON.stringify({percent:o.percent,mode:u?`permanent`:`quarantine`,digest:o.digest})});if(!i.ok){let e=await i.json().catch(()=>({}));throw e.error===`stale_preview`&&S(!0),Error(C(e.error,e.message,e.trashDir))}let a=await i.json();if(!a.ok)throw a.error===`stale_preview`&&S(!0),Error(C(a.error,a.message,a.trashDir));S(!0),h(n(u?`storage.cleanup.donePermanent`:`storage.cleanup.doneQuarantine`,{count:String(a.count),size:Th(a.bytes,t)})),r()}catch(e){v(Wh(e,n(`storage.cleanup.cleanupFailed`)))}finally{p(!1)}}};return(0,J.jsxs)(`section`,{className:`storage-cleanup-pane`,children:[(0,J.jsx)(`p`,{className:`muted storage-manual-panel__help`,children:n(`storage.cleanup.help`)}),(0,J.jsxs)(`div`,{className:`storage-manual-panel__controls`,children:[(0,J.jsxs)(`label`,{className:`storage-manual-panel__slider`,children:[(0,J.jsx)(`span`,{className:`muted mono`,style:{minWidth:`3.5rem`,fontVariantNumeric:`tabular-nums`},children:n(`storage.cleanup.percent`,{percent:String(i)})}),(0,J.jsx)(`input`,{type:`range`,min:1,max:100,value:i,onChange:e=>a(Number(e.target.value)),disabled:f,style:{flex:1,minWidth:0},"aria-label":n(`storage.cleanup.slider`)})]}),(0,J.jsx)(`div`,{className:`storage-manual-panel__presets`,children:Uh.map(e=>(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm${i===e?` active`:``}`,disabled:f,onClick:()=>a(e),children:w(e)},e))}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-sm`,disabled:f,onClick:()=>void T(),children:n(`storage.cleanup.preview`)})]}),m&&(0,J.jsx)(`p`,{className:`muted storage-manual-panel__status`,children:m}),g&&!c&&(0,J.jsx)(`p`,{className:`storage-manual-panel__status`,style:{color:`var(--red)`},children:g}),c&&o&&(0,J.jsx)(`div`,{className:`modal-overlay`,role:`dialog`,"aria-modal":`true`,"aria-labelledby":`storage-cleanup-confirm-title`,onClick:()=>!f&&S(),children:(0,J.jsxs)(`div`,{className:`modal-card`,onClick:e=>e.stopPropagation(),children:[(0,J.jsx)(`h3`,{id:`storage-cleanup-confirm-title`,children:n(`storage.cleanup.confirmTitle`)}),(0,J.jsx)(`p`,{children:n(`storage.cleanup.confirmBody`,{count:String(o.count),size:Th(o.bytes,t),percent:String(o.percent)})}),o.candidates.length>0&&(0,J.jsxs)(`ul`,{className:`mono muted`,style:{maxHeight:160,overflow:`auto`,fontSize:`var(--text-caption)`},children:[o.candidates.slice(0,8).map(e=>(0,J.jsx)(`li`,{children:e.relPath},e.relPath)),o.count>8&&(0,J.jsx)(`li`,{children:n(`storage.cleanup.moreFiles`,{n:String(Math.max(0,o.count-8))})})]}),(0,J.jsxs)(`label`,{style:{display:`flex`,gap:8,alignItems:`center`,marginTop:12},children:[(0,J.jsx)(`input`,{type:`checkbox`,checked:u,disabled:f,onChange:e=>d(e.target.checked)}),(0,J.jsx)(`span`,{children:n(`storage.cleanup.permanent`)})]}),(0,J.jsx)(`p`,{className:`muted`,style:{marginTop:8,fontSize:`var(--text-caption)`},children:n(u?`storage.cleanup.permanentWarn`:`storage.cleanup.quarantineNote`)}),g&&(0,J.jsx)(`p`,{style:{marginTop:12,color:`var(--red)`},children:g}),(0,J.jsxs)(`div`,{className:`dialog-actions`,style:{marginTop:16},children:[(0,J.jsx)(`button`,{ref:y,type:`button`,className:`btn btn-ghost`,disabled:f,onClick:()=>S(),children:n(`storage.cleanup.cancel`)}),(0,J.jsx)(`button`,{type:`button`,className:u?`btn btn-danger`:`btn`,disabled:f||o.count===0,onClick:()=>void E(),children:n(u?`storage.cleanup.confirmPermanent`:`storage.cleanup.confirmQuarantine`)})]})]})})]})}function Kh({apiBase:e,locale:t,t:n,onDone:r,reloadToken:i,onEntriesChange:a}){let[o,s]=(0,_.useState)(!1),[c,l]=(0,_.useState)(null),[u,d]=(0,_.useState)(null),[f,p]=(0,_.useState)(null),m=(0,_.useRef)(null),h=(0,_.useRef)(null),g=(0,_.useRef)(!1);(0,_.useEffect)(()=>{g.current=o},[o]);let v=(0,_.useCallback)(()=>l(null),[]);(0,_.useEffect)(()=>{if(!c)return;h.current=document.activeElement,m.current?.focus();let e=e=>{e.key===`Escape`&&!g.current&&v()};return window.addEventListener(`keydown`,e),()=>{window.removeEventListener(`keydown`,e),h.current?.focus()}},[c,v]);let y=(0,_.useCallback)(async t=>{let r=await fetch(`${e}/api/storage/trash`,{signal:t});if(!r.ok)throw Error(n(`storage.trash.listFailed`));let i=await r.json(),o=Array.isArray(i.entries)?i.entries:[];return a?.(o),o},[e,a,n]),b=ml(`storage-trash:${e}`,[e,i],y,{isEmpty:e=>e.length===0}).state,x=b.data??[],S=(e,t)=>{switch(e){case`codex_busy`:return n(`storage.trash.err.codex_busy`);case`invalid_trash`:return n(`storage.trash.err.invalid_trash`);case`missing_trash`:return n(`storage.trash.err.missing_trash`);case`dest_exists`:return n(`storage.trash.err.dest_exists`);case`fs_failed`:return n(`storage.trash.err.fs_failed`);case`db_reconcile_failed`:return n(`storage.trash.err.db_reconcile_failed`);case`storage_mutation_busy`:return n(`storage.trash.err.storage_mutation_busy`);case`restore_failed`:return n(`storage.trash.err.restore_failed`);case`restore_worker_timeout`:return n(`storage.trash.err.restore_worker_timeout`);case`restore_worker_aborted`:return n(`storage.trash.err.restore_worker_aborted`);case`restore_worker_failed`:return t??n(`storage.trash.err.restore_worker_failed`);default:return t??n(`storage.trash.restoreFailed`)}},C=async()=>{if(c){s(!0),p(null);try{let i=await fetch(`${e}/api/storage/trash/restore`,{method:`POST`,headers:{"content-type":`application/json`},body:JSON.stringify({id:c.id})});if(!i.ok){let e=await i.json().catch(()=>({}));throw Error(S(e.error,e.message))}let a=await i.json();if(!a.ok)throw Error(S(a.error,a.message));v(),d(n(`storage.trash.done`,{count:String(a.count),size:Th(a.bytes,t)})),r()}catch(e){p(Wh(e,n(`storage.trash.restoreFailed`)))}finally{s(!1)}}},w=e=>{let n=e.quarantinedAt??Number(e.epoch.split(`-`)[0]);return!Number.isFinite(n)||n<=0?`—`:new Date(n).toLocaleString(t)},T=e=>e===`permanent`?n(`storage.trash.mode.permanent`):e===`quarantine`?n(`storage.trash.mode.quarantine`):`—`;return(0,J.jsxs)(`section`,{className:`storage-cleanup-pane storage-quarantine-pane`,children:[(0,J.jsx)(`p`,{className:`muted storage-manual-panel__help`,children:n(`storage.trash.help`)}),u&&(0,J.jsx)(`p`,{className:`muted storage-manual-panel__status`,children:u}),f&&!c&&(0,J.jsx)(`p`,{className:`storage-manual-panel__status`,style:{color:`var(--red)`},role:`alert`,children:f}),b.showError&&!c&&(0,J.jsx)(`p`,{className:`storage-manual-panel__status`,style:{color:`var(--red)`},role:`alert`,children:b.error instanceof Error?b.error.message:n(`storage.trash.listFailed`)}),b.refreshing&&!b.showSkeleton&&(0,J.jsx)(_l,{live:!b.showError,children:n(`storage.trash.loading`)}),b.showSkeleton?(0,J.jsx)(gl,{label:n(`storage.trash.loading`),rows:2}):x.length===0?(0,J.jsx)(`p`,{className:`muted storage-manual-panel__status`,children:n(`storage.trash.empty`)}):(0,J.jsx)(`div`,{className:`tbl-wrap storage-manual-panel__table`,children:(0,J.jsxs)(`table`,{className:`tbl`,children:[(0,J.jsx)(`thead`,{children:(0,J.jsxs)(`tr`,{children:[(0,J.jsx)(`th`,{children:n(`storage.trash.col.when`)}),(0,J.jsx)(`th`,{className:`num`,children:n(`storage.trash.col.files`)}),(0,J.jsx)(`th`,{className:`num`,children:n(`storage.trash.col.size`)}),(0,J.jsx)(`th`,{children:n(`storage.trash.col.mode`)}),(0,J.jsx)(`th`,{children:n(`storage.trash.col.id`)}),(0,J.jsx)(`th`,{})]})}),(0,J.jsx)(`tbody`,{children:x.map(e=>(0,J.jsxs)(`tr`,{children:[(0,J.jsx)(`td`,{className:`muted`,children:w(e)}),(0,J.jsx)(`td`,{className:`num`,children:e.fileCount}),(0,J.jsx)(`td`,{className:`num mono`,children:Th(e.bytes,t)}),(0,J.jsx)(`td`,{className:`muted`,children:T(e.mode)}),(0,J.jsx)(`td`,{className:`mono`,style:{fontSize:`var(--text-caption)`},children:e.id}),(0,J.jsx)(`td`,{children:(0,J.jsx)(`button`,{type:`button`,className:`btn btn-sm`,disabled:o,onClick:()=>{p(null),l(e)},children:n(`storage.trash.restore`)})})]},e.id))})]})}),c&&(0,J.jsx)(`div`,{className:`modal-overlay`,role:`dialog`,"aria-modal":`true`,"aria-labelledby":`storage-trash-confirm-title`,onClick:()=>!o&&v(),children:(0,J.jsxs)(`div`,{className:`modal-card`,onClick:e=>e.stopPropagation(),children:[(0,J.jsx)(`h3`,{id:`storage-trash-confirm-title`,children:n(`storage.trash.confirmTitle`)}),(0,J.jsx)(`p`,{children:n(`storage.trash.confirmBody`,{count:String(c.fileCount),size:Th(c.bytes,t),id:c.id})}),f&&(0,J.jsx)(`p`,{style:{marginTop:12,color:`var(--red)`},children:f}),(0,J.jsxs)(`div`,{className:`dialog-actions`,style:{marginTop:16},children:[(0,J.jsx)(`button`,{ref:m,type:`button`,className:`btn btn-ghost`,disabled:o,onClick:()=>v(),children:n(`storage.trash.cancel`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn`,disabled:o,onClick:()=>void C(),children:n(`storage.trash.confirmRestore`)})]})]})})]})}function qh(e){let{job:t,...n}=e;return n}function Jh(e){let t=String(Math.max(0,Math.round(e.trigger.archivedBytesOver/Hh*100)/100));return e.target.reduceToBytes===void 0?{policy:qh(e),thresholdGb:t,targetMode:`percent`,percent:String(Math.min(100,Math.max(1,Math.floor(e.target.removeOldestPercent??25)))),reduceGb:`4`}:{policy:qh(e),thresholdGb:t,targetMode:`reduce`,percent:`25`,reduceGb:String(Math.max(0,Math.round(e.target.reduceToBytes/Hh*100)/100))}}async function Yh(e){await new Promise(t=>window.setTimeout(t,e))}function Xh({apiBase:e,locale:t,t:n,onDone:r}){let i=`ocx.storage.cleanup-policy.v1:${e}`,a=gr(i),o=(0,_.useRef)(!!a),[s,c]=(0,_.useState)(()=>a?.policy??null),[l,u]=(0,_.useState)(()=>!a),[d,f]=(0,_.useState)(!1),[p,m]=(0,_.useState)(!1),[h,g]=(0,_.useState)(null),[v,y]=(0,_.useState)(null),[b,x]=(0,_.useState)(()=>a?.targetMode??`percent`),[S,C]=(0,_.useState)(()=>a?.percent??`25`),[w,T]=(0,_.useState)(()=>a?.reduceGb??`4`),[E,D]=(0,_.useState)(()=>a?.thresholdGb??`5`),O=(0,_.useRef)(null),k=(0,_.useRef)(!1),A=(0,_.useRef)(!1),j=(0,_.useRef)(0),M=(0,_.useCallback)(e=>{let t=Jh(e);c(t.policy),D(t.thresholdGb),x(t.targetMode),C(t.percent),T(t.reduceGb),o.current=!0,br(i,t),k.current=!1},[i]),N=(0,_.useCallback)(()=>{k.current=!0},[]),P=(0,_.useCallback)(e=>{A.current=e},[]),F=(0,_.useCallback)(async t=>{let r=++j.current;o.current||u(!0),y(null);try{let n=await fetch(`${e}/api/storage/cleanup-policy`,{signal:t});if(!n.ok)throw Error(`load_failed`);let i=await n.json();if(t?.aborted||r!==j.current||k.current||A.current)return;M(i)}catch{if(t?.aborted||r!==j.current)return;o.current||(c(null),y(n(`storage.policy.loadFailed`)))}finally{!t?.aborted&&r===j.current&&u(!1)}},[e,M,n]);(0,_.useEffect)(()=>{let e=new AbortController,t=window.setTimeout(()=>{F(e.signal)},0);return()=>{window.clearTimeout(t),j.current+=1,e.abort()}},[F]),(0,_.useEffect)(()=>()=>{O.current?.abort(),O.current=null},[]);let I=()=>{if(!s)return null;let e=E.trim();if(e===``)return null;let t=Number(e);if(!Number.isFinite(t)||t<0)return null;let n;if(b===`reduce`){let e=w.trim();if(e===``)return null;let t=Number(e);if(!Number.isFinite(t)||t<0)return null;n={reduceToBytes:Math.floor(t*Hh)}}else{let e=Number(S);if(!Number.isFinite(e)||e<1||e>100)return null;n={removeOldestPercent:Math.min(100,Math.max(1,Math.floor(e)))}}return{enabled:s.enabled,trigger:{archivedBytesOver:Math.floor(t*Hh)},target:n,schedule:s.schedule,mode:s.mode}},L=async t=>{let r=I();if(!r){y(n(`storage.policy.invalid`));return}let i={...r,...t};f(!0),y(null),g(null);try{let t=await fetch(`${e}/api/storage/cleanup-policy`,{method:`PUT`,headers:{"content-type":`application/json`},body:JSON.stringify(i)});if(!t.ok){y(n(`storage.policy.saveFailed`));return}let r=await t.json();if(!r.policy){y(n(`storage.policy.saveFailed`));return}M(r.policy),g(n(`storage.policy.saved`))}catch{y(n(`storage.policy.saveFailed`))}finally{f(!1)}},R=async()=>{O.current?.abort();let i=new AbortController;O.current=i;let{signal:a}=i;m(!0),y(null),g(null);try{let i=I();if(!i){y(n(`storage.policy.invalid`));return}let o=await fetch(`${e}/api/storage/cleanup-policy`,{method:`PUT`,headers:{"content-type":`application/json`},body:JSON.stringify(i),signal:a});if(a.aborted)return;if(!o.ok){y(n(`storage.policy.saveFailed`));return}let s=await o.json();if(a.aborted)return;if(!s.policy){y(n(`storage.policy.saveFailed`));return}M(s.policy);let c=await fetch(`${e}/api/storage/cleanup-policy/run`,{method:`POST`,signal:a});if(a.aborted)return;if(c.status===409){let e=await c.json().catch(()=>({}));if(a.aborted)return;e.policy&&M(e.policy),y(n(`storage.policy.alreadyRunning`));return}if(!c.ok){let e=await c.json().catch(()=>({}));if(a.aborted)return;if(e.policy&&M(e.policy),e.error===`already_running`){y(n(`storage.policy.alreadyRunning`));return}y(n(`storage.policy.runFailed`));return}let l=await c.json();if(a.aborted)return;if(l.policy&&M(l.policy),l.error===`already_running`){y(n(`storage.policy.alreadyRunning`));return}if(!l.started||!l.job?.startedAt){y(n(`storage.policy.runFailed`));return}let u=l.job.startedAt,d=Date.now()+12e4,f,p;for(;Date.now()=u&&r.lastOutcome){f=r.lastOutcome;break}}}if(a.aborted)return;if(p&&M(p),!f){y(n(`storage.policy.runFailed`));return}f.skipped===`disabled`?g(n(`storage.policy.skippedDisabled`)):f.ok&&f.metadataPersistenceError?(y(n(`storage.policy.metadataSaveWarning`)),f.removed!==void 0&&r()):f.skipped===`under_threshold`?g(n(`storage.policy.skippedUnder`)):f.skipped===`nothing_selected`?g(n(`storage.policy.skippedEmpty`)):f.deferred===`codex_busy`||f.error===`codex_busy`?y(n(`storage.cleanup.err.codex_busy`)):f.ok?(g(f.mode===`permanent`?n(`storage.policy.donePermanent`,{count:String(f.removed??0),size:Th(f.freedBytes??0,t)}):n(`storage.policy.doneQuarantine`,{count:String(f.removed??0),size:Th(f.freedBytes??0,t)})),r()):y(n(`storage.policy.runFailed`))}catch(e){if(a.aborted||e instanceof DOMException&&e.name===`AbortError`)return;y(n(`storage.policy.runFailed`))}finally{O.current===i&&(O.current=null),a.aborted||m(!1)}},z=e=>e===void 0?n(`storage.policy.never`):new Date(e).toLocaleString(t);return l&&!s?(0,J.jsx)(`section`,{className:`storage-cleanup-pane`,children:(0,J.jsx)(`p`,{className:`muted storage-policy-help`,children:n(`storage.policy.loading`)})}):s?(0,J.jsxs)(`section`,{className:`storage-cleanup-pane`,children:[(0,J.jsx)(`p`,{className:`muted storage-policy-help`,children:n(`storage.policy.help`)}),(0,J.jsx)(`div`,{className:`storage-policy-enable`,children:(0,J.jsxs)(`div`,{className:`storage-policy-enable-row`,children:[(0,J.jsx)(`button`,{type:`button`,className:`toggle${s.enabled?` on`:``}`,disabled:d||p,"aria-pressed":s.enabled,"aria-label":n(`storage.policy.enabled`),title:n(`storage.policy.enabledHint`),onClick:()=>void L({enabled:!s.enabled}),children:(0,J.jsx)(`span`,{className:`toggle-knob`})}),(0,J.jsx)(`span`,{children:n(`storage.policy.enabled`)})]})}),(0,J.jsxs)(`div`,{className:`storage-policy-fields`,children:[(0,J.jsxs)(`div`,{className:`field storage-policy-trigger`,children:[(0,J.jsx)(`label`,{className:`field-label`,htmlFor:`storage-policy-threshold`,children:n(`storage.policy.trigger`)}),(0,J.jsxs)(`div`,{className:`storage-policy-trigger-row`,children:[(0,J.jsx)(`span`,{className:`storage-policy-trigger-hint`,children:n(`storage.policy.threshold`)}),(0,J.jsxs)(`span`,{className:`codex-auto-switch-input-wrap`,onBlur:e=>{e.currentTarget.contains(e.relatedTarget)||(P(!1),L())},children:[(0,J.jsx)(`input`,{id:`storage-policy-threshold`,className:`input mono codex-auto-switch-input`,type:`number`,min:0,step:.1,inputMode:`decimal`,value:E,disabled:d||p,"aria-label":n(`storage.policy.threshold`),onFocus:()=>P(!0),onChange:e=>{N(),D(e.target.value)},onKeyDown:e=>{e.nativeEvent.isComposing||d||p||e.key===`Enter`&&(e.preventDefault(),L())}}),(0,J.jsx)(`span`,{className:`codex-auto-switch-unit`,"aria-hidden":`true`,children:`GiB`}),(0,J.jsx)(ko,{disabled:d||p,incrementLabel:n(`storage.policy.thresholdInc`),decrementLabel:n(`storage.policy.thresholdDec`),onIncrement:()=>{N(),D(Oo(E,.1,0,1e4,.1))},onDecrement:()=>{N(),D(Oo(E,-.1,0,1e4,.1))}})]})]})]}),(0,J.jsxs)(`fieldset`,{className:`field storage-policy-target`,children:[(0,J.jsx)(`legend`,{className:`field-label`,children:n(`storage.policy.target`)}),(0,J.jsxs)(`label`,{className:`storage-policy-target-row`,children:[(0,J.jsx)(`input`,{type:`radio`,name:`storage-policy-target`,checked:b===`percent`,disabled:d||p,onChange:()=>{N(),x(`percent`)}}),(0,J.jsx)(`span`,{className:`storage-policy-target-label`,children:n(`storage.policy.targetPercent`)}),b===`percent`&&(0,J.jsxs)(`span`,{className:`codex-auto-switch-input-wrap`,onBlur:e=>{e.currentTarget.contains(e.relatedTarget)||(P(!1),L())},children:[(0,J.jsx)(`input`,{id:`storage-policy-percent`,className:`input mono codex-auto-switch-input`,type:`number`,min:1,max:100,step:1,inputMode:`numeric`,value:S,disabled:d||p,"aria-label":n(`storage.policy.targetPercent`),onFocus:()=>P(!0),onChange:e=>{N(),C(e.target.value)},onKeyDown:e=>{e.nativeEvent.isComposing||d||p||e.key===`Enter`&&(e.preventDefault(),L())}}),(0,J.jsx)(`span`,{className:`codex-auto-switch-unit`,"aria-hidden":`true`,children:`%`}),(0,J.jsx)(ko,{disabled:d||p,incrementLabel:n(`storage.policy.percentInc`),decrementLabel:n(`storage.policy.percentDec`),onIncrement:()=>{N(),C(Oo(S,1,1,100))},onDecrement:()=>{N(),C(Oo(S,-1,1,100))}})]})]}),(0,J.jsxs)(`label`,{className:`storage-policy-target-row`,children:[(0,J.jsx)(`input`,{type:`radio`,name:`storage-policy-target`,checked:b===`reduce`,disabled:d||p,onChange:()=>{N(),x(`reduce`)}}),(0,J.jsx)(`span`,{className:`storage-policy-target-label`,children:n(`storage.policy.targetReduce`)}),b===`reduce`&&(0,J.jsxs)(`span`,{className:`codex-auto-switch-input-wrap`,onBlur:e=>{e.currentTarget.contains(e.relatedTarget)||(P(!1),L())},children:[(0,J.jsx)(`input`,{id:`storage-policy-reduce`,className:`input mono codex-auto-switch-input`,type:`number`,min:0,step:.1,inputMode:`decimal`,value:w,disabled:d||p,"aria-label":n(`storage.policy.targetReduce`),onFocus:()=>P(!0),onChange:e=>{N(),T(e.target.value)},onKeyDown:e=>{e.nativeEvent.isComposing||d||p||e.key===`Enter`&&(e.preventDefault(),L())}}),(0,J.jsx)(`span`,{className:`codex-auto-switch-unit`,"aria-hidden":`true`,children:`GiB`}),(0,J.jsx)(ko,{disabled:d||p,incrementLabel:n(`storage.policy.reduceInc`),decrementLabel:n(`storage.policy.reduceDec`),onIncrement:()=>{N(),T(Oo(w,.1,0,1e4,.1))},onDecrement:()=>{N(),T(Oo(w,-.1,0,1e4,.1))}})]})]})]}),(0,J.jsxs)(`div`,{className:`storage-policy-selects`,children:[(0,J.jsxs)(`label`,{className:`field`,htmlFor:`storage-policy-schedule`,children:[(0,J.jsx)(`span`,{className:`field-label`,children:n(`storage.policy.schedule`)}),(0,J.jsxs)(`select`,{id:`storage-policy-schedule`,className:`input`,value:s.schedule,disabled:d||p,onChange:e=>{let t=e.target.value;L({schedule:t})},children:[(0,J.jsx)(`option`,{value:`manual`,children:n(`storage.policy.schedule.manual`)}),(0,J.jsx)(`option`,{value:`startup`,children:n(`storage.policy.schedule.startup`)}),(0,J.jsx)(`option`,{value:`daily`,children:n(`storage.policy.schedule.daily`)}),(0,J.jsx)(`option`,{value:`weekly`,children:n(`storage.policy.schedule.weekly`)})]})]}),(0,J.jsxs)(`label`,{className:`field`,htmlFor:`storage-policy-mode`,children:[(0,J.jsx)(`span`,{className:`field-label`,children:n(`storage.policy.mode`)}),(0,J.jsxs)(`select`,{id:`storage-policy-mode`,className:`input`,value:s.mode,disabled:d||p,onChange:e=>{let t=e.target.value;L({mode:t})},children:[(0,J.jsx)(`option`,{value:`quarantine`,children:n(`storage.policy.mode.quarantine`)}),(0,J.jsx)(`option`,{value:`permanent`,children:n(`storage.policy.mode.permanent`)})]})]})]}),s.mode===`permanent`&&(0,J.jsx)(`p`,{className:`err storage-policy-warn`,role:`status`,children:n(`storage.policy.permanentWarn`)})]}),(0,J.jsxs)(`div`,{className:`storage-policy-meta`,children:[(0,J.jsxs)(`div`,{className:`storage-policy-meta-item`,children:[(0,J.jsx)(`span`,{className:`muted`,children:n(`storage.policy.lastRun`)}),(0,J.jsxs)(`span`,{className:`storage-policy-meta-value`,children:[z(s.lastRun?.at),s.lastRun?` · ${n(`storage.policy.lastRunDetail`,{count:String(s.lastRun.removed),size:Th(s.lastRun.freedBytes,t)})}`:``]})]}),(0,J.jsxs)(`div`,{className:`storage-policy-meta-item`,children:[(0,J.jsx)(`span`,{className:`muted`,children:n(`storage.policy.nextRun`)}),(0,J.jsx)(`span`,{className:`storage-policy-meta-value`,children:z(s.nextRun)})]})]}),(0,J.jsxs)(`div`,{className:`storage-policy-actions`,children:[(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,disabled:d||p,onClick:()=>void L(),children:n(`storage.policy.save`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-sm`,disabled:d||p,onClick:()=>void R(),children:n(p?`storage.policy.running`:`storage.policy.runNow`)}),(0,J.jsx)(`span`,{className:`storage-policy-actions__status${v?` is-error`:``}`,role:v?`alert`:`status`,"aria-live":`polite`,children:v??h??``})]})]}):(0,J.jsx)(`section`,{className:`storage-cleanup-pane`,children:v&&(0,J.jsx)(`p`,{className:`err`,role:`alert`,children:v})})}function Zh({apiBase:e,locale:t,t:n,archivedCount:r,showQuarantine:i,trashReloadToken:a,onDone:o,onTrashEntriesChange:s}){let[c,l]=(0,_.useState)(`policy`),u=(0,_.useRef)(null),d=(0,_.useRef)(null),f=[{id:`policy`,label:n(`storage.cleanupCard.tab.policy`),ref:u},{id:`quarantine`,label:n(`storage.cleanupCard.tab.quarantine`),ref:d}],p=e=>{l(e),window.requestAnimationFrame(()=>(e===`policy`?u:d).current?.focus())},m=e=>{e.key===`ArrowLeft`||e.key===`ArrowRight`?(e.preventDefault(),p(c===`policy`?`quarantine`:`policy`)):e.key===`Home`?(e.preventDefault(),p(`policy`)):e.key===`End`&&(e.preventDefault(),p(`quarantine`))};return(0,J.jsxs)(`section`,{className:`panel storage-cleanup-card`,"aria-labelledby":`storage-cleanup-card-title`,children:[(0,J.jsx)(`div`,{className:`page-tabs storage-cleanup-card__tabs`,role:`tablist`,"aria-label":n(`storage.cleanupCard.tabs`),children:f.map(({id:e,label:t,ref:n})=>(0,J.jsx)(`button`,{type:`button`,role:`tab`,ref:n,id:`storage-cleanup-tab-${e}`,"aria-selected":c===e,"aria-controls":`storage-cleanup-panel-${e}`,tabIndex:c===e?0:-1,className:`page-tab${c===e?` page-tab--active`:``}`,onKeyDown:m,onClick:()=>p(e),children:t},e))}),(0,J.jsx)(`h3`,{id:`storage-cleanup-card-title`,className:`panel-title`,children:n(`storage.cleanupCard.title`)}),(0,J.jsxs)(`div`,{className:`storage-cleanup-card__stack`,children:[(0,J.jsxs)(`div`,{id:`storage-cleanup-panel-policy`,role:`tabpanel`,"aria-labelledby":`storage-cleanup-tab-policy`,className:`storage-cleanup-card__body storage-cleanup-policy-split`,"data-active":c===`policy`?`true`:`false`,"aria-hidden":c!==`policy`,...c===`policy`?{}:{inert:!0},children:[(0,J.jsx)(Xh,{apiBase:e,locale:t,t:n,onDone:o}),(0,J.jsxs)(`aside`,{className:`storage-cleanup-manual`,"aria-labelledby":`storage-cleanup-manual-title`,children:[(0,J.jsx)(`h4`,{id:`storage-cleanup-manual-title`,className:`storage-cleanup-manual__title`,children:n(`storage.cleanup.title`)}),r>0?(0,J.jsx)(Gh,{apiBase:e,locale:t,t:n,onDone:o}):(0,J.jsx)(`p`,{className:`muted storage-manual-panel__status`,children:n(`storage.cleanup.noArchives`)})]})]}),(0,J.jsx)(`div`,{id:`storage-cleanup-panel-quarantine`,role:`tabpanel`,"aria-labelledby":`storage-cleanup-tab-quarantine`,className:`storage-cleanup-card__body`,"data-active":c===`quarantine`?`true`:`false`,"aria-hidden":c!==`quarantine`,...c===`quarantine`?{}:{inert:!0},children:i?(0,J.jsx)(Kh,{apiBase:e,locale:t,t:n,onDone:o,reloadToken:a,onEntriesChange:s}):(0,J.jsx)(`p`,{className:`muted storage-manual-panel__status`,children:n(`storage.trash.empty`)})})]})]})}function Qh({apiBase:e}){let{t,locale:n}=ct(),r=`ocx.storage.report.v1:${e}`,i=gr(r),[a,o]=(0,_.useState)(null),[s,c]=(0,_.useState)(0),l=(0,_.useRef)(!1),[u,d]=(0,_.useState)({apiBase:e,settled:!1,hasEntries:!1}),f=(0,_.useCallback)(async n=>{try{let i=await fetch(`${e}/api/storage`,{signal:n});if(!i.ok)throw Error(t(`storage.error`));let a=await i.json();return br(r,a),l.current&&(l.current=!1,o(t(`storage.rescanned`))),a}catch(e){throw l.current&&(l.current=!1,o(t(`storage.error`))),n.aborted?e:Error(t(`storage.error`),{cause:e})}},[e,r,t]),p=ml(`storage-report:${e}`,[e],f,{isEmpty:e=>e.total.fileCount===0&&e.error===void 0}),m=p.state,h=m.data??i,g=m.refreshing||m.showSkeleton&&!h,v=p.refresh,y=(0,_.useCallback)(()=>{o(null),l.current=!0,v(),c(e=>e+1)},[v]),b=(0,_.useCallback)(t=>{d({apiBase:e,settled:!0,hasEntries:t.length>0})},[e]),x=u.apiBase===e&&u.settled,S=u.apiBase===e&&u.hasEntries,C=h?.error!==void 0,w=!g&&!m.showError&&!C&&h.total.fileCount===0&&x&&!S,T=h?.buckets.find(e=>e.key===`archived_sessions`)?.fileCount??0,E=!!h&&!C,D=E&&(h.total.fileCount>0||!x||S);return(0,J.jsxs)(J.Fragment,{children:[(0,J.jsxs)(`div`,{className:`page-head`,children:[(0,J.jsx)(`h2`,{id:`storage-page-title`,children:t(`storage.title`)}),(0,J.jsxs)(`div`,{className:`storage-page-head-actions`,children:[(0,J.jsx)(`span`,{className:`storage-page-head-feedback`,role:`status`,"aria-live":`polite`,children:a??``}),(0,J.jsxs)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,disabled:g,onClick:()=>void y(),children:[(0,J.jsx)(pe,{}),` `,t(`storage.refresh`)]})]})]}),(0,J.jsx)(`p`,{className:`page-sub`,children:t(`storage.subtitle`)}),h&&h.error===void 0&&(0,J.jsxs)(`p`,{className:`storage-page-meta`,children:[(0,J.jsx)(`code`,{className:`mono storage-page-meta__home`,title:h.codexHome,children:h.codexHome}),(0,J.jsx)(`span`,{className:`storage-page-meta__sep`,"aria-hidden":`true`,children:`·`}),(0,J.jsxs)(`span`,{children:[t(`storage.snapshot.lastScan`),`:`,` `,new Date(h.generatedAt).toLocaleString(n)]})]}),m.showSkeleton&&!h?(0,J.jsx)(gl,{label:t(`storage.loading`),rows:5}):m.kind===`failed-cold`&&!h?(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)(`div`,{className:`alert alert-err`,role:`alert`,children:m.error instanceof Error?m.error.message:t(`storage.error`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:()=>v(),children:t(`common.retry`)})]}):C?(0,J.jsx)(J.Fragment,{children:(0,J.jsx)(`div`,{className:`alert alert-err`,role:`alert`,children:t(`storage.error`)})}):(0,J.jsxs)(J.Fragment,{children:[m.showError&&(0,J.jsx)(`div`,{className:`alert alert-err`,role:`alert`,children:t(`storage.error`)}),w?(0,J.jsx)(Ot,{title:t(`storage.empty`)}):h&&h.total.fileCount>0&&(0,J.jsx)(Vh,{report:h,locale:n,apiBase:e})]}),h&&h.error===void 0&&m.refreshing&&!m.showSkeleton&&(0,J.jsx)(_l,{live:!m.showError,children:t(`storage.loading`)}),E&&(0,J.jsx)(Zh,{apiBase:e,locale:n,t,archivedCount:T,showQuarantine:D,trashReloadToken:s,onDone:()=>void y(),onTrashEntriesChange:b})]})}var $h=`/api/codex-auth/features/default-mode-request-user-input`;function eg({apiBase:e}){let t=Q(),[n,r]=(0,_.useState)(!1),[i,a]=(0,_.useState)(!1),[o,s]=(0,_.useState)(!1),[c,l]=(0,_.useState)(!1),[u,d]=(0,_.useState)(null),f=(0,_.useRef)(!1),p=(0,_.useRef)(!1),m=(0,_.useRef)(0),h=(0,_.useCallback)(async()=>{if(f.current)return;let t=++m.current,n=Vn(15e3);try{let i=await fetch(`${e}${$h}`,{signal:n.signal});if(!i.ok)throw Error(`load`);let o=await i.json();if(f.current||t!==m.current)return;p.current=o.enabled===!0,r(p.current),a(!0),l(!1)}catch{!f.current&&t===m.current&&l(!0)}finally{n.clear()}},[e]);(0,_.useEffect)(()=>{let e=window.setTimeout(()=>{h()},0),t=Gn(()=>{h()},3e4);return()=>{window.clearTimeout(e),t()}},[h]);let g=(0,_.useCallback)(async()=>{if(f.current||!i||c)return;let n=!p.current,o=p.current;p.current=n,r(n),f.current=!0,s(!0),d(null),m.current++;try{let i=await fetch(`${e}${$h}`,{method:`PUT`,headers:{"content-type":`application/json`},body:JSON.stringify({enabled:n})}),o=await Pt(i)??{};if(o.ok!==!0)throw Error(String(i.status));p.current=o.enabled===!0,r(p.current),a(!0),d({tone:`ok`,message:t(o.changed===!0?`codexAuth.requestUserInputUpdatedRestart`:`codexAuth.requestUserInputUpdated`)})}catch(e){p.current=o,r(o);let n=e instanceof Error&&e.message&&!/^HTTP \d{3}$/.test(e.message)?e.message:t(`codexAuth.requestUserInputUpdateFailed`);d({tone:`err`,message:n})}finally{f.current=!1,s(!1)}},[e,i,c,t]),v=o||!i||c;return(0,J.jsxs)(`div`,{className:`card card-row codex-request-user-input-card`,style:{marginTop:16},"aria-busy":o||!i&&!c||void 0,children:[(0,J.jsxs)(`div`,{className:`codex-request-user-input-copy`,children:[(0,J.jsx)(`strong`,{children:t(`codexAuth.requestUserInput`)}),(0,J.jsx)(`div`,{className:`card-sub`,role:c?`alert`:void 0,children:t(c?`codexAuth.requestUserInputLoadFailed`:`codexAuth.requestUserInputDesc`)}),(0,J.jsx)(`code`,{className:`mono codex-request-user-input-config`,children:`[features] +default_mode_request_user_input = true`})]}),(0,J.jsxs)(`div`,{className:`codex-request-user-input-controls`,children:[c&&(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:()=>{h()},children:t(`common.retry`)}),(0,J.jsx)(`button`,{type:`button`,className:`toggle ${n?`on`:``}`,onClick:()=>{g()},disabled:v,"aria-pressed":n,"aria-label":t(`codexAuth.requestUserInput`),title:t(`codexAuth.requestUserInput`),children:(0,J.jsx)(`span`,{className:`toggle-knob`})})]}),u&&(0,J.jsx)(`div`,{className:`codex-request-user-input-feedback${u.tone===`err`?` is-error`:``}`,role:u.tone===`err`?`alert`:`status`,"aria-atomic":`true`,children:u.message})]})}function tg({apiBase:e}){let t=Q(),[n,r]=(0,_.useState)(!1),[i,a]=(0,_.useState)(!1),[o,s]=(0,_.useState)(!1),[c,l]=(0,_.useState)(!1),[u,d]=(0,_.useState)(null),f=(0,_.useRef)(!1),p=(0,_.useRef)(!1),m=(0,_.useRef)(0),h=(0,_.useCallback)(async()=>{if(p.current)return;let t=++m.current,n=Vn(15e3);try{let i=await fetch(`${e}/api/settings`,{signal:n.signal});if(!i.ok)throw Error(`load`);let o=await i.json();if(p.current||t!==m.current)return;if(typeof o.codexAccountPickerEnabled!=`boolean`)throw Error(`shape`);f.current=o.codexAccountPickerEnabled,r(o.codexAccountPickerEnabled),a(!0),l(!1)}catch{!p.current&&t===m.current&&l(!0)}finally{n.clear()}},[e]);(0,_.useEffect)(()=>{let e=window.setTimeout(()=>{h()},0),t=Gn(()=>{h()},3e4);return()=>{window.clearTimeout(e),t()}},[h]);let g=(0,_.useCallback)(async()=>{if(p.current||!i)return;let n=f.current,o=!n;f.current=o,r(o),p.current=!0,s(!0),d(null),m.current+=1;try{let n=await Pt(await fetch(`${e}/api/settings`,{method:`PUT`,headers:{"content-type":`application/json`},body:JSON.stringify({codexAccountPickerEnabled:o})}))??{};if(n.ok!==!0||typeof n.codexAccountPickerEnabled!=`boolean`)throw Error(`unconfirmed`);f.current=n.codexAccountPickerEnabled,r(n.codexAccountPickerEnabled),a(!0),l(!1),d(n.catalogRefreshPending===!0?{tone:`warn`,message:t(`codexAuth.catalogRefreshPending`)}:{tone:`ok`,message:t(`codexAuth.accountPickerUpdated`)})}catch{f.current=n,r(n),d({tone:`err`,message:t(`codexAuth.accountPickerUpdateFailed`)})}finally{p.current=!1,s(!1)}},[e,i,t]),v=c&&!i;return(0,J.jsxs)(`div`,{className:`card card-row codex-account-picker-card`,"aria-busy":o||!i&&!v||void 0,children:[(0,J.jsxs)(`div`,{className:`codex-account-picker-copy`,children:[(0,J.jsx)(`strong`,{children:t(`codexAuth.accountPickerTitle`)}),(0,J.jsx)(`div`,{className:`card-sub`,role:v?`status`:void 0,children:t(v?`codexAuth.accountPickerLoadFailed`:i?n?`codexAuth.accountPickerOnDesc`:`codexAuth.accountPickerOffDesc`:`common.loading`)}),i&&n&&(0,J.jsx)(`div`,{className:`card-sub faint`,children:t(`codexAuth.accountPickerCompatibility`)}),i&&c&&(0,J.jsx)(`div`,{className:`card-sub faint`,role:`status`,children:t(`codexAuth.accountPickerRefreshFailed`)})]}),(0,J.jsxs)(`div`,{className:`codex-account-picker-controls`,children:[c&&(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:()=>{h()},disabled:o,children:t(`common.retry`)}),i&&(0,J.jsx)(`button`,{type:`button`,className:`toggle ${n?`on`:``}`,onClick:()=>{g()},disabled:o,"aria-pressed":n,"aria-label":t(`codexAuth.accountPickerTitle`),title:t(`codexAuth.accountPickerTitle`),children:(0,J.jsx)(`span`,{className:`toggle-knob`})})]}),u&&(0,J.jsx)(`div`,{className:`codex-account-picker-feedback is-${u.tone}`,role:u.tone===`err`?`alert`:`status`,"aria-atomic":`true`,children:u.message})]})}function ng(e){if(!e||typeof e!=`object`)return`absent`;let t=e.providers;if(!t||typeof t!=`object`||Array.isArray(t)||!Object.hasOwn(t,`openai`))return`absent`;let n=t.openai;if(!n||typeof n!=`object`||Array.isArray(n))return`absent`;let r=n;return r.disabled===!0?`disabled`:r.codexAccountMode===`direct`?`direct`:r.codexAccountMode===void 0||r.codexAccountMode===`pool`?`pool`:`absent`}function rg({state:e,busy:t,onEnable:n}){let r=Q();return e===null?null:(0,J.jsxs)(`div`,{className:`panel openai-account-mode-banner`,style:{marginBottom:16},children:[(0,J.jsxs)(`div`,{className:`row`,children:[(0,J.jsx)(`strong`,{children:r(`codexAuth.accountModeTitle`)}),e===`pool`?(0,J.jsx)(`span`,{className:`badge badge-accent openai-account-mode-banner__badge-slot`,children:r(`codexAuth.accountModePool`)}):e===`direct`?(0,J.jsx)(`span`,{className:`badge badge-green openai-account-mode-banner__badge-slot`,children:r(`codexAuth.accountModeDirect`)}):null]}),e===`pool`&&(0,J.jsx)(`p`,{className:`card-sub openai-account-mode-banner__desc`,children:r(`codexAuth.accountModePoolDesc`)}),e===`direct`&&(0,J.jsxs)(`p`,{className:`card-sub openai-account-mode-banner__desc`,children:[r(`codexAuth.accountModeDirectDesc`),` `,(0,J.jsx)(`button`,{type:`button`,className:`link-btn`,onClick:()=>pt(`providers`),children:r(`codexAuth.openProviders`)})]}),(e===`absent`||e===`disabled`)&&(0,J.jsxs)(`div`,{className:`row`,style:{alignItems:`center`,marginTop:8},children:[(0,J.jsx)(`p`,{className:`card-sub`,style:{flex:1,margin:0},children:r(`codexAuth.openaiUnavailableDesc`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-primary btn-sm`,disabled:t,onClick:n,children:r(t?`codexAuth.enablingOpenai`:`codexAuth.enableOpenai`)})]}),e===`invalid`&&(0,J.jsxs)(`p`,{className:`card-sub openai-account-mode-banner__desc`,children:[r(`codexAuth.openaiMissing`),` `,(0,J.jsx)(`button`,{type:`button`,className:`link-btn`,onClick:()=>pt(`providers`),children:r(`codexAuth.openProviders`)})]})]})}function ig(e){if(!e||typeof e!=`object`)return;let t=e.providers;if(!t||typeof t!=`object`||Array.isArray(t)||!Object.hasOwn(t,`openai`))return;let n=t.openai;if(!(!n||typeof n!=`object`||Array.isArray(n)))return n}function ag({apiBase:e}){let t=Q(),n=`ocx.codex-auth.config.v1:${e}`,r=gr(n),[i,a]=(0,_.useState)(()=>r?.bannerState??null),[o,s]=(0,_.useState)(()=>r?.accountModeState??null),c=(0,_.useRef)(null),[l,u]=(0,_.useState)(!1),[d,f]=(0,_.useState)(``),p=(0,_.useCallback)(async()=>{let t=Vn(15e3);try{let r=await fetch(`${e}/api/config`,{signal:t.signal});if(!r.ok)throw Error(String(r.status));let i=await r.json(),o=Gs(ig(i));if(o===`absent`||o===`disabled`||o===`invalid`){a(o);let e=o===`disabled`?`disabled`:`absent`;s(e),br(n,{bannerState:o,accountModeState:e});return}let c=ng(i);a(c),s(c),br(n,{bannerState:c,accountModeState:c})}catch{}finally{t.clear()}},[e,n]);(0,_.useEffect)(()=>{c.current!==e&&(c.current=e,Promise.resolve().then(()=>{p()}));let t=Gn(()=>{p()},3e4);return()=>{t()}},[e,p]);let m=async()=>{u(!0),f(``);try{if(i!==`absent`&&i!==`disabled`)return;await Qs(e,i),await p()}catch(e){e instanceof Zs?f(t(e.i18nKey)):f(e instanceof Error?e.message:t(`prov.saveFailed`))}finally{u(!1)}};return(0,J.jsx)(J.Fragment,{children:(0,J.jsx)(Cs,{apiBase:e,accountModeState:o,banner:(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)(rg,{state:i,busy:l,onEnable:()=>{m()}}),d&&(0,J.jsx)(`div`,{className:`notice notice-err`,role:`alert`,children:d})]}),advancedExtras:(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)(tg,{apiBase:e}),(0,J.jsx)(eg,{apiBase:e})]})})})}var og={"base-instructions":`codexSet.layer.base-instructions`,"model-switch":`codexSet.layer.model-switch`,personality:`codexSet.layer.personality`,"context-window-guidance":`codexSet.layer.context-window-guidance`,realtime:`codexSet.layer.realtime`,"agents-md":`codexSet.layer.agents-md`,permissions:`codexSet.layer.permissions`,collaboration:`codexSet.layer.collaboration`,environment:`codexSet.layer.environment`,"environments-instructions":`codexSet.layer.environments-instructions`,apps:`codexSet.layer.apps`,plugins:`codexSet.layer.plugins`,tools:`codexSet.layer.tools`,skills:`codexSet.layer.skills`,"multi-agent-mode":`codexSet.layer.multi-agent-mode`,"git-attribution":`codexSet.layer.git-attribution`},sg={"base-instructions":`codexSet.about.base-instructions`,"model-switch":`codexSet.about.model-switch`,personality:`codexSet.about.personality`,"context-window-guidance":`codexSet.about.context-window-guidance`,realtime:`codexSet.about.realtime`,"agents-md":`codexSet.about.agents-md`,permissions:`codexSet.about.permissions`,collaboration:`codexSet.about.collaboration`,environment:`codexSet.about.environment`,"environments-instructions":`codexSet.about.environments-instructions`,apps:`codexSet.about.apps`,plugins:`codexSet.about.plugins`,tools:`codexSet.about.tools`,skills:`codexSet.about.skills`,"multi-agent-mode":`codexSet.about.multi-agent-mode`,"git-attribution":`codexSet.about.git-attribution`},cg={"model-switch":`codexSet.condition.model-switch`,realtime:`codexSet.condition.realtime`,"agents-md":`codexSet.condition.agents-md`,plugins:`codexSet.condition.plugins`,"git-attribution":`codexSet.condition.git-attribution`},lg={base:`codexSet.class.base`,"config-toggle":`codexSet.class.config-toggle`,"feature-gated":`codexSet.class.feature-gated`,"runtime-conditional":`codexSet.class.runtime-conditional`,"extension-unknown":`codexSet.class.extension-unknown`};function ug({descriptor:e,toggle:t,bytes:n,transitionOnly:r=!1,busy:i,writesRefused:a,onToggle:o,onSelectBase:s,baseSelection:c,onOpen:l}){let u=Q(),d=og[e.id],f=d?u(d):e.id,p=cg[e.id],m=t?.defaultedUserValue??e.default??!0,h=c?.kind==="default";return(0,J.jsxs)(`li`,{className:`codex-set-prompt__row`,"data-layer-id":e.id,"data-layer-class":e.class,children:[(0,J.jsx)(`span`,{className:`codex-set-prompt__pos`,"aria-hidden":`true`,children:e.order===null?`·`:e.order+1}),(0,J.jsx)(`button`,{type:`button`,className:`link-btn codex-set-prompt__name`,onClick:()=>l(e.id),children:f}),e.key&&(0,J.jsx)(`code`,{className:`codex-set-prompt__key`,children:e.key}),n!==null&&n>0&&(0,J.jsx)(`span`,{className:`codex-set-prompt__bytes`,title:u(`codexSet.dialog.sourceBytes`,{bytes:n}),children:n>=1024?Math.round(n/1024)+` KB`:n+` B`}),e.class===`base`&&s?(0,J.jsx)(`button`,{type:`button`,role:`switch`,className:`toggle ${h?`on`:``}`,"aria-checked":h,"aria-label":f,disabled:i||a||c?.kind===`external`,onClick:()=>{s(!h)},children:(0,J.jsx)(`span`,{className:`toggle-knob`})}):e.class===`config-toggle`?(0,J.jsx)(`button`,{type:`button`,role:`switch`,className:`toggle ${m?`on`:``}`,"aria-checked":m,"aria-label":f,disabled:i||a,onClick:()=>{o(e.id,!m)},children:(0,J.jsx)(`span`,{className:`toggle-knob`})}):e.class===`feature-gated`?(0,J.jsxs)(`span`,{className:`codex-set-prompt__note`,children:[u(`codexSet.row.featureGated`),` `,(0,J.jsx)(`button`,{type:`button`,className:`link-btn`,onClick:()=>pt(`integrations/codex`),children:u(`codexSet.row.openFeatures`)})]}):(0,J.jsx)(`span`,{className:`codex-set-prompt__note codex-set-prompt__note--locked`,children:u(r?`codexSet.row.onChange`:p||`codexSet.row.alwaysOn`)})]})}function dg({descriptor:e,toggle:t,text:n,busy:r,onToggle:i,onClose:a}){let o=Q(),s=(0,_.useRef)(null),c=`codex-set-layer-dialog-`+e.id;(0,_.useEffect)(()=>{let e=s.current,t=document.activeElement;return e&&!e.open&&e.showModal(),()=>{e?.open&&e.close(),t&&typeof t.focus==`function`&&t.focus()}},[]);let l=(0,_.useCallback)(e=>{e.preventDefault(),a()},[a]),u=lg[e.class],d=og[e.id],f=sg[e.id],p=cg[e.id];return(0,J.jsxs)(`dialog`,{ref:s,className:`modal-overlay`,"aria-labelledby":c,onCancel:l,children:[(0,J.jsx)(`button`,{type:`button`,className:`modal-backdrop-dismiss`,"aria-label":o(`common.close`),tabIndex:-1,onClick:a}),(0,J.jsxs)(`div`,{className:`modal-card codex-set-layer-dialog`,onClick:e=>e.stopPropagation(),role:`document`,children:[(0,J.jsxs)(`div`,{className:`modal-head`,children:[(0,J.jsx)(`h3`,{id:c,children:d?o(d):e.id}),e.class===`config-toggle`&&t&&(0,J.jsx)(`button`,{type:`button`,role:`switch`,className:`toggle ${t.defaultedUserValue?`on`:``}`,"aria-checked":t.defaultedUserValue,"aria-label":d?o(d):e.id,disabled:r,onClick:()=>{i(e.id,!t.defaultedUserValue)},children:(0,J.jsx)(`span`,{className:`toggle-knob`})}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:a,children:o(`common.close`)})]}),(0,J.jsx)(`p`,{className:`muted small`,children:o(f||`codexSet.dialog.unknownLayer`)}),(0,J.jsxs)(`div`,{className:`codex-set-layer-dialog__line`,children:[(0,J.jsx)(`span`,{className:`muted text-label`,children:o(`codexSet.dialog.class`)}),(0,J.jsx)(`span`,{children:o(u)})]}),e.key&&(0,J.jsxs)(`div`,{className:`codex-set-layer-dialog__line`,children:[(0,J.jsx)(`span`,{className:`muted text-label`,children:o(`codexSet.dialog.key`)}),(0,J.jsx)(`code`,{className:`api-code`,children:e.key}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:()=>{navigator.clipboard?.writeText(e.key??``)},children:o(`codexSet.dialog.copyKey`)})]}),e.class===`config-toggle`&&t&&(0,J.jsxs)(`div`,{className:`codex-set-layer-dialog__line`,children:[(0,J.jsx)(`span`,{className:`muted text-label`,children:o(`codexSet.dialog.fileValue`)}),(0,J.jsx)(`span`,{children:t.userFileValue===null?o(`codexSet.dialog.absentDefault`,{value:String(t.default)}):o(`codexSet.dialog.setValue`,{value:String(t.userFileValue),fallback:String(t.default)})})]}),e.class===`runtime-conditional`&&p&&(0,J.jsx)(`p`,{className:`muted small`,children:o(p)}),n?.reason===`ok`&&n.text?(0,J.jsxs)(J.Fragment,{children:[(0,J.jsxs)(`div`,{className:`codex-set-layer-dialog__line`,children:[(0,J.jsx)(`span`,{className:`muted text-label`,children:o(`codexSet.dialog.sourceText`)}),(0,J.jsx)(`span`,{className:`muted small`,children:o(`codexSet.dialog.sourceBytes`,{bytes:n.bytes})})]}),(0,J.jsx)(`pre`,{className:`api-code codex-set-layer-dialog__text`,children:n.text})]}):(0,J.jsx)(`p`,{className:`muted small codex-set-layer-dialog__no-text`,children:n?.reason===`empty-source`?o(`codexSet.dialog.emptySource`,{path:n.sourcePath??``}):n?.reason===`not-rendered`?o(`codexSet.dialog.notRendered`):n?.reason===`not-exposed`?o(`codexSet.dialog.notExposed`):o(`codexSet.dialog.textUnavailable`)})]})]})}function fg({layer:e,index:t,total:n,busy:r,onToggle:i,onEdit:a,onDelete:o,onMove:s}){let c=Q();return(0,J.jsxs)(`li`,{className:`codex-set-prompt__row codex-set-custom__row`,"data-custom-id":e.id,onKeyDown:i=>{!i.altKey||r||(i.key===`ArrowUp`&&t>0?(i.preventDefault(),s(e.id,-1)):i.key===`ArrowDown`&&ta(e.id),children:e.title}),(0,J.jsxs)(`span`,{className:`codex-set-custom__reorder`,children:[(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,"aria-label":c(`codexSet.custom.moveUp`,{title:e.title}),disabled:t===0||r,onClick:()=>s(e.id,-1),children:`↑`}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,"aria-label":c(`codexSet.custom.moveDown`,{title:e.title}),disabled:t===n-1||r,onClick:()=>s(e.id,1),children:`↓`})]}),(0,J.jsx)(`button`,{type:`button`,role:`switch`,className:`toggle ${e.enabled?`on`:``}`,"aria-checked":e.enabled,"aria-label":e.title,disabled:r,onClick:()=>i(e.id,!e.enabled),children:(0,J.jsx)(`span`,{className:`toggle-knob`})}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm codex-set-custom__delete`,"aria-label":c(`codexSet.custom.delete`,{title:e.title}),disabled:r,onClick:()=>o(e.id),children:`×`})]})}var pg=8192,mg=[{rule:`identity`,level:`warn`,messageKey:`codexSet.lint.identity`,pattern:/you\s+are\s+(claude|grok|gemini|gpt-|chatgpt)/gi},{rule:`foreign-tool`,level:`warn`,messageKey:`codexSet.lint.foreignTool`,pattern:/\b(Read|Edit|Write|Bash|Glob|Grep)\s+tool\b/g},{rule:`placeholder`,level:`warn`,messageKey:`codexSet.lint.placeholder`,pattern:/\$\{\{[\s\S]*?\}\}/g},{rule:`apply-patch`,level:`warn`,messageKey:`codexSet.lint.applyPatch`,pattern:/apply_patch\s+(?:is|must|should|means|works)/gi},{rule:`approval-vocab`,level:`warn`,messageKey:`codexSet.lint.approvalVocab`,pattern:/\b(always-approve|ask mode|acceptEdits)\b/gi},{rule:`environment`,level:`warn`,messageKey:`codexSet.lint.environment`,pattern:/\b(your (?:cwd|working directory) is|today's date is|you have no network access|you are running on (?:macos|linux|windows))/gi}];function hg(e){let t=0;for(let n of e){let e=n.codePointAt(0);t+=e<128?1:e<2048?2:e<65536?3:4}return t}function gg(e){let t=[];for(let n of mg){let r=new RegExp(n.pattern.source,n.pattern.flags);for(let i=r.exec(e);i!==null;i=r.exec(e))t.push({level:n.level,rule:n.rule,messageKey:n.messageKey,span:[i.index,i.index+i[0].length]}),i[0].length===0&&(r.lastIndex+=1)}return hg(e)>pg&&t.push({level:`info`,rule:`size`,messageKey:`codexSet.lint.size`}),t.sort((e,t)=>(e.span?.[0]??1/0)-(t.span?.[0]??1/0))}var _g=65536;function vg(e){let t=0;for(let n of e){let e=n.codePointAt(0);t+=e<128?1:e<2048?2:e<65536?3:4}return t}function yg(e){return e.replace(/\r\n/g,` +`).replace(/\r/g,` +`).replace(/\t/g,` `)}function bg(e){let t=0;for(let n of e){let e=n.codePointAt(0);if(n!==` +`&&(e<32||e===127)||e>=55296&&e<=57343)return{position:t};t+=1}return null}function xg(e,t){let n=e.title;if(n.trim().length===0)return{kind:`title-empty`};if(n.length>80)return{kind:`title-too-long`,length:n.length};if(/[\r\n]/.test(n))return{kind:`title-multiline`};let r=yg(e.body),i=vg(r);if(i>65536)return{kind:`body-too-large`,bytes:i};let a=bg(r);if(a)return{kind:`invalid-character`,position:a.position};let o=vg([...t.filter(t=>t.enabled&&t.id!==e.id).map(e=>e.body),...e.enabled?[r]:[]].join(` + +`));return o>131072?{kind:`composed-too-large`,bytes:o}:null}function Sg(e){let t=new Set(e.map(e=>e.id));for(;;){let e=``;for(let t=0;t<6;t+=1)e+=`abcdefghijklmnopqrstuvwxyz0123456789`[Math.floor(Math.random()*36)];if(!t.has(e))return e}}function Cg(e,t,n){let r=[...e],i=r.findIndex(e=>e.id===t);if(i===-1)return r;let a=i+n;if(a<0||a>=r.length)return r;let[o]=r.splice(i,1);return r.splice(a,0,o),r}function wg({layer:e,seed:t,others:n,busy:r,navigation:i,onSave:a,onClose:o}){let s=Q(),c=(0,_.useRef)(null),[l,u]=(0,_.useState)(e?.title??t?.title??``),[d,f]=(0,_.useState)(e?.body??t?.body??``),p=(0,_.useRef)(new Map),m=e?.id??null,h=(0,_.useRef)(m),g=(0,_.useRef)({title:l,body:d});(0,_.useEffect)(()=>{g.current={title:l,body:d}},[l,d]),(0,_.useEffect)(()=>{if(h.current===m)return;h.current!==null&&p.current.set(h.current,g.current),h.current=m;let t=m===null?void 0:p.current.get(m);u(t?.title??e?.title??``),f(t?.body??e?.body??``)},[m,e]);let[v,y]=(0,_.useState)(!1),b=`codex-set-custom-dialog`,x=e?.title??t?.title??``,S=e?.body??t?.body??``,C=l!==x||d!==S;(0,_.useEffect)(()=>{let e=c.current,t=document.activeElement;return e&&!e.open&&e.showModal(),()=>{e?.open&&e.close(),t&&typeof t.focus==`function`&&t.focus()}},[]);let w=(0,_.useCallback)(()=>{if(C){y(!0);return}o()},[C,o]),T=(0,_.useCallback)(e=>{e.preventDefault(),w()},[w]),E={id:e?.id??null,title:l,body:d,enabled:e?.enabled??!0},D=xg(E,n),O=yg(d),k=O!==d,A=(0,_.useMemo)(()=>gg(O),[O]),j=vg(O),M=D?D.kind===`title-empty`?s(`codexSet.custom.titleRequired`):D.kind===`title-too-long`?s(`codexSet.custom.titleTooLong`,{count:D.length,max:80}):D.kind===`title-multiline`?s(`codexSet.custom.titleMultiline`):D.kind===`body-too-large`?s(`codexSet.custom.bodyTooLarge`,{bytes:D.bytes,max:_g}):D.kind===`composed-too-large`?s(`codexSet.custom.composedTooLarge`,{bytes:D.bytes}):s(`codexSet.custom.invalidCharacter`,{position:D.position}):null;return(0,J.jsxs)(`dialog`,{ref:c,className:`modal-overlay`,"aria-labelledby":b,onCancel:T,children:[(0,J.jsx)(`button`,{type:`button`,className:`modal-backdrop-dismiss`,"aria-label":s(`common.close`),tabIndex:-1,onClick:w}),(0,J.jsxs)(`div`,{className:`modal-card codex-set-custom-dialog`,onClick:e=>e.stopPropagation(),role:`document`,children:[(0,J.jsxs)(`div`,{className:`modal-head`,children:[(0,J.jsx)(`h3`,{id:b,children:s(e?`codexSet.custom.editTitle`:`codexSet.custom.newTitle`)}),i&&(0,J.jsxs)(`span`,{className:`codex-set-custom-dialog__nav`,children:[(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,"aria-label":s(`codexSet.custom.prevLayer`),disabled:i.position<=1||r,onClick:i.onPrev,children:`←`}),(0,J.jsx)(`span`,{className:`codex-set-custom-dialog__nav-pos`,children:s(`codexSet.custom.navPosition`,{position:i.position,total:i.total})}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,"aria-label":s(`codexSet.custom.nextLayer`),disabled:i.position>=i.total||r,onClick:i.onNext,children:`→`})]})]}),(0,J.jsxs)(`label`,{className:`field`,children:[(0,J.jsx)(`span`,{className:`muted text-label`,children:s(`codexSet.custom.titleLabel`)}),(0,J.jsx)(`input`,{type:`text`,value:l,maxLength:100,onChange:e=>u(e.target.value)})]}),(0,J.jsxs)(`label`,{className:`field`,children:[(0,J.jsx)(`span`,{className:`muted text-label`,children:s(`codexSet.custom.bodyLabel`)}),(0,J.jsx)(`textarea`,{rows:10,value:d,onChange:e=>f(e.target.value)})]}),(0,J.jsx)(`p`,{className:`muted small`,children:s(`codexSet.custom.bodySize`,{bytes:j,max:_g})}),k&&(0,J.jsx)(`p`,{className:`muted small codex-set-custom-dialog__normalized`,children:s(`codexSet.custom.normalized`)}),M&&(0,J.jsx)(`div`,{className:`notice notice-err`,role:`alert`,children:M}),A.length>0&&(0,J.jsx)(`ul`,{className:`codex-set-custom-dialog__lint`,children:A.map((e,t)=>(0,J.jsxs)(`li`,{"data-lint-rule":e.rule,"data-lint-level":e.level,children:[s(e.messageKey),e.span&&(0,J.jsx)(`code`,{className:`codex-set-custom-dialog__span`,children:O.slice(e.span[0],e.span[1])})]},e.rule+`:`+t))}),v?(0,J.jsxs)(`div`,{className:`modal-actions codex-set-custom-dialog__discard`,role:`alertdialog`,"aria-labelledby":`codex-set-custom-dialog-discard`,children:[(0,J.jsx)(`span`,{id:`codex-set-custom-dialog-discard`,className:`muted small`,children:s(`codexSet.custom.discardPrompt`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-sm`,onClick:()=>y(!1),children:s(`codexSet.custom.keepEditing`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-danger btn-sm`,onClick:o,children:s(`common.discard`)})]}):(0,J.jsxs)(`div`,{className:`modal-actions`,children:[(0,J.jsx)(`button`,{type:`button`,className:`btn btn-primary btn-sm`,disabled:D!==null||r,onClick:()=>a({...E,body:O}),children:s(`common.save`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-sm`,onClick:w,children:s(`common.cancel`)})]})]})]})}var Tg=Object.freeze([{id:`concise`,nameKey:`codexSet.preset.concise.name`,descriptionKey:`codexSet.preset.concise.description`,provenanceKey:`codexSet.preset.concise.provenance`,body:[`Answer directly. Skip preamble, restatement of the question, and summaries of the work about to be done.`,`Prefer a short paragraph over a list, and a list over a table, unless the structure carries real meaning.`,`When the answer is a single fact, give the fact and stop.`].join(` +`)},{id:`plan-first`,nameKey:`codexSet.preset.planFirst.name`,descriptionKey:`codexSet.preset.planFirst.description`,provenanceKey:`codexSet.preset.planFirst.provenance`,body:[`Before changing anything non-trivial, state the plan in two or three sentences: what will change, where, and how it will be verified.`,`If the plan turns out to be wrong mid-way, say so and revise it rather than continuing quietly.`].join(` +`)},{id:`explain-why`,nameKey:`codexSet.preset.explainWhy.name`,descriptionKey:`codexSet.preset.explainWhy.description`,provenanceKey:`codexSet.preset.explainWhy.provenance`,body:[`When a choice had alternatives, name the alternative and why it lost.`,`Explain reasoning where it changes what the reader should do, not as a narration of every step.`,`State uncertainty plainly instead of presenting a guess as a conclusion.`].join(` +`)},{id:`test-first`,nameKey:`codexSet.preset.testFirst.name`,descriptionKey:`codexSet.preset.testFirst.description`,provenanceKey:`codexSet.preset.testFirst.provenance`,body:[`For a behavior change, write the failing test first and show that it fails for the expected reason.`,`A test that passes before the change is not evidence; say so rather than counting it.`].join(` +`)},{id:`korean`,nameKey:`codexSet.preset.korean.name`,descriptionKey:`codexSet.preset.korean.description`,provenanceKey:`codexSet.preset.korean.provenance`,body:[`Reply in Korean regardless of the language of the request, unless explicitly asked for another language.`,`Keep code, identifiers, file paths, and command output unchanged.`,`Write plain Korean: no translationese, one consistent register throughout.`].join(` +`)}]);function Eg({onBlank:e,onPreset:t,disabled:n,presets:r=Tg}){let i=Q(),[a,o]=(0,_.useState)(!1),s=(0,_.useRef)(null),c=a&&!n;(0,_.useEffect)(()=>{if(!c)return;let e=e=>{s.current?.contains(e.target)||o(!1)},t=e=>{e.key===`Escape`&&o(!1)};return document.addEventListener(`mousedown`,e),document.addEventListener(`keydown`,t),()=>{document.removeEventListener(`mousedown`,e),document.removeEventListener(`keydown`,t)}},[c]);let l=e=>{o(!1),e()};return r.length===0?(0,J.jsx)(`button`,{type:`button`,className:`btn btn-sm codex-set-custom__add`,disabled:n,onClick:e,children:i(`codexSet.custom.add`)}):(0,J.jsxs)(`div`,{className:`codex-set-preset`,ref:s,children:[(0,J.jsx)(`button`,{type:`button`,className:`btn btn-sm codex-set-custom__add`,"aria-expanded":c,disabled:n,onClick:()=>o(e=>!e),children:i(`codexSet.custom.add`)}),c&&(0,J.jsxs)(`div`,{className:`codex-set-preset__menu`,children:[(0,J.jsx)(`button`,{type:`button`,className:`codex-set-preset__item`,onClick:()=>l(e),children:(0,J.jsx)(`span`,{className:`codex-set-preset__name`,children:i(`codexSet.preset.blank`)})}),r.map(e=>(0,J.jsxs)(`button`,{type:`button`,className:`codex-set-preset__item`,"data-preset-id":e.id,onClick:()=>l(()=>t(e.body,i(e.nameKey))),children:[(0,J.jsx)(`span`,{className:`codex-set-preset__name`,children:i(e.nameKey)}),(0,J.jsx)(`span`,{className:`codex-set-preset__desc`,children:i(e.descriptionKey)}),(0,J.jsx)(`span`,{className:`codex-set-preset__provenance`,children:i(e.provenanceKey)}),(0,J.jsx)(`span`,{className:`codex-set-preset__preview`,children:e.body})]},e.id))]})]})}var Dg=48;function Og({variants:e,selection:t,maxVariants:n,busy:r,onSelect:i,onSave:a,onDelete:o,onClose:s}){let c=Q(),l=(0,_.useRef)(null),u=[{kind:`default`},...e.map(e=>({kind:`variant`,variant:e})),...e.lengthe.variant?.id===t.id)):0,[f,p]=(0,_.useState)(d),m=u[Math.min(f,u.length-1)],[h,g]=(0,_.useState)(m.variant?.title??``),[v,y]=(0,_.useState)(m.variant?.body??``),[b,x]=(0,_.useState)(m.variant?.id??null);(m.variant?.id??null)!==b&&(x(m.variant?.id??null),g(m.variant?.title??``),y(m.variant?.body??``));let S=(0,_.useCallback)(e=>{p(t=>{let n=t+e;return n<0?u.length-1:n>=u.length?0:n})},[u.length]);(0,_.useEffect)(()=>{let e=l.current;e&&!e.open&&e.showModal()},[]);let C=(0,_.useRef)(null),w=e=>{C.current={x:e.clientX,y:e.clientY}},T=e=>{let t=C.current;if(C.current=null,!t||r)return;let n=e.clientX-t.x,i=e.clientY-t.y;Math.abs(n){if(r)return;let t=e.target.tagName;t!==`TEXTAREA`&&t!==`INPUT`&&(e.key===`ArrowLeft`&&(e.preventDefault(),S(-1)),e.key===`ArrowRight`&&(e.preventDefault(),S(1)))},D=m.kind==="default"?t.kind==="default":m.kind===`variant`&&t.kind===`variant`&&t.id===m.variant.id,O=t.kind===`external`;return(0,J.jsx)(`dialog`,{ref:l,className:`modal-overlay codex-set-base-dialog`,"aria-label":c(`codexSet.base.title`),onClose:s,onKeyDown:E,onPointerDown:w,onPointerUp:T,children:(0,J.jsxs)(`div`,{className:`modal-card`,children:[(0,J.jsxs)(`div`,{className:`row`,children:[(0,J.jsx)(`strong`,{children:c(`codexSet.base.title`)}),(0,J.jsxs)(`span`,{className:`codex-set-base-dialog__nav`,children:[(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,"aria-label":c(`codexSet.base.prev`),disabled:r||u.length<2,onClick:()=>S(-1),children:`←`}),(0,J.jsx)(`span`,{className:`codex-set-base-dialog__pos`,"data-slot-kind":m.kind,children:c(`codexSet.base.position`,{position:f+1,total:u.length})}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,"aria-label":c(`codexSet.base.next`),disabled:r||u.length<2,onClick:()=>S(1),children:`→`})]})]}),(0,J.jsx)(`p`,{className:`card-sub`,children:c(`codexSet.base.swipeHint`)}),u.length>1&&(0,J.jsx)(`div`,{className:`codex-set-base-dialog__dots`,"aria-hidden":`true`,children:u.map((e,t)=>(0,J.jsx)(`span`,{className:`codex-set-base-dialog__dot${t===f?` active`:``}`},t))}),O&&(0,J.jsx)(`div`,{className:`notice notice-err`,role:`alert`,children:c(`codexSet.base.externalBlocked`,{path:t.path})}),m.kind==="default"?(0,J.jsxs)(`div`,{className:`codex-set-base-dialog__default`,children:[(0,J.jsx)(`strong`,{children:c(`codexSet.base.defaultTitle`)}),(0,J.jsx)(`p`,{className:`muted small`,children:c(`codexSet.base.defaultBody`)})]}):(0,J.jsxs)(J.Fragment,{children:[(0,J.jsxs)(`label`,{className:`field`,children:[(0,J.jsx)(`span`,{children:c(`codexSet.base.variantTitle`)}),(0,J.jsx)(`input`,{type:`text`,value:h,disabled:r||O,onChange:e=>g(e.target.value)})]}),(0,J.jsxs)(`label`,{className:`field`,children:[(0,J.jsx)(`span`,{children:c(`codexSet.base.variantBody`)}),(0,J.jsx)(`textarea`,{rows:12,value:v,disabled:r||O,onChange:e=>y(e.target.value)})]}),(0,J.jsx)(`p`,{className:`muted small`,children:c(`codexSet.base.replacesWarning`)})]}),(0,J.jsxs)(`div`,{className:`modal-actions`,children:[m.kind!=="default"&&(0,J.jsx)(`button`,{type:`button`,className:`btn btn-primary btn-sm`,disabled:r||O||v.trim().length===0,onClick:()=>a({id:m.variant?.id??null,title:h,body:v}),children:c(`common.save`)}),!D&&m.kind!==`new`&&(0,J.jsx)(`button`,{type:`button`,className:`btn btn-sm`,disabled:r||O,onClick:()=>i(m.kind==="default"?{kind:`default`}:{kind:`variant`,id:m.variant.id}),children:c(`codexSet.base.use`)}),D&&(0,J.jsx)(`span`,{className:`pill`,children:c(`codexSet.base.inUse`)}),m.kind===`variant`&&(0,J.jsx)(`button`,{type:`button`,className:`btn btn-danger btn-sm`,disabled:r||O,onClick:()=>o(m.variant.id),children:c(`common.delete`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-sm`,onClick:s,children:c(`common.close`)})]})]})})}function kg(e){return`codex-prompt:`+e}var Ag={"journal-present":`codexSet.drift.journalPresent`,"projection-stale":`codexSet.drift.projectionStale`,"store-missing":`codexSet.drift.storeMissing`,"owned-malformed":`codexSet.drift.ownedMalformed`};function jg({apiBase:e}){let t=Q(),n=kg(e),[r,i]=(0,_.useState)(``),[a,o]=(0,_.useState)(null),[s,c]=(0,_.useState)(null),[l,u]=(0,_.useState)(null),[d,f]=(0,_.useState)(null),[p,m]=(0,_.useState)(null),[h,g]=(0,_.useState)(null),[v,y]=(0,_.useState)(!1),[b,x]=(0,_.useState)(null),[S,C]=(0,_.useState)(null),w=(0,_.useCallback)(()=>{C(null)},[]),T=(0,_.useCallback)(async t=>{let n=await fetch(e+`/api/codex-prompt`,{signal:t});if(!n.ok)throw Error(String(n.status));return await n.json()},[e]),E=ml(n,[e],T,{isEmpty:e=>e.inventory.length===0}),D=E.data,O=E.state,k=async(r,a)=>{if(D){o(r),i(``);try{let o=await fetch(e+`/api/codex-prompt/toggle`,{method:`PUT`,headers:{"content-type":`application/json`},body:JSON.stringify({id:r,enabled:a,revision:D.revision})}),s=await o.json();if(!o.ok||!s.ok||!s.snapshot){if(s.code===`stale_revision`){E.refresh(),i(t(`codexSet.prompt.staleRevision`));return}i(s.message??t(`codexSet.prompt.writeFailed`)),E.refresh();return}K(n,s.snapshot),w(),C(null)}catch{i(t(`codexSet.prompt.writeFailed`)),E.refresh()}finally{o(null)}}},A=async(r,s)=>{if(!(!D||a!==null)){o(`base`),i(``);try{let a=await fetch(e+r,{method:`PUT`,headers:{"content-type":`application/json`},body:JSON.stringify({...s,revision:D.revision})}),o=await a.json();if(!a.ok||!o.ok||!o.snapshot){if(o.code===`stale_revision`){E.refresh(),i(t(`codexSet.prompt.staleRevision`));return}i(o.message??t(`codexSet.prompt.writeFailed`)),E.refresh();return}K(n,o.snapshot),w(),C(null)}catch{i(t(`codexSet.prompt.writeFailed`)),E.refresh()}finally{o(null)}}},j=async(r,s)=>{if(!D||a!==null)return!1;o(s),i(``);let c=D.custom;try{let a=await fetch(e+`/api/codex-prompt/custom`,{method:`PUT`,headers:{"content-type":`application/json`},body:JSON.stringify({layers:r,revision:D.revision})}),o=await a.json();return!a.ok||!o.ok||!o.snapshot?o.code===`stale_revision`?(E.refresh(),i(t(`codexSet.prompt.staleRevision`)),!1):(i(o.message??t(`codexSet.prompt.writeFailed`)),K(n,{...D,custom:c}),E.refresh(),!1):(K(n,o.snapshot),w(),!0)}catch{return i(t(`codexSet.prompt.writeFailed`)),E.refresh(),!1}finally{o(null)}},M=async e=>{if(!D)return;let t=D.custom,n=e.id===null?[...t,{id:Sg(t),title:e.title,body:e.body,enabled:!0}]:t.map(t=>t.id===e.id?{...t,title:e.title,body:e.body}:t);await j(n,e.id??`new`)&&u(null)},N=async r=>{if(D){o(`adopt`),i(``);try{let a=await fetch(e+`/api/codex-prompt/adopt`,{method:`POST`,headers:{"content-type":`application/json`},body:JSON.stringify(r?{confirm:!0,revision:D.revision}:{confirm:!1})}),o=await a.json();if(!a.ok||!o.ok){i(o.message??t(`codexSet.custom.adoptRefused`)),m(null),g(o.code===`adopt_unsupported_form`?{path:o.path,line:o.line,rawLine:o.rawLine}:null);return}if(o.snapshot){K(n,o.snapshot),w(),m(null);return}m(o.preview??null)}catch{i(t(`codexSet.prompt.writeFailed`))}finally{o(null)}}},P=async r=>{if(!(!D||D.drift===null)){y(!0),i(``);try{let a=await fetch(e+`/api/codex-prompt/repair`,{method:`POST`,headers:{"content-type":`application/json`},body:JSON.stringify(r?{confirm:!0,revision:D.revision}:{confirm:!1})}),o=await a.json();if(!a.ok||!o.ok){i(o.message??t(`codexSet.prompt.repairFailed`));return}o.snapshot?K(n,o.snapshot):E.refresh(),w()}catch{i(t(`codexSet.prompt.repairFailed`))}finally{y(!1)}}},F=[...D?.inventory??[]].filter(e=>e.class!==`extension-unknown`).sort((e,t)=>(e.order??2**53-1)-(t.order??2**53-1)),I=new Set([`realtime`,`model-switch`]),L=F.filter(e=>!I.has(e.id)),R=F.filter(e=>I.has(e.id)),z=F.find(e=>e.id===s)??null,B=l===null||l===`new`?-1:D?.custom.findIndex(e=>e.id===l)??-1,V=B>=0?D.custom[B]:null,H=l!==null&&l!==`new`&&D!==void 0&&B<0;return(0,_.useEffect)(()=>{if(S!==null)return;let t=new AbortController,n=!1;return(async()=>{try{let r=await fetch(e+`/api/codex-prompt/text`,{signal:t.signal});if(!r.ok){n||C({ok:!1});return}let i=await r.json();n||C(i)}catch{n||C({ok:!1})}})(),()=>{n=!0,t.abort()}},[S,e]),(0,J.jsxs)(`div`,{className:`panel codex-set-prompt`,children:[(0,J.jsx)(`div`,{className:`row`,children:(0,J.jsx)(`strong`,{children:t(`codexSet.prompt.title`)})}),(0,J.jsx)(`p`,{className:`card-sub`,children:t(`codexSet.prompt.timing`)}),O.refreshing&&(0,J.jsx)(_l,{live:!O.showError,children:t(`common.loading`)}),O.showSkeleton&&(0,J.jsx)(gl,{label:t(`common.loading`),rows:5}),D&&!D.readable&&(0,J.jsx)(`div`,{className:`notice notice-err`,role:`alert`,children:t(`codexSet.prompt.unreadable`)}),O.showError&&(0,J.jsx)(`div`,{className:`notice notice-err`,role:`alert`,children:t(`codexSet.prompt.loadFailed`)}),(r||H)&&(0,J.jsx)(`div`,{className:`notice notice-err`,role:`alert`,children:H?t(`codexSet.custom.layerGone`):r}),D?.drift&&(0,J.jsxs)(`div`,{className:`notice codex-set-prompt__drift`,role:`alert`,"data-drift":D.drift,children:[(0,J.jsx)(`span`,{children:t(Ag[D.drift])}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-sm`,disabled:v,onClick:()=>{P(!0)},children:t(`codexSet.prompt.repair`)})]}),(0,J.jsx)(`ul`,{className:`codex-set-prompt__rows`,children:L.map(e=>(0,J.jsx)(ug,{descriptor:e,toggle:D?.toggles.find(t=>t.id===e.id),bytes:S?.layers?.[e.id]?.bytes??null,busy:a===e.id,writesRefused:D?.readable===!1,onToggle:(e,t)=>{k(e,t)},onSelectBase:e.class===`base`&&D?(t=>{if(t){A(`/api/codex-prompt/base/select`,{kind:`default`});return}let n=D.baseVariants[0];if(!n){c(e.id);return}A(`/api/codex-prompt/base/select`,{kind:`variant`,id:n.id})}):void 0,baseSelection:D?.baseSelection,onOpen:c},e.id))}),R.length>0&&(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)(`div`,{className:`row codex-set-prompt__group`,children:(0,J.jsx)(`strong`,{children:t(`codexSet.group.transition`)})}),(0,J.jsx)(`p`,{className:`muted small`,children:t(`codexSet.group.transitionDesc`)}),(0,J.jsx)(`ul`,{className:`codex-set-prompt__rows`,children:R.map(e=>(0,J.jsx)(ug,{descriptor:e,toggle:D?.toggles.find(t=>t.id===e.id),bytes:S?.layers?.[e.id]?.bytes??null,transitionOnly:!0,busy:a===e.id,writesRefused:D?.readable===!1,onToggle:(e,t)=>{k(e,t)},onOpen:c},e.id))})]}),D&&!D.extensionLayersEnumerable&&(0,J.jsx)(`p`,{className:`muted small codex-set-prompt__extensions`,children:t(`codexSet.prompt.extensionsUnknown`)}),z&&(z.class===`base`&&D?(0,J.jsx)(Og,{variants:D.baseVariants,selection:D.baseSelection,maxVariants:D.maxBaseVariants,busy:a!==null||!D.readable,onSelect:e=>{A(`/api/codex-prompt/base/select`,e)},onSave:e=>{A(`/api/codex-prompt/base`,e)},onDelete:e=>{A(`/api/codex-prompt/base`,{id:e,delete:!0})},onClose:()=>c(null)}):(0,J.jsx)(dg,{descriptor:z,toggle:D?.toggles.find(e=>e.id===z.id),text:S?.layers?.[z.id],busy:a!==null,onToggle:(e,t)=>{k(e,t)},onClose:()=>c(null)})),D&&(0,J.jsxs)(`section`,{className:`codex-set-custom`,children:[(0,J.jsxs)(`div`,{className:`row`,children:[(0,J.jsx)(`strong`,{children:t(`codexSet.custom.heading`)}),D.developerInstructionsState===`external`?null:(0,J.jsx)(Eg,{disabled:D.custom.length>=32||a!==null||!D.readable,onBlank:()=>{x(null),u(`new`)},onPreset:(e,t)=>{x({body:e,title:t}),u(`new`)}})]}),D.custom.length>=32&&(0,J.jsx)(`p`,{className:`muted small`,children:t(`codexSet.custom.limitReached`,{max:32})}),D.developerInstructionsState===`external`&&D.modelInstructionsFile===null&&(0,J.jsxs)(`div`,{className:`codex-set-custom__adopt`,children:[(0,J.jsx)(`p`,{className:`muted small`,children:t(`codexSet.custom.notOwned`)}),h&&(0,J.jsx)(`p`,{className:`muted small codex-set-custom__adopt-refusal`,children:t(`codexSet.custom.adoptUnsupported`,{path:h.path??``,line:h.line??0})}),p?(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)(`pre`,{className:`api-code codex-set-custom__adopt-preview`,children:p.decodedBody}),(0,J.jsxs)(`div`,{className:`modal-actions`,children:[(0,J.jsx)(`button`,{type:`button`,className:`btn btn-primary btn-sm`,disabled:a!==null,onClick:()=>{N(!0)},children:t(`codexSet.custom.adoptConfirm`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-sm`,onClick:()=>m(null),children:t(`common.cancel`)})]})]}):(0,J.jsx)(`button`,{type:`button`,className:`btn btn-sm`,disabled:a!==null,onClick:()=>{N(!1)},children:t(`codexSet.custom.adopt`)})]}),D.modelInstructionsFile!==null&&(0,J.jsx)(`p`,{className:`muted small codex-set-custom__replaced`,children:t(`codexSet.custom.baseReplaced`,{path:D.modelInstructionsFile})}),(0,J.jsx)(`ul`,{className:`codex-set-prompt__rows`,children:D.custom.map((e,t)=>(0,J.jsx)(fg,{layer:e,index:t,total:D.custom.length,busy:a!==null||!D.readable,onToggle:(e,t)=>{j(D.custom.map(n=>n.id===e?{...n,enabled:t}:n),e)},onEdit:u,onDelete:f,onMove:(e,t)=>{j(Cg(D.custom,e,t),e)}},e.id))}),d&&(0,J.jsxs)(`div`,{className:`notice codex-set-custom__confirm`,role:`alertdialog`,"aria-labelledby":`codex-set-delete-confirm`,children:[(0,J.jsx)(`span`,{id:`codex-set-delete-confirm`,children:t(`codexSet.custom.deleteConfirmNamed`,{title:D.custom.find(e=>e.id===d)?.title??``})}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-danger btn-sm`,onClick:()=>{let e=d;f(null),j(D.custom.filter(t=>t.id!==e),e)},children:t(`common.delete`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-sm`,onClick:()=>f(null),children:t(`common.cancel`)})]})]}),l&&D&&!H&&(0,J.jsx)(wg,{layer:l===`new`?null:V,seed:l===`new`?b:null,others:D.custom,busy:a!==null,navigation:B>=0&&D.custom.length>1?{position:B+1,total:D.custom.length,onPrev:()=>{B>0&&u(D.custom[B-1].id)},onNext:()=>{B{u(null),x(null)}})]})}function Mg(){return window.location.hash.replace(/^#\/?/,``)===`codex-set/prompt`?`prompt`:`multiauth`}function Ng(e){window.location.hash=e===`prompt`?`codex-set/prompt`:`codex-set`}function Pg(e){e.key===`ArrowLeft`||e.key===`Home`?(e.preventDefault(),Ng(`multiauth`),document.getElementById(`codex-set-tab-multiauth`)?.focus()):(e.key===`ArrowRight`||e.key===`End`)&&(e.preventDefault(),Ng(`prompt`),document.getElementById(`codex-set-tab-prompt`)?.focus())}function Fg({apiBase:e}){let t=Q(),[n,r]=(0,_.useState)(Mg),[i,a]=(0,_.useState)(()=>Mg()===`prompt`),[o,s]=(0,_.useState)(()=>Mg()===`multiauth`);(0,_.useEffect)(()=>{let e=()=>r(Mg());return window.addEventListener(`hashchange`,e),()=>window.removeEventListener(`hashchange`,e)},[]);let c=i||n===`prompt`,l=o||n===`multiauth`;return c!==i&&a(!0),l!==o&&s(!0),(0,J.jsxs)(J.Fragment,{children:[(0,J.jsxs)(`div`,{className:`page-tabs`,role:`tablist`,"aria-label":t(`nav.codexSet`),children:[(0,J.jsx)(`button`,{type:`button`,role:`tab`,id:`codex-set-tab-multiauth`,"aria-selected":n===`multiauth`,"aria-controls":`codex-set-panel-multiauth`,tabIndex:n===`multiauth`?0:-1,className:`page-tab${n===`multiauth`?` page-tab--active`:``}`,onClick:()=>Ng(`multiauth`),onKeyDown:Pg,children:t(`codexSet.tab.multiauth`)}),(0,J.jsx)(`button`,{type:`button`,role:`tab`,id:`codex-set-tab-prompt`,"aria-selected":n===`prompt`,"aria-controls":`codex-set-panel-prompt`,tabIndex:n===`prompt`?0:-1,className:`page-tab${n===`prompt`?` page-tab--active`:``}`,onClick:()=>Ng(`prompt`),onKeyDown:Pg,children:t(`codexSet.tab.prompt`)})]}),c&&(0,J.jsx)(`div`,{role:`tabpanel`,id:`codex-set-panel-prompt`,"aria-labelledby":`codex-set-tab-prompt`,hidden:n!==`prompt`,children:(0,J.jsx)(jg,{apiBase:e})}),l&&(0,J.jsx)(`div`,{role:`tabpanel`,id:`codex-set-panel-multiauth`,"aria-labelledby":`codex-set-tab-multiauth`,hidden:n!==`multiauth`,children:(0,J.jsx)(ag,{apiBase:e})})]})}var Ig=[`opencode`,`pi`,`omp`,`hermes`,`openclaw`,`kimi`,`gajae`,`dsh`,`mcode`,`zcode`,`prime`,`aside`],Lg={opencode:`api.clientConfig.clientOpencode`,pi:`api.clientConfig.clientPi`,omp:`api.clientConfig.clientOmp`,hermes:`api.clientConfig.clientHermes`,openclaw:`api.clientConfig.clientOpenclaw`,kimi:`api.clientConfig.clientKimi`,gajae:`api.clientConfig.clientGajae`,dsh:`api.clientConfig.clientDsh`,mcode:`api.clientConfig.clientMcode`,zcode:`api.clientConfig.clientZcode`,prime:`api.clientConfig.clientPrime`,aside:`api.clientConfig.clientAside`},Rg={opencode:`/provider-icons/opencode.svg`,pi:`/provider-icons/pi.svg`,omp:`/provider-icons/oh-my-pi.svg`,hermes:`/provider-icons/hermes-agent.svg`,openclaw:`/provider-icons/openclaw.svg`,kimi:`/provider-icons/kimi-color.svg`,gajae:`/provider-icons/gajae-code.svg`,dsh:`/provider-icons/deepseek-harness.svg`,mcode:`/provider-icons/minimax.svg`,zcode:`/provider-icons/zcode.svg`,prime:`/provider-icons/prime-agent.svg`,aside:`/provider-icons/aside.svg`},zg=new Set([`opencode`,`kimi`,`prime`,`aside`,`hermes`]),Bg={codex:`/provider-icons/openai.svg`,claude:`/provider-icons/claude-color.svg`,claudeDesktop:`/provider-icons/claude-color.svg`,grok:`/provider-icons/grok.svg`,cursor:`/provider-icons/cursor-color.svg`},Vg={...Bg,opencode:Rg.opencode??null,pi:Rg.pi??null,omp:Rg.omp??null,hermes:Rg.hermes??null,openclaw:Rg.openclaw??null,kimi:Rg.kimi??null,gajae:Rg.gajae??null,dsh:Rg.dsh??null,mcode:Rg.mcode??null,zcode:Rg.zcode??null,prime:Rg.prime??null,aside:Rg.aside??null},Hg=[Bg.grok],Ug=new Set([...[...zg].map(e=>Rg[e]).filter(e=>e!==void 0),...Hg]);function Wg(e){return Vg[e]}function Gg({src:e,label:t,size:n=20,className:r}){let i=r?`client-mark ${r}`:`client-mark`,a={"--client-mark-size":String(n)+`px`};return e?Ug.has(e)?(0,J.jsx)(`span`,{className:`${i} client-mark--mask`,style:{...a,maskImage:`url(${e})`,WebkitMaskImage:`url(${e})`},"aria-hidden":`true`}):(0,J.jsx)(`span`,{className:`${i} client-mark--img`,style:a,"aria-hidden":`true`,children:(0,J.jsx)(`img`,{src:e,alt:``,width:n,height:n})}):(0,J.jsx)(`span`,{className:`${i} client-mark--monogram`,style:a,"aria-hidden":`true`,children:t.slice(0,1)})}function Kg(){return typeof document>`u`?null:document.querySelector(`meta[name="opencodex-runtime-role"]`)?.getAttribute(`content`)?.trim()||null}function qg(){return Kg()===`client`}function Jg(e){return e.replace(/\/+$/,``)}function Yg(e){return new URL(e||`/`,window.location.href)}function Xg(e){try{let t=new URL(e);return t.protocol!==`http:`&&t.protocol!==`https:`||t.username||t.password||t.pathname!==`/`||t.search||t.hash?null:t.origin}catch{return null}}function Zg(e,t,n,r){let i=Jg(t);return{id:e,baseUrl:i,serverOrigin:n,bootstrapPath:`${i}/opencodex-session`,transport:r}}function Qg(e){let t=Yg(e),n=Jg(e);return{connected:!1,machine:Zg(`machine`,n,t.origin,`same-origin`),shared:Zg(`shared`,n,t.origin,`same-origin`)}}function $g(e){if(!e||typeof e!=`object`||Array.isArray(e))return!1;let t=e;return t.mode===`client`&&t.connected===!0&&t.protocolVersion===1&&(t.managementTransport===`direct`||t.managementTransport===`relay`)&&typeof t.machineBase==`string`&&typeof t.sharedBase==`string`&&typeof t.sharedServerOrigin==`string`&&typeof t.apiKeyId==`string`&&t.apiKeyId.trim().length>0&&typeof t.connectedAt==`string`}function e_(e,t){if(!$g(t))throw TypeError(`machine status response is invalid`);let n=Qg(e),r=Xg(t.machineBase),i=Xg(t.sharedServerOrigin);if(!r||r!==n.machine.serverOrigin||!i)throw TypeError(`machine status target origins are invalid`);let a;try{a=new URL(t.sharedBase)}catch{throw TypeError(`machine status shared target is invalid`)}if(a.username||a.password||a.search||a.hash)throw TypeError(`machine status shared target is invalid`);if(t.managementTransport===`direct`){if(a.origin!==i||a.pathname!==`/`)throw TypeError(`machine status direct target is inconsistent`)}else if(a.origin!==r||a.pathname!==`/api/machine/hub-relay`)throw TypeError(`machine status relay target is inconsistent`);return{connected:!0,machine:Zg(`machine`,Jg(e),r,`same-origin`),shared:t.managementTransport===`relay`?Zg(`shared`,`${Jg(e)}/api/machine/hub-relay`,i,`relay`):Zg(`shared`,i,i,`direct`),apiKeyId:t.apiKeyId}}function t_(e,t){return t[e].baseUrl}async function n_(e,t){let n=Qg(e);if(Kg()!==`client`)return n;let r;try{r=await fetch(`${n.machine.baseUrl}/api/machine/status`,{signal:t,cache:`no-store`})}catch(e){throw Error(`local machine plane unavailable`,{cause:e})}if(r.status===404)return n;if(!r.ok)throw Error(`local machine plane refused discovery (${r.status})`);let i=await r.json().catch(()=>null);if(!$g(i))throw Error(`local machine plane returned invalid status`);return e_(e,i)}function r_(e){return e?[`responses`,`chat`,`messages`]:[`responses`,`chat`]}function i_(e){let t=e.id.indexOf(`/`),n=typeof e.owned_by==`string`&&e.owned_by.trim()?e.owned_by.trim():void 0,r=e.is_combo===!0?`combo`:t>0?e.id.slice(0,t):n??`openai`,i=t<0&&r===`openai`,a=r!==`openai`&&r!==`combo`;return{id:e.id,displayName:e.id,provider:r,native:i,custom:a}}function a_(e){return e.id}function o_(e){if(!e||typeof e!=`object`)return!1;let t=e;return t.ambiguous===!0||!(typeof t.requests7d!=`number`||!Number.isFinite(t.requests7d)||typeof t.totalRequests!=`number`||!Number.isFinite(t.totalRequests)||t.lastUsedAt!==void 0&&(typeof t.lastUsedAt!=`string`||Number.isNaN(new Date(t.lastUsedAt).getTime())))}var s_=new Set([`required`,`accepted`,`rejected`]);function c_(e){return Array.isArray(e)&&e.length>0&&e.every(e=>{if(!e||typeof e!=`object`)return!1;let t=e;return typeof t.endpoint==`string`&&s_.has(t.bearer)&&s_.has(t.dedicated)&&s_.has(t.xApiKey)})}var l_={baseUrl:`http://127.0.0.1:10100/v1`,responses:`http://127.0.0.1:10100/v1/responses`,chatCompletions:`http://127.0.0.1:10100/v1/chat/completions`,messages:`http://127.0.0.1:10100/v1/messages`,models:`http://127.0.0.1:10100/v1/models`};function u_(e){let t=e||l_.responses,n=t.match(/^(.*)\/v1\/responses\/?$/),r=n?`${n[1]}/v1`:t.replace(/\/responses\/?$/,``);return{baseUrl:r,responses:t,chatCompletions:`${r}/chat/completions`,messages:`${r}/messages`,models:`${r}/models`}}function d_(e,t){let n=new Date(e);return!e||Number.isNaN(n.getTime())?`—`:n.toLocaleDateString(t)}function f_({url:e}){return(0,J.jsx)(p_,{text:e,hintKey:`api.copyUrlHint`,copiedKey:`api.urlCopied`,className:`api-endpoint-url-btn`,children:(0,J.jsx)(`code`,{className:`api-code api-code-inline api-endpoint-url`,children:e})})}function p_({text:e,hintKey:t,copiedKey:n,className:r,children:i}){let{t:a}=ct(),o=(0,_.useId)(),s=(0,_.useRef)(null),[c,l]=(0,_.useState)(!1),[u,d]=(0,_.useState)(!1),[f,p]=(0,_.useState)(null),m=(0,_.useRef)(null),h=c||u;(0,_.useEffect)(()=>()=>{m.current!==null&&window.clearTimeout(m.current)},[]),(0,_.useLayoutEffect)(()=>{if(!h)return;let e=()=>{let e=s.current;if(!e)return;let t=e.getBoundingClientRect();p({top:Math.max(8,t.top-8),left:t.left+t.width/2})};return e(),window.addEventListener(`scroll`,e,!0),window.addEventListener(`resize`,e),()=>{window.removeEventListener(`scroll`,e,!0),window.removeEventListener(`resize`,e)}},[h]);let g=()=>l(!0),v=()=>l(!1),y=async()=>{try{await navigator.clipboard.writeText(e),d(!0),m.current!==null&&window.clearTimeout(m.current),m.current=window.setTimeout(()=>d(!1),1500)}catch{d(!1)}},b=h&&f?(0,mt.createPortal)((0,J.jsx)(`span`,{id:o,className:`ocx-tooltip-bubble api-copy-tip-fixed`,role:`tooltip`,style:{top:f.top,left:f.left},children:a(u?n:t)}),document.body):null,x={ref:s,className:`ocx-tooltip ${r}`,onMouseEnter:g,onMouseLeave:v,onFocus:g,onBlur:v,onClick:e=>{if(typeof window<`u`&&window.getSelection()?.toString())return;let t=e.target;if(t instanceof HTMLElement&&t!==e.currentTarget){let n=t.getBoundingClientRect();if(t.scrollHeight>t.clientHeight+1&&e.clientX>=n.right-16||t.scrollWidth>t.clientWidth+1&&e.clientY>=n.bottom-16)return}y()},onKeyDown:e=>{e.key===`Escape`&&v()},"aria-label":a(t),"aria-describedby":h?o:void 0};return(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)(`button`,{type:`button`,...x,children:i}),b]})}function m_({text:e}){return(0,J.jsx)(p_,{text:e,hintKey:`api.copyExampleHint`,copiedKey:`api.exampleCopied`,className:`api-example-copy-btn`,children:(0,J.jsx)(`code`,{className:`api-code api-example-pre`,children:e})})}function h_(e,t){return t(e===`required`?`api.auth.required`:e===`accepted`?`api.auth.accepted`:`api.auth.rejected`)}function g_({endpoints:e,claudeCodeEnabled:t,authMatrix:n}){let{t:r}=ct();return(0,J.jsxs)(`div`,{className:`panel api-panel`,children:[(0,J.jsx)(`h3`,{className:`panel-title`,children:r(`api.endpointsTitle`)}),(0,J.jsxs)(`div`,{className:`api-endpoints`,children:[(0,J.jsxs)(`div`,{children:[(0,J.jsx)(`span`,{className:`muted small`,children:r(`api.baseUrl`)}),(0,J.jsx)(f_,{url:e.baseUrl})]}),(0,J.jsxs)(`div`,{children:[(0,J.jsx)(`span`,{className:`muted small`,children:r(`api.responsesEndpoint`)}),(0,J.jsx)(f_,{url:e.responses})]}),(0,J.jsxs)(`div`,{children:[(0,J.jsx)(`span`,{className:`muted small`,children:r(`api.chatCompletionsEndpoint`)}),(0,J.jsx)(f_,{url:e.chatCompletions})]}),t&&(0,J.jsxs)(`div`,{children:[(0,J.jsx)(`span`,{className:`muted small`,children:r(`api.messagesEndpoint`)}),(0,J.jsx)(f_,{url:e.messages})]}),(0,J.jsxs)(`div`,{children:[(0,J.jsx)(`span`,{className:`muted small`,children:r(`api.modelsEndpoint`)}),(0,J.jsx)(f_,{url:e.models})]})]}),(0,J.jsx)(`p`,{className:`muted small`,children:r(`api.endpointNote`)}),(0,J.jsxs)(`div`,{className:`api-auth-matrix-block`,children:[(0,J.jsx)(`h4`,{className:`api-auth-matrix-title`,children:r(`api.authTitle`)}),(0,J.jsx)(`div`,{className:`api-auth-matrix-scroll`,children:(0,J.jsxs)(`table`,{className:`api-auth-matrix`,children:[(0,J.jsx)(`thead`,{children:(0,J.jsxs)(`tr`,{children:[(0,J.jsx)(`th`,{children:r(`api.auth.endpoint`)}),(0,J.jsx)(`th`,{children:(0,J.jsx)(`code`,{children:`Authorization: Bearer`})}),(0,J.jsx)(`th`,{children:(0,J.jsx)(`code`,{children:`x-opencodex-api-key`})}),(0,J.jsx)(`th`,{children:(0,J.jsx)(`code`,{children:`x-api-key`})})]})}),(0,J.jsx)(`tbody`,{children:n.map(e=>(0,J.jsxs)(`tr`,{children:[(0,J.jsx)(`td`,{children:(0,J.jsx)(`code`,{children:e.endpoint})}),(0,J.jsx)(`td`,{children:h_(e.bearer,r)}),(0,J.jsx)(`td`,{children:h_(e.dedicated,r)}),(0,J.jsx)(`td`,{children:h_(e.xApiKey,r)})]},e.endpoint))})]})}),(0,J.jsx)(`p`,{className:`muted small`,children:r(`api.authLoopback`)}),(0,J.jsx)(`p`,{className:`muted small`,children:r(`api.authBaseUrlNote`)})]})]})}function __({keys:e,keysLoading:t=!1,keysLoadFailed:n,newName:r,creating:i,newKey:a,copied:o,confirmDelete:s,localeTag:c,showKeyList:l=!0,onNewNameChange:u,onCreate:d,onDismissNewKey:f,onCopyKey:p,onConfirmDelete:m,onCancelDelete:h,onDelete:g}){let{t:_}=ct();return(0,J.jsxs)(J.Fragment,{children:[a&&(0,J.jsxs)(`div`,{className:`panel api-panel panel-accent api-newkey-panel`,children:[(0,J.jsx)(`h3`,{className:`panel-title`,children:_(`api.newKeyTitle`)}),(0,J.jsx)(`p`,{className:`muted small`,children:_(`api.newKeyNote`)}),(0,J.jsxs)(`div`,{className:`api-form-row`,children:[(0,J.jsx)(`code`,{className:`api-code`,style:{flex:1,wordBreak:`break-all`},children:a}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-sm btn-ghost`,onClick:p,children:o?(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)(ue,{}),` `,_(`api.copied`)]}):_(`api.copy`)})]}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-sm btn-ghost`,style:{alignSelf:`flex-start`},onClick:f,children:_(`api.dismiss`)})]}),(0,J.jsxs)(`div`,{className:`panel api-panel api-generate-panel`,children:[(0,J.jsx)(`h3`,{className:`panel-title`,children:_(`api.generateTitle`)}),(0,J.jsxs)(`div`,{className:`api-form-row`,children:[(0,J.jsx)(`input`,{id:`api-key-name`,type:`text`,placeholder:_(`api.keyNamePlaceholder`),"aria-label":_(`api.keyNamePlaceholder`),value:r,maxLength:64,onChange:e=>u(e.target.value),className:`input`}),(0,J.jsxs)(`button`,{type:`button`,className:`btn btn-primary`,onClick:d,disabled:i,children:[(0,J.jsx)(fe,{}),` `,_(i?`api.generating`:`api.generate`)]})]})]}),l&&(0,J.jsxs)(`div`,{className:`panel api-panel`,style:{marginTop:`1rem`},"aria-busy":t,children:[(0,J.jsx)(`h3`,{className:`panel-title`,children:t?_(`api.activeKeysLoading`):_(`api.activeKeys`,{count:e.length})}),t?(0,J.jsx)(`div`,{className:`api-active-keys-skeleton`,role:`status`,"aria-label":_(`common.loading`)}):e.length>0?(0,J.jsx)(`div`,{className:`tbl-wrap`,children:(0,J.jsxs)(`table`,{className:`tbl`,children:[(0,J.jsx)(`thead`,{children:(0,J.jsxs)(`tr`,{children:[(0,J.jsx)(`th`,{children:_(`api.colName`)}),(0,J.jsx)(`th`,{children:_(`api.colKey`)}),(0,J.jsx)(`th`,{children:_(`api.colCreated`)}),(0,J.jsx)(`th`,{})]})}),(0,J.jsx)(`tbody`,{children:e.map(e=>(0,J.jsxs)(`tr`,{children:[(0,J.jsx)(`td`,{children:e.name}),(0,J.jsx)(`td`,{children:(0,J.jsx)(`code`,{children:e.prefix})}),(0,J.jsx)(`td`,{children:d_(e.createdAt,c)}),(0,J.jsx)(`td`,{children:s===e.id?(0,J.jsxs)(`span`,{className:`api-actions`,children:[(0,J.jsx)(`button`,{type:`button`,className:`btn btn-sm btn-danger`,onClick:()=>g(e.id),children:_(`api.confirm`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-sm btn-ghost`,onClick:h,children:_(`common.cancel`)})]}):(0,J.jsx)(`button`,{type:`button`,className:`btn btn-sm btn-ghost`,"aria-label":_(`api.deleteAria`),onClick:()=>m(e.id),children:(0,J.jsx)(de,{})})})]},e.id))})]})}):n?(0,J.jsx)(`p`,{className:`muted`,children:_(`api.keysLoadFailed`)}):(0,J.jsx)(`p`,{className:`muted`,children:_(`api.noKeys`)})]})]})}function v_({filteredModels:e,modelsLoading:t,modelsRefreshing:n=!1,modelsLoadFailed:r,modelCount:i,hasModelData:a,modelQuery:o,copiedModelId:s,modelTests:c,claudeCodeEnabled:l,onModelQueryChange:u,onCopyModelId:d,onTestModel:f,onRetryModels:p,canTestModels:m,sourceLabel:h,protocolLabel:g}){let{t:_}=ct();return(0,J.jsxs)(`div`,{className:`panel api-panel api-models-panel`,children:[(0,J.jsxs)(`div`,{className:`api-panel-head`,children:[(0,J.jsx)(`h3`,{className:`panel-title`,children:_(`api.modelsTitle`)}),(0,J.jsx)(`span`,{className:`muted mono text-label`,children:_(`api.modelsCount`,{count:e.length})})]}),(0,J.jsx)(`p`,{className:`muted small`,children:_(`api.modelsSubtitle`)}),(0,J.jsx)(`input`,{type:`search`,className:`input`,value:o,onChange:e=>u(e.target.value),placeholder:_(`api.modelsSearch`),"aria-label":_(`api.modelsSearch`)}),r&&(0,J.jsxs)(`div`,{className:`api-models-error`,children:[(0,J.jsx)(`p`,{className:`muted small`,role:`alert`,children:_(`api.modelsLoadFailed`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:p,children:_(`common.retry`)})]}),n&&!t&&(0,J.jsx)(`p`,{className:`muted small`,"aria-live":`polite`,children:_(`api.modelsLoading`)}),t?(0,J.jsx)(gl,{label:_(`api.modelsLoading`),rows:3}):a?e.length===0?(0,J.jsx)(`p`,{className:`muted small api-models-empty`,children:i===0?_(`api.modelsEmpty`):_(`api.modelsNoMatch`,{query:o.trim()})}):(0,J.jsx)(`div`,{className:`api-models-scroll`,children:(0,J.jsxs)(`table`,{className:`tbl`,children:[(0,J.jsx)(`thead`,{children:(0,J.jsxs)(`tr`,{children:[(0,J.jsx)(`th`,{children:_(`api.colModel`)}),(0,J.jsx)(`th`,{children:_(`api.colSource`)}),(0,J.jsx)(`th`,{children:_(`api.colProtocols`)})]})}),(0,J.jsx)(`tbody`,{children:e.map(e=>{let t=a_(e),n=r_(l);return(0,J.jsxs)(`tr`,{children:[(0,J.jsx)(`td`,{children:(0,J.jsxs)(`div`,{className:`api-model-cell`,children:[(0,J.jsx)(`code`,{children:t}),e.displayName!==e.id&&(0,J.jsx)(`span`,{className:`muted small`,children:e.displayName})]})}),(0,J.jsx)(`td`,{children:h(e)}),(0,J.jsx)(`td`,{children:(0,J.jsxs)(`div`,{className:`api-model-actions`,children:[(0,J.jsx)(`button`,{type:`button`,className:`btn btn-sm btn-ghost`,onClick:()=>{d(t)},children:_(s===t?`api.modelCopied`:`api.copyModelId`)}),n.map(n=>{let r=c[t]?.[n],i=r?.state??`idle`;return(0,J.jsxs)(`span`,{className:`api-model-test-chip`,children:[(0,J.jsx)(`button`,{type:`button`,className:`btn btn-sm btn-ghost`,disabled:i===`testing`||!m,title:m?void 0:_(`api.auth.testNeedsFreshKey`),onClick:()=>{f(e,n)},children:_(`api.auth.testProtocol`,{protocol:g(n)})}),i!==`idle`&&(0,J.jsx)(`span`,{className:`api-test-note api-test-note--${i}`,role:`status`,"aria-live":`polite`,"aria-atomic":`true`,children:i===`testing`?_(`api.testingModel`):i===`ok`?_(`api.testSucceeded`):r?.detail??_(`api.testFailed`)})]},n)})]})})]},t)})})]})}):null]})}function y_({endpoints:e,claudeCodeEnabled:t}){let{t:n}=ct(),r=JSON.stringify(n(`api.usageSampleInput`)),i=`curl ${e.chatCompletions} \\ + -H "x-opencodex-api-key: ocx_YOUR_KEY_HERE" \\ + -H "Content-Type: application/json" \\ + -d '{ + "model": "gpt-5.4", + "messages": [{"role": "user", "content": ${r}}] + }'`,a=`curl ${e.responses} \\ + -H "x-opencodex-api-key: ocx_YOUR_KEY_HERE" \\ + -H "Content-Type: application/json" \\ + -d '{ + "model": "gpt-5.4", + "input": ${r} + }'`,o=`curl ${e.messages} \\ + -H "x-opencodex-api-key: ocx_YOUR_KEY_HERE" \\ + -H "Content-Type: application/json" \\ + -d '{ + "model": "claude-sonnet-4-6", + "max_tokens": 64, + "messages": [{"role": "user", "content": ${r}}] + }'`;return(0,J.jsxs)(`section`,{className:`panel api-panel awi-usage-panel`,children:[(0,J.jsx)(`h3`,{className:`panel-title`,children:n(`api.workspace.usageExamples`)}),(0,J.jsxs)(`div`,{className:`awi-usage-panel-body`,children:[(0,J.jsxs)(`div`,{className:`awi-usage-example`,children:[(0,J.jsx)(`h4`,{className:`awi-usage-example-title`,children:n(`api.usageChatTitle`)}),(0,J.jsx)(m_,{text:i})]}),(0,J.jsxs)(`div`,{className:`awi-usage-example`,children:[(0,J.jsx)(`h4`,{className:`awi-usage-example-title`,children:n(`api.usageResponsesTitle`)}),(0,J.jsx)(m_,{text:a})]}),t&&(0,J.jsxs)(`div`,{className:`awi-usage-example`,children:[(0,J.jsx)(`h4`,{className:`awi-usage-example-title`,children:n(`api.usageMessagesTitle`)}),(0,J.jsx)(m_,{text:o})]})]})]})}function b_({client:e,apiBase:t,onOpenDetails:n,onCopy:r,onDownload:i}){let a=Q(),[o,s]=(0,_.useState)(0),c=[t,e,String(o)].join(`|`),[l,u]=(0,_.useState)(null);(0,_.useEffect)(()=>{let n=new AbortController,r=!1;return(async()=>{try{let i=await fetch(`${t}/api/client-config?client=${encodeURIComponent(e)}`,{signal:n.signal});if(!i.ok)throw Error(String(i.status));let a=await i.json();if(r)return;u({key:c,data:a,failed:!1})}catch{if(r)return;u({key:c,data:null,failed:!0})}})(),()=>{r=!0,n.abort()}},[t,e,c]);let d=l!==null&&l.key===c?l:null,f=d?.data??null,p=d?.failed??!1,m=d===null,h=a(Lg[e]),g=Rg[e],v=f?.text??``,y=(0,_.useCallback)(t=>{f&&n(e,f,v,t.currentTarget)},[e,f,v,n]);return(0,J.jsxs)(`li`,{className:`awi-clientconfig-row`,children:[(0,J.jsx)(`span`,{className:`awi-clientconfig-mark`,children:(0,J.jsx)(Gg,{src:g??null,label:h,size:20})}),(0,J.jsxs)(`span`,{className:`awi-clientconfig-identity`,children:[(0,J.jsx)(`span`,{className:`awi-clientconfig-name`,children:h}),(0,J.jsx)(`span`,{className:`muted text-label awi-clientconfig-meta`,children:m?a(`api.clientConfig.loading`):p||!f?(0,J.jsx)(`span`,{role:`alert`,children:a(`api.clientConfig.rowError`,{client:h})}):a(`api.clientConfig.rowMeta`,{destination:f.destination,count:f.modelCount})})]}),(0,J.jsx)(`span`,{className:`awi-clientconfig-row-actions`,children:p&&!m?(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:()=>s(e=>e+1),children:a(`common.retry`)}):(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)(`button`,{type:`button`,className:`btn btn-primary btn-sm`,"aria-label":a(`api.clientConfig.copyAria`,{client:h}),disabled:!f,onClick:()=>{f&&r(e,v)},children:a(`api.clientConfig.copy`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-sm`,"aria-label":a(`api.clientConfig.downloadAria`,{client:h}),disabled:!f,onClick:()=>{f&&i(e,f,v)},children:a(`api.clientConfig.download`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,"aria-label":a(`api.clientConfig.detailsAria`,{client:h}),disabled:!f,onClick:y,children:a(`api.clientConfig.details`)})]})})]})}function x_({client:e,envelope:t,json:n,hasKeys:r,onClose:i,onCopy:a,onDownload:o}){let s=Q(),c=(0,_.useRef)(null),l=`awi-clientconfig-dialog-${e}`;(0,_.useEffect)(()=>{let e=c.current;return e&&!e.open&&e.showModal(),()=>{e?.open&&e.close()}},[]);let u=(0,_.useCallback)(e=>{e.preventDefault(),i()},[i]);return(0,J.jsxs)(`dialog`,{ref:c,className:`modal-overlay`,"aria-labelledby":l,onCancel:u,children:[(0,J.jsx)(`button`,{type:`button`,className:`modal-backdrop-dismiss`,"aria-label":s(`common.close`),tabIndex:-1,onClick:i}),(0,J.jsxs)(`div`,{className:`modal-card awi-clientconfig-dialog`,onClick:e=>e.stopPropagation(),role:`document`,children:[(0,J.jsxs)(`div`,{className:`modal-head`,children:[(0,J.jsx)(`h3`,{id:l,children:s(Lg[e])}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:i,children:s(`common.close`)})]}),(0,J.jsx)(`pre`,{className:`api-code api-example-pre awi-clientconfig-json`,tabIndex:0,role:`group`,"aria-label":s(`api.clientConfig.jsonLabel`,{client:s(Lg[e])}),children:n}),(0,J.jsx)(`p`,{className:`muted small awi-clientconfig-count`,children:s(`api.clientConfig.modelCount`,{count:t.modelCount})}),t.modelsWithoutLimits>0&&(0,J.jsx)(`p`,{className:`muted small awi-clientconfig-degraded`,children:s(`api.clientConfig.missingLimits`,{count:t.modelsWithoutLimits,total:t.modelCount})}),!r&&(0,J.jsx)(`p`,{className:`muted small awi-clientconfig-nokey`,children:s(`api.clientConfig.noKeyYet`,{env:t.apiKeyEnv})}),(0,J.jsxs)(`div`,{className:`awi-clientconfig-line`,children:[(0,J.jsx)(`span`,{className:`muted text-label`,children:s(`api.clientConfig.destination`)}),(0,J.jsx)(m_,{text:t.destination})]}),(0,J.jsxs)(`div`,{className:`awi-clientconfig-line`,children:[(0,J.jsx)(`span`,{className:`muted text-label`,children:s(`api.clientConfig.envHint`)}),(0,J.jsx)(m_,{text:t.exportHint})]}),(0,J.jsx)(`p`,{className:`muted small awi-clientconfig-merge`,children:s(`api.clientConfig.mergeWarning`)}),(0,J.jsx)(`p`,{className:`muted text-label awi-clientconfig-where-title`,children:s(`api.clientConfig.whereDisclosure`)}),(0,J.jsx)(`p`,{className:`muted small`,children:s(`api.clientConfig.whereBody`)}),(0,J.jsxs)(`div`,{className:`modal-actions`,children:[(0,J.jsx)(`button`,{type:`button`,className:`btn btn-primary btn-sm`,onClick:a,children:s(`api.clientConfig.copy`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-sm`,onClick:o,children:s(`api.clientConfig.download`)})]})]})]})}function S_({apiBase:e,baseUrl:t,hasKeys:n}){let r=Q(),[i,a]=(0,_.useState)(null),[o,s]=(0,_.useState)(``),c=(0,_.useRef)(null);(0,_.useEffect)(()=>{if(i!==null)return;let e=c.current;e&&(c.current=null,e.isConnected&&e.focus())},[i]);let l=(0,_.useCallback)(async(e,t,n)=>{try{await navigator.clipboard.writeText(t),s(n?r(`api.clientConfig.copiedAnnounceClient`,{client:r(Lg[e])}):r(`api.clientConfig.copiedAnnounce`))}catch{s(r(`api.clientConfig.copyFailed`))}},[r]),u=(0,_.useCallback)((e,t,n)=>{let i=URL.createObjectURL(new Blob([n],{type:t.mediaType})),a=document.createElement(`a`);a.href=i,a.download=t.filename,a.click(),URL.revokeObjectURL(i),s(r(`api.clientConfig.downloadedAnnounce`,{filename:t.filename,destination:t.destination}))},[r]),d=(0,_.useCallback)(()=>a(null),[]),f=(0,_.useCallback)((e,t,n,r)=>{c.current=r,a({client:e,envelope:t,json:n})},[]);return(0,J.jsxs)(`section`,{className:`panel api-panel awi-clientconfig-panel`,children:[(0,J.jsx)(`div`,{className:`api-panel-head awi-clientconfig-head`,children:(0,J.jsx)(`h3`,{className:`panel-title`,children:r(`api.clientConfig.title`)})}),(0,J.jsx)(`ul`,{className:`awi-clientconfig-rows`,"aria-label":r(`api.clientConfig.rowsLabel`),children:Ig.map(t=>(0,J.jsx)(b_,{client:t,apiBase:e,onOpenDetails:f,onCopy:(e,t)=>{l(e,t,!0)},onDownload:u},t))}),(0,J.jsxs)(`div`,{className:`awi-clientconfig-line`,children:[(0,J.jsx)(`span`,{className:`muted text-label`,children:r(`api.baseUrl`)}),(0,J.jsx)(m_,{text:t})]}),(0,J.jsx)(`div`,{className:`sr-only`,"aria-live":`polite`,"aria-atomic":`true`,children:o}),i&&(0,J.jsx)(x_,{client:i.client,envelope:i.envelope,json:i.json,hasKeys:n,onClose:d,onCopy:()=>{l(i.client,i.json,!1)},onDownload:()=>u(i.client,i.envelope,i.json)})]})}function C_({keys:e,keysLoading:t,keysLoadFailed:n,attributionSince:r,localeTag:i,busy:a,onSelect:o}){let s=Q();return(0,J.jsxs)(`div`,{className:`panel api-panel awi-keylist-panel`,"aria-busy":t,children:[(0,J.jsx)(`div`,{className:`api-panel-head`,children:(0,J.jsx)(`h3`,{className:`panel-title`,children:t?s(`api.activeKeysLoading`):s(`api.activeKeys`,{count:e.length})})}),t?(0,J.jsx)(`div`,{className:`api-active-keys-skeleton`,role:`status`,"aria-label":s(`common.loading`)}):e.length===0?(0,J.jsx)(`p`,{className:`muted small`,children:s(n?`api.keysLoadFailed`:`api.noKeys`)}):(0,J.jsx)(`div`,{className:`tbl-wrap`,children:(0,J.jsxs)(`table`,{className:`tbl awi-keylist-table`,children:[(0,J.jsx)(`thead`,{children:(0,J.jsxs)(`tr`,{children:[(0,J.jsx)(`th`,{children:s(`api.colName`)}),(0,J.jsx)(`th`,{children:s(`api.colKey`)}),(0,J.jsx)(`th`,{children:s(`api.attribution.requests7d`)}),(0,J.jsx)(`th`,{children:s(`api.attribution.lastUsed`)})]})}),(0,J.jsx)(`tbody`,{children:e.map(e=>(0,J.jsxs)(`tr`,{children:[(0,J.jsx)(`td`,{children:(0,J.jsx)(`button`,{type:`button`,className:`awi-keylist-name`,disabled:a,onClick:()=>o(e.id),children:e.name})}),(0,J.jsx)(`td`,{children:(0,J.jsx)(`code`,{children:e.prefix})}),(0,J.jsx)(`td`,{children:r?e.usage.ambiguous?s(`api.attribution.railAmbiguous`):e.usage.requests7d.toLocaleString(i):s(`api.attribution.unavailable`)}),(0,J.jsx)(`td`,{children:!r||e.usage.ambiguous?`—`:e.usage.lastUsedAt?d_(e.usage.lastUsedAt,i):s(`api.attribution.neverUsed`)})]},e.id))})]})})]})}function w_({keys:e,apiBase:t,attributionSince:n,historyTruncated:r,authMatrix:i,keysLoading:a,keysLoadFailed:o,endpoints:s,claudeCodeEnabled:c,localeTag:l,newName:u,creating:d,newKey:f,copied:p,rotationSecret:m=null,rotationCopied:h=!1,filteredModels:g,modelsLoading:v,modelsRefreshing:y=!1,modelsLoadFailed:b,modelCount:x,hasModelData:S,modelQuery:C,copiedModelId:w,modelTests:T,canTestModels:E,onNewNameChange:D,onCreate:O,onDismissNewKey:k,onCopyKey:A,onDelete:j,onRename:M,onRotationStart:N,onRotationCommit:P,onRotationAbort:F,onCopyRotationSecret:I,onDismissRotationSecret:L,onModelQueryChange:R,onCopyModelId:z,onTestModel:B,onRetryModels:V,sourceLabel:H,protocolLabel:U}){let W=Q(),[ee,G]=(0,_.useState)(null),[K,q]=(0,_.useState)(!1),[Y,te]=(0,_.useState)(!1),[ne,re]=(0,_.useState)(!1),[ie,ae]=(0,_.useState)(!1),[oe,se]=(0,_.useState)(``),[ce,le]=(0,_.useState)(!1),[ue,de]=(0,_.useState)(!1),[fe,pe]=(0,_.useState)(!1),[X,me]=(0,_.useState)(!1),[ge,_e]=(0,_.useState)(!1),Z=ee?e.find(e=>e.id===ee)??null:null,ve=Z?m?.id===Z.id?m.rotationId:Z.pendingRotation?.id:void 0,ye=ne||ce||X,be=async e=>{if(!(!Z||X)){me(!0),_e(!1);try{let t=ve;(e===`start`?await N?.(Z.id)??!1:t&&(await(e===`commit`?P:F)?.(Z.id,t)??!1))||_e(!0)}finally{me(!1)}}},xe=(0,_.useMemo)(()=>[{id:`keys`,label:W(`api.section.keys`),meta:a?void 0:String(e.length)},{id:`connect`,label:W(`api.section.connect`)},{id:`endpoints`,label:W(`api.section.endpoints`)},{id:`models`,label:W(`api.section.models`),meta:String(x)},{id:`examples`,label:W(`api.section.examples`)}],[W,e.length,a,x]),Ce=()=>{q(!1),te(!1)},we=()=>{G(null),Ce(),ae(!1),de(!1),pe(!1)};(0,_.useEffect)(()=>{if(!K)return;let e=window.setTimeout(()=>te(!0),300);return()=>window.clearTimeout(e)},[K]);let Te=()=>{Z&&q(!0)},Ee=async()=>{if(!(!Z||!Y||ne)){re(!0),pe(!1);try{await j(Z.id)?(Ce(),G(null)):pe(!0)}finally{re(!1)}}},De=()=>{Z&&(se(Z.name),de(!1),ae(!0))},Oe=async()=>{if(!Z||ce)return;let e=oe.trim();if(!e||e===Z.name){ae(!1);return}le(!0),de(!1);try{await M(Z.id,e)?ae(!1):de(!0)}finally{le(!1)}};return(0,J.jsxs)(`div`,{className:`apikeys-workspace-shell`,children:[!Z&&(0,J.jsx)(yp,{scope:`api`,items:xe,ariaLabel:W(`api.workspace.sections`)}),(0,J.jsx)(`div`,{className:`apikeys-workspace-root`,children:(0,J.jsx)(`section`,{className:`apikeys-workspace-main`,"aria-label":W(`api.workspace.details`),children:Z?(0,J.jsxs)(`div`,{className:`awi-detail`,children:[(0,J.jsx)(`div`,{className:`awi-detail-toolbar`,children:(0,J.jsxs)(`button`,{type:`button`,className:`awi-back`,onClick:we,disabled:ye,children:[(0,J.jsx)(Se,{className:`awi-back-chevron`,"aria-hidden":`true`}),W(`modal.back`)]})}),(0,J.jsxs)(`div`,{className:`awi-detail-body`,children:[(0,J.jsxs)(`div`,{className:`awi-detail-head`,children:[(0,J.jsx)(`h2`,{className:`awi-detail-title`,children:Z.name}),(0,J.jsx)(`span`,{className:`awi-detail-actions`,children:K?(0,J.jsxs)(J.Fragment,{children:[(0,J.jsxs)(`button`,{type:`button`,className:`btn btn-danger btn-sm awi-confirm-delete`,onClick:()=>{Ee()},disabled:!Y||ne,children:[(0,J.jsx)(he,{}),` `,W(ne?`api.key.deleting`:`api.confirm`)]},`confirm-delete`),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:Ce,disabled:ne,children:W(`common.cancel`)})]}):(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:De,disabled:ie,children:W(`api.key.rename`)},`rename`),(0,J.jsxs)(`button`,{type:`button`,className:`btn btn-danger btn-sm`,onClick:Te,"aria-label":W(`api.deleteAria`),children:[(0,J.jsx)(he,{}),` `,W(`api.workspace.deleteKey`)]},`request-delete`)]})})]}),K&&(0,J.jsx)(`p`,{className:`muted awi-delete-hint`,children:W(`api.workspace.deleteConfirm`)}),fe&&(0,J.jsx)(`p`,{className:`awi-delete-error`,role:`alert`,children:W(`api.deleteFailed`)}),ie&&(0,J.jsxs)(`div`,{className:`awi-rename`,children:[(0,J.jsx)(`label`,{className:`awi-rename-label`,htmlFor:`awi-key-name`,children:W(`api.key.name`)}),(0,J.jsx)(`input`,{id:`awi-key-name`,className:`input`,type:`text`,value:oe,maxLength:64,disabled:ce,onChange:e=>se(e.target.value),onKeyDown:e=>{e.key===`Enter`&&(e.preventDefault(),Oe())}}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-sm`,onClick:()=>{Oe()},disabled:ce,children:W(ce?`api.key.renaming`:`api.key.saveName`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:()=>ae(!1),disabled:ce,children:W(`common.cancel`)}),ue&&(0,J.jsx)(`p`,{className:`awi-rename-error`,role:`alert`,children:W(`api.key.renameFailed`)})]}),(0,J.jsxs)(`div`,{className:`awi-section`,children:[(0,J.jsx)(`h3`,{className:`awi-section-title`,children:W(`api.workspace.keyDetails`)}),(0,J.jsxs)(`dl`,{className:`awi-kv`,children:[(0,J.jsxs)(`div`,{className:`awi-kv-row`,children:[(0,J.jsx)(`dt`,{children:W(`api.workspace.keyPrefix`)}),(0,J.jsx)(`dd`,{children:(0,J.jsx)(`code`,{children:Z.prefix})})]}),(0,J.jsxs)(`div`,{className:`awi-kv-row`,children:[(0,J.jsx)(`dt`,{children:W(`api.colCreated`)}),(0,J.jsx)(`dd`,{children:d_(Z.createdAt,l)})]})]})]}),(0,J.jsxs)(`div`,{className:`awi-section`,"aria-live":`polite`,children:[(0,J.jsx)(`h3`,{className:`awi-section-title`,children:W(`api.rotation.title`)}),ve?(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)(`p`,{className:`muted`,children:W(`api.rotation.pending`)}),Z.pendingRotation&&(0,J.jsxs)(`p`,{className:`muted`,children:[W(`api.rotation.expires`),` `,d_(Z.pendingRotation.expiresAt,l)]}),m?.id===Z.id&&(0,J.jsxs)(`div`,{className:`api-key-reveal`,role:`status`,children:[(0,J.jsx)(`p`,{children:W(`api.rotation.secretOnce`)}),(0,J.jsx)(`code`,{children:m.key}),(0,J.jsxs)(`span`,{children:[(0,J.jsx)(`button`,{type:`button`,className:`btn btn-sm`,onClick:I,children:W(h?`api.copied`:`api.copy`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:L,children:W(`common.close`)})]})]}),(0,J.jsxs)(`div`,{className:`awi-detail-actions`,children:[(0,J.jsx)(`button`,{type:`button`,className:`btn btn-sm`,disabled:X,onClick:()=>{be(`commit`)},children:W(`api.rotation.commit`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,disabled:X,onClick:()=>{be(`abort`)},children:W(`api.rotation.abort`)})]})]}):(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)(`p`,{className:`muted`,children:W(`api.rotation.description`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,disabled:X,onClick:()=>{be(`start`)},children:W(X?`api.rotation.starting`:`api.rotation.start`)})]}),ge&&(0,J.jsx)(`p`,{className:`awi-delete-error`,role:`alert`,children:W(`api.rotation.failed`)})]}),(0,J.jsxs)(`div`,{className:`awi-section`,children:[(0,J.jsx)(`h3`,{className:`awi-section-title`,children:W(`api.attribution.title`)}),n?Z.usage.ambiguous?(0,J.jsx)(`p`,{className:`muted`,children:W(`api.attribution.ambiguous`)}):(0,J.jsxs)(`dl`,{className:`awi-kv`,children:[(0,J.jsxs)(`div`,{className:`awi-kv-row`,children:[(0,J.jsx)(`dt`,{children:W(`api.attribution.requests7d`)}),(0,J.jsx)(`dd`,{children:Z.usage.requests7d.toLocaleString(l)})]}),(0,J.jsxs)(`div`,{className:`awi-kv-row`,children:[(0,J.jsx)(`dt`,{children:W(r?`api.attribution.totalRequestsAvailable`:`api.attribution.totalRequests`)}),(0,J.jsx)(`dd`,{children:Z.usage.totalRequests.toLocaleString(l)})]}),(0,J.jsxs)(`div`,{className:`awi-kv-row`,children:[(0,J.jsx)(`dt`,{children:W(`api.attribution.lastUsed`)}),(0,J.jsx)(`dd`,{children:Z.usage.lastUsedAt?d_(Z.usage.lastUsedAt,l):W(`api.attribution.neverUsed`)})]}),(0,J.jsxs)(`div`,{className:`awi-kv-row`,children:[(0,J.jsx)(`dt`,{children:W(r?`api.attribution.sinceAvailable`:`api.attribution.since`)}),(0,J.jsx)(`dd`,{children:d_(n,l)})]})]}):(0,J.jsx)(`p`,{className:`muted`,children:W(`api.attribution.unavailableDetail`)})]})]})]}):(0,J.jsx)(`div`,{className:`awi-overview`,children:(0,J.jsxs)(`div`,{className:`awi-overview-section`,children:[(0,J.jsxs)(`div`,{id:gp(`api`,`keys`),className:`awi-section-anchor`,children:[(0,J.jsx)(__,{keys:e,keysLoading:a,keysLoadFailed:o,newName:u,creating:d,newKey:f,copied:p,confirmDelete:null,localeTag:l,showKeyList:!1,onNewNameChange:D,onCreate:O,onDismissNewKey:k,onCopyKey:A,onConfirmDelete:()=>{},onCancelDelete:()=>{},onDelete:()=>{}}),(0,J.jsx)(C_,{keys:e,keysLoading:a,keysLoadFailed:o,attributionSince:n,localeTag:l,busy:ye,onSelect:e=>{G(e),Ce(),ae(!1),de(!1),pe(!1)}})]}),(0,J.jsx)(`div`,{id:gp(`api`,`connect`),className:`awi-section-anchor`,children:(0,J.jsx)(S_,{apiBase:t,baseUrl:s.baseUrl,hasKeys:e.length>0})}),(0,J.jsx)(`div`,{id:gp(`api`,`endpoints`),className:`awi-section-anchor`,children:(0,J.jsx)(g_,{endpoints:s,claudeCodeEnabled:c,authMatrix:i})}),(0,J.jsx)(`div`,{id:gp(`api`,`models`),className:`awi-section-anchor`,children:(0,J.jsx)(v_,{filteredModels:g,modelsLoading:v,modelsRefreshing:y,modelsLoadFailed:b,modelCount:x,hasModelData:S,modelQuery:C,copiedModelId:w,modelTests:T,claudeCodeEnabled:c,onModelQueryChange:R,onCopyModelId:z,onTestModel:B,onRetryModels:V,canTestModels:E,sourceLabel:H,protocolLabel:U})}),(0,J.jsx)(`div`,{id:gp(`api`,`examples`),className:`awi-section-anchor`,children:(0,J.jsx)(y_,{endpoints:s,claudeCodeEnabled:c})})]})})})})]})}var T_=[],E_=15e3;function D_(e){let t=e.replace(/\/$/,``);if(!t)return l_;try{return new URL(t).host?u_(`${t}/v1/responses`):l_}catch{return l_}}function O_(e){return!e||!c_(e.authMatrix)||!Array.isArray(e.keys)||e.keys.some(e=>!e||!o_(e.usage)||!k_(e.pendingRotation))?null:e}function k_(e){if(e===void 0)return!0;if(!e||typeof e!=`object`||Array.isArray(e))return!1;let t=e;return typeof t.id==`string`&&!!t.id&&typeof t.createdAt==`string`&&!Number.isNaN(Date.parse(t.createdAt))&&typeof t.expiresAt==`string`&&!Number.isNaN(Date.parse(t.expiresAt))}function A_({apiBase:e,active:t=!0}){let{t:n,locale:r}=ct(),i=et.find(e=>e.code===r)?.htmlLang,a=`ocx.apikeys.list.v2:${e}`,o=`ocx.apikeys.models.v1:${e}`,s=`api-keys:${e}`,c=`api-models:${e}`,l=vr(a),u=vr(o),d=O_(l?.data??null),f=u?.data??null,[p,m]=(0,_.useState)(null),[h,g]=(0,_.useState)(``),[v,y]=(0,_.useState)(null),[b,x]=(0,_.useState)({}),[S,C]=(0,_.useState)(``),[w,T]=(0,_.useState)(!1),[E,D]=(0,_.useState)(null),[O,k]=(0,_.useState)(!1),[A,j]=(0,_.useState)(null),[M,N]=(0,_.useState)(!1),P=(0,_.useRef)(!1),F=(0,_.useCallback)(async t=>{let r=await Ft(await fetch(`${e}/api/keys`,{signal:t}));if(!r||!c_(r.authMatrix))throw Error(n(`api.keysLoadFailed`));let i=r.keys??[];if(i.some(e=>!o_(e.usage)||!k_(e.pendingRotation)))throw Error(n(`api.keysLoadFailed`));let o=i,s=u_(r.endpoint??``),c={keys:o,endpoints:{baseUrl:r.baseUrl??s.baseUrl,responses:r.responsesEndpoint??r.endpoint??l_.responses,chatCompletions:r.chatCompletionsEndpoint??s.chatCompletions,messages:r.messagesEndpoint??s.messages,models:r.modelsEndpoint??s.models},claudeCodeEnabled:r.claudeCodeEnabled!==!1,...r.attributionSince?{attributionSince:r.attributionSince}:{},...r.historyTruncated===!0?{historyTruncated:!0}:{},authMatrix:r.authMatrix};return yr(a,c),c},[e,a,n]),I=(0,_.useCallback)(async t=>{let r=await fetch(`${e}/v1/models`,{signal:t});if(!r.ok)throw Error(n(`api.modelsLoadFailed`));let i=await r.json(),a=Array.isArray(i)?i:typeof i==`object`&&i&&Array.isArray(i.data)?i.data:null;if(!a)throw Error(n(`api.modelsLoadFailed`));let s=a.filter(e=>typeof e==`object`&&!!e&&typeof e.id==`string`).map(e=>i_(e)).sort((e,t)=>a_(e).localeCompare(a_(t)));return yr(o,s),s},[e,o,n]),L=ml(s,[e],F,{isEmpty:e=>e.keys.length===0,initialData:d??void 0,initialDataCachedAt:l?.cachedAt??null,staleAfterMs:6e4,enabled:t}),R=ml(c,[e],I,{isEmpty:e=>e.length===0,initialData:f??void 0,initialDataCachedAt:u?.cachedAt??null,staleAfterMs:6e4,enabled:t}),z=L.state,B=R.state,V=z.data??d,H=B.data??f??T_,U=V?.keys??[],W=V?.endpoints??D_(e),ee=V?.claudeCodeEnabled??!0,G=V?.attributionSince,K=V?.historyTruncated===!0,q=V?.authMatrix??[],Y=L.refresh,te=R.refresh,ne=(0,_.useMemo)(()=>{let e=h.trim().toLowerCase();return e?H.filter(t=>a_(t).toLowerCase().includes(e)||t.displayName.toLowerCase().includes(e)||t.provider.toLowerCase().includes(e)):H},[h,H]),re=async t=>{if(P.current)return!1;P.current=!0,T(!0),m(null);try{let r=t??S,i=await Pt(await fetch(`${e}/api/keys`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({name:r||`default`})}),n(`api.createFailed`));return typeof i?.key!=`string`||i.key.length===0?(m(n(`api.createFailed`)),!1):(D(i.key),C(``),Y(),!0)}catch{return m(n(`api.createFailed`)),!1}finally{P.current=!1,T(!1)}},ie=async t=>{m(null);let n=Vn(E_);try{return(await fetch(`${e}/api/keys`,{method:`DELETE`,headers:{"Content-Type":`application/json`},body:JSON.stringify({id:t}),signal:n.signal})).ok?(Y(),!0):!1}catch{return!1}finally{n.clear()}},ae=async(t,n)=>{m(null);let r=Vn(E_);try{return(await fetch(`${e}/api/keys`,{method:`PATCH`,headers:{"Content-Type":`application/json`},body:JSON.stringify({id:t,name:n}),signal:r.signal})).ok?(Y(),!0):!1}catch{return!1}finally{r.clear()}},oe=async t=>{m(null);let r=Vn(E_);try{let i=await Pt(await fetch(`${e}/api/keys/rotate`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({id:t}),signal:r.signal}),n(`api.rotation.startFailed`));return!i||typeof i.key!=`string`||!i.key||typeof i.rotationId!=`string`||!i.rotationId?!1:(j({id:t,key:i.key,rotationId:i.rotationId}),Y(),!0)}catch{return!1}finally{r.clear()}},se=async(t,n,r)=>{m(null);let i=Vn(E_);try{return(await fetch(`${e}${r===`commit`?`/api/keys/rotate/commit`:`/api/keys/rotate`}`,{method:r===`commit`?`POST`:`DELETE`,headers:{"Content-Type":`application/json`},body:JSON.stringify({id:t,rotationId:n}),signal:i.signal})).ok?(j(e=>e?.id===t?null:e),Y(),!0):!1}catch{return!1}finally{i.clear()}},ce=async()=>{if(A)try{await navigator.clipboard.writeText(A.key),N(!0),window.setTimeout(()=>N(!1),2e3)}catch{m(n(`api.key.copyFailed`))}},le=async()=>{if(E){m(null);try{await navigator.clipboard.writeText(E),k(!0),window.setTimeout(()=>k(!1),2e3)}catch{k(!1),m(n(`api.key.copyFailed`))}}},ue=async e=>{try{await navigator.clipboard.writeText(e),y(e),window.setTimeout(()=>y(t=>t===e?null:t),2e3)}catch{}},de=e=>e.native?n(`api.sourceNative`):e.provider===`combo`?n(`api.sourceCombo`):e.custom?n(`api.sourceCustom`):jn(e.provider,n),fe=e=>n(e===`responses`?`api.protocolResponses`:e===`messages`?`api.protocolMessages`:`api.protocolChatCompletions`),pe=(e,t)=>e===`responses`?{url:W.responses,body:{model:t,input:`ping`,max_output_tokens:1,stream:!1}}:e===`messages`?{url:W.messages,body:{model:t,max_tokens:1,messages:[{role:`user`,content:`ping`}]}}:{url:W.chatCompletions,body:{model:t,messages:[{role:`user`,content:`ping`}],max_tokens:1,stream:!1}},X=(e,t,n)=>x(r=>({...r,[e]:{...r[e],[t]:n}})),me=async(e,t)=>{if(!E)return;let r=a_(e),i=pe(t,r);X(r,t,{state:`testing`});try{let e=await fetch(i.url,{method:`POST`,headers:{"Content-Type":`application/json`,"x-opencodex-api-key":E},body:JSON.stringify(i.body)});if(!e.ok){let n=await e.text();X(r,t,{state:`error`,detail:n.slice(0,160)||String(e.status)});return}X(r,t,{state:`ok`})}catch(e){X(r,t,{state:`error`,detail:e instanceof Error?e.message:n(`api.testFailed`)})}},he=n(`api.subtitle`).split(`{authHeader}`);return(0,J.jsxs)(`section`,{className:`api-page`,"aria-busy":z.refreshing||B.refreshing||void 0,children:[(0,J.jsx)(`div`,{className:`page-head`,children:(0,J.jsx)(`h2`,{children:n(`api.title`)})}),(0,J.jsxs)(`p`,{className:`page-sub`,children:[he[0],(0,J.jsx)(`code`,{children:`x-opencodex-api-key`}),he[1]]}),p&&(0,J.jsx)($,{tone:`err`,children:p}),z.showError&&V&&(0,J.jsx)($,{tone:`err`,children:n(`api.keysLoadFailed`)}),z.showSkeleton&&!V?(0,J.jsx)(gl,{label:n(`api.activeKeysLoading`),rows:4}):z.kind===`failed-cold`&&!V?(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)($,{tone:`err`,children:z.error instanceof Error?z.error.message:n(`api.keysLoadFailed`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:()=>Y(),children:n(`common.retry`)})]}):(0,J.jsx)(J.Fragment,{children:(0,J.jsx)(w_,{keys:U,apiBase:e,attributionSince:G,historyTruncated:K,authMatrix:q,keysLoading:!1,keysLoadFailed:z.showError,endpoints:W,claudeCodeEnabled:ee,localeTag:i,newName:S,creating:w,newKey:E,copied:O,rotationSecret:A,rotationCopied:M,filteredModels:ne,modelsLoading:B.showSkeleton&&!B.data&&!f,modelsRefreshing:B.refreshing&&B.showError&&(B.data!==void 0||f!==null),modelsLoadFailed:B.showError,modelCount:H.length,hasModelData:B.data!==void 0||f!==null,modelQuery:h,copiedModelId:v,modelTests:b,onNewNameChange:C,onCreate:()=>{re()},onDismissNewKey:()=>D(null),onCopyKey:()=>{le()},onDelete:ie,onRename:ae,...qg()?{onRotationStart:oe,onRotationCommit:(e,t)=>se(e,t,`commit`),onRotationAbort:(e,t)=>se(e,t,`abort`),onCopyRotationSecret:()=>{ce()},onDismissRotationSecret:()=>j(null)}:{},onModelQueryChange:g,onCopyModelId:e=>{ue(e)},onTestModel:(e,t)=>{me(e,t)},onRetryModels:()=>{te({forceLoading:!0})},canTestModels:E!==null,sourceLabel:de,protocolLabel:fe})})]})}function j_(e,t){let n=(e??[]).map(e=>({value:e,label:fl(e)}));return[{value:``,label:t},...n]}function M_(e){let t=e.autoConnectSupported===!0;return{autoConnectSupported:t,systemEnv:t&&e.systemEnv===!0}}function N_(){if(typeof crypto<`u`&&typeof crypto.randomUUID==`function`)try{return crypto.randomUUID()}catch{}let e=new Uint8Array(16);if(typeof crypto<`u`&&typeof crypto.getRandomValues==`function`)crypto.getRandomValues(e);else for(let t=0;t<16;t++)e[t]=Math.floor(Math.random()*256);e[6]=e[6]&15|64,e[8]=e[8]&63|128;let t=Array.from(e,e=>e.toString(16).padStart(2,`0`)).join(``);return`${t.slice(0,8)}-${t.slice(8,12)}-${t.slice(12,16)}-${t.slice(16,20)}-${t.slice(20)}`}var P_=829800;function F_(e,t=`en`){if(e>=1e6){let n=e/1e6,r=n.toFixed(1).replace(/\.0$/,``);return Number.isInteger(n)||Number(r)*1e6===e?new Intl.NumberFormat(t,{notation:`compact`,compactDisplay:`short`,maximumFractionDigits:+!Number.isInteger(n)}).format(e):`${Math.round(e/1e3)}k`}return`${Math.round(e/1e3)}k`}var I_=[`ANTHROPIC_MODEL`,`ANTHROPIC_DEFAULT_OPUS_MODEL`,`ANTHROPIC_DEFAULT_SONNET_MODEL`,`ANTHROPIC_DEFAULT_HAIKU_MODEL`,`ANTHROPIC_DEFAULT_FABLE_MODEL`];function L_(e){let t=`http://127.0.0.1:${e.port}`,n=e.authMode===`auto`?e.markerMode??`subscription`:e.authMode,r=e.autoContext&&e.maxContextTokens===null,i=I_.filter(t=>e.effectiveModelEnv[t]).map(t=>`export ${t}=${e.effectiveModelEnv[t]}`);return[`export ANTHROPIC_BASE_URL=${t}`,...n===`proxy`?[`export ANTHROPIC_AUTH_TOKEN=opencodex-proxy`]:[`# no ANTHROPIC_AUTH_TOKEN: your claude.ai login (and connectors) stay active`],`export CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY=1`,...n===`proxy`?['[ -z "${CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST+x}" ] && export CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST=1']:[],...r?[`export CLAUDE_CODE_AUTO_COMPACT_WINDOW=${e.autoCompactWindow??829800}`]:[],...i,`claude`].join(` +`)}function R_(e){return e?e.backend??`auto`:`inherit`}function z_(e,t){if(t!==`inherit`)return t===`auto`?{...e,backend:void 0}:{...e,backend:t}}function B_(e,t){return{...e,model:t}}function V_(e){if(!e)return null;let t=(e.model??``).trim();return e.backend?{backend:e.backend,model:t}:t?{backend:null,model:t}:null}function H_({label:e,checked:t,onChange:n,disabled:r=!1,describedBy:i}){return(0,J.jsxs)(`label`,{className:`toggle`,children:[(0,J.jsx)(`input`,{type:`checkbox`,checked:t,disabled:r,"aria-label":e,"aria-describedby":i,onChange:e=>n(e.target.checked)}),(0,J.jsx)(`span`,{className:`slider`,"aria-hidden":`true`})]})}function U_({supported:e,checked:t,onChange:n}){let r=Q(),i=e?void 0:`claude-system-env-unsupported`;return(0,J.jsxs)(`div`,{className:`setting-row`,children:[(0,J.jsxs)(`div`,{className:`setting-label`,children:[(0,J.jsx)(`span`,{className:`title`,children:r(`claude.systemEnv`)}),e?(0,J.jsx)(`span`,{className:`desc`,children:r(`claude.systemEnvDesc`)}):(0,J.jsx)(`span`,{className:`desc`,id:i,children:(0,J.jsx)(ut,{k:`claude.systemEnvUnsupported`,cmd:`ocx claude`})}),e&&t&&(0,J.jsx)(`span`,{className:`desc`,style:{color:`var(--red)`},children:r(`claude.systemEnvWarn`)})]}),(0,J.jsx)(H_,{label:r(`claude.systemEnv`),checked:e&&t,disabled:!e,describedBy:i,onChange:n})]})}function W_({value:e,tierHaikuModel:t,options:n,onChange:r}){let i=Q(),a=t??e;return(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)(`p`,{className:`muted text-label`,style:{margin:`0 0 8px`},children:i(`claude.smallFastModelAccurateHint`)}),(0,J.jsx)(Dt,{value:e,options:n,onChange:r,label:i(`claude.smallFastModel`),style:{maxWidth:420}}),a===``&&(0,J.jsx)(`p`,{className:`notice-warn`,role:`status`,style:{marginTop:8},children:i(`claude.smallFastModelNativeWarning`)})]})}function G_(e,t){return e&&[`claude-json-oauth`,`claude-credentials-file`,`macos-keychain`,`exported-env`].includes(e)?t(`claude.authSource.${e}`):t(`claude.authSource.unknown`)}function K_({state:e,autoCompactOptions:t,availableModels:n,onStateChange:r}){let i=Q();return(0,J.jsxs)(`div`,{className:`card`,style:{overflow:`hidden`},children:[(0,J.jsxs)(`div`,{className:`setting-row`,children:[(0,J.jsxs)(`div`,{className:`setting-label`,children:[(0,J.jsx)(`span`,{className:`title`,children:i(`claude.authMode`)}),(0,J.jsx)(`span`,{className:`desc`,children:i(`claude.authModeHint`)})]}),(0,J.jsx)(`div`,{className:`setting-controls`,children:(0,J.jsx)(Dt,{value:e.authMode,options:[{value:`auto`,label:i(`claude.authModeAuto`)},{value:`subscription`,label:i(`claude.authModeSubscription`)},{value:`proxy`,label:i(`claude.authModeProxy`)}],onChange:t=>r({...e,authMode:t}),label:i(`claude.authMode`),style:{minWidth:220},align:`right`,portal:!0})})]}),e.authModeOrigin&&(0,J.jsxs)(`div`,{className:`claude-effective-auth${e.authModeOrigin===`auto-unknown`?` warn`:``}`,role:`status`,children:[(0,J.jsx)(`span`,{className:`claude-effective-auth-label`,children:i(`claude.effectiveMode.label`)}),(0,J.jsxs)(`span`,{children:[e.authModeOrigin===`manual`?i(`claude.effectiveMode.manual`,{mode:e.markerMode===`proxy`?i(`claude.authModeProxy`):i(`claude.authModeSubscription`)}):e.authModeOrigin===`auto-present`?i(`claude.effectiveMode.autoPresent`,{source:G_(e.authFoundBy,i)}):e.authModeOrigin===`auto-absent`?i(`claude.effectiveMode.autoAbsent`):i(`claude.effectiveMode.autoUnknown`),e.admissionKeyActive===!0?` ${i(`claude.effectiveMode.admissionKey`)}`:``]})]}),(0,J.jsx)(U_,{supported:e.autoConnectSupported,checked:e.systemEnv,onChange:t=>r({...e,systemEnv:t})}),(0,J.jsxs)(`div`,{className:`setting-row`,children:[(0,J.jsxs)(`div`,{className:`setting-label`,children:[(0,J.jsx)(`span`,{className:`title`,children:i(`claude.fastMode`)}),(0,J.jsx)(`span`,{className:`desc`,children:i(`claude.fastModeDesc`)})]}),(0,J.jsx)(`div`,{className:`setting-controls`,children:(0,J.jsx)(Dt,{value:e.fastMode===null?`auto`:e.fastMode?`on`:`off`,options:[{value:`auto`,label:i(`claude.fastAuto`)},{value:`on`,label:i(`claude.fastOn`)},{value:`off`,label:i(`claude.fastOff`)}],onChange:t=>r({...e,fastMode:t===`auto`?null:t===`on`}),label:i(`claude.fastMode`),style:{minWidth:140},align:`right`,portal:!0})})]}),(0,J.jsxs)(`div`,{className:`setting-row`,children:[(0,J.jsxs)(`div`,{className:`setting-label`,children:[(0,J.jsx)(`span`,{className:`title`,children:i(`claude.autoContext`)}),(0,J.jsx)(`span`,{className:`desc`,children:i(`claude.autoContextDesc`)}),e.maxContextTokens!==null&&(0,J.jsx)(`span`,{className:`desc`,style:{color:`var(--muted)`},children:i(`claude.autoContextInert`)})]}),(0,J.jsx)(H_,{label:i(`claude.autoContext`),checked:e.autoContext,onChange:t=>r({...e,autoContext:t})})]}),e.autoContext&&(0,J.jsxs)(`div`,{className:`setting-row`,children:[(0,J.jsxs)(`div`,{className:`setting-label`,children:[(0,J.jsx)(`span`,{className:`title`,children:i(`claude.autoCompactWindow`)}),(0,J.jsx)(`span`,{className:`desc`,children:i(`claude.autoCompactWindowDesc`)}),e.autoCompactWindow!==null&&(0,J.jsx)(`span`,{className:`desc`,style:{color:`var(--red)`},children:i(`claude.autoCompactWindowWarn`)})]}),(0,J.jsx)(`div`,{className:`setting-controls`,children:(0,J.jsx)(Dt,{value:e.autoCompactWindow===null?``:String(e.autoCompactWindow),options:t,onChange:t=>r({...e,autoCompactWindow:t===``?null:Number(t)}),label:i(`claude.autoCompactWindow`),style:{minWidth:130},align:`right`,portal:!0})})]}),(0,J.jsxs)(`div`,{className:`setting-row`,children:[(0,J.jsxs)(`div`,{className:`setting-label`,children:[(0,J.jsx)(`span`,{className:`title`,children:i(`claude.injectAgents`)}),(0,J.jsx)(`span`,{className:`desc`,children:i(`claude.injectAgentsDesc`)})]}),(0,J.jsx)(H_,{label:i(`claude.injectAgents`),checked:e.injectAgents,onChange:t=>r({...e,injectAgents:t})})]}),[`webSearchSidecar`,`visionSidecar`].map(t=>{let a=e[t],o=t===`webSearchSidecar`?`claude.webSearchSidecar`:`claude.visionSidecar`,s=t===`webSearchSidecar`?`claude.webSearchSidecarHint`:`claude.visionSidecarHint`,c=`claude-sidecar-models-${t}`;return(0,J.jsxs)(`div`,{className:`setting-row`,style:{alignItems:`flex-start`},children:[(0,J.jsxs)(`div`,{className:`setting-label setting-copy`,style:{flex:1},children:[(0,J.jsx)(`span`,{className:`title`,children:i(o)}),(0,J.jsx)(`span`,{className:`desc`,children:i(s)})]}),(0,J.jsxs)(`div`,{className:`setting-controls`,style:{display:`flex`,gap:8},children:[(0,J.jsx)(Dt,{value:R_(a),options:[{value:`inherit`,label:i(`claude.useMainSetting`)},{value:`auto`,label:i(`dash.backendAuto`)},{value:`openai`,label:i(`dash.backendOpenAI`)},{value:`anthropic`,label:i(`dash.backendAnthropic`)}],onChange:n=>{r({...e,[t]:z_(a,n)})},label:i(`dash.sidecarBackend`),portal:!0}),(0,J.jsx)(`input`,{className:`input mono`,value:a?.model??``,onChange:n=>{r({...e,[t]:B_(a,n.target.value)})},placeholder:i(`claude.sidecarModelPlaceholder`),disabled:!a,list:a?c:void 0,"aria-label":i(`dash.sidecarModel`),style:{minWidth:210},autoComplete:`off`}),a&&(0,J.jsx)(`datalist`,{id:c,children:n.map(e=>(0,J.jsx)(`option`,{value:e},e))})]})]},t)})]})}function q_({manualEnv:e}){let t=Q();return(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)(`p`,{className:`muted text-label`,style:{margin:`0 0 8px`},children:(0,J.jsx)(ut,{k:`claude.quickstartHint`,cmd:`ocx claude`})}),(0,J.jsx)(`pre`,{className:`mono card`,style:{padding:`10px 14px`,overflowX:`auto`,margin:0},children:`ocx claude`}),(0,J.jsxs)(`details`,{style:{margin:`10px 0 0`},children:[(0,J.jsx)(`summary`,{className:`muted text-label`,style:{cursor:`pointer`,padding:`2px 2px`},children:t(`claude.manualEnv`)}),(0,J.jsx)(`pre`,{className:`mono card text-label`,style:{padding:`10px 14px`,overflowX:`auto`,margin:`6px 0 0`},children:e})]})]})}function J_({rows:e,onRowsChange:t}){let n=Q();return(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)(`p`,{className:`muted text-label`,style:{margin:`0 0 8px`},children:n(`claude.modelMapHint`)}),(0,J.jsx)(`div`,{className:`stack`,style:{gap:8},children:e.map((r,i)=>(0,J.jsxs)(`div`,{className:`row`,style:{gap:8},children:[(0,J.jsx)(`input`,{className:`input mono`,value:r.from,placeholder:n(`claude.mapFrom`),"aria-label":n(`claude.mapFrom`),onChange:n=>t(e.map((e,t)=>t===i?{...e,from:n.target.value}:e)),style:{flex:1}}),(0,J.jsx)(`span`,{className:`muted`,"aria-hidden":!0,children:`→`}),(0,J.jsx)(`input`,{className:`input mono`,value:r.to,placeholder:n(`claude.mapTo`),"aria-label":n(`claude.mapTo`),onChange:n=>t(e.map((e,t)=>t===i?{...e,to:n.target.value}:e)),style:{flex:1}}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-icon btn-sm`,onClick:()=>t(e.filter((e,t)=>t!==i)),"aria-label":n(`claude.removeMapping`),style:{color:`var(--red)`},children:(0,J.jsx)(de,{})})]},r.id))}),(0,J.jsx)(`div`,{style:{marginTop:8},children:(0,J.jsxs)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:()=>t([...e,{id:N_(),from:``,to:``}]),children:[(0,J.jsx)(fe,{}),` `,n(`claude.addMapping`)]})})]})}var Y_=`etc`;function X_(e){let t=new Map;for(let n of e){let e=/\(([^)]+)\)\s*$/.exec(n.display_name),r=e?e[1]:Y_,i=t.get(r);i?i.push(n):t.set(r,[n])}return Array.from(t)}function Z_({aliases:e}){let t=Q();return(0,J.jsxs)(`div`,{className:`claude-aliases`,children:[(0,J.jsx)(`p`,{className:`muted text-label claude-aliases-hint`,children:t(`claude.aliasesHint`)}),e.length===0?(0,J.jsx)(`div`,{className:`muted text-label`,children:t(`claude.none`)}):(0,J.jsx)(`div`,{className:`claude-aliases-scroll`,children:X_(e).map(([e,n])=>(0,J.jsxs)(`div`,{className:`claude-aliases-group`,children:[(0,J.jsxs)(`div`,{className:`claude-aliases-group-label`,children:[e===Y_?t(`claude.aliasProviderOther`):e,(0,J.jsx)(`span`,{className:`claude-aliases-group-count`,children:n.length})]}),(0,J.jsx)(`div`,{className:`claude-aliases-chips`,children:n.map(e=>(0,J.jsxs)(`span`,{className:`claude-aliases-chip`,children:[(0,J.jsx)(`code`,{className:`claude-aliases-chip-id`,children:e.id}),e.display_name?(0,J.jsx)(`span`,{className:`claude-aliases-chip-name`,children:e.display_name}):null]},e.id))})]},e))})]})}function Q_({apiBase:e,active:t=!0}){let n=Q(),{locale:r}=ct(),i=et.find(e=>e.code===r)?.htmlLang??`en`,a=`ocx.claude-code.v1:${e}`,o=`claude-code:${e}`,s=(0,_.useMemo)(()=>vr(a),[a]),c=s?.data??null,[l,u]=(0,_.useState)(()=>c?.state??null),[d,f]=(0,_.useState)(()=>c?.rows??[]),[p,m]=(0,_.useState)(!!c),[h,g]=(0,_.useState)(``),[v,y]=(0,_.useState)(!1),[b,x]=(0,_.useState)(`settings`),[S,C]=(0,_.useState)(!1),w=(0,_.useRef)(!1),T=(0,_.useCallback)(async t=>{let r=await Pt(await fetch(`${e}/api/claude-code`,{signal:t}),n(`claude.loadFail`));if(!r)throw Error(n(`claude.loadFail`));let i={...r,authMode:r.authMode===`proxy`||r.authMode===`subscription`?r.authMode:`auto`,...M_(r),fastMode:r.fastMode??null,maxContextTokens:r.maxContextTokens??null,autoContext:r.autoContext!==!1,autoCompactWindow:r.autoCompactWindow??null,injectAgents:r.injectAgents!==!1,effectiveModelEnv:r.effectiveModelEnv??{}},o=Object.entries(r.modelMap??{}).map(([e,t])=>({id:N_(),from:e,to:String(t)})),s={state:i,rows:o};if(t.aborted)throw Error(`Claude Code request aborted`);return u(i),f(o),m(!0),yr(a,s),s},[e,a,n]),E=ml(o,[e],T,{isEmpty:()=>!1,enabled:t,initialData:c??void 0,initialDataCachedAt:s?.cachedAt??null,staleAfterMs:6e4}),D=E.state,O=D.data??c,k=l??O?.state??null,A=p?d:O?.rows??d,j=(0,_.useMemo)(()=>j_(k?.available,n(`claude.smallFastModelUnsetOption`)),[k?.available,n]),M=(0,_.useMemo)(()=>{let e=[1e5,2e5,25e4,3e5,35e4,4e5,5e5,6e5,75e4,P_,9e5,1e6].sort((e,t)=>e-t),t=k?.autoCompactWindow??null,r=t!==null&&!e.includes(t)?[...e,t].sort((e,t)=>e-t):e;return[{value:``,label:n(`claude.autoCompactDefault`,{value:F_(P_,i)})},...r.map(e=>({value:String(e),label:F_(e,i)}))]},[k?.autoCompactWindow,n,i]),N=async()=>{if(!k||w.current)return;w.current=!0,C(!0),g(``);let t=!k.enabled;try{await Pt(await fetch(`${e}/api/claude-code`,{method:`PUT`,headers:{"Content-Type":`application/json`},body:JSON.stringify({enabled:t})}),n(`claude.saveFailed`)),u({...k,enabled:t}),E.refresh()}catch(e){y(!1),g(e instanceof Error&&e.message?e.message:n(`claude.networkError`))}finally{w.current=!1,C(!1)}},P=async()=>{if(!k)return;g(``);let t={};for(let e of A)e.from.trim()&&e.to.trim()&&(t[e.from.trim()]=e.to.trim());try{await Pt(await fetch(`${e}/api/claude-code`,{method:`PUT`,headers:{"Content-Type":`application/json`},body:JSON.stringify({enabled:k.enabled,authMode:k.authMode,systemEnv:k.systemEnv,fastMode:k.fastMode,autoContext:k.autoContext,autoCompactWindow:k.autoCompactWindow,injectAgents:k.injectAgents,smallFastModel:k.smallFastModel,modelMap:t,webSearchSidecar:V_(k.webSearchSidecar),visionSidecar:V_(k.visionSidecar)})}),n(`claude.saveFailed`)),y(!0),g(n(`claude.saved`)),E.refresh()}catch(e){y(!1),g(e instanceof Error&&e.message?e.message:n(`claude.networkError`))}};if(D.kind===`disabled`&&!O)return null;if(D.showSkeleton&&!O)return(0,J.jsx)(gl,{label:n(`claude.loading`),rows:3});if(D.kind===`failed-cold`){let e=D.error instanceof Error?D.error.message:n(`claude.loadFail`);return(0,J.jsxs)(`div`,{className:`claudecode-workspace-shell`,children:[(0,J.jsx)($,{tone:`err`,children:e}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:()=>E.refresh(),children:n(`common.retry`)})]})}if(!k)return null;let F=[{id:`settings`,label:n(`claude.workspace.settings`),body:(0,J.jsx)(K_,{state:k,autoCompactOptions:M,availableModels:k.available??[],onStateChange:u})},{id:`quickstart`,label:n(`claude.quickstart`),body:(0,J.jsx)(q_,{manualEnv:L_(k)})},{id:`smallFast`,label:n(`claude.smallFastModel`),body:(0,J.jsx)(W_,{value:k.smallFastModel,tierHaikuModel:k.tierModels?.haiku,options:j,onChange:e=>u({...k,smallFastModel:e})})},{id:`modelMap`,label:n(`claude.modelMap`),meta:String(A.length),body:(0,J.jsx)(J_,{rows:A,onRowsChange:e=>{m(!0),f(e)}})},{id:`aliases`,label:n(`claude.aliases`),meta:String(k.aliases.length),body:(0,J.jsx)(Z_,{aliases:k.aliases})}],I=F.find(e=>e.id===b)??F[0],L=b===`settings`||b===`smallFast`||b===`modelMap`;return(0,J.jsxs)(`div`,{className:`claudecode-workspace-shell`,children:[h&&(0,J.jsx)($,{tone:v?`ok`:`err`,children:h}),D.showError&&(0,J.jsx)($,{tone:`err`,children:n(`claude.loadFail`)}),k&&(0,J.jsxs)(`div`,{className:`claudecode-connection-head`,children:[(0,J.jsx)(`span`,{id:`claudecode-connection-label`,children:n(`claude.enabledLabel`)}),(0,J.jsx)(Tt,{on:k.enabled,onClick:()=>void N(),disabled:S,label:n(`claude.toggleAria`)})]}),(0,J.jsxs)(`div`,{className:`claudecode-workspace-root`,children:[(0,J.jsx)(`aside`,{className:`claudecode-workspace-rail`,"aria-label":n(`claude.pageTitle`),children:(0,J.jsx)(`div`,{className:`claudecode-workspace-rail-list`,children:F.map(e=>(0,J.jsx)(`button`,{type:`button`,className:`claudecode-workspace-rail-row${b===e.id?` claudecode-workspace-rail-row--selected`:``}`,onClick:()=>x(e.id),"aria-current":b===e.id?`true`:void 0,children:(0,J.jsx)(`span`,{className:`claudecode-workspace-rail-name`,children:e.label})},e.id))})}),(0,J.jsxs)(`section`,{className:`claudecode-workspace-main`,"aria-label":I.label,children:[(0,J.jsxs)(`div`,{className:`ccw-main-head`,children:[(0,J.jsxs)(`h3`,{className:`ccw-main-title`,children:[I.label,I.meta==null?null:(0,J.jsx)(`span`,{className:`count`,children:I.meta})]}),(0,J.jsx)(`div`,{className:`claudecode-workspace-save`,"data-visible":L?`true`:`false`,children:(0,J.jsx)(`button`,{type:`button`,className:`btn btn-primary btn-sm`,disabled:!L,tabIndex:L?0:-1,"aria-hidden":!L,onClick:()=>{P()},children:n(`common.save`)})})]}),(0,J.jsx)(`div`,{className:`ccw-body`,children:I.body})]})]})]})}function $_(e,t,n){let r=t.trim().toLowerCase(),i=(r?e.filter(e=>e.label.toLowerCase().includes(r)||e.route.toLowerCase().includes(r)):e).toSorted((e,t)=>Number(!e.available)-Number(!t.available)),a=i.slice(0,n);return{total:e.length,showSearch:e.length>4,shown:a,hidden:i.length-a.length,noMatch:e.length>0&&i.length===0}}function ev(e){return new Set(Object.keys(e))}function tv(e,t){return t!==null&&e===t}function nv(e){return e||(typeof localStorage>`u`?void 0:localStorage)}function rv(e){return{read(t){let n=nv(t);if(!n)return null;try{let t=n.getItem(e);if(t===null)return null;let r=JSON.parse(t);return Array.isArray(r)?new Set(r.filter(e=>typeof e==`string`)):null}catch{return null}},write(t,n){let r=nv(n);if(r)try{r.setItem(e,JSON.stringify([...t]))}catch{}}}}function iv(e,t){let n=new Set(e);return n.has(t)?n.delete(t):n.add(t),n}var av=[`opus`,`fable`,`sonnet`,`haiku`],ov=rv(`ocx.claudeDesktop.collapsedFamilies.v2`),sv={opus:`claudeDesktop.family.opus`,fable:`claudeDesktop.family.fable`,sonnet:`claudeDesktop.family.sonnet`,haiku:`claudeDesktop.family.haiku`};function cv(e){return{version:1,assignments:Object.fromEntries(Object.entries(e.assignments).map(([e,t])=>[e,{...t}])),defaults:{...e.defaults},...e.appliedFingerprint===void 0?{}:{appliedFingerprint:e.appliedFingerprint},...e.appliedAt===void 0?{}:{appliedAt:e.appliedAt}}}function lv(e){let t={...e.profile.assignments};for(let n of e.models){let e=t[n.route]??n.assignment;t[n.route]={family:av.includes(e?.family)?e.family:`opus`,alias:typeof e?.alias==`string`?e.alias:``}}return{version:1,assignments:t,defaults:{opus:e.profile.defaults.opus??null,fable:e.profile.defaults.fable??null,sonnet:e.profile.defaults.sonnet??null,haiku:e.profile.defaults.haiku??null}}}function uv(e,t){return e&&typeof e==`object`&&`error`in e&&typeof e.error==`string`?e.error:t}function dv(e,t){return e?e>=1048576?t(`claudeDesktop.contextM`,{n:Math.round(e/1048576)}):e>=1e6?t(`claudeDesktop.contextM`,{n:e/1e6}):t(`claudeDesktop.contextK`,{n:Math.round(e/1e3)}):null}function fv(e){return vr(e)?.data??null}function pv(e){return vr(e)?.cachedAt??null}function mv(e){let t=fv(e);return{held:t,data:t?.data??null,profile:t?.profile??null,savedProfile:t?.profile?cv(t.profile):null,destinations:t?.data?Object.fromEntries(t.data.models.map(e=>[e.route,t.profile.assignments[e.route]?.family??`opus`])):{}}}function hv({apiBase:e,active:t=!0,onPortChange:n}){let{t:r,locale:i}=ct(),a=et.find(e=>e.code===i)?.htmlLang,o=`ocx.claude-desktop.v1:${e}`,s=`claude-desktop:${e}`,c=(0,_.useMemo)(()=>mv(o),[o]),[l,u]=(0,_.useState)(()=>c.profile),[d,f]=(0,_.useState)(()=>c.savedProfile),[p,m]=(0,_.useState)(()=>c.destinations),[h,g]=(0,_.useState)(null),[v,y]=(0,_.useState)(``),[b,x]=(0,_.useState)(null),[S,C]=(0,_.useState)({}),[w,T]=(0,_.useState)({}),[E,D]=(0,_.useState)(()=>ov.read()??new Set(av)),[O,k]=(0,_.useState)({}),A=(0,_.useRef)(null),j=(0,_.useCallback)(async t=>{let n=await Pt(await fetch(`${e}/api/claude-desktop`,{signal:t}),r(`claudeDesktop.loadFail`));if(!n||!(`profile`in n)||!(`models`in n))throw Error(uv(n,r(`claudeDesktop.loadFail`)));let i=lv(n),a={data:n,profile:i};if(t.aborted)throw Error(`Claude Desktop request aborted`);if(u(i),f(cv(i)),m(Object.fromEntries(n.models.map(e=>[e.route,i.assignments[e.route]?.family??`opus`]))),ov.read()===null){let e=Object.fromEntries(av.map(e=>[e,0]));for(let t of n.models)e[i.assignments[t.route]?.family??`opus`]+=1;D(ev(e))}return yr(o,a),a},[e,o,r,m,u,f]),M=ml(s,[e],j,{isEmpty:()=>!1,enabled:t,initialData:c.held??void 0,initialDataCachedAt:pv(o),staleAfterMs:6e4}),N=M.state,P=N.data??(c.data&&c.profile?{data:c.data,profile:c.profile}:null),F=P?.data??null,I=l??P?.profile??null,L=d??P?.profile??null,R=P?Object.fromEntries(P.data.models.map(e=>[e.route,P.profile.assignments[e.route]?.family??`opus`])):{},z=Object.keys(p).length>0?p:R;(0,_.useEffect)(()=>{if(n){if(typeof F?.port==`number`){n(F.port);return}N.kind===`failed-cold`&&n(null)}},[F?.port,N.kind,n]);let B=(0,_.useMemo)(()=>I!==null&&L!==null&&JSON.stringify(I)!==JSON.stringify(L),[I,L]),V=(0,_.useMemo)(()=>{let e=Object.fromEntries(av.map(e=>[e,[]]));if(!F||!I)return e;for(let t of F.models)e[I.assignments[t.route]?.family??`opus`].push(t);return e},[F,I]),H=(0,_.useMemo)(()=>{let e={};for(let t of av){let n=V[t].filter(e=>e.available).map(e=>e.route).sort(),r=I?.defaults[t]??null;e[t]=r&&n.includes(r)?r:n[0]??null}return e},[V,I]),U=`ocx.claude-desktop.status.v1:${e}`,W=`claude-desktop-status:${e}`,ee=vr(U),G=ml(W,[e],async t=>{let n=await Ft(await fetch(`${e}/api/claude-desktop/status`,{signal:t}));if(!n)throw Error(`Claude Desktop status unavailable`);return yr(U,n),n},{isEmpty:()=>!1,pollMs:5e3,enabled:t,initialData:ee?.data??void 0}),K=G.state,q=K.data??ee?.data??null,Y=K.showError,te=(e,t)=>{!I||I.assignments[e]?.family===t||(u(n=>{if(!n)return n;let r=n.assignments[e];if(!r||r.family===t)return n;let i={...n.assignments,[e]:{...r,family:t}},a={...n.defaults};return a[r.family]===e&&(a[r.family]=Object.keys(i).filter(t=>t!==e&&i[t].family===r.family).sort()[0]??null),a[t]===null&&(a[t]=e),{...n,assignments:i,defaults:a}}),m(n=>({...n,[e]:t})),y(r(`claudeDesktop.moved`,{route:e,family:r(sv[t])})))},ne=e=>{let t=iv(E,e);ov.write(t),D(t)},re=async t=>{if(!(!I||b)){x(`save`),g(null);try{await Pt(await fetch(`${e}/api/claude-desktop`,{method:`PUT`,headers:{"Content-Type":`application/json`},body:JSON.stringify({profile:I})}),r(`claudeDesktop.saveFailed`)),f(cv(I)),t?(x(`apply`),(await Pt(await fetch(`${e}/api/claude-desktop/apply`,{method:`POST`}),r(`claudeDesktop.applyFailed`)))?.saved===!1?(g({tone:`warn`,text:r(`claudeDesktop.appliedMarkerUnsaved`)}),y(r(`claudeDesktop.appliedMarkerUnsaved`))):(g({tone:`ok`,text:r(`claudeDesktop.savedApplied`)}),y(r(`claudeDesktop.savedAppliedAnnounce`)))):(g({tone:`ok`,text:r(`claudeDesktop.saved`)}),y(r(`claudeDesktop.savedAnnounce`))),G.refresh()}catch(e){let t=e instanceof Error?e.message:r(`claudeDesktop.updateFailed`);g({tone:`err`,text:t}),y(t)}finally{x(null)}}},ie=()=>{if(!I)return;let e=URL.createObjectURL(new Blob([`${JSON.stringify(I,null,2)}\n`],{type:`application/json`})),t=document.createElement(`a`);t.href=e,t.download=`claude-desktop-profile.json`,t.click(),URL.revokeObjectURL(e),y(r(`claudeDesktop.exported`))},ae=async e=>{let t=e.target.files?.[0];if(e.target.value=``,t)try{let e=JSON.parse(await t.text());if(e.version!==1||!e.assignments||!e.defaults)throw Error(r(`claudeDesktop.importExpected`));let n=lv({...F,profile:e});u(n),g({tone:`ok`,text:r(`claudeDesktop.importReady`)}),y(r(`claudeDesktop.importedAnnounce`))}catch(e){let t=e instanceof Error?e.message:r(`claudeDesktop.importInvalid`);g({tone:`err`,text:t}),y(r(`claudeDesktop.importFailed`,{error:t}))}},oe=(e,t)=>{e.preventDefault();let n=e.dataTransfer.getData(`text/plain`);n&&te(n,t)};if(N.kind===`disabled`&&!P)return null;if(N.showSkeleton&&!P)return(0,J.jsx)(gl,{label:r(`claudeDesktop.loading`),rows:4});if(N.kind===`failed-cold`){let e=N.error instanceof Error?N.error.message:r(`claudeDesktop.loadFail`);return(0,J.jsxs)(`div`,{className:`claude-desktop-error`,children:[(0,J.jsx)($,{tone:`err`,children:e}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost`,onClick:()=>M.refresh(),children:r(`claudeDesktop.retry`)})]})}return!F||!I?null:(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)(`div`,{className:`claude-desktop-toolbar`,children:(0,J.jsxs)(`div`,{className:`claude-profile-tools`,children:[(0,J.jsx)(`input`,{ref:A,type:`file`,accept:`application/json,.json`,hidden:!0,onChange:e=>void ae(e)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:()=>A.current?.click(),children:r(`claudeDesktop.importJson`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:ie,children:r(`claudeDesktop.exportJson`)})]})}),(0,J.jsxs)(`div`,{className:`claude-status-bar ${Y&&!q?`not-applied`:q?q.desiredEnabled?q.activeProfile===!1?`not-applied`:q.stale?`stale`:q.applied?`applied`:`not-applied`:`not-applied`:`pending`}`,"aria-busy":!q&&!Y||void 0,children:[(0,J.jsx)(`span`,{className:`claude-status-dot`}),(0,J.jsx)(`span`,{children:Y&&!q?r(`claudeDesktop.loadFail`):q?q.desiredEnabled?q.activeProfile===!1?r(`claudeDesktop.status.notActiveProfile`):q.stale?r(`claudeDesktop.status.stale`):q.applied?r(`claudeDesktop.status.applied`):r(`claudeDesktop.status.notApplied`):r(`claudeDesktop.status.disabled`):r(`claudeDesktop.loading`)}),q?.health.lastRequestAt&&(0,J.jsxs)(`span`,{className:`claude-status-health`,children:[r(`claudeDesktop.health.lastRequest`),`:`,` `,new Date(q.health.lastRequestAt).toLocaleTimeString(a)]}),q&&q.health.requestCount>0&&(0,J.jsx)(`span`,{className:`claude-status-health`,children:r(`claudeDesktop.health.stats`,{count:q.health.requestCount,errors:q.health.errorCount})})]}),(0,J.jsx)(`div`,{className:`sr-only`,"aria-live":`polite`,"aria-atomic":`true`,children:v}),h&&(0,J.jsx)($,{tone:h.tone,children:h.text}),N.showError&&(0,J.jsx)($,{tone:`err`,children:r(`claudeDesktop.loadFail`)}),Y&&q&&(0,J.jsx)($,{tone:`err`,children:r(`claudeDesktop.loadFail`)}),(0,J.jsxs)(`div`,{className:`claude-profile-bar`,children:[(0,J.jsx)(`span`,{className:`claude-dirty${B?` active`:``}`,children:r(B?`claudeDesktop.unsaved`:`claudeDesktop.upToDate`)}),(0,J.jsxs)(`div`,{className:`claude-save-actions`,children:[(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost`,disabled:!B||b!==null,onClick:()=>void re(!1),children:r(b===`save`?`claudeDesktop.saving`:`common.save`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-primary`,disabled:b!==null,onClick:()=>void re(!0),children:b===`apply`?r(`claudeDesktop.applying`):b===`save`?r(`claudeDesktop.saving`):q?.desiredEnabled===!1?r(`claudeDesktop.enableApply`):r(`claudeDesktop.saveApply`)})]})]}),F.models.length===0&&(0,J.jsx)(Ot,{title:r(`claudeDesktop.emptyTitle`),children:r(`claudeDesktop.emptyHint`)}),(0,J.jsx)(`div`,{className:`ocx-group-stack`,"aria-label":r(`claudeDesktop.assignmentsLabel`),children:av.map(e=>{let t=V[e],n=$_(t,S[e]??``,w[e]??6),i=E.has(e),a=H[e];return(0,J.jsxs)(`section`,{className:`ocx-group${i?` collapsed`:``}`,"aria-labelledby":`claude-lane-${e}`,onDragOver:e=>e.preventDefault(),onDrop:t=>oe(t,e),children:[(0,J.jsxs)(`header`,{className:`ocx-group-head${i?``:` open`}`,children:[(0,J.jsx)(`h3`,{id:`claude-lane-${e}`,className:`ocx-group-heading`,children:(0,J.jsxs)(`button`,{type:`button`,className:`ocx-group-toggle`,"aria-expanded":!i,"aria-controls":`claude-lane-body-${e}`,onClick:()=>ne(e),children:[(0,J.jsx)(Se,{className:`ocx-chevron`,width:14,height:14,"aria-hidden":`true`,style:{transform:i?`none`:`rotate(90deg)`}}),(0,J.jsx)(`span`,{className:`ocx-group-name`,children:r(sv[e])}),(0,J.jsx)(`span`,{className:`ocx-group-count`,children:r(t.length===1?`claudeDesktop.modelCountOne`:`claudeDesktop.modelCountMany`,{count:t.length})}),a&&(0,J.jsx)(`code`,{className:`claude-lane-default`,title:a,children:a})]})}),t.length>0&&I.defaults[e]===null&&(0,J.jsx)(`span`,{className:`claude-default-needed`,children:r(`claudeDesktop.chooseDefault`)}),a&&a!==I.defaults[e]&&(0,J.jsx)(`span`,{className:`claude-default-needed`,title:a,children:r(`claudeDesktop.temporaryDefault`)})]}),!i&&(0,J.jsxs)(`div`,{id:`claude-lane-body-${e}`,children:[n.showSearch&&(0,J.jsx)(`input`,{className:`input claude-lane-search`,type:`search`,placeholder:r(`models.search`),"aria-label":r(`models.search`),value:S[e]??``,onChange:t=>{let n=t.target.value;C(t=>({...t,[e]:n})),T(t=>({...t,[e]:6}))}}),(0,J.jsxs)(`div`,{className:`claude-lane-models`,children:[t.length===0?(0,J.jsx)(`div`,{className:`claude-lane-empty`,children:r(`claudeDesktop.laneEmpty`)}):n.noMatch?(0,J.jsx)(`div`,{className:`claude-lane-empty`,children:r(`claudeDesktop.laneNoMatch`)}):n.shown.map(t=>{let n=I.assignments[t.route],i=dv(t.contextWindow,r),a=z[t.route]??`opus`,o=O[t.route]??tv(t.route,H[e]);return(0,J.jsxs)(`article`,{className:`claude-model-card${o?` open`:``}`,draggable:t.available,onDragStart:e=>{e.dataTransfer.effectAllowed=`move`,e.dataTransfer.setData(`text/plain`,t.route)},children:[(0,J.jsxs)(`button`,{type:`button`,className:`claude-model-summary`,"aria-expanded":o,"aria-controls":`claude-model-body-${t.route}`,onClick:()=>k(e=>({...e,[t.route]:!o})),children:[(0,J.jsx)(Se,{className:`ocx-chevron`,width:12,height:12,"aria-hidden":`true`,style:{transform:o?`rotate(90deg)`:`none`}}),(0,J.jsxs)(`span`,{className:`claude-model-names`,children:[(0,J.jsx)(`strong`,{title:t.label,children:t.label}),(0,J.jsx)(`code`,{title:t.route,children:t.route})]}),i&&(0,J.jsx)(`span`,{className:`claude-model-context`,children:i}),!i&&(0,J.jsx)(`span`,{className:`claude-model-context claude-model-context-unknown`,children:r(`claudeDesktop.contextUnknown`)}),t.supports1m===!0&&(0,J.jsx)(`span`,{className:`claude-1m-chip`,children:r(`claudeDesktop.supports1m`)}),t.effortSupported===!1&&(0,J.jsx)(`span`,{className:`claude-effort-badge off`,children:r(`claudeDesktop.effort.displayOnly`)}),t.effortSupported===!0&&(0,J.jsx)(`span`,{className:`claude-effort-badge on`,children:r(`claudeDesktop.effort.supported`)}),I.defaults[e]===t.route&&(0,J.jsx)(`span`,{className:`claude-row-default`,children:r(`claudeDesktop.defaultBadge`)}),(0,J.jsx)(`span`,{className:`badge ${t.available?`badge-green`:`badge-muted`}`,children:t.available?r(`claudeDesktop.available`):r(`claudeDesktop.unavailable`)})]}),o&&(0,J.jsxs)(`div`,{className:`claude-model-body`,id:`claude-model-body-${t.route}`,children:[H[e]===t.route&&I.defaults[e]!==t.route&&(0,J.jsx)(`span`,{className:`claude-effective-default`,children:r(`claudeDesktop.temporaryDefault`)}),(0,J.jsxs)(`div`,{className:`claude-field`,children:[(0,J.jsx)(`span`,{children:r(`claudeDesktop.alias`)}),(0,J.jsx)(`code`,{className:`claude-alias`,title:n.alias,children:n.alias})]}),(0,J.jsxs)(`label`,{className:`claude-default-radio`,children:[(0,J.jsx)(`input`,{type:`radio`,name:`default-${e}`,checked:I.defaults[e]===t.route,disabled:!t.available,onChange:()=>u(n=>n&&{...n,defaults:{...n.defaults,[e]:t.route}})}),r(`claudeDesktop.useAsDefault`,{family:r(sv[e])})]}),(0,J.jsxs)(`div`,{className:`claude-move-row`,children:[(0,J.jsx)(`label`,{htmlFor:`move-${t.route}`,children:r(`claudeDesktop.moveTo`)}),(0,J.jsx)(`select`,{id:`move-${t.route}`,className:`input`,value:a,disabled:!t.available,onChange:e=>m(n=>({...n,[t.route]:e.target.value})),children:av.map(e=>(0,J.jsx)(`option`,{value:e,children:r(sv[e])},e))}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,disabled:!t.available||a===e,onClick:()=>te(t.route,a),children:r(`claudeDesktop.move`)})]})]})]},t.route)}),n.hidden>0&&(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm claude-lane-more`,onClick:()=>T(t=>({...t,[e]:(t[e]??6)+6})),children:r(`models.showMore`,{n:n.hidden})})]})]})]},e)})})]})}var gv=`integrations/claude`,_v=`integrations/claude/desktop`;function vv(e=typeof window<`u`?window.location.hash:``){return dt(e)===_v?`desktop`:`code`}function yv(e){let t=gr(`ocx.claude-desktop.v1:${e}`);return typeof t?.data?.port==`number`?t.data.port:null}function bv({apiBase:e,active:t=!0}){let[n,r]=(0,_.useState)(vv),i=Q(),a=(0,_.useRef)(null),o=(0,_.useRef)(null),s=yv(e),[c,l]=(0,_.useState)(null),u=c?.base===e?c.port:s,d=c?.base===e,f=(0,_.useCallback)(t=>{l(n=>n?.base===e&&n.port===t?n:{base:e,port:t})},[e]);(0,_.useEffect)(()=>{let e=()=>r(vv());return window.addEventListener(`hashchange`,e),window.addEventListener(`popstate`,e),()=>{window.removeEventListener(`hashchange`,e),window.removeEventListener(`popstate`,e)}},[]);let p=e=>{pt(e===`desktop`?_v:gv),r(e),window.requestAnimationFrame(()=>{(e===`code`?a:o).current?.focus({preventScroll:!0})})},m=e=>{e.key===`ArrowLeft`||e.key===`ArrowRight`?(e.preventDefault(),p(n===`code`?`desktop`:`code`)):e.key===`Home`?(e.preventDefault(),p(`code`)):e.key===`End`&&(e.preventDefault(),p(`desktop`))};return(0,J.jsxs)(`section`,{className:`claude-page`,children:[(0,J.jsxs)(`div`,{className:`claude-page-intro`,children:[(0,J.jsx)(`div`,{className:`page-head`,children:(0,J.jsx)(`h2`,{children:i(n===`code`?`claude.pageTitle`:`claudeDesktop.title`)})}),n===`code`?(0,J.jsx)(`p`,{className:`page-sub`,children:i(`claude.subtitle`)}):(0,J.jsx)(`p`,{className:`page-sub`,children:u==null?i(d?`claudeDesktop.loadFail`:`claudeDesktop.loading`):i(`claudeDesktop.subtitle`,{port:u})})]}),(0,J.jsxs)(`div`,{className:`claude-tabs`,role:`tablist`,"aria-label":i(`claude.tabsLabel`),children:[(0,J.jsx)(`button`,{type:`button`,role:`tab`,ref:a,"aria-selected":n===`code`,"aria-controls":`claude-code-panel`,id:`claude-code-tab`,className:n===`code`?`active`:``,tabIndex:n===`code`?0:-1,onKeyDown:m,onClick:()=>p(`code`),children:i(`claude.tabCode`)}),(0,J.jsx)(`button`,{type:`button`,role:`tab`,ref:o,"aria-selected":n===`desktop`,"aria-controls":`claude-desktop-panel`,id:`claude-desktop-tab`,className:n===`desktop`?`active`:``,tabIndex:n===`desktop`?0:-1,onKeyDown:m,onClick:()=>p(`desktop`),children:i(`claude.tabDesktop`)})]}),(0,J.jsx)(`div`,{id:`claude-code-panel`,role:`tabpanel`,"aria-labelledby":`claude-code-tab`,hidden:n!==`code`,children:(0,J.jsx)(Q_,{apiBase:e,active:t&&n===`code`},e)}),(0,J.jsx)(`div`,{id:`claude-desktop-panel`,role:`tabpanel`,"aria-labelledby":`claude-desktop-tab`,hidden:n!==`desktop`,children:(0,J.jsx)(hv,{apiBase:e,active:t&&n===`desktop`,onPortChange:f},e)})]})}function xv(e,t,n,r){let i=e.filter(e=>r===`native`===e.native).map(e=>({...e,alias:t.get(e.id)??null,enabled:!n.has(e.id)})).toSorted((e,t)=>Number(!e.enabled)-Number(!t.enabled));return{rows:i,total:i.length,enabled:i.filter(e=>e.enabled).length}}var Sv=rv(`ocx.grok.collapsedGroups.v2`),Cv=[{id:`native`,tkey:`grok.groupNative`},{id:`routed`,tkey:`grok.groupRouted`}],wv=new Set(Cv.map(e=>e.id));function Tv(e,t){return e?e>=1048576?t(`claudeDesktop.contextM`,{n:Math.round(e/1048576)}):e>=1e6?t(`claudeDesktop.contextM`,{n:e/1e6}):t(`claudeDesktop.contextK`,{n:Math.round(e/1e3)}):`—`}function Ev({apiBase:e,active:t=!0}){let n=Q(),r=`ocx.grok.status.v1:${e}`,i=vr(r),a=i?.data??null,[o,s]=(0,_.useState)(null),[c,l]=(0,_.useState)(()=>Sv.read()??new Set(wv)),[u,d]=(0,_.useState)(null),[f,p]=(0,_.useState)(null),[m,h]=(0,_.useState)(``),g=(0,_.useCallback)(async t=>{let i=await Pt(await fetch(`${e}/api/grok`,{signal:t}),n(`grok.loadFail`));if(!i)throw Error(n(`grok.loadFail`));let a={...i,candidates:i.candidates??[],excluded:i.excluded??[]};return yr(r,a),a},[e,r,n]),v=`grok-status:${e}`,y=ml(v,[e],g,{isEmpty:()=>!1,initialData:a??void 0,initialDataCachedAt:i?.cachedAt??null,staleAfterMs:6e4,enabled:t}),{state:b}=y,x=y.refresh,S=b.data??a,C=(0,_.useMemo)(()=>new Set(S?.excluded??[]),[S]),w=o??C,T=(0,_.useMemo)(()=>o!==null&&(o.size!==C.size||[...o].some(e=>!C.has(e))),[o,C]),E=(0,_.useMemo)(()=>new Map((S?.models??[]).map(e=>[e.id,e.alias])),[S]),D=e=>{let t=iv(c,e);Sv.write(t),l(t)},O=e=>{let t=e?new Set(Cv.map(e=>e.id)):new Set;Sv.write(t),l(t)},k=(e,t)=>{s(n=>{let r=new Set(n??C);return t?r.delete(e):r.add(e),r})},A=async t=>{if(!u){d(`save`),p(null);try{await Pt(await fetch(`${e}/api/grok/selection`,{method:`PUT`,headers:{"Content-Type":`application/json`},body:JSON.stringify({excluded:[...w]})}),n(`grok.saveFailed`));let i=[...w];if(S&&K(v,{...S,excluded:i}),s(null),t){d(`apply`);let t=await fetch(`${e}/api/grok/apply`,{method:`POST`});if(!t.ok){let e=await t.json().catch(()=>({}));throw Error(e.message??e.error??n(`grok.applyFailed`))}let r=await t.json().catch(()=>({}));r.skippedReason?(p({tone:`err`,text:r.message??n(`grok.applySkipped`)}),h(r.message??n(`grok.applySkipped`))):(p({tone:`ok`,text:n(`grok.savedApplied`)}),h(n(`grok.savedApplied`))),await x()}else p({tone:`ok`,text:n(`grok.saved`)}),h(n(`grok.saved`)),S&&yr(r,{...S,excluded:i})}catch(e){let t=e instanceof Error?e.message:n(`grok.saveFailed`);p({tone:`err`,text:t}),h(t)}finally{d(null)}}};if(b.showSkeleton&&!S)return(0,J.jsx)(`section`,{className:`grok-page`,children:(0,J.jsx)(gl,{label:n(`grok.loading`),rows:4})});if(b.kind===`failed-cold`){let e=b.error instanceof Error?b.error.message:n(`grok.loadFail`);return(0,J.jsxs)(`section`,{className:`grok-page`,children:[(0,J.jsx)(`div`,{className:`alert alert-err`,role:`alert`,children:e}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:()=>x(),children:n(`common.retry`)})]})}return(0,J.jsxs)(`section`,{className:`grok-page`,"aria-busy":b.refreshing||void 0,children:[(0,J.jsx)(`h2`,{className:`page-title`,children:n(`grok.title`)}),(0,J.jsx)(`p`,{className:`page-sub`,children:n(`grok.subtitle`)}),(0,J.jsx)(`div`,{className:`sr-only`,"aria-live":`polite`,"aria-atomic":`true`,children:m}),f&&(0,J.jsx)($,{tone:f.tone,children:f.text}),b.showError&&(0,J.jsx)($,{tone:`err`,children:n(`grok.loadFail`)}),S&&S.candidates.length>0&&(0,J.jsxs)(`div`,{className:`claude-profile-bar`,children:[(0,J.jsx)(`span`,{className:`claude-dirty${T?` active`:``}`,children:n(T?`grok.unsaved`:`grok.upToDate`)}),(0,J.jsxs)(`div`,{className:`claude-save-actions`,children:[(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost`,disabled:!T||u!==null,onClick:()=>void A(!1),children:n(u===`save`?`grok.saving`:`common.save`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-primary`,disabled:!T||u!==null,onClick:()=>void A(!0),children:n(u===`apply`?`grok.applying`:u===`save`?`grok.saving`:`grok.saveApply`)})]})]}),S?.present?(0,J.jsxs)(J.Fragment,{children:[(0,J.jsxs)(`div`,{className:`grok-endpoint`,children:[(0,J.jsx)(`span`,{children:n(`grok.endpoint`)}),(0,J.jsx)(`code`,{children:S.baseUrl??`—`})]}),(0,J.jsx)(`p`,{className:`page-sub`,children:(0,J.jsx)(`code`,{children:S.configPath})})]}):(0,J.jsxs)(Ot,{title:n(`grok.notConfiguredTitle`),children:[n(`grok.notConfiguredHint`),(0,J.jsx)(`br`,{}),(0,J.jsx)(`code`,{children:S?.configPath})]}),S&&S.candidates.length>0&&(0,J.jsxs)(`div`,{className:`ocx-group-stack`,children:[(0,J.jsxs)(`div`,{className:`row`,style:{gap:6,margin:`2px 0 10px`},children:[(0,J.jsxs)(`button`,{type:`button`,className:`btn btn-ghost btn-sm text-caption`,onClick:()=>O(!0),disabled:u!==null,children:[(0,J.jsx)(Se,{width:12,height:12,"aria-hidden":`true`}),` `,n(`models.collapseAll`)]}),(0,J.jsxs)(`button`,{type:`button`,className:`btn btn-ghost btn-sm text-caption`,onClick:()=>O(!1),disabled:u!==null,children:[(0,J.jsx)(Se,{width:12,height:12,"aria-hidden":`true`,style:{transform:`rotate(90deg)`}}),` `,n(`models.expandAll`)]})]}),Cv.map(e=>{let t=xv(S.candidates,E,w,e.id);if(t.total===0)return null;let r=c.has(e.id);return(0,J.jsxs)(`section`,{className:`ocx-group${r?` collapsed`:``}`,"aria-labelledby":`grok-group-${e.id}`,children:[(0,J.jsx)(`header`,{className:`ocx-group-head${r?``:` open`}`,children:(0,J.jsx)(`h3`,{id:`grok-group-${e.id}`,className:`ocx-group-heading`,children:(0,J.jsxs)(`button`,{type:`button`,className:`ocx-group-toggle`,"aria-expanded":!r,"aria-controls":`grok-group-body-${e.id}`,onClick:()=>D(e.id),children:[(0,J.jsx)(Se,{className:`ocx-chevron`,width:14,height:14,"aria-hidden":`true`,style:{transform:r?`none`:`rotate(90deg)`}}),(0,J.jsx)(`span`,{className:`ocx-group-name`,children:n(e.tkey)}),(0,J.jsx)(`span`,{className:`ocx-group-count`,children:n(`grok.enabledCount`,{on:t.enabled,total:t.total})})]})})}),!r&&(0,J.jsx)(`div`,{id:`grok-group-body-${e.id}`,className:`grok-model-list`,children:t.rows.map(e=>(0,J.jsxs)(`div`,{className:`grok-model-row`,children:[(0,J.jsx)(Tt,{on:e.enabled,onClick:()=>k(e.id,!e.enabled),disabled:u!==null,label:n(`grok.toggleModel`,{id:e.id})}),(0,J.jsxs)(`span`,{className:`grok-model-names`,children:[(0,J.jsx)(`strong`,{title:e.id,children:e.id}),(0,J.jsx)(`code`,{title:e.alias??void 0,children:e.alias??`—`})]}),(0,J.jsx)(`span`,{className:`claude-model-context`,children:Tv(e.contextWindow,n)})]},e.id))})]},e.id)})]})]})}async function Dv(e,t){try{let n=await fetch(`${e}/api/native-integrations/cursor`,{signal:t});if(!n.ok)return null;let r=await Ft(n);return!r||typeof r!=`object`||!r.gateway||!r.privateInference?null:r}catch{return null}}function Ov({value:e,label:t}){let n=Q(),[r,i]=(0,_.useState)(!1),a=(0,_.useRef)(null);(0,_.useEffect)(()=>()=>{a.current!==null&&window.clearTimeout(a.current)},[]);let o=async()=>{try{await navigator.clipboard.writeText(e),i(!0),a.current!==null&&window.clearTimeout(a.current),a.current=window.setTimeout(()=>i(!1),1500)}catch{i(!1)}};return(0,J.jsxs)(`div`,{className:`cursor-gateway-row`,children:[(0,J.jsx)(`span`,{className:`cursor-gateway-label`,children:t}),(0,J.jsx)(`code`,{className:`cursor-gateway-value`,children:e}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:()=>void o(),"aria-label":`${n(`integrations.cursor.copy`)} ${t}`,children:n(r?`integrations.cursor.copied`:`integrations.cursor.copy`)})]})}function kv({labelKey:e,installed:t,path:n,version:r}){let i=Q();return(0,J.jsxs)(`div`,{className:`cursor-detect-row`,"data-installed":t?`true`:`false`,children:[(0,J.jsx)(`span`,{className:`cursor-detect-name`,children:i(e)}),(0,J.jsx)(`span`,{className:`badge ${t?`badge-green`:`badge-muted`}`,children:i(t?`integrations.cursor.detected`:`integrations.cursor.notFound`)}),t&&n&&(0,J.jsxs)(`span`,{className:`cursor-detect-path muted`,children:[r?`${r} · `:``,n]})]})}function Av({apiBase:e,active:t}){let{t:n,locale:r}=ct(),[i,a]=(0,_.useState)(()=>Date.now()),o=(0,_.useCallback)(async t=>{let n=await Dv(e,t);if(!n)throw Error(`cursor status unavailable`);return a(Date.now()),n},[e]),s=ml(`integration-cursor-page:${e}`,[e],o,{isEmpty:()=>!1,enabled:t,pollMs:15e3,pauseWhenHidden:!0}),c=s.state.data??null,l=Ti(n);return(0,J.jsxs)(`section`,{className:`integration-native-page cursor-page`,"aria-labelledby":`cursor-integration-title`,children:[(0,J.jsx)(`h3`,{id:`cursor-integration-title`,children:n(`integrations.cursor.title`)}),(0,J.jsx)(`p`,{children:n(`integrations.cursor.intro`)}),s.state.showError&&(0,J.jsx)($,{tone:`err`,children:n(`integrations.cursor.unavailable`)}),!c&&!s.state.showError&&(0,J.jsx)(gl,{label:n(`integrations.cursor.loading`),rows:4}),c&&(0,J.jsxs)(J.Fragment,{children:[(0,J.jsxs)(`div`,{className:`cursor-card`,children:[(0,J.jsx)(`h4`,{children:n(`integrations.cursor.detection`)}),(0,J.jsx)(kv,{labelKey:`integrations.cursor.privateInference`,installed:c.privateInference.installed,path:c.privateInference.path,version:c.privateInference.version}),(0,J.jsx)(kv,{labelKey:`integrations.cursor.regular`,installed:c.regularCursor.installed,path:c.regularCursor.path,version:null}),!c.privateInference.installed&&(0,J.jsxs)($,{tone:`warn`,children:[n(c.regularCursor.installed?`integrations.cursor.regularOnly`:`integrations.cursor.nothingFound`),` `,(0,J.jsx)(`a`,{href:c.guideUrl,target:`_blank`,rel:`noreferrer`,"data-cursor-guide":`notice`,children:n(`integrations.cursor.guide`)})]})]}),(0,J.jsxs)(`div`,{className:`cursor-card`,children:[(0,J.jsx)(`h4`,{children:n(`integrations.cursor.gateway`)}),(0,J.jsx)(`p`,{className:`muted`,children:n(`integrations.cursor.gatewayHint`)}),(0,J.jsx)(Ov,{label:n(`integrations.cursor.baseUrl`),value:c.gateway.baseUrl}),c.gateway.apiKeyMode===`placeholder`?(0,J.jsx)(Ov,{label:n(`integrations.cursor.apiKey`),value:c.gateway.placeholder}):(0,J.jsxs)(`div`,{className:`cursor-gateway-row`,children:[(0,J.jsx)(`span`,{className:`cursor-gateway-label`,children:n(`integrations.cursor.apiKey`)}),(0,J.jsx)(`span`,{className:`cursor-gateway-value`,children:n(`integrations.cursor.apiKeyCredential`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:()=>pt(`integrations/keys`),children:n(`integrations.tab.keys`)})]})]}),(0,J.jsxs)(`div`,{className:`cursor-card`,"data-seen":c.lastSeen?`true`:`false`,children:[(0,J.jsx)(`h4`,{children:n(`integrations.cursor.connection`)}),c.lastSeen?(0,J.jsx)(`p`,{children:(0,J.jsx)(`span`,{className:`badge ${i-c.lastSeen.at<864e5?`badge-green`:`badge-muted`}`,children:n(`integrations.cursor.seen`,{time:wi(c.lastSeen.at,l,i),ua:c.lastSeen.userAgent})})}):(0,J.jsx)(`p`,{className:`muted`,children:n(`integrations.cursor.neverSeen`)})]}),(0,J.jsxs)(`div`,{className:`cursor-card`,children:[(0,J.jsx)(`h4`,{children:n(`integrations.cursor.models`)}),(0,J.jsx)(`p`,{className:`muted`,children:c.effortTable.source===`bundle`?n(`integrations.cursor.ladderFromBundle`,{version:c.effortTable.version??n(`integrations.cursor.unknownVersion`)}):n(`integrations.cursor.ladderFromStatic`)}),(0,J.jsxs)(`table`,{className:`cursor-model-table`,children:[(0,J.jsx)(`thead`,{children:(0,J.jsxs)(`tr`,{children:[(0,J.jsx)(`th`,{children:n(`integrations.cursor.colModel`)}),(0,J.jsx)(`th`,{children:n(`integrations.cursor.colReasoning`)}),(0,J.jsx)(`th`,{children:n(`integrations.cursor.colContext`)})]})}),(0,J.jsx)(`tbody`,{children:c.models.map(e=>(0,J.jsxs)(`tr`,{children:[(0,J.jsx)(`td`,{children:(0,J.jsx)(`code`,{children:e.id})}),(0,J.jsx)(`td`,{children:e.reasoning?e.reasoning.join(` · `):(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)(`span`,{className:`cursor-no-control`,title:n(`integrations.cursor.noControlTitle`),"aria-label":n(`integrations.cursor.noControlTitle`),children:n(`integrations.cursor.noControl`)}),e.effortRows.length>0?(0,J.jsx)(`span`,{className:`cursor-effort-rows`,children:n(e.effortRows.length===1?`integrations.cursor.effortRowsOne`:`integrations.cursor.effortRowsMany`,{n:e.effortRows.length})}):(0,J.jsx)(`span`,{className:`cursor-effort-rows muted`,children:n(`integrations.cursor.effortRowsOff`)})]})}),(0,J.jsx)(`td`,{children:e.context?`${Rn(e.context.defaultWindow,r)} · ${Rn(e.context.longWindow,r)}`:n(`integrations.cursor.singleWindow`)})]},e.id))})]}),c.models.some(e=>e.tableLess)&&(0,J.jsx)(`p`,{className:`muted`,"data-cursor-tableless-hint":!0,children:n(`integrations.cursor.tableLessHint`)})]}),(0,J.jsx)(`p`,{children:(0,J.jsx)(`a`,{href:c.guideUrl,target:`_blank`,rel:`noreferrer`,children:n(`integrations.cursor.guide`)})})]})]})}var jv={"not-installed":`integrations.state.notInstalled`,unknown:`integrations.state.unknown`,absent:`integrations.state.absent`,current:`integrations.state.current`,stale:`integrations.state.stale`,conflict:`integrations.state.conflict`,unsafe:`integrations.state.unsafe`},Mv={"not-installed":`badge badge-muted`,unknown:`badge badge-muted`,absent:`badge badge-muted`,current:`badge badge-green`,stale:`badge badge-amber`,conflict:`badge integration-badge--danger`,unsafe:`badge integration-badge--danger-outline`};function Nv({state:e,installed:t,id:n}){let r=Q(),i=e===`unknown`||e===`not-installed`||t?e:`not-installed`;return(0,J.jsx)(`span`,{id:n,className:Mv[i],"data-integration-state":i,children:r(jv[i])})}function Pv({copyKey:e,vars:t}){let n=Q()(e,t),r=t?.path;if(!r||!n.includes(r))return(0,J.jsx)(`p`,{children:n});let[i,...a]=n.split(r);return(0,J.jsxs)(`p`,{children:[i,(0,J.jsx)(`code`,{children:r}),a.join(r)]})}function Fv({copy:e,onConfirm:t,onClose:n}){let r=Q(),i=(0,_.useRef)(null),[a,o]=(0,_.useState)(!1),[s,c]=(0,_.useState)(null),l=`integration-consequence-dialog-title`;(0,_.useEffect)(()=>{let e=i.current;return e&&!e.open&&e.showModal(),()=>{e?.open&&e.close()}},[]);let u=(0,_.useCallback)(e=>{e.preventDefault(),a||n()},[n,a]),d=(0,_.useCallback)(async()=>{if(!a){o(!0),c(null);try{await t()}catch(e){c(e instanceof Error?e.message:r(`integrations.error.generic`)),o(!1)}}},[t,a,r]),f=[(0,J.jsx)(Pv,{copyKey:e.changesKey,vars:e.vars},`changes`),(0,J.jsx)(Pv,{copyKey:e.breakageKey,vars:e.vars},`breakage`),(0,J.jsx)(Pv,{copyKey:e.undoKey,vars:e.vars},`undo`)];return e.sideEffectKey&&f.push((0,J.jsx)(Pv,{copyKey:e.sideEffectKey,vars:e.vars},`side-effect`)),(0,J.jsxs)(`dialog`,{ref:i,className:`modal-overlay`,"aria-labelledby":l,onCancel:u,children:[(0,J.jsx)(`button`,{type:`button`,className:`modal-backdrop-dismiss`,"aria-label":r(`common.close`),tabIndex:-1,onClick:()=>{a||n()}}),(0,J.jsxs)(`div`,{className:`modal-card integration-consequence-dialog`,role:`document`,children:[(0,J.jsxs)(`div`,{className:`modal-head`,children:[(0,J.jsx)(`h3`,{id:l,children:r(e.titleKey,e.vars)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:n,disabled:a,children:r(`common.close`)})]}),(0,J.jsx)(`div`,{className:`integration-consequence-body`,children:f}),s&&(0,J.jsx)($,{tone:`err`,children:s}),(0,J.jsx)(`div`,{className:`modal-actions`,children:(0,J.jsx)(`button`,{type:`button`,className:`btn btn-primary`,onClick:()=>void d(),disabled:a,children:r(e.confirmKey)})})]})]})}var Iv=[`opencode`,`pi`,`omp`,`hermes`,`openclaw`,`kimi`,`gajae`,`dsh`,`mcode`,`zcode`,`prime`,`aside`],Lv=new Set([`not_installed`,`conflict`,`unsafe`,`non_loopback`,`drift_requires_confirm`,`snapshot_expired`,`write_failed`]),Rv=new Set([`integration_unsafe`,`integration_conflict`,`integration_drift_confirmation_required`,`integration_snapshot_expired`,`integration_mutation_failed`]),zv=new Set([`absent`,`current`,`stale`,`conflict`,`unsafe`]);function Bv(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function Vv(e){return!Bv(e)||!Lv.has(String(e.reason))?!1:typeof e.error==`string`&&Rv.has(String(e.code))&&Iv.includes(e.clientId)&&zv.has(String(e.state))&&typeof e.message==`string`}var Hv=class extends Error{refusal;status;body;constructor(e,t){let n=Vv(t)?t:null;super(n?.message??t.error??t.message??String(e)),this.name=`IntegrationApiError`,this.status=e,this.body=t,this.refusal=n}};async function Uv(e){try{let t=await e.json();return Bv(t)?t:{}}catch{return{}}}async function Wv(e){if(!e.ok)throw new Hv(e.status,await Uv(e));let t=await Ft(e);if(t==null)throw new Hv(e.status,{});return t}async function Gv(e,t){return Wv(await fetch(`${e}/api/client-integrations`,{signal:t}))}async function Kv(e,t,n){return Wv(await fetch(`${e}/api/client-integrations/${encodeURIComponent(t)}`,{signal:n}))}async function qv(e,t,n){let r=t?`?client=${encodeURIComponent(t)}`:``;return Wv(await fetch(`${e}/api/client-integrations/journal${r}`,{signal:n}))}async function Jv(e,t,n,r,i){return Wv(await fetch(`${e}/api/client-integrations/${encodeURIComponent(t)}`,{method:`PUT`,headers:{"Content-Type":`application/json`},body:JSON.stringify(i===!0?{enabled:n,overwriteConflict:!0}:{enabled:n}),signal:r}))}async function Yv(e,t,n=!1,r){return Wv(await fetch(`${e}/api/client-integrations/restore`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({opId:t,confirmDrift:n}),signal:r}))}async function Xv(e){try{let t=await e;return t.ok?await Ft(t)??null:null}catch{return null}}async function Zv(e,t){let n=await Xv(fetch(`${e}/api/startup-health`,{signal:t}));return n?{routingInjected:n.routingInjected===!0,status:typeof n.status==`string`?n.status:void 0,recommendedCommand:typeof n.recommendedCommand==`string`?n.recommendedCommand:null}:null}async function Qv(e,t){let n=await fetch(`${e}/api/keys`,{signal:t});if(!n.ok)throw Error(`/api/keys responded ${n.status}`);let r=await Ft(n);if(!r||!Array.isArray(r.keys))throw Error(`/api/keys returned an unexpected body`);return r.keys.length}async function $v(e,t){let n=await Xv(fetch(`${e}/api/claude-code`,{signal:t}));return n?{enabled:n.enabled===!0,authMode:typeof n.authMode==`string`?n.authMode:void 0}:null}async function ey(e,t){let n=await Xv(fetch(`${e}/api/claude-desktop/status`,{signal:t}));return!n||typeof n.desiredEnabled!=`boolean`||typeof n.installed!=`boolean`||typeof n.observedKind!=`string`?null:{desiredEnabled:n.desiredEnabled,installed:n.installed,observedKind:n.observedKind,applied:n.applied===!0,stale:n.stale===!0,drift:n.drift===!0,driftReason:typeof n.driftReason==`string`?n.driftReason:null,activeProfile:typeof n.activeProfile==`boolean`?n.activeProfile:null,appliedAt:typeof n.appliedAt==`string`?n.appliedAt:null}}async function ty(e,t){let n=await Xv(fetch(`${e}/api/grok`,{signal:t}));return n?{present:n.present===!0,models:Array.isArray(n.models)?n.models:[]}:null}var ny=new Set([`claude`,`grok`,`codex`,`claude-desktop`]),ry=new Set([`native_integration_refused`,`native_integration_failed`]),iy=new Set([`not_installed`,`orphaned_marker`,`home_mismatch`,`config_busy`,`write_failed`,`metadata_unreadable`,`cleanup_incomplete`,`desired_state_changed`]);function ay(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function oy(e){return ay(e)?typeof e.error==`string`&&ry.has(String(e.code))&&ny.has(String(e.clientId))&&iy.has(String(e.reason))&&typeof e.message==`string`:!1}var sy=class extends Error{refusal;status;body;constructor(e,t){let n=oy(t)?t:null;super(n?.message??t.error??t.message??String(e)),this.name=`NativeApiError`,this.status=e,this.body=t,this.refusal=n}};async function cy(e){try{let t=await e;return t.ok?await Ft(t)??null:null}catch{return null}}async function ly(e){try{let t=await e.json();return ay(t)?t:{}}catch{return{}}}async function uy(e){if(!e.ok)throw new sy(e.status,await ly(e));let t=await Ft(e);if(t==null)throw new sy(e.status,{});return t}function dy(e,t){return cy(fetch(`${e}/api/native-integrations`,{signal:t}))}async function fy(e,t,n,r){return uy(await fetch(`${e}/api/native-integrations/${encodeURIComponent(t)}`,{method:`PUT`,headers:{"Content-Type":`application/json`},body:JSON.stringify({enabled:n}),signal:r}))}var py={integration_mutation_busy:`integrations.error.busy`};function my(e){return e instanceof Hv?e.refusal:null}function hy(e){return e instanceof sy?e.refusal:null}function gy(e){return e===`conflict`?`integrations.error.conflict`:e===`unsafe`?`integrations.error.unsafe`:e===`non_loopback`?`integrations.error.nonLoopback`:`integrations.error.generic`}var _y=new Set([`non_loopback`]);function vy(e,t,n){return t.reason===`orphaned_marker`?e(`integrations.native.error.orphanedMarker`,{path:n??``}):t.reason===`home_mismatch`?`${e(`integrations.native.error.homeMismatch`)} ${t.message}`:t.reason===`not_installed`?e(`integrations.native.error.notInstalled`):t.reason===`config_busy`?e(`integrations.native.error.configBusy`):t.reason===`metadata_unreadable`?e(`integrations.native.error.desktopUnsafeMetadata`,{path:n??``}):t.reason===`cleanup_incomplete`?e(`integrations.native.error.desktopCleanupIncomplete`,{paths:(t.residualPaths??[]).join(`, `)}):t.message||e(`integrations.error.generic`)}function yy(e,t,n,r){let i=hy(t);if(i)return vy(e,i,r);let a=my(t);if(!a){let r=py[t instanceof Hv?String(t.body.code??``):``];return r?e(r):t instanceof Error&&t.message?t.message:n??e(`integrations.error.generic`)}let o=_y.has(a.reason)?e(gy(a.reason),{client:a.clientId}):a.message||e(gy(a.reason));return a.snapshotPath?e(a.residual?`integrations.error.residual`:`integrations.error.recover`,{message:o,path:a.snapshotPath}):a.reason===`conflict`||a.reason===`unsafe`?`${e(gy(a.reason))} ${o}`:o}function by({apiBase:e,row:t,onClose:n,onRestored:r}){let i=Q(),a=(0,_.useRef)(null),o=(0,_.useRef)(null),s=(0,_.useRef)(null),c=(0,_.useRef)(!1),[l,u]=(0,_.useState)(!1),[d,f]=(0,_.useState)(!1),[p,m]=(0,_.useState)(null);(0,_.useEffect)(()=>{let e=a.current,t=document.activeElement;return o.current=t?.tagName===`BUTTON`?t:null,s.current=t?.closest?.(`section, [role='region'], main`)??null,e&&!e.open&&e.showModal(),()=>{e?.open&&e.close();let t=s.current;if(c.current&&t?.isConnected){t.hasAttribute(`tabindex`)||t.setAttribute(`tabindex`,`-1`),t.focus?.();return}let n=o.current;if(n?.isConnected){n.focus?.();return}t?.isConnected&&(t.hasAttribute(`tabindex`)||t.setAttribute(`tabindex`,`-1`),t.focus?.())}},[]);let h=(0,_.useCallback)(e=>{e.preventDefault(),d||n()},[n,d]),g=async()=>{if(!d){f(!0),m(null);try{await Yv(e,t.opId,l),c.current=!0,r(),n()}catch(e){if(my(e)?.reason===`drift_requires_confirm`){u(!0),f(!1);return}m(yy(i,e)),f(!1)}}};return(0,J.jsxs)(`dialog`,{ref:a,className:`modal-overlay`,"aria-labelledby":`integration-restore-title`,onCancel:h,children:[(0,J.jsx)(`button`,{type:`button`,className:`modal-backdrop-dismiss`,"aria-label":i(`common.close`),tabIndex:-1,onClick:()=>{d||n()}}),(0,J.jsxs)(`div`,{className:`modal-card integration-restore-dialog`,role:`document`,children:[(0,J.jsx)(`div`,{className:`modal-head`,children:(0,J.jsx)(`h3`,{id:`integration-restore-title`,children:i(l?`integrations.restore.driftTitle`:`integrations.restore.title`)})}),(0,J.jsx)(`div`,{className:`modal-desc`,children:i(l?`integrations.restore.driftBody`:`integrations.restore.body`)}),(0,J.jsx)(`p`,{className:`integration-path`,children:t.configPath}),p&&(0,J.jsx)($,{tone:`err`,children:p}),(0,J.jsxs)(`div`,{className:`modal-actions`,children:[(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost`,onClick:n,disabled:d,children:i(`common.cancel`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-primary`,onClick:()=>void g(),disabled:d,children:i(d?`integrations.restore.pending`:l?`integrations.restore.confirmDrift`:`integrations.restore.confirm`)})]})]})]})}var xy={opencode:`integrations.tab.opencode`,pi:`integrations.tab.pi`,omp:`integrations.tab.omp`,hermes:`integrations.tab.hermes`,openclaw:`integrations.tab.openclaw`,kimi:`integrations.tab.kimi`,gajae:`integrations.tab.gajae`,dsh:`integrations.tab.dsh`,mcode:`integrations.tab.mcode`,zcode:`integrations.tab.zcode`,prime:`integrations.tab.prime`,aside:`integrations.tab.aside`},Sy={apply:`integrations.kind.apply`,disable:`integrations.kind.disable`,refresh:`integrations.kind.refresh`,restore:`integrations.kind.restore`,overwrite:`integrations.kind.overwrite`};function Cy(e){return e===`current`||e===`stale`}function wy(e){let t={id:`codex`,hash:`integrations/codex`,labelKey:`integrations.tab.codex`,toggle:`codex`,toggleBlocked:null,togglePath:null,status:null,detail:null,detailVars:null};return e?e.routingInjected===!0?{...t,state:e.status===`error`?`stale`:`current`,installed:!0,applied:!0,detailKey:`integrations.detail.codexRouted`}:{...t,state:`absent`,installed:!0,applied:!1,detail:e.recommendedCommand??null,detailKey:e.recommendedCommand?null:`integrations.detail.codexAbsent`}:{...t,state:`unknown`,installed:!1,applied:!1,detailKey:null}}function Ty(e,t){let n={hash:`integrations/keys`,labelKey:`integrations.tab.keys`};return e===`checking`?{...n,state:`checking`,detailKey:`integrations.detail.keyChecking`,detailVars:null}:e===`unavailable`||t===null?{...n,state:`unavailable`,detailKey:`integrations.detail.keyUnavailable`,detailVars:null}:{...n,state:t>0?`issued`:`none-issued`,detailKey:t>0?`integrations.detail.keyCount`:`integrations.detail.keyNone`,detailVars:t>0?{count:String(t)}:null}}function Ey(e){return e?e.enabled===!0?e.authMode===`subscription`?`claude.authModeSubscription`:e.authMode===`proxy`?`claude.authModeProxy`:e.authMode===`auto`?`claude.authModeAuto`:null:`integrations.detail.claudeOff`:null}function Dy(e,t,n){let r={id:`claude`,hash:`integrations/claude`,labelKey:`integrations.tab.claude`,toggle:`claude`,toggleBlocked:t?.disableBlocked??null,togglePath:t?.configPath??null,status:null,detail:null,detailVars:null},i=Ey(e);if(n===void 0){if(!e)return{...r,state:`unknown`,installed:!1,applied:!1,detailKey:i};let t=e.enabled===!0;return{...r,state:t?`current`:`absent`,installed:!0,applied:t,detailKey:i}}return n?t?{...r,state:t.state,installed:t.installed,applied:t.state===`current`,detailKey:i}:{...r,toggle:null,state:`unknown`,installed:!1,applied:!1,detailKey:i}:{...r,state:`unknown`,installed:!1,applied:!1,detailKey:i}}function Oy(e,t,n){let r={id:`claudeDesktop`,hash:`integrations/claude/desktop`,labelKey:`claudeDesktop.title`,toggle:`claude-desktop`,toggleBlocked:t?.disableBlocked??null,togglePath:t?.configPath??null,status:null,detail:null,detailVars:null};if(!e||!n||!t||typeof e.desiredEnabled!=`boolean`)return{...r,toggle:null,state:`unknown`,installed:!1,applied:!1,detailKey:null};let i=e.desiredEnabled;if(!i)return e.applied===!0||e.driftReason===`desired_off_gateway_selected`?{...r,state:`stale`,installed:e.installed===!0,applied:!0,toggleOn:!1,detailKey:`integrations.detail.desktopDesiredOffCleanupPending`}:{...r,state:`absent`,installed:e.installed===!0,applied:!1,toggleOn:!1,detailKey:`integrations.detail.desktopDesiredOff`};if(e.applied!==!0)return{...r,state:`absent`,installed:e.installed===!0,applied:!1,toggleOn:i,detailKey:`integrations.detail.desktopDesiredOnNotApplied`};let a=e.stale===!0||e.activeProfile===!1;return{...r,state:a?`stale`:`current`,installed:!0,applied:!0,toggleOn:i,detailKey:e.activeProfile===!1?`integrations.detail.desktopNotServed`:a?`integrations.detail.desktopStale`:`integrations.detail.desktopCurrent`}}function ky(e){if(!e)return{detailKey:null,detailVars:null};let t=e.present===!0;return{detailKey:t?`integrations.detail.grokModels`:`integrations.detail.grokAbsent`,detailVars:t?{count:String(e.models?.length??0)}:null}}function Ay(e,t,n){let r={id:`grok`,hash:`integrations/grok`,labelKey:`integrations.tab.grok`,toggle:`grok`,toggleBlocked:t?.disableBlocked??null,togglePath:t?.configPath??null,status:null,detail:null},i=ky(e);if(n===void 0){if(!e)return{...r,state:`unknown`,installed:!1,applied:!1,...i};let t=e.present===!0;return{...r,state:t?`current`:`absent`,installed:t,applied:t,...i}}return n?t?{...r,state:t.state,installed:t.installed,applied:t.state===`current`,...i}:{...r,toggle:null,state:`unknown`,installed:!1,applied:!1,...i}:{...r,state:`unknown`,installed:!1,applied:!1,...i}}function jy(e,t=Date.now()){let n={id:`cursor`,hash:`integrations/cursor`,labelKey:`integrations.tab.cursor`,toggle:null,toggleBlocked:null,togglePath:null,status:null,detail:null,detailVars:null};if(!e)return{...n,state:`unknown`,installed:!1,applied:!1,detailKey:null};if(!e.privateInference.installed)return{...n,state:`not-installed`,installed:!1,applied:!1,detailKey:`integrations.detail.cursorAbsent`};let r=e.lastSeen!==null&&t-e.lastSeen.at<864e5;return{...n,state:r?`current`:`absent`,installed:!0,applied:r,detailKey:r?`integrations.detail.cursorSeen`:`integrations.detail.cursorNeverSeen`}}function My(e){return{id:e.clientId,hash:`integrations/${e.clientId}`,labelKey:xy[e.clientId],state:e.installed?e.state:`not-installed`,installed:e.installed,applied:e.installed&&Cy(e.state),detail:e.configPath,detailKey:null,detailVars:null,toggle:e.clientId,toggleBlocked:null,togglePath:e.configPath,status:e}}function Ny(e){let t=e.native?.find(e=>e.clientId===`claude`),n=e.native?.find(e=>e.clientId===`grok`),r=new Map(e.clients.map(e=>[e.clientId,e])),i=[wy(e.codex),Dy(e.claude,t,e.nativeSettled),Oy(e.claudeDesktop,e.native?.find(e=>e.clientId===`claude-desktop`),e.nativeSettled),Ay(e.grok,n,e.nativeSettled),jy(e.cursor)];for(let t of Iv){let n=r.get(t);if(n){i.push(My(n));continue}e.clientsSettled||i.push({id:t,hash:`integrations/${t}`,labelKey:xy[t],state:`unknown`,installed:!1,applied:!1,detail:null,detailKey:null,detailVars:null,toggle:null,toggleBlocked:null,togglePath:null,status:null})}return{keysRow:Ty(e.keyPhase,e.keyCount),rows:i}}function Py(e){return{detected:e.filter(e=>e.installed).length,applied:e.filter(e=>e.applied).length,stale:e.filter(e=>e.state===`stale`).length,unknown:e.filter(e=>e.state===`unknown`).length}}var Fy=6;function Iy({row:e,showClient:t,onRestore:n}){let r=Q();return(0,J.jsxs)(`li`,{className:`integration-history-row`,children:[(0,J.jsx)(`span`,{className:`integration-history-kind`,children:r(Sy[e.kind])}),t&&(0,J.jsx)(`span`,{className:`integration-history-client`,children:e.clientId}),(0,J.jsx)(`span`,{className:`integration-history-at`,children:new Date(e.at).toLocaleString()}),e.snapshot===`expired`?(0,J.jsx)(`span`,{className:`badge badge-muted`,children:r(`integrations.action.snapshotExpired`)}):(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:()=>n(e),children:e.undoable?r(`integrations.action.undo`):r(`integrations.action.restorePoint`)})]})}function Ly({rows:e,showClient:t,onRestore:n}){let r=Q(),[i,a]=(0,_.useState)(Fy),[o,...s]=e;if(!o)return null;let c=s.slice(0,i),l=s.length-c.length;return(0,J.jsxs)(`div`,{className:`integration-history`,children:[(0,J.jsx)(`ul`,{className:`integration-history-list`,children:(0,J.jsx)(Iy,{row:o,showClient:t,onRestore:n})}),s.length>0&&(0,J.jsxs)(`details`,{className:`integration-history-older`,children:[(0,J.jsx)(`summary`,{children:r(`integrations.rollback.older`)}),(0,J.jsx)(`ul`,{className:`integration-history-list`,children:c.map(e=>(0,J.jsx)(Iy,{row:e,showClient:t,onRestore:n},e.opId))}),l>0&&(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm integration-history-more`,onClick:()=>a(e=>e+Fy),children:r(`integrations.rollback.showMore`,{n:String(Math.min(l,Fy))})})]})]})}var Ry={titleKey:`integrations.dialog.grok.title`,changesKey:`integrations.dialog.grok.changes`,breakageKey:`integrations.dialog.grok.breakage`,undoKey:`integrations.dialog.grok.undo`,confirmKey:`integrations.dialog.grok.confirm`},zy={titleKey:`integrations.dialog.desktop.title`,changesKey:`integrations.dialog.desktop.changes`,breakageKey:`integrations.dialog.desktop.breakage`,undoKey:`integrations.dialog.desktop.undo`,sideEffectKey:`integrations.dialog.desktop.restart`,confirmKey:`integrations.dialog.desktop.confirm`};function By(e){return e.state===`current`||e.state===`stale`}function Vy({row:e,pending:t,result:n,onOpen:r,onToggle:i,onOverwrite:a}){let o=Q(),s=e.detail??(e.detailKey?o(e.detailKey,e.detailVars??void 0):null),c=e.toggleBlocked!==null&&(e.applied||e.toggleBlocked.reason===`orphaned_marker`),l=c&&e.toggleBlocked&&(e.toggle===`claude`||e.toggle===`grok`)?yy(o,new sy(409,{error:`native integration change refused`,code:`native_integration_refused`,clientId:e.toggle,reason:e.toggleBlocked.reason,message:e.toggleBlocked.message}),void 0,e.togglePath??void 0):null;return(0,J.jsxs)(`li`,{className:`integration-card`,"data-client":e.id,children:[(0,J.jsxs)(`div`,{className:`integration-card-head`,children:[(0,J.jsx)(Gg,{src:Wg(e.id),label:o(e.labelKey),size:20}),(0,J.jsx)(`h4`,{children:(0,J.jsx)(`button`,{type:`button`,className:`integration-card-link`,onClick:r,children:o(e.labelKey)})}),(0,J.jsx)(Nv,{state:e.state,installed:e.installed})]}),s&&(0,J.jsx)(`p`,{className:e.detail?`integration-path`:`integration-meta`,children:s}),n?.tone===`err`&&(0,J.jsx)($,{tone:`err`,children:n.text}),n?.tone===`ok`&&(0,J.jsx)($,{tone:`ok`,children:n.text}),(0,J.jsxs)(`div`,{className:`integration-card-actions`,children:[e.toggle&&i&&(0,J.jsxs)(`div`,{className:`integration-toggle-control`,children:[(0,J.jsx)(Tt,{on:e.toggleOn??e.applied,onClick:i,disabled:e.state===`unknown`||!e.installed||e.state===`conflict`||e.state===`unsafe`||c||t,label:e.applied?o(`integrations.action.disable`):o(`integrations.action.apply`)}),l&&(0,J.jsx)(`p`,{className:`integration-toggle-blocked`,children:l})]}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost`,onClick:r,tabIndex:-1,children:o(`integrations.action.settings`)}),a&&(0,J.jsx)(`button`,{type:`button`,className:`btn btn-danger`,onClick:a,disabled:t,children:o(`integrations.action.overwrite`)})]})]})}function Hy({apiBase:e,active:t=!0}){let n=Q(),[r,i]=(0,_.useState)(!1),[a,o]=(0,_.useState)(null),[s,c]=(0,_.useState)(null),[l,u]=(0,_.useState)({}),[d,f]=(0,_.useState)(null),[p,m]=(0,_.useState)(null),h=(0,_.useRef)(null);(0,_.useEffect)(()=>{if(d!==null)return;let e=h.current;e&&(h.current=null,e.isConnected&&e.focus())},[d]);let g=(0,_.useCallback)(async t=>(await Gv(e,t)).clients,[e]),v=(0,_.useCallback)(async t=>(await qv(e,void 0,t)).operations,[e]),y=(0,_.useCallback)(t=>Zv(e,t),[e]),b=(0,_.useCallback)(t=>Qv(e,t),[e]),x=(0,_.useCallback)(t=>$v(e,t),[e]),S=(0,_.useCallback)(t=>ey(e,t),[e]),C=(0,_.useCallback)(t=>ty(e,t),[e]),w=(0,_.useCallback)(t=>Dv(e,t),[e]),T=(0,_.useCallback)(async t=>(await dy(e,t))?.clients??null,[e]),E=ml(`integration-states:${e}`,[e],g,{isEmpty:e=>e.length===0,enabled:t,sessionCacheKey:`ocx.integrations.states.v1:${e}`}),D=ml(`integration-journal-all:${e}`,[e],v,{isEmpty:e=>e.length===0,enabled:t,sessionCacheKey:`ocx.integrations.journal.v1:${e}`}),O=ml(`integration-codex:${e}`,[e],y,{isEmpty:e=>e===null,enabled:t,sessionCacheKey:`ocx.integrations.codex.v1:${e}`}),k=ml(`integration-keys:${e}`,[e],b,{isEmpty:()=>!1,enabled:t,sessionCacheKey:`ocx.integrations.keys.v1:${e}`}),A=ml(`integration-claude:${e}`,[e],x,{isEmpty:e=>e===null,enabled:t,sessionCacheKey:`ocx.integrations.claude.v1:${e}`}),j=ml(`integration-claude-desktop:${e}`,[e],S,{isEmpty:e=>e===null,enabled:t,sessionCacheKey:`ocx.integrations.claude-desktop.v1:${e}`}),M=ml(`integration-grok:${e}`,[e],C,{isEmpty:e=>e===null,enabled:t,sessionCacheKey:`ocx.integrations.grok.v1:${e}`}),N=ml(`integration-cursor:${e}`,[e],w,{isEmpty:e=>e===null,enabled:t,sessionCacheKey:`ocx.integrations.cursor.v1:${e}`}),P=ml(`integration-native:${e}`,[e],T,{isEmpty:e=>e===null,enabled:t,sessionCacheKey:`ocx.integrations.native.v1:${e}`}),F=E.state.data??[],I=D.state.data??[],L=F.filter(By),R=F.filter(e=>e.installed),z=E.state.kind!==`cold`&&E.state.kind!==`retrying-cold`,B=P.state.data??null,V=B!==null,H=k.state.kind===`cold`||k.state.kind===`retrying-cold`?`checking`:k.state.kind===`failed-cold`||k.state.kind===`failed-with-stale`?`unavailable`:`settled`,{keysRow:U,rows:W}=Ny({clients:F,clientsSettled:z,codex:O.state.data??null,keyCount:k.state.data??null,keyPhase:H,claude:A.state.data??null,claudeDesktop:j.state.data??null,grok:M.state.data??null,cursor:N.state.data??null,native:B,nativeSettled:V}),ee=Py(W),G=()=>{E.refresh(),D.refresh(),O.refresh(),k.refresh(),A.refresh(),j.refresh(),M.refresh(),P.refresh()},K=async()=>{if(r||L.length===0)return;let t=[n(`integrations.bulk.title`),n(`integrations.bulk.body`)].join(` + +`);if(!confirm(t))return;i(!0),o(null);let a=[];for(let t of L)try{await Jv(e,t.clientId,!1)}catch(e){a.push(`${t.clientId}: ${yy(n,e)}`)}let s=!1;try{s=(await Gv(e)).clients.some(By)}catch{a.push(n(`integrations.error.stale`))}s&&a.length===0&&a.push(n(`integrations.error.stale`)),G(),i(!1),o(a.length===0?{tone:`ok`,text:n(`integrations.bulk.success`)}:{tone:`err`,text:n(`integrations.bulk.partial`,{clients:a.join(`; `)})})},q=I[0]?.at,[Y,te]=(0,_.useState)(null),ne=()=>{P.refresh(),A.refresh(),M.refresh()},re=(e,t)=>{u(n=>{let r={...n};return t?r[e]=t:delete r[e],r})},ie=async(t,r)=>{if(!Y&&t.toggle){te(t.id),re(t.id,null);try{if(t.status)await Jv(e,t.status.clientId,r),G();else if(t.toggle===`claude`||t.toggle===`grok`||t.toggle===`codex`||t.toggle===`claude-desktop`){let i=await fy(e,t.toggle,r);i.reason===`non_loopback_removed`?re(t.id,{tone:`ok`,text:n(i.changed?`integrations.native.msg.nonLoopbackRemoved`:`integrations.native.msg.nonLoopbackRemovedNoop`)}):i.reason===`non_loopback_superseded`&&re(t.id,{tone:`ok`,text:n(`integrations.native.msg.nonLoopbackSuperseded`)}),ne()}}catch(e){re(t.id,{tone:`err`,text:yy(n,e,void 0,t.togglePath??void 0)}),(t.toggle===`claude`||t.toggle===`grok`||t.toggle===`codex`||t.toggle===`claude-desktop`)&&ne()}finally{te(null)}}},ae=(e,t)=>{if(e.status||t||e.id===`claude`||e.toggle===null){ie(e,t);return}let n=document.activeElement;h.current=n?.tagName===`BUTTON`?n:null,f(e)},oe=async t=>{if(t.status){te(t.id),re(t.id,null);try{await Jv(e,t.status.clientId,!0,void 0,!0),G()}catch(e){throw re(t.id,{tone:`err`,text:yy(n,e,void 0,t.togglePath??void 0)}),e}finally{te(null)}}};return(0,J.jsxs)(`section`,{className:`integrations-overview`,children:[(0,J.jsxs)(`div`,{className:`integration-summary`,children:[(0,J.jsxs)(`div`,{className:`integration-summary-cell`,children:[(0,J.jsx)(`span`,{className:`integration-summary-label`,children:n(`integrations.summary.detected`)}),(0,J.jsx)(`strong`,{children:ee.detected})]}),(0,J.jsxs)(`div`,{className:`integration-summary-cell`,children:[(0,J.jsx)(`span`,{className:`integration-summary-label`,children:n(`integrations.summary.applied`)}),(0,J.jsx)(`strong`,{children:ee.applied})]}),(0,J.jsxs)(`div`,{className:`integration-summary-cell`,children:[(0,J.jsx)(`span`,{className:`integration-summary-label`,children:n(`integrations.summary.stale`)}),(0,J.jsx)(`strong`,{children:ee.stale})]}),ee.unknown>0&&(0,J.jsxs)(`div`,{className:`integration-summary-cell`,children:[(0,J.jsx)(`span`,{className:`integration-summary-label`,children:n(`integrations.state.unknown`)}),(0,J.jsx)(`strong`,{children:ee.unknown})]}),(0,J.jsxs)(`div`,{className:`integration-summary-cell`,children:[(0,J.jsx)(`span`,{className:`integration-summary-label`,children:n(`integrations.summary.lastChange`)}),(0,J.jsx)(`strong`,{children:q?new Date(q).toLocaleString():n(`integrations.status.unknown`)})]}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost`,onClick:()=>void K(),disabled:r||L.length===0,children:n(`integrations.summary.disableAll`)})]}),(0,J.jsx)(`h3`,{children:n(`integrations.catalog.title`)}),(0,J.jsx)(Uy,{row:U}),(0,J.jsx)(`p`,{className:`page-sub`,children:n(`integrations.onboarding`)}),E.state.kind===`failed-cold`&&(0,J.jsx)($,{tone:`err`,children:n(`integrations.error.load`)}),E.state.kind===`failed-with-stale`&&(0,J.jsx)($,{tone:`err`,children:n(`integrations.error.stale`)}),a&&(0,J.jsx)($,{tone:a.tone,children:a.text}),W.length===0?E.state.kind===`failed-cold`?null:(0,J.jsx)(`p`,{className:`page-sub`,children:n(`common.loading`)}):(0,J.jsx)(`ul`,{className:`integration-cards`,children:W.map(e=>(0,J.jsx)(Vy,{row:e,pending:Y!==null,result:l[e.id]??null,onOpen:()=>pt(e.hash),onToggle:e.toggle?()=>ae(e,!(e.toggleOn??e.applied)):null,onOverwrite:e.status!==null&&e.status.state===`conflict`&&e.installed?()=>m(e):null},e.id))}),z&&R.length===0&&(0,J.jsxs)(`div`,{className:`integration-empty`,children:[(0,J.jsx)(`h4`,{children:n(`integrations.empty.title`)}),(0,J.jsx)(`p`,{children:n(`integrations.empty.body`)})]}),(0,J.jsx)(`h3`,{children:n(`integrations.rollback.title`)}),D.state.showSkeleton?(0,J.jsx)(gl,{label:n(`integrations.rollback.title`),rows:2}):D.state.kind===`failed-cold`?(0,J.jsxs)($,{tone:`err`,children:[n(`integrations.rollback.failed`),` `,(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:()=>void D.refresh(),children:n(`common.retry`)})]}):I.length===0?(0,J.jsxs)(`div`,{className:`integration-empty`,children:[(0,J.jsx)(`p`,{children:n(`integrations.rollback.empty`)}),(0,J.jsx)(`p`,{className:`page-sub`,children:n(`integrations.rollback.emptyBody`)})]}):(0,J.jsx)(Ly,{rows:I,showClient:!0,onRestore:c}),s&&(0,J.jsx)(by,{apiBase:e,row:s,onClose:()=>c(null),onRestored:G}),d&&(0,J.jsx)(Fv,{copy:{...d.toggle===`claude-desktop`?zy:Ry,vars:{path:d.togglePath??``}},onClose:()=>f(null),onConfirm:async()=>{await ie(d,!1),f(null)}}),p&&p.status&&(0,J.jsx)(Fv,{copy:{titleKey:`integrations.dialog.overwrite.title`,changesKey:p.status.reason===`foreign-edit`?`integrations.dialog.overwrite.changesForeign`:`integrations.dialog.overwrite.changesUnowned`,breakageKey:`integrations.dialog.overwrite.breakage`,undoKey:`integrations.dialog.overwrite.undo`,confirmKey:`integrations.dialog.overwrite.confirm`,vars:{path:p.status.configPath}},onClose:()=>m(null),onConfirm:async()=>{await oe(p),m(null)}})]})}function Uy({row:e}){let t=Q(),n=e.detailKey?t(e.detailKey,e.detailVars??void 0):null;return(0,J.jsxs)(`div`,{className:`integration-api-keys-row`,"data-client":`keys`,"data-key-state":e.state,children:[(0,J.jsxs)(`div`,{className:`integration-api-keys-copy`,children:[(0,J.jsx)(`h4`,{children:t(e.labelKey)}),n&&(0,J.jsx)(`p`,{className:`integration-meta`,children:n})]}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost`,onClick:()=>pt(e.hash),children:t(`integrations.action.manageKeys`)})]})}function Wy(e,t){return{titleKey:`integrations.dialog.overwrite.title`,changesKey:e===`foreign-edit`?`integrations.dialog.overwrite.changesForeign`:`integrations.dialog.overwrite.changesUnowned`,breakageKey:`integrations.dialog.overwrite.breakage`,undoKey:`integrations.dialog.overwrite.undo`,confirmKey:`integrations.dialog.overwrite.confirm`,vars:{path:t}}}var Gy={opencode:`integrations.semantics.opencode`,pi:`integrations.semantics.pi`,omp:`integrations.semantics.omp`,hermes:`integrations.semantics.hermes`,openclaw:`integrations.semantics.openclaw`,kimi:`integrations.semantics.kimi`,gajae:`integrations.semantics.gajae`,dsh:`integrations.semantics.dsh`,mcode:`integrations.semantics.mcode`,zcode:`integrations.semantics.zcode`,prime:`integrations.semantics.prime`,aside:`integrations.semantics.aside`},Ky={opencode:`integrations.tab.opencode`,pi:`integrations.tab.pi`,omp:`integrations.tab.omp`,hermes:`integrations.tab.hermes`,openclaw:`integrations.tab.openclaw`,kimi:`integrations.tab.kimi`,gajae:`integrations.tab.gajae`,dsh:`integrations.tab.dsh`,mcode:`integrations.tab.mcode`,zcode:`integrations.tab.zcode`,prime:`integrations.tab.prime`,aside:`integrations.tab.aside`};function qy({apiBase:e,client:t,active:n=!0}){let r=Q(),[i,a]=(0,_.useState)(!1),[o,s]=(0,_.useState)(null),[c,l]=(0,_.useState)(null),[u,d]=(0,_.useState)(!1),f=(0,_.useCallback)(n=>Kv(e,t,n),[e,t]),p=(0,_.useCallback)(async n=>(await qv(e,t,n)).operations,[e,t]),m=ml(`integration-state:${e}:${t}`,[e,t],f,{isEmpty:()=>!1,enabled:n,sessionCacheKey:`ocx.integrations.state.v1:${e}:${t}`}),h=ml(`integration-journal:${e}:${t}`,[e,t],p,{isEmpty:e=>e.length===0,enabled:n,sessionCacheKey:`ocx.integrations.client-journal.v1:${e}:${t}`}),g=m.state.data??null,v=h.state.data??[],y=()=>{m.refresh(),h.refresh()},b=async n=>{if(!(!g||i)){a(!0),s(null);try{await Jv(e,t,n),y()}catch(e){s(yy(r,e))}finally{a(!1)}}},x=async()=>{if(g){s(null);try{await Jv(e,t,!0,void 0,!0),y()}catch(e){throw s(yy(r,e)),e}}},S=()=>void b(!(g&&(g.state===`current`||g.state===`stale`)));if(!g)return(0,J.jsx)(`section`,{className:`integration-client-page`,children:m.state.kind===`failed-cold`?(0,J.jsx)($,{tone:`err`,children:r(`integrations.error.load`)}):(0,J.jsx)(`p`,{className:`page-sub`,children:r(`common.loading`)})});let C=g.state===`current`||g.state===`stale`,w=!g.installed||g.state===`conflict`||g.state===`unsafe`;return(0,J.jsxs)(`section`,{className:`integration-client-page`,children:[(0,J.jsxs)(`div`,{className:`integration-client-head`,children:[(0,J.jsx)(Gg,{src:Wg(t),label:r(Ky[t]),size:24}),(0,J.jsx)(`h3`,{children:r(Ky[t])}),(0,J.jsx)(Nv,{state:g.state,installed:g.installed,id:`integration-state-${t}`}),(0,J.jsx)(Tt,{on:C,onClick:S,disabled:w||i,label:r(C?`integrations.action.disable`:`integrations.action.apply`)})]}),g.state===`stale`&&(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost`,onClick:()=>void b(!0),disabled:i,children:r(`integrations.action.refresh`)}),g.installed&&g.state===`conflict`&&(0,J.jsx)(`button`,{type:`button`,className:`btn btn-danger`,onClick:()=>d(!0),disabled:i,children:r(`integrations.action.overwrite`)}),(0,J.jsx)(`p`,{className:`page-sub`,children:r(Gy[t])}),(0,J.jsx)(`p`,{className:`integration-path`,children:g.configPath}),g.appliedAt&&(0,J.jsxs)(`p`,{className:`integration-meta`,children:[r(`integrations.status.appliedAt`),`: `,new Date(g.appliedAt).toLocaleString()]}),g.retentionDegraded&&(0,J.jsx)($,{tone:`err`,children:r(`integrations.retention.degraded`)}),o&&(0,J.jsx)($,{tone:`err`,children:o}),(0,J.jsx)(`h4`,{children:r(`integrations.rollback.title`)}),h.state.showSkeleton?(0,J.jsx)(gl,{label:r(`integrations.rollback.title`),rows:2}):h.state.kind===`failed-cold`?(0,J.jsxs)($,{tone:`err`,children:[r(`integrations.rollback.failed`),` `,(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:()=>void h.refresh(),children:r(`common.retry`)})]}):v.length===0?(0,J.jsx)(`p`,{className:`page-sub`,children:r(`integrations.rollback.empty`)}):(0,J.jsx)(Ly,{rows:v,onRestore:l}),c&&(0,J.jsx)(by,{apiBase:e,row:c,onClose:()=>l(null),onRestored:y}),u&&(0,J.jsx)(Fv,{copy:Wy(g.reason,g.configPath),onClose:()=>d(!1),onConfirm:async()=>{await x(),d(!1)}})]})}var Jy=[{id:`overview`,hash:`integrations`,labelKey:`integrations.tab.overview`},{id:`keys`,hash:`integrations/keys`,labelKey:`integrations.tab.keys`},{id:`codex`,hash:`integrations/codex`,labelKey:`integrations.tab.codex`},{id:`claude`,hash:`integrations/claude`,labelKey:`integrations.tab.claude`},{id:`grok`,hash:`integrations/grok`,labelKey:`integrations.tab.grok`},{id:`cursor`,hash:`integrations/cursor`,labelKey:`integrations.tab.cursor`},{id:`opencode`,hash:`integrations/opencode`,labelKey:`integrations.tab.opencode`},{id:`pi`,hash:`integrations/pi`,labelKey:`integrations.tab.pi`},{id:`omp`,hash:`integrations/omp`,labelKey:`integrations.tab.omp`},{id:`hermes`,hash:`integrations/hermes`,labelKey:`integrations.tab.hermes`},{id:`openclaw`,hash:`integrations/openclaw`,labelKey:`integrations.tab.openclaw`},{id:`kimi`,hash:`integrations/kimi`,labelKey:`integrations.tab.kimi`},{id:`gajae`,hash:`integrations/gajae`,labelKey:`integrations.tab.gajae`},{id:`dsh`,hash:`integrations/dsh`,labelKey:`integrations.tab.dsh`},{id:`mcode`,hash:`integrations/mcode`,labelKey:`integrations.tab.mcode`},{id:`zcode`,hash:`integrations/zcode`,labelKey:`integrations.tab.zcode`},{id:`prime`,hash:`integrations/prime`,labelKey:`integrations.tab.prime`},{id:`aside`,hash:`integrations/aside`,labelKey:`integrations.tab.aside`}],Yy=new Set([`opencode`,`pi`,`omp`,`hermes`,`openclaw`,`kimi`,`gajae`,`dsh`,`mcode`,`zcode`,`prime`,`aside`]);function Xy(e=window.location.hash){let t=dt(e);return t===`integrations/claude/desktop`?`claude`:Jy.find(e=>e.hash===t)?.id??`overview`}function Zy(e){return`integrations-tab-${e}`}function Qy(e){return`integrations-panel-${e}`}function $y(e){return e===`overview`||e===`keys`?null:Vg[e]??null}function eb({apiBase:e,machineApiBase:t=e,connected:n=!1}){let r=Q(),[i,a]=(0,_.useState)(Xy),[o,s]=(0,_.useState)(()=>new Set([Xy()])),c=(0,_.useRef)(null),[l,u]=(0,_.useState)([]),[d,f]=(0,_.useState)(!1);c.current===null&&(c.current=new Map),(0,_.useEffect)(()=>{if(!n)return;let e=new AbortController;return fetch(`${t}/api/machine/clients`,{signal:e.signal}).then(e=>e.ok?e.json():null).then(t=>{!e.signal.aborted&&Array.isArray(t?.selectedClients)&&u(t.selectedClients.filter(e=>typeof e==`string`))}).catch(()=>{}),()=>e.abort()},[n,t]);let p=async()=>{f(!0);try{await fetch(`${t}/api/machine/sync`,{method:`POST`,headers:{"Content-Type":`application/json`},body:`{}`})}finally{f(!1)}},m=e=>{a(e),s(t=>t.has(e)?t:new Set([...t,e]))};(0,_.useEffect)(()=>{let e=()=>m(Xy());return window.addEventListener(`hashchange`,e),window.addEventListener(`popstate`,e),()=>{window.removeEventListener(`hashchange`,e),window.removeEventListener(`popstate`,e)}},[]);let h=(e,t)=>{let n=Jy.find(t=>t.id===e);n&&(pt(n.hash),m(e),t&&window.requestAnimationFrame(()=>{c.current.get(e)?.focus({preventScroll:!0})}))},g=e=>{let t=Jy.findIndex(e=>e.id===i),n=null;e.key===`ArrowLeft`?n=(t-1+Jy.length)%Jy.length:e.key===`ArrowRight`?n=(t+1)%Jy.length:e.key===`Home`?n=0:e.key===`End`&&(n=Jy.length-1),n!==null&&(e.preventDefault(),h(Jy[n].id,!0))};return(0,J.jsxs)(`section`,{className:`integrations-page`,children:[(0,J.jsx)(`div`,{className:`page-head`,children:(0,J.jsx)(`h2`,{children:r(`nav.integrations`)})}),(0,J.jsx)(`p`,{className:`page-sub`,children:r(`integrations.subtitle`)}),n&&(0,J.jsxs)(`section`,{className:`notice`,"aria-label":r(`connection.clients.title`),children:[(0,J.jsx)(`strong`,{children:r(`connection.clients.title`)}),(0,J.jsx)(`span`,{children:l.length>0?l.join(`, `):r(`connection.clients.none`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,disabled:d,onClick:()=>void p(),children:r(d?`connection.clients.syncing`:`connection.clients.sync`)})]}),(0,J.jsx)(`div`,{className:`page-tabs`,role:`tablist`,"aria-label":r(`integrations.tabsLabel`),children:Jy.map(e=>(0,J.jsxs)(`button`,{ref:t=>{t?c.current.set(e.id,t):c.current.delete(e.id)},type:`button`,role:`tab`,id:Zy(e.id),"aria-selected":i===e.id,"aria-controls":Qy(e.id),tabIndex:i===e.id?0:-1,className:`page-tab${i===e.id?` page-tab--active`:``}`,onClick:()=>h(e.id,!0),onKeyDown:g,children:[$y(e.id)&&(0,J.jsx)(Gg,{src:$y(e.id),label:r(e.labelKey),size:14}),r(e.labelKey)]},e.id))}),Jy.map(t=>{if(!o.has(t.id))return null;let n=i===t.id;return(0,J.jsxs)(`div`,{role:`tabpanel`,id:Qy(t.id),"aria-labelledby":Zy(t.id),hidden:!n,children:[t.id===`overview`&&(0,J.jsx)(Hy,{apiBase:e,active:n}),t.id===`keys`&&(0,J.jsx)(A_,{apiBase:e,active:n}),t.id===`codex`&&(0,J.jsxs)(`section`,{className:`integration-native-page`,"aria-labelledby":`codex-integration-title`,children:[(0,J.jsx)(`h3`,{id:`codex-integration-title`,children:r(`integrations.codex.title`)}),(0,J.jsx)(`p`,{children:r(`integrations.codex.body`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost`,onClick:()=>pt(`startup`),children:r(`integrations.codex.openService`)})]}),t.id===`claude`&&(0,J.jsx)(bv,{apiBase:e,active:n}),t.id===`grok`&&(0,J.jsx)(Ev,{apiBase:e,active:n}),t.id===`cursor`&&(0,J.jsx)(Av,{apiBase:e,active:n}),Yy.has(t.id)&&(0,J.jsx)(qy,{apiBase:e,client:t.id,active:n})]},t.id)})]})}function tb(e){if(!e||typeof e!=`object`)return!1;let t=e;return typeof t.supported==`boolean`&&typeof t.installed==`boolean`&&typeof t.running==`boolean`&&typeof t.stale==`boolean`&&typeof t.summary==`string`}var nb={native:`startup.status.native`,protected:`startup.status.protected`,"at-risk":`startup.status.atRisk`},rb={native:`startup.summary.native`,protected:`startup.summary.protected`,"at-risk":`startup.summary.atRisk`},ib={service:`startup.protection.service`,shim:`startup.protection.shim`,none:`startup.protection.none`};function ab({ok:e,yes:t,no:n}){return(0,J.jsx)(`span`,{className:`badge ${e?`badge-green`:`badge-amber`}`,children:e?t:n})}function ob({failed:e,data:t}){let{t:n}=ct(),r=e?`startup-hero--risk`:t.status===`protected`?`startup-hero--safe`:t.status===`at-risk`?`startup-hero--risk`:`startup-hero--native`,i=e||t.status===`at-risk`?_e:ue,a=t.routingKind===`opencodex-local`?`startup.routing.proxy`:t.routingKind===`custom-local`?`startup.routing.customLocal`:t.routingKind===`custom-remote`?`startup.routing.customRemote`:t.routingKind===`unknown`?`startup.routing.unknown`:`startup.routing.native`;return(0,J.jsx)(J.Fragment,{children:(0,J.jsxs)(`section`,{className:`panel startup-hero ${r}`,"aria-live":`polite`,children:[(0,J.jsx)(`div`,{className:`startup-hero-icon`,children:(0,J.jsx)(i,{})}),(0,J.jsxs)(`div`,{className:`startup-hero-copy`,children:[(0,J.jsx)(`span`,{className:`badge ${e||t.status===`at-risk`?`badge-amber`:`badge-green`}`,children:n(e?`startup.status.atRisk`:nb[t.status])}),(0,J.jsx)(`h3`,{children:n(e?`startup.error`:rb[t.status])}),(0,J.jsx)(`p`,{children:e?n(`startup.staleData`):t.status===`at-risk`?n(xr(t)):n(`startup.safeDetail`)}),(0,J.jsxs)(`p`,{className:`muted startup-state-line`,children:[n(a),` · `,n(ib[t.protection]),` · `,n(t.autostartEnabled?`startup.enabled`:`startup.disabled`)]}),(0,J.jsx)(`p`,{className:`muted text-label`,children:n(`startup.subtitle`)})]})]})})}function sb({data:e,failed:t,loading:n=!1,installBusy:r,installResult:i,onInstall:a}){let{t:o}=ct(),s=e.serviceSupported&&e.serviceInstalled&&e.serviceStale&&!e.serviceConflict,c=e.shimInstalled&&!e.shimHealthy,l=r!==null||t||n;return(0,J.jsxs)(`section`,{className:`panel startup-details`,children:[(0,J.jsxs)(`div`,{className:`panel-head`,children:[(0,J.jsx)(`h3`,{className:`panel-title`,children:o(`startup.details`)}),(0,J.jsx)(`span`,{className:`muted mono`,children:e.platform})]}),(0,J.jsxs)(`div`,{className:`startup-detail-row`,children:[(0,J.jsxs)(`div`,{children:[(0,J.jsx)(`strong`,{children:o(`startup.service`)}),(0,J.jsx)(`span`,{children:o(`startup.serviceHint`)})]}),(0,J.jsxs)(`div`,{className:`startup-detail-actions`,children:[(0,J.jsx)(ab,{ok:e.serviceViable,yes:o(`startup.viable`),no:o(e.serviceConflict?`startup.conflict`:e.serviceStale?`startup.stale`:e.serviceInstalled?`startup.unhealthy`:e.serviceSupported?`startup.notInstalled`:`startup.unsupported`)}),e.serviceSupported&&!e.serviceInstalled&&(0,J.jsx)(`button`,{type:`button`,className:`btn btn-primary btn-sm`,"aria-label":`${o(`startup.service`)} - ${o(`startup.install`)}`,disabled:l,onClick:()=>a(`install-service`),children:o(r===`install-service`?`startup.installing`:`startup.install`)}),s&&(0,J.jsx)(`button`,{type:`button`,className:`btn btn-primary btn-sm`,"aria-label":`${o(`startup.service`)} - ${o(`startup.repair`)}`,disabled:l,onClick:()=>a(`install-service`,{repair:!0}),children:o(r===`install-service`?`startup.repairing`:`startup.repair`)})]})]}),(0,J.jsxs)(`div`,{className:`startup-detail-row`,children:[(0,J.jsxs)(`div`,{children:[(0,J.jsx)(`strong`,{children:o(`startup.shim`)}),(0,J.jsx)(`span`,{children:o(`startup.shimHint`)})]}),(0,J.jsxs)(`div`,{className:`startup-detail-actions`,children:[(0,J.jsx)(ab,{ok:e.shimHealthy&&e.autostartEnabled,yes:o(e.shimCoverage===`cli-only`?`startup.cliOnly`:`startup.healthy`),no:o(e.shimInstalled?e.shimHealthy&&!e.autostartEnabled?`startup.installedDisabled`:`startup.stale`:`startup.notInstalled`)}),!e.shimInstalled&&(0,J.jsx)(`button`,{type:`button`,className:`btn btn-primary btn-sm`,"aria-label":`${o(`startup.shim`)} - ${o(`startup.install`)}`,disabled:l,onClick:()=>a(`install-shim`),children:o(r===`install-shim`?`startup.installing`:`startup.install`)}),c&&(0,J.jsx)(`button`,{type:`button`,className:`btn btn-primary btn-sm`,"aria-label":`${o(`startup.shim`)} - ${o(`startup.repair`)}`,disabled:l,onClick:()=>a(`install-shim`,{repair:!0}),children:o(r===`install-shim`?`startup.repairing`:`startup.repair`)})]})]}),i&&(0,J.jsx)(`div`,{className:`notice ${i.kind===`success`?`notice-ok`:`notice-warn`} startup-action-notice`,role:`status`,"aria-live":`polite`,children:i.kind===`success`?i.action===`install-service`?o(i.repair?`startup.serviceRepaired`:`startup.serviceInstalled`):o(i.repair?`startup.shimRepaired`:`startup.shimInstalled`):`${o(`startup.installFailed`)} ${i.detail??``}`})]})}function cb({tray:e,trayLoading:t,trayError:n,trayBusy:r,onTrayAction:i}){let{t:a}=ct();return(0,J.jsxs)(`section`,{className:`panel startup-actions`,children:[(0,J.jsxs)(`div`,{className:`panel-head`,children:[(0,J.jsx)(`h3`,{className:`panel-title`,children:a(`startup.tray.title`)}),(0,J.jsx)(we,{})]}),(0,J.jsx)(`p`,{className:`muted`,children:a(`startup.tray.hint`)}),(0,J.jsxs)(`div`,{className:`startup-detail-row`,children:[(0,J.jsxs)(`div`,{children:[(0,J.jsx)(`strong`,{children:a(`startup.tray.login`)}),(0,J.jsx)(`span`,{children:a(`startup.tray.notProtection`)})]}),t||n||!e?(0,J.jsx)(`span`,{className:`badge badge-amber`,children:a(t?`startup.tray.loading`:`startup.tray.unavailable`)}):(0,J.jsx)(ab,{ok:e.running&&!e.stale,yes:a(`startup.tray.running`),no:a(e.stale?`startup.tray.stale`:e.installed?`startup.tray.stopped`:`startup.tray.notInstalled`)})]}),(0,J.jsxs)(`div`,{className:`startup-tray-buttons`,children:[!t&&!n&&e&&!e.installed&&!e.stale&&(0,J.jsx)(`button`,{type:`button`,className:`btn btn-primary`,disabled:r,onClick:()=>i(`install`),children:a(`startup.tray.install`)}),!t&&!n&&e?.installed&&!e.stale&&!e.running&&(0,J.jsx)(`button`,{type:`button`,className:`btn btn-primary`,disabled:r,onClick:()=>i(`start`),children:a(`startup.tray.start`)}),!t&&!n&&e?.running&&!e.stale&&(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost`,disabled:r,onClick:()=>i(`stop`),children:a(`startup.tray.stop`)}),!t&&!n&&e&&(e.installed||e.stale)&&(0,J.jsx)(`button`,{type:`button`,className:`btn btn-danger`,disabled:r,onClick:()=>{window.confirm(a(`startup.tray.uninstall`))&&i(`uninstall`)},children:a(`startup.tray.uninstall`)})]}),(n||e?.stale)&&(0,J.jsx)(`div`,{className:`notice notice-warn startup-tray-error`,role:`alert`,children:a(`startup.tray.error`)})]})}function lb({data:e,copied:t,onCopy:n}){let{t:r}=ct(),i=e.serviceInstalled&&!e.serviceConflict?e.commands.repairService:e.commands.installService;return(0,J.jsxs)(`section`,{className:`panel startup-actions`,children:[(0,J.jsxs)(`div`,{className:`panel-head`,children:[(0,J.jsx)(`h3`,{className:`panel-title`,children:r(`startup.recovery`)}),(0,J.jsx)(se,{})]}),(0,J.jsxs)(`details`,{className:`startup-recovery-details`,open:e.status!==`protected`,children:[(0,J.jsx)(`summary`,{className:`muted`,children:r(`startup.recoveryHint`)}),(0,J.jsxs)(`div`,{className:`startup-command-list`,children:[e.serviceSupported&&(0,J.jsxs)(`div`,{className:`startup-command-row`,children:[(0,J.jsxs)(`div`,{children:[(0,J.jsx)(`strong`,{children:r(`startup.command.service`)}),(0,J.jsx)(`code`,{children:i})]}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:()=>n(i),children:r(t===i?`startup.copied`:`startup.copy`)})]}),(0,J.jsxs)(`div`,{className:`startup-command-row`,children:[(0,J.jsxs)(`div`,{children:[(0,J.jsx)(`strong`,{children:r(`startup.command.shim`)}),(0,J.jsx)(`code`,{children:e.commands.installShim})]}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:()=>n(e.commands.installShim),children:t===e.commands.installShim?r(`startup.copied`):r(`startup.copy`)})]}),(0,J.jsxs)(`div`,{className:`startup-command-row`,children:[(0,J.jsxs)(`div`,{children:[(0,J.jsx)(`strong`,{children:r(`startup.command.native`)}),(0,J.jsx)(`code`,{children:e.commands.restoreNative})]}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:()=>n(e.commands.restoreNative),children:t===e.commands.restoreNative?r(`startup.copied`):r(`startup.copy`)})]})]}),e.status===`at-risk`&&(0,J.jsxs)(`div`,{className:`notice notice-warn startup-action-notice`,role:`alert`,children:[(0,J.jsx)(we,{}),` `,r(`startup.recommended`,{cmd:e.recommendedCommand??e.commands.installService})]})]})]})}var ub=`ocx.startup.page.v1:`;function db(e,t){let n=t===`win32`?`; `:` && `;return e.join(n)}function fb(e,t,n){if(!e)return{warning:null,fix:null};let r=!!e.catalogClamp?.active,i=!!e.newerAvailable,a=(r?e.catalogClamp?.runtimeVersion:e.version)??e.version??`unknown`,o=(e.catalogClamp?.removedEfforts??[]).join(`, `),s=db([`ocx doctor --fix-codex-runtime`,`ocx sync`],n);return r?{warning:o?t(`startup.codexRuntime.clampHiddenWithEfforts`,{version:a,efforts:o}):t(`startup.codexRuntime.clampHidden`,{version:a}),fix:i?s:`ocx sync`}:i?{warning:t(`startup.codexRuntime.olderBinary`,{version:a}),fix:s}:{warning:null,fix:null}}function pb({apiBase:e,machineApiBase:t=e,connected:n=!1}){let{t:r}=ct(),i=`${ub}${e}`,a=(0,_.useMemo)(()=>gr(i),[i]),o=`startup-page:${e}`,[s,c]=(0,_.useState)(null),[l,u]=(0,_.useState)(()=>a?.tray??null),[d,f]=(0,_.useState)(()=>!a?.data),[p,m]=(0,_.useState)(!1),[h,g]=(0,_.useState)(!1),[v,y]=(0,_.useState)(null),[b,x]=(0,_.useState)(null),[S,C]=(0,_.useState)(()=>a?.warning??null),[w,T]=(0,_.useState)(()=>a?.fix??null),[E,D]=(0,_.useState)(()=>!a?.data),O=(0,_.useRef)(!!a?.data),k=(0,_.useRef)(0),[A,j]=(0,_.useState)(null),[M,N]=(0,_.useState)(!1);(0,_.useEffect)(()=>{if(!n)return;let e=new AbortController;return fetch(`${t}/api/machine/shim`,{signal:e.signal}).then(e=>e.ok?e.json():null).then(t=>{e.signal.aborted||j(t)}).catch(()=>{e.signal.aborted||j(null)}),()=>e.abort()},[n,t]);let P=async e=>{N(!0);try{let n=await fetch(`${t}/api/machine/shim`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({action:e})});if(n.ok){let e=await n.json();j(e.shim??null)}}finally{N(!1)}};(0,_.useEffect)(()=>()=>{k.current+=1},[e]);let F=(0,_.useCallback)(async t=>{let n=++k.current,a=O.current;a||(f(!0),D(!0));try{let a=fetch(`${e}/api/settings`,{signal:t}).then(async e=>e.ok?await e.json():null).catch(()=>null),o=await fetch(`${e}/api/startup-health`,{signal:t});if(!o.ok)throw Error(`fetch failed`);let s=await o.json();x(e=>e?.kind===`error`&&(s.status===`native`&&e.forLocalRouting===!0||(e.action===`install-service`?s.serviceViable:s.shimInstalled&&s.shimHealthy))?null:e),O.current=!0;let c=gr(i);br(i,{data:s,warning:c?.warning??null,fix:c?.fix??null,tray:c?.tray??null});let l=s.platform===`win32`?fetch(`${e}/api/windows-tray`,{signal:t}).then(async e=>{if(!e.ok)throw Error(`tray status failed`);let t=await e.json();if(!tb(t))throw Error(`invalid tray status`);return{tray:t,error:!1}}).catch(()=>({tray:null,error:!0})):Promise.resolve({tray:null,error:!1});return Promise.all([a,l]).then(([e,a])=>{if(t.aborted||n!==k.current)return;let o=s.platform===`win32`?a.tray:null;if(s.platform===`win32`?(u(o),g(a.error)):(u(null),g(!1)),f(!1),D(!1),e){let t=fb(e.codexRuntime,r,s.platform);C(t.warning),T(t.fix),br(i,{data:s,warning:t.warning,fix:t.fix,tray:o});return}let c=gr(i);br(i,{data:s,warning:c?.warning??null,fix:c?.fix??null,tray:o})}),s}catch(e){throw t.aborted?e:(a||(u(null),g(!0),C(null),T(null)),D(!1),f(!1),e)}},[e,i,r]),I=ml(o,[e],F,{isEmpty:()=>!1,initialData:a?.data??void 0}),L=I.state,R=I.refresh,z=L.data??a?.data??null,B=L.refreshing,V=!!z?.diagnosticStale||L.showError;(0,_.useEffect)(()=>{if(!z?.diagnosticStale)return;let e=window.setTimeout(R,2e3);return()=>window.clearTimeout(e)},[z,R]);let H=async e=>{try{await navigator.clipboard.writeText(e),c(e),window.setTimeout(()=>c(t=>t===e?null:t),1600)}catch{c(null)}},U=async t=>{m(!0),g(!1);try{let n=await fetch(`${e}/api/windows-tray`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({action:t})});if(!n.ok)throw Error(`tray action failed`);let r=await n.json();if(!tb(r.status))throw Error(`invalid tray action status`);u(r.status),g(!1)}catch{u(null),g(!0)}finally{m(!1)}},W=async(t,n)=>{y(t),x(null);try{let r=await fetch(`${e}/api/startup-action`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({action:t,repair:n?.repair===!0})});if(!r.ok){let e=await r.json().catch(()=>null);throw Error(typeof e?.error==`string`?e.error:`installation failed`)}x({kind:`success`,action:t,repair:n?.repair===!0}),R()}catch(e){x({kind:`error`,action:t,repair:n?.repair===!0,detail:e instanceof Error?e.message:String(e),forLocalRouting:z?.localRoutingDependency===!0})}finally{y(null)}};return(0,J.jsxs)(J.Fragment,{children:[(0,J.jsxs)(`div`,{className:`page-head`,children:[(0,J.jsx)(`h2`,{children:r(`startup.title`)}),(0,J.jsx)(`div`,{className:`startup-page-head-actions`,children:(0,J.jsxs)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:()=>R(),disabled:B,children:[(0,J.jsx)(pe,{}),` `,r(`startup.refresh`)]})})]}),n&&(0,J.jsxs)(`section`,{className:`notice startup-page-notice`,"aria-label":r(`connection.machine.title`),children:[(0,J.jsx)(`strong`,{children:r(`connection.machine.title`)}),(0,J.jsx)(`span`,{children:A?.healthy?r(`connection.machine.shimHealthy`):r(`connection.machine.shimNeedsAttention`)}),(0,J.jsxs)(`div`,{className:`startup-page-head-actions`,children:[(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,disabled:M,onClick:()=>void P(`repair`),children:r(`connection.machine.repairShim`)}),A?.installed&&(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,disabled:M,onClick:()=>void P(`uninstall`),children:r(`connection.machine.removeShim`)})]})]}),L.showSkeleton&&!z?(0,J.jsx)(gl,{label:r(`startup.loading`),rows:5}):L.kind===`failed-cold`?(0,J.jsxs)(`div`,{className:`startup-page-notice`,children:[(0,J.jsx)($,{tone:`err`,children:L.error instanceof Error?L.error.message:r(`startup.error`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:()=>R(),children:r(`common.retry`)})]}):z?(0,J.jsxs)(J.Fragment,{children:[L.showError&&(0,J.jsx)($,{tone:`err`,children:r(`startup.error`)}),V&&(0,J.jsx)(`div`,{className:`notice notice-warn startup-page-notice`,role:`alert`,children:r(`startup.staleData`)}),(E||S)&&(0,J.jsx)(`div`,{className:`startup-runtime-notice-slot${E&&!S?` startup-runtime-notice-slot--pending`:``}`,"aria-hidden":E&&!S?!0:void 0,children:S&&(0,J.jsxs)(`div`,{className:`notice notice-warn startup-page-notice startup-runtime-notice`,role:`status`,children:[(0,J.jsx)(`p`,{className:`startup-runtime-notice__text`,children:S}),w&&(0,J.jsxs)(`div`,{className:`startup-runtime-notice__fix`,children:[(0,J.jsx)(`code`,{children:w}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:()=>void H(w),children:r(s===w?`startup.copied`:`startup.copy`)})]})]})}),(0,J.jsx)(ob,{failed:V,data:z}),(0,J.jsx)(sb,{data:z,failed:V,loading:B,installBusy:v,installResult:b,onInstall:(e,t)=>{W(e,t)}}),z.platform===`win32`&&(0,J.jsx)(cb,{tray:l,trayLoading:d,trayError:h,trayBusy:p,onTrayAction:e=>{U(e)}}),(0,J.jsx)(lb,{data:z,copied:s,onCopy:e=>{H(e)}})]}):null]})}var mb=3e5,hb=6e5,gb=`https://github.com/lidge-jun/opencodex`;async function _b(e,t){let n=await fetch(e,{signal:t});return n.ok?await n.json():null}function vb({apiBase:e,onOpenUpdate:t}){let n=Q(),[r,i]=(0,_.useState)(!1),[a,o]=(0,_.useState)(null),s=G(`sidebar-star:${e}`,[e],t=>_b(`${e}/api/github/star`,t),{pollMs:mb}),c=G(`sidebar-update-badge:${e}`,[e],t=>_b(`${e}/api/update/badge`,t),{pollMs:hb}),l=s.data?.state??null,u=a!==null&&a.basedOn===l?a.state:l??`not-starred`,d=s.data?.url??gb,f=u===`starred`,p=c.data,m=p?.updateAvailable===!0,h=p?.latestVersion??null,g=()=>window.open(d,`_blank`,`noopener,noreferrer`),v=async()=>{if(!(f||r)){if(u===`unauthenticated`){g();return}i(!0);try{let t=await fetch(`${e}/api/github/star`,{method:`POST`}),n=t.ok?await t.json():null;if(n?.ok===!0){o({state:`starred`,basedOn:l});return}n?.state&&o({state:n.state,basedOn:l}),g()}catch{g()}finally{i(!1),s.refresh()}}},y=n(f?`sidebar.starred`:u===`unauthenticated`?`sidebar.starUnauthenticated`:`sidebar.star`),b=m&&h?n(`sidebar.updateAvailable`,{version:h}):n(`sidebar.checkUpdate`);return(0,J.jsxs)(`div`,{className:`sidebar-github-row`,children:[(0,J.jsxs)(`a`,{className:`sidebar-link sidebar-github-link`,href:d,target:`_blank`,rel:`noreferrer`,children:[(0,J.jsx)(Ce,{}),` `,n(`common.github`)]}),(0,J.jsxs)(`div`,{className:`sidebar-github-actions`,children:[(0,J.jsx)(`button`,{type:`button`,className:`sidebar-orb${f?` sidebar-orb--starred`:``}`,onClick:()=>{v()},disabled:r||f,"aria-label":y,"aria-pressed":f,title:y,children:(0,J.jsx)(Ie,{"aria-hidden":`true`,...f?{fill:`currentColor`}:{}})}),(0,J.jsxs)(`button`,{type:`button`,className:`sidebar-orb${m?` sidebar-orb--update`:``}`,onClick:t,"aria-label":b,title:b,children:[(0,J.jsx)(xe,{"aria-hidden":`true`}),m&&(0,J.jsx)(`span`,{className:`sidebar-orb-dot`,"aria-hidden":`true`})]})]})]})}var yb=`opencodex-admin-token-dialog`,bb=`OpenCodex`;function xb(e,t=it()){let n=Ze[t],r=n[`auth.adminTokenTitle`];return new Promise(t=>{let i=document.activeElement instanceof HTMLElement?document.activeElement:null,a=!1,o=document.createElement(`dialog`);o.id=yb,o.className=`modal-overlay`,o.setAttribute(`aria-labelledby`,`${yb}-title`);let s=document.createElement(`form`);s.className=`modal-card`,s.method=`post`,s.action=window.location.href,s.autocomplete=`on`;let c=document.createElement(`div`);c.className=`modal-head`;let l=document.createElement(`h3`);l.id=`${yb}-title`,l.textContent=r,c.append(l);let u=document.createElement(`div`),d=document.createElement(`label`);d.className=`field-label`,d.htmlFor=`${yb}-username`,d.textContent=n[`auth.adminAccountLabel`];let f=document.createElement(`input`);f.id=d.htmlFor,f.className=`input`,f.type=`text`,f.name=`username`,f.autocomplete=`username`,f.value=bb,f.readOnly=!0,u.append(d,f);let p=document.createElement(`div`);p.style.marginTop=`var(--space-4)`;let m=document.createElement(`label`);m.className=`field-label`,m.htmlFor=`${yb}-password`,m.textContent=n[`auth.adminTokenFieldLabel`];let h=document.createElement(`input`);h.id=m.htmlFor,h.className=`input`,h.type=`password`,h.name=`password`,h.autocomplete=`current-password`,h.required=!0,h.spellcheck=!1,h.autocapitalize=`none`,p.append(m,h);let g=document.createElement(`div`);g.className=`notice notice-err`,g.setAttribute(`role`,`alert`),g.hidden=!0;let _=document.createElement(`div`);_.className=`modal-actions`;let v=document.createElement(`button`);v.type=`button`,v.className=`btn btn-ghost`,v.textContent=n[`common.cancel`];let y=document.createElement(`button`);y.type=`submit`,y.className=`btn btn-primary`,y.textContent=n[`common.ok`],_.append(v,y),s.append(c,u,p,g,_),o.append(s);let b=e=>{a||(a=!0,o.open&&o.close(),o.remove(),i?.focus(),t(e))};s.addEventListener(`submit`,t=>{t.preventDefault();let r=h.value.trim();if(!r){h.value=``,h.reportValidity();return}h.disabled=!0,y.disabled=!0,g.hidden=!0,e(r).then(e=>{if(!a){if(e===`accepted`){b(r);return}h.value=``,h.disabled=!1,y.disabled=!1,g.textContent=e===`rejected`?n[`auth.adminTokenRejected`]:n[`auth.adminTokenUnavailable`],g.hidden=!1,h.focus()}}).catch(()=>{a||(h.value=``,h.disabled=!1,y.disabled=!1,g.textContent=n[`auth.adminTokenUnavailable`],g.hidden=!1,h.focus())})}),v.addEventListener(`click`,()=>b(null)),o.addEventListener(`cancel`,e=>{e.preventDefault(),b(null)}),document.body.append(o),typeof o.showModal==`function`?o.showModal():o.setAttribute(`open`,``),queueMicrotask(()=>h.focus())})}var Sb=`opencodex-api-token`,Cb=`/api/settings`,wb=1e4,Tb=15e3,Eb=`X-OpenCodex-Machine-Session`,Db=`X-OpenCodex-Machine-GUI-Origin`,Ob=`X-OpenCodex-Machine-CSRF-Token`,kb=!1,Ab=null,jb=null,Mb=xb,Nb=wb,Pb=Tb,Fb=new Map;function Ib(){return{token:null,csrfToken:null,browserOrigin:null,serverOrigin:null}}function Lb(){return jb||zb(Qg(``)),jb}function Rb(e,t){return e.baseUrl===t.baseUrl&&e.serverOrigin===t.serverOrigin&&e.transport===t.transport}function zb(e){jb=e;for(let t of[`machine`,`shared`]){let n=Fb.get(t);Fb.set(t,n&&Rb(n.target,e[t])?{...n,target:e[t]}:{target:e[t],session:Ib(),resolutionInFlight:null,promptCancelled:!1})}}function Bb(e){return Lb(),Fb.get(e)}function Vb(e,t){let n=Bb(e);t!==null&&n.session.token===t&&(n.session=Ib())}function Hb(e,t,n,r,i){let a=Bb(e);return!t?.startsWith(`ocx_session_`)||!n||r!==window.location.origin||i!==a.target.serverOrigin?(a.session=Ib(),!1):(a.session={token:t,csrfToken:n,browserOrigin:r,serverOrigin:i},a.promptCancelled=!1,!0)}function Ub(e){return!!Bb(e).session.token?.startsWith(`ocx_session_`)}async function Wb(e){let t=Bb(e);if(!t.session.token?.startsWith(`ocx_session_`))return!1;let n=Vn(wb);try{return(await window.fetch(`${t.target.baseUrl}/api/session/logout`,{method:`POST`,signal:n.signal})).ok?(t.session=Ib(),t.promptCancelled=!1,!0):!1}catch{return!1}finally{n.clear()}}function Gb(e){let t=document.querySelector(`meta[name="${e}"]`),n=t?.content.trim()||null;return t?.remove(),n}function Kb(){let e={token:Gb(`opencodex-session-token`),csrf:Gb(`opencodex-session-csrf`),browser:Gb(`opencodex-session-origin`),server:Gb(`opencodex-session-server-origin`)};for(let t of[`machine`,`shared`])Bb(t).target.serverOrigin===e.server&&Hb(t,e.token,e.csrf,e.browser,e.server)}function qb(e,t){for(let n of e.match(/]*>/gi)??[])if(n.match(/\bname=["']([^"']+)["']/i)?.[1]===t)return n.match(/\bcontent=["']([^"']*)["']/i)?.[1]?.trim()||null;return null}function Jb(e,t){return Hb(e,qb(t,`opencodex-session-token`),qb(t,`opencodex-session-csrf`),qb(t,`opencodex-session-origin`),qb(t,`opencodex-session-server-origin`))}function Yb(){try{sessionStorage.removeItem(Sb)}catch{}}function Xb(e){return new URL(e.baseUrl||`/`,window.location.href)}function Zb(e,t){let n=Xb(e);if(t.origin!==n.origin)return!1;let r=n.pathname.replace(/\/$/,``);return r===``||t.pathname===r||t.pathname.startsWith(`${r}/`)}function Qb(e,t){if(!Zb(e,t))return null;let n=Xb(e).pathname.replace(/\/$/,``);return t.pathname.slice(n.length)||`/`}function $b(e){let t;try{t=new URL(e instanceof Request?e.url:String(e),window.location.href)}catch{return null}let n=Lb();return t.href===new URL(n.shared.bootstrapPath,window.location.href).href?{plane:`shared`,bootstrap:!0}:n.shared.transport===`relay`&&Zb(n.shared,t)?{plane:`shared`,bootstrap:!1}:Qb(n.machine,t)?.startsWith(`/api/machine/`)?{plane:`machine`,bootstrap:!1}:Qb(n.shared,t)?.startsWith(`/api/`)?{plane:`shared`,bootstrap:!1}:t.href===new URL(n.machine.bootstrapPath,window.location.href).href?{plane:`machine`,bootstrap:!0}:null}function ex(e,t,n,r){let i=Bb(e),a=new Headers(n?.headers??(t instanceof Request?t.headers:void 0)),o=r===void 0?i.session.token:r,s=(n?.method??(t instanceof Request?t.method:`GET`)).toUpperCase();if(o&&a.set(`X-OpenCodex-API-Key`,o),o?.startsWith(`ocx_session_`)&&i.session.browserOrigin&&i.session.csrfToken&&(a.set(`X-OpenCodex-GUI-Origin`,i.session.browserOrigin),s!==`GET`&&s!==`HEAD`&&a.set(`X-OpenCodex-CSRF-Token`,i.session.csrfToken)),e===`shared`&&i.target.transport===`relay`){let e=Bb(`machine`).session;e.token&&a.set(Eb,e.token),e.browserOrigin&&a.set(Db,e.browserOrigin),s!==`GET`&&s!==`HEAD`&&e.csrfToken&&a.set(Ob,e.csrfToken)}return a}function tx(e,t,n,r){let i=ex(e,t,n,r);return t instanceof Request?[new Request(t,{headers:i}),n?{...n,headers:i}:void 0]:[t,{...n,headers:i}]}async function nx(e){if(!Ab)return{kind:`failed`};let t=Bb(e),n=Vn(Nb);try{let[r,i]=tx(e,t.target.bootstrapPath,{cache:`no-store`,signal:n.signal},null),a=await Ab(r,i);return a.ok?Jb(e,await a.text())?{kind:`minted`,token:Bb(e).session.token}:{kind:`unavailable`}:a.status>=400&&a.status<500?{kind:`unavailable`}:{kind:`failed`}}catch{return{kind:`failed`}}finally{n.clear()}}async function rx(e,t){if(!Ab)return`unavailable`;try{let[n,r]=tx(e,`${Bb(e).target.baseUrl}${Cb}`,{cache:`no-store`},t),i=await Ab(n,r);return i.status===401?`rejected`:i.ok?`accepted`:`unavailable`}catch{return`unavailable`}}async function ix(e,t,n){let r=Bb(e);if(r.promptCancelled||n?.aborted)return null;if(!r.resolutionInFlight){let n=(async()=>{let n=r.session.token;if(n&&n!==t)return n;let i,a=await Promise.race([nx(e),new Promise(e=>{i=setTimeout(()=>e({kind:`failed`}),Pb)})]).finally(()=>clearTimeout(i));if(a.kind===`minted`)return a.token;if(a.kind===`failed`)return null;let o=await Mb(t=>rx(e,t));return o?(r.session={token:o,csrfToken:null,browserOrigin:null,serverOrigin:r.target.serverOrigin},o):(r.promptCancelled=!0,null)})().finally(()=>{r.resolutionInFlight===n&&(r.resolutionInFlight=null)});r.resolutionInFlight=n}if(!n)return r.resolutionInFlight;let i,a=new Promise(e=>{i=()=>e(null),n.addEventListener(`abort`,i,{once:!0})});return Promise.race([r.resolutionInFlight,a]).finally(()=>{i&&n.removeEventListener(`abort`,i)})}function ax(){if(kb)return;kb=!0,Yb(),Lb(),Kb();let e=window.fetch.bind(window);Ab=e,window.fetch=async(t,n)=>{let r=$b(t);if(!r)return e(t,n);let i=Bb(r.plane),a=i.session.token,[o,s]=tx(r.plane,t,n),c=await e(o,s);if(r.bootstrap||c.status!==401)return c;let l=i.session.token;if(l&&l!==a){let[i,a]=tx(r.plane,t,n),o=await e(i,a);if(o.status!==401)return o;Vb(r.plane,l)}else Vb(r.plane,a);let u=n?.signal??(t instanceof Request?t.signal:void 0),d=await ix(r.plane,a,u??void 0);if(!d)return c;let[f,p]=tx(r.plane,t,n,d),m=await e(f,p);return m.status===401&&Vb(r.plane,d),m}}var ox=/^ocx_pair_[A-Za-z0-9_-]{43}$/;async function sx(e,t,n){let r=t.trim();if(!ox.test(r))throw Error(`pairing_code_invalid`);let i=await(n??((e,t)=>window.fetch(e,t)))(e.bootstrapPath,{method:`POST`,headers:{"Content-Type":`application/json`,Accept:`text/html`},body:JSON.stringify({grant:r})});if(!i.ok)throw Error(`pairing_refused`);if(!Jb(`shared`,await i.text()))throw Error(`pairing_response_invalid`);return!0}function cx({target:e,onConnected:t}){let n=Q(),[r,i]=(0,_.useState)(``),[a,o]=(0,_.useState)(!1),[s,c]=(0,_.useState)(!1);return(0,_.createElement)(`section`,{className:`card connect-pairing`,"aria-labelledby":`connect-pairing-title`},(0,_.createElement)(`h2`,{id:`connect-pairing-title`},n(`connection.pairing.title`)),(0,_.createElement)(`p`,null,n(e.transport===`relay`?`connection.pairing.relayWarning`:`connection.pairing.body`)),(0,_.createElement)(`form`,{onSubmit:async n=>{if(n.preventDefault(),!a){o(!0),c(!1);try{await sx(e,r),t()}catch{c(!0)}finally{o(!1)}}},className:`api-form-row`},(0,_.createElement)(`label`,{htmlFor:`connect-pairing-code`,className:`field-label`},n(`connection.pairing.code`)),(0,_.createElement)(`input`,{id:`connect-pairing-code`,name:`pairingCode`,value:r,onChange:e=>i(e.currentTarget.value),autoComplete:`off`,spellCheck:!1,disabled:a,className:`input mono`,"aria-invalid":s||void 0,"aria-describedby":s?`connect-pairing-error`:void 0}),(0,_.createElement)(`button`,{type:`submit`,className:`btn btn-primary`,disabled:a||!r.trim()},n(a?`connection.pairing.submitting`:`connection.pairing.submit`)),s?(0,_.createElement)(`p`,{id:`connect-pairing-error`,className:`alert alert-err`,role:`alert`},n(`connection.pairing.error`)):null))}var lx=new Set([`dashboard`,`startup`,`providers`,`models`,`subagents`,`logs`,`usage`,`storage`,`codex-set`,`integrations`]);function ux(e){let t=dt(e??(typeof window<`u`?window.location.hash:``)).split(`/`)[0];return t===`debug`?`logs`:t===`codex-auth`?`codex-set`:t===`combos`||t===`routing`||t===`lab`?`models`:t===`api`||t===`claude`||t===`grok`?`integrations`:lx.has(t)?t:`dashboard`}var dx=[`dashboard/providers`,`dashboard/models`],fx=[`models/combos`,`models/routing`,`models/compatibility`],px=[`integrations/keys`,`integrations/codex`,`integrations/claude`,`integrations/claude/desktop`,`integrations/grok`,`integrations/cursor`,`integrations/opencode`,`integrations/pi`,`integrations/omp`,`integrations/hermes`,`integrations/openclaw`,`integrations/kimi`,`integrations/gajae`,`integrations/dsh`,`integrations/mcode`,`integrations/zcode`,`integrations/prime`,`integrations/aside`];function mx(e,t){return e===t||t===`logs`&&e===`logs/debug`||t===`codex-set`&&e===`codex-set/prompt`||t===`models`&&fx.includes(e)||t===`dashboard`&&(e===`dashboard/update`||dx.includes(e))||t===`integrations`&&px.includes(e)}function hx(e){let t=ux(e);return e===`debug`||e.startsWith(`debug/`)?{page:`logs`,replaceTo:`logs/debug`}:e===`codex-auth`||e.startsWith(`codex-auth/`)?{page:`codex-set`,replaceTo:`codex-set`}:e===`combos`||e.startsWith(`combos/`)?{page:`models`,replaceTo:`models/combos`}:e===`routing`||e.startsWith(`routing/`)?{page:`models`,replaceTo:`models/routing`}:e===`lab`||e.startsWith(`lab/`)?{page:`models`,replaceTo:`models/compatibility`}:e===`api`?{page:`integrations`,replaceTo:`integrations/keys`}:e===`claude`?{page:`integrations`,replaceTo:`integrations/claude`}:e===`grok`?{page:`integrations`,replaceTo:`integrations/grok`}:e===`providers/workspace`?{page:`providers`,replaceTo:`providers`}:mx(e,t)?{page:t,replaceTo:null}:{page:t,replaceTo:t}}var gx=[`ocx-global-view`,`ocx-view`,`ocx-providers-view`,`ocx-subagents-view`,`ocx-storage-view`,`ocx-codexauth-view`,`ocx-apikeys-view`,`ocx-claudecode-view`,`ocx-usage-view`,`ocx-logs-view`,`ocx-models-view`,`ocx-dashboard-view`];function _x(){try{for(let e of gx)localStorage.removeItem(e)}catch{}}function vx(){let[e,t]=(0,_.useState)(ux);(0,_.useEffect)(()=>{_x()},[]);let n=(0,_.useCallback)(e=>{let n=hx(e);n.replaceTo&&ft(n.replaceTo),t(n.page)},[]);return(0,_.useEffect)(()=>{let e=()=>{n(dt(window.location.hash))};return window.addEventListener(`hashchange`,e),window.addEventListener(`popstate`,e),()=>{window.removeEventListener(`hashchange`,e),window.removeEventListener(`popstate`,e)}},[n]),(0,_.useEffect)(()=>{let n=hx(dt(window.location.hash));n.replaceTo&&ft(n.replaceTo),n.page!==e&&t(n.page)},[e]),{page:e,setPageState:t,navigateToPage:(e,n)=>{pt(n?`${e}/${n}`:e),t(e)}}}var yx=15e3;function bx(e,t,n){return typeof e?.message==`string`&&e.message.trim()?e.message:typeof e?.error==`string`&&e.error.trim()?e.error:n(t)}function xx(e){return(e instanceof DOMException||e instanceof Error)&&e.name===`AbortError`}async function Sx(e,t={}){let{fetchFn:n=fetch,timeoutMs:r=yx,formatFailure:i=e=>`Failed to stop proxy (HTTP ${e}).`,mode:a=`standalone`}=t,o;try{o=await n(`${e}${a===`client`?`/api/machine/disconnect`:`/api/stop`}`,{method:`POST`,...a===`client`?{headers:{"Content-Type":`application/json`},body:`{}`}:{},signal:AbortSignal.timeout(r)})}catch(e){return xx(e),{accepted:!0}}let s=await o.json().catch(()=>null);return!o.ok||s?.success===!1?{accepted:!1,message:bx(s,o.status,i)}:{accepted:!0}}var Cx={dashboard:`nav.dashboard`,startup:`nav.startup`,providers:`nav.providers`,models:`nav.models`,subagents:`nav.subagents`,logs:`nav.logs`,usage:`nav.usage`,storage:`nav.storage`,"codex-set":`nav.codexSet`,integrations:`nav.integrations`},wx=``,Tx=Qg(wx);zb(Tx),ax();var Ex=`ocx-theme`,Dx=[{id:`dashboard`,tkey:`nav.dashboard`,Icon:te},{id:`codex-set`,tkey:`nav.codexSet`,Icon:Ee},{id:`providers`,tkey:`nav.providers`,Icon:ne},{id:`models`,tkey:`nav.models`,Icon:re},{id:`subagents`,tkey:`nav.subagents`,Icon:ie},{id:`logs`,tkey:`nav.logs`,Icon:ae},{id:`usage`,tkey:`nav.usage`,Icon:ce},{id:`storage`,tkey:`nav.storage`,Icon:le},{id:`integrations`,tkey:`nav.integrations`,Icon:Ne}],Ox={light:Ae,dark:je,system:Me},kx={light:`theme.light`,dark:`theme.dark`,system:`theme.system`};function Ax(e){if(!e||typeof e!=`object`||!(`version`in e))return null;let t=e.version;return typeof t==`string`&&t.length>0?t:null}function jx(){let e=localStorage.getItem(Ex);return e===`light`||e===`dark`?e:`system`}function Mx(){let{page:e,navigateToPage:t}=vx(),[n,r]=(0,_.useState)(Nf);(0,_.useEffect)(()=>{let e=()=>r(Nf());return window.addEventListener(`hashchange`,e),window.addEventListener(`popstate`,e),()=>{window.removeEventListener(`hashchange`,e),window.removeEventListener(`popstate`,e)}},[]);let[i,a]=(0,_.useState)(jx),{locale:o,setLocale:s}=ct(),c=Q(),[l,u]=(0,_.useState)(Tx),[d,f]=(0,_.useState)(()=>!qg()),[p,m]=(0,_.useState)(!1),[h,g]=(0,_.useState)(()=>Ub(`shared`)),[v,y]=(0,_.useState)(!1);(0,_.useEffect)(()=>{let e=new AbortController;return n_(wx,e.signal).then(async t=>{if(zb(t),u(t),t.connected&&!Ub(`shared`))try{let n=await fetch(t.shared.bootstrapPath,{cache:`no-store`,signal:AbortSignal.any([e.signal,AbortSignal.timeout(5e3)])});n.ok&&Jb(`shared`,await n.text())}catch{}e.signal.aborted||(g(Ub(`shared`)),m(!1),f(!0))}).catch(()=>{e.signal.aborted||(m(!0),f(!0))}),()=>e.abort()},[]);let b=t_(`machine`,l),x=t_(`shared`,l),[S,C]=(0,_.useState)(!1),w=(0,_.useRef)(null),T=(0,_.useRef)(null),E=(0,_.useRef)(!1);(0,_.useEffect)(()=>{let e=()=>C(!1);return window.addEventListener(`hashchange`,e),window.addEventListener(`popstate`,e),()=>{window.removeEventListener(`hashchange`,e),window.removeEventListener(`popstate`,e)}},[]),(0,_.useEffect)(()=>{let e=document.documentElement;i===`system`?(e.removeAttribute(`data-theme`),localStorage.removeItem(Ex)):(e.setAttribute(`data-theme`,i),localStorage.setItem(Ex,i))},[i]);let D=G(`app-healthz:${b}`,[b,d],async e=>{let t=await fetch(`${b}/healthz`,{signal:e});return t.ok?Ax(await t.json()):null},{pollMs:3e4,enabled:d}),O=()=>a(e=>e===`light`?`dark`:e===`dark`?`system`:`light`),k=Ox[i],A=D.data??`2.42.0`,[j,M]=(0,_.useState)(!1);(0,_.useEffect)(()=>{if(!S)return;let e=e=>{e.key===`Escape`&&C(!1)};window.addEventListener(`keydown`,e);let t=document.body.style.overflow;return document.body.style.overflow=`hidden`,()=>{window.removeEventListener(`keydown`,e),document.body.style.overflow=t}},[S]),(0,_.useEffect)(()=>{if(S){E.current=!0;let e=setTimeout(()=>T.current?.focus(),200);return()=>clearTimeout(e)}E.current&&(E.current=!1,w.current?.focus())},[S]),(0,_.useEffect)(()=>{let e=window.matchMedia(`(min-width: 761px)`),t=()=>{e.matches&&C(!1)};return e.addEventListener(`change`,t),()=>e.removeEventListener(`change`,t)},[]);let[N,P]=(0,_.useState)(0),{restarting:F,restart:I}=sl(x,{onSettled:()=>P(e=>e+1)}),L=async()=>{if(!confirm(c(l.connected?`connection.disconnectConfirm`:`dash.stopConfirm`)))return;M(!0);let e=await Sx(b,{formatFailure:e=>c(`dash.stopFailed`,{status:String(e)}),mode:l.connected?`client`:`standalone`});e.accepted||(M(!1),alert(e.message))},R=async()=>{if(v)return;y(!0);let e=await Wb(`shared`);y(!1),e?g(!1):alert(c(`connection.sessionLogoutFailed`))},z=(0,J.jsxs)(`div`,{className:`brand`,children:[(0,J.jsx)(`span`,{className:`brand-logo`,role:`img`,"aria-label":c(`app.logoAria`)}),(0,J.jsx)(`span`,{className:`name`,children:`opencodex`}),(0,J.jsxs)(`span`,{className:`ver`,children:[`v`,A]})]});return(0,J.jsxs)(`div`,{className:`app`,children:[(0,J.jsxs)(`header`,{className:`mobile-topbar`,inert:S,children:[(0,J.jsx)(`button`,{ref:w,type:`button`,className:`menu-toggle`,onClick:()=>C(e=>!e),"aria-expanded":S,"aria-controls":`app-sidebar`,"aria-label":c(S?`nav.closeMenu`:`nav.openMenu`),title:c(S?`nav.closeMenu`:`nav.openMenu`),children:(0,J.jsx)(oe,{})}),z,(0,J.jsxs)(`div`,{className:`mobile-topbar-actions`,children:[l.connected&&h&&(0,J.jsx)(`button`,{type:`button`,className:`sidebar-orb`,onClick:()=>{R()},disabled:v,"aria-label":c(v?`connection.sessionLoggingOut`:`connection.sessionLogout`),title:c(`connection.sessionLogout`),children:(0,J.jsx)(de,{})}),(0,J.jsx)(`button`,{type:`button`,className:`sidebar-orb sidebar-orb--danger`,onClick:L,disabled:j,"aria-label":c(l.connected?`connection.disconnect`:`dash.stop`),title:c(l.connected?`connection.disconnect`:`dash.stop`),children:(0,J.jsx)(we,{})}),(0,J.jsx)(`button`,{type:`button`,className:`sidebar-orb`,onClick:()=>{I()},disabled:F,"aria-label":c(`dash.codexRestart`),title:c(`dash.codexRestart`),children:(0,J.jsx)(pe,{})})]})]}),S&&(0,J.jsx)(`div`,{className:`drawer-scrim`,onClick:()=>C(!1),"aria-hidden":`true`}),(0,J.jsxs)(`aside`,{id:`app-sidebar`,className:`sidebar${S?` open`:``}`,ref:T,tabIndex:-1,children:[(0,J.jsxs)(`div`,{className:`drawer-head`,children:[z,(0,J.jsx)(`button`,{type:`button`,className:`menu-toggle drawer-close`,onClick:()=>C(!1),"aria-label":c(`nav.closeMenu`),title:c(`nav.closeMenu`),children:(0,J.jsx)(de,{})})]}),(0,J.jsx)(`nav`,{children:Dx.map(n=>{let{id:r,tkey:i,Icon:a}=n,o=r===e;return(0,J.jsx)(`div`,{className:`nav-entry`,children:(0,J.jsxs)(`button`,{type:`button`,className:`nav-item${o?` active`:``}`,"data-page":r,onClick:()=>{t(r),C(!1)},"aria-current":o?`page`:void 0,children:[(0,J.jsx)(a,{}),` `,c(i)]})},r)})}),(0,J.jsxs)(`div`,{className:`sidebar-foot`,children:[(0,J.jsxs)(`div`,{className:`lang-toggle`,children:[(0,J.jsx)(Ne,{"aria-hidden":!0}),(0,J.jsx)(Dt,{value:o,options:et.map(e=>({value:e.code,label:Qe(e.code)})),onChange:e=>s(e),label:c(`lang.label`),placement:`right`,portal:!1,style:{flex:1,minWidth:0,width:`100%`}})]}),(0,J.jsxs)(`button`,{type:`button`,className:`theme-toggle`,onClick:O,"aria-label":`${c(`theme.label`)}: ${c(kx[i])}`,title:`${c(`theme.label`)}: ${c(kx[i])}`,children:[(0,J.jsx)(k,{}),` `,(0,J.jsx)(`span`,{className:`mode`,children:c(kx[i])})]}),(0,J.jsxs)(`div`,{className:`sidebar-action-row`,children:[(0,J.jsx)(`span`,{className:`sidebar-action-label`,children:c(`dash.actions`)}),(0,J.jsxs)(`div`,{className:`sidebar-action-orbs`,children:[l.connected&&h&&(0,J.jsx)(`button`,{type:`button`,className:`sidebar-orb`,onClick:()=>{R()},disabled:v,"aria-label":c(v?`connection.sessionLoggingOut`:`connection.sessionLogout`),title:c(`connection.sessionLogout`),children:(0,J.jsx)(de,{})}),(0,J.jsx)(`button`,{type:`button`,className:`sidebar-orb sidebar-orb--danger`,onClick:L,disabled:j,"aria-label":c(j?`dash.stopping`:l.connected?`connection.disconnect`:`dash.stop`),title:c(j?`dash.stopping`:l.connected?`connection.disconnect`:`dash.stop`),children:(0,J.jsx)(we,{})}),(0,J.jsx)(`button`,{type:`button`,className:`sidebar-orb`,onClick:()=>{I()},disabled:F,"aria-label":c(F?`dash.codexRestarting`:`dash.codexRestart`),title:c(F?`dash.codexRestarting`:`dash.codexRestart`),children:(0,J.jsx)(pe,{})})]})]}),(0,J.jsx)(vb,{apiBase:x,onOpenUpdate:()=>{C(!1),t(`dashboard`,`update`)}})]})]}),(0,J.jsx)(`main`,{className:`main`,inert:S,children:(0,J.jsx)(`div`,{className:`main-inner${e===`models`&&n===`combos`?` main-inner--combos`:``}`,children:(0,J.jsx)(vl,{pageName:c(Cx[e]),title:c(`errorBoundary.title`),message:c(`errorBoundary.message`),detailsLabel:c(`errorBoundary.details`),reloadLabel:c(`errorBoundary.reload`),children:d?(0,J.jsxs)(J.Fragment,{children:[p&&(0,J.jsx)(`div`,{className:`alert alert-err`,role:`alert`,children:c(`connection.machineUnavailable`)}),l.connected&&!h&&(0,J.jsx)(cx,{target:l.shared,onConnected:()=>g(!0)}),e===`dashboard`&&(0,J.jsx)(Xr,{apiBase:x}),e===`startup`&&(0,J.jsx)(pb,{apiBase:x,machineApiBase:b,connected:l.connected}),e===`providers`&&(0,J.jsx)(Jc,{apiBase:x}),e===`models`&&(0,J.jsx)(mp,{apiBase:x,restartEpoch:N},x),e===`subagents`&&(0,J.jsx)(Ep,{apiBase:x},x),e===`logs`&&(0,J.jsx)(th,{apiBase:x}),e===`usage`&&(0,J.jsx)(Ch,{apiBase:x,connected:l.connected,apiKeyId:l.apiKeyId}),e===`storage`&&(0,J.jsx)(Qh,{apiBase:x}),e===`codex-set`&&(0,J.jsx)(Fg,{apiBase:x}),e===`integrations`&&(0,J.jsx)(eb,{apiBase:x,machineApiBase:b,connected:l.connected})]}):(0,J.jsx)(`div`,{className:`alert`,children:c(`connection.discovering`)})},e)})})]})}g.createRoot(document.getElementById(`root`)).render((0,J.jsx)(_.StrictMode,{children:(0,J.jsx)(lt,{children:(0,J.jsx)(Mx,{})})})); \ No newline at end of file diff --git a/go/internal/embeddedui/static/assets/index-DL9-iS6J.css b/go/internal/embeddedui/static/assets/index-DL9-iS6J.css new file mode 100644 index 0000000000..97242350bf --- /dev/null +++ b/go/internal/embeddedui/static/assets/index-DL9-iS6J.css @@ -0,0 +1 @@ +.provider-catalog{flex-direction:column;gap:10px;display:flex}.provider-catalog-tabs{border-bottom:1px solid var(--border);gap:4px;display:flex}.provider-catalog-tab{appearance:none;color:var(--muted);font:inherit;cursor:pointer;background:0 0;border:none;border-bottom:2px solid #0000;padding:8px 12px}.provider-catalog-tab.active{color:var(--fg);border-bottom-color:var(--accent)}.provider-catalog-accounts-hint{padding:2px 2px 0}.provider-catalog-search{width:100%}.provider-catalog-rows{flex-direction:column;gap:6px;max-height:360px;display:flex;overflow-y:auto}.provider-catalog-badges{flex-shrink:0;align-items:center;gap:4px;display:flex}.provider-catalog-empty{padding:8px}.provider-catalog-account-row .sub{text-overflow:ellipsis;overflow:hidden}.provider-catalog-rows .provider-icon{flex:none}.provider-catalog-rows .provider-icon+div{flex:auto;min-width:0}.provider-catalog-account-row-head{justify-content:space-between;align-items:center;gap:10px;width:100%;display:flex}.list-row.provider-catalog-account-row--waiting{cursor:default;flex-direction:column;align-items:stretch;gap:10px}.provider-catalog-footer{align-items:center;gap:8px;display:flex}.bar-warn{background:var(--amber)}.quota-row--warn .quota-label{color:var(--amber)}.quota-val--warn{color:var(--amber);align-items:center;gap:3px;display:inline-flex}.quota-stacked,.quota-stacked--pending{flex-direction:column;gap:10px;min-height:64px;display:flex}.quota-stacked-row--skeleton .quota-stacked-bar-row{min-height:18px}.quota-stacked-row{flex-direction:column;gap:4px;display:flex}.quota-stacked-head{justify-content:space-between;align-items:baseline;gap:8px;display:flex}.quota-stacked-limit{font-weight:600}.quota-stacked-limit-group{overflow-wrap:anywhere;flex-wrap:wrap;flex:auto;align-items:baseline;gap:4px;min-width:0;display:inline-flex}.quota-window-partial{border:1px solid color-mix(in srgb, var(--amber) 45%, transparent);color:var(--amber);white-space:nowrap;border-radius:999px;flex:none;padding:1px 5px;font-size:10px;font-weight:600;line-height:1.3}.quota-stacked-reset{overflow-wrap:anywhere;min-width:0;font-size:12px}.quota-stacked-bar-row{align-items:center;gap:8px;display:flex}.quota-stacked-bar{flex:1}.quota-stacked-used{font-variant-numeric:tabular-nums;white-space:nowrap;font-size:12px}.quota-stacked-used--warn{color:var(--amber)}.quota-stacked-limit-reached{color:var(--amber);align-items:center;gap:4px;font-size:12px;display:inline-flex}.main-inner:has(.pws-shell-container){max-width:1440px}.pws-shell-container{width:100%;min-width:0;container:provider-workspace/inline-size}.pws-root{gap:var(--space-4);grid-template-columns:minmax(240px,280px) minmax(0,1fr);width:100%;max-width:100%;min-height:480px;display:grid}.pws-rail{gap:var(--space-3);border-right:1px solid var(--border);padding-right:var(--space-3);flex-direction:column;min-width:0;display:flex}.pws-search-row{align-items:center;gap:6px;display:flex}.pws-search-wrap{flex:1;min-width:0;position:relative}.pws-search-icon{color:var(--muted);pointer-events:none;position:absolute;top:50%;left:8px;transform:translateY(-50%)}.pws-search-wrap .pws-search-input{width:100%;padding-left:32px}.pws-filter-wrap{position:relative}.pws-filter-btn{appearance:none;border:1px solid var(--border);border-radius:var(--radius-sm);cursor:pointer;color:var(--muted);min-width:var(--control-md);min-height:var(--control-md);background:0 0;padding:6px;position:relative}.pws-filter-btn--active{color:var(--text);border-color:var(--text)}.pws-filter-dot{background:var(--accent);border-radius:50%;width:6px;height:6px;position:absolute;top:3px;right:3px}.pws-filter-menu{z-index:30;background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-sm);flex-direction:column;gap:4px;min-width:230px;padding:10px;display:flex;position:absolute;top:calc(100% + 6px);right:0;box-shadow:0 8px 24px #0000001f}.pws-filter-title{margin-bottom:2px;font-weight:600}.pws-filter-head{text-transform:uppercase;letter-spacing:.04em;color:var(--muted);margin-top:8px;font-size:11px}.pws-filter-option{cursor:pointer;align-items:center;gap:8px;padding:3px 2px;display:flex}.pws-filter-label{flex:1}.pws-filter-count{color:var(--muted);font-variant-numeric:tabular-nums;font-size:12px}.pws-sort-grid{grid-template-columns:1fr 1fr;gap:4px;display:grid}.pws-sort-btn{appearance:none;border:1px solid var(--border);border-radius:var(--radius-sm);cursor:pointer;color:var(--muted);background:0 0;padding:4px 8px;font-size:12px}.pws-sort-btn--active{color:var(--text);border-color:var(--text)}.pws-filter-footer{justify-content:flex-end;margin-top:8px;display:flex}.pws-rail-list{flex-direction:column;gap:4px;min-height:0;display:flex;overflow-y:auto}.pws-rail-empty{padding:8px 4px;font-size:12px}.pws-rail-group{flex-direction:column;gap:2px;display:flex}.pws-rail-group-head{justify-content:space-between;align-items:center;gap:var(--space-2);font-size:var(--text-caption);font-weight:var(--weight-medium);color:var(--muted);padding:var(--space-2) var(--space-2) var(--space-1);display:flex}.pws-rail-group-label{text-overflow:ellipsis;white-space:nowrap;min-width:0;overflow:hidden}.pws-rail-group-count{font-variant-numeric:tabular-nums;flex-shrink:0}.providers-workspace-rail-row{grid-template-columns:var(--icon-lg) minmax(0, 1fr) max-content;align-items:center;gap:var(--space-2);width:100%;min-height:var(--pws-rail-row-height,46px);appearance:none;border-radius:var(--radius-sm);padding:var(--space-1-5) var(--space-2);cursor:pointer;text-align:left;color:inherit;font:inherit;transition:background var(--motion-fast), box-shadow var(--motion-fast);background:0 0;border:none;display:grid;overflow:hidden}.providers-workspace-rail-row:hover{background:var(--surface)}.providers-workspace-rail-row--selected{background:var(--accent-soft);box-shadow:inset 0 0 0 1px var(--border)}.pws-rail-row-wrap{position:relative}.pws-rail-row-remove{right:var(--space-2);appearance:none;border-radius:var(--radius-xs);background:var(--surface);width:24px;height:24px;color:var(--red);cursor:pointer;opacity:0;pointer-events:none;transition:opacity var(--motion-fast), background var(--motion-fast);border:none;justify-content:center;align-items:center;padding:0;display:inline-flex;position:absolute;top:50%;transform:translateY(-50%)}.pws-rail-row-wrap:hover .pws-rail-row-remove,.pws-rail-row-wrap:focus-within .pws-rail-row-remove{opacity:1;pointer-events:auto}.pws-rail-row-wrap:hover .providers-workspace-rail-row--selected~.pws-rail-row-remove{background:var(--raised)}.pws-rail-row-wrap:hover .providers-workspace-rail-trail{opacity:0}.providers-workspace-rail-trail{transition:opacity var(--motion-fast)}.pws-rail-row-remove:hover{background:var(--raised)}@media (hover:none){.pws-rail-row-remove{display:none}}.providers-workspace-rail-icon{width:var(--icon-lg);height:var(--icon-lg);flex-shrink:0;justify-content:center;align-items:center;display:inline-flex}.providers-workspace-rail-icon img,.providers-workspace-rail-icon .provider-icon-mask{width:var(--icon-lg);height:var(--icon-lg);display:block}.provider-icon-fallback{border-radius:var(--radius-xs);width:100%;height:100%;font-weight:var(--weight-semibold);-webkit-user-select:none;user-select:none;justify-content:center;align-items:center;font-size:13px;line-height:1;display:inline-flex}.provider-icon-mask{-webkit-mask-position:50%;mask-position:50%;-webkit-mask-size:contain;mask-size:contain;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}.providers-workspace-rail-copy{gap:var(--space-0-5);flex-direction:column;min-width:0;display:flex;overflow:hidden}.providers-workspace-rail-primary{align-items:center;gap:var(--space-1-5);min-width:0;display:flex}.providers-workspace-rail-name-label{white-space:nowrap;text-overflow:ellipsis;min-width:0;font-size:var(--text-body);font-weight:var(--weight-medium);line-height:var(--leading-ui);overflow:hidden}.providers-workspace-rail-secondary{white-space:nowrap;text-overflow:ellipsis;min-width:0;min-height:1.15em;font-size:var(--text-caption);line-height:var(--leading-ui);color:var(--muted);overflow:hidden}.pwi-rail-badge{border:1px solid var(--border);color:var(--muted);border-radius:999px;padding:1px 6px;font-size:10px}.pwi-rail-badge--free{color:var(--green);border-color:var(--green)}.pwi-rail-badge--local{color:var(--amber);border-color:var(--amber)}.providers-workspace-rail-trail{flex-shrink:0;align-items:center;gap:4px;display:inline-flex}.pwi-default-star{color:var(--amber);display:inline-flex}.providers-workspace-rail-status{border-radius:50%;width:8px;height:8px;display:inline-block}.providers-workspace-rail-status--active{background:var(--green)}.providers-workspace-rail-status--warning{background:var(--amber)}.providers-workspace-rail-status--inactive{background:var(--muted)}.pws-main{min-width:0;max-width:100%;padding:0 var(--space-2);overflow:hidden}.pws-detail-placeholder{flex-direction:column;align-items:flex-start;gap:8px;padding:24px 8px;display:flex}.pws-empty-root{justify-content:center;padding:48px 16px;display:flex}.pws-empty-hero{text-align:center;flex-direction:column;align-items:center;gap:16px;max-width:640px;display:flex}.pws-empty-tiles{grid-template-columns:repeat(3,minmax(0,1fr));gap:10px;width:100%;display:grid}.pws-empty-tile{appearance:none;border:1px solid var(--border);border-radius:var(--radius-sm);cursor:pointer;color:inherit;font:inherit;background:0 0;flex-direction:column;align-items:center;gap:6px;padding:14px 12px;display:flex}.pws-empty-tile:hover{background:var(--surface)}.pws-empty-tile-label{font-weight:600}.pws-empty-tile-desc{font-size:12px}@container provider-workspace (width<=920px){.pws-root{gap:var(--space-3);grid-template-columns:240px minmax(0,1fr)}}@container provider-workspace (width<=680px){.pws-root{grid-template-columns:minmax(0,1fr);min-height:auto}.pws-rail{--pws-rail-row-height:var(--control-touch);border-right:none;border-bottom:1px solid var(--border);padding-right:0;padding-bottom:var(--space-3);max-height:320px;overflow-y:auto}.pws-main{padding:var(--space-2) 0 0}}@media (width<=768px){.pws-root{grid-template-columns:1fr;min-height:auto}.pws-rail{border-right:none;border-bottom:1px solid var(--border);max-height:320px;padding-bottom:12px;padding-right:0;overflow-y:auto}.pws-main{padding-top:8px}.pws-empty-tiles{grid-template-columns:1fr}}@media (width<=360px){.main-inner:has(.pws-shell-container)>.page-head{flex-wrap:wrap;align-items:flex-start}.main-inner:has(.pws-shell-container)>.page-head .row{flex-wrap:wrap;justify-content:flex-start;width:100%}}.pws-detail-back-link{color:var(--accent,#3b82f6);font:inherit;cursor:pointer;background:0 0;border:none;align-items:center;gap:4px;margin-bottom:8px;padding:0;font-size:.82rem;display:inline-flex}.pws-detail-back-link:hover{text-decoration:underline}.pws-detail-back-chevron{width:12px;height:12px;transform:rotate(180deg)}.pws-detail-head-main{align-items:center;gap:10px;margin-bottom:12px;display:flex}.pws-detail-head-main .pws-detail-title{align-items:center;gap:8px;display:flex}.pws-detail-actions{align-items:center;gap:8px;margin-left:auto;display:flex}.pws-detail-toggle{align-items:center;gap:6px;display:flex}.pws-detail-toggle-label{color:var(--muted);font-size:.82rem}.btn-icon-only{padding:4px 6px}.pws-overview-layout{grid-template-columns:1fr 280px;align-items:start;gap:24px;display:grid}.pws-overview-main{flex-direction:column;gap:20px;min-width:0;display:flex}.pws-overview-sidebar{flex-direction:column;align-self:start;gap:20px;display:flex}@container provider-workspace (width<=920px){.pws-overview-layout{grid-template-columns:minmax(0,1fr)}}.pws-auth-summary{align-items:center;gap:8px;font-size:.88rem;display:flex}.pws-auth-summary--warn{border:1px solid color-mix(in srgb, var(--yellow,#eab308) 45%, transparent);background:color-mix(in srgb, var(--yellow,#eab308) 12%, transparent);color:inherit;border-radius:8px;align-items:flex-start;padding:10px 12px}.pws-auth-summary-body{flex-wrap:wrap;flex:1;align-items:center;gap:8px 10px;min-width:0;display:flex}.pws-auth-summary--warn svg{color:var(--yellow,#eab308);flex-shrink:0;margin-top:2px}.pws-auth-dot{background:var(--green,#22c55e);border-radius:50%;flex-shrink:0;width:8px;height:8px}.pws-notes-section{min-width:0}.pws-notes-display{text-align:left;width:100%;font:inherit;color:inherit;cursor:pointer;background:0 0;border:1px solid #0000;border-radius:6px;min-height:62px;padding:8px 10px;font-size:.85rem;transition:border-color .15s;display:block}.pws-notes-display:hover{border-color:var(--border)}.pws-notes-textarea{border:1px solid var(--accent,#3b82f6);width:100%;font:inherit;color:inherit;background:var(--bg);resize:vertical;border-radius:6px;min-height:62px;padding:8px 10px;font-size:.85rem}.pws-notes-textarea:focus{outline:none;box-shadow:0 0 0 2px #3b82f640}@media (width<=768px){.pws-overview-layout{grid-template-columns:1fr}.pws-overview-sidebar{position:static}}.pws-detail{flex-direction:column;gap:0;width:100%;min-width:0;max-width:960px;display:flex}.pws-detail-icon{flex-shrink:0;justify-content:center;align-items:center;width:32px;height:32px;display:flex}.pws-detail-icon img,.pws-detail-icon .provider-icon-mask{width:28px;height:28px}.pws-detail-title-wrap{flex:1;min-width:0}.pws-detail-title{margin:0;font-size:1.15rem;font-weight:600;line-height:1.3}.pws-detail-tabs{scrollbar-width:thin;border-bottom:1px solid var(--border);gap:0;margin-top:8px;margin-bottom:16px;display:flex;overflow-x:auto}.pws-detail-tab{appearance:none;font:inherit;color:var(--muted);cursor:pointer;background:0 0;border:none;border-bottom:2px solid #0000;padding:8px 14px;font-size:.82rem;font-weight:500;transition:color .15s,border-color .15s}.pws-detail-tab:hover{color:var(--text)}.pws-detail-tab--active{color:var(--text);border-bottom-color:var(--text)}.pws-detail-tab:focus-visible{outline:2px solid var(--accent-ring);outline-offset:-2px}.pws-detail-panel:focus{outline:none}.pws-section{margin-bottom:16px}.pws-section-title{text-transform:uppercase;letter-spacing:.04em;color:var(--muted);margin:0 0 8px;font-size:.7rem;font-weight:600}.pws-section--side{margin-bottom:12px}.pws-kv{flex-direction:column;gap:0;margin:0;display:flex}.pws-kv-row{gap:12px;padding:5px 0;font-size:.84rem;line-height:1.4;display:flex}.pws-kv-row dt{width:130px;color:var(--muted);flex-shrink:0;font-weight:400}.pws-kv-row dd{word-break:break-word;flex:1;min-width:0;margin:0}.pws-kv-row dd code{font-size:.8rem}.pws-kv-mono{font-variant-numeric:tabular-nums}.pws-status-ok{color:var(--green,#22c55e);align-items:center;gap:4px;display:flex}.pws-status-warn{color:var(--amber,#f59e0b);align-items:center;gap:4px;display:flex}.pws-stats-note{margin-top:6px;font-size:.75rem}.pws-edit-settings-link,.pws-view-usage-link{margin-top:8px;font-size:.8rem}.pws-section-head{justify-content:space-between;align-items:baseline;gap:12px;margin-bottom:12px;display:flex}.pws-section-head .pws-section-title{margin-bottom:0}.pws-model-search{width:100%;margin-bottom:18px}.pws-custom-model-label{margin-bottom:6px;display:block}.pws-custom-model-row{gap:8px;margin-bottom:12px}.pws-custom-model-row .input{flex:1;min-width:0}.pws-model-list{flex-wrap:wrap;align-items:center;gap:12px;margin:4px 0 0;padding:0;list-style:none;display:flex}.pws-model-expand{color:inherit;font:inherit;cursor:pointer;text-align:left;background:0 0;border:0;margin:0;padding:0;display:inline}.pws-model-chip{border:1px solid color-mix(in oklab, var(--border) 85%, transparent);background:color-mix(in oklab, var(--surface,var(--panel)) 92%, var(--text) 4%);border-radius:8px;align-items:center;gap:10px;max-width:100%;padding:8px 10px 8px 12px;display:inline-flex}.pws-model-chip-main{min-width:0;color:inherit;font:inherit;cursor:pointer;text-align:left;background:0 0;border:none;align-items:center;margin:0;padding:0;display:inline-flex}.pws-model-chip-main:hover .pws-model-id{color:var(--accent,var(--text))}.pws-model-chip-main:focus-visible{outline:2px solid var(--accent-ring);outline-offset:2px;border-radius:4px}.pws-model-id{font-family:var(--mono,ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);color:var(--text);white-space:nowrap;font-size:.8rem;font-weight:600;line-height:1.35}.pws-model-flag{flex-shrink:0}.pws-inline-error{align-items:center;gap:10px;margin-top:8px;display:flex}.pws-usage-block+.pws-usage-block{border-top:1px solid color-mix(in oklab, var(--border) 45%, transparent);margin-top:32px;padding-top:24px}.pws-usage-metrics{grid-template-columns:repeat(2,minmax(0,1fr));gap:20px 28px;margin-top:14px;display:grid}.pws-usage-metric{flex-direction:column;gap:8px;min-width:0;display:flex}.pws-usage-metric-value{font-variant-numeric:tabular-nums;letter-spacing:-.02em;color:var(--text);font-size:1.35rem;font-weight:650;line-height:1.15}.pws-usage-metric-label{font-size:.8rem;line-height:1.3}.pws-usage-meta{margin-top:16px}.pws-usage-metrics-3{grid-template-columns:repeat(3,minmax(0,1fr))}.pws-cost-disclaimer{margin-top:8px;font-size:.75rem}.pws-model-table{border-collapse:collapse;width:100%;font-size:.8rem}.pws-model-table th{text-align:left;border-bottom:1px solid var(--border);padding:6px 8px;font-weight:600}.pws-model-table td{border-bottom:1px solid var(--border-faint,#8080801a);padding:6px 8px}.pws-model-table .num{text-align:right;font-variant-numeric:tabular-nums}.pws-model-table .mono{font-family:var(--font-mono,monospace);font-size:.78rem}.pws-model-row:hover{background:var(--bg-subtle,#8080800d)}.pws-share-bar{background:var(--border-faint,#80808026);border-radius:3px;min-width:60px;height:6px}.pws-share-bar-fill{background:var(--green,#22c55e);border-radius:3px;height:100%}.pws-model-detail{background:var(--bg-subtle,#8080800d)}.pws-model-detail td{padding:8px 8px 8px 24px}.pws-model-detail-grid{grid-template-columns:repeat(2,1fr);gap:8px 24px;font-size:.8rem;display:grid}@media (width<=600px){.pws-usage-metrics-3{grid-template-columns:1fr}}@media (width<=640px){.pws-usage-metrics{grid-template-columns:1fr;gap:16px}}.pwi-auth-section{flex-direction:column;gap:12px;display:flex}.pwi-auth-body{flex-direction:column;gap:10px;display:flex}.pwi-auth-status-row{align-items:center;gap:8px;display:flex}.pwi-auth-dot{border-radius:var(--radius-round);background:var(--muted);flex-shrink:0;width:8px;height:8px}.pwi-auth-dot--ok{background:var(--green)}.pwi-auth-dot--off{background:var(--muted)}.pwi-auth-dot--warn{background:var(--amber)}.pwi-auth-status-text{font-size:var(--text-control);color:var(--text)}.pwi-auth-state{min-height:36px;font-size:var(--text-label);color:var(--muted);background:var(--raised);border-radius:var(--radius-xs);align-items:center;gap:8px;padding:8px 10px;display:flex}.pwi-auth-state--error{color:var(--red);background:var(--red-soft);justify-content:space-between}.pwi-auth-state--empty{justify-content:center}.pwi-auth-actions{flex-wrap:wrap;gap:8px;display:flex}.pwi-auth-optin-row{border:1px solid var(--border-soft);border-radius:var(--radius-xs);background:var(--raised);justify-content:space-between;align-items:center;gap:16px;padding:10px 12px;display:flex}.pwi-auth-optin-copy{flex-direction:column;gap:3px;min-width:0;display:flex}.pwi-auth-optin-label{font-size:var(--text-control);color:var(--text);font-weight:600}.pwi-auth-optin-mixed{color:var(--amber);font-weight:600}.pwi-auth-optin-error{color:var(--red);font-size:var(--text-caption)}.pwi-auth-wait{flex-direction:column;gap:6px;display:flex}.pwi-auth-wait-title{font-size:var(--text-control);color:var(--text);font-weight:600}.pwi-auth-wait-copy{font-size:var(--text-label);color:var(--muted);line-height:1.45}.pwi-device-code-wrap{border:1px solid var(--border);background:var(--surface);border-radius:10px;flex-wrap:wrap;align-items:center;gap:10px;margin:6px 0;padding:12px;display:flex}.pwi-device-code{letter-spacing:.14em;color:var(--text);-webkit-user-select:all;user-select:all;font-size:20px;font-weight:800}.pwi-auth-row-copy{flex-direction:column;align-items:flex-start;gap:2px;min-width:0;display:flex}.pwi-auth-row-secondary{text-overflow:ellipsis;max-width:100%;font-size:var(--text-label);color:var(--muted);overflow:hidden}.pwi-auth-list{flex-direction:column;gap:4px;margin:0;padding:0;list-style:none;display:flex}.pwi-auth-row{border-radius:var(--radius-xs);transition:background var(--motion-fast);align-items:center;gap:8px;padding:6px 8px;display:flex}.pwi-auth-row:hover{background:var(--hover)}.pwi-auth-row--active{background:var(--accent-soft)}.pwi-auth-acct{border-radius:var(--radius-xs);flex-direction:column;gap:2px;display:flex}.pwi-auth-acct--active{background:var(--accent-soft)}.pwi-auth-acct--active .pwi-auth-row--active{background:0 0}.pwi-auth-acct-quota{padding:0 8px 8px 26px}.pwi-auth-acct-quota-stale{margin:4px 0 0;font-size:.85em}.quota-observed{margin:0 0 4px;font-size:.85em}.pwi-auth-row-main{appearance:none;min-width:0;color:inherit;text-align:left;cursor:pointer;border-radius:var(--radius-2xs);background:0 0;border:0;flex:1;align-items:center;gap:8px;padding:2px;display:flex}.pwi-auth-row-main:disabled{cursor:default;opacity:.72}.pwi-auth-row-main:focus-visible,.pwi-auth-row-remove:focus-visible{outline:2px solid var(--accent-ring);outline-offset:2px}.pwi-auth-row-label{font-size:var(--text-control);color:var(--text);text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.pwi-auth-row-remove{cursor:pointer;color:var(--muted);border-radius:var(--radius-2xs);transition:color var(--motion-fast), background var(--motion-fast);background:0 0;border:none;padding:2px}.pwi-auth-row-remove:hover{color:var(--red);background:var(--red-soft)}.pwi-auth-add-key{font-size:var(--text-label);color:var(--accent);cursor:pointer;text-align:left;background:0 0;border:none;padding:4px 0}.pwi-auth-add-key:hover{text-decoration:underline}@keyframes pwi-spin{to{transform:rotate(360deg)}}.pwi-spin-inline{border:2px solid var(--border);border-top-color:var(--accent);border-radius:var(--radius-round);width:14px;height:14px;animation:.7s linear infinite pwi-spin;display:inline-block}.pwi-settings-form{flex-direction:column;gap:14px;display:flex;position:relative}.pwi-settings-field{flex-direction:column;gap:4px;display:flex}.pwi-settings-label{font-size:var(--text-label);color:var(--muted);letter-spacing:.01em;font-weight:500}.pwi-settings-textarea{width:100%;min-height:60px;font:inherit;font-size:var(--text-control);color:var(--text);background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-xs);resize:vertical;transition:border-color var(--motion-fast);padding:8px 10px}.pwi-settings-textarea:focus{border-color:var(--accent-ring);outline:none}.pwi-settings-hint{font-size:var(--text-caption);color:var(--muted);line-height:1.4}.pwi-settings-mode-msg{font-size:var(--text-caption);line-height:1.4}.pwi-settings-mode-msg--ok{color:var(--green)}.pwi-settings-mode-msg--err{color:var(--red)}.pwi-settings-sticky-bar{z-index:2;background:var(--bg);border-top:1px solid var(--border-soft);align-items:center;gap:8px;padding:10px 0;display:flex;position:sticky;bottom:0}.pwi-settings-sticky-bar-actions{gap:8px;margin-left:auto;display:flex}.pwi-settings-msg{font-size:var(--text-label);border-radius:var(--radius-xs);padding:6px 10px;line-height:1.4}.pwi-settings-msg--ok{background:var(--green-soft);color:var(--green)}.pwi-settings-msg--err{background:var(--red-soft);color:var(--red)}.pwi-settings-dirty{font-size:var(--text-caption);color:var(--amber)}.pwi-pacing-card{border:1px solid var(--border);border-radius:var(--radius-sm);background:color-mix(in srgb, var(--surface) 88%, var(--accent) 12%);flex-direction:column;gap:12px;padding:14px;display:flex}.pwi-pacing-head{justify-content:space-between;align-items:flex-start;gap:14px;display:flex}.pwi-pacing-head h3,.pwi-pacing-card h4{color:var(--text);font-size:var(--text-control);margin:0}.pwi-pacing-head p{color:var(--muted);font-size:var(--text-caption);margin:4px 0 0;line-height:1.45}.pwi-pacing-toggle{white-space:nowrap;font-size:var(--text-label);align-items:center;gap:6px;display:flex}.pwi-pacing-grid{grid-template-columns:repeat(2,minmax(0,1fr));align-items:end;gap:10px;display:grid}.pwi-pacing-grid--model{grid-template-columns:minmax(160px,2fr) repeat(2,minmax(110px,1fr)) auto}.pwi-pacing-status{grid-template-columns:repeat(3,minmax(0,1fr));gap:8px;display:grid}.pwi-pacing-status span{border:1px solid var(--border-soft);border-radius:var(--radius-xs);color:var(--muted);font-size:var(--text-caption);padding:8px 10px}.pwi-pacing-status strong{color:var(--text);font-size:var(--text-label);text-overflow:ellipsis;white-space:nowrap;display:block;overflow:hidden}.pwi-pacing-overrides{flex-direction:column;gap:6px;display:flex}.pwi-pacing-row{border-radius:var(--radius-xs);background:var(--bg);grid-template-columns:minmax(0,1fr) auto auto;align-items:center;gap:10px;padding:7px 8px;display:grid}.pwi-pacing-row code{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.pwi-pacing-row span{color:var(--muted);font-size:var(--text-caption)}@media (width<=760px){.pwi-pacing-head{flex-direction:column}.pwi-pacing-grid,.pwi-pacing-grid--model,.pwi-pacing-status{grid-template-columns:1fr}.pwi-pacing-grid--model .btn{width:100%}.pwi-pacing-row{grid-template-columns:minmax(0,1fr) auto}.pwi-pacing-row span{grid-area:2/1/auto/-1}}.pwi-json-panel{flex-direction:column;gap:8px;display:flex}.pwi-json-panel-header{justify-content:space-between;align-items:center;gap:8px;display:flex}.pwi-json-panel-title{font-size:var(--text-control);color:var(--text);font-weight:600}.pwi-json-panel-actions{gap:6px;display:flex}.pwi-json-panel-desc{font-size:var(--text-caption);color:var(--muted);line-height:1.4}.pwi-json-textarea{width:100%;min-height:180px;font-family:ui-monospace,SF Mono,Cascadia Code,Segoe UI Mono,Menlo,monospace;font-size:var(--text-label);tab-size:2;color:var(--text);background:var(--raised);border:1px solid var(--border);border-radius:var(--radius-xs);resize:vertical;transition:border-color var(--motion-fast);padding:10px 12px;line-height:1.5}.pwi-json-textarea:focus{border-color:var(--accent-ring);outline:none}.dialog-backdrop{z-index:900;background:#00000073;justify-content:center;align-items:center;display:flex;position:fixed;inset:0}.dialog{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);width:90%;max-width:420px;box-shadow:var(--shadow);flex-direction:column;gap:16px;padding:24px;display:flex}.dialog-actions{justify-content:flex-end;gap:8px;display:flex}.login-url-block{flex-direction:column;gap:6px;margin:6px 0;display:flex}.login-url-block-text{overflow-wrap:anywhere;border:1px solid var(--border);border-radius:var(--radius-xs);background:var(--surface);max-width:100%;font-size:var(--text-label);color:var(--text);-webkit-user-select:all;user-select:all;padding:8px 10px;display:block}.login-url-block-actions{flex-wrap:wrap;align-items:center;gap:10px;display:flex}.login-url-block-open{font-size:var(--text-label);color:var(--accent);cursor:pointer;text-decoration:underline}.login-hint{flex-direction:column;gap:8px;display:flex}.login-hint-device{border:1px solid var(--border);background:var(--surface);border-radius:10px;flex-wrap:wrap;align-items:center;gap:10px;padding:12px;display:flex}.login-hint-device-code{letter-spacing:.14em;color:var(--text);-webkit-user-select:all;user-select:all;font-size:20px;font-weight:800}.login-hint-paste{flex-direction:column;gap:6px;display:flex}.login-hint-paste-row{gap:8px;display:flex}.login-hint-paste-input{flex:1}.open-browser-pref{cursor:pointer;align-items:flex-start;gap:8px;margin:2px 0;display:flex}.open-browser-pref input{flex-shrink:0;margin-top:2px}.open-browser-pref-copy{flex-direction:column;gap:2px;display:flex}.pws-dashboard{--fg-muted:var(--muted);flex-direction:column;gap:14px;max-width:100%;padding:4px 0;display:flex}.pws-dashboard-header{flex-direction:row;justify-content:space-between;align-items:flex-start;gap:12px;display:flex}.pws-dashboard-header-text{flex-direction:column;gap:2px;min-width:0;display:flex}.pws-dashboard-title{margin:0;font-size:1.1rem;font-weight:600}.pws-dashboard-subtitle{margin:0;font-size:.82rem}.pws-dashboard-summary{gap:10px;display:flex}.pws-dashboard-card{border:1px solid var(--border);background:var(--bg-secondary,var(--bg));border-radius:8px;flex-direction:column;align-items:center;gap:2px;min-width:80px;padding:10px 20px;display:flex}.pws-dashboard-card-count{font-size:1.25rem;font-weight:700;line-height:1}.pws-dashboard-card--ok .pws-dashboard-card-count{color:var(--green,#22c55e)}.pws-dashboard-card--warn .pws-dashboard-card-count{color:var(--yellow,#eab308)}.pws-dashboard-card--muted .pws-dashboard-card-count{color:var(--fg-muted,#888)}.pws-dashboard-card-label{color:var(--fg-muted,#888);letter-spacing:.02em;font-size:.7rem}.pws-dashboard-columns{grid-template-columns:minmax(0,1.4fr) minmax(0,1fr);align-items:start;gap:20px;display:grid}.pws-dashboard-section{flex-direction:column;gap:4px;min-width:0;display:flex}.pws-dashboard-section-title{text-transform:uppercase;letter-spacing:.04em;color:var(--fg-muted,#888);margin:0;padding-bottom:2px;font-size:.68rem;font-weight:600}.pws-dashboard-attention .pws-dashboard-section-title{color:var(--yellow,#eab308);text-transform:none;letter-spacing:0;align-items:center;gap:6px;font-size:.82rem;display:inline-flex}.pws-dashboard-row--attention{border-color:color-mix(in srgb, var(--yellow,#eab308) 40%, var(--border));background:color-mix(in srgb, var(--yellow,#eab308) 8%, var(--bg-secondary,var(--bg)))}.pws-dashboard-rows{flex-direction:column;gap:0;display:flex}.pws-dashboard-row{cursor:pointer;text-align:left;color:inherit;font:inherit;background:0 0;border:none;border-radius:6px;grid-template-columns:22px 1fr auto auto;align-items:center;gap:8px;width:100%;padding:6px;transition:background .12s;display:grid}.pws-dashboard-row:hover{background:var(--bg-hover,#80808014)}.pws-dashboard-row-icon{justify-content:center;align-items:center;width:20px;height:20px;display:flex}.pws-dashboard-row-icon img,.pws-dashboard-row-icon .provider-icon-mask{width:18px;height:18px}.pws-dashboard-row-info{flex-direction:column;gap:1px;min-width:0;display:flex}.pws-dashboard-row-name{white-space:nowrap;text-overflow:ellipsis;font-size:.85rem;font-weight:500;overflow:hidden}.pws-dashboard-row-meta{font-size:.72rem}.pws-dashboard-row-count{white-space:nowrap;font-size:.8rem}.pws-dashboard-row-chevron{width:12px;height:12px;color:var(--fg-muted,#888);flex-shrink:0}.pws-dashboard-row-bars{grid-column:1/-1;min-height:64px;padding-top:1px;padding-left:30px}.pws-capacity-label,.pws-capacity-recovery,.pws-capacity-incomplete{font-size:.7rem}.pws-capacity-label{color:var(--fg-muted,#888);margin-bottom:3px}.pws-capacity-details{border-top:1px solid var(--border-soft);gap:7px;margin-top:8px;padding-top:7px;display:grid}.pws-capacity-recovery{color:var(--fg-muted,#888);flex-wrap:wrap;justify-content:space-between;align-items:flex-start;gap:12px;display:flex}.pws-capacity-recovery span{overflow-wrap:anywhere;min-width:0}.pws-capacity-recovery strong{color:var(--text);white-space:nowrap;margin-left:auto}@container (width<=520px){.pws-capacity-recovery{grid-template-columns:minmax(0,1fr);gap:3px;display:grid}.pws-capacity-recovery strong{white-space:normal;margin-left:0}}.pws-capacity-current{padding-top:2px}.pws-capacity-incomplete{color:var(--amber)}.pws-dashboard-section--rate-limits,.pws-dashboard-section--recent{min-height:180px}.pws-dashboard-empty{min-height:3rem;margin:8px 0 0}.pws-dashboard-row--skeleton{pointer-events:none;cursor:default}.pws-skel{border-radius:var(--radius-2xs,4px);background:linear-gradient(90deg, var(--raised,#eee) 0%, var(--surface,#f7f7f7) 50%, var(--raised,#eee) 100%);background-size:200% 100%;animation:1.2s ease-in-out infinite codex-auth-skeleton-shimmer;display:block}.pws-dashboard-row-icon.pws-skel{border-radius:6px;width:22px;height:22px}.pws-skel--name{width:7rem;height:.85rem}.pws-skel--meta{width:5rem;height:.7rem;margin-top:4px}.pws-skel--count{width:4rem;height:.75rem;margin-left:auto}.providers-workspace--boot{min-height:600px}.providers-workspace-rail--boot{opacity:.35;min-height:600px}@container provider-workspace (width<=920px){.pws-dashboard-columns{grid-template-columns:1fr;gap:14px}}@media (width<=900px){.pws-dashboard-columns{grid-template-columns:1fr;gap:14px}}@media (width<=600px){.pws-dashboard-summary{flex-direction:column}.pws-dashboard-card{flex-direction:row;justify-content:space-between;min-width:0}}.combos-workspace-root{grid-template-columns:minmax(340px,28vw) 1fr;align-items:stretch;width:100%;height:100%;min-height:100%;display:grid;overflow:hidden}.combos-workspace-shell{flex-direction:column;width:100%;height:100%;min-height:0;display:flex;overflow:hidden}.combos-workspace-shell-banner{flex-shrink:0;padding:10px 16px 0}.combos-workspace-shell-body{flex-direction:column;flex:auto;min-height:0;display:flex;overflow:hidden}.combos-workspace-rail{border-right:1px solid var(--border);background:var(--rail);flex-direction:column;height:100%;min-height:0;display:flex;position:sticky;top:0;overflow-y:auto}.combos-workspace-rail-header{border-bottom:1px solid var(--border-soft);flex-shrink:0;justify-content:space-between;align-items:center;gap:8px;padding:14px 16px 12px;display:flex}.combos-workspace-rail-title{font-size:var(--text-title);color:var(--text);font-weight:600}.combos-workspace-rail-count{font-family:var(--mono);font-size:var(--text-caption);color:var(--faint)}.combos-workspace-rail-list{flex-direction:column;flex:auto;display:flex;overflow-y:auto}.combos-workspace-rail-group+.combos-workspace-rail-group{margin-top:10px}.combos-workspace-rail-group-head{color:var(--muted);font-size:var(--text-caption);align-items:center;gap:7px;padding:8px 16px 6px;font-weight:600;display:flex}.combos-workspace-rail-row{border:none;border-bottom:1px solid var(--border-soft);cursor:pointer;min-height:40px;font:inherit;font-size:var(--text-control);color:var(--text);text-align:left;background:0 0;grid-template-columns:26px minmax(0,1fr) 3.5rem 14px;align-items:center;column-gap:8px;width:100%;padding:11px 12px 11px 14px;transition:background .12s;display:grid}.combos-workspace-rail-row:hover{background:var(--hover)}.combos-workspace-rail-row.combos-workspace-rail-row--selected{background:var(--accent-soft);box-shadow:inset 0 0 0 1px var(--border)}.combos-workspace-rail-row:focus-visible{outline:1px solid var(--accent);outline-offset:-1px}.combos-workspace-rail-icon{width:26px;height:26px;color:var(--text);flex-shrink:0;justify-content:center;align-items:center;display:flex}.combos-workspace-rail-name{text-overflow:ellipsis;white-space:nowrap;min-width:0;font-family:var(--mono);font-size:var(--text-label);overflow:hidden}.combos-workspace-rail-meta{font-family:var(--mono);font-size:var(--text-caption);color:var(--faint);text-align:right}.combos-workspace-rail-chevron{width:14px;height:14px;color:var(--faint)}.combos-workspace-main{background:var(--bg);flex-direction:column;min-width:0;height:100%;min-height:0;display:flex;overflow:hidden}.combos-workspace-overview,.combos-workspace-detail{flex:auto;min-height:0;padding:20px 24px 32px;overflow-y:auto}.combos-workspace-overview-head{justify-content:space-between;align-items:baseline;gap:12px;margin-bottom:16px;display:flex}.combos-workspace-overview-title{margin:0;font-size:20px;font-weight:600}.combos-workspace-detail-head{border-bottom:1px solid var(--border-soft);flex-wrap:wrap;align-items:center;gap:10px;margin-bottom:16px;padding-bottom:12px;display:flex}.combos-workspace-detail-title{font-size:var(--text-title);font-weight:600;font-family:var(--mono);min-width:0;margin:0}.combos-workspace-detail-actions{flex-wrap:wrap;align-items:center;gap:8px;margin-left:auto;display:flex}.combos-workspace-segmented{border:1px solid var(--border);border-radius:var(--radius-pill);background:var(--surface);gap:2px;margin-bottom:16px;padding:2px;display:inline-flex}.combos-workspace-segmented .btn{border-radius:var(--radius-pill);min-width:0;min-height:0;font-size:var(--text-label);line-height:inherit;border:none;padding:4px 12px}.combos-workspace-tab-content:not([hidden]){flex-direction:column;gap:16px;max-width:720px;display:flex}.cwi-search-row{border-bottom:1px solid var(--border-soft);flex-shrink:0;align-items:center;gap:8px;padding:10px 14px 12px;display:flex}.cwi-search-wrap{flex:1;min-width:0;position:relative}.cwi-search-wrap .cwi-search-icon{width:14px;height:14px;color:var(--faint);pointer-events:none;position:absolute;top:50%;left:10px;transform:translateY(-50%)}.cwi-search-input{width:100%;padding-left:32px!important}.cwi-count-strip{flex-wrap:wrap;gap:10px;margin-bottom:18px;display:flex}.cwi-count-pill{border:1px solid var(--border-soft);border-radius:var(--radius);background:var(--raised);align-items:baseline;gap:6px;padding:8px 12px;display:inline-flex}.cwi-count-pill strong{font-family:var(--mono);font-size:var(--text-subtitle)}.cwi-count-pill span{font-size:var(--text-label);color:var(--muted)}.cwi-field>p.muted{max-width:var(--prose-measure);overflow-wrap:anywhere}.cwi-capabilities{border:1px solid var(--border-soft);border-radius:var(--radius);background:var(--raised);flex-direction:column;gap:10px;padding:12px;display:flex}.cwi-capability-row{justify-content:space-between;align-items:center;gap:12px;display:flex}.cwi-capability-label{font-size:13px;font-weight:500}.cwi-capability-hint{margin:3px 0 0;font-size:12px}.cwi-target-list{flex-direction:column;gap:8px;display:flex}.cwi-target-row{grid-template-columns:28px auto minmax(0,1fr) minmax(0,1.2fr) 4.5rem auto auto;align-items:center;gap:8px;display:grid}.cwi-target-row--failover{grid-template-columns:28px auto minmax(0,1fr) minmax(0,1.2fr) auto auto}.cwi-target-row--dragging{opacity:.55}.cwi-target-row--drop{box-shadow:inset 0 0 0 1px var(--accent);border-radius:var(--radius)}.cwi-target-grip{border-radius:var(--radius);width:28px;height:32px;color:var(--faint);cursor:grab;touch-action:none;background:0 0;border:none;justify-content:center;align-items:center;padding:0;display:inline-flex}.cwi-target-grip:hover{color:var(--muted);background:var(--hover)}.cwi-target-grip:active{cursor:grabbing}.cwi-target-reorder{flex-direction:column;flex-shrink:0;gap:0;display:inline-flex}.cwi-target-reorder .btn{min-width:0;padding:2px 4px;line-height:1}.cwi-target-actions{flex-shrink:0;gap:2px;display:flex}.cwi-quota-badge{border:1px solid var(--border);border-radius:var(--radius-pill);background:var(--raised);min-height:24px;color:var(--muted);font-size:var(--text-caption);white-space:nowrap;justify-content:center;align-items:center;padding:3px 7px;font-weight:600;line-height:1.2;display:inline-flex}.cwi-quota-badge--available{border-color:color-mix(in srgb, var(--green) 36%, var(--border));background:var(--green-soft);color:var(--green)}.cwi-quota-badge--exhausted{border-color:color-mix(in srgb, var(--red) 36%, var(--border));background:var(--red-soft);color:var(--red)}.cwi-quota-banner{border:1px solid color-mix(in srgb, var(--red) 36%, var(--border));border-radius:var(--radius-sm);background:var(--red-soft);max-width:720px;color:var(--red);font-size:var(--text-control);margin-bottom:16px;padding:10px 12px;font-weight:550;line-height:1.4}.cwi-strategy-seg{border-radius:var(--radius-pill);background:var(--surface-soft,var(--raised));gap:2px;padding:3px;display:inline-flex}.cwi-strategy-seg .btn{border-radius:var(--radius-pill);border:none;min-width:88px;padding:5px 12px}.cwi-strategy-seg .btn-ghost{color:var(--muted);background:0 0}.cwi-copy-chip{font-family:var(--mono);font-size:var(--text-label);cursor:pointer}.cwi-form-grid{flex-direction:column;gap:12px;display:flex}.cwi-field label{font-size:var(--text-label);color:var(--muted);margin-bottom:4px;font-weight:600;display:block}.cwi-field .input,.cwi-field select.input{width:100%}.cwi-attention-list{flex-direction:column;gap:0;display:flex}.cwi-attention-row{border:none;border-bottom:1px solid var(--border-soft);width:100%;font:inherit;color:var(--text);text-align:left;cursor:pointer;background:0 0;align-items:center;gap:10px;padding:10px 0;display:flex}.cwi-attention-row:hover{color:var(--accent)}.cwi-modal-form{flex-direction:column;gap:14px;max-height:min(70vh,560px);padding-right:2px;display:flex;overflow-y:auto}.cwi-modal-actions{justify-content:flex-end;gap:8px;margin-top:16px;display:flex}@media (width<=939px){.combos-workspace-root{grid-template-rows:minmax(220px,38vh) 1fr;grid-template-columns:1fr}.combos-workspace-rail{border-right:none;border-bottom:1px solid var(--border);position:relative}.cwi-capability-row{align-items:flex-start}.cwi-target-row,.cwi-target-row--failover{grid-template-columns:28px auto 1fr auto}.cwi-target-row>.input:nth-child(3),.cwi-target-row>.input:nth-child(4){grid-column:3/-1}.cwi-target-row>.cwi-quota-badge{grid-column:3;justify-self:start}}.pwi-dot{background:var(--muted);border-radius:50%;flex-shrink:0;width:7px;height:7px;display:inline-block}.pwi-back-overview{white-space:nowrap;flex:none;gap:4px;margin-right:2px}.pwi-section{background:0 0;border:none;border-radius:0;min-width:0;padding:0}.pwi-section-title{font-size:var(--text-label);color:var(--muted);text-transform:uppercase;letter-spacing:.05em;margin:0 0 4px;font-weight:650}.pwi-empty-right-icon{color:var(--faint)}.pwi-empty-right-sub{font-size:var(--text-control);color:var(--muted);max-width:44ch;margin:0;line-height:1.5}.pwi-json-unsaved-card,.pwi-remove-confirm-card{width:min(420px,92vw);max-width:420px;padding:20px 22px 16px}.pwi-json-unsaved-title,.pwi-remove-confirm-title{font-size:var(--text-subtitle);color:var(--text);margin:0 0 8px;font-weight:650}.pwi-json-unsaved-desc,.pwi-remove-confirm-desc{font-size:var(--text-control);margin:0 0 18px;line-height:1.45}.pwi-json-unsaved-actions,.pwi-remove-confirm-actions{flex-wrap:wrap;justify-content:flex-end;gap:8px;display:flex}.pwi-remove-confirm-danger{background:var(--red)!important;color:#fff!important;border-color:#0000!important}.pwi-remove-confirm-danger:hover:not(:disabled){filter:brightness(1.08)}.main-inner:has(#models-panel-catalog:not([hidden])){max-width:1200px}.main-inner:has(#models-panel-routing:not([hidden])){max-width:1200px}.models-workspace-shell{width:100%;min-width:0;container:models-workspace/inline-size}.models-tab-panel{min-width:0}.models-workspace-root{gap:var(--space-4);grid-template-columns:minmax(240px,280px) minmax(0,1fr);width:100%;max-width:100%;min-height:480px;display:grid}.models-workspace-rail{gap:var(--space-3);border-right:1px solid var(--border);padding-right:var(--space-3);flex-direction:column;min-width:0;display:flex}.models-workspace-rail-header{justify-content:space-between;align-items:baseline;gap:8px;display:flex}.models-workspace-rail-title{font-size:var(--text-body);font-weight:var(--weight-semibold);color:var(--text)}.models-workspace-rail-count{font-family:var(--mono);color:var(--faint);font-variant-numeric:tabular-nums;font-size:11px}.models-workspace-rail-list{flex-direction:column;gap:2px;min-height:0;max-height:640px;display:flex;overflow-y:auto}.models-workspace-rail-row{gap:var(--space-0-5);appearance:none;border-radius:var(--radius-sm);width:100%;min-height:46px;padding:var(--space-1-5) var(--space-2);cursor:pointer;text-align:left;color:inherit;font:inherit;background:0 0;border:none;flex-direction:column;display:flex;overflow:hidden}.models-workspace-rail-row:hover{background:var(--surface-2)}.models-workspace-rail-row--selected{background:var(--surface-2);box-shadow:inset 0 0 0 1px var(--border)}.models-workspace-rail-name{font-size:var(--text-body);font-weight:var(--weight-medium);color:var(--text);white-space:nowrap;text-overflow:ellipsis;overflow:hidden}.models-workspace-rail-meta{font-size:var(--text-caption);color:var(--faint);white-space:nowrap;text-overflow:ellipsis;font-variant-numeric:tabular-nums;overflow:hidden}.models-workspace-main{gap:var(--space-3);flex-direction:column;min-width:0;display:flex}.models-control-top-row{align-items:center;column-gap:clamp(var(--space-3), 2vw, var(--space-6));row-gap:var(--space-2);margin-bottom:var(--space-3);grid-template-columns:minmax(0,1fr) auto;min-width:0;display:grid}.models-shadow-row{align-items:center;gap:var(--space-2);flex-wrap:nowrap;min-width:0;display:flex}.models-shadow-label,.models-shadow-warning{white-space:nowrap}.models-shadow-model-slot{flex:0 auto;min-width:9.5rem;max-width:12.5rem}.models-shadow-model-slot .custom-select{width:auto;min-width:100%;max-width:100%}.models-shadow-model-slot .select-trigger{justify-content:space-between;width:auto;min-width:100%;max-width:100%}.models-shadow-model-slot .select-trigger>span{text-overflow:ellipsis;white-space:nowrap;min-width:0;overflow:hidden}.models-v2-mode-row{align-items:center;gap:var(--space-2);flex-wrap:nowrap;flex:none;justify-self:end;min-width:0;min-height:28px;display:flex}.models-v2-detail-row{gap:var(--space-2);margin-bottom:var(--space-2);flex-wrap:wrap;align-items:center}.models-v2-keep-native-row{min-width:0;margin-top:calc(var(--space-1) * -1);grid-column:2;justify-content:flex-end;justify-self:end;display:flex}.models-v2-keep-native{align-items:center;gap:var(--space-2);min-width:0;display:inline-flex}.models-v2-keep-native-label{color:var(--muted);white-space:nowrap;text-overflow:ellipsis;min-width:0;overflow:hidden}.models-v2-keep-native-info{width:24px;height:24px;color:var(--muted);cursor:help;flex:0 0 24px;justify-content:center;align-items:center;display:inline-flex}.models-provider-head{row-gap:var(--space-2);column-gap:var(--space-2);flex-wrap:wrap;align-items:center;min-width:0}.models-provider-head>span.text-body,.models-provider-toggle>span.text-body{overflow-wrap:anywhere;min-width:0}.models-provider-toggle{min-width:0}.models-provider-toggle>*{text-overflow:ellipsis;white-space:nowrap;min-width:0;overflow:hidden}.models-provider-toggle>svg{flex:none}.models-provider-actions{justify-content:flex-end;align-items:center;gap:var(--space-2);flex-wrap:wrap;min-width:0;max-width:100%;margin-left:auto}.models-cap-cluster{align-items:center;gap:var(--space-1);flex-wrap:wrap;display:flex}.models-provider-actions .btn-ghost.models-alias-edit{opacity:.75;transition:opacity var(--motion-fast)}.models-provider-head:hover .models-provider-actions .btn-ghost.models-alias-edit,.models-provider-head:focus-within .models-provider-actions .btn-ghost.models-alias-edit,.models-provider-actions .btn-ghost.models-alias-edit:focus-visible{opacity:1}@media (hover:none){.models-provider-actions .btn-ghost.models-alias-edit{opacity:1}}@media (prefers-reduced-motion:reduce){.models-provider-actions .btn-ghost.models-alias-edit{transition:none}}.models-provider-list{gap:var(--space-2);flex-direction:column;display:flex}.models-provider-card{margin-bottom:0;overflow:hidden}.models-provider-body{padding:var(--space-3) var(--space-4)}.models-provider-body>.input{width:100%;margin-bottom:var(--space-2)}.models-provider-hint{margin:0 0 var(--space-2);max-width:var(--prose-measure)}.models-chip{padding:var(--space-0-5) var(--space-2);border:1px solid var(--border);border-radius:var(--radius-pill);display:inline-block}.models-chip--tip{margin-bottom:var(--space-1)}.models-model-row{padding:var(--space-2) 0}.models-show-more{margin-top:var(--space-2)}.models-segmented{border:1px solid var(--border);border-radius:var(--radius-pill);background:var(--surface);gap:2px;padding:2px;display:inline-flex}.models-segmented .btn{border-radius:var(--radius-pill);min-width:0;min-height:0;font-size:var(--text-label);line-height:inherit;border:none;padding:4px 12px}.models-cap-row{gap:var(--space-2);margin-bottom:var(--space-3);flex-wrap:wrap}.models-custom-summary{gap:var(--space-2);margin-bottom:var(--space-2)}.models-order-hint{align-items:flex-start;gap:var(--space-2);margin-bottom:var(--space-4);max-width:var(--prose-measure)}.models-order-hint>svg{margin-top:var(--space-0-5);flex-shrink:0}.models-collapse-controls{gap:var(--space-2);margin:0 0 var(--space-2)}.models-combos-card{margin-bottom:var(--space-3)}.models-combos-card--pending{min-height:var(--space-12)}.models-combos-empty-head{padding:var(--space-3);justify-content:space-between;gap:var(--space-2)}.models-combos-add{padding:var(--space-2) var(--space-3) var(--space-3) calc(var(--space-8) + var(--space-0-5));gap:var(--space-2);text-decoration:none}.models-help-link{margin-top:var(--space-3)}.models-context-fields,.models-field-stack{gap:var(--space-4);flex-direction:column;display:flex}.models-field{gap:var(--space-2);flex-direction:column;display:flex}.models-field-row{gap:var(--space-2)}.models-modality-option{gap:var(--space-2);cursor:pointer}.modal-card .models-modality-option input[type=checkbox]{flex:none;width:13px;height:13px;margin:0}.row.models-model-row,.row.models-cap-row,.row.models-custom-summary,.row.models-order-hint,.row.models-collapse-controls,.row.models-combos-empty-head,.row.models-combos-add,.row.models-field-row,.row.models-modality-option{gap:var(--space-2)}@media (width<=1160px){.models-control-top-row{grid-template-columns:1fr}.models-shadow-row{flex-wrap:wrap;flex:100%}.models-shadow-model-slot{flex:12rem;width:auto;min-width:min(100%,10rem);max-width:100%}.models-v2-mode-row{flex-wrap:wrap;justify-self:start}.models-v2-keep-native-row{grid-column:1;justify-content:flex-start;justify-self:start;margin-top:0}.models-v2-keep-native{white-space:normal}}@container models-workspace (width<=720px){.models-workspace-root{grid-template-columns:1fr}.models-workspace-rail{border-right:none;border-bottom:1px solid var(--border);padding-right:0;padding-bottom:var(--space-3)}.models-workspace-rail-list{max-height:240px}.models-provider-actions{justify-content:flex-start;width:100%;margin-left:0}}@media (width<=768px){.models-workspace-root{grid-template-columns:1fr}.models-workspace-rail{border-right:none;border-bottom:1px solid var(--border);padding-right:0;padding-bottom:var(--space-3)}.models-workspace-rail-list{max-height:240px}.models-provider-actions{justify-content:flex-start;width:100%;margin-left:0}}.main-inner:has(.dashboard-workspace-shell){max-width:1200px}.dashboard-workspace-shell{width:100%;min-width:0}.dashboard-workspace-main{gap:var(--space-4);flex-direction:column;min-width:0;display:flex}.dashboard-workspace-main .tbl-wrap{max-height:520px;padding:0 var(--space-2) var(--space-2);overflow-y:auto}.dashboard-workspace-main .tbl{margin-top:var(--space-3)}.dashboard-workspace-main .tbl thead th{z-index:1;background:var(--surface);position:sticky;top:0}.dash-overview-stack{gap:var(--space-4);flex-direction:column;display:flex}.dash-overview-tools{gap:var(--space-4);grid-template-columns:repeat(auto-fit,minmax(min(100%,21rem),1fr));align-items:stretch;display:grid}.dash-overview-tools>.panel{box-sizing:border-box;min-width:0;height:100%}.dash-sidecar-grid{gap:var(--space-4);grid-template-columns:repeat(auto-fit,minmax(min(100%,39rem),1fr));align-items:stretch;display:grid}.dash-sidecar-row-card .dash-delegation-controls .custom-select:nth-child(2){min-width:min(100%,6.5rem);max-width:9rem}.dash-sidecar-copy{flex:auto;min-width:0}.dash-sidecar-row-card .dash-sidecar-copy{overflow-wrap:break-word;flex:1 1 0;min-width:min(100%,14rem)}.dash-sidecar-row-card .dash-delegation-controls{flex-flow:column;flex:0 0 min(100%,26rem);justify-content:flex-start;align-items:stretch;gap:12px;min-height:3.6875rem}.dash-sidecar-row-card .dash-delegation-controls .custom-select:first-child{min-width:min(100%,10.5rem);max-width:14rem}.dash-sidecar-row-card .dash-delegation-controls .select-trigger{justify-content:space-between;width:100%;max-width:100%}.dash-sidecar-row-card .dash-delegation-controls .select-trigger>span{text-overflow:ellipsis;white-space:nowrap;min-width:0;overflow:hidden}.dash-sidecar-row-card{flex-wrap:wrap;align-content:start;min-width:0;container:sidecar-card/inline-size}.dash-sidecar-row-card .dash-sidecar-copy .setting-hint{min-height:3lh}.dash-vision-sidecar-card .dash-sidecar-copy{min-width:min(100%,14rem)}.dash-vision-sidecar-card .dash-delegation-controls{min-width:0;max-width:100%}.dash-sidecar-select-row{flex-wrap:wrap;justify-content:flex-start;align-items:center;gap:8px;width:100%;min-width:0;display:flex}.dash-sidecar-select-row .custom-select:first-child{flex:70%;min-width:min(100%,9rem);max-width:16rem}.dash-sidecar-select-row .custom-select:nth-child(2){flex:30%;min-width:min(100%,6rem);max-width:9rem}.dash-sidecar-select-row .custom-select .select-trigger{width:100%;max-width:100%}.dash-sidecar-trailing-row{justify-content:flex-end;align-items:center;gap:8px;width:100%;display:flex}.dash-sidecar-toggle-label{text-align:right;min-width:0}.dash-vision-advanced-trigger{appearance:none;color:var(--muted);font-size:var(--text-control);cursor:pointer;background:0 0;border:none;align-items:center;gap:6px;padding:0;display:inline-flex}.dash-vision-advanced-trigger:hover:not(:disabled){color:var(--text)}.dash-vision-advanced-trigger:disabled{opacity:.6;cursor:not-allowed}.dash-vision-advanced-trigger:focus-visible{outline:2px solid var(--accent-ring);outline-offset:2px;border-radius:var(--radius-2xs)}.dash-vision-number{flex-direction:column;gap:4px;min-width:0;display:flex}.dash-vision-number .codex-auto-switch-input-wrap{width:min(11.5rem,100%)}.dash-vision-number .codex-auto-switch-input{min-width:0}.dash-vision-advanced-popover{box-sizing:border-box;background:var(--raised);border:1px solid var(--border);border-radius:var(--radius);flex-direction:column;gap:12px;width:max-content;min-width:16rem;max-width:min(22rem,100vw - 2rem);padding:12px 14px;display:flex;overflow-y:auto;box-shadow:0 4px 24px #00000024}.dash-vision-advanced-popover-title{font-weight:var(--weight-semibold)}.dash-vision-advanced-popover .dash-vision-number .codex-auto-switch-input-wrap{width:100%}.dash-vision-advanced-popover .dash-vision-number .codex-auto-switch-input{flex:1 1 0}.dash-vision-advanced-popover .dash-vision-number{gap:4px}@container sidecar-card (width<=36rem){.dash-sidecar-row-card .dash-sidecar-copy,.dash-sidecar-row-card .dash-delegation-controls{flex:0 100%;min-width:0}.dash-sidecar-row-card .dash-delegation-controls{align-items:stretch}.dash-vision-sidecar-card .dash-sidecar-select-row{justify-content:flex-start}}@container sidecar-card (width<=22rem){.dash-sidecar-row-card .dash-sidecar-copy,.dash-sidecar-row-card .dash-delegation-controls{flex-basis:100%;min-width:0}.dash-sidecar-row-card .dash-delegation-controls{flex-wrap:wrap;justify-content:flex-start}}.dash-model-acc{gap:var(--space-2);flex-direction:column;display:flex}.dash-model-group{border:1px solid var(--border);border-radius:var(--radius);background:var(--surface);overflow:hidden}.dash-model-head{appearance:none;cursor:pointer;width:100%;font:inherit;color:var(--text);text-align:left;transition:background var(--motion-fast);background:0 0;border:none;align-items:center;gap:8px;padding:10px 14px;display:flex}.dash-model-head:hover{background:var(--hover)}.dash-model-head .count{font-family:var(--font-code);font-weight:var(--weight-medium);color:var(--faint);font-size:var(--text-label)}.dash-model-chips{border-top:1px solid var(--border-soft);flex-wrap:wrap;gap:6px;padding:12px 14px 14px;display:flex}.dash-model-chip{font-family:var(--font-code);font-size:var(--text-label);border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--raised);color:var(--text);white-space:nowrap;padding:3px 8px}.main-inner:has(.storage-workspace-root){max-width:1200px}.storage-workspace-root{gap:var(--space-4);grid-template-columns:minmax(0,1fr);align-items:start;width:100%;max-width:100%;display:grid}.storage-workspace-rail{gap:var(--space-2);border-bottom:1px solid var(--border);padding-bottom:var(--space-3);flex-direction:column;min-width:0;min-height:0;display:flex}.storage-workspace-rail-header{flex:none;justify-content:space-between;align-items:baseline;gap:8px;display:flex}.storage-workspace-rail-title{font-size:var(--text-body);font-weight:var(--weight-semibold);color:var(--text)}.storage-workspace-rail-count{font-family:var(--mono);color:var(--faint);font-variant-numeric:tabular-nums;font-size:11px}.storage-workspace-rail-list{overscroll-behavior:auto;scrollbar-gutter:stable;flex-direction:column;gap:2px;min-height:0;max-height:min(14rem,40vh);padding-right:10px;display:flex;overflow-y:auto}.storage-workspace-rail-row{justify-content:center;gap:var(--space-0-5);appearance:none;border-radius:var(--radius-sm);width:100%;height:3.25rem;min-height:3.25rem;padding:var(--space-1) var(--space-2);cursor:pointer;text-align:left;color:inherit;font:inherit;transition:background var(--motion-fast), box-shadow var(--motion-fast);background:0 0;border:none;flex-direction:column;flex:none;display:flex;overflow:hidden}.storage-workspace-rail-row:hover{background:var(--surface)}.storage-workspace-rail-row--selected{background:var(--accent-soft);box-shadow:inset 0 0 0 1px var(--border)}.storage-workspace-rail-row:focus-visible{outline:2px solid var(--accent-ring);outline-offset:-2px}.storage-workspace-rail-primary{justify-content:space-between;align-items:center;gap:var(--space-2);display:flex}.storage-workspace-rail-name{text-overflow:ellipsis;white-space:nowrap;min-width:0;font-size:var(--text-body);font-weight:var(--weight-medium);line-height:var(--leading-ui);overflow:hidden}.storage-workspace-rail-size{font-family:var(--mono);color:var(--muted);font-variant-numeric:tabular-nums;flex-shrink:0;padding-right:2px;font-size:11px}.storage-workspace-rail-meta{text-overflow:ellipsis;white-space:nowrap;min-width:0;font-size:var(--text-caption);line-height:var(--leading-ui);color:var(--muted);overflow:hidden}.storage-workspace-rail-empty{color:var(--muted);padding:8px 4px;font-size:12px}.storage-workspace-main{flex-direction:column;min-width:0;max-width:100%;min-height:0;padding:0;display:flex}.stw-overview,.stw-detail-body{min-height:0;overflow-x:hidden}.stw-detail-body{padding-top:14px}.storage-workspace-main .stw-summary{gap:var(--space-3);grid-template-columns:repeat(auto-fit,minmax(min(100%,11rem),1fr));margin-bottom:16px;display:grid}.stw-summary-card{border:1px solid var(--border);border-radius:var(--radius);background:var(--surface);flex-direction:column;gap:6px;min-width:0;padding:12px 14px;display:flex}.stw-summary-label{font-size:var(--text-label);font-weight:var(--weight-medium);color:var(--muted)}.stw-summary-value{font-size:var(--text-body);font-weight:var(--weight-semibold);font-variant-numeric:tabular-nums;color:var(--text);text-overflow:ellipsis;line-height:1.3;overflow:hidden}.stw-summary-value.mono,.stw-home-path{font-family:var(--mono);font-size:12px;font-weight:var(--weight-medium);text-overflow:ellipsis;white-space:nowrap;word-break:normal;overflow:hidden}.stw-section{margin-bottom:16px}.stw-section-title{text-transform:uppercase;letter-spacing:.04em;color:var(--muted);margin:0 0 8px;font-size:.7rem;font-weight:600}.stw-hint{color:var(--muted);padding:4px 0 12px;font-size:13px}.stw-file-row{align-items:center;gap:var(--space-3);border-bottom:1px solid var(--border-soft,var(--border));padding:6px 0;display:flex}.stw-file-row:last-child{border-bottom:none}.stw-file-path{text-overflow:ellipsis;white-space:nowrap;min-width:0;font-family:var(--mono);color:var(--text);flex:auto;font-size:12px;overflow:hidden}.stw-file-size{font-family:var(--mono);color:var(--muted);font-variant-numeric:tabular-nums;flex-shrink:0;font-size:12px}.stw-file-bucket{color:var(--faint);flex-shrink:0;font-size:11px}.stw-detail{flex-direction:column;max-width:760px;height:100%;min-height:0;display:flex}.stw-detail-toolbar{border-bottom:1px solid var(--border);background:0 0;flex:none;align-items:center;gap:8px;margin-bottom:0;padding:0 0 10px;display:flex}.stw-detail-back{appearance:none;border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--raised);color:var(--text);font:inherit;font-size:var(--text-control);font-weight:var(--weight-medium);line-height:var(--leading-ui);cursor:pointer;transition:background var(--motion-fast), border-color var(--motion-fast), color var(--motion-fast);align-items:center;gap:6px;margin:0;padding:6px 12px 6px 8px;display:inline-flex}.stw-detail-back:hover{background:var(--raised-hover,var(--surface));border-color:var(--faint);color:var(--text);text-decoration:none}.stw-detail-back:focus-visible{outline:2px solid var(--accent-ring);outline-offset:1px}.stw-detail-back-chevron{width:14px;height:14px;color:var(--muted);flex-shrink:0;transform:rotate(180deg)}.stw-detail-back:hover .stw-detail-back-chevron{color:var(--text)}.stw-detail-title{word-break:break-word;min-width:0;margin:0 0 12px;font-size:1.1rem;font-weight:600}.stw-kv{flex-direction:column;margin:0 0 16px;display:flex}.stw-kv-row{gap:12px;padding:4px 0;font-size:.84rem;line-height:1.4;display:flex}.stw-kv-row dt{width:110px;color:var(--muted);flex-shrink:0;font-weight:400}.stw-kv-row dd{word-break:break-word;flex:1;min-width:0;margin:0}.stw-kv-row dd code,.stw-kv-mono{font-family:var(--mono);font-variant-numeric:tabular-nums;font-size:.8rem}.storage-policy-help{font-size:var(--text-sm);line-height:var(--leading-body);margin:0}.storage-policy-enable{flex-wrap:wrap;align-items:center;gap:12px;display:flex}.storage-policy-enable-row{cursor:default;align-items:center;gap:10px;min-width:0;display:inline-flex}.storage-policy-fields{gap:10px;max-width:none;margin-top:0;display:grid}.storage-policy-fields .field{gap:4px;margin:0;display:grid}.storage-policy-fields .field-label{margin-bottom:0}.storage-policy-fields fieldset.field{border:none;min-width:0;padding:0}.storage-policy-trigger-row{flex-wrap:wrap;align-items:center;gap:10px;min-height:32px;display:flex}.storage-policy-trigger-hint{font-size:var(--text-control);line-height:var(--leading-ui);color:var(--text);flex:0 auto}.storage-policy-target{gap:6px;display:grid}.storage-policy-target>.field-label{padding:0}.storage-policy-target-row{flex-wrap:wrap;align-items:center;gap:10px;min-height:32px;display:flex}.storage-policy-target-label{font-size:var(--text-control);line-height:var(--leading-ui);color:var(--text);flex:0 auto}.storage-policy-target-row .codex-auto-switch-input-wrap,.storage-policy-trigger-row .codex-auto-switch-input-wrap{flex:none}.storage-policy-selects{grid-template-columns:repeat(2,minmax(0,1fr));gap:8px 12px;display:grid}.storage-policy-warn{font-size:var(--text-sm);line-height:var(--leading-body);margin:0}.storage-policy-meta{border-top:1px solid var(--border);grid-template-columns:repeat(2,minmax(0,1fr));gap:8px 12px;margin-top:6px;padding-top:12px;display:grid}.storage-policy-meta-item{min-width:0;font-size:var(--text-sm);line-height:var(--leading-body);align-items:baseline;gap:8px;display:flex}.storage-policy-meta-item>.muted{flex:none}.storage-policy-meta-value{font-variant-numeric:tabular-nums;min-width:0}.storage-policy-actions{flex-wrap:wrap;align-items:center;gap:8px;min-height:28px;margin-top:0;display:flex}.storage-policy-actions__status{min-width:7rem;font-size:var(--text-label);color:var(--muted);line-height:var(--leading-body)}.storage-policy-actions__status.is-error{color:var(--red)}.storage-page-head-actions{align-items:center;gap:10px;min-width:0;display:flex}.storage-page-head-feedback{text-align:right;min-width:7rem;max-width:16rem;font-size:var(--text-label);color:var(--muted);line-height:var(--leading-body)}.storage-page-meta{font-size:var(--text-sm);color:var(--muted);line-height:var(--leading-body);flex-wrap:wrap;align-items:baseline;gap:6px 10px;margin:-12px 0 16px;display:flex}.storage-page-meta__home{text-overflow:ellipsis;white-space:nowrap;max-width:min(100%,42rem);color:var(--text);font-size:inherit;overflow:hidden}.storage-page-meta__sep{color:var(--faint)}.storage-cleanup-card{justify-items:start;gap:10px;margin-top:16px;padding:14px 16px;display:grid}.storage-cleanup-card>.panel-title{width:100%;margin:0}.storage-cleanup-card__tabs{justify-self:start}.storage-cleanup-card__stack{width:100%;min-width:0;display:grid}.storage-cleanup-card__body{grid-area:1/1;width:100%;min-width:0}.storage-cleanup-card__body[data-active=false]{visibility:hidden;pointer-events:none}.storage-cleanup-policy-split{grid-template-columns:minmax(0,1fr);align-items:start;gap:12px 20px;display:grid}.storage-cleanup-pane{gap:10px;min-width:0;display:grid}.storage-cleanup-manual{border-top:1px solid var(--border);align-content:start;gap:8px;min-width:0;padding-top:12px;display:grid}.storage-cleanup-manual__title{font-size:var(--text-body);font-weight:var(--weight-semibold);line-height:var(--leading-ui);color:var(--text);margin:0}.storage-cleanup-manual .storage-cleanup-pane{gap:8px}.storage-cleanup-manual .storage-manual-panel__controls{flex-direction:column;align-items:stretch}.storage-cleanup-manual .storage-manual-panel__slider{flex:auto}.storage-cleanup-pane.storage-quarantine-pane{grid-template-rows:auto auto 1fr auto;align-content:start;min-height:8rem;display:grid}.storage-manual-panel{gap:10px;padding:14px 16px;display:grid}.storage-manual-panel>.panel-title{margin:0}.storage-manual-panel__help{font-size:var(--text-sm);line-height:var(--leading-body);margin:0}.storage-manual-panel__controls{flex-wrap:wrap;align-items:center;gap:10px;display:flex}.storage-manual-panel__slider{flex:220px;align-items:center;gap:8px;min-width:0;display:flex}.storage-manual-panel__presets{flex-wrap:wrap;gap:6px;display:flex}.storage-manual-panel__status{font-size:var(--text-sm);line-height:var(--leading-body);margin:0}.storage-manual-panel__table{margin:0}@media (width<=520px){.storage-cleanup-card__tabs{width:100%}.storage-cleanup-card__tabs.usage-segmented{width:100%;display:flex}.storage-cleanup-card__tabs .usage-segmented-btn{flex:1 1 0;min-width:0}.storage-policy-selects,.storage-policy-meta{grid-template-columns:minmax(0,1fr)}.storage-policy-enable{align-items:flex-start}}@media (width<=768px){.storage-workspace-rail{position:static}}.main-inner:has(.subagents-workspace-shell){max-width:1200px}.subagents-workspace-shell{width:100%;min-width:0;container:subagents-workspace/inline-size}.subagents-workspace-root{gap:var(--space-4);grid-template-columns:minmax(0,1fr);width:100%;max-width:100%;min-height:0;display:grid}.subagents-workspace-rail{gap:var(--space-3);border-bottom:1px solid var(--border);padding-bottom:var(--space-3);flex-direction:column;min-width:0;display:flex}.subagents-workspace-section{gap:var(--space-2);flex-direction:column;min-width:0;display:flex}.subagents-workspace-section+.subagents-workspace-section{margin-top:var(--space-3);padding-top:var(--space-4);border-top:1px solid var(--border)}.swi-delegation{border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--surface);flex-direction:column;display:flex}.swi-delegation-row{justify-content:space-between;align-items:flex-start;gap:var(--space-4);padding:var(--space-3);display:flex}.swi-delegation-row+.swi-delegation-row{border-top:1px solid var(--border-soft)}.swi-delegation-row .setting-copy{flex:auto;min-width:0}.swi-delegation-row .setting-hint{max-width:72ch;margin-top:3px;line-height:1.5}.swi-delegation-controls{align-items:center;gap:var(--space-2);flex-wrap:wrap;flex-shrink:0;justify-content:flex-end;display:flex}.swi-rail-icon{width:15px;height:15px;color:var(--faint);flex-shrink:0}.subagents-workspace-rail-header{justify-content:space-between;align-items:baseline;gap:8px;display:flex}.subagents-workspace-rail-title{font-size:var(--text-body);font-weight:var(--weight-semibold);color:var(--text)}.subagents-workspace-rail-count{font-family:var(--mono);color:var(--faint);font-variant-numeric:tabular-nums;font-size:11px}.swi-picker-box{gap:var(--space-2);border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--surface);min-height:0;padding:var(--space-2);flex-direction:column;display:flex}.swi-picker-box .subagents-workspace-rail-search{flex:none}.subagents-workspace-rail-list{flex-direction:column;gap:4px;min-height:0;display:flex}.swi-picker-box{--swi-row-step:46px}.swi-picker-box .subagents-workspace-rail-list{overscroll-behavior:auto;scrollbar-gutter:stable;max-height:min(456px,52vh);overflow-y:auto}@supports (height:round(down, 50vh, 46px)){.swi-picker-box .subagents-workspace-rail-list{max-height:calc(round(down, min(456px, 52vh) + 4px, var(--swi-row-step)) - 4px)}}.subagents-workspace-rail-group{flex-direction:column;gap:2px;min-width:0;display:flex}.subagents-workspace-rail-group+.subagents-workspace-rail-group{margin-top:8px}.subagents-workspace-rail-group-head{justify-content:space-between;align-items:center;gap:var(--space-2);font-size:var(--text-caption);font-weight:var(--weight-medium);color:var(--muted);padding:var(--space-2) var(--space-2) var(--space-1);display:flex}.subagents-workspace-rail-group-count{font-variant-numeric:tabular-nums;flex-shrink:0}.subagents-workspace-rail-row{min-width:0;max-width:100%;min-height:42px;padding:var(--space-1) var(--space-1-5);border-radius:var(--radius-sm);transition:background var(--motion-fast);align-items:center;gap:6px;display:flex}.subagents-workspace-rail-row:hover{background:var(--surface)}.subagents-workspace-rail-row--selected{background:var(--accent-soft);box-shadow:inset 0 0 0 1px var(--border)}.subagents-workspace-rail-row-main{appearance:none;cursor:pointer;min-width:0;font:inherit;font-size:var(--text-body);color:var(--text);text-align:left;border-radius:var(--radius-sm);background:0 0;border:none;flex:auto;align-items:center;gap:8px;padding:4px 6px;display:flex}.subagents-workspace-rail-row-main:focus-visible{outline:2px solid var(--accent-ring);outline-offset:-2px}.subagents-workspace-rail-name{text-overflow:ellipsis;white-space:nowrap;min-width:0;font-family:var(--mono);flex:auto;font-size:12.5px;overflow:hidden}.subagents-workspace-rail-toggle{appearance:none;border:1px solid var(--border);border-radius:var(--radius-sm);width:26px;height:26px;color:var(--faint);cursor:pointer;transition:background var(--motion-fast), border-color var(--motion-fast), color var(--motion-fast);background:0 0;flex-shrink:0;justify-content:center;align-items:center;display:inline-flex}.subagents-workspace-rail-toggle:hover{background:var(--surface);border-color:var(--muted)}.subagents-workspace-rail-toggle:focus-visible{outline:2px solid var(--accent-ring);outline-offset:1px}.subagents-workspace-rail-toggle--on{border-color:var(--accent);color:var(--accent);background:var(--accent-soft)}.subagents-workspace-rail-toggle--disabled{opacity:.4;cursor:not-allowed}.swi-rail-priority{font-family:var(--mono);color:var(--accent);text-align:center;font-variant-numeric:tabular-nums;flex-shrink:0;width:14px;font-size:10.5px;font-weight:700}.subagents-workspace-rail-empty{color:var(--muted);padding:8px 4px;font-size:12px}.subagents-workspace-main{min-width:0;max-width:100%;padding:0;overflow:hidden}.swi-featured-head{justify-content:space-between;align-items:baseline;gap:12px;margin-bottom:6px;display:flex}.swi-featured-title{color:var(--text);margin:0;font-size:1.15rem;font-weight:600}.swi-featured-count{font-family:var(--mono);color:var(--faint);font-variant-numeric:tabular-nums;font-size:12px}.swi-featured-hint{color:var(--muted);align-items:flex-start;gap:8px;max-width:80ch;margin:0 0 16px;font-size:12.5px;line-height:1.5;display:flex}.swi-featured-hint svg{flex-shrink:0;margin-top:2px}.swi-featured-list{flex-direction:column;gap:8px;margin-bottom:18px;display:flex}.swi-featured-row{border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--surface);align-items:center;gap:10px;padding:9px 12px;display:flex}.swi-featured-pos{font-family:var(--mono);color:var(--accent);font-variant-numeric:tabular-nums;flex-shrink:0;width:18px;font-size:13px;font-weight:700}.swi-featured-name{text-overflow:ellipsis;white-space:nowrap;min-width:0;font-family:var(--mono);color:var(--text);flex:auto;font-size:13px;overflow:hidden}.swi-featured-actions{flex-shrink:0;align-items:center;gap:4px;display:inline-flex}.swi-featured-empty{border:1px dashed var(--border);border-radius:var(--radius-sm);text-align:center;color:var(--muted);margin-bottom:18px;padding:24px 16px;font-size:13px}.swi-save-row{align-items:center;gap:10px;display:flex}.swi-detail{flex-direction:column;max-width:720px;display:flex}.swi-detail-back{color:var(--accent);font:inherit;cursor:pointer;background:0 0;border:none;align-items:center;gap:4px;margin-bottom:10px;padding:0;font-size:.82rem;display:inline-flex}.swi-detail-back:hover{text-decoration:underline}.swi-detail-back-chevron{width:12px;height:12px;transform:rotate(180deg)}.swi-detail-head{align-items:center;gap:10px;margin-bottom:16px;display:flex}.swi-detail-icon{width:32px;height:32px;color:var(--text);flex-shrink:0;justify-content:center;align-items:center;display:flex}.swi-detail-title{word-break:break-all;min-width:0;margin:0;font-size:1.15rem;font-weight:600;line-height:1.3}.swi-detail-section{margin-bottom:18px}.swi-detail-section-title{text-transform:uppercase;letter-spacing:.04em;color:var(--muted);margin:0 0 8px;font-size:.7rem;font-weight:600}.swi-detail-kv{flex-direction:column;margin:0;display:flex}.swi-detail-kv-row{gap:12px;padding:5px 0;font-size:.84rem;line-height:1.4;display:flex}.swi-detail-kv-row dt{width:130px;color:var(--muted);flex-shrink:0;font-weight:400}.swi-detail-kv-row dd{word-break:break-word;flex:1;min-width:0;margin:0}.swi-detail-kv-row dd code{font-size:.8rem}.swi-detail-actions{flex-wrap:wrap;align-items:center;gap:8px;margin-top:4px;display:flex}@container subagents-workspace (width<=720px){.subagents-workspace-root{min-height:auto}.swi-picker-box .subagents-workspace-rail-list{max-height:min(318px,46vh)}@supports (height:round(down, 50vh, 46px)){.swi-picker-box .subagents-workspace-rail-list{max-height:calc(round(down, min(318px, 46vh) + 4px, var(--swi-row-step)) - 4px)}}}@media (width<=768px){.subagents-workspace-root{min-height:auto}.swi-picker-box .subagents-workspace-rail-list{max-height:min(272px,44vh)}@supports (height:round(down, 50vh, 46px)){.swi-picker-box .subagents-workspace-rail-list{max-height:calc(round(down, min(272px, 44vh) + 4px, var(--swi-row-step)) - 4px)}}}.integration-badge--danger{background:var(--red-soft);color:var(--red)}.integration-badge--danger-outline{color:var(--red);border-color:var(--red);background:0 0}.integration-summary{border:1px solid var(--border);border-radius:var(--radius);background:var(--raised);flex-wrap:wrap;align-items:center;gap:18px;margin-bottom:14px;padding:14px 16px;display:flex}.integration-summary-cell{flex-direction:column;gap:2px;display:flex}.integration-summary-label{font-size:var(--text-caption);color:var(--muted)}.integration-summary .btn{margin-left:auto}.integration-cards{grid-template-columns:repeat(auto-fill,minmax(min(260px,100%),1fr));gap:12px;margin:14px 0;padding:0;list-style:none;display:grid}.integration-api-keys-row{border:1px solid var(--border);border-radius:var(--radius);background:var(--raised);flex-wrap:wrap;align-items:center;gap:10px;margin:14px 0 0;padding:12px 14px;display:flex}.integration-api-keys-copy{flex-direction:column;flex:220px;gap:2px;min-width:0;display:flex}.integration-api-keys-copy h4{margin:0}.integration-api-keys-row .integration-meta{min-height:0;margin:0}.integration-card{border:1px solid var(--border);border-radius:var(--radius);background:var(--raised);flex-direction:column;gap:8px;padding:14px;display:flex;position:relative}.integration-card-head{justify-content:space-between;align-items:center;gap:8px;display:flex}.integration-card-head h4{margin:0}.integration-card-link{appearance:none;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:none;margin:0;padding:0}.integration-card-link:after{content:"";border-radius:var(--radius);position:absolute;inset:0}.integration-card-link:focus-visible{outline:none}.integration-card-link:focus-visible:after{outline:2px solid var(--accent-ring);outline-offset:-2px}.integration-card:hover{border-color:var(--accent-ring)}.integration-card-actions{z-index:1;flex-wrap:wrap;align-items:center;gap:10px;min-width:0;margin-top:auto;display:flex;position:relative}.integration-card-actions .btn{margin-left:auto}.integration-empty{border:1px dashed var(--border);border-radius:var(--radius);text-align:center;color:var(--muted);padding:20px}.integration-client-head{align-items:center;gap:10px;display:flex}.integration-client-head h3{margin:0}.client-mark{width:var(--client-mark-size,20px);height:var(--client-mark-size,20px);flex:none;justify-content:center;align-items:center;display:inline-flex}.client-mark--img img{border-radius:2px;width:100%;height:100%}.client-mark--mask{background:var(--text);-webkit-mask-position:50%;mask-position:50%;-webkit-mask-size:contain;mask-size:contain;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}.client-mark--monogram{font-size:var(--text-label);font-weight:var(--weight-semibold);color:var(--muted);border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--raised);line-height:1}.integration-card-head h4{flex:auto;min-width:0}.page-tab>.client-mark{vertical-align:-2px;margin-right:6px}.integration-client-head .switch{margin-left:auto}.integration-path{font-family:var(--font-code);font-size:var(--text-caption);color:var(--muted);overflow-wrap:anywhere;margin:4px 0}.integration-meta{font-size:var(--text-caption);color:var(--muted)}.integration-card .integration-path,.integration-card .integration-meta{min-height:calc(var(--text-caption) * var(--leading-ui) * 2);margin:0}.integration-history{border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--raised);margin:8px 0;overflow:hidden}.integration-history-list{margin:0;padding:0;list-style:none}.integration-history-row{flex-wrap:wrap;align-items:center;gap:10px;padding:8px 12px;display:flex}.integration-history-list .integration-history-row+.integration-history-row,.integration-history-older{border-top:1px solid var(--border-soft)}.integration-history-older>summary{color:var(--muted);font-size:var(--text-caption);cursor:pointer;padding:8px 12px}.integration-history-older>summary:focus-visible{outline:2px solid var(--accent-ring);outline-offset:-2px}.integration-history-older .integration-history-row:first-child{border-top:1px solid var(--border-soft)}.integration-history-more{margin:8px 12px}.integration-history-kind{font-family:var(--font-code);font-size:var(--text-caption);font-weight:var(--weight-semibold)}.integration-history-client{font-size:var(--text-caption);color:var(--muted)}.integration-history-at{font-size:var(--text-caption);color:var(--muted);margin-right:auto}@media (width<=420px){.integration-history-at{flex:100%;margin-right:0}.integration-history-row .btn{margin-left:auto}}.integration-restore-dialog .integration-path{margin:10px 0}.integration-consequence-body{flex-direction:column;gap:12px;display:flex}.integration-consequence-body p{font-size:var(--text-caption);line-height:var(--leading-body);margin:0}.integration-consequence-body code{font-family:var(--font-code);font-size:inherit;overflow-wrap:anywhere}.claudecode-connection-head{align-items:center;gap:10px;padding:10px 0;display:flex}.claudecode-connection-head .switch{margin-left:auto}.cursor-page{flex-direction:column;gap:14px;display:flex}.cursor-card{border:1px solid var(--border);border-radius:var(--radius);background:var(--raised);flex-direction:column;gap:8px;padding:14px;display:flex}.cursor-card h4{margin:0}.cursor-detect-row{flex-wrap:wrap;align-items:center;gap:10px;display:flex}.cursor-detect-name{font-weight:var(--weight-semibold);min-width:12ch}.cursor-detect-path{font-family:var(--font-code);font-size:var(--text-caption);overflow-wrap:anywhere}.cursor-gateway-row{grid-template-columns:minmax(7ch,auto) 1fr auto;align-items:center;gap:10px;display:grid}.cursor-gateway-label{font-weight:var(--weight-semibold)}.cursor-gateway-value{font-family:var(--font-code);border-radius:var(--radius);background:var(--bg);border:1px solid var(--border);overflow-wrap:anywhere;padding:4px 8px}.cursor-model-table{border-collapse:collapse;width:100%;font-size:var(--text-caption)}.cursor-model-table th,.cursor-model-table td{text-align:left;border-bottom:1px solid var(--border);vertical-align:top;padding:6px 8px}.cursor-model-table th{font-weight:var(--weight-semibold);color:var(--muted)}.cursor-effort-rows{margin-left:.5rem;font-size:.85em}.main-inner:has(.usage-workspace-shell){max-width:1200px}.usage-workspace-shell{width:100%;min-width:0;container:usage-workspace/inline-size}.usage-workspace-root{grid-template-columns:minmax(0,1fr);gap:0;width:100%;max-width:100%;min-height:0;display:grid}.usw-section-block+.usw-section-block{margin-top:var(--space-5);padding-top:var(--space-4);border-top:1px solid var(--border)}.usage-workspace-rail{gap:var(--space-3);border-bottom:1px solid var(--border);padding-bottom:var(--space-3);flex-direction:column;min-width:0;display:flex}.usage-workspace-rail-header{justify-content:space-between;align-items:baseline;gap:8px;display:flex}.usage-workspace-rail-title{font-size:var(--text-body);font-weight:var(--weight-semibold);color:var(--text)}.usage-workspace-rail-list{flex-direction:column;gap:2px;min-height:0;display:flex}.usage-workspace-rail-row{gap:var(--space-0-5);appearance:none;border-radius:var(--radius-sm);min-width:0;max-width:100%;min-height:46px;padding:var(--space-1-5) var(--space-2);cursor:pointer;text-align:left;color:inherit;font:inherit;background:0 0;border:none;flex-direction:column;display:flex;overflow:hidden}.usage-workspace-rail-row:hover{background:var(--raised)}.usage-workspace-rail-row--selected{background:var(--raised);box-shadow:inset 0 0 0 1px var(--border)}.usage-workspace-rail-name{font-size:var(--text-body);font-weight:var(--weight-medium);color:var(--text);white-space:nowrap;text-overflow:ellipsis;overflow:hidden}.usage-workspace-rail-meta{font-size:var(--text-caption);color:var(--faint);white-space:nowrap;text-overflow:ellipsis;font-variant-numeric:tabular-nums;overflow:hidden}.usage-workspace-main,.usw-body{gap:var(--space-4);flex-direction:column;min-width:0;display:flex}.usw-section{flex-direction:column;gap:0;min-width:0;display:flex}.usw-section .h-section{margin:0 0 var(--space-3);font-size:var(--text-title);font-weight:var(--weight-semibold);color:var(--text);line-height:1.2}.usw-section-toolbar{margin:0 0 var(--space-5);max-width:220px}.usw-section .tbl-wrap{min-width:0;padding:var(--space-3);overscroll-behavior:auto;scrollbar-gutter:stable;max-height:min(574px,58vh);overflow-y:auto}.usw-section .tbl-wrap thead th{top:calc(-1 * var(--space-3));z-index:1;background:var(--surface);box-shadow:0 calc(-1 * var(--space-3)) 0 var(--surface), inset 0 -1px 0 var(--border);border-bottom-color:#0000;position:sticky}.usw-section .usage-cards{margin-top:0}@container usage-workspace (width<=720px){.usage-workspace-root{min-height:auto}}@media (width<=768px){.usage-workspace-root{min-height:auto}}.usage-source-row{color:var(--text-secondary);justify-content:space-between;align-items:center;gap:12px;margin:0 0 14px;display:flex}.usage-scope-control{gap:6px;display:inline-flex}@media (width<=640px){.usage-source-row{flex-direction:column;align-items:flex-start}}.main-inner:has(.claudecode-workspace-shell){max-width:1200px}.claudecode-workspace-shell{width:100%;min-width:0}.claudecode-workspace-root{gap:var(--space-4);grid-template-columns:minmax(240px,280px) minmax(0,1fr);width:100%;max-width:100%;min-height:0;display:grid}.claude-effective-auth{font-size:var(--text-caption);color:var(--muted);border-bottom:1px solid var(--border-soft);flex-direction:column;gap:2px;margin-top:-4px;padding:0 16px 12px;line-height:1.45;display:flex}.claude-effective-auth.warn{color:var(--amber)}.claude-effective-auth-label{font-weight:var(--weight-semibold);color:var(--faint);text-transform:uppercase;letter-spacing:.04em;font-size:10.5px}.claudecode-workspace-rail{gap:var(--space-3);border-right:1px solid var(--border);padding-right:var(--space-3);flex-direction:column;min-width:0;display:flex}.claudecode-workspace-rail-list{flex-direction:column;gap:2px;min-height:0;display:flex}.claudecode-workspace-rail-row{gap:var(--space-0-5);appearance:none;border-radius:var(--radius-sm);width:100%;min-height:46px;padding:var(--space-1-5) var(--space-2);cursor:pointer;text-align:left;color:inherit;font:inherit;background:0 0;border:none;flex-direction:column;display:flex;overflow:hidden}.claudecode-workspace-rail-row:hover{background:var(--raised)}.claudecode-workspace-rail-row--selected{background:var(--raised);box-shadow:inset 0 0 0 1px var(--border)}.claudecode-workspace-rail-name{font-size:var(--text-body);font-weight:var(--weight-medium);color:var(--text);white-space:nowrap;text-overflow:ellipsis;overflow:hidden}.claudecode-workspace-main{gap:var(--space-3);flex-direction:column;min-width:0;display:flex}.ccw-main-head{justify-content:space-between;align-items:center;gap:var(--space-3);min-width:0;min-height:32px;display:flex}.ccw-main-title{min-width:0;font-size:var(--text-control);font-weight:var(--weight-semibold);line-height:var(--leading-ui);color:var(--text);align-items:baseline;gap:8px;margin:0;display:flex}.ccw-main-title .count{color:var(--muted);font-weight:var(--weight-medium);font-family:var(--font-code);font-size:var(--text-label)}.claudecode-workspace-save[data-visible=false]{visibility:hidden;pointer-events:none}.ccw-body{gap:var(--space-3);flex-direction:column;min-width:0;display:flex}.claude-aliases{min-width:0}.claude-aliases-hint{margin:0 0 8px}.claude-aliases-scroll{overscroll-behavior:contain;scrollbar-gutter:stable;flex-direction:column;gap:10px;max-height:min(22rem,48vh);padding-right:2px;display:flex;overflow-y:auto}.claude-aliases-group{min-width:0}.claude-aliases-group-label{font-size:10.5px;font-weight:var(--weight-semibold);letter-spacing:.04em;text-transform:uppercase;color:var(--muted);align-items:baseline;gap:6px;margin:0 0 5px;line-height:1.2;display:flex}.claude-aliases-group-count{font-family:var(--font-code);font-weight:var(--weight-medium);color:var(--faint);letter-spacing:0;text-transform:none;font-size:10px}.claude-aliases-chips{flex-wrap:wrap;gap:4px;display:flex}.claude-aliases-chip{border:1px solid var(--border-soft,var(--border));border-radius:var(--radius-xs);background:var(--raised);max-width:100%;color:var(--text);flex-direction:column;align-items:flex-start;gap:1px;padding:3px 8px;line-height:1.35;display:inline-flex;overflow:hidden}.claude-aliases-chip-id{font-family:var(--font-code);white-space:nowrap;text-overflow:ellipsis;max-width:100%;font-size:11px;overflow:hidden}.claude-aliases-chip-name{color:var(--muted);white-space:nowrap;text-overflow:ellipsis;max-width:100%;font-size:10px;overflow:hidden}.claudecode-workspace-save{align-items:center;gap:var(--space-2);display:flex}@media (width<=768px){.claudecode-workspace-root{grid-template-columns:1fr}.claudecode-workspace-rail{border-right:none;border-bottom:1px solid var(--border);padding-right:0;padding-bottom:var(--space-3)}}:is(.main-inner:has(.api-page),.main-inner:has(.apikeys-workspace-shell)){box-sizing:border-box;max-width:1200px}.api-page{flex-direction:column;gap:0;min-width:0;display:flex}.api-page .page-head{margin-bottom:2px}.api-page .page-sub{margin:0 0 1.15rem}.api-page>.notice{margin-bottom:.85rem}.api-page .apikeys-workspace-shell{margin-top:.15rem}.apikeys-workspace-shell{width:100%;min-width:0;container:apikeys-workspace/inline-size}.apikeys-workspace-root{gap:var(--space-3);grid-template-columns:minmax(0,1fr);align-items:start;width:100%;max-width:100%;min-height:0;display:grid}.awi-section-anchor{gap:var(--space-3);flex-direction:column;min-width:0;display:flex}.awi-keylist-panel .awi-keylist-name{appearance:none;font:inherit;color:var(--accent-text,var(--text));font-weight:var(--weight-semibold);text-align:left;cursor:pointer;overflow-wrap:anywhere;background:0 0;border:none;padding:0}.awi-keylist-panel .awi-keylist-name:hover{text-decoration:underline}.awi-keylist-panel .awi-keylist-name:disabled{cursor:default;opacity:.6;text-decoration:none}.awi-keylist-panel .awi-keylist-name:focus-visible{outline:2px solid var(--accent-ring);outline-offset:2px;border-radius:var(--radius-sm)}.apikeys-workspace-main{min-width:0;max-width:100%;padding:0 var(--space-2);flex-direction:column;display:flex}.awi-detail{flex-direction:column;flex:1;min-height:0;display:flex}.awi-detail-toolbar{border-bottom:1px solid var(--border);background:0 0;flex:none;align-items:center;gap:8px;margin-bottom:0;padding:0 0 10px;display:flex}.awi-detail-body{min-height:0;padding-top:14px}.awi-overview{align-items:start;row-gap:var(--space-3);grid-template-columns:minmax(0,1fr);padding-top:0;display:grid}.awi-overview-section{gap:var(--space-3);flex-direction:column;min-width:0;display:flex}.awi-overview-section>.panel{flex:none;margin-top:0!important}.awi-overview .api-panel{gap:10px;padding:18px}.awi-overview .api-auth-list{gap:8px}.awi-overview .api-endpoints{margin-top:4px}.awi-overview-section>.panel>p.muted.small{margin:0;line-height:1.35}.awi-overview-section>.api-models-panel{flex-direction:column;flex:none;display:flex;overflow:visible}.awi-overview-section .api-models-panel>.input,.awi-overview-section .api-models-panel>.api-panel-head,.awi-overview-section .api-models-panel>.muted,.awi-overview-section .api-models-panel>.api-models-error{flex:none}.awi-overview-section .api-models-panel>.api-models-scroll{overscroll-behavior:auto;scrollbar-gutter:stable;min-width:0;max-height:min(574px,58vh);margin-top:.5rem;overflow:auto}.awi-overview-section .api-models-scroll>.tbl{table-layout:auto;width:100%}.awi-overview-section .api-models-scroll .api-model-cell{overflow-wrap:anywhere;min-width:0}.awi-overview-section .api-models-scroll thead th{z-index:1;background:var(--surface);box-shadow:inset 0 -1px 0 var(--border);border-bottom-color:#0000;position:sticky;top:0}.api-models-error{justify-content:space-between;align-items:center;gap:var(--space-2);flex-wrap:wrap;margin-top:.75rem;display:flex}.api-models-error p{min-width:0;margin:0}.api-models-empty{margin-top:.75rem}.api-example-copy-btn.ocx-tooltip,.api-example-copy-btn{width:100%;min-width:0;max-width:100%;display:block}.api-example-pre{overscroll-behavior:auto;white-space:pre;min-height:0;max-height:none;font-size:var(--text-label);line-height:var(--leading-relaxed);box-sizing:border-box;pointer-events:auto;padding:9px 11px;overflow:auto visible}.api-auth-matrix-block{margin-top:var(--space-3);gap:var(--space-2);flex-direction:column;min-width:0;display:flex}.api-auth-matrix-title{font-size:var(--text-label);margin:0;font-weight:600}.api-auth-matrix{border-collapse:collapse;width:100%;font-size:var(--text-label)}.api-auth-matrix-scroll{overscroll-behavior-x:contain;max-width:100%;overflow-x:auto}.api-auth-matrix th,.api-auth-matrix td{text-align:left;border-bottom:1px solid var(--border);white-space:nowrap;padding:4px 8px 4px 0}.api-auth-matrix th{color:var(--text-muted);font-weight:600}.api-auth-matrix code{font-size:inherit}.awi-rename{align-items:center;gap:var(--space-2);margin-bottom:var(--space-3);flex-wrap:wrap;min-width:0;display:flex}.awi-rename-error,.awi-delete-error{color:var(--danger,#c44);font-size:var(--text-label);flex-basis:100%;margin:0}.awi-rename-label{font-size:var(--text-label);color:var(--text-muted)}.awi-rename .input{flex:12rem;min-width:0}.api-model-test-chip{align-items:center;gap:4px;min-width:0;display:inline-flex}.api-model-actions{flex-wrap:nowrap;align-items:center;gap:4px;min-width:0;display:flex}.api-model-actions .btn{white-space:nowrap}.awi-usage-panel-body{gap:var(--space-3);flex-direction:column;min-width:0;display:flex}.awi-usage-example{gap:8px;min-width:0;padding-top:2px;display:grid}.awi-usage-example+.awi-usage-example{border-top:1px solid var(--border-soft,var(--border));padding-top:10px}.awi-usage-example-title{font-size:var(--text-control);font-weight:var(--weight-semibold);line-height:var(--leading-ui);color:var(--text);margin:0}.awi-overview-section>.api-generate-panel,.awi-overview-section>.api-newkey-panel{margin-top:0}.awi-overview-section>.awi-clientconfig-panel,.awi-clientconfig-panel.api-panel{overflow:visible}.awi-clientconfig-head{gap:var(--space-2);flex-wrap:wrap}.awi-clientconfig-rows{gap:var(--space-1);flex-direction:column;min-width:0;margin:0;padding:0;list-style:none;display:flex}.awi-clientconfig-row{align-items:center;gap:var(--space-2);min-width:0;padding:var(--space-2);border:1px solid var(--border);border-radius:var(--radius);background:var(--surface);display:flex}.awi-clientconfig-mark{border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--raised);flex:none;justify-content:center;align-items:center;width:28px;height:28px;display:inline-flex;overflow:hidden}.awi-clientconfig-mark:has(img){background:0 0;border-color:#0000}.awi-clientconfig-mark img{border-radius:var(--radius-sm);width:100%;height:100%}.awi-clientconfig-mark-mask{background:var(--text);width:20px;height:20px;-webkit-mask-position:50%;mask-position:50%;-webkit-mask-size:contain;mask-size:contain;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}.awi-clientconfig-mark:has(.awi-clientconfig-mark-mask){background:0 0;border-color:#0000}.awi-clientconfig-monogram{font-size:var(--text-label);font-weight:var(--weight-semibold);color:var(--muted);line-height:1}.awi-clientconfig-identity{flex-direction:column;flex:auto;gap:2px;min-width:0;display:flex}.awi-clientconfig-name{font-size:var(--text-control);font-weight:var(--weight-semibold);color:var(--text)}.awi-clientconfig-meta{text-overflow:ellipsis;white-space:nowrap;min-width:0;display:block;overflow:hidden}.awi-clientconfig-row-actions{align-items:center;gap:var(--space-1);flex:none;display:inline-flex}.awi-clientconfig-row-actions .btn{white-space:nowrap}.awi-clientconfig-dialog .awi-clientconfig-json{margin:0 0 var(--space-2)}@container apikeys-workspace (width<=560px){.awi-clientconfig-row{flex-wrap:wrap}.awi-clientconfig-row-actions{justify-content:flex-end;width:100%}}.awi-clientconfig-json{margin:0}.awi-clientconfig-json:focus-visible{outline:2px solid var(--accent-ring);outline-offset:1px}.awi-clientconfig-count,.awi-clientconfig-degraded,.awi-clientconfig-nokey,.awi-clientconfig-merge{margin:0;line-height:1.35}.awi-clientconfig-line{gap:4px;min-width:0;display:grid}.awi-clientconfig-where-title{margin:var(--space-2) 0 2px}.awi-section{margin-bottom:20px}.awi-section-title{text-transform:uppercase;letter-spacing:.04em;color:var(--muted);margin:0 0 8px;font-size:.7rem;font-weight:600}.awi-kv{flex-direction:column;margin:0;display:flex}.awi-kv-row{gap:12px;padding:5px 0;font-size:.84rem;line-height:1.4;display:flex}.awi-kv-row dt{width:130px;color:var(--muted);flex-shrink:0;font-weight:400}.awi-kv-row dd{overflow-wrap:anywhere;flex:1;min-width:0;margin:0}.awi-kv-row dd code{font-size:.8rem}.awi-detail-head{justify-content:space-between;align-items:center;gap:12px;margin-bottom:16px;display:flex}.awi-back{appearance:none;border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--raised);color:var(--text);font:inherit;font-size:var(--text-control);font-weight:var(--weight-medium);line-height:var(--leading-ui);cursor:pointer;transition:background var(--motion-fast), border-color var(--motion-fast), color var(--motion-fast);align-items:center;gap:6px;margin:0;padding:6px 12px 6px 8px;display:inline-flex}.awi-back:hover{background:var(--raised-hover,var(--surface));border-color:var(--faint);color:var(--text);text-decoration:none}.awi-back:focus-visible{outline:2px solid var(--accent-ring);outline-offset:1px}.awi-back-chevron{width:14px;height:14px;color:var(--muted);flex-shrink:0;transform:rotate(180deg)}.awi-detail-title{word-break:break-all;min-width:0;margin:0;font-size:1.15rem;font-weight:600}.awi-detail-actions{flex-shrink:0;align-items:center;gap:8px;display:inline-flex}@container apikeys-workspace (width<=720px){.apikeys-workspace-main{padding:var(--space-2) 0 0}}@media (width<=768px){.apikeys-workspace-main{padding:var(--space-2) 0 0}}.codex-set-prompt__rows{flex-direction:column;margin:12px 0 0;padding:0;list-style:none;display:flex}.codex-set-prompt__row{border-bottom:1px solid var(--border);align-items:center;gap:10px;padding:7px 0;display:flex}.codex-set-prompt__row:last-child{border-bottom:0}.codex-set-prompt__row[data-layer-class=config-toggle],.codex-set-prompt__row[data-layer-class=base]{border-left:3px solid var(--green);margin-left:-11px;padding-left:8px}.codex-set-prompt__name{text-align:left;flex:none}.codex-set-prompt__pos{text-align:right;font-variant-numeric:tabular-nums;width:18px;color:var(--faint);flex:none;font-size:11px}.codex-set-prompt__group{border-top:1px solid var(--border);margin-top:20px;padding-top:14px}.codex-set-prompt__group strong{font-size:var(--text-caption);text-transform:uppercase;letter-spacing:.05em;color:var(--faint)}.codex-set-prompt__row[data-layer-class=config-toggle] .codex-set-prompt__name{font-weight:500}.codex-set-prompt__bytes{text-align:right;font-variant-numeric:tabular-nums;min-width:48px;color:var(--faint);flex:none;font-size:11px}.codex-set-prompt__row[data-layer-class=config-toggle] .toggle,.codex-set-prompt__row[data-layer-class=base] .toggle{flex-shrink:0;margin-left:auto}.codex-set-prompt__key{text-overflow:ellipsis;white-space:nowrap;min-width:0;color:var(--muted);flex:auto;font-size:12px;overflow:hidden}.codex-set-prompt__row .codex-set-prompt__key{font-size:var(--text-caption);opacity:.6}.codex-set-prompt__note{color:var(--muted);white-space:nowrap;margin-left:auto;font-size:12px}.codex-set-prompt__extensions{margin-top:12px}.codex-set-layer-dialog{max-width:520px}.codex-set-layer-dialog__line{align-items:baseline;gap:8px;margin-top:8px;display:flex}.codex-set-layer-dialog__no-text{border-top:1px solid var(--border);margin-top:14px;padding-top:12px}.codex-set-layer-dialog__text.api-code{white-space:pre-wrap;overflow-wrap:anywhere;word-break:normal;max-height:320px;margin-top:8px;font-size:12px;line-height:1.5;overflow:hidden auto}.codex-set-custom{border-top:1px solid var(--border);margin-top:24px;padding-top:16px}.codex-set-custom__add{margin-left:auto}.codex-set-custom__reorder{gap:2px;margin-left:auto;display:inline-flex}.codex-set-custom__adopt{margin-top:12px}.codex-set-custom__adopt-preview{white-space:pre-wrap;word-break:break-word;max-height:200px;overflow:auto}.codex-set-custom__confirm{align-items:center;gap:8px;margin-top:12px;display:flex}.codex-set-custom-dialog{max-width:640px}.codex-set-custom-dialog .field{margin-top:12px;display:block}.codex-set-custom-dialog .field>span{margin-bottom:4px;display:block}.codex-set-custom-dialog .field>input{width:100%}.codex-set-custom-dialog textarea{width:100%;font-family:var(--mono,monospace);resize:vertical;font-size:13px}.codex-set-custom-dialog__lint{color:var(--muted);flex-direction:column;gap:6px;margin:12px 0 0;padding:0;font-size:12px;list-style:none;display:flex}.codex-set-custom-dialog__span{margin-left:6px}.codex-set-custom-dialog__discard{align-items:center;gap:8px}.codex-set-custom-dialog__nav{align-items:center;gap:4px;margin-left:auto;display:inline-flex}.codex-set-custom-dialog__nav-pos{text-align:center;font-variant-numeric:tabular-nums;min-width:44px;color:var(--muted);font-size:12px}.codex-set-prompt__drift{align-items:center;gap:10px;margin-top:12px;display:flex}.codex-set-prompt__drift button{flex:none;margin-left:auto}.codex-set-custom__adopt-refusal{margin-top:8px}.codex-set-preset{margin-left:auto;position:relative}.codex-set-preset>summary{cursor:pointer;list-style:none}.codex-set-preset>summary::-webkit-details-marker{display:none}.codex-set-preset__menu{z-index:10;border:1px solid var(--border);background:var(--bg);border-radius:8px;flex-direction:column;gap:2px;min-width:280px;max-width:380px;padding:6px;display:flex;position:absolute;right:0}.codex-set-preset__item{text-align:left;cursor:pointer;background:0 0;border:0;border-radius:6px;flex-direction:column;gap:2px;padding:8px;display:flex}.codex-set-preset__item:hover:not(:disabled){background:var(--bg-subtle,#7f7f7f14)}.codex-set-preset__desc,.codex-set-preset__provenance{color:var(--muted);font-size:12px}.codex-set-preset__preview{white-space:pre-wrap;max-height:120px;font-family:var(--mono,monospace);color:var(--muted);margin-top:4px;font-size:11px;overflow:auto}.codex-set-base-dialog__nav{align-items:center;gap:4px;margin-left:auto;display:inline-flex}.codex-set-base-dialog__pos{text-align:center;font-variant-numeric:tabular-nums;min-width:44px;color:var(--muted);font-size:12px}.codex-set-base-dialog__default{border:1px solid var(--border);background:var(--surface-2,transparent);border-radius:8px;flex-direction:column;gap:6px;padding:12px;display:flex}.codex-set-base-dialog{touch-action:pan-y}.codex-set-base-dialog__dots{justify-content:center;gap:6px;margin:8px 0 4px;display:flex}.codex-set-base-dialog__dot{border-radius:var(--radius-round);background:var(--border);width:6px;height:6px;transition:background .15s}.codex-set-base-dialog__dot.active{background:var(--green)}.main-inner:has(#models-panel-compatibility:not([hidden])){box-sizing:border-box;max-width:1200px}.lab-page{gap:var(--space-3);flex-direction:column;min-width:0;display:flex}.lab-page .page-head{margin-bottom:2px}.lab-page .page-sub{margin:0 0 .5rem}.lab-status-grid{gap:var(--space-2);grid-template-columns:repeat(auto-fit,minmax(9rem,1fr));display:grid}.lab-status-card{border:1px solid var(--border);border-radius:var(--radius);background:var(--surface);min-width:0;padding:.65rem .75rem}.lab-status-card .label{font-size:var(--text-label);color:var(--muted);margin-bottom:.2rem;display:block}.lab-status-card .value{font-variant-numeric:tabular-nums;font-weight:var(--weight-semibold)}.lab-filters{gap:var(--space-2);flex-wrap:wrap;align-items:flex-end;display:flex}.lab-filter-field{flex-direction:column;gap:.25rem;min-width:10rem;display:flex}.lab-filter-field label{font-size:var(--text-label);color:var(--muted)}.lab-filter-field input,.lab-filter-field select{border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--surface);min-height:2rem;color:var(--text);font:inherit;padding:.25rem .5rem}.lab-matrix-block{border:1px solid var(--border);border-radius:var(--radius);background:var(--surface);padding:.75rem}.lab-matrix-title{font-size:var(--text-control);font-weight:var(--weight-semibold);margin:0 0 .65rem}.lab-matrix-scroll{max-width:100%;overflow:auto}.lab-matrix{border-collapse:collapse;width:100%;font-size:var(--text-control)}.lab-matrix th,.lab-matrix td{border:1px solid var(--border-soft);text-align:left;vertical-align:top;padding:.45rem .55rem}.lab-matrix th{background:var(--raised);font-weight:var(--weight-semibold);white-space:nowrap}.lab-matrix td.subject{font-family:var(--font-code);font-size:var(--text-label);overflow-wrap:anywhere;max-width:14rem}.lab-matrix td.kind{color:var(--muted);font-size:var(--text-label);white-space:nowrap}.lab-verdict-stack{flex-direction:column;gap:.25rem;display:flex}.lab-verdict-badge{border-radius:var(--radius-pill);font:inherit;font-size:var(--text-label);font-weight:var(--weight-semibold);color:inherit;white-space:nowrap;border:1px solid #0000;align-items:center;gap:.35rem;padding:.1rem .45rem;display:inline-flex}button.lab-verdict-badge{appearance:none;cursor:pointer}.lab-verdict-badge .suite{font-weight:var(--weight-regular);color:var(--muted);font-family:var(--font-code)}.lab-verdict-badge[data-verdict=VERIFIED],.lab-verdict-badge[data-verdict=PROBED]{background:color-mix(in srgb, var(--green) 14%, transparent);border-color:color-mix(in srgb, var(--green) 35%, transparent)}.lab-verdict-badge[data-verdict=CLAIMED],.lab-verdict-badge[data-verdict=UNKNOWN]{background:color-mix(in srgb, var(--muted) 12%, transparent);border-color:color-mix(in srgb, var(--muted) 30%, transparent)}.lab-verdict-badge[data-verdict=DEGRADED]{background:#c47a0024;border-color:#c47a0059}.lab-verdict-badge[data-verdict=BLOCKED],.lab-verdict-badge[data-verdict=UNSUPPORTED]{background:color-mix(in srgb, var(--red) 12%, transparent);border-color:color-mix(in srgb, var(--red) 35%, transparent)}.lab-detail-table{border-collapse:collapse;width:100%;font-size:var(--text-control)}.lab-detail-table th,.lab-detail-table td{border-bottom:1px solid var(--border-soft);text-align:left;vertical-align:top;padding:.45rem .55rem}.lab-detail-table th{color:var(--muted);font-weight:var(--weight-semibold);font-size:var(--text-label)}.lab-detail-table td.mono{font-family:var(--font-code);font-size:var(--text-label);overflow-wrap:anywhere}.lab-toolbar{gap:var(--space-2);flex-wrap:wrap;justify-content:space-between;align-items:center;display:flex}.lab-toolbar .btn-ghost{align-items:center;gap:.35rem;display:inline-flex}.lab-layout{gap:var(--space-3);grid-template-columns:minmax(0,1fr) minmax(220px,320px);align-items:start;display:grid}.lab-main{min-width:0}.lab-detail-pane{border:1px solid var(--border);border-radius:var(--radius);padding:var(--space-3);background:var(--surface)}.lab-detail-head{justify-content:space-between;align-items:center;gap:var(--space-2);margin-bottom:var(--space-2);display:flex}.lab-detail-meta{gap:var(--space-1);margin:0 0 var(--space-3);display:grid}.lab-detail-meta dt{font-size:var(--text-label);color:var(--muted)}.lab-detail-section h4{margin:0 0 var(--space-1)}.lab-detail-list{gap:var(--space-1);margin:0;padding:0;list-style:none;display:grid}.lab-load-more{margin-top:var(--space-2);justify-content:center;display:flex}.lab-verdict-badge--selected{outline:2px solid var(--accent)}.lab-detail-table tbody tr.selected{background:var(--raised)}@media (width<=960px){.lab-layout{grid-template-columns:1fr}}@media (width<=720px){.lab-filters{flex-direction:column;align-items:stretch}.lab-filter-field{width:100%}}:root{--lightningcss-light:initial;--lightningcss-dark: ;color-scheme:light dark;--bg:var(--lightningcss-light,#fff)var(--lightningcss-dark,#212121);--rail:var(--lightningcss-light,#f9f9f9)var(--lightningcss-dark,#171717);--surface:var(--lightningcss-light,#fff)var(--lightningcss-dark,#262626);--raised:var(--lightningcss-light,#f4f4f4)var(--lightningcss-dark,#303030);--raised-hover:var(--lightningcss-light,#ececec)var(--lightningcss-dark,#3a3a3a);--border:var(--lightningcss-light,#e6e6e6)var(--lightningcss-dark,#3d3d3d);--border-soft:var(--lightningcss-light,#f0f0f0)var(--lightningcss-dark,#333);--hover:var(--lightningcss-light,#0d0d0d08)var(--lightningcss-dark,#ffffff08);--text:var(--lightningcss-light,#0d0d0d)var(--lightningcss-dark,#ececec);--muted:var(--lightningcss-light,#6e6e6e)var(--lightningcss-dark,#a6a6a6);--faint:var(--lightningcss-light,#707070)var(--lightningcss-dark,#9a9a9a);--accent:var(--lightningcss-light,#0d0d0d)var(--lightningcss-dark,#ececec);--accent-hover:var(--lightningcss-light,#3d3d3d)var(--lightningcss-dark,#fff);--accent-ink:var(--lightningcss-light,#fff)var(--lightningcss-dark,#0d0d0d);--accent-soft:var(--lightningcss-light,#0d0d0d0f)var(--lightningcss-dark,#ffffff17);--accent-ring:var(--lightningcss-light,#00000080)var(--lightningcss-dark,#ffffff61);--green:var(--lightningcss-light,#0a7d5c)var(--lightningcss-dark,#4ecb9d);--green-soft:var(--lightningcss-light,#10a37f1a)var(--lightningcss-dark,#4ecb9d21);--red:var(--lightningcss-light,#b91c1c)var(--lightningcss-dark,#f87171);--red-soft:var(--lightningcss-light,#b91c1c17)var(--lightningcss-dark,#f8717121);--amber:var(--lightningcss-light,#9a4a08)var(--lightningcss-dark,#fbbf24);--amber-soft:var(--lightningcss-light,#b453091a)var(--lightningcss-dark,#fbbf2421);--blue:var(--lightningcss-light,#1d4ed8)var(--lightningcss-dark,#7aa2ff);--blue-soft:var(--lightningcss-light,#1d4ed81a)var(--lightningcss-dark,#7aa2ff29);--space-0-5:2px;--space-1:4px;--space-1-5:6px;--space-2:8px;--space-3:12px;--space-4:16px;--space-5:20px;--space-6:24px;--space-8:32px;--space-10:40px;--space-12:48px;--space-16:64px;--prose-measure:70ch;--radius-2xs:4px;--radius:12px;--radius-sm:8px;--radius-xs:6px;--radius-lg:16px;--radius-round:50%;--radius-pill:999px;--font-ui:"OpenAI Sans", "Pretendard Variable", Pretendard, "Noto Sans KR", "Apple SD Gothic Neo", "Malgun Gothic", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, system-ui, sans-serif;--font-code:ui-monospace, "SFMono-Regular", "Cascadia Code", "JetBrains Mono", "Noto Sans Mono CJK KR", Menlo, Consolas, monospace;--font:var(--font-ui);--mono:var(--font-code);--text-micro:10px;--text-caption:11px;--text-label:12px;--text-control:13px;--text-body:14px;--text-subtitle:16px;--text-title:20px;--text-display:24px;--weight-regular:400;--weight-medium:500;--weight-semibold:600;--weight-bold:700;--leading-tight:1.2;--leading-ui:1.35;--leading-body:1.5;--leading-relaxed:1.6;--tracking-normal:0;--tracking-wide:.04em;--control-sm:28px;--control-md:34px;--control-lg:40px;--control-touch:44px;--icon-sm:14px;--icon-md:16px;--icon-lg:20px;--motion-fast:.12s;--motion-normal:.18s;--z-sticky:20;--z-overlay:30;--z-popover:40;--z-modal:50;--shadow:0 1px 2px var(--lightningcss-light,#1018280f)var(--lightningcss-dark,#00000080), 0 10px 28px var(--lightningcss-light,#10182812)var(--lightningcss-dark,#0000004d);--shadow-sm:0 1px 2px var(--lightningcss-light,#1018280f)var(--lightningcss-dark,#0006);--toggle-w:36px;--toggle-h:20px;--toggle-dot:14px;--toggle-off-bg:var(--lightningcss-light,#d4d4d4)var(--lightningcss-dark,#4a4a4a);--toggle-on-bg:var(--lightningcss-light,#0d0d0d)var(--lightningcss-dark,#4ecb9d);--toggle-dot-color:var(--lightningcss-light,#fff)var(--lightningcss-dark,#0d0d0d)}@media (prefers-color-scheme:dark){:root{--lightningcss-light: ;--lightningcss-dark:initial}}:root[data-theme=light]{--lightningcss-light:initial;--lightningcss-dark: ;color-scheme:light}:root[data-theme=dark]{--lightningcss-light: ;--lightningcss-dark:initial;color-scheme:dark}:root{--glass-rail:var(--lightningcss-light,#f9f9f9a8)var(--lightningcss-dark,#1717179e);--glass-panel:var(--lightningcss-light,#ffffffc7)var(--lightningcss-dark,#262626d1);--glass-blur:saturate(1.6) blur(22px)}*{box-sizing:border-box}html,body,#root{height:100%}html{background:var(--bg);overflow-x:hidden}body{color:var(--text);font-family:var(--font-ui);font-size:var(--text-body);line-height:var(--leading-body);-webkit-font-smoothing:antialiased;text-rendering:optimizelegibility;background:0 0;margin:0;overflow-x:hidden}body:before{content:"";z-index:-1;pointer-events:none;filter:blur(70px);background:radial-gradient(42% 38% at 12% 6%,var(--lightningcss-light,#a4c4ff6b)var(--lightningcss-dark,#6082c829),#0000 70%),radial-gradient(46% 42% at 88% 18%,var(--lightningcss-light,#a8e2c561)var(--lightningcss-dark,#58a08221),#0000 70%),radial-gradient(40% 36% at 70% 92%,var(--lightningcss-light,#ffe0c24d)var(--lightningcss-dark,#b48c6414),#0000 72%);position:fixed;inset:-20%}a{color:var(--text);text-decoration:underline;-webkit-text-decoration-color:var(--faint);text-decoration-color:var(--faint);text-underline-offset:2px}a:hover{-webkit-text-decoration-color:var(--text);text-decoration-color:var(--text)}code{font-family:var(--font-code);font-size:var(--text-label)}.mono{font-family:var(--font-code);font-variant-numeric:tabular-nums}.model-label{align-items:center;gap:var(--space-2);display:inline-flex}h1,h2,h3,h4{font-weight:var(--weight-semibold);letter-spacing:0;line-height:var(--leading-tight);margin:0}.text-micro{line-height:var(--leading-ui);font-size:var(--text-micro)!important}.text-caption{line-height:var(--leading-ui);font-size:var(--text-caption)!important}.text-label{line-height:var(--leading-ui);font-size:var(--text-label)!important}.text-control{line-height:var(--leading-ui);font-size:var(--text-control)!important}p.muted.text-label,p.muted.text-control{max-width:var(--prose-measure)}.text-body{line-height:var(--leading-body);font-size:var(--text-body)!important}.text-subtitle{line-height:var(--leading-tight);font-size:var(--text-subtitle)!important}.text-title{line-height:var(--leading-tight);font-size:var(--text-title)!important}.text-display{line-height:var(--leading-tight);font-size:var(--text-display)!important}.font-regular{font-weight:var(--weight-regular)!important}.font-medium{font-weight:var(--weight-medium)!important}.font-semibold{font-weight:var(--weight-semibold)!important}.font-bold{font-weight:var(--weight-bold)!important}.leading-tight{line-height:var(--leading-tight)!important}.leading-ui{line-height:var(--leading-ui)!important}.leading-body{line-height:var(--leading-body)!important}.leading-relaxed{line-height:var(--leading-relaxed)!important}::selection{background:var(--accent-soft)}input[type=checkbox],input[type=radio]{accent-color:var(--accent)}::-webkit-scrollbar{width:10px;height:10px}::-webkit-scrollbar-thumb{background:var(--border);border-radius:var(--radius-pill);border:2px solid var(--bg)}::-webkit-scrollbar-thumb:hover{background:var(--faint)}:focus-visible{outline:2px solid var(--accent-ring);outline-offset:2px;border-radius:var(--radius-2xs)}.app{grid-template-columns:232px 1fr;min-height:100dvh;display:grid}.sidebar{height:100dvh;z-index:var(--z-overlay);border-right:1px solid var(--border);background:var(--glass-rail);-webkit-backdrop-filter:var(--glass-blur);flex-direction:column;align-self:start;gap:4px;padding:18px 14px;display:flex;position:sticky;top:0}.brand{align-items:center;gap:10px;padding:6px 8px 14px;display:flex}.brand-logo{background:var(--text);flex-shrink:0;width:26px;height:26px;-webkit-mask:url(/logo.png) 50%/contain no-repeat;mask:url(/logo.png) 50%/contain no-repeat}.brand .name{font-weight:var(--weight-semibold);font-size:var(--text-subtitle);letter-spacing:0;line-height:26px}.brand .ver{font-family:var(--font-code);font-size:var(--text-micro);color:var(--muted);line-height:var(--leading-tight);background:var(--raised);border:1px solid var(--border);border-radius:var(--radius-pill);align-self:center;padding:2px 6px}.sidebar nav{flex-direction:column;gap:4px;display:flex}.nav-item{border-radius:var(--radius-sm);text-align:left;cursor:pointer;width:100%;color:var(--muted);font:inherit;font-size:var(--text-control);font-weight:var(--weight-medium);transition:background var(--motion-fast), color var(--motion-fast);background:0 0;border:none;align-items:center;gap:10px;padding:8px 10px;display:flex}.nav-item:hover,.nav-item.active{background:var(--accent-soft);color:var(--text)}.nav-item.active{font-weight:var(--weight-semibold)}.nav-item svg{width:17px;height:17px;color:var(--faint);flex-shrink:0}.nav-item.active svg{color:var(--text)}.nav-entry{border-radius:var(--radius-sm);align-items:center;min-width:0;display:flex}.nav-entry .nav-item{flex:auto;min-width:0}.nav-entry-claude{transition:background var(--motion-fast), color var(--motion-fast);padding-right:4px}.nav-entry-claude:hover,.nav-entry-claude.active{background:var(--accent-soft);color:var(--text)}.nav-entry-claude .nav-item,.nav-entry-claude .nav-item:hover,.nav-entry-claude .nav-item.active{background:0 0;width:auto}.sidebar-foot{flex-direction:column;gap:2px;margin-top:auto;padding-top:12px;display:flex}.drawer-head{align-items:flex-start;display:flex}.drawer-head .brand{flex:auto;min-width:0}.mobile-topbar{display:none}.menu-toggle{border-radius:var(--radius-sm);min-width:44px;min-height:44px;color:var(--muted);cursor:pointer;transition:background var(--motion-fast), color var(--motion-fast);background:0 0;border:none;justify-content:center;align-items:center;padding:8px;display:none}.menu-toggle:hover{background:var(--accent-soft);color:var(--text)}.menu-toggle svg{width:20px;height:20px}.drawer-scrim{z-index:var(--z-overlay);background:var(--lightningcss-light,#14141452)var(--lightningcss-dark,#00000085);display:none;position:fixed;inset:0}.sidebar-link{color:var(--muted);font-size:var(--text-control);border-radius:var(--radius-sm);align-items:center;gap:9px;padding:8px 10px;text-decoration:none;display:flex}.sidebar-link:hover{background:var(--accent-soft);color:var(--text);text-decoration:none}.sidebar-link svg{width:16px;height:16px}.sidebar-github-row{align-items:center;gap:4px;min-width:0;padding-right:10px;display:flex}.sidebar-github-link{flex:auto;min-width:0}.sidebar-github-actions{flex:none;align-items:center;gap:4px;display:flex}.sidebar-orb{border:1px solid var(--border);border-radius:var(--radius-pill);background:var(--raised);width:28px;height:28px;color:var(--muted);cursor:pointer;transition:background var(--motion-fast), color var(--motion-fast), border-color var(--motion-fast);flex:0 0 28px;justify-content:center;align-items:center;padding:0;display:inline-flex;position:relative}.sidebar-orb svg{width:14px;height:14px}.sidebar-orb:hover:not(:disabled){background:var(--accent-soft);color:var(--text);border-color:var(--accent-ring)}.sidebar-orb:focus-visible{outline:2px solid var(--accent-ring);outline-offset:2px}.sidebar-orb--starred{color:var(--amber);cursor:default;opacity:1}.sidebar-orb--update{color:var(--blue);border-color:var(--blue);background:var(--blue-soft)}.sidebar-orb--update:hover:not(:disabled){color:var(--blue);border-color:var(--blue);background:var(--blue-soft);filter:brightness(1.06)}.sidebar-orb-dot{background:var(--blue);border:1.5px solid var(--rail);border-radius:50%;width:7px;height:7px;position:absolute;top:1px;right:1px}.lang-toggle{width:100%;color:var(--muted);font-size:var(--text-control);border-radius:var(--radius-sm);transition:background var(--motion-fast), color var(--motion-fast);align-items:center;gap:9px;padding:8px 10px;display:flex;position:relative}.lang-toggle:hover{background:var(--accent-soft);color:var(--text)}.lang-toggle svg{flex-shrink:0;width:16px;height:16px}.lang-toggle .custom-select{width:100%;position:static!important}.lang-toggle .select-trigger{width:100%;color:inherit;font-size:var(--text-control);min-height:auto;box-shadow:none;-webkit-backdrop-filter:none;background:0 0;border:none;justify-content:space-between;padding:0}.lang-toggle .select-trigger:hover:not(:disabled){color:inherit;box-shadow:none;background:0 0;border:none}.lang-toggle .select-dropdown{background:var(--glass-rail);-webkit-backdrop-filter:var(--glass-blur);border:1px solid var(--border);box-shadow:var(--shadow-sm)}.lang-toggle .select-dropdown-beside{min-width:10rem;max-height:min(60vh,20rem);inset:auto auto 0 calc(100% + 20px);overflow-y:auto}.theme-toggle{text-align:left;cursor:pointer;width:100%;color:var(--muted);font:inherit;font-size:var(--text-control);border-radius:var(--radius-sm);transition:background var(--motion-fast), color var(--motion-fast);background:0 0;border:none;align-items:center;gap:9px;padding:8px 10px;display:flex}.theme-toggle:hover{background:var(--accent-soft);color:var(--text)}.theme-toggle svg{flex-shrink:0;width:16px;height:16px}.theme-toggle .mode{text-transform:capitalize}.stop-toggle{color:var(--red)}.stop-toggle:hover{background:var(--red-soft);color:var(--red)}.stop-toggle:disabled{opacity:.5;cursor:default}.sidebar-action-row{align-items:center;gap:4px;min-width:0;padding-right:10px;display:flex}.sidebar-action-label{min-width:0;color:var(--muted);font-size:var(--text-control);flex:auto;padding:8px 10px 8px 35px}.sidebar-action-orbs{flex:none;align-items:center;gap:4px;display:flex}.sidebar-orb--danger{color:var(--red)}.sidebar-orb--danger:hover:not(:disabled){background:var(--red-soft);color:var(--red);border-color:var(--red)}.sidebar-orb:disabled{opacity:.5;cursor:default}.main{min-width:0}.main-inner{max-width:980px;margin:0 auto;padding:32px 36px 64px;container-type:inline-size}.main-inner.main-inner--combos{flex-direction:column;max-width:none;height:100dvh;min-height:100dvh;margin:0;padding:0;display:flex;overflow:hidden}.main-inner.main-inner--combos>.models-tab-panel--fill:not([hidden]),.main-inner.main-inner--combos>.models-tab-panel--fill:not([hidden])>.combos-workspace-shell{flex-direction:column;flex:auto;height:100%;min-height:0;display:flex}.main-inner.main-inner--combos>.page-head,.main-inner.main-inner--combos>.page-tabs,.main-inner.main-inner--combos>.codex-stale-banner,.main-inner.main-inner--combos>.page-sub{flex-shrink:0;padding-inline:36px}.main-inner.main-inner--combos>.page-sub{margin-bottom:10px}.main-inner.main-inner--combos>.page-tabs{margin-inline:36px;padding-inline:0}.main-inner.main-inner--combos:not(:has(.combos-workspace-shell)){max-width:1200px;height:auto;min-height:0;margin:0 auto;padding:32px 0 64px;display:block;overflow:visible}.main-inner.main-inner--combos:not(:has(.combos-workspace-shell))>.models-tab-panel--fill:not([hidden]){flex:0 auto;height:auto;padding-inline:36px;display:block}.page-head{justify-content:space-between;align-items:center;gap:16px;margin-bottom:6px;display:flex}.page-head h2{font-size:var(--text-title)}.page-head-actions{flex:none;align-items:center;gap:6px;display:flex}.codex-stale-banner{border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--raised);color:var(--text);align-items:center;gap:10px;margin:8px 0 4px;padding:10px 12px;display:flex}.codex-stale-banner-text{min-width:0;font-size:var(--text-control);flex:auto}.page-sub{color:var(--muted);font-size:var(--text-body);max-width:var(--prose-measure);margin:4px 0 22px}.page-tabs{border-bottom:1px solid var(--border);flex-wrap:wrap;gap:2px;margin:2px 0 14px;display:flex;overflow:visible}.page-tab{white-space:nowrap;appearance:none;color:var(--muted);cursor:pointer;font:inherit;font-size:var(--text-control);background:0 0;border:none;border-bottom:2px solid #0000;flex:none;margin-bottom:-1px;padding:8px 12px}.page-tab:hover{color:var(--text)}.page-tab--active{color:var(--text);border-bottom-color:var(--accent);font-weight:var(--weight-semibold)}.page-tab:focus-visible{outline:2px solid var(--accent-ring);outline-offset:-2px}.section-tabs{z-index:3;background:var(--bg);margin-top:0;padding-top:6px;position:sticky;top:0}.section-tab-meta{color:var(--muted);font-size:var(--text-label);font-weight:var(--weight-regular);font-variant-numeric:tabular-nums;margin-left:6px}.page-tab--active>.section-tab-meta{color:var(--text)}[id*=-section-]{scroll-margin-top:56px}.page-sub b{color:var(--text);font-weight:var(--weight-semibold)}.api-page h2 svg{vertical-align:-.16em;width:1em;height:1em}.api-page .page-sub code{font-size:var(--text-label);color:var(--text);background:var(--raised);border:1px solid var(--border-soft);border-radius:var(--radius-xs);padding:1px 4px}.api-endpoints{grid-template-columns:repeat(2,minmax(0,1fr));gap:8px 12px;margin-top:8px;display:grid}.api-endpoints>div{flex-direction:column;align-items:stretch;gap:4px;min-width:0;display:flex}.api-endpoints>div>.muted{flex:none;line-height:1.3}.api-endpoints .ocx-tooltip,.api-endpoints .api-endpoint-url-btn{text-align:left;z-index:1;appearance:none;width:100%;max-width:100%;color:inherit;font:inherit;cursor:pointer;background:0 0;border:0;margin:0;padding:0;position:relative;display:block!important}.api-endpoints .ocx-tooltip:focus-within,.api-endpoints .ocx-tooltip:hover,.api-endpoints .api-endpoint-url-btn:focus-within,.api-endpoints .api-endpoint-url-btn:hover{z-index:5}.api-endpoints .api-endpoint-url{box-sizing:border-box;white-space:nowrap;text-overflow:ellipsis;pointer-events:none;width:100%;max-width:100%;display:block;overflow:hidden}.api-endpoints .api-endpoint-url-btn:hover .api-endpoint-url,.api-endpoints .api-endpoint-url-btn:focus-visible .api-endpoint-url{border-color:color-mix(in srgb, var(--accent) 40%, var(--border))}.api-example-copy-btn,.ocx-tooltip.api-example-copy-btn{appearance:none;width:100%;max-width:100%;color:inherit;font:inherit;text-align:left;cursor:pointer;z-index:1;background:0 0;border:0;align-items:stretch;margin:0;padding:0;display:block;position:relative}.api-example-copy-btn:hover,.api-example-copy-btn:focus-within,.ocx-tooltip.api-example-copy-btn:hover,.ocx-tooltip.api-example-copy-btn:focus-within{z-index:5}.api-example-copy-btn .api-example-pre{box-sizing:border-box;pointer-events:auto;width:100%;max-width:100%;margin:0;display:block}.api-example-copy-btn:hover .api-example-pre,.api-example-copy-btn:focus-visible .api-example-pre{border-color:color-mix(in srgb, var(--accent) 40%, var(--border))}.ocx-tooltip-bubble.api-copy-tip-fixed{z-index:var(--z-popover,40);pointer-events:none;white-space:nowrap;width:max-content;max-width:min(320px,90vw);position:fixed;inset:0 auto auto 0;transform:translate(-50%,-100%)}@media (width<=720px){.api-endpoints{grid-template-columns:minmax(0,1fr)}}.api-auth-list{gap:8px;margin:0;padding-left:1.1rem;display:grid}.api-auth-list li{color:var(--muted);font-size:var(--text-label)}.api-model-actions{align-items:center;gap:6px;display:inline-flex}.api-test-note{font-size:var(--text-label)}.api-test-note--ok{color:var(--green,#22c55e)}.api-test-note--error{color:var(--red)}.btn{border-radius:var(--radius-pill);font:inherit;font-size:var(--text-control);font-weight:var(--weight-medium);line-height:var(--leading-ui);cursor:pointer;transition:background var(--motion-fast), border-color var(--motion-fast), opacity var(--motion-fast);white-space:nowrap;border:1px solid #0000;justify-content:center;align-items:center;gap:7px;padding:8px 16px;display:inline-flex}a.btn,a.btn:hover{text-decoration:none}.btn svg{width:15px;height:15px}.btn:disabled{opacity:.55;cursor:default}.btn-primary{background:var(--accent);color:var(--accent-ink)}.btn-primary:hover:not(:disabled){background:var(--accent-hover)}.btn-ghost{background:var(--bg);color:var(--text);border-color:var(--border)}.btn-ghost:hover:not(:disabled){background:var(--raised)}.btn-danger{color:var(--red);background:0 0;border-color:#f871714d}.btn-danger:hover:not(:disabled){background:var(--red-soft)}.btn-sm{font-size:var(--text-label);border-radius:var(--radius-pill);padding:4px 12px}.btn-icon{appearance:none;font:inherit;cursor:pointer;width:28px;height:28px;color:var(--muted);border-radius:var(--radius-sm);transition:background var(--motion-fast), color var(--motion-fast);background:0 0;border:none;justify-content:center;align-items:center;padding:0;display:inline-flex}.btn-icon:hover{background:var(--raised-hover);color:var(--text)}.btn-icon:focus-visible{outline:2px solid var(--accent-ring);outline-offset:1px}.btn-icon svg{width:16px;height:16px;display:block}.btn.btn-ghost.btn-icon{width:28px;height:28px;padding:0}.card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);min-width:0}.panel{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:18px}.panel-accent{border-color:color-mix(in srgb, var(--accent) 28%, var(--border));background:color-mix(in srgb, var(--accent) 5%, var(--surface))}.api-panel{flex-direction:column;gap:10px;display:flex;overflow:hidden}.api-panel .panel-title,.api-panel .muted{margin:0}.api-panel-head{justify-content:space-between;align-items:baseline;gap:12px;min-width:0;display:flex}.api-panel-head .panel-title{min-width:0}.api-panel-head .muted{text-align:right;flex:none}.api-form-row{align-items:center;gap:8px;min-width:0;display:flex}.api-form-row .input{flex:1;min-width:0}.api-code{min-width:0;color:var(--text);background:var(--raised);border:1px solid var(--border-soft);border-radius:var(--radius-sm);font-family:var(--font-code);font-size:var(--text-label);line-height:var(--leading-relaxed);white-space:pre;margin:0;padding:9px 11px;display:block;overflow-x:auto}.api-code-inline{white-space:nowrap}.api-actions{justify-content:flex-end;align-items:center;gap:4px;display:flex}.dash-sync-summary{justify-content:space-between;align-items:center;gap:16px;display:flex}.dash-sync-copy{flex:auto;min-width:0}.dash-sync-hint{-webkit-line-clamp:2;-webkit-box-orient:vertical;margin:2px 0 0;display:-webkit-box;overflow:hidden}.maintenance-actions{flex:none;justify-content:flex-end;align-items:center;gap:8px;display:flex}.maintenance-update-anchor{background:0 0;border:0;width:0;height:0;padding:0;overflow:hidden}.maintenance-notice{align-items:flex-start;margin:14px 0 0}.action-toast{right:var(--space-6);bottom:var(--space-6);z-index:var(--z-modal);max-width:min(480px,100vw - 48px);box-shadow:var(--shadow);animation:sync-toast-in var(--motion-normal) ease-out;align-items:flex-start;margin:0;position:fixed}@keyframes sync-toast-in{0%{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}.spin-icon{animation:.9s linear infinite spin}.action-toast.notice{max-width:min(480px,100vw - 48px)}.action-toast-dismiss{border-radius:var(--radius-sm);min-width:24px;min-height:24px;color:var(--muted);cursor:pointer;transition:color var(--motion-fast), background var(--motion-fast);background:0 0;border:none;flex:none;justify-content:center;align-self:flex-start;align-items:center;margin:-4px -6px 0 2px;padding:4px;display:inline-flex}.action-toast-dismiss:hover{color:var(--text);background:color-mix(in srgb, var(--muted) 14%, transparent)}.action-toast-dismiss:focus-visible{outline:2px solid var(--accent);outline-offset:1px}.update-row{justify-content:space-between;align-items:center;gap:12px;margin-bottom:14px;display:flex}.update-row .field-label{margin:0}.update-empty{margin-bottom:14px;padding:18px}.update-box{flex-direction:column;gap:12px;display:flex}.update-command{font-size:var(--text-label);flex-wrap:wrap;align-items:flex-start;gap:6px;display:flex}.update-command .chip{white-space:pre-wrap;word-break:break-all;overflow-wrap:anywhere;flex:auto;min-width:0;max-width:100%;line-height:1.5}.update-recheck{flex-wrap:wrap;align-items:center;gap:10px;display:flex}.update-recheck .btn{flex:none}.update-recheck-reason{min-width:0;font-size:var(--text-label);color:var(--muted);flex:200px}.update-restart{border-top:1px solid var(--border-soft);padding-top:12px}.injection-head{flex-wrap:wrap;align-items:center;gap:10px;display:flex}.injection-label{font-weight:var(--weight-semibold)}@media (width<=800px){.injection-head{gap:8px}.injection-label{flex:1 0 calc(100% - 70px);order:1}.injection-head>.badge{order:1;margin-left:auto}.injection-head>.custom-select{order:2}}.stat-row{grid-template-columns:repeat(6,1fr);gap:12px;margin-bottom:28px;display:grid}.stat-row>.stat{flex-direction:column;justify-content:center;min-height:80px;display:flex}@container (width<=820px){.stat-row{grid-template-columns:repeat(3,1fr)}}@container (width<=480px){.stat-row{grid-template-columns:repeat(2,1fr)}}.stat{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);transition:border-color var(--motion-fast);padding:14px 16px}.stat:hover{border-color:var(--faint)}.stat .label{font-size:var(--text-label);color:var(--muted);font-weight:var(--weight-medium);align-items:center;gap:6px;margin-bottom:6px;display:flex}.stat .label svg{width:14px;height:14px}.stat .value{font-size:var(--text-title);font-weight:var(--weight-semibold);letter-spacing:0;line-height:var(--leading-tight)}.stat .value.mono{font-family:var(--font-code);font-size:var(--text-subtitle)}.startup-health-bar{box-sizing:border-box;width:100%;min-height:var(--control-lg);margin:calc(-1 * var(--space-3)) 0 var(--space-6);padding:0 var(--space-3);align-items:center;gap:var(--space-2);min-width:0;color:var(--muted);background:var(--hover);border:none;border-block:1px solid var(--border-soft);font:inherit;font-size:var(--text-control);line-height:var(--leading-ui);text-align:start;cursor:pointer;transition:background var(--motion-fast), color var(--motion-fast);text-decoration:none;display:flex}.startup-health-bar:hover{background:var(--raised);color:var(--text)}.startup-health-bar:active{transform:translateY(1px)}.startup-health-bar:focus-visible{outline:2px solid var(--accent-ring);outline-offset:2px}.startup-health-bar__summary{text-overflow:ellipsis;white-space:nowrap;min-width:0;overflow:hidden}.startup-health-slot{min-height:var(--control-lg)}.startup-health-bar--pending{pointer-events:none;color:var(--faint)}.dash-stat-coverage{min-height:1.25em;margin-top:2px;line-height:1.25}.dash-overview-head{flex-direction:column;margin-bottom:0;display:flex}.mem-head{flex-wrap:wrap;justify-content:space-between;align-items:center;gap:12px;margin-bottom:12px;display:flex}.mem-head-title{align-items:center;gap:8px;display:flex}.mem-head-actions{flex-wrap:wrap;align-items:center;gap:10px;display:flex}.mem-inflight{align-items:baseline;gap:6px;display:inline-flex}.mem-inflight-label{font-size:var(--text-label);color:var(--muted)}.mem-inflight-value{font-size:var(--text-control);font-weight:var(--weight-semibold)}.mem-status{flex-wrap:wrap;align-items:center;gap:10px;margin-top:10px;display:flex}.mem-status:empty{display:none}.mem-stats{grid-template-columns:repeat(4,minmax(0,1fr));margin-bottom:0}@container (width<=660px){.mem-stats{grid-template-columns:repeat(2,minmax(0,1fr))}}@container (width<=380px){.mem-stats{grid-template-columns:minmax(0,1fr)}}.mem-stats .stat-sub{font-size:var(--text-caption);color:var(--faint);margin-top:4px}.stat .value--warn{color:var(--amber)}.stat .value--danger{color:var(--red)}.mem-pressure{border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--raised);margin-bottom:12px;padding:12px 14px}.mem-pressure--warn{border-color:color-mix(in srgb, var(--amber) 42%, var(--border));background:var(--amber-soft)}.mem-pressure--over{border-color:color-mix(in srgb, var(--red) 42%, var(--border));background:var(--red-soft)}.mem-pressure-head{flex-wrap:wrap;justify-content:space-between;align-items:baseline;gap:12px;display:flex}.mem-pressure-label{font-size:var(--text-label);font-weight:var(--weight-medium);color:var(--muted);align-items:baseline;gap:6px;display:inline-flex}.mem-pressure-metric{font-size:var(--text-caption);color:var(--faint)}.mem-pressure-figure{font-size:var(--text-subtitle);font-weight:var(--weight-semibold);line-height:var(--leading-tight);white-space:nowrap}.mem-pressure-limit{color:var(--faint);font-weight:var(--weight-regular)}.mem-pressure-track{border-radius:var(--radius-2xs);background:color-mix(in srgb, var(--muted) 20%, transparent);height:5px;margin:8px 0 6px;overflow:hidden}.mem-pressure-fill{border-radius:var(--radius-2xs);background:var(--green);width:100%;height:100%;transform:scaleX(var(--mem-scale,0));transform-origin:0;transition:transform var(--motion-normal);display:block}.mem-pressure--warn .mem-pressure-fill{background:var(--amber)}.mem-pressure--over .mem-pressure-fill{background:var(--red)}.mem-pressure-foot{font-size:var(--text-caption);color:var(--muted)}.dash-overview-head .stat-row{margin-bottom:0}.dash-overview-head .startup-health-bar,.dash-overview-head .startup-health-bar--pending{margin:var(--space-4) 0 0}.model-group-head{font-size:var(--text-control);font-weight:var(--weight-semibold);color:var(--text);align-items:baseline;gap:8px;margin:0 0 8px;display:flex}.model-group-head .count{font-family:var(--font-code);font-weight:var(--weight-medium);color:var(--faint);font-size:var(--text-label)}.group-head{cursor:pointer;background:var(--surface);transition:background var(--motion-fast);padding:10px 12px}.group-head:hover{background:var(--hover)}.group-head.open{border-bottom:1px solid var(--border-soft)}.models-combos-card{overflow:hidden}.models-combo-row{min-height:var(--control-lg);padding:0 var(--space-3) 0 calc(var(--space-8) + var(--space-0-5));gap:var(--space-2);align-items:center}.model-grid{grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:8px;display:grid}.model-card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-sm);transition:border-color var(--motion-fast), background var(--motion-fast);padding:10px 12px}.model-card:hover{border-color:var(--faint);background:var(--hover)}.model-card .id{font-family:var(--font-code);font-weight:var(--weight-semibold);font-size:var(--text-control);letter-spacing:0;color:var(--text)}.badge{font-size:var(--text-caption);font-weight:var(--weight-semibold);line-height:var(--leading-ui);border-radius:var(--radius-pill);font-family:var(--font-code);letter-spacing:0;border:1px solid #0000;align-items:center;gap:5px;padding:2px 8px;display:inline-flex}.badge-accent{background:var(--accent-soft);color:var(--text)}.badge-green{background:var(--green-soft);color:var(--green)}.badge-amber{background:var(--amber-soft);color:var(--amber)}.badge-muted{background:var(--raised);color:var(--muted);border:1px solid var(--border)}.badge-clickable{cursor:pointer;transition:filter var(--motion-fast);appearance:none}.badge-clickable:hover{filter:brightness(1.1)}.badge-disabled{opacity:.5;cursor:default}.card-badges{flex-wrap:wrap;align-items:center;gap:8px;min-width:0;display:inline-flex}.card-badges .badge{flex-shrink:0}.confirm-icon{border-radius:var(--radius-round);background:var(--amber-soft);width:44px;height:44px;color:var(--amber);justify-content:center;align-items:center;margin:0 auto 12px;display:flex}.credit-list{flex-direction:column;gap:6px;display:flex}.credit-item{border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--raised);transition:border-color var(--motion-fast);padding:8px 10px}.credit-next{border-color:var(--amber);background:var(--amber-soft)}.credit-item-head{color:var(--text);font-size:var(--text-label);font-weight:var(--weight-semibold);align-items:center;gap:6px;margin-bottom:3px;display:flex}.credit-item-head svg{color:var(--amber);flex-shrink:0}.credit-item-dates{font-size:var(--text-caption);font-family:var(--font-code);color:var(--muted);justify-content:space-between;display:flex}.credit-urgent{color:var(--red);font-weight:var(--weight-semibold)}.dot{border-radius:var(--radius-round);flex-shrink:0;width:7px;height:7px}.dot-green{background:var(--green);box-shadow:0 0 0 3px var(--green-soft)}.dot-red{background:var(--red);box-shadow:0 0 0 3px var(--red-soft)}.tbl{border-collapse:collapse;width:100%;font-size:var(--text-control)}.tbl thead th{text-align:left;color:var(--muted);font-weight:var(--weight-medium);font-size:var(--text-label);border-bottom:1px solid var(--border);padding:9px 12px}.tbl tbody td{border-bottom:1px solid var(--border-soft);padding:10px 12px}.tbl tbody tr:last-child td{border-bottom:none}.tbl tbody tr:hover td{background:var(--hover)}.checkbox{cursor:pointer;align-items:center;gap:8px;display:flex}.tbl .num{text-align:right;font-family:var(--font-code);font-variant-numeric:tabular-nums}.tbl-wrap{border:1px solid var(--border);border-radius:var(--radius);background:var(--surface);padding:var(--space-3);overflow-x:auto}.api-models-scroll{overscroll-behavior:contain;min-height:0;max-height:min(360px,50vh);margin-top:.75rem;overflow:auto}.api-models-scroll thead th{z-index:1;background:var(--panel,var(--raised));position:sticky;top:0}.api-models-panel{min-height:0}.awi-overview-section .api-models-panel>.input,.awi-overview-section .api-models-panel>.api-panel-head,.awi-overview-section .api-models-panel>.muted{flex:none}.input,textarea.input{border-radius:var(--radius-sm);background:var(--raised);border:1px solid var(--border);width:100%;color:var(--text);font:inherit;font-size:var(--text-control);line-height:var(--leading-ui);transition:border-color var(--motion-fast);padding:8px 11px}.input::placeholder{color:var(--faint)}.input:focus{border-color:var(--faint);box-shadow:0 0 0 3px var(--accent-soft);outline:none}textarea.input{resize:vertical;font-family:var(--font-code);line-height:var(--leading-relaxed)}.field-label{font-size:var(--text-label);color:var(--muted);font-weight:var(--weight-medium);margin-bottom:5px;display:block}select.input{appearance:none}.select-sm{border-radius:var(--radius-pill);background:color-mix(in oklab, canvas 75%, transparent);-webkit-backdrop-filter:blur(12px)saturate(1.3);border:1px solid var(--border);color:var(--text);font:inherit;font-size:var(--text-control);line-height:var(--leading-ui);cursor:pointer;transition:border-color var(--motion-fast), box-shadow var(--motion-fast);padding:6px 12px;box-shadow:0 2px 8px #0000000f}.select-sm:focus{border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-soft);outline:none}.select-sm:hover:not(:disabled){border-color:var(--faint);box-shadow:0 2px 12px #0000001a}.select-sm:disabled{opacity:.5;cursor:default;-webkit-backdrop-filter:none}.select-trigger{border-radius:var(--radius-pill);background:color-mix(in oklab, canvas 75%, transparent);-webkit-backdrop-filter:blur(12px)saturate(1.3);border:1px solid var(--border);color:var(--text);font:inherit;font-size:var(--text-control);line-height:var(--leading-ui);cursor:pointer;transition:border-color var(--motion-fast), box-shadow var(--motion-fast);white-space:nowrap;align-items:center;gap:6px;padding:6px 12px;display:inline-flex;box-shadow:0 2px 8px #0000000f}.select-trigger:hover:not(:disabled){border-color:var(--faint);box-shadow:0 2px 12px #0000001a}.select-trigger:disabled{opacity:.5;cursor:default}.select-dropdown{z-index:var(--z-popover);background:var(--surface);-webkit-backdrop-filter:blur(20px)saturate(1.4);border:1px solid var(--border);border-radius:var(--radius);min-width:100%;max-height:280px;padding:4px;position:absolute;top:calc(100% + 4px);left:0;overflow-y:auto;box-shadow:0 8px 32px #00000029}.select-dropdown-portal{position:fixed;inset:auto}.select-dropdown-right{right:0;left:auto!important}.select-dropdown-beside{min-width:10rem;max-height:min(60vh,20rem);inset:auto auto 0 calc(100% + 12px);overflow-y:auto}.select-option{text-align:left;border-radius:var(--radius-sm);width:100%;color:var(--text);font:inherit;font-size:var(--text-control);line-height:var(--leading-ui);cursor:pointer;transition:background var(--motion-fast), box-shadow var(--motion-fast);white-space:nowrap;background:0 0;border:none;padding:7px 12px;display:block}.select-option:hover:not(.select-option-active){background:var(--hover)}.select-option.active{background:var(--accent-soft);color:var(--accent);font-weight:var(--weight-semibold)}.select-option-active{background:var(--hover)}.select-option.active.select-option-active{background:var(--accent-soft);box-shadow:inset 0 0 0 1px var(--accent)}.switch{border-radius:var(--radius-pill);border:1px solid var(--border);cursor:pointer;background:var(--raised);vertical-align:middle;width:34px;height:20px;transition:background var(--motion-normal), border-color var(--motion-normal);appearance:none;flex-shrink:0;align-items:center;padding:2px;line-height:0;display:inline-flex}.switch.on{background:var(--toggle-on-bg);border-color:var(--toggle-on-bg)}.switch.mixed{background:var(--amber-soft);border-color:var(--amber)}.switch:disabled{opacity:.6;cursor:default}.switch.switch-labeled{border-radius:var(--radius-sm);gap:var(--space-1);background:0 0;border:0;flex:none;width:auto;height:auto;padding:0;line-height:normal}.switch.switch-labeled:before{content:"";border-radius:var(--radius-pill);border:1px solid var(--border);background:var(--raised);box-sizing:border-box;width:34px;height:20px;transition:background var(--motion-normal), border-color var(--motion-normal);flex-shrink:0}.switch.switch-labeled.on:before{background:var(--toggle-on-bg);border-color:var(--toggle-on-bg)}.switch.switch-labeled.mixed:before{background:var(--amber-soft);border-color:var(--amber)}.switch.switch-labeled{position:relative}.switch.switch-labeled .knob{position:absolute;left:3px}.switch.switch-labeled.on .knob{transform:translate(14px)}.switch.switch-labeled.mixed .knob{transform:translate(7px)}.switch-labeled-text{white-space:nowrap}.switch .knob{border-radius:var(--radius-round);width:14px;height:14px;transition:transform var(--motion-normal), background var(--motion-normal);background:var(--lightningcss-light,#fff)var(--lightningcss-dark,#ececec);transform:translate(0);box-shadow:0 0 0 1px #10182814,0 1px 1px #1018282e}.switch.on .knob{background:var(--toggle-dot-color);transform:translate(14px);box-shadow:0 0 0 1px #0000001f}.switch.mixed .knob{transform:translate(7px)}.muted{color:var(--muted)}.faint{color:var(--faint)}.row{align-items:center;gap:10px;display:flex}.spread{justify-content:space-between;align-items:center;gap:12px;display:flex}.setting-hint{font-size:var(--text-control);line-height:var(--leading-body);max-width:var(--prose-measure);margin-top:3px}.stack{flex-direction:column;display:flex}.chip{font-family:var(--font-code);font-size:var(--text-label);line-height:var(--leading-ui);background:var(--raised);border:1px solid var(--border);border-radius:var(--radius-xs);color:var(--text);padding:1px 7px}.empty{text-align:center;border:1px dashed var(--border);border-radius:var(--radius);color:var(--muted);padding:56px 20px}.empty svg{width:30px;height:30px;color:var(--faint);margin-bottom:12px}.empty .title{color:var(--text);font-weight:var(--weight-semibold);margin-bottom:6px}.notice{font-size:var(--text-control);line-height:var(--leading-body);border-radius:var(--radius-sm);max-width:var(--prose-measure);align-items:center;gap:8px;margin-bottom:14px;padding:9px 12px;display:flex}.notice svg{flex-shrink:0;width:15px;height:15px}.toast-notice-host{z-index:var(--z-modal);pointer-events:none;justify-content:center;align-items:flex-start;padding:12vh 16px 16px;display:flex;position:fixed;inset:0}.toast-notice{pointer-events:auto;max-width:min(var(--prose-measure), calc(100vw - 32px));box-shadow:var(--shadow);animation:toast-notice-in var(--motion-normal) ease-out;margin-bottom:0}.toast-notice-copy{flex:1;min-width:0}.toast-notice-dismiss{color:inherit;opacity:.7;cursor:pointer;background:0 0;border:0;flex-shrink:0;padding:0 2px;font-size:1.1rem;line-height:1}.toast-notice-dismiss:hover{opacity:1}@keyframes toast-notice-in{0%{opacity:0;transform:translateY(-8px)}to{opacity:1;transform:translateY(0)}}@media (prefers-reduced-motion:reduce){.toast-notice{animation:none}}.notice-ok{background:var(--lightningcss-light,#ecfdf5)var(--lightningcss-dark,color-mix(in oklab, var(--green) 18%, var(--surface)));color:var(--lightningcss-light,#065f46)var(--lightningcss-dark,#d1fae5);border:1px solid color-mix(in srgb, var(--green) 32%, transparent)}.notice-ok svg{color:var(--green)}.notice-err{background:var(--lightningcss-light,#fef2f2)var(--lightningcss-dark,color-mix(in oklab, var(--red) 18%, var(--surface)));color:var(--lightningcss-light,#991b1b)var(--lightningcss-dark,#fee2e2);border:1px solid color-mix(in srgb, var(--red) 32%, transparent)}.notice-err svg{color:var(--red)}.h-section{font-size:var(--text-control);font-weight:var(--weight-semibold);line-height:var(--leading-ui);color:var(--text);align-items:center;gap:8px;margin:30px 0 12px;display:flex}.h-section .count{color:var(--muted);font-weight:var(--weight-medium);font-family:var(--font-code);font-size:var(--text-label)}.spin{border:2px solid var(--border);border-top-color:var(--accent);border-radius:var(--radius-round);width:14px;height:14px;animation:.7s linear infinite spin;display:inline-block}@keyframes spin{to{transform:rotate(360deg)}}.data-surface-status{align-items:center;gap:var(--space-2);min-height:var(--control-md);color:var(--muted);font-size:var(--text-control);line-height:var(--leading-body);display:flex}.data-surface-skeleton{gap:var(--space-2);display:grid}.data-surface-skeleton__row{min-height:var(--control-lg)}.data-surface-skeleton__block{min-height:var(--control-lg);border:1px solid var(--border-soft);border-radius:var(--radius-sm);background:linear-gradient(90deg, var(--raised) 0%, var(--surface) 50%, var(--raised) 100%);background-size:200% 100%;width:100%;animation:1.2s ease-in-out infinite codex-auth-skeleton-shimmer;display:block}@media (prefers-reduced-motion:reduce){*{transition:none!important;animation:none!important}}@supports not ((-webkit-backdrop-filter:blur(1px)) or (backdrop-filter:blur(1px))){.sidebar,.lang-toggle .select-dropdown{background:var(--rail)}.select-dropdown,.modal-card{background:var(--surface)}}@media (prefers-reduced-transparency:reduce){body:before{display:none}.sidebar,.lang-toggle .select-dropdown{background:var(--rail);-webkit-backdrop-filter:none}.select-dropdown,.modal-card{background:var(--surface);-webkit-backdrop-filter:none}.modal-overlay{-webkit-backdrop-filter:none}}.modal-overlay{-webkit-backdrop-filter:blur(40px)saturate(1.2);z-index:var(--z-modal);background:var(--lightningcss-light,#11131cc7)var(--lightningcss-dark,#000000d1);justify-content:center;align-items:flex-start;padding:8vh 16px;display:flex;position:fixed;inset:0}dialog.modal-overlay{width:100%;max-width:none;height:100%;max-height:none;color:inherit;border:none;margin:0}dialog.modal-overlay::backdrop{background:0 0}.modal-card{z-index:1;background:color-mix(in oklab, canvas 92%, transparent);-webkit-backdrop-filter:blur(20px)saturate(1.4);border:1px solid var(--border);border-radius:var(--radius-lg);width:100%;max-width:520px;box-shadow:var(--shadow-sm);max-height:84vh;padding:20px;position:relative;overflow-y:auto}.modal-head{justify-content:space-between;align-items:center;margin-bottom:16px;display:flex}.modal-head h3{font-size:var(--text-subtitle)}.card-head{flex-wrap:wrap;align-items:center;gap:8px;min-width:0;padding:10px 16px 4px;display:flex}.card-head strong{overflow-wrap:anywhere;min-width:0}.card-sub{font-size:var(--text-label);line-height:var(--leading-body);color:var(--muted);overflow-wrap:anywhere;min-width:0;padding:0 16px 8px}.card-active{border-color:var(--accent-ring)}.card-right{font-size:var(--text-caption);color:var(--faint);align-items:center;gap:4px;margin-left:auto;display:flex}.btn-icon-danger.card-right{appearance:none;color:var(--red);cursor:pointer;background:0 0;border:1px solid #0000;justify-content:center}.btn-icon-danger.card-right:hover{background:var(--red-soft);border-color:var(--red-soft);color:var(--red)}.card-row{justify-content:space-between;align-items:center;padding:14px 16px;display:flex}.badge-primary{background:var(--accent-soft);color:var(--accent-hover)}.dot-blue{background:var(--accent);box-shadow:0 0 0 3px var(--accent-soft)}.dot-muted{background:var(--muted)}.dot-amber{background:var(--amber);box-shadow:0 0 0 3px var(--amber-soft)}.section-sep{align-items:center;gap:10px;margin:20px 0 12px;display:flex}.section-label{font-size:var(--text-label);color:var(--muted);font-weight:var(--weight-medium);white-space:nowrap}.sep-line{background:var(--border);flex:1;height:1px}.quota-compact{gap:4px;padding:0 16px 10px;display:grid}.codex-account-quota-slot{box-sizing:border-box;min-height:22px}.codex-auth-load-skeleton{display:contents}.codex-auth-load-skeleton__main{border-color:var(--border)}.codex-auth-load-skeleton__line{vertical-align:middle;background:linear-gradient(90deg, var(--raised) 0%, var(--surface) 50%, var(--raised) 100%);background-size:200% 100%;border-radius:4px;height:.9em;animation:1.2s ease-in-out infinite codex-auth-skeleton-shimmer;display:inline-block}.codex-auth-load-skeleton__line--sub{width:12rem;position:absolute;top:.35em;left:16px}.codex-auth-load-skeleton__main .card-sub{min-height:calc(var(--leading-body) * 1em + 10px);position:relative}.codex-auth-load-skeleton__strut{visibility:hidden;white-space:nowrap;display:inline-block}.codex-auth-load-skeleton__empty{pointer-events:none}.codex-auth-pool-empty{box-sizing:border-box}.openai-account-mode-banner__desc{margin:6px 0 0;padding-left:0;padding-right:0}.openai-account-mode-banner__desc--pending{visibility:hidden;min-height:1.35em}.openai-account-mode-banner__badge-slot{justify-content:center;min-width:4.5rem}.codex-ticket-badge-slot{visibility:hidden;pointer-events:none;justify-content:center;min-width:2.25rem}.quota-compact--pending{box-sizing:border-box;height:22px;min-height:22px;max-height:22px;overflow:hidden}.quota-compact--pending .quota-row--skeleton{height:18px;min-height:0}.quota-skel{border-radius:var(--radius-2xs);background:linear-gradient(90deg, var(--raised) 0%, var(--surface) 50%, var(--raised) 100%);background-size:200% 100%;height:8px;animation:1.2s ease-in-out infinite codex-auth-skeleton-shimmer;display:block}.quota-skel--reset{width:34px}.quota-skel--bar{align-self:center;width:100%;height:5px}.quota-skel--val{justify-self:end;width:28px}.quota-row{grid-template-columns:minmax(34px,max-content) max-content minmax(34px,max-content) minmax(38px,max-content) minmax(58px,1fr) minmax(30px,max-content);align-items:center;gap:8px;min-width:0;display:grid}.quota-label{font-size:var(--text-caption);color:var(--muted);font-weight:var(--weight-semibold)}.quota-val{font-size:var(--text-caption);font-family:var(--font-code);color:var(--text);text-align:right}.quota-reset-label{text-overflow:ellipsis;white-space:nowrap;font-size:var(--text-caption);color:var(--faint);overflow:hidden}.quota-reset-day,.quota-reset-time{font-size:var(--text-caption);color:var(--muted);white-space:nowrap}.quota-reset-time{font-family:var(--font-code)}.bar{background:var(--raised);border-radius:var(--radius-2xs);min-width:0;height:5px;overflow:hidden}.bar-fill{border-radius:var(--radius-2xs);width:100%;height:100%;transform:scaleX(var(--bar-scale,0));transform-origin:0;transition:transform var(--motion-normal)}.bar-green{background:var(--green)}.bar-amber{background:var(--amber)}.quota-row--skeleton{min-height:18px}.quota-skel{border-radius:var(--radius-2xs);background:color-mix(in srgb, var(--muted) 22%, transparent);min-height:8px;display:inline-block}.quota-skel--label,.quota-skel--reset{width:34px}.quota-skel--day{width:36px}.quota-skel--time{width:38px}.quota-skel--bar{width:100%;min-height:5px}.quota-skel--val{width:30px}.openai-account-mode-banner__badge-slot--pending{visibility:hidden}.toggle{width:var(--toggle-w);height:var(--toggle-h);border-radius:var(--radius-pill);background:var(--toggle-off-bg);border:1px solid var(--border);cursor:pointer;transition:background var(--motion-normal), border-color var(--motion-normal);flex-shrink:0;position:relative}.toggle.on{background:var(--toggle-on-bg);border-color:var(--toggle-on-bg)}.toggle-knob{border-radius:var(--radius-round);width:16px;height:16px;transition:left var(--motion-normal), background var(--motion-normal);background:var(--lightningcss-light,#fff)var(--lightningcss-dark,#ececec);position:absolute;top:50%;left:1px;transform:translateY(-50%)}.toggle.on .toggle-knob{background:var(--toggle-dot-color);left:17px}.toggle:disabled{cursor:not-allowed;opacity:.55}.codex-auto-switch-card{flex-wrap:wrap;gap:16px}.codex-auto-switch-copy{flex:auto;min-width:0}.codex-auto-switch-copy .card-sub{padding:2px 0 0}.codex-auto-switch-controls{flex:none;align-items:flex-end;gap:12px;margin-left:auto;display:flex}.codex-request-user-input-card{flex-wrap:wrap;gap:16px}.codex-request-user-input-copy{flex:auto;min-width:0}.codex-request-user-input-copy .card-sub{padding:2px 0 0}.codex-request-user-input-config{overflow-wrap:anywhere;font-size:var(--text-label);color:var(--muted);margin-top:6px;display:block}.codex-request-user-input-controls{flex:none;align-items:flex-end;gap:12px;margin-left:auto;display:flex}.codex-request-user-input-controls>.toggle{margin-left:auto}.codex-request-user-input-feedback{color:var(--muted);font-size:var(--text-label);line-height:var(--leading-body);text-align:right;flex:1 0 100%;margin-top:-8px}.codex-request-user-input-feedback.is-error{color:var(--red)}.codex-account-picker-card{flex-wrap:wrap;gap:16px;margin-top:16px}.codex-auth-advanced{margin-top:8px}.codex-auth-advanced__toggle{width:100%;color:var(--muted);font-size:var(--text-label);font-weight:var(--weight-medium);cursor:pointer;text-align:left;background:0 0;border:0;align-items:center;gap:6px;padding:8px 2px;display:flex}.codex-auth-advanced__toggle:hover{color:var(--text)}.codex-auth-advanced__chevron{transition:transform var(--motion-fast,.12s)}.codex-auth-advanced__chevron.is-open{transform:rotate(90deg)}.codex-auth-advanced__boxes{gap:0;display:grid}.codex-account-picker-copy{flex:34rem;min-width:0}.codex-account-picker-copy .card-sub{padding:4px 0 0}.codex-account-picker-controls{flex:none;align-items:center;gap:12px;margin-left:auto;display:flex}.codex-account-picker-feedback{font-size:var(--text-label);line-height:var(--leading-body);text-align:right;flex:1 0 100%;margin-top:-8px}.codex-account-picker-feedback.is-ok{color:var(--green)}.codex-account-picker-feedback.is-warn{color:var(--amber)}.codex-account-picker-feedback.is-err{color:var(--red)}.codex-auto-switch-toggle-slot{flex:none;justify-content:flex-end;align-items:center;min-height:36px;margin-left:auto;display:inline-flex}.codex-auto-switch-threshold{flex-direction:column;align-items:flex-start;margin:0;display:flex}.codex-auto-switch-threshold .field-label{white-space:nowrap;margin-bottom:4px}.codex-auto-switch-input-wrap{border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--surface);align-items:stretch;gap:0;width:max-content;max-width:100%;min-height:32px;display:inline-flex;overflow:hidden}.codex-auto-switch-input-wrap:focus-within{border-color:var(--accent-ring);box-shadow:0 0 0 1px var(--accent-ring)}.codex-auto-switch-input-wrap .input,.codex-auto-switch-input-wrap .codex-auto-switch-input{box-shadow:none;background:0 0;border:none;border-radius:0;align-self:stretch;height:auto;min-height:0}.codex-auto-switch-input-wrap .input:focus{box-shadow:none;border-color:#0000}.codex-auto-switch-input{text-align:right;font-variant-numeric:tabular-nums;width:84px}.codex-auto-switch-input[readonly]{cursor:progress;opacity:.7}.codex-auto-switch-input::-webkit-outer-spin-button{-webkit-appearance:none;margin:0}.codex-auto-switch-input::-webkit-inner-spin-button{-webkit-appearance:none;margin:0}.codex-auto-switch-input[type=number]{appearance:textfield}.codex-auto-switch-unit{color:var(--muted);font-size:var(--text-control);flex:none;align-items:center;padding-right:8px;display:inline-flex}.codex-auto-switch-feedback{color:var(--muted);font-size:var(--text-label);line-height:var(--leading-body);text-align:right;flex:1 0 100%;margin-top:-8px}.codex-auto-switch-feedback.is-error{color:var(--red)}.ocx-stepper{flex-direction:column;flex:none;gap:2px;display:inline-flex}.ocx-stepper__btn{appearance:none;border:1px solid var(--border);border-radius:var(--radius-2xs);background:var(--raised);width:22px;height:16px;color:var(--muted);cursor:pointer;justify-content:center;align-items:center;padding:0;display:inline-flex}.ocx-stepper__btn:hover:not(:disabled){color:var(--text);border-color:var(--faint)}.ocx-stepper__btn:disabled{opacity:.45;cursor:not-allowed}.ocx-stepper__btn:focus-visible{outline:2px solid var(--accent-ring);outline-offset:1px}.ocx-stepper__btn svg{display:block}.codex-auto-switch-input-wrap .ocx-stepper{border-left:1px solid var(--border);align-self:stretch;gap:0}.codex-auto-switch-input-wrap .ocx-stepper__btn{background:var(--raised);border:none;border-radius:0;flex:1 1 0;width:26px;height:auto;min-height:0}.codex-auto-switch-input-wrap .ocx-stepper__btn+.ocx-stepper__btn{border-top:1px solid var(--border)}.codex-auto-switch-input-wrap .ocx-stepper__btn:hover:not(:disabled){background:var(--surface);border-color:#0000}.codex-auto-switch-input-wrap .ocx-stepper__btn:focus-visible{outline-offset:-2px;z-index:1}.codex-auth-page-head{align-items:flex-start}.codex-auth-page-head__actions{align-items:center;gap:10px;min-width:0;display:flex}.codex-auth-spark-toggle{white-space:nowrap;align-items:center;gap:8px;display:inline-flex}.codex-auth-spark-toggle__label{color:var(--muted);font-size:12px}.codex-auth-action-btn:hover:not(:disabled){background:var(--raised-hover);border-color:var(--faint);color:var(--text)}.codex-auth-action-btn.btn-primary:hover:not(:disabled){background:var(--accent-hover);color:var(--accent-ink);border-color:#0000}.codex-auth-action-btn:focus-visible{outline:2px solid var(--accent-ring);outline-offset:1px}.codex-auth-page-head__feedback{min-width:8rem;max-width:18rem;min-height:calc(var(--leading-body) * 1em);text-align:right;font-size:var(--text-label);line-height:var(--leading-body);color:var(--muted);text-overflow:ellipsis;white-space:nowrap;justify-content:flex-end;align-items:center;display:inline-flex;overflow:hidden}.codex-auth-page-head__feedback.is-ok{color:var(--green)}.codex-auth-page-head__feedback.is-warn{color:var(--amber);text-overflow:clip;white-space:normal;overflow:visible}.codex-auth-page-head__feedback.is-err{color:var(--red)}.codex-auth-pause-label{text-align:center;display:inline-block}@keyframes codex-auth-skeleton-shimmer{0%{background-position:100% 0}to{background-position:-100% 0}}.account-pool-strategy-card{gap:8px;margin-top:16px;padding:14px 16px;display:grid}.account-pool-strategy-card>strong{margin:0;display:block}.account-pool-strategy-card>.card-sub,.account-pool-strategy-controls>.card-sub,.account-pool-strategy-controls .field .card-sub,.anthropic-pool-card__field .card-sub{margin:0;padding:0}.account-pool-strategy-card__retry{justify-self:start}.account-pool-strategy-card__error{color:var(--danger,#c44)}.account-pool-strategy-controls{gap:8px;margin:0;display:grid}.account-pool-strategy-controls .field{gap:6px;margin:0;display:grid}.account-pool-strategy-controls .field-label{margin-bottom:0}.account-pool-strategy-controls .custom-select{width:100%;max-width:100%}.account-pool-strategy-controls .select-trigger{border-radius:var(--radius-sm);justify-content:space-between;width:100%;max-width:100%}.account-pool-strategy-controls .select-trigger>span{text-overflow:ellipsis;white-space:nowrap;min-width:0;overflow:hidden}.anthropic-pool-card{margin-top:12px}.anthropic-pool-card__notice{border:1px solid var(--border,#c9a227);background:color-mix(in srgb, var(--warn,#c9a227) 12%, transparent);border-radius:6px;margin:10px 16px 0;padding:10px 16px}.anthropic-pool-card__field{margin-top:12px;padding:0 16px;display:block}.anthropic-pool-card__field--quota-window{padding-bottom:var(--space-4)}.api-active-keys-skeleton{border:1px solid var(--border-soft);border-radius:var(--radius);background:linear-gradient(90deg, var(--raised) 0%, var(--surface) 50%, var(--raised) 100%);background-size:200% 100%;min-height:96px;animation:1.2s ease-in-out infinite codex-auth-skeleton-shimmer}.notice-warn{font-size:var(--text-label);line-height:var(--leading-body);border-radius:var(--radius-sm);background:var(--lightningcss-light,#fffbeb)var(--lightningcss-dark,color-mix(in oklab, var(--amber) 20%, var(--surface)));color:var(--lightningcss-light,#92400e)var(--lightningcss-dark,#fef3c7);border:1px solid color-mix(in srgb, var(--amber) 32%, transparent);max-width:var(--prose-measure);align-items:center;gap:6px;margin-bottom:12px;padding:8px 10px;display:flex}.notice-warn svg{width:14px;height:14px;color:var(--amber);flex-shrink:0}.startup-runtime-notice__text,.startup-runtime-notice__fix code{color:inherit}.codex-pool-strategy-card{padding:14px 16px}.codex-pool-strategy-card .card-sub{padding:0}.codex-account-more{flex-wrap:wrap;align-items:center;display:inline-flex}.codex-account-more>summary{cursor:pointer;justify-content:center;min-width:28px;list-style:none}.codex-account-more>summary::-webkit-details-marker{display:none}.codex-account-more-body{flex-wrap:wrap;flex-basis:100%;align-items:center;gap:8px;padding-top:6px;display:flex}.codex-account-identity{justify-content:space-between;align-items:center;gap:8px;min-width:0;padding:0 16px 6px;display:flex}.codex-account-identity-copy{font-size:var(--text-label);line-height:var(--leading-body);color:var(--muted);overflow-wrap:anywhere;min-width:0}.codex-account-priority{flex-wrap:wrap;flex:none;align-items:center;gap:8px;min-width:0;padding:0;display:flex}.codex-account-priority-label{font-size:var(--text-label);color:var(--muted);font-weight:var(--weight-medium);white-space:nowrap}.codex-account-priority .select-trigger{max-width:100%;font-size:var(--text-label);padding:4px 9px}.startup-page-head-actions{flex-shrink:0;align-items:center;gap:8px;display:flex}.startup-runtime-notice-slot{min-height:52px;margin-bottom:12px}.startup-runtime-notice-slot--pending{border-radius:var(--radius-sm);background:color-mix(in srgb, var(--amber-soft) 45%, transparent)}.startup-runtime-notice-slot .startup-runtime-notice{margin-bottom:0}.notice.notice-warn.startup-page-notice{box-sizing:border-box;width:100%;max-width:none}.notice.notice-warn.startup-runtime-notice{flex-direction:column;align-items:stretch;gap:8px;display:flex}.startup-runtime-notice__text{max-width:none;margin:0}.startup-runtime-notice__fix{justify-content:space-between;align-items:center;gap:12px;min-width:0;display:flex}.startup-runtime-notice__fix code{overflow-wrap:anywhere;flex:auto;min-width:0;margin:0}.startup-runtime-notice__fix .btn{flex:none}.startup-hero{align-items:flex-start;gap:16px;margin-bottom:16px;display:flex}.startup-hero--safe{border-color:color-mix(in srgb, var(--green) 34%, var(--border));background:color-mix(in srgb, var(--green-soft) 64%, var(--surface))}.startup-hero--risk{border-color:color-mix(in srgb, var(--amber) 40%, var(--border));background:color-mix(in srgb, var(--amber-soft) 72%, var(--surface))}.startup-hero--native{border-color:color-mix(in srgb, var(--accent) 20%, var(--border))}.startup-hero-icon{border-radius:var(--radius);background:var(--raised);flex:none;place-items:center;width:42px;height:42px;display:grid}.startup-hero-icon svg{width:21px;height:21px}.startup-hero-copy h3{font-size:var(--text-title);margin:10px 0 4px}.startup-hero-copy p{color:var(--muted);line-height:var(--leading-body);max-width:var(--prose-measure);margin:0}.startup-state-line{font-size:var(--text-control);margin:8px 0 0}.startup-recovery-details>summary{cursor:pointer;align-items:center;gap:6px;list-style:none;display:inline-flex}.startup-recovery-details>summary::-webkit-details-marker{display:none}.startup-recovery-details>summary:before{content:"";border-left:5px solid var(--muted);width:0;height:0;transition:transform var(--motion-fast);border-top:4px solid #0000;border-bottom:4px solid #0000}.startup-recovery-details[open]>summary:before{transform:rotate(90deg)}.startup-recovery-details[open]>summary{margin-bottom:8px}.startup-details,.startup-actions{margin-bottom:16px}.startup-actions .panel-head>svg{flex:none;width:18px;height:18px}.startup-detail-row{border-top:1px solid var(--border-soft);justify-content:space-between;align-items:center;gap:16px;padding:12px 0;display:flex}.startup-detail-row>div{flex-direction:column;gap:3px;min-width:0;display:flex}.startup-detail-row>.startup-detail-actions{flex-direction:row;flex:none;justify-content:flex-end;align-items:center;gap:8px}.startup-detail-row span:not(.badge){color:var(--muted);font-size:var(--text-label);line-height:var(--leading-body)}.startup-actions>.muted{font-size:var(--text-control);max-width:var(--prose-measure);margin:-4px 0 14px}.startup-tray-buttons{flex-wrap:wrap;gap:10px;min-height:36px;margin-top:16px;display:flex}.startup-tray-error{margin-top:12px}.startup-command-list{border:1px solid var(--border-soft);border-radius:var(--radius-sm);overflow:hidden}.startup-command-row{justify-content:space-between;align-items:center;gap:16px;padding:12px;display:flex}.startup-command-row+.startup-command-row{border-top:1px solid var(--border-soft)}.startup-command-row>div{flex-direction:column;gap:5px;min-width:0;display:flex}.startup-command-row strong{font-size:var(--text-control)}.startup-command-row code{color:var(--muted);font-size:var(--text-label);overflow-wrap:anywhere}.startup-action-notice{margin:14px 0 0}@media (width<=700px){.startup-command-row,.startup-detail-row{align-items:flex-start}.startup-detail-row>.startup-detail-actions{flex-direction:column;align-items:flex-end}}.modal-desc{font-size:var(--text-control);line-height:var(--leading-body);color:var(--muted);max-width:var(--prose-measure);margin-bottom:14px}.modal-actions{gap:8px;margin-top:16px;display:flex}.modal-actions .btn{flex:1}.log-reqid{-webkit-line-clamp:2;word-break:break-all;-webkit-box-orient:vertical;max-width:14ch;display:-webkit-box;overflow:hidden}.main-inner:has(.logs-page){max-width:1200px}.log-col-model{overflow-wrap:break-word;word-break:normal;max-width:16ch}.log-col-tokens{min-width:10ch}.log-col-time{white-space:nowrap;vertical-align:middle}.log-col-duration{white-space:nowrap}table.logs-table{table-layout:fixed;width:100%;min-width:1100px}.logs-table col.logs-col-time{width:12%}.logs-table col.logs-col-tokens{width:9%}.logs-table col.logs-col-rate{width:7%}.logs-table col.logs-col-cost{width:8%}.logs-table col.logs-col-model{width:15%}.logs-table col.logs-col-effort{width:9%}.logs-table col.logs-col-provider{width:13%}.logs-table col.logs-col-status{width:8%}.logs-table col.logs-col-request{width:11%}.logs-table col.logs-col-duration{width:8%}.log-col-rate{white-space:nowrap;min-width:7ch}.log-col-cost{white-space:nowrap;min-width:10ch}.logs-table tbody td{overflow:hidden}.log-reasoning-cell{overflow-wrap:anywhere}.log-status-cell{align-items:flex-start;gap:var(--space-0-5);min-width:7ch;line-height:var(--leading-tight);flex-direction:column;display:inline-flex}.log-detail-btn{cursor:pointer;color:var(--accent-hover);font:inherit;font-size:var(--text-caption);white-space:normal;text-align:left;background:0 0;border:none;padding:0;text-decoration:underline}.log-detail-btn:focus-visible{outline-offset:-2px}.logs-auto-refresh{align-items:center;gap:var(--space-2);cursor:pointer;display:inline-flex}.logs-toolbar{align-items:center;gap:var(--space-2);margin-bottom:var(--space-3);flex-wrap:wrap;display:flex}.logs-segmented{border-radius:var(--radius-pill);background:var(--surface-soft,var(--raised));padding:var(--space-1);gap:var(--space-1);display:inline-flex}.logs-segmented .btn{border-radius:var(--radius-pill);min-width:64px;padding:var(--space-1-5) var(--space-3);border:none}.logs-filter-field{align-items:center;gap:var(--space-2);display:inline-flex}.logs-filter-field .input{min-width:220px;max-width:360px}.logs-conversation-totals{margin-bottom:var(--space-3)}.logs-table-wrap{overflow-anchor:none;scrollbar-gutter:stable;max-height:calc(100dvh - 260px);overflow-y:auto}.logs-table thead{z-index:1;background:var(--surface);position:sticky;top:0}.logs-virtual-spacer{border:0;padding:0}.logs-stack-end{align-items:flex-end;gap:var(--space-0-5);flex-direction:column;display:inline-flex}.logs-stack-start{align-items:flex-start;gap:var(--space-0-5);flex-direction:column;display:inline-flex}.logs-model-cell{align-items:center;gap:var(--space-2);flex-wrap:wrap;display:inline-flex}.logs-detail-info{margin-left:var(--space-2)}.log-detail-grid{gap:var(--space-2) var(--space-3);font-size:var(--text-control);grid-template-columns:max-content minmax(0,1fr);display:grid}.log-detail-break{word-break:break-all}.log-detail-card{max-width:760px}.log-detail-section{border-top:1px solid var(--border-soft);padding:14px 0}.log-detail-section:first-of-type{border-top:0;padding-top:0}.log-detail-section-title{font-size:var(--text-label);font-weight:var(--weight-semibold);color:var(--text);margin:0 0 10px}.log-detail-request-row{justify-content:space-between;align-items:center;gap:8px;min-width:0;display:flex}.log-detail-request-row>span{min-width:0}.log-detail-notes{color:var(--muted);font-size:var(--text-label);margin:10px 0 0;padding-left:18px}.log-detail-notes-line{font-size:var(--text-label);margin:8px 0 0}.log-detail-attempts-wrap{border:1px solid var(--border-soft);border-radius:var(--radius-sm);overflow-x:auto}.log-detail-attempts{min-width:680px}.log-detail-attempts th,.log-detail-attempts td{vertical-align:top}.log-detail-raw{margin-top:14px}.log-detail-raw>summary{cursor:pointer;color:var(--muted);font-size:var(--text-label)}.log-detail-raw[open]>summary{margin-bottom:6px}.usage-cost-row{border:1px solid var(--border-soft);border-radius:var(--radius-sm);background:var(--raised);align-items:baseline;gap:10px;margin:10px 0 4px;padding:10px 14px;display:flex}.usage-cost-value{font-size:var(--text-lg,1.15em)}@media (width<=760px){.modal-overlay{padding:16px 10px}.log-detail-card{max-height:calc(100dvh - 32px);padding:16px}.log-detail-grid{grid-template-columns:minmax(7rem,max-content) minmax(0,1fr);gap:8px 10px}.log-detail-request-row{flex-direction:column;align-items:flex-start}}.log-detail-json{background:var(--raised);border:1px solid var(--border-soft);border-radius:var(--radius-sm);font-family:var(--font-code);font-size:var(--text-caption);line-height:var(--leading-body);white-space:pre-wrap;word-break:break-all;max-height:40vh;margin:0;padding:10px 12px;overflow:auto}.setup-guide{font-size:var(--text-control);line-height:var(--leading-body);border:1px solid var(--border);border-radius:var(--radius-sm);margin-bottom:4px;padding:8px 12px}.setup-guide summary{cursor:pointer;color:var(--accent-hover);font-weight:var(--weight-medium)}.setup-guide summary:hover{text-decoration:underline}.setup-guide a{color:var(--accent-hover)}.list-row{text-align:left;border-radius:var(--radius-sm);border:1px solid var(--border);background:var(--raised);cursor:pointer;width:100%;color:var(--text);font:inherit;transition:background var(--motion-fast), border-color var(--motion-fast);justify-content:space-between;align-items:center;gap:10px;padding:11px 13px;display:flex}.list-row:hover{background:var(--raised-hover);border-color:var(--accent-ring)}.list-row .title{font-weight:var(--weight-semibold);font-size:var(--text-body)}.list-row .sub{font-size:var(--text-label);color:var(--muted);margin-top:2px}.prov-card{overflow:hidden}.prov-card-main{flex-wrap:wrap;justify-content:space-between;align-items:flex-start;gap:12px;padding:15px 16px;display:flex}.prov-card-disabled{opacity:.62}.prov-card-info{flex:360px;align-items:flex-start;gap:11px;min-width:0;display:flex}.prov-card-copy{flex:1;min-width:0}.prov-title{flex-wrap:wrap;align-items:center;gap:8px;min-width:0;margin-bottom:5px;display:flex}.provider-icon{border-radius:var(--radius-xs);background:var(--raised);border:1px solid var(--border-soft);width:31px;height:31px;color:var(--text);flex:none;justify-content:center;align-items:center;display:inline-flex}.provider-icon img{object-fit:contain;width:19px;height:19px;display:block}.provider-icon-mask{background:currentColor;width:19px;height:19px;display:block;-webkit-mask-position:50%;mask-position:50%;-webkit-mask-size:contain;mask-size:contain;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}.provider-icon-sm .provider-icon-mask{width:15px;height:15px}.provider-icon--plate{background:#f4f4f4;border-color:#0000001f}.provider-icon--plate-dark{background:#1c1c1c;border-color:#ffffff24}.prov-meta{flex-wrap:wrap;align-items:center;gap:5px;min-width:0;display:flex}.prov-meta>span{text-overflow:ellipsis;white-space:nowrap;min-width:0;max-width:100%;overflow:hidden}.provider-quota{padding-left:58px}.provider-actions{flex-shrink:0;align-items:center;gap:8px;margin-left:auto;display:flex}.openai-mode-row{flex-wrap:wrap;align-items:center;gap:10px;margin-top:10px;display:flex}.openai-mode-control .usage-segmented-btn{min-width:76px}.link-btn{color:var(--accent-hover);font:inherit;font-size:var(--text-control);cursor:pointer;background:0 0;border:none;padding:6px 2px;text-decoration:underline}.link-btn svg{flex-shrink:0;width:14px;height:14px}.prov-accounts-toggle{border:none;border-top:1px solid var(--border-soft);width:100%;color:var(--muted);font:inherit;font-size:var(--text-label);cursor:pointer;background:0 0;justify-content:center;align-items:center;gap:6px;min-height:24px;padding:4px 0;display:flex}.prov-accounts-toggle:hover{color:var(--text);background:var(--raised)}.prov-accounts-toggle .chev{transition:transform var(--motion-normal) ease;display:inline-flex}.prov-accounts-toggle .chev svg{width:12px;height:12px;transform:rotate(90deg)}.prov-accounts-toggle.open .chev svg{transform:rotate(-90deg)}.prov-accounts-list{border-top:1px solid var(--border-soft);flex-direction:column;gap:2px;padding:6px 16px 10px;display:flex}.prov-account-row{border-radius:var(--radius-xs);align-items:center;gap:8px;width:100%;min-height:32px;padding:6px 8px;display:flex}button.prov-account-row{text-align:left;color:var(--text);font:inherit;font-size:var(--text-control);line-height:var(--leading-ui);cursor:pointer;background:0 0;border:none}button.prov-account-row:hover,.prov-account-row:hover{background:var(--raised)}button.prov-account-row.active{cursor:default}.prov-account-row-main{appearance:none;min-width:0;color:inherit;text-align:left;cursor:pointer;font:inherit;font-size:var(--text-control);line-height:var(--leading-ui);background:0 0;border:0;flex:auto;align-items:center;gap:8px;padding:0;display:flex}.prov-account-row-main:disabled{cursor:default;opacity:.72}.prov-account-row .prov-account-email{text-overflow:ellipsis;white-space:nowrap;flex:auto;min-width:0;overflow:hidden}.prov-account-row .badge{flex:none}.prov-account-reauth{color:var(--accent);cursor:pointer;font:inherit;font-size:var(--text-label);border-radius:var(--radius-xs);background:0 0;border:none;flex:none;padding:4px 6px}.prov-account-reauth:hover:not(:disabled){color:var(--accent-hover);background:var(--raised)}.prov-account-reauth:disabled{opacity:.6;cursor:default}.prov-account-remove{color:var(--muted);cursor:pointer;border-radius:var(--radius-xs);background:0 0;border:none;flex:none;justify-content:center;align-items:center;padding:4px;display:inline-flex}.prov-account-remove:hover{color:var(--red);background:var(--red-soft)}.prov-account-add{color:var(--muted);font-size:var(--text-label)}.prov-account-add:hover{color:var(--accent-hover)}.prov-account-keyform{cursor:default;gap:6px}.prov-account-keyform:hover{background:0 0}.prov-account-keyform .input-sm{min-width:0;font-size:var(--text-label);height:var(--control-sm);flex:auto;padding:4px 8px}.oauth-grid{grid-template-columns:minmax(140px,max-content) minmax(0,1fr) max-content;align-items:center;gap:10px 16px;display:grid}.oauth-row{grid-column:1/-1;grid-template-columns:subgrid;align-items:center;min-height:30px;display:grid}.oauth-name{font-size:var(--text-control);font-weight:var(--weight-semibold);align-items:center;gap:8px;min-width:0;display:inline-flex}.oauth-name-text{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.provider-icon-sm{border-radius:var(--radius-xs);width:24px;height:24px}.provider-icon-sm img{width:15px;height:15px}.oauth-status{font-size:var(--text-control);align-items:center;gap:7px;min-width:0;display:inline-flex}.oauth-email{text-overflow:ellipsis;white-space:nowrap;min-width:0;overflow:hidden}.oauth-actions{justify-content:flex-end;align-items:center;gap:8px;min-width:0;display:inline-flex}.oauth-login-hint{font-size:var(--text-label);line-height:var(--leading-body);flex-direction:column;grid-column:1/-1;align-items:stretch;gap:8px;display:flex}.oauth-login-hint-links{flex-wrap:wrap;align-items:center;gap:8px;display:inline-flex}.oauth-device-code-wrap{border:1px solid var(--border);background:var(--surface);border-radius:10px;flex-wrap:wrap;align-items:center;gap:10px;padding:12px;display:flex}.oauth-device-code-label{font-size:var(--text-label);color:var(--text);font-weight:600}.oauth-device-code{letter-spacing:.14em;color:var(--text);-webkit-user-select:all;user-select:all;font-size:20px;font-weight:800}.oauth-login-paste{align-items:center;gap:8px;width:100%;display:flex}.oauth-login-paste .input{min-width:0;font-size:var(--text-label);flex:1;padding:6px 10px}@media (width<=760px){.app{grid-template-rows:auto 1fr;grid-template-columns:1fr}.mobile-topbar{z-index:var(--z-sticky);border-bottom:1px solid var(--border);background:var(--glass-rail);min-width:0;-webkit-backdrop-filter:var(--glass-blur);align-items:center;gap:2px;padding:4px 10px;display:flex;position:sticky;top:0}.mobile-topbar .brand{flex:auto;min-width:0;padding:4px}.mobile-topbar .brand .name{text-overflow:ellipsis;white-space:nowrap;min-width:0;overflow:hidden}.mobile-topbar .brand .ver{flex-shrink:0}.mobile-topbar .stop-toggle{justify-content:center;width:auto;min-width:44px;min-height:44px;padding:8px}.mobile-topbar-actions{flex:none;align-items:center;gap:6px;display:flex}.mobile-topbar-actions .sidebar-orb{flex:0 0 44px;width:44px;min-width:44px;height:44px;min-height:44px}.mobile-topbar-actions .sidebar-orb svg{width:18px;height:18px}.menu-toggle{display:inline-flex}.sidebar{z-index:var(--z-popover);width:min(280px,84vw);height:100dvh;padding-bottom:calc(18px + env(safe-area-inset-bottom));visibility:hidden;transition:transform var(--motion-normal) ease, visibility var(--motion-normal);background:var(--lightningcss-light,#f9f9f9f7)var(--lightningcss-dark,#171717f5);outline:none;position:fixed;top:0;bottom:0;left:0;overflow-y:auto;transform:translate(-100%)}.sidebar.open{visibility:visible;transform:translate(0);box-shadow:0 12px 40px var(--lightningcss-light,#14141438)var(--lightningcss-dark,#00000080)}.drawer-scrim{display:block}.main-inner{padding:22px 18px 48px}.main-inner.main-inner--combos{height:100%;min-height:0;padding:0;overflow:hidden}.main-inner.main-inner--combos>.page-head,.main-inner.main-inner--combos>.page-tabs,.main-inner.main-inner--combos>.page-sub{padding-inline:18px}.main-inner.main-inner--combos>.page-tabs{margin-inline:18px;padding-inline:0}.main-inner.main-inner--combos:not(:has(.combos-workspace-shell))>.models-tab-panel--fill:not([hidden]){padding-inline:0}.main-inner.main-inner--combos:not(:has(.combos-workspace-shell)){padding:22px 18px 48px}.main-inner.main-inner--combos:not(:has(.combos-workspace-shell))>.page-head{padding-inline:0}.main-inner.main-inner--combos:not(:has(.combos-workspace-shell))>.page-tabs{padding-inline:0}.main-inner.main-inner--combos:not(:has(.combos-workspace-shell))>.page-sub{padding-inline:0}.main-inner.main-inner--combos:not(:has(.combos-workspace-shell))>.page-tabs{margin-inline:0}.setting-row{flex-wrap:wrap}.setting-row .setting-copy{flex:100%!important}.setting-row .setting-controls{width:100%}.setting-row .setting-controls .select-sm{flex:1 1 0;min-width:0}.setting-row>.select-sm{width:100%}.api-form-row{flex-direction:column;align-items:stretch}.api-form-row .btn{width:100%;min-height:40px}.codex-auto-switch-card{flex-direction:column;align-items:stretch}.codex-auto-switch-controls{justify-content:space-between;align-items:flex-end;width:100%}.codex-auto-switch-feedback{text-align:left;margin-top:-6px}.stat-row>.stat{min-width:100px}.tbl{min-width:460px}.usage-cards{grid-template-columns:repeat(2,minmax(0,1fr))!important}.provider-quota{padding-left:16px}.prov-meta{grid-template-columns:max-content minmax(0,1fr);width:100%;display:grid}.prov-meta .chip~span:not(:last-child){display:none}.oauth-grid{grid-template-columns:minmax(0,auto) minmax(0,1fr) max-content;column-gap:8px}.sidebar .lang-toggle .select-dropdown-beside{inset:auto auto calc(100% + 6px) 0}}.usage-cards{grid-template-columns:repeat(3,minmax(0,1fr));gap:12px;margin-top:8px;display:grid}.usage-cards .stat-value{font-size:var(--text-title);font-weight:var(--weight-semibold);margin-top:4px}.usage-head{flex-wrap:wrap;align-items:flex-start}.usage-filters{flex-wrap:wrap;justify-content:flex-end;align-items:center;gap:8px;display:flex}.usage-segmented{border:1px solid var(--border);border-radius:var(--radius-pill);background:var(--surface);gap:2px;padding:2px;display:inline-flex}.usage-segmented-btn{color:var(--muted);border-radius:var(--radius-pill);cursor:pointer;font:inherit;white-space:nowrap;background:0 0;border:none;justify-content:center;align-items:center;gap:6px;padding:4px 12px;display:inline-flex}.usage-segmented-btn.active{background:var(--raised);color:var(--text);font-weight:var(--weight-semibold)}.usage-source-mark{width:var(--icon-sm);height:var(--icon-sm);object-fit:contain;flex:none}:root[data-theme=dark] .usage-source-mark--mono{filter:invert()}@media (prefers-color-scheme:dark){:root:not([data-theme=light]) .usage-source-mark--mono{filter:invert()}}@media (width<=760px){.usage-segmented-btn{min-height:var(--control-touch)}.openai-mode-row{flex-direction:column;align-items:stretch;gap:6px}.openai-mode-control{width:100%}.openai-mode-control .usage-segmented-btn{flex:1 1 0;min-width:0}}@media (width<=640px){.usage-source-btn .usage-source-label-collapsible{display:none}}@media (width<=360px){.mobile-topbar .brand .ver{display:none}.usage-filters{flex-direction:column;align-items:stretch;width:100%}.usage-segmented{width:100%}.usage-segmented-btn{flex:1 1 0}}.panel-title{font-size:var(--text-body);font-weight:var(--weight-semibold);color:var(--text);margin:0 0 12px}.panel-head{justify-content:space-between;align-items:center;gap:12px;margin-bottom:12px;display:flex}.panel-head .panel-title{margin:0}.panel-head .input{max-width:220px}.heatmap{--hm-cell:11px;--hm-gap:3px;flex-direction:column;gap:6px;padding-bottom:4px;display:flex;overflow-x:auto}.heatmap-months{font-size:var(--text-caption);color:var(--muted);width:max-content;margin-bottom:-2px;display:grid}.heatmap-day-spacer{grid-column:1}.heatmap-month{white-space:nowrap}.heatmap-body{gap:var(--hm-gap);width:max-content;display:flex}.heatmap-days{grid-template-rows:repeat(7, var(--hm-cell));row-gap:var(--hm-gap);font-size:var(--text-micro);color:var(--muted);background:var(--surface);z-index:1;flex-shrink:0;align-items:center;width:25px;display:grid;position:sticky;left:0}.heatmap-grid{gap:var(--hm-gap);display:grid}.heatmap-week{grid-template-rows:repeat(7, var(--hm-cell));gap:var(--hm-gap);display:grid}.heatmap-cell{width:var(--hm-cell);height:var(--hm-cell);border-radius:var(--radius-2xs);background:var(--border)}.heatmap-cell-0{background:var(--border)}.heatmap-cell-1{background:color-mix(in oklch, var(--green) 25%, var(--surface))}.heatmap-cell-2{background:color-mix(in oklch, var(--green) 50%, var(--surface))}.heatmap-cell-3{background:color-mix(in oklch, var(--green) 75%, var(--surface))}.heatmap-cell-4{background:var(--green)}.heatmap-legend{font-size:var(--text-label);align-self:flex-end;align-items:center;gap:4px;display:inline-flex;position:sticky;right:0}.heatmap-legend .heatmap-cell{width:10px;height:10px}.heatmap-tip{z-index:10;pointer-events:none;background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-sm);box-shadow:var(--shadow-sm);white-space:nowrap;font-size:var(--text-label);padding:6px 10px;position:fixed;transform:translate(-50%,-100%)translateY(-8px)}.heatmap-tip-date{font-weight:var(--weight-semibold);color:var(--text);margin-bottom:2px}.heatmap-tip-val{color:var(--text);font-variant-numeric:tabular-nums}.heatmap-tip-req{font-size:var(--text-caption)}.usage-bar{background:var(--border);border-radius:var(--radius-pill);width:100%;min-width:60px;height:6px;overflow:hidden}.usage-bar-fill{background:var(--green);border-radius:var(--radius-pill);height:100%}.daybars{grid-template-columns:repeat(7,1fr);align-items:end;gap:10px;height:180px;padding-top:8px;display:grid}.daybar{flex-direction:column;justify-content:flex-end;align-items:center;gap:6px;height:100%;display:flex;position:relative}.daybar-track{background:var(--border);border-radius:var(--radius-xs);flex:1;align-items:flex-end;width:100%;max-width:48px;display:flex;overflow:hidden}.daybar-stack{border-radius:var(--radius-xs) var(--radius-xs) 0 0;width:100%;height:100%;min-height:2px;transform:scaleY(var(--daybar-scale,0));transform-origin:bottom;transition:transform var(--motion-normal) ease;flex-direction:column-reverse;display:flex;overflow:hidden}.daybar-seg{width:100%;min-height:1px}.daybar-count{font-size:var(--text-label);font-weight:var(--weight-semibold);color:var(--text)}.daybar-label{font-size:var(--text-caption);white-space:nowrap}.daybar:hover .daybar-track{outline:1px solid var(--border)}.daybar-tip{z-index:5;background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-sm);min-width:160px;box-shadow:var(--shadow-sm);pointer-events:none;padding:8px 10px;position:absolute;bottom:calc(100% + 6px);left:50%;transform:translate(-50%)}.daybar-tip-date{font-size:var(--text-label);font-weight:var(--weight-semibold);color:var(--text);white-space:nowrap;margin-bottom:6px}.daybar-tip-row{font-size:var(--text-label);line-height:var(--leading-relaxed);align-items:center;gap:8px;display:flex}.daybar-tip-swatch{border-radius:var(--radius-2xs);flex-shrink:0;width:10px;height:10px}.daybar-tip-name{color:var(--text);white-space:nowrap;text-overflow:ellipsis;flex:1;max-width:160px;overflow:hidden}.daybar-tip-val{color:var(--muted);font-variant-numeric:tabular-nums}.toggle{width:var(--toggle-w);height:var(--toggle-h);flex-shrink:0;display:inline-block;position:relative}.toggle input{opacity:0;width:0;height:0;position:absolute}.toggle .slider{background:var(--toggle-off-bg);border-radius:var(--radius-pill);cursor:pointer;transition:background var(--motion-normal) ease;position:absolute;inset:0}.toggle .slider:after{content:"";width:var(--toggle-dot);height:var(--toggle-dot);background:var(--toggle-dot-color);border-radius:var(--radius-round);transition:transform var(--motion-normal) ease;position:absolute;top:50%;left:3px;transform:translateY(-50%)}.toggle input:checked+.slider{background:var(--toggle-on-bg)}.toggle input:checked+.slider:after{transform:translate(calc(var(--toggle-w) - var(--toggle-dot) - 6px), -50%)}.toggle input:focus-visible+.slider{outline:2px solid var(--accent-ring);outline-offset:2px}.toggle input:disabled+.slider{opacity:.5;cursor:not-allowed}.setting-row{justify-content:space-between;align-items:center;gap:16px;padding:12px 16px;display:flex}.setting-row+.setting-row{border-top:1px solid var(--border-soft)}.dash-delegation-summary{justify-content:space-between;align-items:center;gap:16px;display:flex}.dash-delegation-controls{flex-wrap:wrap;justify-content:flex-end;align-items:center;gap:8px;min-width:0;display:flex}.setting-label{flex-direction:column;gap:2px;min-width:0;display:flex}.setting-label .title{font-size:var(--text-body);font-weight:var(--weight-semibold);color:var(--text)}.setting-label .desc{font-size:var(--text-label);color:var(--muted);line-height:var(--leading-body)}.model-row-wrap{position:relative}.model-tip{z-index:10;background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);pointer-events:none;min-width:320px;max-width:480px;max-height:360px;font-size:var(--text-control);line-height:var(--leading-relaxed);white-space:nowrap;padding:12px 16px;overflow-y:auto;box-shadow:0 6px 20px #00000059}.model-tip.has-actions{pointer-events:auto}.model-tip-id{font-family:var(--mono);font-size:var(--text-body);font-weight:var(--weight-semibold);color:var(--text);white-space:normal;word-break:break-all;margin-bottom:2px}.model-tip-display{color:var(--muted);margin-bottom:6px}.model-tip-grid{grid-template-columns:auto 1fr;gap:4px 16px;margin-bottom:6px;display:grid}.model-tip-key{color:var(--muted)}.model-tip-val{color:var(--text);font-family:var(--mono);font-size:var(--text-label)}.model-tip-actions{border-top:1px solid var(--border-soft);gap:6px;margin-top:8px;padding-top:8px;display:flex}.ocx-tooltip{align-items:center;display:inline-flex;position:relative}.ocx-tooltip-bubble{z-index:var(--z-popover);background:color-mix(in oklab, canvas 84%, transparent);-webkit-backdrop-filter:blur(20px)saturate(1.4);border:1px solid var(--border);border-radius:var(--radius-sm);width:max-content;color:var(--text);font-size:var(--text-label);line-height:var(--leading-body);white-space:normal;pointer-events:none;animation:ocx-tooltip-in var(--motion-fast) ease-out;padding:8px 12px;position:absolute;box-shadow:0 8px 32px #00000029}.ocx-tooltip-bubble--top{bottom:calc(100% + 8px);left:50%;transform:translate(-50%)}.ocx-tooltip-bubble--bottom{top:calc(100% + 8px);left:50%;transform:translate(-50%)}.ocx-tooltip-bubble--left{top:50%;right:calc(100% + 8px);transform:translateY(-50%)}.ocx-tooltip-bubble--right{top:50%;left:calc(100% + 8px);transform:translateY(-50%)}@keyframes ocx-tooltip-in{0%{opacity:0}to{opacity:1}}.modal-backdrop-dismiss{z-index:0;cursor:pointer;background:0 0;border:0;margin:0;padding:0;position:fixed;inset:0}.claude-page{flex-direction:column;min-width:0;display:flex}.claude-page-intro .page-head{margin-bottom:6px}.claude-page-intro .page-sub{margin:4px 0 14px}.claude-tabs{border:1px solid var(--border);border-radius:var(--radius-pill);background:var(--surface);box-sizing:border-box;flex:none;align-items:stretch;gap:2px;min-height:42px;margin-bottom:22px;padding:3px;display:inline-flex}.claude-desktop-toolbar{justify-content:flex-end;margin-bottom:12px;display:flex}.claude-tabs button{border-radius:var(--radius-pill);min-width:88px;min-height:34px;color:var(--muted);font:inherit;cursor:pointer;background:0 0;border:0;padding:6px 14px;font-size:13px;font-weight:550;line-height:1.2}.claude-tabs button:hover{color:var(--text);background:var(--hover)}.claude-tabs button.active{color:var(--text);background:var(--raised);box-shadow:var(--shadow-sm)}.claude-desktop-loading{color:var(--muted);padding:28px 4px}.claude-desktop-error{flex-direction:column;align-items:flex-start;gap:12px;display:flex}.claude-desktop-head{align-items:flex-start}.claude-desktop-head .page-sub{margin-bottom:0}.claude-profile-tools,.claude-save-actions{flex-wrap:wrap;align-items:center;gap:8px;display:flex}.claude-profile-bar{z-index:8;border:1px solid var(--border);border-radius:var(--radius);background:var(--surface);box-shadow:var(--shadow-sm);justify-content:space-between;align-items:center;gap:12px;margin:18px 0 16px;padding:10px 12px;display:flex;position:sticky;top:12px}.claude-dirty{color:var(--muted);font-size:12.5px;font-weight:550}.claude-dirty.active{color:var(--amber)}.ocx-group-stack{flex-direction:column;gap:10px;display:flex}.ocx-group{border:1px solid var(--border);border-radius:var(--radius);background:var(--surface);min-width:0;overflow:hidden}.ocx-group-head{background:var(--raised);justify-content:space-between;align-items:center;gap:12px;padding:12px 14px;display:flex}.ocx-group-head.open{border-bottom:1px solid var(--border-soft)}.ocx-group-heading{flex:1;min-width:0;margin:0;font-size:14px}.ocx-group-toggle{width:100%;min-width:0;color:inherit;cursor:pointer;text-align:left;background:0 0;border:0;align-items:baseline;gap:10px;padding:0;display:flex}.ocx-group-name{font-size:14px;font-weight:600}.ocx-group-count{color:var(--muted);flex-shrink:0;font-size:11.5px}.ocx-chevron{color:var(--muted);transition:transform var(--motion-fast);flex-shrink:0;align-self:center}.claude-model-names{flex-direction:column;flex:1;gap:1px;min-width:0;display:flex}.claude-model-names strong{color:var(--text);text-overflow:ellipsis;white-space:nowrap;font-size:13px;display:block;overflow:hidden}.claude-model-names code{color:var(--text);font-family:var(--font-code);font-weight:var(--weight-semibold);font-size:var(--text-control);letter-spacing:0;text-overflow:ellipsis;white-space:nowrap;display:block;overflow:hidden}.claude-lane-default{min-width:0;color:var(--faint);font-family:var(--font-code);font-size:11px;font-weight:var(--weight-semibold);text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.claude-default-radio{color:var(--muted);cursor:pointer;align-items:center;gap:6px;font-size:11.5px;display:inline-flex}.claude-default-needed{color:var(--amber);font-size:11.5px;font-weight:550}.claude-effective-default{color:var(--amber);margin-top:6px;font-size:11px;font-weight:550;display:inline-block}.claude-lane-models{flex-direction:column;gap:9px;min-height:104px;padding:10px;display:flex}.claude-lane-search{width:calc(100% - 20px);min-height:32px;margin:10px 10px 0;font-size:12.5px}.claude-lane-more{justify-content:center;align-self:stretch}.grok-endpoint{border:1px solid var(--border);border-radius:var(--radius);background:var(--surface);font-size:var(--text-control);align-items:center;gap:10px;margin:14px 0 6px;padding:10px 14px;display:flex}.grok-endpoint>span{color:var(--muted)}.grok-model-list{flex-direction:column;display:flex}.grok-model-row{border-top:1px solid var(--border-soft);align-items:center;gap:12px;padding:7px 14px;display:flex}.grok-model-row:first-child{border-top:0}.grok-model-names{flex-direction:column;flex:1;gap:1px;min-width:0;display:flex}.grok-model-names strong{color:var(--text);text-overflow:ellipsis;white-space:nowrap;font-size:12.5px;overflow:hidden}.grok-model-names code{color:var(--muted);text-overflow:ellipsis;white-space:nowrap;font-size:10.5px;overflow:hidden}.claude-lane-empty{border:1px dashed var(--border);border-radius:var(--radius-sm);min-height:82px;color:var(--muted);text-align:center;place-items:center;padding:12px;font-size:12px;display:grid}.claude-model-card{border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--bg);box-shadow:var(--shadow-sm)}.claude-model-card[draggable=true]{cursor:grab}.claude-model-card[draggable=true]:active{cursor:grabbing}.claude-model-summary{width:100%;color:inherit;cursor:pointer;text-align:left;background:0 0;border:0;align-items:center;gap:10px;padding:9px 12px;display:flex}.claude-model-summary:hover{background:var(--hover)}.claude-model-context{color:var(--muted);flex-shrink:0;font-size:11px}.claude-model-context-unknown{color:var(--faint);font-style:italic}.claude-row-default{color:var(--green);flex-shrink:0;font-size:10.5px;font-weight:600}.claude-1m-chip{border-radius:var(--radius-xs);background:color-mix(in srgb, var(--accent) 15%, transparent);color:var(--accent);letter-spacing:.02em;flex-shrink:0;padding:1px 6px;font-size:10px;font-weight:700}.claude-model-body{border-top:1px solid var(--border-soft);padding:10px 12px 12px}.claude-field{margin-top:10px;display:block}.claude-model-body>.claude-field:first-child{margin-top:0}.claude-field>span,.claude-move-row>label{color:var(--muted);margin-bottom:4px;font-size:11.5px;font-weight:550;display:block}.claude-alias{border:1px solid var(--border);border-radius:var(--radius-xs);background:var(--raised);width:100%;min-height:34px;color:var(--text);text-overflow:ellipsis;white-space:nowrap;padding:7px 10px;font-size:11.5px;display:block;overflow:hidden}.claude-default-radio{color:var(--text);margin-top:10px}.claude-move-row{grid-template-columns:minmax(0,1fr) auto;align-items:end;gap:6px;margin-top:10px;display:grid}.claude-move-row>label{grid-column:1/-1;margin:0}.claude-move-row .input{min-width:0;height:34px;padding-block:4px}.sr-only{clip:rect(0, 0, 0, 0);white-space:nowrap;border:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}@media (width<=760px){.claude-tabs{width:100%;display:flex}.claude-tabs button{flex:1;min-height:44px}.claude-desktop-head{flex-direction:column}.claude-profile-tools{width:100%}.claude-profile-tools .btn{flex:1;min-height:44px}.claude-profile-bar{flex-direction:column;align-items:stretch;top:8px}.claude-save-actions .btn{flex:1;min-height:44px}.ocx-group-head{align-items:flex-start}.claude-move-row .input,.claude-move-row .btn{min-height:44px}}.claude-status-bar{border:1px solid var(--border);border-radius:var(--radius);background:var(--surface);color:var(--muted);align-items:center;gap:10px;margin-bottom:14px;padding:8px 14px;font-size:12.5px;display:flex}.claude-status-bar.applied{border-color:var(--green)}.claude-status-bar.stale{border-color:var(--amber)}.claude-status-bar.not-applied,.claude-status-bar.pending{border-color:var(--border)}.claude-status-dot{background:var(--muted);border-radius:50%;flex-shrink:0;width:8px;height:8px}.claude-status-bar.applied .claude-status-dot{background:var(--green)}.claude-status-bar.stale .claude-status-dot{background:var(--amber)}.claude-status-health{margin-left:auto;font-size:11.5px}.claude-effort-badge{border-radius:var(--radius-xs);letter-spacing:.02em;flex-shrink:0;padding:1px 6px;font-size:10px;font-weight:600;display:inline-block}.claude-effort-badge.on{background:color-mix(in srgb, var(--green) 15%, transparent);color:var(--green)}.claude-effort-badge.off{background:color-mix(in srgb, var(--muted) 12%, transparent);color:var(--muted)} diff --git a/go/internal/embeddedui/static/favicon.png b/go/internal/embeddedui/static/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..3a50bfa241d4897eab6f159e8fed742d29386a6b GIT binary patch literal 16089 zcmV;~J|@A5P)VAc2ICP^5@TC{iQ{e26r`M-NX>P^2kfrz^Y@$1H>TPCMeSLjYRTZ_{ZK{Zf zD*l_O9sVN!L%822*A~C(e_dPdt6kwvMWJ8U#@&+(zp0*OG}q35lY7!mkEz@8ef5KW zPWMn{zf@`hZP!WoSHnP0T>ers%GPC9<-YlL{%2V~`mWk;4H4W$cvo>13SYjTII90W z^bzNpj#2gZ^_#i6jxC6@&wfyg1oyN7s#;(GngL`EsE8N$jEoW&o3u^t8G=cG6Yt4V z+IGxj-s$WjQRnj&Uf}Ql~&~YD-p6K5rbjfzAU*^-~D}kk-Gh-AU+H+7_4bB&%|^Qn2K~FKrJQY z2o+Hm=cH9i^O*IBw8T&)1gK|{NU~zTp5RSiO%gPPLo!~C@{(4A5Ox9zMPM}&r)BiF zZ{^k{zodPFboD)1x>p3r1cYt_*zveolI=%638=!qBC-l?^LYw5ly0ttZJTd9JcLjF z9>lj_F8AgCNDD7YBGcGXcov0N66#%9? zR*QF?IfW+ovIRJVr#v0?FTMzuX|M&QW`JNmgVTg4J^XABMmuJ&BCGz(7mw2 zN_wob6%yT}kQT*d;dvu|wNYesX&hY(vJyW;P92JXts($d`(A1M`gt||rmXu+Ro0A} zsiB%_DRr%3+S2_BA6X;D|DQ^Rf@n%PJ4Jr3Y$CU4U1+TeJtr;E<6=t6Ck0lMolrI0 zOF^D;(a`#|(#1-zRBB2)EwxH8kq_~g!isZ0N2x{3zlA|EYc{${MtGGLnQK7hOPguM zfwg7qM64F!IUp?O)&adlT#+*@;_RPx@_T%K=vRgWRsnhhYs4vq;O8WA&8|kCEE;`Y z)n$rPTvFRb9Mc`TULkEEoqk(6w{nSoKkP?)-13)mO>iq5kAafwZ7J43K9%oyKI~Md1D)BS3PiE8JQ{3xm|6UmNz(lZm1$ez%JY z)G9j2=tOI`rXo*|RZj@@-&0X)12a_6$$D+paMXQj;HNXFlN_4pa#ATvA^U8quH(!t z^enD?ufdmbGJujup#06&RaqZL9izV0xj};LV&Fx$t=F_VIHg-_r0701a+~yb-mATA zu&PW@xm=1e!SrKLlM`O{TTN834j;w27hnm9eq|ms|O-zG^ zdNJt=RUBHzDOd(I0Jp#_Ezk{{CP-4C6hK$y#F{>9Kc|-vKpw)j(n8I)1`c(EY2l$^ z6eFe)SJ9;`^b@R+R=N-gQUv-5O*JZD*r`cm$dAT|04y8Z8!)K`O+OC?1S+*FQlrwv zL^Lqarv8Be{v#SFMSfhNR)o5hno0Or=gmmFD!2OT(h)W5m42*T^s@_$ntBvv#nP#` zO2@kj&Ep^$(PRTD@uQIulxcanniz6EaKENHT(?ejb?8?~FIhupSEp%%fLI;D)gmW6 zAY77)!qgg%m=0M<|BD@42ZYLVLHaoIzJ-=yHwmHJI1E^iNvM@lXB6}5moV>=6>3et zl>T!At86n^bh~AloOca+iUwM{O#}S{jMM?3m{i@x)n?>H$?Rn*auKaUHUQ>cs<7^UUYCv_K>z;tzxg@z)z#TWPe1)Meg5;Gr-_p$()QbKN84<>Ep5K}=Ctm*>r#gwqfdyT z)r!R+#sJN!J`YdT#_&^U=ft0>o4RLgUL|0vscIM;2W$1G)QTOnAG zAqyN9qO9&z1vuWZN+qh>irNDMG|(QPuC8HW)F&Q)g8q2NAL-tE@19U!)5zxPaDKXFMaDr3}{_4A2^DtU=psvklFd@e)nD z?pm6C;e|AEWYbOE?AmSaQxifxg6nOc9MO>(&HBSbn)B;> zFXa%NHt;xx*aInqVMQH;DU}yfk{BgQO$n&9z}Vil)7*}~fpkn&xP8{FS@f%4|B7z8 z1SyC`0=#UPCL;ad+bizZnGV|W21M_@ZrNBS820|!^?1Bu< zjM@_H27BH>Dk=O3ZBy4ieyl+keLQ_t|G3I`q&(Y3H4Hrct9tndg8MEL`6k_Q#~pOjO*hfM{`D`~Xrqnj#1l@SkACc9JU04;t zpw|&Z3RmejWg>S=^`fPGe_wwpoQ;T&l|kCV%{}`l+Kuy2Zj+2tTBv*mKmkl}*36kR z>C#KTOE=zl6OA7~j*dV6Q}mIKe1vs#itwrhX$Edo+`+zX+o>a1{rv-u=1o1|4zak=#y818P8E9Rs9S>6tpX8uhEz?!G1J zy}z$7L$Ur85t92Wx~#jrkdp9H1mM+8%JK395Y#oSiw&+H{_uzN?QegZ)?9NF~o3rx7DYD5R6u!?y2Ctr+8}3M1Bu-}D|6gE#&|mflk|lze1WDvJ)KTD`4sxx=ROz2{Hj%}7!hpXi7~2T>4Grn)09Jl>noq( z^n_BEDAJ$xJ$M1QM3wyg?|-K+fBDNy5MMs?O#0HvC$s9oJXvxi#BQZ*GyE+3} zL1|2tZ7_xxBtS~{QFoL@hZU2lwXRf10|A)jD5N1o+tJy<_pZMBM|9TNXVa7&rqIO~ zUrg_L&wIFiFA(41+N5m4{{FxK&m)cYWmmh+(gj@@D_$7`O*V@B28jS1LR!Hl`TqC6 zPhbD~*J;;XccmZv;0J7EG7pGM_lMDQBh3yKeh=l@ zv89dLX-}egLHZds^t!re`SRs-^2uLf>;9Z`zDlQ@ati+rAcCWeUiY0d;@LHfM%%Jw z%h*$R>#fv|OYR_QQk;6IiP3ywgtfzW41wJ;QsP74LO?m~~opW9$nJHwRb{ z5`s?i=g+6(k3Ww7@|VBRv>#8S0}nbV5`lq0N({NkE+gC`c_^Mm_Doa(mTD4{uU15r zL}U6~YGG%{@9Se1@1>VsqC*ZjgvO2?OE=$qGfkQ_DRRBqdZpEnxO0maFQ(gXzny;f zyWeq?4&nU;7$e z^qud})mLB5F@w}FEFvd?c_ShlaF7#=k`&?v#2xOiap9vXQgme}QC3 zg|)_xWxHdKJ@@4MkWfpOE)4_$i2)U&iJ}j#XjM@Q6&B9C=lhbR!u-Da)vq$4{_JNz zFyrJNQAD=)P;_rT>@ zzB|YAC4yFWiOxUgoO9^rn{T3Z*PTeKdRM7R*aTFGcox;+ho?YLz6p!%s^T%0E?vUk zyAgu+i6U(Zfasud#*IS`;{9~sfd_Hc5LaJspOXfDK4>fRIrPw}^uh}-awHMb#2C)T z$Zd+0CN&$41W->f-VH44@wBciaCt*0Se`4%jyjj5)zJ#v?hIm4h*3haXO(M~!rgMKrE(7J-{n zI#MK%biI{|5GoyF3Ba$Ma}3%mQQQk8Z?w_IoEO-3+iktY-x6||!?5X8It0xy3Ehw>cE^U9)hd+r>|K$0QY1R~gL zue~y3Pb7wp2gr9|BDuMifD-P1;DHC|{`>CZ01hMHb5UHV#JBt|b}Wr0 zgzq(Y0Sa*~pCwfB0nQ`_SI&7WL3%{BooM^i1cRUo zl0wMEuf6u#{Cq%I!PR5STf&`cQ(iENlnYpS0?DG75(G+YjKSJS-yj4Si6}?gVv8+; zjgwmUT-Mgr#fh(@jyfvz&oHVg8Zmr0ee&p|>C>P7bo4R;QimocWX>Et8Zz)RuP6D{xi8Qdb)w%B|r^ zTyXyR9FAtS;nyTrY)Q0i=~8w#v0g~LHAau7(W6H(Qz6}qgqYMLXqu~Y1&cOYa(qO~ zK9FYp#TvB}O)pM@bav8>H~y5vvG9pO>2v4KqZKQb(;926N$an_KAn2%sR4m`fn*e? zLiqp!+M*l_Rl6%!u4JnkfOm$OtVxif)*^(#pQBreHd0#F_TTv)2qzeOTCG^53bzY^ zuo34sRJrC+NMjv9=95o7Mb}+-9gBFj_)P(d4?y+x(RSN!Pe*aWP5Uw3ci(*sYc{eo(>zDGRow+baTP(i5E;xc)2$(nmMvdSQ>RX)*|TTS z@DU?u;ev&<*Is+l@y8ucYmXbph=(+Y$d8lPHiqc?6)RTIuYdh(`ps{CL(f0|9QF3~ zvp?S7@6I)}E?+#W#}I z48*DrDhdC2>3}2>qK-3Oewn`Yt#8rHH)hhv5hGcpB53x#?|qL|K}tv^yp6J;r!cp z(9@$>!1>5ZLOP-vnTgbF!ts5zS`5#%)?S;}v)=mavl=+=xZ@b%h(N+lfI5U7#3#HZ zuCq0!8}|n$Dj@g$Os6q~o4bCv-hAUtU!hZRK7W^8c4p**lU+Q($I0DbMy<%G$N|89 z`1A>%=H$>CYp%)SPYgaC=?l7A%^|1-0~4hlyo(nte)4)gAsk`Ol%KKraA@)UR9eHSfRveYMmvjr`*HVY#ZUm}QpLKhPW|vEMfsWxl!$XXnvt`*H>LvE-yT|vTTUY6e*=oVSF{v*PK z(#wMfzuc!mL&=u(pX|=nu>)`fkblDsH#pITUAs^lhQlKFIpj)c*WGgo7iID5qE%%B zM8r8{huMW3fv~2E8qz>?y%KLg=D>RnWK5AbA^76dD^%b=|M|~2mjp>EOwmAsKT@QE zz-{zS3eOZcy;;qy}sspdi$#IWD(Hg+o9B zhz9`qk3IGnUHGjFIc9(=8Xa|u9POvB7LWT`{5}|X(nqWomUb~R8V|WZ{7wQ!x-c|E z6NBq#L#rSRiB%v7rqxb>m6rsNB3LhK6m`b4$v}&Jcw*(Tw6Wn~TEI=QPd=60wu+~O zMKqPKG$o`^aj4vDizU)}3^6~B*w5wRPA6}Vsj(PCZ^ z0bfcYh$Il8$G)U#4El&Db~cA2gb-uU^K_4L)^p*aMQq3k0i;s44*p_KS+}%II^tWI zzC+Ydq?5yGaGbRGPbU(P;E7H|x(tL%IDxlK7cz?x&@piIoB#V8`rF_BMq|c|VIwjE zINwpLBX<(72clN4bm?G>E1c>GNM$7bB7))PIk^gAI8}b`0f$s=|D?-tl0!0_Q3cvW zlD41v`g%h!s19*IdkM{)d{G!E+=#7~(xt0&awBd#u(r~Wo4Umh^ngEajD5(IB2Xx#p zB5tKu6)KMq`-*;c8bgWFEJ_c!mfhVw92yUCN{DYo&;t3i(dUWuyMNAt)43>EWBy{Lo zv)`g&-95DCnrrZLufP79;&`3nReh*90u@BNT;kG+G3ep|D)bwS-e@a0wI+xs+lCKg za)B&_c;VJX4@BDLm)w&qA_xI?67mZ{BCGaVj%h9KZsgj)P0C8~}x#ve+vp zx)Y^Zx_z+xJ)rNr^Uh!lr7?Wtl6hpr@UHtEl4IlR*JR=7oRF^EV2PJC5 ztrUa6g&#efZl~t=Y(&?WF8wV!&RXrhWL4M>t7=FfbA}*mJ+d1i36lVlFXa;wl5L09Gx#eiuNi<2~7}g zjiHakUeMk|G(IEhb;3xGi60Om7h|((*4*LRdh4w@oeo$br+^((bLPxpIB;Mb`c;k&X z_*fPcL~_VAGp_usvPvBHFUCT}O^gM&Qf>S@civo|F^uVLiop{;4sTO9|`h z`4JRkH9_GK6p4-~afCL1`O9C@(MKOm^XARvJ$5*JY5e$g=%f=*bmGbT7$G?7 z0f{qwU^B3|4qWJHHRksHR&RwhnOC3B2!a6RoE|3yMsXGr`3H#s$e%naXl;NHq*6wq z)}q=aP2m#L&at#igIov=1oXm-FVeYRJC|0hTp42r1;j{y;Bag%1x(+5`)&RO>jdL{ z@~ESD4XbwC5hW3VS#gZHrjY&m9|xVc;L;O zJC}NU-PZ0H+0$*D3&vJ>cs3#vPr#i*rA**jCWsye#+}+L_dxjFA8x5SO zg?Og1&LddMNq=r9o)NgY&7R8Rk3U9NTyX^#$ppeIxZaGQR*REDs1(AsIRuhG+@nX0 zqB(Qt(&?w4L9=Jic7~#=5YYB+>5`vX&WKP#uZbcR5>c83ed~%g258l)mAp#_MUY&$ z;rHW}3`%U^WDfNeGBM(Y;tDG|ZMrpr8>#~mX879S=(6+~pj zKI8-uTsp86$I>2zx06&5sLq(Q4zzzVh7;L}?;h68wE<5)`ILiS;9g6Dd*6we%{i}MAYK?0o((S;`1?rC>Fc~ zhNxg+TC~v3MTK`Ak=rmauBh{s zh>uQy@(%1JAdk|@_aXta5El0=<_WK7_;B`|o_*$7qr)|{WL|(Ria>Cj9S*mI;Ro;P zjhS!IK?fZ~+ibf{+*ISQ{P+$#=b!@O?G)YR%SEIHxHS7mSO17Uuu|xSaE&0_0Yh<+0jic3F?0*!<1jFDi<2DK z+zIgrmE=cHt2``^+wP%`)OE3eS( zSp`OoY*A&0PZ=`z}T@4e{Q#!^%aOiT2NT7-x%0|8$M3(#ajqzlMv-K`9XxVtoVjqEcHNxFpZ~VY0kDnXi3b(4 zW5%rM=O?O5BRWb0SCTDd+4+jY9s}M+rtI}l1>`e=#tOH5sEJ#M9qUHy1l81WO&SdD zd8k)xFhu|iQULodOk6|zTB<-K;?wr4eX}eZSl6&g9bh8RRT9~Q?v?z0)|a;*Hr{v> z`d=UZFb6hJ`J4LEI#S0A@qpGBIDXl(<+Oao3U_EEfXdOcUbaOb0pb=i7g&-x1mmqz zNb*)&$=8qfm3lJXNvvQBMJ-8jN=_8cJAw2PD#=&rw#K=(E)|^aE4220ex<5i`8(;( zp!kU814~D7z$huCq9--z^q_LNoh|)*+UI(c)(hf0THR{$9@uFh$641z3l_|Gu>cPg z00~h^t}_Vx2-Lb{uVhH}wBvwM7pPQ(^Y!C)uo7s)N$6>qSi?XOTQqi^8?2K~5xy1% zrBH9&!Wvy}YsZwWe`CvKy%s1?Yd6!O`ZAzIs4g8Z^((RdjW^zyPCe~Zj&Xw^WZys( zq`6)eZbI+})-ZxWa@HSbDB%`D8U+JXf?AG4#eI{$`Z<2UuTP8%3W2b=1zc1|pcT&psdGIQMI>z0P{h27i3No&z)zM05iALO4JILao?F6^tZ5y4V2jB@-@y z)K)eNvUNB}64C=I1Y#GTdspG!D*i6_dyqt0wTeFbna|LxufEC$ zKdiOZT44)x5RWP>D@eneK5(HASt5|b1mUBVVYn#a?MkbP^Xia58Q_ycH^q2!g2J`# zXm!$r3G46yS5mPnQPe0=gwtbbnIInoGOVkU)?aVEI7kfn(!4O`$Z~^VG!xRpT#gj=j(%gLHSbC8j zaUD*Fr&7CQ!=h?9oQ*fyi1XaMjd^$v-E+^q^tT5eq@8x&*|g{Kr9>P- z0w8w~RES=Pj<-u3p?`pF1q4xGuyuB}eR3)8+SMJzbbPGX0m3>vyEqtyN+yX$>iE8J zzLd2&Lo&)rs2m_B6nXj%boS6XWsZQL9&z$03TN@oArPTUX}d}*ePPSoOE101C2*MM zdh4&pi8wx4EmIl#2uQ(LehD7ingCMcA$>e`BeY8gq;EcXGHto#mb}#qrP|QZpZ(lt zY0o|P;?qGOp7-B>e~x0g$;X{Q{hk1%Keywipo{`nlFK{vbW;zx<6&Lq(bvgKW=-V^ zr0>wbpEoX7StQv?>&8iGTFlnPO%Fgi`Usmf6CXKb0h2Pct}dJ;{w5uH#1Tw@s11O0 zgjztx4@cpkZ@}`QhaTdh7xV|B*>~R$v-;D?Ho1c5lO6wEFX6M3LW>5(AiueS&`!GR z&b#P@6Hnwy6-4*IoqR4(f4>uVyY5QUuDv!Eg!owgYp=dW2OWGM=Md!h6CO{*x59rG zBr;9xyN-k9t?gkxIZUgRfp9u#xi-Oaj5hA1kCYQd;^;(^NFJz3NG_<%U;XlzG;!i2 z{vGQ^NEoNAAotG)OoBK^j^f|mo;Qy+d)H=k>n*p2H>M2c5k$PzAN)})3N3xM`rrQU z5;DEC`|f+tg%@1F!5Ne)@#&c(N7C4_V_1Ye`~0)+paavCEq7Wt#I&3ot}BFf(8ugY zNMt69|Efg`=Fg`yPCK18*kD6N>=jo^<9!K8`vM-YbCB_a4Y1i}@1)DVcNrhGqXR>! zWRCa57%ls8!3IljtEJ?WT_g;52Y|sAuS2FzrE9Lfh7LI302;IA7+Se%B`*TG|EY%@ z$_ohP;=Zj<9x z97C^3?!pj6gd-w;DCIulh$HFXLk^*pD_r@UV}t?TGl2IIzWw$*TE2W4OU{YwPT-2* z>#n_y-u14{SgOWiS`Cf#@^fg{LC5EJ;z^;Ae04=jQ+z;+Tmc{-^UjueFE1A243;_E ztu43MGV4#y0K_|X@O~W-f{U`nk)mI&mY0aufb=9yczDFfku+)ILdOSRE1cE{s&U=(l}Gw(^fyQZ2$ZzW08|236@K9kNSM)q{v%fFz_0e^D zQun^~R$I}gn{GlgXTHHT@3B-mc?lv zYmbqa_G~9~bkf2F3uy6@#dORkkLGg{5jJK!!dvjNGC43CHxTIz@JGG@Z&9mTVPNfY zi{QEjRcbFugN1&yNmXa`#ox-dWt_Yt1V9pt#s-FOeEsWwUx0FxP$)WX9367-AzW4D z_WH+>1A&G`Sof}k6jcBe^>!cv8)(861&dZU+K{rbbd#N=csV1ng9}|=d+k;B*}r?q zCH(rkQKLtPOr`7HU0fgN>nW;s#l|N35Q2z0yC4DNh#YFTv`v!O0dknVUsn2eykGFj zi@~Bsqp_;!iYu?A>#x7w1y6V*14_g?_{|x3XAt7zAcFKvU7PPVj#;*P$I?8O!REMeYtv=lzl_Vck3ar+4uS#sh*XL+luJ(j!trtN{}K3M#_-9bFqK5g z)WJTzsv6vB7FTBkC6;j3QKOJCJy?n1AP>riTZ31wUV4sT2-H8_G^FQKNW}q;AvJDbi?P2IErp??#m(AKJD+rK5wEE?Y`_@BJY< z@Sp=Z!iQ8fBA_7jQox9NtK7Sfrc9Z_d;d5r98^S|Y#rJaJ)B&6$mQs_5G;r`ffj_# z6H09cBT0P%2I=2RFZ~|xaYwncY;Oi2_>}Bn!|04N&Sal>*6i8*yE6zql1;8%>24V+ zi4ExQ8vasTQt1;$)$`$?D8CZXJfbRMq|Xrt3|R!d4nFu`4jl0*>D}EN_$eV?UUvDr z-~XP^L_Y7l^IR0v@9!I&-D(W;LaUPW2+~jA(J1uH#WvsnG*{HC=es{b; z@l&7rl&c5y+qdw0Pj?SZnlzEV{jG1Y|BIqg+!Oybs)Kw-QK_1!te|40FPWM_J9D$< zcs#$6Ixe}>K?@fxr0urbj<(t6y-cu>c)GMb1YSImVXavEsi&MwC!BBsO8~&e#`p7$ zx~nS_N=CfKUvgGiP7Dr4&Ae|w7TicnN`XjPP7-|co8M#(z*{wSK`1s$fa~A=?swBw zS6#&~aDGpfQ0?ohy&;>J)|e8uhHP&V&_3 zeSSZFw<3t&D$+l|d)#nB=q)$j!j>|NdZh}m;q#rFZc0~PaRnVZbt)%Hu-6Y_vZK?# zZYHzLBa(%1gQU=9-l|Nyt+s9n@-No%^qMt$7EL{5Do6R`mG)Zy30&|; zibsC(lb_ISx7|iR_`y}IF5H0{_H40ZhDU1NZK{^A5VC;(C zthvUTZkKF$WxR;$$ib3mag{o`RHAfqMUvbpNBts1_(+_E3uxTh`(Q%db>ER+yey6TW$Y8O81*T;t;R9H+KfZ7Ha>O|t_WD}M* zv|#iB2)EsKJN@;qf8~S+l1E(D?W>6pIKsc!A%oM!a5x3FOJVyr{CyCNWb`Gs3n``0R8sWO|1W`AB!kYVX+sf+#Junshg|Qtu>ty0a3Sy}N;NwKI%P+f(<}a8}Yp*>nyo8~h93U-& zh?Ibe;SH_5y}S(#YU#iO4y4mgJ(V`tV1rQET!MkvIdJzr@{#{duf0B#?z;0%mQL6Y zktT#9?5#Y=ndm>W1$5%e7=d~c`uqRum5Q#W{@@va!*ICMM?d;8dgAfN>DJqBWe#J@ z8UYn~kB{z%&s>Q|(d$bBI?I=_Kp)RP_Z;1K|9$k}gMXu!U!K9qCvZ4N9R0R!yuS!9Y(m-A zlpUtf?tAP`@7;Q9J`e+h$XnaeoI-FJT}d}iL&XNzbtkS%zxeqtIIyKTKRhS)NLuq2 zm(U2qW}~pKFaNuHZJhk1z0G{kIxY^KMH4%Z(j~gT>HHx87>K^~(o5-_|Nd`&UogK* zsJlD(*+VUe)V@Cm3kNic>R7U53B5IEj;~yr%jyP_2$eWEd27Nt6ZqT%oP7juMfNm? z@e>G($j1c>AE$ZZZQ|El^JCtFATN@xL!vFlu?F5y#J;pG|5|=~m?eNhg-POLZ?-+R8ef_2V1FN83oeKJWHW6O9EWfX zl`sqw2S)T#AvFR6ppp)C*X^j70s=l)UU>!Q z53pnA#1l`XkA3W8oLj&p`aK--Af@1i74`enD@%}6e5p&2E;fK%h+JNN+2wT4z4y>A zyZje@_1tr4`|Y;p-*kAl6v+8cuas#@zc+bwAp(+C1+UKJhT=YI>0v{gl<4XLd8V$F z&!Ja-Uq239{^?JD$~qcB7I+JI0kgg!f?-qe*~Ll!9UejQ9$vZJ5%%uAL~z%1k_H<$ zpo(t1@kV;$iN|Tj9pBGcy*>Bba}e8P=PaAExA%|vhX!Vv`ZWT`7z$Lqs-9#$pV8kB4 zz51YKotOb3;K;lCAGn|X@P|LpBM(2q6W({9ed&{*JesCV*})v-RQEc#Cds-wmyxzb zd>LTX+8iMg*e^z)FR|8BtQQ~62v3t2=C{PvED^|1fFxpBN8xaWKHfI|r$7CPUY;?7 zhL0FdTWqlfO`g032XGKslyodAa4x#&I~>Tl;KB>JI7P(v{r5jW|N7TIxxZ(gd4{Tlk10j-Z!be34((vh{o4%hfvgtfdLRnkF$iV=}4UI+t}0wywe8)#N@l zjh(i0Dj?CxbwQgF!s@PE7&W!4a|u`PlB3|Uks>>7!>a({R-C+j?X+o3IREGDv-#Xz z4$&g{gf`IQNnRgF$GlP<|Nle0*CTFl*p%=w7*SaQXq=fGjv)1TP1d^=^Qp0-uV4@s zh^>jj>w#eNm1g&m-UHzG;Nft|+i$y#cj}-TH_Za#l9R+(R5nB(OLx<`)~%t5sY+&K zs`Kk=G@1xSj}~lzda+T~+1Y^RO<1+&yjrF0B{NQjpnjSbJrq^a;iZHahb5w_)#rs} z>8l?2gGrJ2L-!j!v{#-{?@>@h`z5Lci zsTv!Vo(TMvYHqO;vxqW%A?F=7-snOg<;q$NUBbu&C8Y7j3WjeH+YA2&9amW|1Hcv}w>6DH= z6pku!cId5Pe}ld+&t#QO$-i+9a`0uj&pHWHQf&?&&5(>zpxn+A0PH|LbdG#2XhWH` zDS3_q#Gb;jTS*`Bd(OF$^df&}Z*IsIlHZ9+q6E}TG*dz-$!lTj#I>GhX->i-dJZ|1 z?Yh%3%R4pIy_&s)#m%viDEN$PPXKlR;jAKOs8)I!O$J$9aXL=v%DG5BHyL&L-h@&w zO`|o*kU9gg{<5IYrBr@5Wq~MN9jx6qhgkK7_J+EMIti)A^Sgu7G;L`?5x{y|wIp3? z^{C!#A}&=}PT3%m?{#yD`qr9&B`zp)n;j=!isbcqfvQFPE^DENHn!)qZAmqiM%1!3 z%+Q=`P%p71879Vi%BpMxD&&Xv##z%X{2m3HZx_q4(k)v_^2g7%*~rWzhXR07aB48a zX?3qE8FjG!v@tf+v4`5WS`A~cZcf==vv9S;hRE!E0Q-Oy^JWNDSW%p52rn!1VhDj0 z%N0=$Xko^8)jJ8Ah@$DLvKhQ=F%0|XDk_%^=yOf z?L8V9)3qxZdgZy-5k@vowUJg~urg}QZLrXb2|R1WOxcgLJ8Mdlm6r9!h^3DekChw0Nbl+;62RcVH>C6;1>z$*8L z^DR@t)#)WYX?dYJQkhy>J`})H>bETV+z&agmU=4_R9lJk`Z=V=TwUL#nV9nnlJ%sB zFFv2nOdbBzEQx)CV1&nB)DbZjDWPHVzM51YYL_v3-mZ17IC9{myljn3qSVNU*2zlm_(ODx*eEGBwE3dvRnDnnPHiE7LVlb=syRcz zDVwyqV_tY@Ik#@ngK=cqzFE(z_Xw9bx#2smk}@F$=tyAY&{RsmgU`3#!YJEWr36#S zMQ_(i@zOd&96-}n8s1eOqdE)pRVV1JNVHU0zO25g15f^0guWK9wMJ8F-}oytKugED zg}(-uG+Gqf`*j4Ae=6y2YhQ@wFHfN;y;=8L+!z&LKjqW1XMol7u+h5)m_q?wbFMJj z8y-lFv2b~!hM=lEBB9GAv`i6|p!=&sbZW2;CYAJaR^`~xdF;Cke}9;l9DbIM;}Bxz zQSbsZg`{r)<)tV|U>BH*evv=gSjr}?$SN+-_A7nX>FIP#E5-*)KDP_E;!_$a5q>F) zW`@v3Ia1w+_<*H=Fk@BHhd-UAq76-hmR@9OLA+R*m4pSF6oo-~ zG?i;pn0kKcm5Thq)J^!k9f1)^x9m=Zak2^MV`Xx7vENHlwTaO6n?Z88DA`om-C6$a zW}XOKn#LylaGf7hxptiYXNg6JMMY!`ApHWX~so!ocl`CB1f z)@iGCg-22fZ|YCCO*y2ged&HlbXtSR0g4f)nWv#oy^f*4M9@?^LN8tCV10PMz0rs& f!O!$l81H`oN)SMe`wznf00000NkvXXu0mjf@xCr> literal 0 HcmV?d00001 diff --git a/go/internal/embeddedui/static/icons.svg b/go/internal/embeddedui/static/icons.svg new file mode 100644 index 0000000000..e9522193d9 --- /dev/null +++ b/go/internal/embeddedui/static/icons.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/go/internal/embeddedui/static/index.html b/go/internal/embeddedui/static/index.html new file mode 100644 index 0000000000..999f63007d --- /dev/null +++ b/go/internal/embeddedui/static/index.html @@ -0,0 +1,25 @@ + + + + + + + + opencodex · proxy dashboard + + + + + +
+ + diff --git a/go/internal/embeddedui/static/logo.png b/go/internal/embeddedui/static/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..894ad8ca71eb375e188a6bd8ec430dddcecbe1b4 GIT binary patch literal 117483 zcmWh!XE+<)+qOcD(jrwO_}30qt9FfFt0`485mZp4T6=|PZCyr_)+lP$7R0K(_iU`% zTM%N#-rl~S&iQiAb$%5spKt>`$V~<=1_}y_o7&Hw7*SAA{hLxz+@Sq89H9Q5 zQBYD)Xg|?}qLK?Q=_cG>8r}I)JnuXF=a=jY(kEIXbdYqD!z=iH`+3>mVsebVfp`4Z7QXt$Q*HNXA-eD3{l6>*YigdXDZ2;t_t&?}T z+^f_BWItZM)#qfRb+&*!$1p;xzvp$Y5BQ}gA#XF2^!QY0!(cIF3274p8kqHacyy73 zNlD=m19d{13dkCk@?4SODd5YJ0Obar(jrHR6ddOF)=AQ?T+3@$O!mI-c5Z7}%-zQw zeF$DWEmLi1=O6y~E{45oH(#YxH2tRpLPJQ-n2~K9&R# zdY!B)HkbH`kP=2S()B@Y;V0zt;R^LLP&ADqP!5!2__VvJ#uAtQ@pB~XPyyH5p7VKE1xrK3!{1^;?&W!2dreQG^<|?4T)hZUPNy8eMLYZ(*ARlBH(af=lJ)zzlPw)3;Vr`pq+k!5fanwVzFbE;5LUdQMBC57|XyrtjM&i%5?0hevn*HQWo?0DVQ(@u*@Svxa{6x44WBqBvmjf;?^ME6SCHA)g;tZA z_UDrPD#>dno<#In&)YwYn5v}gqLU6M*t{;r1AX|h-df`1ny;NJSg<)%)WN|(amw{3 zbrkiJ73bVmGm8wFl{TPAu~Y8B%Jvoe4r$x+5GKUVHYJ&$jL38TVGolNmIGQrHv1=Y z%A{#~1!S4y3TFoVTKZ_bl~s@$GwV~AzCv5afOH#_2f>Iuw{Ck+97EPR_d+RDX!?FV z_h}g?Pm?_i6H{2QG#OkD9YMec30H=yMwc=ebm!X%lSNRPL!$~vG@w>q(rmqR5g=1X zT+kh)6dKNxiFYjl|1y=@j-1|0TS}XLp#zZL<~lY6JkT0X@lwV{hZpX!QhM*Iejd;Q zx~xcS@}L@{`KD{Ko?e=SQ(pU2PAcgEdQEc~7$V@KpTRRf0zMXdcwmlW>wB52B`b+` zos;u&U*IAA6$B4hb^4}ZMWUcp%))wTQ^v0nhPQdaMvLYjDZvwYK5>mT`W?SA+r^Bbbql;6&RNa{pB| z>G}Np88j$jmDP9JnMNYAFI~8$iJ-5PM)fPlB>V-iJ2nb2+}`+z{+a8=UNxf~4D_R8GM)7DaI_;+=*<1h z2sumk_Am7gkIGaT8cLCFgHT{~Y3&v3>CS{6AL;~z&`m_Wwrt_Ft}d@)FAboYP;SX)SkTQjKjB{4MF_R-71c z6LUkgh~4%8CD5-@AFWZtg{6}gHIZ=YE0X}x6k?P{#j^((M6)-40S`KNHsObO?yvh; zCui;$wYqfdS^kP4(Hx|i zQcfv-{E~CvHYMl{meL@?Bjho8CZvz+?x(&%#co)ql~Ss#JMoFdXKDeuJ9KKjzeXqE zXkzkHC5@G33utTZ*($86Ff$zC$>&%I-D^1t_08>0qdErvZ{c0sg>z1 zH`GYSdD&b$JEgePEYtw+5<+p2fT*Sjxa-bYk9wo_zeVyZ7t4673Ob4~9;>7qCQ3i3 zkHLzf!Hc!7gHWZV*jedoT+v1z^67KUzjMb#2T4yZ@*r*ycEg<2kO(+9aWvmJ6~8p;q&LxA#U z0O%9{i=00p6`7f58_}Ybd{wuB`%|yd%APlFZ-y7E{ES(;m_1aXPng!^W!K+Mj=YVR zAFbX!#acncgI-?NXz4>_?$NTm{?9&1^4u%^kgAih%abxQwwCP)FDs%hZIbA;{H=A$iHhsN&8lG(43m!F)kb&(U=}?pQX<%G%=p^&T-O-&$N4~p3il3ZE5gup07-5xOW&p>Zw>e?2x{ZaKJuk?JGDK*R1H`i)E zJRVwdM<}G{g^qezfi@VJxutYeZBQrDi#Mj#* zQBWJw84(sl+|{Ea&F<=b!uutnxt`naPke$q6$D!BWi2mkh$>LyX?Q@-^yImg<`G62 zfxh<9?3LDU*7Qynq)x4E{#~rna=S{^4^(V_oV2zMz=$9{&ppFGo3qLEpw`CVgQs?JrN-)$o+K^%4PD zYegwz`u-=7f=R^Imqk%EFXw{brOrAAQk*XSyGSRZ=fg(@KhKNWj84-_ychEU`>fvQ zV@1J9{LsQC$wbvA&6MMOwSoKYg2nECfl$_E6fQK25vtpy)5}f^4S-X=s4%o0Ih4Nn zS2w;`+!9T5i2K@h)45=`w7fyjSDj(&Qw#NZ<9&eq)tkCmJgrB6rIlbpM z4tdBN$E33wAvv~{+&MWW82rM{LxXsvuMyyt*54;bN%EfvAb;QBFA@k);xqOaeTSOu z83r_K2SZmfwn`dWbHrx#Ffn$|$bWkVC!>}ID&~K3I%IQv41p{~i;Kj3QEK2L?`J0T z5!P)IhQpoCLo?WSsBVR^jBW9lHgLZKmuVb-=Xt1zUX^}EuiiIKND}LZ4?i9 z5yE@wqxH3@pzCPiBoF27k}@KFxB3Ha>E+>iXOB`+MmS+esbEhit4rC7aX9u53~lxG zV-nK1GQ3Gd?PK5{e=%z~(d-k$47;c}XHLstPws(xmhQ!$6u4=>Z3U=Z+xIIHF=`mN zVE1!vU^1o59(Yk(M!5xU%$whom7wk$Bcr>RZ6|+_O&i2A&^u!-4|-w!DhsgIUsAIL z2L6^I+AF^&>GrcjKXQKvCu9ugqRjyf;O|(CmP-?6Jnbl_Y^pIn`S;}OShblcRwipa z8cMMht*JWxwOE-L`mEx0vl5(+gqKv1{bL4DBqkwPv(`LsZBz7;O%C%r(dX`sR+L%T z0V1l9o#o=He%P(UfkUHUNs2OHOVp$`aOSYrXHZSVbnfu;RemRHMp7~N1K4%y1_?%^Rn6dUVWmVrV#MDa60+fu#-Iq)&`-gf zLNE26SG3v0c~_?+Zp!rK2P^yKla?x63rk&Ao}es3OmiJz7^^=GjIiwN`h|a@Q*C>6usnUi54`16QDHo^IY%Ik$B4)w_IeYq)$dPXixK*XRYC-{%%HFM{g&h#KgM!p^104TsdpA*iu>y?R71C$Ocw;(MRJ-I0Nr=N*rKi3MlI%cK)JMfzE z3>o>nG_QT&5UITTjPb3W0?g=;XD63(ho`wxv-QjGM9f1TZjNe$9}ETs!$QGk?2U?6 z)~VoyrMvFrX4n#u`qM<3kGC`RYUu6*nxZm+#-BkRYrx55pks2)~9-= z$!@M0ELW{!nBq3toE=s<1)Qph-VV$|ZI8+sXG!p2?ugi=BHyO&RCa{@E+}{drw?;f zLFz%QDhvt;2JHVL)h)bCA=)zQbUSqTj^Y7IZ^s5EbM-i3# z_aVpL?q_V%;1bWH0si@r@^Vd)B(Ov=*L!>JxPW{$^umic{{;^rr_awx^)W6sb*?O&7iK)`ih)+vadKp{Xf>?vwyU?^e z-m309K4YJwDi6`DbsPsg`ult(c|t$_i((S2auk3N;@+PE{I~rw4x9)L5IY6_iIn() zMSsU&8}|NyDMV{WCB6;Ekm_PSZs<|VRHg{h}e>T`{A~iy!;_g|mqu0-UNC55}D1eo|J-L&ChN0fAWMf}+3 zTz#IBL@T4#Nycr?_85HS6cGdM`tC_zl7=X*aVR7z^UMz6zUY)(SYubKjJt^HdeVb) zzExaDCeF_ayD_t*?7#@E-fZ)+P0F*2JK$fQN8JR)7wig#T@|n`(9Scfo&ej#733^#ha<0y4Q>t(n&220fTv!f(J+@h6gD5P($P2d>hoo+Bo#87_`Yr>;y)ac&Q z@$}w7Q>-pm%eeYBOfh}%Gpy`c=p`6$-7N0Jyj2=pCL=Ei4}WNre=QqrfzrjelYSe9 zkh)RI|KRm@_hVjPB=8ZSd4|_TeXH$IG+7q^|WQnPV3-kgs0oM_JT_0BOsCzelbr_&Hn=y(CV2_FY=5g$k7 zELp7^R1BOskC;{s%N{Kr%nG@Y-hKSvOb$aTP|~N;w4q_e`~6VY!~_dncg&(2&IpEx=xMGoWFW=&@>SDJB^8gPD--&+hfi*2C*VrwX-E79#?L!@ zxT;{9Qd6i+P!novD0BzG+V`8id1)sCP5p%9S&eSm4)0A}XX9RZsNtS2mH0-9`E`5K ze)Uvg5vi$m^#CgLznG;dWk1BOI%$K!c5ERfD7LEAdk)k3HEAiuzQFx`A6l}^VUH&E zb}}?HJvsszq8m5hv0E5TwJrYqFygb$m&!%Cgm-F~-?}fA+_^u~7?a)m7h|`)_INa*Hf0}*h;2FCK^ZsA`f*evf+T;fH^1jTG6e7( zUa$Qn%e5?ck;veYB4_PU_@&sUL13?TXsgsQh}EGuZU7qMYF^@!yMw(c5_b|5!RXf? z0B`&%C!Ut)T+Oho8fnyFrpKV?KfXchm?3KK?~f4s?**mHs$QICCiSBBAo1+9OC=O` zDQ32>9u~`Wu*Huu2{(yXtuF2N>2;5l6$`XjA{?a2<6Cu5n7i6XS%=`;IAng+eTj0Ei7d(3^!#~0j7T?#-D}dMD)VFcgWdrFf+J!f=h3HpSv6{lW%RZwajq#j{5@nFLWa4SAm%)&DOIg#7>Uk?H0p{$z7IS2mkN&F@} zf|YvmYDW9-tHkQmrKXYh#PQM6y#hxbm?sj$Gpk0?{o50IjBpqFOXDu0(XuelsRc8T zR~TfD<&i4YRJQfhq@P9-T7HA_z;h>L{V^E7AJ?FdZ75NxWkH^Srj&=?Zid-j=hn~Q zm7(Q=rTFLek7LNhz`uovj(Ne`?gtj!_RqKdgh+edA>=g@($w$}zW8I0(Sb#WB|?Pm z??le|w=ph3Nsy08y-B@NlyPZhn>oyar`xvPOeK`NU%}RZC=g?<)*;przZXXjYFCIX zAemL!OelrP)`K?^7@7m%)G{6!TcH|JO&A&mIwsvTx@;Y zBiiH-9<4lwT+EQe2qTm1`(emY4j9`4W^ZDWdhGGs%wb{X8dd{w2#eD$8|nDWAX`OU z&#xYLEn5G|j6Dr9kjC83Ch41YUA$_Hd6VU@$0_9#bv^1N*6m%Dvi7@r`SHyUz3nNt z)3&JcvI4h22L zBNpM2360O+Sp4U4+e~;1`{bKT5g}ivu(th!AC^^HRh>)?aA!6Jb8(1CL_W`hJ%a5P z{D7sI`RW)4I8H5oBUC&mvHz&~uooCgI}BIMJ~KRl1Lh^gnr;kC5}`gTA`z8k>ueW3 z;bl6tHAPF>mnNpTDbb&!<9Y~>@txiDBYPj(MRvncI1yy58Z24jLQ7N2Z^D`F$>acy zU~$NUP+^o!^acSskh(L(N@<(KgflrQ16DJPPZD!_0m z3}EV4^v6p(*@OvzKOC&=xCuP6MZ^}>;>UCNV(Vn7)9-48m?laN)>xZJr$@YBPLDDN z(nVWz3Gv}>4x6B3x3CoGpx*r2zydF3YYRjE)^MtSIxb4bvf3@q8S1HM;7u{#lZPfJ zXJ$4VOvV#99P;iajGES^Ojv+MbIW~!24t?Vc3lwb0g{R ziY?eG!2(tZqBl97!KU>%B5|-iALT`9!-9)n%d8c)nOB28GyEpuSm&3qHXjgKS*7C?Uj^63cY zd~8Al&8`|blFG2fB-^EXxjaE6oI>+FhSgZtaAYxo#_ zK7Tsx(G$Tn;JiVRPgt=_-CshE0gsc2Cvm0VX98|`;fYUi_*nEe^wO&g4!Ax*VDv#> z{%Zh_sf?>)8k7*+lt^S&jf>K`#_d*{XfmC95at07kCWED>rFZqKad_Tei>Pzao`zn z9*6=tYS!%w<-F&@C#S5%8HvD!h<4848TU)@v@v-n)rn-Q0inyYF}>`R$1fOaAdgl5 zXn=|%QZus48Y0je#e|*;LX@$PERU*4SzTcjV-|#}kTE@Gej^lF;Dgq;uZu&tat!+N z3V>k63=|P8T4`*-V9rDuEV#$h(`LTEU|)T-nAEr4$j$coc5fpzO>fnaDVaw{0hfkV zoL{@B8PQkh9Fg96Q+RJMpPcXPP?q~5cmC!{%`=ewhCgC5;<;yRlLh;GJH#>9g1q8t zYyTefuzf9ogN6?_J~XF$o=J~>e;*tDIHl>CQAIn#aYfLK#Xi7ykHmDv?q7>}hL8)c z3(R%FD4##Ea@OB0LH!BY{hq$MnZ`GnUs{mbqoTI!PxLa)9bS~1OhA{9h|s2CF$}>q z^AWs9>!{ZOqAz=VNGi^%s)zI+aPU``9$c%THz8 zz-8#*x(1f}vFhs15BNtv=v7Ol*(<;`jx{1H7Y)LBH_nj5HhTrqdl15K~??k_{R7kd=DwZC)PWGkO@{*;YyJJJ=h4g1Qs(yL7Uiq zqUL%ENV?HHIkKe$@TBc&uKZHxhnSC<-ibtHnl)_HOOyX}wMyrNId7O~Sc9Gj<&s@( zJ0vz+RM<)+4c3Y_LJ6g`doTQnUy607pjZ7qtAXNkn6-*>!owRl29^|85F+YhuaEHM z|AK&$vikD$lWf7yKeFytz3Y@g7J|RkEKzlRze-VsuqKHOfcRnK!Wu8v$9wv`ODyT~ zKvodyUrYLA&kvHJ;Y-D3kF|26|0v(?=|P(9aFm`#3aWumYTo(d0o6LZbi=STl6l7l zYk;?A{;)luRWd>43VS{CVDIBq{)yK+2j03;tpA_M(CAln!TmaEh3c`7C+)_^79Vcp zM5{y&8-4^ayRkol#opI-cf5Joy139uuVI2swa;^ioD5gq>4m$%)G#8usE3Db6fvQ4ID}L_@-h06tj`?uF?lQ4 z@=(D;Z~0YiOE1`N$7jO(%z}eP?uXW4&jD5ggAt!Kg=(^$8r8oDPF^WkCS7Qf+mOm> zD?SmJNLtP9kyj&zFBde%8}2;so8g;=nD z>N-TpqVtdf=Z%Cl&8YLqA}+jsquMfEy4ggSv9moFixoxgBpnD7s*sVRuw*NLFf23E z@}r^FSNgrh&JWWt?sWHNyQv7tVNb%B@zI!?31D? znC8^0km=yw(_F@6HXdbQWxvbli4@D^QMWSFj}~%EecgOS)q6aVDcF;CyV^((@J#Q< zN`h$nccySZLtp29#s|Sr%G>9UIsb{A+L4=95P^TW79&o_jsh9~zkFk`g^W;L2M&e> z_6g*r__;@=>`iyc5?}t2N#`zVl54L+Wztbc0MDUxqh-Up?8cBmAs*w&+`|#{_v@{N z9qfE2Av~cC`Q1UN>$}IQQ*WBMB`8ql1@jcp1R0^{ zRj!-jiR`Yk#-G_J#;a}9S(67!tr<+k&r(99b`VQLN(;9?r)`E59Sp9|0IHauos!$1 z_{LepY=B%8@Q6g(p5D^I+uytLW%=k0B18c4y6;K5pj*m;uzSt*-u2375w9IsB*Jw1 zb7g8h>^M(qmnIjZ!8kny+L(+t1zriUFpNh{5Qm_Jik4Ss2Z&cc^z8eR)WGQMc6pOz zBSO%ltu*go>{Myrqv|G(vf27qSvt5piA#BmxOu}b@6B!nOqDO0nRMGWZFh9k*D73v zDwSmJAZLm>#Wt6d+IsRj1G!Fe=@~fet03yS87Pmlnm7L$0OOddvuhYCqWQ4L82_44&k|G=i#q!XL#m&*GP!MLZ9qT!%`av z)~)^0RIQ=X@+ocG`Fr3@sny3vlEJV&O2D!8*}URs3osbmqNsu`^dEsiC{7B1FMAx^*0P$f?Gje zIiFLkyxl8T|4@sV0>Icmf@0Q*)lI#3Ee2H|EE8KEBg(vd6Roms9W+pX0_YkTQ*}5l zrq|ISzo`8xdm6^S=>Zb(WONYUDR zRvMF=+LbPbv)cqD;@pbE{K*gNao)eY(Vh$B6y=)R28Wl{9N$1*l3v)@B@+J&fCTnh zJD;j%gBTdGblU9(>3c-Ae$ahbo0aEjeep+QfWoY25tr*CX`FWc+K=arZr4~(ns>4) z+UmIOCI6>`YIpa9;7UezwzRFzk;BPRq(5{Evo~C^g(^lR{?JZ}+%x&Ae&;hAYU>N8 zx{*iN!rK|zgis}wZ2uM1OL3E(-J+oNyw-b-GZu1%rvwLN&*L6+ z4QR2cW-|hA6gHuSIhMVaVnz?Ot>M4*=*}V5q=SBqPE{EeL{mhNb1^>uth;<)3zv&B zw6y{pp+CDU(4kUXVoLkJV)pzbb$oHWO+J0&`p1bD-UqdIj+tJgDCCLJ*Y6nvwuFe` zq$zWY^$&boY57{gl8P`r3$ZaJ1W?1mgI+UkOEI->Sm`)6M@J1z=TKW%klAuR`+d@D zeM*lFLZ=v0N2S%Zo>zfM5{01utYrnli>&SesR}#!+93Yxhp$PhG_j?- zE0cJ7g%EmfO#ZOw96RH2Sainb=+}Sl(P1$K+TM_{uUayQvYfY|ClHaBg{wb_wjz-B%YD8&r}!(ro#vMkiH~oY0wo)r}5gE(gwu zUqdG{OK9v{07UTW%;R!%l-wW5i5n@DK2I}9`85A=-m=`#$OIz{e0mIFvCNn~Mf%zZ zII~(m1g@NmWBsVD9z$c_S|{TQ@G*-fO6DzAfW@Y_z5FMAlkpQQ;A`*7MN~mF>o(zy zlvht8R~J49)}-U6r(hgpyj9DOn0!Wj9s$32L@yKP3Jii#tV$PL3{FnB=j2c(u-)jG zXO9753Q8YIfKQDW<3_9DO*M_@fpi?azIwIE&UYxj|GFp+Rr?40nd}jK3eI889jeHd z55;ww9x~#e4e04Y&4Gm$*6!N6V_xs~1VD5%t_$LGwfi_6wbfXaX9g~<*90upd<49~ zs~yaKpEe!5Ak&oMuj|F&lfqZv1lh`9c>PR&9Uqdmr>>A~F2st(4WB>ZbuQ@C-%4EK zWy=r5wtoroRa=&tUQY=#joWxtcYon3JF{*g#LMfR1$S%*=_sJDTus%GzL5&UZ1}-G`3KzY!xRN4*f08)mscs z@;+;T==tBgC0^oDCM`-01;L(X1*4~V>F0DfffV;xw9mWgm=^?G$ z`EZc#RB1nYC-SVMtqgA|q6$}RF7sHY;@a;n6Mb(;AG7fi@Z?{Fk;<@NXL1c-w;A79q8Tm#Vxu+6+oYUtr$O9;cbFGOkw6Iy7oFq>s46yOos|)8 zYI1z^!PBxMbE#vU&t6j*Cz`v(D!84Q@rNEm-4(uHvoHBZNy_0}ch_ZiVe;S8k4r_* zKh;{-w2mE->o}X{ZHo?f%{R`>Hy{faFdgRD&0f8K<+9qNfU0cd>yrKud}8rmxU+%b z06nKA?P3I;E=Cc^wt?RsdN-pd%_=B@A{6z zf+SD)DqH$;1TGL;Lss;k7;V@m$f2CaRzs_rB&L6vN$XxB4@ll<^_R{Yi|HP2&hE(P zPqFE%D5!tF5g@38v9Cpb0p;;qI6Y4T$+}r*-P}}A3sx$i@MJBlwxY#e6bHY0P?t7u z>h$RM*ja4FSXXg{nkZ}Vv{R8namCe&`No5yia8qXX2H@tiiYP=wblz~-f}rTF&j<8 zuR6To2|xC&aD55sR7&T_xLYrxYc?U131zk)Uv^J(p;`L2JD;z&y()CtAf@q@z~c=R zx$V`9or9;r^+x^+NC$D#oo&5f#A^a8O-G?PlcljaK3(a}3t5~1WqI_kj7L4}HP@00 zrTa+R44wqWpvM|LOL!#*s6UK8I@)l3B|{_1zXmq7Sue?HTuJCG@-kokgCHuS^niM_ zN7wQ960T1w#^MVrEGx#|3A2V83(}2`nX?pD%$1CtZ8;SMHH_7=ZsC8Dk*cDTwG?WH zwxd=ZZ0DinwnwixW~#3lzKL%d`TGDa!>NBz1!P6Kh&xx`2x5p-LFBbo8VTux9&DSh zz;i?Nl+$o0|Gh)cM?l6QxmN3u5Z2(H1z%z=WvsgHZ)Rai{F|Kc=XBMehHy2D|FTfc z%6NCBZz|TLWZP&O#9uw(03=~z(h=awCgrL3*x5-fA2bW_@;I6Uku?IQG4M+(t1h1r}00{(M34}{%+3z`@(ty%<1B_ zB0o&Z@IuyQv{B|C?Slnm{UOHoJEe0RzWloJ`Ui!V$jEr1K-n%u<3#5uX|3&Dhw!p-(ez1owef&rJfE7+~PGjH2ORYi-Hc{Wv; ze*qkf;KN#egn-a#4+EAT+S8#$h$AsRI!*X7_Ozpm$w8%r37Xx45ECuRGRH8T9Ue}g zrBdLy18g8pC;koUC3T>)9Xaq(2fXtitk+sXK1W_$-xdb4_Q0 z9bOZ}6g~l^wp-3e2zpnJx4-CrW4DMrq)ekJrq#2D(XW{8hJ@3G{oWg5c+PX8NA>dI zHt=b^%5KXfSVR*w2k?ejP0|RX?WLs3bVGSC`O}I;q-mit3;mwFM(IW8lz~@?6Sw2R zw|l(`2PCC8onznfY2O$m?(}Srrk5MUg@`+txXk$ZGT$^Ef_J&{$6A>3jq~QoBQ(u% z>bO45S4&b1QrN~>r60xsQ9Wfedd0`x-@+(k%H>$vWHiFcVLz@hF}HqutlT<%;9XUI z@$+_vf06cUGu4rKj*AB2*HpM0sfNTJ{YI{f5T~N9F>`%j$WEWK#e z7tYMcuU);lyFte$Y*nzsIE-j&Np+SMdC@}?Pv*D|42avQ-2=g1O zv!@~uoQmR$D=h!jVmGHEhegSHDr%wez-=MT4VNblA0NE(3=MLSw*A&aF`dB8c9DYN zpYts`G%P)F59-*xPgb-^i);22mKVf!rW~qi^I-S=`}Y*NW-9j>;qb*zZ4#=hf@4CO z5<4;j8A|E@yE(?V{}*8Gi|@%hTX+729qFKxfH8SCnw;i6NAdW;JzOGevGp1XjV~XP|}3K$-OSMSBYrHf@LU0PH+*! z@3_Wl-YFBcE)&fO%7TmzpYf1Ky;;q-*9D%<=hU4;Yz0zT5KawNG(GTtm%bgjd$e%= z*r{l~a_nrZ`lSh9rO~^#`Kp!yw~-Vd+ILxro(S}HRe!bktW6EO?5*n&^9}v>*0#SL z`=#VTmVa<{Zoct;%zVCK?9B2z`+{xQa*6}gGfV)c3?fU98Oqeo!-f~$)GieJPxkmc z8S8z(4uY_)*QzDzjcIbH?h^cq7~a7Qwov#b8~Xj8a*z((@8&h17>t_(lZ^0EA`}Iy ztoUdYvoFN1@p#NS@BuF+rC#wdK(WW6;+cuc?7ib5?E5Ji-FjXA-aYM5c@f6Fb)3U_ zVVge;pT7p>hhF+zK!2ZD?4?zH=U%g(q`o-svZ-f5uOG&Dg04qz;Kv0hR_E2&cCE(D zo9;$`*^Kmioj4qL{9$SSsrt?b`TFa?%0C|#I{rZy-v)&zR};5WPQ~%*;rBsRpr_^6 zhqQ399+dE$>I7TJPShl3C(6qd7o1A2Ddrx>qbG3U?^lKaC2tL0naAwog5Xu8mqQ4S z<{LN_^7?P%&wa^H_=DJZg^}_Z>e(9yJMO;2O{83%B|SRUJ7@u45`x!`nW}DWPkSA>>HOz&MA%gbGmv&A`Z69?Tk|+qXn9;Fx$kPdgq}h8N z*M>-w@|%I`NW z3`%7tzY;2{N3JkVMfzX6D0*a@#xXq|MX00e;(s47sx7~&@XezS>56ag@@5?Z23l;t z%{Z?Cf92}mNKpfNULPj_P6wQd_-NGjZhit>h((b!%F9x~A#488ejXvXR`!)^HeF|NVpO&u=eC)nt=^c_oZ21*EJ! z;(MlxUx^5(*|_gYwWN4!tqw<=v!phb>b*ldI3%I_(f4ET!S7_h|Me0hM#7eAmWI|n zoGSHv0$yxRlRu7}DW){uo7ftt2{NL&JVx`sliglTizF~nofdLa?`_@qqqx-8Y9SD~G-C5B$%W>cF{v>2ruz*yKmmK#yIr@e*^g1BEc<4*Oov<9_e@mWk}L zu-JWbXITBURjKR3dz;rw^BtSrBZKlWqFKLCPY$0nIV zQa~-5qPn^NP}1>2GTo#lpK*EQ$y?R~{$FDX8q z%Wm=2jh*E?6=BdShav*s)bv|;uU)$;n2h4X#xieh;%z=z&!=50w9CCyYlCZjwcWea zpy?SfbGq!Q7~>Y;>=CriY!?_8zSyf6gVX6Nh~DtlbO>`}@t-l@7=VNxXdI9&E*b=` z9QH!qvo1J9?7t(C90#v{O%(0Uw@Y+!hhY3y$VVaV?n{u(pkBNwAIHsmezT7htd56{ zwF2}H{b-tag4hb7M9?4Y&CP0lh@ zRnnt@S|1`FX4t3`FUK48_A(6=a(%fZ$-T4d^)tz8=lN88C|=-pf8~wWZsB!XJLXOe z@~J^V#xxZc0_m2e!icX(}Go*q?oM4aTd-(4aFjhXclbZ7^S`z2~8%~HcV zLer|o&RAl-MK4VVsYLuDOykFf1ZyZul-74XPK=%TaWeA?$P3=xsU1 z9W}Vc;V)D%d%qd~zSNe(&s&NhQCmYjgncLCDRxRqzfgqOe|HxfID4`>0k({l9A=g> z2taLqye_vn45ZCsI^)#6Q@<1+a`gQMj^)YjSt`o%GL3-0QV}RAy}!(kBJqbMkA!e@ z{5{M_{5;(xiN_TAy$diDQ0gOQl=8hu{d&b$7}(e~rx=QLJ)pz`~C?DQ>L zD7#(e?D{u^5-`n3@l zg*-^C67U31r3kpsG8+gt!R96Sn(lPF2#0qFx~o_MUE-kL0V<6(nBE@tl;_@6P4nNc zk}Jj@d|BAHo_VCkc-Al0%5)Y_V7?~bP}Ohyuj2Za@2R>5<)MtKaCe>Q3}9DoA;8>o zGy2#`s-RiBQ|zQ(dQfOOw7jDiFCF~AlrKbxEi~YN%lc7-&o;EHMbc_YBh7C1&i~u6 z^|<(dop`IKxiQc>yy}6ZO$_kJd*z;OuhyyT`Gnjq^3s_uT7C5BO;dy$kRLcJ`O31b zd1@3^Uri%f{6eaLq(IJF;@-ITXKDa3lb%^&A!fPBhY(j3`6IJP(XW-7Eybg@{_0~V%yXI3* zau`2bd zM*IAJy{{nU`D-n{|D)(E7@BO`Fg#jX=?0}FL_&HJDjs5`;6maMi{E`dm6V2o1xJtJPF2bnuoIJt0HleV8NMf>>_hAZa6MDhoE0{V zVcaOMDAbJz$H^M@7#Agexbl(%Sa(YG`p%2@+HEb7%V)HLc0l%#HpOoY z!>(6}zbo2{GWrP{Zk1^Mk(4}=g)`TFj>DYc>^!r+f8UgR>_@7UzO1;FMAG)&^_Rr%IhW4Cmz&|4_bc#QI2-1L}l3YWxN&J6Sud*mh$* zeSq+guoFFTk}mT}s-T)ZbbB+OZ#Fd=gd}Wp-Q=FETv^i_ZrEA4{<5Vo-wp*yEz^NC zbof*c4@ia1G7pd&yem-^^$@LH+4JOSwsRF=;-bKzZM+I&PYfQ)$GsIQ=nnr7v#~W( zMVm3Nt#{uy-KlaZ*yy(fd=5>PQ#F4F00zq9`)_J6Q!M8-gGaSNpyP2t&LANdiUp=< zfdhdczC=pC_#R~_@$97NK(Z7e&n)%kE`liAh*_v|iC-AjXtAaNr60TU&&v}HGVS~G zW+$RWotfhTV=qsNFM7g2d(G1dx?a@}i)Pt{03`ysOs-_v9H|a`Z_TD^@&v;S^c1Vp zURvYdi9FdF-g5{P`ku*ELK+jOge`Y5? z6cs#FXCC=XFgKt_v%aytqq!LFFHF$dZ9UFA^?2SdVe$eE!c*t%XtXf6B@a8x$WwNz*0r#WKV>C!a;=JJFas8aD zV?Wx%sU%|J-ARAw^@&H9zTrvIJT;}i5O*2~(c9qV&sh@Z`@-n4;q>$yi0K>JcO{dR z_6qV;l4JJX;l}U>{kc7LCQ}?#ku56BvZg#cS%(!K2o_)TZT3>@v|oNX zDvc!BQOBy{T?4yzZSDo9y)paA^VNu% z9c*LAKj8+`sr}*~F0e;eE^F20s%45@`^QT^JOMM*=%)y923C1GdnLfDDrp<$JktOr zDph6yk0{WgBH7{4MU(Z$-`Ce0B!{v4XUAlU593$D?kq0GQG_XXLFG5sS!%cwsWAby zwN`pM(+zs5JiI-1D(W?yJ%UHR=(EDmPho{L5L!0Ca(Vo<)JL>W<*B~I%J;^uiuMQp zqLy5QNFT;Lv&*}4HBj@GSxVAOC4>CC5x7FJY9MHa!cUDjgdBG@=dt|&Q)~}=i1rPC zRBzp{J|g#*2xcJ+N{F&e7~DF0bs5NBnDamy9k^904@i3{(%$hzymM701!CqMR`y5v zKhiDT+VE2zdL4wy0i^;I%xRaNA+})(%XixI+mEnb!?OA<$z$eZN?~y2~f*Wo}1q|X@Z3ZH6fiI4qU=FA}i8$q) zOI+5G7m0z3FTHFv$7+bDtz6_P3i6g0tUauVfJ%Wp=WAJNKMP6{`5**Tf86iyTh8 z-6aPnJ-j!qcUHHOhLXVUD%cWKI)36~mpKXNRrM(1twHzX%oQ}JJrE0qcg)3~DMN>B??xtPH1)KYeh(k@w?Pg%0pUV{K6(NtkM{o6hW(|2F`}b7O!# z`p!9!)Xrh>8S5jqd1bTyl?Ee{X*B86s?Jku=mc+q>=PZZFiTa{_+1h1?GUiIAcE;O zSfr2hxZ*o5i46QkKQQW;$ zkE;g4UXd2cz#*#Zz|K_RfR4F`dAm(cxn6px*%Or;aahiw zk5m4O4g)HxJYN5AkLHcHnDYGFXEHpB1{nVG+LdkH*fvjIy{{=#wcYDKKlF9M)MV7o zqpQx@S1$L)tCRE8O^D44T`PDG&0mSDgs4J+c3=}`js*LRD(zuDzq?kJ)P>v#D61c8 z@zp)DPEW#y9Y0q;X`E<%7s}y6Ze*{nc<*xV{WgI(7;I%IBegA69T%l!c1WrIltg6e z1Bx@ImaNn1ye!vaW4R8*Q5Ce^Tn(BH1E%(QI>Y|WmC3^0(Xl6wb?;R(62Gh6O;1tX z&RiDdBz~X$ACZ{6HxXlfK=mkpICSn?-d zPmCj0_J0Q5TvZ_DJ^=SG2tCA8Z)H77q}WgHaHRm{4iB;mR?*2Obz_$-ck4Ie3G10n zvV(=aXaAxVFL6F2EI=$^#{bN;6&+wgFy(eN-i3S+B0czzlO)s)2BwW%{$kfq+i!pfCf#?GgV)&_Vz2!#e&h_Q}kjrg*-!O5gti8~H>u zI!}){;McZHizS4(lH-7&M0MNQ5GC_dDB<;vzGInsAZ(!BR$1$;a#$`;YBP?f>J}-; zt3<;&xz$Xg<485c+G}gmOa%lqKWSdR`u2C^nZP2)s~U^Lz#g4~h1)F@%dYvJ-8qEV z74LTbIIyno;;mC%*PfJGGLY?7AH{U6kPT~b^9?V`*t>$($~t27(!doBFYpkxO#5Pp ztLl9AX?}dVZ_NWQPW-MMs5q>rje6w3^dzOW$)|0`p4fc(?E6*l6YmK2C2t*2eCc%r z^y{bKm;OkU@BXk%ONdScc>fwjad?B8Za6pemV(=Rl5e71=Q6`yxX&F`w zQwqCOz*G!8Sk^6q3sS?j%ge^!KLIC%oyxcGZoVNMCDB}~-v7pA7xKQp^UkXUUWNdb zH?Zt%CD?j{M_ah;6LzGP!;cGZYJJw{G8c>go!1fG7<8zyYT{r29Y9RB-TN2n41^BF zzm`3O`#Py-13}@C{yN%!dX4WUb;o@6BurkOn3YC23Lk$|IFIbQJW1Vu#vhYQ1IRA% zw(OFSVw+Q^Ikj9l&w4R0tRbd8vx97dUwt4TBsD=|31-f z%#$xM-kkR9AiZHnpMLVrx18e^1)Yo5O~GgyfiE|lbCrbtph-fITlK`n9^D-&V8t1$)cXtmd}gh!5^N<%$3j&Q(vn_+be(y58Y)GJ_-teV_>5Y>$O^ z0-^_(m0YhIV6iTq#J=SG{>M}pk&Gx;CGp!$@lOiGAMldkH-7uxSo(qe#fq=diY`=V z6cF)4Fur^zrM;rt_X(J!!_W2>d-k2539yMfd`;@>TemW7SuWr|c?fR*iGfDJj>*(- zA+R@u&C!jniiz`{<-a^Db8dEvq1)o|us%1aB=s+Tp{zsEo^wYoY|d^iWyNE=51G}hr&`k{w2wlCR-RH| z_41lm5KK?h4*UvM14HaXavrz*5q`&t;I;0eJ&q6KU5cOmLC;4~OcUCoF*W0-c*bfy z1r;HsWh=Y+UO$OooW*Gdg~>CAvv>XaJCfLAj@*;p>D~IiJhQa|VTSduTiG2%I6L!8 z_WY1O6BsFRl-KtbS37*PW;s*0ymzbE57JlkjyNjD0#qrK!pGhmwu;YV7K{a4D9rMG z$W7`P4vecVDw+5w4N*3ydrZ{zuFYjr!d|a*h!%Zavc3G&!;TMsq4(LqZ4dU#j$RSs zr`^}WmQ7pHa=As#Y4{ZHY$Yq#^ZA7nJsptjX!vQ;v&dQhqh`CMUGubPv|rzsKZRwH z`TK7S2c9K@W+@tiCkQE=Ptf?mvV36@@kOs%rjOeF%aU+=r5p*EP3eX z_}Rbd{Z;fa{XU%5s!FZDf^8`P62PRQEDw3q(c6!j@UN2VVgQc#m1FBINx*CWk&0^f z#3_@Qj3qprKqWiM*V;juVP2o-DC`5S5llhcC+=U303Zf4N{~ z$8Qgv>_yspPMd`aO@BQK$24qDNUg@EL{ew8&_{|pA}(5ci=WX+N%b0#QjK@*7-`e( zqZRgH4by)`1CIvgCl?eK;0uAc>wv4xeiges$-u6bf}ShPF|Qr?%G+-JpI4hM(@S3C z2%EJOhKEw5m@?1Pmhv8gG6&ChM35OOV$Zi`dv6-JsXzIepQnzbsmM#Dosj~CTe>LL z@OZL{gZ}}8dd4#1*0`3Re{uu0mGUF8ep74U_Z=P%bN^RtkYdX*%{ICA)l>pX z>mV=Y{Cd%#rOZ0Y6>obRlWFU)(jvHD9& zr;HZ_g9$b=yv=A^yrFkkWMa8Ae@=~YM8H}_Swg`OzS;=n^D)dU|4W~;sM`IFzEQ2V zQj`c}a%+raee4ZCe7``sX2sPjdg$&W=6~aumub{W_O>x-J+KjjRv;J9 zC+(OI2VHu-1EnvP;E&<--&n%2Pw1erxFQnoz(>Cr@fp# z!@BV^=Ak0l5)-VyT12}?IL~z>y)ST3ZqRl)73W11F?Fm@-|5wTuNPcvMpA6BT7y6N_%}(c;Z0Z#g?3QX&m>p?sHift^Z4Ehi^;6at9r$MQztCg625e zxZ&^Q6{hqz+83+LNCIiA2R87wt&>;5)&1(WnXhZGBZ}l4H~GPn&}Y!JIO{Au^M;(a zvgsS%-M83f_95qd*v*wlK@3{u>`9QMP5A0r0)Oa8@F%{!*axZSPDKBY&W77`5CIpG zPnm9l@H@qLiGWWe`A-u zXZYa{KIQ;<&5e^Q-y?fke?9&*k#p1Xth_UX^_KM@<`~4Yvu*5EtP=G;$JkG%N+NCl zU}>?EM8hShf0q91fI;dg^V2jsehxadMCzDj-BX!0c=8y1Z8@4DdzPzt@R*5Y;qmoO z&^%r5UI&4UC_e^Qo@(9+V#0WIZ*PQo|ILf*A?20~?T1)>+3R*L$kNj$f^JH-kDgS| zDFhIcYi;5Y_$VD>S72^w14TJy&#Fw22|H0VYCX=nb@^_M6|-?UjlOAdsO%!tO@;{e zCf7-C7)m3U^7}uhqLt=Vpu5Srl~;vT|jeHu_iA@!PF}!}SZRb^7xX1>S=+IX{xOZQOB*Zv$$*d5h9x2*C6X0{pk) z7=KL5h?_S)L^2>mE!P*qD|w=Cx5LCW73f^fI;8tV0ZUJQPM@3^wLEjcaHcL};~5Gd z#nK~p*6ngbh>0We6`q@m+2hL+%plMO$+VZ7u$M;A7b0JzI9lcA0@BA9S(3+~bM_O3 znA7-q4e3+~J^=_OfxxMXO=|{d(A$fC(a#IV^3%A%(*eh?yU^%Fhb3-OUN$kj%5{M5 zGj_8wAx-SK{kj%H?xB*$;*EM?#6SMA*Z$=D4?AOk&%1Z>c1Lvw0b z+B`HeQDv4Crt9ySZi$3{bS~j>-sVWVTQ-=cPPThtI^4w2-|Zv482liT@bJIH+`0c; z|6I}?oA0imsWNTZ542WN(SLL{zR*;#+%l8BpK0|1C(w(5N2$A4p4JinmAtB)x%V)R zqdl<1Sdrs|#|OWI;}e!yw^sX)Z~{XQEaG!q#g z7_*oRCuNZLXP_aUm5F0Wa6FYk+B=xly@IPUqDgiKZciE@M1t9I&j!tg4jm}|W_WY( z@%&m&00rKK-dGCWy7%8*Ygbj8US10?d=(=|MZ@}bwOsT}-aKK@;`p$T43ih_iq$<} zqrh3QCv{{TOr}#e_nPLe;DMtIKCdrb;wJn@p+=7pn3ay_x6ZV(2>z9)BaWb}A(1;8 zykdhH7eC8dr0fu7ynk}f;M;*py*IBexlv+uBQf}yAf99MX`Y=+3+BV(i^I|LcDD!y zozz96WX@H~nR`=~PoX)#$}u)qZ^`e#edz7W-?zbFAZk6 zP-f%NNVECFK4lt;I6&3*b+!c0VFw>qy4vSrhCVP((t`<#%3c9aHbwZC+NfWy)dC}Poi~$f?T~xO1 zQqy)LcjwQCYE!Hmk;9^*)UYG(sq+_*CRFEUKVM6R#gd;^q29Co?Ra&{NAyBE+3)W? zry$oMBeNaYG1W3&^?qM|+%Hy(bd6%=XQ#d~Al%9*pyTL0Z~1D=1xxGLGne$3J#bJ{ zRh!QmL5X^Dn6*Eu90(sHv0YP|(~>1FH@T{a1pa96tF;;%vJ{vU;J&5HwU>%baV~8;gPUmh z#6)Q_fG=IoLoM)~L*!0gq}X}Kdu4BMsF~J`ch5O3soHs-8=zT|g4+<~x*xD*YEXJ! zoRrvyL8`H|=_%|QpAOi4NPP&lYWO-ecR&-Gx}-X~O)53{IFbN;TY0KmQw9v*)&@3) zy1Iv7<=um=Hjd@ACb&{L?aV8&^_N!X>3?Z$6=8G6$05(oloI(rG|`PrBq^S88_;0C zl|^-+uC+grN}R`t_h{<2ycv5KUOXh zTyG?Ted7>Vf3WCCpl-H4Lp4&#?-8nr+)kG5AqLyK0h>4#H<8U3W;Q_knosN(&^TVD z?CNsH6Sa3Vg8)%{&_L|O#s~9`8vg5Ncn&2xyLI+Ee;yRe*mBP1tRL&y?1GgM`WjXO zC4S)3F_K`L&hu^CJC@Bi1%PeECzz3zYAfr!v&0VG6rHbHmwFWDp!}{?*q+(U&p&P= zWQzBaK8Qfn0gJ9d_t@b0Pe|^rnUBM$oCkiWzgy&yY&*ASK^xWpXX_>9VpC$5o$a!X z=j`Me(H_z2fq4`E_r#uNzRN*Wu=#G4EI+jNsT$C4^Di!4Dy`oNErL(&Z zSx)P`Jhsnnn??P*RiXkWsZmwuG@zi%d5T!lP(B1dLOyi9osg&d64O40ogi_YGQyi1 zSzyxcAB5N}RWX9F(;a~-@fXm9 zkrtGa6N=ulwJKw&w`Fyf>_b4E&4ne!ZP%!ZI;mcXg~Rt`R*@-n{0DKaqv=l;9=MPN zG!g9G*KlTW!P|gyCOdpHHb;nxmC|3cltQaBae4l7_eI?5N@(1fhGZ_DBFQ~RfVx*J zB9Bw8$Bi^~khS|bIEsFoYoN=&dG%dH=5}^U;V&81ipF2sqChdcgZgvd*H+8jt)Mg8 zw&D*_l%jxbRyp~@!Dl_GO#Q5IBn{nRsJxd2+IN&XagZ_c>?8YHh7?k@aFgfxSnu(~ zyD?xocPfx&{Dz;oJlVIgc?a7(s~-QBuwmz$#qd`GZ?>{bx2Rm~%ePL!e!0iZg%6*e z#XPv{x`5rd*F%SkQN-o_RFwNPqLBF03IlFY5Vx_=ZoF-D87QK93Ar+r5&V*<zacZ)vY71gF_@Zgn6%hTNP>mAQe_&s0Y>l^w=H2dtF4LorE1wl$0 z-0`yS*HK`=_mhdGsif8K$(4ohpg|$KDLnZ1GMYew__9EwK9@3KoM6q{Vl8h?TIhb( zyU{>w?K-K!tI;%}rZ>f!!JOQs((H+83r%K_c)agCjUbPDa$qir38!{fs>IxavwO;F z2fbOEKdOZ}sqm-u3z*to`i}obnE>KywdNnjXkRb14hR43BE*A3i=UmK|HipF8-tjZ z0hVLEjzk(eJa%rx&Nd9Mvoho6k(4!#ca=VE9&4F10Ea(lC|@;ye{2!PZO8OnnYzSp zoWyr~XC{NQi6^>!D99CGkG)gEe=BT0Uxy9^+jrNgUu2k$riBEBFOHu!Rr2;ZeD=ub*ed1R~8{PyBLuptkZd*6osSr>o*wi;yh$}hC3 z-HKxOtycwXPFUcLMkWAv8Z`1)-KzGU0Ls(qnrHfT!<9hlHku}vR~lO{ej)Mit!w&| z+{Yc4rq~L?&|SCvk-P&ZUvHo490AXX{tGE(lb$rG;wy(tE8Sp)puuB>40%e8 z6~VO7G{-Pf4pKx96-^6lKERgTmsw-Qer>n?G&Ob6sjbKO&MFw***|GPN!1`V?Nz$l zKm%kb7oRn6`1Zjw`(DS@4ps@HsxdXv<&jE|$H}vosLVQDW*acbHH5c)mpo@KX?ga8 zYf2ZhKu%x{5aX+6Xi!deLjIWmU-5Q5uV=={M({a@^tTQKm9>$7@mMo41ke~2sKx``@P)ErN%R9J52@SmOX4cv+z#1(XTec`n^U9XQ z%y|mguypKrbrSXcy~nG#YbS%0+pgn4bM6OcFB#%cU)&^8>%h3x!MJ~?DQ{~$UYQ$k zRU4rm9J)+v9Rl6g%AfX3MKC~C-xPL1`UH=^oT@CghlwY9@{j@dN#(Y6)1+!lPFgU9 z-2ngWpt_wCQwKu;MAa^@?aH+VthN0u*i=^RhUa0Kz+)7(PY4L#0*l5QcsX(9E1KGF zO>xb(_l&;NHPJECOQ}`m;X1lLjTEii6KM(A98zKn3nfK zN;0O|FG#qysERBCIxBpX@-~5zt-?_>8h;l z9eC*?dKYPFqE@tXQ5xavN}GapL zqC#iohF{AlhWG;3e8)sw<-fiXPQ_oh9bj-rhUNu~EXB;440( zaUOWkw8o~Ez3PYB(*p->v02|Q5J|Q)6c`6R2x`DTBPUSVcMCc9WoFYe!8RN%R|311 zkyAPm=aKf~)1Ihhqyf{*rP;rWypsml@XP1CyKWCq3TkSnVTZ5xy*9`ELL~idsW%ladXN=v2?@Y z)egL&<)r9eJ(i?}al0+7|J~!i;NJ1pu%h5_xF$XhZGtYO9={FR(eYCf#&Mvxl8nH3Z~pvnrz!31Pv3yZ)_?Q|e+d?~FeG@(2~{rvHm{vQ*&a?-N#wDA`U zmxR@zkKS^{F%O>}_H1CqVRGm}#+~*p&CB!@@ae{z!iQ@zqHM3gGaMr>82_82JgyD7 z=C5*ko^dS~mRNPP%lSKGWDf>qr%p+?i_5AZWBxNI9ra!?P_~urbdd$|0J8FW$5K$a zH{?>+?VN{q;5R`9R)w>sh|&LEcOE;UpibwB_;F)Y6Fu74>0m7!)vUTNogQ0f`PDwc zwF+8gEaDrCEuU`2t_q$vhbo= zN3_8B$2f`i=~Y2NWxtSj=3i%COoSHY^#i~v_j0fsWNg0=&};npNBd(!z~_UYoK4r{ z!~3)CzMMGdlU-LYTJsBa+Su~cIu~ev?h%hvwJC|8YCe@{Z1d9|yZ>Srvmqv5g*WRM z%+yP`wXzUv+@zkjESq`HPEE3*HiJTI*3bqtU7pFX>dA?|!H+*K$AQCKbDw$X<;Dj5 z0qJ(u9m6Am#B$Bh>z&1(Tz1-I>dF&o1UI4@!F&htp1vXlv=tk|gtDp?f5c;c zJzjk(Ea|R4qjQ!bM~@3H3&kCK9iEvDhV02XEYgjPdXIfiZqvhLdq`9`HI4Ee5$dbA zjN=iHAfAJBYn0{>6DuRsc~`YnfW>o!kDgUM0quL27WjP{2deXMoLRI1NQ&#wx>S1a zULVWk$jiRV2ql;sAm~t(PKm-WB7-TFcdp(y0gN@l{1a9S8TkqJyp9%kz(7^z{!7^M)zQRHT zD9s6^5fkan<1IHP*Ztnl2$E)!GL>BbUq?q6$zE^!eY@2^&g&`pe!n?iu?0fDnSr_& z;=7#Kfj>U|tSbqu^oDxx9#nVG?{@Wl^5f}s;D0-n*d0i-kiv#rVnZ2K$v^+`65b@W zyi2N)OhRJ|cgk4~PG#IWcBqmHHmSv0C?D$bH4AhBZP|c!)nFB~gA?Q31XuG~p*H4yZ?&zMaxS~g zdCtzf5o{32^}Hy%OzqFA*xwHs%ul?bdL&i3G~H=W;JVQ(k+D0S6xr_{kTf0yDJ}mL z4Vj=JY5tQU!Fj- zo(F93Fb1rqkT5gT{yEnQT)A(?YL2)dwD8O*OwfKzb=erjNS|9|GXEWen;*8 zkM1rCIw{s-q<6cTRYAw;RCNw*ka)iMtnv%q^o;v6Jm@l%zBn>R|5=6t`zIL%DRXWT zv87a#;s53(!rz&uM2FWgd1+d9L3JASxXieBW8;4l--JQ{^_BrFfBf#avvPKLEl`J< zfw(K<;Eiusmt4A4*p1FgIYgajoG=no8oC}E>~}UK5E&}gB;LLYxQmdOedQMw=W{o# z2Tf|tULn`MB0ph{%?mA~NC6&)?{Vbuyf;ugH_#m-=a*k$=)*vQpfs;MB z`V7|TWn3jmi5#InSAw9i!0CHjGv&5iKhLi9tM-8Q@U+IrF8UM9LM846j{of<1u7wz zX4xokzI@A4bE#N`ydE7WFn>*$(^2f!O-YFpx4X|@;hwQ0ZIoYe5d|y-AWlB& zw$)y)O02c7k~()CNZHxtZKpDa2X@Wh(0ovddSCFoDRCeSym?J4AFW?FfzxdFBN zZ5p4$&Q)(1E#2@*(}Ec2@5u%AC!VBciAJgwl?iFtRtcklMPYHByov1Pb7171cvuYvSTP2#sfx^yEyu?u&%Uvx4B9tIC%eG8@xG8xsPG~*)EL+Tu; z(S0fvU|`-^&GMZ;s+h6%q3Mo8p9=X)nu`ST+Kn6)a07&epvOc9{(F(J)6l7Y;>uaKbl_JGyFRX zev$rO2~jHa&_5O-zL3yEda11;qW&;ik@74P?ir4FUJ%;P|7t3QyuB}y!dA{O`=DW8 zt=~9`StsI3=uYP0{;J85yI(@^3t6*0(GDW1V=HpRRw!Y<6aFt=@?ouiFv)u`$1cmXxN`GKppF7D3R9@?UNc7=FTT)(dbg4NY$kP^xyMrYA?O9kmiDgW z;rHsTKHv3~hh@f!pQm-Y$i}+?zeqk`+v7!Y>Rfm-)JS8FMb|tN68~~8zNiBR@e)jo zK8a=In;}emsw@1Wr!1t_>EYYf?aJZwYa&|Le+d%y>d~yKeEYiX>|KhCK8xWJKRkJ& z`X8q845p8>dK9(V$q7{;a4eTEGuD`-K(^)!6ixirz!${9R6W-(G_sy_ZuiV0%ch4d6xw8ueZ$}IbBAeT* z05HVyN_xWLuxKh|x@Mf+iWH%tiB-VTG9c%^LRTWxaK!CGzj&xTW^DTUv-q4}B%)J2 zOwbI#7sGR!hm}~>gDYE3H?kYM(75~Tx5OVC4};XUx~s-zw&`aNgLQglhLYMhf-)R< zXESE|n>({i$EgO^hRc6>*A>iSr7s_g1^z?I1I7W9{*WZVdcQ7!4y9Td6lfGKyQVCx zIOzy$UlI4-yfBe9_n$!$Zxk_S{}&_CJui?28gNj$p7=CRJdvl0asR9s%?LV{!x<*s%^1?}tv&FGY1W*c9q zC}M)FgoP4Bj(<-bh7_@{N0I#P!(5q7H54C|E+8wnP4QK6<;j{9&BKRjaXEbB0lrI% z2ursxFJSu6TVngawZ39`>NE*YL_^7xGjTRNSTi=TeUD4&LF)50`u2S)=zP!d#tNfh zg@Le07tFP`Gi?>Q&)GU>?dCCLfC>t_FCHBZWxrl*oC|w9c+1^B_QSP0FJf`&iP0HWWrw2{!w57cN1d!+;9qVV9ljBbyz9r_4LO)0uHK$0xOX%nu+HCu zCyoBl^K_D_0e$_UwwCjg!LpGOxnLSrNdpw-ZRv01B2RgUG-Cf-)ZC~+U|MuOUCT_mlV<;fFwPwZJ!b}m`xxZVMVyOJjM*@n5L9mFu z=>R26Nz2=!KuAgl#e=}WDt8bCc~zwBv0DN#!D2DID1-|CAL><=@bJT${KV@%!hZd0 z9;*75dqgdYk0@?&qR(^gL$_Y!cq;9`DmL7PE&3pSD8<1(pkdJ(^nYsw- z=)4;+7igjZ^2=xhx{MNOT{|URr96M<=7Z8;%561j+5A507F5AzEqhCO%LAu9H|uYZ z@7pKTHeZ#Yj#~dE!h%bgn0}0GWPJX`mb&2+3OcN;k+O@`A-@JwLfL`EFRv-8bId-S z4g#^tjUF*;2(PJ29oE0Ri@a@R2FuTjI)@JZ9&ME{32X5uO6=xtKG~`o@5&)$hnE?N zgu^<1QRj1%k*1dSd@c-HC8FC&u3u{AZndZwB!-)js6sY)u79a{4QZi#=afU`UFdpQ z?E-(Y#1l8{nPS6Nh8180C~at~u=-_4>DXJF&W(W#>BDK&QQy!etoEg~fM%xka8Cwm%Gn)P+pLyR^l723WO=_xSbvx;vN8tK z1`Q)@82I!5vC}}zLNgD(XVh%lK!MvjDIdcVLAMd4Ew!Evw7B?1Jq=)s!3RG9V`8s1 zbftk=MYL|=;~HX_eth3gA-b|my5X*ERCNZLqMB7B=%W`XP;W;nQR}P1R{HhM)xp&- z$Q-~28Z1TNKytbtbM&l-kRvIwlFX>tj`w+9a~hpmZZ(rWptOfW*Q}~+P-o}kD{^Sh zTLQuG%Wqb!2IK1!eCK+N+heftu>wO2@hroCdXVBFoNQun?)kri2piJ+Z{GrRk1~c4$yAfyL>bLkE2sljx6v=01b()s_6P?t_di|bx+FTqKmRP#?KMnUQ#P3CFLcG**(A!29f2> zhKui9zx97FCw52HTu&L?l{d@sKSgU!y5_3zd*6iDu6s666i0>|Z)szD9ph{5O~B1g zd|0lV4U}~0i|E`B530GCh274&WL4r!6^4}jC6ZxL%X7Kpp~NywW3%f|!)p7PiKfz_ z`yWg!MjpvE1?Lfa#SB(04guQSQ?LJjj5V4z5p&f_n>X+H6Ce2}eY{ei zMJ1~v#LDFBJ1XLYdj$?ts#tj?yjFhZsF5He>eb~1QUZV;i7-r^%K5Xbuk^wM5a~~= z=y3d;c`LYRtmGj}I9~g2hife^#$}kxDt_e4K0kkxAHR=a-h2zeLoxyOViNJM>}+mb zve+DI<$ifR49E@~^%HOG#BYy$?l7h>V*p-ADNXjgq>#Ls7q-AzUY2C>VtyUT@XCLs z`#7wSjc%q7$gXs_x5O8CA0m6F%xlynQS0^;TWBc}p1}CKDZLwf z4PhrYVogn;%pV>}aH!c^ne&_B zvmPq;cRu~U`v!TNb+p67VDX!|AbwucA3eI$yj+)S;#77FCMw#+;g-C0z9R84#p3;M z%7xnN1GT-x|2YHDceHV|L?4O>e1VN#9#?Da34fD)dKF7L-a-n5fU>V?zc}S*KZpMT z+4Km(C71<)%Pc8pGg2Lnv8)r|jJiD#DqSSnJ*``{IjwZ zv}VjJZQf^l)Va|}3T(#20%bBvol?l;*-qbR^cC+?Fst!AEdr;lH4f6lrxGpbqSOOa&Z;6f;_X^boy3ej^;hf9#l4JxB4SZzsh@HMCq{-6j0k= z)6EG;^S(Eh5UnSZlK-8h=$|2FzMhK8r}B=M2_qVrYqE1g&_Azt>KwiiW^P@jJJyG& zXh+Z*bqH!KH4P#cSyUJBG4+#;in{Hxrp88%I(7Q)iZZC^ajz^2-HA%TC?b&@Bgs_A zEfVd2_9vV=A@YckL2aInP4v4`H|xlA{h6F&OSPVkuVlb&TkK~aGmiF^;b;abcQtLb z;mnwwGIpG z#}PzqOmHacZriea{%lg8jWAcQ8>O=URA3C|T%+PHl7(8M{+CreZNl3$`=e=a7l{e0 zWHmo{dXkA;O$=~M380#-TFy$T()${}Esn254v(nv5~icdQV0`Io(l8-St9qLRtpAZ zpmC;wTNC1*rxn?>E4Rp4`nO0KXXSUO;4w|8arK?3FfJKQVZA}_#5+gd2%c07K+YZW zDhlo0@lE~*0d#&BeJd5A&}H(;lppU>@bDHfk`FKZ3GQG5sFzm)@IK^UVJ zU+)mDSagyL3s3jm_p6!CFBO`p!^F5>-gG_+at~@XgMuV0-*|_<^qXsHu_as8lj2x` zqSjMC{07v->x)}(P`+8!sowzmm$m=yM({|`ujo1LsS{(AjsK5Tn7(T4%=bRNv#vSj zx^U+sU$hgZ(2)ykerGhTLumzkwPLr$lXx09w)%vEN>cSX?jF$uc;dHr`?l=XHXFQ# zN6)1pa{E&4tiz46mJdDg-6p5=M^dnl>1MB70|=AhH=X!`s$_%?*}s^SpjVQ@cC-?4 zpFQfu^H_Xkt=I(rj)t(b7+hcW;+10VA}Ko`wIzyW=haP+`@K~DntoKKHtS%TpQEhL_Bgyh8Z4~)?!x0?M{K+8bk9r+~p|(74y^o0I!f72{?p_~1N>_lYaa95-?Zd>J=- zPUoHpsy9|+{edP7UraWT-?)PHy=HS($3y@~n60ouj@n!>YM*9@XK!HURaMS3b44QT zyzF@2w%%K1-CGB+q2TduQCXa{^$&%JU{FGyX}PL!s&0IyrEZlAF!{6oVf8~?n9bn^ zfO@2;Vut-Zo)9c4y;=6-$!X!q*2y4H?9es4&oQlfN#5Xj?Z|PE3xt)8^ne=AJ~}Je zpJxJgM<*~!T3a*RR5Y^purro}FL=6FF0zvn4#(%ymtVB2=^qtNy@tv+;LYp1W__f;iS?2i?2J6~g( zc-O{>4hJ}v%_UCQzj{87#~27WfpETCfg58Kw!=quJpL|0;_*>>K%L_qJL*;0&Vu@n z!0gW3W22zo0MEk}Z;B7qhkJuT)$QyM%Y!i+cSUSwrs4}e-c-p**5R=~j~kua(chn6 zYrUG1fQsi1XRA`_qw-aCy{oT&E8O6^94}nF;^gGcwd#3F1=vowX#$~&qEQ8jX;A?I z`%upBfQprSC7kqAV(M=H?)L)2tdf}OSryh%&&zIvGrTRP$SxhwpBlSre7L?#P*g(4 zG!#FM4!27n_}G{-&ATQBdJf9mmwT0L5~#493JR5-vTj{s#x$$II{`Hg$`}mU4zoT6 zWS&0enI;UT>{n`PnCmLAQi)M#L+U@u!0RgTI`Un~$Lj!)M#`)b1MXMJ{V+V%dfi&< zSI11(^P*p8t9VZ7j`fD}DjF)O+5&H$j+>o_U@@-x#sZH8>cu4^+$LvA&cg4%G}Zv+ z2fsK_EBjePeQZLHU9#X3L#FF^QubTnd8aSt!Sc^{DBS=dVaKEss;XNFhx|F(RXfFh z)h2(dfQ;>PMgGUeo&W2iPCmv#H>Yw5vyG*hYbl1IDVp11_Ycf)v){X;-A> z-blgrcuvhAGHCMaUy3KC7i9}o-&{}Om$FU1&Pf4|2?qEIXIKXg3(>DHM=*U?$hN;R z^atWjJKL%|9 zv>4ksDeVH{X8gSh+Ss;BCc2>9jjO{z+oz)3@-V|11?aP*%XV3RBF^Sx@OtsIv5XC0_@C+hB%7aZc-(@r2n+xAm^1w2v<19R*qvYvIzGC@*25%3=TBP(zo#NNES4l+`CmlYyc8(91DB$nA9|M|X z51r#pnP&rbeIgy@@8gM1h4>{ZccP)5Vz>{tbg2P63J`wIN; zYps6|-vv;>!2NdMFfnA8aVk((2G0Qt&m;ZZ5*b(gek?eAPX99<2arnlSkxj)4ysuX zX(dw%^6`&agjy27>$V$RultqIe|bq_9iKw8+pl2-+Pr7KlX+5Pkw+0`lx=;P{^!IaM3h zgKE2)$41Uy@L0v)svJq+vi=Fb1;CDloik&uGX=KQAXKze{q7tJ*C~vFpyCH8sP{ z*7VQka#rI?>@&V^pZq7{_j!QhvqE2;O>to6`uwgj&oJ<7U!5;tu;oCI{oxvpyzYES z(cpUAKL>o26=}1MSU=sEb0F^k7${Ud=|;6rJGv6Q?U?lEVh6u>#@c{q!LBcZZf?Ry z&2S+zCsL_*PG;Fpb0@)1i@vKkF^OBt;1!-{dsm5qYS+c1X;FRPdG4!>ukK?&POwU- zY3IrayMWESIe=EQssZa{Rlv7J#`{lT2Q2<%D)#3LIG^QTi|qM{aQJ-QjbGQ=!l}MH zg8x4Cw!oSA{q73noZMCWs<7M&R4d0r5Wp=^KP>iA*r)wp6*DYnahGv379hDb3#@kz zbo{>pYv&UQaH|Lw&RHvapZX66br%qnT%3%kfUj(Z*P5v0WXgx4gU2b~c|i{!GdSLv zwtB3Q*Ki}7Rg7uYSBc#u0pcVw&m&(?!L<7Ry4~+vYyB-Tpdx`=PX{nee-Mt>t`Z(?|*JVIwt|h(^=4YNxT{G6~$Ll*&eFEd1?}Z z&X-xQY#)_RS2|}o)b;9|q=#DpqcBt8uFBLXRlvY&>h&ad_oir6JXHydQfdb0&yS4! z`^Bp$JYa0dc|Nzhd`V=ouZv9ohhnSWr@$$mi8l;h8yWUh_=phR5h!CpopgpXxi--y z_nXJ=ddl!VvAM{{ueClX0Or|{KX*^d=cH;?_avb~UMI;R%cksY3a*N_Tfpgpi5p)R zz&VKVD|fh#;z+W1WEH#%gxHVLD}UN~GOl@^U-4A^R)M|>*f@6Fx56tItX1NW*KdXG zKN`66Y1oI5{f=KV9I?L>*T~t@NDWfs6R|JN-{yYkhdU6MX}A_{(~^<5*6MPJ)$k zXa&X$Dk^9=5K#e)0fqxC>sl2ICP-g7bLB^Qj@J`l-%c*Yr;}fSZt4i0j$o8r~q=|hvJ-D%14L_VTY_Kz6ZjQ5WFaAaG0kHgFQ0MQgt7@F_x@&`S z=VQyu@CF0!eR589^8A&MWl!`!b*=TXxY>DoET+cUgX{-&hhw&)?Mq_Y^{9=p2!w z(}4p%H-@gLM1yt30e6WcfnQCo5+3GR1^0?yl#3rXRF581vf%6ve8NQT$Twi5b7Q#_p;4T65}x z6M`I@{J%d2U2y~TtKsHCB{p16!p5d?PU%>ciCrFP-<@avT#|6-H-2ye3una1q&h}| zM}tOZoDLvY1H|3HACV(z+zR9Vgm6mkk3pJ)^Aom|zb9mSAl~_XCpZRA>nE^g zGN8Z-`)9d3L+uPw=^gDu=^*RkO7i?X@~6c!!`Fl^_XT|m*uY%=iKwBjbtgDohZQtGLxN*dQRRzP&4loFD;8zI(fKdo3PIZkE6n&EAbfNL*ho_JK$!D3e!g; zz7Z?YKPaBj^M^XHZ%$gyL)ZJX|G!vkeKu?u^bFL=;LbD=T+{&bb6q8q0?xdSM#^@e zXegQ880L4w_wozl?TPn~!2d?LL%0?HAB3%bQ)c%*I`XD;&pav+R1S{M<(5~CCp;_{mfZgKT`_aG`^+y{YkhUxFjV%)`mB7Gc~%&p#tPdfcDY9-3-10C zPxCE;KO2jnKOurYZt#`=k$=XQv%rj>8VLZ7g)5uU6&D_J_JyOgj>NdCREt?vkx3p- zwIG4&xn0$XD@@1Ht+JhJfe8L)r*O`N^(%)(ef;=v$P8B5M)iRpb{+=)Bk{@4^Dy*h zA~@o!G3ZG6hbw=XSLGb8RCg%KAerVQ$x7fog={}A_AU4`u?Xlb;pCU#t7nhJ;-P$w zh>tm#U;e|lAl#z4ZtPKxq{CB@&ZZb_6tAj(<`09BdaW#Nk=GPdz`RHf_9Fyr?oYjv z%6++4Uy~1fl;sjerdtKJDmWv*3NIARt+8p;17aoqR>4lmsd#Wco9FU1x?FSg9`Wov zzlz5^<9|NK*Ez{jw&di&qzQdB@hfKqa81)Kc*E&Nta-}k{(m~&a(V)G7G(T!y4o>g z#Wxfb&qvN3i8pM+s;*03CRknd?|ejEw>YGxSnyAESQ%_3)b?&n7OKF6j;my;_^1N; zioLn>^}H-<;T${o<@nC^{z=OpfDHIn6Z9*cPP0Yj?5v}z4YsWU8kWoOL8Lzri*tTl z-0|mX-<&FZ1U{F{iyD&`m0+l;+YT@|q2Sl(uucV>s{!>YF~#dH0m)3O?sX*%UtUU9Wos1m$u` zqe^HV2*0@!0rS)1?ErqEn3iWgG_ojHA?;5=FtNa6P zDX-->SzZI5NAGlTGV&@}9Sx@wM^9bn#Jl53*_4Z0%gk)uE#0#*G{z46lpwOxB7i6X zlt5j3RP+>l8DKeZ?uPk(VXSyw=`UBmCe0VchCzQnRqiCG1pM6@KXR@0HL;V~J7Tuu3}k;e0{@8s6!ouotc+bv_g0xXKZ{qz1tXUn=Wmrn zFmP2_ItRif9*J~8*zt|We3y^3c)A*oogJ#acm@JxMgEjND*a=?uWSVi>r%;=^?2M^ zW;NySbnR?`ANeuw`TFZ6c*eft(9echv&SqIAgA~_#ohnEimC5>t&g9XF%YN$ zS8{g-?t;D2kJ2Fr237ELz2~vA_piq*K0gBBbv+WP2gP%Cr8krXr}>raU65dYxi9q_ z$C%qPII$Dvwe#Zf3kPbZzr?dnmR%(m?(r1+JKyE9E?MVjqb{&~ zRUp+VMb&44RDW1hpqd1wst?tV<3HtlaZF9&Zei>+JbSQU>)L-4=WmKTmh4wK(Q2~O zF6m*Nbxy^7a=^m>=`cA3_o`T+^jpH2ugCRwMF7jMd8x%hN=^l2E~#N~ReEwD-Ps)5 zIbK(Qs1h;egU1OMJ)VkytRCC(F^@3=h6)HuPnF+e8!iqH6ipW_xj&WsC?Bi>EMu7p z5^Oh^OjUKcostRdNF_TQfU^DV@Ud+BOgvKi!0@}XVZx)1CpcJP1?x~9E8N~F3CO)o z{!5(8=lWQ;qMHk?-vjtk`cQJ620`1=K%Oc+sH9Hex9h_t8x_9_L+&}R5&*S`N`aaZ zj2Tkzy}Hbd=_{b9G9*QV0}1x;1_Fa~PF=1DUi{XC8+7dNET+J}FV3r1{IDQT&T*Xn zST`&u)>9y)f)?|JI?YEN&&OWiUlX6~eSH8{IyY_-((&%>4(s8aG|PZ1)mb0vnXYm= zXXEZpS?R{vSJqLp0a(`ov3iD%!Gq6jMrOqm|7IT4UGR2+q(c2o0<4;G>w@hn#*%e` zzp*Yp&Qpm97iC+>6VDv*R^qJFY?o_m@^1GN1*TR$%sAkDT+yj8ds387p)cgdM&B1R z9JoJ?3+8A8iQ5kh`p*9+ah-p=K2^Iam?J*9_RNDC3zs}F%_>Q_+qRo-cTQe0&qm=G z=GPdUK%6i*qhtVeMGbZZ9tk?v;CWjY2(skc0&k0VUN0N{FaR^3YycHtR*68T3k8ID zjpaPWUH)%cYkhf~e+MjDTCvDaZRWu|B<~!=)N?g_p3I=B?xeFW*2g|re=5jT4yb}4 z>y2gR0L1l`uH4vE{c&LFk`5KD8L$~d@lk@Cp>V#fWW(Qi%!|T!oUpya2IfsY-eLU9 z0L6W%abw+KUn;<>{#{Tyhh6USYnY!9{&m~XP}r&Phvi0{ccluV%GMMQspC^@2=p|} z6e^o!J3}5gF|2HRGkQWA3OlR#GPUzbJLEqLw^fgxv!24XdyduWX%(EZsCS{ABe3(h zf$xryrUC#}DnL{Lh3hKk%hz$(a8AVxLLTQze-{jtb13-~pt@j|=a7yX14SFpVP9D? zkvWe~B7bhI;!XQ_F2E{Z=Rtn(Sn1Nqy)LkdnRS~qkQW7p>Nne6=de6hQ`>h*g?hgl z`>TQnURT$c?=@TpaT=_F!J^)3z++5`@ORczk4MfLn(UkE?HX6#=_&QXdaDu-hZ!oV z;)R)NCSt#1es#Q>oJoNy)8M7-gMBBDE39_wbzEWD-9~3lf2_mlTM-?GxS*@rINMNk zu^ygNekX%=gO^ouf^_cqftoT@r6Q_~B->ODroc#nt5SdpYK|{FR(-1er%sf>r=I<* zpstG2d(v}@p@;X@y&60+?;V0Q@y236IL&nbGsOUUTPM0AavEVU&Q_-Z^5| zwsKDAjNA_w{Pb%jD}%bD$8Fs?q~)dz9Uxcd95_4uE?DgMUh%yDqhgi+gK>U~M(5GvJLOwpyXHf#=P=xwrdrKPQ!q&@Up^sg*)mx z+nI|QRp75=V4uJ9r>4)CkW!Y*)SS>NV^QGD%2Yxz5GbJ?a4N7(I_|fVf7*uvj|o6v zNzhijV*k!?lzyu~r~1fGA-`{|^nah2_J1&@{7-Rr!gM)6pBMb03>cWOo*VD#4yc;; zRYBB&2k&}U0c`h*E&k8sPSL7@Wwv8kI8Y)F$Ws^Cm7Xh*;lzPIH0D4X`*r~B5)3SE zT}9GTc!vocj)US+@yz@yzOZj))3`oG$&WLF$m{d*=FJDvmq{JYuwN+*L%p~_JuQ=< zt#A?T(Is7up2JC|y%l!kHgZzuoF3U;90sX4$UM2Y#phfS!q@NefX}8>ah)n=k&Xj- zm$4{tV5Zf1cBm>qcl4{k%3!tz&WeAy$G;0U%%1`kw^so3??76OUDftP!GGU?!c@a>UrVBEzD4WFRr4E#ojq^2U z+goF=PKOSxu`SYa34{W*3Y@boCs6F~U1GrX+#UqTOZ7;!3M@>Q^?>D6jJ*pc*e;WR z{5<$Wu|sJlFD_VA0flwxFy8T`_{BC#M+(1EC%j(D*(vkk{IBzmli*qLsAO|(I{#9~ zEDzFCI-DNc*#N$4LdUN_7TYK#c7}m;onf*P9DS!io+5OCPtj2{aXOR@M*X2|nE+sY zm2IlmCb1tSr#ogK?h+lAE9+j4mHzvk##hFE2v5i3fM|H}TWKsxbtEXoH~F@6ld?>x#Dei8?8AZpq%Xm~f z1wtxNvjcVkgWItjieDuo1K}l651#=$Fxe;pP#}bL9lw=4&KOl4H=ZbO+C_e>=VcN6 zUlE&-T#h^cOJZ+?q=kL+>V4&;YMfm?>$4iGPEPLgHN-nl1 zHHNqj;Mw>TF7AQv>|6y3SCsAXHD!ycD0}2#<{SUZ4^1%JA#^#+(v;KH%WX25SM{Rnf zWKeqKu~fVuA8L#{+*9XS77kLp5Yz<><FW_^fMJ=|f?; zYNs&W)m6M={c5GX!p>!EUfB`;SH)qlqOD|icEGZCag^z+`giE?d$*36BTi6VbAfI( z1B2(;p(7fMAb2@_D?bj-9jTxr#R@zjN%WF8afKbd%Idmnonur^?v52mpabVO(fK+T*SC|U3+^hBP(a4@ zF2$Y2?^$d8!nM{LXth5+^LOLVHc}-rE)cOS6(Dn6r{5|ds%cl(zOr8o(5N4`(i`ci zYl^pOj|i7+R-qr!Tm% z0iz>VdQkyJ1@(!sC@{i0c|8~X{o?qj&+B5Pael^kzrL5W9W~{s0+0iG_j(K$ex|E* zg-+kyrLvy5uIkJ6U+Gybv^@>z@b3=H79^UksOfi?m~}zRZKA_8&_;Qj8Kl^jRp6Qc zVAA9EDDz}?Q1Og5uL2X=HJ)SsvJTSXHANLRXA*uEiTQ^#IIXZv`75_mw3MA@A05_r zMcD!J&-J?EaJR!r=ZCITx>NS9eyg^V*r;eYIqvayWqb|{Y!*TPIiu{}t5w&SjsiXh zmK8Wua^!EPUMiKPX zD{L;Hxc(h5spO8^cH2z5zFYuWh5cbt;qE+7#Vcieon^u`3e1B7SUj)v(1H5tKr*60 z6MY-?=>jYAkA0}b2LEF__ey3Km29iP!SZup=n@HsBgzh)9+0-u0qfias8b|#QzN=|W=Aoj+vb#M4 z6yT&2?QNc1YyEkui=P}-L9+_p7^D>M=+yjd8)e5CCU$=3TuDD?Vt6qe>yPGVYOJGBw4o6Xj4tG`HQ9k6_I$4<~{Lg(eZZS5a zZkTR`*OS1j3hIoHnWjokR5?4?pQ7ckP1<&~07?DlOUTHf{~m3eBN5?N5SiU&x&0>+~ihlt3Fhp3Rsk%;<2*@{H=mlrCV;V z{7a1)w&%7e3y&4I&$|3phvE_Ya=66DNIQLn`8m;ApDdi9^4h6vt90MJ-ikE$Z*#l{ z*`f*nV2Z0;4^$_F;;*9Gs2SI0NgVf)7V&NdtlU?1wn zigm?Ud#a`>u{cJjgRbcDAh5A_%2&prf*SIV*O(T94W3tR91uC9a?Yb@I-M%oOheIB z{J4OUoBDjpTI-j>{{GJw2CnGxxH14$<5d~Eax66#SihT|&aXQvKtWpUU>wMmjj&!= z4y4t+BaeOK|CBF-jtaWmhR1{ff(wsr{^uZ?=iMSe)(y&_>>+2Pa^{8sjCG1~vh3uJ`f=mw z_+uYLAIG;TZqtX7xDE4l68{;KI7vtTSVkAXQSLnN-r`Yo*+-O5s)BT6vg@9ms7{%v z0u85_$*WRMY5Ct}XUJC-6kVT{Jc^H$#{s94%h6LtRXJ@He6sL$6(A7oZpJ1*``!Oj z@htHM>^4@7i3(O|;F#F4gK+?0`PH@jUj;4%VFxt4+FK=d3)Vd_KWvlLTL+bmD9~XV zO7<*DhpHbw=k%g{y;^6W#*Y8eU(^hWvelFe>#!ZE_q{Q=GXLlYtZQXsE}_CcCy#D; z7O)G-?mEk$UK4yzs;_9MYgNEe*A*S5%Zla^J{A7tvBEa4tNd?k`bHf)dR5@jGUVPd|WZh0&-D;a37tKvvVqt>iEIBRnp|14l2-KgY&X{s^cfYkBBdQ%RK zZTV3U2O*wqr~sk^Tuw^3u*2gyeooE2a6-X;qx#Bv`KXB7C*8cW$F`gpWSdi=Cw}~a zvhrgn=F$CTJHoq5ED9S@?&L3NFz-q(WfO`w-t-Fb`*y6~LH$j!vI|yRud1t(7Cc7& z*~U09yTFEZ(-!j4o5MV$tuX&2`mCPGcWs$p{H>A|g?rV)d_1r0oNZbqPOVcb0Z{;F zIIL_|yrZuSLn%=?Pt`xR=ghfUELZX9iV8+8VNvoUK&J z1D<2r$=_Wc=vJWP?=HY_$4o1Oty)Zn$4Y4mC>7u#e{4XfgM;aUkJ1ANjmn^~4fBit zIceyCP4QlhRf6vRtpE4Mx5dw5>OU6}B`uYhRnub}gj7&sKAi(AXHiKY%dEze&#{cy zC(})u)xt59MJ1q>4RU?{t_1U?_NJ(87?s@E+@o9Gxp2J7Z|!C03w5jlj?$eAj98ETi0iAq zIB9ftlKzvW(m$6@XWIdtFurhcxLMVAG^8E#9ThX^N~Ihplnanfp{hO7af(!Zv!gnv zR)eO3A70~WIi{bd#Nzr6&0kBA5~E2j5jD>VvVAE5nfNCU-jmKqfsReu0Q-gp`+~F z>DAfeUAoFy-N4{^mEC8ZPQlK}`5gA)f~zx74j@NL6^wa0Uj<|Q;~AFDfF`#KjcxJcXqS}*Obl_hIB!k?In*f ziB$YDK5v8ARRvV8$N1pvNXev*SHqvO4M$6kje8z;)G#W=D1cDF&utX=tH7b^GR;Zg z#pyNfPu(f1L_p~wjnWy!k*v`L+&qUmaZ~gwg8jbm@7Knc#P)Rn3pNqqqJS7d=2Wy( z*Ho}m0Tp$PkB|7jGHy2>4otZngADVAw5nv&*|`evT(1Ic6||TR%7iviO@A^U2(CPq z>J!=Q51n4Pf8X31`iaiyN+@DI8RD5#rgcFmyjTD1j zCZq<}F{i)`3jjMLyl2_>ok2zaa?O0#@8xt?2!Bc>2cMR|;|1I$` z9W{&EyQ%2;T9DY`Otl?%!PCs{!FBf2~N_Bk_aJiqXHyMyFsBpb1 z`AmT?Q&-`W!SE;!5=BgVb202UbEKvpnB z+>8%`JNCuEhx);`e69kdv)JTl|67m*+%yXqS!TsI7Y^dAORT5*%WL>nhLUF$0F8WO zziJ^|6=-p6SF@IfF@X7U_N?-Zq+(-jVOU82FX@L4?mozJVli(jV9^I2Doed?O^gK~1hz_yJ2v#hKOQaWC5`pTcX{ zzA63RcXA0xlImTz@#=m7{V&0OGo*gNEn$b6F$P z%$5K9mcO66)_NO!1AzNPAX7;JzYT%1=Jk9|iR~)qS3#5m7yfnt;tYO2esrz%(g^+; zKv0gI_Crzcdd!f#bpXozra*bmcmHd)E!XsR|! zw>S=JZ3&J$(owuu{3`6n;(}(E+^AT>bg?bdo9evkBWWpmmOeXo1$0g+3UDh!<2LRZ zIvf90*BxEg4;!TlzzlXQSJl7bp)(j~6bM2c=y1C$kpO%Wto*+XEB`n1r}V8BS|UGP zhFt-L8fT|p91HhsJIfQXSNh{H>ygj>Z^88c{(r^nz$-#m`Smx~Ki=&snb4k;oiooU z54K^wD1EXmtLr?bIJVpuCP?ZJ`E+f#AGeM~jWa(F=VXL~Fkff=Wsp%xjT#5;R|PQS zMU9KX7B0%CQJ2bgkk3ny065wmb|@_D3iFlvRqYv5S(Xjfx&Y%8QaOhbln04HR0TW+5Eoo4zFkt_ z0@D_EhWWKAB_ry9W$0*kPN~kR*Rq%n2gW_9{UX@vH+TKZ>*_Td^ z_55EwMo7NZ9dfpREcQHjFoOJb2>Q4PGU;Sea4EJ|eMNC6%X7AGdyR>{J0a5BMkn0GABR**iA zS*16mm9w~KF`LJIYz=0pAn0(?@t|x?l?vl<+;|NR+rOI`yB%*dDY^2$)9cCLw!&r= z%ba|b4xPWNekOLO3XkPIbojZpW10n4VB>;+)~_-cW|rG^&Y+B#>EQn=7%RG4+&SJX z2HidLy%f)s-2x%xnFm?%R)H%z3IqK9eDI^zS|0)1{@ygNWN~3Wc zM~7gVF3_k%DF0uJApSrkJ{kOP#@~(jy9s}p=Tal!!Y9uF`; z^BXcwH!7h}dc{8S7@yJeb#9yWlpm>uT2&zC!g}Ltt4Wen0;K3E8fys5%)h!z&12@- z0sLw3R#g6fozb!5m-}$n9WSnqN+6uiZ~)8%oUu&FfMdFaAHPKO$^1KcH*uYLoyIhE zo!fJNeCN+t*d-h)9=HVK&M{7HRu0Al`C6Bqbk{4xLguj^K5ym1!B&AnWsnTarMM1g5S@FF~0@R7?BaKT#zvs_1k&?_k7IOP5C$(Ih>rz&as`kUfICpTIC

;}4eRhDvaX%7&DD$px5auWA0JMY?9^58!dcXhqPT%b~3U$A9wnxl7NoBi z-?>>3nEL~4kxV>X?C(EWjo&RCB$8sg=v+o zDErGdm7b>kIk+Spd>oRqocluG_kuM%RoNQ6RrM!zoZhN&sqDeoEcU1Ts7gZa)Kl+T zy5s!qk^{$_%bZ+x!isW&REq!=0I|VwaRF}+1RoO5fb*_d$%8WB)a0ee%$a{zRC1MV z*pXOIOydN+d;(m@9n)0sc0A#NEtOa)U91DgEN@j`ffu&L_w1h+0Y8I19%sPE-?$!N zp9y-B9-CCMU=UaGp$u8n-8Ckpswh9wi<&dA{Dt{xC@*oxYCpmqqz9 z`YH##e0}&0w7)?X_Xzd`ZFijhNSeJ<{<9sBLt(E9xEzd~K3s9w=WvFxp-OHg-l&Wc zJ8%lCjFvOuyw0x%Ey1_~0Qdg$`2ZYQVCVP>s24cX$HHA;S8&nPmH7W=lDFceyN0r5 zUHs2FycN^`KRGh%%dof&+hv=|ND%-z=rS$lv2zxe_*4lI*W)0TiOvnN%0G3Cj|K8K z{$&!73qT)@Kcu0?fq7Qr)J;jU{0x>ZY3&LF2=8BT)&zHD$Le~;3xof}r=6}b*iYJJ z8^w=GqL6Nt?4a*E*;PmAMt-zs$iqeGi|PyKKFg` z8`n3ezBztfiN~}*9okpH@nixl%i)qWrZJ6|6CYi}zEwP7zj8d>c@5AJb<<6%ydbk6 z0k+`)bOz|0lL1yeUvf!;8>mwRj{Ujs$Hf#|UdO3f1ab~S`*iU;)>_|9JI<+04}Y_+ zIB+U{xtziwb{T$5OpE1L|8n|Y1yE;je4ZC!y2QrW4UgFbq#dv*2fj^H|9Hp0QP9Ug z$gcw88(}s;>4jx-|JN{B zB<)k zkpCZ!u{|FI3MU&Mt7}M4Idzv@@wESD`bhmTeKjj~YQ|M1buM_gSBTk%9bLvRuIp@^ z6ZpKY`s9FvZLq!y;#^dK$KwiHS*}a)x4)V4U6GvLHvJ+!g+Dy5>b&~R*Idw2_RBox zg6r>&^;Qqiqk#NVk7DxgcKGHe!-C!_IXM$_Q5F}>Rnk-iZDm_Lru2uVDI;V84c*lBfA@%}1x)P5Vzdw%g(Uh}yg;h-7$)xaD zJrd+&?pNt56M)ypI)I0ZH`NptZGm@hi3H$fA|X<7WDR4VE2rl8WReXx2FQ=&&fY)t@Q!$S_&@ys!Bfp z-vj^uZB{fdu{nz3#mV6eE&ncs46j~meK5Sgj;sH>1ZEA!lgHkz_2>ebx|>t~6>bqmrpWCxQ>A~s36;)p)2Wt>K zJ22#WE`V?_<#v1xgDtkNf~555A$RimFp#bCm$O zz8H@ui^tDmw)H=Sk6jhmll4#z*42~o$#dKtu+q!PjH?=h&X$zyr{SAH4v$qlW~`Yc z4ENOMjX+#O_4M>0<7Rrv4UR8YsJ}$s3 z@T&UcHVmTdORNj-Px&4C^)iV7^NBiPy{bor&fgG6+-skxhde$HoZ9{R8{(L4gk?-x zEQ5Npsk4)8$2xM@r21yPrjPB{{uKmo_eOxTpNC?!cJSmzfYd%Bz zA^`-*H!l1`uupmRp!+Xi!ht*xOLf*N@OlU+rO~ZdR098=dCpzj|dbGN8tH;Fcq!(N;f9%wOjTz2d2DUCuPA|2lgxcT|G;8fQ2^cl8b5_kPhoZmAvNe!Y6j;Y=vdS zB%bTKfDC)0WOfcfWx|CUpO7Way34c zPP3ovYk#EiwUM~*GpMT=FV)8>67*e9U%b|OO(aZLili%xqhG~1h1*%G{Roh*3+}G) z_+r~C_T6nidDTw^E#(k+UNJc-ECiqL5(;N@iU*{b&-{1e_=?D=-w1}6^|rw@)E0|R z{@GgV+kyt~A)o@NN?tnMIRnUXcyS~$&ztA}ouMO0A>F)=bx|iB?WBW`QWpG9U2uY` zc&c=AGw#s0cuneYu_#CZR?35K2;^^ktKdeA?~|}~aMD6Kss(*6aB}ddK%4^$CsGQ0 zRDi<1@R*Ye))VuN*ZGgzA|KeEzfqxrtkPG7VVGDfu{OsAFQq4xPxZrf8N*Pwd0ss_ zQ2)>1j(^2t$`4y>eF@OV#dECx37=S|I8=@EDxQIJ`-4FD_#_cPKudtQ88W>z z2C$BIYzwJhIzf+V2HA1 zr3e^2N6Wb%V#d;hY z(*7TaE!6*RB=HZ6w|RJ6Ifmt11v$I>W${|~bAiogq!ZSYvJ0oH?2p?!UAX`~DXty* zQ?)+hZhNJG&TyD{l{6$BM^gnBZp(G~{G?xQ!{;wWu>PrQt-l@9L~jXWL)!c*lzJ61 z7uUT!jP{#hff({z1^U$_1xQE9lO-MHePMb!jg!}Ou(R_bLDo4Mxx0g6xfr;eEDWMq zM;&8P&pi=P9tj}$p17M(ePI7?p&7poa4t3nO8sS9_m~ZJqyhn0FR>2x}aCxqac=?|}6Hyx~>$%VW-Z<6?tIQ=H9I>m+a%;BSOnkHTVVB?mtq!@mat2X|ln zV{5J79c^yX`T`{v+l8_nhwFG<$$3;~d2M1N$fsT8nXip>Mo|SK4(2K#yK5XATz@W; zQT(c)#o&?|;eKcQ8{lsJona`<13M7UoMd?-rmVgsW)N-~z-NBCgp2jU2?@7Npn4V5 z$-CEOGDHQyiboZwRJ#fwRN|q6qRY?~=&~-deLDDi29P`#fS{ZaZ5n^8Bp|ORz&--W z1@cq{d#uZS^EhMSAi`sz#?$qEGBZeucazp@OsmbT=T#h*FkQHuA0dRe>LmbISjTc;5fpBWT}_pj{23;w@#}jp!xu z_Q3bS&VN+^P)?zOU6v|SRkU9TL7xqrbyM-i^Z@EW6~8etQjUR+jDd3!M3@G?zQ;Or zfnNpKyi5OXc)Wvjsym1*SSJ?)CJOf&5^M1sh&kLMozFpG8W18!F z9*T7+PX+G0F;>6725i#>4J8ww!~e)j^_%-ri3ZBTeV#`Gv&;6Cv8L)tc+`f;7%#we zHh2mQcxOBw{qp!kdd}jpoGzdu-%3Ah4@y3Dto)00S?|%R`*5JE)W$1W41MD7bzizGdS7L$A zburK#2qS$pkl0=gK9;k1_MHHr3hPPfRE-Pk#sw$_RMua&Ne%ajI#@z3RX6``MS}L0 zc)R1lsL$`wJ4Ztu;9~&%SkQwp_Nss9OU|K{f3S@>J7=DmAMC>g25yf4i%A(?&zYwV zSDdfAKziE*(#rnaxaK*vkqq~Vc*bDte2)3z{ZkZHb=b)D`Two3yN6;;K-%aZ32ak~ zX;s4O`l+rvdr|GUKc=~ak3Ap%|7dLL^$2al?P8M(*!lKO&hlPgYyIK1)~}A^eB%pg zyRlHg-_=#V$dI7*G_*rTnS8Q#^;;H=OyMasGzThmxf#or6?KLhe_e2|zvzR5Vuej8N$TY27_tnrbZ6bgBwu*w;P{_}U28iYE7!df_h!@(joA2#g8DD4SXb;1Uj&fdgjs z{M3O=1?~*s*cS&$)FsYn@YJ}%6r@v)Kg-X0;WtOJ4$lfYA7Oo}Bw?9x!S<*}r9;L= z){85OU(_EaBe>q=R@n9mpMN@D5B$yXJ2%3q^l}k?&{o_yDwfdDAfDc^0m(mAhctL%%fP3?^r3$dN#RC4vKpOcG< zFMhnE0u%Ftyerxjz9=l>-|SzG7iVuvjJNVvv=4<{N*-*>8#m>+JQ;V*yB)r1t@X>| zbLM%6tMu#wwHrIeH5Yu9ER{Sc?*=}V>vsNm?04{zn96@r(BLt^bEzYosd{$3N8TB}cL<*J;*K^(rHAj-ZUpDUb(y!M^yP z9sA+m#;cyZT&Z-|ycw9X! zcz5Uma{g!FL7urC1_gCjQ8|Em3y7MNQE^DKZXF3@n&fU>au z+#3*V8|q#DO1DUhkNIvJ3*YgweE6q8%ALc$RG?;zsFI0JZuYqfC-ORui<*sK8BkB^ zMhx}II>9=awB?$a$KzGNYwu7arl^xUd`?+t@C#FIU{#7OWGeC+xTKm8#rX12Ao@ zz+1I-*YTP&cBM!@&-JTJDi;vliWTv{GQKjEtH3KjVfr}*d>%pL(Fi1;7duux9+`KS zEi&Co=Nv#>=Erj6s&ssvQ7ufv_SGwWl^ob_w;CGti~-33Y?t&^z^np!J_^|rqXk<8 z=Za&MJn&+%)GuDcnSh*L%*}zEkK}J>=gvRWObxbG3AfVoB3f#tIFA+UQ`tIi?1c4r zEIDAaPx3XE3w5A)RrQsfO#4yi@R0<+S%bWsHRF!sqj1*Y1oN9)R9}wc8}TUce}(lx ztUIL3|5ZZPB~U5>I}MWxWnbxE&xdS(ey#OYkp$$LfSU!c*gxCydFbT%SjYAAg4Sdf zpu@;YPt(MxYNKqT8=HIRxrHz))8mY%$^u=Wo@AVy$srA;fGXokC)@}5(;!>KHfrGx z^N5SSPLYZ3uipHLwbow?u*X;aoSe>)m43RS02$9eNT9>7ui^g;EbM@)A5IofUv3p{ zHRcF@493_$)>R3cd(N%`#d%C4?r&?X;U3{E3nu~m`qHEEmwmcKhV_WoT(IZkN^i+y zl}NF!)T>|$BRFAEJTb;FFIf07V#Tk!vr~x<>I3U@(v$r10>U*2QjCwuBL*>LM_3Q_ ziEnl|o>ZUt{|R_a@XdjL9}BMrD!n9M%I@*E^Jm72YN4USFXV&umx}`T^$dS{t@T&q z#_Q3*cKp@}j}O`e+xKnw2;3)%y#bW2*cZ9JN*vHuoPDbphR0n3IK6(V7B{Hk&MUxW zI#r-%hSh5EiU&NWvi~g%=3~Qn@XJyRU@BOo17@IjGMsCF&E`wP@N%W98gLiPxF8QI zNpOh+)4l{r3Vz>!oe+bjYR`^~pmsjw&y9D|z-#ziPU)fwlnO`@G#NnibH1lo5coJe z8)V1i-x%z1%yGk&TVX4Y!|R|fq2bc`2kMG}g?)~7#H-o4U+#}VlV^C8&p43c1jB)# zlc@ty>`Td|{F4(imf7u`S%Iz_Q)jnm|LJ@DHWJcR2@9Ujel7(Dyesf;e>>oF;nAn^ zAEnPq$6Z38?1gD4x+;0dM{L*OO@}`l`}{u|vjH!r*_bS9=IcZ}{hOUYc z$*W5iRZ(_O#fQqioetSbEnh2^}2NTQODtsyOam^K*3eK>yy6 zQUB(64s|Vz5%-5@fK{N#LAla{lG!;D20Q$K2H*X&!C}y3aAjxj?vA*qKg0XB@q&H7(_TYI;N1}sZn$^T;U9t z+pYo#7CiIKDIx_@tRSYhg<FV{DqXP-Z^MUtUJHR5X&_J77p}TSdbl3oCd>j{EjH`P zqQ-~$R(j)rhK22_TceSDsDP%%`Y51D5Hi;kE+~B{xfIy)HKwD2121${@>aOS$6Q?S z<*{cUbhJ4!T7i4dhAr-|J^%7}e3=VYtD*u6^Q-hcNrEP}fn$sdZm-95{Znys_OI7k zzbWKim}VY?ww*6Kjs-1{i(Y)o(;%d+8%3U zb+X_Lo%^qf3$X5)EZP<)e>_&z``+q2+qCmDjv!T9o6SSj&sKs?JcLE_-6ej78CM z1DxejfcrDFFSbfVxcvs3Xgm-!z9>Gp@lFFc6+o%}Ts;SvP7ho!r=jzlOHR7vgAFU| zsI?Ss0U6574wY?O@W%cX?<~7=1SLcMz6`Sf`~^tVZxs|&f{?q8T?+d7Ia8Lc3NV=j z;5ik5xlX5}q~lhmS0Gk_n<|r_uiB?R9skTvHC}k0kJ%2{?kb-_`P8B@ZtwWrhBGxv z_b#BJ&K`#KPy1c-r^biEa+5IRgXy|5)p6xF*e?ezRfiYcZt*71zHDHz$7%E2==rb@SNyquNs;X({`2 zckc>sly2Q(ZM1=n?p*VF?ofFxg4^wwBL7wJ3LMJnWZ=G8M_k-Ev7A-VQi%cT@~K$Z z_(vjve+qU8#AF-kp+9dl##aLaet_0Uu>4LhDhZqB-1Z(n>fV{|9oqJyL!t26{!}dZSYFXE8`1Qzdp8*d`^JWc92i?8+n`z zHuv{5t$C=jNyRTtjjG_xk3A*}=D2VCtCA;YKT6+@Cil&63A`+(53d>;s2eqlgljEQ zKltisZVt4le+RZ4IG1=P%`&ln;<%`Q!T|*Jnr-s8TBD$_fpv|2u0~#z8D}V}F=Y^U z0f)zm^?Fse9SUzVf=p&?#A+3byu8@lYeLN z|4KN{zIy!EthIh?WY$&E;sOzaHBQ+(pv2$md;-(E!r?E$U-i6S=@645?uXAYSiKK;EmM&EulRK=H5qP!-ot7QEAQ*AW}KghVB4IZhnF(I;Kv;do^osNnAcsA{Ly zX|18(Vqcddxy{+B-4;KE)&f*xrmnG$T&aAp>yLGn>lr>bKIQ(Mf$0wy{BgeuOCAb6 zd==T0 zwf@ms>%C~&i|_c;$xxQ8llr(|R2OPBrtv za_2R!>ttZQRp3>!@?Bv!maGHiXk4bCw#Dqu4+LKcifZbe^@lW4kGWVWAIB`11#nZ{ zoM0$^6{tBLF^FV+9#4L68s$N|S1avRQlXN=RrJsDIr=VWDqB+tAo9eCuv(CdbT~0k z35N=X$e*)Wya`i}WLY*hlgF6F!C(s^x@z&f>k(Jz)H-?IsRQxTPAQTEJLiA=-2U;C2@tvA8iPy=y^Qc>&-~#O8n&(!Ta7Q zKL^$cC>v5q)@u5yk^`j&mPPe{-ZD;V8dB!MeR2H*-H2j}&k}kV&#K z!B1uD$OD$nj}^}1bxs6WzxR~x{aJRF>bS~MR6VBe0A><+upQIJY1y~NbNl`I(fh#4 zY&CtYoV~2LYNdhNQJl1OM0n3JT*>~NldgL8xbCThJPxYtxV;b16^C2aJ zn(e{5_@5VbqP`W64)YvNZ-H645zw=-Mewx-t84%feW+4lw7Ns)@ygZt9~3un&qTZA z8|{IE9{$D~x-G81D%MMVX3UDnDw%YNsnVgOtqjWnh%%7Q zk&uCkaaX2Js_0_-+b|veUGePxbusPpVAR8v+iEb<5pi+c?Ev%LsrK^uBG4iqc&>74 zY{wwWbd(X~hd|y4-vQ@G9h`iygH(5woZzTCbq*@bH}cNO2L~puuYgWn=RPU`OBo)B zY5E_8&jll$t|0H~wZJR!C;;~xR{+3C3)_Oz3;yS84rnldt3)7=a~+B5lWA3esoJRV zR5IXLxz{Qu_)^&g--#<*c8_tee)6h(K-sF&iwka;h`CuCrk^)0d2UYtJ}d%j-nd|4 zoL9ln@$PI^>Bt4c{GYRPUmU*qEbaEEbjkcNk5^-7#C<{cFIsDTN5q1BT&MJRI*qlT+f^%+mYD{z#58N-0 z5hevH2=g;K4*GbF9S-~EyHn$Zgj@4K& zKTdC5P+~q^K;wYQW2VN%;Yi*fY=I9yH_qcnf|!35#2kI4!;75DN3Azv=Jv;iU*?C- z5Kovtq=5^1xA46WT5EmXTI(0bW8wUiyDADxR2>!PoZTS3t|(tr{;hacwC-N#X)~;# zoWMDW>U1icWzNuMC9|{Td8Coxo73XA-cQ zJXdovvYbkv%IRF zqze0Sd`|6+d1gE3I@s0)d{@_%d&cdoFx)D;P@Qv885t`k&pW17n-0Vkpy$BmH0(2B z_`CdH86Wp~TPz02uf%N!0#gS2oJII-(&+@0Tv6c3vaud-1ux$RD}2=kES=*dy);z( z;++C?7YMNMYfU^x?)5-yqt-p-v3m0a$56GywrB7Gskg;*!AAr@dgk0&d4$ z%!8yCEF2_Npm)KZ$3TrU>e$&)w_ZbyIrE_~Kmj7^b1T*-;cq-&-OO+sXP;-Yom*YA1yFj;>O?{PEBz*^TtP3Smp5wc92Ch7hE58{CQ!bPn z!HR8*lL4m7w0TT%bD#%{$3ZN&)6FedXOcksi1{|V0t4&|A1&}TH;#%=C5NhWI_TFN zl(LO_Zq9X;uem^k`cjFhlHZ+Ynya|m%>ERhqa7(9=W)Y?mwCiM1Azo9=bpTh) z*mA+^?3f)mIRL_AmWLOgar^vpcS&rQ%#X8Q1x6{;t=Lxd+1N_@F$4O_cNmoL5kMxP z3B38*DAM3yp!(v#f%-*0JH1WcJyY^z|N$L=P@$%Fd13bTVd4*oQ;A&`=P8|tvqtI`|WVkQB<3W78*K6bp| z*j$U?|F7dCNN*1uddC8@u+(7ZuJ|2?p!c*)8|4&dFDGoKe1@@u02) z5XYcWfHO3u3@i-x2?{(Dkm~}Ti|fCCt@X9>njo$;MY<{wXPrlb-bb&sJ_5cM!18h+ zWoPIBS0x9?-&xE?d{;Poz6Y$H+bc(R0L1*O#dzwNg8&A8br*y>RjcJWL18;ovN3=n zFABuZX^UVrlYzhaKTiFp-Qn?7c!NawlnSISXe&GAw_4ORITyU9f~bW(Ej!WQy z>mPMqjw5E@T&e+7ZCnmCQ;G z7vR~sd3^Ktt$3`n;*E+&1n{7)xgdQ`0&c|^TorcZ#t9?A>oP1-SDqwPAvEX&X3C~!M zpQWc>)JzG}RyL^kLqKryp&ZBu>+$8Wo+9Zf8&I~2JRx5kfRwy^PT8MI+LW%U#G*qUuV9Q-=;mO zgs{TgE$|_y)^|JqiKacbt7tkN96d$%X87F41t#O&KkHuk(iSs!Z;#-g_>RHyk1h1C9;j8v1|IQugA;w!(u>k7`!!1@x<_H`Q4+fEAFa^QinTIjHC;0CgFZ>Nk^s zTo>@JFqZulx<47ukgk!@AP*;Tw{tZNch5jS6ute1s+! z0xHAIKlNO6!UOAJGmh&P0I?3UO||l#6Nf6;v#d%64oW%8@jh`^;u03MC{H;n&IaIH zBuTT{XF$L%(sC=taSXCe zCSB^S0QUdtl~+7wz7(&_FONm~mkM6oUfCsHdoI=hyf?h1!hIz@7u>7!s-P}WXDjah zPl3zIj&VFw2YHjWzvuri*INHD68C4}c|RwBTNCG4e+ug>pGO+1PG<{Psu(wEFCPIo z!KddZnFlIhBDhkJN=~>>bxtW>{pHk9J|}uQj{iD*>F>6=3+BIc%-yMe3>b3qqjcEu z&U&o$#ewGA;Li0by_(2lQw3r?R^x|YnAbVLxxm1(b6Xz&$_`j(>e;CZTpVQbI|^6h zt7(}ast>2A1cc>OB`Fq9)N^9+$Q)yi-<3AhE%&A)lA^`9e?+!_$r=-NADb?sv? z)#h}loVrSk@SM_h%Cqlh|L@|_#${LspdM$aDSHRlZVdRE0%RAom3&o_;=rEu!-1H2 zbc-~z-V^Z{;8~#;q@fa=O8(rKN(D2PLD{j=v-3~o+q_thd2sqrI=~>N5(Q;DXfqga z_;q0SuCyv>2ab2vpGpMs7}rU$jj2A8hpKP@#1jB-MF)%;+?h&#%vS3)_{+ZHv}_2Rv@GL zLp`c`SU-2{dmQ$SP|1Som;2!BSArhyZpOOn{K`#Zqm12ke6*!(i}`2T*oLRwo$a#T z6@C2A_T=oe0!|#;DxuiuZd&P31v^Y491nQS@yz|OuN+x8t7vim_?FYn2)wS%Dvq7c z=d~^=S|H1N(uaARkMkIw1>XLHn8D?{|IQ~S{3?8QtNXdW`i*TS`bHWPS&;qS@0lbv;qZMYuOa=|Zy7GGoAQGl^%rYNsv0OW;~ zEAIaDbEL|iu((7++1Ne?Pl*-oPG>w;Jl)NFFfgq~uiS4nD^iV1@|x%HKljIp9k;=8 za`L#KUj=yesDpo_JXMl{^myE{KKG&U(M?q=TgSeg-C=$Asw&>d+=wsaT@k+I^yTWf zB+Ow8^78)hXu`#blc1M({Qoh2=N7+Ck4~qo&oykfs9m*h2o~Gi#jS4RSvG4L-C@rH%{E_h7}8g=dm@Y+}mh>ro1zirSjUdNASDCfkYRxk7Z6UwIW8aro#gR&V8GU_px z3nF;^QoQ|f772iIx-OAWfWE~TydDzFtb;mqz|J=@;8=S?p zDh@C`w-3NQZPiN8ib)k%C1Ii5sZ*)Gu`g$22!@^EDjICam^ogL1mFQdM>#+`a~k1` zM7zoWRIo{2C$gdQWD;;azJGl!o~>PmyHA&lsSQ|AXWRyzQ5DsX3Sh1d-<9z>wJAu- za5fzK1oFq?@A+o`2gf&-q??N}p9`S6TIg7THR?nq0%ye?vzzTpdK`Q>s52eL0_L50 z#p2%S=JtvP(_=lU|EwqMlgE=6{UMF5*-rrZb@t7?v;5N_!#q``s>gio$>}SNT`JpS znelhk56Xr#cU*cB64E2K^+0UihKY66e;4nvIE+`<9bP)TXFFB&l|3?jbhgEygtsC^O23y36bCn&Jn~DKJ;M#&c{N%8{!_362*mxY0;1 zoP2S#$-U7rA>`n}0HMHfh2HnaNIy(>URh6#u>#DL743}AvpuPWN+^p8`gq;(!vT|Z zaB9|VsKBR!=ehtka2x)w?3decy*%cHf^IFx5{W4%W2a{Pcy`52>$5K14IcK3^v>c2 z0dH8G-zm(-(qWnl#8(3^6$W8j9D{6sDQ4D`F0c>P?}^S$19~N+!Z%kBX;ylzXdj7h zcaGJobCr@RQ09)94g(3EQ^4tzkNs7`c8Nm8dtT2v=kZ}3HCLPxTrR$uPl$n z^Z)!n6fY!7zSM$3RWd+45zqafj^yjeT_m1YvSc5VK+if?)9|O})zAtU6*j0D2o*$f zEVy149DfdAN|y>Nc+4trR`My@Y`>Topbj?^++1>$G?Z)m&Jyjp| zf~B(QZcPi%Oe$PpS+G0@vSUBk7yE^xuW(M`U4>~C?r|Rq2dlcw1IG;4-#tDzdQKpS zOdN;Cc-Re~+MW+H$=SUb(`JGZ65Y|KLH+SDddK1v8_&Oi9!#4r0#qL<| zfc>=}hC5LV2<#*Y5cyjL1U5oU1lVTWe=bNj@0yieIR|20^Lb8o9*8N(o3M2#Cjj|} z&y$t2V{m#PfbI2hjIWe3KgvFxuT?;%f(+B@K+PpMY=@N}aGR=6?n}+;D1Bfupq`~O z4W@%*h1Xrs;6xOIC>MSeP04v0;E1#J)%YdL&w-qOzmn=H8&HW5o_EQTvOVVCy@JU> z7;z!n=c|K?w+?eRAzM|q!Yq|!ydVG%XuHG&>9UMnf^zp7zXr%oRuu&(l_1P0*K-0e zU);ti(G5vX^xcgT{1!dOTuJ1Pm-3N+* zS-n1q)6VLhaHV(kH9+Q>fnNEwOHLd}<1q(W#fwX(oQlGEN0y3zRa`R1za6G=;CGL*6!thQMc#P4vve4| ziu02g!}Z;3&z%pczIZI%m~-8`zt!`;s*`o#y31fX1uBr~R_3~l$AO{)SQYHJK7*Cw zfAYI>7%omvO1I4CZCVJp2m8MjGXgmakZ(6|pmg%7VC(qEqTan`2j($SNd~W)$JOiH zmIDdOhsj+Y=WBmAL2tLs)Vf;rj`~3W&;1u(2ajBmhI$^%vMYUI9XIula;u=~k|Wo* z3W``%fZ=N{S;Awtu7GvG`g8pv{|t=G2l_X+UuI?c*?`yl&o<9vt}uzmQUy&sC9wY_uRbRZ=ZZ&P$;8A#$ZJiDVT)sn4JmYmGmm658YsZsn%Z`x2?H2sy&Vc_c z?sjn&00SZ$vH~;awK7s=14_n9r;0{pzdTMSbDVbeWZW%Zhonh8XI9P?pGb#Wnf_&~YWFFgBPKU?tl`I#O*%_-+$>xHR z0)f@=!!oJB$o*9OIz1!!;Kz>kDZa0aZxvjQHV=y=r%QTVpi=LNcfqL&=;~TE&RMqt z1q8+|7Dhe_dxvxIuaYx&{+wR>dx$oE!SPo>%a35HSNYD&Vm_9a`#_#iAE+Ou#d_R^ zE6%wcCm}A$z;-NO2gcpBzS4uM!*E0(w*;senrlRkz;nX%J0eKg?uLbyO`p!K* z`{SVGl9bA!6%ZnSEHh8U6AE| z6@M%X7H+GqEBR0smWvlAqFgR;IeT|?*TH~T#f~bl#3m3e7Jy70aK++1u81YtKeL{d4Y6X zaNmXok2Le{KNFefA~5~!NCfhgv`1ib8srxjpD_r#gdmUISaTrbF;TWrfd%uX+Ax1v z@5y)!f(!rDV#R8lnXfHgCww|~l6;WrR$x}io%MOaY5rFMQ6)jF4;L^vNK`hU)**C> z0m_1bN$EtbyWj*u$>IE{8qbP%>>rB?0O}a^<**NVMA}n3Va(&P!o{Rmm+hYW<78YF zg=5U0lJiLHa2T%evsx!|YWh?D!hGOwr8Dlk>%U4GTpVI8}TD(p0Bk@0zD4bqpaA*D} z;O^{_b(Ma?&(wLfIgY)0eyM^t18vfA$u0+EUL?jrNy(S>+}jYRh6dZR;}r|adTJ6S z><5q27uBn_m0wplhk8(B&vvH#2<69D=~z~#;e2BfOOQSuvyHJ_*cYyIsIY_kT|+FX zY^WQGoq;%zE4?HvMrr=e{u@J{p_;+z9$C3-ZB5?Jt2~hdzkojL&@+9ph`5teei4 zu~dtiIxyqlu!?jU~=J{!Wo$}Qb!MIsReKu;Bu3j zcSRELWwAZ&<+%PRJqoCtxmuH1sN4ArRO($LCpOXycM z0Ls3TM}7{B^;jO}i<8zW*r@=|zN5y)@s75lg0k|l6KsFYGAo;RHlxldoL4fXjZPmo zWIKfmN^b6d4P|jO__rHJRoL%%tR3!j{2^T%KNyIVv9MB9aO6R#lr9+&ft8DLvi#3L z=7LQHPTe5oW-6y*Gn+0bbDd7NijU41l)>>MfScjuH{#s>p`$O1JO5iD>tpes?|M>x zwYE03No{Z`F=ZBmA@HGdvk4Es%FEOfwqLP8j$s|=Q zt1H-6CG6_;IFy%zl-hq`;)7N2Rb%T`ue)ICs+VOnH!rb330);vcXAl`oO;U?KKyyQ6DK2 z7Z;;dGR1A~IqzlwE}{r#Vv@DF-|CtI4d$=n+Xc!=+dOx~xLm0Q#eoY4MLe$pC+iB| z1URQ8;Muri{kLnazY}`{+=yi0F+of9!Tl)xpv)|f0&gXUOQf7Fs4>ZNkHqxJ?Pus>eh$8xZK^V)d>V9bN! zo%`pulj&;r_yFrGeL5MP%-oLYsz+N6UzMMr%t)7QlzG9~pFFR8HLpF_BySbgsl-E_ zuf_spQpd_hlusxcM*{n#aDRNw$*Ft_kGq5d&vmiKB^Y-e0Vq&a#GEjbB)|=%3+8O} zTjP}K*eRw10j8&}tEs-M*CiF~xXi0F26k?4!-lAU2v@k{=0N#Yz+YNxeN)^S-->hi zWiBioIH^VVjaLO~%!|9O^mQqw{!g&!kGo4x8p<)buPc!_y$g=3-jMu`rz*^axdfiL;KOZ}L#M0Gq zd{zB2{jI^7l<#u9wSw1NQOQvy1M|c6d7Xsv$E=4mw*xrqY;bF2V1KRj;%Kt&*&kIq zg_rIc{;y)w-98yml*1npPBE(JG3X>cC+w+E7=stnWd1lL9PFL(S?(~`>W)GKtXb5#d*h_=VqoX#8=SAeai=Gn%p zAd2;Qh5*~K5Aj$kTgz*T7Z(6k8x?dNKDpq*fU6$0U_D;R?t+=q57KoDX<4@Xjc=*! zj|90WELF6tcGIG;gcn+$89R4vu}=1}H9S_=Coxjh=kZc>oLzP2`MAR7JOBBWAa0v+haf6_Kt6=98gvAXG2VoZ!98Zcb1wM`z@{bv-dTq%$7xV1KhaDQvV=!5;4j_Ym z{@n=P-nQ2IJhF{^PoABI$4beFa;YT80XUDn3TRxEQ)9okV^Q7{@jKT*sMXzkoOF1P zgybb(`+Gz5;cUf~$_BYk1sW`S+8;jSMcHsYhkA9-1zu;vE)c14b_oLW>Evd)RHEV5 zHykOBca`KTSyemr#tYU(d*MZK$d?NWj45v6Da)}UfxT4JSNh;<$j3JDZ-vp^4&}n@ z>g^!a*2#X>)XD$KC$Y~;$Lg5dtHLrVx(c_rp2S*SF|9gx3Be?5a?Dp?sDc<@SJ%3g zx{A*V@X>%2_&X=yeqA6_G%7>sct^nFgn>aNWx<7kw_-iO9=!g`TI&m94Zs$G?L~1d zHx=T<0{M1EuE4TN&bpw_?O2y8usj;cOfLMZ7XK-yS2S19eH(mm33;gij~$cyuXNkl z2)AV#Dlti3am`33PwKPCTu)8k=1hjFhrFscF!DOKOWxhozWR;pJUZPvA7wvPn6?d) z7Y^(y=sUZsd`ry`p-n0N7$b81%4Oq|+^;%UVfOtL6&tDmTJbn3tQ$P1@LypEpP%Ta z!T@&;&m(VYmVf2>%LU+e%(-l@qT>cEjkhy0<+QFY4xW;a2exu7Y{Q8Gws+3UdjlvL zs>Hx08yG-&WxkRt>8J&P`If^wBJlsCwbskmTAv+&kZ%Uuj39@1UY!A}0OQ8IN`~AN zDa)wFcZEx(-}9=&($V?};5bUs1M&2_NpMfO6$DIO{(9!=R1HO{EhD;%X5d%2453g#|1V zk4?&%I?48z;_g2;uR>a?e}_wJsE_1>lN9dL`Ln{#N~g|;D!W@lEW_V<%;(f^rt9oQ z73NpPrZV2-hhP*KbW9`Tstq%Woik&sIOK`5RRtROTk(Mo(8;n2UNO+RWWdo-^}0dg zV?L)0RXLDy{sh8PBmw#E?}|l0PsRs>_Wz#|AG5g}0rc%~H^}3GyjA0YZIqlYs4H9P zz#)G>5o;TshC5okt5v$HWXyAq#MJ+}*gpa5b5P>GSZ>!JmnzV^Iw*7c*@H2Y@eter zDxMs_YFr(dB2O-mcVNia(D9T0dp%~mo{pIye%GI6KwkM8Cj@z&1B22(%Ug}T!Y_Qw zg8OzsUtvwkel_+3d3z)Sxj9x95UbzJ5B^rg=^JS)EIADl0MuuPHLi3quEJ`rGf5Wk zT-TQSs>p3#U%k*&LFv2ar_wF{pT3s ze+A^o1N+Jr!T&}~|6dinWwKSt>YSU43$UCV@j2J$8SMM;k{Iu6#aMCQ6T6#YLKh%a zQL;_|`r)vJCu4T&oj?!$I_av}1TH)~#d_yN!GXB@t%8Nq2g;)SkFhFob|3ri58r+O z+Eunx@$mol_9tMwElE`%obQ?Lxntd`q6&&ChGJ4c!A2TTQ9*v?mmdYx#<@khRj`59 z2D=r+Rs=!O2GMTC0Y^|#CPk$|P(%g?P(Yyt0hz0ys;H_vo_mJ3pZER|$930PDP!!&WwzRj93vnckbLB?mN4f(5~=((%(_g+$l;<7tnWfay&CJ>SU08<|BZr zkz;%UScgGiRD;Tn&2^M9?j#>PSk3{}l=Hy=yV6ab=PVj!aQq=3+^%y3mo#8sc`l!c z-h^}c<+_~q|H8G_&suA}626v~j|^CUjxQd+YTTJ`wP3Z+I2$iHv#dVk*+=0{ry6)c+OJ$SJa8xGF9HsNlhduY$iD z94u;}D`Vuq&3$%3NwsCC?0B3cRaNq$k|pL3f2(Jm`HVjU!b@P&oV=b%5VouAfpy2@ ztH7%oE0@@yj`I3r5#VmnmcL4OuHcwv@|R!xdt(F>2QZc2u&nCXt+P=6v5L)i5-2lZ zU4!6%CxBns47OJ~nE;ie;{w>ER0#m`hu04@hS$QrSFGXSw>k>KWw)wC)QEtrwJ2dU**HxUae`Vio<4SXUC26 zPA9AJsY#Nf>Po-l1V9m21G+@U#Q}W++7M1R98dwmjX1s>)Bf)O!R|8LDd7SrHy%nie7v)`4-0}P-!&XzS~Fk!!_WOD z_#%*~q(UVY89Z~!^lGD9_3V|$5H}5C-6_z)qJkjziEXp*$75V^ABeoJ`eHkBpvJml zd0pU8cyTD?Q?o0o9m=K1s194jW^S$De;7Sh1*TmnQ9rG)lNQaM=Jj{;^hKO5Plr49dvf`ngA#;caEtHVUkrjfWairrG$V~fC{Z+uL=(%$aP#HwC z;44yDC)WbJ8+_3&7tUQ4PNRxSkg|^|F=FRXcIg}d`^`mdPs8eO^{N@`N6|wimrAn$ z0LrFGL_t)(UKG#$pN>8h@H@Szg-T8al*J_@T!;Hsi9~MooNGXig9VWt&l8|xTFO@# z9~2(3ec<^@j~IOOG1m(Feo?#ya|h0=ckf;4f^`0N{yYKl>TWiZ0A+ia$RH274(w_y z0Q}L=McM?HNzhWVC|z_ivTbu4_7BxZhm}WRGgaJh3ED&k?o*wgV!85h#f#I&krRN* zuw4h8@Q%(sjF@^=;HyAH8BS$HZom~k>{JRMFu1yRk6nVpJo5lofTCn&AXO#L^LyS` zVm9DiqJ6Fm#y%9ltFi48ztoun^{S{knWUV8^Mcp@P(~F*ltXdwe=umj2A-9&yv!f! z0Q=yT>}s|Ec~#@ieDbqpEUAO*@FAoO{`gpg?TW|G*_8@nY`acB6^OYR4K*8vG*K^T zcle(B3i`SA^%FxKD;bnuDC{}FLRIJEiY}gKy(mBA!~pB$8jySx@;&he04B6YGuwy# zDSf+R7dW%M+C)qhMF+=4`D7Km4~-ow+f{h%N`-q!FU$0`S#?d(lfH#hU86;_RaDl@Z;S-+UlK#h6=PiHtNn3>rE4#c}Tnebm-!{nutwR=>f zV#!h15cBTFWLhd*V;pBcXBpJ*ZXC*S<3|8os*FfwKb4SOKdSA-$+%wz&xxU`WJU#l zrkBT;ieSVu0L-fkbdG;Ec9&eS9Bk}v`%DZbHzU}+O&njM=~2Zq^D_x#%C?k__*^$G zc`dj6Iuc zCN(Sz^P|9<+ao`G%y;&;2SG?(=b*<6+^~-aXd|bjc_{oG?U9p41@c+cRK2rhPGFSp zyTrovcMLzi68d;H5`f(8QzhrD>*_c6kLS8KdU*YTN+K1GB{nkO6;2%szSX!We{;S) zk>BZ!F<$lG^;4ZY;PW4q;hZa+GC;+6CCqM6Qx2%=sy-b*{Cl!DwmNrvP=7T+&edh0 z=cIuHrIOL{mG6kZb*z%k4O18xy1uzCJENMKWZ>dyPB(_w2hPCUGBYwRS>faYZQyjr z|0D1lr$&`8IXmvgg6kqZH|{Kl3IwT}OCtF5E0NrG>XWWGyH|d%5+Du&u8*X}$yxqR zdrI3_P2b1jwLLYU#bE}_LcbmP^|v{en}DBr zY@Dt0SfRgl$%TtA2P20Xn>9?_@EW#p{_5-q>v4P5Lxnl_;3jSb+yS-&xvG<{u50g% zZJn73v3$o(zSFR9ppgD*em_u5Q&q;8$JItMl_9&}-x(Y7`gXzpGiG{F=@O@2UH{0h z3T)0Ey96UkzSf3!xy-WzNX1jW_V;8=5$DH|cAONibHA0nt1_wYfPnWvz*#D_vc9r$ z2b|UTxa2~O0|Q(Ya9Bp9%}Kx_f6Vil2)UK$if`lQ3n}uM(5gyQQvFPVJHi<=~DM(!;<}3Csbl3+z>2$gko{1z{y0p3CXRm&ep` zmn^7jZt(Fs53=e<87kJjL)SAod=;$t=P&0e&~Z-h7Nxo1kL{{H*ujx*e(-0R580r+ zif^QOHKzPuLtAL8K%wMU_N+Gfsir$s&{Ke?KmmEo?>X=r5qa$1mN=T30{&A|_--u> z+bheB=cwk{#4)0h`6TCyeyAwQJgKdbhE$fEqx|qV)hdMnPo*chQCWq0;w;MCp#uerF?wrDb zEYI7L+L)p%mEv7+VZ*6{D3@wCJ~h}A-c`^?IhfaD-1${TulTq_YUIoRj~+bDf;va#sy z|FlB-De)04)*sV%_KIyCASl4YYYzNeu*_uc&C%wTp~rz7X{%izm2ap7f|CpEOYwjS zC*zC)a0Ys%JM5qB=H%z&nLR+$AxF55%i~-x)V4d?Vnn zTJhP{Q)7btoNV!>@W`=Rql3E0I%(HcQFt^dD$#b=@VrW<)&>s>^N@ymYoZGLFYs5~ zG=dqKD#ihj8Yl&FT_DCgZw4h-s?16O77tFgNB#ViaYWVup3AAMQ}B2-JzL{IuYjg2 z%1Kvot^){ecRAJo+!E^Vm+48*&{pGG1^~-ul-f+ zRD!|&#es}<#-OHTP8xi-#dm=Wx(WKPi({VE;lfE=CPW;hRRG|2j=s_d10~Dhd|By^ z^{eP!iDdB!+9F+HB99x(t!zup4lqv+M-cZovCF!gY%$+#7rEH)w5H{sg*{vmbeJ!P zODeD_KHd1_rcCb^iAZinhBOg3IPj&vzn*w-em_lMn9dZdo&KsmmY8W)vAMzyFLL5zkK9J4J;bzDS&=(0ZLsU!gH1U zol&~v%atpUyxcApG{mIGnqqxX0Lu*s4or(} z)CD#KFy@72;PGVHIU(YOW!z^5{6}K@;wRy=uMSYKZ)fMqCzM>Sbip3=#P!uRr9anK z{(dmNczILokK%xwvC0J&-kp!>qTGsC_M7U_o1=%o-qbm-Roo_fBcbK4L;<0C0?;cmrj1<64 z6~l}>r&EIE^-Ik{jH*9XkDbNQLLQFr+S^aWu5bCBbOreN8?T@4wqCo|`XIWS#=EAe z;hmEdI51|AMZWXerC1<@i|FuLWn>kzFG&vYCwORQdZkCaY@XJ1ue8(|uFtn}f=HD7f+*}%VBYyInx z`#F$-Wmh=jk{RsF*)5+tVe31twSIXdr@2cb>eI=5ClZ<`;IrkNB(YwwI2=($$?Pzo zOMqNr!1Y|bWd6H;@8JQB&}9}Ch%=+EqX{$2I4ar+q>P(+atTI98|hpPaQKP1d*a~9 zG92OmwYYOUfz0oQ83pWr0%i&bRh{lyWvr~5-0%LD0i`Nvsj;m1MV&eOQ$daEpgwO$ z()EL}ZSRAS&>b3^xD)c9K=KFTJntT-5|33px?(ww1rJ#7Fa!>_K7)09Gd}-3IP6h5RU~{~3y6=(=#u9~>hkRYj4VnTcCZ+_c7*kAEj&>{- z45{B}lnOlgSY^A+bAA-(M_`*wq{DYY*ghAKo!WlHTI<8Y`EVvdB`$ni83Z~4^HdcF zvgw!y$ifjo6dy=4KJ|dWP|SGXeRwL^Zb9Ki9)P)v9?V zi+j_<)khtwzES6Dwl4AH#qs|;AW7%NvI@l2%niRCgMX@U5JMgC-U3c$mX*g?@qab! z{#ra9`-dTSF1oAM5;z$YZ&=6aamB3KY3lyN*IK_ezDMBf0NXNuCx879G|N}!j6Xok|Z%)Vk%wE6w7p%7-#f%qV=g+vSEH^(Ak68+AC7bRH9_oa@>(P71?wm| z861>>SfC0oCx9eX|d!$0Zyv+0VORpl}wG_Xk4G(>Q-w+?c!scFSZLDqmOn zVf$dY@_atGKP4W&V6F98u+UlsOSd6UUOSaVgpy-%SR(gaF&QtV=nMYO4mgN_-f^ZbxAFmk}7Q#a$uFPzAA^YP=KA zsQzfIcKr*H{pROnm2Au>)5bPg+yXOYh}G)kt08O3=ZqEufeTt3$T=uf0bX5mz?ZXH zSuRBqa0=q@8G!$jF)Q#Q(*4bNRPd%~^I~{xfc;!H34ya?)WL3mVVWqPN|IRTRicuK z#D5!s;5*`O@Wt`}iqNH!$$>oc?m!ZQ9@+&z$8|QMl0?;4z6<|!`0r`#Um0V3vk6?B zv@u3-l7##?{wvIMI?E)bqsfWSA>_RZTmSxTc=qpJM@E^sA9tPc0%_*;oALKs*IJ(( zxO$A4Hic2u#!ipKtBl_cGknS}t1)wNgvX)6st%`=FT3PJ#RKI}*q-%`b@BJzcKl4U z5`>k)3^~Q+ardea_r*zqx`w~8REp>l4pnlY(Dy|W@c!@~HEyQCwD>vj;}K9#D( z76zW2@EapBs7g*(CtVk4)wn3Ia0aK^UNUzvsN>2pTtO$sqUa(B@ZA6gV5N)v_{(Xm z-aWwor(+!Qd+}u9HHN0-_?q4q|Lfe>rIr+9;r`$ER%m%BFMZWeAgl0?%^?Ea8yQ{R7upzde%A z9}isQ$Ezqy`akRXP9*YwCuR^Hqy1JK{vU)atUFa$Kj=?YTbFP+c~x=os*As_?cs1d z-c14&sPXwuNNigL_DS~T0vz+^vLV-ZSF&C@PQEhpU}Vj|YOVDJ@vJI?ihIq6?+g=w z?u4->7@h7&zbl?M|5zCC6O_sRbiAv(Ad6S}a=OCbY8=vtQr_GH;2**B@4PcsccG}i zYF432x-!63fl|?79dP2sz@00M5AJf-*v6^^I`_{*+@1Y z4a~v%uCVPVhJJW_&mdoCJT^K#ntrM{qHK-Z^7xBTwri+m_8fFGlOek{Gl}q6y zxv%`mIe&4?2xOp1aLTXPy)jm7f7M#++bCE$T~`4{jR)!~dB;t4P-iaDP=Kx4RDoTM zk9w^q3ny7lUYtQu8*XIaXTU|rLEbwXbjg8paJ7&Rf3psGt%rKMB!8ri-`w8CRV>P`ln(g3;!p9VeD`kq z(+mK9<*qW|uDDExX)8vj8BYbMu2h+p;){=Su}Xeh{<&BP_-Emp@CuySI1q^V`mJ!H zn*eZ_j64_DFU54%SFE)@H?r(Z5-JDgWB23mAnalYb$g5oL9D|)MVyo0hkTGhGECYUFu zBRB3y!`Zq6)agPk?h6C7>suA|_JArbq2RU-+_~+wn0C)cnWxqE|1s)41GvL)0B~Qd zpR0jOr>Xo;iQ}JvSwfr{O11}lr|tL}@^qJPeqZu15LSUhCtQ~aF>3Vs*SQqq@^AKtU_M2H?|AarkahzW90)%7gbQ@@u>XA1<$liN1J_` zZB+R<(oi~Jdva~uW2)4PTD!pZscar+S@>?x>Cz=Fm`E_6$eRkj6CEVK4@HoFY1}P; zSIh+P_f1srXP%DW3#Wkp>(*MI6N~Vk41DB$Hk6#aw+PZkxs+|C-cR3G`HXmU`Z#>x z3U#aePT|l&=svF>&-5MV&nz$YrReKs8^xS5vMT6T0eO-za9tNv zxi0pf=Ujh^rZPml8@~pxiJhjN{%#!qw3s5tfUo+>dTKy(F%9yP=WoQKozq8peoMUi za*aNpspNF}#p9gPdjM8}|K0eG!876D_{w5u++8r^b1slnZMm-MpFtC^tKe1zSOt&? zO6pZ5wnaYIB_=GRn}t9gtLr>2YRZ!NRCma{C`<8O`BpVPT~K7&m91d{f_~^;!DHI4 zKfabUUBFReqj=`W9o)`60^xv>3CbgJ$9*S~fRBiqh&wRrh#&cKn1p?0|1Vo>eNEuS zlkkcm@3@)ma<-ZEgJVzyP2}tC<1t3Qmd$b`O0D;xfLIxeW3CI_ie}~f{HJIlJq57bhco^(s@pLgeS~3rvYn0}=IdDeF`r$)%VV|?=b)7x zVLPO!R;fGLm^Zx6tK{)omsB|!oR1uvSskv!?Ny?qc)-BKGdGUstmj@!V|$ol1@ndf z(dON2lO5PrGjIx5T|fM~Ee3ubuMYR}QC{BtpI$rup}-iP_D306XZ-t0;J~ko$AdTE zab#jx-Z-T^>?b_7EVJ{yJF#!U>4xO@K*FIG?Q(*V|CM~o=JT8?Tu+^IL3$F8Tzqgz z1D>n=>jl@US92yAhAN%#6{tF}cG;N&U8R62Uub|l2)RfszX|XUV;cT#APLB4Pzid@ z+0&Vw!8_jWz(zIi3F{J=Hny$a8&-TJKPS4cfuy9nE92MYrpZCoM%C|O}=0Hb*;#4&og1#C%nCt9Zv{;@g$Kb3aa&7bE~G6Jr8ScqNDv zej0$Xt`b!BUj>eiKUbWArV-wP?P34&TI>IaOgQ%y$H4Cb2R;*ZPLlr_I1cbS4}Q$W z>C6R@J5Bvt@pvQq!)JSnx1{9^b^-zp_*~Mb=yL#?fSdZS^uj?LgPD?p?LdJ(+iL|5 ztcT;7!Oz)*3vSGtOLDkQm5eC6aW=yK!!mZkQ;oa20aCK_p9+ZBH`hVBxb8;%uBfrT7LNdU7gOY!lT0f-^JdoSf^O)5aM{ z#aEtZhq)C~XirBFKJ5$e`LQVFlwso*%lPV<(isD?no2~#%7RmB4onqSLD{&oe^pdqP(X)H&TW+~bAWQTtolxx?k-*d5GOLs zLj~~4hg@K;Y}R2BuPo2=ogJxhRc}e_zVr-uFcm#jGiCRPc*D&U>9W8pa z!F(&(@EV@u*NR;|Wp`{x=ft(=KL&(ElyP<-=!8{*ImVnbDxmAYb!VVic0BKR&KZDw zb??cTYWp*5txt_vfLFzxIR_h-QRxA7h6aMq(k{a;W;fu@97{D`%8*!REAAj(V?ao4 z*x_VyPQ~L^IgSDcb^x|x=8Hk43qYJOA#K%H6&O@sZX6u%I0JLZ=uhdM@A5c#ywt2k zr~)9$p`ItBPOAWq{HVShNLCn=JRzU%^)TfZ`PPS$)%i2iRM$8mtKLdtTXVY0dRO4h zpch5p|H4QFo}qxP@V4SR&*v8tpP~IiRAIbUcFJ<*@AT1Eht5u~8-95N|EFo{U)h_I z!C~F8NKCk>p#WOMWpZI`x>MB`Nax+htU?200NjEk;Pg)F?~E0<55}J3j|I(iJj}iw zs9}Eyo>$}U|7v8z2r6p4*dbYVr?Y&O=O<%xk1Mdcx6(7~&;>&LUGc+@D=?UI;-;Jq zgO5ur)Nd6eyC6N;e8dGgtfzGB))-_PP5>2eswjWr`U-5+7%INio*rCO(&7LX^{j%V z8fQ)>D&S?DP(IV)pqd?1HlzX!KQhB>dF*`UAy^cA3Z8!?ZW11VcjPhnR{@h{W4<4V zZNgXJ(F4sM9ZZAuvgp;tcMgpE?6ua%#MdexiWyh+%AYDO*>W-AVCrS!u&g`(2KrS0n9ae#?6UY@;XBv>0SEXoFCeu;!WH8Sp;FOuJaJJ7| zYyJ0Yt*?v)GpDVCUjZ9sxP>?faG9~^zU7y~(cUpWe(?>k#V1eeD)}&&IWR)muEh5T z@?#x~wv1&P;P$9D)G-DOydzY6DKM-+7q6@2D0xR-lwJ;l>13A6B?t_1PPVQnz9s;u z5(55>bQLb}AKES3o7yZV6C1Vz1~3e~PM^-_nP=voe|J;wRf53hP)0tlY=4Pkmnbe4<^Fc;82XaHrNk=y?|4wQjr`vX96Eh{?;ppWD38K^1Net;8!&F-o&ne6 zZK`*R!2dVaS|1&cFLJ?j-ud%lM%J(MvFiFV_*3~WUU$X$UzL0)x(;W!u1e&%&!gK4 zSQ-2juoP3MbQY${2CFO=+jsSO;Mf^i#*Rmouw)R+Pilg;UIrs;9=BW#)Pcha7!uujE|tCA~Xw=l>nC zTj-_m*Jt7;C9y)ieZuE&lRR!O2etM!W8c;8;?l>ajx#Y1_{7#TWfuKeEcZCR9HDemjqPfqzc;^pT}zn20Y$f z5?TSd;!_0}3{)&(WhvucZdWDU?R&<;LsU(gA4D+1;Hql?jhvHd*y-Ls&IJ;niysBw-K1NRa{yZrFkO|fW^ z@BE!@SFy&)cx;lkwGoHzM{H6uOp}%_Cg0`lN+WCq9+v1vQh;?C@DvTj+fG8z1$x&H z1`ei&B@G=7HUs)CxU+r$_VxeK_>vU{CeG6HZvPlTI;96 zvGeVKo5KL30vg(z<3rhn=x0rp}*$JLl@IS@EyzO6fZnKj*@|pN<<9{G>L%Gmq;JQr5fy%~?_v_!$$G z9;cPAm!aPR2kBP7B0nr6{#Ij&&X&9Xou=yk(^~5nMh1Ts z)*UDiVI6W1Q~`4}*N_g@Wts81n@Qk+vqUgwepH{8?elX<<^g|W-O7)U=L#&8E-F6o z9JfimsLfsOZ1AhjV^QPa0_6@yxCFr2S*0gVES!8wFWAoY&o-!ZdmDm(V&CZw{FCCH z|HtC)I#~NW@tPQsCD$?KI;ebDs?1yv3XYD~DT(hDaMOqan^@#ExEzQ&-< z?Nwr-Fh!Lvu{eqZhLg%}9a7bQKJ(Aue~3qipR(5a+Bna_|5nkT(!C^^DCQT+o_xm+f)G(&$%}XurEH&qSgVp;Emv}9y?SCShl|mcR?J0-EWt$7QpqI4HR>W*~lfb zVVFu3P(JQ2b;Gu*{EK=$Vuc1_|`cg*M1Sg5$xC9Ti@Rv?QV z^1z~!)WEn7D<<%~W4;6WHK6J0@OhNGOK2v2vTkr;Oa9@z|NM*$7tUZC=bVRz5B#k_ zTRnTb6jOWuI+B5W4w-j1&NvUn^xA4R@?klZ91K7_ZY}`2z>R;EEJ{{Yl#Qg0xnISf z>!-3W2ky*|8v9BH1(r%S=idt!ggF_I-?SOG-i2+n0$W^!#=I+>QF5?Nsc}Q#SBZ?n z4DRPZkAm`j`#Z7f{<-)l&Ns%4z!Pzf-}QHI2r=&}0Yg2r-uXCfnm>ikyzq5?48Y$f z;4yLfQhZL!!N{-1%kA*k1zx`HFnZ$Oihotuu2tRVKL%6*tn0uf236Lf&O4=GzwF>$ zR<#RaoYYhUqCmP@T!R3>=hZpahtsP9sVYI3(pEB_fajmWv;TY-n0FOZIz-TP1sxC1 zO^;O&PytWXRWvRQ93Rg+r{y&t>c1*TgXM6#V7=oz*gVcE2|*gppR3~Zuk7B*aVQ?& z{BUha` zr&e^hpH4}xo(f!!h5~@f0bIhf&N$C~xs|P1R5HPI@VC?VgfG-Zp2Ipk6LCrazGtoV zA7bIripKy<$Bik<;`ny;6@9$UbydKx0)gWxgFN@cpudW(m>t-uW8{H1o#NoY{8U8& zfJzJy$Xp<;5(G8&%6FCAN)FtljD6n)Ugnh(0~NScsS*(!^Ml!>sVgd}!$ju62>$t~ z?d5B&e-#_<|J6|fu*I2^A6dWBu;oZ^@HQl;`~EH2m{n|d5)D*tu7 zC_i?zvK?cKlYwog)73rvsy($`5X^?VS)!v^1&Au(RnJAIhO=9gQ99>Q0HchJX?K5E z+<>zoxiLUP!*+a}C8zODK=V%moN`67+De-Bqx$IjRL@LZuv+GSmt`Zbe7BU&Z zu2VZdir{c3{OPfHuAk>lcm98Qt@XQLGp;A%vD~xp+`oEfo@J}Xo@K4Zp6M$3s$a!} z!Y^kZ4inY23MVfW?UXNJJFZ&=cST?I!x&eMW72e(%l+PijkyybH=YC+#e7vd1`oxt z<8OMsJMRMeL_P(23ecT_I^%Oi>HoMXW99Jqhg%!xj$hYf*TOG_z@Og-;CFvjKb0P^ zxa0({C%??UI>t6GDDi7*yrO?nD&XS2^0)$yYV)Zs&?~)HfT~IdJlv1^-HqkiNCZ?J zC+BMH-T8dA`=IL2ox=dH_`-8}oZsKL7SHn^i-7+*u|NK!aXz=maoo}jSQ}J{t_^9HRy3X-n`dEu?q_Sm2W0$|@kKJMxN91rO16Zm| zb0UD_r|J{WD|&1g_@}NTt=0UV7}-uy?K4=b@!|iSL1RBhcodK;{(m?YA3Ykt^B^sL z;vM3!s6&?@$-%Z**CkXav#O_f<~GQ8w?P@U%fSBs`^zBa)6`ynu5_z@SEsk!k=q5;mIX7+~=hL04&$& zwL=C(SGfLBBmvLHwbQ4FKP>9!?q4@35m54?1M{n74Bi#LJhr&W63eXGGLJl_IIYY2 zQ^3Q=E;uU?P<$vknSTY^Ow;v?prU3vQm32%abGT3afN}7Y4WwSJr#8Gy4pH7<#K^M z3(opD*;E_0Atn2rV!A)u;1}_7$IwT@r@vsW^~FKw;h6HzkNV^@e_Ua&UJ+)wyD@Tl z=y1~|L_1ik0=(00#{CxY z@EQx_&Q~B<88y;mLvvHR6Qfo16d+_kasjwX4k~>lnB<2u-afvehOYqLVY;du+EOLJ07_n(sg>T`c?W-j;;E0eK`=~F;Rdx zk;4ToCo2QoG&m@}Iv;gq8YsFMCG<6x(ecUu6@6}_#uAg04semLOYrfp+)(REEc!bw z^!u~1@5aNCbZ79tDch`aSm@+NeW|2S`Cio*(&WCnqQ;exB8E z{pRa;?-4=&ZQ}Q7zy6O}YyBjAkc8>0KAEos)B`+T$d4Fhu>Zid)}MD|9sw$ z)BWmWIozi!UGR1oUeR!|LGi+Rbcuztc~z#oR5C1+$SGM|J6AfoU2*YD>5%&^d2`Pw z6$mQg{GS!1=qU%mpv@r3f7}*@M3CY>JAp#68 z{LX8wkB7kTwz5^cDiA@PCfMAH=l`cWm46dpb~~mrvz{sn$W@>G{xT*b*iPwBIhvAN z*(v^20lW&5s;#QWJg5W&`*E)lT`CflN^Tw}Ol}m)WwYAp&7z_PQfq%taevj05a2oZ;bSs|NHW4@&Tl%LKH2;0)>?mM zt@Q)2py;p}q_}T>&Y0K!`daIK1E{XTr;2gwQ?(M>fku@Op{?)-loY)x=qs7|LpH3V zsr`5M$3+1k`vfOS2Y6+yl9l@kY%L#>~LO@klKf`Q;ZtG3avuRkH5tTh*_UWA&+H z=fmnZ+jB*uN=#IO*d;ezAXoh>eD30%^8YFkt1zM}E-7%lV7=#e0_;i!gevg5x{B#4 z8$u&-PVMTeKBie*mu#>@C>pL0MUyjFELA@%J_5ok^&g6DLX|l^!U=P}^2JSsqUJKeeB+El)v^nur~xP1U{tSTNAkY`fxK={Q=*II8C&-8Oq zZtm)LN|3)KcJup_YpvHtn;Y!OI50*Me(W<1g^(=aQLn0TRwRW02b#^?z5jN z%TNI309-P-k!h;HUuBXmSX9nmfz5=5<4J)p_r+~5hhv^r=3yAjx_*?iodDv!z^KA0SOH3L2_^2@vZThIrui*XAwyM>%dL-`x)F#o@L zt@TxFtyjRKzW-sZ_0@5#zI5tns?r5J)gJAjT0=1HC(Ak5obA|#)lEj{I~Cq2jCP5u zDzfiW0^k-{*+vt>gv672Mijv2?@RzP3CQ=Cuf`PS--LyL&qWfT09=8O(ihjAjDxd@30QOq zhT>E8$@Q~ePOMb2<#^y=?*bLKn}9>w(GvP`zQX*faX=ocv8epW1!L96?rMLOWew~I z&n39u4%@#T=T5%&#t8POkM;cfNCHyFx8hD8AEjZ?&#(EZH8;tdN|IO}6?~F^bu*xJ zay9rlt@HU+@n+EqdB16`^_yXKqr(5`csLtXHpJJFhHA4D{11cvC-J80$0Y%~70fh_Ds&BR6l*eM3gvO1r(v1rE z>e+qg-wdu@eaG|D;dB32e5~hl*IHj6vt#+L|Fg01@7bvTbnNPv&-~T%ezYft6AE9a z_Qg6vUpo2r2duSzMFjtp<3U&`Sp~r^STSF$|1NMJ2QjRRQCu8OyI56~%AOP!RW|SV z!+J`0*~Vg>`_fq@gcGU)3N~VO-2oW);dsU0D(JdCyE;n2Rp|^yIkie$IzT$Woy8Bt zT7Z|s)cYwRc=uRrbOM*@8y^)9M_P<@Gx|9N{x6TsfnF5X@?B?@h%w;fwJr#(L2zSQ zxI$Yw6en?$FL<#mP7VdUhazZZ8)w_<7~89L2`T~Gv00}dzNTK8a(>Let3-ewtDt_6 z7AJv9uZ|y-0nb%I_-gpy>CXRk@u94nFwOr|%n0NweK#oRA<4GAm?EE$TXm^USCc0%_pyDuL(%W|z3Ay4*jvQ$Yv+E1nd1v(E9F z;*I&b9Gmj|Og#U;9pLnawbm!BwcZJ~>cw_d0^<5kV9Hs5ylXk#0ev3q2zUt=3L?+k zXO-wF{@fq(xXif(R9*>=PMh`kdm{ovJJvbmxyet26cEljzf`9@fm6&ir|8+93 zz7HGYiPtdra;ZSo1s8SRfigd1SAAmLtal}TKN_FOenb3zQ#=BD68`e8Uj={O(ii(z z{xLaM;cGQ!IraZxvAFmRA?J-)>u_3c^v@#c;hBKMhAe74D@;d>P;bGg;>LPyjA_@l z@{z1Jt(#@ize|Lie=8c5?n;NNjK+a4GOZNCfaQXX3#u-_x**_^nMz?5z~ldF(-Ft7 z;-8Ds5A(w3Pfq*eYppMgT>kw_A3(AVFl1YtW<=fm>^5gu@1%IZa3fiDbG;&PYH*)R7$m&QAz-mH+ z+d4U1AajGO4Dkr>8J`k>?~VYRvjDG)?SVfS9uHvV?m$!(-VqR!fM?=Sz+YW!eRDht zcn)|5yFE`YJf9ac971Ur{#Ci@TT)PDD^H zmBduRUD+@uOIVbRDcx}!#g9w+xSiq~`9(b*huFm9cL`pPeIg!ztqAz})xMnW$Ith2 z;7^}&zQ}TNa8f?QysP9$>Hcc)_wn)g@X3(zxp=(zMKRNnGapq#!KIQ9uRH##xHIKJ z+1=435EbrJIHdYgzg2NQr|KvlRd&rb{K5wG8X!9-A9s$2^{c%CBr`;>y6*Vu>Lah` z6ea&8+v2(%&rU7{zUhcLP50qzt-l>x178V;z8dQUJ{+DkG8l3{e2n!I{H{hH4@8i^ z9@pP0z~WJuEqF7%7N>xH4OWP&#EA_bUz4lo9E*3v*=}9mJT8hp_MzG&&q#CPBdR~; zV=9;&;SQF;mtP&71X|W(1~%1JB@)aZ>!P}@FpIBOKIPq?F?khQ6 z{Mw2p&2-~O(HQV!_tK-_K!q+C*81gzP zoenr*;-JKO#=5!s{;T7?-Iv41d7cQz{+;mUF*V+N$A~(?-#80^uQO$`aoXMNv*ESC zD>1Fe@2@NQt3_stFO|qBz~r?kc=x4fb3kQaQDd_P2@V$Kon=D03fx?u%#Wg}lB5nW zvX5>tWp!R{gya$prn4x3s{YVMk#F`FWmink@q_{MWZPdE)8jn7kAM6=Kl%~&qmltE z9Y!f$)j4HDr}7!_*e+#!Ja#QSy=JWTjdA@|_H-P!J-6((Kbl0dqr(_k)$4duw%pND z*BDcjT_1TTKz5Qz06aHKshH<}RM1oo-SNU1>qWA|sxE^}=g=$<(q-P=yW&|tr}s~r zcYFhYQD%jgwk`Sp&tm4d{W5O-5pdq zRAa@4?~+86yBc%0F&=LhkhnevvJ8gP%}mr5yj{sQIsQu5JZ-Ov3$*-ucat5wu6(*H z%J&W%{@(S)?eD;>*D-?ouYnmZ{Af>g$G^^$vEq^CWW73_T#d&Rr_H?nLcG?Sivb^v z&wu}#7~5MyZxz~V#u>hoMfJhO0eV%p8o&viuBfb?`N-$~r_Ta^X6#9x&;1V(oIexx1Ymuz zvnuChy&uI)z%v0HUlcPEFN@iThvF{-64OIol>7=9oQYr_p9aD{o(=Xd= z#Xq;t>13>f^01tEPQCKWZ5m^X!&0y91b77=&$p)i{3;cw-GF5vR9 z!-KBRX*q7z$99$Mj<&;)N+ySg>i0c3#o0OolnTCzF?L=xs7lz**s;9|_!EHTJ}aJ( zMpsX@>jp=)LB3S-pw6@2uzkLgcM87W2uZ+|xJ&uPu{XfYs5jAxvW5K1sm=UyS$@Ri zsQ`~Z9G?z;92N!Uqk*K20ITf5jin3nXrIT;_xxQja>2Xe5!)-?^E&IG0#(J=!5rgj zk4vm|U^`;pasgEF&V0MaIEr7CNzqb))Rk&%I?Pbm!ef_fUvdWLAI5onk6*3k=iraB zF>fq4%aO-b!k`iiHTL%5nn6(7JBW^1tpc-INn(Ht~lBAclQw| z?62Z`4bF8zUG1*hVV5cs{Zw(TqkYdK0M~&EG+n@`Xd)91TwEV6xKFQ7`9NMeey}b& znoDjRs89KKy2Ae_XZ^VWP*2hw;3)uqWGr6Ft%F$x=9iDt>2Ni4Zi@0ifaECwSkd3< z6UA>Ej{$N~(bGl;sE4EYeX04D0A8!?m&Y1|rL*0v!vK_hc0k1ov9LY%fo&WJF+Z%k zEO@<=9d*R|SB1yT1$O3%$CYW^Db|Z{0)a9*d0YaeP#A9r1jnYLdWeRBf9oA|IU zx-!LqDHf^(Ao>2KYppMXXZ&~I)7~E(ZxU4rlCsf`uPhzzRn#TU|5 zap?t9p_SkqkX>i&tS->00dYogm)U`{E57mA1?NfIWq_!X73Jf8xewJZ^XLGS`#Qkh z0H@vgzB3#;&z*KEd%swq_iWs)Gq05#Dgii}mCw(`Ou&!F)}qHq0&)knhe$rwDF+Aa zoBz~T`BYIrsq~F~sRW|}13b?&2E6|U2PhY`TwrHV;r@{Z%f<4jy2=J!u&n}W6%?J1 zb27%dKK=2H44=n#e2!nwtbzq@{rmd(yze7oCqETnls!9qs&I+hAVQr)-A#b1}f-N5%6F zFBOl^e+jVaOgYK_tUE;xfsUYMGT=lC;mB6@m4A3sgPzHpw zpN+*m`7w_bNx*5k?~~VB?;m&Qmp2?*{l{Gb^Fu-TT2y`p^*=_Rx8wJ%0MOhrc=A?) zvLgg@{O#I;=yKX_|au+bY>$9=Zg%3nFXiH;xw!u4)`BAaj6N0WJ4i zt$9#<@c1n8`#S8d`S;gaKNbO!gFf@3^jYBx^2cqIt{r}&4pElNu~7FH#ur0h1DkbS zk86KOyehh52a0blDyDRD;CZDJ*AJJYpwDZAD*tp??)t6xTW2OQE~&ulC0na7-}P2wp|bBKrayBg;3j+&orhnNeH+CQ_b!#poaIbQQ_CG{%iNkGR_>AYxF>43 zD|6>c%~@_#+h3Y`tF$)^d1Rp>l14Uv>JLJ>a$sIP9Po$YT*;XL!EL<9B2cMcb1=xx&` zTGib3lGWl@Pb?vM-YZ&X#TC46bw(2t~_ob3`J8(x~)-s1NL-!v@B1$#0O;vT)th-PL~sA*5G3f?62# zsp87R%cc6Mb0z@8KmX5~sjM9T&e4t3$lr5R?EUw#TQ9McUCE-b*^P~|XYSEE`_6Xh zSE@$2@}O+rkHOUtClh#w4$T*Z@C0m(Zt4e#hx#W!2oAx`l|zw5$Dk!0aUCtC%mCB` zRtuO_Aj0ndFPHg0?}{Ps>cUgrUPCtxj;>mO5;#>|DoFWqwSGvQ8K!V%_*J>vfuFf*a1 zD7a|dMF_jMLnzzs7xlJ%E9Py2?`)=2>&u0@V5{M!5-kDt4cx3kr!i?ycJJTRmj&x6 z!{D5v%SF%|`gHoIX=MMKreLOlhrmP6uJJ^gwwrH&9S=I}(xXsV{PR%9%C5uTn=#Ds z^zReXuJ9zG4Dv&;LVKtA2U94)s8Zia;d)7qj$&% zIm=IqP%A$7Ng~5;^oAKJf28Dq$vS24X7+!TEbG~G8(Kk&6rEE1)?f!+7|F3(dxrhV zi_in2FgwxuEInBlU%2fn+mA0e2a7+`f2YsE|B=G;`vJ{{seHr18!Nm>k~fX=5M5E( z6WQJw1w(!o>tAick5l;kb2ORIFsf#1|1=|Mnr#jIg1oOiguhq8eIFCg>cbER(^Y39rsD7cjpa{s=its@Y^x7L@a z@#IxOa&X1Tw6JFTADWP2GLt(`CPI!;$iz{7`D-Vfhj<-FEQ^RxZ)T~U}8to zQ`*;Av`u#x*#?4uUU5CSpZ_JwXyHPn3GZe1xPOPKi!%@}YP4^qxQu>9h;UL{PT^47Ur6yA0D`G8m>SkK5@5U4g2)a!!G~chpiIy7b#vZl?q}sbz%?uM8>P5#FNUd zN0U2v&8*k=Tl?nZXMVD@dvsg@#?H&ib)I zM6S@HVQ|RapJ3A~8-eCxCRpO@IR3w~|H@WZgU!MFsyiD?g$(hE!i*31QvUhc#wh*_ zCeMrKK-Hk#poHv^tO1O#9{A|7dgmRh?n_3(*Y1jRN7X$vK0lEUtA(&A^?128FG`!b ztM_=;XswYcEgkjXXN)ABau&*Y^o>-f>7J#<>QS^f53fNCpsZAho`vZO;hP;YKf$N@ zVWaV$DS3R%@ja;N@Z>*nWn|=@RC`OX?1gV;NS+IdVfPGqaKLIbz(0zlX=hKkjo!I0 zI&hrj60`r-dl*9uN$t3m&RpT8Rbv=inY@Pj=+$>Jj?Zjdc-mop`nWbuX1m(kkJ|(E zytyKb-!iSv)>F>=OLnoKY{y4OfUUzu9+{wQO*WLG%Ig_H=CBSU^cLCRIz5xn)<*?&6d znKxR_C78y3zSz1Fcf>Kz54TivM}O2*#9mMp_l0*}HKT=HmWqfngPnh6gu&-6&?!z~#y)k(<`GYBZgOy}Ydd?La4TnLp2eaxCLB3A>Vjb@M3a(4()ju#W4! ze|0N#%fK^^`|-+yVneo+?0r+u&@v89QG2UamV|z*8wrKro1+gIW6V9jOPylFHMp;| z$T3p52#agEi~q>EHeY3|oz>#TBm4KWnzlQ8l^(pYnDAdYlQc3oNBcpHJsnyG>aTFn zfbxPbW6~I^`#QW~uyxB?0LEC{x|?#*Y|c+~vWvb~j7fBJVrL@a69HmW=?tq#=VC2j z8s!2kOurK3%$4r|ZFoIfo+Y&2J~uHLAk-^!Gt3_#^fbaWe{X%+(a%`{G9JmANya$vOgSGA_lCAN{V?L8%kB^-R(aB}SgKgFC*RvADcVW$a2!T5-!t7mQ@o0XLwe z30AB*Awj!<^H+pL)Gyo)S$67y!^%O@wf&{k!F0V%-x;tzpa`Dx`g*UFI)y;8<+4Hz zj}z1#kX=|{D3O1KR4=SGkrW`bod>z8BMdzU7bhO;El*4jFn&xv=D!26`kW4@BP=HYN zohChJaaEp>;JHV40p-A&+DIn#l*Kaqua+(J0XOgaHQ^ZDtD8$=N$2Xo1fuEEKruy% zGQ4wu)R$Cg4IEH;D;y1jh*g)?7=v7-T6X75h&f_%qLmqdrr27hbxD9qH$U}c<1|FW zf|^!Ys4O;ptNN*`j0(_^DR7jd@M=MVh6(_HW1B zZvLq%vUI>uM=`N%1^$&PU2`12og_iG7ab_bFBZe@~o zkiUIb#U0BdV{uGqeWx6U4$V&)`FomdiE2{9Dw?*&m1eXti1G&j>>(q`l2b4<9U}~|4Vr4 z7=lcYeCMHN=9}Uvk?-xBI9kZ?wYW7mga*bAr6vl*WQ4INRDNGeyU2Q2QVl&H=wKIP zYKno=T(`xC5~F^}`Uee_ zo0OjR8G+5H?r-y6z^*KJI8-x%6>2EP4Y}>uMjI@8>JS6 zOuMtzN;vpa?^lQ2#+A`k{FnvpbFTF|pYdICJ?UiH5A@7cAx>w7Dr!)4*FW+tI^Qe=m6GqqB@=gGbmmd1tyQ#^asdPm`< za-&`Z1{ntN3DDpQnvC?$iRV@lX@C` zl`Zl=X)s1ELLb~&FqzcsU9nxYvQi%BLl_xz=k|kFJN%f$XVR-Hp>cEESb+ohG)m3} zSV(#?dK}?Q^)pS_GeBWfoktsxP)H3Cc&%78t{>GB`Uz0r#^Ndrlk7p*!?3l6W zPnE8IRBt)55Z-Fwp;0cY-R+8Kbx>up(0+7Gn1NVfa-*rSO6AJGqZmMd%t5q2>{5DJ zQ$ckQhv<+^9$qrBYW65n(l(P6UXM+tvj5>kzn-cI8AQdCyo^Z}FQ(L871Yt4$-eIp|Rd+w6{Ze<>I=Ax}^d zNFCI{-?2iR>p$u@EL3HlLKS#)^(|c|1{{QDT^;Zyh*4Ro_mKA3>F?!$gNU`lgpNCY z@K&~{QH}(LL*%vObv}EzMWQpC8=XWac*+p&PD2umTV1_=8Eob<{7tICTPzgy*6G@> zu$36=DZ7DIjOZ2^TEkBJ{6Y{cg&IK5l(D`OB5X`? zaC6?HpfiZWw-`EayuZU67=Rlj}~^tf!GrXCD_z70rqPEVpXPr9ayi zQ+z#}w?(}y8#QBexcK6{{nvLOz`_oIJa!sddz6hm^8&0`$3lKKwGf`o>yz>R zre7-gl+T$&q#`!#Pv8#&Oyz0Pgl5*=;c$<Dv^F(~U%D~(qA?SWwK z<1|gtwV{K`PBm~8afq;~x4-^XJGAuz8b7LlTH5OJeAD{u%(BX?VcqI(IT2B#i`o&1 z>PuQm@TURCT&&E{lmGSe-c{bq#SWq`U*=$G{ns2AcYKazZ-+dkti6V2iZOw8?}b(g z5AdI(6fZo3N-(S2`W$?s)*mk*yEfmp8^<7u21rwd1-#d`qNhR6zR(rbcA-8I`Mp5}OtbWEJ zCE9oQ_?vr8)|_^nTJouNo@@GFAzTF%QqTLYFa9Ws&l389r^t&!|>mH*~UpW ztO-7TwUdcq4%v=NeJp>37`m_2`a+Z{8SqN9%b=0%eZ1389<9!?wUl_tU9JD%GfRG( z9X51%yzpy)Ii;f6^o6{#U@3e1-~~+C3d@LNrwMN7{2Oiyd7B>sP?O!y7P(#g%Mfiq zHgl0bS1~vDk$(dO3;S`lz42>({8BRC_e5FNLtB&1H|r|qXqIi7_0B8m(7+xQEpxuF zqw&-JCj|aP%AKX0|7Ax4*{@I`cpZ~d%jzDV@o86h4ejs*KXM8MZNxk;)|nz?-FV%A zW|An=@W1bV!=_#LDnJmpkXv*~elb#~#!z6!ev+=;^8ONc?!TvhErH|sMqv}sZ;;_Q$#iV>UURf&YPqU5)nWcy-}{zT)=pw( z!Xf(|w3wO)q5}?j&DqqFZ|12C(_X?}7J9$gZ>1D?zfu;kHc7Z=q0YwFssJJ*?&Yob zl*%Mz|Ffcp2{IZK)4Jx$T%+4RE+m*RhN@J)p_bd+@YUm+aE^Sds7HCD)QQC`0{+~k zp-joq^MXRwtZG|l$##*wijVW)l2g1{$g%`VE`3?KPNtAdBYVoIW|m1;o!k7O`|QTJ zHu%if_^Wok<@e#pFyftu!uD z0IQ1m>`wQ_Z!gd9g+d--fKI4$_+7xE38%*2vL4)6jqXCjW-Ts8p@=4pCJ?LJz@(Io8(HIXw?f`moLJ!*q;t>Ixx2~7 z3ZuEjsv+z7y}}O~Aiw>vii!0rsM#4?YF=ehiOoRY?7tf#;UHGE&SXBN_FJFs$-phP zA999D)|SRTWBtUG(#n(As%mt(apKK%xTJhXm74xX_K!`yPPTs3WIclQC<*2EdiqW? z!`7Z1?V*(2)fb~Q^YPxf?ACn4N2#t#^c5U>Uzw@Fs-Jb3?p-M>kf^lnNe@~NX~-Zh zxbh4$cGUw8^k@WPTgl@j+K_ui@FhTIn;L&eGdaB0p4Nu56>`%0GeDfuiC`(@5w>fv zdC&LjYp6Qc2I()Q3FOCO{qq8kLUnb7_Bl$Pv&4WjL_e!FK5ZocJ3Zj(CW~zR_;8UB zrRAQwINE)E-5S65_x7y#U|7#5MpseJDb*hL$W{bd`km*X#+tWuP6^>B5sEEv2%%yp zFyswaN%q>(mMW;eg!%@V8;jfv6s4Sos*v5n_v~e)cG>K1<3UCBks{dqVCo_a|Q&!)j`tJsw$8>UL)w~`oyUReY4vI`%mGYxS`E(v+Cm?d$~-|ACW?ulT` zhpjr@HEYIfHLjLc%{dLMbMUsW`s&p)KZSv5e!9}YJJ$QSe+D;)=X&!SKyqzz3M=Q- zSBNi{Knj&qxsuJ^hspuz0#h%YR)`;99cGtct|&_owV0>7lu+f-@XNgbDVx5M;jah< zWQFraY#-~RwG>7J$bz)3$51&(j1E1geHWN(?;TEcIuB9p>vJf*mP(OTbVq1b>p~X0 ziP#yBwRh&}5GuqC_I3TRu;h11RmvXCK5t%pKEdttp^q{diMLmYtsG(dTj^}1baTNk zymV`K7UhjMlsCs|jlW(tQMn8eX zx;l|V^9tP!yW-);aAw*a@3;>rVLFu%M}IbDq!-g4{CD#hCp7YT{saGwPH_RnOB4UH zTWhF|%cgqwzx>-jsI3HaQ|2anTM#7}G8Kj5pfPg1T6+nvJLR5w)fs{t>D(C~+<@oR z9^8TarZ5Yu;%a$UyG5H!a=~l?U7!YH^T2FSrk<#7r>&pk3P6-S%e*(wXFW@8zNN3*^sY{gkx$+ zEF_-e*1e!adisXQXM)>pV@`H9ZU+~Mcnk7%`@#3F#H|_k$Fw1ZsI3QIy^|=%cE2hQ z?%uNCECYnU*ZrE^!!-x9%ee5m#E32V&ioIbxT)mwhu$81zp3(#X(uZ>dw_O?(WCjJ zfpo+-u0l>x=Bd75{>tby%g4qi**Or8m7sYkUX2@Pr1KMh{w-lFwbU8acV5}M?}5z; z*3OrlGgJxxZo}M}J#wQvN2lhML|vUCqMxNoKrqEX4Z1z*w!rk)*VKh5GlK*ev|$K~ zBU{4v?nP+1MZ9Nuh&5H(j^&h2ZORYErSIr(OSHW7TA-%8R3m%fPWh`FR$t$mQD>pp zN!`@X})wqZ=SC~3_wws(bahG>a3U<($%P!m=lwD;(*rjMr z(SATEiSGe#=9U5dL@C4i&k}?Pf|-D)3JmsH^=qRe$l|}JvN$x_N?x${ z*WK+9Gp99Gi`?KzZ!PdeuowA`rsK~ftGme=3^*uzK%$K_pITQM&64=yHGi_dLXOEY z^EnzYIJcN%zf&Fp`ixUNsDfz^e68hX_B&FoxO=ufv$k(do2`W<%U7k!g!Cm)t1mr} zvi7?EeHTF;?E5gp8YnQ+e4z#XJXrC|hBsECs9MY}_BK1JcL>0%~^_yq4zVAumP_S#QEi%^Hczz(gKe3ldTpxQkBYVW^_^8|tF@wci=RhPr(*HzGy1JG-7u3WxSSGg}!o1X|JK zqL&>TU=I13-IEA7OyHuNU^2Q4tSeL3=lD|WJ@kR3%$P@grR{53b;t2? z%)UlXGIH@bIsk6oQxb7i2u4x6aYZ!5`liGqoZ0-%ol;p%yGnuT!w7IdY_9V9G~E&T z?u^8yDE_nyP5Fhj`DgD|=VfZY1Ya|1Sr&OZ3rb*~9xT96&Ad849=UjrJ!yB3QN0wm zf&H|2ex5bkYAq`HR=%zONt~yzoV_7>tn zo@x^Z#}U{JIU9c1<$TwAl2jU?WG7ltnvGH~QIqXwp7n9Zie|X1<4+Eseh!aQ@Rz9t z`v=rtQd4?fYCD_!-ooe;O3yO@7k|^YJbWbWw>YBsFMZ?$6ywDUb;`w5EP=R1@qNEV zw#6$-{q|gtzoH6tZS=?jiP}mqc4c2+O+z=aFEuIK6tZ;#w_i)L+3hR?|hY% zZk?)eJHYDshzMZh^Y+`G6a=b})5@_SzGEe_R;Vyt`u%o|X2*_cP%{;@Kg8hH?+W@E z+J55Y=-X&G={>M=m@3$s!;wGJ8EME@r$y_%z3)PcZm} zzrsFdiK~0}M0T-wTEm(bMl|d)g(0m6G|;efwOAzD; zLoK#{b=9;QumBZ;`8GKWa zw$}5>32&4W1eU$^1ajEx%a@R9dy@iXpzEfo9q;K(V7@YvQ0I`z$w109=f*wsWL0+^ zYCGU^@%apJb&nfGtlsr8w!Vgr*Q1#O8&*;K|`GKCn6JXRV{ z6#RXzCA&u!V<2&6LK*@`*#bFpp=wQ;HQ5+sl6-v_SK~%7SA*e7kLFCY?x{tMI=|r8 zk8_CYZUR!+8P*)h)}7Tgio%R*9D9rsZm9HuDoYIC z+p5&LGN0b!S$eL+J6!#c_Tv&?<%2b?SIFLr-wA#ED`WxQb_by$)ydIQl_1j^{qEn{ zCV9(*m0;D52`r$_eO~`c1h`W_RGV6yu{lSGn)GAqw2c`>(?#VU?oZdO$93=R#)C%O zl_jitbc5t|n0DEC{S7U!bvN3DBP6jJucS*Ga&EoHW~9hXnTZkLE0UBJZd=?V=X!;=L^nH{CDKfNuB zN@&t=$Wexc2$Lx{J7d-lI^Y%!xj&tfqwmBF7HK`!J9W}cU;Ex&xTbC~5Y2WSCJ;&DJ$PEsdx-pBThBl3>eFQ&^NXaY zeOd74k>V%oU%NVKPbAMQUVL{+PpD346p5#V{v+R&;_ynVx+lL*Qmo1f!ch4!ww5Di zC)OTW2>{dIesL23vlFqxAB9CVeCz?cG#jLcBK{0_;!SzPahDN2WgU=_XJP4wk&Du{k==>B@oBe>=quES{0qgi5Xd+%TAX=%{M{z1SdgzJAQ2k#8Rr9%E+ya%xcNY<9|?U)7;Oqq3~zr zz&v2U1F%kxj(YjU=z1&kcIstSP6~;u(|?3Y?W}7jP2a2Q(q^n{@QaF{$&v2-B(!pG zIY5hDm%cz^S{ZxLzi}n%om7^kKVQ*7gMwGZbMJ9XB(bF(VL z%GEEOV3tNEN{Zd%Y~(EV))rNYI{xT>V*W}mK0ms))UErXj{LY!xBjXT49VHUGghF~ z4G$AThAZ5fA?M z#CYu(t<;nZ3QF54o|8&4X?owTjs5{TXdTR<)}{YhLO-QTU&n@-Xu27Kbhn!B4dYKx zroxb_z?Gc?q(5m&Z@s_tC`fkIS|2@tq_pc32hN^?Bxb>0eyh*g3<3fzPHy56*sC2@ z=_aIN)l0Oc^JP>XisHDw{@XU9R=zHZjoJJ(I_Ib&9@=l4_O_`&>&&um?n4N~;zzv# zjf(pNidAh?SdEaR_+>wzTD?YwH87;VIEh;{ zfthGgxkCQ!A~UE4t)V!+Ki38$DJO2L2Nx-X|1iJIh0L-vUrN!fobp%a+9qy0+SAH zdbtJtEI9 z6tG~(es8-ITIEfSsVdQ0)NH z`;VN+;|TvfXgiWW*9L4cjc=V+V(@0&{`AgY;#bOe<=2M?Q78mo*wloXBz(_q>;oBT zdVOsrGv6DH3ROy8{Sj-gRc)E)2Uw47n;T6F*kU?Wq!PF@bg!i&RbDAbkMlGCW6f71 zx(}o+dsYd)m~dKdhMa4F;Hg(TjWix$+8w&X$CB(QztUXJ*5NfN*T$GG6!tY$M{PcT zp0S*Ko)CX=D`Sa`U>VMl-gG2c*XywDRkf9EB=auY>y!jTALBoM+BXEs6zKgc$uHD; z$puRPp82;QTt7Was=}It=@QX5n4g0$TSfHdGa?V`H^|r2nF#&U4xkC%PPTkqDPPo^Bp+iPjmhTSB^pWfv0>Kr9BxNe8xqOXCXA>!YFy zHzfIWR{~3<0TRLD8ZT*&EFU%nw*XeQ_0K%|ltBqkk*xkI;bca=kRSj)b9oNm_+xP^ zenwbT?BX2UgODLOI=o*J?nijB{fe>e$Eb7}6FAL$l(xd)_Vj;q?sRP~Xw z;DBqgFqLxLEzWJdC+_`}szQoA*%|vU6N6O$C+{DiMyK@f+j+QBf_3wgGY?DJBem|i z-i#<00jOWteFV5{<=*t4Ielrdlj1eNeaYZz85ashWQifXfxN|XoLcD3=hqhn znyfW08!F(sDIn$;MOrKN*NlR~zc)Hgic!E^3cxZxFGFb8N1l~9bZP`{?EXS6HJM9& z(@Bu?J+zRC4H@FLluFTFGD`qWy3gkW9z2WQr`kUkj7m6yTeL+b!7ih+6?@jLuDKV( zZ@Ab8efSuR>|Ete4j=s5p+gUMOKOsRIKg>XvJJUkG+mYl{=Q(lz*Z!9BeMcXm^cut zgUv~YiL*hiz5rImgeLQv^P(M5zg0B9fSFet6n0tAAa0fkjRO%Xlf{&)%k?gmf0yeA zukX~>L8($ajYA;1+sHa2OMlk=D?9SNS3cj{M=GJDJGRi?@=hrI0or@)w zvI!}Ytx?j}CbGLF*Qmtu7gjQ({`hovgQn4LHh$gNYcDRLHGH3V)(vdMx55<#u?NyZ zN=93R46!Q<4_3=`0qN|-(4qHze?!TJdMGgK>$&w>GK#wC{{(fJKG_cAu5;ULeNL=_ z8$>eHo6%%CSZ+!>!vmnkDu!rU)%eAtVl}&ggE!1k1&RKnvf6o#|C3`F%Tvr--7lhE znzbb5Z=7|pbCqbVi#~t(DQXd6Guz((SF~t`Jkif(xLf>u>BJhux3um0orzaGy=cg` z%c)ow3Ka#!yF(>AA->ht+#%#)b{&9Nw^L7(SB>_)XCLv|SD31p+n6w7jG@#y$RMcL z=*G5Q+;kkoQ3T>uxccm}DngG_?kgpfw|*09p9?&5w{X6!#K)fc;u2GdMZ`70j$qs+ zHOXw`#wubLE~>l5fNowz=sX$!3P~E@svhG!iY(Ltz(RXO9S7tEp_F5}l#T6ZrhGc!@`f*eU(?(7x!Mv7C>6_2MJVva+(jcDx0~ zCxulmFH{6yY-FHsEATUtd8mu9FD8Ap*~N>K$Ieb3DlJmxG`V!frq%kkj@n;#AfK!% zq}4R{@b}o-AC_TYD|I)z6UjeYK(?bfQO49e@#{B$cOx2s=h7P-u8i31_xO9sXUqiV9&i1wA6JJ19M&^0~O zd|eq%w%{~ofP1lA*!giMZWJ64)? z!cEl|+|@=YiLdIecUleBJTS6*C?JIXlwB$49S*gUv9}bxHAm!NU5S8-yS zDAaKk7~&=$6uUtbzc^nZ_)oI;jsVa?kV2g|Eq=F||MJ^^{O0;?TSpWan+7+S4eKR& z_Z#2~ea2Fy#rRl9^;f3r(9&YNA~dO#eRQJh-fIAIS>(DTohfFPe`Y}g-PR(*B&lpF z1c6K9pqFJdTCAVso;lq~zEin^;sHl^7Lp~+_0-3H)~J9}N1|aCZ6{&XMmX-Lzt!6n zJ~LY-RJUC8QLS6^?QK>Cs&rGdDawr3A%OKAON<8TTKNN;2;-nKjlLi~R-T>K({UONd>!3}qyQ{?*@6d6 zE3jtD91b_w%h}%zc5?Z9XaU#0=R^~ayRqCWD~wCUuXlAOocfCos6i=~5bRE}r%9^` z!q06ai@Zw%5sxe@M*3bVbvpir0Y*=boFf&5mM=inwX1^yW+I=o%rP)|mleE!$}wdX zMpk@sxs%8@&qJduAZ);5bS?6ss!A5YCGD*q=dWltST?#WaFWi6ICln^G8yFj>Rp9o z>i6G;fB#Xo?c@KazCDs&k-95l@*zhFi$F$ctS*(@P5py{t?-W0?~YBl00 zk{Q<(q)!bVNAQiOz7xuz^izD^4;3^xv0}xjS8NB9{a<4k88~1-LSHFrzyhAX9KE88w;3b8Uj4zqAq6cfC6r7V>m8$ z>R_H?$m$|_i2%iU&pOgU1jLN4(9U}8(mbqdw$r-VUv?SLk+mJU@smi(XwduSB{(Y*;%`n|xpIuN|7Ey?Gk1UF&M#_{Q z1eXPinUB`(e{Zd2sN%@8_j)n?r~SD?^DwnkDrh}C*`!GdWLZ=}F!e3c9yaisSH3mv zOQlVlxIg|Q`>`L@FHvV6z@fnv?ufM=Yjje)n^qo0lKnKU*QFl4_oTSn++n%SrImT#EKQ3-$ka5P^lXJFS4w z^PIN?d*|Z|_WLNeG%H?b^BwaN?fZ)jx5S~`1#U4|l{2f4d1|&0gpTS@M=$O17V)9$ zc&hY3nM3&SlS&lprEieZ+V(P%KBhR$(ZX3 z8(F{VIgY8W{aV<{*38l69OWQTH+8Of)znM`nBjt{#SWR>EAh@AWf$|QCbJa1L_Bpx zqP00bxn?b>oEL`^K9ucSbL=PwzazFko(PiUs9BW}+~HesBR|C#QNlr)g2gm$PY+>S zm7y(|muEdlYgJ3Ay$Yk)d~s4A?d7|!H<{UD4QBOE}yfetMFN>Zv}6r@3`8 z!0PCxNc;gVqDuA+^<{rFWw8}6Y8}$6YQ0{#&3jg!R6S}SCp);L+?q6galp&0V%SY~ z%^yv!!U`kT(bMMwG1V6d6#NROvxQJpy|I`D}+tr053YD1CA>lK-emY57)a;5a+VWr7Ib|{ppESsU$#qhny z-l}gO^4a5C{8JBx?e64!zniDpQQTe91EB9>n{Te%er5Mkz>s=c#MXndp19PjN}n_2 z7%hiBjD7?zYO!#NcgitQ+4JAxSV0`T8?$r#r3FBEPrvKKq`dQmtIIv(CLltG?vK9o zZRxdk#968LxbuxPOJlCDz0cvkr&t(|)qYZQ;s( zZ?IR?lk8kReW1!vH!K6@u0$o5u}qkA=VB{=cbC~IqW;CS5@F#`Tu#_I{rNp~>h&}& z;l#5>*Py14pWe7d?4X*KDz@!4{g_Ui69US+TGNAdm-Ii3O6PtbYg+$l!l;fy-z zJA#4%Grr*IR7!_6JN zCWVv0O4)UOXj4T3L8ryxPIkKIHxAN5zI~%SM}|WV2v^W$_F;=_82lDSl-s-roHX@V zb7KO<-f!0ta%D&E5cF&3R`sJCV_4QMuKK@XqAo2}T3f9ZJ2$`&m9Og~{YYl6IdCR7 z`t}I*QTmDHi#ocnbTnj|&7(r64x;{<*D|T=MEtZaB6Gpqt%AxZ7M5+FxgGf6tqdj^ zX~>cCE_5?1^9<8^Yf_M1GF*scc3&*-`zCbIL@{0(2PJwKPy2E3yD945OR-8P-)7|# zUZiw=B0@@9HP?o7`KPHQ6RsU?rLj=7|n5TU|XyPWpb`C*tP&|ufQOA1qhmyx-s z(bD6Qa*A4IwBj{^geRg-cW?%{<}Z7D6w8i$$LF;7!a^m@LQ&Wvg^_?eMF&fwr#6IH z{J)y#V7HI^IUgG`X!viYt6w>(q(iG;mpbc|O)9m3CCZs4n9Ycn{czDe({aK=_zO%# z_xKx>;ijrN0@18xS%&hik&i*S{K3jMhW0bq4)9~Nn~$$Tn*`HD^!kH;FpD0242Mrj zbA~cjwc1p~dT+E&3P0;R%Z`;e1G~M6wv-EX&G?FVuO2;qR%ggPftD?i&hS?Ajba}! zXQD4q+ep&sE8fwia-uV6%MTLSw+hkLd)*10?^}#gopA~WFHUaUs#goSyKf4m>82Pw zOfV$fHzSsL&jm-e_B*eQlPV*?{^W$$-Q#io%qxl&^&L5mElfDjgLmNrqDlCoLCR>U4@wC?+ir4CrQT-@Xyy&u@(>d4*IQV8J&bVJIlg* zQ{GIfr;SdVR_$=`?C#NZym|LL;3RE^H4|1Kh> z-FM%AV83kpeBSTZ>+yU(^bpA3*B8Iyc=I+5wT3VpGqyhan)NEl%b}Q{_wz5GsD83; za(<`!?~ke6^ZoMwY$;L^2dgnA&&iF162s?lC#2rV>(S2B+5l|NZg^Va^eM~PTUUnD zFDM?r$){J;>wSM}u|-pU$@+a%=#pa^^svwWdCIfQ8xeVUZ_~7e`7v*SMfV1as_b>7 z2!D$YJ+DO6$$#p%VH%W`HSMAT#k5oSWEi{B2Q;bAQ!VGlYRXcD)k9#K3;Wv9bv4SL zWCRw3;7g0=Ytim!7b;G3D{}biyw#0l!Y5C!MkI068OS91`bT3J0d?6gY6W%uKf!!) zvo082k0zmxUNJoLu9(!NV1N$kDT(hlZN1#9aIC$Rf%xxp1eeB!lX^z%X)9yY$$0X= zyWHz1t&MKb8t2TeA#T+51KyeKZ?UKLQ`Aco&8#C&rRP1Rm$O8U=e(pfC1+_;TzUKD zvG!t(YLom9K{|FnzManyxg>ui>7LD8I-NjB`&AEc6Ry*akMqLmU&-Tomg1#S5_5H| zgDwZX`U#hw>9$2wlywH z9<^kd6a{ibXI4M?{KWGGsqG)C(97B8#-Y&j|M$=b z2H0vwQW<~!kDhZK)P`KSpmS`d5Upvl5bCAC(tVv+&u@0sCPipiA#bvq{xZN4v@fc% zBUw?FVz=fX_)ua7`a2&a2Nr zTi8aJk4A`$PV*2PfiJw84zD*9$(KJQ^pDtX_|;ejm>9cesPT6Imwj}{L$zDvSA{n9ihD8AE-W66{-@6PSYkp|x9) zM3nV*h-CbGyV|7v7J@MPmiEVl1e1ZYA3LndVzNHRgUF;3=cGtldTGYuL+QKtU0>y} z^{!q^m4cQB>o-is#oPRrVPL+A)v3#U1>PNwhR(MHDgo)!0kV^~SyuF?syH33P0ROa z)Ex~a^607^i6d`@mkI4iysmeUrlpbh`*th?mRi86&bDw~SQg7WPwYYa&mD^nBOXZ$ zZe+EI&9yrO$z3Q3Hac`#3wGr?NuqB3HPzRtRW;rzspUC;WXwOHJ~v_L8PAf{g*1Dg zZD4;aci0g(eXH$C_=&hdmtZS0JR@4{tb%`J@eZ#;N@?hy*RhC{-Sjf^BrYp0>~LoH z(N%3?Ni+A^$m&Q9D2E7(!9K1OvGIONI0o^iaD9j_CV!XjD9H=X*RnN*0|z5j5Ba{*Q1=xF z)uFr2>7)?<MXzFv+AJ2IM4epL=uOyQ~uKBKbNsLxroTih(p?P6EiSIsvbtySriE>jmCv@#XD zF>+J%C8F>SA>TR`c%oRc%vB3w+XR0!;9lT!#@906` zS_*C@wf%Un3cK(no3)Bn_<*0|c#L0=OP6JzU{5~7Wf)05S;&NrURCn-qo7x6ebxl{ zW!l5sdXB1Iy>pd!pqG+I{{*C;?%V{)nJ;_srM9%BVR;JO&yLx}4xI{B$eqdHqdeD` zK-NRYki)J&?XPG~#=uT8w|V5K-F#v@nRw0)vgYt~I%XxAug z>Fc=7={yx-&y1!jcGOUf!)t+l03eaVuDdU^C+zVHPwWi#G2a%mgJ1f^3e8#~unfdG zr_{x*0*>%Seeht*f~*hx#(-CjHJmy~F9;a=jX<8eeT0Z|UJkwS>!P^pEv?x(DA!?9 zYx&;wSNiN^iJNc4=F#@yn9JLm23H2WHW-!PsBr z+~Fl|Z%!+)*|+}786^IH_U`^JySCBc2Y*40jeqyjM&v!=gOW*}sWoh}OIxD+ac&N0 zEfMF#lcXSLwt8L#9$74(uB2qGGqv<{p^TIJcUBGEl6|||hHH0-FXN1F2w|7a)wTK7EB>WXdLQMZB_D2fEiOESt!`R2e-4)G+sfZ>;R3 zEGM#B0dKE|0=9_v&hDiQEbp3ES&Yn`R|~l6TfD!z2s*H;AU;v(TMBwiAO$hg{i80i zJLZYIu;z<8mRrOovDh>54-UxZe3#+{-L$$1U_@$D%k9*yL z%=kPYK?NbOkq|;rW~Lc(Gdqp}r!?X$0xc_wI0`xVs8@FI1}*ppJkT?n|6eMRBdGls z8tuRT+TXJIN=+45&$8!HNfNyp3@$WtijauTYtI(FC4dv0MGvjW9y5J?6~E&u&Lk>c zHGO(JNkr@M-9n%B@b$aBJ$;TDAc84zU$J1`rE0TZbb0YdEPJ(abjrdjbApDfMX=;| znggT`u7sL;r;=_dL#cKtct3gVPs$pqJ|Hb@1Po2u?lIuUM8qz#4hm%7_9eZx`_fn5 zS?y?x6pN;7J>8mYN#(gUGjZVcHU|3&?wFPc95@;P1o>YQKNErX^dRa9m#8 z9$<|r$uYFR#1BLvmS^q_-Aa0y^2xff*04pktV$}^P+ZZXYI0(B`M(hRm`Cp#Q`qvc z5+h1P-Bk{hY{MD<=VU(vp3PUw#AC$Im0c<7+H`lE<fFrua&c2>*Q$Ckdy$H;=AGdhGcq==5dw**48xo;wUQ)RU!x zQIa*9I~cbH+<=`k!&D$&3~N`U?urZ*aOjCh<~H5|s^Zke(vGZl>qwo^tO2_xuLR+& z1W#$_LIU&mbzpv9Z^n~Bi(+<+WHx#AnQZ?iqHj7Z^HK}2`O{;P8qSREM*AHsTYaae zd>+{Ok;4iz1h1HhtfygE&}1@3U^ z{TynT#|itjgod(-U07WVl2!H(7oX5#6S|9k^?EJfawuyc>nImuXZGOS^f3gfPQ5B0 zDAX`@oZF5!l7if>3AD2=0_VqHvZfiF1FkI((;+YaC|>X16wid$bcE-zW&4{Q8J~|o zT-|C8>kRgFdh#eCY+&RSAAi7O6Ft??1hz6&Ext+GdUinu`UMp$6Y*YNBPgAo!T6!Q zK0j^dXjeeWo5K1bZ)ex?|Q(gvK$LzZbS zYT6e+UEH;~n@K+&J~h~IWlC-zbj~}}b`7a)$e2hbN!lQ=Ib~PAvC7PzW$E$$JT{cLg$t&J#Mt_&iKCnTGMoz=Wi=xZ}diP+9hwn?)4U{(xqP{vgwl> zV|u?n9MKsMNNk-16_R zc7$}PnE2(-S_gs08rGkE2iqgH2z9^s9RO*g^IrDRMu!)vv zO!?xPZV-0}?7l1(kCqe0I*%8e2!D66l#{-1=aLgcm5Oo8`;U%y-8T#_Il#NTHwQY1E@8TFpX?3QTOX7Monmmc-fd4YM_&~6@T1zGnB0!eExIpK-n|q_= zq`)gbqJ2tKl|*0z2;r2Ei;k0RMflhKIef-(2fkCO9ZfrjwnMI2^j%9aseU?t{MZU4 zD~ozk!!orZ=%op*>ybmU29DGkPEm$0e8;}mvL5dVdV~`DPD=6vvc!I0!fPeXu4*@g ztWzM=;4*~wcbw_R>gTLKqzC9lkdesi>+lJKwVp|hVI?51lO=*9tAw^*|1x#3x;Zy( zDTCT}9bt^TETi>U8f#;{$NXBgZf4N=VUokU(T2N65t(}G(KkBsa9JhiG-1)-W2J&R z6~j5vTfhMn%<^gI5JQYXoo$L|aBsN z|GD`m2MTXJNDbh&$ic7X?A2uxL@#(OcFwor;bOmp`Np`XEqycKg}2oqNq?WLjhTlP4d>wv&V+_e~EQwEjgYS|M2(uVO(T~ zXSKvH=Ntx6LKAZYlk8=Gd-2MwW%mv^&f!y6HVEhigifKHHi3dTMnN6%7?6w&KXu?- zW5w#(flTCwwH%@+i0a_noJ%5_yx z{kPMnt_J8@6B_;|JN_SQ{q=X!u9@st5z6(^UdC^U4KJynFstyW091o&x6+`O^8gWN zlv^?(C7EBB{Kkk+{^D?MS-`K9{&2oi?rt`f7elu%1z^fhlg4=)F`zYMD(GK_Ks(S0 z%2#HG81}7+KsMH`He8blGIh=Fo)Vq*d+loM+;oY4&Ysj3<#BIXxxYAwxIZz7by7K2 zbW9x`5IzH2D0v%zKEfXROex$6cjhL;QM%>lxk5Kf`JUNP*J&#GnUA@x5+ehw??;?W zWXd-@`3;aDmI#5rleluTi(Tm7$WI?_=NoWOfj*cAK~fPf*Zp)$HeT|r4OVyjlhR{= z(yiv(K>PH1<x;{Or~TMw0j(2&y$L=HV+`U#o!?40?1iaANfWKe4Wf z3vgqNqS5meG>-B5*SGz__V0aR(jZtwa-EXJTtsOLNocx{p4*&sd@-o+B(b82YZZWQ ztTF-^Vr=^E7puhcH6Vl;#Ir}Ie`G!ezvlBdOh@)#cgA-mEG9V32JVi5eUqLp3+K>j zVBz{`iKL?c&f-TPX0sU0-vQm&G&y3qeE|^9LEEC`Z+3aUilVVoxM;mm z+HJhHrn9V}>11pPS!`>N?os^bsTNU6am>2vAR_SaX2Z+jmKHqdM@>V~s7adpP0qPp zv_VYPoOqloXkRGKTSwc(2lOfWBb{XUaKMX7Pt1!RiVnT0_9J`7np;3T`#FDZu@_{t z>r8ATEE0cQD)9HUAHE9^Y@HY^KV~uYIv8$%gVc}ZZXh)yHI>84H3_jQz#)tU&DFB^ z&at@F@8aXM>%Q9nQ=2>u?bbx1cq%GgN{MCmQxTfMp?Q8 z$b&tt&7#tBbqv5;|5Vi8gBWVn;l8HCM(fK;i17TIYfV76^)3XQZ>b5RFCr~^@z2G9 znerDACkbdgU0tIAy{K6+QJO=G6!yQG3O_7V-S3>u2k|A2tAn&NxI%BW0fw_Cvo+l^ zIh~S-S051#MeK&3fiBBK+?pQ)fPQtGCk6`nOJPjocy4&EDAr|EE*P9{XE#lug;amv zu$^P}@5;~l7N1vVq+Lwl6Q#;THEcrT_*�%%k`_Z3iufWLV-}x&tH+cRv4)6 z2$KzbBqIq<{lxJMpo3^gi8z;m-A>5>?d5ZnF^xvtc;DcU2mIx16Ia)UP2!CzfZ~7o zuE+6;93q;2)I!D<VfKjp1$To~i5%BdskqOgX&jD8|A2WH;)RDQ50)2e zbjiEuEt_AiZ04Hs$inzSxmZuTQh^SO_RLYZPtCntpFRVNY#q1rTWS11ya0v5Y$${^ zq&klG7bmCbEpor)V#&Xursvb5Lw;FrRP~M2^1JfBOf3dW~RtPaFzTNadERpQ*I=#p0CzU#ftqj^Gz%~l=gSPKQoB9YIHwz z$yD*2`mv3GY|nt8jX;vi)uYv!S=i;*oOtb`m|4g3HnNSLGyS%_+K-C&eUH}mK)FD@ zdMNF5$aFw%(bDjGL5Y4lZ5O`qcg4$?E!iJ13rF$5xjCNJW*6~$i44bm zAwb?1K^xVI>XfJY-%wFu6pVmLU7NxCnaOX?Knw)yG}A*y^kH|+7JLo+*@IbS6uG7e zu^qf6Gyxe%HE?zK*`3H@X9Cik>UfHG6!cMD0j)Rvb95{9OEV<_#m_p~w7@GO7%M-O zReF_4WaU$oo@-Wy#V+Y?1IDDRThmd+ewh;hk$Jna&uPnMhZ$R|E*r{^?JqF~za17W z-3yLR@-u5!tFBWE_$~n%o<>(6v(yelqQdQY0chm(C=e(4VO9I-vg^ZFL8wb$I_K<5aWlhA0-<^22Y zv+5yI!(n{Ey=JC=A!p;!pZ;Eq-Z$itRkC^`&>Hq%{_Gzu_+$rfqC5UOsvg0be`{fP z^?#Qx517J_3XZC6JdkQy&Xy;Hsl!Ly`hYKBOCsC8gVu}WBk&b$*cwOU>mZgu;rznY z6hx@M9eZwe>rpkET}-1?IFp0XVkJ(fT}i9(Njoh{Y%%UNr?k%?J{JA1X_fHhJaxup6ti0{PyJ9Al{LcvS3ydHH9QYLT%exHc=QsD zjppB}`ruKjvv{ZnOy2YP3c3t?gx`jTO{;VhC!kGjq5kPHG_lVd@#N53xqOGimu-09 z1F$;7`_&n=H*ta@X`*J;F*87>iO0tSEQK+%1S%$2R;@1htFD>&w^nEz$<-_#Rzilj z-x^YZ6{t|eMt|>kLc8yNUILeGOk&LA-OMk>N_VdR_<#P0&i-5w;i0sA*Co z$M?SIaODY;;FN3xy_j;J|IZ=fXtOK);xM`_bUSq+pGjQexK+8l2)QiEg)n1%u6zHN z>G8gp(>vDf4ZfJIy5^y;*OG9iw?ZOM3~Nbh3@#DP9;!f#za8RHEv_y$NS)s-7_&iQ z3dkoaQnV1azoee*;k+Zx8hd5&Y897cHr;9E+|1fVQBvL;yj~U3wpe#l`E8q9_s2a4 z5=vf`y9iVSe~R>!VAHolpd}fvrYmejcnQ4xp&G6EjZR^W``Ax~#tWGT_c=B@RWJ~g zZAL=)*({$Yeo*l1newtoSM4swD=54^-g-OKv!Lzga(=-mt8#gI9qudhHBSq7RK?^k zoVe*^571VK<+K9)6!zFs#!GppMk(6n;+#bqdP?1-?_$aEW71E?>zd01=)mj#N8%Yp zTk~zR4vQFr9;s=Pi`)rBphu1}m<*$DofLa(UM?2CQ~DC#k-{c6{G%$xBXGUeiS?E?7ns2ZM6-*G7=h^9_>u6FZ(z0N;EXmO(2yv9WdmTdjs)g zEe!=-nkfra)6=o&~bWWXNb+{I)W=FPcpNwGv1=7d|`s*wo zlk@>R%2@9u@V2X54Lo=wG&g~|JIe4hbEFVW^@e&aG5Jp?T{|I`4a-5%mp_!>7ck4EaOy+C=Z&Pe9ZD%zQGiOMck@yOEl@H>u99 zhQL+Sf!PhWI6?`5atdMGm#cBOwa_EbEkqy7e{M-yv@s0Y(XEy%0caCX1blWGHhpEV} z_k!qtXCdZbPRb$P%<%xKRxwGe+kByk(PEr0GRS<`?Utyx*TPg_3=|K52Q!#P($@hW zeUZy;F3$xQwL8>ZZ)zwfn^@uUWGly;V0<+?g<0}bvCZ+m)JVu}~7Qu*+05qr^&Bz12Pt9En22xzVLWpQ~v zv;WFWCoEU70A1_5Lsfy)!S1N88`N1%=Rj6NB)BqwJMuxv!hbU8?u>&Ga#INQ`0~Z) z@>67QNx0;TFSB>J?KOMqdr&{!V{pJ$iJ>6O#N2~E;Mgft?V^VDy?p>;U8>XKki zENIwdZmf5iOPr#1_~MCk$NHuwXF)lO#ec3zNW)*tpv#Zj_dS{X1HqZTX7>-Ck?6Rt zp{;r2MNIz8lFoJ%U9HsF`4MOyJlV8wnUR0UiGp|E$v*~O@HVfzX`&!om8&FnfZE2c zPgS|zeXg1Eu}nVlAOEvMZ2#6HM)04F*~HbCxl9#k|EB%37Le2mV;g1vb7RW1p-A1w!PvnQg$O}C=Ol7gt8N8n74!u02s4Zhzy`y@bpanU{xav6zHr9!*$jn+ z{wAP#&1!Ut7c*-`O0VmO26ju!6LB>kgN^s;m@Wq7%4b;PLe{?T;pNr44!`Auw0nFO zjQ8Zl)1)B&6ON3P;e4BdpoG}f74|UB9GyzldEE4sFdpYf0R@4X`ZH-kxUr2Ud7c-8 zSm%4RURy1^+1j4mDH2;A&9vjjNCO(g^h3_<-bp8R=~2e2(sG1+&#AJcW74U}i?;U(5xr>_g*{r8LGr#v-O$8&}2!x(PFFUyQn7 zmvSFT%Mbtc?E>b)b<1X4BhCPB|1m`%Q`5P246SV{H(T#EUYdExz}B>*J{sKlwIJwP zE;u>Y6UELdpA`6r4!z z5ck-GGLcmrMXE^Pvg-Brx0Viw)^AZ~Bu!NL(z}zDK78kUZ>K*}mR+z|LRdTL_#+cU zREy;45%A?KREju&YhpF40|6^m72TNmC2YLI#F1#+rD4!j6Ao$4-W#dY7h z=_5BlxhvXl>Y+cy6$I9#lynkD?o*WKN*lCuNu%I&8*M{0~Tr-o>QT(-^{+nl|isT#wFpl37L)$g95HSJ1Y{sx_))Bl2gdjDr$j9>VjakX&1b zB!&FCi2&**P79`;w(OVk9Q)WQoDc+9E+DTu>k_OD?mC~rmqWK38FBS2VeD>dpLsjd zshA9FzlYy4S?XiSl&peQ_ro`f4aRUH@JK3k#53~D;5IOXlC_rEEChF+3B}bDmlN{@ zys+nq1PbvgSe-q2H!Qory&${vC_Dt84`+YQ z8zz2&)1Es;Brj$Y(V#;ji7vKRG+W>TM>5Y3@<|@MeQZu?tZL@ArXtX{&QAE(?2}EM z^AIK2hVJD_3uJU%po^AGYoKK<>N|SRytW!;xo;*4TYwakdfKqbnTry}5^(eF+>jRf zn+?jf?9!fhqXV1E6OY&w@3ypo)_hYEFU1jy_?|ANb!>@WCn!U-hSw=!`R{E3m{)`* z-;_#&rq902w%X@4LJFnYNng|nrF2YGGM(Ub+u0bHA#{61#>y^N?VF;Gb}@WNwtny7 z_J?AS*c>Fy0d4;t>1kI|$JC#O>XGF8QPw`72@GA?z)oYrl)SuK*uMY`B^Eb}F$7~w z3ot_IM4c7Zpyk2wM61GaO`g|pvNeD|2ym`S<63Rod(`a}G(dtAk~STu-k=fI7G;PZ zBrp{eVhwRbo#q>GwL*?`(rSeK5Zd5NLiIjc<;kPNi5S%i%cwpnumMYJJz|CzE!gb9 zu|^tejt^;8GL1PL_I^ctcL?r5mFOED{tp3EXt;0EVKs{EOG)_Qn9660$YSm{qZlz= z5bfc!A>$Iqe~w{ORvyWMdM|tA_lZi^20mTEy5kTkBZLfQ|mx^D5N!a zpW*|JkP_`7p1nU17W$=I#K!{+tE#BYtEB6WjOhiRHF7$!%q*OTTxx>|ObH8uhW%;@ z@ARWZW~=)Udd~3<#ADiYecAfy%NUk<7 z#1$Sq3m*9^$DV(gbE=vzE|F87p~qd;M44?k8_2x~B`YTTgeqn~^)A-hZ8}_E3GD~~ zWn1FXZ;%*IptO!m;=Yg>jIF7RlqEiU?VnT0o>aj~6i)M7wZKoi`rXv!GY1-f5l*>? z7(e*r2z$S`kKgH6An~c>9^k_+K?&8{ytVL901XxTT;fFz^7ujDy~j>0OnZ9!%L!=S ziskl*rP&X?F(n1s${d>Rnz1~&#OR@u*I~n_T(X`DZ1i&rLM469pFnZ$M#*I?>Jm$?=!d|_;z~76R$f_qgejV=%*coWJSz5%v7o4FBv4UJO zu8@>%+a676&>l?kX(2fLs8u;CXv`MC3&`k=BpSUUmcnZN{A@#aZ!O#%M6p~u_s6{k zpIIbIs34rREiV&M%fu#IVa)kBUk+)We2AZ?@-46Ozu z4<^=g4$}|*R0P>Mavp1`HCAgd)?9cG(vIXPg$V_Sf=z6Ibq42(iXA{WcjSRyXK(sX zl-QS<0ubU->*`m*0X$kUsk%UGBk1P$F{hFKKtlQ2FRFhL^MwV*{WG)Yn<;q1zp2Of zP-HrxM8aTzxSf}_nqT1hK z{3rI|Yl`oFS2L?Se{z>mEAN3l*IFgt`dKB+kwS*MJ`!CLs+%9H)GnfnutaZNF&1*4%A3$vQ;ZTV!xYcyQJyk2&Lc#kT5 z^Zbk4(`*`(PfzHYR;FE>19WU1Fc^#r_dwcfrEGYnR}|EHBA@( zj&bi8Jc3gov`O)2*2%tCOTLw}&qcnUhE^Cl12a16Y%rR2WBPw@>O6a&z$3C{^Omnl zP~=Ak(T(r~pH=g|bfz8re9*J$4@+86^Jp1Vf^r|?O9G!7?EKr#h{l4d(%h1 z%AW#R{v^&?Gtv-GF8PH#!74UFe(P)N?6+UPu##QA3IvjN>_jz6Ja66yYEc?ib}hdG z>cC<*42}nf(MB=bHUQXFKUO4M2q3S?w`%6lme`*oM z!8F=lo=2UCtBT3X><6Xuegmvsd}dBG)_nj-`c@cI2R0&&Rxh=)ufY^^8^xaY!RoI+ z%(Y#4B~ZES{`r!`7nGv?coN@ie+6S}MB2W~@`7eD$A2uX63&Z#598Nh&_^~^DjNI| zq?n|rOiYa@8drUS5{##S`DGpzryl4rD9WNXlhax<+gIsqmtEea6T|`B zLpD5Y0b|df#`F1b)%VRI*xh^xR7ff*q<;t=T7SI$nRTAG=lKipa?8vskbwI8_}MKv zYD1!jWJ~kE+%~G{rn@e<=aBMz>-uH;%&DyKFQ2i$*RZK@2~8$ zhEKM)eFcFB*HP+Pv`7fQIO!`3>WU2C7+!!)3iwx% zA;0~e;9tX2q>eLoMi!8=auyIx_h*MSthl0WENerF^QR_DEzoB1sbhhU*Y_C**+7uCkt|oSXLB+9v!HRcIh!;QI4& z8?0ev*6P~B=>q|%YD|;~1y$rd+&<_3Lhj`Nt=xAYmI!@lBl;~Pst=0 z`~C=q9H7?u0<4?lgC@W!avShUvappnCZ%DA?3C=oq zDCWhG#DUoyCjqy@zn{91tcp*d5XE`xH4e-rprrjy$(L^O5a54a{a?O<6j7;9+FQ~S zk_>!$JZRfS*vIV!OC!@7Nr)6h(3v6B@W@DnTcoNpb~s-{ws8|wKI1kIgNK;~uZ-_4 zac!PHC!YdQLXUj+;m4=t2PSVB&teVE|1EOyYvexoRN<_{USz8cr>5HA+|#eg7;zZH-;*91-XVq_nhizhT`k=rg;Yga;qD#uH(scVc)u!BIE6NOU?I zl>}Z<()ngyFRvvQi@!!mwi;Q!EsAzMC1vP>y`|EAxc-Tw&eN>a!55pMTsvX5WRrbP^+&85V>H9kMuMOXPS?=F1iAh<^lFulS6*8~_Oc{` zdVR=AYyFn&58yHqnGhX|OP?`!EKA6Xw_Vq23?q&QbL#jt0YPmrt7yg`tFsrD0r03L z;|5nX87e;%Mg3aEE<<4Y&MLdmX9G7ov&#^S+Q|F41qwK4HJ>Cgq*{z0?8uN@Kua;L zed8;B+o@muvi|1H(s#nLr1I}j3B#c-S^MTq;|%JMwC03zM_z>JkR`oM^J+=aTK=Yn zY(loG$(;e45E#|C|$x~WrZqwKFVw){Ib!Z#%k)QQr)#v(0?LF9Bp8S&5#G4bosp7^4y&6*ve zdSpQZe7#ds{g}9ey9Ip)lEns_kWI)W3LfFNrGY>lL3fD3fH+DYyEIOyvJr!KCuVpm z^n6pjtf8O5R1Zau?L18r zDZ3-6%wXuR#(PY1?IC-*TyJ*4? zACyv8xt4b;G3LZHtwq|!&Y8n(A|Tg1SUoq}o_pQ1F!ot#Zk9P%op%6ds(Gu_An$L- zh3c3Mk-;~v;O`|`kmFLtebP+#NLf>g;=11sad*W%^z5Si@AaS@q_o>>5}@9;7z7L{ z)g)i1%6jd$K+M$OBY3wLSN)MAcE%Qd_-?t!DO6A{M)@9h=!o_T=M;bS`K_wYnw~G~ zNs=v`R^e|sq6HZ?e7^KqE_4=LQIGZ z#D|{Capo6862|?Ikq8Bnyq5PHmB zL(A?qZ&`oVe(IE5tXD;`Kg%d8vxSXr`mT2F?7r@c;zg*E*>>a068y8RqS zD0il<{_Tx+%-}P54c#XkU4}h?bEa(pO@gs^=LQfP?OU>b$+2=L;w84%@e@xn4(rQA zk`7^h!lt+iLmkAEwz;%d=y*GM?TNDy8q35~p-3^$@m$>jZeeM3Ql|A#qI-~lvVrug zl@CfA8olV7WGW9y`3I4yp~cuuZ|jxzUnsF(-#;9#>0#Ha1;Arg7o zZLfS^{jDRnArQ$`UkmPe^n8KJ_T=+gYzd0@R-)!am2wDd zxU+p#Z*H=YQx6-NaoUTjYetY6qQwG&=Qy9&%2rIdUktDangqrgU2=foX$@e`0*ZM}=Z*u}cA^Z|<2_TSL8s#WkP$^QHc z^=02p{1@5oN`vf#ho`?q-G=Xwec|hKhcr4LTH{^q@gNoR_Ix>>&t!2V% z3|oG2x7m)2eHN=%sC6nWN6RM2cO)gN(J9Zh7v3>TOselqT^(J&*_yBc0E`14E8muF zhMI4CD9nk4DQ>2ze6utKYpdY4gkv?Lq8G;q8BZVoKpCH-&3N_G8_ z@&K(X_O*z=o6AH38{wr1Pmbyv1ACCsQ^@$QZw3CEjD4wh%u%%3FEO9v;-@}7XnRE? z-y6cW{Vo?fJ{l0|O!ve6LAMq%1-eQl;KTQYPsDfe;tG>Tr{becXWjCyRTOY2T-KN@ zh=1vtGAy29`kknZe8dW|oZyS#TElxO3A)oC65Jwpz8IH~o9;928j2{$Z`fW_(EkLV zM64B);TYzOF+)|T)_h3VkYnzpc0g{4K_t143A`+E}%I3;{Np(}~I zcoajM;YdV#;|JmQv;0#;<%f`wm^M;_=cifaDvXXTxR_G_P$C=b^nE4a7rZZ@=Es)3 zXobwkF{+xV@|J{Yf+QS5DEC@NzRQCoO*_5-$1vQ#;Ui}$08?Z141GM*Ih)Ys?-8v$ z!oyi&*a0}3Bn3ZG0uF{m=piKB`mNyq-G~Z?J7#F{FCZmAf4otUrM1km&x~6flqVP7 zz#3!3yp5;)S|zj1JgM>r^*Gt{O1yLqsLM}6TZM972&sa<)y#@rg zc;(RYb(NtGcy)g8QX>Cf}X1f3Bzl;Kbt2X==YNR|5V@<`%@O(|4Wj` z9#|d7B6FIRShW0qf{9P>@PFyJFB_pT8bn*~but#>)HzbGuJ&ww*SQ3{)LQ;LZ+Av5 zS$C$2Rcxr82f-O|Z;h4kyML!U;0iv)CWLOv13n zlbu{O!WVopmKZeN6f5Twkb_D5t-MDfmp~ayTY=|}oa1K~uON2xoUCh3CtlRZMU0r` z)YlAo%TGnoty{%(0dLoBma%$R@dMgi7kz3ePaD@ zYL|jZm_@MlA(+>4o0MsBlrHrZj=GV!q&Sb=D!*K59_Y6db%bHJ0dV_7zu6q)+l5DT z=}^n$9wWXRNWyBpQw7`+1xw8YtZvkS^Thf#sM{B5=*wpy@>yMp zdZhWbEN+$K-%BDVDEmSo?p@E?#54to%_d(;X77j@OKLs@Zs$^v$z=MfvvVgpoxg#l zHjPyEbMoRq^OQRIDc|s_J_2V!hnMhA_z`Imp=3d^Sf;Rb8AK=aEDUn%f9a-1r}Hfe zxRrQ`XDHhEoxXcB*Eka8c?W4Bj;OWT`~5ld-FDos&$e&h50%{Rw^I8uTM*rL2*NVB8ltHN zztf;uUbI$?HWb+v?m>hy=BtgOWj5fFBhv4#X7IJS(D6mXLb+)XpMob zN8Au!{a*m-1Qz?w|Id*h%Y;X!SuJFpB)iOCmDF(E?%G72&R-ONO#j@3q4LE}e@aj4 zw+rf69Ib3~9N=h28~N#UeRn57r`%3y&Oxd`-ks0(E*QDQp)+XaR{^O4go;Pip9|ns zGUN>ZoYLt4|1+;Hn5@C?iGw3Qs=tc&>bD9S$_AaR&LOKZae9=qb4W+00)hzuG98w? zOUlnF9d;-lRg%Iq-IytQt63zb=uCsyZZKjUJ8bC^&~D5fHuCjJqK1B;{IkN#N}ioy zjDd^;oV)H4xi$35w7nmxrb?uz7IvUuf z8h~T53!NG~mrQi+rq{b9;T+vM9B=MGTOX*xhTS0_92EHj?Y@GJZ2+IRZw?1zDo z$6+$w&S#h(S0>2`%ct6NpBE`CJJPO#y6am7yw27t{Y~tc`RVMN>vsKg{dawyQ=A?Y zesuma@w;wJ9c?$x6;`PPs(9MXNHbs<`n*n!M{@ zH;(RF2khz`+b!~016cL$>G zoGV@5*k6}KTr?@G^yl=Z0)^5M)9?7IaL(DJ@<*3osX&Q(c0PP)`Z*Fmm2D_pD0^T! z>TNDJ9?ZYvVK)&#zV4X;aLVP5k7ouWC6E%rWmBrY%bc(c)8{YcRl#?+O!UO(s`Ge_ z>Eb!8>%eK^BpolV569~S2)i;F*NPWcUj?iV++3iYjHeohu1^IPU2$!yF`gzH+-4fA zj+-UDeLGs(RD0nIt9-&#)ivtJ0VAsY%k+jl+(OP3Wi?a#7kY9$|Z~@A_Gr zxWpLBw#Gik@utF|b98^MojzZxS1ObfDPlYjlR9;CQ zW*7XG4Wlf|&Rspn^EtMOr5leN3vQ!go1?|a4BLXj&@RR(-laaPuK~JDRte<-`owuU zAg#(d*;K{XPDZE#gUZVIx^oDm>p-uo%fGAO&&fm=B)Yy>ZpA~DJXUf#d8+YZo2mc^ z?;a-r(Ist_&Z~M8z?|BKYU}#i(d&e#s{YRSR%K$d%xAV+7wn_zyW>actn-73o*f=_ ze#@Aj?JE1M3io-=7$qHi^I_6wb*+nSovk>ZoM7Ni>F`61udCaZE28rWbC`WNMi@|7 znXcbXz;3V|&#vEd2CU08@wWoji6M3URed7eO7~TJHO?JRm401eqgMSoy-jphW$}}w z#?f|3&Dx~LL4U`#@qE?SE*MSva`jZ;;%jPbb^_@%N#MS#L`e0`a`G5;IL5qo{-N-7 znl$bBxWl~;KUF=4)k^MN>~@cJT>C0X?)qAT>uS9CoLkI1` and layer-name + metadata the authoring tool left behind are removed, and the gradient id + `未命名的渐变_6` ("unnamed gradient 6") is renamed `minimax-wave` because a + non-ASCII id collides awkwardly across inlined documents. + +Two marks are TRACED rather than fetched. Their vendors publish no usable +vector, and a trace that follows the source pixels is a truer mark than a +monogram. What is still refused either way: a horizontal wordmark squeezed into +this square slot, and a full-frame silhouette plate that renders as a filled box +at 20px. + +- `hermes-agent.svg` — traced 2026-08-31 from + `NousResearch/hermes-agent` `apps/desktop/assets/icon.png` (574273 bytes, + 1024x1024 RGBA), the icon the Hermes desktop application itself ships, so this + is the product's own mark. Two earlier candidates were rejected: + `website/static/img/favicon.svg` is 113 bytes and its entire body is one + `` element with no path data, and `nousresearch.com/safari-pinned-tab.svg` + opens with the full 512-unit frame as its first path, so it renders as a black + square. Traced with + `potrace -s --flat --turdsize 8 --alphamax 1.0 --opttolerance 0.2` over the + mask `alpha > 128 AND mean(rgb) < 110`, which keeps the black artwork and + discards the light plate behind it. One path, `currentColor`, squared to + `viewBox="0 0 823 823"` by centering the 823x806 trace. Named + `hermes-agent` rather than `hermes` because Hermes is also a provider name + and this directory is one flat namespace. +- `gajae-code.svg` — traced 2026-08-31 from `Yeachan-Heo/gajae-code` + `assets/character.png` (3190496 bytes, 1550x2048 RGBA), the mascot. No SVG + exists upstream: `assets/` and `docs/` hold only raster, `public/` is a 404, + the five plausible `logo.svg`/`favicon.svg` paths all 404, no published + `@gajae-code/*` tarball at 0.15.6 contains one, and `docs/brand-assets.md` + lists the marks as PNG. The source is a vertical lockup, so only the mascot is + traced — rows 1650-1682 are fully transparent, which is the seam the crop uses, + and the `gajae-code` wordmark below it is discarded. The artwork is upscaled + pixel art, so tracing at source resolution followed every staircase and gave a + 1.3 MB file; downsampling to a 128px box (Lanczos, then a 0.6px Gaussian) + first gives ~31 KB. Seven color layers, k-means++ seeded at 3 so the + quantization is deterministic, painted largest-area first. The smallest layer + is 292 px and a fixed area floor would have dropped it — it is the visor + green, which is the feature that makes the character recognizable, so the + floor is a fraction of the opaque area instead. + +## How a mark is painted + +Provenance is not the only fact that has to survive a handoff. Every mark is +drawn one of two ways, and picking wrong makes a logo vanish rather than look +slightly off: + +- **image** — the `` is rendered as-is, keeping its own colors. Correct for + anything multi-color, and for a single ink that *is* the brand. +- **mask** — the file is used as a shape and filled with the surrounding text + color, so it follows the theme. Correct for a neutral silhouette, which would + otherwise be invisible against one of the two surfaces. + +The set lives in `gui/src/components/integration-marks.ts`. It is derived from +`MONOCHROME_CLIENT_MARKS` for export clients, plus `MASKED_NATIVE_MARKS` for rows +that have no export client to be keyed by. + +Decisions that are not obvious from looking at the file: + +- `grok.svg` **is masked.** One `#000000` fill on transparency measured about + 1.9:1 on the dark card surface (`rgb(48,48,48)`) — effectively gone. Masking + does not modify xAI's file; it reads it as a shape, which is how xAI renders it + on their own dark surfaces. 11.17:1 dark and 17.67:1 light afterwards. +- `openai.svg` **is not masked**, despite also being a single fill. That fill is + #10A37F, OpenAI's brand green, and repainting it discards information a reader + uses to identify the mark. Neutrality is the test, not ink count. +- `deepseek-harness.svg` **is not masked** for the same reason: #4d6bfe is + DeepSeek blue. Its dark-theme contrast is adequate; if it ever is not, the fix + is a surface change, not a repaint. +- `hermes-agent.svg` **is masked.** The trace is one near-black path, so it is + invisible on `#0d1117` untinted. Nothing about the Hermes brand is carried by + that particular black. +- `minimax.svg` and `gajae-code.svg` **are not masked.** A gradient wave and a + seven-layer mascot respectively; masking would flatten both to one ink. +- `prime-agent.svg` **is masked.** White on transparency, so as an image it was + invisible in light mode. This one shipped broken. +- `opencode.svg` (#211E1E) and `kimi-color.svg` (#1A1A1A) **are masked.** Both + near-black single inks, invisible in dark mode as images. Both shipped broken + too, which is what established the rule. +- `aside.svg` **is masked.** It already paints with `currentColor`, so it would + follow the theme either way; masking keeps it consistent with the other + silhouettes rather than depending on inherited color. + +Both directions are enforced in `gui/tests/integration-marks.test.ts`, including a +luminance check that fails any single-ink near-neutral mark left as an image. That +direction was missing until it caught `grok`; the same class of defect had already +shipped once for `prime`, `opencode` and `kimi`. + +## Provider marks (2026-09-01) + +Sourced for the providers that were rendering a coloured initial tile. Every +entry below was fetched from the vendor's own domain, taken from the registry's +`baseUrl`/`dashboardUrl` rather than guessed. + +Published as SVG and committed with only comments, ``/`` and +`data-name` attributes stripped: + +- `digitalocean.svg` — `digitalocean.com` favicon, 32x32. +- `featherless.svg` — `featherless.ai/favicon.svg`, 256x256. +- `kilo.svg` — `kilo.ai/favicon/favicon.svg`, 32x32. Keeps its `oklch()` plate. +- `nanogpt.svg` — `nano-gpt.com/logo.svg`, 181x187, gradient. +- `nebius.svg` — `nebius.com/favicon/favicon.svg`, 96x96. +- `neuralwatt.svg` — the site's Webflow-hosted brand asset, 32x32. +- `parallel.svg` — `parallel.ai/icon.svg`, 96x96. +- `scaleway.svg` — `scaleway.com/favicon/website/favicon.svg`, 16x16. +- `synthetic.svg` — `synthetic.new/favicon.svg`, 54x54. +- `zai.svg` — `z-cdn.chatglm.cn/z-ai/static/logo.svg`, 30x30. +- `zenmux.svg` — the site's CDN-hosted brand mark, 160x160. + +Traced from raster, because the vendor publishes no square SVG mark. Same +technique as `hermes-agent.svg` and `gajae-code.svg`: `potrace -s --flat` for a +single-ink silhouette, k-means colour layers (seeded at 3, largest area first) +for multi-colour art, downsampled to a 160px box first so the trace does not +follow every upscaled pixel edge. + +- `cerebras.svg`, `novita.svg`, `siliconflow.svg`, `deepinfra.svg` — single-ink. +- `baseten.svg`, `hyperbolic.svg`, `sambanova.svg`, `umans.svg`, `venice.svg`, + `vultr.svg`, `bizrouter.svg`, `orcarouter.svg` — colour-layered. +- `nous.svg` — traced from `nousresearch.com/apple-touch-icon.png` (180x180). This + is the Nous Research company mark, distinct from `hermes-agent.svg`, which is + the Hermes product's own icon. Attributing one to the other would be wrong even + though the same organization ships both. + +Found on a docs subdomain after the vendor's marketing site offered only a +wordmark: + +- `together.svg` — `docs.together.ai/favicon.svg`, 1.07:1. +- `litellm.svg` — `docs.litellm.ai/img/logo.svg`, 1:1. The marketing site's SVGs + are third-party model logos, not LiteLLM's own mark. + +**The plate problem, recorded because the first pass shipped it.** A favicon is +usually a glyph on a filled rounded square. Tracing luminance alone captured the +square and produced a solid box: `baseten` came out 97.7% ink, `bizrouter` 89.3%. +The fix reads the border ring, takes its median colour as the plate when the ring +is uniform, and masks by distance from that colour instead of by darkness. It +found real plates behind `baseten` (#19e76e), `cerebras` (#ef5b27), `hyperbolic` +(#1a1a1a), `umans`/`bizrouter` (#000000) and `orcarouter` (#ffffff). + +### Rejected, and why + +- **`nousresearch.com/safari-pinned-tab.svg`** — opens with the full 512-unit + frame as its first path, so it renders as a black square. This is the identical + candidate the Hermes client mark rejected. The apple-touch-icon was used instead. +- **LiteLLM's marketing-site SVGs** — third-party model logos, and the favicon + traces to a muddy blob with no legible silhouette at 19px. The docs logo was + used instead. +- **Wordmarks refused** for `cerebras` (843x320 from Sanity CDN), `siliconflow` + (188x28), `zhipu-bigmodel` (123x25), `vultr` (218x52), `baseten` (1001x151) and + `chutes` (192x30). A lockup in the rail's 19px box is an illegible smear, so + each was replaced by a traced square mark or left to the fallback. + +### Still unmarked, and what was searched + +Six ids keep the fallback tile. Each was probed at its registry `baseUrl` and +`dashboardUrl`, plus the vendor's docs subdomain and the conventional icon paths +(`/favicon.svg`, `/favicon.ico`, `/apple-touch-icon.png`, `/logo.svg`, `/icon.svg`): + +- `chutes` — `chutes.ai` and `docs.chutes.ai` serve only the 192x30 wordmark. +- `nscale` — `nscale.com` and `docs.nscale.com` returned no icon at any path. +- `volcengine`, `volcengine-coding-plan`, `volcengine-agent-plan` — the Ark + console's SVGs are UI glyphs rather than a product mark. +- `tencent-coding-plan` — `cloud.tencent.com` serves a 32x32 favicon whose trace + is unreadable at 19px. + +These are recorded results, not skipped work. Inventing a mark, or borrowing a +neighbouring brand's, is a misattribution that outlives the commit. + +`zhipu-bigmodel` and `zhipu-bigmodel-coding` share `zai.svg`: Z.AI and BigModel +are the same company, and the mainland console publishes only the wordmark. + +## Meta (2026-09-03) + +- `meta.svg` — the `aria-label="Meta symbol"` inline SVG that `dev.meta.ai` + renders in its own navigation header, read 2026-09-03 through a signed-in + browser session. Meta publishes no square vector at the conventional paths: + `dev.meta.ai/favicon.svg`, `/icon.svg` and `/logo.svg` all 404, and the + site's declared icon is a 32x32 `.ico` on `static.xx.fbcdn.net`. The rendered + header mark is therefore the first-party vector, taken from the developer + console the two providers actually belong to. + + Path data and gradient stops are verbatim. Three normalizations: React's + generated gradient ids (`_r_d_`, `_r_e_`, `_r_f_`) become + `meta-mark-a/-b/-c`, because a generated id collides when several marks are + inlined into one document — the same reason `minimax.svg` renamed its + `未命名的渐变_6`; the presentational `height`/`width`/`role`/`aria-label` + are dropped in favour of the `viewBox`; and `xmlns` is added so the file + stands alone. + + Wired to both `meta-model` (the direct Meta Model API provider) and + `meta-muse` (the Muse Code credential import). One brand, two credentials — + the same shape as the three Alibaba ids sharing `alibaba-color.svg`. + **Not masked:** three linear gradients in Meta brand blue + (#0064E0 -> #0278F1), and masking flattens a gradient to a single ink. diff --git a/go/internal/embeddedui/static/provider-icons/alibaba-color.svg b/go/internal/embeddedui/static/provider-icons/alibaba-color.svg new file mode 100644 index 0000000000..69e374735d --- /dev/null +++ b/go/internal/embeddedui/static/provider-icons/alibaba-color.svg @@ -0,0 +1 @@ +Alibaba Cloud \ No newline at end of file diff --git a/go/internal/embeddedui/static/provider-icons/antigravity-color.svg b/go/internal/embeddedui/static/provider-icons/antigravity-color.svg new file mode 100644 index 0000000000..bf746e1246 --- /dev/null +++ b/go/internal/embeddedui/static/provider-icons/antigravity-color.svg @@ -0,0 +1 @@ +Antigravity \ No newline at end of file diff --git a/go/internal/embeddedui/static/provider-icons/aside.svg b/go/internal/embeddedui/static/provider-icons/aside.svg new file mode 100644 index 0000000000..a303dd7e60 --- /dev/null +++ b/go/internal/embeddedui/static/provider-icons/aside.svg @@ -0,0 +1,3 @@ + + + diff --git a/go/internal/embeddedui/static/provider-icons/baseten.svg b/go/internal/embeddedui/static/provider-icons/baseten.svg new file mode 100644 index 0000000000..2fa74a269b --- /dev/null +++ b/go/internal/embeddedui/static/provider-icons/baseten.svg @@ -0,0 +1,13 @@ + \ No newline at end of file diff --git a/go/internal/embeddedui/static/provider-icons/bizrouter.svg b/go/internal/embeddedui/static/provider-icons/bizrouter.svg new file mode 100644 index 0000000000..f0644866a3 --- /dev/null +++ b/go/internal/embeddedui/static/provider-icons/bizrouter.svg @@ -0,0 +1,41 @@ + \ No newline at end of file diff --git a/go/internal/embeddedui/static/provider-icons/cerebras.svg b/go/internal/embeddedui/static/provider-icons/cerebras.svg new file mode 100644 index 0000000000..56d52a3d3c --- /dev/null +++ b/go/internal/embeddedui/static/provider-icons/cerebras.svg @@ -0,0 +1,26 @@ + + + + +Created by potrace 1.16, written by Peter Selinger 2001-2019 + + + + + diff --git a/go/internal/embeddedui/static/provider-icons/claude-color.svg b/go/internal/embeddedui/static/provider-icons/claude-color.svg new file mode 100644 index 0000000000..9c2cb59c26 --- /dev/null +++ b/go/internal/embeddedui/static/provider-icons/claude-color.svg @@ -0,0 +1 @@ +Claude \ No newline at end of file diff --git a/go/internal/embeddedui/static/provider-icons/cline-color.svg b/go/internal/embeddedui/static/provider-icons/cline-color.svg new file mode 100644 index 0000000000..90a30f1acc --- /dev/null +++ b/go/internal/embeddedui/static/provider-icons/cline-color.svg @@ -0,0 +1,16 @@ + + + Cline + + + + + + + + + + + + + \ No newline at end of file diff --git a/go/internal/embeddedui/static/provider-icons/cloudflare-ai-gateway-color.svg b/go/internal/embeddedui/static/provider-icons/cloudflare-ai-gateway-color.svg new file mode 100644 index 0000000000..cf80692961 --- /dev/null +++ b/go/internal/embeddedui/static/provider-icons/cloudflare-ai-gateway-color.svg @@ -0,0 +1 @@ +Cloudflare \ No newline at end of file diff --git a/go/internal/embeddedui/static/provider-icons/commandcode-color.svg b/go/internal/embeddedui/static/provider-icons/commandcode-color.svg new file mode 100644 index 0000000000..4f257b4ecc --- /dev/null +++ b/go/internal/embeddedui/static/provider-icons/commandcode-color.svg @@ -0,0 +1 @@ +Command Code diff --git a/go/internal/embeddedui/static/provider-icons/copilot-color.svg b/go/internal/embeddedui/static/provider-icons/copilot-color.svg new file mode 100644 index 0000000000..c5b411ad2f --- /dev/null +++ b/go/internal/embeddedui/static/provider-icons/copilot-color.svg @@ -0,0 +1 @@ +Copilot \ No newline at end of file diff --git a/go/internal/embeddedui/static/provider-icons/cursor-color.svg b/go/internal/embeddedui/static/provider-icons/cursor-color.svg new file mode 100644 index 0000000000..d903610064 --- /dev/null +++ b/go/internal/embeddedui/static/provider-icons/cursor-color.svg @@ -0,0 +1,2 @@ + +Cursor \ No newline at end of file diff --git a/go/internal/embeddedui/static/provider-icons/deepinfra.svg b/go/internal/embeddedui/static/provider-icons/deepinfra.svg new file mode 100644 index 0000000000..44c1eb0f4f --- /dev/null +++ b/go/internal/embeddedui/static/provider-icons/deepinfra.svg @@ -0,0 +1,75 @@ + + + + +Created by potrace 1.16, written by Peter Selinger 2001-2019 + + + + + diff --git a/go/internal/embeddedui/static/provider-icons/deepseek-color.svg b/go/internal/embeddedui/static/provider-icons/deepseek-color.svg new file mode 100644 index 0000000000..f3be195b8e --- /dev/null +++ b/go/internal/embeddedui/static/provider-icons/deepseek-color.svg @@ -0,0 +1 @@ +DeepSeek \ No newline at end of file diff --git a/go/internal/embeddedui/static/provider-icons/deepseek-harness.svg b/go/internal/embeddedui/static/provider-icons/deepseek-harness.svg new file mode 100644 index 0000000000..653b77e157 --- /dev/null +++ b/go/internal/embeddedui/static/provider-icons/deepseek-harness.svg @@ -0,0 +1,3 @@ + + + diff --git a/go/internal/embeddedui/static/provider-icons/digitalocean.svg b/go/internal/embeddedui/static/provider-icons/digitalocean.svg new file mode 100644 index 0000000000..2d9a943e90 --- /dev/null +++ b/go/internal/embeddedui/static/provider-icons/digitalocean.svg @@ -0,0 +1,10 @@ + + + + + + + \ No newline at end of file diff --git a/go/internal/embeddedui/static/provider-icons/discord.svg b/go/internal/embeddedui/static/provider-icons/discord.svg new file mode 100644 index 0000000000..0440ab877a --- /dev/null +++ b/go/internal/embeddedui/static/provider-icons/discord.svg @@ -0,0 +1 @@ +Discord \ No newline at end of file diff --git a/go/internal/embeddedui/static/provider-icons/featherless.svg b/go/internal/embeddedui/static/provider-icons/featherless.svg new file mode 100644 index 0000000000..dd171d68e4 --- /dev/null +++ b/go/internal/embeddedui/static/provider-icons/featherless.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/go/internal/embeddedui/static/provider-icons/firepass-color.svg b/go/internal/embeddedui/static/provider-icons/firepass-color.svg new file mode 100644 index 0000000000..01f493c58c --- /dev/null +++ b/go/internal/embeddedui/static/provider-icons/firepass-color.svg @@ -0,0 +1 @@ +Fireworks \ No newline at end of file diff --git a/go/internal/embeddedui/static/provider-icons/fireworks-color.svg b/go/internal/embeddedui/static/provider-icons/fireworks-color.svg new file mode 100644 index 0000000000..01f493c58c --- /dev/null +++ b/go/internal/embeddedui/static/provider-icons/fireworks-color.svg @@ -0,0 +1 @@ +Fireworks \ No newline at end of file diff --git a/go/internal/embeddedui/static/provider-icons/gajae-code.svg b/go/internal/embeddedui/static/provider-icons/gajae-code.svg new file mode 100644 index 0000000000..d4705e6ac3 --- /dev/null +++ b/go/internal/embeddedui/static/provider-icons/gajae-code.svg @@ -0,0 +1,410 @@ + \ No newline at end of file diff --git a/go/internal/embeddedui/static/provider-icons/gemini-color.svg b/go/internal/embeddedui/static/provider-icons/gemini-color.svg new file mode 100644 index 0000000000..822a18de8a --- /dev/null +++ b/go/internal/embeddedui/static/provider-icons/gemini-color.svg @@ -0,0 +1 @@ +Gemini \ No newline at end of file diff --git a/go/internal/embeddedui/static/provider-icons/github-copilot-color.svg b/go/internal/embeddedui/static/provider-icons/github-copilot-color.svg new file mode 100644 index 0000000000..e713560a88 --- /dev/null +++ b/go/internal/embeddedui/static/provider-icons/github-copilot-color.svg @@ -0,0 +1 @@ +GitHub Copilot \ No newline at end of file diff --git a/go/internal/embeddedui/static/provider-icons/gitlab-duo-color.svg b/go/internal/embeddedui/static/provider-icons/gitlab-duo-color.svg new file mode 100644 index 0000000000..562154d5d3 --- /dev/null +++ b/go/internal/embeddedui/static/provider-icons/gitlab-duo-color.svg @@ -0,0 +1 @@ +GitLab \ No newline at end of file diff --git a/go/internal/embeddedui/static/provider-icons/grok.svg b/go/internal/embeddedui/static/provider-icons/grok.svg new file mode 100644 index 0000000000..7374eefe94 --- /dev/null +++ b/go/internal/embeddedui/static/provider-icons/grok.svg @@ -0,0 +1 @@ +Grok diff --git a/go/internal/embeddedui/static/provider-icons/groq-color.svg b/go/internal/embeddedui/static/provider-icons/groq-color.svg new file mode 100644 index 0000000000..2eecc367cc --- /dev/null +++ b/go/internal/embeddedui/static/provider-icons/groq-color.svg @@ -0,0 +1 @@ +Groq \ No newline at end of file diff --git a/go/internal/embeddedui/static/provider-icons/hermes-agent.svg b/go/internal/embeddedui/static/provider-icons/hermes-agent.svg new file mode 100644 index 0000000000..4761dd73fa --- /dev/null +++ b/go/internal/embeddedui/static/provider-icons/hermes-agent.svg @@ -0,0 +1,207 @@ + \ No newline at end of file diff --git a/go/internal/embeddedui/static/provider-icons/huggingface-color.svg b/go/internal/embeddedui/static/provider-icons/huggingface-color.svg new file mode 100644 index 0000000000..2267427337 --- /dev/null +++ b/go/internal/embeddedui/static/provider-icons/huggingface-color.svg @@ -0,0 +1 @@ +Hugging Face \ No newline at end of file diff --git a/go/internal/embeddedui/static/provider-icons/hyperbolic.svg b/go/internal/embeddedui/static/provider-icons/hyperbolic.svg new file mode 100644 index 0000000000..a71713acf5 --- /dev/null +++ b/go/internal/embeddedui/static/provider-icons/hyperbolic.svg @@ -0,0 +1,18 @@ + \ No newline at end of file diff --git a/go/internal/embeddedui/static/provider-icons/kilo.svg b/go/internal/embeddedui/static/provider-icons/kilo.svg new file mode 100644 index 0000000000..c4b6bb8ba4 --- /dev/null +++ b/go/internal/embeddedui/static/provider-icons/kilo.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/go/internal/embeddedui/static/provider-icons/kimi-color.svg b/go/internal/embeddedui/static/provider-icons/kimi-color.svg new file mode 100644 index 0000000000..39c3e2f9b2 --- /dev/null +++ b/go/internal/embeddedui/static/provider-icons/kimi-color.svg @@ -0,0 +1 @@ +Moonshot AI \ No newline at end of file diff --git a/go/internal/embeddedui/static/provider-icons/kiro-color.svg b/go/internal/embeddedui/static/provider-icons/kiro-color.svg new file mode 100644 index 0000000000..0cf048bec2 --- /dev/null +++ b/go/internal/embeddedui/static/provider-icons/kiro-color.svg @@ -0,0 +1,15 @@ + + +Kiro + + + + + + + + + + + + \ No newline at end of file diff --git a/go/internal/embeddedui/static/provider-icons/litellm.svg b/go/internal/embeddedui/static/provider-icons/litellm.svg new file mode 100644 index 0000000000..9db6d0d066 --- /dev/null +++ b/go/internal/embeddedui/static/provider-icons/litellm.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/go/internal/embeddedui/static/provider-icons/lm-studio-color.svg b/go/internal/embeddedui/static/provider-icons/lm-studio-color.svg new file mode 100644 index 0000000000..c50f3a6526 --- /dev/null +++ b/go/internal/embeddedui/static/provider-icons/lm-studio-color.svg @@ -0,0 +1 @@ +LM Studio \ No newline at end of file diff --git a/go/internal/embeddedui/static/provider-icons/meta.svg b/go/internal/embeddedui/static/provider-icons/meta.svg new file mode 100644 index 0000000000..59d5570e57 --- /dev/null +++ b/go/internal/embeddedui/static/provider-icons/meta.svg @@ -0,0 +1 @@ + diff --git a/go/internal/embeddedui/static/provider-icons/minimax.svg b/go/internal/embeddedui/static/provider-icons/minimax.svg new file mode 100644 index 0000000000..31b734a6cd --- /dev/null +++ b/go/internal/embeddedui/static/provider-icons/minimax.svg @@ -0,0 +1 @@ + diff --git a/go/internal/embeddedui/static/provider-icons/mistral-color.svg b/go/internal/embeddedui/static/provider-icons/mistral-color.svg new file mode 100644 index 0000000000..a02c1ba31c --- /dev/null +++ b/go/internal/embeddedui/static/provider-icons/mistral-color.svg @@ -0,0 +1 @@ +Mistral AI \ No newline at end of file diff --git a/go/internal/embeddedui/static/provider-icons/moonshot-color.svg b/go/internal/embeddedui/static/provider-icons/moonshot-color.svg new file mode 100644 index 0000000000..ed2bbfbfe5 --- /dev/null +++ b/go/internal/embeddedui/static/provider-icons/moonshot-color.svg @@ -0,0 +1 @@ +Moonshot AI \ No newline at end of file diff --git a/go/internal/embeddedui/static/provider-icons/nanogpt.svg b/go/internal/embeddedui/static/provider-icons/nanogpt.svg new file mode 100644 index 0000000000..4344909a8b --- /dev/null +++ b/go/internal/embeddedui/static/provider-icons/nanogpt.svg @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/go/internal/embeddedui/static/provider-icons/nebius.svg b/go/internal/embeddedui/static/provider-icons/nebius.svg new file mode 100644 index 0000000000..4d55b7bf0b --- /dev/null +++ b/go/internal/embeddedui/static/provider-icons/nebius.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/go/internal/embeddedui/static/provider-icons/neuralwatt.svg b/go/internal/embeddedui/static/provider-icons/neuralwatt.svg new file mode 100644 index 0000000000..3339f995c7 --- /dev/null +++ b/go/internal/embeddedui/static/provider-icons/neuralwatt.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/go/internal/embeddedui/static/provider-icons/nous.svg b/go/internal/embeddedui/static/provider-icons/nous.svg new file mode 100644 index 0000000000..5307d5feb2 --- /dev/null +++ b/go/internal/embeddedui/static/provider-icons/nous.svg @@ -0,0 +1,149 @@ + + + + +Created by potrace 1.16, written by Peter Selinger 2001-2019 + + + + + diff --git a/go/internal/embeddedui/static/provider-icons/novita.svg b/go/internal/embeddedui/static/provider-icons/novita.svg new file mode 100644 index 0000000000..75f64f25a4 --- /dev/null +++ b/go/internal/embeddedui/static/provider-icons/novita.svg @@ -0,0 +1,32 @@ + + + + +Created by potrace 1.16, written by Peter Selinger 2001-2019 + + + + + diff --git a/go/internal/embeddedui/static/provider-icons/nvidia-color.svg b/go/internal/embeddedui/static/provider-icons/nvidia-color.svg new file mode 100644 index 0000000000..c3ac1b8df2 --- /dev/null +++ b/go/internal/embeddedui/static/provider-icons/nvidia-color.svg @@ -0,0 +1 @@ +NVIDIA \ No newline at end of file diff --git a/go/internal/embeddedui/static/provider-icons/oh-my-pi.svg b/go/internal/embeddedui/static/provider-icons/oh-my-pi.svg new file mode 100644 index 0000000000..553490c5e9 --- /dev/null +++ b/go/internal/embeddedui/static/provider-icons/oh-my-pi.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/go/internal/embeddedui/static/provider-icons/ollama-color.svg b/go/internal/embeddedui/static/provider-icons/ollama-color.svg new file mode 100644 index 0000000000..1e3879cc60 --- /dev/null +++ b/go/internal/embeddedui/static/provider-icons/ollama-color.svg @@ -0,0 +1 @@ +Ollama \ No newline at end of file diff --git a/go/internal/embeddedui/static/provider-icons/openai.svg b/go/internal/embeddedui/static/provider-icons/openai.svg new file mode 100644 index 0000000000..ef1ef3096d --- /dev/null +++ b/go/internal/embeddedui/static/provider-icons/openai.svg @@ -0,0 +1 @@ +OpenAI diff --git a/go/internal/embeddedui/static/provider-icons/openclaw.svg b/go/internal/embeddedui/static/provider-icons/openclaw.svg new file mode 100644 index 0000000000..dfa44629a2 --- /dev/null +++ b/go/internal/embeddedui/static/provider-icons/openclaw.svg @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/go/internal/embeddedui/static/provider-icons/opencode.svg b/go/internal/embeddedui/static/provider-icons/opencode.svg new file mode 100644 index 0000000000..d617075e7f --- /dev/null +++ b/go/internal/embeddedui/static/provider-icons/opencode.svg @@ -0,0 +1,2 @@ + +OpenCodeX diff --git a/go/internal/embeddedui/static/provider-icons/openrouter-color.svg b/go/internal/embeddedui/static/provider-icons/openrouter-color.svg new file mode 100644 index 0000000000..e0f98db715 --- /dev/null +++ b/go/internal/embeddedui/static/provider-icons/openrouter-color.svg @@ -0,0 +1 @@ +OpenRouter \ No newline at end of file diff --git a/go/internal/embeddedui/static/provider-icons/orcarouter.svg b/go/internal/embeddedui/static/provider-icons/orcarouter.svg new file mode 100644 index 0000000000..4cd5bf9f47 --- /dev/null +++ b/go/internal/embeddedui/static/provider-icons/orcarouter.svg @@ -0,0 +1,175 @@ + \ No newline at end of file diff --git a/go/internal/embeddedui/static/provider-icons/parallel.svg b/go/internal/embeddedui/static/provider-icons/parallel.svg new file mode 100644 index 0000000000..5d87359bf9 --- /dev/null +++ b/go/internal/embeddedui/static/provider-icons/parallel.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/go/internal/embeddedui/static/provider-icons/pi.svg b/go/internal/embeddedui/static/provider-icons/pi.svg new file mode 100644 index 0000000000..976e4ba744 --- /dev/null +++ b/go/internal/embeddedui/static/provider-icons/pi.svg @@ -0,0 +1,21 @@ + + + + + + \ No newline at end of file diff --git a/go/internal/embeddedui/static/provider-icons/prime-agent.svg b/go/internal/embeddedui/static/provider-icons/prime-agent.svg new file mode 100644 index 0000000000..e0009d14ce --- /dev/null +++ b/go/internal/embeddedui/static/provider-icons/prime-agent.svg @@ -0,0 +1,21 @@ + + + + + + diff --git a/go/internal/embeddedui/static/provider-icons/qianfan-color.svg b/go/internal/embeddedui/static/provider-icons/qianfan-color.svg new file mode 100644 index 0000000000..fa72fb221b --- /dev/null +++ b/go/internal/embeddedui/static/provider-icons/qianfan-color.svg @@ -0,0 +1 @@ +Baidu \ No newline at end of file diff --git a/go/internal/embeddedui/static/provider-icons/qwen-portal-color.svg b/go/internal/embeddedui/static/provider-icons/qwen-portal-color.svg new file mode 100644 index 0000000000..bcbcdd90b8 --- /dev/null +++ b/go/internal/embeddedui/static/provider-icons/qwen-portal-color.svg @@ -0,0 +1 @@ +QWen \ No newline at end of file diff --git a/go/internal/embeddedui/static/provider-icons/sambanova.svg b/go/internal/embeddedui/static/provider-icons/sambanova.svg new file mode 100644 index 0000000000..341c74ea0f --- /dev/null +++ b/go/internal/embeddedui/static/provider-icons/sambanova.svg @@ -0,0 +1,276 @@ + \ No newline at end of file diff --git a/go/internal/embeddedui/static/provider-icons/scaleway.svg b/go/internal/embeddedui/static/provider-icons/scaleway.svg new file mode 100644 index 0000000000..4996b509b4 --- /dev/null +++ b/go/internal/embeddedui/static/provider-icons/scaleway.svg @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/go/internal/embeddedui/static/provider-icons/siliconflow.svg b/go/internal/embeddedui/static/provider-icons/siliconflow.svg new file mode 100644 index 0000000000..8537ff8e30 --- /dev/null +++ b/go/internal/embeddedui/static/provider-icons/siliconflow.svg @@ -0,0 +1,18 @@ + + + + +Created by potrace 1.16, written by Peter Selinger 2001-2019 + + + + + diff --git a/go/internal/embeddedui/static/provider-icons/synthetic.svg b/go/internal/embeddedui/static/provider-icons/synthetic.svg new file mode 100644 index 0000000000..87dfcb7383 --- /dev/null +++ b/go/internal/embeddedui/static/provider-icons/synthetic.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/go/internal/embeddedui/static/provider-icons/telegram.svg b/go/internal/embeddedui/static/provider-icons/telegram.svg new file mode 100644 index 0000000000..df6cc589d0 --- /dev/null +++ b/go/internal/embeddedui/static/provider-icons/telegram.svg @@ -0,0 +1 @@ +Telegram \ No newline at end of file diff --git a/go/internal/embeddedui/static/provider-icons/together.svg b/go/internal/embeddedui/static/provider-icons/together.svg new file mode 100644 index 0000000000..2fcc258c45 --- /dev/null +++ b/go/internal/embeddedui/static/provider-icons/together.svg @@ -0,0 +1,18 @@ + + + + + + + + \ No newline at end of file diff --git a/go/internal/embeddedui/static/provider-icons/umans.svg b/go/internal/embeddedui/static/provider-icons/umans.svg new file mode 100644 index 0000000000..c6176f143a --- /dev/null +++ b/go/internal/embeddedui/static/provider-icons/umans.svg @@ -0,0 +1,30 @@ + \ No newline at end of file diff --git a/go/internal/embeddedui/static/provider-icons/venice.svg b/go/internal/embeddedui/static/provider-icons/venice.svg new file mode 100644 index 0000000000..d914e79c3b --- /dev/null +++ b/go/internal/embeddedui/static/provider-icons/venice.svg @@ -0,0 +1,165 @@ + \ No newline at end of file diff --git a/go/internal/embeddedui/static/provider-icons/vercel-ai-gateway-color.svg b/go/internal/embeddedui/static/provider-icons/vercel-ai-gateway-color.svg new file mode 100644 index 0000000000..054f1df640 --- /dev/null +++ b/go/internal/embeddedui/static/provider-icons/vercel-ai-gateway-color.svg @@ -0,0 +1 @@ +Vercel \ No newline at end of file diff --git a/go/internal/embeddedui/static/provider-icons/vllm-color.svg b/go/internal/embeddedui/static/provider-icons/vllm-color.svg new file mode 100644 index 0000000000..f8a5a23134 --- /dev/null +++ b/go/internal/embeddedui/static/provider-icons/vllm-color.svg @@ -0,0 +1 @@ +vLLM \ No newline at end of file diff --git a/go/internal/embeddedui/static/provider-icons/vultr.svg b/go/internal/embeddedui/static/provider-icons/vultr.svg new file mode 100644 index 0000000000..69e60c18d9 --- /dev/null +++ b/go/internal/embeddedui/static/provider-icons/vultr.svg @@ -0,0 +1,15 @@ + \ No newline at end of file diff --git a/go/internal/embeddedui/static/provider-icons/xiaomi-color.svg b/go/internal/embeddedui/static/provider-icons/xiaomi-color.svg new file mode 100644 index 0000000000..68457a9783 --- /dev/null +++ b/go/internal/embeddedui/static/provider-icons/xiaomi-color.svg @@ -0,0 +1 @@ +Xiaomi \ No newline at end of file diff --git a/go/internal/embeddedui/static/provider-icons/zai.svg b/go/internal/embeddedui/static/provider-icons/zai.svg new file mode 100644 index 0000000000..536f3521ee --- /dev/null +++ b/go/internal/embeddedui/static/provider-icons/zai.svg @@ -0,0 +1,218 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/go/internal/embeddedui/static/provider-icons/zcode.svg b/go/internal/embeddedui/static/provider-icons/zcode.svg new file mode 100644 index 0000000000..4f511bd72f --- /dev/null +++ b/go/internal/embeddedui/static/provider-icons/zcode.svg @@ -0,0 +1,219 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/go/internal/embeddedui/static/provider-icons/zenmux.svg b/go/internal/embeddedui/static/provider-icons/zenmux.svg new file mode 100644 index 0000000000..0f0f3b1800 --- /dev/null +++ b/go/internal/embeddedui/static/provider-icons/zenmux.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/go/internal/ocxcli/cli.go b/go/internal/ocxcli/cli.go index 3c893729c3..e347edd2ae 100644 --- a/go/internal/ocxcli/cli.go +++ b/go/internal/ocxcli/cli.go @@ -229,6 +229,10 @@ func Run(args []string, deps Deps) int { return printSubcommandHelp(args[0], deps) } switch args[0] { + case "serve-dashboard": + // Deliberately undocumented while start/service remain TypeScript-owned. + // This release smoke command proves the embedded artifact independently. + return runEmbeddedDashboard(args[1:], deps) case "--version", "-v", "version": fmt.Fprintf(deps.Stdout, "opencodex %s\n", deps.Version) return ExitOK diff --git a/go/internal/ocxcli/delegate.go b/go/internal/ocxcli/delegate.go index 0076f13883..4ead7c256a 100644 --- a/go/internal/ocxcli/delegate.go +++ b/go/internal/ocxcli/delegate.go @@ -40,7 +40,7 @@ func typeScriptCLIPath() (string, error) { if info, err := os.Stat(configured); err == nil && !info.IsDir() { return configured, nil } - return "", fmt.Errorf("OCX_TYPESCRIPT_CLI is not a readable file: %s", configured) + return "", fmt.Errorf("OCX_TYPESCRIPT_CLI is not a readable TypeScript CLI file: %s; install or update the full OpenCodex distribution, or point OCX_TYPESCRIPT_CLI at src/cli/index.ts", configured) } dir, err := os.Getwd() if err != nil { @@ -57,5 +57,5 @@ func typeScriptCLIPath() (string, error) { } dir = parent } - return "", errors.New("TypeScript lifecycle owner not found; set OCX_TYPESCRIPT_CLI to src/cli/index.ts") + return "", errors.New("this standalone ocx binary needs the TypeScript lifecycle owner for this command; install or update the full OpenCodex distribution, or set OCX_TYPESCRIPT_CLI to an explicit src/cli/index.ts path (and OCX_BUN to Bun if it is not on PATH)") } diff --git a/go/internal/ocxcli/delegate_test.go b/go/internal/ocxcli/delegate_test.go new file mode 100644 index 0000000000..eeedffddc2 --- /dev/null +++ b/go/internal/ocxcli/delegate_test.go @@ -0,0 +1,38 @@ +package ocxcli + +import ( + "os" + "strings" + "testing" +) + +func TestTypeScriptLifecycleOwnerMissingExplainsStandaloneRepair(t *testing.T) { + t.Setenv("OCX_TYPESCRIPT_CLI", "") + t.Setenv("OCX_BUN", "") + cwd, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { _ = os.Chdir(cwd) }) + if err := os.Chdir(t.TempDir()); err != nil { + t.Fatal(err) + } + _, err = typeScriptCLIPath() + if err == nil { + t.Fatal("missing lifecycle owner unexpectedly resolved") + } + message := err.Error() + for _, want := range []string{"standalone ocx binary", "OCX_TYPESCRIPT_CLI", "OCX_BUN", "install or update"} { + if !strings.Contains(message, want) { + t.Fatalf("error %q does not explain %q", message, want) + } + } +} + +func TestConfiguredTypeScriptLifecycleOwnerErrorNamesRepairPath(t *testing.T) { + t.Setenv("OCX_TYPESCRIPT_CLI", "/definitely/missing/ocx-cli.ts") + _, err := typeScriptCLIPath() + if err == nil || !strings.Contains(err.Error(), "OCX_TYPESCRIPT_CLI") || !strings.Contains(err.Error(), "install or update") { + t.Fatalf("configured owner error = %v", err) + } +} diff --git a/go/internal/ocxcli/embedded_dashboard.go b/go/internal/ocxcli/embedded_dashboard.go new file mode 100644 index 0000000000..0fe49e46fb --- /dev/null +++ b/go/internal/ocxcli/embedded_dashboard.go @@ -0,0 +1,32 @@ +package ocxcli + +import ( + "flag" + "fmt" + "net" + "net/http" + + "github.com/lidge-jun/opencodex/go/internal/embeddedui" +) + +func runEmbeddedDashboard(args []string, deps Deps) int { + flags := flag.NewFlagSet("serve-dashboard", flag.ContinueOnError) + flags.SetOutput(deps.Stderr) + listen := flags.String("listen", "127.0.0.1:10100", "listener address") + if err := flags.Parse(args); err != nil || flags.NArg() != 0 { + fmt.Fprintln(deps.Stderr, "Usage: ocx serve-dashboard [--listen ]") + return ExitUsage + } + listener, err := net.Listen("tcp", *listen) + if err != nil { + fmt.Fprintln(deps.Stderr, err) + return ExitFailure + } + defer listener.Close() + fmt.Fprintf(deps.Stdout, "OpenCodex embedded dashboard listening on http://%s\n", listener.Addr()) + if err := http.Serve(listener, embeddedui.NewHandler(deps.Version)); err != nil && err != http.ErrServerClosed { + fmt.Fprintln(deps.Stderr, err) + return ExitFailure + } + return ExitOK +} diff --git a/scripts/build-go-release-artifact.sh b/scripts/build-go-release-artifact.sh index 6ac0712db7..54f23be328 100755 --- a/scripts/build-go-release-artifact.sh +++ b/scripts/build-go-release-artifact.sh @@ -34,7 +34,14 @@ if [ "$goos" = windows ]; then fi mkdir -p "$output_dir" +# The release artifact embeds the actual Vite output; source builds retain the checked-in fallback snapshot. +"$repo_root/scripts/sync-go-embedded-dashboard.sh" +version="$(sed -n 's/^[[:space:]]*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$repo_root/package.json" | head -n 1)" +if [ -z "$version" ]; then + echo "could not read package version for Go release artifact" >&2 + exit 1 +fi cd "$repo_root/go" GOOS="$goos" GOARCH="$goarch" CGO_ENABLED=0 \ - go build -buildvcs=false -trimpath -o "$output_dir/$filename" ./cmd/ocx + go build -buildvcs=false -trimpath -ldflags "-X main.version=$version" -o "$output_dir/$filename" ./cmd/ocx printf '%s\n' "$output_dir/$filename" diff --git a/scripts/sync-go-embedded-dashboard.sh b/scripts/sync-go-embedded-dashboard.sh new file mode 100755 index 0000000000..af5197afe2 --- /dev/null +++ b/scripts/sync-go-embedded-dashboard.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +# Refresh the Go release binary's go:embed tree from the Vite dashboard build. +# This runs only on a release build host. A small checked-in snapshot remains so +# go build works for source users and CI without Bun installed. +set -euo pipefail + +repo_root="$(cd "$(dirname "$0")/.." && pwd)" +bun_bin="${OCX_BUN:-}" +if [ -z "$bun_bin" ]; then bun_bin="$(command -v bun || true)"; fi +if [ -z "$bun_bin" ]; then + echo "sync-go-embedded-dashboard: Bun is required to build the release dashboard (set OCX_BUN)" >&2 + exit 1 +fi +cd "$repo_root/gui" +"$bun_bin" install --frozen-lockfile +"$bun_bin" run build +[ -f dist/index.html ] || { echo "sync-go-embedded-dashboard: gui/dist/index.html missing after build" >&2; exit 1; } +target="$repo_root/go/internal/embeddedui/static" +find "$target" -mindepth 1 -delete +cp -R dist/. "$target/" +printf 'embedded dashboard refreshed from %s\n' "$repo_root/gui/dist" From a574cabd2f42d6ab6e2a66bad99a5c2fe3d64346 Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Mon, 7 Sep 2026 08:38:21 +0800 Subject: [PATCH 104/165] feat(go): flip start to sole Go listener with port reclaim Go start binds the runtime listener directly with embedded dashboard, /healthz attestation, /readyz, /api/stop drain, and runtime-port records compatible with the TS format. Port reclaim handles stale pid records, verified TS runtimes (TERM + bounded wait), and refuses foreign listeners. stop becomes Go-owned with a graceful-then-SIGTERM ladder. Release workflow builds and attaches static Go artifacts per release tag. Darwin Stat_t build fix unblocks the release matrix. --- .github/workflows/release.yml | 31 ++ go/internal/ocxcli/cli.go | 8 +- go/internal/ocxcli/doctor_owner_darwin.go | 24 ++ go/internal/ocxcli/doctor_owner_unix.go | 2 +- go/internal/ocxcli/runtime_server.go | 352 ++++++++++++++++++++++ go/internal/ocxcli/runtime_server_test.go | 153 ++++++++++ 6 files changed, 567 insertions(+), 3 deletions(-) create mode 100644 go/internal/ocxcli/doctor_owner_darwin.go create mode 100644 go/internal/ocxcli/runtime_server.go create mode 100644 go/internal/ocxcli/runtime_server_test.go diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f053574295..5fa1c37687 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -387,6 +387,25 @@ jobs: npm view @bitkyc08/opencodex versions dist-tags --json || true exit 1 + # ADR-0008 increment 7 (#41): the Go binary is the release runtime. Build the + # static cross-platform artifacts and attach them to the GitHub Release; the + # npm package remains the source distribution channel, but every release tag + # now ships a TypeScript-free single-binary artifact. + - name: Setup Go for single-binary artifacts + uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5 + with: + go-version-file: go/go.mod + cache: false + + - name: Build Go single-binary release artifacts + run: | + set -euo pipefail + mkdir -p .tmp/go-release + for target in linux/amd64 linux/arm64 darwin/amd64 darwin/arm64 windows/amd64; do + scripts/build-go-release-artifact.sh "$target" ".tmp/go-release" + done + ls -la .tmp/go-release + - name: Create GitHub release if: ${{ inputs.dry-run != true }} env: @@ -422,3 +441,15 @@ jobs: gh release create "$release_tag" --target "$GITHUB_SHA" --title "$release_tag" \ --notes-file "$notes_file" ${prerelease_flag:+$prerelease_flag} + + - name: Attach Go artifacts to release + if: ${{ inputs.dry-run != true }} + env: + GH_TOKEN: ${{ github.token }} + RELEASE_VERSION: ${{ inputs.version }} + run: | + set -euo pipefail + release_tag="v${RELEASE_VERSION}" + for artifact in .tmp/go-release/ocx-*; do + gh release upload "$release_tag" "$artifact" --clobber + done diff --git a/go/internal/ocxcli/cli.go b/go/internal/ocxcli/cli.go index e347edd2ae..b631784bef 100644 --- a/go/internal/ocxcli/cli.go +++ b/go/internal/ocxcli/cli.go @@ -49,8 +49,8 @@ type Command struct { var Commands = []Command{ {Name: "setup", Aliases: []string{"init"}, Usage: "ocx setup", Summary: "Interactive setup.", Owner: TypeScriptOwned}, - {Name: "start", Usage: "ocx start [--port ]", Summary: "Start the proxy.", Owner: TypeScriptOwned}, - {Name: "stop", Usage: "ocx stop", Summary: "Stop the proxy.", Owner: TypeScriptOwned}, + {Name: "start", Usage: "ocx start [--port ]", Summary: "Start the proxy.", Owner: GoOwned}, + {Name: "stop", Usage: "ocx stop", Summary: "Stop the proxy.", Owner: GoOwned}, {Name: "restore", Aliases: []string{"eject"}, Usage: "ocx restore [back]", Summary: "Restore native Codex configuration.", Owner: TypeScriptOwned}, {Name: "recover-history", Usage: "ocx recover-history --legacy-openai --yes", Summary: "Recover legacy history.", Owner: TypeScriptOwned}, {Name: "uninstall", Aliases: []string{"remove"}, Usage: "ocx uninstall", Summary: "Remove OpenCodex integration.", Owner: TypeScriptOwned}, @@ -263,6 +263,10 @@ func Run(args []string, deps Deps) int { return runStatus(args[1:], deps) case "doctor": return RunDoctorCommand(args[1:], deps.Stdout, deps.Stderr, DoctorCommandDeps{}) + case "start": + return runStart(args[1:], deps) + case "stop": + return runStop(args[1:], deps) default: // The ownership registry above and this switch must be reconciled by // TestOwnershipMapMatchesDispatch; this is defensive for future edits. diff --git a/go/internal/ocxcli/doctor_owner_darwin.go b/go/internal/ocxcli/doctor_owner_darwin.go new file mode 100644 index 0000000000..66a65d1b2b --- /dev/null +++ b/go/internal/ocxcli/doctor_owner_darwin.go @@ -0,0 +1,24 @@ +//go:build darwin + +package ocxcli + +import ( + "os" + "syscall" +) + +func doctorOwnedByCurrentUser(info os.FileInfo) bool { + stat, ok := info.Sys().(*syscall.Stat_t) + return ok && int(stat.Uid) == os.Getuid() +} + +// doctorSameFullFileIdentity matches the POSIX dev/inode/size/mtime/ctime +// evidence TypeScript validates immediately before recovery rename. Darwin's +// syscall.Stat_t spells the timestamp fields *spec rather than the Linux *t. +func doctorSameFullFileIdentity(left, right os.FileInfo) bool { + a, aok := left.Sys().(*syscall.Stat_t) + b, bok := right.Sys().(*syscall.Stat_t) + return aok && bok && a.Dev == b.Dev && a.Ino == b.Ino && a.Size == b.Size && + a.Mtimespec.Sec == b.Mtimespec.Sec && a.Mtimespec.Nsec == b.Mtimespec.Nsec && + a.Ctimespec.Sec == b.Ctimespec.Sec && a.Ctimespec.Nsec == b.Ctimespec.Nsec +} diff --git a/go/internal/ocxcli/doctor_owner_unix.go b/go/internal/ocxcli/doctor_owner_unix.go index db6163ff26..826046c3ac 100644 --- a/go/internal/ocxcli/doctor_owner_unix.go +++ b/go/internal/ocxcli/doctor_owner_unix.go @@ -1,4 +1,4 @@ -//go:build !windows +//go:build !windows && !darwin package ocxcli diff --git a/go/internal/ocxcli/runtime_server.go b/go/internal/ocxcli/runtime_server.go new file mode 100644 index 0000000000..c6685a7b74 --- /dev/null +++ b/go/internal/ocxcli/runtime_server.go @@ -0,0 +1,352 @@ +package ocxcli + +import ( + "context" + "crypto/rand" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "io" + "net" + "net/http" + "os" + "os/signal" + "path/filepath" + "strconv" + "strings" + "syscall" + "time" + + "github.com/lidge-jun/opencodex/go/internal/config" + "github.com/lidge-jun/opencodex/go/internal/embeddedui" + "github.com/lidge-jun/opencodex/go/internal/managementauth" + "github.com/lidge-jun/opencodex/go/internal/sidecar" +) + +// processInspector is deliberately narrow so reclaim decisions can be tested +// without giving tests permission to signal arbitrary processes. +type processInspector interface { + Alive(pid int) bool + Command(pid int) (string, error) + Terminate(pid int, signal string) error +} + +type osProcessInspector struct{} + +func (osProcessInspector) Alive(pid int) bool { return doctorProcessAlive(pid) } +func (osProcessInspector) Command(pid int) (string, error) { + if runtimeGOOS() == "linux" { + raw, err := os.ReadFile(fmt.Sprintf("/proc/%d/cmdline", pid)) + return strings.ReplaceAll(string(raw), "\x00", " "), err + } + return "", errors.New("process command inspection is unavailable on this platform") +} +func (osProcessInspector) Terminate(pid int, signal string) error { + if signal != "TERM" { + return errors.New("unsupported signal") + } + process, err := os.FindProcess(pid) + if err != nil { + return err + } + return process.Signal(syscall.SIGTERM) +} + +var runtimeGOOS = func() string { return runtimeGOOSValue } +var runtimeGOOSValue = "linux" + +type portReclaimer struct { + home string + process processInspector +} + +func (r portReclaimer) reclaim(port int) error { + if port < 1 { + return nil + } + pid := readRecordedPID(filepath.Join(r.home, "ocx.pid")) + if pid == 0 { + if listenerAvailable(port) { + return nil + } + return fmt.Errorf("port %d is occupied by a process that is not the recorded OpenCodex runtime", port) + } + if !r.process.Alive(pid) { + return removeRuntimeRecords(r.home) + } + command, err := r.process.Command(pid) + if err != nil || !isKnownTypeScriptRuntime(command) { + return fmt.Errorf("port %d is occupied by a process that is not a reclaimable TypeScript OpenCodex runtime", port) + } + if err := r.process.Terminate(pid, "TERM"); err != nil { + return fmt.Errorf("stop stale TypeScript OpenCodex runtime %d: %w", pid, err) + } + deadline := time.Now().Add(5 * time.Second) + for r.process.Alive(pid) && time.Now().Before(deadline) { + time.Sleep(25 * time.Millisecond) + } + if r.process.Alive(pid) { + return fmt.Errorf("TypeScript OpenCodex runtime %d did not exit; refusing to steal port %d", pid, port) + } + if !listenerAvailable(port) { + return fmt.Errorf("port %d remains occupied after TypeScript OpenCodex runtime stopped", port) + } + return removeRuntimeRecords(r.home) +} + +func listenerAvailable(port int) bool { + l, err := net.Listen("tcp", net.JoinHostPort("127.0.0.1", strconv.Itoa(port))) + if err != nil { + return false + } + _ = l.Close() + return true +} +func readRecordedPID(path string) int { + raw, err := os.ReadFile(path) + if err != nil { + return 0 + } + value, err := strconv.Atoi(strings.TrimSpace(string(raw))) + if err != nil || value < 1 { + return 0 + } + return value +} +func removeRuntimeRecords(home string) error { + for _, name := range []string{"ocx.pid", "runtime-port.json"} { + if err := os.Remove(filepath.Join(home, name)); err != nil && !errors.Is(err, os.ErrNotExist) { + return err + } + } + return nil +} +func isKnownTypeScriptRuntime(command string) bool { + normalized := strings.ToLower(strings.ReplaceAll(command, "\\", "/")) + return strings.Contains(normalized, "src/cli/index.ts") || strings.Contains(normalized, "src/cli.ts") || (strings.Contains(normalized, "opencodex") && strings.Contains(normalized, " start")) +} + +type standaloneServer struct { + listener net.Listener + http *http.Server + handler http.Handler + home string + pid int + port int + secret string + version string + started time.Time +} + +func newStandaloneServer(listen, version string) (*standaloneServer, error) { + listener, err := net.Listen("tcp", listen) + if err != nil { + return nil, err + } + home, err := config.Dir() + if err != nil { + _ = listener.Close() + return nil, err + } + if err := os.MkdirAll(home, 0o700); err != nil { + _ = listener.Close() + return nil, err + } + port := listener.Addr().(*net.TCPAddr).Port + secretRaw := make([]byte, 32) + if _, err := rand.Read(secretRaw); err != nil { + _ = listener.Close() + return nil, err + } + server := &standaloneServer{listener: listener, home: home, pid: os.Getpid(), port: port, secret: base64.RawURLEncoding.EncodeToString(secretRaw), version: version, started: time.Now()} + server.handler = server.routes() + server.http = &http.Server{Handler: server.handler, ReadHeaderTimeout: 5 * time.Second, IdleTimeout: 30 * time.Second} + if err := server.writeRuntime(); err != nil { + _ = listener.Close() + return nil, err + } + return server, nil +} + +func (s *standaloneServer) routes() http.Handler { + dashboard := embeddedui.NewHandler(s.version) + goOwned := sidecar.NewHandler(sidecar.Config{Service: "opencodex", Version: s.version, StartedAt: s.started, ConfigDir: s.home}) + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/healthz": + w.Header().Set("Content-Type", "application/json") + if challenge := r.Header.Get(attestationChallengeHeader); challenge != "" { + w.Header().Set(attestationProofHeader, managementauth.CreateLocalAttestationProof(s.secret, challenge, int64(s.pid), s.port)) + } + _ = json.NewEncoder(w).Encode(map[string]any{"status": "ok", "service": "opencodex", "version": s.version, "uptime": time.Since(s.started).Seconds(), "pid": s.pid, "port": s.port}) + case "/readyz": + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(map[string]any{"service": "opencodex", "version": s.version, "uptime": time.Since(s.started).Seconds(), "pid": s.pid, "port": s.port, "status": "ready"}) + case "/api/stop": + if r.Method != http.MethodPost { + http.NotFound(w, r) + return + } + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(map[string]any{"ok": true}) + go func() { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + _ = s.Close(ctx) + }() + default: + if strings.HasPrefix(r.URL.Path, "/api/") || r.URL.Path == "/v1/responses" { + goOwned.ServeHTTP(w, r) + return + } + dashboard.ServeHTTP(w, r) + } + }) +} +func (s *standaloneServer) writeRuntime() error { + if err := os.WriteFile(filepath.Join(s.home, "ocx.pid"), []byte(strconv.Itoa(s.pid)+"\n"), 0o600); err != nil { + return err + } + raw, err := json.MarshalIndent(RuntimeState{PID: int64(s.pid), Port: s.port, Hostname: "127.0.0.1", AttestationSecret: s.secret}, "", " ") + if err != nil { + return err + } + return os.WriteFile(filepath.Join(s.home, "runtime-port.json"), append(raw, '\n'), 0o600) +} +func (s *standaloneServer) Serve() error { return s.http.Serve(s.listener) } +func (s *standaloneServer) Close(ctx context.Context) error { + err := s.http.Shutdown(ctx) + _ = removeRuntimeRecords(s.home) + return err +} + +func runStart(args []string, deps Deps) int { + portOverride := 0 + for i := 0; i < len(args); i++ { + if args[i] == "--port" && i+1 < len(args) { + parsed, err := strconv.Atoi(args[i+1]) + if err != nil || parsed < 1 || parsed > 65535 { + fmt.Fprintln(deps.Stderr, "Usage: ocx start [--port ]") + return ExitUsage + } + portOverride = parsed + i++ + continue + } + fmt.Fprintln(deps.Stderr, "Usage: ocx start [--port ]") + return ExitUsage + } + cfg, err := config.Load() + if err != nil { + fmt.Fprintln(deps.Stderr, err) + return ExitFailure + } + port, host := cfg.ListenTarget() + if portOverride > 0 { + port = portOverride + } + if host == "" { + host = "127.0.0.1" + } + home, err := config.Dir() + if err != nil { + fmt.Fprintln(deps.Stderr, err) + return ExitFailure + } + if err := (portReclaimer{home: home, process: osProcessInspector{}}).reclaim(port); err != nil { + fmt.Fprintln(deps.Stderr, err) + return ExitFailure + } + server, err := newStandaloneServer(net.JoinHostPort(host, strconv.Itoa(port)), deps.Version) + if err != nil { + fmt.Fprintln(deps.Stderr, err) + return ExitFailure + } + fmt.Fprintf(deps.Stdout, "OpenCodex listening on http://%s\n", server.listener.Addr()) + return serveUntilSignal(server, deps.Stderr) +} +func serveUntilSignal(server *standaloneServer, stderr io.Writer) int { + done := make(chan error, 1) + go func() { done <- server.Serve() }() + signals := make(chan os.Signal, 1) + signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM) + select { + case <-signals: + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + if err := server.Close(ctx); err != nil { + fmt.Fprintln(stderr, err) + return ExitFailure + } + return ExitOK + case err := <-done: + if err != nil && !errors.Is(err, http.ErrServerClosed) { + fmt.Fprintln(stderr, err) + return ExitFailure + } + return ExitOK + } +} + +// runStop implements the Go-owned stop ladder for the standalone runtime: +// graceful drain through POST /api/stop when the listener answers, then +// SIGTERM with a bounded wait, then a hard refusal rather than SIGKILL-by- +// default: killing an unknown owner is the same hazard port reclaim guards +// against. Runtime records are cleared only after the process is gone. +func runStop(args []string, deps Deps) int { + if len(args) != 0 { + fmt.Fprintln(deps.Stderr, "Usage: ocx stop") + return ExitUsage + } + state, err := deps.ReadRuntime() + if err != nil { + if errors.Is(err, os.ErrNotExist) { + fmt.Fprintln(deps.Stdout, "No proxy is running.") + return ExitOK + } + fmt.Fprintln(deps.Stderr, err) + return ExitFailure + } + home, err := config.Dir() + if err != nil { + fmt.Fprintln(deps.Stderr, err) + return ExitFailure + } + if !(osProcessInspector{}).Alive(int(state.PID)) { + _ = removeRuntimeRecords(home) + fmt.Fprintf(deps.Stdout, "No proxy is running (stale record for PID %d removed).\n", state.PID) + return ExitOK + } + client := &http.Client{Timeout: 10 * time.Second} + stopURL := fmt.Sprintf("http://%s/api/stop", net.JoinHostPort(state.Hostname, strconv.Itoa(state.Port))) + request, requestErr := http.NewRequest(http.MethodPost, stopURL, nil) + graceful := false + if requestErr == nil { + if response, doErr := client.Do(request); doErr == nil { + _ = response.Body.Close() + graceful = response.StatusCode == http.StatusOK + } + } + if !graceful { + process, signalErr := os.FindProcess(int(state.PID)) + if signalErr == nil { + signalErr = process.Signal(syscall.SIGTERM) + } + if err := signalErr; err != nil { + fmt.Fprintf(deps.Stderr, "Failed to stop proxy (PID %d): %v\n", state.PID, err) + return ExitFailure + } + } + deadline := time.Now().Add(8 * time.Second) + for (osProcessInspector{}).Alive(int(state.PID)) && time.Now().Before(deadline) { + time.Sleep(50 * time.Millisecond) + } + if (osProcessInspector{}).Alive(int(state.PID)) { + fmt.Fprintf(deps.Stderr, "Proxy (PID %d) did not exit after stop request.\n", state.PID) + return ExitFailure + } + _ = removeRuntimeRecords(home) + fmt.Fprintf(deps.Stdout, "Proxy (PID %d) stopped.\n", state.PID) + return ExitOK +} diff --git a/go/internal/ocxcli/runtime_server_test.go b/go/internal/ocxcli/runtime_server_test.go new file mode 100644 index 0000000000..054ab19aff --- /dev/null +++ b/go/internal/ocxcli/runtime_server_test.go @@ -0,0 +1,153 @@ +package ocxcli + +import ( + "bytes" + "context" + "errors" + "net" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "strings" + "testing" + "time" +) + +func TestReclaimPortRemovesStaleProcessRecords(t *testing.T) { + dir := t.TempDir() + if err := os.WriteFile(filepath.Join(dir, "ocx.pid"), []byte("999999\n"), 0o600); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(dir, "runtime-port.json"), []byte("{\"pid\":999999,\"port\":10100}"), 0o600); err != nil { + t.Fatal(err) + } + listener, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Skipf("loopback listeners unavailable: %v", err) + } + defer listener.Close() + port := listener.Addr().(*net.TCPAddr).Port + reclaimer := portReclaimer{home: dir, process: &fakeProcess{alive: false}} + if err := reclaimer.reclaim(port); err != nil { + t.Fatalf("reclaim stale state: %v", err) + } + for _, name := range []string{"ocx.pid", "runtime-port.json"} { + if _, err := os.Stat(filepath.Join(dir, name)); !errors.Is(err, os.ErrNotExist) { + t.Fatalf("%s remains: %v", name, err) + } + } +} + +func TestReclaimPortTerminatesKnownTypeScriptOwner(t *testing.T) { + dir := t.TempDir() + if err := os.WriteFile(filepath.Join(dir, "ocx.pid"), []byte("42\n"), 0o600); err != nil { + t.Fatal(err) + } + process := &fakeProcess{alive: true, command: "bun src/cli/index.ts start"} + reclaimer := portReclaimer{home: dir, process: process} + // Hold no listener and use a port outside the recorded state: the reclaimer + // must still terminate the live recorded TS runtime and clear the records. + if err := reclaimer.reclaim(10100); err != nil { + t.Fatalf("reclaim TS owner: %v", err) + } + if !process.terminated || process.signal != "TERM" { + t.Fatalf("process = %#v", process) + } +} + +func TestReclaimPortRefusesForeignListener(t *testing.T) { + listener, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + defer listener.Close() + port := listener.Addr().(*net.TCPAddr).Port + process := &fakeProcess{alive: true, command: "python foreign-server.py"} + reclaimer := portReclaimer{home: t.TempDir(), process: process} + if err := reclaimer.reclaim(port); err == nil || !strings.Contains(err.Error(), "occupied") { + t.Fatalf("reclaim error = %v", err) + } + if process.terminated { + t.Fatal("foreign process was terminated") + } +} + +func TestStandaloneServerOwnsListenerDashboardHealthAndGoRoutes(t *testing.T) { + home := t.TempDir() + t.Setenv("OPENCODEX_HOME", home) + server, err := newStandaloneServer("127.0.0.1:0", "9.9.9") + if err != nil { + t.Fatal(err) + } + defer server.Close(context.Background()) + go server.Serve() + base := "http://" + server.listener.Addr().String() + for _, path := range []string{"/", "/healthz", "/api/custom-models"} { + response, err := http.Get(base + path) + if err != nil { + t.Fatalf("GET %s: %v", path, err) + } + body := new(bytes.Buffer) + _, _ = body.ReadFrom(response.Body) + response.Body.Close() + if response.StatusCode != http.StatusOK || body.Len() == 0 { + t.Fatalf("GET %s = %d %q", path, response.StatusCode, body.String()) + } + } + ready := httptest.NewRecorder() + server.handler.ServeHTTP(ready, httptest.NewRequest(http.MethodGet, "/readyz", nil)) + if ready.Code != http.StatusOK || !strings.Contains(ready.Body.String(), "\"status\":\"ready\"") { + t.Fatalf("ready = %d %s", ready.Code, ready.Body.String()) + } + raw, err := os.ReadFile(filepath.Join(home, "runtime-port.json")) + if err != nil { + t.Fatal(err) + } + if !strings.Contains(string(raw), "\"pid\"") || !strings.Contains(string(raw), "\"attestationSecret\"") { + t.Fatalf("runtime record = %s", raw) + } +} + +func TestStandaloneServerApiStopDrainsAndReleasesRecords(t *testing.T) { + home := t.TempDir() + t.Setenv("OPENCODEX_HOME", home) + server, err := newStandaloneServer("127.0.0.1:0", "9.9.9") + if err != nil { + t.Skipf("loopback listeners unavailable: %v", err) + } + go server.Serve() + base := "http://" + server.listener.Addr().String() + response, err := http.Post(base+"/api/stop", "application/json", nil) + if err != nil { + t.Fatalf("POST /api/stop: %v", err) + } + response.Body.Close() + if response.StatusCode != http.StatusOK { + t.Fatalf("POST /api/stop = %d", response.StatusCode) + } + deadline := time.Now().Add(5 * time.Second) + for _, statErr := os.Stat(filepath.Join(home, "runtime-port.json")); !errors.Is(statErr, os.ErrNotExist) && time.Now().Before(deadline); { + time.Sleep(25 * time.Millisecond) + _, statErr = os.Stat(filepath.Join(home, "runtime-port.json")) + } + if _, statErr := os.Stat(filepath.Join(home, "runtime-port.json")); !errors.Is(statErr, os.ErrNotExist) { + t.Fatalf("runtime record remains: %v", statErr) + } +} + +type fakeProcess struct { + alive bool + command string + terminated bool + signal string +} + +func (p *fakeProcess) Alive(int) bool { return p.alive } +func (p *fakeProcess) Command(int) (string, error) { return p.command, nil } +func (p *fakeProcess) Terminate(_ int, signal string) error { + p.terminated = true + p.signal = signal + p.alive = false + return nil +} From 310f25fa88bc1bf75c665c140c885af49b12ffaf Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Mon, 7 Sep 2026 08:38:21 +0800 Subject: [PATCH 105/165] feat(go): flip start to sole Go listener with port reclaim Go start binds the runtime listener directly with embedded dashboard, /healthz attestation, /readyz, /api/stop drain, and runtime-port records compatible with the TS format. Port reclaim handles stale pid records, verified TS runtimes (TERM + bounded wait), and refuses foreign listeners. stop becomes Go-owned with a graceful-then-SIGTERM ladder. Release workflow builds and attaches static Go artifacts per release tag. Darwin Stat_t build fix unblocks the release matrix. --- .github/workflows/release.yml | 31 ++ go/internal/ocxcli/cli.go | 8 +- go/internal/ocxcli/doctor_owner_darwin.go | 24 ++ go/internal/ocxcli/doctor_owner_unix.go | 2 +- go/internal/ocxcli/runtime_server.go | 352 ++++++++++++++++++++++ go/internal/ocxcli/runtime_server_test.go | 153 ++++++++++ 6 files changed, 567 insertions(+), 3 deletions(-) create mode 100644 go/internal/ocxcli/doctor_owner_darwin.go create mode 100644 go/internal/ocxcli/runtime_server.go create mode 100644 go/internal/ocxcli/runtime_server_test.go diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f053574295..5fa1c37687 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -387,6 +387,25 @@ jobs: npm view @bitkyc08/opencodex versions dist-tags --json || true exit 1 + # ADR-0008 increment 7 (#41): the Go binary is the release runtime. Build the + # static cross-platform artifacts and attach them to the GitHub Release; the + # npm package remains the source distribution channel, but every release tag + # now ships a TypeScript-free single-binary artifact. + - name: Setup Go for single-binary artifacts + uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5 + with: + go-version-file: go/go.mod + cache: false + + - name: Build Go single-binary release artifacts + run: | + set -euo pipefail + mkdir -p .tmp/go-release + for target in linux/amd64 linux/arm64 darwin/amd64 darwin/arm64 windows/amd64; do + scripts/build-go-release-artifact.sh "$target" ".tmp/go-release" + done + ls -la .tmp/go-release + - name: Create GitHub release if: ${{ inputs.dry-run != true }} env: @@ -422,3 +441,15 @@ jobs: gh release create "$release_tag" --target "$GITHUB_SHA" --title "$release_tag" \ --notes-file "$notes_file" ${prerelease_flag:+$prerelease_flag} + + - name: Attach Go artifacts to release + if: ${{ inputs.dry-run != true }} + env: + GH_TOKEN: ${{ github.token }} + RELEASE_VERSION: ${{ inputs.version }} + run: | + set -euo pipefail + release_tag="v${RELEASE_VERSION}" + for artifact in .tmp/go-release/ocx-*; do + gh release upload "$release_tag" "$artifact" --clobber + done diff --git a/go/internal/ocxcli/cli.go b/go/internal/ocxcli/cli.go index e347edd2ae..b631784bef 100644 --- a/go/internal/ocxcli/cli.go +++ b/go/internal/ocxcli/cli.go @@ -49,8 +49,8 @@ type Command struct { var Commands = []Command{ {Name: "setup", Aliases: []string{"init"}, Usage: "ocx setup", Summary: "Interactive setup.", Owner: TypeScriptOwned}, - {Name: "start", Usage: "ocx start [--port ]", Summary: "Start the proxy.", Owner: TypeScriptOwned}, - {Name: "stop", Usage: "ocx stop", Summary: "Stop the proxy.", Owner: TypeScriptOwned}, + {Name: "start", Usage: "ocx start [--port ]", Summary: "Start the proxy.", Owner: GoOwned}, + {Name: "stop", Usage: "ocx stop", Summary: "Stop the proxy.", Owner: GoOwned}, {Name: "restore", Aliases: []string{"eject"}, Usage: "ocx restore [back]", Summary: "Restore native Codex configuration.", Owner: TypeScriptOwned}, {Name: "recover-history", Usage: "ocx recover-history --legacy-openai --yes", Summary: "Recover legacy history.", Owner: TypeScriptOwned}, {Name: "uninstall", Aliases: []string{"remove"}, Usage: "ocx uninstall", Summary: "Remove OpenCodex integration.", Owner: TypeScriptOwned}, @@ -263,6 +263,10 @@ func Run(args []string, deps Deps) int { return runStatus(args[1:], deps) case "doctor": return RunDoctorCommand(args[1:], deps.Stdout, deps.Stderr, DoctorCommandDeps{}) + case "start": + return runStart(args[1:], deps) + case "stop": + return runStop(args[1:], deps) default: // The ownership registry above and this switch must be reconciled by // TestOwnershipMapMatchesDispatch; this is defensive for future edits. diff --git a/go/internal/ocxcli/doctor_owner_darwin.go b/go/internal/ocxcli/doctor_owner_darwin.go new file mode 100644 index 0000000000..66a65d1b2b --- /dev/null +++ b/go/internal/ocxcli/doctor_owner_darwin.go @@ -0,0 +1,24 @@ +//go:build darwin + +package ocxcli + +import ( + "os" + "syscall" +) + +func doctorOwnedByCurrentUser(info os.FileInfo) bool { + stat, ok := info.Sys().(*syscall.Stat_t) + return ok && int(stat.Uid) == os.Getuid() +} + +// doctorSameFullFileIdentity matches the POSIX dev/inode/size/mtime/ctime +// evidence TypeScript validates immediately before recovery rename. Darwin's +// syscall.Stat_t spells the timestamp fields *spec rather than the Linux *t. +func doctorSameFullFileIdentity(left, right os.FileInfo) bool { + a, aok := left.Sys().(*syscall.Stat_t) + b, bok := right.Sys().(*syscall.Stat_t) + return aok && bok && a.Dev == b.Dev && a.Ino == b.Ino && a.Size == b.Size && + a.Mtimespec.Sec == b.Mtimespec.Sec && a.Mtimespec.Nsec == b.Mtimespec.Nsec && + a.Ctimespec.Sec == b.Ctimespec.Sec && a.Ctimespec.Nsec == b.Ctimespec.Nsec +} diff --git a/go/internal/ocxcli/doctor_owner_unix.go b/go/internal/ocxcli/doctor_owner_unix.go index db6163ff26..826046c3ac 100644 --- a/go/internal/ocxcli/doctor_owner_unix.go +++ b/go/internal/ocxcli/doctor_owner_unix.go @@ -1,4 +1,4 @@ -//go:build !windows +//go:build !windows && !darwin package ocxcli diff --git a/go/internal/ocxcli/runtime_server.go b/go/internal/ocxcli/runtime_server.go new file mode 100644 index 0000000000..c6685a7b74 --- /dev/null +++ b/go/internal/ocxcli/runtime_server.go @@ -0,0 +1,352 @@ +package ocxcli + +import ( + "context" + "crypto/rand" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "io" + "net" + "net/http" + "os" + "os/signal" + "path/filepath" + "strconv" + "strings" + "syscall" + "time" + + "github.com/lidge-jun/opencodex/go/internal/config" + "github.com/lidge-jun/opencodex/go/internal/embeddedui" + "github.com/lidge-jun/opencodex/go/internal/managementauth" + "github.com/lidge-jun/opencodex/go/internal/sidecar" +) + +// processInspector is deliberately narrow so reclaim decisions can be tested +// without giving tests permission to signal arbitrary processes. +type processInspector interface { + Alive(pid int) bool + Command(pid int) (string, error) + Terminate(pid int, signal string) error +} + +type osProcessInspector struct{} + +func (osProcessInspector) Alive(pid int) bool { return doctorProcessAlive(pid) } +func (osProcessInspector) Command(pid int) (string, error) { + if runtimeGOOS() == "linux" { + raw, err := os.ReadFile(fmt.Sprintf("/proc/%d/cmdline", pid)) + return strings.ReplaceAll(string(raw), "\x00", " "), err + } + return "", errors.New("process command inspection is unavailable on this platform") +} +func (osProcessInspector) Terminate(pid int, signal string) error { + if signal != "TERM" { + return errors.New("unsupported signal") + } + process, err := os.FindProcess(pid) + if err != nil { + return err + } + return process.Signal(syscall.SIGTERM) +} + +var runtimeGOOS = func() string { return runtimeGOOSValue } +var runtimeGOOSValue = "linux" + +type portReclaimer struct { + home string + process processInspector +} + +func (r portReclaimer) reclaim(port int) error { + if port < 1 { + return nil + } + pid := readRecordedPID(filepath.Join(r.home, "ocx.pid")) + if pid == 0 { + if listenerAvailable(port) { + return nil + } + return fmt.Errorf("port %d is occupied by a process that is not the recorded OpenCodex runtime", port) + } + if !r.process.Alive(pid) { + return removeRuntimeRecords(r.home) + } + command, err := r.process.Command(pid) + if err != nil || !isKnownTypeScriptRuntime(command) { + return fmt.Errorf("port %d is occupied by a process that is not a reclaimable TypeScript OpenCodex runtime", port) + } + if err := r.process.Terminate(pid, "TERM"); err != nil { + return fmt.Errorf("stop stale TypeScript OpenCodex runtime %d: %w", pid, err) + } + deadline := time.Now().Add(5 * time.Second) + for r.process.Alive(pid) && time.Now().Before(deadline) { + time.Sleep(25 * time.Millisecond) + } + if r.process.Alive(pid) { + return fmt.Errorf("TypeScript OpenCodex runtime %d did not exit; refusing to steal port %d", pid, port) + } + if !listenerAvailable(port) { + return fmt.Errorf("port %d remains occupied after TypeScript OpenCodex runtime stopped", port) + } + return removeRuntimeRecords(r.home) +} + +func listenerAvailable(port int) bool { + l, err := net.Listen("tcp", net.JoinHostPort("127.0.0.1", strconv.Itoa(port))) + if err != nil { + return false + } + _ = l.Close() + return true +} +func readRecordedPID(path string) int { + raw, err := os.ReadFile(path) + if err != nil { + return 0 + } + value, err := strconv.Atoi(strings.TrimSpace(string(raw))) + if err != nil || value < 1 { + return 0 + } + return value +} +func removeRuntimeRecords(home string) error { + for _, name := range []string{"ocx.pid", "runtime-port.json"} { + if err := os.Remove(filepath.Join(home, name)); err != nil && !errors.Is(err, os.ErrNotExist) { + return err + } + } + return nil +} +func isKnownTypeScriptRuntime(command string) bool { + normalized := strings.ToLower(strings.ReplaceAll(command, "\\", "/")) + return strings.Contains(normalized, "src/cli/index.ts") || strings.Contains(normalized, "src/cli.ts") || (strings.Contains(normalized, "opencodex") && strings.Contains(normalized, " start")) +} + +type standaloneServer struct { + listener net.Listener + http *http.Server + handler http.Handler + home string + pid int + port int + secret string + version string + started time.Time +} + +func newStandaloneServer(listen, version string) (*standaloneServer, error) { + listener, err := net.Listen("tcp", listen) + if err != nil { + return nil, err + } + home, err := config.Dir() + if err != nil { + _ = listener.Close() + return nil, err + } + if err := os.MkdirAll(home, 0o700); err != nil { + _ = listener.Close() + return nil, err + } + port := listener.Addr().(*net.TCPAddr).Port + secretRaw := make([]byte, 32) + if _, err := rand.Read(secretRaw); err != nil { + _ = listener.Close() + return nil, err + } + server := &standaloneServer{listener: listener, home: home, pid: os.Getpid(), port: port, secret: base64.RawURLEncoding.EncodeToString(secretRaw), version: version, started: time.Now()} + server.handler = server.routes() + server.http = &http.Server{Handler: server.handler, ReadHeaderTimeout: 5 * time.Second, IdleTimeout: 30 * time.Second} + if err := server.writeRuntime(); err != nil { + _ = listener.Close() + return nil, err + } + return server, nil +} + +func (s *standaloneServer) routes() http.Handler { + dashboard := embeddedui.NewHandler(s.version) + goOwned := sidecar.NewHandler(sidecar.Config{Service: "opencodex", Version: s.version, StartedAt: s.started, ConfigDir: s.home}) + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/healthz": + w.Header().Set("Content-Type", "application/json") + if challenge := r.Header.Get(attestationChallengeHeader); challenge != "" { + w.Header().Set(attestationProofHeader, managementauth.CreateLocalAttestationProof(s.secret, challenge, int64(s.pid), s.port)) + } + _ = json.NewEncoder(w).Encode(map[string]any{"status": "ok", "service": "opencodex", "version": s.version, "uptime": time.Since(s.started).Seconds(), "pid": s.pid, "port": s.port}) + case "/readyz": + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(map[string]any{"service": "opencodex", "version": s.version, "uptime": time.Since(s.started).Seconds(), "pid": s.pid, "port": s.port, "status": "ready"}) + case "/api/stop": + if r.Method != http.MethodPost { + http.NotFound(w, r) + return + } + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(map[string]any{"ok": true}) + go func() { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + _ = s.Close(ctx) + }() + default: + if strings.HasPrefix(r.URL.Path, "/api/") || r.URL.Path == "/v1/responses" { + goOwned.ServeHTTP(w, r) + return + } + dashboard.ServeHTTP(w, r) + } + }) +} +func (s *standaloneServer) writeRuntime() error { + if err := os.WriteFile(filepath.Join(s.home, "ocx.pid"), []byte(strconv.Itoa(s.pid)+"\n"), 0o600); err != nil { + return err + } + raw, err := json.MarshalIndent(RuntimeState{PID: int64(s.pid), Port: s.port, Hostname: "127.0.0.1", AttestationSecret: s.secret}, "", " ") + if err != nil { + return err + } + return os.WriteFile(filepath.Join(s.home, "runtime-port.json"), append(raw, '\n'), 0o600) +} +func (s *standaloneServer) Serve() error { return s.http.Serve(s.listener) } +func (s *standaloneServer) Close(ctx context.Context) error { + err := s.http.Shutdown(ctx) + _ = removeRuntimeRecords(s.home) + return err +} + +func runStart(args []string, deps Deps) int { + portOverride := 0 + for i := 0; i < len(args); i++ { + if args[i] == "--port" && i+1 < len(args) { + parsed, err := strconv.Atoi(args[i+1]) + if err != nil || parsed < 1 || parsed > 65535 { + fmt.Fprintln(deps.Stderr, "Usage: ocx start [--port ]") + return ExitUsage + } + portOverride = parsed + i++ + continue + } + fmt.Fprintln(deps.Stderr, "Usage: ocx start [--port ]") + return ExitUsage + } + cfg, err := config.Load() + if err != nil { + fmt.Fprintln(deps.Stderr, err) + return ExitFailure + } + port, host := cfg.ListenTarget() + if portOverride > 0 { + port = portOverride + } + if host == "" { + host = "127.0.0.1" + } + home, err := config.Dir() + if err != nil { + fmt.Fprintln(deps.Stderr, err) + return ExitFailure + } + if err := (portReclaimer{home: home, process: osProcessInspector{}}).reclaim(port); err != nil { + fmt.Fprintln(deps.Stderr, err) + return ExitFailure + } + server, err := newStandaloneServer(net.JoinHostPort(host, strconv.Itoa(port)), deps.Version) + if err != nil { + fmt.Fprintln(deps.Stderr, err) + return ExitFailure + } + fmt.Fprintf(deps.Stdout, "OpenCodex listening on http://%s\n", server.listener.Addr()) + return serveUntilSignal(server, deps.Stderr) +} +func serveUntilSignal(server *standaloneServer, stderr io.Writer) int { + done := make(chan error, 1) + go func() { done <- server.Serve() }() + signals := make(chan os.Signal, 1) + signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM) + select { + case <-signals: + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + if err := server.Close(ctx); err != nil { + fmt.Fprintln(stderr, err) + return ExitFailure + } + return ExitOK + case err := <-done: + if err != nil && !errors.Is(err, http.ErrServerClosed) { + fmt.Fprintln(stderr, err) + return ExitFailure + } + return ExitOK + } +} + +// runStop implements the Go-owned stop ladder for the standalone runtime: +// graceful drain through POST /api/stop when the listener answers, then +// SIGTERM with a bounded wait, then a hard refusal rather than SIGKILL-by- +// default: killing an unknown owner is the same hazard port reclaim guards +// against. Runtime records are cleared only after the process is gone. +func runStop(args []string, deps Deps) int { + if len(args) != 0 { + fmt.Fprintln(deps.Stderr, "Usage: ocx stop") + return ExitUsage + } + state, err := deps.ReadRuntime() + if err != nil { + if errors.Is(err, os.ErrNotExist) { + fmt.Fprintln(deps.Stdout, "No proxy is running.") + return ExitOK + } + fmt.Fprintln(deps.Stderr, err) + return ExitFailure + } + home, err := config.Dir() + if err != nil { + fmt.Fprintln(deps.Stderr, err) + return ExitFailure + } + if !(osProcessInspector{}).Alive(int(state.PID)) { + _ = removeRuntimeRecords(home) + fmt.Fprintf(deps.Stdout, "No proxy is running (stale record for PID %d removed).\n", state.PID) + return ExitOK + } + client := &http.Client{Timeout: 10 * time.Second} + stopURL := fmt.Sprintf("http://%s/api/stop", net.JoinHostPort(state.Hostname, strconv.Itoa(state.Port))) + request, requestErr := http.NewRequest(http.MethodPost, stopURL, nil) + graceful := false + if requestErr == nil { + if response, doErr := client.Do(request); doErr == nil { + _ = response.Body.Close() + graceful = response.StatusCode == http.StatusOK + } + } + if !graceful { + process, signalErr := os.FindProcess(int(state.PID)) + if signalErr == nil { + signalErr = process.Signal(syscall.SIGTERM) + } + if err := signalErr; err != nil { + fmt.Fprintf(deps.Stderr, "Failed to stop proxy (PID %d): %v\n", state.PID, err) + return ExitFailure + } + } + deadline := time.Now().Add(8 * time.Second) + for (osProcessInspector{}).Alive(int(state.PID)) && time.Now().Before(deadline) { + time.Sleep(50 * time.Millisecond) + } + if (osProcessInspector{}).Alive(int(state.PID)) { + fmt.Fprintf(deps.Stderr, "Proxy (PID %d) did not exit after stop request.\n", state.PID) + return ExitFailure + } + _ = removeRuntimeRecords(home) + fmt.Fprintf(deps.Stdout, "Proxy (PID %d) stopped.\n", state.PID) + return ExitOK +} diff --git a/go/internal/ocxcli/runtime_server_test.go b/go/internal/ocxcli/runtime_server_test.go new file mode 100644 index 0000000000..054ab19aff --- /dev/null +++ b/go/internal/ocxcli/runtime_server_test.go @@ -0,0 +1,153 @@ +package ocxcli + +import ( + "bytes" + "context" + "errors" + "net" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "strings" + "testing" + "time" +) + +func TestReclaimPortRemovesStaleProcessRecords(t *testing.T) { + dir := t.TempDir() + if err := os.WriteFile(filepath.Join(dir, "ocx.pid"), []byte("999999\n"), 0o600); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(dir, "runtime-port.json"), []byte("{\"pid\":999999,\"port\":10100}"), 0o600); err != nil { + t.Fatal(err) + } + listener, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Skipf("loopback listeners unavailable: %v", err) + } + defer listener.Close() + port := listener.Addr().(*net.TCPAddr).Port + reclaimer := portReclaimer{home: dir, process: &fakeProcess{alive: false}} + if err := reclaimer.reclaim(port); err != nil { + t.Fatalf("reclaim stale state: %v", err) + } + for _, name := range []string{"ocx.pid", "runtime-port.json"} { + if _, err := os.Stat(filepath.Join(dir, name)); !errors.Is(err, os.ErrNotExist) { + t.Fatalf("%s remains: %v", name, err) + } + } +} + +func TestReclaimPortTerminatesKnownTypeScriptOwner(t *testing.T) { + dir := t.TempDir() + if err := os.WriteFile(filepath.Join(dir, "ocx.pid"), []byte("42\n"), 0o600); err != nil { + t.Fatal(err) + } + process := &fakeProcess{alive: true, command: "bun src/cli/index.ts start"} + reclaimer := portReclaimer{home: dir, process: process} + // Hold no listener and use a port outside the recorded state: the reclaimer + // must still terminate the live recorded TS runtime and clear the records. + if err := reclaimer.reclaim(10100); err != nil { + t.Fatalf("reclaim TS owner: %v", err) + } + if !process.terminated || process.signal != "TERM" { + t.Fatalf("process = %#v", process) + } +} + +func TestReclaimPortRefusesForeignListener(t *testing.T) { + listener, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + defer listener.Close() + port := listener.Addr().(*net.TCPAddr).Port + process := &fakeProcess{alive: true, command: "python foreign-server.py"} + reclaimer := portReclaimer{home: t.TempDir(), process: process} + if err := reclaimer.reclaim(port); err == nil || !strings.Contains(err.Error(), "occupied") { + t.Fatalf("reclaim error = %v", err) + } + if process.terminated { + t.Fatal("foreign process was terminated") + } +} + +func TestStandaloneServerOwnsListenerDashboardHealthAndGoRoutes(t *testing.T) { + home := t.TempDir() + t.Setenv("OPENCODEX_HOME", home) + server, err := newStandaloneServer("127.0.0.1:0", "9.9.9") + if err != nil { + t.Fatal(err) + } + defer server.Close(context.Background()) + go server.Serve() + base := "http://" + server.listener.Addr().String() + for _, path := range []string{"/", "/healthz", "/api/custom-models"} { + response, err := http.Get(base + path) + if err != nil { + t.Fatalf("GET %s: %v", path, err) + } + body := new(bytes.Buffer) + _, _ = body.ReadFrom(response.Body) + response.Body.Close() + if response.StatusCode != http.StatusOK || body.Len() == 0 { + t.Fatalf("GET %s = %d %q", path, response.StatusCode, body.String()) + } + } + ready := httptest.NewRecorder() + server.handler.ServeHTTP(ready, httptest.NewRequest(http.MethodGet, "/readyz", nil)) + if ready.Code != http.StatusOK || !strings.Contains(ready.Body.String(), "\"status\":\"ready\"") { + t.Fatalf("ready = %d %s", ready.Code, ready.Body.String()) + } + raw, err := os.ReadFile(filepath.Join(home, "runtime-port.json")) + if err != nil { + t.Fatal(err) + } + if !strings.Contains(string(raw), "\"pid\"") || !strings.Contains(string(raw), "\"attestationSecret\"") { + t.Fatalf("runtime record = %s", raw) + } +} + +func TestStandaloneServerApiStopDrainsAndReleasesRecords(t *testing.T) { + home := t.TempDir() + t.Setenv("OPENCODEX_HOME", home) + server, err := newStandaloneServer("127.0.0.1:0", "9.9.9") + if err != nil { + t.Skipf("loopback listeners unavailable: %v", err) + } + go server.Serve() + base := "http://" + server.listener.Addr().String() + response, err := http.Post(base+"/api/stop", "application/json", nil) + if err != nil { + t.Fatalf("POST /api/stop: %v", err) + } + response.Body.Close() + if response.StatusCode != http.StatusOK { + t.Fatalf("POST /api/stop = %d", response.StatusCode) + } + deadline := time.Now().Add(5 * time.Second) + for _, statErr := os.Stat(filepath.Join(home, "runtime-port.json")); !errors.Is(statErr, os.ErrNotExist) && time.Now().Before(deadline); { + time.Sleep(25 * time.Millisecond) + _, statErr = os.Stat(filepath.Join(home, "runtime-port.json")) + } + if _, statErr := os.Stat(filepath.Join(home, "runtime-port.json")); !errors.Is(statErr, os.ErrNotExist) { + t.Fatalf("runtime record remains: %v", statErr) + } +} + +type fakeProcess struct { + alive bool + command string + terminated bool + signal string +} + +func (p *fakeProcess) Alive(int) bool { return p.alive } +func (p *fakeProcess) Command(int) (string, error) { return p.command, nil } +func (p *fakeProcess) Terminate(_ int, signal string) error { + p.terminated = true + p.signal = signal + p.alive = false + return nil +} From 39aceb3bfa848ce63b653ce5b0417b57df3a2fd0 Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Mon, 7 Sep 2026 10:40:38 +0800 Subject: [PATCH 106/165] feat(go): add abort-safe SSE inspection lifecycle Co-Authored-By: Claude Code --- go/internal/sidecar/hotpath_relay.go | 230 ++++++++++++++++++++-- go/internal/sidecar/hotpath_relay_test.go | 106 ++++++++++ go/internal/sidecar/sse_stream.go | 47 ++++- 3 files changed, 363 insertions(+), 20 deletions(-) diff --git a/go/internal/sidecar/hotpath_relay.go b/go/internal/sidecar/hotpath_relay.go index fb6b67e6f1..7a5515fd15 100644 --- a/go/internal/sidecar/hotpath_relay.go +++ b/go/internal/sidecar/hotpath_relay.go @@ -28,6 +28,7 @@ package sidecar import ( "bytes" + "context" "fmt" "io" "net" @@ -37,6 +38,7 @@ import ( "path/filepath" "strconv" "strings" + "time" "github.com/lidge-jun/opencodex/go/internal/config" "github.com/lidge-jun/opencodex/go/internal/jsonwire" @@ -621,7 +623,7 @@ func doDirectRelay(w http.ResponseWriter, r *http.Request, cfg Config, plan *rel if requestRoot != nil { pipeline.imageAliases = imageAliasesFromRequest(requestRoot) } - if err := relayResponsesSSEWithFlush(w, upstreamResp.Body, pipeline); err != nil { + if err := relayResponsesSSEWithFlush(w, upstreamResp.Body, pipeline, ResponsesSSERelayOptions{Context: r.Context()}); err != nil { fmt.Fprintf(os.Stderr, "ocx-sidecar: relay stream write: %v\n", err) } } else if err := streamCopyWithFlush(w, upstreamResp.Body); err != nil { @@ -668,18 +670,206 @@ func doDirectRelay(w http.ResponseWriter, r *http.Request, cfg Config, plan *rel } } +// ResponsesSSETerminal is the outcome observed by the relay's independent raw +// inspection branch. HTTPStatus is non-zero only for a synthetic transport +// failure (502); Synthetic distinguishes adapter EOF/read-reset outcomes from +// an upstream Responses terminal. +type ResponsesSSETerminal struct { + Status ResponsesSSETerminalStatus + HTTPStatus int + Synthetic bool +} + +// ResponsesSSERelayOptions controls lifecycle reporting without changing the +// client-facing byte relay. The raw inspection callback runs before the relay +// checks cancellation for a delivered chunk, so a terminal in the same read +// wins over a client disconnect. OnDone is called exactly once. +type ResponsesSSERelayOptions struct { + Context context.Context + OnTerminal func(ResponsesSSETerminal) + OnCancel func() + OnDone func() + DrainTimeout time.Duration + DrainByteLimit int64 +} + +const ( + defaultResponsesSSEDrainTimeout = 15 * time.Second + defaultResponsesSSEDrainBytes = 32 * 1024 * 1024 +) + +// relayLifecycle owns callback finality for one stream. Keeping it separate +// from ResponsesSSEStream lets the raw inspector and client repair branch +// share terminal state without either branch mutating the other's payload. +type relayLifecycle struct { + options ResponsesSSERelayOptions + terminal bool + cancelled bool + done bool +} + +func (l *relayLifecycle) terminalOutcome(outcome ResponsesSSETerminal) { + if l.terminal || l.cancelled { + return + } + l.terminal = true + if l.options.OnTerminal != nil { + l.options.OnTerminal(outcome) + } +} + +func (l *relayLifecycle) cancel() { + if l.terminal || l.cancelled { + return + } + l.cancelled = true + if l.options.OnCancel != nil { + l.options.OnCancel() + } +} + +func (l *relayLifecycle) finish() { + if l.done { + return + } + l.done = true + if l.options.OnDone != nil { + l.options.OnDone() + } +} + +func responsesSSEContextDone(ctx context.Context) bool { + return ctx != nil && ctx.Err() != nil +} + +func responsesSSEDrainLimits(options ResponsesSSERelayOptions) (time.Duration, int64) { + timeout := options.DrainTimeout + if timeout <= 0 { + timeout = defaultResponsesSSEDrainTimeout + } + limit := options.DrainByteLimit + if limit <= 0 { + limit = defaultResponsesSSEDrainBytes + } + return timeout, limit +} + +// drainResponsesSSEAfterCancel keeps consuming only the raw inspection branch +// for a bounded window. A terminal observed during the drain wins over client +// cancellation; otherwise the cancellation callback fires once and no +// synthetic failed outcome is produced. +func drainResponsesSSEAfterCancel(src io.Reader, inspector *ResponsesSSEStream, lifecycle *relayLifecycle, options ResponsesSSERelayOptions) { + if lifecycle.terminal { + return + } + timeout, byteLimit := responsesSSEDrainLimits(options) + deadline := time.Now().Add(timeout) + buf := make([]byte, 32*1024) + var drained int64 + for drained < byteLimit && time.Now().Before(deadline) { + remaining := byteLimit - drained + readBuffer := buf + if int64(len(readBuffer)) > remaining { + readBuffer = readBuffer[:remaining] + } + n, err := src.Read(readBuffer) + if n > 0 { + drained += int64(n) + observeResponsesSSEChunk(inspector, lifecycle, readBuffer[:n]) + if lifecycle.terminal { + return + } + } + if err != nil { + break + } + } + lifecycle.cancel() +} + +// observeResponsesSSEChunk feeds raw upstream bytes to the inspection branch. +// Inspection errors are isolated from client delivery: an oversized or +// malformed inspection frame must not corrupt the bytes the client receives. +func observeResponsesSSEChunk(inspector *ResponsesSSEStream, lifecycle *relayLifecycle, chunk []byte) { + if lifecycle.terminal || len(chunk) == 0 { + return + } + _, _ = inspector.Feed(chunk) + if inspector.TerminalSeen() { + lifecycle.terminalOutcome(ResponsesSSETerminal{Status: inspector.TerminalStatus()}) + } +} + +func finishResponsesSSEInspection(inspector *ResponsesSSEStream, lifecycle *relayLifecycle, cleanEOF bool, cancelled bool) { + if lifecycle.terminal { + return + } + if cleanEOF { + _, _ = inspector.Finish() + if inspector.TerminalSeen() { + lifecycle.terminalOutcome(ResponsesSSETerminal{Status: inspector.TerminalStatus()}) + return + } + if !cancelled { + lifecycle.terminalOutcome(ResponsesSSETerminal{Status: ResponsesSSEIncomplete, Synthetic: true}) + } + return + } + _, _ = inspector.FinishPartial() + if inspector.TerminalSeen() { + lifecycle.terminalOutcome(ResponsesSSETerminal{Status: inspector.TerminalStatus()}) + return + } + if cancelled { + lifecycle.cancel() + return + } + lifecycle.terminalOutcome(ResponsesSSETerminal{Status: ResponsesSSEFailed, HTTPStatus: http.StatusBadGateway, Synthetic: true}) +} + +// closeOnResponsesSSEContextCancel closes a parked upstream body when the +// client request is cancelled. Closing the body is the Go equivalent of the +// TypeScript reader.cancel wake-up and prevents a leaked read goroutine. +func closeOnResponsesSSEContextCancel(ctx context.Context, src io.Reader) func() { + closer, ok := src.(io.Closer) + if ctx == nil || !ok { + return func() {} + } + stop := make(chan struct{}) + go func() { + select { + case <-ctx.Done(): + _ = closer.Close() + case <-stop: + } + }() + return func() { close(stop) } +} + // relayResponsesSSEWithFlush feeds upstream transport chunks through the // Responses field-backfill and terminal boundary, flushing each emitted block. // It stops reading after the first terminal so a gateway cannot append frames -// after completion and hold the client request open. -func relayResponsesSSEWithFlush(w http.ResponseWriter, src io.Reader, pipeline responseRepairPipeline) error { - stream := NewResponsesSSEStream(pipeline) +// after completion and hold the client request open. Raw chunks are inspected +// before the client-facing repair branch, and lifecycle callbacks are final. +func relayResponsesSSEWithFlush(w http.ResponseWriter, src io.Reader, pipeline responseRepairPipeline, options ...ResponsesSSERelayOptions) error { + var opts ResponsesSSERelayOptions + if len(options) > 0 { + opts = options[0] + } + lifecycle := &relayLifecycle{options: opts} + clientStream := NewResponsesSSEStream(pipeline) + rawInspector := NewResponsesSSEStream() + stopCancelWatcher := closeOnResponsesSSEContextCancel(opts.Context, src) + defer stopCancelWatcher() + defer lifecycle.finish() + flusher, canFlush := w.(http.Flusher) write := func(out []byte) error { if len(out) == 0 { return nil } if _, err := w.Write(out); err != nil { + lifecycle.cancel() return err } if canFlush { @@ -691,47 +881,61 @@ func relayResponsesSSEWithFlush(w http.ResponseWriter, src io.Reader, pipeline r for { n, readErr := src.Read(buf) if n > 0 { - out, err := stream.Feed(buf[:n]) + chunk := append([]byte(nil), buf[:n]...) + // Inspect before checking cancellation: a terminal delivered in the + // same read wins over a client disconnect. + observeResponsesSSEChunk(rawInspector, lifecycle, chunk) + out, err := clientStream.Feed(chunk) if err != nil { return err } if err := write(out); err != nil { return err } - if stream.TerminalSeen() { - tail, err := stream.Finish() + if clientStream.TerminalSeen() { + if !lifecycle.terminal { + lifecycle.terminalOutcome(ResponsesSSETerminal{Status: clientStream.TerminalStatus()}) + } + tail, err := clientStream.Finish() if err != nil { return err } return write(tail) } + if responsesSSEContextDone(opts.Context) { + drainResponsesSSEAfterCancel(src, rawInspector, lifecycle, opts) + return nil + } } if readErr == io.EOF { - out, err := stream.Finish() + out, err := clientStream.Finish() if err != nil { return err } if err := write(out); err != nil { return err } + finishResponsesSSEInspection(rawInspector, lifecycle, true, false) return nil } if readErr != nil { - partial, err := stream.FinishPartial() + partial, err := clientStream.FinishPartial() if err != nil { return err } if err := write(partial); err != nil { return err } - if stream.TerminalSeen() { - if !stream.DoneSeen() { + finishResponsesSSEInspection(rawInspector, lifecycle, false, responsesSSEContextDone(opts.Context)) + if clientStream.TerminalSeen() { + if !clientStream.DoneSeen() { return write([]byte("data: [DONE]\n\n")) } return nil } - // Go read errors have different text from Bun's fetch errors. Keep - // the documented static TS fallback envelope for byte-stable tails. + if lifecycle.cancelled || lifecycle.terminal { + return nil + } return write([]byte("\n\nevent: response.failed\ndata: {\"type\":\"response.failed\",\"response\":{\"status\":\"failed\",\"error\":{\"type\":\"upstream_error\",\"code\":\"upstream_reset\",\"message\":\"Upstream stream terminated unexpectedly\"},\"last_error\":{\"type\":\"upstream_error\",\"code\":\"upstream_reset\",\"message\":\"Upstream stream terminated unexpectedly\"}}}\n\ndata: [DONE]\n\n")) } } diff --git a/go/internal/sidecar/hotpath_relay_test.go b/go/internal/sidecar/hotpath_relay_test.go index 26a4bdfc86..b4300637f7 100644 --- a/go/internal/sidecar/hotpath_relay_test.go +++ b/go/internal/sidecar/hotpath_relay_test.go @@ -2,7 +2,9 @@ package sidecar import ( "bytes" + "context" "encoding/json" + "errors" "io" "net/http" "net/http/httptest" @@ -520,3 +522,107 @@ func TestDirectRelayBodyBound(t *testing.T) { t.Fatalf("body = %s", got) } } + +func TestRelayResponsesSSEPreservesCanonicalRawFrameBytes(t *testing.T) { + input := ": upstream keepalive\r\n\r\nevent: response.completed\r\ndata: {\"type\":\"response.completed\",\"response\":{\"status\":\"completed\"}}\r\n\r\n" + rec := httptest.NewRecorder() + if err := relayResponsesSSEWithFlush(rec, strings.NewReader(input), responseRepairPipeline{}); err != nil { + t.Fatal(err) + } + want := input + "data: [DONE]\n\n" + if got := rec.Body.String(); got != want { + t.Fatalf("raw canonical stream changed\n got: %q\nwant: %q", got, want) + } +} + +func TestRelayResponsesSSEReportsFirstTerminalAndDoneOnce(t *testing.T) { + input := "data: {\"type\":\"response.completed\",\"response\":{\"status\":\"completed\"}}\n\ndata: [DONE]\n\ndata: {\"type\":\"response.failed\"}\n\n" + var got []ResponsesSSETerminal + done := 0 + cancel := 0 + rec := httptest.NewRecorder() + err := relayResponsesSSEWithFlush(rec, strings.NewReader(input), responseRepairPipeline{}, ResponsesSSERelayOptions{ + OnTerminal: func(outcome ResponsesSSETerminal) { got = append(got, outcome) }, + OnCancel: func() { cancel++ }, + OnDone: func() { done++ }, + }) + if err != nil { + t.Fatal(err) + } + if len(got) != 1 || got[0].Status != ResponsesSSECompleted || got[0].Synthetic { + t.Fatalf("terminal outcomes = %#v, want one upstream completed outcome", got) + } + if cancel != 0 || done != 1 { + t.Fatalf("cancel=%d done=%d, want 0 and 1", cancel, done) + } + if !strings.Contains(rec.Body.String(), "data: [DONE]") || strings.Contains(rec.Body.String(), "response.failed") { + t.Fatalf("client body crossed terminal boundary: %q", rec.Body.String()) + } +} + +func TestRelayResponsesSSECleanEOFReportsSyntheticIncomplete(t *testing.T) { + var got []ResponsesSSETerminal + done := 0 + rec := httptest.NewRecorder() + err := relayResponsesSSEWithFlush(rec, strings.NewReader("data: {\"type\":\"response.created\"}\n\n"), responseRepairPipeline{}, ResponsesSSERelayOptions{ + OnTerminal: func(outcome ResponsesSSETerminal) { got = append(got, outcome) }, + OnDone: func() { done++ }, + }) + if err != nil { + t.Fatal(err) + } + if len(got) != 1 || got[0].Status != ResponsesSSEIncomplete || !got[0].Synthetic { + t.Fatalf("terminal outcomes = %#v, want one synthetic incomplete outcome", got) + } + if done != 1 || !strings.Contains(rec.Body.String(), "adapter_eof") { + t.Fatalf("done=%d body=%q, want one cleanup and adapter_eof", done, rec.Body.String()) + } +} + +type responsesSSEErrorReader struct { + data []byte + done bool +} + +func (r *responsesSSEErrorReader) Read(p []byte) (int, error) { + if !r.done { + r.done = true + return copy(p, r.data), errors.New("upstream reset") + } + return 0, errors.New("upstream reset") +} + +func TestRelayResponsesSSEReadErrorReportsSyntheticFailure(t *testing.T) { + var got []ResponsesSSETerminal + rec := httptest.NewRecorder() + err := relayResponsesSSEWithFlush(rec, &responsesSSEErrorReader{data: []byte("data: {\"type\":\"response.created\"}\n\n")}, responseRepairPipeline{}, ResponsesSSERelayOptions{ + OnTerminal: func(outcome ResponsesSSETerminal) { got = append(got, outcome) }, + }) + if err != nil { + t.Fatal(err) + } + if len(got) != 1 || got[0].Status != ResponsesSSEFailed || got[0].HTTPStatus != http.StatusBadGateway || !got[0].Synthetic { + t.Fatalf("terminal outcomes = %#v, want synthetic failed 502", got) + } +} + +func TestRelayResponsesSSECancellationSuppressesSyntheticFailure(t *testing.T) { + ctx, cancelContext := context.WithCancel(context.Background()) + cancelContext() + cancel := 0 + terminals := 0 + done := 0 + rec := httptest.NewRecorder() + err := relayResponsesSSEWithFlush(rec, strings.NewReader("data: {\"type\":\"response.created\"}\n\n"), responseRepairPipeline{}, ResponsesSSERelayOptions{ + Context: ctx, + OnTerminal: func(ResponsesSSETerminal) { terminals++ }, + OnCancel: func() { cancel++ }, + OnDone: func() { done++ }, + }) + if err != nil { + t.Fatal(err) + } + if terminals != 0 || cancel != 1 || done != 1 { + t.Fatalf("terminals=%d cancel=%d done=%d, want 0,1,1", terminals, cancel, done) + } +} diff --git a/go/internal/sidecar/sse_stream.go b/go/internal/sidecar/sse_stream.go index 5280b25714..6abe290835 100644 --- a/go/internal/sidecar/sse_stream.go +++ b/go/internal/sidecar/sse_stream.go @@ -29,17 +29,28 @@ var adapterEOFIncompletePayload = []byte("{\"type\":\"response.incomplete\",\"re // Atomic increment keeps independently relayed streams race-free. var syntheticSSEItemOrdinal atomic.Int64 +// ResponsesSSETerminalStatus is the first protocol terminal observed in a +// Responses stream. +type ResponsesSSETerminalStatus string + +const ( + ResponsesSSECompleted ResponsesSSETerminalStatus = "completed" + ResponsesSSEFailed ResponsesSSETerminalStatus = "failed" + ResponsesSSEIncomplete ResponsesSSETerminalStatus = "incomplete" +) + // ResponsesSSEStream incrementally frames, repairs, and terminal-bounds one // Responses SSE stream. Feed accepts arbitrary transport chunks; its output // contains only complete client-dispatchable SSE events. Finish must be called // once on clean upstream EOF to dispatch an unterminated final event and, if // no Responses terminal was observed, synthesize adapter_eof plus [DONE]. type ResponsesSSEStream struct { - buffer []byte - terminal bool - done bool - pendingDone []sseFrame - pipeline responseRepairPipeline + buffer []byte + terminal bool + terminalStatus string + done bool + pendingDone []sseFrame + pipeline responseRepairPipeline } type sseFrame struct { @@ -61,6 +72,14 @@ func NewResponsesSSEStream(pipeline ...responseRepairPipeline) *ResponsesSSEStre // response.incomplete event crossed the client boundary. func (s *ResponsesSSEStream) TerminalSeen() bool { return s.terminal } +// TerminalStatus reports the first Responses terminal status, or an empty +// string before a terminal. A failed/incomplete payload is intentionally not +// reclassified here: callers that need provider-specific policy can inspect +// the raw payload branch independently. +func (s *ResponsesSSEStream) TerminalStatus() ResponsesSSETerminalStatus { + return ResponsesSSETerminalStatus(s.terminalStatus) +} + // DoneSeen reports whether upstream supplied any [DONE] data event. A [DONE] // before a Responses terminal is held until a terminal arrives. func (s *ResponsesSSEStream) DoneSeen() bool { return s.done } @@ -160,6 +179,7 @@ func (s *ResponsesSSEStream) processFrame(out *bytes.Buffer, frame sseFrame) { out.Write(frame.delimiter) if hasData && responsesSSETerminal(payload) { s.terminal = true + s.terminalStatus = responsesSSETerminalStatus(payload) for _, pending := range s.pendingDone { out.Write(pending.block) out.Write(pending.delimiter) @@ -329,12 +349,25 @@ func backfillSSEOutputItem(item *jsonwire.Value, slot, inferredStatus string) bo } func responsesSSETerminal(payload []byte) bool { + return responsesSSETerminalStatus(payload) != "" +} + +func responsesSSETerminalStatus(payload []byte) string { event, err := jsonwire.Parse(payload) if err != nil || event.Kind() != jsonwire.Object { - return false + return "" } typeName, _ := stringMember(event, "type") - return typeName == "response.completed" || typeName == "response.failed" || typeName == "response.incomplete" + switch typeName { + case "response.completed": + return "completed" + case "response.failed": + return "failed" + case "response.incomplete": + return "incomplete" + default: + return "" + } } func replaceSSEDataPayload(block, payload []byte) []byte { From 35b84c743815975f1d612be7c2304a8785b0de31 Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Mon, 7 Sep 2026 10:41:56 +0800 Subject: [PATCH 107/165] feat(go): add graceful sidecar shutdown drain Add admission fencing and bounded cancellation for Go HTTP and WebSocket turns, and forward the configured shutdown deadline from the TypeScript supervisor. Preserve the existing native SSE relay while making its upstream work drain-aware. Co-Authored-By: Claude Code --- go/cmd/ocx-sidecar/main.go | 8 +- go/internal/sidecar/hotpath.go | 13 +- go/internal/sidecar/shutdown.go | 205 +++++++++++++++++++++++ go/internal/sidecar/shutdown_response.go | 12 ++ go/internal/sidecar/shutdown_runtime.go | 97 +++++++++++ go/internal/sidecar/shutdown_test.go | 151 +++++++++++++++++ go/internal/sidecar/sidecar.go | 11 +- go/internal/sidecar/ws_bridge.go | 20 ++- src/server/go-sidecar.ts | 6 + src/server/index.ts | 1 + 10 files changed, 511 insertions(+), 13 deletions(-) create mode 100644 go/internal/sidecar/shutdown.go create mode 100644 go/internal/sidecar/shutdown_response.go create mode 100644 go/internal/sidecar/shutdown_runtime.go create mode 100644 go/internal/sidecar/shutdown_test.go diff --git a/go/cmd/ocx-sidecar/main.go b/go/cmd/ocx-sidecar/main.go index bb2bfa0d7b..3b9723d7e3 100644 --- a/go/cmd/ocx-sidecar/main.go +++ b/go/cmd/ocx-sidecar/main.go @@ -12,7 +12,6 @@ package main import ( - "context" "fmt" "net" "net/http" @@ -70,6 +69,9 @@ func serve() error { WriteRelaySecret: os.Getenv("OCX_SIDECAR_WRITE_RELAY_SECRET"), HotPathRelay: os.Getenv(sidecar.HotPathRelayEnv) != "", } + shutdownTracker := sidecar.NewShutdownTracker() + cfg.ShutdownTracker = shutdownTracker + shutdownTimeout := sidecar.ParseShutdownTimeout(os.Getenv(sidecar.ShutdownTimeoutEnv)) if cfg.Version == "" { fmt.Fprintln(os.Stderr, "ocx-sidecar: warning: OCX_SIDECAR_VERSION is unset; reporting version 0.0.0") } @@ -98,9 +100,7 @@ func serve() error { select { case sig := <-signals: fmt.Fprintf(os.Stderr, "ocx-sidecar: received %s; shutting down\n", sig) - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - if err := server.Shutdown(ctx); err != nil { + if err := sidecar.ShutdownServer(server, shutdownTracker, shutdownTimeout); err != nil { return fmt.Errorf("shutdown: %w", err) } return nil diff --git a/go/internal/sidecar/hotpath.go b/go/internal/sidecar/hotpath.go index 56512d4484..425f1fd5e1 100644 --- a/go/internal/sidecar/hotpath.go +++ b/go/internal/sidecar/hotpath.go @@ -44,9 +44,9 @@ const ( // the handler answer 404 for a non-POST request (the same allowlist shape as // the write-relay routes) instead of letting ServeMux synthesise a 405 that // would probe the seam's existence. -func mountDataPlaneSeam(mux *http.ServeMux, cfg Config) { +func mountDataPlaneSeam(mux *http.ServeMux, cfg Config, tracker *ShutdownTracker) { mux.HandleFunc("/v1/responses", func(w http.ResponseWriter, r *http.Request) { - dataPlaneSeam(w, r, cfg) + dataPlaneSeam(w, r, cfg, tracker) }) } @@ -55,7 +55,7 @@ func mountDataPlaneSeam(mux *http.ServeMux, cfg Config) { // that does not carry the parent request token: the sidecar must never invent // a public data-plane listener of its own, and while the seam is mounted the // in-process front door remains the only way a request reaches it. -func dataPlaneSeam(w http.ResponseWriter, r *http.Request, cfg Config) { +func dataPlaneSeam(w http.ResponseWriter, r *http.Request, cfg Config, tracker *ShutdownTracker) { if cfg.RequestToken == "" || !managementauth.EqualSecret(r.Header.Get(SidecarRequestHeader), cfg.RequestToken) { http.NotFound(w, r) return @@ -64,6 +64,13 @@ func dataPlaneSeam(w http.ResponseWriter, r *http.Request, cfg Config) { http.NotFound(w, r) return } + lease, admitted := tracker.Register(r.Context()) + if !admitted { + writeDrainingResponse(w) + return + } + defer lease.Release() + r = r.WithContext(lease.Context()) parent, ok := privateParentBridgeURL(cfg.ParentURL, DataPlaneBridgePath) if !ok || cfg.BridgeToken == "" { diff --git a/go/internal/sidecar/shutdown.go b/go/internal/sidecar/shutdown.go new file mode 100644 index 0000000000..1e8f795c9d --- /dev/null +++ b/go/internal/sidecar/shutdown.go @@ -0,0 +1,205 @@ +package sidecar + +import ( + "context" + "sync" +) + +// ShutdownTracker is the sidecar's admission fence and in-flight turn registry. +// BeginShutdown prevents new turns but deliberately leaves existing turns alone; +// AbortAll is the bounded-drain fallback that cancels and closes them. +// +// A turn must call Release exactly once. The returned context is cancelled when +// the caller's context is cancelled or when AbortAll is invoked. +type ShutdownTracker struct { + mu sync.Mutex + draining bool + nextID uint64 + turns map[uint64]*shutdownTurn + done chan struct{} +} + +type shutdownTurn struct { + cancel context.CancelFunc + onAbort func() + aborted bool + abortInvoked bool +} + +type shutdownAbort struct { + cancel context.CancelFunc + onAbort func() +} + +// ShutdownLease owns one admitted turn. Release is idempotent. OnAbort installs +// a transport close callback for resources (such as hijacked WebSocket sockets) +// that net/http.Server.Shutdown cannot see. +type ShutdownLease struct { + tracker *ShutdownTracker + id uint64 + ctx context.Context + cancel context.CancelFunc + + once sync.Once +} + +// NewShutdownTracker returns an initially open tracker. +func NewShutdownTracker() *ShutdownTracker { + return &ShutdownTracker{ + turns: make(map[uint64]*shutdownTurn), + done: closedShutdownChannel(), + } +} + +func closedShutdownChannel() chan struct{} { + ch := make(chan struct{}) + close(ch) + return ch +} + +// Register admits one turn unless shutdown has started. The parent context is +// retained as the first cancellation source, while AbortAll is the second. +func (t *ShutdownTracker) Register(parent context.Context) (*ShutdownLease, bool) { + if parent == nil { + parent = context.Background() + } + ctx, cancel := context.WithCancel(parent) + + t.mu.Lock() + defer t.mu.Unlock() + if t.draining { + cancel() + return nil, false + } + if len(t.turns) == 0 { + t.done = make(chan struct{}) + } + t.nextID++ + id := t.nextID + t.turns[id] = &shutdownTurn{cancel: cancel} + return &ShutdownLease{tracker: t, id: id, ctx: ctx, cancel: cancel}, true +} + +// Context returns the cancellation-aware context for this turn. +func (l *ShutdownLease) Context() context.Context { return l.ctx } + +// OnAbort registers a callback invoked by AbortAll. If the turn was already +// aborted, the callback runs immediately rather than leaving a transport open. +func (l *ShutdownLease) OnAbort(callback func()) { + if callback == nil { + return + } + l.tracker.mu.Lock() + turn, active := l.tracker.turns[l.id] + if active && !turn.aborted { + turn.onAbort = callback + l.tracker.mu.Unlock() + return + } + if active && turn.aborted { + if turn.abortInvoked { + l.tracker.mu.Unlock() + return + } + turn.onAbort = callback + turn.abortInvoked = true + l.tracker.mu.Unlock() + callback() + return + } + l.tracker.mu.Unlock() + callback() +} + +// Release removes the turn and cancels its derived context. It is safe to call +// from both normal completion and request-cancellation paths. +func (l *ShutdownLease) Release() { + if l == nil { + return + } + l.once.Do(func() { + l.cancel() + l.tracker.release(l.id) + }) +} + +func (t *ShutdownTracker) release(id uint64) { + t.mu.Lock() + defer t.mu.Unlock() + if _, ok := t.turns[id]; !ok { + return + } + delete(t.turns, id) + if len(t.turns) == 0 { + close(t.done) + } +} + +// BeginShutdown closes the admission fence. It is an irreversible, idempotent +// latch and returns true only for the caller that closed it. +func (t *ShutdownTracker) BeginShutdown() bool { + t.mu.Lock() + defer t.mu.Unlock() + if t.draining { + return false + } + t.draining = true + return true +} + +// IsDraining reports whether new turns are rejected. +func (t *ShutdownTracker) IsDraining() bool { + t.mu.Lock() + defer t.mu.Unlock() + return t.draining +} + +// Active reports the number of admitted turns that have not released. +func (t *ShutdownTracker) Active() int { + t.mu.Lock() + defer t.mu.Unlock() + return len(t.turns) +} + +// Wait blocks until all admitted turns release or ctx expires. BeginShutdown +// need not be called first; this makes the method useful in focused tests. +func (t *ShutdownTracker) Wait(ctx context.Context) error { + if ctx == nil { + ctx = context.Background() + } + t.mu.Lock() + done := t.done + t.mu.Unlock() + select { + case <-done: + return nil + case <-ctx.Done(): + return ctx.Err() + } +} + +// AbortAll cancels every active turn and invokes each registered transport +// callback. Callbacks run outside the mutex because closing a socket can cause +// handler code to synchronously release its lease. +func (t *ShutdownTracker) AbortAll() { + t.mu.Lock() + turns := make([]shutdownAbort, 0, len(t.turns)) + for _, turn := range t.turns { + if turn.aborted { + continue + } + turn.aborted = true + if turn.onAbort != nil { + turn.abortInvoked = true + } + turns = append(turns, shutdownAbort{cancel: turn.cancel, onAbort: turn.onAbort}) + } + t.mu.Unlock() + + for _, turn := range turns { + turn.cancel() + if turn.onAbort != nil { + turn.onAbort() + } + } +} diff --git a/go/internal/sidecar/shutdown_response.go b/go/internal/sidecar/shutdown_response.go new file mode 100644 index 0000000000..22b68aaf2a --- /dev/null +++ b/go/internal/sidecar/shutdown_response.go @@ -0,0 +1,12 @@ +package sidecar + +import "net/http" + +// writeDrainingResponse matches the TypeScript drainingResponse contract: +// 503 JSON server_error, a stable message, and a five-second retry hint. +func writeDrainingResponse(w http.ResponseWriter) { + w.Header().Set("Content-Type", "application/json") + w.Header().Set("Retry-After", "5") + w.WriteHeader(http.StatusServiceUnavailable) + _, _ = w.Write([]byte(`{"error":{"message":"Service shutting down","type":"server_error","code":"server_is_overloaded"}}`)) +} diff --git a/go/internal/sidecar/shutdown_runtime.go b/go/internal/sidecar/shutdown_runtime.go new file mode 100644 index 0000000000..5861592fdb --- /dev/null +++ b/go/internal/sidecar/shutdown_runtime.go @@ -0,0 +1,97 @@ +package sidecar + +import ( + "context" + "errors" + "net/http" + "strconv" + "strings" + "time" +) + +const ( + defaultShutdownTimeout = 5 * time.Second + ShutdownTimeoutEnv = "OCX_SIDECAR_SHUTDOWN_TIMEOUT_MS" +) + +// ParseShutdownTimeout reads the parent-provided millisecond budget. An absent +// or invalid value falls back to the TypeScript default of 5s; zero is a valid +// explicit immediate-drain budget and negative values clamp to zero. +func ParseShutdownTimeout(raw string) time.Duration { + trimmed := strings.TrimSpace(raw) + if trimmed == "" { + return defaultShutdownTimeout + } + value, err := strconv.ParseInt(trimmed, 10, 64) + if err != nil { + return defaultShutdownTimeout + } + if value < 0 { + return 0 + } + return time.Duration(value) * time.Millisecond +} + +// DrainAndShutdown closes admission, waits for active handlers up to one +// absolute deadline, then cancels remaining turns. The caller must still call +// http.Server.Shutdown afterward; this split lets tests and the command keep +// listener closure separate from turn cancellation. +func DrainAndShutdown(ctx context.Context, tracker *ShutdownTracker, timeout time.Duration) bool { + if tracker == nil { + return true + } + if timeout < 0 { + timeout = 0 + } + tracker.BeginShutdown() + deadlineCtx, cancel := context.WithTimeout(ctxOrBackground(ctx), timeout) + defer cancel() + if err := tracker.Wait(deadlineCtx); err == nil { + return true + } else if !errors.Is(err, context.DeadlineExceeded) && !errors.Is(err, context.Canceled) { + return false + } + tracker.AbortAll() + return false +} + +func ctxOrBackground(ctx context.Context) context.Context { + if ctx == nil { + return context.Background() + } + return ctx +} + +// ShutdownServer drains turns against one absolute budget, then asks net/http +// to close idle and active connections. A server shutdown timeout is bounded +// by the same remaining deadline, never a second independent grace period. +func ShutdownServer(server *http.Server, tracker *ShutdownTracker, timeout time.Duration) error { + if server == nil { + return nil + } + if tracker == nil { + if timeout < 0 { + timeout = 0 + } + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + return server.Shutdown(ctx) + } + if timeout < 0 { + timeout = 0 + } + deadline := time.Now().Add(timeout) + tracker.BeginShutdown() + waitCtx, cancelWait := context.WithDeadline(context.Background(), deadline) + defer cancelWait() + if err := tracker.Wait(waitCtx); err != nil { + tracker.AbortAll() + } + remaining := time.Until(deadline) + if remaining < 0 { + remaining = 0 + } + shutdownCtx, cancelShutdown := context.WithTimeout(context.Background(), remaining) + defer cancelShutdown() + return server.Shutdown(shutdownCtx) +} diff --git a/go/internal/sidecar/shutdown_test.go b/go/internal/sidecar/shutdown_test.go new file mode 100644 index 0000000000..9b5820a27d --- /dev/null +++ b/go/internal/sidecar/shutdown_test.go @@ -0,0 +1,151 @@ +package sidecar + +import ( + "context" + "errors" + "net/http" + "net/http/httptest" + "sync/atomic" + "testing" + "time" +) + +func TestShutdownTrackerAdmissionLatchAndAbort(t *testing.T) { + tracker := NewShutdownTracker() + lease, ok := tracker.Register(context.Background()) + if !ok || lease == nil { + t.Fatal("initial turn was not admitted") + } + if tracker.Active() != 1 { + t.Fatalf("active = %d, want 1", tracker.Active()) + } + if !tracker.BeginShutdown() || !tracker.IsDraining() { + t.Fatal("shutdown did not latch") + } + if tracker.BeginShutdown() { + t.Fatal("second shutdown begin should be idempotent") + } + if rejected, admitted := tracker.Register(context.Background()); admitted || rejected != nil { + t.Fatal("new turn admitted after shutdown") + } + + var aborted atomic.Int32 + lease.OnAbort(func() { aborted.Add(1) }) + tracker.AbortAll() + select { + case <-lease.Context().Done(): + case <-time.After(time.Second): + t.Fatal("abort did not cancel turn context") + } + if aborted.Load() != 1 { + t.Fatalf("abort callback count = %d, want 1", aborted.Load()) + } + lease.Release() + if tracker.Active() != 0 { + t.Fatalf("active after release = %d, want 0", tracker.Active()) + } + if err := tracker.Wait(context.Background()); err != nil { + t.Fatalf("wait after release: %v", err) + } +} + +func TestShutdownTrackerWaitHonorsDeadlineThenReleases(t *testing.T) { + tracker := NewShutdownTracker() + lease, ok := tracker.Register(context.Background()) + if !ok { + t.Fatal("turn was not admitted") + } + tracker.BeginShutdown() + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond) + defer cancel() + if err := tracker.Wait(ctx); !errors.Is(err, context.DeadlineExceeded) { + t.Fatalf("wait error = %v, want deadline exceeded", err) + } + lease.Release() + if err := tracker.Wait(context.Background()); err != nil { + t.Fatalf("wait after release: %v", err) + } +} + +func TestParseShutdownTimeout(t *testing.T) { + cases := []struct { + raw string + want time.Duration + }{ + {"", defaultShutdownTimeout}, + {"not-a-number", defaultShutdownTimeout}, + {"0", 0}, + {" 1250 ", 1250 * time.Millisecond}, + {"-1", 0}, + } + for _, tc := range cases { + if got := ParseShutdownTimeout(tc.raw); got != tc.want { + t.Errorf("ParseShutdownTimeout(%q) = %s, want %s", tc.raw, got, tc.want) + } + } +} + +func TestDrainAndShutdownAbortsAfterDeadline(t *testing.T) { + tracker := NewShutdownTracker() + lease, ok := tracker.Register(context.Background()) + if !ok { + t.Fatal("turn was not admitted") + } + if DrainAndShutdown(context.Background(), tracker, 0) { + t.Fatal("drain unexpectedly completed while turn was held") + } + select { + case <-lease.Context().Done(): + case <-time.After(time.Second): + t.Fatal("deadline drain did not cancel the held turn") + } + lease.Release() + if !tracker.IsDraining() || tracker.Active() != 0 { + t.Fatalf("tracker state = draining=%v active=%d", tracker.IsDraining(), tracker.Active()) + } +} + +func TestDataPlaneSeamRejectsAfterShutdownBegins(t *testing.T) { + tracker := NewShutdownTracker() + tracker.BeginShutdown() + h := NewHandler(Config{ + RequestToken: "request-token", + BridgeToken: "bridge-token", + ShutdownTracker: tracker, + }) + req := httptest.NewRequest(http.MethodPost, "/v1/responses", nil) + req.Header.Set(SidecarRequestHeader, "request-token") + req.Header.Set("Content-Type", "application/json") + rec := httptest.NewRecorder() + h.ServeHTTP(rec, req) + if rec.Code != http.StatusServiceUnavailable { + t.Fatalf("status = %d, want 503", rec.Code) + } + if got := rec.Header().Get("Retry-After"); got != "5" { + t.Fatalf("Retry-After = %q, want 5", got) + } + if got, want := rec.Body.String(), `{"error":{"message":"Service shutting down","type":"server_error","code":"server_is_overloaded"}}`; got != want { + t.Fatalf("body = %q, want %q", got, want) + } +} + +func TestWebSocketBridgeRejectsUpgradeAfterShutdownBegins(t *testing.T) { + tracker := NewShutdownTracker() + tracker.BeginShutdown() + h := NewHandler(Config{ + RequestToken: "request-token", + BridgeToken: "bridge-token", + ShutdownTracker: tracker, + }) + req := httptest.NewRequest(http.MethodGet, ResponsesWSBridgePath, nil) + req.Header.Set(SidecarRequestHeader, "request-token") + req.Header.Set("Upgrade", "websocket") + rec := httptest.NewRecorder() + h.ServeHTTP(rec, req) + if rec.Code != http.StatusServiceUnavailable { + t.Fatalf("status = %d, want 503", rec.Code) + } + if got := rec.Header().Get("Retry-After"); got != "5" { + t.Fatalf("Retry-After = %q, want 5", got) + } +} diff --git a/go/internal/sidecar/sidecar.go b/go/internal/sidecar/sidecar.go index 513fff5b8a..ce168b4a0b 100644 --- a/go/internal/sidecar/sidecar.go +++ b/go/internal/sidecar/sidecar.go @@ -63,6 +63,9 @@ type Config struct { // the TypeScript parent must pass OPENCODEX_GO_HOTPATH_RELAY=1 in the // environment to arm it. HotPathRelay bool + // ShutdownTracker is the process-lifetime admission fence. A nil tracker + // creates a fresh one, while the command wires one into all long-lived routes. + ShutdownTracker *ShutdownTracker } const ( @@ -94,8 +97,12 @@ type healthPayload struct { // The TypeScript front door only forwards declared routes, so this handler // never sees another request while the seam is wired correctly. func NewHandler(cfg Config) http.Handler { + tracker := cfg.ShutdownTracker + if tracker == nil { + tracker = NewShutdownTracker() + } mux := http.NewServeMux() - mountResponsesWebSocketBridge(mux, cfg) + mountResponsesWebSocketBridge(mux, cfg, tracker) writeRelay := managementauth.NewWriteRelayVerifier(cfg.WriteRelaySecret) mux.HandleFunc("GET /api/system/health", func(w http.ResponseWriter, r *http.Request) { version := cfg.Version @@ -310,7 +317,7 @@ func NewHandler(cfg Config) http.Handler { relayPublicWrite(w, r, cfg, writeRelay, path) }) } - mountDataPlaneSeam(mux, cfg) + mountDataPlaneSeam(mux, cfg, tracker) return mux } diff --git a/go/internal/sidecar/ws_bridge.go b/go/internal/sidecar/ws_bridge.go index 74a2bd4279..7409d7790f 100644 --- a/go/internal/sidecar/ws_bridge.go +++ b/go/internal/sidecar/ws_bridge.go @@ -5,6 +5,7 @@ package sidecar import ( "bufio" "bytes" + "context" "crypto/sha1" "encoding/base64" "encoding/binary" @@ -34,12 +35,18 @@ type wsBridgeRequest struct { Admission json.RawMessage `json:"admission"` } -func mountResponsesWebSocketBridge(mux *http.ServeMux, cfg Config) { +func mountResponsesWebSocketBridge(mux *http.ServeMux, cfg Config, tracker *ShutdownTracker) { mux.HandleFunc(ResponsesWSBridgePath, func(w http.ResponseWriter, r *http.Request) { if cfg.RequestToken == "" || !managementauth.EqualSecret(r.Header.Get(SidecarRequestHeader), cfg.RequestToken) || r.Method != http.MethodGet || !strings.EqualFold(r.Header.Get("Upgrade"), "websocket") { http.NotFound(w, r) return } + lease, admitted := tracker.Register(r.Context()) + if !admitted { + writeDrainingResponse(w) + return + } + defer lease.Release() if cfg.BridgeToken == "" { http.NotFound(w, r) return @@ -58,6 +65,7 @@ func mountResponsesWebSocketBridge(mux *http.ServeMux, cfg Config) { if err != nil { return } + lease.OnAbort(func() { _ = conn.Close() }) defer conn.Close() if _, err = rw.WriteString("HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: " + websocketAccept(key) + "\r\n\r\n"); err != nil { return @@ -75,7 +83,7 @@ func mountResponsesWebSocketBridge(mux *http.ServeMux, cfg Config) { _ = rw.Flush() return } - bridgeWSFrames(rw.Writer, cfg, input) + bridgeWSFrames(rw.Writer, cfg, input, lease.Context()) _ = rw.Flush() _, _ = rw.Write([]byte{0x88, 0x00}) _ = rw.Flush() @@ -169,14 +177,18 @@ func protocolWSError(w *bufio.Writer, m string) { sendWSError(w, 502, map[string]any{"type": "protocol_error", "code": "websocket_protocol_error", "message": m}, nil) } -func bridgeWSFrames(w *bufio.Writer, cfg Config, input wsBridgeRequest) { +func bridgeWSFrames(w *bufio.Writer, cfg Config, input wsBridgeRequest, contexts ...context.Context) { + ctx := context.Background() + if len(contexts) > 0 && contexts[0] != nil { + ctx = contexts[0] + } parent, ok := privateParentBridgeURL(cfg.ParentURL, ResponsesWSParentBridgePath) if !ok { sendWSError(w, 503, map[string]any{"type": "server_error", "message": "responses bridge unavailable"}, nil) return } body, _ := json.Marshal(input) - req, e := http.NewRequest(http.MethodPost, parent.String(), bytes.NewReader(body)) + req, e := http.NewRequestWithContext(ctx, http.MethodPost, parent.String(), bytes.NewReader(body)) if e != nil { sendWSError(w, 503, map[string]any{"type": "server_error", "message": "responses bridge unavailable"}, nil) return diff --git a/src/server/go-sidecar.ts b/src/server/go-sidecar.ts index 09431249a0..08384a49d9 100644 --- a/src/server/go-sidecar.ts +++ b/src/server/go-sidecar.ts @@ -49,6 +49,9 @@ export const GO_SIDECAR_REQUEST_TOKEN_ENV = "OCX_SIDECAR_REQUEST_TOKEN"; /** HMAC secret for parent-admission claims on Go-owned write routes. */ export const GO_SIDECAR_WRITE_RELAY_SECRET_ENV = "OCX_SIDECAR_WRITE_RELAY_SECRET"; +/** Graceful drain budget forwarded to the Go child in milliseconds. */ +export const GO_SIDECAR_SHUTDOWN_TIMEOUT_ENV = "OCX_SIDECAR_SHUTDOWN_TIMEOUT_MS"; + /** Readiness marker the Go binary prints on stdout after binding. */ export const GO_SIDECAR_READY_PREFIX = "ocx-sidecar-ready"; @@ -83,6 +86,8 @@ export type GoSidecarSupervisorConfig = { bridgeToken: string; requestToken: string; writeRelaySecret: string; + /** Graceful drain timeout shared with the TypeScript front door. */ + shutdownTimeoutMs?: number; /** Mints a proof bound to one admitted write's method, path and body bytes. */ createWriteRelayHeaders?: (request: { method: string; @@ -369,6 +374,7 @@ export function activateGoSidecar( [GO_SIDECAR_BRIDGE_TOKEN_ENV]: liveStateBridge.bridgeToken, [GO_SIDECAR_REQUEST_TOKEN_ENV]: liveStateBridge.requestToken, [GO_SIDECAR_WRITE_RELAY_SECRET_ENV]: liveStateBridge.writeRelaySecret, + [GO_SIDECAR_SHUTDOWN_TIMEOUT_ENV]: String(liveStateBridge.shutdownTimeoutMs ?? 5000), }, }); } catch (error) { diff --git a/src/server/index.ts b/src/server/index.ts index 5a89485f9c..f95b38deb0 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -2708,6 +2708,7 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server createGoSidecarWriteRelayHeaders(writeRelaySecret, request.principal, request) : undefined, From b91906c0c67953a3e4065150c8e32bcb9c170478 Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Mon, 7 Sep 2026 10:47:24 +0800 Subject: [PATCH 108/165] fix(go): align Responses relay parity edges Co-Authored-By: Claude Code --- go/internal/sidecar/hotpath_relay.go | 31 ++++++++++----- go/internal/sidecar/responses_pipeline.go | 40 ++++++++++++++------ go/internal/sidecar/responses_repair_test.go | 23 +++++++++++ go/internal/sidecar/sse_stream.go | 20 ++++------ go/internal/sidecar/sse_stream_test.go | 13 +++++++ 5 files changed, 94 insertions(+), 33 deletions(-) diff --git a/go/internal/sidecar/hotpath_relay.go b/go/internal/sidecar/hotpath_relay.go index 7a5515fd15..726d40a52f 100644 --- a/go/internal/sidecar/hotpath_relay.go +++ b/go/internal/sidecar/hotpath_relay.go @@ -257,11 +257,11 @@ func requestQualifiesForRelay(cfg Config, contentType string, headers http.Heade return nil, refusal } plan.modelID = modelID + provider := providers.Find(plan.providerName) + if refusal := streamRelayRefusal(provider, modelID); refusal != nil { + return nil, refusal + } if streaming { - provider := providers.Find(plan.providerName) - if refusal := streamRelayRefusal(provider, modelID); refusal != nil { - return nil, refusal - } plan.streaming = true } return plan, nil @@ -619,7 +619,10 @@ func doDirectRelay(w http.ResponseWriter, r *http.Request, cfg Config, plan *rel w.WriteHeader(upstreamResp.StatusCode) if upstreamResp.StatusCode >= 200 && upstreamResp.StatusCode < 300 && strings.Contains(strings.ToLower(contentType), "text/event-stream") { requestRoot, _ := jsonwire.Parse(body) - pipeline := responseRepairPipeline{modelID: plan.modelID} + // The admitted provider class is openai-responses. The TypeScript + // response-model rewrite is Anthropic-only, so preserve upstream model + // metadata here rather than rewriting ordinary OpenAI model aliases. + pipeline := responseRepairPipeline{} if requestRoot != nil { pipeline.imageAliases = imageAliasesFromRequest(requestRoot) } @@ -657,7 +660,10 @@ func doDirectRelay(w http.ResponseWriter, r *http.Request, cfg Config, plan *rel // the backfill changed nothing or the body is not a JSON object, so // assigning unconditionally preserves raw-bytes relay parity. requestRoot, _ := jsonwire.Parse(body) - pipeline := responseRepairPipeline{modelID: plan.modelID} + // The admitted provider class is openai-responses. The TypeScript + // response-model rewrite is Anthropic-only, so preserve upstream model + // metadata here rather than rewriting ordinary OpenAI model aliases. + pipeline := responseRepairPipeline{} if requestRoot != nil { pipeline.imageAliases = imageAliasesFromRequest(requestRoot) } @@ -794,7 +800,10 @@ func observeResponsesSSEChunk(inspector *ResponsesSSEStream, lifecycle *relayLif if lifecycle.terminal || len(chunk) == 0 { return } - _, _ = inspector.Feed(chunk) + if _, err := inspector.Feed(chunk); err != nil { + fmt.Fprintf(os.Stderr, "ocx-sidecar: SSE inspection feed: %v\n", err) + return + } if inspector.TerminalSeen() { lifecycle.terminalOutcome(ResponsesSSETerminal{Status: inspector.TerminalStatus()}) } @@ -805,7 +814,9 @@ func finishResponsesSSEInspection(inspector *ResponsesSSEStream, lifecycle *rela return } if cleanEOF { - _, _ = inspector.Finish() + if _, err := inspector.Finish(); err != nil { + fmt.Fprintf(os.Stderr, "ocx-sidecar: SSE inspection finish: %v\n", err) + } if inspector.TerminalSeen() { lifecycle.terminalOutcome(ResponsesSSETerminal{Status: inspector.TerminalStatus()}) return @@ -815,7 +826,9 @@ func finishResponsesSSEInspection(inspector *ResponsesSSEStream, lifecycle *rela } return } - _, _ = inspector.FinishPartial() + if _, err := inspector.FinishPartial(); err != nil { + fmt.Fprintf(os.Stderr, "ocx-sidecar: SSE inspection partial finish: %v\n", err) + } if inspector.TerminalSeen() { lifecycle.terminalOutcome(ResponsesSSETerminal{Status: inspector.TerminalStatus()}) return diff --git a/go/internal/sidecar/responses_pipeline.go b/go/internal/sidecar/responses_pipeline.go index a7435605db..55964e15d0 100644 --- a/go/internal/sidecar/responses_pipeline.go +++ b/go/internal/sidecar/responses_pipeline.go @@ -142,23 +142,39 @@ func (p responseRepairPipeline) repairReasoningItem(v *jsonwire.Value) bool { func imageAliasesFromRequest(root *jsonwire.Value) map[string]imageAlias { out := map[string]imageAlias{} - tools := root.Find("tools") - if tools == nil || tools.Kind() != jsonwire.Array { - return out - } - for _, tool := range tools.Elements() { - if tool == nil || tool.Kind() != jsonwire.Object { - continue - } - name, ok := stringMember(tool, "name") - if !ok { - continue + collect := func(tools *jsonwire.Value) { + if tools == nil || tools.Kind() != jsonwire.Array { + return } - if strings.HasPrefix(name, "image_gen.") && len(name) > len("image_gen.") { + for _, tool := range tools.Elements() { + if tool == nil || tool.Kind() != jsonwire.Object { + continue + } + typeName, _ := stringMember(tool, "type") + if typeName != "function" { + continue + } + name, ok := stringMember(tool, "name") + if !ok || !strings.HasPrefix(name, "image_gen.") || len(name) <= len("image_gen.") { + continue + } local := strings.TrimPrefix(name, "image_gen.") out[name] = imageAlias{name: local, namespace: "image_gen"} out["image_gen__"+local] = imageAlias{name: local, namespace: "image_gen"} } } + collect(root.Find("tools")) + input := root.Find("input") + if input != nil && input.Kind() == jsonwire.Array { + for _, item := range input.Elements() { + if item == nil || item.Kind() != jsonwire.Object { + continue + } + typeName, _ := stringMember(item, "type") + if typeName == "additional_tools" { + collect(item.Find("tools")) + } + } + } return out } diff --git a/go/internal/sidecar/responses_repair_test.go b/go/internal/sidecar/responses_repair_test.go index afd101b1e3..f3402849b9 100644 --- a/go/internal/sidecar/responses_repair_test.go +++ b/go/internal/sidecar/responses_repair_test.go @@ -6,6 +6,8 @@ import ( "os" "path/filepath" "testing" + + "github.com/lidge-jun/opencodex/go/internal/jsonwire" ) // repairGolden is one row of the committed golden file. The expected bytes are @@ -84,6 +86,27 @@ func TestRepairResponsesJSONEmptyStringIDReplacesInPlace(t *testing.T) { } } +func TestImageAliasesFromRequestIncludesAdditionalToolsGroups(t *testing.T) { + root, err := jsonwire.Parse([]byte(`{"tools":[],"input":[{"type":"additional_tools","tools":[{"type":"function","name":"image_gen.create"}]}]}`)) + if err != nil { + t.Fatal(err) + } + aliases := imageAliasesFromRequest(root) + alias, ok := aliases["image_gen__create"] + if !ok || alias.name != "create" || alias.namespace != "image_gen" { + t.Fatalf("aliases = %#v, want grouped image-gen alias", aliases) + } +} + +func TestResponseRepairPipelinePreservesOpenAIResponseModel(t *testing.T) { + input := []byte(`{"model":"upstream-model","output":[]}`) + pipeline := responseRepairPipeline{} + out, changed := pipeline.repairJSON(input) + if string(out) != string(input) || changed { + t.Fatalf("pipeline changed ordinary model response: got %q changed=%v", out, changed) + } +} + func TestResponseRepairPipelineRunsOrderedModelAndImageRestoresBeforeBackfill(t *testing.T) { input := `{"model":"upstream","output":[{"type":"function_call","name":"image_gen__create","arguments":"{}"},{"type":"message","content":[{"type":"output_text","text":"ok"}]}]}` p := responseRepairPipeline{modelID: "client-model", imageAliases: map[string]imageAlias{"image_gen__create": {name: "create", namespace: "image_gen"}}} diff --git a/go/internal/sidecar/sse_stream.go b/go/internal/sidecar/sse_stream.go index 6abe290835..8b8850a302 100644 --- a/go/internal/sidecar/sse_stream.go +++ b/go/internal/sidecar/sse_stream.go @@ -376,25 +376,21 @@ func replaceSSEDataPayload(block, payload []byte) []byte { newline = []byte("\r\n") } lines := bytes.Split(block, []byte("\n")) - var out bytes.Buffer + kept := make([][]byte, 0, len(lines)) replaced := false - for index, original := range lines { + for _, original := range lines { line := bytes.TrimSuffix(original, []byte("\r")) - if index > 0 { - out.Write(newline) - } if bytes.HasPrefix(line, []byte("data:")) { - if !replaced { - out.WriteString("data: ") - out.Write(payload) - replaced = true + if replaced { + continue } - continue + line = append([]byte("data: "), payload...) + replaced = true } - out.Write(line) + kept = append(kept, line) } if !replaced { return block } - return out.Bytes() + return bytes.Join(kept, newline) } diff --git a/go/internal/sidecar/sse_stream_test.go b/go/internal/sidecar/sse_stream_test.go index 2928a7876d..4de7f818ad 100644 --- a/go/internal/sidecar/sse_stream_test.go +++ b/go/internal/sidecar/sse_stream_test.go @@ -152,6 +152,19 @@ func TestResponsesSSEMalformedOutputIndexUsesProcessGlobalFallback(t *testing.T) } } +func TestReplaceSSEDataPayloadDropsSplitDataLinesWithoutBlankFrames(t *testing.T) { + block := []byte("event: response.output_item.added\ndata: {\"type\":\"response.output_item.added\",\ndata: \"output_index\":0}\nid: 1") + payload := []byte("{\"type\":\"response.output_item.added\",\"output_index\":0}") + got := string(replaceSSEDataPayload(block, payload)) + want := "event: response.output_item.added\ndata: {\"type\":\"response.output_item.added\",\"output_index\":0}\nid: 1" + if got != want { + t.Fatalf("split data rewrite = %q, want %q", got, want) + } + if bytes.Contains([]byte(got), []byte("\n\nid: 1")) { + t.Fatalf("rewrite introduced a blank SSE frame: %q", got) + } +} + func TestResponsesSSEOutputIndexUsesNumberIsIntegerSemantics(t *testing.T) { cases := map[string]string{"1.0": "1", "1e0": "1", "1e3": "1000"} for index, slot := range cases { From 4bfd6c5a3c4bc4c0e6ba3d9152c540dc808ad808 Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Mon, 7 Sep 2026 10:52:56 +0800 Subject: [PATCH 109/165] fix(go): complete runtime and model suffix parity Co-Authored-By: Claude Code --- go/internal/ocxcli/cli_test.go | 5 ++ go/internal/ocxcli/runtime_server.go | 50 ++++++++++--- go/internal/ocxcli/runtime_server_test.go | 40 +++++++++- go/internal/sidecar/model_suffix.go | 76 +++++++++++++++++++ go/internal/sidecar/model_suffix_test.go | 91 +++++++++++++++++++++++ 5 files changed, 249 insertions(+), 13 deletions(-) create mode 100644 go/internal/sidecar/model_suffix.go create mode 100644 go/internal/sidecar/model_suffix_test.go diff --git a/go/internal/ocxcli/cli_test.go b/go/internal/ocxcli/cli_test.go index 105069288e..d1dcf9bfa1 100644 --- a/go/internal/ocxcli/cli_test.go +++ b/go/internal/ocxcli/cli_test.go @@ -199,6 +199,11 @@ func TestVersionAndRegistry(t *testing.T) { func TestOwnershipMapMatchesDispatch(t *testing.T) { for _, command := range Commands { for _, name := range append([]string{command.Name}, command.Aliases...) { + // Lifecycle commands own a real listener/process and intentionally block + // until a signal; ownership is asserted above without launching them. + if name == "start" || name == "stop" { + continue + } t.Run(name, func(t *testing.T) { var delegated []string deps := depsFor(RuntimeState{}, &bytes.Buffer{}, &bytes.Buffer{}) diff --git a/go/internal/ocxcli/runtime_server.go b/go/internal/ocxcli/runtime_server.go index c6685a7b74..8b7b3b1810 100644 --- a/go/internal/ocxcli/runtime_server.go +++ b/go/internal/ocxcli/runtime_server.go @@ -134,6 +134,7 @@ type standaloneServer struct { home string pid int port int + hostname string secret string version string started time.Time @@ -159,11 +160,12 @@ func newStandaloneServer(listen, version string) (*standaloneServer, error) { _ = listener.Close() return nil, err } - server := &standaloneServer{listener: listener, home: home, pid: os.Getpid(), port: port, secret: base64.RawURLEncoding.EncodeToString(secretRaw), version: version, started: time.Now()} + server := &standaloneServer{listener: listener, home: home, pid: os.Getpid(), port: port, hostname: listenHost(listener.Addr()), secret: base64.RawURLEncoding.EncodeToString(secretRaw), version: version, started: time.Now()} server.handler = server.routes() server.http = &http.Server{Handler: server.handler, ReadHeaderTimeout: 5 * time.Second, IdleTimeout: 30 * time.Second} if err := server.writeRuntime(); err != nil { _ = listener.Close() + _ = removeRuntimeRecords(home) return nil, err } return server, nil @@ -175,21 +177,28 @@ func (s *standaloneServer) routes() http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/healthz": - w.Header().Set("Content-Type", "application/json") + if r.Method != http.MethodGet { + http.NotFound(w, r) + return + } if challenge := r.Header.Get(attestationChallengeHeader); challenge != "" { w.Header().Set(attestationProofHeader, managementauth.CreateLocalAttestationProof(s.secret, challenge, int64(s.pid), s.port)) } - _ = json.NewEncoder(w).Encode(map[string]any{"status": "ok", "service": "opencodex", "version": s.version, "uptime": time.Since(s.started).Seconds(), "pid": s.pid, "port": s.port}) + writeRuntimeJSON(w, http.StatusOK, Health{Status: "ok", Service: "opencodex", Version: s.version, Uptime: time.Since(s.started).Seconds(), PID: int64(s.pid), Port: s.port}) case "/readyz": - w.Header().Set("Content-Type", "application/json") - _ = json.NewEncoder(w).Encode(map[string]any{"service": "opencodex", "version": s.version, "uptime": time.Since(s.started).Seconds(), "pid": s.pid, "port": s.port, "status": "ready"}) + if r.Method != http.MethodGet { + http.NotFound(w, r) + return + } + writeRuntimeJSON(w, http.StatusOK, readiness{Service: "opencodex", Version: s.version, Uptime: time.Since(s.started).Seconds(), PID: int64(s.pid), Port: s.port, Status: "ready"}) + case "/readyz/": + http.NotFound(w, r) case "/api/stop": if r.Method != http.MethodPost { http.NotFound(w, r) return } - w.Header().Set("Content-Type", "application/json") - _ = json.NewEncoder(w).Encode(map[string]any{"ok": true}) + writeRuntimeJSON(w, http.StatusOK, map[string]any{"ok": true}) go func() { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() @@ -204,11 +213,29 @@ func (s *standaloneServer) routes() http.Handler { } }) } + +func writeRuntimeJSON(w http.ResponseWriter, status int, payload any) { + w.Header().Set("Content-Type", "application/json") + raw, err := json.Marshal(payload) + if err != nil { + return + } + w.WriteHeader(status) + _, _ = w.Write(raw) +} + +func listenHost(addr net.Addr) string { + host, _, err := net.SplitHostPort(addr.String()) + if err != nil || host == "" || host == "0.0.0.0" || host == "::" { + return "127.0.0.1" + } + return strings.Trim(host, "[]") +} func (s *standaloneServer) writeRuntime() error { if err := os.WriteFile(filepath.Join(s.home, "ocx.pid"), []byte(strconv.Itoa(s.pid)+"\n"), 0o600); err != nil { return err } - raw, err := json.MarshalIndent(RuntimeState{PID: int64(s.pid), Port: s.port, Hostname: "127.0.0.1", AttestationSecret: s.secret}, "", " ") + raw, err := json.MarshalIndent(RuntimeState{PID: int64(s.pid), Port: s.port, Hostname: s.hostname, AttestationSecret: s.secret}, "", " ") if err != nil { return err } @@ -271,6 +298,7 @@ func serveUntilSignal(server *standaloneServer, stderr io.Writer) int { go func() { done <- server.Serve() }() signals := make(chan os.Signal, 1) signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM) + defer signal.Stop(signals) select { case <-signals: ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) @@ -319,7 +347,11 @@ func runStop(args []string, deps Deps) int { return ExitOK } client := &http.Client{Timeout: 10 * time.Second} - stopURL := fmt.Sprintf("http://%s/api/stop", net.JoinHostPort(state.Hostname, strconv.Itoa(state.Port))) + host := strings.TrimSpace(state.Hostname) + if host == "" || host == "0.0.0.0" || host == "::" || host == "[::]" { + host = "127.0.0.1" + } + stopURL := fmt.Sprintf("http://%s/api/stop", net.JoinHostPort(strings.Trim(host, "[]"), strconv.Itoa(state.Port))) request, requestErr := http.NewRequest(http.MethodPost, stopURL, nil) graceful := false if requestErr == nil { diff --git a/go/internal/ocxcli/runtime_server_test.go b/go/internal/ocxcli/runtime_server_test.go index 054ab19aff..2485997b89 100644 --- a/go/internal/ocxcli/runtime_server_test.go +++ b/go/internal/ocxcli/runtime_server_test.go @@ -3,6 +3,7 @@ package ocxcli import ( "bytes" "context" + "encoding/json" "errors" "net" "net/http" @@ -12,6 +13,8 @@ import ( "strings" "testing" "time" + + "github.com/lidge-jun/opencodex/go/internal/managementauth" ) func TestReclaimPortRemovesStaleProcessRecords(t *testing.T) { @@ -97,15 +100,44 @@ func TestStandaloneServerOwnsListenerDashboardHealthAndGoRoutes(t *testing.T) { } ready := httptest.NewRecorder() server.handler.ServeHTTP(ready, httptest.NewRequest(http.MethodGet, "/readyz", nil)) - if ready.Code != http.StatusOK || !strings.Contains(ready.Body.String(), "\"status\":\"ready\"") { - t.Fatalf("ready = %d %s", ready.Code, ready.Body.String()) + if ready.Code != http.StatusOK || ready.Header().Get("Content-Type") != "application/json" { + t.Fatalf("ready headers = %d %q", ready.Code, ready.Header().Get("Content-Type")) + } + var readyBody map[string]any + if err := json.Unmarshal(ready.Body.Bytes(), &readyBody); err != nil { + t.Fatalf("decode ready = %v", err) + } + if readyBody["service"] != "opencodex" || readyBody["version"] != "9.9.9" || readyBody["status"] != "ready" { + t.Fatalf("ready body = %#v", readyBody) + } + if pid, ok := readyBody["pid"].(float64); !ok || int64(pid) != int64(server.pid) { + t.Fatalf("ready pid = %#v, want %d", readyBody["pid"], server.pid) + } + if port, ok := readyBody["port"].(float64); !ok || int(port) != server.port { + t.Fatalf("ready port = %#v, want %d", readyBody["port"], server.port) + } + for _, method := range []string{http.MethodPost, http.MethodPut, http.MethodDelete, http.MethodHead} { + rejected := httptest.NewRecorder() + server.handler.ServeHTTP(rejected, httptest.NewRequest(method, "/readyz", nil)) + if rejected.Code != http.StatusNotFound { + t.Fatalf("%s /readyz = %d, want 404", method, rejected.Code) + } + } + trailing := httptest.NewRecorder() + server.handler.ServeHTTP(trailing, httptest.NewRequest(http.MethodGet, "/readyz/", nil)) + if trailing.Code != http.StatusNotFound { + t.Fatalf("GET /readyz/ = %d, want 404", trailing.Code) } raw, err := os.ReadFile(filepath.Join(home, "runtime-port.json")) if err != nil { t.Fatal(err) } - if !strings.Contains(string(raw), "\"pid\"") || !strings.Contains(string(raw), "\"attestationSecret\"") { - t.Fatalf("runtime record = %s", raw) + var state RuntimeState + if err := json.Unmarshal(raw, &state); err != nil { + t.Fatalf("decode runtime record: %v", err) + } + if state.PID != int64(server.pid) || state.Port != server.port || state.Hostname != "127.0.0.1" || !managementauth.IsAttestationSecret(state.AttestationSecret) { + t.Fatalf("runtime record = %#v", state) } } diff --git a/go/internal/sidecar/model_suffix.go b/go/internal/sidecar/model_suffix.go new file mode 100644 index 0000000000..bf2ede5d01 --- /dev/null +++ b/go/internal/sidecar/model_suffix.go @@ -0,0 +1,76 @@ +package sidecar + +import ( + "strings" + "unicode" + + "github.com/lidge-jun/opencodex/go/internal/jsonwire" +) + +// StripBracketedModelSuffix mirrors the TypeScript openai-chat adapter. It +// removes one trailing bracket group from a model id when the provider opts +// into the compatibility behavior. Whitespace is considered only while +// locating a terminal suffix; if no suffix exists, the original string is +// returned unchanged. +func StripBracketedModelSuffix(modelID string) string { + trimmed := strings.TrimRightFunc(modelID, unicode.IsSpace) + suffixEnd := len(trimmed) + if suffixEnd == 0 || modelID[suffixEnd-1] != ']' { + return modelID + } + + suffixStart := -1 + for i := suffixEnd - 2; i >= 0 && modelID[i] != ']'; i-- { + if modelID[i] == '[' { + suffixStart = i + } + } + if suffixStart < 0 { + return modelID + } + return modelID[:suffixStart] +} + +// NormalizeOpenAIChatRequestModel applies the provider-scoped wire-model +// normalization used by the openai-chat adapter. The input is a complete JSON +// request body; when the flag is absent or false, or the body has no string +// model field, the original bytes are returned untouched. A changed body is +// encoded with the same ordered JSON representation used by the relay. +func NormalizeOpenAIChatRequestModel(provider *jsonwire.Value, raw []byte) ([]byte, bool, error) { + if !openAIChatBracketStripEnabled(provider) { + return raw, false, nil + } + root, err := jsonwire.Parse(raw) + if err != nil { + return nil, false, err + } + if root == nil || root.Kind() != jsonwire.Object { + return raw, false, nil + } + model := root.Find("model") + if model == nil || model.Kind() != jsonwire.String { + return raw, false, nil + } + wireModel := StripBracketedModelSuffix(model.String()) + if wireModel == model.String() { + return raw, false, nil + } + root.Set("model", jsonwire.StringValue(wireModel)) + encoded, err := root.Encode() + if err != nil { + return nil, false, err + } + return encoded, true, nil +} + +func openAIChatBracketStripEnabled(provider *jsonwire.Value) bool { + if provider == nil || provider.Kind() != jsonwire.Object { + return false + } + adapter, ok := stringMember(provider, "adapter") + if !ok || adapter != "openai-chat" { + return false + } + enabled, ok := boolMember(provider, "modelSuffixBracketStrip") + return ok && enabled +} diff --git a/go/internal/sidecar/model_suffix_test.go b/go/internal/sidecar/model_suffix_test.go new file mode 100644 index 0000000000..179e9a6b19 --- /dev/null +++ b/go/internal/sidecar/model_suffix_test.go @@ -0,0 +1,91 @@ +package sidecar + +import ( + "strings" + "testing" + + "github.com/lidge-jun/opencodex/go/internal/jsonwire" +) + +func TestStripBracketedModelSuffix(t *testing.T) { + long := "model" + strings.Repeat("[", 100_000) + "x" + validLong := "model[" + strings.Repeat("x", 100_000) + "]" + unmatchedClosing := "model" + strings.Repeat("x", 100_000) + "]" + cases := []struct { + name string + in string + want string + }{ + {"trailing marker", "glm-5.2[1m]", "glm-5.2"}, + {"bare id", "glm-5.2", "glm-5.2"}, + {"trailing unicode whitespace after suffix", "glm-5.2[1m] ", "glm-5.2"}, + {"trailing whitespace without suffix", "glm-5.2 \t\r\n", "glm-5.2 \t\r\n"}, + {"interior group", "a[b]c", "a[b]c"}, + {"empty group", "model[]", "model"}, + {"only final group", "model[first][second]", "model[first]"}, + {"long malformed suffix", long, long}, + {"long valid suffix", validLong, "model"}, + {"long unmatched closing bracket", unmatchedClosing, unmatchedClosing}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + if got := StripBracketedModelSuffix(tc.in); got != tc.want { + t.Fatalf("StripBracketedModelSuffix() = %q, want %q", got, tc.want) + } + }) + } +} + +func TestNormalizeOpenAIChatRequestModel(t *testing.T) { + provider := func(raw string) *jsonwire.Value { + value, err := jsonwire.Parse([]byte(raw)) + if err != nil { + t.Fatal(err) + } + return value + } + cases := []struct { + name string + config string + body string + want string + changed bool + }{ + { + name: "flagged chat strips suffix", + config: `{"adapter":"openai-chat","modelSuffixBracketStrip":true}`, + body: `{"model":"glm-5.2[1m]","messages":[]}`, + want: `{"model":"glm-5.2","messages":[]}`, + changed: true, + }, + { + name: "unflagged chat preserves bytes", + config: `{"adapter":"openai-chat"}`, + body: `{"model":"glm-5.2[1m]","messages":[]}`, + want: `{"model":"glm-5.2[1m]","messages":[]}`, + }, + { + name: "other adapter preserves bytes", + config: `{"adapter":"anthropic","modelSuffixBracketStrip":true}`, + body: `{"model":"glm-5.2[1m]","messages":[]}`, + want: `{"model":"glm-5.2[1m]","messages":[]}`, + }, + { + name: "bare model preserves original formatting", + config: `{"adapter":"openai-chat","modelSuffixBracketStrip":true}`, + body: `{"model":"glm-5.2","messages":[]}`, + want: `{"model":"glm-5.2","messages":[]}`, + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + got, changed, err := NormalizeOpenAIChatRequestModel(provider(tc.config), []byte(tc.body)) + if err != nil { + t.Fatal(err) + } + if changed != tc.changed || string(got) != tc.want { + t.Fatalf("got %q changed=%v, want %q changed=%v", got, changed, tc.want, tc.changed) + } + }) + } +} From 61f7cc52c21b886bc14ad922330544f79bca2326 Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Mon, 7 Sep 2026 14:55:44 +0800 Subject: [PATCH 110/165] fix(go): guard process state by runtime identity --- go/cmd/ocx-sidecar/main.go | 2 + go/cmd/ocx-sidecar/processstatecheck.go | 58 +++ go/internal/ocxcli/doctor_actions.go | 2 +- go/internal/ocxcli/process_command_unix.go | 42 ++ go/internal/ocxcli/process_command_windows.go | 78 ++++ go/internal/ocxcli/process_state.go | 324 ++++++++++++++ .../ocxcli/process_state_oracle_test.go | 45 ++ go/internal/ocxcli/process_state_test.go | 198 +++++++++ go/internal/ocxcli/runtime_server.go | 112 +++-- go/internal/ocxcli/runtime_server_test.go | 154 ++++++- go/internal/ocxcli/status_diagnostics.go | 14 +- go/internal/ocxcli/status_domains.go | 48 ++- .../ocxcli/testdata/pid-parse-oracle.tsv | 406 ++++++++++++++++++ tests/process-state-go-parity.test.ts | 85 ++++ 14 files changed, 1493 insertions(+), 75 deletions(-) create mode 100644 go/cmd/ocx-sidecar/processstatecheck.go create mode 100644 go/internal/ocxcli/process_command_unix.go create mode 100644 go/internal/ocxcli/process_command_windows.go create mode 100644 go/internal/ocxcli/process_state.go create mode 100644 go/internal/ocxcli/process_state_oracle_test.go create mode 100644 go/internal/ocxcli/process_state_test.go create mode 100644 go/internal/ocxcli/testdata/pid-parse-oracle.tsv create mode 100644 tests/process-state-go-parity.test.ts diff --git a/go/cmd/ocx-sidecar/main.go b/go/cmd/ocx-sidecar/main.go index 3b9723d7e3..b0fceff6f2 100644 --- a/go/cmd/ocx-sidecar/main.go +++ b/go/cmd/ocx-sidecar/main.go @@ -43,6 +43,8 @@ func run() error { return runLabCheck() case "routingcheck": return runRoutingCheck() + case "processstatecheck": + return runProcessStateCheck() } return fmt.Errorf("unknown subcommand %q", os.Args[1]) } diff --git a/go/cmd/ocx-sidecar/processstatecheck.go b/go/cmd/ocx-sidecar/processstatecheck.go new file mode 100644 index 0000000000..f9ea2ae411 --- /dev/null +++ b/go/cmd/ocx-sidecar/processstatecheck.go @@ -0,0 +1,58 @@ +package main + +// The processstatecheck subcommand is the differential-oracle entry point for +// the Go process-state model (ticket #34). It calls the production parser and +// command-line matcher directly, so the Bun test cannot accidentally validate +// a second implementation of either security-sensitive rule. + +import ( + "encoding/json" + "fmt" + "io" + "os" + + "github.com/lidge-jun/opencodex/go/internal/ocxcli" +) + +type processStateCheckInput struct { + Parse []string `json:"parse"` + Match []string `json:"match"` +} + +type processStateCheckOutput struct { + Parse []int64 `json:"parse"` + Match []bool `json:"match"` +} + +func runProcessStateCheck() error { + var raw []byte + if len(os.Args) > 2 { + raw = []byte(os.Args[2]) + } else { + var err error + raw, err = io.ReadAll(os.Stdin) + if err != nil { + return fmt.Errorf("processstatecheck: read stdin: %w", err) + } + } + var input processStateCheckInput + if err := json.Unmarshal(raw, &input); err != nil { + return fmt.Errorf("processstatecheck: decode input: %w", err) + } + output := processStateCheckOutput{ + Parse: make([]int64, len(input.Parse)), + Match: make([]bool, len(input.Match)), + } + for index, value := range input.Parse { + output.Parse[index] = ocxcli.ParsePIDFile(value) + } + for index, value := range input.Match { + output.Match[index] = ocxcli.IsOcxStartCommandLine(value) + } + encoded, err := json.Marshal(output) + if err != nil { + return fmt.Errorf("processstatecheck: encode output: %w", err) + } + fmt.Println(string(encoded)) + return nil +} diff --git a/go/internal/ocxcli/doctor_actions.go b/go/internal/ocxcli/doctor_actions.go index de64abbfb8..da63f4df83 100644 --- a/go/internal/ocxcli/doctor_actions.go +++ b/go/internal/ocxcli/doctor_actions.go @@ -15,7 +15,7 @@ import ( // that asynchronous probe is moved. func doctorLiveProxyPID() int64 { pid := readStatusPIDFile() - if pid > 0 && doctorProcessAlive(int(pid)) { + if pid > 0 { return pid } return 0 diff --git a/go/internal/ocxcli/process_command_unix.go b/go/internal/ocxcli/process_command_unix.go new file mode 100644 index 0000000000..555b1042c0 --- /dev/null +++ b/go/internal/ocxcli/process_command_unix.go @@ -0,0 +1,42 @@ +//go:build !windows + +package ocxcli + +import ( + "context" + "errors" + "fmt" + "os" + "os/exec" + "strconv" + "strings" + "time" +) + +// readProcessCommandLine mirrors the Unix portion of the TypeScript probe: +// procfs first on Linux, then fixed absolute ps paths. A failed probe is +// reported as an error so destructive callers can retain fail-open behavior. +func readProcessCommandLine(pid int) (string, error) { + if pid <= 0 { + return "", errors.New("invalid pid") + } + if runtimeGOOS() == "linux" { + if raw, err := os.ReadFile(fmt.Sprintf("/proc/%d/cmdline", pid)); err == nil { + if value := strings.TrimSpace(strings.ReplaceAll(string(raw), "\x00", " ")); value != "" { + return value, nil + } + } + } + for _, ps := range []string{"/bin/ps", "/usr/bin/ps"} { + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + output, err := exec.CommandContext(ctx, ps, "-p", strconv.Itoa(pid), "-o", "command=").CombinedOutput() + cancel() + if err != nil { + continue + } + if value := strings.TrimSpace(string(output)); value != "" { + return value, nil + } + } + return "", errors.New("process command inspection is unavailable on this platform") +} diff --git a/go/internal/ocxcli/process_command_windows.go b/go/internal/ocxcli/process_command_windows.go new file mode 100644 index 0000000000..0c7b63f320 --- /dev/null +++ b/go/internal/ocxcli/process_command_windows.go @@ -0,0 +1,78 @@ +//go:build windows + +package ocxcli + +import ( + "context" + "errors" + "fmt" + "os/exec" + "path/filepath" + "regexp" + "strconv" + "strings" + "syscall" + "time" + + "golang.org/x/sys/windows" +) + +var wmicCommandLinePattern = regexp.MustCompile(`(?m)^CommandLine=(.*)$`) + +// readProcessCommandLine mirrors the Windows portion of the TypeScript probe. +// Executables are built from GetSystemDirectory rather than PATH or environment +// variables because this probe guards destructive process actions. +func readProcessCommandLine(pid int) (string, error) { + if pid <= 0 { + return "", errors.New("invalid pid") + } + systemDir, err := windows.GetSystemDirectory() + if err != nil || systemDir == "" { + if err == nil { + err = errors.New("empty system directory") + } + return "", fmt.Errorf("resolve trusted Windows system directory: %w", err) + } + + wmic := filepath.Join(systemDir, "wbem", "WMIC.exe") + if output, runErr := runWindowsProcessProbe(wmic, + "process", "where", "ProcessId="+strconv.Itoa(pid), "get", "CommandLine", "/VALUE", + ); runErr == nil { + if match := wmicCommandLinePattern.FindStringSubmatch(strings.ReplaceAll(output, "\r", "")); len(match) == 2 { + if commandLine := strings.TrimSpace(match[1]); commandLine != "" { + return commandLine, nil + } + } + } + + powershell := filepath.Join(systemDir, "WindowsPowerShell", "v1.0", "powershell.exe") + output, err := runWindowsProcessProbe(powershell, + "-NoProfile", + "-NoLogo", + "-NonInteractive", + "-Command", + `(Get-CimInstance Win32_Process -Filter "ProcessId = `+strconv.Itoa(pid)+`").CommandLine`, + ) + if err != nil { + return "", fmt.Errorf("inspect process command line: %w", err) + } + if commandLine := strings.TrimSpace(output); commandLine != "" { + return commandLine, nil + } + return "", errors.New("process command line is empty") +} + +func runWindowsProcessProbe(executable string, args ...string) (string, error) { + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + defer cancel() + command := exec.CommandContext(ctx, executable, args...) + command.SysProcAttr = &syscall.SysProcAttr{HideWindow: true} + output, err := command.Output() + if err != nil { + if ctx.Err() != nil { + return "", ctx.Err() + } + return "", err + } + return string(output), nil +} diff --git a/go/internal/ocxcli/process_state.go b/go/internal/ocxcli/process_state.go new file mode 100644 index 0000000000..93e4c03675 --- /dev/null +++ b/go/internal/ocxcli/process_state.go @@ -0,0 +1,324 @@ +package ocxcli + +import ( + "encoding/json" + "errors" + "os" + "path/filepath" + "strconv" + "strings" + "unicode/utf8" +) + +// This file is the Go mirror of src/config/process-state.ts (#34): it owns +// ocx.pid parsing, command-line identity, guarded runtime-record cleanup, and +// the byte-compatible state writes. The invariant carried over from +// TypeScript: a live PID alone proves nothing — the OS can recycle it — so +// destructive callers must additionally verify the process command line. + +// pidFileMaxValue bounds parsePidFile output like parsePidFile's safe-integer +// gate in TypeScript (2^53, exclusive — 9007199254740992 itself fails +// Number.isSafeInteger only above, so the bound is 2^53-1): a wider int64 +// would otherwise accept values the pid-file writer can never have produced. +const pidFileMaxValue = int64(1)<<53 - 1 + +// jsWhitespace matches ECMAScript's String.prototype.trim and regex \\s +// whitespace exactly. In particular, U+0085 and U+001C..U+001F are not +// JavaScript whitespace even though some Go Unicode helpers classify them as +// space characters. +func jsWhitespace(r rune) bool { + switch { + case r >= 0x0009 && r <= 0x000D: + return true + case r == 0x0020 || r == 0x00A0 || r == 0x1680: + return true + case r >= 0x2000 && r <= 0x200A: + return true + case r == 0x2028 || r == 0x2029 || r == 0x202F || r == 0x205F || r == 0x3000 || r == 0xFEFF: + return true + default: + return false + } +} + +func trimJS(raw string) string { + start := 0 + for start < len(raw) { + r, size := utf8.DecodeRuneInString(raw[start:]) + if !jsWhitespace(r) { + break + } + start += size + } + end := len(raw) + for end > start { + r, size := utf8.DecodeLastRuneInString(raw[:end]) + if !jsWhitespace(r) { + break + } + end -= size + } + return raw[start:end] +} + +// parsePidFile mirrors parsePidFile in src/config/process-state.ts: strictly +// decimal digits (after JavaScript trim), a positive safe integer, and nothing +// else. A leading sign, decimal point, inner whitespace, or U+0085 makes the +// file invalid rather than best-effort parsed. +func parsePidFile(raw string) int64 { + trimmed := trimJS(raw) + if len(trimmed) == 0 { + return 0 + } + for _, r := range trimmed { + if r < '0' || r > '9' { + return 0 + } + } + pid, err := strconv.ParseInt(trimmed, 10, 64) + if err != nil || pid <= 0 || pid > pidFileMaxValue { + return 0 + } + return pid +} + +// ParsePIDFile exposes the production parser to the differential oracle. A +// zero result has the same null meaning as TypeScript's parsePidFile. +func ParsePIDFile(raw string) int64 { return parsePidFile(raw) } + +// readPidFileValue is readPidFileValue in TypeScript: the raw parsed value +// with no liveness or identity check. Discovery-only. +func readPidFileValue(path string) int64 { + raw, err := os.ReadFile(path) + if err != nil { + return 0 + } + return parsePidFile(string(raw)) +} + +// readIdentityCheckedPID mirrors TypeScript's readPid: a PID is returned only +// when it is alive and its command line identifies an OpenCodex start process. +// Command-line inspection remains fail-open when unavailable for compatibility +// with locked-down hosts. +func readIdentityCheckedPID(path string, inspector processInspector) int64 { + pid := readPidFileValue(path) + if pid == 0 || !inspector.Alive(int(pid)) { + return 0 + } + command, err := inspector.Command(int(pid)) + if err == nil && !isOcxStartCommandLine(command) { + return 0 + } + return pid +} + +// isOcxStartCommandLine accepts source launches, package launches, and the +// installed ocx/opencodex command, but requires `start` as a separate argument. +// The executable marker must appear as a real token (path segment, package path, +// npm rename directory, or standalone command word), not as a substring of a +// test or builder path; `start` must appear as a standalone word — not as a +// substring of start-guard/test paths. +func isOcxStartCommandLine(commandLine string) bool { + normalized := strings.ToLower(strings.ReplaceAll(commandLine, "\\", "/")) + hasOcxEntrypoint := strings.Contains(normalized, "src/cli.ts") || + strings.Contains(normalized, "src/cli/index.ts") || + strings.Contains(normalized, "@bitkyc08/opencodex") || + strings.Contains(normalized, "@bitkyc08/.opencodex-") || + hasStandaloneOcxWord(normalized) + return hasOcxEntrypoint && hasStandaloneStartWord(normalized) +} + +// IsOcxStartCommandLine exposes the production identity matcher to the +// differential oracle without duplicating its security-sensitive logic. +func IsOcxStartCommandLine(commandLine string) bool { return isOcxStartCommandLine(commandLine) } + +// hasStandaloneOcxWord matches /(?:^|[\s/"'])(?:ocx|opencodex)(?:\.cmd)?(?:$|[\s"'])/ +// from TypeScript: `ocx`/`opencodex` optionally with the .cmd extension, as a +// whole word separated by start/end, whitespace, slash, or quote. +func hasStandaloneOcxWord(normalized string) bool { + for offset := 0; offset < len(normalized); { + ocxIndex := strings.Index(normalized[offset:], "ocx") + opencodexIndex := strings.Index(normalized[offset:], "opencodex") + index := -1 + word := "" + switch { + case ocxIndex < 0 && opencodexIndex < 0: + return false + case ocxIndex < 0 || (opencodexIndex >= 0 && opencodexIndex < ocxIndex): + index, word = opencodexIndex+offset, "opencodex" + default: + index, word = ocxIndex+offset, "ocx" + } + // The shorter spelling is contained in opencodex; skip it unless the + // candidate really starts and ends as a standalone token. + if wordBoundaryBefore(normalized, index) { + end := index + len(word) + if strings.HasPrefix(normalized[end:], ".cmd") { + end += len(".cmd") + } + if wordBoundaryAfter(normalized, end) { + return true + } + } + offset = index + len(word) + } + return false +} + +func previousRune(text string, index int) (rune, bool) { + if index <= 0 { + return 0, false + } + r, size := utf8.DecodeLastRuneInString(text[:index]) + return r, size > 0 +} + +func nextRune(text string, index int) (rune, bool) { + if index >= len(text) { + return 0, false + } + r, _ := utf8.DecodeRuneInString(text[index:]) + return r, true +} + +// wordBoundaryBefore matches the left side of the ocx/opencodex regex. The +// executable token additionally allows a slash before it; start does not. +func wordBoundaryBefore(text string, index int) bool { + if index == 0 { + return true + } + r, ok := previousRune(text, index) + return ok && (jsWhitespace(r) || r == '/' || r == '"' || r == '\'') +} + +func wordBoundaryAfter(text string, index int) bool { + if index >= len(text) { + return true + } + r, ok := nextRune(text, index) + return ok && (jsWhitespace(r) || r == '"' || r == '\'') +} + +// startBoundaryBefore matches the left side of the TypeScript start regex. +// Keep slash out: `ocx /start` must not be accepted. +func startBoundaryBefore(text string, index int) bool { + if index == 0 { + return true + } + r, ok := previousRune(text, index) + return ok && (jsWhitespace(r) || r == '"' || r == '\'') +} + +// hasStandaloneStartWord mirrors /(?:^|[\s"'])start(?:$|[\s"'])/: `start` as a +// whole word. Note the TypeScript class deliberately excludes `/` on the right +// side — `ocx start-guard` must not match. +func hasStandaloneStartWord(normalized string) bool { + for offset := 0; offset < len(normalized); { + index := strings.Index(normalized[offset:], "start") + if index < 0 { + return false + } + index += offset + if startBoundaryBefore(normalized, index) && wordBoundaryAfter(normalized, index+len("start")) { + return true + } + offset = index + len("start") + } + return false +} + +// removePidIfValueIs is removePidIfValueIs in TypeScript: deletion is +// authorized only by the exact value observed before an in-flight probe, so a +// replacement runtime that rewrote the pid file mid-probe keeps its state. +func removePidIfValueIs(path string, snapshot int64) { + if _, err := os.Stat(path); err != nil { + return + } + if readPidFileValue(path) != snapshot { + return + } + _ = os.Remove(path) +} + +// removeRuntimePortIfPidIs mirrors TypeScript's null snapshot semantics: +// malformed or missing current records compare as null, so an unreadable +// record is removed when the caller also snapshotted it as unreadable. +func removeRuntimePortIfPidIs(path string, snapshotPid int64) { + record, err := readStatusRuntimeRecordAt(path) + currentPid := int64(0) + if err == nil { + currentPid = record.PID + } + if currentPid != snapshotPid { + return + } + _ = os.Remove(path) +} + +// readStatusRuntimeRecordAt parses a runtime-port.json at an explicit path +// with ReadStatusRuntime's validation, so the snapshot guards above can key +// on the record's pid without re-resolving the config directory. +func readStatusRuntimeRecordAt(path string) (StatusRuntimeRecord, error) { + raw, err := os.ReadFile(path) + if err != nil { + return StatusRuntimeRecord{}, err + } + var record StatusRuntimeRecord + if err := json.Unmarshal(raw, &record); err != nil { + return StatusRuntimeRecord{}, err + } + if record.PID <= 0 || record.Port < 1 || record.Port > 65535 { + return StatusRuntimeRecord{}, errors.New("invalid runtime record") + } + return record, nil +} + +// removeRuntimeRecordsFor clears this runtime's records only when they still +// name expectedPid, mirroring removePid(expectedPid)+removeRuntimePort(pid). +// Callers that hold no identity (expectedPid 0) fall back to the old +// unconditional removal, which remains correct for the error paths of +// newStandaloneServer where no other runtime can have written yet. +func removeRuntimeRecordsFor(home string, expectedPid int64) error { + pidPath := filepath.Join(home, "ocx.pid") + runtimePath := filepath.Join(home, "runtime-port.json") + if expectedPid > 0 { + removePidIfValueIs(pidPath, expectedPid) + removeRuntimePortIfPidIs(runtimePath, expectedPid) + return nil + } + for _, path := range []string{pidPath, runtimePath} { + if err := os.Remove(path); err != nil && !os.IsNotExist(err) { + return err + } + } + return nil +} + +// writeStateFileAtomic mirrors atomicWriteFile's core contract for process +// state: a hardened 0600 temp sibling, fsync, then rename over the target. It +// publishes exactly payload bytes (writePid writes no trailing newline in +// TypeScript, so neither may this). +func writeStateFileAtomic(path string, payload []byte) error { + temp, err := os.CreateTemp(filepath.Dir(path), ".ocx-state-*") + if err != nil { + return err + } + name := temp.Name() + defer os.Remove(name) + if err := temp.Chmod(0o600); err != nil { + _ = temp.Close() + return err + } + if _, err := temp.Write(payload); err != nil { + _ = temp.Close() + return err + } + if err := temp.Sync(); err != nil { + _ = temp.Close() + return err + } + if err := temp.Close(); err != nil { + return err + } + return os.Rename(name, path) +} diff --git a/go/internal/ocxcli/process_state_oracle_test.go b/go/internal/ocxcli/process_state_oracle_test.go new file mode 100644 index 0000000000..e8abe48c73 --- /dev/null +++ b/go/internal/ocxcli/process_state_oracle_test.go @@ -0,0 +1,45 @@ +package ocxcli + +import ( + "encoding/json" + "os" + "path/filepath" + "strconv" + "strings" + "testing" +) + +// TestParsePidFileTypeScriptMatrix pins the Go parser to the exact TypeScript +// parsePidFile matrix (generated by running src/config/process-state.ts +// semantics through bun, including randomized input). Both sides must agree +// on every row: this gate decides whether a recorded PID is acted upon. +func TestParsePidFileTypeScriptMatrix(t *testing.T) { + raw, err := os.ReadFile(filepath.Join("testdata", "pid-parse-oracle.tsv")) + if err != nil { + t.Fatalf("read oracle: %v", err) + } + for _, line := range strings.Split(strings.TrimRight(string(raw), "\n"), "\n") { + if line == "" { + continue + } + parts := strings.SplitN(line, "\t", 2) + if len(parts) != 2 { + t.Fatalf("oracle row not \t: %q", line) + } + var input string + if err := json.Unmarshal([]byte(parts[0]), &input); err != nil { + t.Fatalf("decode oracle input %q: %v", parts[0], err) + } + want := int64(0) + if parts[1] != "null" { + value, err := strconv.ParseInt(parts[1], 10, 64) + if err != nil { + t.Fatalf("decode oracle value %q: %v", parts[1], err) + } + want = value + } + if got := parsePidFile(input); got != want { + t.Errorf("parsePidFile(%q) = %d, TypeScript says %d", input, got, want) + } + } +} diff --git a/go/internal/ocxcli/process_state_test.go b/go/internal/ocxcli/process_state_test.go new file mode 100644 index 0000000000..180c599cdb --- /dev/null +++ b/go/internal/ocxcli/process_state_test.go @@ -0,0 +1,198 @@ +package ocxcli + +import ( + "encoding/json" + "os" + "path/filepath" + "strconv" + "strings" + "testing" +) + +func TestParsePidFile(t *testing.T) { + cases := []struct { + raw string + want int64 + }{ + {raw: "12345", want: 12345}, + {raw: " 12345\n", want: 12345}, + {raw: "007", want: 7}, + {raw: "0", want: 0}, + {raw: "12x", want: 0}, + {raw: "not-json", want: 0}, + {raw: "", want: 0}, + {raw: " ", want: 0}, + {raw: "+7", want: 0}, + {raw: "-1", want: 0}, + {raw: "3.5", want: 0}, + {raw: "12 34", want: 0}, + {raw: "99999999999999999999", want: 0}, + // 2^53+1 exceeds TypeScript's safe-integer gate; Go int64 would keep + // it, so the digit-length bound must reject it first. + {raw: "9007199254740993", want: 0}, + {raw: "12\t", want: 12}, + {raw: "\n123", want: 123}, + {raw: "1_000", want: 0}, + {raw: "0x10", want: 0}, + // Arabic-Indic digits: \d in JS matches them, but the Go rune loop + // accepts ASCII only. TypeScript trims them to null below because + // parseInt... both sides must end at 0/null regardless of mechanism. + {raw: "١٢٣", want: 0}, + } + for _, tc := range cases { + if got := parsePidFile(tc.raw); got != tc.want { + t.Errorf("parsePidFile(%q) = %d, want %d", tc.raw, got, tc.want) + } + } +} + +func TestIsOcxStartCommandLine(t *testing.T) { + recognised := []string{ + "bun run src/cli.ts start", + `"C:/tools/bun/bin/bun.exe" "run" "src/cli.ts" "start"`, + "bun C:/tools/bun/install/global/node_modules/@bitkyc08/opencodex/src/cli.ts start", + "opencodex start", + "bun src/cli/index.ts start", + `C:\Users\u\AppData\Roaming\npm\ocx.cmd start`, + // npm's in-place global-update rename keeps a service wrapper pointed at + // the hidden `.opencodex-*` directory while files move underneath it. + "bun C:/tools/bun/install/global/node_modules/@bitkyc08/.opencodex-3f2a/src/cli/index.ts start", + // The compiled Go runtime is its own process shape: it must stay + // recognisable to stop/reclaim identity checks on every platform. + "/usr/local/bin/ocx start", + "/home/u/.opencodex/bin/opencodex start --port 10100", + } + for _, command := range recognised { + if !isOcxStartCommandLine(command) { + t.Errorf("isOcxStartCommandLine(%q) = false, want true", command) + } + } + rejected := []string{ + "bun run src/cli.ts status", + "bun test C:/work/opencodex/tests/config.test.ts", + "notepad.exe", + "ocx start-guard", + "python server.py --flag ocx startx", + } + for _, command := range rejected { + if isOcxStartCommandLine(command) { + t.Errorf("isOcxStartCommandLine(%q) = true, want false", command) + } + } +} + +func TestReadPidFileValue(t *testing.T) { + path := filepath.Join(t.TempDir(), "ocx.pid") + if got := readPidFileValue(path); got != 0 { + t.Fatalf("missing pid file value = %d, want 0", got) + } + if err := os.WriteFile(path, []byte("111"), 0o600); err != nil { + t.Fatal(err) + } + if got := readPidFileValue(path); got != 111 { + t.Fatalf("pid file value = %d, want 111", got) + } + if err := os.WriteFile(path, []byte("garbage"), 0o600); err != nil { + t.Fatal(err) + } + if got := readPidFileValue(path); got != 0 { + t.Fatalf("garbage pid file value = %d, want 0", got) + } +} + +func TestRemovePidIfValueIs(t *testing.T) { + path := filepath.Join(t.TempDir(), "ocx.pid") + if err := os.WriteFile(path, []byte("111"), 0o600); err != nil { + t.Fatal(err) + } + removePidIfValueIs(path, 222) + if _, err := os.Stat(path); err != nil { + t.Fatalf("pid file removed for non-matching snapshot: %v", err) + } + removePidIfValueIs(path, 111) + if _, err := os.Stat(path); !os.IsNotExist(err) { + t.Fatalf("pid file kept for matching snapshot: %v", err) + } + // A missing file is a no-op, not an error. + removePidIfValueIs(path, 111) +} + +func TestRemoveRuntimePortIfPidIs(t *testing.T) { + path := filepath.Join(t.TempDir(), "runtime-port.json") + record, err := json.Marshal(StatusRuntimeRecord{PID: 111, Port: 10100, Hostname: "127.0.0.1"}) + if err != nil { + t.Fatal(err) + } + if err := os.WriteFile(path, record, 0o600); err != nil { + t.Fatal(err) + } + removeRuntimePortIfPidIs(path, 222) + if _, err := os.Stat(path); err != nil { + t.Fatalf("runtime record removed for non-matching pid: %v", err) + } + removeRuntimePortIfPidIs(path, 111) + if _, err := os.Stat(path); !os.IsNotExist(err) { + t.Fatalf("runtime record kept for matching pid: %v", err) + } +} + +func TestRemoveRuntimeRecordsForOnlyClearsMatchingRecords(t *testing.T) { + home := t.TempDir() + pidPath := filepath.Join(home, "ocx.pid") + runtimePath := filepath.Join(home, "runtime-port.json") + if err := os.WriteFile(pidPath, []byte("42"), 0o600); err != nil { + t.Fatal(err) + } + record, err := json.Marshal(StatusRuntimeRecord{PID: 43, Port: 10100, Hostname: "127.0.0.1"}) + if err != nil { + t.Fatal(err) + } + if err := os.WriteFile(runtimePath, record, 0o600); err != nil { + t.Fatal(err) + } + if err := removeRuntimeRecordsFor(home, 42); err != nil { + t.Fatalf("removeRuntimeRecordsFor: %v", err) + } + if _, err := os.Stat(pidPath); !os.IsNotExist(err) { + t.Fatalf("pid file kept for matching pid: %v", err) + } + // A torn state pair (pid file 42, runtime record 43) must not lose the + // runtime record a concurrent replacement runtime still owns. + if _, err := os.Stat(runtimePath); err != nil { + t.Fatalf("runtime record removed for non-matching pid: %v", err) + } +} + +func TestWriteStateFileAtomicPublishesExactBytes(t *testing.T) { + path := filepath.Join(t.TempDir(), "ocx.pid") + if err := os.WriteFile(path, []byte("stale-content"), 0o600); err != nil { + t.Fatal(err) + } + payload := []byte(strconv.Itoa(os.Getpid())) + if err := writeStateFileAtomic(path, payload); err != nil { + t.Fatalf("writeStateFileAtomic: %v", err) + } + raw, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + if string(raw) != string(payload) { + t.Fatalf("pid file bytes = %q, want %q (no trailing newline: byte parity with writePid)", raw, payload) + } + info, err := os.Stat(path) + if err != nil { + t.Fatal(err) + } + if info.Mode().Perm() != 0o600 { + t.Fatalf("pid file mode = %v, want 0600", info.Mode().Perm()) + } + entries, err := os.ReadDir(filepath.Dir(path)) + if err != nil { + t.Fatal(err) + } + for _, entry := range entries { + if strings.HasPrefix(entry.Name(), ".ocx-state-") { + t.Fatalf("residual temp file %q left behind", entry.Name()) + } + } +} diff --git a/go/internal/ocxcli/runtime_server.go b/go/internal/ocxcli/runtime_server.go index a0b43f4f58..3f50dfe7a4 100644 --- a/go/internal/ocxcli/runtime_server.go +++ b/go/internal/ocxcli/runtime_server.go @@ -13,6 +13,7 @@ import ( "os" "os/signal" "path/filepath" + "runtime" "strconv" "strings" "syscall" @@ -35,12 +36,9 @@ type processInspector interface { type osProcessInspector struct{} func (osProcessInspector) Alive(pid int) bool { return doctorProcessAlive(pid) } + func (osProcessInspector) Command(pid int) (string, error) { - if runtimeGOOS() == "linux" { - raw, err := os.ReadFile(fmt.Sprintf("/proc/%d/cmdline", pid)) - return strings.ReplaceAll(string(raw), "\x00", " "), err - } - return "", errors.New("process command inspection is unavailable on this platform") + return readProcessCommandLine(pid) } func (osProcessInspector) Terminate(pid int, signal string) error { if signal != "TERM" { @@ -53,8 +51,8 @@ func (osProcessInspector) Terminate(pid int, signal string) error { return process.Signal(syscall.SIGTERM) } -var runtimeGOOS = func() string { return runtimeGOOSValue } -var runtimeGOOSValue = "linux" +// runtimeGOOS is a test seam for platform-branched process inspection. +var runtimeGOOS = func() string { return runtime.GOOS } type portReclaimer struct { home string @@ -65,7 +63,13 @@ func (r portReclaimer) reclaim(port int) error { if port < 1 { return nil } - pid := readRecordedPID(filepath.Join(r.home, "ocx.pid")) + pidPath := filepath.Join(r.home, "ocx.pid") + runtimePath := filepath.Join(r.home, "runtime-port.json") + pid := int(readPidFileValue(pidPath)) + // Snapshot both records BEFORE the liveness probe: a replacement runtime + // can rewrite them while the probe is in flight, and the purge below is + // authorized only by the exact values observed here (#34). + runtimeRecordPid := readRuntimePortSnapshotForGuard(runtimePath) if pid == 0 { if listenerAvailable(port) { return nil @@ -73,26 +77,38 @@ func (r portReclaimer) reclaim(port int) error { return fmt.Errorf("port %d is occupied by a process that is not the recorded OpenCodex runtime", port) } if !r.process.Alive(pid) { - return removeRuntimeRecords(r.home) + // Stale by liveness: purge only the exact records snapshotted above, + // so a replacement runtime that started mid-check keeps its state. + removePidIfValueIs(pidPath, int64(pid)) + removeRuntimePortIfPidIs(runtimePath, runtimeRecordPid) + return nil } - command, err := r.process.Command(pid) - if err != nil || !isKnownTypeScriptRuntime(command) { - return fmt.Errorf("port %d is occupied by a process that is not a reclaimable TypeScript OpenCodex runtime", port) + command, commandErr := r.process.Command(pid) + if !isOcxStartCommandLine(command) { + if commandErr == nil { + // A readable command line that is not an ocx start command means + // the PID was recycled: refuse to touch it (#34). + return fmt.Errorf("port %d is occupied by a process that is not a reclaimable OpenCodex runtime", port) + } + // Command inspection unavailable: fail open to the previous + // alive-PID behavior (#34 compatibility for locked-down hosts). } if err := r.process.Terminate(pid, "TERM"); err != nil { - return fmt.Errorf("stop stale TypeScript OpenCodex runtime %d: %w", pid, err) + return fmt.Errorf("stop stale OpenCodex runtime %d: %w", pid, err) } deadline := time.Now().Add(5 * time.Second) for r.process.Alive(pid) && time.Now().Before(deadline) { time.Sleep(25 * time.Millisecond) } if r.process.Alive(pid) { - return fmt.Errorf("TypeScript OpenCodex runtime %d did not exit; refusing to steal port %d", pid, port) + return fmt.Errorf("OpenCodex runtime %d did not exit; refusing to steal port %d", pid, port) } if !listenerAvailable(port) { - return fmt.Errorf("port %d remains occupied after TypeScript OpenCodex runtime stopped", port) + return fmt.Errorf("port %d remains occupied after OpenCodex runtime stopped", port) } - return removeRuntimeRecords(r.home) + removePidIfValueIs(pidPath, int64(pid)) + removeRuntimePortIfPidIs(runtimePath, runtimeRecordPid) + return nil } func listenerAvailable(port int) bool { @@ -103,28 +119,15 @@ func listenerAvailable(port int) bool { _ = l.Close() return true } -func readRecordedPID(path string) int { - raw, err := os.ReadFile(path) - if err != nil { - return 0 - } - value, err := strconv.Atoi(strings.TrimSpace(string(raw))) - if err != nil || value < 1 { - return 0 - } - return value -} -func removeRuntimeRecords(home string) error { - for _, name := range []string{"ocx.pid", "runtime-port.json"} { - if err := os.Remove(filepath.Join(home, name)); err != nil && !errors.Is(err, os.ErrNotExist) { - return err - } - } - return nil -} -func isKnownTypeScriptRuntime(command string) bool { - normalized := strings.ToLower(strings.ReplaceAll(command, "\\", "/")) - return strings.Contains(normalized, "src/cli/index.ts") || strings.Contains(normalized, "src/cli.ts") || (strings.Contains(normalized, "opencodex") && strings.Contains(normalized, " start")) + +// readRuntimePortSnapshotForGuard reads the pid the snapshot guard should +// compare against. An unreadable record is represented by zero, matching the +// TypeScript null snapshot so malformed state can be purged when unchanged. +func readRuntimePortSnapshotForGuard(path string) int64 { + if record, err := readStatusRuntimeRecordAt(path); err == nil { + return record.PID + } + return 0 } type standaloneServer struct { @@ -165,7 +168,7 @@ func newStandaloneServer(listen, version string) (*standaloneServer, error) { server.http = &http.Server{Handler: server.handler, ReadHeaderTimeout: 5 * time.Second, IdleTimeout: 30 * time.Second} if err := server.writeRuntime(); err != nil { _ = listener.Close() - _ = removeRuntimeRecords(home) + _ = removeRuntimeRecordsFor(home, int64(server.pid)) return nil, err } return server, nil @@ -232,20 +235,27 @@ func listenHost(addr net.Addr) string { return strings.Trim(host, "[]") } +// writeRuntime publishes both state files byte-compatibly with TypeScript: +// ocx.pid is exactly the decimal pid with no trailing newline (writePid in +// src/config/process-state.ts), runtime-port.json is two-space-indented JSON +// with one trailing newline. Both go through the atomic temp/rename writer so +// a crash mid-write can never publish a torn pid file. func (s *standaloneServer) writeRuntime() error { - if err := os.WriteFile(filepath.Join(s.home, "ocx.pid"), []byte(strconv.Itoa(s.pid)+"\n"), 0o600); err != nil { + if err := writeStateFileAtomic(filepath.Join(s.home, "ocx.pid"), []byte(strconv.Itoa(s.pid))); err != nil { return err } raw, err := json.MarshalIndent(RuntimeState{PID: int64(s.pid), Port: s.port, Hostname: s.hostname, AttestationSecret: s.secret}, "", " ") if err != nil { return err } - return os.WriteFile(filepath.Join(s.home, "runtime-port.json"), append(raw, '\n'), 0o600) + return writeStateFileAtomic(filepath.Join(s.home, "runtime-port.json"), append(raw, '\n')) } func (s *standaloneServer) Serve() error { return s.http.Serve(s.listener) } func (s *standaloneServer) Close(ctx context.Context) error { err := s.http.Shutdown(ctx) - _ = removeRuntimeRecords(s.home) + // Guarded by this server's own pid: a replacement runtime that already + // rewrote the records must not lose them to our shutdown sweep (#34). + _ = removeRuntimeRecordsFor(s.home, int64(s.pid)) return err } @@ -342,11 +352,19 @@ func runStop(args []string, deps Deps) int { fmt.Fprintln(deps.Stderr, err) return ExitFailure } - if !(osProcessInspector{}).Alive(int(state.PID)) { - _ = removeRuntimeRecords(home) + inspector := osProcessInspector{} + if !inspector.Alive(int(state.PID)) { + _ = removeRuntimeRecordsFor(home, state.PID) fmt.Fprintf(deps.Stdout, "No proxy is running (stale record for PID %d removed).\n", state.PID) return ExitOK } + if command, commandErr := inspector.Command(int(state.PID)); commandErr == nil && !isOcxStartCommandLine(command) { + // #34: the PID is alive but no longer names an OpenCodex runtime — + // the OS recycled it. Signaling it would kill an unrelated process. + fmt.Fprintf(deps.Stderr, "Recorded PID %d is alive but is not an OpenCodex runtime; refusing to stop it.\n", state.PID) + fmt.Fprintln(deps.Stderr, "Stop the actual proxy (see 'ocx status'), or remove the stale record manually.") + return ExitFailure + } client := &http.Client{Timeout: 10 * time.Second} host := strings.TrimSpace(state.Hostname) if host == "" || host == "0.0.0.0" || host == "::" || host == "[::]" { @@ -372,14 +390,14 @@ func runStop(args []string, deps Deps) int { } } deadline := time.Now().Add(8 * time.Second) - for (osProcessInspector{}).Alive(int(state.PID)) && time.Now().Before(deadline) { + for inspector.Alive(int(state.PID)) && time.Now().Before(deadline) { time.Sleep(50 * time.Millisecond) } - if (osProcessInspector{}).Alive(int(state.PID)) { + if inspector.Alive(int(state.PID)) { fmt.Fprintf(deps.Stderr, "Proxy (PID %d) did not exit after stop request.\n", state.PID) return ExitFailure } - _ = removeRuntimeRecords(home) + _ = removeRuntimeRecordsFor(home, state.PID) fmt.Fprintf(deps.Stdout, "Proxy (PID %d) stopped.\n", state.PID) return ExitOK } diff --git a/go/internal/ocxcli/runtime_server_test.go b/go/internal/ocxcli/runtime_server_test.go index 2485997b89..c8b6fff545 100644 --- a/go/internal/ocxcli/runtime_server_test.go +++ b/go/internal/ocxcli/runtime_server_test.go @@ -5,6 +5,7 @@ import ( "context" "encoding/json" "errors" + "fmt" "net" "net/http" "net/http/httptest" @@ -42,6 +43,46 @@ func TestReclaimPortRemovesStaleProcessRecords(t *testing.T) { } } +func TestReclaimPortKeepsReplacementRuntimeRecords(t *testing.T) { + // #34: a replacement start rewrote the records while the stale-owner probe + // was in flight; the guarded purge must keep the new runtime's state. + dir := t.TempDir() + if err := os.WriteFile(filepath.Join(dir, "ocx.pid"), []byte("999999\n"), 0o600); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(dir, "runtime-port.json"), []byte(`{"pid":999999,"port":10101,"hostname":"127.0.0.1"}`), 0o600); err != nil { + t.Fatal(err) + } + listener, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Skipf("loopback listeners unavailable: %v", err) + } + defer listener.Close() + // The replacement runtime rewrites both records during the stale-owner + // liveness probe, exactly as a concurrent start would. + process := &fakeProcess{alive: false, onAliveProbe: func() { + _ = os.WriteFile(filepath.Join(dir, "ocx.pid"), []byte("7\n"), 0o600) + _ = os.WriteFile(filepath.Join(dir, "runtime-port.json"), []byte(`{"pid":8,"port":10101,"hostname":"127.0.0.1"}`), 0o600) + }} + reclaimer := portReclaimer{home: dir, process: process} + if err := reclaimer.reclaim(listener.Addr().(*net.TCPAddr).Port); err != nil { + t.Fatalf("reclaim against replaced records: %v", err) + } + // The rewritten pid file names the replacement runtime (pid 7) and must + // survive: the stale snapshot (999999) authorized no deletion of it. + raw, err := os.ReadFile(filepath.Join(dir, "ocx.pid")) + if err != nil || strings.TrimSpace(string(raw)) != "7" { + t.Fatalf("replacement pid file lost or altered: contents=%q err=%v", raw, err) + } + runtimeRaw, err := os.ReadFile(filepath.Join(dir, "runtime-port.json")) + if err != nil { + t.Fatalf("replacement runtime record removed: %v", err) + } + if !strings.Contains(string(runtimeRaw), `"pid":8`) { + t.Fatalf("replacement runtime record altered: %s", runtimeRaw) + } +} + func TestReclaimPortTerminatesKnownTypeScriptOwner(t *testing.T) { dir := t.TempDir() if err := os.WriteFile(filepath.Join(dir, "ocx.pid"), []byte("42\n"), 0o600); err != nil { @@ -76,6 +117,41 @@ func TestReclaimPortRefusesForeignListener(t *testing.T) { } } +func TestReclaimPortRefusesReusedPidWhoseCommandOnlySubstringMatches(t *testing.T) { + // #34: the OS recycled the recorded PID for a test/builder process whose + // command line merely contains ocx-ish substrings. Termination must be + // refused even though the pid is alive. + dir := t.TempDir() + if err := os.WriteFile(filepath.Join(dir, "ocx.pid"), []byte("42\n"), 0o600); err != nil { + t.Fatal(err) + } + process := &fakeProcess{alive: true, command: "bun test C:/work/opencodex/tests/start-guard.test.ts"} + reclaimer := portReclaimer{home: dir, process: process} + if err := reclaimer.reclaim(10100); err == nil || !strings.Contains(err.Error(), "occupied") { + t.Fatalf("reclaim error = %v", err) + } + if process.terminated { + t.Fatal("reused pid was terminated") + } +} + +func TestReclaimPortFailsOpenWhenCommandInspectionUnavailable(t *testing.T) { + // #34 compatibility: a host where the command line cannot be read keeps + // the previous alive-PID behavior instead of wedging reclaim. + dir := t.TempDir() + if err := os.WriteFile(filepath.Join(dir, "ocx.pid"), []byte("42\n"), 0o600); err != nil { + t.Fatal(err) + } + process := &fakeProcess{alive: true, commandUnavailable: true} + reclaimer := portReclaimer{home: dir, process: process} + if err := reclaimer.reclaim(10100); err != nil { + t.Fatalf("reclaim with unavailable inspection: %v", err) + } + if !process.terminated { + t.Fatal("pid with unreadable command line was not terminated") + } +} + func TestStandaloneServerOwnsListenerDashboardHealthAndGoRoutes(t *testing.T) { home := t.TempDir() t.Setenv("OPENCODEX_HOME", home) @@ -169,17 +245,83 @@ func TestStandaloneServerApiStopDrainsAndReleasesRecords(t *testing.T) { } type fakeProcess struct { - alive bool - command string - terminated bool - signal string + alive bool + command string + commandUnavailable bool + terminated bool + signal string + onAliveProbe func() } -func (p *fakeProcess) Alive(int) bool { return p.alive } -func (p *fakeProcess) Command(int) (string, error) { return p.command, nil } +func (p *fakeProcess) Alive(int) bool { + if p.onAliveProbe != nil { + p.onAliveProbe() + } + return p.alive +} +func (p *fakeProcess) Command(int) (string, error) { + if p.commandUnavailable { + return "", errors.New("process command inspection is unavailable on this platform") + } + return p.command, nil +} func (p *fakeProcess) Terminate(_ int, signal string) error { p.terminated = true p.signal = signal p.alive = false return nil } + +func TestRunStopRefusesForeignReusedPidWithoutSignaling(t *testing.T) { + // #34: runtime-port.json names a PID the OS recycled for this test binary + // (command line "go test ...", not an ocx start command). Stop must refuse + // to signal it and preserve the record for the actual owner/operator. + home := t.TempDir() + t.Setenv("OPENCODEX_HOME", home) + record, err := json.Marshal(RuntimeState{PID: int64(os.Getpid()), Port: 10100, Hostname: "127.0.0.1", AttestationSecret: "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG"}) + if err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(home, "runtime-port.json"), record, 0o600); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(home, "ocx.pid"), []byte(fmt.Sprintf("%d\n", os.Getpid())), 0o600); err != nil { + t.Fatal(err) + } + var out, stderr bytes.Buffer + code := runStop(nil, Deps{Version: "2.42.0", Stdout: &out, Stderr: &stderr, ReadRuntime: ReadRuntime}) + if code != ExitFailure { + t.Fatalf("stop exit = %d, want failure; stderr=%q", code, stderr.String()) + } + if !strings.Contains(stderr.String(), "refusing to stop it") { + t.Fatalf("stop stderr missing refusal: %q", stderr.String()) + } + // A readable foreign process is not ours to clean up. Preserve both + // records so the actual owner/operator can inspect or remove them. + if _, statErr := os.Stat(filepath.Join(home, "runtime-port.json")); statErr != nil { + t.Fatalf("runtime record was removed on refusal: %v", statErr) + } + if _, statErr := os.Stat(filepath.Join(home, "ocx.pid")); statErr != nil { + t.Fatalf("pid record was removed on refusal: %v", statErr) + } +} + +func TestRunStopClearsStaleRecordForDeadPid(t *testing.T) { + home := t.TempDir() + t.Setenv("OPENCODEX_HOME", home) + record, err := json.Marshal(RuntimeState{PID: 999999, Port: 10100, Hostname: "127.0.0.1", AttestationSecret: "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG"}) + if err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(home, "runtime-port.json"), record, 0o600); err != nil { + t.Fatal(err) + } + var out, stderr bytes.Buffer + code := runStop(nil, Deps{Version: "2.42.0", Stdout: &out, Stderr: &stderr, ReadRuntime: ReadRuntime}) + if code != ExitOK || !strings.Contains(out.String(), "stale record") { + t.Fatalf("stop dead record = code %d out %q stderr %q", code, out.String(), stderr.String()) + } + if _, statErr := os.Stat(filepath.Join(home, "runtime-port.json")); !errors.Is(statErr, os.ErrNotExist) { + t.Fatalf("stale runtime record kept: %v", statErr) + } +} diff --git a/go/internal/ocxcli/status_diagnostics.go b/go/internal/ocxcli/status_diagnostics.go index bbe67763b5..7ea42914b8 100644 --- a/go/internal/ocxcli/status_diagnostics.go +++ b/go/internal/ocxcli/status_diagnostics.go @@ -28,12 +28,13 @@ type StatusRuntimeRecord struct { // StatusHealth is the public, secret-free healthz projection used by status. type StatusHealth struct { - OK bool - URL string - Message string - PID int64 - Version string - Uptime float64 + OK bool + URL string + Message string + PID int64 + PIDPresent bool + Version string + Uptime float64 } // StatusProbe is the shared, minimal liveness evidence required by the future @@ -152,6 +153,7 @@ func probeStatusHealth(port int, hostname string, client *http.Client) StatusHea return result } result.OK, result.PID = true, int64(pid) + _, result.PIDPresent = body["pid"] versionText := "" if versionOK { result.Version = version diff --git a/go/internal/ocxcli/status_domains.go b/go/internal/ocxcli/status_domains.go index 32d3b98600..69692fa629 100644 --- a/go/internal/ocxcli/status_domains.go +++ b/go/internal/ocxcli/status_domains.go @@ -309,15 +309,7 @@ func readStatusPIDFile() int64 { if err != nil { return 0 } - raw, err := os.ReadFile(dir + string(os.PathSeparator) + "ocx.pid") - if err != nil { - return 0 - } - pid, err := strconv.ParseInt(strings.TrimSpace(string(raw)), 10, 64) - if err != nil || pid <= 0 { - return 0 - } - return pid + return readIdentityCheckedPID(filepath.Join(dir, "ocx.pid"), osProcessInspector{}) } // CollectStatusDomains mirrors status's no-live-proxy branch. It is a @@ -333,16 +325,38 @@ func CollectStatusDomains(deps StatusDomainDeps) StatusDomains { pid := deps.ReadPID() port, hostname := cfg.ListenTarget() source := "config" - liveRuntime := false - if runtime, err := deps.ReadRuntime(); err == nil && pid > 0 && runtime.PID == pid { + var runtimeRecord *StatusRuntimeRecord + if runtime, err := deps.ReadRuntime(); err == nil { + runtimeCopy := runtime + runtimeRecord = &runtimeCopy port, hostname, source = runtime.Port, runtime.Hostname, "runtime" - liveRuntime = true } client := &http.Client{Timeout: 800 * time.Millisecond} if deps.HTTPClient != nil { client = deps.HTTPClient } health := probeStatusHealth(port, hostname, client) + liveRuntime := false + if source == "runtime" && health.OK && runtimeRecord != nil && (!health.PIDPresent || health.PID == runtimeRecord.PID) { + liveRuntime = true + if health.PIDPresent { + pid = health.PID + } else { + pid = 0 + } + } else if source == "runtime" { + port, hostname = cfg.ListenTarget() + source = "config" + runtimeRecord = nil + health = probeStatusHealth(port, hostname, client) + } + if source == "config" && health.OK { + if health.PIDPresent { + pid = health.PID + } else { + pid = 0 + } + } var pidValue *int64 if pid > 0 { pidValue = &pid @@ -367,13 +381,17 @@ func CollectStatusDomains(deps StatusDomainDeps) StatusDomains { proxyVersion = health.Version } healthMessage := health.Message - if health.OK && liveRuntime { - healthMessage = "ok (pid " + strconv.FormatInt(pid, 10) + ")" + if health.OK && source == "runtime" && liveRuntime { + pidText := "unknown" + if health.PIDPresent { + pidText = strconv.FormatInt(pid, 10) + } + healthMessage = "ok (pid " + pidText + ")" } return StatusDomains{ SchemaVersion: 1, Proxy: StatusProxyDomain{ - Running: pid > 0 && health.OK, + Running: (source == "runtime" && liveRuntime) || (source == "config" && health.OK), PID: pidValue, Health: StatusHealthDomain{OK: health.OK, URL: health.URL, Message: healthMessage}, }, diff --git a/go/internal/ocxcli/testdata/pid-parse-oracle.tsv b/go/internal/ocxcli/testdata/pid-parse-oracle.tsv new file mode 100644 index 0000000000..b8357d0b61 --- /dev/null +++ b/go/internal/ocxcli/testdata/pid-parse-oracle.tsv @@ -0,0 +1,406 @@ +"/952\t2b\t\n/\"b-y.0./5" null +"9c1'" null +"\"2x3\t9yb10" null +"+1663a176y074a" null +"/\n37b01+3z30\n2-\"" null +"1/026y 09/\n\tx1b5'0-/\t" null +"../\n'16\t3+" null +"'0+" null +"\n6-b\"\n-\n\n\n28+0y" null +"-" null +"3 x1z\n/z.337255y6zx4 a" null +".0a" null +"\t3a66\ny" null +"b.a2..08258 .454 " null +"5\t\ty+b-5+'81\tyy4y\n" null +"xz \"-3/z5" null +"b" null +"/5+43" null +"/+zb\t'\nby\t32+0" null +"6\ny" null +"70\"a\"'51/.\tzx-\n82" null +"y\t63yy24\t3y0051' 9" null +"1/02\tazz44\t\"642bcab\"4" null +"-9\n+.407257a6y3/y1+5" null +"4\t6'y2" null +"+yc9'4x\n/a857c\t28y1c5 +" null +"9" 9 +"b2yc3-5b- +'14\"" null +".-zy6c8" null +".91\"-027c3-7 \t39caz" null +"-.5/7a\"yb4" null +"8+" null +"y8 1\n" null +"cca\n-z2//62 '5\t-5\ta4by/\n" null +".b7\n9cb" null +"+\n15 22/0 x\ny6y1bxb49 " null +" '\t73\t6\tb.z'c'91\t" null +"xa7\t4544b.+z3/3za\t3a" null +"5x+ 6\"9/4\"91-1.\"\ncc11\nz" null +"/.0\"\n'b.'yya//+z/4" null +"8yy'bxay52\t.7b9-x'\n" null +"'/\n\"+1ax7a859'6 7b3" null +"8 xc5bx\t" null +"\"cz /\"2.9y/" null +"5b9\n/1b\n61'841/\n7b0" null +"\n89158+'82-x/4z-z 05" null +"z0 aa" null +"3+c.064" null +"1c9x489x013" null +"5z.99 z4c179\nbx8\t9" null +"75ccbz7\nx6xy6'" null +" 847." null +"'55-9acc\"z'" null +"05c4 " null +"-b " null +"cba68\t9'z36 c1\n7x26--" null +".9\"0\"\"\tc\t\"68/" null +"1'5/5/\"xa6a0x0a." null +"7-85z0+" null +"624a83c'4x\n0\tz\"1/5c5" null +"\n.y7" null +"49//a\t\n7b95" null +"+xb\t-584" null +"b6.33 2\"0y +\n'" null +"3260y+ x'-484y'\"" null +"yx/\"" null +"z6a30+ 32ya3+0b3-" null +"0x ay04\nx4y9x3.z" null +"y71\t" null +"5468" 5468 +"x\ta891-1y8b/41y0\"z" null +"-\t74a0-0z02+6cz3c0xa6" null +"9\t0" null +"az9 8 ya" null +".z9+'/" null +"\t0+z7.4x83c1a\tbc138442" null +"9+3" null +"a4'zc50b74+c+x" null +"b/-'1\n." null +".9a7xc\n4y' " null +"x" null +"1 4483x54" null +"y'1z'-yx/ 16\"'4\n'4.6+2" null +"6+\"796" null +"955x100x7" null +"\"3-95" null +"4" 4 +"cyy9285-1ab\t3\n157.0x" null +" 997.'75-" null +"2\nxy0'cb.zbc31y8yy-z6\n" null +"8a7+2/z.'" null +"3'y7c 941\t" null +"\nx x804a29b z9.6" null +"3-" null +"y/08.4x7a.9\tb+" null +"0/03za\n" null +"\tc85\"ay/4.. +-7cb-1\tazb/" null +"6389\"57x38'32" null +"0 214ab18" null +"905za3b3\t7.5ba.6\nb39" null +"yb3.c3\"-3+\nb6\t" null +"\txz.2ya" null +"293b8\"2 " null +"\n2\"\"6\t" null +"y44a8-8x1'65." null +"\t8+z9x6cy" null +" 7xc3ax0x165-\t" null +"z9" null +"xc.8za'b3a+\t836\n6y" null +"-6 x" null +"+ +.z\n3b" null +"yxx/951\"+80-98z61\"42" null +"0+xc 855973.\t035b24" null +"/1\" 7/53/205470" null +"/6.x\ty\n24\tza2z8\n" null +"\n7a\nza\n 88 617a23\n" null +"8653\n35 x76a\t" null +"\t8" 8 +"a\n7a\n5581y\"" null +"'75" null +"-1" null +"+/83\n\"/4x7b.zc\t'938xz607" null +"yx.\n'c5 \"+\t871z6bx" null +"50y//2 \t8b 9a\taz\t137'2" null +"\"x.\n14-.+'b4\nc" null +"\n1y9-46.8+ 1/\"x" null +"79-\n\t.215\t150\n0/b8x1b" null +" .77 a6+b''-z5ac4" null +"a/ a 6a" null +"74xy6777" null +"0ca67" null +"\nb5\n2/\"7+58c6" null +"'61c\tb .9\"3\n\t\n73 x89x7" null +"5y47\n6ya8 3" null +"9'\"573a09by.x" null +"039x6'x\t/\n+99. 'b7-79az" null +"-019 82 63 9\"x5" null +"7 \"\t" null +"/29x623\"--" null +"/1+x'" null +"'5'az /5b " null +"-+b0\"/zx45" null +".+2y 8\"y" null +"3" 3 +"z4z8 yya016x\t\"-" null +"2azx2+4" null +"7++b\n.8y.5x57/a.635+323" null +".+'\n" null +"87x6\"xc6\nx5752y2-abx" null +"2.6/.+-+.by\n 7z.93y3" null +"b1b2/b/34.5-c" null +"+2\"99." null +"5+\t33426'z929y0/y\tb+" null +"'6\"+50'55c+c84706." null +"2c43\t-x\n0c0z'\n9\t3cz19\"" null +"c57-cz\n" null +" xyz41 \"8zb1b2\t\"a7\t36ac/" null +"x'y\t \"0z.zy" null +"17+0186\n/'a" null +"\n67\t-\n\t2c+\"-8/" null +"z\"z.'52" null +" 9/\n 5'5/.9x-1'6y" null +"-yz/8xz550./." null +"\"90c9ba5" null +"z1\"b/yb578c1\"\t8c\"86" null +"6b" null +"5" 5 +"b" null +"0- +77x4 3\"" null +"2++0'\nc560c" null +"1" 1 +"9" 9 +"a'" null +"9\"06\n9\t\t-2\"..\n" null +"0\t+974\n\t/\t039+ zyb67/9" null +"." null +"\t2'4" null +".y23y37-+z0'5b/-" null +"62y95z 'z" null +"x\"1\"zz0cz+b/.y\t+0757/" null +"a1" null +"\t50282acy.4c'" null +"'0\n'z7/ xy\n" null +"a" null +".4y5-3c27" null +"78225 1c\t3\"x539\t" null +"6-\nb/xy2+.x\n." null +" +czycy-" null +"6ba7cy5c/ b+/" null +"84\"y1\t286a\nx9+/\"/' c0 \"6" null +"5cy57\n6'\t+020\n..-" null +"+'a4+44/4b544\"z7.." null +"2" 2 +"\t46\"6\n\"49\t\nzz" null +"8+y3\n" null +"1x8c37/x75/xy36c.8-" null +"x4cy\"'-5/'.7\n+zx1\n" null +"4z-2za2cx796a8a -7048/5z" null +"8x x19.'/3yc.211" null +"202y.b\n" null +"-ay98\n yyc.1" null +"a\n78yaz82aycz/'2x+z0'\"\t" null +"cz\"" null +"5c \n0 4zx" null +"x3/0.8y- 2/2" null +".\t0b8\n667c28b2z'a\t\n-" null +"5" 5 +"+.7 .7x5z5+ /65641a88-0" null +"'70b6 8x\"x\ty\"z+z" null +"89c14\n+c95z\n" null +" x7zx2a3+7 \n4'a8y\n" null +"..99ca7+/8." null +"0x/7a3- 1 /8y5+4ab.34c/2" null +"38b580\ta\tc.1/zz+-'6/" null +" 14ya\ny-30y3x" null +"'1\tz\n4a8 x5''-5c\"b6017'" null +"6\n\t4cybb\"54\n b\"" null +"6807cc2.\"x' /3\n1'x4z a.1" null +"0\n7x3\".005.\"30byzx3\t57/'" null +"396" 396 +"c1ac\t80054" null +"-/4 a" null +"19- -59" null +"z+a690\t50-9z\" +y" null +"bac6a9-3 z5\t82113\n9" null +"c 70-z1463/\n.4" null +"c.a'1xxb30b\n+467+.c6+y" null +"a" null +"y'\"\"7xz\tax.a7" null +"2y\"8\"bxz\n9-7..484 91a" null +" 7" 7 +"\nxb246y79a624bzz." null +"\"037x7+53-z'1y\t6+\n3\"1\nz." null +"20yx7c4-cb7.6c2\"" null +"00 bc\t+\"\n631 c\"z7'565 6/" null +"c\na" null +"-0./x2c\n7/\nb/ 16c" null +"1z0\t204b2 by" null +"3-14b8+a83x+90\tazx4'/x" null +"4" 4 +"a.452\t8yx65" null +"6z36-" null +"5\n55z3.c4\ny\"06a/" null +"b3c..0b+36'zc+5z628" null +"-\t8c74'z\n \n" null +"8x/2.8\ncc7\"ya-+29777.ax" null +"abc\"-b936" null +"928\"6-98c-+\t07'4a5" null +"64\n\n1z\nay99'\n-.- x. c-" null +"\"2 1z\" 0x.\t\"48\"'x3\n6" null +"5\t-5z230za\"" null +"4/ 2y/\tccxx17.8" null +"a" null +"y .'+0\ta.0-" null +"8-a/8/a6" null +"57'48/\ny\".z24z6\nz-42" null +" 9x8-a5b\"" null +"/a/\t56- 92+9." null +"7\"0y 69+7y8++6/1c" null +"z" null +"a\n yx67'" null +"y8+c3\t\txy\t /+\n7\"\n\"1'" null +"x\n62zxx \t\t5\n.2\n06c24'b" null +"6778b\n4" null +".6ya0-/z25a6/.\"z4\"\"6\"\n\n" null +"6x854 55'\n5a\"''6\"xca5\t\t0" null +" 1 zx49\n\nx" null +"\n11aa-4y" null +"55+632+a -c3c" null +"z'\n'b\"778/23xz.5''4\n1" null +"1 x.y56zy377\t'z70" null +"5y\" -" null +"0" null +"78+za" null +" 6/c61c21xa'a\"4\n-24+y87" null +"1" 1 +"\"ca'x4.c-.-\n\"88za9\"" null +"5xb9x+\"b\"y\n+" null +"34" 34 +"/2z153" null +"73 z\"82/ 8a" null +"\t'b/\"61-+25" null +"z184/\t\t-. \ncc35b\"\"70" null +" \nz6b'4\t603y8/" null +"'\"" null +"bxy18/" null +" 9" 9 +"3'59 " null +"x'\" z201+xb9" null +".y7'a'\t93" null +"'479/8\t00" null +"1-177+' 6\na'" null +"+\t8'7a40.0" null +"-9y\nx'\"c\n 0xc\t4\n \t40" null +"99\"-.8+8bcbcy- 2y27-b7" null +"9\t2z\t/" null +"0b++7-\"xax0\tx\t0y+36" null +"y45 ab2\t+422578/.6\n0" null +" zzz4x746" null +"aa-z3z/y9067+''az" null +"5'z +b\n\t99" null +"09\"yyy27x5" null +"-\tc9722xb3\t4" null +"36c02+0\"95" null +"4a5.9/692" null +"\t8 +\t/a1\n39/58" null +".-3//2+x4z91'\"49" null +"cb5x4z/ 2\n.-\"3x80y\t'.77" null +"x82 782z" null +"\"7\"2-+\"'+51-8/6'z+" null +"\"b2'-b' \n9-02" null +"\"a\t64x314" null +"'42a2ba.z 5/740-cx+45" null +"88z\n8\n0.z\t8.\"-/7205\tz8\t\"" null +"8c \"5y-579" null +"4265yy06y\nc\n1\"4323.c " null +"yyy\"b4/c/0\"9/" null +"1+y9z5+6" null +"\"/" null +"b\"/9acb36x495\n\n-3" null +"5c70'98/71" null +" 5\t 7b253a39/x1" null +"'x.\"c4'2+a81\t 0.9" null +"/ -9 c65 7a92z -'\n-0 6'" null +" y1\t5c-\"cc0\"2575/\nyc2" null +"x 09/3\n67a-y \t3.7" null +"37y \"+cc28zx'\"c" null +"8/\t/\ty254\t30" null +"\"y3\tb5" null +"'\t3/573\"8y9c4..+2c\nz/2" null +"c''cc+ -" null +"5.c552a2'//.y5'37'\n\"32/+" null +".b30" null +"\"55.y3b1\"" null +"08" 8 +"b+027z." null +"0.\t\t127 " null +"3x6z2+." null +"96x6/2\t/7a92a\taa0'" null +" \nb6\n8534c35c" null +"6' b7a.1bc\tx595'466b\t+" null +"+9a9/81'b\nc5+9" null +"35'" null +"+9 1\t\n88\n11'5.+" null +"x\n21y7'/0\"9\"+-y\ncz" null +"93yz/8c\"" null +"'b\"a3027\"1" null +"\t1 y4+b 5" null +"\n9\t5 a32-3bz4-\"a\t3'67" null +"6546+-\"cx0\n'282b\n+/5" null +"73 x+..by-\n7a" null +"\t52/x3-ac793a " null +"\"5\t99c772c45\n2b\"" null +"1a'z-\t.3axyc6/" null +"3\"\t3.6\n0c\"-24 5631" null +" 5 z8\t-4\"" null +"\t+y16a82 \"\t'8/y5x6.zx" null +"'4x\"yx2z/" null +"\n 91'9z9 2y'3" null +"389-07c6.\n0 010" null +"6/\t5+ab0y" null +"a" null +"5zz.\n17b'.++.\t ." null +"5.c\t" null +"a4\t8\n" null +"+\nz6'2y+17" null +"ay\"32-b +69871c" null +"-471-09'b6\n8'" null +"5. .3+\"'0 1.x+17\n ++" null +"'\n123 .\n\t78z\t0+\tzx03" null +"5c.\t7y69/\t\n.a3" null +"'0.\n\tc4+/b9\"c3\"3" null +"26c\t+8\nx8" null +"9+1 a'z.51 2 0881'81\"" null +"06b+0+-5" null +" 7 4/x-" null +".c4\ta+b9091z8y-\"7.b-" null +"15xz'8cx-y" null +" 74\nx6+z" null +"/.006x5b" null +" \n'\tz\"05xxy3\n\n" null +"67/+\t" null +" 8by3" null +"\t.x" null +"-c-xa1c3.53\"yc7" null +"6907460\t6/' \nz4118a+bc" null +"0b\n\n7\n0y8z+3b5515' " null +"c\tbx02\nc\t80\" c87b13.5a" null +"3x75\n79y6b7a71\n0\n\"3526\"" null +"/3240.53\n3 c" null +"y96" null +"\n\n343+" null +"a\"" null +"\t1+z'-65" null +"91'4\t9" null +"y0x2\tb61.'ca'b/'9" null +"\ny4y+a9a" null +"x0" null +".\nb58+4cy8" null +"''z'6.b5z76z78861735" null +"9007199254740991" 9007199254740991 +"9007199254740992" null +"9007199254740993" null +"9223372036854775807" null +"9223372036854775808" null +"000000000000005" 5 \ No newline at end of file diff --git a/tests/process-state-go-parity.test.ts b/tests/process-state-go-parity.test.ts new file mode 100644 index 0000000000..205f627eba --- /dev/null +++ b/tests/process-state-go-parity.test.ts @@ -0,0 +1,85 @@ +import { describe, expect, test } from "bun:test"; +import { mkdtempSync, readFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join, resolve } from "node:path"; +import { isOcxStartCommandLine, parsePidFile } from "../src/config/process-state"; + +const repoRoot = resolve(import.meta.dir, ".."); +const goRoot = join(repoRoot, "go"); + +function goToolchainAvailable(): boolean { + return Bun.spawnSync(["go", "version"], { stdout: "ignore", stderr: "ignore" }).success; +} + +function buildSidecarBinary(): string { + const dir = mkdtempSync(join(tmpdir(), "ocx-go-process-state-")); + const binary = join(dir, process.platform === "win32" ? "ocx-sidecar.exe" : "ocx-sidecar"); + const build = Bun.spawnSync(["go", "build", "-o", binary, "./cmd/ocx-sidecar"], { + cwd: goRoot, + env: { ...process.env, CGO_ENABLED: "0" }, + stdout: "pipe", + stderr: "pipe", + }); + if (build.exitCode !== 0) { + throw new Error( + `go build ./cmd/ocx-sidecar failed (${build.exitCode}):\n${new TextDecoder().decode(build.stderr)}`, + ); + } + return binary; +} + +function readParseVectors(): string[] { + return readFileSync(join(goRoot, "internal", "ocxcli", "testdata", "pid-parse-oracle.tsv"), "utf8") + .split("\n") + .filter(line => line.length > 0) + .map(line => { + const separator = line.indexOf("\t"); + if (separator < 0) throw new Error(`invalid PID oracle row: ${line}`); + return JSON.parse(line.slice(0, separator)) as string; + }); +} + +const matcherVectors = [ + "bun run src/cli.ts start", + '"C:/tools/bun/bin/bun.exe" "run" "src/cli/index.ts" "start"', + "bun C:/tools/bun/install/global/node_modules/@bitkyc08/opencodex/src/cli.ts start", + "bun C:/tools/bun/install/global/node_modules/@bitkyc08/.opencodex-3f2a/src/cli/index.ts start", + "opencodex start", + "C:/Users/u/AppData/Roaming/npm/ocx.cmd start", + "/usr/local/bin/ocx start --port 10100", + "/home/u/.opencodex/bin/opencodex start", + "bun run src/cli.ts status", + "bun test C:/work/opencodex/tests/config.test.ts", + "notepad.exe", + "ocx start-guard", + "ocx /start", + "python server.py --flag ocx startx", + "C:/work/opencodex-start-guard/bin/server.exe", + "bun run src/cli.ts restart", +]; + +const describeGo = goToolchainAvailable() ? describe : describe.skip; + +describeGo("Go process-state differential oracle (ticket #34)", () => { + test("matches TypeScript PID parsing and command identity", () => { + const binary = buildSidecarBinary(); + const parse = readParseVectors(); + const input = JSON.stringify({ parse, match: matcherVectors }); + const result = Bun.spawnSync([binary, "processstatecheck", input], { + env: { ...process.env, CGO_ENABLED: "0" }, + stdout: "pipe", + stderr: "pipe", + }); + if (result.exitCode !== 0) { + throw new Error( + `ocx-sidecar processstatecheck failed (${result.exitCode}):\n${new TextDecoder().decode(result.stderr)}`, + ); + } + const output = JSON.parse(new TextDecoder().decode(result.stdout)) as { + parse: number[]; + match: boolean[]; + }; + expect(output.parse).toEqual(parse.map(value => parsePidFile(value) ?? 0)); + expect(output.match).toEqual(matcherVectors.map(value => isOcxStartCommandLine(value))); + }, { timeout: 120_000 }); +}); From 5a7cdc88bf9c0a57ce59514fa74b9ef462dcd2d4 Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Mon, 7 Sep 2026 17:45:07 +0800 Subject: [PATCH 111/165] fix(go): embedded dashboard serves live gui/dist with thin fallback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The embedded dashboard snapshot under go/internal/embeddedui/static was a 3.1MB Vite minified bundle checked into git (issue #40's single-binary work). It tripped the deterministic PR hygiene gate (empty_catch has no label escape, and generated build output is never committed — the repo ignores gui/dist for the same reason) and was already stale relative to the tree's own gui/dist. Rework the embed to mirror the TypeScript runtime (src/server/gui-static.ts findGuiDist): the handler serves a live dashboard build from /gui/dist when the binary runs from a checkout or packaged tree that carries one, and falls back to a thin hand-written page that health-checks the management API. static/ now carries source assets only (the fallback page and the gui/public icon mirrors); generated Vite output is staged into a gitignored assets/ dir by scripts/sync-go-embedded-dashboard.sh ahead of release builds, so the release ocx artifact still embeds the full dashboard. Updates embeddedui tests for the resolution contract (live overlay takes precedence; unknown dist assets 404 rather than shadowing the fallback; mirrored icons serve from the embed), and tightens the traversal guard to reject any raw path containing ".." before path.Clean collapses it. Co-Authored-By: Claude Code --- .gitignore | 7 + go/internal/embeddedui/embeddedui.go | 207 +++++++++++++----- go/internal/embeddedui/embeddedui_test.go | 143 ++++++++++-- .../static/assets/index-BU1tE0sr.js | 112 ---------- .../static/assets/index-DL9-iS6J.css | 1 - go/internal/embeddedui/static/index.html | 77 ++++++- scripts/sync-go-embedded-dashboard.sh | 26 ++- 7 files changed, 372 insertions(+), 201 deletions(-) delete mode 100644 go/internal/embeddedui/static/assets/index-BU1tE0sr.js delete mode 100644 go/internal/embeddedui/static/assets/index-DL9-iS6J.css diff --git a/.gitignore b/.gitignore index e0982e1afb..8e42bfca59 100644 --- a/.gitignore +++ b/.gitignore @@ -64,3 +64,10 @@ tests/.tmp-* # `go build -o bin` cannot be committed. Nothing in `src/` imports `go/` at # runtime — the TypeScript server spawns the built binary as a child process. go/bin/ + +# Embedded dashboard build output (ADR-0008). go/internal/embeddedui/static/ +# carries only hand-written source assets; the release Vite build is read from +# gui/dist at runtime and must never be committed into the embed tree (the +# deterministic PR hygiene gate rejects generated build output, matching the +# repository-wide gui/dist convention). +go/internal/embeddedui/static/assets/ diff --git a/go/internal/embeddedui/embeddedui.go b/go/internal/embeddedui/embeddedui.go index f255470c69..dee06be006 100644 --- a/go/internal/embeddedui/embeddedui.go +++ b/go/internal/embeddedui/embeddedui.go @@ -1,9 +1,15 @@ -// Package embeddedui serves the dashboard baked into a release ocx binary. +// Package embeddedui serves the dashboard for the standalone ocx runtime. // -// static is checked in as the release snapshot. scripts/build-go-release-artifact.sh -// refreshes it from gui/dist before a release build. Keeping a snapshot in-tree -// is intentional: go build must remain deterministic and usable by contributors -// and CI that do not have Bun or the GUI dependency tree installed. +// Resolution order mirrors the TypeScript runtime (src/server/gui-static.ts +// findGuiDist): a live dashboard build under /gui/dist is served when the +// binary runs from a checkout or a packaged tree that carries it; otherwise the +// binary falls back to the small static page embedded below. +// +// static/ carries hand-written source assets only (the thin fallback page and +// the provider-icon set mirrored from gui/public). Generated Vite build output +// is never checked in here — it must not enter git history (the repository +// ignores gui/dist for the same reason) — so a checkout's gui/dist, refreshed +// by the release build, is read from disk instead. package embeddedui import ( @@ -13,7 +19,9 @@ import ( "io/fs" "mime" "net/http" + "os" "path" + "path/filepath" "strings" "time" ) @@ -21,67 +29,156 @@ import ( //go:embed static var files embed.FS -// NewHandler returns the complete self-contained dashboard HTTP surface. The -// caller supplies its version because release builds stamp it with ldflags. -func NewHandler(version string) http.Handler { - root, err := fs.Sub(files, "static") +// Handler serves the dashboard HTTP surface. The caller supplies its version +// because release builds stamp it with ldflags. +type Handler struct { + version string + // findDist returns the absolute path of a live dashboard build to serve, or + // "" when the embedded fallback should be used. nil means the default + // resolver (an upward search from the working directory for gui/dist). + findDist func() string +} + +// NewHandler returns the complete dashboard HTTP surface. The caller supplies +// its version because release builds stamp it with ldflags. +func NewHandler(version string) *Handler { + return &Handler{version: version} +} + +// NewHandlerWithResolver is NewHandler with an explicit dashboard-build +// resolver; tests use it to point at a synthetic build without touching the +// working directory. +func NewHandlerWithResolver(version string, findDist func() string) *Handler { + return &Handler{version: version, findDist: findDist} +} + +func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet && r.Method != http.MethodHead { + w.Header().Set("Allow", "GET, HEAD") + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + if r.URL.Path == "/healthz" { + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(map[string]any{ + "status": "ok", "service": "opencodex", "version": h.version, + "uptime": 0, "pid": 0, "port": 0, + }) + return + } + if dist := h.dashboardDir(); dist != "" && serveFrom(dist, w, r) { + return + } + serveEmbedded(w, r) +} + +// dashboardDir resolves the runtime dashboard build to serve, if any. +func (h *Handler) dashboardDir() string { + if h.findDist != nil { + return h.findDist() + } + return findGuiDist() +} + +// findGuiDist walks upward from the working directory looking for a gui/dist +// with an index.html, mirroring findGuiDist() in src/server/gui-static.ts. A +// release binary executed from a packaged install finds nothing and falls back +// to the embedded page. +func findGuiDist() string { + dir, err := os.Getwd() if err != nil { - panic(err) + return "" } - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodGet && r.Method != http.MethodHead { - w.Header().Set("Allow", "GET, HEAD") - http.Error(w, "method not allowed", http.StatusMethodNotAllowed) - return - } - if r.URL.Path == "/healthz" { - w.Header().Set("Content-Type", "application/json") - _ = json.NewEncoder(w).Encode(map[string]any{ - "status": "ok", "service": "opencodex", "version": version, - "uptime": 0, "pid": 0, "port": 0, - }) - return - } - name, spa := embeddedName(r.URL.Path) - if name == "" { - http.NotFound(w, r) - return + for { + candidate := filepath.Join(dir, "gui", "dist") + if info, statErr := os.Stat(filepath.Join(candidate, "index.html")); statErr == nil && !info.IsDir() { + return candidate } - body, err := fs.ReadFile(root, name) - if err != nil && spa { - name, body, err = "index.html", nil, nil - body, err = fs.ReadFile(root, name) + parent := filepath.Dir(dir) + if parent == dir { + return "" } - if err != nil { - http.NotFound(w, r) - return - } - contentType := mime.TypeByExtension(path.Ext(name)) - if contentType == "" { - contentType = "application/octet-stream" - } - if strings.HasSuffix(name, ".html") { - contentType = "text/html; charset=utf-8" - } - w.Header().Set("Content-Type", contentType) - if strings.HasSuffix(name, ".html") { - w.Header().Set("Cache-Control", "no-store") - } else { - w.Header().Set("Cache-Control", "public, max-age=31536000, immutable") + dir = parent + } +} + +// serveFrom serves one file from a live dashboard build directory. It reports +// whether the request was fully handled (a file existed, or an extensionless +// path resolved to the build's index.html). +func serveFrom(dist string, w http.ResponseWriter, r *http.Request) bool { + name, spa := embeddedName(r.URL.Path) + if name == "" { + http.NotFound(w, r) + return true + } + body, err := os.ReadFile(filepath.Join(dist, filepath.FromSlash(name))) + if err != nil && spa { + body, err = os.ReadFile(filepath.Join(dist, "index.html")) + if err == nil { + name = "index.html" } - w.Header().Set("X-Content-Type-Options", "nosniff") - if r.Method == http.MethodHead { - return + } + if err != nil { + if os.IsNotExist(err) { + return false } - http.ServeContent(w, r, name, time.Time{}, bytes.NewReader(body)) - }) + http.NotFound(w, r) + return true + } + serveBytes(w, r, name, body) + return true +} + +func serveEmbedded(w http.ResponseWriter, r *http.Request) { + name, spa := embeddedName(r.URL.Path) + if name == "" { + http.NotFound(w, r) + return + } + body, err := fs.ReadFile(files, path.Join("static", name)) + if err != nil && spa { + name, body, err = "index.html", nil, nil + body, err = fs.ReadFile(files, path.Join("static", name)) + } + if err != nil { + http.NotFound(w, r) + return + } + serveBytes(w, r, name, body) +} + +func serveBytes(w http.ResponseWriter, r *http.Request, name string, body []byte) { + contentType := mime.TypeByExtension(path.Ext(name)) + if contentType == "" { + contentType = "application/octet-stream" + } + if strings.HasSuffix(name, ".html") { + contentType = "text/html; charset=utf-8" + } + w.Header().Set("Content-Type", contentType) + if strings.HasSuffix(name, ".html") { + w.Header().Set("Cache-Control", "no-store") + } else { + w.Header().Set("Cache-Control", "public, max-age=31536000, immutable") + } + w.Header().Set("X-Content-Type-Options", "nosniff") + if r.Method == http.MethodHead { + return + } + http.ServeContent(w, r, name, time.Time{}, bytes.NewReader(body)) } func embeddedName(requestPath string) (name string, spa bool) { - cleaned := path.Clean("/" + requestPath) - if strings.Contains(requestPath, "\\") || strings.Contains(cleaned, "..") { + // Path traversal must be refused on the RAW path: path.Clean collapses + // "/provider-icons/../x" to "/x" before any ".." check can see it, and the + // join with the embed or dist root happens after this guard. Both the + // literal form and the URL-encoded form are rejected by net/http before a + // request reaches a handler (r.URL.Path is decoded), so checking the + // decoded path here is sufficient. + if strings.Contains(requestPath, "\\") || strings.Contains(requestPath, "..") { return "", false } + cleaned := path.Clean("/" + requestPath) name = strings.TrimPrefix(cleaned, "/") if name == "" { return "index.html", false diff --git a/go/internal/embeddedui/embeddedui_test.go b/go/internal/embeddedui/embeddedui_test.go index 3944cc5bce..f9bc1d30cc 100644 --- a/go/internal/embeddedui/embeddedui_test.go +++ b/go/internal/embeddedui/embeddedui_test.go @@ -1,41 +1,156 @@ package embeddedui import ( + "io" "net/http" "net/http/httptest" + "os" + "path/filepath" "strings" "testing" ) -func TestHandlerServesEmbeddedDashboardAndHealth(t *testing.T) { - handler := NewHandler("9.9.9") - for _, test := range []struct{ path, wantType, wantBody string }{ - {"/", "text/html", "opencodex"}, - {"/dashboard/providers", "text/html", "opencodex"}, - {"/healthz", "application/json", "\"service\":\"opencodex\""}, +func get(t *testing.T, handler http.Handler, path string) *httptest.ResponseRecorder { + t.Helper() + response := httptest.NewRecorder() + handler.ServeHTTP(response, httptest.NewRequest(http.MethodGet, path, nil)) + return response +} + +// fallbackHandler pins the resolver to "no live build" so fallback tests stay +// hermetic even when the checkout carries a gui/dist build. +func fallbackHandler() http.Handler { + return NewHandlerWithResolver("9.9.9", func() string { return "" }) +} + +func TestFallbackServesThinDashboardAndHealth(t *testing.T) { + handler := fallbackHandler() + for _, test := range []struct{ path, wantBody string }{ + {"/", "opencodex proxy dashboard"}, + {"/dashboard/providers", "opencodex proxy dashboard"}, // SPA fallback to index.html } { - request := httptest.NewRequest(http.MethodGet, test.path, nil) - response := httptest.NewRecorder() - handler.ServeHTTP(response, request) + response := get(t, handler, test.path) if response.Code != http.StatusOK { t.Fatalf("%s status = %d", test.path, response.Code) } - if !strings.Contains(response.Header().Get("Content-Type"), test.wantType) { + if !strings.Contains(response.Header().Get("Content-Type"), "text/html") { t.Fatalf("%s content type = %q", test.path, response.Header().Get("Content-Type")) } if !strings.Contains(response.Body.String(), test.wantBody) { t.Fatalf("%s body = %q", test.path, response.Body.String()) } } + health := get(t, handler, "/healthz") + if health.Code != http.StatusOK || !strings.Contains(health.Body.String(), "\"service\":\"opencodex\"") { + t.Fatalf("/healthz = %d %q", health.Code, health.Body.String()) + } +} + +func TestFallbackServesProviderIconsFromMirroredSource(t *testing.T) { + handler := fallbackHandler() + for _, path := range []string{"/favicon.png", "/provider-icons/openai.svg", "/icons.svg"} { + response := get(t, handler, path) + if response.Code != http.StatusOK || response.Body.Len() == 0 { + t.Fatalf("GET %s = %d len=%d", path, response.Code, response.Body.Len()) + } + } +} + +func TestLiveDashboardOverlayTakesPrecedence(t *testing.T) { + dist := t.TempDir() + assets := filepath.Join(dist, "assets") + if err := os.MkdirAll(assets, 0o755); err != nil { + t.Fatal(err) + } + liveIndex := "live" + if err := os.WriteFile(filepath.Join(dist, "index.html"), []byte(liveIndex), 0o644); err != nil { + t.Fatal(err) + } + liveAsset := "console.log('live');" + if err := os.WriteFile(filepath.Join(assets, "index-deadbeef.js"), []byte(liveAsset), 0o644); err != nil { + t.Fatal(err) + } + handler := NewHandlerWithResolver("9.9.9", func() string { return dist }) + + root := get(t, handler, "/") + if root.Code != http.StatusOK || root.Body.String() != liveIndex { + t.Fatalf("/ = %d %q, want the live build", root.Code, root.Body.String()) + } + asset := get(t, handler, "/assets/index-deadbeef.js") + if asset.Code != http.StatusOK || asset.Body.String() != liveAsset { + t.Fatalf("asset = %d %q, want the live build file", asset.Code, asset.Body.String()) + } + // A request that exists only in the embedded fallback must not shadow the + // live build: unknown paths 404 instead of silently serving the fallback. + missing := get(t, handler, "/assets/nope.js") + if missing.Code != http.StatusNotFound { + t.Fatalf("missing asset = %d, want 404", missing.Code) + } + // Extensionless SPA paths fall back to the live build's index.html. + spa := get(t, handler, "/logs") + if spa.Code != http.StatusOK || spa.Body.String() != liveIndex { + t.Fatalf("/logs = %d %q, want live index", spa.Code, spa.Body.String()) + } +} + +func TestFallbackStillServedWhenLiveBuildLacksFile(t *testing.T) { + dist := t.TempDir() + if err := os.WriteFile(filepath.Join(dist, "index.html"), []byte("live"), 0o644); err != nil { + t.Fatal(err) + } + handler := NewHandlerWithResolver("9.9.9", func() string { return dist }) + // favicon.png lives in the embedded tree (mirrored from gui/public), not in + // the Vite build output; a live build must still fall back to the embed for + // assets it does not ship. + response := get(t, handler, "/favicon.png") + if response.Code != http.StatusOK || response.Body.Len() == 0 { + t.Fatalf("favicon = %d len=%d, want embedded fallback", response.Code, response.Body.Len()) + } } func TestHandlerRejectsEscapingPathsAndUnknownAsset(t *testing.T) { - handler := NewHandler("9.9.9") - for _, path := range []string{"/../go.mod", "/assets/missing.js"} { - response := httptest.NewRecorder() - handler.ServeHTTP(response, httptest.NewRequest(http.MethodGet, path, nil)) + handler := fallbackHandler() + for _, path := range []string{"/../go.mod", "/assets/missing.js", "/provider-icons/../secret"} { + response := get(t, handler, path) if response.Code != http.StatusNotFound { t.Fatalf("%s status = %d, want 404", path, response.Code) } } } + +func TestHandlerHeadAndMethodGuard(t *testing.T) { + handler := fallbackHandler() + head := httptest.NewRecorder() + handler.ServeHTTP(head, httptest.NewRequest(http.MethodHead, "/", nil)) + if head.Code != http.StatusOK { + t.Fatalf("HEAD / = %d", head.Code) + } + post := httptest.NewRecorder() + handler.ServeHTTP(post, httptest.NewRequest(http.MethodPost, "/", nil)) + if post.Code != http.StatusMethodNotAllowed { + t.Fatalf("POST / = %d, want 405", post.Code) + } + _ = io.Discard +} + +func TestFindGuiDistWalksUpFromWorkingDirectory(t *testing.T) { + original, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { _ = os.Chdir(original) }) + // The embeddedui package lives under /go/internal/embeddedui; the + // repository root's gui/dist must be found two levels up. gui/dist exists + // only when a GUI build ran locally, so make the assertion tolerant: when + // the tree has no gui/dist, the walk must still terminate and find nothing + // instead of walking off the filesystem root. + got := findGuiDist() + root := filepath.Dir(filepath.Dir(filepath.Dir(original))) + if info, statErr := os.Stat(filepath.Join(root, "gui", "dist", "index.html")); statErr == nil && !info.IsDir() { + if got != filepath.Join(root, "gui", "dist") { + t.Fatalf("findGuiDist() = %q, want %q", got, filepath.Join(root, "gui", "dist")) + } + } else if got != "" { + t.Fatalf("findGuiDist() = %q, want \"\" when no gui/dist exists", got) + } +} diff --git a/go/internal/embeddedui/static/assets/index-BU1tE0sr.js b/go/internal/embeddedui/static/assets/index-BU1tE0sr.js deleted file mode 100644 index fadc9538c2..0000000000 --- a/go/internal/embeddedui/static/assets/index-BU1tE0sr.js +++ /dev/null @@ -1,112 +0,0 @@ -var e=Object.create,t=Object.defineProperty,n=Object.getOwnPropertyDescriptor,r=Object.getOwnPropertyNames,i=Object.getPrototypeOf,a=Object.prototype.hasOwnProperty,o=(e,t)=>()=>(t||(e((t={exports:{}}).exports,t),e=null),t.exports),s=(e,i,o,s)=>{if(i&&typeof i==`object`||typeof i==`function`)for(var c=r(i),l=0,u=c.length,d;li[e]).bind(null,d),enumerable:!(s=n(i,d))||s.enumerable});return e},c=(n,r,o)=>(o=n==null?{}:e(i(n)),s(r||!n||!n.__esModule||!a.call(n,`default`)?t(o,`default`,{value:n,enumerable:!0}):o,n));(function(){let e=document.createElement(`link`).relList;if(e&&e.supports&&e.supports(`modulepreload`))return;for(let e of document.querySelectorAll(`link[rel="modulepreload"]`))n(e);new MutationObserver(e=>{for(let t of e)if(t.type===`childList`)for(let e of t.addedNodes)e.tagName===`LINK`&&e.rel===`modulepreload`&&n(e)}).observe(document,{childList:!0,subtree:!0});function t(e){let t={};return e.integrity&&(t.integrity=e.integrity),e.referrerPolicy&&(t.referrerPolicy=e.referrerPolicy),t.credentials=e.crossOrigin===`use-credentials`?`include`:e.crossOrigin===`anonymous`?`omit`:`same-origin`,t}function n(e){if(e.ep)return;e.ep=!0;let n=t(e);fetch(e.href,n)}})();var l=o((e=>{var t=Symbol.for(`react.transitional.element`),n=Symbol.for(`react.portal`),r=Symbol.for(`react.fragment`),i=Symbol.for(`react.strict_mode`),a=Symbol.for(`react.profiler`),o=Symbol.for(`react.consumer`),s=Symbol.for(`react.context`),c=Symbol.for(`react.forward_ref`),l=Symbol.for(`react.suspense`),u=Symbol.for(`react.memo`),d=Symbol.for(`react.lazy`),f=Symbol.for(`react.activity`),p=Symbol.iterator;function m(e){return typeof e!=`object`||!e?null:(e=p&&e[p]||e[`@@iterator`],typeof e==`function`?e:null)}var h={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},g=Object.assign,_={};function v(e,t,n){this.props=e,this.context=t,this.refs=_,this.updater=n||h}v.prototype.isReactComponent={},v.prototype.setState=function(e,t){if(typeof e!=`object`&&typeof e!=`function`&&e!=null)throw Error(`takes an object of state variables to update or a function which returns an object of state variables.`);this.updater.enqueueSetState(this,e,t,`setState`)},v.prototype.forceUpdate=function(e){this.updater.enqueueForceUpdate(this,e,`forceUpdate`)};function y(){}y.prototype=v.prototype;function b(e,t,n){this.props=e,this.context=t,this.refs=_,this.updater=n||h}var x=b.prototype=new y;x.constructor=b,g(x,v.prototype),x.isPureReactComponent=!0;var S=Array.isArray;function C(){}var w={H:null,A:null,T:null,S:null},T=Object.prototype.hasOwnProperty;function E(e,n,r){var i=r.ref;return{$$typeof:t,type:e,key:n,ref:i===void 0?null:i,props:r}}function D(e,t){return E(e.type,t,e.props)}function O(e){return typeof e==`object`&&!!e&&e.$$typeof===t}function k(e){var t={"=":`=0`,":":`=2`};return`$`+e.replace(/[=:]/g,function(e){return t[e]})}var A=/\/+/g;function j(e,t){return typeof e==`object`&&e&&e.key!=null?k(``+e.key):t.toString(36)}function M(e){switch(e.status){case`fulfilled`:return e.value;case`rejected`:throw e.reason;default:switch(typeof e.status==`string`?e.then(C,C):(e.status=`pending`,e.then(function(t){e.status===`pending`&&(e.status=`fulfilled`,e.value=t)},function(t){e.status===`pending`&&(e.status=`rejected`,e.reason=t)})),e.status){case`fulfilled`:return e.value;case`rejected`:throw e.reason}}throw e}function N(e,r,i,a,o){var s=typeof e;(s===`undefined`||s===`boolean`)&&(e=null);var c=!1;if(e===null)c=!0;else switch(s){case`bigint`:case`string`:case`number`:c=!0;break;case`object`:switch(e.$$typeof){case t:case n:c=!0;break;case d:return c=e._init,N(c(e._payload),r,i,a,o)}}if(c)return o=o(e),c=a===``?`.`+j(e,0):a,S(o)?(i=``,c!=null&&(i=c.replace(A,`$&/`)+`/`),N(o,r,i,``,function(e){return e})):o!=null&&(O(o)&&(o=D(o,i+(o.key==null||e&&e.key===o.key?``:(``+o.key).replace(A,`$&/`)+`/`)+c)),r.push(o)),1;c=0;var l=a===``?`.`:a+`:`;if(S(e))for(var u=0;u{t.exports=l()})),d=o((e=>{function t(e,t){var n=e.length;e.push(t);a:for(;0>>1,a=e[r];if(0>>1;ri(c,n))li(u,c)?(e[r]=u,e[l]=n,r=l):(e[r]=c,e[s]=n,r=s);else if(li(u,n))e[r]=u,e[l]=n,r=l;else break a}}return t}function i(e,t){var n=e.sortIndex-t.sortIndex;return n===0?e.id-t.id:n}if(e.unstable_now=void 0,typeof performance==`object`&&typeof performance.now==`function`){var a=performance;e.unstable_now=function(){return a.now()}}else{var o=Date,s=o.now();e.unstable_now=function(){return o.now()-s}}var c=[],l=[],u=1,d=null,f=3,p=!1,m=!1,h=!1,g=!1,_=typeof setTimeout==`function`?setTimeout:null,v=typeof clearTimeout==`function`?clearTimeout:null,y=typeof setImmediate<`u`?setImmediate:null;function b(e){for(var i=n(l);i!==null;){if(i.callback===null)r(l);else if(i.startTime<=e)r(l),i.sortIndex=i.expirationTime,t(c,i);else break;i=n(l)}}function x(e){if(h=!1,b(e),!m){if(n(c)!==null)m=!0,S||(S=!0,O());else{var t=n(l);t!==null&&j(x,t.startTime-e)}}}var S=!1,C=-1,w=5,T=-1;function E(){return g?!0:!(e.unstable_now()-Tt&&E());){var o=d.callback;if(typeof o==`function`){d.callback=null,f=d.priorityLevel;var s=o(d.expirationTime<=t);if(t=e.unstable_now(),typeof s==`function`){d.callback=s,b(t),i=!0;break b}d===n(c)&&r(c),b(t)}else r(c);d=n(c)}if(d!==null)i=!0;else{var u=n(l);u!==null&&j(x,u.startTime-t),i=!1}}break a}finally{d=null,f=a,p=!1}}}finally{i?O():S=!1}}}var O;if(typeof y==`function`)O=function(){y(D)};else if(typeof MessageChannel<`u`){var k=new MessageChannel,A=k.port2;k.port1.onmessage=D,O=function(){A.postMessage(null)}}else O=function(){_(D,0)};function j(t,n){C=_(function(){t(e.unstable_now())},n)}e.unstable_IdlePriority=5,e.unstable_ImmediatePriority=1,e.unstable_LowPriority=4,e.unstable_NormalPriority=3,e.unstable_Profiling=null,e.unstable_UserBlockingPriority=2,e.unstable_cancelCallback=function(e){e.callback=null},e.unstable_forceFrameRate=function(e){0>e||125o?(r.sortIndex=a,t(l,r),n(c)===null&&r===n(l)&&(h?(v(C),C=-1):h=!0,j(x,a-o))):(r.sortIndex=s,t(c,r),m||p||(m=!0,S||(S=!0,O()))),r},e.unstable_shouldYield=E,e.unstable_wrapCallback=function(e){var t=f;return function(){var n=f;f=t;try{return e.apply(this,arguments)}finally{f=n}}}})),f=o(((e,t)=>{t.exports=d()})),p=o((e=>{var t=u();function n(e){var t=`https://react.dev/errors/`+e;if(1{function n(){if(!(typeof __REACT_DEVTOOLS_GLOBAL_HOOK__>`u`||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!=`function`))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(n)}catch(e){console.error(e)}}n(),t.exports=p()})),h=o((e=>{var t=f(),n=u(),r=m();function i(e){var t=`https://react.dev/errors/`+e;if(1B||(e.current=z[B],z[B]=null,B--)}function U(e,t){B++,z[B]=e.current,e.current=t}var W=V(null),ee=V(null),G=V(null),K=V(null);function q(e,t){switch(U(G,t),U(ee,e),U(W,null),t.nodeType){case 9:case 11:e=(e=t.documentElement)&&(e=e.namespaceURI)?Vd(e):0;break;default:if(e=t.tagName,t=t.namespaceURI)t=Vd(t),e=Hd(t,e);else switch(e){case`svg`:e=1;break;case`math`:e=2;break;default:e=0}}H(W),U(W,e)}function J(){H(W),H(ee),H(G)}function Y(e){e.memoizedState!==null&&U(K,e);var t=W.current,n=Hd(t,e.type);t!==n&&(U(ee,e),U(W,n))}function te(e){ee.current===e&&(H(W),H(ee)),K.current===e&&(H(K),Qf._currentValue=R)}var ne,re;function ie(e){if(ne===void 0)try{throw Error()}catch(e){var t=e.stack.trim().match(/\n( *(at )?)/);ne=t&&t[1]||``,re=-1)`:-1i||c[r]!==l[i]){var u=` -`+c[r].replace(` at new `,` at `);return e.displayName&&u.includes(``)&&(u=u.replace(``,e.displayName)),u}while(1<=r&&0<=i);break}}}finally{ae=!1,Error.prepareStackTrace=n}return(n=e?e.displayName||e.name:``)?ie(n):``}function se(e,t){switch(e.tag){case 26:case 27:case 5:return ie(e.type);case 16:return ie(`Lazy`);case 13:return e.child!==t&&t!==null?ie(`Suspense Fallback`):ie(`Suspense`);case 19:return ie(`SuspenseList`);case 0:case 15:return oe(e.type,!1);case 11:return oe(e.type.render,!1);case 1:return oe(e.type,!0);case 31:return ie(`Activity`);default:return``}}function ce(e){try{var t=``,n=null;do t+=se(e,n),n=e,e=e.return;while(e);return t}catch(e){return` -Error generating stack: `+e.message+` -`+e.stack}}var le=Object.prototype.hasOwnProperty,ue=t.unstable_scheduleCallback,de=t.unstable_cancelCallback,fe=t.unstable_shouldYield,pe=t.unstable_requestPaint,X=t.unstable_now,me=t.unstable_getCurrentPriorityLevel,he=t.unstable_ImmediatePriority,ge=t.unstable_UserBlockingPriority,_e=t.unstable_NormalPriority,Z=t.unstable_LowPriority,ve=t.unstable_IdlePriority,ye=t.log,be=t.unstable_setDisableYieldValue,xe=null,Se=null;function Ce(e){if(typeof ye==`function`&&be(e),Se&&typeof Se.setStrictMode==`function`)try{Se.setStrictMode(xe,e)}catch{}}var we=Math.clz32?Math.clz32:De,Te=Math.log,Ee=Math.LN2;function De(e){return e>>>=0,e===0?32:31-(Te(e)/Ee|0)|0}var Oe=256,ke=262144,Ae=4194304;function je(e){var t=e&42;if(t!==0)return t;switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:return 64;case 128:return 128;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:return e&261888;case 262144:case 524288:case 1048576:case 2097152:return e&3932160;case 4194304:case 8388608:case 16777216:case 33554432:return e&62914560;case 67108864:return 67108864;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 0;default:return e}}function Me(e,t,n){var r=e.pendingLanes;if(r===0)return 0;var i=0,a=e.suspendedLanes,o=e.pingedLanes;e=e.warmLanes;var s=r&134217727;return s===0?(s=r&~a,s===0?o===0?n||(n=r&~e,n!==0&&(i=je(n))):i=je(o):i=je(s)):(r=s&~a,r===0?(o&=s,o===0?n||(n=s&~e,n!==0&&(i=je(n))):i=je(o)):i=je(r)),i===0?0:t!==0&&t!==i&&(t&a)===0&&(a=i&-i,n=t&-t,a>=n||a===32&&n&4194048)?t:i}function Ne(e,t){return(e.pendingLanes&~(e.suspendedLanes&~e.pingedLanes)&t)===0}function Pe(e,t){switch(e){case 1:case 2:case 4:case 8:case 64:return t+250;case 16:case 32:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return t+5e3;case 4194304:case 8388608:case 16777216:case 33554432:return-1;case 67108864:case 134217728:case 268435456:case 536870912:case 1073741824:return-1;default:return-1}}function Fe(){var e=Ae;return Ae<<=1,!(Ae&62914560)&&(Ae=4194304),e}function Ie(e){for(var t=[],n=0;31>n;n++)t.push(e);return t}function Le(e,t){e.pendingLanes|=t,t!==268435456&&(e.suspendedLanes=0,e.pingedLanes=0,e.warmLanes=0)}function Re(e,t,n,r,i,a){var o=e.pendingLanes;e.pendingLanes=n,e.suspendedLanes=0,e.pingedLanes=0,e.warmLanes=0,e.expiredLanes&=n,e.entangledLanes&=n,e.errorRecoveryDisabledLanes&=n,e.shellSuspendCounter=0;var s=e.entanglements,c=e.expirationTimes,l=e.hiddenUpdates;for(n=o&~n;0`u`||window.document===void 0||window.document.createElement===void 0),Jt=!1;if(qt)try{var Yt={};Object.defineProperty(Yt,"passive",{get:function(){Jt=!0}}),window.addEventListener(`test`,Yt,Yt),window.removeEventListener(`test`,Yt,Yt)}catch{Jt=!1}var Xt=null,Zt=null,Qt=null;function $t(){if(Qt)return Qt;var e,t=Zt,n=t.length,r,i=`value`in Xt?Xt.value:Xt.textContent,a=i.length;for(e=0;e=Mn),Fn=` `,In=!1;function Ln(e,t){switch(e){case`keyup`:return An.indexOf(t.keyCode)!==-1;case`keydown`:return t.keyCode!==229;case`keypress`:case`mousedown`:case`focusout`:return!0;default:return!1}}function Rn(e){return e=e.detail,typeof e==`object`&&`data`in e?e.data:null}var zn=!1;function Bn(e,t){switch(e){case`compositionend`:return Rn(t);case`keypress`:return t.which===32?(In=!0,Fn):null;case`textInput`:return e=t.data,e===Fn&&In?null:e;default:return null}}function Vn(e,t){if(zn)return e===`compositionend`||!jn&&Ln(e,t)?(e=$t(),Qt=Zt=Xt=null,zn=!1,e):null;switch(e){case`paste`:return null;case`keypress`:if(!(t.ctrlKey||t.altKey||t.metaKey)||t.ctrlKey&&t.altKey){if(t.char&&1=t)return{node:n,offset:t-e};e=r}a:{for(;n;){if(n.nextSibling){n=n.nextSibling;break a}n=n.parentNode}n=void 0}n=lr(n)}}function dr(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?dr(e,t.parentNode):`contains`in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function fr(e){e=e!=null&&e.ownerDocument!=null&&e.ownerDocument.defaultView!=null?e.ownerDocument.defaultView:window;for(var t=St(e.document);t instanceof e.HTMLIFrameElement;){try{var n=typeof t.contentWindow.location.href==`string`}catch{n=!1}if(n)e=t.contentWindow;else break;t=St(e.document)}return t}function pr(e){var t=e&&e.nodeName&&e.nodeName.toLowerCase();return t&&(t===`input`&&(e.type===`text`||e.type===`search`||e.type===`tel`||e.type===`url`||e.type===`password`)||t===`textarea`||e.contentEditable===`true`)}var mr=qt&&`documentMode`in document&&11>=document.documentMode,hr=null,gr=null,_r=null,vr=!1;function yr(e,t,n){var r=n.window===n?n.document:n.nodeType===9?n:n.ownerDocument;vr||hr==null||hr!==St(r)||(r=hr,`selectionStart`in r&&pr(r)?r={start:r.selectionStart,end:r.selectionEnd}:(r=(r.ownerDocument&&r.ownerDocument.defaultView||window).getSelection(),r={anchorNode:r.anchorNode,anchorOffset:r.anchorOffset,focusNode:r.focusNode,focusOffset:r.focusOffset}),_r&&cr(_r,r)||(_r=r,r=Td(gr,`onSelect`),0>=o,i-=o,di=1<<32-we(t)+i|n<h?(g=d,d=null):g=d.sibling;var _=p(i,d,s[h],c);if(_===null){d===null&&(d=g);break}e&&d&&_.alternate===null&&t(i,d),a=o(_,a,h),u===null?l=_:u.sibling=_,u=_,d=g}if(h===s.length)return n(i,d),bi&&pi(i,h),l;if(d===null){for(;hg?(_=h,h=null):_=h.sibling;var y=p(a,h,v.value,l);if(y===null){h===null&&(h=_);break}e&&h&&y.alternate===null&&t(a,h),s=o(y,s,g),d===null?u=y:d.sibling=y,d=y,h=_}if(v.done)return n(a,h),bi&&pi(a,g),u;if(h===null){for(;!v.done;g++,v=c.next())v=f(a,v.value,l),v!==null&&(s=o(v,s,g),d===null?u=v:d.sibling=v,d=v);return bi&&pi(a,g),u}for(h=r(h);!v.done;g++,v=c.next())v=m(h,a,g,v.value,l),v!==null&&(e&&v.alternate!==null&&h.delete(v.key===null?g:v.key),s=o(v,s,g),d===null?u=v:d.sibling=v,d=v);return e&&h.forEach(function(e){return t(a,e)}),bi&&pi(a,g),u}function b(e,r,o,c){if(typeof o==`object`&&o&&o.type===y&&o.key===null&&(o=o.props.children),typeof o==`object`&&o){switch(o.$$typeof){case _:a:{for(var l=o.key;r!==null;){if(r.key===l){if(l=o.type,l===y){if(r.tag===7){n(e,r.sibling),c=a(r,o.props.children),c.return=e,e=c;break a}}else if(r.elementType===l||typeof l==`object`&&l&&l.$$typeof===O&&ma(l)===r.type){n(e,r.sibling),c=a(r,o.props),xa(c,o),c.return=e,e=c;break a}n(e,r);break}t(e,r),r=r.sibling}o.type===y?(c=Qr(o.props.children,e.mode,c,o.key),c.return=e,e=c):(c=Zr(o.type,o.key,o.props,null,e.mode,c),xa(c,o),c.return=e,e=c)}return s(e);case v:a:{for(l=o.key;r!==null;){if(r.key===l){if(r.tag===4&&r.stateNode.containerInfo===o.containerInfo&&r.stateNode.implementation===o.implementation){n(e,r.sibling),c=a(r,o.children||[]),c.return=e,e=c;break a}n(e,r);break}t(e,r),r=r.sibling}c=ti(o,e.mode,c),c.return=e,e=c}return s(e);case O:return o=ma(o),b(e,r,o,c)}if(F(o))return h(e,r,o,c);if(M(o)){if(l=M(o),typeof l!=`function`)throw Error(i(150));return o=l.call(o),g(e,r,o,c)}if(typeof o.then==`function`)return b(e,r,ba(o),c);if(o.$$typeof===C)return b(e,r,Hi(e,o),c);Sa(e,o)}return typeof o==`string`&&o!==``||typeof o==`number`||typeof o==`bigint`?(o=``+o,r!==null&&r.tag===6?(n(e,r.sibling),c=a(r,o),c.return=e,e=c):(n(e,r),c=$r(o,e.mode,c),c.return=e,e=c),s(e)):n(e,r)}return function(e,t,n,r){try{ya=0;var i=b(e,t,n,r);return va=null,i}catch(t){if(t===ca||t===ua)throw t;var a=qr(29,t,null,e.mode);return a.lanes=r,a.return=e,a}}}var wa=Ca(!0),Ta=Ca(!1),Ea=!1;function Da(e){e.updateQueue={baseState:e.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,lanes:0,hiddenCallbacks:null},callbacks:null}}function Oa(e,t){e=e.updateQueue,t.updateQueue===e&&(t.updateQueue={baseState:e.baseState,firstBaseUpdate:e.firstBaseUpdate,lastBaseUpdate:e.lastBaseUpdate,shared:e.shared,callbacks:null})}function ka(e){return{lane:e,tag:0,payload:null,callback:null,next:null}}function Aa(e,t,n){var r=e.updateQueue;if(r===null)return null;if(r=r.shared,jl&2){var i=r.pending;return i===null?t.next=t:(t.next=i.next,i.next=t),r.pending=t,t=Wr(e),Ur(e,null,n),t}return Br(e,r,t,n),Wr(e)}function ja(e,t,n){if(t=t.updateQueue,t!==null&&(t=t.shared,n&4194048)){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,Be(e,n)}}function Ma(e,t){var n=e.updateQueue,r=e.alternate;if(r!==null&&(r=r.updateQueue,n===r)){var i=null,a=null;if(n=n.firstBaseUpdate,n!==null){do{var o={lane:n.lane,tag:n.tag,payload:n.payload,callback:null,next:null};a===null?i=a=o:a=a.next=o,n=n.next}while(n!==null);a===null?i=a=t:a=a.next=t}else i=a=t;n={baseState:r.baseState,firstBaseUpdate:i,lastBaseUpdate:a,shared:r.shared,callbacks:r.callbacks},e.updateQueue=n;return}e=n.lastBaseUpdate,e===null?n.firstBaseUpdate=t:e.next=t,n.lastBaseUpdate=t}var Na=!1;function Pa(){if(Na){var e=$i;if(e!==null)throw e}}function Fa(e,t,n,r){Na=!1;var i=e.updateQueue;Ea=!1;var a=i.firstBaseUpdate,o=i.lastBaseUpdate,s=i.shared.pending;if(s!==null){i.shared.pending=null;var c=s,l=c.next;c.next=null,o===null?a=l:o.next=l,o=c;var u=e.alternate;u!==null&&(u=u.updateQueue,s=u.lastBaseUpdate,s!==o&&(s===null?u.firstBaseUpdate=l:s.next=l,u.lastBaseUpdate=c))}if(a!==null){var d=i.baseState;o=0,u=l=c=null,s=a;do{var f=s.lane&-536870913,p=f!==s.lane;if(p?(Pl&f)===f:(r&f)===f){f!==0&&f===Qi&&(Na=!0),u!==null&&(u=u.next={lane:0,tag:s.tag,payload:s.payload,callback:null,next:null});a:{var m=e,g=s;f=t;var _=n;switch(g.tag){case 1:if(m=g.payload,typeof m==`function`){d=m.call(_,d,f);break a}d=m;break a;case 3:m.flags=m.flags&-65537|128;case 0:if(m=g.payload,f=typeof m==`function`?m.call(_,d,f):m,f==null)break a;d=h({},d,f);break a;case 2:Ea=!0}}f=s.callback,f!==null&&(e.flags|=64,p&&(e.flags|=8192),p=i.callbacks,p===null?i.callbacks=[f]:p.push(f))}else p={lane:f,tag:s.tag,payload:s.payload,callback:s.callback,next:null},u===null?(l=u=p,c=d):u=u.next=p,o|=f;if(s=s.next,s===null){if(s=i.shared.pending,s===null)break;p=s,s=p.next,p.next=null,i.lastBaseUpdate=p,i.shared.pending=null}}while(1);u===null&&(c=d),i.baseState=c,i.firstBaseUpdate=l,i.lastBaseUpdate=u,a===null&&(i.shared.lanes=0),Hl|=o,e.lanes=o,e.memoizedState=d}}function Ia(e,t){if(typeof e!=`function`)throw Error(i(191,e));e.call(t)}function La(e,t){var n=e.callbacks;if(n!==null)for(e.callbacks=null,e=0;ea?a:8;var o=I.T,s={};I.T=s,ws(e,!1,t,n);try{var c=i(),l=I.S;l!==null&&l(s,c),typeof c==`object`&&c&&typeof c.then==`function`?Cs(e,t,na(c,r),uu(e)):Cs(e,t,r,uu(e))}catch(n){Cs(e,t,{then:function(){},status:`rejected`,reason:n},uu())}finally{L.p=a,o!==null&&s.types!==null&&(o.types=s.types),I.T=o}}function ps(){}function ms(e,t,n,r){if(e.tag!==5)throw Error(i(476));var a=hs(e).queue;fs(e,a,t,R,n===null?ps:function(){return gs(e),n(r)})}function hs(e){var t=e.memoizedState;if(t!==null)return t;t={memoizedState:R,baseState:R,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:To,lastRenderedState:R},next:null};var n={};return t.next={memoizedState:n,baseState:n,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:To,lastRenderedState:n},next:null},e.memoizedState=t,e=e.alternate,e!==null&&(e.memoizedState=t),t}function gs(e){var t=hs(e);t.next===null&&(t=e.alternate.memoizedState),Cs(e,t.next.queue,{},uu())}function _s(){return Vi(Qf)}function vs(){return bo().memoizedState}function ys(){return bo().memoizedState}function bs(e){for(var t=e.return;t!==null;){switch(t.tag){case 24:case 3:var n=uu();e=ka(n);var r=Aa(t,e,n);r!==null&&(fu(r,t,n),ja(r,t,n)),t={cache:Ji()},e.payload=t;return}t=t.return}}function xs(e,t,n){var r=uu();n={lane:r,revertLane:0,gesture:null,action:n,hasEagerState:!1,eagerState:null,next:null},Ts(e)?Es(t,n):(n=Vr(e,t,n,r),n!==null&&(fu(n,e,r),Ds(n,t,r)))}function Ss(e,t,n){Cs(e,t,n,uu())}function Cs(e,t,n,r){var i={lane:r,revertLane:0,gesture:null,action:n,hasEagerState:!1,eagerState:null,next:null};if(Ts(e))Es(t,i);else{var a=e.alternate;if(e.lanes===0&&(a===null||a.lanes===0)&&(a=t.lastRenderedReducer,a!==null))try{var o=t.lastRenderedState,s=a(o,n);if(i.hasEagerState=!0,i.eagerState=s,sr(s,o))return Br(e,t,i,0),Ml===null&&zr(),!1}catch{}if(n=Vr(e,t,i,r),n!==null)return fu(n,e,r),Ds(n,t,r),!0}return!1}function ws(e,t,n,r){if(r={lane:2,revertLane:ld(),gesture:null,action:r,hasEagerState:!1,eagerState:null,next:null},Ts(e)){if(t)throw Error(i(479))}else t=Vr(e,n,r,2),t!==null&&fu(t,e,2)}function Ts(e){var t=e.alternate;return e===$a||t!==null&&t===$a}function Es(e,t){ro=no=!0;var n=e.pending;n===null?t.next=t:(t.next=n.next,n.next=t),e.pending=t}function Ds(e,t,n){if(n&4194048){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,Be(e,n)}}var Os={readContext:Vi,use:Co,useCallback:lo,useContext:lo,useEffect:lo,useImperativeHandle:lo,useLayoutEffect:lo,useInsertionEffect:lo,useMemo:lo,useReducer:lo,useRef:lo,useState:lo,useDebugValue:lo,useDeferredValue:lo,useTransition:lo,useSyncExternalStore:lo,useId:lo,useHostTransitionStatus:lo,useFormState:lo,useActionState:lo,useOptimistic:lo,useMemoCache:lo,useCacheRefresh:lo};Os.useEffectEvent=lo;var ks={readContext:Vi,use:Co,useCallback:function(e,t){return yo().memoizedState=[e,t===void 0?null:t],e},useContext:Vi,useEffect:$o,useImperativeHandle:function(e,t,n){n=n==null?null:n.concat([e]),Zo(4194308,4,as.bind(null,t,e),n)},useLayoutEffect:function(e,t){return Zo(4194308,4,e,t)},useInsertionEffect:function(e,t){Zo(4,2,e,t)},useMemo:function(e,t){var n=yo();t=t===void 0?null:t;var r=e();if(io){Ce(!0);try{e()}finally{Ce(!1)}}return n.memoizedState=[r,t],r},useReducer:function(e,t,n){var r=yo();if(n!==void 0){var i=n(t);if(io){Ce(!0);try{n(t)}finally{Ce(!1)}}}else i=t;return r.memoizedState=r.baseState=i,e={pending:null,lanes:0,dispatch:null,lastRenderedReducer:e,lastRenderedState:i},r.queue=e,e=e.dispatch=xs.bind(null,$a,e),[r.memoizedState,e]},useRef:function(e){var t=yo();return e={current:e},t.memoizedState=e},useState:function(e){e=Fo(e);var t=e.queue,n=Ss.bind(null,$a,t);return t.dispatch=n,[e.memoizedState,n]},useDebugValue:ss,useDeferredValue:function(e,t){return us(yo(),e,t)},useTransition:function(){var e=Fo(!1);return e=fs.bind(null,$a,e.queue,!0,!1),yo().memoizedState=e,[!1,e]},useSyncExternalStore:function(e,t,n){var r=$a,a=yo();if(bi){if(n===void 0)throw Error(i(407));n=n()}else{if(n=t(),Ml===null)throw Error(i(349));Pl&127||Ao(r,t,n)}a.memoizedState=n;var o={value:n,getSnapshot:t};return a.queue=o,$o(Mo.bind(null,r,o,e),[e]),r.flags|=2048,Yo(9,{destroy:void 0},jo.bind(null,r,o,n,t),null),n},useId:function(){var e=yo(),t=Ml.identifierPrefix;if(bi){var n=fi,r=di;n=(r&~(1<<32-we(r)-1)).toString(32)+n,t=`_`+t+`R_`+n,n=ao++,0<\/script>`,o=o.removeChild(o.firstChild);break;case`select`:o=typeof r.is==`string`?s.createElement(`select`,{is:r.is}):s.createElement(`select`),r.multiple?o.multiple=!0:r.size&&(o.size=r.size);break;default:o=typeof r.is==`string`?s.createElement(a,{is:r.is}):s.createElement(a)}}o[qe]=t,o[Je]=r;a:for(s=t.child;s!==null;){if(s.tag===5||s.tag===6)o.appendChild(s.stateNode);else if(s.tag!==4&&s.tag!==27&&s.child!==null){s.child.return=s,s=s.child;continue}if(s===t)break a;for(;s.sibling===null;){if(s.return===null||s.return===t)break a;s=s.return}s.sibling.return=s.return,s=s.sibling}t.stateNode=o;a:switch(Pd(o,a,r),a){case`button`:case`input`:case`select`:case`textarea`:r=!!r.autoFocus;break a;case`img`:r=!0;break a;default:r=!1}r&&Cc(t)}}return Oc(t),wc(t,t.type,e===null?null:e.memoizedProps,t.pendingProps,n),null;case 6:if(e&&t.stateNode!=null)e.memoizedProps!==r&&Cc(t);else{if(typeof r!=`string`&&t.stateNode===null)throw Error(i(166));if(e=G.current,Di(t)){if(e=t.stateNode,n=t.memoizedProps,r=null,a=vi,a!==null)switch(a.tag){case 27:case 5:r=a.memoizedProps}e[qe]=t,e=!!(e.nodeValue===n||r!==null&&!0===r.suppressHydrationWarning||jd(e.nodeValue,n)),e||wi(t,!0)}else e=Bd(e).createTextNode(r),e[qe]=t,t.stateNode=e}return Oc(t),null;case 31:if(n=t.memoizedState,e===null||e.memoizedState!==null){if(r=Di(t),n!==null){if(e===null){if(!r)throw Error(i(318));if(e=t.memoizedState,e=e===null?null:e.dehydrated,!e)throw Error(i(557));e[qe]=t}else Oi(),!(t.flags&128)&&(t.memoizedState=null),t.flags|=4;Oc(t),e=!1}else n=ki(),e!==null&&e.memoizedState!==null&&(e.memoizedState.hydrationErrors=n),e=!0;if(!e)return t.flags&256?(Ya(t),t):(Ya(t),null);if(t.flags&128)throw Error(i(558))}return Oc(t),null;case 13:if(r=t.memoizedState,e===null||e.memoizedState!==null&&e.memoizedState.dehydrated!==null){if(a=Di(t),r!==null&&r.dehydrated!==null){if(e===null){if(!a)throw Error(i(318));if(a=t.memoizedState,a=a===null?null:a.dehydrated,!a)throw Error(i(317));a[qe]=t}else Oi(),!(t.flags&128)&&(t.memoizedState=null),t.flags|=4;Oc(t),a=!1}else a=ki(),e!==null&&e.memoizedState!==null&&(e.memoizedState.hydrationErrors=a),a=!0;if(!a)return t.flags&256?(Ya(t),t):(Ya(t),null)}return Ya(t),t.flags&128?(t.lanes=n,t):(n=r!==null,e=e!==null&&e.memoizedState!==null,n&&(r=t.child,a=null,r.alternate!==null&&r.alternate.memoizedState!==null&&r.alternate.memoizedState.cachePool!==null&&(a=r.alternate.memoizedState.cachePool.pool),o=null,r.memoizedState!==null&&r.memoizedState.cachePool!==null&&(o=r.memoizedState.cachePool.pool),o!==a&&(r.flags|=2048)),n!==e&&n&&(t.child.flags|=8192),Ec(t,t.updateQueue),Oc(t),null);case 4:return J(),e===null&&xd(t.stateNode.containerInfo),Oc(t),null;case 10:return Fi(t.type),Oc(t),null;case 19:if(H(Xa),r=t.memoizedState,r===null)return Oc(t),null;if(a=!!(t.flags&128),o=r.rendering,o===null){if(a)Dc(r,!1);else{if(Vl!==0||e!==null&&e.flags&128)for(e=t.child;e!==null;){if(o=Za(e),o!==null){for(t.flags|=128,Dc(r,!1),e=o.updateQueue,t.updateQueue=e,Ec(t,e),t.subtreeFlags=0,e=n,n=t.child;n!==null;)Xr(n,e),n=n.sibling;return U(Xa,Xa.current&1|2),bi&&pi(t,r.treeForkCount),t.child}e=e.sibling}r.tail!==null&&X()>Ql&&(t.flags|=128,a=!0,Dc(r,!1),t.lanes=4194304)}}else{if(!a){if(e=Za(o),e!==null){if(t.flags|=128,a=!0,e=e.updateQueue,t.updateQueue=e,Ec(t,e),Dc(r,!0),r.tail===null&&r.tailMode===`hidden`&&!o.alternate&&!bi)return Oc(t),null}else 2*X()-r.renderingStartTime>Ql&&n!==536870912&&(t.flags|=128,a=!0,Dc(r,!1),t.lanes=4194304)}r.isBackwards?(o.sibling=t.child,t.child=o):(e=r.last,e===null?t.child=o:e.sibling=o,r.last=o)}return r.tail===null?(Oc(t),null):(e=r.tail,r.rendering=e,r.tail=e.sibling,r.renderingStartTime=X(),e.sibling=null,n=Xa.current,U(Xa,a?n&1|2:n&1),bi&&pi(t,r.treeForkCount),e);case 22:case 23:return Ya(t),Ha(),r=t.memoizedState!==null,e===null?r&&(t.flags|=8192):e.memoizedState!==null!==r&&(t.flags|=8192),r?n&536870912&&!(t.flags&128)&&(Oc(t),t.subtreeFlags&6&&(t.flags|=8192)):Oc(t),n=t.updateQueue,n!==null&&Ec(t,n.retryQueue),n=null,e!==null&&e.memoizedState!==null&&e.memoizedState.cachePool!==null&&(n=e.memoizedState.cachePool.pool),r=null,t.memoizedState!==null&&t.memoizedState.cachePool!==null&&(r=t.memoizedState.cachePool.pool),r!==n&&(t.flags|=2048),e!==null&&H(ia),null;case 24:return n=null,e!==null&&(n=e.memoizedState.cache),t.memoizedState.cache!==n&&(t.flags|=2048),Fi(qi),Oc(t),null;case 25:return null;case 30:return null}throw Error(i(156,t.tag))}function Ac(e,t){switch(gi(t),t.tag){case 1:return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 3:return Fi(qi),J(),e=t.flags,e&65536&&!(e&128)?(t.flags=e&-65537|128,t):null;case 26:case 27:case 5:return te(t),null;case 31:if(t.memoizedState!==null){if(Ya(t),t.alternate===null)throw Error(i(340));Oi()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 13:if(Ya(t),e=t.memoizedState,e!==null&&e.dehydrated!==null){if(t.alternate===null)throw Error(i(340));Oi()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 19:return H(Xa),null;case 4:return J(),null;case 10:return Fi(t.type),null;case 22:case 23:return Ya(t),Ha(),e!==null&&H(ia),e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 24:return Fi(qi),null;case 25:return null;default:return null}}function jc(e,t){switch(gi(t),t.tag){case 3:Fi(qi),J();break;case 26:case 27:case 5:te(t);break;case 4:J();break;case 31:t.memoizedState!==null&&Ya(t);break;case 13:Ya(t);break;case 19:H(Xa);break;case 10:Fi(t.type);break;case 22:case 23:Ya(t),Ha(),e!==null&&H(ia);break;case 24:Fi(qi)}}function Mc(e,t){try{var n=t.updateQueue,r=n===null?null:n.lastEffect;if(r!==null){var i=r.next;n=i;do{if((n.tag&e)===e){r=void 0;var a=n.create,o=n.inst;r=a(),o.destroy=r}n=n.next}while(n!==i)}}catch(e){Hu(t,t.return,e)}}function Nc(e,t,n){try{var r=t.updateQueue,i=r===null?null:r.lastEffect;if(i!==null){var a=i.next;r=a;do{if((r.tag&e)===e){var o=r.inst,s=o.destroy;if(s!==void 0){o.destroy=void 0,i=t;var c=n,l=s;try{l()}catch(e){Hu(i,c,e)}}}r=r.next}while(r!==a)}}catch(e){Hu(t,t.return,e)}}function Pc(e){var t=e.updateQueue;if(t!==null){var n=e.stateNode;try{La(t,n)}catch(t){Hu(e,e.return,t)}}}function Fc(e,t,n){n.props=Is(e.type,e.memoizedProps),n.state=e.memoizedState;try{n.componentWillUnmount()}catch(n){Hu(e,t,n)}}function Ic(e,t){try{var n=e.ref;if(n!==null){switch(e.tag){case 26:case 27:case 5:var r=e.stateNode;break;case 30:r=e.stateNode;break;default:r=e.stateNode}typeof n==`function`?e.refCleanup=n(r):n.current=r}}catch(n){Hu(e,t,n)}}function Lc(e,t){var n=e.ref,r=e.refCleanup;if(n!==null){if(typeof r==`function`)try{r()}catch(n){Hu(e,t,n)}finally{e.refCleanup=null,e=e.alternate,e!=null&&(e.refCleanup=null)}else if(typeof n==`function`)try{n(null)}catch(n){Hu(e,t,n)}else n.current=null}}function Rc(e){var t=e.type,n=e.memoizedProps,r=e.stateNode;try{a:switch(t){case`button`:case`input`:case`select`:case`textarea`:n.autoFocus&&r.focus();break a;case`img`:n.src?r.src=n.src:n.srcSet&&(r.srcset=n.srcSet)}}catch(t){Hu(e,e.return,t)}}function zc(e,t,n){try{var r=e.stateNode;Fd(r,e.type,n,t),r[Je]=t}catch(t){Hu(e,e.return,t)}}function Bc(e){return e.tag===5||e.tag===3||e.tag===26||e.tag===27&&Zd(e.type)||e.tag===4}function Vc(e){a:for(;;){for(;e.sibling===null;){if(e.return===null||Bc(e.return))return null;e=e.return}for(e.sibling.return=e.return,e=e.sibling;e.tag!==5&&e.tag!==6&&e.tag!==18;){if(e.tag===27&&Zd(e.type)||e.flags&2||e.child===null||e.tag===4)continue a;e.child.return=e,e=e.child}if(!(e.flags&2))return e.stateNode}}function Hc(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?(n.nodeType===9?n.body:n.nodeName===`HTML`?n.ownerDocument.body:n).insertBefore(e,t):(t=n.nodeType===9?n.body:n.nodeName===`HTML`?n.ownerDocument.body:n,t.appendChild(e),n=n._reactRootContainer,n!=null||t.onclick!==null||(t.onclick=Rt));else if(r!==4&&(r===27&&Zd(e.type)&&(n=e.stateNode,t=null),e=e.child,e!==null))for(Hc(e,t,n),e=e.sibling;e!==null;)Hc(e,t,n),e=e.sibling}function Uc(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.insertBefore(e,t):n.appendChild(e);else if(r!==4&&(r===27&&Zd(e.type)&&(n=e.stateNode),e=e.child,e!==null))for(Uc(e,t,n),e=e.sibling;e!==null;)Uc(e,t,n),e=e.sibling}function Wc(e){var t=e.stateNode,n=e.memoizedProps;try{for(var r=e.type,i=t.attributes;i.length;)t.removeAttributeNode(i[0]);Pd(t,r,n),t[qe]=e,t[Je]=n}catch(t){Hu(e,e.return,t)}}var Gc=!1,Kc=!1,qc=!1,Jc=typeof WeakSet==`function`?WeakSet:Set,Yc=null;function Xc(e,t){if(e=e.containerInfo,Rd=sp,e=fr(e),pr(e)){if(`selectionStart`in e)var n={start:e.selectionStart,end:e.selectionEnd};else a:{n=(n=e.ownerDocument)&&n.defaultView||window;var r=n.getSelection&&n.getSelection();if(r&&r.rangeCount!==0){n=r.anchorNode;var a=r.anchorOffset,o=r.focusNode;r=r.focusOffset;try{n.nodeType,o.nodeType}catch{n=null;break a}var s=0,c=-1,l=-1,u=0,d=0,f=e,p=null;b:for(;;){for(var m;f!==n||a!==0&&f.nodeType!==3||(c=s+a),f!==o||r!==0&&f.nodeType!==3||(l=s+r),f.nodeType===3&&(s+=f.nodeValue.length),(m=f.firstChild)!==null;)p=f,f=m;for(;;){if(f===e)break b;if(p===n&&++u===a&&(c=s),p===o&&++d===r&&(l=s),(m=f.nextSibling)!==null)break;f=p,p=f.parentNode}f=m}n=c===-1||l===-1?null:{start:c,end:l}}else n=null}n||={start:0,end:0}}else n=null;for(zd={focusedElem:e,selectionRange:n},sp=!1,Yc=t;Yc!==null;)if(t=Yc,e=t.child,t.subtreeFlags&1028&&e!==null)e.return=t,Yc=e;else for(;Yc!==null;){switch(t=Yc,o=t.alternate,e=t.flags,t.tag){case 0:if(e&4&&(e=t.updateQueue,e=e===null?null:e.events,e!==null))for(n=0;n title`))),Pd(o,r,n),o[qe]=e,ot(o),r=o;break a;case`link`:var s=Vf(`link`,`href`,a).get(r+(n.href||``));if(s){for(var c=0;cg&&(o=g,g=h,h=o);var _=ur(s,h),v=ur(s,g);if(_&&v&&(p.rangeCount!==1||p.anchorNode!==_.node||p.anchorOffset!==_.offset||p.focusNode!==v.node||p.focusOffset!==v.offset)){var y=d.createRange();y.setStart(_.node,_.offset),p.removeAllRanges(),h>g?(p.addRange(y),p.extend(v.node,v.offset)):(y.setEnd(v.node,v.offset),p.addRange(y))}}}}for(d=[],p=s;p=p.parentNode;)p.nodeType===1&&d.push({element:p,left:p.scrollLeft,top:p.scrollTop});for(typeof s.focus==`function`&&s.focus(),s=0;sn?32:n,I.T=null,n=ou,ou=null;var o=nu,s=iu;if(tu=0,ru=nu=null,iu=0,jl&6)throw Error(i(331));var c=jl;if(jl|=4,El(o.current),vl(o,o.current,s,n),jl=c,nd(0,!1),Se&&typeof Se.onPostCommitFiberRoot==`function`)try{Se.onPostCommitFiberRoot(xe,o)}catch{}return!0}finally{L.p=a,I.T=r,Ru(e,t)}}function Vu(e,t,n){t=ri(n,t),t=Hs(e.stateNode,t,2),e=Aa(e,t,2),e!==null&&(Le(e,2),td(e))}function Hu(e,t,n){if(e.tag===3)Vu(e,e,n);else for(;t!==null;){if(t.tag===3){Vu(t,e,n);break}if(t.tag===1){var r=t.stateNode;if(typeof t.type.getDerivedStateFromError==`function`||typeof r.componentDidCatch==`function`&&(eu===null||!eu.has(r))){e=ri(n,e),n=Us(2),r=Aa(t,n,2),r!==null&&(Ws(n,r,t,e),Le(r,2),td(r));break}}t=t.return}}function Uu(e,t,n){var r=e.pingCache;if(r===null){r=e.pingCache=new Al;var i=new Set;r.set(t,i)}else i=r.get(t),i===void 0&&(i=new Set,r.set(t,i));i.has(n)||(zl=!0,i.add(n),e=Wu.bind(null,e,t,n),t.then(e,e))}function Wu(e,t,n){var r=e.pingCache;r!==null&&r.delete(t),e.pingedLanes|=e.suspendedLanes&n,e.warmLanes&=~n,Ml===e&&(Pl&n)===n&&(Vl===4||Vl===3&&(Pl&62914560)===Pl&&300>X()-Xl?!(jl&2)&&yu(e,0):Wl|=n,Kl===Pl&&(Kl=0)),td(e)}function Gu(e,t){t===0&&(t=Fe()),e=Hr(e,t),e!==null&&(Le(e,t),td(e))}function Ku(e){var t=e.memoizedState,n=0;t!==null&&(n=t.retryLane),Gu(e,n)}function qu(e,t){var n=0;switch(e.tag){case 31:case 13:var r=e.stateNode,a=e.memoizedState;a!==null&&(n=a.retryLane);break;case 19:r=e.stateNode;break;case 22:r=e.stateNode._retryCache;break;default:throw Error(i(314))}r!==null&&r.delete(t),Gu(e,n)}function Ju(e,t){return ue(e,t)}var Yu=null,Xu=null,Zu=!1,Qu=!1,$u=!1,ed=0;function td(e){e!==Xu&&e.next===null&&(Xu===null?Yu=Xu=e:Xu=Xu.next=e),Qu=!0,Zu||(Zu=!0,cd())}function nd(e,t){if(!$u&&Qu){$u=!0;do for(var n=!1,r=Yu;r!==null;){if(!t){if(e!==0){var i=r.pendingLanes;if(i===0)var a=0;else{var o=r.suspendedLanes,s=r.pingedLanes;a=(1<<31-we(42|e)+1)-1,a&=i&~(o&~s),a=a&201326741?a&201326741|1:a?a|2:0}a!==0&&(n=!0,sd(r,a))}else a=Pl,a=Me(r,r===Ml?a:0,r.cancelPendingCommit!==null||r.timeoutHandle!==-1),!(a&3)||Ne(r,a)||(n=!0,sd(r,a))}r=r.next}while(n);$u=!1}}function rd(){id()}function id(){Qu=Zu=!1;var e=0;ed!==0&&Gd()&&(e=ed);for(var t=X(),n=null,r=Yu;r!==null;){var i=r.next,a=ad(r,t);a===0?(r.next=null,n===null?Yu=i:n.next=i,i===null&&(Xu=n)):(n=r,(e!==0||a&3)&&(Qu=!0)),r=i}tu!==0&&tu!==5||nd(e,!1),ed!==0&&(ed=0)}function ad(e,t){for(var n=e.suspendedLanes,r=e.pingedLanes,i=e.expirationTimes,a=e.pendingLanes&-62914561;0s)break;var u=c.transferSize,d=c.initiatorType;u&&Id(d)&&(c=c.responseEnd,o+=u*(c`u`?null:document;function xf(e,t,n){var r=bf;if(r&&typeof t==`string`&&t){var i=wt(t);i=`link[rel="`+e+`"][href="`+i+`"]`,typeof n==`string`&&(i+=`[crossorigin="`+n+`"]`),hf.has(i)||(hf.add(i),e={rel:e,crossOrigin:n,href:t},r.querySelector(i)===null&&(t=r.createElement(`link`),Pd(t,`link`,e),ot(t),r.head.appendChild(t)))}}function Sf(e){_f.D(e),xf(`dns-prefetch`,e,null)}function Cf(e,t){_f.C(e,t),xf(`preconnect`,e,t)}function wf(e,t,n){_f.L(e,t,n);var r=bf;if(r&&e&&t){var i=`link[rel="preload"][as="`+wt(t)+`"]`;t===`image`&&n&&n.imageSrcSet?(i+=`[imagesrcset="`+wt(n.imageSrcSet)+`"]`,typeof n.imageSizes==`string`&&(i+=`[imagesizes="`+wt(n.imageSizes)+`"]`)):i+=`[href="`+wt(e)+`"]`;var a=i;switch(t){case`style`:a=Af(e);break;case`script`:a=Pf(e)}mf.has(a)||(e=h({rel:`preload`,href:t===`image`&&n&&n.imageSrcSet?void 0:e,as:t},n),mf.set(a,e),r.querySelector(i)!==null||t===`style`&&r.querySelector(jf(a))||t===`script`&&r.querySelector(Ff(a))||(t=r.createElement(`link`),Pd(t,`link`,e),ot(t),r.head.appendChild(t)))}}function Tf(e,t){_f.m(e,t);var n=bf;if(n&&e){var r=t&&typeof t.as==`string`?t.as:`script`,i=`link[rel="modulepreload"][as="`+wt(r)+`"][href="`+wt(e)+`"]`,a=i;switch(r){case`audioworklet`:case`paintworklet`:case`serviceworker`:case`sharedworker`:case`worker`:case`script`:a=Pf(e)}if(!mf.has(a)&&(e=h({rel:`modulepreload`,href:e},t),mf.set(a,e),n.querySelector(i)===null)){switch(r){case`audioworklet`:case`paintworklet`:case`serviceworker`:case`sharedworker`:case`worker`:case`script`:if(n.querySelector(Ff(a)))return}r=n.createElement(`link`),Pd(r,`link`,e),ot(r),n.head.appendChild(r)}}}function Ef(e,t,n){_f.S(e,t,n);var r=bf;if(r&&e){var i=at(r).hoistableStyles,a=Af(e);t||=`default`;var o=i.get(a);if(!o){var s={loading:0,preload:null};if(o=r.querySelector(jf(a)))s.loading=5;else{e=h({rel:`stylesheet`,href:e,"data-precedence":t},n),(n=mf.get(a))&&Rf(e,n);var c=o=r.createElement(`link`);ot(c),Pd(c,`link`,e),c._p=new Promise(function(e,t){c.onload=e,c.onerror=t}),c.addEventListener(`load`,function(){s.loading|=1}),c.addEventListener(`error`,function(){s.loading|=2}),s.loading|=4,Lf(o,t,r)}o={type:`stylesheet`,instance:o,count:1,state:s},i.set(a,o)}}}function Df(e,t){_f.X(e,t);var n=bf;if(n&&e){var r=at(n).hoistableScripts,i=Pf(e),a=r.get(i);a||(a=n.querySelector(Ff(i)),a||(e=h({src:e,async:!0},t),(t=mf.get(i))&&zf(e,t),a=n.createElement(`script`),ot(a),Pd(a,`link`,e),n.head.appendChild(a)),a={type:`script`,instance:a,count:1,state:null},r.set(i,a))}}function Of(e,t){_f.M(e,t);var n=bf;if(n&&e){var r=at(n).hoistableScripts,i=Pf(e),a=r.get(i);a||(a=n.querySelector(Ff(i)),a||(e=h({src:e,async:!0,type:`module`},t),(t=mf.get(i))&&zf(e,t),a=n.createElement(`script`),ot(a),Pd(a,`link`,e),n.head.appendChild(a)),a={type:`script`,instance:a,count:1,state:null},r.set(i,a))}}function kf(e,t,n,r){var a=(a=G.current)?gf(a):null;if(!a)throw Error(i(446));switch(e){case`meta`:case`title`:return null;case`style`:return typeof n.precedence==`string`&&typeof n.href==`string`?(t=Af(n.href),n=at(a).hoistableStyles,r=n.get(t),r||(r={type:`style`,instance:null,count:0,state:null},n.set(t,r)),r):{type:`void`,instance:null,count:0,state:null};case`link`:if(n.rel===`stylesheet`&&typeof n.href==`string`&&typeof n.precedence==`string`){e=Af(n.href);var o=at(a).hoistableStyles,s=o.get(e);if(s||(a=a.ownerDocument||a,s={type:`stylesheet`,instance:null,count:0,state:{loading:0,preload:null}},o.set(e,s),(o=a.querySelector(jf(e)))&&!o._p&&(s.instance=o,s.state.loading=5),mf.has(e)||(n={rel:`preload`,as:`style`,href:n.href,crossOrigin:n.crossOrigin,integrity:n.integrity,media:n.media,hrefLang:n.hrefLang,referrerPolicy:n.referrerPolicy},mf.set(e,n),o||Nf(a,e,n,s.state))),t&&r===null)throw Error(i(528,``));return s}if(t&&r!==null)throw Error(i(529,``));return null;case`script`:return t=n.async,n=n.src,typeof n==`string`&&t&&typeof t!=`function`&&typeof t!=`symbol`?(t=Pf(n),n=at(a).hoistableScripts,r=n.get(t),r||(r={type:`script`,instance:null,count:0,state:null},n.set(t,r)),r):{type:`void`,instance:null,count:0,state:null};default:throw Error(i(444,e))}}function Af(e){return`href="`+wt(e)+`"`}function jf(e){return`link[rel="stylesheet"][`+e+`]`}function Mf(e){return h({},e,{"data-precedence":e.precedence,precedence:null})}function Nf(e,t,n,r){e.querySelector(`link[rel="preload"][as="style"][`+t+`]`)?r.loading=1:(t=e.createElement(`link`),r.preload=t,t.addEventListener(`load`,function(){return r.loading|=1}),t.addEventListener(`error`,function(){return r.loading|=2}),Pd(t,`link`,n),ot(t),e.head.appendChild(t))}function Pf(e){return`[src="`+wt(e)+`"]`}function Ff(e){return`script[async]`+e}function If(e,t,n){if(t.count++,t.instance===null)switch(t.type){case`style`:var r=e.querySelector(`style[data-href~="`+wt(n.href)+`"]`);if(r)return t.instance=r,ot(r),r;var a=h({},n,{"data-href":n.href,"data-precedence":n.precedence,href:null,precedence:null});return r=(e.ownerDocument||e).createElement(`style`),ot(r),Pd(r,`style`,a),Lf(r,n.precedence,e),t.instance=r;case`stylesheet`:a=Af(n.href);var o=e.querySelector(jf(a));if(o)return t.state.loading|=4,t.instance=o,ot(o),o;r=Mf(n),(a=mf.get(a))&&Rf(r,a),o=(e.ownerDocument||e).createElement(`link`),ot(o);var s=o;return s._p=new Promise(function(e,t){s.onload=e,s.onerror=t}),Pd(o,`link`,r),t.state.loading|=4,Lf(o,n.precedence,e),t.instance=o;case`script`:return o=Pf(n.src),(a=e.querySelector(Ff(o)))?(t.instance=a,ot(a),a):(r=n,(a=mf.get(o))&&(r=h({},n),zf(r,a)),e=e.ownerDocument||e,a=e.createElement(`script`),ot(a),Pd(a,`link`,r),e.head.appendChild(a),t.instance=a);case`void`:return null;default:throw Error(i(443,t.type))}else t.type===`stylesheet`&&!(t.state.loading&4)&&(r=t.instance,t.state.loading|=4,Lf(r,n.precedence,e));return t.instance}function Lf(e,t,n){for(var r=n.querySelectorAll(`link[rel="stylesheet"][data-precedence],style[data-precedence]`),i=r.length?r[r.length-1]:null,a=i,o=0;o title`):null)}function Uf(e,t,n){if(n===1||t.itemProp!=null)return!1;switch(e){case`meta`:case`title`:return!0;case`style`:if(typeof t.precedence!=`string`||typeof t.href!=`string`||t.href===``)break;return!0;case`link`:if(typeof t.rel!=`string`||typeof t.href!=`string`||t.href===``||t.onLoad||t.onError)break;switch(t.rel){case`stylesheet`:return e=t.disabled,typeof t.precedence==`string`&&e==null;default:return!0}case`script`:if(t.async&&typeof t.async!=`function`&&typeof t.async!=`symbol`&&!t.onLoad&&!t.onError&&t.src&&typeof t.src==`string`)return!0}return!1}function Wf(e){return!(e.type===`stylesheet`&&!(e.state.loading&3))}function Gf(e,t,n,r){if(n.type===`stylesheet`&&(typeof r.media!=`string`||!1!==matchMedia(r.media).matches)&&!(n.state.loading&4)){if(n.instance===null){var i=Af(r.href),a=t.querySelector(jf(i));if(a){t=a._p,typeof t==`object`&&t&&typeof t.then==`function`&&(e.count++,e=Jf.bind(e),t.then(e,e)),n.state.loading|=4,n.instance=a,ot(a);return}a=t.ownerDocument||t,r=Mf(r),(i=mf.get(i))&&Rf(r,i),a=a.createElement(`link`),ot(a);var o=a;o._p=new Promise(function(e,t){o.onload=e,o.onerror=t}),Pd(a,`link`,r),n.instance=a}e.stylesheets===null&&(e.stylesheets=new Map),e.stylesheets.set(n,t),(t=n.state.preload)&&!(n.state.loading&3)&&(e.count++,n=Jf.bind(e),t.addEventListener(`load`,n),t.addEventListener(`error`,n))}}var Kf=0;function qf(e,t){return e.stylesheets&&e.count===0&&Xf(e,e.stylesheets),0Kf?50:800)+t);return e.unsuspend=n,function(){e.unsuspend=null,clearTimeout(r),clearTimeout(i)}}:null}function Jf(){if(this.count--,this.count===0&&(this.imgCount===0||!this.waitingForImages)){if(this.stylesheets)Xf(this,this.stylesheets);else if(this.unsuspend){var e=this.unsuspend;this.unsuspend=null,e()}}}var Yf=null;function Xf(e,t){e.stylesheets=null,e.unsuspend!==null&&(e.count++,Yf=new Map,t.forEach(Zf,e),Yf=null,Jf.call(e))}function Zf(e,t){if(!(t.state.loading&4)){var n=Yf.get(e);if(n)var r=n.get(null);else{n=new Map,Yf.set(e,n);for(var i=e.querySelectorAll(`link[data-precedence],style[data-precedence]`),a=0;a{function n(){if(!(typeof __REACT_DEVTOOLS_GLOBAL_HOOK__>`u`||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!=`function`))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(n)}catch(e){console.error(e)}}n(),t.exports=h()}))(),1),_=c(u(),1),v=new Map,y=3e4,b=`ocx-resource-deadline`,x={data:void 0,error:void 0,loading:!1,refreshing:!1,hasSucceeded:!1,lastAttemptOk:!1};function S(e){let t=v.get(e);return t||(t={snapshot:{data:void 0,error:void 0,loading:!1,refreshing:!1,hasSucceeded:!1,lastAttemptOk:!1},listeners:new Set,pollByListener:new Map,pauseWhenHiddenByListener:new Map,fetcherByListener:new Map,deadlineByListener:new Map,subscriberCount:0,pollIntervalMs:void 0,inflight:null,inflightOwner:null,generation:0,seedNeedsRevalidate:!1,lastSettledAt:void 0},v.set(e,t)),t}function C(e){for(let t of e.listeners)t()}var w=new Map;function T(e){for(let[t,n]of e.pollByListener)if(typeof n==`number`&&n>0&&e.pauseWhenHiddenByListener.get(t)===!1)return!0;return!1}function E(e){if(e.stores.size===0)return!1;if(!M())return!0;for(let t of e.stores)if(T(t))return!0;return!1}function D(e){for(let t of e.stores){let e=N(t);e&&z(t,e.fetcher,{replaceInflight:!1,owner:e.owner,deadlineMs:e.deadlineMs})}}function O(e,t){let n=E(t);if(n&&t.timer===null){t.timer=setInterval(()=>D(t),e);return}!n&&t.timer!==null&&(clearInterval(t.timer),t.timer=null),t.stores.size===0&&w.delete(e)}function k(){for(let[e,t]of[...w])O(e,t)}function A(e){let t=e.pollIntervalMs;if(t===void 0)return;let n=w.get(t);e.pollIntervalMs=void 0,n&&(n.stores.delete(e),O(t,n))}function j(e,t){let n=w.get(t);n||(n={timer:null,stores:new Set},w.set(t,n)),n.stores.add(e),e.pollIntervalMs=t,O(t,n)}function M(){return typeof document<`u`&&document.visibilityState===`hidden`}function N(e){if(!M())return P(e);for(let[t,n]of e.pollByListener)if(!(typeof n!=`number`||n<=0)&&e.pauseWhenHiddenByListener.get(t)===!1){let n=e.fetcherByListener.get(t);if(n)return{owner:t,fetcher:n,deadlineMs:e.deadlineByListener.get(t)}}return null}function P(e){for(let[t,n]of e.pollByListener)if(typeof n==`number`&&n>0){let n=e.fetcherByListener.get(t);if(n)return{owner:t,fetcher:n,deadlineMs:e.deadlineByListener.get(t)}}for(let[t,n]of e.fetcherByListener)return{owner:t,fetcher:n,deadlineMs:e.deadlineByListener.get(t)};return null}function F(e){let t;for(let n of e.pollByListener.values())typeof n==`number`&&n>0&&(t=t===void 0?n:Math.min(t,n));if(t===void 0){A(e),R(e);return}if(t===e.pollIntervalMs){let n=w.get(t);n&&O(t,n),L(e);return}A(e),j(e,t),L(e)}var I=null;function L(e){if(typeof document>`u`||I)return;let t=()=>{if(k(),!M())for(let e of w.values())for(let t of e.stores){let e=P(t);e&&z(t,e.fetcher,{replaceInflight:!1,owner:e.owner,deadlineMs:e.deadlineMs})}};document.addEventListener(`visibilitychange`,t),I=t}function R(e){I&&(w.size>0||(typeof document<`u`&&document.removeEventListener(`visibilitychange`,I),I=null))}async function z(e,t,n){let r=n?.replaceInflight!==!1;if(e.inflight&&!r)return;r&&e.inflight?.abort();let i=new AbortController;e.inflight=i,e.inflightOwner=n?.owner??null;let a=++e.generation,o=n?.deadlineMs??y,s=!1,c=setTimeout(()=>{s=!0,i.abort(b)},o),l=e.snapshot.data===void 0||n?.forceLoading===!0;e.snapshot={...e.snapshot,loading:l?!0:e.snapshot.loading,refreshing:!0},C(e);try{let n=await Promise.race([t(i.signal),new Promise((e,t)=>{i.signal.addEventListener(`abort`,()=>{s?t(Error(`resource request timed out after ${o}ms`)):e(null)},{once:!0})})]);if(a!==e.generation||i.signal.aborted)return;e.seedNeedsRevalidate=!1,e.lastSettledAt=Date.now(),e.snapshot={data:n,error:void 0,loading:!1,refreshing:!1,hasSucceeded:!0,lastAttemptOk:!0}}catch(t){if(a!==e.generation||i.signal.aborted&&!s)return;e.seedNeedsRevalidate=!1,e.snapshot={...e.snapshot,error:t===void 0?Error(`resource load failed`):t,loading:!1,refreshing:!1,lastAttemptOk:!1}}finally{clearTimeout(c),e.inflight===i&&(e.inflight=null,e.inflightOwner=null),C(e)}}function B(e,t){return e.inflightOwner===t&&(e.inflight?.abort(),e.inflight=null,e.inflightOwner=null,e.generation++,e.snapshot.refreshing&&(e.snapshot={...e.snapshot,refreshing:!1},C(e)),!0)}function V(e,t){A(t),R(t),setTimeout(()=>{t.subscriberCount===0&&v.get(e)===t&&(t.inflight?.abort(),t.inflight=null,t.inflightOwner=null,v.delete(e))},0)}function H(e,t,n){let{fetcher:r,pollMs:i,pauseWhenHidden:a=!0,deadlineMs:o,staleAfterMs:s}=n,c=S(e);if(c.listeners.add(t),c.pollByListener.set(t,i),c.pauseWhenHiddenByListener.set(t,a),c.fetcherByListener.set(t,r),c.deadlineByListener.set(t,o),c.subscriberCount++,c.subscriberCount===1){let e=typeof s==`number`&&c.lastSettledAt!==void 0&&Date.now()-c.lastSettledAt>s;(c.snapshot.data===void 0||c.seedNeedsRevalidate||e)&&z(c,r,{replaceInflight:!0,owner:t,deadlineMs:o})}return F(c),()=>{c.listeners.delete(t),c.pollByListener.delete(t),c.pauseWhenHiddenByListener.delete(t),c.fetcherByListener.delete(t),c.deadlineByListener.delete(t),c.subscriberCount--;let n=B(c,t);if(c.subscriberCount===0){V(e,c);return}if(n){let e=P(c);e&&z(c,e.fetcher,{replaceInflight:!0,owner:e.owner,deadlineMs:e.deadlineMs})}F(c)}}function U(e,t,n,r){let i=S(e);i.subscriberCount===0&&i.snapshot.data===void 0&&(K(e,t),typeof r==`number`&&typeof n==`number`&&Date.now()-n{c.current=t});let l=(0,_.useCallback)(e=>c.current(e),[]),u=(0,_.useRef)(null),d=(0,_.useCallback)(t=>r?(u.current=t,H(e,t,{fetcher:l,pollMs:i,pauseWhenHidden:a,deadlineMs:o,staleAfterMs:s})):()=>{},[e,l,i,r,a,o,s]),f=(0,_.useCallback)(()=>r?S(e).snapshot:x,[e,r]),p=(0,_.useSyncExternalStore)(d,f,f),m=(0,_.useCallback)(t=>{if(!r)return;let n=S(e);z(n,l,{replaceInflight:!0,owner:u.current,forceLoading:t?.forceLoading,deadlineMs:u.current?n.deadlineByListener.get(u.current):o})},[e,l,r,o]);return{...p,refresh:m}}function ee(e,t){if(e===null)return!1;if(e.length!==t.length)return!0;for(let n=0;n{let n=a.current,r=o.current;a.current=t,o.current=e,ee(n,t)&&(r===null||r===e)&&i.refresh({forceLoading:!0})}),i}function K(e,t){let n=S(e);n.inflight?.abort(),n.inflight=null,n.inflightOwner=null,n.generation++,n.snapshot={data:t,error:void 0,loading:!1,refreshing:!1,hasSucceeded:!0,lastAttemptOk:!0},n.seedNeedsRevalidate=n.subscriberCount===0,n.lastSettledAt=Date.now(),C(n)}var q=o((e=>{var t=Symbol.for(`react.transitional.element`),n=Symbol.for(`react.fragment`);function r(e,n,r){var i=null;if(r!==void 0&&(i=``+r),n.key!==void 0&&(i=``+n.key),`key`in n)for(var a in r={},n)a!==`key`&&(r[a]=n[a]);else r=n;return n=r.ref,{$$typeof:t,type:e,key:i,ref:n===void 0?null:n,props:r}}e.Fragment=n,e.jsx=r,e.jsxs=r})),J=o(((e,t)=>{t.exports=q()}))(),Y=e=>({viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,strokeWidth:2,strokeLinecap:`round`,strokeLinejoin:`round`,...e}),te=e=>(0,J.jsxs)(`svg`,{...Y(e),children:[(0,J.jsx)(`rect`,{x:`3`,y:`3`,width:`7`,height:`7`,rx:`1.5`}),(0,J.jsx)(`rect`,{x:`14`,y:`3`,width:`7`,height:`7`,rx:`1.5`}),(0,J.jsx)(`rect`,{x:`3`,y:`14`,width:`7`,height:`7`,rx:`1.5`}),(0,J.jsx)(`rect`,{x:`14`,y:`14`,width:`7`,height:`7`,rx:`1.5`})]}),ne=e=>(0,J.jsxs)(`svg`,{...Y(e),children:[(0,J.jsx)(`rect`,{x:`3`,y:`4`,width:`18`,height:`7`,rx:`2`}),(0,J.jsx)(`rect`,{x:`3`,y:`13`,width:`18`,height:`7`,rx:`2`}),(0,J.jsx)(`path`,{d:`M7 7.5h.01M7 16.5h.01`})]}),re=e=>(0,J.jsxs)(`svg`,{...Y(e),children:[(0,J.jsx)(`path`,{d:`M12 2 4 6v6l8 4 8-4V6l-8-4Z`}),(0,J.jsx)(`path`,{d:`m4 6 8 4 8-4M12 10v8`})]}),ie=e=>(0,J.jsxs)(`svg`,{...Y(e),children:[(0,J.jsx)(`rect`,{x:`4`,y:`8`,width:`16`,height:`11`,rx:`3`}),(0,J.jsx)(`path`,{d:`M12 8V4M8 2h8`}),(0,J.jsx)(`circle`,{cx:`9`,cy:`13`,r:`1`}),(0,J.jsx)(`circle`,{cx:`15`,cy:`13`,r:`1`})]}),ae=e=>(0,J.jsx)(`svg`,{...Y(e),children:(0,J.jsx)(`path`,{d:`M8 6h13M8 12h13M8 18h13M3 6h.01M3 12h.01M3 18h.01`})}),oe=e=>(0,J.jsx)(`svg`,{...Y(e),children:(0,J.jsx)(`path`,{d:`M4 6h16M4 12h16M4 18h16`})}),se=e=>(0,J.jsxs)(`svg`,{...Y(e),children:[(0,J.jsx)(`path`,{d:`m4 17 6-5-6-5`}),(0,J.jsx)(`path`,{d:`M12 19h8`})]}),ce=e=>(0,J.jsx)(`svg`,{...Y(e),children:(0,J.jsx)(`path`,{d:`M22 12h-4l-3 9L9 3l-3 9H2`})}),le=e=>(0,J.jsxs)(`svg`,{...Y(e),children:[(0,J.jsx)(`path`,{d:`M22 12H2`}),(0,J.jsx)(`path`,{d:`M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11Z`}),(0,J.jsx)(`path`,{d:`M6 16h.01M10 16h.01`})]}),ue=e=>(0,J.jsx)(`svg`,{...Y(e),children:(0,J.jsx)(`path`,{d:`m20 6-11 11-5-5`})}),de=e=>(0,J.jsx)(`svg`,{...Y(e),children:(0,J.jsx)(`path`,{d:`M18 6 6 18M6 6l12 12`})}),fe=e=>(0,J.jsx)(`svg`,{...Y(e),children:(0,J.jsx)(`path`,{d:`M12 5v14M5 12h14`})}),pe=e=>(0,J.jsx)(`svg`,{...Y(e),children:(0,J.jsx)(`path`,{d:`M21 12a9 9 0 0 1-9 9 9.8 9.8 0 0 1-6.7-2.7L3 16M3 21v-5h5M3 12a9 9 0 0 1 9-9 9.8 9.8 0 0 1 6.7 2.7L21 8M21 3v5h-5`})}),X=e=>(0,J.jsx)(`svg`,{...Y(e),children:(0,J.jsx)(`path`,{d:`M8 5v14M16 5v14`})}),me=e=>(0,J.jsx)(`svg`,{...Y(e),children:(0,J.jsx)(`path`,{d:`m7 4 13 8-13 8Z`})}),he=e=>(0,J.jsx)(`svg`,{...Y(e),children:(0,J.jsx)(`path`,{d:`M3 6h18M8 6V4h8v2M19 6l-1 14H6L5 6`})}),ge=e=>(0,J.jsxs)(`svg`,{...Y(e),children:[(0,J.jsx)(`path`,{d:`M12 20h9`}),(0,J.jsx)(`path`,{d:`M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4Z`})]}),_e=e=>(0,J.jsxs)(`svg`,{...Y(e),children:[(0,J.jsx)(`path`,{d:`M10.3 3.7 1.8 18a2 2 0 0 0 1.7 3h17a2 2 0 0 0 1.7-3L13.7 3.7a2 2 0 0 0-3.4 0Z`}),(0,J.jsx)(`path`,{d:`M12 9v4M12 17h.01`})]}),Z=e=>(0,J.jsxs)(`svg`,{...Y(e),children:[(0,J.jsx)(`circle`,{cx:`12`,cy:`12`,r:`10`}),(0,J.jsx)(`path`,{d:`M12 16v-4M12 8h.01`})]}),ve=e=>(0,J.jsxs)(`svg`,{...Y(e),children:[(0,J.jsx)(`circle`,{cx:`11`,cy:`11`,r:`7`}),(0,J.jsx)(`path`,{d:`m21 21-4.3-4.3`})]}),ye=e=>(0,J.jsx)(`svg`,{...Y(e),children:(0,J.jsx)(`path`,{d:`M12 19V5M5 12l7-7 7 7`})}),be=e=>(0,J.jsx)(`svg`,{...Y(e),children:(0,J.jsx)(`path`,{d:`M12 5v14M19 12l-7 7-7-7`})}),xe=e=>(0,J.jsx)(`svg`,{...Y(e),children:(0,J.jsx)(`path`,{d:`M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3`})}),Se=e=>(0,J.jsx)(`svg`,{...Y(e),children:(0,J.jsx)(`path`,{d:`m9 18 6-6-6-6`})}),Ce=e=>(0,J.jsx)(`svg`,{...Y(e),children:(0,J.jsx)(`path`,{d:`M9 19c-5 1.5-5-2.5-7-3m14 6v-3.9a3.4 3.4 0 0 0-.9-2.6c3-.3 6.2-1.5 6.2-6.7A5.2 5.2 0 0 0 20 4.8 4.9 4.9 0 0 0 19.9 1S18.7.6 16 2.5a13.4 13.4 0 0 0-7 0C6.3.6 5.1 1 5.1 1A4.9 4.9 0 0 0 5 4.8a5.2 5.2 0 0 0-1.4 3.7c0 5.1 3.1 6.4 6.1 6.7a3.4 3.4 0 0 0-.9 2.5V22`})}),we=e=>(0,J.jsxs)(`svg`,{...Y(e),children:[(0,J.jsx)(`path`,{d:`M18.4 5.6a9 9 0 1 1-12.8 0`}),(0,J.jsx)(`path`,{d:`M12 2v10`})]}),Te=e=>(0,J.jsx)(`svg`,{...Y(e),children:(0,J.jsx)(`path`,{d:`M15 3h6v6M10 14 21 3M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6`})}),Ee=e=>(0,J.jsxs)(`svg`,{...Y(e),children:[(0,J.jsx)(`circle`,{cx:`7.5`,cy:`15.5`,r:`4.5`}),(0,J.jsx)(`path`,{d:`m10.7 12.3 9.6-9.6M16 7l3 3M14 9l2 2`})]}),De=e=>(0,J.jsxs)(`svg`,{...Y(e),children:[(0,J.jsx)(`rect`,{x:`4`,y:`11`,width:`16`,height:`10`,rx:`2`}),(0,J.jsx)(`path`,{d:`M8 11V7a4 4 0 0 1 8 0v4`})]}),Oe=e=>(0,J.jsxs)(`svg`,{...Y(e),children:[(0,J.jsx)(`path`,{d:`M2 9a3 3 0 0 1 0 6v2a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-2a3 3 0 0 1 0-6V7a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2Z`}),(0,J.jsx)(`path`,{d:`M13 5v2`}),(0,J.jsx)(`path`,{d:`M13 17v2`}),(0,J.jsx)(`path`,{d:`M13 11v2`})]}),ke=e=>(0,J.jsx)(`svg`,{...Y(e),children:(0,J.jsx)(`path`,{d:`M9 17H7A5 5 0 0 1 7 7h2M15 7h2a5 5 0 0 1 0 10h-2M8 12h8`})}),Ae=e=>(0,J.jsxs)(`svg`,{...Y(e),children:[(0,J.jsx)(`circle`,{cx:`12`,cy:`12`,r:`4`}),(0,J.jsx)(`path`,{d:`M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4`})]}),je=e=>(0,J.jsx)(`svg`,{...Y(e),children:(0,J.jsx)(`path`,{d:`M21 12.8A9 9 0 1 1 11.2 3 7 7 0 0 0 21 12.8Z`})}),Me=e=>(0,J.jsxs)(`svg`,{...Y(e),children:[(0,J.jsx)(`rect`,{x:`2`,y:`3`,width:`20`,height:`14`,rx:`2`}),(0,J.jsx)(`path`,{d:`M8 21h8M12 17v4`})]}),Ne=e=>(0,J.jsxs)(`svg`,{...Y(e),children:[(0,J.jsx)(`circle`,{cx:`12`,cy:`12`,r:`9`}),(0,J.jsx)(`path`,{d:`M3 12h18M12 3a14 14 0 0 1 0 18M12 3a14 14 0 0 0 0 18`})]}),Pe=e=>(0,J.jsxs)(`svg`,{...Y(e),children:[(0,J.jsx)(`path`,{d:`m18 14 4 4-4 4`}),(0,J.jsx)(`path`,{d:`m18 2 4 4-4 4`}),(0,J.jsx)(`path`,{d:`M2 18h1.973a4 4 0 0 0 3.3-1.7l5.454-8.6a4 4 0 0 1 3.3-1.7H22`}),(0,J.jsx)(`path`,{d:`M2 6h1.972a4 4 0 0 1 3.6 2.2`}),(0,J.jsx)(`path`,{d:`M22 18h-6.041a4 4 0 0 1-3.3-1.8l-.359-.45`})]}),Fe=e=>(0,J.jsxs)(`svg`,{...Y(e),children:[(0,J.jsx)(`circle`,{cx:`9`,cy:`6`,r:`1`,fill:`currentColor`,stroke:`none`}),(0,J.jsx)(`circle`,{cx:`15`,cy:`6`,r:`1`,fill:`currentColor`,stroke:`none`}),(0,J.jsx)(`circle`,{cx:`9`,cy:`12`,r:`1`,fill:`currentColor`,stroke:`none`}),(0,J.jsx)(`circle`,{cx:`15`,cy:`12`,r:`1`,fill:`currentColor`,stroke:`none`}),(0,J.jsx)(`circle`,{cx:`9`,cy:`18`,r:`1`,fill:`currentColor`,stroke:`none`}),(0,J.jsx)(`circle`,{cx:`15`,cy:`18`,r:`1`,fill:`currentColor`,stroke:`none`})]}),Ie=e=>(0,J.jsx)(`svg`,{...Y(e),children:(0,J.jsx)(`path`,{d:`m12 2 3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z`})}),Le=e=>(0,J.jsx)(`svg`,{...Y(e),children:(0,J.jsx)(`path`,{d:`M4 5h16l-6 7v5l-4 2v-7L4 5z`})}),Re={"nav.dashboard":`Dashboard`,"uptime.day":`d`,"uptime.hour":`h`,"uptime.minute":`m`,"uptime.second":`s`,"nav.startup":`Startup`,"nav.providers":`Providers`,"nav.models":`Models`,"nav.combos":`Combos`,"nav.subagents":`Subagents`,"nav.logs":`Logs & Debug`,"nav.usage":`Usage`,"common.github":`GitHub`,"sidebar.star":`Star on GitHub`,"sidebar.starred":`Starred on GitHub`,"sidebar.starUnauthenticated":`Open GitHub to star (gh CLI is not signed in)`,"sidebar.starFailed":`Could not star through gh. Opening GitHub instead.`,"sidebar.updateAvailable":`Update available: {version}`,"sidebar.checkUpdate":`Check for updates`,"common.save":`Save`,"common.saving":`Saving…`,"common.cancel":`Cancel`,"common.discard":`Discard`,"common.delete":`Delete`,"common.close":`Close`,"common.ok":`OK`,"common.remove":`Remove`,"common.loading":`Loading…`,"common.retry":`Retry`,"auth.adminTokenTitle":`OpenCodex admin token (OPENCODEX_ADMIN_AUTH_TOKEN)`,"auth.adminAccountLabel":`Account`,"auth.adminTokenFieldLabel":`Admin token`,"auth.adminTokenRejected":`That admin token was rejected. Check it and try again.`,"auth.adminTokenUnavailable":`The admin token could not be verified. Try again.`,"app.logoAria":`opencodex logo`,"app.claudeOn":`Claude ON`,"app.claudeOff":`Claude OFF`,"theme.label":`Theme`,"theme.light":`Light`,"theme.dark":`Dark`,"theme.system":`System`,"lang.label":`Language`,"lang.nativeName":`English`,"provider.name.commandCodeAuth":`Command Code - Auth`,"provider.name.commandCodeApi":`Command Code - API`,"provider.name.volcengine":`Volcengine Ark`,"provider.name.volcengineCodingPlan":`Volcengine Ark Coding Plan`,"provider.name.volcengineAgentPlan":`Volcengine Ark Agent Plan`,"errorBoundary.title":`Page failed to load`,"errorBoundary.message":`This section hit a rendering error. Reload it to try again.`,"errorBoundary.details":`Error`,"errorBoundary.reload":`Reload`,"routing.title":`Routing Intelligence (beta)`,"routing.subtitle":`Policy profiles, dry-run evaluation, and source-backed routing analytics.`,"routing.loadFailed":`Could not load routing data`,"routing.empty":"No routing profiles configured. Add `routingProfiles` to config.json.","routing.revision":`rev`,"routing.detail":`Profile`,"routing.createProfile":`Create profile`,"routing.dryRunError":`Dry-run failed (HTTP {status})`,"routing.removeConfirm":`Remove profile {id}?`,"routing.unknownEvidence.allow":`allow`,"routing.unknownEvidence.penalize":`penalize`,"routing.unknownEvidence.exclude":`exclude`,"routing.removeCandidate":`Remove candidate {provider}/{model}`,"routing.candidates":`Candidates`,"routing.require":`Hard requirements`,"routing.optimize":`Optimization weights`,"routing.limits":`Limits`,"routing.unknownEvidence":`Unknown evidence policy`,"routing.compatibility.title":`Compatibility policy`,"routing.compatibility.enabled":`Require Compatibility Lab evidence`,"routing.compatibility.requiredSuites":`Required suites`,"routing.compatibility.loadingCatalog":`Loading lab catalog…`,"routing.compatibility.catalogUnavailable":`Lab catalog unavailable — enter suite ids manually in config.json.`,"routing.compatibility.layer.protocol_conformance":`Protocol conformance`,"routing.compatibility.layer.live_route_compatibility":`Live route compatibility`,"routing.compatibility.minStatus":`Minimum compatibility status`,"routing.none":`none`,"routing.unavailable":`–`,"routing.dryRun":`Dry-run evaluation`,"routing.dryRunContext":`Request context window (tokens)`,"routing.dryRunTools":`Request requires tools`,"routing.dryRunImage":`Request requires image input`,"routing.dryRunStructured":`Request requires structured output`,"routing.dryRunRun":`Evaluate candidates`,"routing.candidate":`Candidate`,"routing.eligible":`Eligible`,"routing.exclusions":`Exclusions`,"routing.costCap":`Cost cap`,"routing.capOutcome.satisfied":`within limit`,"routing.capOutcome.exceeded":`over limit`,"routing.capOutcome.unknown-allowed":`unknown (allowed)`,"routing.capOutcome.unknown-excluded":`unknown (excluded)`,"routing.exclusion.capability-unsatisfied":`capability not met`,"routing.exclusion.unknown-capability":`unknown capability`,"routing.exclusion.cost-limit":`over cost cap`,"routing.exclusion.cost-limit-unknown":`unknown cost under cap`,"routing.exclusion.cooldown":`cooldown`,"routing.exclusion.unknown-health":`unknown health`,"routing.exclusion.unknown-quota":`unknown quota`,"routing.exclusion.unknown-price":`unknown price`,"routing.exclusion.other":`exclusion: {code}`,"routing.score":`Score`,"routing.selected":`selected`,"routing.yes":`yes`,"routing.no":`no`,"routing.analytics":`Routing analytics`,"routing.analyticsTotal":`Requests`,"routing.analyticsSuccessRate":`Success`,"routing.analyticsFallbackRate":`Fallback`,"routing.analyticsP50":`p50`,"routing.analyticsP95":`p95`,"routing.analyticsP99":`p99`,"routing.analyticsCooldown":`Cooldown failures`,"routing.analyticsConfidence":`Confidence`,"routing.analyticsTruncated":`truncated history`,"routing.analyticsRequests":`Requests`,"routing.analyticsEmpty":`No analytics yet — send some requests first.`,"startup.title":`Startup safety`,"startup.subtitle":`Verify that Codex can reach opencodex after a restart, before local proxy routing becomes a reconnect loop.`,"startup.refresh":`Refresh`,"startup.backToDashboard":`Back to Dashboard`,"startup.loading":`Checking startup protection…`,"startup.error":`Could not read startup protection.`,"startup.staleData":`The latest startup check failed. The values below are stale and must not be treated as proof of protection.`,"startup.status.native":`Native routing`,"startup.status.protected":`Restart protected`,"startup.status.atRisk":`Action required`,"startup.summary.native":`Codex does not depend on the local proxy`,"startup.summary.protected":`opencodex will be available after restart`,"startup.summary.atRisk":`Codex can lose model access after restart`,"startup.riskDetail":`Codex is pinned to the local proxy, but no persistent service or healthy launcher shim will start it again.`,"startup.riskDetailCustomLocal":`Codex points to a custom local gateway. opencodex cannot manage or verify that gateway's restart lifecycle.`,"startup.riskDetailWindowsShim":`The launcher shim protects supported CLI scripts, but Codex Desktop and direct codex.exe launches can bypass it on Windows.`,"startup.safeDetail":`The current routing and startup mechanism are consistent. No manual ocx start should be required after restart.`,"startup.routing":`Codex routing`,"startup.routing.proxy":`Local proxy`,"startup.routing.native":`Native OpenAI`,"startup.routing.customLocal":`Custom local gateway`,"startup.routing.customRemote":`Custom remote gateway`,"startup.routing.unknown":`Unknown or invalid routing`,"startup.restartProtection":`Restart protection`,"startup.preference":`On-demand startup`,"startup.enabled":`Enabled`,"startup.disabled":`Disabled`,"startup.protection.service":`Background service`,"startup.protection.shim":`Launcher shim`,"startup.protection.none":`Not installed`,"startup.details":`Protection details`,"startup.service":`Background service`,"startup.serviceHint":`Starts at login and restarts the proxy after a crash.`,"startup.installed":`Installed`,"startup.notInstalled":`Not installed`,"startup.unsupported":`Unsupported`,"startup.shim":`Codex launcher shim`,"startup.shimHint":`Runs ocx ensure when a supported Codex script launcher starts.`,"startup.healthy":`Healthy`,"startup.cliOnly":`CLI only`,"startup.stale":`Stale`,"startup.viable":`Ready`,"startup.unhealthy":`Installed but unhealthy`,"startup.conflict":`Service conflict`,"startup.installedDisabled":`Installed but disabled`,"startup.install":`Install`,"startup.installing":`Installing…`,"startup.repair":`Repair`,"startup.repairing":`Repairing…`,"startup.serviceInstalled":`Background service installed successfully.`,"startup.serviceRepaired":`Background service repaired successfully.`,"startup.shimInstalled":`Codex launcher shim installed successfully.`,"startup.shimRepaired":`Codex launcher shim repaired successfully.`,"startup.installFailed":`Installation failed:`,"startup.tray.title":`Windows system tray`,"startup.tray.hint":`Install a login tray icon for one-click proxy start, stop, restart, dashboard, and status controls.`,"startup.tray.login":`Start tray at Windows login`,"startup.tray.notProtection":`The tray is a controller, not restart protection. A viable background service is still required for unattended proxy recovery.`,"startup.tray.running":`Running`,"startup.tray.stopped":`Installed, hidden`,"startup.tray.stale":`Repair required`,"startup.tray.notInstalled":`Not installed`,"startup.tray.loading":`Checking…`,"startup.tray.unavailable":`Status unavailable`,"startup.tray.install":`Install and show tray`,"startup.tray.start":`Show tray icon`,"startup.tray.stop":`Exit tray icon`,"startup.tray.uninstall":`Remove login tray`,"startup.tray.error":`The Windows tray action failed. Check ocx tray status for details.`,"startup.recovery":`Repair options`,"startup.recoveryHint":`Use the one-click installers above, or copy a command for manual repair. The background service is recommended for Codex Desktop and Windows executables.`,"startup.command.service":`Recommended: persistent background service`,"startup.command.shim":`Alternative: CLI launcher shim`,"startup.command.native":`Fail-safe: restore native Codex routing`,"startup.copy":`Copy`,"startup.copied":`Copied`,"startup.recommended":`Recommended repair: {cmd}`,"startup.navRisk":`Startup protection requires attention`,"startup.codexRuntime.clampHidden":`Some reasoning effort options were hidden because OpenCodex used Codex {version}.`,"startup.codexRuntime.clampHiddenWithEfforts":`Some reasoning effort options were hidden because OpenCodex used Codex {version} (removed: {efforts}).`,"startup.codexRuntime.olderBinary":`OpenCodex is using an older Codex binary ({version}). A newer installation is available.`,"dash.subtitle":`Live status of the local opencodex proxy, its providers, and the models routed into Codex.`,"dash.workspace.overview":`Overview`,"dash.workspace.sections":`Sections`,"dash.status":`Status`,"dash.online":`Online`,"dash.offline":`Offline`,"dash.version":`Version`,"dash.uptime":`Uptime`,"dash.providers":`Providers`,"dash.tokens30d":`Tokens (30d)`,"dash.coverage":`{pct} coverage`,"dash.mem.title":`Memory observability`,"dash.mem.hint":`Read-only runtime diagnostics. Observed memory is max(RSS, external, ArrayBuffers) so Windows working-set trimming does not hide committed retention.`,"dash.mem.rss":`Resident set (RSS)`,"dash.mem.jsHeap":`JS heap in use`,"dash.mem.jsHeapArena":`arena {total}`,"dash.mem.pressure":`Against warn threshold`,"dash.mem.pressureOf":`{pct}% of threshold`,"dash.mem.pressureUnknown":`No threshold reported`,"dash.mem.jscHeap":`JSC heap`,"dash.mem.external":`External`,"dash.mem.arrayBuffers":`ArrayBuffers`,"dash.mem.observed":`Observed`,"dash.mem.runtime":`Runtime counters`,"dash.mem.growth":`Observed drift / hour`,"dash.mem.perHour":`/h`,"dash.mem.store":`Continuation store`,"dash.mem.storeHint":`Proxy previous_response_id cache. Rising total bytes under a rising heap points at conversation retention rather than the runtime allocator.`,"dash.mem.storeEntries":`Entries`,"dash.mem.storeTotal":`Total`,"dash.mem.storeLargest":`Largest`,"dash.mem.storeOldest":`Oldest`,"dash.mem.threshold":`Warn threshold`,"dash.mem.lastWarn":`Last warning`,"dash.mem.never":`Never`,"dash.mem.details":`Details`,"dash.mem.unavailable":`Memory diagnostics unavailable (older proxy).`,"dash.mem.inFlight":`In-flight requests`,"dash.mem.restart":`Drain & restart`,"dash.mem.restartConfirm":`Wait for {count} in-flight request(s), then restart (up to {seconds}s; remaining requests are cut on timeout).`,"dash.mem.draining":`Draining {count} request(s)… restarting when complete`,"dash.mem.reconnecting":`Proxy restarting… waiting to reconnect`,"dash.mem.restartFailed":`Drain & restart failed. Check that the proxy is running.`,"dash.mem.restartNoSupervisor":`No restart protection detected. The proxy may stay down after restart unless you start it again.`,"dash.activeProviders":`Active providers`,"dash.noProviders":`No providers configured. Run {cmd}.`,"dash.col.name":`Name`,"dash.col.adapter":`Adapter`,"dash.col.baseUrl":`Base URL`,"dash.col.model":`Model`,"dash.modelsNoResults":`No models match your search.`,"dash.availableModels":`Available models`,"dash.noModels":`No models found. Check provider API keys.`,"dash.cannotConnect":`Cannot connect to proxy. Is it running?`,"dash.runStart":`Run {cmd} to start the proxy.`,"dash.stop":`Stop Proxy`,"dash.stopConfirm":`Stop the proxy and restore native Codex?`,"dash.stopFailed":`Failed to stop proxy (HTTP {status}).`,"dash.maSwitchFailed":`Mode switch failed (HTTP {status}).`,"dash.maNetworkError":`Network error — is the proxy running?`,"dash.stopping":`Stopping…`,"dash.actions":`Proxy`,"dash.codexRestart":`Reload Codex models`,"dash.codexRestarting":`Stopping…`,"dash.codexRestartConfirm":`Stop Codex app-servers so they reload the model list? Any Codex turn in progress is interrupted, and Codex does not relaunch on its own — reopen it afterwards.`,"dash.codexRestartDone":`Stopped {count} Codex app-server(s). Reopen Codex to load the current model list.`,"dash.codexRestartNothing":`No Codex app-server is running. The next launch reads the current model list.`,"dash.codexRestartUnknown":`Could not list processes, so nothing was stopped.`,"dash.codexRestartPartial":`{count} app-server(s) did not exit. Stop them manually if the model list stays stale.`,"dash.codexRestartFailed":`Failed to reload Codex models (HTTP {status}).`,"dash.codexRestartUnreachable":`Could not reach the proxy.`,"dash.codexRestartMalformed":`The proxy returned an unexpected response.`,"dash.codexRestartTimeout":`The proxy did not answer in time. It may still be stopping app-servers.`,"models.staleBanner":`Codex is showing an older model list than this catalog. Restart Codex to reload it.`,"dash.codexAutoStart":`Start opencodex with Codex`,"dash.codexAutoStartHint":`Allows an installed launcher shim to run ocx ensure. This setting does not install restart protection; check Startup safety for the effective state.`,"dash.searchModel":`Search sidecar model`,"dash.searchModelHint":`Model used for web_search on non-OpenAI routed models. Requires ChatGPT login.`,"dash.searchReasoning":`Search reasoning effort`,"dash.visionModel":`Vision sidecar model`,"dash.visionModelHint":`Model used to describe images for text-only routed models. Requires ChatGPT login.`,"dash.webSearchSidecar":`Web search sidecar`,"dash.webSearchSidecarHint":`Choose the backend and model used for web search on routed models.`,"dash.webSearchStream":`Stream answers live`,"dash.webSearchStreamHint":`Stream the model’s leading text and reasoning live until it decides on a tool call; the rest of the turn stays buffered for search interception. Text written before a search may partially repeat.`,"dash.visionSidecar":`Vision sidecar`,"dash.visionSidecarHint":`Choose the backend and model used to describe images for text-only routed models.`,"dash.visionOff":`Off`,"dash.visionAdvanced":`Advanced settings`,"dash.visionMaxDescriptions":`Maximum descriptions per turn`,"dash.visionMaxDescriptionsInvalid":`Enter a positive integer.`,"dash.visionTimeout":`Timeout`,"dash.visionTimeoutInvalid":`Enter an integer from {min} to {max} milliseconds.`,"dash.visionAdvancedPopover":`Advanced vision settings`,"dash.shadowCallIntercept":`Shadow Call Intercept`,"dash.shadowCallInterceptHint":`Intercepts Codex App's background helper calls ({models}) for title generation and commit messages and redirects them to your chosen model.`,"dash.shadowCallWarning":`⚠ When enabled, ALL requests for {models} will be replaced with the selected model.`,"dash.shadowCallOriginal":`Original`,"dash.shadowCallModel":`Replacement model`,"dash.shadowCallTooltip":`Codex App makes background helper calls for thread title generation, commit message generation, and skill orchestration. The helper model changed across client versions, so opencodex intercepts every model in this set: {models}. Enable this to redirect those calls to your chosen model.`,"models.shadowCallIntercept":`Shadow Call Intercept`,"models.shadowCallInterceptHint":`Intercepts Codex App's background helper calls ({models}) for titles and commit messages and redirects them to your chosen model.`,"dash.sidecarBackend":`Backend`,"dash.sidecarModel":`Model`,"dash.backendAuto":`Auto`,"dash.backendOpenAI":`OpenAI`,"dash.backendAnthropic":`Anthropic`,"dash.sidecarSaved":`Sidecar settings saved. Applied on the next request.`,"dash.sidecarSaveFailed":`Failed to save sidecar settings.`,"dash.injectionLabel":`Sub-agent delegation`,"dash.injectionHint":`Pick the model Codex should hand sub-agent work to. The two switches below decide where that pick is used.`,"dash.injectionManage":`Open settings`,"dash.syncCodexSubagentDefaults":`Also save as a Codex default`,"dash.syncCodexSubagentDefaultsHint":`On, the pick above is written into Codex's own config, so new tasks start with that model too. Off, it is remembered only here. It takes effect on the next sync or restart, and your hand-written [agents] settings are left alone.`,"dash.multiAgentGuidance":`Tell Codex how to split work`,"dash.multiAgentGuidanceHint":`Sends a short note telling Codex how to hand work to sub-agents. On v2 it names the models it may use and which to prefer; on v1 it only applies at max or ultra reasoning effort. Off, no note is added.`,"dash.injectionNone":`None`,"dash.injectionEffortLabel":`Reasoning effort`,"dash.injectionEffortNone":`Model default`,"dash.effortCapLabel":`V2 ultra effort limit`,"dash.subagentEffortCapLabel":`V2 sub-agent effort limit`,"dash.effortCapHelp":`Limits the reasoning effort for V2 ultra-mode turns. When set, incoming max-effort requests (from ultra mode) are capped to the selected level. The sub-agent limit applies only to spawned child agents. Caps only lower effort, never raise it. If a model doesn't support the capped level, it snaps down to the nearest supported level.`,"dash.effortCapNone":`No cap`,"dash.maintenance":`Maintenance`,"dash.maintenanceHint":`Refresh Codex's model catalog or install a newer opencodex release.`,"dash.syncModels":`Sync models`,"dash.syncModelsHint":`Rewrite Codex's model catalog from the providers you have connected.`,"dash.syncRun":`Sync now`,"dash.syncing":`Syncing…`,"dash.syncOk":`Sync complete. {count} model(s) appended.`,"dash.syncStaleHint":`If Codex still shows an older list, restart its long-lived app-server ({cmd}).`,"dash.syncFailed":`Sync failed: {error}`,"dash.projectConfigTitle":`Project Codex config bypasses OpenCodex`,"dash.projectConfigHint":`These repo-local settings override the OpenCodex proxy (e.g. route to OpenCode Go directly). Remove them so ~/.codex/config.toml routing applies in that project.`,"dash.checkUpdate":`Check update`,"dash.updateTitle":`Update opencodex`,"dash.updateDesc":`Check npm for the selected channel, then choose whether to restart the proxy after installation.`,"dash.updateChannel":`Channel`,"dash.updateChecking":`Checking for updates…`,"dash.updateInstalled":`Installed`,"dash.updateLatest":`Latest`,"dash.updateAvailable":`Update available`,"dash.updateCurrent":`Up to date`,"dash.updateCommand":`Command`,"dash.updateSource":`This is a source checkout. Update it from the terminal with the shown command.`,"dash.updateUnavailable":`Could not read the latest version from npm. Try again later.`,"dash.updateRetry":`Retry`,"dash.updateRecheck":`Re-check`,"dash.updateCannotAuto":`One-click update is unavailable ({reason}).`,"dash.updateReason.source_checkout":`source checkout`,"dash.updateReason.latest_unavailable":`npm registry unreachable`,"dash.updateReason.already_latest":`already on latest`,"dash.updateReason.unknown":`update unavailable`,"dash.updateRestart":`Restart after update`,"dash.updateRestartHint":`Recommended. The current GUI keeps running the old code until the proxy restarts.`,"dash.runUpdate":`Update`,"dash.updateReconnecting":`Waiting for the restarted proxy…`,"dash.updateStatus.running":`Updating opencodex.`,"dash.updateStatus.restarting":`Update installed. Restarting proxy.`,"dash.updateStatus.succeeded":`Update finished.`,"dash.updateVersionTransition":`{currentVersion} -> {latestVersion}.`,"dash.updateStatus.failed":`Update failed.`,"prov.subtitle":`Configure the upstream providers opencodex routes into Codex. Log in with an account, add a provider, or edit the raw config.`,"prov.add":`Add Provider`,"prov.editJson":`Edit JSON`,"prov.accountLogin":`Account login`,"prov.noOauth":`No OAuth providers available.`,"prov.loggedIn":`logged in`,"prov.notLoggedIn":`not logged in`,"prov.logout":`Logout`,"prov.login":`Login`,"prov.loginWith":`Login with {provider}`,"prov.waitingBrowser":`Waiting for browser…`,"prov.didntOpen":`Didn't open? Click here`,"prov.copyLink":`Copy link`,"prov.dontOpenBrowser":`Don't open a browser on the proxy machine`,"prov.dontOpenBrowserHint":`Useful for a different browser profile, or when the dashboard is not on the proxy's machine.`,"prov.linkCopied":`Copied`,"prov.linkCopyUnavailable":`Clipboard unavailable`,"prov.deviceCode":`Device code`,"prov.copyCode":`Copy code`,"prov.codeCopied":`Code copied`,"prov.editAlias":`Edit alias`,"prov.aliasPrompt":`Display name (leave empty to clear)`,"prov.aliasSaved":`Alias saved`,"prov.aliasSaveFailed":`Could not save alias`,"prov.accountId":`ID`,"prov.pasteRedirect":`Paste redirect URL or code`,"prov.pasteRedirectHint":`If the browser shows a localhost error, copy the full URL from its address bar and paste it here (or paste the authorization code).`,"prov.pasteSubmit":`Submit`,"prov.pasteSubmitting":`Submitting…`,"prov.pasteOk":`Code submitted — finishing login…`,"prov.pasteFail":`Could not submit code: {error}`,"prov.port":`Port`,"prov.default":`Default`,"prov.loadingConfig":`Loading…`,"prov.saved":`Saved! Restart proxy to apply.`,"prov.loadConfigFail":`Failed to load config`,"prov.invalidJson":`Invalid JSON`,"prov.saveFailed":`Save failed`,"prov.loginFailStart":`{provider} login failed to start`,"prov.loginError":`{provider} login error: {error}`,"prov.loginRequestFail":`{provider} login request failed`,"prov.loginCancelled":`{provider} login cancelled`,"prov.loginTimeout":`{provider} login timed out — browser closed or never finished. Try again.`,"prov.loginOk":`Logged in to {provider}. Run {cmd} (or it applies live) to list its models.`,"prov.loginSameAccount":`Still the same {provider} account — switch accounts in the browser, then try Add account again.`,"oauthTos.highTitle":`{provider}: subscription OAuth risk`,"oauthTos.elevatedTitle":`{provider}: unofficial OAuth bridge`,"oauthTos.anthropicBody":`Directly reusing Claude subscription OAuth tokens through a third-party proxy such as OpenCodex is not a supported Anthropic integration and may lead to access restrictions. Supported Agent SDK integrations that use Claude subscriptions are separate.`,"oauthTos.highBody":`OpenCodex connects {provider} through a third-party OAuth path. Unsupported use may lead to access limits or suspension.`,"oauthTos.elevatedBody":`OpenCodex connects {provider} through an unofficial OAuth path. Use the official client when possible; unusual or automated traffic may be treated as abuse and access may be limited or suspended.`,"oauthTos.saferPath":`Safer option: configure an API key in OpenCodex instead.`,"oauthTos.acknowledge":`I understand the risk and want to continue with OAuth anyway.`,"oauthTos.continue":`Continue with OAuth`,"prov.logoutOk":`Logged out of {provider}.`,"prov.logoutFail":`Could not log out of {provider}. Your account state is unchanged.`,"prov.removed":`Removed "{name}".`,"prov.removedDefault":`Removed "{name}". Default provider is now "{defaultProvider}".`,"prov.removeFail":`Failed to remove "{name}".`,"prov.removeLastProvider":`You can't remove this provider when no other enabled provider can become the default.`,"prov.removeHasDependentCombos":`Remove or update these dependent combos first: {combos}.`,"prov.setDefault":`Set as default`,"prov.setDefaultSuccess":`"{name}" is now the default provider.`,"prov.setDefaultFail":`Couldn't set "{name}" as the default provider.`,"prov.defaultDisabled":`Enable this provider before making it the default.`,"prov.updateFail":`Couldn't update this provider.`,"prov.networkError":`Network error. Check that the proxy is running and try again.`,"prov.added":`Added "{name}". Live now — run {cmd} (or restart) to list its models in Codex's picker.`,"prov.removeConfirm":`Remove provider "{name}"? Its models disappear from Codex's picker.`,"prov.hasApiKey":`api key configured`,"prov.hasHeaders":`custom headers configured`,"prov.accounts":`Accounts ({n})`,"prov.accountsAria":`Toggle {name} accounts`,"prov.accountActive":`Active`,"prov.accountReauth":`Re-login`,"prov.reauthenticate":`Re-authenticate`,"prov.reauthAccountMissing":`Selected account was not found after login`,"prov.reauthIdentityMismatch":`Signed-in account did not match the selected account`,"prov.accountAdd":`Add account`,"prov.accountNoLabel":`account {id}`,"prov.accountSwitchTitle":`Use this account`,"prov.accountSwitched":`Switched to {email}.`,"prov.accountSwitchFail":`Failed to switch account`,"prov.accountRemoved":`Removed {email}.`,"prov.accountRemoveFail":`Could not remove {email}. The account is unchanged.`,"prov.accountRemoveAria":`Remove {email}`,"prov.accountRemoveConfirm":`Remove account {email}? Its login is deleted from this proxy.`,"prov.keyAdd":`Add API key`,"prov.keyAdded":`Added API key to {name}.`,"prov.keyAddFail":`Failed to add API key`,"prov.keyPlaceholder":`Paste API key`,"prov.keySwitchTitle":`Use this key`,"prov.keySwitched":`Switched to key {key}.`,"prov.keySwitchFail":`Failed to switch key`,"prov.keyRemoved":`Removed key {key}.`,"prov.keyRemoveAria":`Remove key {key}`,"prov.keyRemoveConfirm":`Remove API key {key}? It is deleted from this proxy's config.`,"prov.activeBadge":`Active`,"prov.disabledBadge":`Disabled`,"prov.defaultBadge":`Default`,"prov.enable":`Enable`,"prov.disable":`Disable`,"prov.enabled":`Enabled "{name}". Its models can appear in Codex again.`,"prov.disabled":`Disabled "{name}". Settings are kept, but its models are hidden.`,"prov.enableFail":`Failed to enable "{name}".`,"prov.disableFail":`Failed to disable "{name}".`,"prov.enableAria":`Enable provider {name}`,"prov.disableAria":`Disable provider {name}`,"prov.defaultCannotDisable":`Default provider can't be disabled`,"prov.openaiAccountMode":`Codex account mode`,"prov.openaiModePool":`Pool`,"prov.openaiModeDirect":`Direct`,"prov.openaiPoolDesc":`Default. Rotate the main login and added accounts using affinity, quota, cooldown, and failover.`,"prov.openaiDirectDesc":`Use only the current/main Codex login. Stored pool accounts are not read or rotated.`,"prov.openaiModeSaved":`OpenAI account mode changed to {mode}.`,"prov.openaiModeSaveFailed":`Could not change the OpenAI account mode.`,"prov.openaiApiDesc":`Uses an OpenAI API key and never uses Codex account credentials.`,"prov.manageCodexAccounts":`Manage Codex accounts`,"prov.openaiApiMissing":`API key required`,"prov.openaiApiSetup":`Set up API key`,"models.tab.catalog":`Models`,"models.tab.combos":`Combos`,"models.tab.compatibility":`Compatibility`,"models.tab.routing":`Routing (beta)`,"models.tabsLabel":`Model surfaces`,"models.subtitle.combos":`Ordered groups of models that answer as one id. Chain targets with failover or spread the load with a balancing strategy.`,"models.subtitle.compatibility":`Read-only compatibility verdict matrix from lab projection evidence.`,"models.subtitle.routing":`Policy profiles, dry-run evaluation, and source-backed routing analytics.`,"models.subtitle":`Toggle which models Codex sees — native GPT passthrough and routed providers, grouped by provider (click a header to collapse). Hidden models stay off the catalog + model picker but remain directly callable by exact id. Changes apply on the next Codex turn — opencodex invalidates Codex's 5-min model cache so no restart is needed.`,"models.nativeGroupLabel":`OpenAI native`,"models.nativeHint":"Passthrough models use the Pool or Direct account option selected on Providers. Toggling one off hides it from the Codex picker (the catalog entry is kept, so re-enabling restores it exactly). Adding a model here registers a routed `openai/` selector, not a new bare passthrough id.","models.active":`{active}/{total} visible`,"models.workspace.providers":`Providers`,"models.workspace.allProviders":`All providers`,"models.workspace.mainAria":`Model details`,"models.allOn":`All on`,"models.allOff":`All off`,"models.presetLabel":`Models`,"models.presetMode_preset":`Preset`,"models.presetMode_all":`All`,"models.presetMode_custom":`Custom`,"models.presetSummary":`{count} of {total} shown — core preset v{version}`,"models.presetUpdateAvailable":`Preset v{version} available`,"models.presetAppliedToast":`{provider}: preset applied — {count} models selected`,"models.presetClearedToast":`{provider}: showing all models`,"models.presetEmpty":`{provider}: preset matched no models — selection unchanged`,"models.presetConfirmReplace":`Replace your selection with the {count}-model preset?`,"models.cap350k":`Cap 350k`,"models.capApplied":`Context cap applied — takes effect on the next Codex turn.`,"models.capSaveFailed":`Failed to save context cap`,"models.contextCapped":`350k cap`,"models.contextCapLabel":`Default window / cap`,"models.v2Label":`Sub-agent`,"models.shadowCallOriginal":`⚠ {models} →`,"models.v2DocsLink":`What is v1 / v2?`,"models.v2Mode_v1":`v1`,"models.v2Mode_default":`base`,"models.v2Mode_v2":`v2`,"models.v2ModeDesc_v1":`All models → v1 surface`,"models.v2ModeDesc_default":`Upstream defaults (sol/terra=v2, luna=v1)`,"models.v2ModeDesc_v2":`All models → v2 surface`,"models.keepNativeOnV1":`Keep ChatGPT on v1`,"models.keepNativeOnV1Hint":`ChatGPT encrypts v2 child tasks only when a ChatGPT-native parent stays on v2, so Grok and Claude cannot read them. Turn this on to keep Sol/Terra on v1 and avoid that encryption. Routed parents keep v2.`,"models.v2Help":`Controls the multi-agent surface for all models. - -v1: Classic single-thread agent. Every model uses the v1 collab surface. -base: Upstream defaults — sol/terra use v2, luna uses v1, others follow the codex feature flag. -v2: Multi-thread agent with spawn_agent. Every model uses the v2 collab surface. - -On v2, Keep ChatGPT on v1 leaves Sol/Terra on the v1 surface so they can still spawn Grok or Claude. ChatGPT encrypts v2 child tasks; routed models cannot read them. Routed parents stay on v2. - -Changes apply to new sessions.`,"dash.multiAgent":`Sub-agent`,"models.v2Conflict":`[agents] max_threads is set — codex will refuse to start; remove it from config.toml`,"models.v2Applied":`Sub-agent mode updated — applies to new sessions (restart the Codex app to refresh the picker)`,"models.v2ThreadsLabel":`Max threads`,"models.v2ThreadsDefault":`default (4)`,"models.v2ThreadsApplied":`Thread limit updated — applies to new sessions`,"models.v2ThreadsInvalid":`Thread limit must be an integer >= 1`,"models.v2ThreadsApply":`Apply`,"models.capValue":`Default {value}`,"models.contextSettings":`Custom windows`,"models.contextSettingsTitle":`Custom windows — {provider}`,"models.contextDefault":`Provider default`,"models.contextModel":`Model`,"models.contextModelOverride":`Model override`,"models.contextHint":`Write the actual Codex window here when you already know it. This fills a missing upstream window and only lowers a larger reported one. Leave blank to use the provider Default window / cap, or 128k if that cap is off.`,"models.contextAutomatic":`Automatic discovery`,"models.contextSaved":`Context windows updated — takes effect on the next Codex turn.`,"models.contextUnchanged":`No context window changes to save.`,"models.contextSaveFailed":`Failed to save context windows`,"models.contextInvalid":`Context windows must be positive whole numbers`,"models.contextCappedValue":`{value} cap`,"models.setAll":`Set all`,"models.setAllHint":`Turn on the {value} default window for every routed provider. Relays that omit context_window / context_length get this as the actual Codex window. Use Custom windows on a provider row to set one model by hand. Native providers are unaffected.`,"models.collapseAll":`Collapse all`,"models.expandAll":`Expand all`,"models.orderHint":`Picker order: Subagents picks (in the selected order) → remaining routed models alphabetically by provider, then model ID → native models. Visibility switches only filter models; they do not change this order.`,"models.custom":`Custom…`,"models.customApply":`Apply`,"models.customPlaceholder":`Tokens (e.g. 420000)`,"models.customAdd":`Add custom model`,"models.customAddTitle":`Add custom model — {provider}`,"models.customEditTitle":`Edit custom model — {provider}`,"models.customAdded":`Custom model added`,"models.customUpdated":`Custom model updated`,"models.customDeleted":`Custom model deleted`,"models.customSaveFailed":`Failed to save custom model`,"models.customSaving":`Saving…`,"models.customAddBtn":`Add`,"models.customEditBtn":`Update`,"models.customEdit":`Edit`,"models.customDelete":`Delete`,"models.customDeleteConfirm":`Delete the {name} model?`,"models.customBadge":`Custom`,"models.customSummary":`{count} custom`,"models.customFieldModelId":`Model ID (endpoint slug)`,"models.customFieldModelIdPlaceholder":`e.g. qwen4-max-preview`,"models.customFieldDisplayName":`Display name (optional)`,"models.customFieldDisplayNamePlaceholder":`e.g. Qwen 4 Max Preview`,"models.customFieldContext":`Context window`,"models.customFieldModalities":`Input modalities`,"models.customFieldReasoning":`Reasoning effort`,"models.customFieldReasoningOverride":`Override reasoning effort`,"models.reasoningEffort.none":`None`,"models.reasoningEffort.minimal":`Minimal`,"models.reasoningEffort.low":`Low`,"models.reasoningEffort.medium":`Medium`,"models.reasoningEffort.high":`High`,"models.reasoningEffort.xhigh":`Extra high`,"models.reasoningEffort.max":`Maximum`,"models.tipProvider":`Provider`,"models.tipContext":`Context`,"models.tipModalities":`Modalities`,"models.tipStatus":`Status`,"models.tipActive":`Active`,"models.tipDisabled":`Disabled`,"models.applied":`Applied — takes effect on the next Codex turn.`,"models.saveFailed":`Save failed`,"models.networkError":`Network error — is the proxy running?`,"models.loadFail":`Failed to load models — is the proxy running?`,"models.noRouted":`No routed models`,"models.noRoutedHint":`Log into a provider or add one first.`,"models.emptyDiscovery":`No models were discovered. Check the provider endpoint or add a static/custom model.`,"models.emptyDiscoveryDisabled":`Live model discovery is off and no static models are configured.`,"models.discoveryFailedBadge":`Discovery failed`,"models.discoveryFailedHttp":`Model discovery failed (HTTP {status}).`,"models.discoveryFailedBlocked":`Model discovery was blocked by the destination policy.`,"models.discoveryFailedInvalidResponse":`Model discovery returned an invalid response.`,"models.discoveryFailedNetwork":`Model discovery failed due to a network error.`,"models.discoveryFailedProvider":`The provider reported a model discovery error.`,"models.discoveryFailedGeneric":`Model discovery failed.`,"models.openProviderSettings":`Open provider settings`,"models.loading":`Loading…`,"models.search":`Search models…`,"models.showMore":`Show {n} more`,"models.allowlistLabel":`Only selected`,"models.allowlistHint":`Only checked models ship to the catalog (empty = all). Useful for providers exposing thousands of models.`,"models.selectedCount":`{n} selected`,"sub.subtitle":`Codex's {cmd} advertises only the first 5 models (by priority) as overrides. Pick up to 5 here — native gpt or routed — and opencodex sets their catalog priority so exactly these lead. Any other model is still callable by its exact name; this only controls what's shown.`,"sub.featured":`Featured`,"sub.advanced":`Advanced`,"sub.orderHintAria":`How this order is used`,"sub.orderHint":`The order shown here sets positions 1–5 at the top of the Codex model picker and the default model candidates for {cmd}.`,"sub.noneSelected":`None selected — pick from the list below.`,"sub.models":`Models`,"sub.search":`Search models (native gpt + routed)…`,"sub.settings":`Settings`,"sub.sections":`Subagent sections`,"sub.delegation.model":`Model to call first`,"sub.delegation.modelHint":`The model Codex reaches for first when it hands off work. Featured above is the list it may call; this is the one it calls first.`,"sub.noModels":`No models — log into a provider or add one first.`,"sub.saved":`Saved {n} models. Start a new Codex session (or run {cmd}) to see them as spawn_agent overrides.`,"sub.saveFailed":`Save failed`,"sub.networkError":`Network error — is the proxy running?`,"sub.loadFail":`Failed to load models — is the proxy running?`,"sub.loading":`Loading…`,"sub.moveUp":`Move {m} up`,"sub.moveDown":`Move {m} down`,"sub.removeAria":`Remove {m}`,"sub.workspace.addToFeatured":`Add {m} to featured`,"sub.workspace.allModels":`All models`,"sub.workspace.featuredFull":`Featured list is full (max 5)`,"sub.workspace.mainAria":`Subagent model details`,"sub.workspace.notFeatured":`Not featured`,"sub.workspace.priority":`Priority`,"sub.ultraMode":`Ultra mode`,"sub.ultraModeHint":`Enable the Proactive multi-agent delegation policy for every model and reasoning effort (does not change reasoning effort itself). Writes features.multi_agent_v2.multi_agent_mode_hint_text in config.toml.`,"sub.ultraModeV2Required":`Requires the v2 multi-agent surface — enable multi_agent_v2 and select v2 in the Sub-agent mode control first.`,"sub.ultraModeText":`Ultra mode delegation text`,"sub.ultraModePreset":`Restore preset`,"sub.ultraModeLoadFail":`Failed to load Ultra mode settings — is the proxy running?`,"sub.ultraModeSaveFail":`Failed to save Ultra mode settings`,"sub.ultraModeSaved":`Ultra mode saved. Applies to new Codex sessions.`,"sub.workspace.removeFromFeatured":`Remove {m} from featured`,"sub.workspace.selectModel":`Select a model`,"sub.workspace.selectModelDesc":`Pick a model from the list to see details and feature it for spawn_agent.`,"sub.workspace.selector":`Public selector`,"logs.title":`Request Logs`,"logs.tabLogs":`Logs`,"logs.tabDebug":`Debug`,"logs.subtitle":`Recent requests routed through the local opencodex proxy, newest first.`,"logs.autoRefresh":`Auto-refresh`,"logs.noRequests":`No requests yet.`,"logs.loadError":`Could not load request logs.`,"logs.filter.surface.label":`Surface`,"logs.filter.surface.all":`All`,"logs.filter.surface.claude":`Claude`,"logs.filter.surface.codex":`Codex`,"logs.filter.surface.grok":`Grok`,"logs.filter.interceptedHelpersOnly":`Intercepted helpers only`,"logs.badge.interceptedHelper":`I · {model}`,"logs.badge.interceptedHelperTitle":`Intercepted helper request`,"logs.filter.conversation.label":`Conversation`,"logs.filter.conversation.placeholder":`Paste conversation id`,"logs.filter.conversation.clear":`Clear`,"logs.filter.model.label":`Model`,"logs.filter.model.placeholder":`Filter by model or provider`,"logs.filter.conversation.apply":`Filter logs`,"logs.conversation.totals":`{requests} requests · {tokens} tokens · {cost}`,"logs.conversation.scope":`Totals cover the currently loaded Logs ring only.`,"logs.conversation.excluded":`({unpriced} unpriced, {unmetered} unmetered excluded from ~$)`,"logs.cost.approximate":`{amount}`,"logs.cost.lowerBound":`≥{amount}`,"logs.cost.unavailable":`—`,"logs.detail.conversation":`Conversation`,"logs.badge.claude":`Claude`,"logs.badge.grok":`Grok`,"logs.col.time":`Time`,"logs.col.request":`Request`,"logs.col.model":`Model`,"logs.col.effort":`Effort`,"logs.col.provider":`Provider`,"logs.col.status":`Status`,"logs.col.tokens":`Tokens`,"logs.col.tokPerSec":`tok/s`,"logs.col.estimatedCost":`~$`,"logs.metric.tokPerSecTitle":`Output tokens per second over the full request duration`,"logs.metric.estimatedCostTitle":`API list-price equivalent, not an actual charge; unmatched pricing is unavailable`,"usage.cost.total":`API list-price equivalent (this range)`,"usage.cost.disclaimer":`Not a billing receipt. Subscription usage or provider credits may apply instead.`,"usage.cost.unpricedNote":`{count} requests excluded (no price or usage)`,"logs.detail.section.basic":`Basic information`,"logs.detail.route.section":`Route decision`,"logs.detail.route.kind":`Route kind`,"logs.detail.route.profile":`Profile`,"logs.detail.route.selected":`Selected`,"logs.detail.route.candidates":`Candidates`,"logs.detail.route.unknown":`No route trace recorded for this request (pre-trace row).`,"logs.detail.section.performance":`Performance`,"logs.detail.section.cost":`API list-price equivalent`,"logs.detail.section.attempts":`Combo attempts`,"logs.detail.section.usage":`Raw usage`,"logs.detail.ttft":`TTFT`,"logs.detail.costTotal":`List-price equivalent`,"logs.detail.totalTokens":`Total tokens`,"logs.detail.matchedKey":`Matched price key`,"logs.detail.priceSource":`Price source`,"logs.detail.unavailableReason":`Unavailable reason`,"logs.detail.copyRequestId":`Copy request ID`,"logs.detail.copied":`Copied`,"logs.detail.source.jawcode":`jawcode catalog`,"logs.detail.source.expected":`Expected price overlay`,"logs.detail.source.user":`Provider-configured price overlay`,"logs.detail.verification.verified":`Verified`,"logs.detail.verification.derived":`Derived from base model`,"logs.detail.attempt.target":`Provider / model`,"logs.detail.attempt.reason":`Result / reason`,"logs.detail.attempt.completed":`Completed`,"logs.detail.attempt.e2eNote":`Top-level tok/s is end-to-end; each attempt uses its own duration.`,"logs.detail.attempt.recovery.transient5xx":`Transient 5xx`,"logs.detail.attempt.recovery.connectionReset":`Connection reset`,"logs.detail.attempt.recovery.oauth401":`OAuth re-authentication`,"logs.detail.attempt.recovery.key429":`Key rate-limited (429)`,"logs.detail.attempt.recovery.rateLimit429":`Rate-limited (429)`,"logs.detail.attempt.recovery.anthropicOauth429":`Anthropic OAuth rate-limited (429)`,"logs.detail.attempt.recovery.image413":`Image payload too large (413)`,"logs.detail.attempt.recovery.emptyCompletion":`Empty completion retry`,"logs.detail.attempt.recovery.unknown":`Unknown recovery reason`,"logs.detail.reason.usage_missing":`Usage was not reported.`,"logs.detail.reason.usage_unsupported":`This provider does not report usage.`,"logs.detail.reason.output_missing":`No positive output token count was reported.`,"logs.detail.reason.invalid_duration":`The request duration is not valid.`,"logs.detail.reason.price_unmatched":`No matching price was found.`,"logs.detail.reason.invalid_cache_breakdown":`Cache token details conflict with total input tokens.`,"logs.detail.reason.invalid_usage":`Usage contains an invalid token value.`,"logs.detail.reason.combo_attempt_unavailable":`At least one combo attempt could not be priced.`,"logs.detail.estimate.usage_estimated":`Provider usage is estimated.`,"logs.detail.estimate.cache_detail_missing":`Cache details were unavailable; input is an upper-bound estimate.`,"logs.detail.estimate.expected_price_overlay":`A verified expected list price was used.`,"logs.detail.estimate.provider_cost_overlay":`A provider-configured price overlay was used.`,"logs.detail.estimate.priority_lower_bound":`The confirmed Priority price is unavailable; the displayed estimate is a known lower bound.`,"logs.col.error":`Error`,"logs.col.upstreamReason":`Upstream reason`,"logs.col.duration":`Duration`,"logs.modelTooltip.model":`model`,"logs.modelTooltip.resolvedModel":`resolved model`,"logs.modelTooltip.requestedTier":`requested tier`,"logs.modelTooltip.configuredTier":`configured tier`,"logs.modelTooltip.responseTier":`response tier`,"logs.modelTooltip.supportsTier":`tier support`,"logs.tokens.reported":`reported`,"logs.tokens.unreported":`unreported`,"logs.tokens.unsupported":`unsupported`,"logs.tokens.estimated":`estimated`,"logs.tokens.input":`input`,"logs.tokens.output":`output`,"logs.tokens.cacheRead":`cache read (c)`,"logs.tokens.cacheWrite":`cache write (w)`,"logs.tokens.reasoning":`reasoning`,"logs.tokens.noCache":`no cache data`,"logs.tokens.contextTotal":`active context`,"logs.tokens.noCacheNote":`this provider does not report cache tokens`,"logs.tokens.noCacheCursor":`Cursor cache detail unreported`,"logs.tokens.noCacheCursorNote":`Cursor does not expose cache read/write token counts; this is unknown, not a confirmed cache miss`,"logs.tokens.estimatedNote":`estimated (provider reports no exact usage)`,"logs.details":`Details`,"logs.detailTitle":`Request details`,"logs.detailRaw":`Raw log entry`,"debug.title":`Debug`,"debug.subtitle":`Opt-in provider transport and usage-extraction diagnostics. Request errors and 502s stay on the Logs tab.`,"debug.debug":`Provider debug`,"debug.usage":`Usage extraction`,"debug.injection":`Injection log`,"debug.claude":`Claude inbound`,"debug.claudeInbound.title":`Claude inbound requests`,"debug.claudeInbound.sub":`What Claude Code/Desktop actually sends (thinking, effort, metadata) — no prompt text is stored.`,"debug.claudeInbound.empty":`No requests captured yet. Send a message from Claude while this is on.`,"debug.claudeInbound.time":`Time`,"debug.claudeInbound.endpoint":`Endpoint`,"debug.claudeInbound.model":`Model`,"debug.claudeInbound.none":`none`,"debug.reset":`Clear runtime overrides`,"debug.refresh":`Refresh`,"debug.follow":`Follow`,"debug.streamProvider":`Provider`,"debug.streamUsage":`Usage`,"debug.streamInjection":`Injection`,"debug.loading":`Loading debug settings…`,"debug.loadFailed":`Could not load debug settings.`,"debug.emptyTitle":`Debug logging is off`,"debug.empty":`Turn on Provider debug or Usage extraction in the card above. Lines appear here after you send a request through the proxy.`,"debug.noLinesTitle":`Waiting for lines`,"debug.noLines.provider":`Provider debug is on, but it only records transport anomalies (dropped or malformed frames, and Cursor dial/retry events). A clean request through a provider like Anthropic can produce no lines.`,"debug.noLines.usage":`Usage extraction is on but nothing has been captured yet. Send a chat/request through Codex and it appears here.`,"debug.noLines.injection":`Injection log is on but nothing has been captured yet. It records multi-agent guidance injection and effort-cap decisions on collab and sub-agent turns.`,"usage.title":`Usage`,"usage.subtitle":`Local token accounting from your proxy. Missing usage is never shown as zero.`,"usage.loading":`Loading usage data…`,"usage.empty":`No usage recorded yet. Send a request through the proxy to see activity here.`,"usage.loadError":`Could not load usage data.`,"usage.range.all":`All`,"usage.range.available":`Available history`,"usage.historyTruncated":`Totals cover available history only because older usage was not loaded.`,"usage.historyTruncatedWindow":`Loaded rows have request start times ranging from {start} to {end}. Earlier file entries were omitted by the read limit, so any selected range may be incomplete.`,"usage.range.30d":`30d`,"usage.range.7d":`7d`,"usage.card.requests":`Requests`,"usage.card.measured":`Measured`,"usage.card.reported":`Reported`,"usage.card.totalTokens":`Total tokens`,"usage.card.cachedTokens":`Cache reads`,"usage.card.cachedTokensHint":`Prompt tokens served from the provider cache (reads). Cache writes are shown below when present.`,"usage.card.cacheWriteTokens":`cache writes`,"usage.card.coverage":`Coverage`,"usage.card.activeDays":`Active days`,"usage.section.heatmap":`Daily activity`,"usage.section.overview":`Overview`,"usage.section.models":`Models`,"usage.section.providers":`Providers`,"usage.section.coverage":`Coverage breakdown`,"usage.workspace.report":`Usage report`,"usage.workspace.sections":`Usage sections`,"usage.coverage.measured":`Measured`,"usage.coverage.reported":`Provider reported`,"usage.coverage.estimated":`Estimated`,"usage.coverage.note":`Measured entries include provider-reported and estimated token counts. Unreported and unsupported requests are tracked but never inflated to zero tokens.`,"usage.search.models":`Search models…`,"usage.col.requests":`Requests`,"usage.col.measured":`Measured`,"usage.col.reported":`Reported`,"usage.col.tokens":`Tokens`,"usage.col.share":`Share`,"usage.heatmap.less":`Less`,"usage.heatmap.more":`More`,"usage.dayMon":`Mon`,"usage.dayWed":`Wed`,"usage.dayFri":`Fri`,"usage.heatmap.tooltipTokens":`{tokens} tokens`,"usage.heatmap.tooltipRequests":`{requests} requests`,"nav.storage":`Storage`,"storage.title":`Storage`,"storage.subtitle":`See what’s using CODEX_HOME. Cleanup never touches active sessions.`,"storage.loading":`Scanning storage…`,"storage.empty":`CODEX_HOME is empty or missing — nothing to report.`,"storage.error":`Storage scan failed. Check that CODEX_HOME points at a valid directory.`,"storage.refresh":`Rescan`,"storage.rescanned":`Scan complete.`,"storage.card.total":`Total size`,"storage.card.files":`Files`,"storage.card.home":`CODEX_HOME`,"storage.snapshot.lastScan":`Last scan`,"storage.snapshot.scanning":`Scanning…`,"storage.snapshot.unavailable":`No scan yet.`,"storage.cleanupCard.title":`Free up space`,"storage.cleanupCard.tabs":`Cleanup options`,"storage.cleanupCard.tab.policy":`Policy`,"storage.cleanupCard.tab.quarantine":`Quarantine`,"storage.cleanup.noArchives":`No archived sessions to clean up.`,"storage.section.buckets":`Buckets`,"storage.section.largest":`Largest files`,"storage.workspace.overview":`Overview`,"storage.workspace.selectBucket":`Select a bucket from the list to see its breakdown.`,"storage.col.bucket":`Bucket`,"storage.col.size":`Size`,"storage.col.files":`Files`,"storage.col.oldest":`Oldest`,"storage.col.newest":`Newest`,"storage.col.rows":`DB rows`,"storage.rows.unknown":`unknown (locked)`,"storage.bucket.sessions":`Active sessions`,"storage.bucket.archived_sessions":`Archived sessions`,"storage.bucket.logs_db":`Logs database`,"storage.bucket.state_db":`State database`,"storage.bucket.attachments":`Attachments`,"storage.bucket.deletion_manifests":`Deletion manifests`,"storage.bucket.other":`Other`,"storage.cleanup.title":`Archived cleanup`,"storage.cleanup.help":`Remove the oldest archived sessions by percentage. Active sessions are never touched. Quarantine is the default — files move to CODEX_HOME/.trash.`,"storage.cleanup.slider":`Oldest archived percent`,"storage.cleanup.percent":`{percent}%`,"storage.cleanup.preset":`{percent}`,"storage.cleanup.preview":`Preview`,"storage.cleanup.confirmTitle":`Confirm archived cleanup`,"storage.cleanup.confirmBody":`This will process {count} archived file(s) (~{size}), the oldest {percent}%.`,"storage.cleanup.moreFiles":`…and {n} more`,"storage.cleanup.permanent":`Delete permanently (skip quarantine)`,"storage.cleanup.permanentWarn":`Permanent delete cannot be undone.`,"storage.cleanup.quarantineNote":`Files move to .trash under CODEX_HOME. You can restore them from the Quarantine tab.`,"storage.cleanup.cancel":`Cancel`,"storage.cleanup.confirmQuarantine":`Quarantine`,"storage.cleanup.confirmPermanent":`Delete permanently`,"storage.cleanup.doneQuarantine":`Quarantined {count} file(s) ({size}).`,"storage.cleanup.donePermanent":`Permanently deleted {count} file(s) ({size}).`,"storage.cleanup.previewFailed":`Preview failed.`,"storage.cleanup.cleanupFailed":`Cleanup failed.`,"storage.cleanup.err.codex_busy":`Codex is using state.sqlite — try again after quitting Codex.`,"storage.cleanup.err.stale_preview":`Archived files changed since preview — run Preview again.`,"storage.cleanup.err.restore_pending_overlap":`Selected archives overlap an incomplete trash restore — finish or retry restore first.`,"storage.cleanup.err.referenced_history":`Selected archives are still referenced by forked or paginated history.`,"storage.cleanup.err.invalid_digest":`Preview digest is missing or invalid.`,"storage.cleanup.err.invalid_mode":`Cleanup mode must be quarantine or permanent.`,"storage.cleanup.err.fs_failed":`Filesystem cleanup failed. Some changes may already be applied — check CODEX_HOME/.trash and any recovery path shown.`,"storage.cleanup.err.fs_failed_trash":`Filesystem cleanup failed. Some changes may already be applied — check {trashDir} and manifest.json for recoverable files.`,"storage.cleanup.err.db_reconcile_failed":`Could not update Codex state database.`,"storage.cleanup.err.cleanup_failed":`Cleanup failed.`,"storage.trash.title":`Quarantine`,"storage.trash.help":`Archived sessions moved to CODEX_HOME/.trash. Restore puts JSONL files and thread rows back.`,"storage.trash.empty":`No quarantined entries.`,"storage.trash.loading":`Loading quarantine…`,"storage.trash.col.when":`Quarantined`,"storage.trash.col.files":`Files`,"storage.trash.col.size":`Size`,"storage.trash.col.mode":`Mode`,"storage.trash.col.id":`Entry`,"storage.trash.restore":`Restore`,"storage.trash.confirmTitle":`Restore quarantine entry?`,"storage.trash.confirmBody":`Restore {count} file(s) (~{size}) from {id} back to archived sessions.`,"storage.trash.cancel":`Cancel`,"storage.trash.confirmRestore":`Restore`,"storage.trash.done":`Restored {count} file(s) ({size}).`,"storage.trash.restoreFailed":`Restore failed.`,"storage.trash.listFailed":`Could not list quarantine entries.`,"storage.trash.mode.quarantine":`quarantine`,"storage.trash.mode.permanent":`permanent (incomplete)`,"storage.trash.err.codex_busy":`Codex is using state.sqlite — try again after quitting Codex.`,"storage.trash.err.invalid_trash":`Trash entry id is missing or invalid.`,"storage.trash.err.missing_trash":`Trash entry was not found.`,"storage.trash.err.dest_exists":`Restore destination already exists — remove or rename the archived file and retry.`,"storage.trash.err.fs_failed":`Filesystem restore failed. Some files may already be restored — check archived_sessions and .trash.`,"storage.trash.err.db_reconcile_failed":`Could not restore Codex state database rows.`,"storage.trash.err.storage_mutation_busy":`Another storage cleanup or restore is in progress — try again shortly.`,"storage.trash.err.restore_failed":`Restore failed.`,"storage.trash.err.restore_worker_timeout":`Restore took too long (over 10 minutes) and was stopped.`,"storage.trash.err.restore_worker_aborted":`Restore was cancelled during shutdown.`,"storage.trash.err.restore_worker_failed":`Restore worker crashed or failed unexpectedly.`,"storage.policy.title":`Auto-cleanup policy`,"storage.policy.help":`Optional batch cleanup when archived sessions exceed a threshold. Off by default — never enabled automatically.`,"storage.policy.loading":`Loading policy…`,"storage.policy.loadFailed":`Could not load cleanup policy.`,"storage.policy.saveFailed":`Could not save cleanup policy.`,"storage.policy.runFailed":`Policy run failed.`,"storage.policy.alreadyRunning":`A cleanup policy run is already in progress.`,"storage.policy.invalid":`Invalid policy values.`,"storage.policy.enabled":`Enable auto-cleanup`,"storage.policy.enabledHint":`Default is off. Enabling runs only on the schedule you choose (or Run now).`,"storage.policy.threshold":`When archived size exceeds (GiB)`,"storage.policy.trigger":`Trigger`,"storage.policy.target":`Cleanup target`,"storage.policy.targetPercent":`Remove oldest archived (%)`,"storage.policy.targetReduce":`Reduce archived size to (GiB)`,"storage.policy.thresholdInc":`Increase threshold`,"storage.policy.thresholdDec":`Decrease threshold`,"storage.policy.percentInc":`Increase percent`,"storage.policy.percentDec":`Decrease percent`,"storage.policy.reduceInc":`Increase reduce-to size`,"storage.policy.reduceDec":`Decrease reduce-to size`,"storage.policy.schedule":`Schedule`,"storage.policy.schedule.manual":`Manual only`,"storage.policy.schedule.startup":`On proxy startup`,"storage.policy.schedule.daily":`Daily`,"storage.policy.schedule.weekly":`Weekly`,"storage.policy.mode":`Deletion mode`,"storage.policy.mode.quarantine":`Quarantine (default)`,"storage.policy.mode.permanent":`Permanent delete`,"storage.policy.permanentWarn":`Permanent mode cannot be undone. Prefer quarantine unless you are sure.`,"storage.policy.lastRun":`Last run`,"storage.policy.lastRunDetail":`Removed {count} · freed {size}`,"storage.policy.nextRun":`Next run`,"storage.policy.never":`Never`,"storage.policy.save":`Save`,"storage.policy.runNow":`Run now`,"storage.policy.running":`Running…`,"storage.policy.saved":`Policy saved.`,"storage.policy.skippedDisabled":`Policy is disabled — enable it first.`,"storage.policy.skippedUnder":`Archived size is under the threshold — nothing to do.`,"storage.policy.skippedEmpty":`No archived candidates matched the target.`,"storage.policy.doneQuarantine":`Policy quarantined {count} file(s) ({size}).`,"storage.policy.donePermanent":`Policy permanently deleted {count} file(s) ({size}).`,"storage.policy.metadataSaveWarning":`The policy run finished, but its scheduling metadata could not be saved.`,"modal.addNamed":`Add: {label}`,"modal.add":`Add provider`,"modal.search":`Search providers…`,"modal.logInWith":`Log in with {label}`,"modal.waitingBrowser":`Waiting for browser…`,"modal.providerName":`Provider name`,"modal.adapter":`Adapter`,"modal.baseUrl":`Base URL`,"modal.endpoint":`Endpoint`,"modal.endpoint.tokenPlan":`Token plan`,"modal.endpoint.payAsYouGo":`Pay as you go`,"modal.endpoint.custom":`Custom`,"modal.defaultModel":`Default model (optional)`,"modal.allowPrivateNetwork":`Allow local/private network`,"modal.allowPrivateNetworkHint":`Enable only for intentionally self-hosted providers. Metadata endpoints remain blocked.`,"modal.nameRequired":`Provider name is required`,"modal.baseUrlRequired":`Base URL is required`,"modal.networkError":`Network error — is the proxy running?`,"modal.loginFailStart":`Login failed to start`,"modal.waitingLogin":`Waiting for browser login…`,"modal.loggingIn":`Logging in…`,"modal.loginTimeout":`Login timed out — try again.`,"modal.back":`Back`,"modal.badge.oauth":`OAuth`,"modal.customProvider":`Custom provider`,"modal.failedStatus":`Failed ({status})`,"modal.loginError":`Login error: {error}`,"modal.badge.codexLogin":`Codex login`,"modal.badge.local":`Local`,"modal.badge.apiKey":`API key`,"modal.badge.direct":`Direct`,"modal.badge.pool":`Pool`,"modal.badge.free":`Free`,"modal.invalidPreset":`This built-in provider preset is incomplete. Restart the proxy and try again.`,"modal.freeTierTitle":`Free tier`,"modal.freeTierDefault":`No API key required. Works out of the box.`,"modal.tab.accounts":`Accounts`,"modal.tab.free":`Free`,"modal.tab.paid":`Paid`,"modal.accountsHint":`Sign in to ChatGPT/Codex, OAuth providers, and API-key accounts here. OpenAI is built in — log in rather than adding it again.`,"modal.accountsCodexAuthLink":`Codex Auth`,"modal.notListed":`Provider not listed? Add a custom one`,"modal.catalogLoading":`Loading catalog…`,"modal.accountLogin":`Log in`,"modal.accountLogout":`Log out`,"modal.accountAdd":`Add account`,"modal.accountManage":`Manage`,"modal.accountCodexPool":`ChatGPT account pool`,"modal.accountLoggedIn":`Logged in`,"modal.accountLoggedOut":`Not logged in`,"quota.fiveHourLimit":`5-hour limit`,"quota.ageMinutes":`{n}m`,"quota.ageHours":`{n}h`,"quota.ageDays":`{n}d`,"quota.observedAgo":`Observed {age} ago`,"quota.observedHint":`Meta reports usage only during a streaming response, so this is the last value seen, not a live reading.`,"quota.weeklyLimit":`Weekly limit`,"quota.monthlyLimit":`30-day limit`,"quota.cursorFirstParty":`First-party models`,"quota.cursorApiUsage":`API usage`,"quota.totalSubscriptionCredits":`Total subscription credits`,"quota.creditsBalance":`Credits balance`,"quota.creditsPeriodEnds":`Billing period ends {date}`,"quota.usedPercent":`{pct}% used`,"quota.limitReached":`Limit reached`,"quota.resetsToday":`Resets today at {time}`,"quota.resetsTomorrow":`Resets tomorrow at {time}`,"quota.resetsAt":`Resets {when}`,"quota.resetsRelativeMinutes":`Resets in {n} min`,"quota.resetsRelativeHours":`Resets in {n} h`,"pws.status.ready":`Ready`,"pws.status.needsSetup":`Needs setup`,"pws.status.needsAttention":`Needs attention`,"pws.auth.chatgptPassthrough":`ChatGPT passthrough`,"pws.auth.noKey":`No key needed`,"pws.freeTitle":`Free pricing (a key may still be required)`,"pws.localTitle":`Local runtime`,"pws.modelCountOne":`1 model`,"pws.modelCount":`{count} models`,"pws.rail.suffixDefault":` · default`,"pws.rail.suffixLocal":` · local`,"pws.rail.suffixFree":` · free`,"pws.rail.selectAria":`Select {name} — {status}{suffix}`,"pws.searchPlaceholder":`Search providers…`,"pws.filterAria":`Filter providers`,"pws.providerFiltersAria":`Provider filters`,"pws.filters":`Filters`,"pws.filterStatus":`Status`,"pws.pricing":`Pricing`,"pws.paid":`Paid`,"pws.filterType":`Type`,"pws.type.cloud":`Cloud`,"pws.type.local":`Local`,"pws.type.selfHosted":`Self-hosted`,"pws.type.login":`Login`,"pws.sort":`Sort`,"pws.sortProvidersAria":`Sort providers`,"pws.sort.az":`A–Z`,"pws.sort.za":`Z–A`,"pws.sort.freePaid":`Free first`,"pws.sort.paidFree":`Paid first`,"pws.sort.accountsFirst":`Accounts first`,"pws.resetAll":`Reset all`,"pws.providerList":`Provider list`,"pws.providersAria":`Providers`,"pws.groupReady":`Ready ({count})`,"pws.groupNeedsSetup":`Needs setup ({count})`,"pws.groupDisabled":`Disabled ({count})`,"pws.noSearchResults":`No providers match your search.`,"pws.noMatchFilters":`No providers match the filters.`,"pws.noProvidersConfigured":`No providers configured.`,"pws.workspaceMainAria":`Provider details`,"pws.detailComingSoon":`Detail view coming soon — use the classic view to manage this provider.`,"pws.selectPrompt":`Select a provider from the list.`,"pws.connectFirst":`Connect your first provider`,"pws.empty.browseFree":`Browse free providers`,"pws.empty.browseFreeDesc":`Start without a subscription`,"pws.empty.connectAccount":`Connect an account`,"pws.empty.connectAccountDesc":`Use your ChatGPT or provider login`,"pws.empty.addEndpoint":`Add an endpoint`,"pws.empty.addEndpointDesc":`Custom base URL and API key`,"pws.tab.overview":`Overview`,"pws.tab.models":`Models`,"pws.tab.usage":`Usage`,"pws.tab.accounts":`Accounts`,"pws.tab.settings":`Settings`,"pws.connection":`Connection`,"pws.status.connected":`Connected`,"pws.attentionTitle":`Needs attention`,"pws.attention.reauth":`Active account needs re-authentication`,"pws.attention.reauthForward":`Active Codex account needs re-authentication — open Accounts to fix it`,"pws.attention.missingCredentials":`Missing credentials`,"pws.cell.auth":`Authentication`,"pws.cell.note":`Note`,"pws.cell.defaultModel":`Default model`,"pws.statsAria":`Provider statistics`,"pws.statsTitle":`Statistics`,"pws.stats.totalRequests":`Requests (30d)`,"pws.stats.totalTokens":`Tokens (30d)`,"pws.stats.quotaUpdated":`Quota updated`,"pws.stats.quotaTracked":`Rate limits tracked on the Usage tab.`,"pws.stats.source":`Source`,"pws.usageLast30d":`Usage (last 30 days)`,"pws.estimatedCost":`Estimated cost`,"pws.costDisclaimer":`API list-price estimate, not an actual charge.`,"pws.modelBreakdown":`Model breakdown`,"pws.col.model":`Model`,"pws.col.cost":`Est. cost`,"pws.col.tokens":`Tokens`,"pws.col.requests":`Req.`,"pws.col.share":`Share`,"pws.tokenInput":`Input`,"pws.tokenOutput":`Output`,"pws.metricRequests":`requests`,"pws.metricTokens":`tokens`,"pws.usageUnavailable":`No usage recorded yet.`,"pws.rateLimits":`Rate limits`,"pws.quotaUnavailable":`No quota data for this provider.`,"pws.accountQuotaUnavailable":`Rate-limit data temporarily unavailable; showing last known values when present.`,"pws.selected":`Selected`,"pws.copyModelId":`Copy ID`,"pws.modelCopied":`Copied!`,"pws.modelsAvailable":`{count} available`,"pws.modelSearchPlaceholder":`Filter models…`,"pws.modelsLoading":`Loading models…`,"pws.modelsLoadFailed":`Could not load models.`,"pws.modelsNeedsReauth":`Account needs re-login before live model discovery works. Showing configured models for now.`,"pws.modelsConfiguredFallback":`Showing configured models (live discovery unavailable).`,"pws.modelsTruncated":`Showing first {shown} of {total} models. Filter to narrow the list.`,"pws.retry":`Retry`,"pws.noModels":`No models discovered for this provider.`,"pws.noModelMatch":`No models match the filter.`,"pws.adapterBaseRequired":`Adapter and base URL are required.`,"pws.addAccount":`Add account`,"pws.addKey":`Add API key`,"pws.apiKeys":`API Keys`,"pws.authMode":`Auth mode`,"pws.availableAccounts":`Available accounts`,"pws.accountOrdinal":`Account {count}`,"pws.accountsLoading":`Loading accounts…`,"pws.accountsLoadFailed":`Accounts could not be loaded.`,"pws.retryAccounts":`Retry`,"pws.noAccounts":`No accounts are connected yet.`,"pws.cockpitImportDescription":`Import a Cockpit Tools Antigravity JSON export from this device. The file contents are not shown.`,"pws.cockpitImportFileLabel":`Cockpit Tools Antigravity JSON export`,"pws.cockpitImportChooseFile":`Choose JSON file`,"pws.cockpitImporting":`Importing…`,"pws.cockpitImportInvalid":`The selected file is not a valid JSON export or is too large.`,"pws.cockpitImportFailed":`The account import could not be completed.`,"pws.cockpitImportComplete":`Import complete: {imported} imported, {updated} updated, {failed} failed, {unsupported} unsupported.`,"pws.accountSwitching":`Switching…`,"pws.accountCurrent":`Current account`,"pws.defaultModelNone":`None (use provider default)`,"pws.discardSettings":`Discard`,"pws.jsonEditorDesc":`Edit the raw provider JSON config. Changes are saved immediately.`,"pws.jsonEditorTitle":`JSON editor — {name}`,"pws.jsonRestore":`Restore`,"pws.jsonSave":`Save`,"pws.loggedInTitle":`Logged in`,"pws.notLoggedInTitle":`Not logged in`,"pws.note":`Note`,"pws.allowPrivateNetwork":`Allow local/private network`,"pws.liveModels":`Discover models from provider`,"pws.liveModelsDesc":`Fetch the provider's live model catalog. Turn this off to use only configured/static models.`,"pws.xaiResponsesOptIn":`Use Responses API for Grok 4.5 and 4.6`,"pws.xaiResponsesOptInDesc":`Routes both models through openai-responses. Other Grok models and tier behavior are unchanged.`,"pws.xaiResponsesOptInMixed":`Partially enabled.`,"pws.cursorTransport":`Cursor transport`,"pws.cursorTransportHttp2":`HTTP/2 (default)`,"pws.cursorTransportHttp1":`HTTP/1.1 (proxy compatibility)`,"pws.cursorTransportDesc":`Use HTTP/1.1 when your proxy cannot reliably carry Cursor's HTTP/2 stream.`,"pws.optionalPlaceholder":`Optional`,"pws.providerId":`Provider ID`,"pws.reauth":`Needs re-auth`,"pws.reauthenticate":`Re-authenticate`,"pws.copyDoctor":`Copy ocx doctor`,"pws.doctorCopied":`Copied`,"pws.doctorCopyUnavailable":`Clipboard unavailable`,"pws.healthCooldownHint":`Wait until the cooldown ends. Do not probe this account yet.`,"pws.healthLabel.rateLimited":`Rate limited`,"pws.healthLabel.quotaLimited":`Quota limited`,"pws.healthLabel.reauthRequired":`Reauthentication required`,"pws.healthLabel.refreshFailed":`Refresh failed`,"pws.healthLabel.metadataMismatch":`Metadata mismatch`,"pws.healthLabel.credentialConflict":`Credential conflict`,"pws.healthSummary.rateLimited":`{provider} {account}: rate limited until {until}. Routing for this account is paused until then.`,"pws.healthSummary.quotaLimited":`{provider} {account}: quota limited until {until}. Routing for this account is paused until then.`,"pws.healthSummary.reauthRequired":`{provider} {account}: reauthentication required.`,"pws.healthSummary.credentialConflict":`{provider} {account}: credential conflict.`,"pws.healthSummary.metadataMismatch":`{provider} {account}: metadata mismatch.`,"pws.healthSummary.staleCredentials":`{provider} {account}: incomplete credentials.`,"pws.removeConfirm":`Remove`,"pws.removeConfirmBody":`Remove provider "{name}"? This cannot be undone.`,"pws.removeDefaultConfirmBody":`Remove default provider "{name}"? "{defaultProvider}" will become the default provider. This cannot be undone.`,"pws.removeConfirmTitle":`Remove provider`,"pws.saveSettings":`Save`,"pws.pacingTitle":`Request pacing`,"pws.pacingDesc":`Evenly delay outbound request starts for this provider. Streaming responses may overlap.`,"pws.pacingEnabled":`Enabled`,"pws.pacingRpm":`Requests per minute`,"pws.pacingRpmUnit":`RPM`,"pws.pacingDelay":`Minimum interval (ms)`,"pws.pacingSlowerWins":`The slower provider limit wins. Model overrides can only add more delay.`,"pws.pacingQueued":`queued`,"pws.pacingNextSlot":`until next slot`,"pws.pacingLastModel":`last model`,"pws.pacingNone":`None`,"pws.pacingModelOverrides":`Model overrides`,"pws.pacingModel":`Model`,"pws.pacingAdd":`Add override`,"pws.pacingRemove":`Remove`,"pws.pacingRemoveModel":`Remove request pacing override for {model}`,"pws.pacingRuleRequired":`Enable request pacing only after setting a provider limit or a model override.`,"pws.saving":`Saving…`,"pws.settingsSaved":`Settings saved.`,"pws.accountModeSaved":`Account mode saved.`,"pws.accountModeFailed":`Could not switch the account mode.`,"pws.accountModeConfirm":`Switch the OpenAI account mode? Running conversations will be reassigned to the other mode's account set, and quota usage will be tracked against the new mode.`,"pws.settingsUnsavedBar":`You have unsaved changes.`,"pws.unsavedLeaveBody":`You have unsaved changes. Save them before leaving?`,"pws.unsavedLeaveTitle":`Unsaved changes`,"pws.attentionRequired":`Attention required`,"pws.attentionAria":`{name}: {reason}`,"pws.missingCredentials":`Missing credentials`,"pws.editJsonDesc":`Edit the raw proxy config as JSON`,"pws.updatesUnavailable":`Provider updates are not available.`,"pws.dashboard.title":`Providers overview`,"pws.dashboard.subtitle":`Manage all your model providers in one place.`,"pws.dashboard.rateLimits":`RATE LIMITS`,"pws.capacity.estimate":`Configured-weight pool estimate`,"pws.capacity.currentAccount":`Current effective account`,"pws.capacity.nextRecovery":`Next capacity recovery`,"pws.capacity.recoveryShare":`+{percent}% pool capacity`,"pws.capacity.incomplete":`Incomplete coverage: {excluded} account(s) excluded`,"pws.capacity.uncalibratedPlan":`{count} account(s) on an uncalibrated plan are counted at the baseline seat weight, so this estimate may be conservative`,"pws.capacity.partial":`Partial window coverage: {count} account(s) do not report every displayed limit window`,"pws.capacity.windowPartial":`Partial`,"pws.capacity.windowPartialA11y":`{window}: incomplete account coverage`,"pws.dashboard.recentlyUsed":`RECENTLY USED`,"pws.dashboard.requests":`{count} requests`,"pws.dashboard.checkedAgo":`Checked {time}`,"pws.dashboard.noQuota":`No quota data`,"pws.dashboard.noUsage":`No usage data yet`,"pws.dashboard.noRateLimits":`No rate-limit data yet`,"pws.allProviders":`Provider Overview`,"pws.enabledLabel":`Enabled`,"pws.testConnection":`Test connection`,"pws.testing":`Testing…`,"pws.connectionOk":`Connection OK`,"pws.connectionFailed":`Connection failed`,"pws.connectionNotApplicable":`Not applicable — this provider uses a static model catalog.`,"pws.editSettings":`Edit settings`,"pws.viewUsage":`View detailed usage`,"pws.allSystemsOk":`All systems operational`,"pws.apiKeyConfigured":`API key configured`,"pws.addApiKey":`Add API key`,"pws.loggedInAs":`Logged in as {email}`,"pws.notLoggedIn":`Not logged in`,"pws.passthrough":`Codex passthrough`,"pws.notes":`NOTES`,"pws.notePlaceholder":`Add a note about this provider...`,"pws.noteSaved":`Note saved`,"pws.authSummary":`AUTHENTICATION`,"time.justNow":`Just now`,"time.notChecked":`Not checked`,"time.minutesAgo":`{n}m ago`,"time.hoursAgo":`{n}h ago`,"time.daysAgo":`{n}d ago`,"modal.noMatch":`No match.`,"modal.oauthDefaultNote":`Log in with your account — no API key needed.`,"modal.oauthComingSoon":`OAuth login for {label} arrives in the next update. Use an API key for now.`,"modal.oauthComingSoonShort":`OAuth login for this provider arrives in the next update — use an API key for now.`,"modal.useApiKeyInstead":`Use an API key instead`,"modal.setupGuide":`Setup guide`,"modal.setupStep1Prefix":`Go to`,"modal.setupDashboardLink":`{label} dashboard`,"modal.setupStep1Suffix":`and copy your API key`,"modal.setupStep2":`Paste it in the API key field below`,"modal.setupStep3":`Click Add provider — models are auto-discovered`,"modal.namePlaceholder":`e.g. openrouter`,"modal.duplicateWarn":`Provider "{name}" exists and will be overwritten.`,"modal.forwardHintPrefix":`No key needed — the proxy forwards your`,"modal.forwardCredentials":`codex login`,"modal.forwardHintSuffix":`credentials to this provider.`,"modal.localHint":`No API key is stored. This adds Cursor's static public model catalog for Codex, but live Cursor transport and native file/shell execution remain disabled until audited.`,"modal.getApiKey":`Get your {label} API key`,"modal.apiKey":`API key`,"modal.apiKeyTransport":`API key header`,"modal.apiKeyTransportNative":`x-api-key (Anthropic native)`,"modal.apiKeyTransportBearer":`Authorization: Bearer`,"modal.apiKeyPlaceholder":`sk-… (or $ENV_VAR)`,"modal.defaultModelPlaceholder":`e.g. gpt-5.5`,"modal.baseUrlPlaceholder":`https://...`,"modal.baseUrlPlaceholderError":`Base URL contains an unresolved {placeholder}. Replace it with your actual value.`,"modal.baseUrlPlaceholderHint":`Replace the {placeholder} in the Base URL with your actual Account ID before adding.`,"modal.adding":`Adding…`,"modal.useOauthLogin":`← Use OAuth login`,"nav.codexAuth":`Codex Auth`,"nav.codexSet":`Codex Set`,"codexSet.tab.multiauth":`Multi-auth`,"codexSet.tab.prompt":`Prompt`,"codexSet.prompt.title":`Prompt layers`,"codexSet.prompt.timing":`Applies to newly started sessions. Running sessions keep their current prompt settings.`,"codexSet.prompt.staleRevision":`The configuration changed elsewhere. The list was reloaded.`,"codexSet.prompt.writeFailed":`The change could not be saved.`,"codexSet.prompt.loadFailed":`The prompt layers could not be loaded.`,"codexSet.prompt.repair":`Repair`,"codexSet.prompt.repairFailed":`The repair could not be completed.`,"codexSet.drift.journalPresent":`A previous write did not finish. Recovery runs automatically on the next write.`,"codexSet.drift.projectionStale":`The saved layers and the value in config.toml disagree. Repair rewrites the value from your layers.`,"codexSet.drift.storeMissing":`The layer file is gone while instructions remain in config.toml. Repair keeps the text as one layer and writes a backup first.`,"codexSet.drift.ownedMalformed":`The generated line in config.toml was reshaped by hand, so it is no longer safe to rewrite.`,"codexSet.custom.adoptUnsupported":`The value at {path} line {line} is not a single-line string, so it cannot be imported. Move it by hand to manage it here.`,"codexSet.prompt.unreadable":`The Codex configuration file exists but could not be read, so changes are refused.`,"codexSet.layer.permissions":`Permissions`,"codexSet.layer.collaboration":`Collaboration mode`,"codexSet.layer.environment":`Environment context`,"codexSet.layer.apps":`Apps`,"codexSet.layer.skills":`Skills`,"codexSet.prompt.extensionsUnknown":`Extensions can add their own layers. Codex does not expose them, so they cannot be listed here.`,"codexSet.group.transition":`Transition notices`,"codexSet.group.transitionDesc":`These announce a change rather than describe state, so they appear only when the session enters realtime or switches model.`,"codexSet.custom.slotNote":`Custom layers are joined into one section in this order.`,"codexSet.row.alwaysOn":`Always on`,"codexSet.row.onChange":`Fires on change`,"codexSet.row.featureGated":`Configured under [features]`,"codexSet.row.openFeatures":`Open settings`,"codexSet.dialog.setValue":`{value} (default {fallback})`,"codexSet.dialog.copyKey":`Copy key`,"codexSet.dialog.unknownLayer":`This build has no description for this layer. It comes from a newer Codex runtime than the dashboard.`,"codexSet.custom.heading":`Custom layers`,"codexSet.custom.add":`+ Add layer`,"codexSet.custom.newTitle":`New layer`,"codexSet.custom.editTitle":`Edit layer`,"codexSet.custom.titleLabel":`Title`,"codexSet.custom.bodyLabel":`Instructions`,"codexSet.custom.bodySize":`{bytes} of {max} bytes`,"codexSet.custom.normalized":`Tabs became four spaces and line endings became LF.`,"codexSet.custom.titleRequired":`Enter a title.`,"codexSet.custom.titleTooLong":`Title is {count} characters; the limit is {max}.`,"codexSet.custom.titleMultiline":`A title must be a single line.`,"codexSet.custom.bodyTooLarge":`This layer is {bytes} bytes; the limit is {max}.`,"codexSet.custom.composedTooLarge":`Together the enabled layers would be {bytes} bytes, over the limit.`,"codexSet.custom.invalidCharacter":`A control character at position {position} cannot be saved.`,"codexSet.custom.discardPrompt":`Discard your changes?`,"codexSet.custom.keepEditing":`Keep editing`,"codexSet.custom.delete":`Delete {title}`,"codexSet.custom.deleteConfirm":`Delete this layer? There is no undo.`,"codexSet.custom.layerGone":`That layer was removed elsewhere, so the editor was closed.`,"codexSet.custom.deleteConfirmNamed":`Delete “{title}”? There is no undo.`,"codexSet.custom.moveUp":`Move {title} up`,"codexSet.custom.prevLayer":`Previous layer`,"codexSet.custom.nextLayer":`Next layer`,"codexSet.custom.navPosition":`{position} / {total}`,"codexSet.custom.moveDown":`Move {title} down`,"codexSet.custom.limitReached":`You can keep up to {max} custom layers.`,"codexSet.custom.notOwned":`developer_instructions was written outside opencodex, so it is not edited from here. Import it to manage it as a layer.`,"codexSet.custom.adopt":`Import existing instructions`,"codexSet.custom.adoptConfirm":`Import as a layer`,"codexSet.custom.adoptRefused":`The existing value could not be imported.`,"codexSet.custom.baseReplaced":`model_instructions_file is set to {path}, so something outside opencodex has replaced the base prompt.`,"codexSet.lint.identity":`This claims a different identity from the one Codex establishes.`,"codexSet.lint.foreignTool":`Tools come from the registry; naming one here does not create it.`,"codexSet.lint.placeholder":`No template engine runs over instructions, so this ships literally.`,"codexSet.lint.applyPatch":`apply_patch is defined by the tool registry, not by instructions.`,"codexSet.lint.approvalVocab":`Codex injects its own approval vocabulary; this may contradict it.`,"codexSet.lint.environment":`Environment facts are generated later and may contradict this.`,"codexSet.lint.size":`This layer is over 8 KB. It still saves, but it costs tokens on every request.`,"codexSet.preset.blank":`Blank layer`,"codexSet.preset.concise.name":`Concise output`,"codexSet.preset.concise.description":`Short answers, no preamble, minimal formatting.`,"codexSet.preset.concise.provenance":`Adapted from Claude Code's brevity directives. Our wording, not a copy.`,"codexSet.preset.planFirst.name":`Plan before editing`,"codexSet.preset.planFirst.description":`State the plan, then make the change.`,"codexSet.preset.planFirst.provenance":`Adapted from Claude Code's planning posture. Our wording, not a copy.`,"codexSet.preset.explainWhy.name":`Explain reasoning`,"codexSet.preset.explainWhy.description":`Say why, not only what.`,"codexSet.preset.explainWhy.provenance":`Adapted from Grok Build's confirmation style. Our wording, not a copy.`,"codexSet.preset.testFirst.name":`Test first`,"codexSet.preset.testFirst.description":`Write the failing test before the fix.`,"codexSet.preset.testFirst.provenance":`Adapted from common agent practice. Our wording, not a copy.`,"codexSet.preset.korean.name":`Korean replies`,"codexSet.preset.korean.description":`Answer in Korean whatever language the request uses.`,"codexSet.preset.korean.provenance":`Written for opencodex from a user-requested staple. Our wording, not a copy.`,"codexSet.dialog.class":`Kind`,"codexSet.dialog.key":`Config key`,"codexSet.dialog.fileValue":`Value in this file`,"codexSet.dialog.absentDefault":`not set (defaults to {value})`,"codexSet.dialog.noRenderedText":`Codex does not expose the assembled text of a built-in layer, so this dialog describes the layer and names its key rather than showing its contents.`,"codexSet.dialog.sourceText":`Text sent to the model`,"codexSet.dialog.sourceBytes":`{bytes} bytes`,"codexSet.dialog.notRendered":`This layer sent nothing on the turn we read. Sections are only re-sent when they change, so an unchanged layer is absent from a single sample.`,"codexSet.dialog.emptySource":`The file at {path} exists but is empty, so this layer sends nothing.`,"codexSet.dialog.notExposed":`The base prompt travels outside the message list Codex can print, so it cannot be shown here. Replacing it is possible through model_instructions_file.`,"codexSet.dialog.textUnavailable":`The Codex prompt could not be read on this machine, so the text is unavailable.`,"codexSet.class.base":`Base instructions`,"codexSet.class.config-toggle":`Switchable here`,"codexSet.class.feature-gated":`Feature-gated`,"codexSet.class.runtime-conditional":`Runtime-conditional`,"codexSet.class.extension-unknown":`Extension layer`,"codexSet.layer.base-instructions":`Base instructions`,"codexSet.layer.model-switch":`Model switch notice`,"codexSet.layer.personality":`Personality`,"codexSet.layer.context-window-guidance":`Context window guidance`,"codexSet.layer.realtime":`Realtime`,"codexSet.layer.agents-md":`AGENTS.md`,"codexSet.layer.environments-instructions":`Environments`,"codexSet.layer.plugins":`Plugins`,"codexSet.layer.tools":`Tools`,"codexSet.layer.multi-agent-mode":`Multi-agent mode`,"codexSet.layer.git-attribution":`Commit attribution`,"codexSet.about.base-instructions":`Codex's own instructions. They travel with the request itself and cannot be turned off.`,"codexSet.about.model-switch":`Added when the session changes model mid-conversation.`,"codexSet.about.personality":`Tone and voice guidance, governed by a feature flag.`,"codexSet.about.context-window-guidance":`Advice about the remaining context budget, governed by a feature flag.`,"codexSet.about.realtime":`Added for realtime sessions.`,"codexSet.about.agents-md":`Your project's AGENTS.md files. This page reports the layer; it never edits your project docs.`,"codexSet.about.permissions":`Explains the sandbox and approval settings in force.`,"codexSet.about.collaboration":`Explains the active collaboration mode.`,"codexSet.about.environment":`Working directory, platform, and other environment facts.`,"codexSet.about.environments-instructions":`Guidance for deferred execution environments, governed by a feature flag.`,"codexSet.about.apps":`How to use connected apps.`,"codexSet.about.plugins":`Added when a plugin is selected or any plugin advertises a capability.`,"codexSet.about.tools":`Deferred tool descriptions, governed by a feature flag.`,"codexSet.about.skills":`The list of available skills.`,"codexSet.about.multi-agent-mode":`Subagent instructions, governed by a feature flag.`,"codexSet.about.git-attribution":`Tells the model to add a Co-authored-by: Codex trailer to commits it writes, and a Generated with Codex. line to pull requests it opens. Codex resolves this from your account, so there is no setting for it here or under [features]. When your account has it off, Codex sends the opposite instruction rather than sending nothing.`,"codexSet.condition.model-switch":`Emitted only after a mid-session model change.`,"codexSet.condition.realtime":`Emitted only in a realtime session.`,"codexSet.condition.agents-md":`Emitted when a project doc is found for the working directory.`,"codexSet.condition.plugins":`Emitted when a plugin is selected or any plugin advertises a capability.`,"codexSet.condition.git-attribution":`Set by your account's attribution policy.`,"codexSet.base.title":`Base prompt`,"codexSet.base.prev":`Previous option`,"codexSet.base.next":`Next option`,"codexSet.base.position":`{position} / {total}`,"codexSet.base.swipeHint":`Swipe sideways, use the arrow keys, or press the arrows to move between options. Applies to newly started sessions.`,"codexSet.base.defaultTitle":`Codex's own base prompt`,"codexSet.base.defaultBody":`The default is not stored here, so there is nothing to edit or delete: choosing it simply removes model_instructions_file from your config, and Codex uses the prompt it ships with.`,"codexSet.base.variantTitle":`Name`,"codexSet.base.variantBody":`Prompt`,"codexSet.base.replacesWarning":`This REPLACES Codex's own base prompt rather than adding to it. A short prompt here means a model with short instructions.`,"codexSet.base.use":`Use this one`,"codexSet.base.inUse":`In use`,"codexSet.base.externalBlocked":`model_instructions_file already points at {path}, which opencodex did not write. Clear it yourself before choosing an option here.`,"nav.api":`API`,"nav.integrations":`Integrations`,"nav.openMenu":`Open menu`,"nav.closeMenu":`Close menu`,"integrations.subtitle":`Connect clients to opencodex, manage credentials, and restore client configuration.`,"integrations.tabsLabel":`Integration surfaces`,"integrations.tab.overview":`Overview`,"integrations.tab.keys":`API Keys`,"integrations.tab.codex":`Codex`,"integrations.tab.claude":`Claude`,"integrations.tab.grok":`Grok Build`,"integrations.tab.cursor":`Cursor`,"integrations.tab.opencode":`OpenCode`,"integrations.tab.pi":`Pi`,"integrations.tab.omp":`OMP`,"integrations.tab.hermes":`Hermes`,"integrations.tab.openclaw":`OpenClaw`,"integrations.tab.kimi":`Kimi Code`,"integrations.tab.gajae":`Gajae Code`,"integrations.tab.dsh":`DSH`,"integrations.tab.mcode":`MiniMax Code`,"integrations.tab.zcode":`ZCode`,"integrations.tab.prime":`Prime Agent`,"integrations.tab.aside":`Aside`,"integrations.codex.title":`Codex CLI`,"integrations.codex.body":`Codex wiring is owned by the proxy service. Starting opencodex applies it; stopping the service restores native routing.`,"integrations.codex.openService":`Open service controls`,"integrations.state.notInstalled":`Not installed`,"integrations.state.unknown":`Checking…`,"integrations.detail.codexRouted":`Codex requests go through this proxy`,"integrations.detail.codexAbsent":`Codex is not routed through this proxy yet`,"integrations.detail.keyCount":`{count} key(s) issued`,"integrations.detail.keyNone":`No keys issued`,"integrations.detail.keyChecking":`Checking…`,"integrations.detail.keyUnavailable":`Key status unavailable`,"integrations.detail.claudeOff":`Connection is off`,"integrations.detail.desktopCurrent":`Desktop is running this profile`,"integrations.detail.desktopStale":`The profile file changed after it was applied`,"integrations.detail.desktopNotServed":`The profile exists, but Desktop serves another one`,"integrations.detail.desktopAbsent":`No profile applied`,"integrations.detail.desktopDesiredOff":`Claude Desktop integration is off`,"integrations.detail.desktopDesiredOffCleanupPending":`Claude Desktop is still using the gateway; cleanup is pending`,"integrations.detail.desktopDesiredOnNotApplied":`Integration is on, but Desktop is not using the gateway profile`,"integrations.detail.desktopSelectedElsewhere":`Desktop is using another profile`,"integrations.detail.desktopProfileDrift":`The selected Desktop profile changed`,"integrations.detail.desktopObservedUnsafe":`The selected Desktop profile cannot be changed safely`,"integrations.detail.desktopNotInstalled":`Claude Desktop configuration library is not installed`,"integrations.detail.grokModels":`{count} model(s) wired`,"integrations.detail.grokAbsent":`No opencodex block in the config`,"integrations.detail.cursorSeen":`Cursor called this proxy recently`,"integrations.detail.cursorNeverSeen":`Private Inference installed; no request seen yet`,"integrations.detail.cursorAbsent":`Cursor Private Inference not found`,"integrations.cursor.title":`Cursor`,"integrations.cursor.intro":`Cursor Private Inference runs its agent locally and talks to opencodex on loopback. Regular Cursor cannot: its backend calls the custom endpoint and needs a public HTTPS URL. This page never writes to Cursor; paste the values below into Cursor yourself.`,"integrations.cursor.loading":`Reading Cursor status…`,"integrations.cursor.unavailable":`Could not read the Cursor status from the proxy.`,"integrations.cursor.detection":`Installed builds`,"integrations.cursor.privateInference":`Cursor Private Inference`,"integrations.cursor.regular":`Cursor (regular)`,"integrations.cursor.detected":`Detected`,"integrations.cursor.notFound":`Not found`,"integrations.cursor.regularOnly":`Only regular Cursor was found. It routes custom endpoints through Cursor's servers, so a loopback proxy is unreachable without a public tunnel. See the guide for the Private Inference build.`,"integrations.cursor.nothingFound":`No Cursor install was found in the usual locations. If it is installed elsewhere, the values below still apply.`,"integrations.cursor.gateway":`Gateway values`,"integrations.cursor.gatewayHint":`In Cursor Private Inference open Settings > Models > Gateway, paste these two values, then press Refresh model list.`,"integrations.cursor.baseUrl":`Base URL`,"integrations.cursor.apiKey":`API Key`,"integrations.cursor.apiKeyCredential":`One of your opencodex API keys (this bind requires a credential)`,"integrations.cursor.copy":`Copy`,"integrations.cursor.copied":`Copied`,"integrations.cursor.connection":`Connection`,"integrations.cursor.seen":`Last request from Cursor: {time} ({ua})`,"integrations.cursor.neverSeen":`No request from Cursor since the proxy started. After saving the gateway, press Refresh model list in Cursor.`,"integrations.cursor.models":`What Cursor will show`,"integrations.cursor.modelsHint":`Cursor picks the Reasoning ladder from its own model table, so opencodex can only predict it. Context lists the default and the opt-in window (Cursor's Max Mode).`,"integrations.cursor.ladderFromBundle":`Reasoning ladders read from the installed Cursor Private Inference {version} bundle. Cursor decides them; opencodex only reports its table.`,"integrations.cursor.ladderFromStatic":`Reasoning ladders are a static mirror of Cursor 3.18.25 (no readable Private Inference bundle was found). Context lists the default and the opt-in window.`,"integrations.cursor.unknownVersion":`unknown version`,"integrations.cursor.noControl":`—`,"integrations.cursor.singleWindow":`single window`,"integrations.cursor.noControlTitle":`This id is not in Cursor's built-in effort table, so Cursor shows no Reasoning control.`,"integrations.cursor.effortRowsOne":`1 effort row published`,"integrations.cursor.effortRowsMany":`{n} effort rows published`,"integrations.cursor.effortRowsOff":`no effort rows`,"integrations.cursor.tableLessHint":`Rows marked — get no Reasoning control in Cursor. Turn on cursorEffortRows to publish one picker entry per effort (id--effort), or set modelDefaultReasoningEfforts on the provider for a fixed default.`,"integrations.cursor.colModel":`Model`,"integrations.cursor.colReasoning":`Reasoning`,"integrations.cursor.colContext":`Context`,"integrations.cursor.guide":`Open the Cursor Private Inference guide`,"integrations.dialog.grok.title":`Disable the Grok Build integration?`,"integrations.dialog.grok.changes":`Only the block marked by opencodex will be removed from {path}. Content written outside the block will remain unchanged.`,"integrations.dialog.grok.breakage":`Disabling removes the opencodex model aliases from Grok Build. Models used with your xAI account remain available.`,"integrations.dialog.grok.undo":`If opencodex is running on a loopback address, turning this back on writes a new block from the models currently available.`,"integrations.dialog.grok.confirm":`Disable`,"integrations.dialog.desktop.title":`Disable Claude Desktop integration?`,"integrations.dialog.desktop.changes":`If {path} contains an opencodex-managed gateway profile, Desktop will first select a new credential-free standard profile, then remove the old profile and backup.`,"integrations.dialog.desktop.breakage":`Claude Desktop will return to standard Claude instead of models routed through opencodex.`,"integrations.dialog.desktop.undo":`Turning this back on regenerates the opencodex profile from your saved model assignments.`,"integrations.dialog.desktop.restart":`Claude Desktop reads this configuration only at launch. Fully quit and reopen it for this change to take effect.`,"integrations.dialog.desktop.confirm":`Disable`,"integrations.native.msg.nonLoopbackRemoved":`Grok Build can be registered automatically only while opencodex runs on a loopback address. The previous block that pointed to loopback was removed.`,"integrations.native.msg.nonLoopbackRemovedNoop":`Grok Build can be registered automatically only while opencodex runs on a loopback address. There was no previous block to remove.`,"integrations.native.msg.nonLoopbackSuperseded":`Grok Build can be registered automatically only while opencodex runs on a loopback address. Another process wrote a new block in the meantime, so the block now in the file was not created by this request.`,"integrations.native.error.orphanedMarker":`{path} has an opencodex start marker but no end marker. The file was left unchanged because opencodex cannot determine where its block ends.`,"integrations.native.error.homeMismatch":`The installed service home does not match the current home, so the file was left unchanged.`,"integrations.native.error.notInstalled":`Grok Build is not installed, so there is nothing to change.`,"integrations.native.error.configBusy":`The configuration is being saved elsewhere and could not be changed. Try again shortly.`,"integrations.native.error.desktopUnsafeMetadata":`Claude Desktop metadata at {path} could not be read safely, so its library was not changed.`,"integrations.native.error.desktopCleanupIncomplete":`Claude Desktop is pointed at standard mode, but old opencodex credential files remain at: {paths}.`,"integrations.native.msg.desktopDisabled":`Claude Desktop integration disabled.`,"integrations.native.msg.desktopEnabled":`Claude Desktop integration enabled.`,"integrations.state.absent":`Not applied`,"integrations.state.current":`Applied`,"integrations.state.stale":`Update needed`,"integrations.state.conflict":`Conflict`,"integrations.state.unsafe":`Cannot verify`,"integrations.summary.detected":`Clients detected`,"integrations.summary.applied":`Configured clients`,"integrations.summary.stale":`Update needed`,"integrations.summary.lastChange":`Last change`,"integrations.summary.disableAll":`Disable all…`,"integrations.onboarding":`Applying writes one opencodex provider block after saving a backup. Disable removes only that block, and a retained snapshot can be restored.`,"integrations.empty.title":`No installed clients were detected`,"integrations.empty.body":`Install a supported client, then return here to apply opencodex.`,"integrations.action.apply":`Apply`,"integrations.action.disable":`Disable`,"integrations.action.refresh":`Update`,"integrations.action.settings":`Settings`,"integrations.action.manageKeys":`Manage keys`,"integrations.action.restore":`Restore…`,"integrations.action.undo":`Undo`,"integrations.action.restorePoint":`Restore this point…`,"integrations.action.snapshotExpired":`Backup expired`,"integrations.rollback.title":`Rollback center`,"integrations.rollback.empty":`No apply history yet`,"integrations.rollback.emptyBody":`Every successful write keeps a pre-write snapshot first.`,"integrations.catalog.title":`Clients`,"integrations.rollback.older":`Earlier operations`,"integrations.rollback.showMore":`Show {n} more`,"integrations.rollback.failed":`Could not load the rollback history.`,"integrations.restore.title":`Restore this snapshot?`,"integrations.restore.body":`The current file is backed up first, then the selected snapshot replaces it.`,"integrations.restore.driftTitle":`Newer edits were detected`,"integrations.restore.driftBody":`Changes made after this snapshot will be backed up, then the file will be replaced.`,"integrations.restore.confirm":`Restore`,"integrations.restore.confirmDrift":`Back up newer edits and restore`,"integrations.restore.pending":`Restoring…`,"integrations.restore.manual":`Automatic restore failed: {reason}. Restore manually from {path}.`,"integrations.error.load":`Could not load integration state.`,"integrations.error.stale":`The latest refresh failed. The values below may be stale.`,"integrations.error.busy":`Another change for this client is still running. Try again shortly.`,"integrations.error.conflict":`The config changed after opencodex wrote it. Nothing was removed.`,"integrations.error.unsafe":`The config cannot be changed safely.`,"integrations.error.generic":`The integration change failed. Your previous state was kept.`,"integrations.error.nonLoopback":`{client} can only reach a proxy on localhost — its config has nowhere to put the admission header a remote bind requires, so writing one by hand would not help either. Give it loopback access instead, through a tunnel or a local forwarder.`,"integrations.status.installed":`Installed`,"integrations.status.notInstalled":`Not installed`,"integrations.status.appliedAt":`Applied`,"integrations.status.backup":`Backup`,"integrations.status.lastRestore":`Last restore`,"integrations.status.unknown":`Unknown`,"integrations.bulk.title":`Disable applied client integrations?`,"integrations.bulk.body":`Only the opencodex-owned block is removed. A pre-write snapshot is kept for each client.`,"integrations.bulk.partial":`Some clients could not be disabled: {clients}`,"integrations.bulk.success":`Applied client integrations were disabled.`,"integrations.retention.degraded":`Backup cleanup is behind; older backups may still be on disk.`,"integrations.error.residual":`The file may be in an intermediate state: {message} Restore it from {path}.`,"integrations.error.recover":`{message} A backup is at {path}.`,"integrations.kind.apply":`Applied`,"integrations.kind.disable":`Disabled`,"integrations.kind.refresh":`Updated`,"integrations.kind.restore":`Restored`,"integrations.kind.overwrite":`Overwritten`,"integrations.dialog.overwrite.title":`Replace the block in this config?`,"integrations.dialog.overwrite.changesUnowned":`A block we did not write occupies the settings opencodex needs in {path}. Applying replaces it with the block opencodex would write.`,"integrations.dialog.overwrite.changesForeign":`Your edit inside the opencodex block in {path} will be discarded and replaced with the block opencodex would write.`,"integrations.dialog.overwrite.breakage":`Anything the other block configured stops taking effect. Content elsewhere in the file is left alone.`,"integrations.dialog.overwrite.undo":`A snapshot is saved first, so this appears in the rollback list below and can be undone.`,"integrations.dialog.overwrite.confirm":`Replace`,"integrations.action.overwrite":`Replace`,"integrations.semantics.opencode":`Direct disk launches only; ocx opencode environment injection takes precedence.`,"integrations.semantics.pi":`Applies to new sessions.`,"integrations.semantics.omp":`Restart OMP to load the catalog.`,"integrations.semantics.hermes":`Applies to new sessions.`,"integrations.semantics.openclaw":`Applies immediately to a running gateway.`,"integrations.semantics.kimi":`Restart or run /reload to apply it (v2 watches the file).`,"integrations.semantics.gajae":`Applies to a new session or when opening /model.`,"integrations.semantics.dsh":`OpenCodex manages only llm-pi-ai.providers.opencodex in $DSH_HOME/settings.yaml. DSH hot reloads this provider; your default model and deepseek-official stay unchanged. Currently loopback-only; no real credential is written.`,"integrations.semantics.mcode":`Manages only custom_provider.opencodex. Your default model and MiniMax login stay unchanged.`,"integrations.semantics.zcode":`Manages only provider.opencodex in ~/.zcode/v2/config.json. Your Z.ai login and other providers stay unchanged. Restart ZCode after changes.`,"integrations.semantics.prime":`Manages only providers.opencodex in Prime Agent's models.json — ~/.prime/agent unless PRIME_AGENT_CODING_AGENT_DIR redirects it. Your other providers and model overrides stay unchanged. Applies to new sessions.`,"integrations.semantics.aside":`Manages only providers.opencodex in Aside's models.json for the signed-in account (~/.aside/u/). Your other providers stay unchanged. Aside rewrites this file while running, so fully quit and reopen it after applying.`,"codexAuth.mainAccount":`Main Account`,"codexAuth.logLabel":`Log label`,"codexAuth.codexApp":`Codex App`,"codexAuth.moreActions":`Show more actions`,"codexAuth.copyId":`Copy account ID`,"codexAuth.appLogin":`App login`,"codexAuth.accountPool":`Account Pool`,"codexAuth.accountModeTitle":`OpenAI account mode`,"codexAuth.accountModePool":`Pool mode`,"codexAuth.accountModePoolDesc":`The main login and eligible added accounts rotate here.`,"codexAuth.accountModeDirect":`Direct mode`,"codexAuth.accountModeDirectDesc":`Requests use only the main login; added accounts remain stored for Pool mode.`,"codexAuth.openaiMissing":`The built-in OpenAI provider is not configured.`,"codexAuth.openaiDisabled":`The built-in OpenAI provider is disabled.`,"codexAuth.openaiUnavailableDesc":`Your OpenAI accounts are still available. Enable the provider to route Codex requests.`,"codexAuth.enableOpenai":`Enable OpenAI`,"codexAuth.enablingOpenai":`Enabling...`,"codexAuth.enableOpenaiFailed":`Failed to enable the OpenAI provider.`,"codexAuth.openaiPresetLoadFailed":`Failed to load the OpenAI provider preset.`,"codexAuth.openaiPresetUnavailable":`OpenAI provider preset is unavailable.`,"codexAuth.openProviders":`Open Providers`,"codexAuth.add":`Add`,"codexAuth.sparkQuota":`Codex Spark quota`,"codexAuth.sparkQuotaHint":`Show the GPT-5.3-Codex-Spark weekly window on account cards. Hidden by default because it applies to one model only.`,"codexAuth.sparkQuotaShown":`Codex Spark quota shown`,"codexAuth.sparkQuotaHidden":`Codex Spark quota hidden`,"codexAuth.sparkQuotaFailed":`Could not change the Codex Spark quota setting`,"codexAuth.refreshQuota":`Refresh quotas`,"codexAuth.refreshingQuota":`Refreshing...`,"codexAuth.quotaRefreshed":`Quotas refreshed`,"codexAuth.quotaRefreshFailed":`Failed to refresh quotas`,"codexAuth.pauseExhausted":`Pause exhausted`,"codexAuth.pausingExhausted":`Checking quotas...`,"codexAuth.pauseExhaustedSucceeded":`Accounts at the limit paused: {count}`,"codexAuth.pauseExhaustedNone":`No accounts have confirmed 100% usage.`,"codexAuth.pauseExhaustedFailed":`Failed to check and pause exhausted accounts.`,"codexAuth.noPool":`No pool accounts added yet.`,"codexAuth.pause":`Pause`,"codexAuth.resume":`Resume`,"codexAuth.paused":`PAUSED`,"codexAuth.pauseSucceeded":`{email} is paused`,"codexAuth.resumeSucceeded":`{email} is available to the pool again`,"codexAuth.pauseFailed":`Could not pause {email}. Nothing was changed.`,"codexAuth.resumeFailed":`Could not resume {email}. Nothing was changed.`,"codexAuth.pausedHint":`Excluded from automatic switching, retries, cooldown recovery, and manual selection until resumed.`,"codexAuth.pinned":`PINNED`,"codexAuth.pinnedHint":`You selected this account by hand, so a higher selection order will not move past it. The pin lasts until this account is drained, you select another, or you change any selection order.`,"codexAuth.fiveHour":`5h`,"codexAuth.weekly":`Week`,"codexAuth.monthly":`30d`,"codexAuth.resets":`resets`,"codexAuth.today":`Today`,"codexAuth.current":`CURRENT`,"codexAuth.nextSession":`SELECTED`,"codexAuth.poolPrepared":`PREPARED FOR POOL`,"codexAuth.preparePoolTitle":`Prepare this account for Pool mode?`,"codexAuth.preparePoolDesc":`Direct requests keep using the main login. This account becomes the prepared Pool selection when Pool mode is enabled.`,"codexAuth.prepareForPool":`Prepare for Pool`,"codexAuth.poolPreparedToast":`{email} is prepared for Pool mode`,"codexAuth.switchTitle":`Switch active account?`,"codexAuth.switchDesc":`Takes effect immediately. Existing account-affine threads and requests already in flight keep their captured account; new or unbound requests use the selected account's order tier, and accounts at the same selection order still take turns.`,"codexAuth.cacheWarning":`Prompt cache resets on account switch. New session starts with empty cache.`,"codexAuth.setAsNext":`Use this account next`,"codexAuth.cancel":`Cancel`,"codexAuth.switchBack":`Switch back to Main?`,"codexAuth.switchBackDesc":`Takes effect immediately. Existing account-affine threads and requests already in flight keep their captured account; new or unbound requests use your App login account's order tier, and accounts at the same selection order still take turns.`,"codexAuth.autoSwitch":`Usage-based proactive switching`,"codexAuth.autoSwitchQuotaDesc":`Quota: at {threshold}% usage or above, the next request may move to a lower-usage eligible account, including an already-bound task; Go/Free use 30d only.`,"codexAuth.autoSwitchQuotaOffDesc":`Usage-based proactive switching is off. New/unbound assignment and failure recovery still apply.`,"codexAuth.autoSwitchRoundRobinDesc":`Round-robin assignment does not use this threshold; it continues to rotate new/unbound tasks.`,"codexAuth.autoSwitchFillFirstDesc":`Fill-first: {threshold}% is the drain point for new/unbound tasks; healthy bound tasks keep their account.`,"codexAuth.autoSwitchFillFirstOffDesc":`Fill-first has no usage drain point for new/unbound tasks; cooldown, reauthentication, and failure recovery can still move routing.`,"codexAuth.failureRecoveryNote":`Failure recovery is separate: a request rejected before output with 429/402, cooldown, reauthentication, exclusion, or configured transient failover may select another eligible account.`,"codexAuth.autoSwitchThreshold":`Usage threshold`,"codexAuth.autoSwitchThresholdAria":`Usage threshold, percent`,"codexAuth.autoSwitchThresholdInc":`Increase usage threshold`,"codexAuth.autoSwitchThresholdDec":`Decrease usage threshold`,"codexAuth.autoSwitchLoadFailed":`Usage-based switching setting could not be loaded.`,"codexAuth.autoSwitchThresholdInvalid":`Enter a whole number from 1 to 100`,"codexAuth.autoSwitchUpdated":`Usage-based proactive switching updated`,"codexAuth.autoSwitchUpdateFailed":`The usage-based switching update could not be confirmed. The last confirmed value is shown.`,"codexAuth.requestUserInput":`Ask for input in Default mode`,"codexAuth.requestUserInputDesc":`Lets Codex pause a Default-mode session and ask you questions with the request_user_input tool.`,"codexAuth.requestUserInputUpdated":`Feature flag updated - applies to new sessions.`,"codexAuth.requestUserInputUpdatedRestart":`Feature flag updated - applies to new sessions. Restart the Codex app to pick it up.`,"codexAuth.requestUserInputUpdateFailed":`Could not update the feature flag. Nothing was changed.`,"codexAuth.requestUserInputLoadFailed":`Could not read the feature flag from config.toml.`,"codexAuth.accountPickerTitle":`Target a specific Codex account from the model picker`,"codexAuth.accountPickerOffDesc":`When enabled, ordinary GPT picker rows are replaced by one entry per account selector, so you can choose the exact account for a conversation without logging out. Turning it off removes no accounts.`,"codexAuth.accountPickerOnDesc":`Each selector is a public label for one stored account. Choosing it locks that conversation to the mapped account: it never rotates or falls back, and it does not change the active Pool account.`,"codexAuth.accountPickerCompatibility":`The built-in Codex App login has its own selector; generated maps normally call it main and use a collision-safe suffix such as main-2 when needed. Added accounts receive stable privacy-safe labels, while custom selector labels stay unchanged. Existing conversations and saved model selections continue routing. Turning this off hides generated entries but preserves selectors and exact routes. Plain GPT model IDs keep their Pool or Direct behavior.`,"codexAuth.accountPickerUpdated":`Account targeting updated.`,"codexAuth.accountPickerUpdateFailed":`Could not update account targeting. The last confirmed setting is shown.`,"codexAuth.accountPickerLoadFailed":`Could not load the account-targeting setting.`,"codexAuth.accountPickerRefreshFailed":`Could not refresh this setting. The last confirmed value is still shown.`,"codexAuth.advancedSettings":`Advanced settings`,"codexAuth.advancedSettingsAria":`Show or hide advanced Codex Auth settings`,"codexAuth.catalogRefreshPending":`The change was saved, but the Codex model catalog refresh is pending. Run ocx sync to retry.`,"anthropicPool.title":`Claude account pool (experimental)`,"anthropicPool.enabledDesc":`On 429, cools the account and fails over. New sessions prefer usage under {threshold}% ({window}).`,"anthropicPool.enabledNoProactiveDesc":`On 429, cools the account and fails over. Proactive usage-based switching is off at threshold 0, but new-session selection and 429 recovery still use the {window} window.`,"anthropicPool.disabledDesc":`Uses only the active Claude account. Enable only if you accept experimental routing.`,"anthropicPool.experimentalWarning":`Experimental and not battle-tested. Anthropic may restrict accounts that look like automated multi-account rotation. Same organization can share quota — pooling those accounts will not help. Keep this off unless you understand the risk.`,"anthropicPool.needTwoAccounts":`Add at least two Claude OAuth accounts before enabling the pool.`,"anthropicPool.threshold":`New-session usage threshold`,"anthropicPool.thresholdAria":`New-session usage threshold, percent`,"anthropicPool.thresholdHelp":`0 disables quota-based picking (affinity + active account only). Default 80.`,"anthropicPool.thresholdInvalid":`Enter a whole number from 0 to 100`,"anthropicPool.loadFailed":`Claude pool settings could not be loaded.`,"anthropicPool.saveFailed":`Claude pool settings could not be saved.`,"anthropicPool.on":`On`,"anthropicPool.off":`Off`,"accountPool.strategy":`Rotation strategy`,"accountPool.strategyDesc":`How OpenCodex assigns an account to a new/unbound task.`,"accountPool.strategyQuota":`Quota`,"accountPool.strategyRoundRobin":`Round-robin`,"accountPool.strategyFillFirst":`Fill-first`,"accountPool.strategyHintQuota":`Quota can also rebind an existing task on its next request after the usage threshold is crossed.`,"accountPool.strategyHintRoundRobin":`Round-robin rotates only tasks without a live binding; the usage threshold does not change normal rotation.`,"accountPool.strategyHintFillFirst":`Fill-first uses the threshold as a drain point for unbound tasks; healthy bound tasks keep affinity.`,"accountPool.unboundDefinition":`New/unbound task means a request with no current account binding; an existing visible task can become unbound after a proxy or affinity reset.`,"accountPool.stickyLimit":`New/unbound assignments before rotate`,"accountPool.stickyLimitAria":`New/unbound assignments before rotate`,"accountPool.stickyLimitInc":`Increase sticky limit`,"accountPool.stickyLimitDec":`Decrease sticky limit`,"accountPool.stickyLimitHelp":`Keep the selected account for this many new/unbound task assignments before advancing; the counter increments when the task is bound, not after upstream success.`,"accountPool.stickyLimitInvalid":`Enter a whole number from 1 to 100`,"accountPool.strategyLoadFailed":`Rotation strategy could not be loaded.`,"accountPool.strategyUpdateFailed":`Rotation strategy could not be saved.`,"accountPool.quotaWindow":`Quota window`,"accountPool.quotaWindowDesc":`Which cached usage bar controls quota-based new-session selection, fill-first threshold checks, and eligible 429 replacements.`,"accountPool.quotaWindowFiveHour":`5-hour bar`,"accountPool.quotaWindowWeekly":`Weekly bar`,"accountPool.quotaWindowMaxUtilization":`Higher bar`,"accountPool.quotaWindowHint":`Weekly skips accounts whose 5-hour bar is exhausted while another eligible account remains, but falls back to them when none do. Weekly ties prefer lower 5-hour usage; per-account weekly bars are only known once the Providers page has polled them.`,"accountPool.quotaWindowInert":`Only quota — or fill-first above a 0 threshold — scores a usage bar, so this setting changes nothing for the current rotation strategy.`,"accountPool.priority":`Selection order`,"accountPool.priorityAria":`Selection order for this account`,"accountPool.priorityHint":`Higher numbers are used first. The pool moves to a lower number only when every account above it is drained or unavailable.`,"accountPool.priorityFirst":`First`,"accountPool.priorityEarlier":`Earlier`,"accountPool.priorityNormal":`Normal`,"accountPool.priorityLater":`Later`,"accountPool.priorityLast":`Last`,"accountPool.priorityOption":`{name} ({value})`,"accountPool.priorityCustom":`Custom`,"accountPool.priorityUpdated":`Selection order updated for {email}`,"accountPool.priorityUpdateFailed":`Selection order for {email} could not be saved. The last confirmed value is shown.`,"codexAuth.switched":`{email} is selected for the next request`,"codexAuth.loadFailed":`Codex account settings could not be loaded.`,"codexAuth.switchFailed":`The account could not be switched. Your previous selection is unchanged.`,"codexAuth.removeConfirm":`Remove {id}?`,"codexAuth.removeFailed":`The account could not be removed. Nothing was changed.`,"codexAuth.addTitle":`Add Codex Account`,"codexAuth.addIdLabel":`Account ID (slug)`,"codexAuth.addIdPlaceholder":`codex-work, codex-alt, team...`,"codexAuth.resetCreditsAria":`{count} reset credit(s)`,"codexAuth.addJsonLabel":`auth.json content`,"codexAuth.addHelp":`Copy from another machine's ~/.codex/auth.json, or use codex-auth export.`,"codexAuth.importBtn":`Import`,"codexAuth.importInvalidJson":`Invalid JSON`,"codexAuth.importMissingTokens":`Missing access_token or refresh_token in JSON`,"codexAuth.importMissingId":`Account ID is required`,"codexAuth.accountAdded":`Account added to pool`,"codexAuth.addPickDesc":`Login with another ChatGPT account to add it to the pool.`,"codexAuth.oauthLogin":`OAuth Login`,"codexAuth.oauthDesc":`Opens ChatGPT login in browser`,"codexAuth.deviceLogin":`Device code login`,"codexAuth.deviceDesc":`For a headless or remote proxy: enter a short code on another device`,"codexAuth.importAuthJson":`Import auth.json`,"codexAuth.importAuthJsonDesc":`From another Codex install or codex-auth export`,"codexAuth.back":`Back`,"codexAuth.oauthAlreadyInProgress":`Login already in progress. Complete it in your browser.`,"codexAuth.oauthWaiting":`Waiting for ChatGPT login to complete in your browser...`,"codexAuth.oauthSubmittingCode":`Submitting code…`,"codexAuth.oauthCodeSubmitted":`Code submitted — waiting for login to finish…`,"codexAuth.oauthStatusRetrying":`Network or proxy error while checking login status — retrying…`,"codexAuth.oauthCancelled":`Login was cancelled.`,"codexAuth.loginFailed":`Login failed`,"codexAuth.needsReauth":`Re-login`,"codexAuth.reauthenticate":`Re-authenticate`,"codexAuth.tokenExpired":`Token expired — re-authenticate this account`,"codexAuth.mainTokenExpired":`Token expired — sign in again via Codex App login`,"codexAuth.emailCollision":`This account matches your main Codex login. Use a different account.`,"codexAuth.resetCreditsTitle":`Reset Credits`,"codexAuth.resetCreditsAvailable":`You have {count} reset credit(s) available.`,"codexAuth.resetCreditsDesc":`Each credit resets your current hourly and weekly usage limits instantly.`,"codexAuth.noResetCredits":`You don't have any reset credits.`,"codexAuth.earnCreditsHint":`Credits are earned monthly and via the referral program.`,"codexAuth.creditsExpireNote":`Credits expire 30 days after earning.`,"codexAuth.useOneCredit":`Use 1 Credit`,"codexAuth.confirmResetTitle":`Use Reset Credit?`,"codexAuth.confirmResetDesc":`This will instantly reset your current rate limits. You have {count} credit(s) remaining.`,"codexAuth.irreversible":`This action cannot be undone.`,"codexAuth.useCredit":`Use Credit`,"codexAuth.redeeming":`Resetting...`,"codexAuth.resetSuccess":`Rate limits reset! {remaining} credit(s) remaining.`,"codexAuth.resetSuccessGeneric":`Rate limits reset!`,"codexAuth.resetAlreadyRedeemed":`This credit was already redeemed. Credits unchanged.`,"codexAuth.resetNothingToReset":`No rate-limit window needs resetting right now.`,"codexAuth.resetNoCredit":`No reset credits available.`,"codexAuth.resetError":`Failed to redeem reset credit. Please try again.`,"codexAuth.fifoNote":`The oldest credit is used first.`,"codexAuth.confirmWhichCredit":`Credit from {date} will be used.`,"codexAuth.creditNext":`Next to use`,"codexAuth.creditLabel":`Credit #{n}`,"codexAuth.creditNextBadge":`NEXT`,"codexAuth.creditGranted":`Granted {date}`,"codexAuth.creditExpires":`Expires {date} ({days}d left)`,"api.title":`API Access`,"api.subtitle":`Use generated API keys to access the opencodex proxy from external apps. Keys authenticate via the {authHeader} header; see the table below for what each endpoint accepts.`,"api.baseUrl":`Base URL`,"api.responsesEndpoint":`Responses API`,"api.chatCompletionsEndpoint":`Chat Completions API`,"api.messagesEndpoint":`Messages API`,"api.modelsEndpoint":`Models API`,"api.endpointNote":`Use the base URL with OpenAI-compatible clients. Responses and Chat Completions are exposed under /v1.`,"api.endpointsTitle":`Endpoints`,"api.authTitle":`Authentication`,"api.authLoopback":`Loopback binds (127.0.0.1 or ::1) bypass authentication. Remote binds require a generated ocx_ key or OPENCODEX_API_AUTH_TOKEN.`,"api.authBaseUrlNote":`Configure clients with the base URL, then choose the protocol-specific endpoint below.`,"api.newKeyTitle":`New key created`,"api.newKeyNote":`Copy this key now — it won't be shown again.`,"api.copy":`Copy`,"api.copied":`Copied`,"api.dismiss":`Dismiss`,"api.generateTitle":`Generate key`,"api.keyNamePlaceholder":`Key name (optional)`,"api.generate":`Generate`,"api.generating":`Creating…`,"api.activeKeys":`Active keys ({count})`,"api.activeKeysLoading":`Active keys`,"api.noKeys":`No API keys yet. Generate one above.`,"api.workspace.sections":`API sections`,"api.section.keys":`Keys`,"api.section.connect":`Connect`,"api.section.endpoints":`Endpoints`,"api.section.models":`Models`,"api.section.examples":`Examples`,"api.workspace.details":`API key details`,"api.workspace.keyDetails":`Key details`,"api.workspace.keyPrefix":`Key prefix`,"api.workspace.deleteKey":`Delete key`,"api.workspace.deleteConfirm":`Are you sure you want to delete this key? This cannot be undone.`,"api.workspace.usageExamples":`Usage examples`,"api.copyUrlHint":`Click to copy URL`,"api.urlCopied":`URL copied`,"api.copyExampleHint":`Click to copy example`,"api.exampleCopied":`Example copied`,"api.colName":`Name`,"api.colKey":`Key`,"api.colCreated":`Created`,"api.confirm":`Confirm`,"api.deleteAria":`Delete API key`,"api.modelsTitle":`External model catalog`,"api.modelsCount":`{count} callable`,"api.modelsLoading":`Loading models…`,"api.modelsSearch":`Search models`,"api.modelsSubtitle":`Use these exact model IDs with /v1/models and your chosen inbound protocol.`,"api.modelsEmpty":`No externally callable models are available yet.`,"api.modelsNoMatch":`No models match “{query}”.`,"api.modelsLoadFailed":`Could not load the external model catalog.`,"api.colModel":`Model`,"api.colSource":`Source`,"api.colProtocols":`Protocols`,"api.sourceNative":`ChatGPT pool`,"api.sourceCombo":`Combo route`,"api.sourceCustom":`Custom`,"api.protocolResponses":`Responses`,"api.protocolChatCompletions":`Chat Completions`,"api.protocolMessages":`Messages`,"api.copyModelId":`Copy ID`,"api.modelCopied":`Copied`,"api.testModel":`Test`,"api.testingModel":`Testing…`,"api.testSucceeded":`OK`,"api.testFailed":`Failed`,"api.usageChatTitle":`Chat Completions example`,"api.usageResponsesTitle":`Responses example`,"api.usageMessagesTitle":`Messages example`,"api.usageSampleInput":`Hello, world!`,"api.clientConfig.title":`Client config`,"api.clientConfig.rowsLabel":`Connect a client`,"api.clientConfig.details":`Details`,"api.clientConfig.detailsAria":`{client} config details`,"api.clientConfig.copyAria":`Copy {client} config`,"api.clientConfig.downloadAria":`Download {client} config`,"api.clientConfig.rowMeta":`{destination} · {count} model(s)`,"api.clientConfig.rowError":`Could not build the {client} config.`,"api.clientConfig.copiedAnnounceClient":`{client} config copied to the clipboard.`,"api.clientConfig.clientOpencode":`OpenCode`,"api.clientConfig.clientPi":`Pi`,"api.clientConfig.clientOmp":`OMP`,"api.clientConfig.clientHermes":`Hermes`,"api.clientConfig.clientOpenclaw":`OpenClaw`,"api.clientConfig.clientKimi":`Kimi Code`,"api.clientConfig.clientGajae":`Gajae Code`,"api.clientConfig.clientDsh":`DeepSeek Harness (DSH)`,"api.clientConfig.clientMcode":`MiniMax Code`,"api.clientConfig.clientZcode":`ZCode`,"api.clientConfig.clientPrime":`Prime Agent`,"api.clientConfig.clientAside":`Aside`,"api.clientConfig.copy":`Copy config`,"api.clientConfig.download":`Download`,"api.clientConfig.loading":`Building client config…`,"api.clientConfig.jsonLabel":`{client} config`,"api.clientConfig.destination":`Destination file`,"api.clientConfig.envHint":`Set the key before launching`,"api.clientConfig.mergeWarning":`Merge this into the destination file. Replacing it would drop your other providers and MCP settings.`,"api.clientConfig.modelCount":`{count} model(s) exported`,"api.clientConfig.missingLimits":`{count} of {total} model(s) ship without a context limit; the client applies its own defaults.`,"api.clientConfig.noKeyYet":`{env} has no key behind it yet. Generate a key above before using this config off loopback.`,"api.clientConfig.loadFailed":`Could not read the model list, so no client config was produced.`,"api.clientConfig.copiedAnnounce":`Client config copied to the clipboard.`,"api.clientConfig.copyFailed":`Could not copy the client config.`,"api.clientConfig.downloadedAnnounce":`Downloaded {filename}. Nothing changed yet — merge it into {destination} yourself.`,"api.clientConfig.whereDisclosure":`Where this file goes`,"api.clientConfig.whereBody":`The destination above is the global path. A project-local config file in the working directory takes precedence over it, and the client reads the key from the environment variable named in the config — never from this file.`,"api.keysLoadFailed":`Could not load API keys.`,"api.createFailed":`Could not create API key.`,"api.deleteFailed":`Could not delete API key.`,"api.auth.endpoint":`Endpoint`,"api.auth.required":`Required`,"api.auth.accepted":`Accepted`,"api.auth.rejected":`Not accepted`,"api.auth.testProtocol":`Test {protocol}`,"api.auth.testNeedsFreshKey":`Generate a key and keep its one-time value on screen to run an authenticated test.`,"api.key.name":`Key name`,"api.key.rename":`Rename`,"api.key.saveName":`Save name`,"api.key.renaming":`Saving…`,"api.key.renameFailed":`Could not rename the key. Your draft was kept.`,"api.key.deleting":`Deleting…`,"api.rotation.title":`Key rotation`,"api.rotation.description":`Issue a replacement key while the current key remains valid for a short overlap.`,"api.rotation.start":`Start rotation`,"api.rotation.starting":`Starting…`,"api.rotation.pending":`Rotation is pending. Update and verify the client before committing.`,"api.rotation.expires":`Overlap expires:`,"api.rotation.secretOnce":`Replacement key — shown once. Copy it before closing this notice.`,"api.rotation.commit":`Commit rotation`,"api.rotation.abort":`Abort rotation`,"api.rotation.failed":`The rotation action did not complete. Refresh before retrying.`,"api.rotation.startFailed":`Could not start key rotation.`,"api.key.copyFailed":`Could not copy the key. Select it and copy it manually before dismissing this panel.`,"api.attribution.title":`Attributed usage`,"api.attribution.requests7d":`Requests, last 7 days`,"api.attribution.totalRequests":`Total attributed requests`,"api.attribution.totalRequestsAvailable":`Requests in available history`,"api.attribution.sinceAvailable":`Available attribution since`,"api.attribution.lastUsed":`Last used`,"api.attribution.since":`Attribution available since`,"api.attribution.neverUsed":`Not used since attribution began`,"api.attribution.unavailable":`Usage unavailable`,"api.attribution.unavailableDetail":`No usage has been attributed yet. Requests recorded before attribution began cannot be assigned retroactively.`,"api.attribution.ambiguous":`Two keys share this ID, so usage cannot be attributed to one of them. Give each key a unique ID in the config file.`,"api.attribution.railAmbiguous":`duplicate ID`,"claude.subtitle":`Use GPT, Gemini, and other models inside Claude Code.`,"claude.pageTitle":`Claude Code`,"claude.workspace.settings":`Settings`,"claude.enabledLabel":`Claude connection`,"claude.enabledHint":`When off, Claude Code cannot use this proxy.`,"claude.authMode":`Auth Mode`,"claude.authModeHint":`Subscription requires Claude account, Proxy works without Anthropic account`,"claude.authModeSubscription":`Subscription (Claude account)`,"claude.authModeProxy":`Proxy (no account needed)`,"claude.authModeAuto":`Auto (detect Claude auth)`,"claude.effectiveMode.label":`Effective on next launch`,"claude.effectiveMode.manual":`Manual: {mode}`,"claude.effectiveMode.autoPresent":`Auto: subscription — Claude auth found via {source}`,"claude.effectiveMode.autoAbsent":`Auto: proxy mode — no Claude auth found`,"claude.effectiveMode.autoUnknown":`Auto: subscription — auth could not be verified`,"claude.effectiveMode.admissionKey":`This proxy's API key is still sent.`,"claude.authSource.claude-json-oauth":`Claude account`,"claude.authSource.claude-credentials-file":`credentials file`,"claude.authSource.macos-keychain":`macOS Keychain`,"claude.authSource.exported-env":`environment variable`,"claude.authSource.unknown":`a detected credential`,"claude.systemEnv":`Auto-connect`,"claude.systemEnvDesc":`When on, running claude in any terminal automatically goes through the proxy.`,"claude.systemEnvUnsupported":`Auto-connect is available on macOS only. On this system, start Claude with {cmd}.`,"claude.systemEnvWarn":`⚠ You must fully quit and relaunch your terminal app for this to take effect. Not recommended.`,"claude.fastMode":`Fast Mode (OpenAI)`,"claude.fastModeDesc":`Controls service_tier for OpenAI models. ON = priority (faster). OFF = default. Auto = passthrough (client decides).`,"claude.fastAuto":`Auto`,"claude.fastOn":`ON`,"claude.fastOff":`OFF`,"claude.autoContext":`Use big context automatically`,"claude.autoContextDesc":`Controls how far the 1M marking goes. ON: any model whose window can host the compaction threshold gets a big-context row. OFF: only true 1M models get one.`,"claude.autoContextInert":`Inactive because a legacy context-size value (maxContextTokens) exists in the config file. Remove it there to re-enable.`,"claude.autoCompactWindow":`Auto-summarize point`,"claude.autoCompactDefault":`{value} (default)`,"claude.autoCompactWindowDesc":`Older messages are summarized when the chat reaches this point. It never exceeds each model's own limit, so 200k models are unaffected.`,"claude.autoCompactWindowWarn":`Changing this can break GPT models — set higher than a model's real limit, chats will error before the summary kicks in.`,"claude.injectAgents":`Auto-register subagents`,"claude.injectAgentsDesc":`Registers the models picked on the Subagents tab (plus the current default model) as dispatchable Claude Code agents (ocx-*). Applies from the next session.`,"claude.webSearchSidecar":`Web search sidecar override`,"claude.webSearchSidecarHint":`Override the main web search sidecar for Claude Code requests.`,"claude.visionSidecar":`Vision sidecar override`,"claude.visionSidecarHint":`Override the main vision sidecar for Claude Code requests.`,"claude.useMainSetting":`Use main setting`,"claude.sidecarModelPlaceholder":`Main setting model`,"claude.quickstart":`Get started`,"claude.quickstartHint":`{cmd} opens Claude Code through the proxy. Your claude.ai login stays active.`,"claude.manualEnv":`Manual setup (advanced)`,"claude.smallFastModel":`Background helper model`,"claude.smallFastModelHint":`The model Claude Code uses for background work like chat summaries and topic detection. The haiku subagent alias uses it too. Empty = Claude default (Haiku).`,"claude.smallFastModelAccurateHint":`The model Claude Code uses for background work such as chat summaries and topic detection. The haiku subagent alias uses it too.`,"claude.smallFastModelUnsetOption":`Let Claude Code choose (native model)`,"claude.smallFastModelNativeWarning":`When unset, OpenCodex leaves the helper-model overrides unset. Claude Code may use its native Sonnet model, which may incur charges from your native provider.`,"claude.slotUnset":`Use Claude default`,"claude.modelMap":`Model interception`,"claude.modelMapHint":`Intercepts requests for a specific model and reroutes them to the one you pick. Empty by default — nothing happens until you add a rule.`,"claude.mapFrom":`Original model (e.g. claude-sonnet-4-5)`,"claude.mapTo":`Swap to (e.g. gemini/gemini-3-pro)`,"claude.addMapping":`Add rule`,"claude.removeMapping":`Remove rule`,"claude.aliases":`Available models`,"claude.aliasesHint":`Models that appear in Claude Code's /model menu.`,"claude.aliasProviderOther":`Other`,"claude.loading":`Loading…`,"claude.loadFail":`Failed to load Claude settings`,"claude.saved":`Saved.`,"claude.saveFailed":`Save failed`,"claude.networkError":`Network error — is the proxy running?`,"claude.toggleAria":`Toggle Claude connection`,"claude.none":`None`,"cws.loading":`Loading combos…`,"cws.loadFailed":`Could not load combos.`,"cws.saveFailed":`Could not save combo.`,"cws.removeFailed":`Could not remove combo.`,"cws.saved":`Combo saved.`,"cws.created":`Created {model}.`,"cws.removed":`Removed combo/{id}.`,"cws.renamed":`Renamed {from} to {to}.`,"cws.add":`Add combo`,"cws.addTitle":`Add combo`,"cws.addSubtitle":`Create a virtual model across providers and choose the exact model name clients will request.`,"cws.create":`Create combo`,"cws.railAria":`Combo list`,"cws.searchPlaceholder":`Search combos or targets…`,"cws.noSearchResults":`No combos match your search.`,"cws.group.failover":`Failover`,"cws.group.roundRobin":`Round-robin`,"cws.group.other":`Other strategies`,"cws.targetCount":`{count} targets`,"cws.targetCountOne":`1 target`,"cws.overviewTitle":`Combos`,"cws.overviewBlurb":`Virtual models that route across provider/model targets with failover, round-robin, weighted random, least-used, or soonest quota reset.`,"cws.count.total":`Total`,"cws.count.failover":`Failover`,"cws.count.roundRobin":`Round-robin`,"cws.count.other":`Other`,"cws.howTitle":`How it works`,"cws.howBody":`Ask Codex for the combo's public model name. Without one, the default is combo/. OpenCodex selects a target and hops only on retryable upstream failures. If no target remains available, the request fails closed instead of using the global default provider.`,"cws.attentionTitle":`Needs attention`,"cws.attention.empty":`No targets configured`,"cws.attention.few":`Only one target — failover has nowhere to hop`,"cws.attention.catalogOmitted":`Missing from the model catalog — member capabilities are incomplete or incompatible (missing context window / metadata, or empty modality intersection). Routing by alias still works`,"cws.attention.allTargetsExhausted":`All enabled targets are out of quota`,"cws.emptyTitle":`Create your first combo`,"cws.empty.createDesc":`Name a virtual model and chain two or more backends.`,"cws.backToAll":`Back to all combos`,"cws.allCombos":`All combos`,"cws.copyModel":`Copy id`,"cws.copied":`Copied`,"cws.tabsLabel":`Combo detail sections`,"cws.tab.config":`Config`,"cws.tab.about":`About`,"cws.strategy":`Strategy`,"cws.strategy.failover":`Failover`,"cws.strategy.roundRobin":`Round-robin`,"cws.strategy.random":`Random`,"cws.strategy.leastUsed":`Least-used`,"cws.strategy.resetWindow":`Reset-window`,"cws.strategy.failoverHint":`Try targets in order. If the first fails with a retryable error (rate limit, outage, subscription gate), hop to the next.`,"cws.strategy.roundRobinHint":`Deterministically balance traffic by weight. Keep each selected target for a batch of successful requests, then advance.`,"cws.strategy.randomHint":`Draw one eligible target per request, with odds proportional to weight. No stickiness between requests.`,"cws.strategy.leastUsedHint":`Route each request to the eligible target with the fewest recorded successes. Counts restart with the proxy.`,"cws.strategy.resetWindowHint":`Prefer the eligible target whose quota window resets soonest. Falls back to configuration order when quota data is missing.`,"cws.field.id":`Combo id`,"cws.field.idHint":`Clients will request {model}`,"cws.field.idInternalHint":`Internal combo id. You can change it after creation.`,"cws.field.idHintEdit":`Renaming moves the combo to a new id. Clients request {model}.`,"cws.field.alias":`Public model name`,"cws.field.aliasPlaceholder":`deepseek-v4-flash or vendor/model`,"cws.field.aliasHint":`Optional. Use a bare name with no prefix, a custom prefix like vendor/model, or leave blank to use combo/.`,"cws.field.nativeAlias":`Native OpenAI alias`,"cws.field.nativeAliasHint":`Let this combo own a supported unqualified native OpenAI model id. Account- and provider-qualified OpenAI routes stay separate.`,"cws.field.displayName":`Display name`,"cws.field.displayNameHint":`Picker label for this combo. Required when Native OpenAI alias is enabled.`,"cws.field.stickyLimit":`Sticky successes before rotate`,"cws.field.stickyLimitHint":`Retain the selected target for this many successful requests before the weighted selector advances.`,"cws.field.defaultEffort":`Default reasoning`,"cws.field.defaultEffortNone":`None (target default)`,"cws.field.defaultEffortHint":`Used only when the client omits reasoning effort. Options are the intersection of the selected targets' advertised efforts; targets without catalog effort metadata offer none.`,"cws.capability.imageInputUnavailable":`Unavailable until every selected target supports image input.`,"cws.capability.imageInputHint":`On by default when every target supports images. Turn off to accept text only.`,"cws.capability.imageInput":`Image / multimodal`,"cws.capability.adaptiveEffort":`Adaptive reasoning ladder`,"cws.capability.adaptiveEffortHint":`Off: a target with no reasoning control hides the effort picker for the whole combo. On: those targets stay usable and the picker keeps the levels the remaining targets share.`,"cws.capabilities":`Capabilities`,"cws.field.defaultEffortUnsupported":`This effort is not in the targets' common ladder — it will be ignored or snapped at request time.`,"cws.field.defaultEffortUnsupportedOption":`not in intersection`,"cws.targets":`Targets`,"cws.targets.failoverHint":`Order matters — first is primary.`,"cws.targets.roundRobinHint":`Weights control deterministic relative selection; order breaks ties in the rotation ring.`,"cws.targets.randomHint":`Weights control each draw's odds; order does not matter.`,"cws.targets.leastUsedHint":`Order only breaks ties between equally used targets.`,"cws.targets.resetWindowHint":`Order applies when quota data is missing or tied.`,"cws.target.provider":`Provider`,"cws.target.model":`Model`,"cws.target.weight":`Weight`,"cws.target.pickProvider":`Select provider…`,"cws.target.pickProviderFirst":`Select a provider first…`,"cws.target.pickModel":`Select model…`,"cws.target.noModels":`No models for this provider`,"cws.target.modelPlaceholder":`model id`,"cws.target.add":`Add target`,"cws.target.drag":`Drag to reorder`,"cws.target.moveUp":`Move up`,"cws.target.moveDown":`Move down`,"cws.quota.available":`Available`,"cws.quota.exhausted":`Out of quota`,"cws.quota.unknown":`Quota unknown`,"cws.quota.allExhausted":`All enabled targets are out of quota. Choose another target or wait for quota recovery.`,"cws.aboutTitle":`Runtime`,"cws.aboutBody":`Failed targets cool down briefly, honoring Retry-After. Invalid or context errors do not hop. Each target adapts effort to its own capabilities; exhausted combos fail closed. Logs and Usage retain ordered physical attempts and per-attempt usage.`,"cws.removeConfirmTitle":`Remove {model}?`,"cws.removeConfirmDesc":`This removes the virtual model from config and the Codex catalog. It does not delete any providers.`,"cws.unsavedTitle":`Unsaved changes`,"cws.unsavedDesc":`Discard edits to this combo and continue?`,"cws.keepEditing":`Keep editing`,"cws.err.missingId":`Combo id is required.`,"cws.err.invalidId":`Id must start with a letter or number and use only letters, numbers, dots, underscores, or hyphens (max 64).`,"cws.err.duplicateId":`A combo with this id already exists.`,"cws.err.invalidAlias":`Alias must use letters, numbers, dots, underscores, or hyphens, with at most one "/" segment.`,"cws.err.aliasReservedNamespace":`The alias must not use the reserved "combo/" namespace.`,"cws.err.aliasNativeFamily":`Bare aliases in the OpenAI native family (gpt-*, o1-*, o3-*, o4-*, codex-*) are not allowed.`,"cws.err.unsupportedNativeAlias":`Native alias must be a currently supported bare OpenAI model id.`,"cws.err.missingNativeAliasDisplayName":`A display name is required for native aliases.`,"cws.err.invalidDisplayName":`Display name must be at most 128 characters and contain no control characters.`,"cws.err.duplicateAlias":`Another combo already uses this alias.`,"cws.err.noTargets":`Add at least one target.`,"cws.err.incompleteTarget":`Each target needs a provider and model.`,"cws.target.disabled":`{name} (disabled)`,"cws.err.reservedNamespace":`A physical provider named combo must be renamed before creating combos.`,"cws.err.providerCollision":`The combo ID conflicts with a configured provider name.`,"cws.err.unknownProvider":`Each target must use a configured provider.`,"cws.err.duplicateTarget":`The same provider/model target can appear only once.`,"cws.err.invalidStickyLimit":`Sticky successes must be an integer from 1 to 100.`,"cws.err.invalidWeight":`Each round-robin weight must be an integer from 1 to 10000.`,"cws.err.noEnabledTarget":`At least one target must use an enabled provider.`,"claude.tabsLabel":`Claude client`,"claude.tabCode":`Code`,"claude.tabDesktop":`Desktop`,"claudeDesktop.title":`Claude Desktop`,"claudeDesktop.subtitle":`Route each Claude model family through an available model on port {port}.`,"claudeDesktop.importJson":`Import JSON`,"claudeDesktop.exportJson":`Export JSON`,"claudeDesktop.loading":`Loading Claude Desktop profile…`,"claudeDesktop.loadFail":`Failed to load Claude Desktop profile.`,"claudeDesktop.retry":`Retry`,"claudeDesktop.saveFailed":`Failed to save Claude Desktop profile.`,"claudeDesktop.applyFailed":`Profile was saved, but could not be applied.`,"claudeDesktop.updateFailed":`Claude Desktop update failed.`,"claudeDesktop.savedApplied":`Profile saved and applied to Claude Desktop.`,"claudeDesktop.appliedMarkerUnsaved":`Applied to Claude Desktop, but the applied marker was not saved — the saved-vs-applied state below may read stale until you apply again.`,"claudeDesktop.savedAppliedAnnounce":`Claude Desktop profile saved and applied.`,"claudeDesktop.saved":`Profile saved.`,"claudeDesktop.savedAnnounce":`Claude Desktop profile saved.`,"claudeDesktop.exported":`Profile exported as JSON.`,"claudeDesktop.importExpected":`Expected a version 1 Claude Desktop profile.`,"claudeDesktop.importReady":`JSON imported. Review the draft, then save and apply it.`,"claudeDesktop.importedAnnounce":`Profile JSON imported. Unsaved changes are ready for review.`,"claudeDesktop.importInvalid":`The selected file is not a valid profile.`,"claudeDesktop.importFailed":`Import failed. {error}`,"claudeDesktop.moved":`{route} moved to {family}.`,"claudeDesktop.unsaved":`Unsaved changes`,"claudeDesktop.upToDate":`Profile is up to date`,"claudeDesktop.saving":`Saving…`,"claudeDesktop.applying":`Applying…`,"claudeDesktop.saveApply":`Save & apply`,"claudeDesktop.emptyTitle":`No models available`,"claudeDesktop.emptyHint":`Add or enable a provider, then return to assign Claude Desktop routes.`,"claudeDesktop.assignmentsLabel":`Claude model family assignments`,"claudeDesktop.family.opus":`Opus`,"claudeDesktop.family.fable":`Fable`,"claudeDesktop.family.sonnet":`Sonnet`,"claudeDesktop.family.haiku":`Haiku`,"claudeDesktop.modelCountOne":`{count} model`,"claudeDesktop.modelCountMany":`{count} models`,"claudeDesktop.chooseDefault":`Choose a default`,"claudeDesktop.temporaryDefault":`Temporary default`,"claudeDesktop.laneEmpty":`Drop a model here or use its Move control.`,"claudeDesktop.laneNoMatch":`No model in this family matches your search.`,"nav.grok":`Grok`,"grok.title":`Grok Build`,"grok.subtitle":`Models opencodex has registered in your Grok config.`,"grok.loading":`Loading Grok status…`,"grok.loadFail":`Could not read the Grok config.`,"grok.notConfiguredTitle":`Grok Build is not wired up`,"grok.notConfiguredHint":`Start or restart the proxy with Grok installed and opencodex writes a managed block into:`,"grok.endpoint":`Endpoint`,"grok.colModel":`Model`,"grok.colAlias":`Grok alias`,"grok.colContext":`Context`,"grok.groupNative":`Native models`,"grok.groupRouted":`Routed models`,"grok.enabledCount":`{on} of {total} registered`,"grok.saved":`Selection saved.`,"grok.savedApplied":`Selection saved and written to your Grok config.`,"grok.saveFailed":`Could not save the Grok selection.`,"grok.applyFailed":`Selection saved, but the Grok config could not be updated.`,"grok.applySkipped":`Selection saved. The Grok config was not changed.`,"grok.saveApply":`Save & apply`,"grok.saving":`Saving…`,"grok.applying":`Applying…`,"grok.unsaved":`Unsaved changes`,"grok.upToDate":`Selection is up to date`,"grok.toggleModel":`Register {id} with Grok`,"claudeDesktop.available":`Available`,"claudeDesktop.defaultBadge":`Default`,"claudeDesktop.supports1m":`1M`,"claudeDesktop.unavailable":`Unavailable`,"claudeDesktop.contextM":`{n}M context`,"claudeDesktop.contextK":`{n}k context`,"claudeDesktop.contextUnknown":`context unknown`,"claudeDesktop.alias":`Alias`,"claudeDesktop.useAsDefault":`Use as {family} default`,"claudeDesktop.moveTo":`Move to`,"claudeDesktop.move":`Move`,"claudeDesktop.status.applied":`Applied to Desktop`,"claudeDesktop.status.stale":`Config stale — re-apply`,"claudeDesktop.status.notApplied":`Not applied`,"claudeDesktop.status.notActiveProfile":`Desktop is serving another profile — re-apply`,"claudeDesktop.status.disabled":`Claude Desktop integration is off. Fully quit and reopen Desktop after enabling it.`,"claudeDesktop.enableApply":`Enable and apply`,"claudeDesktop.health.lastRequest":`Last request`,"claudeDesktop.health.stats":`{count} req / {errors} err`,"claudeDesktop.effort.supported":`effort`,"claudeDesktop.effort.displayOnly":`effort (display only)`,"lab.title":`Compatibility Lab`,"lab.subtitle":`Read-only compatibility verdict matrix from lab projection evidence.`,"lab.loadFailed":`Could not load compatibility lab data`,"lab.projectionUnavailable":`Lab projection is not available. Run conformance or live probes first.`,"lab.projectionIncompatible":`Lab projection schema is incompatible. Rebuild the projection.`,"lab.statusTitle":`Projection status`,"lab.matrixTitle":`Compatibility matrix`,"lab.verdictsTitle":`Verdict records`,"lab.filter.layer":`Evidence layer`,"lab.filter.verdict":`Verdict`,"lab.filter.subject":`Subject ID`,"lab.filter.all":`All`,"lab.col.subject":`Subject`,"lab.col.layer":`Layer`,"lab.col.suite":`Suite`,"lab.col.verdict":`Verdict`,"lab.col.asOf":`As of`,"lab.col.protocol":`Protocol conformance`,"lab.col.live":`Live route compatibility`,"lab.col.task":`Task effectiveness`,"lab.empty":`No compatibility verdicts in the projection yet.`,"lab.subjectKind":`Kind`,"lab.observationCount":`Observations`,"lab.eventCount":`Events`,"lab.verdictCount":`Verdicts`,"lab.subjectCount":`Subjects`,"lab.builtAt":`Built`,"lab.loading":`Loading compatibility evidence…`,"lab.loadMore":`Load more`,"lab.detailTitle":`Verdict detail`,"lab.detailClose":`Close`,"lab.detailSubject":`Subject`,"lab.detailObservations":`Observations`,"lab.detailEvents":`Contributing events`,"lab.detailArtifacts":`Artifact metadata`,"lab.production.title":`Observed production traffic`,"lab.production.notVerification":`Not Lab verification`,"lab.production.attempts":`Attempts`,"lab.production.successes":`Successes`,"lab.production.routeErrors":`Route errors`,"lab.production.lastObserved":`Last observed`,"lab.detailLoadFailed":`Could not load verdict detail`,"lab.refresh":`Refresh`,"lab.verdict.UNKNOWN":`Unknown`,"lab.verdict.CLAIMED":`Claimed`,"lab.verdict.PROBED":`Probed`,"lab.verdict.VERIFIED":`Verified`,"lab.verdict.DEGRADED":`Degraded`,"lab.verdict.BLOCKED":`Blocked`,"lab.verdict.UNSUPPORTED":`Unsupported`,"lab.layer.protocol_conformance":`Protocol conformance`,"lab.layer.live_route_compatibility":`Live route compatibility`,"lab.layer.task_effectiveness":`Task effectiveness`,"models.newPolicyGlobal":`New models start disabled`,"models.newPolicyProvider":`New model policy`,"models.newPolicy_inherit":`Inherit`,"models.newPolicy_off":`Off`,"models.newPolicy_on":`On`,"models.newBadge":`NEW`,"models.newCount":`{count} new, off`,"models.aliases":`Aliases`,"models.aliasesTable":`Alias table`,"models.aliasPrompt":`Provider alias (leave empty to clear)`,"models.modelAliasPrompt":`Model alias (leave empty to clear)`,"models.aliasSaved":`Alias saved`,"models.aliasConflict":`That alias conflicts with an existing name`,"models.editProviderAlias":`Edit provider alias`,"models.editModelAlias":`Edit model alias`,"models.useDefaultAliases":`Use default aliases`,"models.useDefaultAliasesGlobal":`Use default aliases globally`,"models.aliasAuto":`auto`,"models.aliasUser":`user`,"models.aliasStale":`stale`,"connection.discovering":`Discovering local and shared targets…`,"connection.machineUnavailable":`The local machine plane is unavailable. Shared requests were not redirected locally.`,"connection.disconnect":`Disconnect from hub`,"connection.disconnectConfirm":`Disconnect this machine from the hub and restart it in standalone mode?`,"connection.pairing.title":`Connect this dashboard to the hub`,"connection.pairing.body":`Paste the one-time pairing code created on the hub.`,"connection.pairing.relayWarning":`This code is exchanged through the fixed hub relay. The relay cannot be redirected to another host.`,"connection.pairing.code":`One-time pairing code`,"connection.pairing.submit":`Connect`,"connection.pairing.submitting":`Connecting…`,"connection.pairing.error":`The pairing code was refused or expired. The code was left in place so you can check it.`,"connection.machine.title":`This machine`,"connection.machine.shimHealthy":`Codex shim is healthy.`,"connection.machine.shimNeedsAttention":`Codex shim needs attention.`,"connection.machine.repairShim":`Repair shim`,"connection.machine.removeShim":`Remove shim`,"connection.clients.title":`Connected clients`,"connection.clients.none":`No client status available`,"connection.clients.sync":`Sync now`,"connection.clients.syncing":`Syncing…`,"connection.sessionLogout":`Log out remote session`,"connection.sessionLoggingOut":`Logging out remote session…`,"connection.sessionLogoutFailed":`Could not log out the remote session. The current session was kept.`,"usage.source.connected":`Source: hub usage`,"usage.source.local":`Source: local usage.jsonl`,"usage.scope.label":`Usage scope`,"usage.scope.machine":`This machine`,"usage.scope.hub":`Hub-wide`,"usage.hubOffline":`Hub usage is unavailable. Local usage was not substituted.`},ze={"nav.dashboard":`Übersicht`,"uptime.day":`T`,"uptime.hour":`Std`,"uptime.minute":`Min`,"uptime.second":`Sek`,"nav.startup":`Startsicherheit`,"nav.providers":`Anbieter`,"nav.models":`Modelle`,"nav.combos":`Combos`,"nav.subagents":`Sub-Agenten`,"routing.title":`Routing-Intelligenz (beta)`,"routing.subtitle":`Policy-Profile, Trockenlauf-Bewertung und routinggestützte Analysen.`,"routing.loadFailed":`Routing-Daten konnten nicht geladen werden`,"routing.empty":"Keine Routing-Profile konfiguriert. Fügen Sie `routingProfiles` zur config.json hinzu.","routing.revision":`rev`,"routing.detail":`Profil`,"routing.createProfile":`Profil erstellen`,"routing.dryRunError":`Trockenlauf fehlgeschlagen (HTTP {status})`,"routing.removeConfirm":`Profil {id} entfernen?`,"routing.unknownEvidence.allow":`zulassen`,"routing.unknownEvidence.penalize":`bestrafen`,"routing.unknownEvidence.exclude":`ausschließen`,"routing.removeCandidate":`Kandidat {provider}/{model} entfernen`,"routing.candidates":`Kandidaten`,"routing.require":`Harte Anforderungen`,"routing.optimize":`Optimierungsgewichte`,"routing.limits":`Grenzen`,"routing.unknownEvidence":`Richtlinie für unbekannte Evidenz`,"routing.compatibility.title":`Kompatibilitätsrichtlinie`,"routing.compatibility.enabled":`Compatibility-Lab-Evidenz erforderlich`,"routing.compatibility.requiredSuites":`Erforderliche Suites`,"routing.compatibility.loadingCatalog":`Lab-Katalog wird geladen…`,"routing.compatibility.catalogUnavailable":`Lab-Katalog nicht verfügbar — Suite-IDs in config.json manuell eintragen.`,"routing.compatibility.layer.protocol_conformance":`Protokollkonformität`,"routing.compatibility.layer.live_route_compatibility":`Live-Route-Kompatibilität`,"routing.compatibility.minStatus":`Mindest-Kompatibilitätsstatus`,"routing.none":`keine`,"routing.unavailable":`–`,"routing.dryRun":`Trockenlauf-Bewertung`,"routing.dryRunContext":`Kontextfenster der Anfrage (Tokens)`,"routing.dryRunTools":`Anfrage benötigt Tools`,"routing.dryRunImage":`Anfrage benötigt Bild-Eingabe`,"routing.dryRunStructured":`Anfrage benötigt strukturierte Ausgabe`,"routing.dryRunRun":`Kandidaten bewerten`,"routing.candidate":`Kandidat`,"routing.eligible":`Geeignet`,"routing.exclusions":`Ausschlüsse`,"routing.costCap":`Kostenobergrenze`,"routing.capOutcome.satisfied":`innerhalb des Limits`,"routing.capOutcome.exceeded":`über dem Limit`,"routing.capOutcome.unknown-allowed":`unbekannt (erlaubt)`,"routing.capOutcome.unknown-excluded":`unbekannt (ausgeschlossen)`,"routing.exclusion.capability-unsatisfied":`Anforderung nicht erfüllt`,"routing.exclusion.unknown-capability":`unbekannte Fähigkeit`,"routing.exclusion.cost-limit":`Kostenobergrenze überschritten`,"routing.exclusion.cost-limit-unknown":`Kosten unbekannt — Obergrenze nicht prüfbar`,"routing.exclusion.cooldown":`Abklingzeit`,"routing.exclusion.unknown-health":`unbekannter Zustand`,"routing.exclusion.unknown-quota":`unbekanntes Kontingent`,"routing.exclusion.unknown-price":`unbekannter Preis`,"routing.exclusion.other":`Ausschluss: {code}`,"routing.score":`Punktzahl`,"routing.selected":`ausgewählt`,"routing.yes":`ja`,"routing.no":`nein`,"routing.analytics":`Routing-Analysen`,"routing.analyticsTotal":`Anfragen`,"routing.analyticsSuccessRate":`Erfolg`,"routing.analyticsFallbackRate":`Fallback`,"routing.analyticsP50":`p50`,"routing.analyticsP95":`p95`,"routing.analyticsP99":`p99`,"routing.analyticsCooldown":`Cooldown-Fehler`,"routing.analyticsConfidence":`Konfidenz`,"routing.analyticsTruncated":`abgeschnittener Verlauf`,"routing.analyticsRequests":`Anfragen`,"routing.analyticsEmpty":`Noch keine Analysen – senden Sie zuerst einige Anfragen.`,"nav.logs":`Protokolle & Diagnose`,"nav.usage":`Nutzung`,"common.github":`GitHub`,"sidebar.star":`Auf GitHub mit Stern markieren`,"sidebar.starred":`Auf GitHub markiert`,"sidebar.starUnauthenticated":`GitHub öffnen, um zu markieren (gh CLI nicht angemeldet)`,"sidebar.starFailed":`Markieren über gh fehlgeschlagen. GitHub wird stattdessen geöffnet.`,"sidebar.updateAvailable":`Update verfügbar: {version}`,"sidebar.checkUpdate":`Nach Updates suchen`,"common.save":`Speichern`,"common.saving":`Speichern…`,"common.cancel":`Abbrechen`,"common.discard":`Verwerfen`,"common.delete":`Löschen`,"common.remove":`Entfernen`,"common.loading":`Lädt…`,"common.retry":`Wiederholen`,"auth.adminTokenTitle":`OpenCodex-Admin-Token (OPENCODEX_ADMIN_AUTH_TOKEN)`,"auth.adminAccountLabel":`Konto`,"auth.adminTokenFieldLabel":`Admin-Token`,"auth.adminTokenRejected":`Der Admin-Token wurde abgelehnt. Prüfen Sie ihn und versuchen Sie es erneut.`,"auth.adminTokenUnavailable":`Der Admin-Token konnte nicht überprüft werden. Versuchen Sie es erneut.`,"theme.label":`Design`,"theme.light":`Hell`,"theme.dark":`Dunkel`,"theme.system":`System`,"lang.label":`Sprache`,"lang.nativeName":`Deutsch`,"provider.name.commandCodeAuth":`Command Code - Auth`,"provider.name.commandCodeApi":`Command Code - API`,"provider.name.volcengine":`Volcengine Ark`,"provider.name.volcengineCodingPlan":`Volcengine Ark Coding-Tarif`,"provider.name.volcengineAgentPlan":`Volcengine Ark Agent-Tarif`,"errorBoundary.title":`Seite konnte nicht geladen werden`,"errorBoundary.message":`In diesem Bereich ist ein Darstellungsfehler aufgetreten. Lade ihn neu, um es noch einmal zu versuchen.`,"errorBoundary.details":`Fehler`,"errorBoundary.reload":`Neu laden`,"startup.title":`Startsicherheit`,"startup.subtitle":`Prüft, ob Codex opencodex nach einem Neustart erreicht, bevor lokales Proxy-Routing in einer Wiederverbindungsschleife endet.`,"startup.refresh":`Aktualisieren`,"startup.backToDashboard":`Zurück zum Dashboard`,"startup.loading":`Startschutz wird geprüft…`,"startup.error":`Startschutz konnte nicht gelesen werden.`,"startup.staleData":`Die aktuelle Prüfung ist fehlgeschlagen. Die Werte unten sind veraltet und kein Nachweis für Schutz.`,"startup.status.native":`Natives Routing`,"startup.status.protected":`Neustartgeschützt`,"startup.status.atRisk":`Aktion erforderlich`,"startup.summary.native":`Codex ist nicht vom lokalen Proxy abhängig`,"startup.summary.protected":`opencodex ist nach einem Neustart verfügbar`,"startup.summary.atRisk":`Codex kann nach einem Neustart den Modellzugriff verlieren`,"startup.riskDetail":`Codex ist auf den lokalen Proxy festgelegt, aber weder ein dauerhafter Dienst noch ein intakter Launcher-Shim startet ihn erneut.`,"startup.riskDetailCustomLocal":`Codex verwendet ein benutzerdefiniertes lokales Gateway. opencodex kann dessen Neustart-Lebenszyklus weder verwalten noch prüfen.`,"startup.riskDetailWindowsShim":`Der Launcher-Shim schützt unterstützte CLI-Skripte, aber Codex Desktop und direkte codex.exe-Aufrufe können ihn unter Windows umgehen.`,"startup.safeDetail":`Routing und Startmechanismus stimmen überein. Nach einem Neustart sollte kein manuelles ocx start nötig sein.`,"startup.routing":`Codex-Routing`,"startup.routing.proxy":`Lokaler Proxy`,"startup.routing.native":`Natives OpenAI`,"startup.routing.customLocal":`Benutzerdefiniertes lokales Gateway`,"startup.routing.customRemote":`Benutzerdefiniertes Remote-Gateway`,"startup.routing.unknown":`Unbekanntes oder ungültiges Routing`,"startup.restartProtection":`Neustartschutz`,"startup.preference":`Start bei Bedarf`,"startup.enabled":`Aktiviert`,"startup.disabled":`Deaktiviert`,"startup.protection.service":`Hintergrunddienst`,"startup.protection.shim":`Launcher-Shim`,"startup.protection.none":`Nicht installiert`,"startup.details":`Schutzdetails`,"startup.service":`Hintergrunddienst`,"startup.serviceHint":`Startet bei der Anmeldung und startet den Proxy nach einem Absturz neu.`,"startup.installed":`Installiert`,"startup.notInstalled":`Nicht installiert`,"startup.unsupported":`Nicht unterstützt`,"startup.shim":`Codex-Launcher-Shim`,"startup.shimHint":`Führt ocx ensure aus, wenn ein unterstützter Codex-Skript-Launcher startet.`,"startup.healthy":`Intakt`,"startup.cliOnly":`Nur CLI`,"startup.stale":`Veraltet`,"startup.viable":`Einsatzbereit`,"startup.unhealthy":`Installiert, aber fehlerhaft`,"startup.conflict":`Dienstkonflikt`,"startup.installedDisabled":`Installiert, aber deaktiviert`,"startup.install":`Installieren`,"startup.installing":`Wird installiert…`,"startup.repair":`Reparieren`,"startup.repairing":`Wird repariert…`,"startup.serviceInstalled":`Hintergrunddienst wurde erfolgreich installiert.`,"startup.serviceRepaired":`Hintergrunddienst wurde erfolgreich repariert.`,"startup.shimInstalled":`Codex-Launcher-Shim wurde erfolgreich installiert.`,"startup.shimRepaired":`Codex-Launcher-Shim wurde erfolgreich repariert.`,"startup.installFailed":`Installation fehlgeschlagen:`,"startup.tray.title":`Windows-Infobereich`,"startup.tray.hint":`Installiert ein Anmeldesymbol für Proxy-Start, Stopp, Neustart, Dashboard und Status per Klick.`,"startup.tray.login":`Infobereich bei Windows-Anmeldung starten`,"startup.tray.notProtection":`Das Symbol ist nur eine Steuerung, kein Neustartschutz. Für unbeaufsichtigte Wiederherstellung bleibt ein funktionsfähiger Hintergrunddienst nötig.`,"startup.tray.running":`Wird ausgeführt`,"startup.tray.stopped":`Installiert, ausgeblendet`,"startup.tray.stale":`Reparatur erforderlich`,"startup.tray.notInstalled":`Nicht installiert`,"startup.tray.loading":`Wird geprüft…`,"startup.tray.unavailable":`Status nicht verfügbar`,"startup.tray.install":`Installieren und anzeigen`,"startup.tray.start":`Symbol anzeigen`,"startup.tray.stop":`Symbol beenden`,"startup.tray.uninstall":`Anmeldesymbol entfernen`,"startup.tray.error":`Die Windows-Infobereichsaktion ist fehlgeschlagen. Details: ocx tray status.`,"startup.recovery":`Reparaturoptionen`,"startup.recoveryHint":`Nutze die Ein-Klick-Installation oben oder kopiere einen Befehl für die manuelle Reparatur. Für Codex Desktop und Windows-Programme wird der Hintergrunddienst empfohlen.`,"startup.command.service":`Empfohlen: dauerhafter Hintergrunddienst`,"startup.command.shim":`Alternative: CLI-Launcher-Shim`,"startup.command.native":`Ausfallsicher: natives Codex-Routing wiederherstellen`,"startup.copy":`Kopieren`,"startup.copied":`Kopiert`,"startup.recommended":`Empfohlene Reparatur: {cmd}`,"startup.navRisk":`Der Startschutz erfordert Aufmerksamkeit`,"startup.codexRuntime.clampHidden":`Einige Reasoning-Effort-Optionen wurden ausgeblendet, weil OpenCodex Codex {version} verwendet hat.`,"startup.codexRuntime.clampHiddenWithEfforts":`Einige Reasoning-Effort-Optionen wurden ausgeblendet, weil OpenCodex Codex {version} verwendet hat (entfernt: {efforts}).`,"startup.codexRuntime.olderBinary":`OpenCodex verwendet eine ältere Codex-Binary ({version}). Eine neuere Installation ist verfügbar.`,"dash.subtitle":`Live-Status des lokalen opencodex-Proxys, seiner Anbieter und der in Codex gerouteten Modelle.`,"dash.workspace.overview":`Übersicht`,"dash.workspace.sections":`Abschnitte`,"dash.status":`Status`,"dash.online":`Online`,"dash.offline":`Offline`,"dash.version":`Version`,"dash.uptime":`Laufzeit`,"dash.providers":`Anbieter`,"dash.tokens30d":`Tokens (30d)`,"dash.coverage":`{pct} Abdeckung`,"dash.mem.title":`Speicherbeobachtung`,"dash.mem.hint":`Schreibgeschützte Laufzeitdiagnose. Beobachteter Speicher ist max(RSS, external, ArrayBuffers), damit Windows-Working-Set-Trimming gebundenen Speicher nicht versteckt.`,"dash.mem.rss":`Resident Set (RSS)`,"dash.mem.jsHeap":`JS-Heap belegt`,"dash.mem.jsHeapArena":`Arena {total}`,"dash.mem.pressure":`Gegen Warnschwelle`,"dash.mem.pressureOf":`{pct}% der Schwelle`,"dash.mem.pressureUnknown":`Keine Schwelle gemeldet`,"dash.mem.jscHeap":`JSC-Heap`,"dash.mem.external":`External`,"dash.mem.arrayBuffers":`ArrayBuffers`,"dash.mem.observed":`Beobachtet`,"dash.mem.runtime":`Laufzeit-Zähler`,"dash.mem.growth":`Beobachtete Drift / Stunde`,"dash.mem.perHour":`/Std`,"dash.mem.store":`Fortsetzungsspeicher`,"dash.mem.storeHint":`Proxy-Cache für previous_response_id. Steigende Gesamtbytes bei steigendem Heap deuten auf Konversationsspeicherung hin, nicht auf den Laufzeit-Allokator.`,"dash.mem.storeEntries":`Einträge`,"dash.mem.storeTotal":`Gesamt`,"dash.mem.storeLargest":`Größter`,"dash.mem.storeOldest":`Ältester`,"dash.mem.threshold":`Warnschwelle`,"dash.mem.lastWarn":`Letzte Warnung`,"dash.mem.never":`Nie`,"dash.mem.details":`Details`,"dash.mem.unavailable":`Speicherdiagnose nicht verfügbar (älterer Proxy).`,"dash.mem.inFlight":`Laufende Anfragen`,"dash.mem.restart":`Abwarten & neu starten`,"dash.mem.restartConfirm":`Auf {count} laufende Anfrage(n) warten, dann neu starten (bis zu {seconds}s; Rest wird bei Timeout abgebrochen).`,"dash.mem.draining":`{count} Anfrage(n) werden abgewartet… Neustart danach`,"dash.mem.reconnecting":`Proxy wird neu gestartet… warte auf Verbindung`,"dash.mem.restartFailed":`Abwarten & Neustart fehlgeschlagen. Prüfen Sie, ob der Proxy läuft.`,"dash.mem.restartNoSupervisor":`Kein Neustartschutz erkannt. Der Proxy bleibt nach dem Neustart möglicherweise aus, bis Sie ihn erneut starten.`,"dash.activeProviders":`Aktive Anbieter`,"dash.noProviders":`Keine Anbieter konfiguriert. Führe {cmd} aus.`,"dash.col.name":`Name`,"dash.col.adapter":`Adapter`,"dash.col.baseUrl":`Basis-URL`,"dash.col.model":`Modell`,"dash.modelsNoResults":`Keine Modelle entsprechen deiner Suche.`,"dash.availableModels":`Verfügbare Modelle`,"dash.noModels":`Keine Modelle gefunden. Prüfe die API-Schlüssel des Anbieters.`,"dash.cannotConnect":`Keine Verbindung zum Proxy. Läuft er?`,"dash.runStart":`Führe {cmd} aus, um den Proxy zu starten.`,"dash.stop":`Proxy stoppen`,"dash.stopConfirm":`Proxy stoppen und natives Codex wiederherstellen?`,"dash.stopFailed":`Proxy konnte nicht gestoppt werden (HTTP {status}).`,"dash.maSwitchFailed":`Moduswechsel fehlgeschlagen (HTTP {status}).`,"dash.maNetworkError":`Netzwerkfehler — läuft der Proxy?`,"dash.stopping":`Wird gestoppt…`,"dash.actions":`Proxy`,"dash.codexRestart":`Codex-Modelle neu laden`,"dash.codexRestarting":`Wird gestoppt…`,"dash.codexRestartConfirm":`Codex-App-Server stoppen, damit sie die Modellliste neu laden? Ein laufender Codex-Vorgang wird unterbrochen, und Codex startet nicht von selbst neu — öffne es danach erneut.`,"dash.codexRestartDone":`{count} Codex-App-Server gestoppt. Öffne Codex erneut, um die aktuelle Modellliste zu laden.`,"dash.codexRestartNothing":`Es läuft kein Codex-App-Server. Der nächste Start liest die aktuelle Modellliste.`,"dash.codexRestartUnknown":`Prozesse konnten nicht aufgelistet werden, daher wurde nichts gestoppt.`,"dash.codexRestartPartial":`{count} App-Server wurden nicht beendet. Beende sie manuell, falls die Modellliste veraltet bleibt.`,"dash.codexRestartFailed":`Codex-Modelle konnten nicht neu geladen werden (HTTP {status}).`,"dash.codexRestartUnreachable":`Der Proxy war nicht erreichbar.`,"dash.codexRestartMalformed":`Der Proxy hat eine unerwartete Antwort gesendet.`,"dash.codexRestartTimeout":`Der Proxy hat nicht rechtzeitig geantwortet. Möglicherweise stoppt er noch App-Server.`,"models.staleBanner":`Codex zeigt eine ältere Modellliste als dieser Katalog. Starte Codex neu, um sie neu zu laden.`,"dash.codexAutoStart":`opencodex mit Codex starten`,"dash.codexAutoStartHint":`Erlaubt einem installierten Launcher-Shim, ocx ensure auszuführen. Diese Einstellung installiert keinen Neustartschutz; prüfe den effektiven Zustand unter Startsicherheit.`,"dash.searchModel":`Such-Sidecar-Modell`,"dash.searchModelHint":`Modell für web_search bei nicht über OpenAI gerouteten Modellen. Erfordert ChatGPT-Login.`,"dash.searchReasoning":`Such-Reasoning-Aufwand`,"dash.visionModel":`Vision-Sidecar-Modell`,"dash.visionModelHint":`Modell zur Beschreibung von Bildern für nur-Text-Routen. Erfordert ChatGPT-Login.`,"dash.webSearchSidecar":`Websuche-Sidecar`,"dash.webSearchSidecarHint":`Backend und Modell für die Websuche gerouteter Modelle auswählen.`,"dash.webSearchStream":`Antworten live streamen`,"dash.webSearchStreamHint":`Führenden Text und Reasoning live streamen, bis das Modell über einen Tool-Aufruf entscheidet; der Rest bleibt für das Abfangen der Suche gepuffert. Text vor einer Suche kann sich teilweise wiederholen.`,"dash.visionSidecar":`Vision-Sidecar`,"dash.visionSidecarHint":`Backend und Modell zur Bildbeschreibung für reine Textmodelle auswählen.`,"dash.visionOff":`Aus`,"dash.shadowCallIntercept":`Shadow-Call-Abfangen`,"dash.shadowCallInterceptHint":`Fängt die Hintergrund-Hilfsaufrufe der Codex-App ({models}) ab und leitet sie an das gewählte Modell um.`,"dash.shadowCallWarning":`⚠ Bei Aktivierung werden ALLE Anfragen an {models} durch das gewählte Modell ersetzt.`,"dash.shadowCallOriginal":`Original`,"dash.shadowCallModel":`Ersatzmodell`,"dash.shadowCallTooltip":`Die Codex-App ruft im Hintergrund ein Hilfsmodell für Titelgenerierung, Commit-Nachrichten und Skill-Orchestrierung auf. Das Modell wechselt zwischen Client-Versionen, daher fängt opencodex diesen Satz ab: {models}.`,"models.shadowCallIntercept":`Shadow-Call-Abfangen`,"models.shadowCallInterceptHint":`Fängt die Hintergrund-Hilfsaufrufe der Codex-App ({models}) ab und leitet sie an das gewählte Modell um.`,"dash.sidecarBackend":`Backend`,"dash.sidecarModel":`Modell`,"dash.backendAuto":`Automatisch`,"dash.backendOpenAI":`OpenAI`,"dash.backendAnthropic":`Anthropic`,"dash.sidecarSaved":`Sidecar-Einstellungen gespeichert. Angewendet bei der nächsten Anfrage.`,"dash.sidecarSaveFailed":`Sidecar-Einstellungen konnten nicht gespeichert werden.`,"dash.injectionLabel":`Sub-Agent-Delegation`,"dash.injectionHint":`Wähle das Modell, an das Codex Sub-Agent-Arbeit übergibt. Wo diese Wahl gilt, entscheiden die beiden Schalter unten.`,"dash.syncCodexSubagentDefaults":`Auch als Codex-Standard speichern`,"dash.syncCodexSubagentDefaultsHint":`Eingeschaltet wird die Wahl von oben in Codex' eigene Konfiguration geschrieben, sodass auch neue Aufgaben mit diesem Modell starten. Ausgeschaltet wird sie nur hier gemerkt. Wirksam beim nächsten Sync oder Neustart; deine selbst geschriebenen [agents]-Einstellungen bleiben unberührt.`,"dash.multiAgentGuidance":`Codex sagen, wie Arbeit aufgeteilt wird`,"dash.multiAgentGuidanceHint":`Schickt Codex eine kurze Notiz, wie Arbeit an Sub-Agenten übergeben werden soll. Auf v2 nennt sie die verfügbaren Modelle und das bevorzugte; auf v1 wirkt sie nur bei Reasoning-Effort max oder ultra. Ausgeschaltet wird keine Notiz angehängt.`,"dash.injectionNone":`Keine`,"dash.injectionEffortLabel":`Reasoning-Aufwand`,"dash.injectionEffortNone":`Modell-Standard`,"dash.effortCapLabel":`V2 Ultra Effort-Limit`,"dash.subagentEffortCapLabel":`V2 Sub-Agent Effort-Limit`,"dash.effortCapHelp":`Begrenzt die Reasoning-Intensität für V2-Ultra-Modus-Turns. Wenn gesetzt, werden eingehende Max-Anfragen (aus dem Ultra-Modus) auf das gewählte Niveau begrenzt. Das Sub-Agent-Limit gilt nur für erzeugte Kind-Agenten. Limits senken die Intensität nur, sie erhöhen sie nie. Wenn ein Modell das gewählte Niveau nicht unterstützt, wird automatisch auf das nächste unterstützte Niveau herabgesetzt.`,"dash.effortCapNone":`Kein Limit`,"dash.maintenance":`Wartung`,"dash.maintenanceHint":`Aktualisiere Codex’ Modellkatalog oder installiere eine neuere opencodex-Version.`,"dash.syncModels":`Modelle synchronisieren`,"dash.syncing":`Synchronisiere…`,"dash.syncOk":`Synchronisierung abgeschlossen. {count} Modell(e) angehängt.`,"dash.syncStaleHint":`Falls Codex weiterhin eine alte Liste zeigt, starte den langlebigen App-Server neu ({cmd}).`,"dash.syncFailed":`Synchronisierung fehlgeschlagen: {error}`,"dash.projectConfigTitle":`Projekt-Codex-Konfig umgeht OpenCodex`,"dash.projectConfigHint":`Diese repo-lokalen Einstellungen überschreiben den OpenCodex-Proxy (z. B. direkt zu OpenCode Go routen). Entferne sie, damit die Routing aus ~/.codex/config.toml in diesem Projekt greift.`,"dash.checkUpdate":`Update prüfen`,"dash.updateTitle":`opencodex aktualisieren`,"dash.updateDesc":`Prüfe npm auf den ausgewählten Kanal und wähle dann, ob der Proxy nach der Installation neu gestartet wird.`,"dash.updateChannel":`Kanal`,"dash.updateChecking":`Updates werden geprüft…`,"dash.updateInstalled":`Installiert`,"dash.updateLatest":`Neueste`,"dash.updateAvailable":`Update verfügbar`,"dash.updateCurrent":`Auf dem neuesten Stand`,"dash.updateCommand":`Befehl`,"dash.updateSource":`Dies ist ein Source-Checkout. Aktualisiere es im Terminal mit dem angezeigten Befehl.`,"dash.updateUnavailable":`Die neueste Version konnte nicht von npm gelesen werden. Versuche es später erneut.`,"dash.updateRetry":`Wiederholen`,"dash.updateRecheck":`Erneut prüfen`,"dash.updateCannotAuto":`Ein-Klick-Update ist nicht verfügbar ({reason}).`,"dash.updateReason.source_checkout":`Quellcode-Checkout`,"dash.updateReason.latest_unavailable":`npm-Registry nicht erreichbar`,"dash.updateReason.already_latest":`bereits auf dem neuesten Stand`,"dash.updateReason.unknown":`Update nicht verfügbar`,"dash.updateRestart":`Nach Update neu starten`,"dash.updateRestartHint":`Empfohlen. Die aktuelle GUI läuft weiter mit altem Code, bis der Proxy neu startet.`,"dash.runUpdate":`Aktualisieren`,"dash.updateReconnecting":`Warten auf den neu gestarteten Proxy…`,"dash.updateStatus.running":`opencodex wird aktualisiert.`,"dash.updateStatus.restarting":`Update installiert. Proxy wird neu gestartet.`,"dash.updateStatus.succeeded":`Update abgeschlossen.`,"dash.updateVersionTransition":`{currentVersion} -> {latestVersion}.`,"dash.updateStatus.failed":`Update fehlgeschlagen.`,"prov.subtitle":`Konfiguriere die Upstream-Anbieter, die opencodex in Codex routet. Melde dich mit einem Konto an, füge einen Anbieter hinzu oder bearbeite die Rohkonfiguration.`,"prov.add":`Anbieter hinzufügen`,"prov.editJson":`JSON bearbeiten`,"prov.accountLogin":`Konto-Login`,"prov.noOauth":`Keine OAuth-Anbieter verfügbar.`,"prov.loggedIn":`angemeldet`,"prov.notLoggedIn":`nicht angemeldet`,"prov.logout":`Abmelden`,"prov.login":`Anmelden`,"prov.loginWith":`Anmelden mit {provider}`,"prov.waitingBrowser":`Warten auf Browser…`,"prov.didntOpen":`Hat sich nicht geöffnet? Hier klicken`,"prov.copyLink":`Link kopieren`,"prov.dontOpenBrowser":`Keinen Browser auf dem Proxy-Rechner öffnen`,"prov.dontOpenBrowserHint":`Nützlich für ein anderes Browser-Profil oder wenn das Dashboard nicht auf dem Proxy-Rechner läuft.`,"prov.linkCopied":`Kopiert`,"prov.linkCopyUnavailable":`Zwischenablage nicht verfügbar`,"prov.deviceCode":`Gerätecode`,"prov.copyCode":`Code kopieren`,"prov.codeCopied":`Code kopiert`,"prov.editAlias":`Alias bearbeiten`,"prov.aliasPrompt":`Anzeigename (leer lassen zum Entfernen)`,"prov.aliasSaved":`Alias gespeichert`,"prov.aliasSaveFailed":`Alias konnte nicht gespeichert werden`,"prov.accountId":`ID`,"prov.pasteRedirect":`Redirect-URL oder Code einfügen`,"prov.pasteRedirectHint":`Zeigt der Browser einen localhost-Fehler, kopiere die vollständige URL aus der Adressleiste und füge sie hier ein (oder den Autorisierungscode).`,"prov.pasteSubmit":`Senden`,"prov.pasteSubmitting":`Wird gesendet…`,"prov.pasteOk":`Code gesendet — Anmeldung wird abgeschlossen…`,"prov.pasteFail":`Code konnte nicht gesendet werden: {error}`,"prov.port":`Port`,"prov.default":`Standard`,"prov.loadingConfig":`Lädt…`,"prov.saved":`Gespeichert! Proxy neu starten, um anzuwenden.`,"prov.loadConfigFail":`Konfiguration konnte nicht geladen werden`,"prov.invalidJson":`Ungültiges JSON`,"prov.saveFailed":`Speichern fehlgeschlagen`,"prov.loginFailStart":`{provider}-Login konnte nicht gestartet werden`,"prov.loginError":`{provider}-Login-Fehler: {error}`,"prov.loginRequestFail":`{provider}-Login-Anfrage fehlgeschlagen`,"prov.loginCancelled":`{provider}-Login abgebrochen`,"prov.loginTimeout":`{provider}-Login abgelaufen — Browser geschlossen oder nicht beendet. Erneut versuchen.`,"prov.loginOk":`Bei {provider} angemeldet. Führe {cmd} aus (oder es gilt live), um seine Modelle aufzulisten.`,"prov.loginSameAccount":`Immer noch dasselbe {provider}-Konto — wechsle im Browser das Konto und versuche „Konto hinzufügen“ erneut.`,"oauthTos.highTitle":`{provider}: Risiko bei Abo-OAuth`,"oauthTos.elevatedTitle":`{provider}: inoffizielle OAuth-Brücke`,"oauthTos.anthropicBody":`Die direkte Wiederverwendung von Claude-Abo-OAuth-Tokens über einen Drittanbieter-Proxy wie OpenCodex ist keine von Anthropic unterstützte Integration und kann zu Zugriffsbeschränkungen führen. Unterstützte Agent-SDK-Integrationen, die Claude-Abos verwenden, sind davon getrennt.`,"oauthTos.highBody":`OpenCodex verbindet {provider} über einen OAuth-Pfad eines Drittanbieters. Bei nicht unterstützter Nutzung kann der Zugriff eingeschränkt oder gesperrt werden.`,"oauthTos.elevatedBody":`OpenCodex verbindet {provider} über einen inoffiziellen OAuth-Pfad. Nutze nach Möglichkeit den offiziellen Client; ungewöhnlicher oder automatisierter Traffic kann als Missbrauch gewertet und der Zugriff eingeschränkt oder gesperrt werden.`,"oauthTos.saferPath":`Sicherere Option: Hinterlege stattdessen einen API-Schlüssel in OpenCodex.`,"oauthTos.acknowledge":`Ich verstehe das Risiko und möchte trotzdem mit OAuth fortfahren.`,"oauthTos.continue":`Mit OAuth fortfahren`,"prov.logoutOk":`Von {provider} abgemeldet.`,"prov.logoutFail":`Abmeldung von {provider} fehlgeschlagen. Der Kontostatus bleibt unverändert.`,"prov.removed":`"{name}" entfernt.`,"prov.removedDefault":`"{name}" entfernt. Standardanbieter ist jetzt "{defaultProvider}".`,"prov.removeFail":`"{name}" konnte nicht entfernt werden.`,"prov.removeLastProvider":`Der Standardanbieter kann nicht entfernt werden, wenn kein anderer aktivierter Anbieter Standard werden kann.`,"prov.removeHasDependentCombos":`Entferne oder aktualisiere zuerst diese abhängigen Combos: {combos}.`,"prov.setDefault":`Als Standard festlegen`,"prov.setDefaultSuccess":`"{name}" ist jetzt der Standardanbieter.`,"prov.setDefaultFail":`"{name}" konnte nicht als Standardanbieter festgelegt werden.`,"prov.defaultDisabled":`Aktiviere diesen Anbieter, bevor du ihn als Standard festlegst.`,"prov.updateFail":`Dieser Anbieter konnte nicht aktualisiert werden.`,"prov.networkError":`Netzwerkfehler. Prüfe, ob der Proxy läuft, und versuche es erneut.`,"prov.added":`"{name}" hinzugefügt. Sofort aktiv — führe {cmd} aus (oder starte neu), um seine Modelle in Codex’ Auswahl zu listen.`,"prov.removeConfirm":`Anbieter "{name}" entfernen? Seine Modelle verschwinden aus Codex’ Auswahl.`,"prov.hasApiKey":`API-Schlüssel konfiguriert`,"prov.hasHeaders":`benutzerdefinierte Header konfiguriert`,"prov.accounts":`Konten ({n})`,"prov.accountsAria":`{name}-Konten umschalten`,"prov.accountActive":`Aktiv`,"prov.accountReauth":`Erneut anmelden`,"prov.reauthenticate":`Erneut authentifizieren`,"prov.reauthAccountMissing":`Ausgewähltes Konto nach dem Login nicht gefunden`,"prov.reauthIdentityMismatch":`Angemeldetes Konto stimmt nicht mit dem ausgewählten Konto überein`,"prov.accountAdd":`Konto hinzufügen`,"prov.accountNoLabel":`Konto {id}`,"prov.accountSwitchTitle":`Dieses Konto verwenden`,"prov.accountSwitched":`Zu {email} gewechselt.`,"prov.accountSwitchFail":`Konto-Wechsel fehlgeschlagen`,"prov.accountRemoved":`{email} entfernt.`,"prov.accountRemoveFail":`{email} konnte nicht entfernt werden. Das Konto bleibt unverändert.`,"prov.accountRemoveAria":`{email} entfernen`,"prov.accountRemoveConfirm":`Konto {email} entfernen? Sein Login wird aus diesem Proxy gelöscht.`,"prov.keyAdd":`API-Schlüssel hinzufügen`,"prov.keyAdded":`API-Schlüssel zu {name} hinzugefügt.`,"prov.keyAddFail":`API-Schlüssel konnte nicht hinzugefügt werden`,"prov.keyPlaceholder":`API-Schlüssel einfügen`,"prov.keySwitchTitle":`Diesen Schlüssel verwenden`,"prov.keySwitched":`Zu Schlüssel {key} gewechselt.`,"prov.keySwitchFail":`Schlüssel-Wechsel fehlgeschlagen`,"prov.keyRemoved":`Schlüssel {key} entfernt.`,"prov.keyRemoveAria":`Schlüssel {key} entfernen`,"prov.keyRemoveConfirm":`API-Schlüssel {key} entfernen? Er wird aus der Proxy-Konfiguration gelöscht.`,"prov.activeBadge":`Aktiv`,"prov.disabledBadge":`Deaktiviert`,"prov.defaultBadge":`Standard`,"prov.enable":`Aktivieren`,"prov.disable":`Deaktivieren`,"prov.enabled":`"{name}" aktiviert. Seine Modelle können wieder in Codex erscheinen.`,"prov.disabled":`"{name}" deaktiviert. Einstellungen bleiben erhalten, aber seine Modelle sind verborgen.`,"prov.enableFail":`"{name}" konnte nicht aktiviert werden.`,"prov.disableFail":`"{name}" konnte nicht deaktiviert werden.`,"prov.enableAria":`Anbieter {name} aktivieren`,"prov.disableAria":`Anbieter {name} deaktivieren`,"prov.defaultCannotDisable":`Standard-Anbieter kann nicht deaktiviert werden`,"prov.openaiAccountMode":`Codex-Kontomodus`,"prov.openaiModePool":`Pool`,"prov.openaiModeDirect":`Direkt`,"prov.openaiPoolDesc":`Standard. Wechselt mit Affinität, Kontingent, Abklingzeit und Ausfallsicherung zwischen Hauptanmeldung und hinzugefügten Konten.`,"prov.openaiDirectDesc":`Verwendet nur die aktuelle primäre Codex-Anmeldung. Gespeicherte Pool-Konten werden weder gelesen noch gewechselt.`,"prov.openaiModeSaved":`OpenAI-Kontomodus wurde zu {mode} geändert.`,"prov.openaiModeSaveFailed":`Der OpenAI-Kontomodus konnte nicht geändert werden.`,"prov.openaiApiDesc":`Verwendet nur einen OpenAI-API-Schlüssel und keine Codex-Kontodaten.`,"prov.manageCodexAccounts":`Codex-Konten verwalten`,"prov.openaiApiMissing":`API-Schlüssel erforderlich`,"prov.openaiApiSetup":`API-Schlüssel einrichten`,"models.tab.catalog":`Modelle`,"models.tab.combos":`Combos`,"models.tab.compatibility":`Kompatibilität`,"models.tab.routing":`Routing (beta)`,"models.tabsLabel":`Modell-Oberflächen`,"models.subtitle.combos":`Geordnete Modellgruppen, die unter einer id antworten. Ziele mit Failover verketten oder die Last mit einer Balancing-Strategie verteilen.`,"models.subtitle.compatibility":`Schreibgeschützte Kompatibilitätsmatrix aus der Lab-Projektion.`,"models.subtitle.routing":`Policy-Profile, Dry-Run-Auswertung und quellenbasierte Routing-Analysen.`,"models.subtitle":`Steuere, welche Modelle Codex sieht — natives GPT-Passthrough und geroutete Anbieter, nach Anbieter gruppiert (Kopfzeile zum Einklappen anklicken). Ausgeblendete Modelle fehlen in Katalog und Auswahl, bleiben aber per genauer ID aufrufbar. Änderungen gelten bei der nächsten Codex-Runde — opencodex invalidiert Codex 5-Minuten-Modell-Cache, kein Neustart nötig.`,"models.nativeGroupLabel":`OpenAI nativ`,"models.nativeHint":"Passthrough-Modelle verwenden die unter Anbieter gewählte Pool- oder Direkt-Option. Ausblenden entfernt sie aus der Codex-Auswahl (Katalogeintrag bleibt, Reaktivierung stellt exakt wieder her). Ein hier hinzugefügtes Modell wird als gerouteter `openai/`-Selektor registriert, nicht als neue reine Passthrough-ID.","models.active":`{active}/{total} sichtbar`,"models.workspace.providers":`Anbieter`,"models.workspace.allProviders":`Alle Anbieter`,"models.workspace.mainAria":`Modelldetails`,"models.allOn":`Alle an`,"models.allOff":`Alle aus`,"models.presetLabel":`Modelle`,"models.presetMode_preset":`Voreinstellung`,"models.presetMode_all":`Alle`,"models.presetMode_custom":`Eigene`,"models.presetSummary":`{count} von {total} angezeigt — Core-Voreinstellung v{version}`,"models.presetUpdateAvailable":`Voreinstellung v{version} verfügbar`,"models.presetAppliedToast":`{provider}: Voreinstellung angewendet — {count} Modelle ausgewählt`,"models.presetClearedToast":`{provider}: alle Modelle werden angezeigt`,"models.presetEmpty":`{provider}: Voreinstellung traf auf kein Modell zu — Auswahl unverändert`,"models.presetConfirmReplace":`Auswahl durch die Voreinstellung mit {count} Modellen ersetzen?`,"models.cap350k":`Limit 350k`,"models.capApplied":`Kontext-Limit angewendet — greift bei der nächsten Codex-Runde.`,"models.capSaveFailed":`Kontext-Limit konnte nicht gespeichert werden`,"models.contextCapped":`350k-Limit`,"models.contextCapLabel":`Standardfenster / Limit`,"models.v2Label":`Sub-Agent`,"models.shadowCallOriginal":`⚠ {models} →`,"models.v2DocsLink":`Was ist v1 / v2?`,"models.v2Mode_v1":`v1`,"models.v2Mode_default":`base`,"models.v2Mode_v2":`v2`,"models.v2ModeDesc_v1":`Alle Modelle → v1-Oberfläche`,"models.v2ModeDesc_default":`Upstream-Standard (sol/terra=v2, luna=v1)`,"models.v2ModeDesc_v2":`Alle Modelle → v2-Oberfläche`,"models.keepNativeOnV1":`ChatGPT auf v1 lassen`,"models.keepNativeOnV1Hint":`ChatGPT-native Eltern verschlüsseln v2-Kindaufgaben — Grok und Claude können sie nicht lesen. An lassen, wenn Sol/Terra weiterhin geroutete Modelle starten sollen. Geroutete Eltern bleiben auf v2.`,"models.v2Help":`Steuert die Multi-Agent-Oberfläche für alle Modelle. - -v1: Klassischer Single-Thread-Agent. Jedes Modell nutzt die v1-Collab-Oberfläche. -base: Upstream-Standard — sol/terra nutzen v2, luna v1, andere folgen dem Codex-Feature-Flag. -v2: Multi-Thread-Agent mit spawn_agent. Jedes Modell nutzt die v2-Collab-Oberfläche. - -Unter v2 lässt „ChatGPT auf v1 lassen“ Sol/Terra auf v1, damit sie weiter Grok oder Claude starten können. ChatGPT verschlüsselt v2-Kindaufgaben; geroutete Modelle können sie nicht lesen. Geroutete Eltern bleiben auf v2. - -Änderungen gelten für neue Sitzungen.`,"dash.multiAgent":`Sub-Agent`,"models.v2Conflict":`[agents] max_threads ist gesetzt — codex verweigert den Start; entferne es aus config.toml`,"models.v2Applied":`Sub-Agent-Modus aktualisiert — gilt für neue Sitzungen (Codex-App neu starten, um die Auswahl zu aktualisieren)`,"models.v2ThreadsLabel":`Max. Threads`,"models.v2ThreadsDefault":`Standard (4)`,"models.v2ThreadsApplied":`Thread-Limit aktualisiert — gilt für neue Sitzungen`,"models.v2ThreadsInvalid":`Thread-Limit muss eine ganze Zahl >= 1 sein`,"models.v2ThreadsApply":`Anwenden`,"models.capValue":`Standard {value}`,"models.contextSettings":`Eigene Fenster`,"models.contextSettingsTitle":`Eigene Fenster — {provider}`,"models.contextDefault":`Anbieterstandard`,"models.contextModel":`Modell`,"models.contextModelOverride":`Modellüberschreibung`,"models.contextHint":`Wenn das Fenster bekannt ist, tragen Sie hier das tatsächliche Codex-Fenster ein. Fehlt ein Upstream-Wert, gilt dieser Eintrag; ein größeres gemeldetes Fenster wird nur nach unten begrenzt, ein kleineres bleibt erhalten. Leer bedeutet das Anbieter-«Standardfenster / Limit», oder 128k wenn das Limit aus ist.`,"models.contextAutomatic":`Automatische Erkennung`,"models.contextSaved":`Kontextfenster aktualisiert — gilt ab der nächsten Codex-Runde.`,"models.contextUnchanged":`Keine Änderungen am Kontextfenster zu speichern.`,"models.contextSaveFailed":`Kontextfenster konnten nicht gespeichert werden`,"models.contextInvalid":`Kontextfenster müssen positive ganze Zahlen sein`,"models.contextCappedValue":`{value}-Limit`,"models.setAll":`Alle setzen`,"models.setAllHint":`Schaltet das Standardfenster {value} für alle gerouteten Anbieter ein. Fehlen context_window / context_length, wird dieser Wert das tatsächliche Codex-Fenster. Für ein einzelnes Modell nutzen Sie «Eigene Fenster» in derselben Zeile. Native Anbieter bleiben unberührt.`,"models.collapseAll":`Alle einklappen`,"models.expandAll":`Alle ausklappen`,"models.orderHint":`Reihenfolge in der Modellauswahl: Subagents-Auswahl (in der festgelegten Reihenfolge) → übrige geroutete Modelle alphabetisch nach Anbieter, dann Modell-ID → native Modelle. Sichtbarkeitsschalter filtern nur; sie ändern diese Reihenfolge nicht.`,"models.custom":`Benutzerdefiniert…`,"models.customApply":`Anwenden`,"models.customPlaceholder":`Tokens (z. B. 420000)`,"models.customAdd":`Benutzerdefiniertes Modell hinzufügen`,"models.customAddTitle":`Benutzerdefiniertes Modell hinzufügen — {provider}`,"models.customEditTitle":`Benutzerdefiniertes Modell bearbeiten — {provider}`,"models.customAdded":`Benutzerdefiniertes Modell hinzugefügt`,"models.customUpdated":`Benutzerdefiniertes Modell aktualisiert`,"models.customDeleted":`Benutzerdefiniertes Modell gelöscht`,"models.customSaveFailed":`Benutzerdefiniertes Modell konnte nicht gespeichert werden`,"models.customSaving":`Wird gespeichert…`,"models.customAddBtn":`Hinzufügen`,"models.customEditBtn":`Aktualisieren`,"models.customEdit":`Bearbeiten`,"models.customDelete":`Löschen`,"models.customDeleteConfirm":`Modell {name} löschen?`,"models.customBadge":`Benutzerdefiniert`,"models.customSummary":`{count} benutzerdefiniert`,"models.customFieldModelId":`Modell-ID (Endpunkt-Slug)`,"models.customFieldModelIdPlaceholder":`z. B. qwen4-max-preview`,"models.customFieldDisplayName":`Anzeigename (optional)`,"models.customFieldDisplayNamePlaceholder":`z. B. Qwen 4 Max Preview`,"models.customFieldContext":`Kontextfenster`,"models.customFieldModalities":`Eingabemodalitäten`,"models.customFieldReasoning":`Reasoning-Aufwand`,"models.customFieldReasoningOverride":`Reasoning-Aufwand überschreiben`,"models.reasoningEffort.none":`Keine`,"models.reasoningEffort.minimal":`Minimal`,"models.reasoningEffort.low":`Niedrig`,"models.reasoningEffort.medium":`Mittel`,"models.reasoningEffort.high":`Hoch`,"models.reasoningEffort.xhigh":`Sehr hoch`,"models.reasoningEffort.max":`Maximal`,"models.tipProvider":`Anbieter`,"models.tipContext":`Kontext`,"models.tipModalities":`Modalitäten`,"models.tipStatus":`Status`,"models.tipActive":`Aktiv`,"models.tipDisabled":`Deaktiviert`,"models.applied":`Angewendet — greift bei der nächsten Codex-Runde.`,"models.saveFailed":`Speichern fehlgeschlagen`,"models.networkError":`Netzwerkfehler — läuft der Proxy?`,"models.loadFail":`Modelle konnten nicht geladen werden — läuft der Proxy?`,"models.noRouted":`Keine gerouteten Modelle`,"models.noRoutedHint":`Melde dich zuerst bei einem Anbieter an oder füge einen hinzu.`,"models.emptyDiscovery":`Es wurden keine Modelle gefunden. Prüfe den Anbieter-Endpunkt oder füge ein statisches/eigenes Modell hinzu.`,"models.emptyDiscoveryDisabled":`Die Live-Modellerkennung ist aus und es sind keine statischen Modelle konfiguriert.`,"models.discoveryFailedBadge":`Erkennung fehlgeschlagen`,"models.discoveryFailedHttp":`Die Modellerkennung ist fehlgeschlagen (HTTP {status}).`,"models.discoveryFailedBlocked":`Die Modellerkennung wurde durch die Zielrichtlinie blockiert.`,"models.discoveryFailedInvalidResponse":`Die Modellerkennung lieferte eine ungültige Antwort.`,"models.discoveryFailedNetwork":`Die Modellerkennung ist an einem Netzwerkfehler gescheitert.`,"models.discoveryFailedProvider":`Der Anbieter meldete einen Fehler bei der Modellerkennung.`,"models.discoveryFailedGeneric":`Die Modellerkennung ist fehlgeschlagen.`,"models.openProviderSettings":`Anbietereinstellungen öffnen`,"models.loading":`Lädt…`,"models.search":`Modelle suchen…`,"models.showMore":`{n} weitere anzeigen`,"models.allowlistLabel":`Nur ausgewählte`,"models.allowlistHint":`Nur geprüfte Modelle gehen in den Katalog (leer = alle). Nützlich für Anbieter mit tausenden Modellen.`,"models.selectedCount":`{n} ausgewählt`,"sub.subtitle":`Codex {cmd} bewirbt nur die ersten 5 Modelle (nach Priorität) als Overrides. Wähle hier bis zu 5 — natives gpt oder geroutet — und opencodex setzt ihre Katalog-Priorität, sodass genau diese führen. Jedes andere Modell bleibt über seinen exakten Namen aufrufbar; dies steuert nur die Anzeige.`,"sub.featured":`Empfohlen`,"sub.advanced":`Erweitert`,"sub.orderHintAria":`Wie diese Reihenfolge verwendet wird`,"sub.orderHint":`Die hier gewählte und angezeigte Reihenfolge bestimmt die Plätze 1–5 oben in der Codex-Modellauswahl und die Standard-Modellkandidaten für {cmd}.`,"sub.noneSelected":`Nichts ausgewählt — wähle aus der Liste unten.`,"sub.models":`Modelle`,"sub.search":`Modelle suchen (nativ gpt + geroutet)…`,"sub.noModels":`Keine Modelle — melde dich zuerst bei einem Anbieter an oder füge einen hinzu.`,"sub.saved":`{n} Modelle gespeichert. Starte eine neue Codex-Sitzung (oder führe {cmd} aus), um sie als spawn_agent-Overrides zu sehen.`,"sub.saveFailed":`Speichern fehlgeschlagen`,"sub.networkError":`Netzwerkfehler — läuft der Proxy?`,"sub.loadFail":`Modelle konnten nicht geladen werden — läuft der Proxy?`,"sub.loading":`Lädt…`,"sub.moveUp":`{m} nach oben`,"sub.moveDown":`{m} nach unten`,"sub.removeAria":`{m} entfernen`,"sub.workspace.addToFeatured":`{m} zu Hervorgehobenen hinzufügen`,"sub.workspace.allModels":`Alle Modelle`,"sub.workspace.featuredFull":`Hervorgehobene Liste ist voll (max. 5)`,"sub.workspace.mainAria":`Subagent-Modelldetails`,"sub.workspace.notFeatured":`Nicht hervorgehoben`,"sub.workspace.priority":`Priorität`,"sub.workspace.removeFromFeatured":`{m} aus Hervorgehobenen entfernen`,"sub.workspace.selectModel":`Modell auswählen`,"sub.workspace.selectModelDesc":`Wählen Sie ein Modell aus der Liste, um Details anzuzeigen und es für spawn_agent hervorzuheben.`,"sub.workspace.selector":`Öffentlicher Selektor`,"sub.ultraMode":`Ultra-Modus`,"sub.ultraModeHint":`Aktiviert die proaktive Multi-Agent-Delegierungsrichtlinie für alle Modelle und Reasoning-Efforts (ändert den Reasoning-Effort selbst nicht). Schreibt features.multi_agent_v2.multi_agent_mode_hint_text in config.toml.`,"sub.ultraModeV2Required":`Erfordert die v2-Multi-Agent-Oberfläche — aktivieren Sie zuerst multi_agent_v2 und wählen Sie v2 in der Subagentenmodus-Steuerung.`,"sub.ultraModeText":`Delegierungstext des Ultra-Modus`,"sub.ultraModePreset":`Voreinstellung wiederherstellen`,"sub.ultraModeLoadFail":`Ultra-Modus-Einstellungen konnten nicht geladen werden — läuft der Proxy?`,"sub.ultraModeSaveFail":`Ultra-Modus-Einstellungen konnten nicht gespeichert werden`,"sub.ultraModeSaved":`Ultra-Modus gespeichert. Gilt für neue Codex-Sitzungen.`,"logs.title":`Anfrage-Protokolle`,"logs.tabLogs":`Protokolle`,"logs.tabDebug":`Diagnose`,"logs.subtitle":`Letzte Anfragen über den lokalen opencodex-Proxy, neueste zuerst.`,"logs.autoRefresh":`Auto-Aktualisierung`,"logs.noRequests":`Noch keine Anfragen.`,"logs.loadError":`Anfrageprotokolle konnten nicht geladen werden.`,"logs.filter.surface.label":`Oberfläche`,"logs.filter.surface.all":`Alle`,"logs.filter.surface.claude":`Claude`,"logs.filter.surface.codex":`Codex`,"logs.filter.surface.grok":`Grok`,"logs.filter.interceptedHelpersOnly":`Nur abgefangene Helfer`,"logs.badge.interceptedHelper":`I · {model}`,"logs.badge.interceptedHelperTitle":`Abgefangene Helfer-Anfrage`,"logs.filter.conversation.label":`Konversation`,"logs.filter.conversation.placeholder":`Konversations-ID einfügen`,"logs.filter.conversation.clear":`Löschen`,"logs.filter.model.label":`Modell`,"logs.filter.model.placeholder":`Modell oder Anbieter filtern`,"logs.filter.conversation.apply":`Logs filtern`,"logs.conversation.totals":`{requests} Anfragen · {tokens} Tokens · {cost}`,"logs.conversation.scope":`Summen gelten nur für den aktuell geladenen Logs-Ring.`,"logs.conversation.excluded":`({unpriced} ohne Preis, {unmetered} ohne Messung vom ~$ ausgenommen)`,"logs.cost.approximate":`{amount}`,"logs.cost.lowerBound":`≥{amount}`,"logs.cost.unavailable":`nicht verfügbar`,"logs.detail.conversation":`Konversation`,"logs.badge.claude":`Claude`,"logs.badge.grok":`Grok`,"logs.col.time":`Zeit`,"logs.col.request":`Anfrage`,"logs.col.model":`Modell`,"logs.col.effort":`Aufwand`,"logs.col.provider":`Anbieter`,"logs.col.status":`Status`,"logs.col.tokens":`Tokens`,"logs.col.tokPerSec":`tok/s`,"logs.col.estimatedCost":`~$`,"logs.metric.tokPerSecTitle":`Ausgabe-Tokens pro Sekunde über die gesamte Anfragedauer`,"logs.metric.estimatedCostTitle":`API-Listenpreis-Äquivalent, keine tatsächliche Belastung; bei fehlendem Preisabgleich nicht verfügbar`,"usage.cost.total":`API-Listenpreis-Äquivalent (dieser Zeitraum)`,"usage.cost.disclaimer":`Kein Abrechnungsbeleg. Stattdessen können Abonnementnutzung oder Anbieter-Guthaben gelten.`,"usage.cost.unpricedNote":`{count} Anfragen ohne Preis oder Nutzung ausgeschlossen`,"logs.detail.section.basic":`Grundinformationen`,"logs.detail.route.section":`Route-Entscheidung`,"logs.detail.route.kind":`Route-Typ`,"logs.detail.route.profile":`Profil`,"logs.detail.route.selected":`Ausgewählt`,"logs.detail.route.candidates":`Kandidaten`,"logs.detail.route.unknown":`Für diese Anfrage wurde keine Route-Entscheidung aufgezeichnet (Zeile vor dem Trace).`,"logs.detail.section.performance":`Leistung`,"logs.detail.section.cost":`API-Listenpreis-Äquivalent`,"logs.detail.section.attempts":`Combo-Versuche`,"logs.detail.section.usage":`Roh-Nutzung`,"logs.detail.ttft":`TTFT`,"logs.detail.costTotal":`Listenpreis-Äquivalent`,"logs.detail.totalTokens":`Tokens gesamt`,"logs.detail.matchedKey":`Zugeordneter Preisschlüssel`,"logs.detail.priceSource":`Preisquelle`,"logs.detail.unavailableReason":`Grund der Nichtverfügbarkeit`,"logs.detail.copyRequestId":`Anfrage-ID kopieren`,"logs.detail.copied":`Kopiert`,"logs.detail.source.jawcode":`jawcode-Katalog`,"logs.detail.source.expected":`Expected-Preis-Overlay`,"logs.detail.source.user":`Anbieter-konfiguriertes Preis-Overlay`,"logs.detail.verification.verified":`Verifiziert`,"logs.detail.verification.derived":`Vom Basismodell abgeleitet`,"logs.detail.attempt.target":`Anbieter / Modell`,"logs.detail.attempt.reason":`Ergebnis / Grund`,"logs.detail.attempt.completed":`Abgeschlossen`,"logs.detail.attempt.e2eNote":`Tok/s auf oberster Ebene ist Ende-zu-Ende; jeder Versuch nutzt seine eigene Dauer.`,"logs.detail.attempt.recovery.transient5xx":`Vorübergehender 5xx-Fehler`,"logs.detail.attempt.recovery.connectionReset":`Verbindung zurückgesetzt`,"logs.detail.attempt.recovery.oauth401":`OAuth-Neuanmeldung`,"logs.detail.attempt.recovery.key429":`Schlüssel ratenbegrenzt (429)`,"logs.detail.attempt.recovery.rateLimit429":`Ratenbegrenzt (429)`,"logs.detail.attempt.recovery.anthropicOauth429":`Anthropic OAuth ratenbegrenzt (429)`,"logs.detail.attempt.recovery.image413":`Bildnutzlast zu groß (413)`,"logs.detail.attempt.recovery.emptyCompletion":`Wiederholung nach leerer Antwort`,"logs.detail.attempt.recovery.unknown":`Unbekannter Wiederherstellungsgrund`,"logs.detail.reason.usage_missing":`Nutzung wurde nicht gemeldet.`,"logs.detail.reason.usage_unsupported":`Dieser Anbieter meldet keine Nutzung.`,"logs.detail.reason.output_missing":`Es wurden keine positiven Ausgabe-Tokens gemeldet.`,"logs.detail.reason.invalid_duration":`Die Anfragedauer ist ungültig.`,"logs.detail.reason.price_unmatched":`Kein passender Preis gefunden.`,"logs.detail.reason.invalid_cache_breakdown":`Cache-Token-Details widersprechen den Eingabe-Tokens.`,"logs.detail.reason.invalid_usage":`Die Nutzung enthält einen ungültigen Token-Wert.`,"logs.detail.reason.combo_attempt_unavailable":`Mindestens ein Combo-Versuch konnte nicht bepreist werden.`,"logs.detail.estimate.usage_estimated":`Die Anbieternutzung ist geschätzt.`,"logs.detail.estimate.cache_detail_missing":`Cache-Details fehlen; Eingabe ist als Obergrenze geschätzt.`,"logs.detail.estimate.expected_price_overlay":`Ein verifizierter Expected-Listenpreis wurde verwendet.`,"logs.detail.estimate.provider_cost_overlay":`Ein vom Anbieter konfiguriertes Preis-Overlay wurde verwendet.`,"logs.detail.estimate.priority_lower_bound":`Der bestätigte Priority-Preis ist nicht verfügbar; die angezeigte Schätzung ist eine bekannte Untergrenze.`,"logs.col.error":`Fehler`,"logs.col.upstreamReason":`Upstream-Grund`,"logs.col.duration":`Dauer`,"logs.modelTooltip.model":`Modell`,"logs.modelTooltip.resolvedModel":`aufgelöstes Modell`,"logs.modelTooltip.requestedTier":`angeforderte Stufe`,"logs.modelTooltip.configuredTier":`konfigurierte Stufe`,"logs.modelTooltip.responseTier":`Antwortstufe`,"logs.modelTooltip.supportsTier":`Stufenunterstützung`,"logs.tokens.reported":`gemeldet`,"logs.tokens.unreported":`nicht gemeldet`,"logs.tokens.unsupported":`nicht unterstützt`,"logs.tokens.estimated":`geschätzt`,"logs.tokens.input":`Eingabe`,"logs.tokens.output":`Ausgabe`,"logs.tokens.cacheRead":`Cache-Treffer (c)`,"logs.tokens.cacheWrite":`Cache-Schreiben (w)`,"logs.tokens.reasoning":`Reasoning`,"logs.tokens.noCache":`keine Cache-Daten`,"logs.tokens.contextTotal":`aktiver Kontext`,"logs.tokens.noCacheNote":`dieser Anbieter meldet keine Cache-Tokens`,"logs.tokens.noCacheCursor":`Cursor-Cache-Details nicht gemeldet`,"logs.tokens.noCacheCursorNote":`Cursor liefert keine Cache-Read/Write-Tokenzahlen; das ist unbekannt und kein bestätigter Cache-Miss`,"logs.tokens.estimatedNote":`Schätzung (Anbieter meldet keine exakte Nutzung)`,"logs.details":`Details`,"logs.detailTitle":`Anfragedetails`,"logs.detailRaw":`Roh-Protokolleintrag`,"debug.title":`Fehlerdiagnose`,"debug.subtitle":`Opt-in-Diagnose für Provider-Transport und Nutzungs-Extraktion. Anfragefehler und 502er bleiben im Protokolle-Tab.`,"debug.debug":`Provider-Diagnose`,"debug.usage":`Nutzungs-Extraktion`,"debug.injection":`Injektions-Log`,"debug.claude":`Claude-Inbound`,"debug.claudeInbound.title":`Claude-Inbound-Anfragen`,"debug.claudeInbound.sub":`Zeigt, was Claude Code/Desktop tatsächlich sendet (thinking, effort, metadata) — kein Prompt-Text wird gespeichert.`,"debug.claudeInbound.empty":`Noch keine Anfragen erfasst. Sende bei aktivierter Erfassung eine Nachricht aus Claude.`,"debug.claudeInbound.time":`Zeit`,"debug.claudeInbound.endpoint":`Endpunkt`,"debug.claudeInbound.model":`Modell`,"debug.claudeInbound.none":`keine`,"debug.reset":`Laufzeit-Überschreibungen löschen`,"debug.refresh":`Aktualisieren`,"debug.follow":`Folgen`,"debug.streamProvider":`Anbieter`,"debug.streamUsage":`Nutzung`,"debug.streamInjection":`Injektion`,"debug.loading":`Lade Diagnose-Einstellungen…`,"debug.loadFailed":`Diagnose-Einstellungen konnten nicht geladen werden.`,"debug.emptyTitle":`Diagnose-Logging ist aus`,"debug.empty":`Aktiviere Provider-Diagnose oder Nutzungs-Extraktion in der Karte oben. Zeilen erscheinen hier, nachdem du eine Anfrage über den Proxy gesendet hast.`,"debug.noLinesTitle":`Warte auf Zeilen`,"debug.noLines.provider":`Anbieter-Debug ist an, erfasst aber nur Transport-Anomalien (verworfene oder fehlerhafte Frames sowie Cursor-Dial/Retry-Ereignisse). Eine saubere Anfrage über einen Anbieter wie Anthropic kann null Zeilen erzeugen.`,"debug.noLines.usage":`Nutzungserfassung ist an, aber es wurde noch nichts erfasst. Sende einen Chat/eine Anfrage über Codex, dann erscheint es hier.`,"debug.noLines.injection":`Injektions-Log ist an, aber es wurde noch nichts erfasst. Es erfasst Multi-Agent-Guidance-Injektion und Effort-Cap-Entscheidungen bei Collab- und Sub-Agent-Turns.`,"usage.title":`Nutzung`,"usage.subtitle":`Lokale Token-Buchhaltung deines Proxys. Fehlende Nutzung wird nie als Null angezeigt.`,"usage.loading":`Lade Nutzungsdaten…`,"usage.empty":`Noch keine Nutzung erfasst. Sende eine Anfrage über den Proxy, um Aktivität hier zu sehen.`,"usage.loadError":`Nutzungsdaten konnten nicht geladen werden.`,"usage.range.all":`Alle`,"usage.range.available":`Verfügbarer Verlauf`,"usage.historyTruncated":`Die Summen beziehen sich nur auf den verfügbaren Verlauf, da ältere Nutzungsdaten nicht geladen wurden.`,"usage.historyTruncatedWindow":`Die geladenen Zeilen haben Anfragestartzeiten zwischen {start} und {end}. Frühere Dateieinträge wurden durch das Leselimit ausgelassen, daher kann jeder gewählte Zeitraum unvollständig sein.`,"usage.range.30d":`30d`,"usage.range.7d":`7d`,"usage.card.requests":`Anfragen`,"usage.card.measured":`Gemessen`,"usage.card.reported":`Gemeldet`,"usage.card.totalTokens":`Gesamt-Tokens`,"usage.card.cachedTokens":`Cache-Treffer-Tokens`,"usage.card.cachedTokensHint":`Prompt-Tokens aus dem Provider-Cache (Treffer). Cache-Schreibvorgänge werden darunter separat angezeigt.`,"usage.card.cacheWriteTokens":`Cache-Schreiben`,"usage.card.coverage":`Abdeckung`,"usage.card.activeDays":`Aktive Tage`,"usage.section.heatmap":`Tägliche Aktivität`,"usage.section.overview":`Übersicht`,"usage.section.models":`Modelle`,"usage.section.providers":`Anbieter`,"usage.section.coverage":`Abdeckungs-Aufschlüsselung`,"usage.workspace.report":`Nutzungsbericht`,"usage.workspace.sections":`Nutzungsabschnitte`,"usage.coverage.measured":`Gemessen`,"usage.coverage.reported":`Anbieter gemeldet`,"usage.coverage.estimated":`Geschätzt`,"usage.coverage.note":`Gemessene Einträge enthalten anbieter-gemeldete und geschätzte Token-Zahlen. Nicht gemeldete und nicht unterstützte Anfragen werden erfasst, aber nie auf Null aufgebläht.`,"usage.search.models":`Modelle suchen…`,"usage.col.requests":`Anfragen`,"usage.col.measured":`Gemessen`,"usage.col.reported":`Gemeldet`,"usage.col.tokens":`Tokens`,"usage.col.share":`Anteil`,"usage.heatmap.less":`Weniger`,"usage.heatmap.more":`Mehr`,"modal.addNamed":`Hinzufügen: {label}`,"modal.add":`Anbieter hinzufügen`,"modal.search":`Anbieter suchen…`,"modal.logInWith":`Anmelden mit {label}`,"modal.waitingBrowser":`Warten auf Browser…`,"modal.providerName":`Anbietername`,"modal.adapter":`Adapter`,"modal.baseUrl":`Basis-URL`,"modal.endpoint":`Endpunkt`,"modal.endpoint.tokenPlan":`Token-Plan`,"modal.endpoint.payAsYouGo":`Pay as you go`,"modal.endpoint.custom":`Benutzerdefiniert`,"modal.defaultModel":`Standardmodell (optional)`,"modal.allowPrivateNetwork":`Lokales/privates Netzwerk erlauben`,"modal.allowPrivateNetworkHint":`Nur für absichtlich selbst gehostete Provider aktivieren. Metadaten-Endpunkte bleiben blockiert.`,"modal.nameRequired":`Anbietername ist erforderlich`,"modal.baseUrlRequired":`Basis-URL ist erforderlich`,"modal.networkError":`Netzwerkfehler — läuft der Proxy?`,"modal.loginFailStart":`Login konnte nicht gestartet werden`,"modal.waitingLogin":`Warten auf Browser-Login…`,"modal.loggingIn":`Anmelden…`,"modal.loginTimeout":`Login-Zeitüberschreitung — versuche es erneut.`,"nav.codexAuth":`Codex-Auth`,"nav.codexSet":`Codex-Einstellungen`,"codexSet.tab.multiauth":`Multi-Auth`,"codexSet.tab.prompt":`Prompt`,"codexSet.prompt.title":`Prompt-Ebenen`,"codexSet.prompt.timing":`Gilt für neu gestartete Sitzungen. Laufende Sitzungen behalten ihre aktuellen Prompt-Einstellungen.`,"codexSet.prompt.staleRevision":`Die Konfiguration wurde anderswo geändert. Die Liste wurde neu geladen.`,"codexSet.prompt.writeFailed":`Die Änderung konnte nicht gespeichert werden.`,"codexSet.prompt.loadFailed":`Die Prompt-Ebenen konnten nicht geladen werden.`,"codexSet.prompt.repair":`Reparieren`,"codexSet.prompt.repairFailed":`Die Reparatur konnte nicht abgeschlossen werden.`,"codexSet.drift.journalPresent":`Ein vorheriger Schreibvorgang wurde nicht abgeschlossen. Die Wiederherstellung läuft beim nächsten Schreibvorgang automatisch.`,"codexSet.drift.projectionStale":`Die gespeicherten Ebenen und der Wert in config.toml stimmen nicht überein. Die Reparatur schreibt den Wert aus deinen Ebenen neu.`,"codexSet.drift.storeMissing":`Die Ebenendatei fehlt, während in config.toml noch Anweisungen stehen. Die Reparatur legt zuerst eine Sicherung an und behält den Text als eine Ebene.`,"codexSet.drift.ownedMalformed":`Die generierte Zeile in config.toml wurde von Hand verändert und kann daher nicht mehr sicher überschrieben werden.`,"codexSet.custom.adoptUnsupported":`Der Wert in {path} Zeile {line} ist keine einzeilige Zeichenkette und kann nicht importiert werden. Verschiebe ihn von Hand, um ihn hier zu verwalten.`,"codexSet.prompt.unreadable":`Die Codex-Konfigurationsdatei existiert, konnte aber nicht gelesen werden, daher wurden Änderungen abgelehnt.`,"codexSet.layer.permissions":`Berechtigungen`,"codexSet.layer.collaboration":`Kollaborationsmodus`,"codexSet.layer.environment":`Umgebungskontext`,"codexSet.layer.apps":`Apps`,"codexSet.layer.skills":`Skills`,"codexSet.prompt.extensionsUnknown":`Erweiterungen können eigene Ebenen hinzufügen. Codex legt sie nicht offen, daher können sie hier nicht aufgeführt werden.`,"codexSet.group.transition":`Übergangshinweise`,"codexSet.group.transitionDesc":`Sie melden eine Änderung, statt einen Zustand zu beschreiben, und erscheinen daher nur beim Wechsel in den Echtzeitmodus oder beim Modellwechsel.`,"codexSet.custom.slotNote":`Benutzerdefinierte Ebenen werden in dieser Reihenfolge zu einem Abschnitt zusammengefügt.`,"codexSet.row.alwaysOn":`Immer aktiv`,"codexSet.row.onChange":`Bei Änderung`,"codexSet.row.featureGated":`Unter [features] konfiguriert`,"codexSet.row.openFeatures":`Einstellungen öffnen`,"codexSet.dialog.setValue":`{value} (Standard {fallback})`,"codexSet.dialog.copyKey":`Schlüssel kopieren`,"codexSet.dialog.unknownLayer":`Dieser Build enthält keine Beschreibung für diese Ebene. Sie stammt aus einer neueren Codex-Laufzeit als das Dashboard.`,"codexSet.custom.heading":`Benutzerdefinierte Ebenen`,"codexSet.custom.add":`+ Ebene hinzufügen`,"codexSet.custom.newTitle":`Neue Ebene`,"codexSet.custom.editTitle":`Ebene bearbeiten`,"codexSet.custom.titleLabel":`Titel`,"codexSet.custom.bodyLabel":`Anweisungen`,"codexSet.custom.bodySize":`{bytes} von {max} Byte`,"codexSet.custom.normalized":`Tabulatoren wurden durch vier Leerzeichen und Zeilenenden durch LF ersetzt.`,"codexSet.custom.titleRequired":`Gib einen Titel ein.`,"codexSet.custom.titleTooLong":`Der Titel hat {count} Zeichen; das Limit liegt bei {max}.`,"codexSet.custom.titleMultiline":`Der Titel muss einzeilig sein.`,"codexSet.custom.bodyTooLarge":`Diese Ebene umfasst {bytes} Byte; das Limit liegt bei {max}.`,"codexSet.custom.composedTooLarge":`Die aktivierten Ebenen würden zusammen {bytes} Byte umfassen und das Limit überschreiten.`,"codexSet.custom.invalidCharacter":`Ein Steuerzeichen an Position {position} kann nicht gespeichert werden.`,"codexSet.custom.discardPrompt":`Änderungen verwerfen?`,"codexSet.custom.keepEditing":`Weiter bearbeiten`,"codexSet.custom.delete":`{title} löschen`,"codexSet.custom.deleteConfirm":`Diese Ebene löschen? Dies kann nicht rückgängig gemacht werden.`,"codexSet.custom.layerGone":`Diese Ebene wurde anderswo entfernt, daher wurde der Editor geschlossen.`,"codexSet.custom.deleteConfirmNamed":`„{title}“ löschen? Dies kann nicht rückgängig gemacht werden.`,"codexSet.custom.moveUp":`{title} nach oben verschieben`,"codexSet.custom.prevLayer":`Vorherige Ebene`,"codexSet.custom.nextLayer":`Nächste Ebene`,"codexSet.custom.navPosition":`{position} / {total}`,"codexSet.custom.moveDown":`{title} nach unten verschieben`,"codexSet.custom.limitReached":`Du kannst bis zu {max} benutzerdefinierte Ebenen speichern.`,"codexSet.custom.notOwned":`developer_instructions wurde außerhalb von opencodex geschrieben und kann daher hier nicht bearbeitet werden. Importiere den Inhalt, um ihn als Ebene zu verwalten.`,"codexSet.custom.adopt":`Vorhandene Anweisungen importieren`,"codexSet.custom.adoptConfirm":`Als Ebene importieren`,"codexSet.custom.adoptRefused":`Der vorhandene Wert konnte nicht importiert werden.`,"codexSet.custom.baseReplaced":`model_instructions_file ist auf {path} gesetzt. Daher wurde der Basis-Prompt außerhalb von opencodex ersetzt.`,"codexSet.lint.identity":`Hier wird eine andere Identität beansprucht als die von Codex festgelegte.`,"codexSet.lint.foreignTool":`Tools stammen aus der Registry; ein Name an dieser Stelle erstellt kein Tool.`,"codexSet.lint.placeholder":`Anweisungen werden nicht von einer Template-Engine verarbeitet. Dieser Inhalt wird daher wörtlich gesendet.`,"codexSet.lint.applyPatch":`apply_patch wird von der Tool-Registry definiert, nicht von Anweisungen.`,"codexSet.lint.approvalVocab":`Codex fügt eigene Begriffe für Genehmigungen ein; dies könnte ihnen widersprechen.`,"codexSet.lint.environment":`Umgebungsdaten werden später erzeugt und könnten dem widersprechen.`,"codexSet.lint.size":`Diese Ebene ist größer als 8 KB. Sie kann trotzdem gespeichert werden, verbraucht aber bei jeder Anfrage Tokens.`,"codexSet.preset.blank":`Leere Ebene`,"codexSet.preset.concise.name":`Knappe Ausgabe`,"codexSet.preset.concise.description":`Kurze Antworten ohne Einleitung und mit minimaler Formatierung.`,"codexSet.preset.concise.provenance":`Angelehnt an die Kürzevorgaben von Claude Code. Eigene Formulierung, keine Kopie.`,"codexSet.preset.planFirst.name":`Vor dem Bearbeiten planen`,"codexSet.preset.planFirst.description":`Zuerst den Plan nennen, dann die Änderung vornehmen.`,"codexSet.preset.planFirst.provenance":`Angelehnt an den Planungsansatz von Claude Code. Eigene Formulierung, keine Kopie.`,"codexSet.preset.explainWhy.name":`Begründung erklären`,"codexSet.preset.explainWhy.description":`Nicht nur sagen, was geschieht, sondern auch warum.`,"codexSet.preset.explainWhy.provenance":`Angelehnt an den Bestätigungsstil von Grok Build. Eigene Formulierung, keine Kopie.`,"codexSet.preset.testFirst.name":`Zuerst testen`,"codexSet.preset.testFirst.description":`Vor der Korrektur einen fehlschlagenden Test schreiben.`,"codexSet.preset.testFirst.provenance":`Angelehnt an gängige Vorgehensweisen von Agenten. Eigene Formulierung, keine Kopie.`,"codexSet.preset.korean.name":`Antworten auf Koreanisch`,"codexSet.preset.korean.description":`Unabhängig von der Sprache der Anfrage auf Koreanisch antworten.`,"codexSet.preset.korean.provenance":`Für opencodex aus einem häufig gewünschten Standard verfasst. Eigene Formulierung, keine Kopie.`,"codexSet.dialog.class":`Art`,"codexSet.dialog.key":`Konfigurationsschlüssel`,"codexSet.dialog.fileValue":`Wert in dieser Datei`,"codexSet.dialog.absentDefault":`nicht festgelegt (Standard: {value})`,"codexSet.dialog.noRenderedText":`Codex legt den zusammengesetzten Text einer integrierten Ebene nicht offen. Dieser Dialog beschreibt daher die Ebene und nennt ihren Schlüssel, statt ihren Inhalt anzuzeigen.`,"codexSet.dialog.sourceText":`An das Modell gesendeter Text`,"codexSet.dialog.sourceBytes":`{bytes} Byte`,"codexSet.dialog.notRendered":`In der gelesenen Runde hat diese Ebene nichts gesendet. Abschnitte werden nur bei Änderungen erneut gesendet, daher kann eine einzelne Stichprobe sie nicht enthalten.`,"codexSet.dialog.emptySource":`Die Datei unter {path} existiert, ist aber leer, daher sendet diese Ebene nichts.`,"codexSet.dialog.notExposed":`Der Basis-Prompt läuft außerhalb der Nachrichtenliste, die Codex ausgeben kann, und lässt sich hier nicht anzeigen. Ersetzen ist über model_instructions_file möglich.`,"codexSet.dialog.textUnavailable":`Der Codex-Prompt konnte auf diesem Rechner nicht gelesen werden, daher ist der Text nicht verfügbar.`,"codexSet.class.base":`Basisanweisungen`,"codexSet.class.config-toggle":`Hier umschaltbar`,"codexSet.class.feature-gated":`Feature-gesteuert`,"codexSet.class.runtime-conditional":`Laufzeitabhängig`,"codexSet.class.extension-unknown":`Erweiterungsebene`,"codexSet.layer.base-instructions":`Basisanweisungen`,"codexSet.layer.model-switch":`Hinweis zum Modellwechsel`,"codexSet.layer.personality":`Persönlichkeit`,"codexSet.layer.context-window-guidance":`Kontextfenster-Hinweise`,"codexSet.layer.realtime":`Echtzeit`,"codexSet.layer.agents-md":`AGENTS.md`,"codexSet.layer.environments-instructions":`Ausführungsumgebungen`,"codexSet.layer.plugins":`Plugins`,"codexSet.layer.tools":`Tools`,"codexSet.layer.multi-agent-mode":`Multi-Agenten-Modus`,"codexSet.layer.git-attribution":`Commit-Attribution`,"codexSet.about.base-instructions":`Codex-eigene Anweisungen. Sie werden mit der Anfrage selbst gesendet und können nicht deaktiviert werden.`,"codexSet.about.model-switch":`Wird hinzugefügt, wenn das Modell während einer Unterhaltung gewechselt wird.`,"codexSet.about.personality":`Vorgaben für Ton und Ausdruck, gesteuert durch ein Feature-Flag.`,"codexSet.about.context-window-guidance":`Hinweise zum verbleibenden Kontextbudget, gesteuert durch ein Feature-Flag.`,"codexSet.about.realtime":`Wird bei Echtzeitsitzungen hinzugefügt.`,"codexSet.about.agents-md":`Die AGENTS.md-Dateien deines Projekts. Diese Seite zeigt die Ebene an, bearbeitet aber nie deine Projektdokumentation.`,"codexSet.about.permissions":`Erläutert die geltenden Sandbox- und Genehmigungseinstellungen.`,"codexSet.about.collaboration":`Erläutert den aktiven Kollaborationsmodus.`,"codexSet.about.environment":`Arbeitsverzeichnis, Plattform und weitere Umgebungsdaten.`,"codexSet.about.environments-instructions":`Vorgaben für verzögerte Ausführungsumgebungen, gesteuert durch ein Feature-Flag.`,"codexSet.about.apps":`Anleitung zur Verwendung verbundener Apps.`,"codexSet.about.plugins":`Wird hinzugefügt, wenn ein Plugin ausgewählt ist oder ein Plugin eine Funktion bereitstellt.`,"codexSet.about.tools":`Verzögert geladene Tool-Beschreibungen, gesteuert durch ein Feature-Flag.`,"codexSet.about.skills":`Liste der verfügbaren Skills.`,"codexSet.about.multi-agent-mode":`Anweisungen für Subagenten, gesteuert durch ein Feature-Flag.`,"codexSet.about.git-attribution":`Lässt das Modell einen Co-authored-by: Codex-Trailer in Commits schreiben, die es erstellt, und eine Zeile Generated with Codex. in Pull Requests, die es öffnet. Codex liest das aus deinem Konto, deshalb ist es weder hier noch unter [features] einstellbar. Ist es im Konto aus, sendet Codex die umgekehrte Anweisung statt gar keine.`,"codexSet.condition.model-switch":`Wird nur nach einem Modellwechsel während der Sitzung ausgegeben.`,"codexSet.condition.realtime":`Wird nur in einer Echtzeitsitzung ausgegeben.`,"codexSet.condition.agents-md":`Wird ausgegeben, wenn für das Arbeitsverzeichnis eine Projektdokumentation gefunden wird.`,"codexSet.condition.plugins":`Wird ausgegeben, wenn ein Plugin ausgewählt ist oder ein Plugin eine Funktion bereitstellt.`,"codexSet.condition.git-attribution":`Wird durch die Attributionsrichtlinie deines Kontos bestimmt.`,"codexSet.base.title":`Basis-Prompt`,"codexSet.base.prev":`Vorherige Option`,"codexSet.base.next":`Nächste Option`,"codexSet.base.position":`{position} / {total}`,"codexSet.base.swipeHint":`Seitlich wischen, die Pfeiltasten oder die Pfeil-Schaltflächen verwenden, um zwischen Optionen zu wechseln. Gilt für neu gestartete Sitzungen.`,"codexSet.base.defaultTitle":`Codex’ eigener Basis-Prompt`,"codexSet.base.defaultBody":`Die Voreinstellung wird hier nicht gespeichert, es gibt also nichts zu bearbeiten oder zu löschen: Ihre Auswahl entfernt einfach model_instructions_file aus der Konfiguration, und Codex nutzt den mitgelieferten Prompt.`,"codexSet.base.variantTitle":`Name`,"codexSet.base.variantBody":`Prompt`,"codexSet.base.replacesWarning":`Das ERSETZT Codex’ eigenen Basis-Prompt, statt ihn zu ergänzen. Ein kurzer Prompt hier bedeutet ein Modell mit kurzen Anweisungen.`,"codexSet.base.use":`Diesen verwenden`,"codexSet.base.inUse":`In Verwendung`,"codexSet.base.externalBlocked":`model_instructions_file zeigt bereits auf {path}, und das hat nicht opencodex geschrieben. Entfernen Sie es selbst, bevor Sie hier auswählen.`,"nav.api":`API`,"nav.integrations":`Integrationen`,"nav.openMenu":`Menü öffnen`,"nav.closeMenu":`Menü schließen`,"integrations.subtitle":`Clients mit opencodex verbinden, Zugangsdaten verwalten und Client-Konfigurationen wiederherstellen.`,"integrations.tabsLabel":`Integrationsbereiche`,"integrations.tab.overview":`Übersicht`,"integrations.tab.keys":`API-Schlüssel`,"integrations.tab.codex":`Codex`,"integrations.tab.claude":`Claude`,"integrations.tab.grok":`Grok Build`,"integrations.tab.opencode":`OpenCode`,"integrations.tab.pi":`Pi`,"integrations.tab.omp":`OMP`,"integrations.tab.hermes":`Hermes`,"integrations.tab.openclaw":`OpenClaw`,"integrations.tab.kimi":`Kimi Code`,"integrations.tab.gajae":`Gajae Code`,"integrations.tab.dsh":`DSH`,"integrations.tab.mcode":`MiniMax Code`,"integrations.tab.zcode":`ZCode`,"integrations.tab.prime":`Prime Agent`,"integrations.tab.aside":`Aside`,"integrations.codex.title":`Codex CLI`,"integrations.codex.body":`Die Codex-Anbindung wird vom Proxy-Dienst verwaltet. Beim Start von opencodex wird sie angewendet; beim Stoppen des Dienstes wird das native Routing wiederhergestellt.`,"integrations.codex.openService":`Dienststeuerung öffnen`,"integrations.state.notInstalled":`Nicht installiert`,"integrations.state.unknown":`Wird geprüft…`,"integrations.detail.codexRouted":`Codex-Anfragen laufen über diesen Proxy`,"integrations.detail.codexAbsent":`Codex läuft noch nicht über diesen Proxy`,"integrations.detail.keyCount":`{count} Schlüssel ausgestellt`,"integrations.detail.keyNone":`Keine Schlüssel ausgestellt`,"integrations.detail.keyChecking":`Wird geprüft…`,"integrations.detail.keyUnavailable":`Schlüsselstatus nicht verfügbar`,"integrations.detail.claudeOff":`Verbindung ist aus`,"integrations.detail.desktopCurrent":`Desktop läuft mit diesem Profil`,"integrations.detail.desktopStale":`Die Profildatei hat sich nach dem Anwenden geändert`,"integrations.detail.desktopNotServed":`Das Profil ist da, Desktop nutzt aber ein anderes`,"integrations.detail.desktopAbsent":`Kein Profil angewendet`,"integrations.detail.desktopDesiredOff":`Die Claude-Desktop-Integration ist deaktiviert`,"integrations.detail.desktopDesiredOffCleanupPending":`Claude Desktop verwendet das Gateway noch; die Bereinigung steht aus`,"integrations.detail.desktopDesiredOnNotApplied":`Die Integration ist aktiviert, aber Desktop verwendet nicht das Gateway-Profil`,"integrations.detail.desktopSelectedElsewhere":`Desktop verwendet ein anderes Profil`,"integrations.detail.desktopProfileDrift":`Das ausgewählte Desktop-Profil wurde geändert`,"integrations.detail.desktopObservedUnsafe":`Das ausgewählte Desktop-Profil kann nicht sicher geändert werden`,"integrations.detail.desktopNotInstalled":`Die Claude-Desktop-Konfigurationsbibliothek ist nicht installiert`,"integrations.dialog.desktop.title":`Claude-Desktop-Integration deaktivieren?`,"integrations.dialog.desktop.changes":`Falls {path} ein von opencodex verwaltetes Gateway-Profil enthält, wählt Desktop zuerst ein neues Standardprofil ohne Zugangsdaten und entfernt danach das alte Profil und dessen Sicherung.`,"integrations.dialog.desktop.breakage":`Claude Desktop verwendet dann statt über opencodex gerouteter Modelle wieder das standardmäßige Claude.`,"integrations.dialog.desktop.undo":`Beim erneuten Aktivieren wird das opencodex-Profil aus deinen gespeicherten Modellzuweisungen neu erstellt.`,"integrations.dialog.desktop.restart":`Claude Desktop liest diese Konfiguration nur beim Start. Beende Desktop vollständig und öffne es erneut, damit die Änderung wirksam wird.`,"integrations.dialog.desktop.confirm":`Deaktivieren`,"integrations.native.error.desktopUnsafeMetadata":`Die Claude-Desktop-Metadaten unter {path} konnten nicht sicher gelesen werden; die Bibliothek wurde nicht geändert.`,"integrations.native.error.desktopCleanupIncomplete":`Claude Desktop zeigt auf den Standardmodus, aber alte opencodex-Zugangsdaten befinden sich noch unter: {paths}.`,"integrations.native.msg.desktopDisabled":`Claude-Desktop-Integration deaktiviert.`,"integrations.native.msg.desktopEnabled":`Claude-Desktop-Integration aktiviert.`,"integrations.detail.grokModels":`{count} Modell(e) verbunden`,"integrations.detail.grokAbsent":`Kein opencodex-Block in der Konfiguration`,"integrations.dialog.grok.title":`Grok-Build-Integration deaktivieren?`,"integrations.dialog.grok.changes":`Aus {path} wird nur der von opencodex markierte Block entfernt. Manuell geschriebener Inhalt außerhalb des Blocks bleibt unverändert.`,"integrations.dialog.grok.breakage":`Nach dem Deaktivieren verschwinden die opencodex-Modellaliase aus Grok Build. Modelle, die mit dem xAI-Konto verwendet wurden, bleiben erhalten.`,"integrations.dialog.grok.undo":`Wenn opencodex unter einer Loopback-Adresse läuft, wird beim erneuten Aktivieren ein neuer Block aus der aktuell verfügbaren Modellliste geschrieben.`,"integrations.dialog.grok.confirm":`Deaktivieren`,"integrations.native.msg.nonLoopbackRemoved":`Grok Build kann nur automatisch registriert werden, wenn opencodex unter einer Loopback-Adresse läuft. Der vorherige Block mit Verweis auf Loopback wurde entfernt.`,"integrations.native.msg.nonLoopbackRemovedNoop":`Grok Build kann nur automatisch registriert werden, wenn opencodex unter einer Loopback-Adresse läuft. Es gab keinen vorherigen Block zu entfernen.`,"integrations.native.msg.nonLoopbackSuperseded":`Grok Build kann nur automatisch registriert werden, wenn opencodex unter einer Loopback-Adresse läuft. Inzwischen hat eine andere Stelle einen neuen Block in die Konfiguration geschrieben; der aktuelle Block in der Datei wurde daher nicht von dieser Anfrage erstellt.`,"integrations.native.error.orphanedMarker":`{path} enthält eine opencodex-Startmarkierung, aber keine Endmarkierung. Die Datei wurde nicht geändert, weil das Ende des Blocks nicht sicher bestimmt werden kann.`,"integrations.native.error.homeMismatch":`Das Home des installierten Dienstes stimmt nicht mit dem aktuellen Home überein; die Datei wurde daher nicht geändert.`,"integrations.native.error.notInstalled":`Grok Build ist nicht installiert; es gibt nichts zu ändern.`,"integrations.native.error.configBusy":`Die Konfiguration wird gerade an anderer Stelle gespeichert und konnte nicht geändert werden. Versuche es in Kürze erneut.`,"integrations.state.absent":`Nicht angewendet`,"integrations.state.current":`Angewendet`,"integrations.state.stale":`Aktualisierung erforderlich`,"integrations.state.conflict":`Konflikt`,"integrations.state.unsafe":`Nicht überprüfbar`,"integrations.summary.detected":`Clients erkannt`,"integrations.summary.applied":`Konfigurierte Clients`,"integrations.summary.stale":`Aktualisierung erforderlich`,"integrations.summary.lastChange":`Letzte Änderung`,"integrations.summary.disableAll":`Alle deaktivieren…`,"integrations.onboarding":`Beim Anwenden wird nach dem Speichern einer Sicherung genau ein opencodex-Anbieterblock geschrieben. Beim Deaktivieren wird nur dieser Block entfernt; eine aufbewahrte Momentaufnahme kann wiederhergestellt werden.`,"integrations.empty.title":`Keine installierten Clients erkannt`,"integrations.empty.body":`Installiere einen unterstützten Client und kehre dann hierher zurück, um opencodex anzuwenden.`,"integrations.action.apply":`Anwenden`,"integrations.action.disable":`Deaktivieren`,"integrations.action.refresh":`Aktualisieren`,"integrations.action.settings":`Einstellungen`,"integrations.action.manageKeys":`Schlüssel verwalten`,"integrations.action.restore":`Wiederherstellen…`,"integrations.action.undo":`Rückgängig`,"integrations.action.restorePoint":`Diesen Stand wiederherstellen…`,"integrations.action.snapshotExpired":`Sicherung abgelaufen`,"integrations.rollback.title":`Wiederherstellungscenter`,"integrations.rollback.empty":`Noch kein Anwendungsverlauf`,"integrations.rollback.emptyBody":`Vor jedem erfolgreichen Schreibvorgang wird zuerst eine Momentaufnahme gespeichert.`,"integrations.catalog.title":`Clients`,"integrations.rollback.older":`Frühere Vorgänge`,"integrations.rollback.showMore":`{n} weitere anzeigen`,"integrations.rollback.failed":`Der Rollback-Verlauf konnte nicht geladen werden.`,"integrations.restore.title":`Diese Momentaufnahme wiederherstellen?`,"integrations.restore.body":`Die aktuelle Datei wird zuerst gesichert und dann durch die ausgewählte Momentaufnahme ersetzt.`,"integrations.restore.driftTitle":`Neuere Änderungen wurden erkannt`,"integrations.restore.driftBody":`Änderungen nach dieser Momentaufnahme werden gesichert; anschließend wird die Datei ersetzt.`,"integrations.restore.confirm":`Wiederherstellen`,"integrations.restore.confirmDrift":`Neuere Änderungen sichern und wiederherstellen`,"integrations.restore.pending":`Wiederherstellung läuft…`,"integrations.restore.manual":`Automatische Wiederherstellung fehlgeschlagen: {reason}. Stelle die Datei manuell aus {path} wieder her.`,"integrations.error.load":`Integrationsstatus konnte nicht geladen werden.`,"integrations.error.stale":`Die letzte Aktualisierung ist fehlgeschlagen. Die folgenden Werte könnten veraltet sein.`,"integrations.error.busy":`Eine andere Änderung für diesen Client läuft noch. Versuche es in Kürze erneut.`,"integrations.error.conflict":`Die Konfiguration wurde geändert, nachdem opencodex sie geschrieben hatte. Es wurde nichts entfernt.`,"integrations.error.unsafe":`Die Konfiguration kann nicht sicher geändert werden.`,"integrations.error.generic":`Die Integrationsänderung ist fehlgeschlagen. Der vorherige Zustand wurde beibehalten.`,"integrations.error.nonLoopback":`{client} erreicht nur einen Proxy auf localhost. In seiner Konfiguration ist kein Platz für den Header, den eine Remote-Bindung verlangt — von Hand geschrieben hilft es also ebenso wenig. Ermöglichen Sie stattdessen Loopback-Zugriff, etwa über einen Tunnel oder lokalen Forwarder.`,"integrations.status.installed":`Installiert`,"integrations.status.notInstalled":`Nicht installiert`,"integrations.status.appliedAt":`Angewendet`,"integrations.status.backup":`Sicherung`,"integrations.status.lastRestore":`Letzte Wiederherstellung`,"integrations.status.unknown":`Unbekannt`,"integrations.bulk.title":`Angewendete Client-Integrationen deaktivieren?`,"integrations.bulk.body":`Nur der opencodex-eigene Block wird entfernt. Für jeden Client wird vorher eine Momentaufnahme gespeichert.`,"integrations.bulk.partial":`Einige Clients konnten nicht deaktiviert werden: {clients}`,"integrations.bulk.success":`Angewendete Client-Integrationen wurden deaktiviert.`,"integrations.retention.degraded":`Die Sicherungsbereinigung ist im Rückstand; ältere Sicherungen könnten noch auf dem Datenträger liegen.`,"integrations.error.residual":`Die Datei könnte sich in einem Zwischenzustand befinden: {message} Stellen Sie sie aus {path} wieder her.`,"integrations.error.recover":`{message} Eine Sicherung liegt unter {path}.`,"integrations.kind.apply":`Angewendet`,"integrations.kind.disable":`Deaktiviert`,"integrations.kind.refresh":`Aktualisiert`,"integrations.kind.restore":`Wiederhergestellt`,"integrations.kind.overwrite":`Überschrieben`,"integrations.dialog.overwrite.title":`Den Block in dieser Konfiguration ersetzen?`,"integrations.dialog.overwrite.changesUnowned":`In {path} belegt ein Block, den nicht wir geschrieben haben, die Stelle, die opencodex braucht. Beim Anwenden wird er durch den Block ersetzt, den opencodex schreibt.`,"integrations.dialog.overwrite.changesForeign":`Deine Änderung im opencodex-Block in {path} wird verworfen und durch den Block ersetzt, den opencodex schreibt.`,"integrations.dialog.overwrite.breakage":`Was der andere Block eingestellt hat, wirkt danach nicht mehr. Der Rest der Datei bleibt unberührt.`,"integrations.dialog.overwrite.undo":`Vorher wird ein Snapshot gespeichert, deshalb steht dieser Schritt unten in der Rollback-Liste und kann zurückgenommen werden.`,"integrations.dialog.overwrite.confirm":`Ersetzen`,"integrations.action.overwrite":`Ersetzen`,"integrations.semantics.opencode":`Gilt nur für direkte Starts von der Festplatte; die Umgebungsinjektion von ocx opencode hat Vorrang.`,"integrations.semantics.pi":`Gilt für neue Sitzungen.`,"integrations.semantics.omp":`Starten Sie OMP neu, um den Katalog zu laden.`,"integrations.semantics.hermes":`Gilt für neue Sitzungen.`,"integrations.semantics.openclaw":`Wird sofort auf ein laufendes Gateway angewendet.`,"integrations.semantics.kimi":`Zum Anwenden neu starten oder /reload ausführen (v2 überwacht die Datei).`,"integrations.semantics.gajae":`Gilt für eine neue Sitzung oder beim Öffnen von /model.`,"integrations.semantics.dsh":`OpenCodex verwaltet nur llm-pi-ai.providers.opencodex in $DSH_HOME/settings.yaml. DSH lädt diesen Anbieter im laufenden Betrieb neu; Ihr Standardmodell und deepseek-official bleiben unverändert. Derzeit nur über Loopback; es werden keine echten Zugangsdaten geschrieben.`,"integrations.semantics.mcode":`Verwaltet nur custom_provider.opencodex. Standardmodell und MiniMax-Anmeldung bleiben unverändert.`,"integrations.semantics.zcode":`Verwaltet nur provider.opencodex in ~/.zcode/v2/config.json. Z.ai-Anmeldung und andere Provider bleiben unverändert. ZCode nach Änderungen neu starten.`,"integrations.semantics.prime":`Verwaltet nur providers.opencodex in der models.json von Prime Agent — ~/.prime/agent, sofern PRIME_AGENT_CODING_AGENT_DIR sie nicht umleitet. Andere Provider und Modell-Overrides bleiben unverändert. Gilt für neue Sitzungen.`,"integrations.semantics.aside":`Verwaltet nur providers.opencodex in der models.json von Aside für das angemeldete Konto (~/.aside/u/). Andere Provider bleiben unverändert. Aside überschreibt diese Datei im laufenden Betrieb, daher nach dem Anwenden vollständig beenden und neu öffnen.`,"codexAuth.mainAccount":`Hauptkonto`,"codexAuth.logLabel":`Log-Kennung`,"codexAuth.codexApp":`Codex App`,"codexAuth.moreActions":`Weitere Aktionen anzeigen`,"codexAuth.copyId":`Konto-ID kopieren`,"codexAuth.appLogin":`App-Login`,"codexAuth.accountPool":`Kontopool`,"codexAuth.accountModeTitle":`OpenAI-Kontomodus`,"codexAuth.accountModePool":`Pool-Modus`,"codexAuth.accountModePoolDesc":`Die Hauptanmeldung und geeignete hinzugefügte Konten wechseln sich hier ab.`,"codexAuth.accountModeDirect":`Direktmodus`,"codexAuth.accountModeDirectDesc":`Anfragen verwenden nur die Hauptanmeldung; hinzugefügte Konten bleiben für den Pool-Modus gespeichert.`,"codexAuth.openaiMissing":`Der integrierte OpenAI-Anbieter ist nicht konfiguriert.`,"codexAuth.openaiDisabled":`Der integrierte OpenAI-Anbieter ist deaktiviert.`,"codexAuth.openaiUnavailableDesc":`Deine OpenAI-Konten sind weiterhin verfügbar. Aktiviere den Anbieter, um Codex-Anfragen weiterzuleiten.`,"codexAuth.enableOpenai":`OpenAI aktivieren`,"codexAuth.enablingOpenai":`Wird aktiviert...`,"codexAuth.enableOpenaiFailed":`OpenAI-Anbieter konnte nicht aktiviert werden.`,"codexAuth.openaiPresetLoadFailed":`OpenAI-Anbieter-Preset konnte nicht geladen werden.`,"codexAuth.openaiPresetUnavailable":`OpenAI-Anbieter-Preset ist nicht verfügbar.`,"codexAuth.openProviders":`Anbieter öffnen`,"codexAuth.add":`Hinzufügen`,"codexAuth.sparkQuota":`Codex-Spark-Kontingent`,"codexAuth.sparkQuotaHint":`Zeigt das GPT-5.3-Codex-Spark-Wochenfenster auf Kontokarten. Standardmäßig ausgeblendet, da es nur für ein Modell gilt.`,"codexAuth.sparkQuotaShown":`Codex-Spark-Kontingent wird angezeigt`,"codexAuth.sparkQuotaHidden":`Codex-Spark-Kontingent ausgeblendet`,"codexAuth.sparkQuotaFailed":`Codex-Spark-Kontingent konnte nicht geändert werden`,"codexAuth.refreshQuota":`Kontingente aktualisieren`,"codexAuth.refreshingQuota":`Aktualisiere…`,"codexAuth.quotaRefreshed":`Kontingente aktualisiert`,"codexAuth.quotaRefreshFailed":`Kontingente konnten nicht aktualisiert werden`,"codexAuth.pauseExhausted":`Ausgeschöpfte pausieren`,"codexAuth.pausingExhausted":`Kontingente werden geprüft…`,"codexAuth.pauseExhaustedSucceeded":`Konten am Limit pausiert: {count}`,"codexAuth.pauseExhaustedNone":`Keine Konten mit bestätigter 100-%-Nutzung.`,"codexAuth.pauseExhaustedFailed":`Ausgeschöpfte Konten konnten nicht geprüft und pausiert werden.`,"codexAuth.noPool":`Noch keine Pool-Konten hinzugefügt.`,"codexAuth.pause":`Pausieren`,"codexAuth.resume":`Fortsetzen`,"codexAuth.paused":`PAUSIERT`,"codexAuth.pauseSucceeded":`{email} ist pausiert`,"codexAuth.resumeSucceeded":`{email} ist wieder im Pool verfügbar`,"codexAuth.pauseFailed":`{email} konnte nicht pausiert werden. Es wurde nichts geändert.`,"codexAuth.resumeFailed":`{email} konnte nicht fortgesetzt werden. Es wurde nichts geändert.`,"codexAuth.pausedHint":`Bis zur Fortsetzung von automatischem Wechsel, Wiederholungen, Cooldown-Wiederherstellung und manueller Auswahl ausgeschlossen.`,"codexAuth.pinned":`ANGEHEFTET`,"codexAuth.pinnedHint":`Du hast dieses Konto von Hand ausgewählt, daher geht eine höhere Auswahlreihenfolge nicht daran vorbei. Die Fixierung gilt, bis dieses Konto aufgebraucht ist, du ein anderes auswählst oder du eine Auswahlreihenfolge änderst.`,"codexAuth.fiveHour":`5 Std.`,"codexAuth.weekly":`Woche`,"codexAuth.monthly":`30d`,"codexAuth.resets":`zurücksetzen`,"codexAuth.today":`Heute`,"codexAuth.current":`AKTUELL`,"codexAuth.nextSession":`AUSGEWÄHLT`,"codexAuth.poolPrepared":`FÜR POOL VORBEREITET`,"codexAuth.preparePoolTitle":`Dieses Konto für den Pool-Modus vorbereiten?`,"codexAuth.preparePoolDesc":`Direkte Anfragen verwenden weiterhin die Hauptanmeldung. Dieses Konto wird zur vorbereiteten Pool-Auswahl, sobald der Pool-Modus aktiviert ist.`,"codexAuth.prepareForPool":`Für Pool vorbereiten`,"codexAuth.poolPreparedToast":`{email} ist für den Pool-Modus vorbereitet`,"codexAuth.switchTitle":`Aktives Konto wechseln?`,"codexAuth.switchDesc":`Wird sofort wirksam. Bereits laufende Anfragen behalten ihr Konto; alles andere wechselt zu diesem Konto, wobei Konten mit derselben Auswahlreihenfolge sich weiterhin abwechseln.`,"codexAuth.cacheWarning":`Prompt-Cache wird beim Kontowechsel zurückgesetzt. Neue Sitzung startet mit leerem Cache.`,"codexAuth.setAsNext":`Dieses Konto als Nächstes verwenden`,"codexAuth.cancel":`Abbrechen`,"codexAuth.switchBack":`Zurück zum Hauptkonto?`,"codexAuth.switchBackDesc":`Wird sofort wirksam. Bereits laufende Anfragen behalten ihr Konto; alles andere wechselt zu deinem App-Login-Konto, wobei Konten mit derselben Auswahlreihenfolge sich weiterhin abwechseln.`,"codexAuth.autoSwitch":`Proaktiver Wechsel nach Nutzung`,"codexAuth.autoSwitchQuotaDesc":`Kontingent: Ab {threshold} % Nutzung kann die nächste Anfrage zu einem geeigneten Konto mit geringerer Nutzung wechseln, auch bei einer bereits gebundenen Aufgabe; Go/Free nutzen nur 30 Tage.`,"codexAuth.autoSwitchQuotaOffDesc":`Der proaktive Wechsel nach Nutzung ist aus. Zuweisung neuer/ungebundener Aufgaben und Fehlerbehebung bleiben aktiv.`,"codexAuth.autoSwitchRoundRobinDesc":`Round-Robin-Zuweisung verwendet diesen Schwellenwert nicht und rotiert weiter neue/ungebundene Aufgaben.`,"codexAuth.autoSwitchFillFirstDesc":`Fill-first: {threshold} % ist der Entleerungspunkt für neue/ungebundene Aufgaben; gesunde gebundene Aufgaben behalten ihr Konto.`,"codexAuth.autoSwitchFillFirstOffDesc":`Fill-first hat keinen nutzungsbasierten Entleerungspunkt für neue/ungebundene Aufgaben; Cooldown, Neuanmeldung und Fehlerbehebung können das Routing weiterhin ändern.`,"codexAuth.failureRecoveryNote":`Fehlerbehebung ist getrennt: Eine Ablehnung vor der Ausgabe mit 429/402, Cooldown, Neuanmeldung, Ausschluss oder konfiguriertes temporäres Failover kann ein anderes geeignetes Konto auswählen.`,"codexAuth.autoSwitchThreshold":`Nutzungsschwelle`,"codexAuth.autoSwitchThresholdAria":`Nutzungsschwelle in Prozent`,"codexAuth.autoSwitchThresholdInc":`Nutzungsschwelle erhöhen`,"codexAuth.autoSwitchThresholdDec":`Nutzungsschwelle verringern`,"codexAuth.autoSwitchLoadFailed":`Die Einstellung für den nutzungsbasierten Wechsel konnte nicht geladen werden.`,"codexAuth.autoSwitchThresholdInvalid":`Gib eine ganze Zahl von 1 bis 100 ein`,"codexAuth.autoSwitchUpdated":`Der proaktive Wechsel nach Nutzung wurde aktualisiert`,"codexAuth.autoSwitchUpdateFailed":`Die Aktualisierung des nutzungsbasierten Wechsels konnte nicht bestätigt werden. Der zuletzt bestätigte Wert wird angezeigt.`,"codexAuth.requestUserInput":`Im Default-Modus nachfragen`,"codexAuth.requestUserInputDesc":`Erlaubt Codex, eine Session im Default-Modus zu pausieren und dir über das request_user_input-Tool Fragen zu stellen.`,"codexAuth.requestUserInputUpdated":`Feature-Flag aktualisiert - gilt für neue Sessions.`,"codexAuth.requestUserInputUpdatedRestart":`Feature-Flag aktualisiert - gilt für neue Sessions. Starte die Codex-App neu, damit es wirksam wird.`,"codexAuth.requestUserInputUpdateFailed":`Feature-Flag konnte nicht aktualisiert werden. Es wurde nichts geändert.`,"codexAuth.requestUserInputLoadFailed":`Feature-Flag konnte nicht aus config.toml gelesen werden.`,"codexAuth.accountPickerTitle":`Ein bestimmtes Codex-Konto in der Modellauswahl verwenden`,"codexAuth.accountPickerOffDesc":`Wenn aktiviert, werden die normalen GPT-Einträge in der Modellauswahl durch je einen Eintrag pro Kontoselektor ersetzt. So kannst du ohne Abmeldung das genaue Konto für eine Unterhaltung wählen. Beim Deaktivieren werden keine Konten entfernt.`,"codexAuth.accountPickerOnDesc":`Jeder Selektor ist eine öffentliche Bezeichnung für ein gespeichertes Konto. Seine Auswahl bindet die Unterhaltung an dieses Konto: keine Pool-Rotation, kein Fallback und keine Änderung des aktiven Pool-Kontos.`,"codexAuth.accountPickerCompatibility":`Die integrierte Codex-App-Anmeldung hat einen eigenen Selektor; generierte Zuordnungen nennen ihn normalerweise main und verwenden bei Bedarf ein kollisionssicheres Suffix wie main-2. Hinzugefügte Konten erhalten stabile, datenschutzfreundliche Bezeichnungen; eigene Selektornamen bleiben erhalten. Bestehende Unterhaltungen und gespeicherte Modellauswahlen werden weiterhin geroutet. Beim Deaktivieren werden nur generierte Einträge ausgeblendet, Selektoren und exakte Routen bleiben bestehen. Einfache GPT-Modell-IDs behalten ihr Pool- oder Direct-Verhalten.`,"codexAuth.accountPickerUpdated":`Kontozielauswahl aktualisiert.`,"codexAuth.accountPickerUpdateFailed":`Die Kontozielauswahl konnte nicht aktualisiert werden. Die zuletzt bestätigte Einstellung wird angezeigt.`,"codexAuth.accountPickerLoadFailed":`Die Einstellung für die Kontozielauswahl konnte nicht geladen werden.`,"codexAuth.accountPickerRefreshFailed":`Diese Einstellung konnte nicht aktualisiert werden. Der zuletzt bestätigte Wert wird weiterhin angezeigt.`,"codexAuth.advancedSettings":`Erweiterte Einstellungen`,"codexAuth.advancedSettingsAria":`Erweiterte Codex-Auth-Einstellungen ein- oder ausblenden`,"codexAuth.catalogRefreshPending":`Die Änderung wurde gespeichert, aber die Aktualisierung des Codex-Modellkatalogs steht noch aus. Führe ocx sync aus, um es erneut zu versuchen.`,"anthropicPool.title":`Claude-Kontenpool (experimentell)`,"anthropicPool.enabledDesc":`Bei 429 wird das Konto gekühlt und umgeschaltet. Neue Sitzungen bevorzugen Nutzung unter {threshold}% ({window}).`,"anthropicPool.enabledNoProactiveDesc":`Bei 429 wird das Konto gekühlt und umgeschaltet. Proaktives nutzungsbasiertes Umschalten ist bei Schwellenwert 0 deaktiviert, aber die Auswahl neuer Sitzungen und die 429-Wiederherstellung verwenden weiterhin das Fenster {window}.`,"anthropicPool.disabledDesc":`Nutzt nur das aktive Claude-Konto. Nur aktivieren, wenn experimentelles Routing akzeptabel ist.`,"anthropicPool.experimentalWarning":`Experimentell und nicht kampferprobt. Anthropic kann Konten einschränken, die wie automatische Multi-Konto-Rotation wirken. Dieselbe Organisation kann Kontingent teilen — Pooling hilft dann nicht. Ausgeschaltet lassen, sofern das Risiko unklar ist.`,"anthropicPool.needTwoAccounts":`Füge mindestens zwei Claude-OAuth-Konten hinzu, bevor du den Pool aktivierst.`,"anthropicPool.threshold":`Nutzungsschwelle für neue Sitzungen`,"anthropicPool.thresholdAria":`Nutzungsschwelle für neue Sitzungen in Prozent`,"anthropicPool.thresholdHelp":`0 deaktiviert die kontingentbasierte Auswahl (nur Affinität + aktives Konto). Standard 80.`,"anthropicPool.thresholdInvalid":`Gib eine ganze Zahl von 0 bis 100 ein`,"anthropicPool.loadFailed":`Claude-Pool-Einstellungen konnten nicht geladen werden.`,"anthropicPool.saveFailed":`Claude-Pool-Einstellungen konnten nicht gespeichert werden.`,"anthropicPool.on":`An`,"anthropicPool.off":`Aus`,"accountPool.strategy":`Rotationsstrategie`,"accountPool.strategyDesc":`Wie OpenCodex einer neuen/ungebundenen Aufgabe ein Konto zuweist.`,"accountPool.strategyQuota":`Kontingent`,"accountPool.strategyRoundRobin":`Round-Robin`,"accountPool.strategyFillFirst":`Fill-first`,"accountPool.strategyHintQuota":`Kontingent kann eine bestehende Aufgabe bei ihrer nächsten Anfrage neu binden, nachdem die Nutzungsschwelle überschritten wurde.`,"accountPool.strategyHintRoundRobin":`Round-Robin rotiert nur Aufgaben ohne aktive Bindung; die Nutzungsschwelle ändert die normale Rotation nicht.`,"accountPool.strategyHintFillFirst":`Fill-first nutzt die Schwelle als Entleerungspunkt für ungebundene Aufgaben; gesunde gebundene Aufgaben behalten ihre Affinität.`,"accountPool.unboundDefinition":`Neue/ungebundene Aufgabe bedeutet eine Anfrage ohne aktuelle Kontobindung; eine sichtbare bestehende Aufgabe kann nach einem Proxy- oder Affinitätsreset ungebunden sein.`,"accountPool.stickyLimit":`Neue/ungebundene Zuweisungen vor Rotation`,"accountPool.stickyLimitAria":`Neue/ungebundene Zuweisungen vor Rotation`,"accountPool.stickyLimitInc":`Sticky-Limit erhöhen`,"accountPool.stickyLimitDec":`Sticky-Limit verringern`,"accountPool.stickyLimitHelp":`So viele neue/ungebundene Aufgaben dem gewählten Konto zuweisen, bevor weitergeschaltet wird; gezählt wird bei der Bindung, nicht nach einem Upstream-Erfolg.`,"accountPool.stickyLimitInvalid":`Gib eine ganze Zahl von 1 bis 100 ein`,"accountPool.strategyLoadFailed":`Rotationsstrategie konnte nicht geladen werden.`,"accountPool.strategyUpdateFailed":`Rotationsstrategie konnte nicht gespeichert werden.`,"accountPool.quotaWindow":`Kontingentfenster`,"accountPool.quotaWindowDesc":`Welcher zwischengespeicherte Nutzungsbalken die kontingentbasierte Auswahl neuer Sitzungen, Fill-first-Schwellenprüfungen und geeignete 429-Ersatzkonten steuert.`,"accountPool.quotaWindowFiveHour":`5-Stunden-Balken`,"accountPool.quotaWindowWeekly":`Wochenbalken`,"accountPool.quotaWindowMaxUtilization":`Höherer Balken`,"accountPool.quotaWindowHint":`Der Wochenbalken überspringt Konten mit erschöpftem 5-Stunden-Balken, solange ein anderes geeignetes Konto verbleibt, greift aber auf sie zurück, wenn keines verbleibt. Gleichstände bevorzugen die geringere 5-Stunden-Nutzung; einzelne Wochenbalken sind erst nach der Abfrage auf der Anbieterseite bekannt.`,"accountPool.quotaWindowInert":`Nur Kontingent — oder Fill-first mit einem Schwellenwert über 0 — bewertet einen Nutzungsbalken; für die aktuelle Rotationsstrategie ändert diese Einstellung daher nichts.`,"accountPool.priority":`Auswahlreihenfolge`,"accountPool.priorityAria":`Auswahlreihenfolge für dieses Konto`,"accountPool.priorityHint":`Höhere Zahlen werden zuerst verwendet. Der Pool geht erst zu einer niedrigeren Zahl über, wenn alle Konten darüber aufgebraucht oder nicht verfügbar sind.`,"accountPool.priorityFirst":`Zuerst`,"accountPool.priorityEarlier":`Früher`,"accountPool.priorityNormal":`Normal`,"accountPool.priorityLater":`Später`,"accountPool.priorityLast":`Zuletzt`,"accountPool.priorityOption":`{name} ({value})`,"accountPool.priorityCustom":`Benutzerdefiniert`,"accountPool.priorityUpdated":`Auswahlreihenfolge für {email} aktualisiert`,"accountPool.priorityUpdateFailed":`Die Auswahlreihenfolge für {email} konnte nicht gespeichert werden. Der zuletzt bestätigte Wert wird angezeigt.`,"codexAuth.switched":`{email} ist für die nächste Anfrage ausgewählt`,"codexAuth.loadFailed":`Die Codex-Kontoeinstellungen konnten nicht geladen werden.`,"codexAuth.switchFailed":`Das Konto konnte nicht gewechselt werden. Die vorherige Auswahl bleibt erhalten.`,"codexAuth.removeConfirm":`{id} entfernen?`,"codexAuth.removeFailed":`Das Konto konnte nicht entfernt werden. Es wurde nichts geändert.`,"codexAuth.addTitle":`Codex-Konto hinzufügen`,"codexAuth.addIdLabel":`Konto-ID (slug)`,"codexAuth.addJsonLabel":`auth.json-Inhalt`,"codexAuth.addHelp":`Kopiere von ~/.codex/auth.json einer anderen Maschine oder nutze codex-auth export.`,"codexAuth.importBtn":`Importieren`,"codexAuth.importInvalidJson":`Ungültiges JSON`,"codexAuth.importMissingTokens":`access_token oder refresh_token fehlen in JSON`,"codexAuth.importMissingId":`Konto-ID ist erforderlich`,"codexAuth.accountAdded":`Konto zum Pool hinzugefügt`,"codexAuth.addPickDesc":`Melde dich mit einem anderen ChatGPT-Konto an, um es zum Pool hinzuzufügen.`,"codexAuth.oauthLogin":`OAuth-Login`,"codexAuth.oauthDesc":`Öffnet ChatGPT-Login im Browser`,"codexAuth.deviceLogin":`Anmeldung per Gerätecode`,"codexAuth.deviceDesc":`Für einen Headless- oder Remote-Proxy: kurzen Code auf einem anderen Gerät eingeben`,"codexAuth.importAuthJson":`auth.json importieren`,"codexAuth.importAuthJsonDesc":`Von einer anderen Codex-Installation oder codex-auth export`,"codexAuth.back":`Zurück`,"codexAuth.oauthAlreadyInProgress":`Login läuft bereits. Schließe es in deinem Browser ab.`,"codexAuth.oauthWaiting":`Warte auf Abschluss des ChatGPT-Logins in deinem Browser…`,"codexAuth.oauthSubmittingCode":`Code wird gesendet…`,"codexAuth.oauthCodeSubmitted":`Code gesendet — warte auf Abschluss der Anmeldung…`,"codexAuth.oauthStatusRetrying":`Beim Prüfen des Login-Status ist ein Netzwerk- oder Proxyfehler aufgetreten — erneuter Versuch…`,"codexAuth.oauthCancelled":`Login wurde abgebrochen.`,"codexAuth.loginFailed":`Login fehlgeschlagen`,"codexAuth.needsReauth":`Erneut anmelden`,"codexAuth.reauthenticate":`Re-authenticate`,"codexAuth.tokenExpired":`Token abgelaufen — dieses Konto erneut authentifizieren`,"codexAuth.mainTokenExpired":`Token abgelaufen — erneut über Codex-App-Login anmelden`,"codexAuth.emailCollision":`Dieses Konto entspricht deinem Haupt-Codex-Login. Nutze ein anderes Konto.`,"codexAuth.resetCreditsTitle":`Gutschriften zurücksetzen`,"codexAuth.resetCreditsAvailable":`Du hast {count} Reset-Gutschrift(en) verfügbar.`,"codexAuth.resetCreditsDesc":`Jede Gutschrift setzt deine aktuellen stündlichen und wöchentlichen Nutzungsgrenzen sofort zurück.`,"codexAuth.noResetCredits":`Du hast keine Reset-Gutschriften.`,"codexAuth.earnCreditsHint":`Gutschriften werden monatlich und über das Empfehlungsprogramm verdient.`,"codexAuth.creditsExpireNote":`Gutschriften verfallen 30 Tage nach Erhalt.`,"codexAuth.useOneCredit":`1 Gutschrift nutzen`,"codexAuth.confirmResetTitle":`Reset-Gutschrift nutzen?`,"codexAuth.confirmResetDesc":`Dies setzt deine aktuellen Ratenbegrenzungen sofort zurück. Du hast noch {count} Gutschrift(en).`,"codexAuth.irreversible":`Diese Aktion kann nicht rückgängig gemacht werden.`,"codexAuth.useCredit":`Gutschrift nutzen`,"codexAuth.redeeming":`Wird zurückgesetzt…`,"codexAuth.resetSuccess":`Ratenbegrenzungen zurückgesetzt! {remaining} Gutschrift(en) übrig.`,"codexAuth.resetSuccessGeneric":`Ratenbegrenzungen zurückgesetzt!`,"codexAuth.resetAlreadyRedeemed":`Diese Gutschrift wurde bereits eingelöst. Gutschriften unverändert.`,"codexAuth.resetNothingToReset":`Kein Ratenbegrenzungs-Fenster muss gerade zurückgesetzt werden.`,"codexAuth.resetNoCredit":`Keine Reset-Gutschriften verfügbar.`,"codexAuth.resetError":`Reset-Gutschrift konnte nicht eingelöst werden. Bitte erneut versuchen.`,"codexAuth.fifoNote":`Die älteste Gutschrift wird zuerst verwendet.`,"codexAuth.confirmWhichCredit":`Gutschrift vom {date} wird verwendet.`,"codexAuth.creditNext":`Als nächstes`,"codexAuth.creditLabel":`Gutschrift #{n}`,"codexAuth.creditNextBadge":`NÄCHSTE`,"codexAuth.creditGranted":`Erhalten {date}`,"codexAuth.creditExpires":`Läuft ab {date} ({days} Tage übrig)`,"api.title":`API-Zugriff`,"api.subtitle":`Mit generierten API-Schlüsseln greifen externe Apps auf den opencodex-Proxy zu. Die Authentifizierung läuft über den {authHeader}-Header; welche Header ein Endpunkt akzeptiert, steht in der Tabelle unten.`,"api.baseUrl":`Basis-URL`,"api.responsesEndpoint":`Responses API`,"api.chatCompletionsEndpoint":`Chat Completions API`,"api.messagesEndpoint":`Messages API`,"api.modelsEndpoint":`Models API`,"api.endpointNote":`Nutze die Basis-URL für OpenAI-kompatible Clients. Responses und Chat Completions liegen unter /v1.`,"api.endpointsTitle":`Gateway-Endpunkte`,"api.authBaseUrlNote":`Konfiguriere Clients mit der Basis-URL und wähle dann den protokollspezifischen Endpunkt unten.`,"api.authTitle":`Authentifizierung`,"api.authLoopback":`Loopback-Binds (127.0.0.1 oder ::1) umgehen die Authentifizierung. Remote-Binds benötigen einen generierten ocx_-Schlüssel oder OPENCODEX_API_AUTH_TOKEN.`,"api.modelsTitle":`Externe Modelle`,"api.modelsCount":`{count} aufrufbar`,"api.modelsSearch":`Modelle suchen`,"api.modelsSubtitle":`Verwende diese exakten Modell-IDs mit /v1/models und dem gewählten eingehenden Protokoll.`,"api.modelsLoading":`Modelle werden geladen…`,"api.modelsEmpty":`Noch keine extern aufrufbaren Modelle verfügbar.`,"api.modelsNoMatch":`Keine Modelle passen zu „{query}“.`,"api.modelsLoadFailed":`Der externe Modellkatalog konnte nicht geladen werden.`,"api.colModel":`Modell`,"api.colSource":`Quelle`,"api.colProtocols":`Protokolle`,"api.copyModelId":`ID kopieren`,"api.modelCopied":`Kopiert`,"api.testModel":`Testen`,"api.testingModel":`Teste…`,"api.testFailed":`Fehlgeschlagen`,"api.protocolResponses":`Responses`,"api.protocolChatCompletions":`Chat Completions`,"api.protocolMessages":`Messages`,"api.sourceNative":`ChatGPT-Pool`,"api.sourceCombo":`Combo`,"api.sourceCustom":`Benutzerdefiniert`,"api.usageResponsesTitle":`Responses-Beispiel`,"api.usageChatTitle":`Chat-Completions-Beispiel`,"api.usageMessagesTitle":`Messages-Beispiel`,"api.testSucceeded":`OK`,"api.newKeyTitle":`Neuer Schlüssel erstellt`,"api.newKeyNote":`Kopiere diesen Schlüssel jetzt — er wird nicht erneut angezeigt.`,"api.copy":`Kopieren`,"api.copied":`Kopiert`,"api.dismiss":`Schließen`,"api.generateTitle":`Schlüssel generieren`,"api.keyNamePlaceholder":`Schlüsselname (optional)`,"api.generate":`Generieren`,"api.generating":`Erstelle…`,"api.activeKeys":`Aktive Schlüssel ({count})`,"api.activeKeysLoading":`Aktive Schlüssel`,"api.noKeys":`Noch keine API-Schlüssel. Erstelle oben einen.`,"api.workspace.sections":`API-Abschnitte`,"api.section.keys":`Schlüssel`,"api.section.connect":`Verbinden`,"api.section.endpoints":`Endpunkte`,"api.section.models":`Modelle`,"api.section.examples":`Beispiele`,"api.workspace.details":`API-Schlüsseldetails`,"api.workspace.keyDetails":`Schlüsseldetails`,"api.workspace.keyPrefix":`Schlüssel-Präfix`,"api.workspace.deleteKey":`Schlüssel löschen`,"api.workspace.deleteConfirm":`Diesen Schlüssel wirklich löschen? Das lässt sich nicht rückgängig machen.`,"api.workspace.usageExamples":`Nutzungsbeispiele`,"api.copyUrlHint":`Klick um URL zu kopieren`,"api.urlCopied":`URL kopiert`,"api.copyExampleHint":`Klick um Beispiel zu kopieren`,"api.exampleCopied":`Beispiel kopiert`,"api.colName":`Name`,"api.colKey":`Schlüssel`,"api.colCreated":`Erstellt`,"api.confirm":`Bestätigen`,"api.deleteAria":`API-Schlüssel löschen`,"api.usageSampleInput":`Hallo, Welt!`,"api.clientConfig.title":`Client-Konfiguration`,"api.clientConfig.rowsLabel":`Client verbinden`,"api.clientConfig.details":`Details`,"api.clientConfig.detailsAria":`Details zur {client}-Konfiguration`,"api.clientConfig.copyAria":`{client}-Konfiguration kopieren`,"api.clientConfig.downloadAria":`{client}-Konfiguration herunterladen`,"api.clientConfig.rowMeta":`{destination} · {count} Modell(e)`,"api.clientConfig.rowError":`Die {client}-Konfiguration konnte nicht erstellt werden.`,"api.clientConfig.copiedAnnounceClient":`{client}-Konfiguration in die Zwischenablage kopiert.`,"api.clientConfig.clientOpencode":`OpenCode`,"api.clientConfig.clientPi":`Pi`,"api.clientConfig.clientOmp":`OMP`,"api.clientConfig.clientHermes":`Hermes`,"api.clientConfig.clientOpenclaw":`OpenClaw`,"api.clientConfig.clientKimi":`Kimi Code`,"api.clientConfig.clientGajae":`Gajae Code`,"api.clientConfig.clientDsh":`DeepSeek Harness (DSH)`,"api.clientConfig.clientMcode":`MiniMax Code`,"api.clientConfig.clientZcode":`ZCode`,"api.clientConfig.clientPrime":`Prime Agent`,"api.clientConfig.clientAside":`Aside`,"api.clientConfig.copy":`Konfiguration kopieren`,"api.clientConfig.download":`Herunterladen`,"api.clientConfig.loading":`Client-Konfiguration wird erstellt…`,"api.clientConfig.jsonLabel":`{client}-Konfiguration`,"api.clientConfig.destination":`Zieldatei`,"api.clientConfig.envHint":`Schlüssel vor dem Start setzen`,"api.clientConfig.mergeWarning":`Führe dies in die Zieldatei ein. Ein Ersetzen würde deine anderen Provider und MCP-Einstellungen entfernen.`,"api.clientConfig.modelCount":`{count} Modell(e) exportiert`,"api.clientConfig.missingLimits":`{count} von {total} Modell(en) haben kein Kontextlimit; der Client verwendet dafür seine eigenen Vorgaben.`,"api.clientConfig.noKeyYet":`Für {env} existiert noch kein Schlüssel. Erzeuge oben einen Schlüssel, bevor du diese Konfiguration außerhalb von Loopback nutzt.`,"api.clientConfig.loadFailed":`Die Modellliste konnte nicht gelesen werden, daher wurde keine Client-Konfiguration erzeugt.`,"api.clientConfig.copiedAnnounce":`Client-Konfiguration in die Zwischenablage kopiert.`,"api.clientConfig.copyFailed":`Client-Konfiguration konnte nicht kopiert werden.`,"api.clientConfig.downloadedAnnounce":`{filename} heruntergeladen. Es hat sich noch nichts geändert — führe die Datei selbst in {destination} ein.`,"api.clientConfig.whereDisclosure":`Wohin diese Datei gehört`,"api.clientConfig.whereBody":`Der Pfad oben ist der globale Speicherort. Eine projektlokale Konfigurationsdatei im Arbeitsverzeichnis hat Vorrang, und der Schlüssel wird aus der in der Konfiguration genannten Umgebungsvariable gelesen — nie aus dieser Datei.`,"api.keysLoadFailed":`API-Schlüssel konnten nicht geladen werden.`,"api.createFailed":`API-Schlüssel konnte nicht erstellt werden.`,"api.deleteFailed":`API-Schlüssel konnte nicht gelöscht werden.`,"api.auth.endpoint":`Endpunkt`,"api.auth.required":`Erforderlich`,"api.auth.accepted":`Akzeptiert`,"api.auth.rejected":`Nicht akzeptiert`,"api.auth.testProtocol":`{protocol} testen`,"api.auth.testNeedsFreshKey":`Für einen authentifizierten Test einen Schlüssel erzeugen und den einmalig angezeigten Wert auf dem Bildschirm lassen.`,"api.key.name":`Schlüsselname`,"api.key.rename":`Umbenennen`,"api.key.saveName":`Namen speichern`,"api.key.renaming":`Wird gespeichert…`,"api.key.renameFailed":`Schlüssel konnte nicht umbenannt werden. Deine Eingabe wurde behalten.`,"api.key.deleting":`Wird gelöscht…`,"api.rotation.title":`Schlüsselrotation`,"api.rotation.description":`Erstellt einen Ersatzschlüssel; der aktuelle Schlüssel bleibt während einer kurzen Übergangszeit gültig.`,"api.rotation.start":`Rotation starten`,"api.rotation.starting":`Wird gestartet…`,"api.rotation.pending":`Die Rotation ist ausstehend. Aktualisiere und prüfe den Client vor dem Abschluss.`,"api.rotation.expires":`Übergangszeit endet:`,"api.rotation.secretOnce":`Ersatzschlüssel — wird nur einmal angezeigt. Vor dem Schließen kopieren.`,"api.rotation.commit":`Rotation abschließen`,"api.rotation.abort":`Rotation abbrechen`,"api.rotation.failed":`Die Rotationsaktion wurde nicht abgeschlossen. Vor einem neuen Versuch aktualisieren.`,"api.rotation.startFailed":`Schlüsselrotation konnte nicht gestartet werden.`,"api.key.copyFailed":`Schlüssel konnte nicht kopiert werden. Vor dem Schließen dieses Panels manuell markieren und kopieren.`,"api.attribution.title":`Zugeordnete Nutzung`,"api.attribution.requests7d":`Anfragen, letzte 7 Tage`,"api.attribution.totalRequests":`Zugeordnete Anfragen gesamt`,"api.attribution.totalRequestsAvailable":`Anfragen im verfügbaren Verlauf`,"api.attribution.sinceAvailable":`Verfügbare Zuordnung seit`,"api.attribution.lastUsed":`Zuletzt verwendet`,"api.attribution.since":`Zuordnung verfügbar seit`,"api.attribution.neverUsed":`Seit Beginn der Zuordnung nicht verwendet`,"api.attribution.unavailable":`Keine Nutzungsdaten`,"api.attribution.unavailableDetail":`Es wurde noch keine Nutzung zugeordnet. Anfragen von vor dem Start der Zuordnung lassen sich nicht rückwirkend zuweisen.`,"api.attribution.ambiguous":`Zwei Schlüssel teilen sich diese ID, daher lässt sich die Nutzung keinem davon zuordnen. Vergib in der Konfigurationsdatei je Schlüssel eine eindeutige ID.`,"api.attribution.railAmbiguous":`doppelte ID`,"claude.subtitle":`GPT, Gemini und andere Modelle in Claude Code verwenden.`,"claude.enabledLabel":`Claude-Verbindung`,"claude.enabledHint":`Wenn aus, kann Claude Code diesen Proxy nicht verwenden.`,"claude.authMode":`Auth-Modus`,"claude.authModeHint":`Subscription erfordert Claude-Konto, Proxy funktioniert ohne Anthropic-Konto`,"claude.authModeSubscription":`Subscription (Claude-Konto)`,"claude.authModeProxy":`Proxy (kein Konto nötig)`,"claude.authModeAuto":`Auto (Claude-Anmeldung erkennen)`,"claude.effectiveMode.label":`Beim nächsten Start aktiv`,"claude.effectiveMode.manual":`Manuell: {mode}`,"claude.effectiveMode.autoPresent":`Auto: Abo — Claude-Anmeldung über {source} gefunden`,"claude.effectiveMode.autoAbsent":`Auto: Proxy-Modus — keine Claude-Anmeldung gefunden`,"claude.effectiveMode.autoUnknown":`Auto: Abo — Anmeldung konnte nicht geprüft werden`,"claude.effectiveMode.admissionKey":`Der API-Schlüssel dieses Proxys wird weiterhin gesendet.`,"claude.authSource.claude-json-oauth":`Claude-Konto`,"claude.authSource.claude-credentials-file":`Anmeldedatei`,"claude.authSource.macos-keychain":`macOS-Schlüsselbund`,"claude.authSource.exported-env":`Umgebungsvariable`,"claude.authSource.unknown":`erkannte Anmeldedaten`,"claude.systemEnv":`Auto-Verbindung`,"claude.systemEnvDesc":`Wenn an, wird claude in jedem Terminal automatisch über den Proxy geleitet.`,"claude.systemEnvUnsupported":`Auto-Verbindung ist nur unter macOS verfügbar. Starten Sie Claude auf diesem System mit {cmd}.`,"claude.systemEnvWarn":`⚠ Die Terminal-App muss vollständig beendet und neu gestartet werden. Nicht empfohlen.`,"claude.fastMode":`Fast Mode (OpenAI)`,"claude.fastModeDesc":`Steuert service_tier für OpenAI-Modelle. ON = Priorität (schneller). OFF = Standard. Auto = Durchleitung.`,"claude.fastAuto":`Auto`,"claude.fastOn":`ON`,"claude.fastOff":`OFF`,"claude.autoContext":`Großen Kontext automatisch nutzen`,"claude.autoContextDesc":`Steuert, wie weit die 1M-Markierung geht. AN: jedes Modell, dessen Fenster den Komprimierungsschwellwert fasst, erhält eine Big-Context-Zeile. AUS: nur echte 1M-Modelle.`,"claude.autoContextInert":`Inaktiv, weil in der Konfigurationsdatei ein alter Kontextgrößen-Wert (maxContextTokens) steht. Dort entfernen, um es wieder zu aktivieren.`,"claude.autoCompactWindow":`Punkt für Auto-Zusammenfassung`,"claude.autoCompactDefault":`{value} (Standard)`,"claude.autoCompactWindowDesc":`Ältere Nachrichten werden an diesem Punkt zusammengefasst. Das eigene Limit jedes Modells wird nie überschritten — 200k-Modelle bleiben unberührt.`,"claude.autoCompactWindowWarn":`Eine Änderung kann GPT-Modelle stören — liegt der Wert über dem echten Modelllimit, kommt es vor der Zusammenfassung zu Fehlern.`,"claude.injectAgents":`Subagenten automatisch registrieren`,"claude.injectAgentsDesc":`Registriert die im Subagenten-Tab gewählten Modelle (plus das aktuelle Standardmodell) als aufrufbare Claude-Code-Agenten (ocx-*). Gilt ab der nächsten Sitzung.`,"claude.webSearchSidecar":`Websuche-Sidecar überschreiben`,"claude.webSearchSidecarHint":`Überschreibt den allgemeinen Websuche-Sidecar für Claude-Code-Anfragen.`,"claude.visionSidecar":`Vision-Sidecar überschreiben`,"claude.visionSidecarHint":`Überschreibt den allgemeinen Vision-Sidecar für Claude-Code-Anfragen.`,"claude.useMainSetting":`Haupteinstellung verwenden`,"claude.sidecarModelPlaceholder":`Modell der Haupteinstellung`,"claude.quickstart":`Erste Schritte`,"claude.quickstartHint":`{cmd} öffnet Claude Code über den Proxy. Dein claude.ai-Login bleibt aktiv.`,"claude.manualEnv":`Manuelle Einrichtung (erweitert)`,"claude.smallFastModel":`Hintergrund-Hilfsmodell`,"claude.smallFastModelHint":`Das Modell für Hintergrundarbeit wie Chat-Zusammenfassungen und Themenerkennung. Auch der haiku-Alias der Subagenten nutzt es. Leer = Claude-Standard (Haiku).`,"claude.smallFastModelAccurateHint":`Das Modell, das Claude Code für Hintergrundaufgaben wie Chat-Zusammenfassungen und Themenerkennung verwendet. Auch der haiku-Alias der Subagenten nutzt es.`,"claude.smallFastModelUnsetOption":`Claude Code wählen lassen (natives Modell)`,"claude.smallFastModelNativeWarning":`Wenn kein Modell gewählt ist, setzt OpenCodex keine Hilfsmodell-Overrides. Claude Code kann dann sein natives Sonnet-Modell verwenden, wodurch Kosten bei deinem nativen Anbieter entstehen können.`,"claude.slotUnset":`Claude-Standard verwenden`,"claude.modelMap":`Modell-Abfangen`,"claude.modelMapHint":`Fängt Anfragen für ein bestimmtes Modell ab und leitet sie an das gewählte Modell um. Standardmäßig leer — wirkt erst mit einer Regel.`,"claude.mapFrom":`Originalmodell (z. B. claude-sonnet-4-5)`,"claude.mapTo":`Ersetzen durch (z. B. gemini/gemini-3-pro)`,"claude.addMapping":`Regel hinzufügen`,"claude.removeMapping":`Regel entfernen`,"claude.aliases":`Verfügbare Modelle`,"claude.aliasesHint":`Modelle, die im /model-Menü von Claude Code erscheinen.`,"claude.aliasProviderOther":`Sonstiges`,"claude.loading":`Lädt…`,"claude.loadFail":`Claude-Einstellungen konnten nicht geladen werden`,"claude.saved":`Gespeichert.`,"claude.saveFailed":`Speichern fehlgeschlagen`,"claude.networkError":`Netzwerkfehler — läuft der Proxy?`,"claude.toggleAria":`Claude-Verbindung umschalten`,"claude.none":`Keine`,"common.close":`Schließen`,"common.ok":`OK`,"app.logoAria":`opencodex-Logo`,"app.claudeOn":`Claude AN`,"app.claudeOff":`Claude AUS`,"usage.dayMon":`Mo`,"usage.dayWed":`Mi`,"usage.dayFri":`Fr`,"usage.heatmap.tooltipTokens":`{tokens} Tokens`,"usage.heatmap.tooltipRequests":`{requests} Anfragen`,"nav.storage":`Speicher`,"storage.title":`Speicher`,"storage.subtitle":`Zeigt, was CODEX_HOME belegt. Die Bereinigung lässt aktive Sitzungen unberührt.`,"storage.loading":`Speicher wird gescannt…`,"storage.empty":`CODEX_HOME ist leer oder fehlt — nichts zu berichten.`,"storage.error":`Speicher-Scan fehlgeschlagen. Prüfe, ob CODEX_HOME auf ein gültiges Verzeichnis zeigt.`,"storage.refresh":`Neu scannen`,"storage.rescanned":`Scan abgeschlossen.`,"storage.card.total":`Gesamtgröße`,"storage.card.files":`Dateien`,"storage.card.home":`CODEX_HOME`,"storage.snapshot.lastScan":`Letzter Scan`,"storage.snapshot.scanning":`Scanne…`,"storage.snapshot.unavailable":`Noch kein Scan.`,"storage.cleanupCard.title":`Speicher freigeben`,"storage.cleanupCard.tabs":`Bereinigungsoptionen`,"storage.cleanupCard.tab.policy":`Richtlinie`,"storage.cleanupCard.tab.quarantine":`Quarantäne`,"storage.cleanup.noArchives":`Keine archivierten Sitzungen zum Bereinigen.`,"storage.section.buckets":`Bereiche`,"storage.section.largest":`Größte Dateien`,"storage.workspace.overview":`Übersicht`,"storage.workspace.selectBucket":`Wähle einen Bucket aus der Liste, um die Aufschlüsselung zu sehen.`,"storage.col.bucket":`Bereich`,"storage.col.size":`Größe`,"storage.col.files":`Dateien`,"storage.col.oldest":`Älteste`,"storage.col.newest":`Neueste`,"storage.col.rows":`DB-Zeilen`,"storage.rows.unknown":`unbekannt (gesperrt)`,"storage.bucket.sessions":`Aktive Sitzungen`,"storage.bucket.archived_sessions":`Archivierte Sitzungen`,"storage.bucket.logs_db":`Log-Datenbank`,"storage.bucket.state_db":`Status-Datenbank`,"storage.bucket.attachments":`Anhänge`,"storage.bucket.deletion_manifests":`Lösch-Manifeste`,"storage.bucket.other":`Sonstiges`,"storage.cleanup.title":`Archivbereinigung`,"storage.cleanup.help":`Entfernt die ältesten archivierten Sitzungen nach Prozentsatz. Aktive Sitzungen werden nie angefasst. Standard ist Quarantäne — Dateien wandern nach CODEX_HOME/.trash.`,"storage.cleanup.slider":`Ältester Archivanteil`,"storage.cleanup.percent":`{percent}%`,"storage.cleanup.preset":`{percent}`,"storage.cleanup.preview":`Vorschau`,"storage.cleanup.confirmTitle":`Archivbereinigung bestätigen`,"storage.cleanup.confirmBody":`Es werden {count} archivierte Datei(en) (~{size}) verarbeitet, die ältesten {percent}%.`,"storage.cleanup.moreFiles":`…und {n} weitere`,"storage.cleanup.permanent":`Dauerhaft löschen (ohne Quarantäne)`,"storage.cleanup.permanentWarn":`Dauerhaftes Löschen kann nicht rückgängig gemacht werden.`,"storage.cleanup.quarantineNote":`Dateien wandern nach .trash unter CODEX_HOME. Du kannst sie im Tab Quarantäne wiederherstellen.`,"storage.cleanup.cancel":`Abbrechen`,"storage.cleanup.confirmQuarantine":`In Quarantäne`,"storage.cleanup.confirmPermanent":`Dauerhaft löschen`,"storage.cleanup.doneQuarantine":`{count} Datei(en) in Quarantäne ({size}).`,"storage.cleanup.donePermanent":`{count} Datei(en) dauerhaft gelöscht ({size}).`,"storage.cleanup.previewFailed":`Vorschau fehlgeschlagen.`,"storage.cleanup.cleanupFailed":`Bereinigung fehlgeschlagen.`,"storage.cleanup.err.codex_busy":`Codex verwendet state.sqlite — beende Codex und versuche es erneut.`,"storage.cleanup.err.stale_preview":`Archivdateien haben sich seit der Vorschau geändert — führe Vorschau erneut aus.`,"storage.cleanup.err.restore_pending_overlap":`Ausgewählte Archive überschneiden sich mit einer unvollständigen Wiederherstellung — zuerst Wiederherstellung abschließen oder erneut versuchen.`,"storage.cleanup.err.referenced_history":`Ausgewählte Archive werden noch von Fork- oder paginierter Historie referenziert.`,"storage.cleanup.err.invalid_digest":`Vorschaudigest fehlt oder ist ungültig.`,"storage.cleanup.err.invalid_mode":`Modus muss quarantine oder permanent sein.`,"storage.cleanup.err.fs_failed":`Dateisystem-Bereinigung fehlgeschlagen. Einige Änderungen können bereits angewendet sein — prüfen Sie CODEX_HOME/.trash und den angezeigten Wiederherstellungspfad.`,"storage.cleanup.err.fs_failed_trash":`Dateisystem-Bereinigung fehlgeschlagen. Einige Änderungen können bereits angewendet sein — prüfen Sie {trashDir} und manifest.json auf wiederherstellbare Dateien.`,"storage.cleanup.err.db_reconcile_failed":`Codex-Statusdatenbank konnte nicht aktualisiert werden.`,"storage.cleanup.err.cleanup_failed":`Bereinigung fehlgeschlagen.`,"storage.trash.title":`Quarantäne`,"storage.trash.help":`Archivierte Sitzungen in CODEX_HOME/.trash. Wiederherstellen legt JSONL-Dateien und Thread-Zeilen zurück.`,"storage.trash.empty":`Keine Quarantäne-Einträge.`,"storage.trash.loading":`Quarantäne wird geladen…`,"storage.trash.col.when":`Quarantäne seit`,"storage.trash.col.files":`Dateien`,"storage.trash.col.size":`Größe`,"storage.trash.col.mode":`Modus`,"storage.trash.col.id":`Eintrag`,"storage.trash.restore":`Wiederherstellen`,"storage.trash.confirmTitle":`Quarantäne-Eintrag wiederherstellen?`,"storage.trash.confirmBody":`{count} Datei(en) (~{size}) aus {id} zurück in archivierte Sitzungen legen.`,"storage.trash.cancel":`Abbrechen`,"storage.trash.confirmRestore":`Wiederherstellen`,"storage.trash.done":`{count} Datei(en) wiederhergestellt ({size}).`,"storage.trash.restoreFailed":`Wiederherstellung fehlgeschlagen.`,"storage.trash.listFailed":`Quarantäne-Einträge konnten nicht geladen werden.`,"storage.trash.mode.quarantine":`quarantine`,"storage.trash.mode.permanent":`permanent (unvollständig)`,"storage.trash.err.codex_busy":`Codex verwendet state.sqlite — beende Codex und versuche es erneut.`,"storage.trash.err.invalid_trash":`Trash-Eintrags-ID fehlt oder ist ungültig.`,"storage.trash.err.missing_trash":`Trash-Eintrag wurde nicht gefunden.`,"storage.trash.err.dest_exists":`Wiederherstellungsziel existiert bereits — entferne oder benenne die Archivdatei um und versuche es erneut.`,"storage.trash.err.fs_failed":`Dateisystem-Wiederherstellung fehlgeschlagen. Einige Dateien können bereits wiederhergestellt sein — prüfe archived_sessions und .trash.`,"storage.trash.err.storage_mutation_busy":`Eine andere Speicher-Bereinigung oder Wiederherstellung läuft — bitte kurz warten.`,"storage.trash.err.db_reconcile_failed":`Codex-Statusdatenbankzeilen konnten nicht wiederhergestellt werden.`,"storage.trash.err.restore_failed":`Wiederherstellung fehlgeschlagen.`,"storage.trash.err.restore_worker_timeout":`Wiederherstellung dauerte zu lange (über 10 Minuten) und wurde abgebrochen.`,"storage.trash.err.restore_worker_aborted":`Wiederherstellung wurde beim Herunterfahren abgebrochen.`,"storage.trash.err.restore_worker_failed":`Wiederherstellungs-Worker ist abgestürzt oder unerwartet fehlgeschlagen.`,"storage.policy.title":`Automatische Bereinigungsrichtlinie`,"storage.policy.help":`Optionale Stapelbereinigung, wenn archivierte Sitzungen einen Schwellwert überschreiten. Standardmäßig aus — wird nie automatisch aktiviert.`,"storage.policy.loading":`Richtlinie wird geladen…`,"storage.policy.loadFailed":`Bereinigungsrichtlinie konnte nicht geladen werden.`,"storage.policy.saveFailed":`Bereinigungsrichtlinie konnte nicht gespeichert werden.`,"storage.policy.runFailed":`Richtlinienlauf fehlgeschlagen.`,"storage.policy.alreadyRunning":`Ein Bereinigungsrichtlinienlauf läuft bereits.`,"storage.policy.invalid":`Ungültige Richtlinienwerte.`,"storage.policy.enabled":`Automatische Bereinigung aktivieren`,"storage.policy.enabledHint":`Standard ist aus. Bei Aktivierung nur nach gewähltem Zeitplan (oder Jetzt ausführen).`,"storage.policy.threshold":`Wenn Archivgröße größer als (GiB)`,"storage.policy.trigger":`Auslöser`,"storage.policy.target":`Bereinigungsziel`,"storage.policy.targetPercent":`Älteste Archive entfernen (%)`,"storage.policy.targetReduce":`Archivgröße reduzieren auf (GiB)`,"storage.policy.thresholdInc":`Schwellwert erhöhen`,"storage.policy.thresholdDec":`Schwellwert verringern`,"storage.policy.percentInc":`Prozent erhöhen`,"storage.policy.percentDec":`Prozent verringern`,"storage.policy.reduceInc":`Zielgröße erhöhen`,"storage.policy.reduceDec":`Zielgröße verringern`,"storage.policy.schedule":`Zeitplan`,"storage.policy.schedule.manual":`Nur manuell`,"storage.policy.schedule.startup":`Beim Proxy-Start`,"storage.policy.schedule.daily":`Täglich`,"storage.policy.schedule.weekly":`Wöchentlich`,"storage.policy.mode":`Löschmodus`,"storage.policy.mode.quarantine":`Quarantäne (Standard)`,"storage.policy.mode.permanent":`Endgültig löschen`,"storage.policy.permanentWarn":`Endgültiger Modus kann nicht rückgängig gemacht werden. Quarantäne bevorzugen, sofern unsicher.`,"storage.policy.lastRun":`Letzter Lauf`,"storage.policy.lastRunDetail":`{count} entfernt · {size} freigegeben`,"storage.policy.nextRun":`Nächster Lauf`,"storage.policy.never":`Nie`,"storage.policy.save":`Speichern`,"storage.policy.runNow":`Jetzt ausführen`,"storage.policy.running":`Läuft…`,"storage.policy.saved":`Richtlinie gespeichert.`,"storage.policy.skippedDisabled":`Richtlinie ist deaktiviert — zuerst aktivieren.`,"storage.policy.skippedUnder":`Archivgröße unter dem Schwellwert — nichts zu tun.`,"storage.policy.skippedEmpty":`Keine Archivkandidaten passend zum Ziel.`,"storage.policy.doneQuarantine":`Richtlinie hat {count} Datei(en) in Quarantäne ({size}).`,"storage.policy.donePermanent":`Richtlinie hat {count} Datei(en) endgültig gelöscht ({size}).`,"storage.policy.metadataSaveWarning":`Der Richtlinienlauf wurde beendet, aber seine Planungsmetadaten konnten nicht gespeichert werden.`,"modal.back":`Zurück`,"modal.badge.oauth":`OAuth`,"modal.customProvider":`Benutzerdefinierter Anbieter`,"modal.failedStatus":`Fehlgeschlagen ({status})`,"modal.loginError":`Login-Fehler: {error}`,"modal.badge.codexLogin":`Codex-Login`,"modal.badge.local":`Lokal`,"modal.badge.apiKey":`API-Schlüssel`,"modal.badge.direct":`Direct`,"modal.badge.pool":`Pool`,"modal.badge.free":`Kostenlos`,"modal.invalidPreset":`Diese integrierte Anbietervorlage ist unvollständig. Starten Sie den Proxy neu und versuchen Sie es erneut.`,"modal.freeTierTitle":`Kostenloser Tarif`,"modal.freeTierDefault":`Kein API-Schlüssel nötig. Funktioniert sofort.`,"modal.tab.accounts":`Konten`,"modal.tab.free":`Kostenlos`,"modal.tab.paid":`Bezahlt`,"modal.accountsHint":`Hier ChatGPT/Codex, OAuth-Provider und API-Key-Konten anmelden. OpenAI ist eingebaut — anmelden statt erneut hinzufügen.`,"modal.accountsCodexAuthLink":`Codex Auth`,"modal.notListed":`Provider nicht dabei? Eigenen hinzufügen`,"modal.catalogLoading":`Katalog wird geladen…`,"modal.accountLogin":`Anmelden`,"modal.accountLogout":`Abmelden`,"modal.accountAdd":`Konto hinzufügen`,"modal.accountManage":`Verwalten`,"modal.accountCodexPool":`ChatGPT-Kontopool`,"modal.accountLoggedIn":`Angemeldet`,"modal.accountLoggedOut":`Nicht angemeldet`,"quota.fiveHourLimit":`5-Stunden-Limit`,"quota.ageMinutes":`{n} Min.`,"quota.ageHours":`{n} Std.`,"quota.ageDays":`{n} T.`,"quota.observedAgo":`Vor {age} erfasst`,"quota.observedHint":`Meta meldet die Nutzung nur während einer Streaming-Antwort. Dies ist der zuletzt erfasste Wert, keine Live-Messung.`,"quota.weeklyLimit":`Wochenlimit`,"quota.monthlyLimit":`30-Tage-Limit`,"quota.cursorFirstParty":`Erstanbieter-Modelle`,"quota.cursorApiUsage":`API-Nutzung`,"quota.totalSubscriptionCredits":`Gesamtes Abo-Guthaben`,"quota.creditsBalance":`Guthabenstand`,"quota.creditsPeriodEnds":`Abrechnungszeitraum endet am {date}`,"quota.usedPercent":`{pct} % genutzt`,"quota.limitReached":`Limit erreicht`,"quota.resetsToday":`Zurücksetzung heute um {time}`,"quota.resetsTomorrow":`Zurücksetzung morgen um {time}`,"quota.resetsAt":`Zurücksetzung {when}`,"quota.resetsRelativeMinutes":`Zurücksetzung in {n} Min.`,"quota.resetsRelativeHours":`Zurücksetzung in {n} Std.`,"pws.status.ready":`Bereit`,"pws.status.needsSetup":`Einrichtung nötig`,"pws.status.needsAttention":`Aufmerksamkeit nötig`,"pws.auth.chatgptPassthrough":`ChatGPT-Passthrough`,"pws.auth.noKey":`Kein Schlüssel nötig`,"pws.freeTitle":`Kostenlos (Schlüssel ggf. erforderlich)`,"pws.localTitle":`Lokale Laufzeit`,"pws.modelCountOne":`1 Modell`,"pws.modelCount":`{count} Modelle`,"pws.rail.suffixDefault":` · Standard`,"pws.rail.suffixLocal":` · lokal`,"pws.rail.suffixFree":` · kostenlos`,"pws.rail.selectAria":`{name} auswählen — {status}{suffix}`,"pws.searchPlaceholder":`Provider durchsuchen…`,"pws.filterAria":`Provider filtern`,"pws.providerFiltersAria":`Provider-Filter`,"pws.filters":`Filter`,"pws.filterStatus":`Status`,"pws.pricing":`Preis`,"pws.paid":`Bezahlt`,"pws.filterType":`Typ`,"pws.type.cloud":`Cloud`,"pws.type.local":`Lokal`,"pws.type.selfHosted":`Selbst gehostet`,"pws.type.login":`Login`,"pws.sort":`Sortierung`,"pws.sortProvidersAria":`Provider sortieren`,"pws.sort.az":`A–Z`,"pws.sort.za":`Z–A`,"pws.sort.freePaid":`Kostenlos zuerst`,"pws.sort.paidFree":`Bezahlt zuerst`,"pws.sort.accountsFirst":`Konten zuerst`,"pws.resetAll":`Alle zurücksetzen`,"pws.providerList":`Provider-Liste`,"pws.providersAria":`Provider`,"pws.groupReady":`Bereit ({count})`,"pws.groupNeedsSetup":`Einrichtung nötig ({count})`,"pws.groupDisabled":`Deaktiviert ({count})`,"pws.noSearchResults":`Keine Provider entsprechen der Suche.`,"pws.noMatchFilters":`Keine Provider entsprechen den Filtern.`,"pws.noProvidersConfigured":`Keine Provider konfiguriert.`,"pws.workspaceMainAria":`Provider-Details`,"pws.detailComingSoon":`Detailansicht folgt — nutze die klassische Ansicht zur Verwaltung.`,"pws.selectPrompt":`Wähle einen Provider aus der Liste.`,"pws.connectFirst":`Verbinde deinen ersten Provider`,"pws.empty.browseFree":`Kostenlose Provider ansehen`,"pws.empty.browseFreeDesc":`Ohne Abo starten`,"pws.empty.connectAccount":`Konto verbinden`,"pws.empty.connectAccountDesc":`ChatGPT- oder Provider-Login nutzen`,"pws.empty.addEndpoint":`Endpunkt hinzufügen`,"pws.empty.addEndpointDesc":`Eigene Base-URL und API-Schlüssel`,"pws.tab.overview":`Übersicht`,"pws.tab.models":`Modelle`,"pws.tab.usage":`Nutzung`,"pws.tab.accounts":`Konten`,"pws.tab.settings":`Einstellungen`,"pws.connection":`Verbindung`,"pws.status.connected":`Verbunden`,"pws.attentionTitle":`Aufmerksamkeit nötig`,"pws.attention.reauth":`Aktives Konto muss erneut authentifiziert werden`,"pws.attention.reauthForward":`Aktives Codex-Konto muss erneut authentifiziert werden — unter Konten beheben`,"pws.attention.missingCredentials":`Anmeldedaten fehlen`,"pws.cell.auth":`Authentifizierung`,"pws.cell.note":`Notiz`,"pws.cell.defaultModel":`Standardmodell`,"pws.statsAria":`Provider-Statistiken`,"pws.statsTitle":`Statistiken`,"pws.stats.totalRequests":`Anfragen (30 T.)`,"pws.stats.totalTokens":`Tokens (30 T.)`,"pws.stats.quotaUpdated":`Kontingent aktualisiert`,"pws.stats.quotaTracked":`Limits siehe Nutzungs-Tab.`,"pws.stats.source":`Quelle`,"pws.usageLast30d":`Nutzung (letzte 30 Tage)`,"pws.estimatedCost":`Geschätzte Kosten`,"pws.costDisclaimer":`Schätzung basierend auf API-Listenpreisen, keine tatsächliche Abrechnung.`,"pws.modelBreakdown":`Modellaufschlüsselung`,"pws.col.model":`Modell`,"pws.col.cost":`Gesch. Kosten`,"pws.col.tokens":`Token`,"pws.col.requests":`Anfr.`,"pws.col.share":`Anteil`,"pws.tokenInput":`Eingabe`,"pws.tokenOutput":`Ausgabe`,"pws.metricRequests":`Anfragen`,"pws.metricTokens":`Tokens`,"pws.usageUnavailable":`Noch keine Nutzung erfasst.`,"pws.rateLimits":`Limits`,"pws.quotaUnavailable":`Keine Kontingentdaten für diesen Provider.`,"pws.accountQuotaUnavailable":`Ratenlimit-Daten vorübergehend nicht verfügbar; falls vorhanden, werden zuletzt bekannte Werte angezeigt.`,"pws.selected":`Ausgewählt`,"pws.copyModelId":`ID kopieren`,"pws.modelCopied":`Kopiert!`,"pws.modelsAvailable":`{count} verfügbar`,"pws.modelSearchPlaceholder":`Modelle filtern…`,"pws.modelsLoading":`Modelle werden geladen…`,"pws.modelsLoadFailed":`Modelle konnten nicht geladen werden.`,"pws.modelsNeedsReauth":`Konto muss neu angemeldet werden, bevor Live-Modelle geladen werden. Zeige konfigurierte Modelle.`,"pws.modelsConfiguredFallback":`Zeige konfigurierte Modelle (Live-Erkennung nicht verfügbar).`,"pws.modelsTruncated":`Zeige die ersten {shown} von {total} Modellen. Filtern, um die Liste einzugrenzen.`,"pws.retry":`Erneut versuchen`,"pws.noModels":`Keine Modelle für diesen Provider gefunden.`,"pws.noModelMatch":`Keine Modelle entsprechen dem Filter.`,"pws.adapterBaseRequired":`Adapter und Basis-URL sind erforderlich.`,"pws.addAccount":`Konto hinzufügen`,"pws.addKey":`API-Schlüssel hinzufügen`,"pws.apiKeys":`API-Schlüssel`,"pws.authMode":`Auth-Modus`,"pws.availableAccounts":`Verfügbare Konten`,"pws.accountOrdinal":`Konto {count}`,"pws.accountsLoading":`Konten werden geladen…`,"pws.accountsLoadFailed":`Konten konnten nicht geladen werden.`,"pws.retryAccounts":`Erneut versuchen`,"pws.noAccounts":`Noch keine Konten verbunden.`,"pws.cockpitImportDescription":`Importieren Sie einen Cockpit-Tools-Antigravity-JSON-Export von diesem Gerät. Der Dateiinhalt wird nicht angezeigt.`,"pws.cockpitImportFileLabel":`Cockpit-Tools-Antigravity-JSON-Export`,"pws.cockpitImportChooseFile":`JSON-Datei auswählen`,"pws.cockpitImporting":`Import wird ausgeführt…`,"pws.cockpitImportInvalid":`Die ausgewählte Datei ist kein gültiger JSON-Export oder zu groß.`,"pws.cockpitImportFailed":`Der Kontoimport konnte nicht abgeschlossen werden.`,"pws.cockpitImportComplete":`Import abgeschlossen: {imported} importiert, {updated} aktualisiert, {failed} fehlgeschlagen, {unsupported} nicht unterstützt.`,"pws.accountSwitching":`Wechsel läuft…`,"pws.accountCurrent":`Aktuelles Konto`,"pws.defaultModelNone":`Keins (Standard des Anbieters verwenden)`,"pws.discardSettings":`Verwerfen`,"pws.jsonEditorDesc":`Bearbeiten Sie die JSON-Konfiguration des Anbieters. Änderungen werden sofort gespeichert.`,"pws.jsonEditorTitle":`JSON-Editor — {name}`,"pws.jsonRestore":`Wiederherstellen`,"pws.jsonSave":`Speichern`,"pws.loggedInTitle":`Angemeldet`,"pws.notLoggedInTitle":`Nicht angemeldet`,"pws.note":`Notiz`,"pws.allowPrivateNetwork":`Lokales/privates Netzwerk erlauben`,"pws.liveModels":`Modelle beim Anbieter erkennen`,"pws.liveModelsDesc":`Lädt den Live-Modellkatalog des Anbieters. Ausschalten, um nur konfigurierte statische Modelle zu verwenden.`,"pws.xaiResponsesOptIn":`Responses API für Grok 4.5 und 4.6 verwenden`,"pws.xaiResponsesOptInDesc":`Leitet beide Modelle über openai-responses. Andere Grok-Modelle und das Tier-Verhalten bleiben unverändert.`,"pws.xaiResponsesOptInMixed":`Teilweise aktiviert.`,"pws.cursorTransport":`Cursor-Transport`,"pws.cursorTransportHttp2":`HTTP/2 (Standard)`,"pws.cursorTransportHttp1":`HTTP/1.1 (Proxy-Kompatibilität)`,"pws.cursorTransportDesc":`Verwenden Sie HTTP/1.1, wenn Ihr Proxy den HTTP/2-Stream von Cursor nicht zuverlässig überträgt.`,"pws.optionalPlaceholder":`Optional`,"pws.providerId":`Anbieter-ID`,"pws.reauth":`Erneute Anmeldung erforderlich`,"pws.reauthenticate":`Erneut authentifizieren`,"pws.copyDoctor":`ocx doctor kopieren`,"pws.doctorCopied":`Kopiert`,"pws.healthCooldownHint":`Warten Sie, bis die Abkühlzeit endet. Prüfen Sie dieses Konto noch nicht.`,"pws.doctorCopyUnavailable":`Zwischenablage nicht verfügbar`,"pws.healthLabel.rateLimited":`Ratelimit`,"pws.healthLabel.quotaLimited":`Kontingent begrenzt`,"pws.healthLabel.reauthRequired":`Erneute Anmeldung erforderlich`,"pws.healthLabel.refreshFailed":`Aktualisierung fehlgeschlagen`,"pws.healthLabel.metadataMismatch":`Metadaten stimmen nicht überein`,"pws.healthLabel.credentialConflict":`Anmeldedaten-Konflikt`,"pws.healthSummary.rateLimited":`{provider} {account}: ratelimited bis {until}. Routing für dieses Konto ist bis dahin pausiert.`,"pws.healthSummary.quotaLimited":`{provider} {account}: Kontingent begrenzt bis {until}. Routing für dieses Konto ist bis dahin pausiert.`,"pws.healthSummary.reauthRequired":`{provider} {account}: erneute Anmeldung erforderlich.`,"pws.healthSummary.credentialConflict":`{provider} {account}: Anmeldedaten-Konflikt.`,"pws.healthSummary.metadataMismatch":`{provider} {account}: Metadaten stimmen nicht überein.`,"pws.healthSummary.staleCredentials":`{provider} {account}: unvollständige Anmeldedaten.`,"pws.removeConfirm":`Entfernen`,"pws.removeConfirmBody":`Anbieter "{name}" entfernen? Dies kann nicht rückgängig gemacht werden.`,"pws.removeDefaultConfirmBody":`Standardanbieter "{name}" entfernen? "{defaultProvider}" wird zum Standardanbieter. Dies kann nicht rückgängig gemacht werden.`,"pws.removeConfirmTitle":`Anbieter entfernen`,"pws.saveSettings":`Speichern`,"pws.pacingTitle":`Anfragetaktung`,"pws.pacingDesc":`Verteilt ausgehende Anfragestarts für diesen Anbieter gleichmäßig. Streaming-Antworten dürfen sich überlappen.`,"pws.pacingEnabled":`Aktiviert`,"pws.pacingRpm":`Anfragen pro Minute`,"pws.pacingRpmUnit":`RPM`,"pws.pacingDelay":`Mindestintervall (ms)`,"pws.pacingSlowerWins":`Das langsamere Anbieterlimit gilt. Modellregeln können nur stärker verzögern.`,"pws.pacingQueued":`in Warteschlange`,"pws.pacingNextSlot":`bis zum nächsten Slot`,"pws.pacingLastModel":`letztes Modell`,"pws.pacingNone":`Keine`,"pws.pacingModelOverrides":`Modellregeln`,"pws.pacingModel":`Modell`,"pws.pacingAdd":`Regel hinzufügen`,"pws.pacingRemove":`Entfernen`,"pws.pacingRemoveModel":`Anfragetaktung für {model} entfernen`,"pws.pacingRuleRequired":`Legen Sie zuerst ein Anbieterlimit oder eine Modellregel fest.`,"pws.saving":`Wird gespeichert…`,"pws.settingsSaved":`Einstellungen gespeichert.`,"pws.accountModeSaved":`Kontomodus gespeichert.`,"pws.accountModeFailed":`Kontomodus konnte nicht gewechselt werden.`,"pws.accountModeConfirm":`OpenAI-Kontomodus wechseln? Laufende Unterhaltungen werden dem anderen Kontosatz zugeordnet und die Quotennutzung wird unter dem neuen Modus erfasst.`,"pws.settingsUnsavedBar":`Es gibt ungespeicherte Änderungen.`,"pws.unsavedLeaveBody":`Es gibt ungespeicherte Änderungen. Vor dem Verlassen speichern?`,"pws.unsavedLeaveTitle":`Ungespeicherte Änderungen`,"pws.attentionRequired":`Aufmerksamkeit erforderlich`,"pws.attentionAria":`{name}: {reason}`,"pws.missingCredentials":`Zugangsdaten fehlen`,"pws.editJsonDesc":`Rohe Proxy-Konfiguration als JSON bearbeiten`,"pws.updatesUnavailable":`Anbieter-Updates sind nicht verfügbar.`,"pws.dashboard.title":`Anbieterübersicht`,"pws.dashboard.subtitle":`Verwalten Sie alle Ihre Modellanbieter an einem Ort.`,"pws.dashboard.rateLimits":`RATE LIMITS`,"pws.capacity.estimate":`Pool-Schätzung anhand konfigurierter Gewichtungen`,"pws.capacity.currentAccount":`Aktuelles effektives Konto`,"pws.capacity.nextRecovery":`Nächste Kapazitätswiederherstellung`,"pws.capacity.recoveryShare":`+{percent} % Pool-Kapazität`,"pws.capacity.incomplete":`Unvollständige Abdeckung: {excluded} Konten ausgeschlossen`,"pws.capacity.uncalibratedPlan":`{count} Konten mit unkalibriertem Tarif werden mit dem Basisgewicht gezählt; diese Schätzung kann daher konservativ sein`,"pws.capacity.partial":`Teilweise Fensterabdeckung: {count} Konten melden nicht jedes angezeigte Limitfenster`,"pws.capacity.windowPartial":`Teilweise`,"pws.capacity.windowPartialA11y":`{window}: unvollständige Kontoabdeckung`,"pws.dashboard.recentlyUsed":`KÜRZLICH VERWENDET`,"pws.dashboard.requests":`{count} Anfragen`,"pws.dashboard.checkedAgo":`Geprüft {time}`,"pws.dashboard.noQuota":`Keine Kontingentdaten`,"pws.dashboard.noUsage":`Noch keine Nutzungsdaten`,"pws.dashboard.noRateLimits":`Noch keine Limit-Daten`,"pws.allProviders":`Anbieterübersicht`,"pws.enabledLabel":`Aktiviert`,"pws.testConnection":`Verbindung testen`,"pws.testing":`Teste…`,"pws.connectionOk":`Verbindung OK`,"pws.connectionFailed":`Verbindung fehlgeschlagen`,"pws.connectionNotApplicable":`Nicht zutreffend — dieser Anbieter verwendet einen statischen Modellkatalog.`,"pws.editSettings":`Einstellungen bearbeiten`,"pws.viewUsage":`Detaillierte Nutzung anzeigen`,"pws.allSystemsOk":`Alle Systeme betriebsbereit`,"pws.apiKeyConfigured":`API-Schlüssel konfiguriert`,"pws.addApiKey":`API-Schlüssel hinzufügen`,"pws.loggedInAs":`Angemeldet als {email}`,"pws.notLoggedIn":`Nicht angemeldet`,"pws.passthrough":`Codex-Passthrough`,"pws.notes":`NOTIZEN`,"pws.notePlaceholder":`Notiz zu diesem Anbieter hinzufügen...`,"pws.noteSaved":`Notiz gespeichert`,"pws.authSummary":`AUTHENTIFIZIERUNG`,"time.justNow":`Gerade eben`,"time.notChecked":`Nicht geprüft`,"time.minutesAgo":`vor {n} Min.`,"time.hoursAgo":`vor {n} Std.`,"time.daysAgo":`vor {n} T.`,"modal.noMatch":`Kein Treffer.`,"modal.oauthDefaultNote":`Mit deinem Konto anmelden — kein API-Schlüssel nötig.`,"modal.oauthComingSoon":`OAuth-Login für {label} kommt im nächsten Update. Nutze vorerst einen API-Schlüssel.`,"modal.oauthComingSoonShort":`OAuth-Login für diesen Anbieter kommt im nächsten Update — nutze vorerst einen API-Schlüssel.`,"modal.useApiKeyInstead":`Stattdessen API-Schlüssel verwenden`,"modal.setupGuide":`Einrichtungsanleitung`,"modal.setupStep1Prefix":`Gehe zu`,"modal.setupDashboardLink":`{label}-Dashboard`,"modal.setupStep1Suffix":`und kopiere deinen API-Schlüssel`,"modal.setupStep2":`Füge ihn unten in das API-Schlüssel-Feld ein`,"modal.setupStep3":`Klicke auf Anbieter hinzufügen — Modelle werden automatisch erkannt`,"modal.namePlaceholder":`z. B. openrouter`,"modal.duplicateWarn":`Anbieter "{name}" existiert und wird überschrieben.`,"modal.forwardHintPrefix":`Kein Schlüssel nötig — der Proxy leitet deine`,"modal.forwardCredentials":`codex login`,"modal.forwardHintSuffix":`Anmeldedaten an diesen Anbieter weiter.`,"modal.localHint":`Es wird kein API-Schlüssel gespeichert. Damit wird Cursors öffentlicher Modellkatalog für Codex hinzugefügt; live Cursor-Transport und native Datei-/Shell-Ausführung bleiben deaktiviert, bis sie geprüft sind.`,"modal.getApiKey":`{label}-API-Schlüssel holen`,"modal.apiKey":`API-Schlüssel`,"modal.apiKeyTransport":`API-Schlüssel-Header`,"modal.apiKeyTransportNative":`x-api-key (Anthropic-Standard)`,"modal.apiKeyTransportBearer":`Authorization: Bearer`,"modal.apiKeyPlaceholder":`sk-… (oder $ENV_VAR)`,"modal.defaultModelPlaceholder":`z. B. gpt-5.5`,"modal.baseUrlPlaceholder":`https://...`,"modal.baseUrlPlaceholderError":`Base-URL enthält einen ungelösten {placeholder}. Ersetze ihn durch deinen tatsächlichen Wert.`,"modal.baseUrlPlaceholderHint":`Ersetze den {placeholder} in der Base-URL durch deine tatsächliche Account-ID, bevor du hinzufügst.`,"modal.adding":`Wird hinzugefügt…`,"modal.useOauthLogin":`← OAuth-Login verwenden`,"codexAuth.addIdPlaceholder":`codex-work, codex-alt, team…`,"codexAuth.resetCreditsAria":`{count} Reset-Guthaben`,"claude.pageTitle":`Claude Code`,"claude.workspace.settings":`Einstellungen`,"cws.loading":`Combos werden geladen…`,"cws.loadFailed":`Combos konnten nicht geladen werden.`,"cws.saveFailed":`Combo konnte nicht gespeichert werden.`,"cws.removeFailed":`Combo konnte nicht entfernt werden.`,"cws.saved":`Combo gespeichert.`,"cws.created":`{model} erstellt.`,"cws.removed":`combo/{id} entfernt.`,"cws.renamed":`{from} wurde in {to} umbenannt.`,"cws.add":`Combo hinzufügen`,"cws.addTitle":`Combo hinzufügen`,"cws.addSubtitle":`Erstellen Sie ein virtuelles Modell über mehrere Anbieter und wählen Sie den exakten Modellnamen für Clients.`,"cws.create":`Combo erstellen`,"cws.railAria":`Combo-Liste`,"cws.searchPlaceholder":`Combos oder Ziele suchen…`,"cws.noSearchResults":`Keine Combos passen zur Suche.`,"cws.group.failover":`Failover`,"cws.group.roundRobin":`Round-Robin`,"cws.group.other":`Weitere Strategien`,"cws.targetCount":`{count} Ziele`,"cws.targetCountOne":`1 Ziel`,"cws.overviewTitle":`Combos`,"cws.overviewBlurb":`Virtuelle Modelle, die über Anbieter/Modell-Ziele mit Failover, Round-Robin, gewichtetem Zufall, seltenst genutztem Ziel oder frühestem Quota-Reset weiterleiten.`,"cws.count.total":`Gesamt`,"cws.count.failover":`Failover`,"cws.count.roundRobin":`Round-Robin`,"cws.count.other":`Weitere`,"cws.howTitle":`So funktioniert es`,"cws.howBody":`Fordern Sie in Codex den öffentlichen Modellnamen der Combo an. Ohne eigenen Namen gilt combo/. OpenCodex wählt ein Ziel und springt nur bei wiederholbaren Upstream-Fehlern. Ist kein Ziel verfügbar, schlägt die Anfrage geschlossen fehl, statt den globalen Standardanbieter zu verwenden.`,"cws.attentionTitle":`Aufmerksamkeit nötig`,"cws.attention.empty":`Keine Ziele konfiguriert`,"cws.attention.few":`Nur ein Ziel — Failover hat kein Ersatzziel`,"cws.attention.catalogOmitted":`Fehlt im Modellkatalog — Mitgliederfähigkeiten sind unvollständig oder inkompatibel (fehlendes Context-Window/Metadaten oder leere Modalitäts-Schnittmenge). Routing per Alias funktioniert weiterhin`,"cws.attention.allTargetsExhausted":`Alle aktivierten Ziele haben ihr Kontingent ausgeschöpft`,"cws.emptyTitle":`Erste Combo erstellen`,"cws.empty.createDesc":`Virtuelles Modell benennen und zwei oder mehr Backends verketten.`,"cws.backToAll":`Zurück zu allen Combos`,"cws.allCombos":`Alle Combos`,"cws.copyModel":`ID kopieren`,"cws.copied":`Kopiert`,"cws.tabsLabel":`Combo-Detailbereiche`,"cws.tab.config":`Konfiguration`,"cws.tab.about":`Info`,"cws.strategy":`Strategie`,"cws.strategy.failover":`Failover`,"cws.strategy.roundRobin":`Round-Robin`,"cws.strategy.random":`Zufall`,"cws.strategy.leastUsed":`Seltenst genutzt`,"cws.strategy.resetWindow":`Reset-Fenster`,"cws.strategy.failoverHint":`Ziele der Reihe nach versuchen. Bei einem wiederholbaren Fehler (Limit, Ausfall, Abo-Sperre) zum nächsten springen.`,"cws.strategy.roundRobinHint":`Datenverkehr deterministisch nach Gewicht verteilen. Das gewählte Ziel für einen Block erfolgreicher Anfragen behalten und dann weiterschalten.`,"cws.strategy.randomHint":`Pro Anfrage ein geeignetes Ziel ziehen, mit Wahrscheinlichkeiten proportional zum Gewicht. Keine Bindung zwischen Anfragen.`,"cws.strategy.leastUsedHint":`Jede Anfrage an das geeignete Ziel mit den wenigsten erfassten Erfolgen weiterleiten. Zählungen starten mit dem Proxy neu.`,"cws.strategy.resetWindowHint":`Bevorzugt das geeignete Ziel, dessen Quota-Fenster am frühesten zurückgesetzt wird. Ohne Quota-Daten gilt die Konfigurationsreihenfolge.`,"cws.field.id":`Combo-ID`,"cws.field.idHintEdit":`Das Ändern der ID benennt die Combo um. Clients fordern {model} an.`,"cws.field.alias":`Öffentlicher Modellname`,"cws.field.aliasPlaceholder":`deepseek-v4-flash oder vendor/model`,"cws.field.aliasHint":`Optional. Verwenden Sie einen Namen ohne Präfix, ein eigenes Präfix wie vendor/model oder lassen Sie das Feld leer für combo/.`,"cws.field.nativeAlias":`Natives OpenAI-Alias`,"cws.field.nativeAliasHint":`Lässt diese Combo eine unterstützte unqualifizierte native OpenAI-Modell-ID übernehmen. Konto- und providerqualifizierte OpenAI-Routen bleiben getrennt.`,"cws.field.displayName":`Anzeigename`,"cws.field.displayNameHint":`Bezeichnung im Modell-Picker. Erforderlich, wenn das native OpenAI-Alias aktiviert ist.`,"cws.field.idHint":`Clients fordern {model} an`,"cws.field.idInternalHint":`Interne Combo-ID. Sie kann nach dem Erstellen geändert werden.`,"cws.field.stickyLimit":`Sticky-Erfolge vor Rotation`,"cws.field.stickyLimitHint":`Das gewählte Ziel für so viele erfolgreiche Anfragen behalten, bevor die gewichtete Auswahl weiterschaltet.`,"cws.field.defaultEffort":`Standard-Reasoning`,"cws.field.defaultEffortNone":`Keine (Ziel-Standard)`,"cws.field.defaultEffortHint":`Nur verwendet, wenn der Client keinen Reasoning-Aufwand sendet. Optionen sind die Schnittmenge der beworbenen Aufwände der gewählten Ziele.`,"cws.capability.imageInputUnavailable":`Erst verfügbar, wenn jedes gewählte Ziel Bildeingabe unterstützt.`,"cws.capability.imageInputHint":`Standardmäßig aktiv, wenn jedes Ziel Bilder unterstützt. Ausschalten für nur Text.`,"cws.capability.imageInput":`Bild / multimodal`,"cws.capability.adaptiveEffort":`Adaptive Denkstufen`,"cws.capability.adaptiveEffortHint":`Aus: Ziele ohne Denkstufen-Regelung blenden die Auswahl für die gesamte Kombination aus. An: Solche Ziele bleiben nutzbar, und die Auswahl zeigt weiterhin die Stufen der übrigen Ziele.`,"cws.capabilities":`Fähigkeiten`,"cws.field.defaultEffortUnsupported":`Dieser Aufwand liegt nicht in der gemeinsamen Leiter der Ziele — er wird zur Anfragezeit ignoriert oder angepasst.`,"cws.field.defaultEffortUnsupportedOption":`nicht in der Schnittmenge`,"cws.targets":`Ziele`,"cws.targets.failoverHint":`Reihenfolge zählt — das erste ist primär.`,"cws.targets.roundRobinHint":`Gewichte steuern die deterministische relative Auswahl; die Reihenfolge löst Gleichstände im Rotationsring.`,"cws.targets.randomHint":`Gewichte steuern die Wahrscheinlichkeit jeder Ziehung; die Reihenfolge spielt keine Rolle.`,"cws.targets.leastUsedHint":`Die Reihenfolge löst nur Gleichstände zwischen gleich oft genutzten Zielen.`,"cws.targets.resetWindowHint":`Die Reihenfolge gilt, wenn Quota-Daten fehlen oder gleich ausfallen.`,"cws.target.provider":`Anbieter`,"cws.target.model":`Modell`,"cws.target.weight":`Gewicht`,"cws.target.pickProvider":`Anbieter wählen…`,"cws.target.pickProviderFirst":`Zuerst Anbieter wählen…`,"cws.target.pickModel":`Modell wählen…`,"cws.target.noModels":`Keine Modelle für diesen Anbieter`,"cws.target.modelPlaceholder":`Modell-ID`,"cws.target.add":`Ziel hinzufügen`,"cws.target.drag":`Ziehen zum Umsortieren`,"cws.target.moveUp":`Nach oben`,"cws.target.moveDown":`Nach unten`,"cws.quota.available":`Verfügbar`,"cws.quota.exhausted":`Kontingent erschöpft`,"cws.quota.unknown":`Kontingent unbekannt`,"cws.quota.allExhausted":`Alle aktivierten Ziele haben ihr Kontingent ausgeschöpft. Wählen Sie ein anderes Ziel oder warten Sie auf die Erholung.`,"cws.aboutTitle":`Laufzeit`,"cws.aboutBody":`Fehlgeschlagene Ziele kühlen kurz ab; Retry-After wird beachtet. Ungültige oder Kontextfehler springen nicht. Jedes Ziel passt den Aufwand an seine Fähigkeiten an; erschöpfte Combos schlagen geschlossen fehl. Protokolle und Nutzung behalten geordnete physische Versuche samt Nutzung je Versuch.`,"cws.removeConfirmTitle":`{model} entfernen?`,"cws.removeConfirmDesc":`Entfernt das virtuelle Modell aus Config und Codex-Katalog. Anbieter bleiben erhalten.`,"cws.unsavedTitle":`Ungespeicherte Änderungen`,"cws.unsavedDesc":`Änderungen an dieser Combo verwerfen und fortfahren?`,"cws.keepEditing":`Weiter bearbeiten`,"cws.err.missingId":`Combo-ID ist erforderlich.`,"cws.err.invalidId":`ID muss mit Buchstabe/Zahl beginnen und darf nur Buchstaben, Zahlen, Punkte, Unterstriche oder Bindestriche enthalten (max. 64).`,"cws.err.duplicateId":`Eine Combo mit dieser ID existiert bereits.`,"cws.err.invalidAlias":`Der Alias darf nur Buchstaben, Zahlen, Punkte, Unterstriche oder Bindestriche enthalten, mit höchstens einem "/"-Segment.`,"cws.err.aliasReservedNamespace":`Der Alias darf den reservierten Namensraum "combo/" nicht verwenden.`,"cws.err.aliasNativeFamily":`Einfache Aliase aus der OpenAI-nativen Familie (gpt-*, o1-*, o3-*, o4-*, codex-*) sind nicht erlaubt.`,"cws.err.unsupportedNativeAlias":`Ein nativer Alias muss eine derzeit unterstützte, unqualifizierte OpenAI-Modell-ID sein.`,"cws.err.missingNativeAliasDisplayName":`Für native Aliase ist ein Anzeigename erforderlich.`,"cws.err.invalidDisplayName":`Der Anzeigename darf höchstens 128 Zeichen und keine Steuerzeichen enthalten.`,"cws.err.duplicateAlias":`Eine andere Combo verwendet diesen Alias bereits.`,"cws.err.noTargets":`Mindestens ein Ziel hinzufügen.`,"cws.err.incompleteTarget":`Jedes Ziel braucht Anbieter und Modell.`,"cws.target.disabled":`{name} (deaktiviert)`,"cws.err.reservedNamespace":`Ein physischer Anbieter namens combo muss vor dem Erstellen von Combos umbenannt werden.`,"cws.err.providerCollision":`Die Combo-ID kollidiert mit einem konfigurierten Anbieternamen.`,"cws.err.unknownProvider":`Jedes Ziel muss einen konfigurierten Anbieter verwenden.`,"cws.err.duplicateTarget":`Dasselbe Anbieter/Modell-Ziel darf nur einmal vorkommen.`,"cws.err.invalidStickyLimit":`Sticky-Erfolge müssen eine Ganzzahl von 1 bis 100 sein.`,"cws.err.invalidWeight":`Jedes Round-Robin-Gewicht muss eine Ganzzahl von 1 bis 10000 sein.`,"cws.err.noEnabledTarget":`Mindestens ein Ziel muss einen aktivierten Anbieter verwenden.`,"claude.tabsLabel":`Claude-Client`,"claude.tabCode":`Code`,"claude.tabDesktop":`Desktop`,"claudeDesktop.title":`Claude Desktop`,"claudeDesktop.subtitle":`Leite jede Claude-Modellfamilie über ein verfügbares Modell auf Port {port}.`,"claudeDesktop.importJson":`JSON importieren`,"claudeDesktop.exportJson":`JSON exportieren`,"claudeDesktop.loading":`Claude-Desktop-Profil wird geladen…`,"claudeDesktop.loadFail":`Claude-Desktop-Profil konnte nicht geladen werden.`,"claudeDesktop.retry":`Erneut versuchen`,"claudeDesktop.saveFailed":`Claude-Desktop-Profil konnte nicht gespeichert werden.`,"claudeDesktop.applyFailed":`Das Profil wurde gespeichert, konnte aber nicht angewendet werden.`,"claudeDesktop.updateFailed":`Claude-Desktop-Aktualisierung fehlgeschlagen.`,"claudeDesktop.savedApplied":`Profil gespeichert und auf Claude Desktop angewendet.`,"claudeDesktop.appliedMarkerUnsaved":`Auf Claude Desktop angewendet, aber die Anwendungsmarkierung wurde nicht gespeichert – der Status unten kann veraltet sein, bis Sie erneut anwenden.`,"claudeDesktop.savedAppliedAnnounce":`Claude-Desktop-Profil gespeichert und angewendet.`,"claudeDesktop.saved":`Profil gespeichert.`,"claudeDesktop.savedAnnounce":`Claude-Desktop-Profil gespeichert.`,"claudeDesktop.exported":`Profil als JSON exportiert.`,"claudeDesktop.importExpected":`Ein Claude-Desktop-Profil der Version 1 wurde erwartet.`,"claudeDesktop.importReady":`JSON importiert. Prüfe den Entwurf und speichere und wende ihn dann an.`,"claudeDesktop.importedAnnounce":`Profil-JSON importiert. Ungespeicherte Änderungen können geprüft werden.`,"claudeDesktop.importInvalid":`Die ausgewählte Datei ist kein gültiges Profil.`,"claudeDesktop.importFailed":`Import fehlgeschlagen. {error}`,"claudeDesktop.moved":`{route} wurde nach {family} verschoben.`,"claudeDesktop.unsaved":`Ungespeicherte Änderungen`,"claudeDesktop.upToDate":`Profil ist aktuell`,"claudeDesktop.saving":`Speichert…`,"claudeDesktop.applying":`Wird angewendet…`,"claudeDesktop.saveApply":`Speichern & anwenden`,"claudeDesktop.emptyTitle":`Keine Modelle verfügbar`,"claudeDesktop.emptyHint":`Füge einen Anbieter hinzu oder aktiviere ihn und weise dann Claude-Desktop-Routen zu.`,"claudeDesktop.assignmentsLabel":`Zuweisungen der Claude-Modellfamilien`,"claudeDesktop.family.opus":`Opus`,"claudeDesktop.family.fable":`Fable`,"claudeDesktop.family.sonnet":`Sonnet`,"claudeDesktop.family.haiku":`Haiku`,"claudeDesktop.modelCountOne":`{count} Modell`,"claudeDesktop.modelCountMany":`{count} Modelle`,"claudeDesktop.chooseDefault":`Standard wählen`,"claudeDesktop.temporaryDefault":`Temporärer Standard`,"claudeDesktop.laneEmpty":`Modell hier ablegen oder die Verschieben-Steuerung verwenden.`,"claudeDesktop.laneNoMatch":`Kein Modell dieser Familie passt zur Suche.`,"nav.grok":`Grok`,"grok.title":`Grok Build`,"grok.subtitle":`Modelle, die opencodex in deiner Grok-Konfiguration registriert hat.`,"grok.loading":`Grok-Status wird geladen…`,"grok.loadFail":`Die Grok-Konfiguration konnte nicht gelesen werden.`,"grok.notConfiguredTitle":`Grok Build ist nicht eingerichtet`,"grok.notConfiguredHint":`Starte den Proxy mit installiertem Grok neu; opencodex schreibt dann einen verwalteten Block nach:`,"grok.endpoint":`Endpunkt`,"grok.colModel":`Modell`,"grok.colAlias":`Grok-Alias`,"grok.colContext":`Kontext`,"grok.groupNative":`Native Modelle`,"grok.groupRouted":`Geroutete Modelle`,"grok.enabledCount":`{on} von {total} registriert`,"grok.saved":`Auswahl gespeichert.`,"grok.savedApplied":`Auswahl gespeichert und in die Grok-Konfiguration geschrieben.`,"grok.saveFailed":`Grok-Auswahl konnte nicht gespeichert werden.`,"grok.applyFailed":`Auswahl gespeichert, aber die Grok-Konfiguration konnte nicht aktualisiert werden.`,"grok.applySkipped":`Auswahl gespeichert. Die Grok-Konfiguration wurde nicht geändert.`,"grok.saveApply":`Speichern & anwenden`,"grok.saving":`Speichern…`,"grok.applying":`Anwenden…`,"grok.unsaved":`Ungespeicherte Änderungen`,"grok.upToDate":`Auswahl ist aktuell`,"grok.toggleModel":`{id} bei Grok registrieren`,"claudeDesktop.available":`Verfügbar`,"claudeDesktop.defaultBadge":`Standard`,"claudeDesktop.supports1m":`1M`,"claudeDesktop.unavailable":`Nicht verfügbar`,"claudeDesktop.contextM":`{n}M Kontext`,"claudeDesktop.contextK":`{n}k Kontext`,"claudeDesktop.contextUnknown":`Kontext unbekannt`,"claudeDesktop.alias":`Alias`,"claudeDesktop.useAsDefault":`Als {family}-Standard verwenden`,"claudeDesktop.moveTo":`Verschieben nach`,"claudeDesktop.move":`Verschieben`,"claudeDesktop.status.applied":`Auf Desktop angewendet`,"claudeDesktop.status.stale":`Konfiguration veraltet — erneut anwenden`,"claudeDesktop.status.notApplied":`Nicht angewendet`,"claudeDesktop.status.notActiveProfile":`Desktop nutzt ein anderes Profil — erneut anwenden`,"claudeDesktop.status.disabled":`Die Claude-Desktop-Integration ist deaktiviert. Beende Desktop nach dem Aktivieren vollständig und öffne es erneut.`,"claudeDesktop.enableApply":`Aktivieren und anwenden`,"claudeDesktop.health.lastRequest":`Letzte Anfrage`,"claudeDesktop.health.stats":`{count} Anf. / {errors} Fehl.`,"claudeDesktop.effort.supported":`effort`,"claudeDesktop.effort.displayOnly":`effort (nur Anzeige)`,"dash.injectionManage":`Einstellungen öffnen`,"sub.settings":`Einstellungen`,"sub.sections":`Subagent-Abschnitte`,"sub.delegation.model":`Zuerst aufgerufenes Modell`,"sub.delegation.modelHint":`Das Modell, zu dem Codex zuerst greift, wenn es Arbeit übergibt. Oben steht, wen es überhaupt aufrufen darf; hier wählst du den Ersten davon.`,"dash.syncModelsHint":`Schreibt Codex' Modellkatalog anhand deiner verbundenen Provider neu.`,"dash.syncRun":`Jetzt synchronisieren`,"lab.title":`Kompatibilitäts-Labor`,"lab.subtitle":`Schreibgeschützte Kompatibilitätsmatrix aus der Lab-Projektion.`,"lab.loadFailed":`Kompatibilitäts-Lab-Daten konnten nicht geladen werden`,"lab.projectionUnavailable":`Lab-Projektion ist nicht verfügbar. Führen Sie zuerst Konformitäts- oder Live-Probes aus.`,"lab.projectionIncompatible":`Lab-Projektionsschema ist inkompatibel. Projektion neu aufbauen.`,"lab.statusTitle":`Projektionsstatus`,"lab.matrixTitle":`Kompatibilitätsmatrix`,"lab.verdictsTitle":`Urteilsdatensätze`,"lab.filter.layer":`Evidenzschicht`,"lab.filter.verdict":`Urteil`,"lab.filter.subject":`Subject-ID`,"lab.filter.all":`Alle`,"lab.col.subject":`Subject`,"lab.col.layer":`Schicht`,"lab.col.suite":`Suite`,"lab.col.verdict":`Urteil`,"lab.col.asOf":`Stand`,"lab.col.protocol":`Protokollkonformität`,"lab.col.live":`Live-Route-Kompatibilität`,"lab.col.task":`Aufgabenwirksamkeit`,"lab.empty":`Noch keine Kompatibilitätsurteile in der Projektion.`,"lab.subjectKind":`Art`,"lab.observationCount":`Beobachtungen`,"lab.eventCount":`Ereignisse`,"lab.verdictCount":`Urteile`,"lab.subjectCount":`Subjects`,"lab.builtAt":`Erstellt`,"lab.loading":`Kompatibilitätsevidenz wird geladen…`,"lab.loadMore":`Load more`,"lab.detailTitle":`Verdict detail`,"lab.detailClose":`Close`,"lab.detailSubject":`Subject`,"lab.detailObservations":`Observations`,"lab.detailEvents":`Contributing events`,"lab.detailArtifacts":`Artifact metadata`,"lab.production.title":`Beobachteter Produktionsverkehr`,"lab.production.notVerification":`Keine Lab-Verifizierung`,"lab.production.attempts":`Versuche`,"lab.production.successes":`Erfolge`,"lab.production.routeErrors":`Routing-Fehler`,"lab.production.lastObserved":`Zuletzt beobachtet`,"lab.detailLoadFailed":`Could not load verdict detail`,"lab.refresh":`Aktualisieren`,"lab.verdict.UNKNOWN":`Unbekannt`,"lab.verdict.CLAIMED":`Behauptet`,"lab.verdict.PROBED":`Geprüft`,"lab.verdict.VERIFIED":`Verifiziert`,"lab.verdict.DEGRADED":`Eingeschränkt`,"lab.verdict.BLOCKED":`Blockiert`,"lab.verdict.UNSUPPORTED":`Nicht unterstützt`,"lab.layer.protocol_conformance":`Protokollkonformität`,"lab.layer.live_route_compatibility":`Live-Route-Kompatibilität`,"lab.layer.task_effectiveness":`Aufgabenwirksamkeit`,"dash.visionAdvanced":`Erweiterte Einstellungen`,"dash.visionMaxDescriptions":`Maximale Beschreibungen pro Turn`,"dash.visionMaxDescriptionsInvalid":`Geben Sie eine positive ganze Zahl ein.`,"dash.visionTimeout":`Timeout`,"dash.visionTimeoutInvalid":`Geben Sie eine ganze Zahl von {min} bis {max} Millisekunden ein.`,"dash.visionAdvancedPopover":`Erweiterte Vision-Einstellungen`,"models.newPolicyGlobal":`Neue Modelle zunächst deaktivieren`,"models.newPolicyProvider":`Richtlinie für neue Modelle`,"models.newPolicy_inherit":`Übernehmen`,"models.newPolicy_off":`Aus`,"models.newPolicy_on":`An`,"models.newBadge":`NEU`,"models.newCount":`{count} neu, aus`,"models.aliases":`Aliase`,"models.aliasesTable":`Alias-Tabelle`,"models.aliasPrompt":`Anbieter-Alias (leer lassen zum Entfernen)`,"models.modelAliasPrompt":`Modell-Alias (leer lassen zum Entfernen)`,"models.aliasSaved":`Alias gespeichert`,"models.aliasConflict":`Dieser Alias steht in Konflikt mit einem vorhandenen Namen`,"models.editProviderAlias":`Anbieter-Alias bearbeiten`,"models.editModelAlias":`Modell-Alias bearbeiten`,"models.useDefaultAliases":`Standard-Aliase verwenden`,"models.useDefaultAliasesGlobal":`Standard-Aliase global verwenden`,"models.aliasAuto":`automatisch`,"models.aliasUser":`benutzerdefiniert`,"models.aliasStale":`veraltet`,"connection.discovering":`Discovering local and shared targets…`,"connection.machineUnavailable":`The local machine plane is unavailable. Shared requests were not redirected locally.`,"connection.disconnect":`Disconnect from hub`,"connection.disconnectConfirm":`Disconnect this machine from the hub and restart it in standalone mode?`,"connection.pairing.title":`Connect this dashboard to the hub`,"connection.pairing.body":`Paste the one-time pairing code created on the hub.`,"connection.pairing.relayWarning":`This code is exchanged through the fixed hub relay. The relay cannot be redirected to another host.`,"connection.pairing.code":`One-time pairing code`,"connection.pairing.submit":`Connect`,"connection.pairing.submitting":`Connecting…`,"connection.pairing.error":`The pairing code was refused or expired. The code was left in place so you can check it.`,"connection.machine.title":`This machine`,"connection.machine.shimHealthy":`Codex shim is healthy.`,"connection.machine.shimNeedsAttention":`Codex shim needs attention.`,"connection.machine.repairShim":`Repair shim`,"connection.machine.removeShim":`Remove shim`,"connection.clients.title":`Connected clients`,"connection.clients.none":`No client status available`,"connection.clients.sync":`Sync now`,"connection.clients.syncing":`Syncing…`,"connection.sessionLogout":`Remote-Sitzung abmelden`,"connection.sessionLoggingOut":`Remote-Sitzung wird abgemeldet…`,"connection.sessionLogoutFailed":`Die Remote-Sitzung konnte nicht abgemeldet werden. Die aktuelle Sitzung bleibt bestehen.`,"usage.source.connected":`Source: hub usage`,"usage.source.local":`Source: local usage.jsonl`,"usage.scope.label":`Usage scope`,"usage.scope.machine":`This machine`,"usage.scope.hub":`Hub-wide`,"usage.hubOffline":`Hub usage is unavailable. Local usage was not substituted.`,"integrations.tab.cursor":`Cursor`,"integrations.detail.cursorSeen":`Cursor hat diesen Proxy kürzlich aufgerufen`,"integrations.detail.cursorNeverSeen":`Private Inference installiert; noch keine Anfrage empfangen`,"integrations.detail.cursorAbsent":`Cursor Private Inference nicht gefunden`,"integrations.cursor.title":`Cursor`,"integrations.cursor.intro":`Cursor Private Inference führt seinen Agenten lokal aus und kommuniziert per Loopback mit opencodex. Die reguläre Cursor-Version kann das nicht: Ihr Backend ruft den benutzerdefinierten Endpunkt auf und benötigt eine öffentliche HTTPS-URL. Diese Seite schreibt niemals in Cursor; fügen Sie die unten stehenden Werte selbst in Cursor ein.`,"integrations.cursor.loading":`Cursor-Status wird gelesen…`,"integrations.cursor.unavailable":`Der Cursor-Status konnte nicht vom Proxy gelesen werden.`,"integrations.cursor.detection":`Installierte Builds`,"integrations.cursor.privateInference":`Cursor Private Inference`,"integrations.cursor.regular":`Cursor (regulär)`,"integrations.cursor.detected":`Erkannt`,"integrations.cursor.notFound":`Nicht gefunden`,"integrations.cursor.regularOnly":`Es wurde nur die reguläre Cursor-Version gefunden. Sie leitet benutzerdefinierte Endpunkte über die Cursor-Server weiter, sodass ein Loopback-Proxy ohne öffentlichen Tunnel nicht erreichbar ist. Informationen zum Private-Inference-Build finden Sie in der Anleitung.`,"integrations.cursor.nothingFound":`An den üblichen Speicherorten wurde keine Cursor-Installation gefunden. Falls Cursor an einem anderen Ort installiert ist, gelten die unten stehenden Werte trotzdem.`,"integrations.cursor.gateway":`Gateway-Werte`,"integrations.cursor.gatewayHint":`Öffnen Sie in Cursor Private Inference Settings > Models > Gateway, fügen Sie diese beiden Werte ein und klicken Sie anschließend auf Refresh model list.`,"integrations.cursor.baseUrl":`Basis-URL`,"integrations.cursor.apiKey":`API-Schlüssel`,"integrations.cursor.apiKeyCredential":`Einer Ihrer opencodex-API-Schlüssel (für diese Anbindung sind Zugangsdaten erforderlich)`,"integrations.cursor.copy":`Kopieren`,"integrations.cursor.copied":`Kopiert`,"integrations.cursor.connection":`Verbindung`,"integrations.cursor.seen":`Letzte Anfrage von Cursor: {time} ({ua})`,"integrations.cursor.neverSeen":`Seit dem Start des Proxys ist keine Anfrage von Cursor eingegangen. Klicken Sie nach dem Speichern des Gateways in Cursor auf Refresh model list.`,"integrations.cursor.models":`Was Cursor anzeigt`,"integrations.cursor.modelsHint":`Cursor wählt die Reasoning-Abstufung anhand seiner eigenen Modelltabelle aus, daher kann opencodex sie nur vorhersagen. Die Kontextspalte zeigt das Standardfenster und das optionale Fenster (Cursors Max Mode).`,"integrations.cursor.ladderFromBundle":`Reasoning-Abstufungen wurden aus dem installierten Cursor-Private-Inference-Bundle {version} gelesen. Cursor legt sie fest; opencodex gibt nur dessen Tabelle wieder.`,"integrations.cursor.ladderFromStatic":`Reasoning-Abstufungen sind ein statischer Spiegel von Cursor 3.18.25 (kein lesbares Private-Inference-Bundle gefunden). Die Kontextspalte zeigt das Standardfenster und das optionale Fenster.`,"integrations.cursor.unknownVersion":`unbekannte Version`,"integrations.cursor.noControl":`—`,"integrations.cursor.singleWindow":`ein Fenster`,"integrations.cursor.noControlTitle":`Diese ID steht nicht in Cursors eingebauter Effort-Tabelle, daher zeigt Cursor keine Reasoning-Steuerung an.`,"integrations.cursor.effortRowsOne":`1 Effort-Zeile veröffentlicht`,"integrations.cursor.effortRowsMany":`{n} Effort-Zeilen veröffentlicht`,"integrations.cursor.effortRowsOff":`keine Effort-Zeilen`,"integrations.cursor.tableLessHint":`Mit — markierte Zeilen erhalten in Cursor keine Reasoning-Steuerung. Aktivieren Sie cursorEffortRows, um pro Effort einen Picker-Eintrag (id--effort) zu veröffentlichen, oder setzen Sie modelDefaultReasoningEfforts beim Provider für einen festen Standard.`,"integrations.cursor.colModel":`Modell`,"integrations.cursor.colReasoning":`Reasoning-Aufwand`,"integrations.cursor.colContext":`Kontext`,"integrations.cursor.guide":`Anleitung zu Cursor Private Inference öffnen`},Be={"nav.dashboard":`Tableau de bord`,"uptime.day":`j`,"uptime.hour":`h`,"uptime.minute":`min`,"uptime.second":`s`,"nav.startup":`Démarrage`,"nav.providers":`Fournisseurs`,"nav.models":`Modèles`,"nav.combos":`Combinaisons`,"nav.subagents":`Sous-agents`,"nav.logs":`Journaux et débogage`,"nav.usage":`Utilisation`,"common.github":`GitHub`,"sidebar.star":`Ajouter une étoile sur GitHub`,"sidebar.starred":`Étoile ajoutée sur GitHub`,"sidebar.starUnauthenticated":`Ouvrir GitHub pour ajouter une étoile (gh CLI n’est pas connecté)`,"sidebar.starFailed":`Impossible d’ajouter une étoile avec gh. Ouverture de GitHub à la place.`,"sidebar.updateAvailable":`Mise à jour disponible : {version}`,"sidebar.checkUpdate":`Rechercher des mises à jour`,"common.save":`Enregistrer`,"common.saving":`Enregistrement…`,"common.cancel":`Annuler`,"common.discard":`Abandonner les modifications`,"common.delete":`Supprimer`,"common.close":`Fermer`,"common.ok":`OK`,"common.remove":`Supprimer`,"common.loading":`Chargement…`,"common.retry":`Réessayer`,"auth.adminTokenTitle":`Jeton d’administration OpenCodex (OPENCODEX_ADMIN_AUTH_TOKEN)`,"auth.adminAccountLabel":`Compte`,"auth.adminTokenFieldLabel":`Jeton d’administration`,"auth.adminTokenRejected":`Ce jeton d’administration a été refusé. Vérifiez-le et réessayez.`,"auth.adminTokenUnavailable":`Le jeton d’administration n’a pas pu être vérifié. Réessayez.`,"app.logoAria":`Logo opencodex`,"app.claudeOn":`Claude ACTIVÉ`,"app.claudeOff":`Claude DÉSACTIVÉ`,"theme.label":`Thème`,"theme.light":`Clair`,"theme.dark":`Sombre`,"theme.system":`Système`,"lang.label":`Langue`,"lang.nativeName":`Français`,"provider.name.commandCodeAuth":`Command Code - Auth`,"provider.name.commandCodeApi":`Command Code - API`,"provider.name.volcengine":`Volcengine Ark`,"provider.name.volcengineCodingPlan":`Volcengine Ark Coding Plan`,"provider.name.volcengineAgentPlan":`Volcengine Ark Agent Plan`,"errorBoundary.title":`Échec du chargement de la page`,"errorBoundary.message":`Une erreur de rendu s’est produite dans cette section. Rechargez-la pour réessayer.`,"errorBoundary.details":`Erreur`,"errorBoundary.reload":`Recharger`,"routing.title":`Routage intelligent (bêta)`,"routing.subtitle":`Profils de stratégie, évaluation à blanc et analyses de routage fondées sur les sources.`,"routing.loadFailed":`Impossible de charger les données de routage`,"routing.empty":"Aucun profil de routage configuré. Ajoutez `routingProfiles` à config.json.","routing.revision":`rév.`,"routing.detail":`Profil`,"routing.createProfile":`Créer un profil`,"routing.dryRunError":`Échec de l’évaluation à blanc (HTTP {status})`,"routing.removeConfirm":`Supprimer le profil {id} ?`,"routing.unknownEvidence.allow":`autoriser`,"routing.unknownEvidence.penalize":`pénaliser`,"routing.unknownEvidence.exclude":`exclure`,"routing.removeCandidate":`Supprimer le candidat {provider}/{model}`,"routing.candidates":`Candidats`,"routing.require":`Exigences strictes`,"routing.optimize":`Pondérations d’optimisation`,"routing.limits":`Limites`,"routing.unknownEvidence":`Stratégie pour les preuves inconnues`,"routing.compatibility.title":`Stratégie de compatibilité`,"routing.compatibility.enabled":`Exiger des preuves du laboratoire de compatibilité`,"routing.compatibility.requiredSuites":`Suites requises`,"routing.compatibility.loadingCatalog":`Chargement du catalogue du laboratoire…`,"routing.compatibility.catalogUnavailable":`Catalogue du laboratoire indisponible — saisissez manuellement les identifiants de suite dans config.json.`,"routing.compatibility.layer.protocol_conformance":`Conformité au protocole`,"routing.compatibility.layer.live_route_compatibility":`Compatibilité du routage en direct`,"routing.compatibility.minStatus":`État de compatibilité minimal`,"routing.none":`aucun`,"routing.unavailable":`–`,"routing.dryRun":`Évaluation à blanc`,"routing.dryRunContext":`Fenêtre de contexte de la requête (jetons)`,"routing.dryRunTools":`La requête nécessite des outils`,"routing.dryRunImage":`La requête nécessite une image en entrée`,"routing.dryRunStructured":`La requête nécessite une sortie structurée`,"routing.dryRunRun":`Évaluer les candidats`,"routing.candidate":`Candidat`,"routing.eligible":`Admissible`,"routing.exclusions":`Exclusions`,"routing.costCap":`Plafond de coût`,"routing.capOutcome.satisfied":`dans la limite`,"routing.capOutcome.exceeded":`limite dépassée`,"routing.capOutcome.unknown-allowed":`inconnu (autorisé)`,"routing.capOutcome.unknown-excluded":`inconnu (exclu)`,"routing.exclusion.capability-unsatisfied":`capacité non satisfaite`,"routing.exclusion.unknown-capability":`capacité inconnue`,"routing.exclusion.cost-limit":`plafond de coût dépassé`,"routing.exclusion.cost-limit-unknown":`coût inconnu sous le plafond`,"routing.exclusion.cooldown":`délai de récupération`,"routing.exclusion.unknown-health":`état de santé inconnu`,"routing.exclusion.unknown-quota":`quota inconnu`,"routing.exclusion.unknown-price":`prix inconnu`,"routing.exclusion.other":`exclusion : {code}`,"routing.score":`Score`,"routing.selected":`sélectionné`,"routing.yes":`oui`,"routing.no":`non`,"routing.analytics":`Analyse du routage`,"routing.analyticsTotal":`Requêtes`,"routing.analyticsSuccessRate":`Réussite`,"routing.analyticsFallbackRate":`Repli`,"routing.analyticsP50":`p50`,"routing.analyticsP95":`p95`,"routing.analyticsP99":`p99`,"routing.analyticsCooldown":`Échecs pendant le délai de récupération`,"routing.analyticsConfidence":`Confiance`,"routing.analyticsTruncated":`historique tronqué`,"routing.analyticsRequests":`Requêtes`,"routing.analyticsEmpty":`Aucune donnée d’analyse pour le moment — envoyez d’abord quelques requêtes.`,"startup.title":`Sécurité du démarrage`,"startup.subtitle":`Vérifiez que Codex peut joindre opencodex après un redémarrage, avant que le routage du proxy local n’entre dans une boucle de reconnexion.`,"startup.refresh":`Actualiser`,"startup.backToDashboard":`Retour au tableau de bord`,"startup.loading":`Vérification de la protection au démarrage…`,"startup.error":`Impossible de lire la protection au démarrage.`,"startup.staleData":`La dernière vérification du démarrage a échoué. Les valeurs ci-dessous sont obsolètes et ne doivent pas être considérées comme une preuve de protection.`,"startup.status.native":`Routage natif`,"startup.status.protected":`Redémarrage protégé`,"startup.status.atRisk":`Action requise`,"startup.summary.native":`Codex ne dépend pas du proxy local`,"startup.summary.protected":`opencodex sera disponible après le redémarrage`,"startup.summary.atRisk":`Codex peut perdre l’accès aux modèles après le redémarrage`,"startup.riskDetail":`Codex est lié au proxy local, mais aucun service persistant ni mécanisme de lancement opérationnel ne le redémarrera.`,"startup.riskDetailCustomLocal":`Codex pointe vers une passerelle locale personnalisée. opencodex ne peut ni gérer ni vérifier le cycle de redémarrage de cette passerelle.`,"startup.riskDetailWindowsShim":`Le mécanisme de lancement protège les scripts CLI pris en charge, mais Codex Desktop et les lancements directs de codex.exe peuvent le contourner sous Windows.`,"startup.safeDetail":`Le routage et le mécanisme de démarrage actuels sont cohérents. Aucun démarrage manuel avec ocx ne devrait être nécessaire après un redémarrage.`,"startup.routing":`Routage de Codex`,"startup.routing.proxy":`Proxy local`,"startup.routing.native":`OpenAI natif`,"startup.routing.customLocal":`Passerelle locale personnalisée`,"startup.routing.customRemote":`Passerelle distante personnalisée`,"startup.routing.unknown":`Routage inconnu ou non valide`,"startup.restartProtection":`Protection au redémarrage`,"startup.preference":`Démarrage à la demande`,"startup.enabled":`Activé`,"startup.disabled":`Désactivé`,"startup.protection.service":`Service en arrière-plan`,"startup.protection.shim":`Mécanisme de lancement`,"startup.protection.none":`Non installé`,"startup.details":`Détails de la protection`,"startup.service":`Service en arrière-plan`,"startup.serviceHint":`Démarre à la connexion et relance le proxy après un plantage.`,"startup.installed":`Installé`,"startup.notInstalled":`Non installé`,"startup.unsupported":`Non pris en charge`,"startup.shim":`Mécanisme de lancement Codex`,"startup.shimHint":`Exécute ocx ensure au démarrage d’un script de lancement Codex pris en charge.`,"startup.healthy":`Opérationnel`,"startup.cliOnly":`CLI uniquement`,"startup.stale":`Obsolète`,"startup.viable":`Prêt`,"startup.unhealthy":`Installé, mais défaillant`,"startup.conflict":`Conflit de service`,"startup.installedDisabled":`Installé, mais désactivé`,"startup.install":`Installer`,"startup.installing":`Installation…`,"startup.repair":`Réparer`,"startup.repairing":`Réparation…`,"startup.serviceInstalled":`Service en arrière-plan installé avec succès.`,"startup.serviceRepaired":`Service en arrière-plan réparé avec succès.`,"startup.shimInstalled":`Mécanisme de lancement Codex installé avec succès.`,"startup.shimRepaired":`Mécanisme de lancement Codex réparé avec succès.`,"startup.installFailed":`Échec de l’installation :`,"startup.tray.title":`Zone de notification Windows`,"startup.tray.hint":`Installez une icône dans la zone de notification à la connexion pour démarrer, arrêter et redémarrer le proxy, ouvrir le tableau de bord et consulter l’état en un clic.`,"startup.tray.login":`Lancer l’icône à la connexion Windows`,"startup.tray.notProtection":`L’icône de notification est un contrôleur, pas une protection au redémarrage. Un service en arrière-plan opérationnel reste nécessaire pour rétablir le proxy sans intervention.`,"startup.tray.running":`En cours d’exécution`,"startup.tray.stopped":`Installé, masqué`,"startup.tray.stale":`Réparation requise`,"startup.tray.notInstalled":`Non installé`,"startup.tray.loading":`Vérification…`,"startup.tray.unavailable":`État indisponible`,"startup.tray.install":`Installer et afficher l’icône`,"startup.tray.start":`Afficher l’icône de notification`,"startup.tray.stop":`Quitter l’icône de notification`,"startup.tray.uninstall":`Supprimer le lancement à la connexion`,"startup.tray.error":`Échec de l’action sur l’icône de notification Windows. Consultez ocx tray status pour plus de détails.`,"startup.recovery":`Options de réparation`,"startup.recoveryHint":`Utilisez les programmes d’installation en un clic ci-dessus ou copiez une commande pour effectuer une réparation manuelle. Le service en arrière-plan est recommandé pour Codex Desktop et les exécutables Windows.`,"startup.command.service":`Recommandé : service persistant en arrière-plan`,"startup.command.shim":`Alternative : mécanisme de lancement CLI`,"startup.command.native":`Solution de secours : restaurer le routage Codex natif`,"startup.copy":`Copier`,"startup.copied":`Copié`,"startup.recommended":`Réparation recommandée : {cmd}`,"startup.navRisk":`La protection au démarrage requiert votre attention`,"startup.codexRuntime.clampHidden":`Certaines options d’effort de raisonnement ont été masquées, car OpenCodex utilisait Codex {version}.`,"startup.codexRuntime.clampHiddenWithEfforts":`Certaines options d’effort de raisonnement ont été masquées, car OpenCodex utilisait Codex {version} (supprimées : {efforts}).`,"startup.codexRuntime.olderBinary":`OpenCodex utilise un binaire Codex plus ancien ({version}). Une installation plus récente est disponible.`,"dash.subtitle":`État en direct du proxy opencodex local, de ses fournisseurs et des modèles routés vers Codex.`,"dash.workspace.overview":`Vue d’ensemble`,"dash.workspace.sections":`Sections`,"dash.status":`État`,"dash.online":`En ligne`,"dash.offline":`Hors ligne`,"dash.version":`Version`,"dash.uptime":`Durée de fonctionnement`,"dash.providers":`Fournisseurs`,"dash.tokens30d":`Jetons (30 j)`,"dash.coverage":`Couverture : {pct}`,"dash.mem.title":`Observabilité de la mémoire`,"dash.mem.hint":`Diagnostics d’exécution en lecture seule. La mémoire observée correspond à max(RSS, external, ArrayBuffers), afin que la réduction de l’ensemble de travail Windows ne masque pas la rétention allouée.`,"dash.mem.rss":`Ensemble résident (RSS)`,"dash.mem.jsHeap":`Tas JS utilisé`,"dash.mem.jsHeapArena":`arène {total}`,"dash.mem.pressure":`Par rapport au seuil d’avertissement`,"dash.mem.pressureOf":`{pct} % du seuil`,"dash.mem.pressureUnknown":`Aucun seuil signalé`,"dash.mem.jscHeap":`Tas JSC`,"dash.mem.external":`Mémoire externe`,"dash.mem.arrayBuffers":`ArrayBuffers`,"dash.mem.observed":`Observée`,"dash.mem.runtime":`Compteurs d’exécution`,"dash.mem.growth":`Dérive observée par heure`,"dash.mem.perHour":`/h`,"dash.mem.store":`Stockage des continuations`,"dash.mem.storeHint":`Cache previous_response_id du proxy. Une hausse du nombre total d’octets accompagnée d’une augmentation du tas indique une rétention des conversations plutôt qu’un effet de l’allocateur d’exécution.`,"dash.mem.storeEntries":`Entrées`,"dash.mem.storeTotal":`Total`,"dash.mem.storeLargest":`Plus grande`,"dash.mem.storeOldest":`Plus ancienne`,"dash.mem.threshold":`Seuil d’avertissement`,"dash.mem.lastWarn":`Dernier avertissement`,"dash.mem.never":`Jamais`,"dash.mem.details":`Détails`,"dash.mem.unavailable":`Diagnostics de mémoire indisponibles (proxy plus ancien).`,"dash.mem.inFlight":`Requêtes en cours`,"dash.mem.restart":`Drainer et redémarrer`,"dash.mem.restartConfirm":`Attendre la fin de {count} requête(s) en cours, puis redémarrer (jusqu’à {seconds} s ; les requêtes restantes seront interrompues à l’expiration du délai).`,"dash.mem.draining":`Drainage de {count} requête(s)… redémarrage une fois terminé`,"dash.mem.reconnecting":`Redémarrage du proxy… attente de la reconnexion`,"dash.mem.restartFailed":`Échec du drainage et du redémarrage. Vérifiez que le proxy est en cours d’exécution.`,"dash.mem.restartNoSupervisor":`Aucune protection au redémarrage détectée. Le proxy peut rester arrêté après le redémarrage, sauf si vous le relancez.`,"dash.activeProviders":`Fournisseurs actifs`,"dash.noProviders":`Aucun fournisseur configuré. Exécutez {cmd}.`,"dash.col.name":`Nom`,"dash.col.adapter":`Adaptateur`,"dash.col.baseUrl":`URL de base`,"dash.col.model":`Modèle`,"dash.modelsNoResults":`Aucun modèle ne correspond à votre recherche.`,"dash.availableModels":`Modèles disponibles`,"dash.noModels":`Aucun modèle trouvé. Vérifiez les clés API des fournisseurs.`,"dash.cannotConnect":`Impossible de se connecter au proxy. Est-il en cours d’exécution ?`,"dash.runStart":`Exécutez {cmd} pour démarrer le proxy.`,"dash.stop":`Arrêter le proxy`,"dash.stopConfirm":`Arrêter le proxy et restaurer Codex natif ?`,"dash.stopFailed":`Échec de l’arrêt du proxy (HTTP {status}).`,"dash.maSwitchFailed":`Échec du changement de mode (HTTP {status}).`,"dash.maNetworkError":`Erreur réseau — le proxy est-il en cours d’exécution ?`,"dash.stopping":`Arrêt…`,"dash.actions":`Proxy`,"dash.codexRestart":`Recharger les modèles Codex`,"dash.codexRestarting":`Arrêt…`,"dash.codexRestartConfirm":`Arrêter les serveurs d’application Codex afin qu’ils rechargent la liste des modèles ? Tout tour Codex en cours sera interrompu et Codex ne redémarrera pas automatiquement — rouvrez-le ensuite.`,"dash.codexRestartDone":`{count} serveur(s) d’application Codex arrêté(s). Rouvrez Codex pour charger la liste actuelle des modèles.`,"dash.codexRestartNothing":`Aucun serveur d’application Codex n’est en cours d’exécution. Le prochain lancement lira la liste actuelle des modèles.`,"dash.codexRestartUnknown":`Impossible de répertorier les processus ; aucun n’a donc été arrêté.`,"dash.codexRestartPartial":`{count} serveur(s) d’application ne se sont pas arrêtés. Arrêtez-les manuellement si la liste des modèles reste obsolète.`,"dash.codexRestartFailed":`Échec du rechargement des modèles Codex (HTTP {status}).`,"dash.codexRestartUnreachable":`Impossible de joindre le proxy.`,"dash.codexRestartMalformed":`Le proxy a renvoyé une réponse inattendue.`,"dash.codexRestartTimeout":`Le proxy n’a pas répondu à temps. Il est peut-être encore en train d’arrêter les serveurs d’application.`,"models.staleBanner":`Codex affiche une liste de modèles plus ancienne que ce catalogue. Redémarrez Codex pour la recharger.`,"dash.codexAutoStart":`Démarrer opencodex avec Codex`,"dash.codexAutoStartHint":`Permet à un mécanisme de lancement installé d’exécuter ocx ensure. Ce réglage n’installe pas de protection au redémarrage ; consultez Sécurité du démarrage pour connaître l’état effectif.`,"dash.searchModel":`Modèle auxiliaire de recherche`,"dash.searchModelHint":`Modèle utilisé pour web_search sur les modèles routés autres qu’OpenAI. Nécessite une connexion à ChatGPT.`,"dash.searchReasoning":`Effort de raisonnement pour la recherche`,"dash.visionModel":`Modèle auxiliaire de vision`,"dash.visionModelHint":`Modèle utilisé pour décrire les images aux modèles routés en mode texte uniquement. Nécessite une connexion à ChatGPT.`,"dash.webSearchSidecar":`Service auxiliaire de recherche Web`,"dash.webSearchSidecarHint":`Choisissez le moteur et le modèle utilisés pour la recherche Web sur les modèles routés.`,"dash.webSearchStream":`Diffuser les réponses en direct`,"dash.webSearchStreamHint":`Diffuse en direct le texte initial et le raisonnement du modèle jusqu’à ce qu’il décide d’appeler un outil ; le reste du tour demeure en mémoire tampon pour intercepter la recherche. Le texte produit avant une recherche peut être partiellement répété.`,"dash.visionSidecar":`Service auxiliaire de vision`,"dash.visionSidecarHint":`Choisissez le moteur et le modèle utilisés pour décrire les images aux modèles routés en mode texte uniquement.`,"dash.visionOff":`Désactivé`,"dash.visionAdvanced":`Paramètres avancés`,"dash.visionMaxDescriptions":`Nombre maximal de descriptions par tour`,"dash.visionMaxDescriptionsInvalid":`Saisissez un entier positif.`,"dash.visionTimeout":`Délai d’expiration`,"dash.visionTimeoutInvalid":`Saisissez un entier compris entre {min} et {max} millisecondes.`,"dash.visionAdvancedPopover":`Paramètres de vision avancés`,"dash.shadowCallIntercept":`Interception des appels fantômes`,"dash.shadowCallInterceptHint":`Intercepte les appels auxiliaires en arrière-plan de l’application Codex ({models}) pour générer les titres et les messages de commit, puis les redirige vers le modèle choisi.`,"dash.shadowCallWarning":`⚠ Lorsque cette option est activée, TOUTES les requêtes destinées à {models} sont remplacées par le modèle sélectionné.`,"dash.shadowCallOriginal":`Original`,"dash.shadowCallModel":`Modèle de remplacement`,"dash.shadowCallTooltip":`L’application Codex effectue des appels auxiliaires en arrière-plan pour générer les titres de fils, les messages de commit et orchestrer les compétences. Le modèle auxiliaire ayant changé selon les versions du client, opencodex intercepte tous les modèles de cet ensemble : {models}. Activez cette option pour rediriger ces appels vers le modèle choisi.`,"models.shadowCallIntercept":`Interception des appels fantômes`,"models.shadowCallInterceptHint":`Intercepte les appels auxiliaires en arrière-plan de l’application Codex ({models}) pour les titres et les messages de commit, puis les redirige vers le modèle choisi.`,"dash.sidecarBackend":`Moteur`,"dash.sidecarModel":`Modèle`,"dash.backendAuto":`Auto`,"dash.backendOpenAI":`OpenAI`,"dash.backendAnthropic":`Anthropic`,"dash.sidecarSaved":`Paramètres des services auxiliaires enregistrés. Ils s’appliqueront à la prochaine requête.`,"dash.sidecarSaveFailed":`Échec de l’enregistrement des paramètres des services auxiliaires.`,"dash.injectionLabel":`Délégation aux sous-agents`,"dash.injectionHint":`Choisissez le modèle auquel Codex doit confier le travail des sous-agents. Les deux options ci-dessous déterminent où ce choix est utilisé.`,"dash.injectionManage":`Ouvrir les paramètres`,"dash.syncCodexSubagentDefaults":`Enregistrer aussi comme valeur par défaut de Codex`,"dash.syncCodexSubagentDefaultsHint":`Si cette option est activée, le choix ci-dessus est inscrit dans la configuration de Codex afin que les nouvelles tâches commencent aussi avec ce modèle. Si elle est désactivée, il n’est mémorisé qu’ici. Il prend effet à la prochaine synchronisation ou au prochain redémarrage, sans modifier vos paramètres [agents] saisis manuellement.`,"dash.multiAgentGuidance":`Indiquer à Codex comment répartir le travail`,"dash.multiAgentGuidanceHint":`Envoie une brève note indiquant à Codex comment confier le travail aux sous-agents. En v2, elle précise les modèles utilisables et celui à privilégier ; en v1, elle ne s’applique qu’avec un effort de raisonnement maximal ou ultra. Si cette option est désactivée, aucune note n’est ajoutée.`,"dash.injectionNone":`Aucun`,"dash.injectionEffortLabel":`Effort de raisonnement`,"dash.injectionEffortNone":`Valeur par défaut du modèle`,"dash.effortCapLabel":`Limite de l’effort ultra en V2`,"dash.subagentEffortCapLabel":`Limite de l’effort des sous-agents en V2`,"dash.effortCapHelp":`Limite l’effort de raisonnement des tours en mode ultra V2. Lorsqu’une limite est définie, les requêtes entrantes avec l’effort maximal (issues du mode ultra) sont plafonnées au niveau sélectionné. La limite des sous-agents s’applique uniquement aux agents enfants créés. Les plafonds ne font que réduire l’effort, jamais l’augmenter. Si un modèle ne prend pas en charge le niveau plafonné, le niveau inférieur pris en charge le plus proche est utilisé.`,"dash.effortCapNone":`Aucune limite`,"dash.maintenance":`Maintenance`,"dash.maintenanceHint":`Actualisez le catalogue de modèles de Codex ou installez une version plus récente d’opencodex.`,"dash.syncModels":`Synchroniser les modèles`,"dash.syncModelsHint":`Réécrit le catalogue de modèles de Codex à partir des fournisseurs connectés.`,"dash.syncRun":`Synchroniser maintenant`,"dash.syncing":`Synchronisation…`,"dash.syncOk":`Synchronisation terminée. {count} modèle(s) ajouté(s).`,"dash.syncStaleHint":`Si Codex affiche toujours une ancienne liste, redémarrez son app-server de longue durée ({cmd}).`,"dash.syncFailed":`Échec de la synchronisation : {error}`,"dash.projectConfigTitle":`La configuration Codex du projet contourne OpenCodex`,"dash.projectConfigHint":`Ces paramètres propres au dépôt remplacent le proxy OpenCodex (par exemple, en routant directement vers OpenCode Go). Supprimez-les afin que le routage défini dans ~/.codex/config.toml s’applique à ce projet.`,"dash.checkUpdate":`Rechercher une mise à jour`,"dash.updateTitle":`Mettre à jour opencodex`,"dash.updateDesc":`Recherchez le canal sélectionné sur npm, puis choisissez de redémarrer ou non le proxy après l’installation.`,"dash.updateChannel":`Canal`,"dash.updateChecking":`Recherche de mises à jour…`,"dash.updateInstalled":`Installée`,"dash.updateLatest":`Dernière version`,"dash.updateAvailable":`Mise à jour disponible`,"dash.updateCurrent":`À jour`,"dash.updateCommand":`Commande`,"dash.updateSource":`Il s’agit d’une extraction du code source. Mettez-la à jour depuis le terminal avec la commande affichée.`,"dash.updateUnavailable":`Impossible de lire la dernière version depuis npm. Réessayez plus tard.`,"dash.updateRetry":`Réessayer`,"dash.updateRecheck":`Revérifier`,"dash.updateCannotAuto":`La mise à jour en un clic est indisponible ({reason}).`,"dash.updateReason.source_checkout":`extraction du code source`,"dash.updateReason.latest_unavailable":`registre npm inaccessible`,"dash.updateReason.already_latest":`dernière version déjà installée`,"dash.updateReason.unknown":`mise à jour indisponible`,"dash.updateRestart":`Redémarrer après la mise à jour`,"dash.updateRestartHint":`Recommandé. L’interface graphique actuelle continue d’exécuter l’ancien code jusqu’au redémarrage du proxy.`,"dash.runUpdate":`Mettre à jour`,"dash.updateReconnecting":`Attente du proxy redémarré…`,"dash.updateStatus.running":`Mise à jour d’opencodex.`,"dash.updateStatus.restarting":`Mise à jour installée. Redémarrage du proxy.`,"dash.updateStatus.succeeded":`Mise à jour terminée.`,"dash.updateVersionTransition":`{currentVersion} -> {latestVersion}.`,"dash.updateStatus.failed":`Échec de la mise à jour.`,"prov.subtitle":`Configurez les fournisseurs en amont vers lesquels opencodex route Codex. Connectez-vous avec un compte, ajoutez un fournisseur ou modifiez la configuration brute.`,"prov.add":`Ajouter un fournisseur`,"prov.editJson":`Modifier le JSON`,"prov.accountLogin":`Connexion au compte`,"prov.noOauth":`Aucun fournisseur OAuth disponible.`,"prov.loggedIn":`connecté`,"prov.notLoggedIn":`non connecté`,"prov.logout":`Se déconnecter`,"prov.login":`Se connecter`,"prov.loginWith":`Se connecter avec {provider}`,"prov.waitingBrowser":`Attente du navigateur…`,"prov.didntOpen":`La page ne s’est pas ouverte ? Cliquez ici`,"prov.copyLink":`Copier le lien`,"prov.dontOpenBrowser":`Ne pas ouvrir de navigateur sur la machine du proxy`,"prov.dontOpenBrowserHint":`Utile pour un autre profil de navigateur, ou quand le tableau de bord n'est pas sur la machine du proxy.`,"prov.linkCopied":`Copié`,"prov.linkCopyUnavailable":`Presse-papiers indisponible`,"prov.deviceCode":`Code de l’appareil`,"prov.copyCode":`Copier le code`,"prov.codeCopied":`Code copié`,"prov.editAlias":`Modifier l’alias`,"prov.aliasPrompt":`Nom d’affichage (laissez vide pour l’effacer)`,"prov.aliasSaved":`Alias enregistré`,"prov.aliasSaveFailed":`Impossible d’enregistrer l’alias`,"prov.accountId":`ID`,"prov.pasteRedirect":`Coller l’URL de redirection ou le code`,"prov.pasteRedirectHint":`Si le navigateur affiche une erreur localhost, copiez l’URL complète depuis sa barre d’adresse et collez-la ici (ou collez le code d’autorisation).`,"prov.pasteSubmit":`Envoyer`,"prov.pasteSubmitting":`Envoi…`,"prov.pasteOk":`Code envoyé — finalisation de la connexion…`,"prov.pasteFail":`Impossible d’envoyer le code : {error}`,"prov.port":`Port`,"prov.default":`Par défaut`,"prov.loadingConfig":`Chargement…`,"prov.saved":`Enregistré ! Redémarrez le proxy pour appliquer les modifications.`,"prov.loadConfigFail":`Échec du chargement de la configuration`,"prov.invalidJson":`JSON non valide`,"prov.saveFailed":`Échec de l’enregistrement`,"prov.loginFailStart":`Impossible de démarrer la connexion à {provider}`,"prov.loginError":`Erreur de connexion à {provider} : {error}`,"prov.loginRequestFail":`Échec de la demande de connexion à {provider}`,"prov.loginCancelled":`Connexion à {provider} annulée`,"prov.loginTimeout":`Délai de connexion à {provider} dépassé — le navigateur a été fermé ou l’opération n’a jamais abouti. Réessayez.`,"prov.loginOk":`Connexion à {provider} réussie. Exécutez {cmd} (ou laissez l’application en direct) pour afficher ses modèles.`,"prov.loginSameAccount":`Il s’agit toujours du même compte {provider} — changez de compte dans le navigateur, puis réessayez d’ajouter un compte.`,"oauthTos.highTitle":`{provider} : risque lié à l’abonnement OAuth`,"oauthTos.elevatedTitle":`{provider} : passerelle OAuth non officielle`,"oauthTos.anthropicBody":`La réutilisation directe des jetons OAuth d’un abonnement Claude par l’intermédiaire d’un proxy tiers tel qu’OpenCodex n’est pas une intégration prise en charge par Anthropic et peut entraîner des restrictions d’accès. Les intégrations Agent SDK prises en charge qui utilisent les abonnements Claude sont distinctes.`,"oauthTos.highBody":`OpenCodex connecte {provider} par un mécanisme OAuth tiers. Une utilisation non prise en charge peut entraîner des limitations d’accès ou une suspension.`,"oauthTos.elevatedBody":`OpenCodex connecte {provider} par un mécanisme OAuth non officiel. Utilisez le client officiel lorsque cela est possible ; un trafic inhabituel ou automatisé peut être considéré comme abusif et l’accès peut être limité ou suspendu.`,"oauthTos.saferPath":`Option plus sûre : configurez plutôt une clé API dans OpenCodex.`,"oauthTos.acknowledge":`Je comprends le risque et souhaite tout de même continuer avec OAuth.`,"oauthTos.continue":`Continuer avec OAuth`,"prov.logoutOk":`Déconnexion de {provider} réussie.`,"prov.logoutFail":`Impossible de se déconnecter de {provider}. L’état de votre compte n’a pas changé.`,"prov.removed":`« {name} » supprimé.`,"prov.removedDefault":`« {name} » supprimé. Le fournisseur par défaut est maintenant « {defaultProvider} ».`,"prov.removeFail":`Échec de la suppression de « {name} ».`,"prov.removeLastProvider":`Vous ne pouvez pas supprimer ce fournisseur si aucun autre fournisseur activé ne peut devenir le fournisseur par défaut.`,"prov.removeHasDependentCombos":`Supprimez ou mettez d’abord à jour les combinaisons dépendantes suivantes : {combos}.`,"prov.setDefault":`Définir par défaut`,"prov.setDefaultSuccess":`« {name} » est maintenant le fournisseur par défaut.`,"prov.setDefaultFail":`Impossible de définir « {name} » comme fournisseur par défaut.`,"prov.defaultDisabled":`Activez ce fournisseur avant de le définir comme fournisseur par défaut.`,"prov.updateFail":`Impossible de mettre à jour ce fournisseur.`,"prov.networkError":`Erreur réseau. Vérifiez que le proxy est en cours d’exécution et réessayez.`,"prov.added":`« {name} » ajouté. Déjà actif — exécutez {cmd} (ou redémarrez) pour afficher ses modèles dans le sélecteur de Codex.`,"prov.removeConfirm":`Supprimer le fournisseur « {name} » ? Ses modèles disparaîtront du sélecteur de Codex.`,"prov.hasApiKey":`clé API configurée`,"prov.hasHeaders":`en-têtes personnalisés configurés`,"prov.accounts":`Comptes ({n})`,"prov.accountsAria":`Afficher ou masquer les comptes de {name}`,"prov.accountActive":`Actif`,"prov.accountReauth":`Se reconnecter`,"prov.reauthenticate":`Se réauthentifier`,"prov.reauthAccountMissing":`Le compte sélectionné n’a pas été trouvé après la connexion`,"prov.reauthIdentityMismatch":`Le compte connecté ne correspond pas au compte sélectionné`,"prov.accountAdd":`Ajouter un compte`,"prov.accountNoLabel":`compte {id}`,"prov.accountSwitchTitle":`Utiliser ce compte`,"prov.accountSwitched":`Compte remplacé par {email}.`,"prov.accountSwitchFail":`Échec du changement de compte`,"prov.accountRemoved":`{email} supprimé.`,"prov.accountRemoveFail":`Impossible de supprimer {email}. Le compte n’a pas été modifié.`,"prov.accountRemoveAria":`Supprimer {email}`,"prov.accountRemoveConfirm":`Supprimer le compte {email} ? Ses données de connexion seront supprimées de ce proxy.`,"prov.keyAdd":`Ajouter une clé API`,"prov.keyAdded":`Clé API ajoutée à {name}.`,"prov.keyAddFail":`Échec de l’ajout de la clé API`,"prov.keyPlaceholder":`Coller la clé API`,"prov.keySwitchTitle":`Utiliser cette clé`,"prov.keySwitched":`Clé remplacée par {key}.`,"prov.keySwitchFail":`Échec du changement de clé`,"prov.keyRemoved":`Clé {key} supprimée.`,"prov.keyRemoveAria":`Supprimer la clé {key}`,"prov.keyRemoveConfirm":`Supprimer la clé API {key} ? Elle sera supprimée de la configuration de ce proxy.`,"prov.activeBadge":`Actif`,"prov.disabledBadge":`Désactivé`,"prov.defaultBadge":`Par défaut`,"prov.enable":`Activer`,"prov.disable":`Désactiver`,"prov.enabled":`« {name} » activé. Ses modèles peuvent de nouveau apparaître dans Codex.`,"prov.disabled":`« {name} » désactivé. Les paramètres sont conservés, mais ses modèles sont masqués.`,"prov.enableFail":`Échec de l’activation de « {name} ».`,"prov.disableFail":`Échec de la désactivation de « {name} ».`,"prov.enableAria":`Activer le fournisseur {name}`,"prov.disableAria":`Désactiver le fournisseur {name}`,"prov.defaultCannotDisable":`Le fournisseur par défaut ne peut pas être désactivé`,"prov.openaiAccountMode":`Mode de compte Codex`,"prov.openaiModePool":`Groupe`,"prov.openaiModeDirect":`Direct`,"prov.openaiPoolDesc":`Par défaut. Alterne entre la connexion principale et les comptes ajoutés selon l’affinité, le quota, le délai de récupération et le repli.`,"prov.openaiDirectDesc":`Utilise uniquement la connexion Codex actuelle ou principale. Les comptes du groupe enregistrés ne sont ni lus ni utilisés en alternance.`,"prov.openaiModeSaved":`Mode de compte OpenAI remplacé par {mode}.`,"prov.openaiModeSaveFailed":`Impossible de modifier le mode de compte OpenAI.`,"prov.openaiApiDesc":`Utilise une clé API OpenAI et n’utilise jamais les identifiants d’un compte Codex.`,"prov.manageCodexAccounts":`Gérer les comptes Codex`,"prov.openaiApiMissing":`Clé API requise`,"prov.openaiApiSetup":`Configurer la clé API`,"models.tab.catalog":`Modèles`,"models.tab.combos":`Combinaisons`,"models.tab.compatibility":`Compatibilité`,"models.tab.routing":`Routage (bêta)`,"models.tabsLabel":`Espaces des modèles`,"models.subtitle.combos":`Groupes ordonnés de modèles qui répondent sous un même identifiant. Enchaînez les cibles avec le repli ou répartissez la charge avec une stratégie d’équilibrage.`,"models.subtitle.compatibility":`Matrice en lecture seule des verdicts de compatibilité issus des preuves de projection du laboratoire.`,"models.subtitle.routing":`Profils de stratégie, évaluation à blanc et analyses de routage fondées sur les sources.`,"models.subtitle":`Choisissez les modèles visibles par Codex — accès direct aux GPT natifs et fournisseurs routés, regroupés par fournisseur (cliquez sur un en-tête pour le réduire). Les modèles masqués sont retirés du catalogue et du sélecteur, mais restent directement accessibles par leur identifiant exact. Les modifications s’appliquent au prochain tour Codex — opencodex invalide le cache de modèles de Codex de 5 min, sans nécessiter de redémarrage.`,"models.nativeGroupLabel":`OpenAI natif`,"models.nativeHint":"Les modèles en accès direct utilisent l’option de compte Groupe ou Direct sélectionnée dans Fournisseurs. La désactivation d’un modèle le masque dans le sélecteur Codex (son entrée de catalogue est conservée afin que sa réactivation la restaure à l’identique). Ajouter un modèle ici enregistre un sélecteur routé `openai/`, et non un nouvel identifiant passthrough brut.","models.active":`{active}/{total} visibles`,"models.workspace.providers":`Fournisseurs`,"models.workspace.allProviders":`Tous les fournisseurs`,"models.workspace.mainAria":`Détails du modèle`,"models.allOn":`Tout activer`,"models.allOff":`Tout désactiver`,"models.presetLabel":`Modèles`,"models.presetMode_preset":`Préréglage`,"models.presetMode_all":`Tous`,"models.presetMode_custom":`Personnalisé`,"models.presetSummary":`{count} sur {total} affichés — préréglage core v{version}`,"models.presetUpdateAvailable":`Préréglage v{version} disponible`,"models.presetAppliedToast":`{provider} : préréglage appliqué — {count} modèles sélectionnés`,"models.presetClearedToast":`{provider} : tous les modèles affichés`,"models.presetEmpty":`{provider} : le préréglage n’a trouvé aucun modèle — sélection inchangée`,"models.presetConfirmReplace":`Remplacer votre sélection par le préréglage de {count} modèles ?`,"models.cap350k":`Plafond de 350k`,"models.capApplied":`Plafond de contexte appliqué — il prendra effet au prochain tour Codex.`,"models.capSaveFailed":`Échec de l’enregistrement du plafond de contexte`,"models.contextCapped":`Plafond de 350k`,"models.contextCapLabel":`Fenêtre par défaut / plafond`,"models.v2Label":`Sous-agent`,"models.shadowCallOriginal":`⚠ {models} →`,"models.v2DocsLink":`Que sont v1 et v2 ?`,"models.v2Mode_v1":`v1`,"models.v2Mode_default":`base`,"models.v2Mode_v2":`v2`,"models.v2ModeDesc_v1":`Tous les modèles → interface v1`,"models.v2ModeDesc_default":`Valeurs par défaut en amont (sol/terra=v2, luna=v1)`,"models.v2ModeDesc_v2":`Tous les modèles → interface v2`,"models.keepNativeOnV1":`Garder ChatGPT sur v1`,"models.keepNativeOnV1Hint":`ChatGPT chiffre les tâches enfants v2 uniquement lorsqu’un parent natif ChatGPT reste sur v2, de sorte que Grok et Claude ne peuvent pas les lire. Activez cette option pour garder Sol/Terra sur v1 et éviter ce chiffrement. Les parents routés restent sur v2.`,"models.v2Help":`Contrôle l’interface multi-agent pour tous les modèles. - -v1 : agent classique à fil unique. Tous les modèles utilisent l’interface collab v1. -base : valeurs par défaut en amont — sol/terra utilisent v2, luna utilise v1 et les autres suivent l’indicateur de fonctionnalité codex. -v2 : agent multifil avec spawn_agent. Tous les modèles utilisent l’interface collab v2. - -En v2, « Garder ChatGPT sur v1 » laisse Sol/Terra sur l’interface v1 afin qu’ils puissent encore lancer Grok ou Claude. ChatGPT chiffre les tâches enfants v2 ; les modèles routés ne peuvent pas les lire. Les parents routés restent sur v2. - -Les modifications s’appliquent aux nouvelles sessions.`,"dash.multiAgent":`Sous-agent`,"models.v2Conflict":`[agents] max_threads est défini — codex refusera de démarrer ; supprimez-le de config.toml`,"models.v2Applied":`Mode sous-agent mis à jour — s’applique aux nouvelles sessions (redémarrez l’application Codex pour actualiser le sélecteur)`,"models.v2ThreadsLabel":`Nombre maximal de fils`,"models.v2ThreadsDefault":`par défaut (4)`,"models.v2ThreadsApplied":`Limite de fils mise à jour — s’applique aux nouvelles sessions`,"models.v2ThreadsInvalid":`La limite de fils doit être un entier >= 1`,"models.v2ThreadsApply":`Appliquer`,"models.capValue":`Défaut {value}`,"models.contextSettings":`Fenêtres perso`,"models.contextSettingsTitle":`Fenêtres perso — {provider}`,"models.contextDefault":`Valeur par défaut du fournisseur`,"models.contextModel":`Modèle`,"models.contextModelOverride":`Remplacement pour le modèle`,"models.contextHint":`Si vous connaissez déjà la fenêtre, écrivez ici la fenêtre Codex réelle. Sans métadonnées amont, cette valeur est utilisée ; une fenêtre plus grande est seulement abaissée, une plus petite est conservée. Laisser vide utilise la « Fenêtre par défaut / plafond » du fournisseur, ou 128k si ce plafond est désactivé.`,"models.contextAutomatic":`Détection automatique`,"models.contextSaved":`Fenêtres de contexte mises à jour — prend effet au prochain tour Codex.`,"models.contextUnchanged":`Aucune modification des fenêtres de contexte à enregistrer.`,"models.contextSaveFailed":`Échec de l’enregistrement des fenêtres de contexte`,"models.contextInvalid":`Les fenêtres de contexte doivent être des nombres entiers positifs`,"models.contextCappedValue":`Plafond de {value}`,"models.setAll":`Tout définir`,"models.setAllHint":`Active la fenêtre par défaut {value} pour chaque fournisseur routé. Si un relais omet context_window / context_length, cette valeur devient la fenêtre Codex réelle. Pour un seul modèle, utilisez « Fenêtres perso » sur la même ligne. Les fournisseurs natifs ne sont pas affectés.`,"models.collapseAll":`Tout réduire`,"models.expandAll":`Tout développer`,"models.orderHint":`Ordre du sélecteur : choix des sous-agents (dans l’ordre sélectionné) → autres modèles routés, classés par ordre alphabétique du fournisseur puis par ID de modèle → modèles natifs. Les options de visibilité ne font que filtrer les modèles ; elles ne modifient pas cet ordre.`,"models.custom":`Personnalisé…`,"models.customApply":`Appliquer`,"models.customPlaceholder":`Jetons (p. ex. 420000)`,"models.customAdd":`Ajouter un modèle personnalisé`,"models.customAddTitle":`Ajouter un modèle personnalisé — {provider}`,"models.customEditTitle":`Modifier le modèle personnalisé — {provider}`,"models.customAdded":`Modèle personnalisé ajouté`,"models.customUpdated":`Modèle personnalisé mis à jour`,"models.customDeleted":`Modèle personnalisé supprimé`,"models.customSaveFailed":`Échec de l’enregistrement du modèle personnalisé`,"models.customSaving":`Enregistrement…`,"models.customAddBtn":`Ajouter`,"models.customEditBtn":`Mettre à jour`,"models.customEdit":`Modifier`,"models.customDelete":`Supprimer`,"models.customDeleteConfirm":`Supprimer le modèle {name} ?`,"models.customBadge":`Personnalisé`,"models.customSummary":`{count} personnalisés`,"models.customFieldModelId":`ID du modèle (slug du point de terminaison)`,"models.customFieldModelIdPlaceholder":`p. ex. qwen4-max-preview`,"models.customFieldDisplayName":`Nom d’affichage (facultatif)`,"models.customFieldDisplayNamePlaceholder":`p. ex. Qwen 4 Max Preview`,"models.customFieldContext":`Fenêtre de contexte`,"models.customFieldModalities":`Modalités d’entrée`,"models.customFieldReasoning":`Effort de raisonnement`,"models.customFieldReasoningOverride":`Remplacer l’effort de raisonnement`,"models.reasoningEffort.none":`Aucun`,"models.reasoningEffort.minimal":`Minimal`,"models.reasoningEffort.low":`Faible`,"models.reasoningEffort.medium":`Moyen`,"models.reasoningEffort.high":`Élevé`,"models.reasoningEffort.xhigh":`Très élevé`,"models.reasoningEffort.max":`Maximum`,"models.tipProvider":`Fournisseur`,"models.tipContext":`Contexte`,"models.tipModalities":`Modalités`,"models.tipStatus":`État`,"models.tipActive":`Actif`,"models.tipDisabled":`Désactivé`,"models.applied":`Appliqué — prend effet au prochain tour Codex.`,"models.saveFailed":`Échec de l’enregistrement`,"models.networkError":`Erreur réseau — le proxy est-il en cours d’exécution ?`,"models.loadFail":`Échec du chargement des modèles — le proxy est-il en cours d’exécution ?`,"models.noRouted":`Aucun modèle routé`,"models.noRoutedHint":`Se connecter d’abord à un fournisseur ou en ajouter un.`,"models.emptyDiscovery":`Aucun modèle n’a été détecté. Vérifiez le point de terminaison du fournisseur ou ajoutez un modèle statique/personnalisé.`,"models.emptyDiscoveryDisabled":`La détection dynamique des modèles est désactivée et aucun modèle statique n’est configuré.`,"models.discoveryFailedBadge":`Échec de la détection`,"models.discoveryFailedHttp":`Échec de la détection des modèles (HTTP {status}).`,"models.discoveryFailedBlocked":`La détection des modèles a été bloquée par la politique de destination.`,"models.discoveryFailedInvalidResponse":`La détection des modèles a renvoyé une réponse non valide.`,"models.discoveryFailedNetwork":`La détection des modèles a échoué en raison d’une erreur réseau.`,"models.discoveryFailedProvider":`Le fournisseur a signalé une erreur de détection des modèles.`,"models.discoveryFailedGeneric":`Échec de la détection des modèles.`,"models.openProviderSettings":`Ouvrir les paramètres du fournisseur`,"models.loading":`Chargement…`,"models.search":`Rechercher des modèles…`,"models.showMore":`Afficher {n} de plus`,"models.allowlistLabel":`Sélection uniquement`,"models.allowlistHint":`Seuls les modèles cochés sont inclus dans le catalogue (vide = tous). Utile pour les fournisseurs proposant des milliers de modèles.`,"models.selectedCount":`{n} sélectionnés`,"sub.subtitle":`La commande {cmd} de Codex ne présente que les 5 premiers modèles (par priorité) comme remplacements. Choisissez-en jusqu’à 5 ici — natifs gpt ou routés — et opencodex définit leur priorité dans le catalogue pour qu’ils apparaissent en tête. Tout autre modèle reste accessible par son nom exact ; ceci contrôle uniquement ce qui est affiché.`,"sub.featured":`À la une`,"sub.advanced":`Avancé`,"sub.orderHintAria":`Comment cet ordre est utilisé`,"sub.orderHint":`L’ordre affiché ici détermine les positions 1 à 5 en haut du sélecteur de modèles Codex et les modèles candidats par défaut pour {cmd}.`,"sub.noneSelected":`Aucun modèle sélectionné — faites votre choix dans la liste ci-dessous.`,"sub.models":`Modèles`,"sub.search":`Rechercher des modèles (gpt natifs + routés)…`,"sub.settings":`Paramètres`,"sub.sections":`Sections des sous-agents`,"sub.delegation.model":`Modèle à appeler en premier`,"sub.delegation.modelHint":`Le modèle que Codex sollicite en premier lorsqu’il délègue une tâche. La liste À la une ci-dessus contient les modèles qu’il peut appeler ; celui-ci est appelé en premier.`,"sub.noModels":`Aucun modèle — connectez-vous d’abord à un fournisseur ou ajoutez-en un.`,"sub.saved":`{n} modèles enregistrés. Démarrez une nouvelle session Codex (ou exécutez {cmd}) pour les voir comme remplacements de spawn_agent.`,"sub.saveFailed":`Échec de l’enregistrement`,"sub.networkError":`Erreur réseau — le proxy est-il en cours d’exécution ?`,"sub.loadFail":`Échec du chargement des modèles — le proxy est-il en cours d’exécution ?`,"sub.loading":`Chargement…`,"sub.moveUp":`Monter {m}`,"sub.moveDown":`Descendre {m}`,"sub.removeAria":`Retirer {m}`,"sub.workspace.addToFeatured":`Ajouter {m} à la sélection À la une`,"sub.workspace.allModels":`Tous les modèles`,"sub.workspace.featuredFull":`La liste À la une est complète (5 maximum)`,"sub.workspace.mainAria":`Détails du modèle de sous-agent`,"sub.workspace.notFeatured":`Non mis à la une`,"sub.workspace.priority":`Priorité`,"sub.ultraMode":`Mode Ultra`,"sub.ultraModeHint":`Activer la politique de délégation multi-agent proactive pour tous les modèles et niveaux de raisonnement (sans modifier le niveau de raisonnement lui-même). Écrit features.multi_agent_v2.multi_agent_mode_hint_text dans config.toml.`,"sub.ultraModeV2Required":`Nécessite l’interface multi-agent v2 — activez multi_agent_v2 et sélectionnez d’abord v2 dans le contrôle du mode Sous-agent.`,"sub.ultraModeText":`Texte de délégation du mode Ultra`,"sub.ultraModePreset":`Rétablir le préréglage`,"sub.ultraModeLoadFail":`Échec du chargement des paramètres du mode Ultra — le proxy est-il en cours d’exécution ?`,"sub.ultraModeSaveFail":`Échec de l’enregistrement des paramètres du mode Ultra`,"sub.ultraModeSaved":`Mode Ultra enregistré. S’applique aux nouvelles sessions Codex.`,"sub.workspace.removeFromFeatured":`Retirer {m} de la sélection À la une`,"sub.workspace.selectModel":`Sélectionner un modèle`,"sub.workspace.selectModelDesc":`Choisissez un modèle dans la liste pour afficher ses détails et le mettre à la une pour spawn_agent.`,"sub.workspace.selector":`Sélecteur public`,"logs.title":`Journaux des requêtes`,"logs.tabLogs":`Journaux`,"logs.tabDebug":`Débogage`,"logs.subtitle":`Requêtes récentes routées par le proxy opencodex local, de la plus récente à la plus ancienne.`,"logs.autoRefresh":`Actualisation automatique`,"logs.noRequests":`Aucune requête pour le moment.`,"logs.loadError":`Impossible de charger les journaux des requêtes.`,"logs.filter.surface.label":`Interface`,"logs.filter.surface.all":`Toutes`,"logs.filter.surface.claude":`Claude`,"logs.filter.surface.codex":`Codex`,"logs.filter.surface.grok":`Grok`,"logs.filter.interceptedHelpersOnly":`Assistants interceptés uniquement`,"logs.badge.interceptedHelper":`I · {model}`,"logs.badge.interceptedHelperTitle":`Requête d'assistant interceptée`,"logs.filter.conversation.label":`Conversation`,"logs.filter.conversation.placeholder":`Coller l’ID de conversation`,"logs.filter.conversation.clear":`Effacer`,"logs.filter.model.label":`Modèle`,"logs.filter.model.placeholder":`Filtrer par modèle ou fournisseur`,"logs.filter.conversation.apply":`Filtrer les journaux`,"logs.conversation.totals":`{requests} requêtes · {tokens} jetons · {cost}`,"logs.conversation.scope":`Les totaux couvrent uniquement le tampon circulaire des journaux actuellement chargé.`,"logs.conversation.excluded":`({unpriced} sans tarif, {unmetered} sans mesure exclus du total en ~$)`,"logs.cost.approximate":`{amount}`,"logs.cost.lowerBound":`≥{amount}`,"logs.cost.unavailable":`indisponible`,"logs.detail.conversation":`Conversation`,"logs.badge.claude":`Claude`,"logs.badge.grok":`Grok`,"logs.col.time":`Heure`,"logs.col.request":`Requête`,"logs.col.model":`Modèle`,"logs.col.effort":`Niveau`,"logs.col.provider":`Fournisseur`,"logs.col.status":`État`,"logs.col.tokens":`Jetons`,"logs.col.tokPerSec":`jetons/s`,"logs.col.estimatedCost":`~$`,"logs.metric.tokPerSecTitle":`Jetons de sortie par seconde sur toute la durée de la requête`,"logs.metric.estimatedCostTitle":`Équivalent au tarif catalogue de l’API, et non montant réellement facturé ; aucun tarif n’est disponible en l’absence de correspondance`,"usage.cost.total":`Équivalent au tarif catalogue de l’API (cette période)`,"usage.cost.disclaimer":`Ceci n’est pas un reçu de facturation. L’utilisation d’un abonnement ou les crédits du fournisseur peuvent s’appliquer à la place.`,"usage.cost.unpricedNote":`{count} requêtes exclues (aucun tarif ni donnée d’utilisation)`,"logs.detail.section.basic":`Informations générales`,"logs.detail.route.section":`Décision de routage`,"logs.detail.route.kind":`Type de route`,"logs.detail.route.profile":`Profil`,"logs.detail.route.selected":`Sélection`,"logs.detail.route.candidates":`Candidats`,"logs.detail.route.unknown":`Aucune trace de routage enregistrée pour cette requête (ligne antérieure à la traçabilité).`,"logs.detail.section.performance":`Performances`,"logs.detail.section.cost":`Équivalent au tarif catalogue de l’API`,"logs.detail.section.attempts":`Tentatives de combinaison`,"logs.detail.section.usage":`Utilisation brute`,"logs.detail.ttft":`TTFT`,"logs.detail.costTotal":`Équivalent au tarif catalogue`,"logs.detail.totalTokens":`Nombre total de jetons`,"logs.detail.matchedKey":`Clé de tarif correspondante`,"logs.detail.priceSource":`Source du tarif`,"logs.detail.unavailableReason":`Motif d’indisponibilité`,"logs.detail.copyRequestId":`Copier l’ID de requête`,"logs.detail.copied":`Copié`,"logs.detail.source.jawcode":`Catalogue jawcode`,"logs.detail.source.expected":`Remplacement par le tarif attendu`,"logs.detail.source.user":`Remplacement par le tarif configuré pour le fournisseur`,"logs.detail.verification.verified":`Vérifié`,"logs.detail.verification.derived":`Dérivé du modèle de base`,"logs.detail.attempt.target":`Fournisseur / modèle`,"logs.detail.attempt.reason":`Résultat / motif`,"logs.detail.attempt.completed":`Terminée`,"logs.detail.attempt.e2eNote":`Le débit global en jetons/s est calculé de bout en bout ; chaque tentative utilise sa propre durée.`,"logs.detail.attempt.recovery.transient5xx":`Erreur 5xx temporaire`,"logs.detail.attempt.recovery.connectionReset":`Réinitialisation de la connexion`,"logs.detail.attempt.recovery.emptyCompletion":`Nouvelle tentative après une réponse vide`,"logs.detail.attempt.recovery.oauth401":`Réauthentification OAuth`,"logs.detail.attempt.recovery.key429":`Clé soumise à une limitation de débit (429)`,"logs.detail.attempt.recovery.rateLimit429":`Limitation de débit (429)`,"logs.detail.attempt.recovery.anthropicOauth429":`Limitation de débit OAuth Anthropic (429)`,"logs.detail.attempt.recovery.image413":`Charge utile d’image trop volumineuse (413)`,"logs.detail.attempt.recovery.unknown":`Motif de récupération inconnu`,"logs.detail.reason.usage_missing":`L’utilisation n’a pas été communiquée.`,"logs.detail.reason.usage_unsupported":`Ce fournisseur ne communique pas l’utilisation.`,"logs.detail.reason.output_missing":`Aucun nombre positif de jetons de sortie n’a été communiqué.`,"logs.detail.reason.invalid_duration":`La durée de la requête n’est pas valide.`,"logs.detail.reason.price_unmatched":`Aucun tarif correspondant n’a été trouvé.`,"logs.detail.reason.invalid_cache_breakdown":`Le détail des jetons du cache est incompatible avec le nombre total de jetons d’entrée.`,"logs.detail.reason.invalid_usage":`Les données d’utilisation contiennent une valeur de jetons non valide.`,"logs.detail.reason.combo_attempt_unavailable":`Au moins une tentative de combinaison n’a pas pu être chiffrée.`,"logs.detail.estimate.usage_estimated":`L’utilisation du fournisseur est estimée.`,"logs.detail.estimate.cache_detail_missing":`Les détails du cache n’étaient pas disponibles ; l’entrée est une estimation de la limite supérieure.`,"logs.detail.estimate.expected_price_overlay":`Un tarif catalogue attendu et vérifié a été utilisé.`,"logs.detail.estimate.provider_cost_overlay":`Un remplacement de tarif configuré pour le fournisseur a été utilisé.`,"logs.detail.estimate.priority_lower_bound":`Le tarif Priority confirmé n’est pas disponible ; l’estimation affichée est une borne inférieure connue.`,"logs.col.error":`Erreur`,"logs.col.upstreamReason":`Motif en amont`,"logs.col.duration":`Durée`,"logs.modelTooltip.model":`modèle`,"logs.modelTooltip.resolvedModel":`modèle résolu`,"logs.modelTooltip.requestedTier":`niveau demandé`,"logs.modelTooltip.configuredTier":`niveau configuré`,"logs.modelTooltip.responseTier":`niveau de réponse`,"logs.modelTooltip.supportsTier":`prise en charge du niveau`,"logs.tokens.reported":`communiqués`,"logs.tokens.unreported":`non communiqués`,"logs.tokens.unsupported":`non pris en charge`,"logs.tokens.estimated":`estimés`,"logs.tokens.input":`entrée`,"logs.tokens.output":`sortie`,"logs.tokens.cacheRead":`lecture du cache (c)`,"logs.tokens.cacheWrite":`écriture dans le cache (w)`,"logs.tokens.reasoning":`raisonnement`,"logs.tokens.noCache":`aucune donnée de cache`,"logs.tokens.contextTotal":`contexte actif`,"logs.tokens.noCacheNote":`ce fournisseur ne communique pas les jetons du cache`,"logs.tokens.noCacheCursor":`détails du cache Cursor non communiqués`,"logs.tokens.noCacheCursorNote":`Cursor n’indique pas le nombre de jetons lus/écrits dans le cache ; la valeur est inconnue et ne constitue pas un défaut de cache confirmé`,"logs.tokens.estimatedNote":`estimés (le fournisseur ne communique pas l’utilisation exacte)`,"logs.details":`Détails`,"logs.detailTitle":`Détails de la requête`,"logs.detailRaw":`Entrée de journal brute`,"debug.title":`Débogage`,"debug.subtitle":`Diagnostics facultatifs du transport des fournisseurs et de l’extraction de l’utilisation. Les erreurs de requête et les erreurs 502 restent dans l’onglet Journaux.`,"debug.debug":`Débogage du fournisseur`,"debug.usage":`Extraction de l’utilisation`,"debug.injection":`Journal des injections`,"debug.claude":`Entrées Claude`,"debug.claudeInbound.title":`Requêtes entrantes Claude`,"debug.claudeInbound.sub":`Ce que Claude Code/Desktop envoie réellement (thinking, effort, métadonnées) — aucun texte de prompt n’est stocké.`,"debug.claudeInbound.empty":`Aucune requête capturée pour le moment. Envoyez un message depuis Claude pendant que cette option est activée.`,"debug.claudeInbound.time":`Heure`,"debug.claudeInbound.endpoint":`Point de terminaison`,"debug.claudeInbound.model":`Modèle`,"debug.claudeInbound.none":`aucun`,"debug.reset":`Effacer les remplacements d’exécution`,"debug.refresh":`Actualiser`,"debug.follow":`Suivre`,"debug.streamProvider":`Fournisseur`,"debug.streamUsage":`Utilisation`,"debug.streamInjection":`Injection`,"debug.loading":`Chargement des paramètres de débogage…`,"debug.loadFailed":`Impossible de charger les paramètres de débogage.`,"debug.emptyTitle":`La journalisation de débogage est désactivée`,"debug.empty":`Activez Débogage du fournisseur ou Extraction de l’utilisation dans la carte ci-dessus. Les lignes apparaîtront ici après l’envoi d’une requête par le proxy.`,"debug.noLinesTitle":`En attente de lignes`,"debug.noLines.provider":`Le débogage du fournisseur est activé, mais il n’enregistre que les anomalies de transport (trames abandonnées ou mal formées, et événements de connexion/nouvelle tentative Cursor). Une requête sans anomalie auprès d’un fournisseur comme Anthropic peut ne produire aucune ligne.`,"debug.noLines.usage":`L’extraction de l’utilisation est activée, mais rien n’a encore été capturé. Envoyez une conversation ou une requête par Codex pour qu’elle apparaisse ici.`,"debug.noLines.injection":`Le journal des injections est activé, mais rien n’a encore été capturé. Il consigne l’injection des directives multi-agents et les décisions de plafonnement du niveau lors des tours collab et des sous-agents.`,"usage.title":`Utilisation`,"usage.subtitle":`Comptabilisation locale des jetons par votre proxy. Une utilisation manquante n’est jamais affichée comme nulle.`,"usage.loading":`Chargement des données d’utilisation…`,"usage.empty":`Aucune utilisation enregistrée pour le moment. Envoyez une requête par le proxy pour voir l’activité ici.`,"usage.loadError":`Impossible de charger les données d’utilisation.`,"usage.range.all":`Tout`,"usage.range.available":`Historique disponible`,"usage.historyTruncated":`Les totaux couvrent uniquement l’historique disponible, car les données d’utilisation plus anciennes n’ont pas été chargées.`,"usage.historyTruncatedWindow":`Les heures de début des requêtes dans les lignes chargées vont de {start} à {end}. Les entrées antérieures du fichier ont été omises en raison de la limite de lecture ; toute période sélectionnée peut donc être incomplète.`,"usage.range.30d":`30 j`,"usage.range.7d":`7 j`,"usage.card.requests":`Requêtes`,"usage.card.measured":`Mesurées`,"usage.card.reported":`Communiquées`,"usage.card.totalTokens":`Nombre total de jetons`,"usage.card.cachedTokens":`Lectures du cache`,"usage.card.cachedTokensHint":`Jetons de prompt servis depuis le cache du fournisseur (lectures). Les écritures dans le cache sont indiquées ci-dessous lorsqu’elles sont présentes.`,"usage.card.cacheWriteTokens":`écritures dans le cache`,"usage.card.coverage":`Couverture`,"usage.card.activeDays":`Jours actifs`,"usage.section.heatmap":`Activité quotidienne`,"usage.section.overview":`Vue d’ensemble`,"usage.section.models":`Modèles`,"usage.section.providers":`Fournisseurs`,"usage.section.coverage":`Répartition de la couverture`,"usage.workspace.report":`Rapport d’utilisation`,"usage.workspace.sections":`Sections d’utilisation`,"usage.coverage.measured":`Mesurée`,"usage.coverage.reported":`Communiquée par le fournisseur`,"usage.coverage.estimated":`Estimée`,"usage.coverage.note":`Les entrées mesurées comprennent les nombres de jetons communiqués par le fournisseur et ceux qui sont estimés. Les requêtes non communiquées ou non prises en charge sont suivies, mais ne sont jamais artificiellement comptées comme zéro jeton.`,"usage.search.models":`Rechercher des modèles…`,"usage.col.requests":`Requêtes`,"usage.col.measured":`Mesurées`,"usage.col.reported":`Communiquées`,"usage.col.tokens":`Jetons`,"usage.col.share":`Part`,"usage.heatmap.less":`Moins`,"usage.heatmap.more":`Plus`,"usage.dayMon":`Lun`,"usage.dayWed":`Mer`,"usage.dayFri":`Ven`,"usage.heatmap.tooltipTokens":`{tokens} jetons`,"usage.heatmap.tooltipRequests":`{requests} requêtes`,"nav.storage":`Stockage`,"storage.title":`Stockage`,"storage.subtitle":`Consultez l’espace utilisé dans CODEX_HOME. Le nettoyage ne touche jamais aux sessions actives.`,"storage.loading":`Analyse du stockage…`,"storage.empty":`CODEX_HOME est vide ou absent — rien à signaler.`,"storage.error":`Échec de l’analyse du stockage. Vérifiez que CODEX_HOME pointe vers un répertoire valide.`,"storage.refresh":`Relancer l’analyse`,"storage.rescanned":`Analyse terminée.`,"storage.card.total":`Taille totale`,"storage.card.files":`Fichiers`,"storage.card.home":`CODEX_HOME`,"storage.snapshot.lastScan":`Dernière analyse`,"storage.snapshot.scanning":`Analyse…`,"storage.snapshot.unavailable":`Aucune analyse pour le moment.`,"storage.cleanupCard.title":`Libérer de l’espace`,"storage.cleanupCard.tabs":`Options de nettoyage`,"storage.cleanupCard.tab.policy":`Politique`,"storage.cleanupCard.tab.quarantine":`Quarantaine`,"storage.cleanup.noArchives":`Aucune session archivée à nettoyer.`,"storage.section.buckets":`Catégories`,"storage.section.largest":`Fichiers les plus volumineux`,"storage.workspace.overview":`Vue d’ensemble`,"storage.workspace.selectBucket":`Sélectionnez une catégorie dans la liste pour afficher sa répartition.`,"storage.col.bucket":`Catégorie`,"storage.col.size":`Taille`,"storage.col.files":`Fichiers`,"storage.col.oldest":`Plus ancien`,"storage.col.newest":`Plus récent`,"storage.col.rows":`Lignes de la BDD`,"storage.rows.unknown":`inconnu (verrouillé)`,"storage.bucket.sessions":`Sessions actives`,"storage.bucket.archived_sessions":`Sessions archivées`,"storage.bucket.logs_db":`Base de données des journaux`,"storage.bucket.state_db":`Base de données d’état`,"storage.bucket.attachments":`Pièces jointes`,"storage.bucket.deletion_manifests":`Manifestes de suppression`,"storage.bucket.other":`Autres`,"storage.cleanup.title":`Nettoyage des archives`,"storage.cleanup.help":`Supprimez un pourcentage des sessions archivées les plus anciennes. Les sessions actives ne sont jamais touchées. La quarantaine est le mode par défaut : les fichiers sont déplacés vers CODEX_HOME/.trash.`,"storage.cleanup.slider":`Pourcentage d’archives les plus anciennes`,"storage.cleanup.percent":`{percent} %`,"storage.cleanup.preset":`{percent}`,"storage.cleanup.preview":`Aperçu`,"storage.cleanup.confirmTitle":`Confirmer le nettoyage des archives`,"storage.cleanup.confirmBody":`Cette opération traitera {count} fichier(s) archivé(s) (~{size}), soit les {percent} % les plus anciens.`,"storage.cleanup.moreFiles":`…et {n} de plus`,"storage.cleanup.permanent":`Supprimer définitivement (sans quarantaine)`,"storage.cleanup.permanentWarn":`La suppression définitive est irréversible.`,"storage.cleanup.quarantineNote":`Les fichiers sont déplacés vers .trash sous CODEX_HOME. Vous pouvez les restaurer depuis l’onglet Quarantaine.`,"storage.cleanup.cancel":`Annuler`,"storage.cleanup.confirmQuarantine":`Mettre en quarantaine`,"storage.cleanup.confirmPermanent":`Supprimer définitivement`,"storage.cleanup.doneQuarantine":`{count} fichier(s) mis en quarantaine ({size}).`,"storage.cleanup.donePermanent":`{count} fichier(s) supprimé(s) définitivement ({size}).`,"storage.cleanup.previewFailed":`Échec de l’aperçu.`,"storage.cleanup.cleanupFailed":`Échec du nettoyage.`,"storage.cleanup.err.codex_busy":`Codex utilise state.sqlite — réessayez après avoir quitté Codex.`,"storage.cleanup.err.stale_preview":`Les fichiers archivés ont changé depuis l’aperçu — relancez l’aperçu.`,"storage.cleanup.err.restore_pending_overlap":`Les archives sélectionnées chevauchent une restauration incomplète depuis la corbeille — terminez ou relancez d’abord la restauration.`,"storage.cleanup.err.referenced_history":`Les archives sélectionnées sont encore référencées par un historique dérivé ou paginé.`,"storage.cleanup.err.invalid_digest":`L’empreinte de l’aperçu est absente ou non valide.`,"storage.cleanup.err.invalid_mode":`Le mode de nettoyage doit être la quarantaine ou la suppression définitive.`,"storage.cleanup.err.fs_failed":`Échec du nettoyage du système de fichiers. Certaines modifications ont peut-être déjà été appliquées — vérifiez CODEX_HOME/.trash et tout chemin de récupération indiqué.`,"storage.cleanup.err.fs_failed_trash":`Échec du nettoyage du système de fichiers. Certaines modifications ont peut-être déjà été appliquées — recherchez les fichiers récupérables dans {trashDir} et manifest.json.`,"storage.cleanup.err.db_reconcile_failed":`Impossible de mettre à jour la base de données d’état de Codex.`,"storage.cleanup.err.cleanup_failed":`Échec du nettoyage.`,"storage.trash.title":`Quarantaine`,"storage.trash.help":`Sessions archivées déplacées vers CODEX_HOME/.trash. La restauration remet en place les fichiers JSONL et les lignes de conversation.`,"storage.trash.empty":`Aucune entrée en quarantaine.`,"storage.trash.loading":`Chargement de la quarantaine…`,"storage.trash.col.when":`Mise en quarantaine`,"storage.trash.col.files":`Fichiers`,"storage.trash.col.size":`Taille`,"storage.trash.col.mode":`Mode`,"storage.trash.col.id":`Entrée`,"storage.trash.restore":`Restaurer`,"storage.trash.confirmTitle":`Restaurer l’entrée de quarantaine ?`,"storage.trash.confirmBody":`Restaurer {count} fichier(s) (~{size}) depuis {id} vers les sessions archivées.`,"storage.trash.cancel":`Annuler`,"storage.trash.confirmRestore":`Restaurer`,"storage.trash.done":`{count} fichier(s) restauré(s) ({size}).`,"storage.trash.restoreFailed":`Échec de la restauration.`,"storage.trash.listFailed":`Impossible d’afficher les entrées de quarantaine.`,"storage.trash.mode.quarantine":`quarantaine`,"storage.trash.mode.permanent":`définitif (incomplet)`,"storage.trash.err.codex_busy":`Codex utilise state.sqlite — réessayez après avoir quitté Codex.`,"storage.trash.err.invalid_trash":`L’ID de l’entrée de la corbeille est absent ou non valide.`,"storage.trash.err.missing_trash":`L’entrée de la corbeille est introuvable.`,"storage.trash.err.dest_exists":`La destination de restauration existe déjà — supprimez ou renommez le fichier archivé, puis réessayez.`,"storage.trash.err.fs_failed":`Échec de la restauration du système de fichiers. Certains fichiers ont peut-être déjà été restaurés — vérifiez archived_sessions et .trash.`,"storage.trash.err.db_reconcile_failed":`Impossible de restaurer les lignes de la base de données d’état de Codex.`,"storage.trash.err.storage_mutation_busy":`Un autre nettoyage ou une autre restauration du stockage est en cours — réessayez dans quelques instants.`,"storage.trash.err.restore_failed":`Échec de la restauration.`,"storage.trash.err.restore_worker_timeout":`La restauration a pris trop de temps (plus de 10 minutes) et a été arrêtée.`,"storage.trash.err.restore_worker_aborted":`La restauration a été annulée lors de l’arrêt.`,"storage.trash.err.restore_worker_failed":`Le processus de restauration s’est interrompu ou a échoué de manière inattendue.`,"storage.policy.title":`Politique de nettoyage automatique`,"storage.policy.help":`Nettoyage facultatif par lots lorsque les sessions archivées dépassent un seuil. Désactivé par défaut — jamais activé automatiquement.`,"storage.policy.loading":`Chargement de la politique…`,"storage.policy.loadFailed":`Impossible de charger la politique de nettoyage.`,"storage.policy.saveFailed":`Impossible d’enregistrer la politique de nettoyage.`,"storage.policy.runFailed":`Échec de l’exécution de la politique.`,"storage.policy.alreadyRunning":`Une exécution de la politique de nettoyage est déjà en cours.`,"storage.policy.invalid":`Valeurs de politique non valides.`,"storage.policy.enabled":`Activer le nettoyage automatique`,"storage.policy.enabledHint":`Cette option est désactivée par défaut. Une fois activée, elle ne s’exécute que selon la planification choisie (ou avec Exécuter maintenant).`,"storage.policy.threshold":`Lorsque la taille des archives dépasse (Gio)`,"storage.policy.trigger":`Déclencheur`,"storage.policy.target":`Objectif du nettoyage`,"storage.policy.targetPercent":`Supprimer les archives les plus anciennes (%)`,"storage.policy.targetReduce":`Réduire la taille des archives à (Gio)`,"storage.policy.thresholdInc":`Augmenter le seuil`,"storage.policy.thresholdDec":`Diminuer le seuil`,"storage.policy.percentInc":`Augmenter le pourcentage`,"storage.policy.percentDec":`Diminuer le pourcentage`,"storage.policy.reduceInc":`Augmenter la taille cible`,"storage.policy.reduceDec":`Diminuer la taille cible`,"storage.policy.schedule":`Planification`,"storage.policy.schedule.manual":`Manuel uniquement`,"storage.policy.schedule.startup":`Au démarrage du proxy`,"storage.policy.schedule.daily":`Quotidienne`,"storage.policy.schedule.weekly":`Hebdomadaire`,"storage.policy.mode":`Mode de suppression`,"storage.policy.mode.quarantine":`Quarantaine (par défaut)`,"storage.policy.mode.permanent":`Suppression définitive`,"storage.policy.permanentWarn":`Le mode définitif est irréversible. Préférez la quarantaine sauf si vous êtes certain de votre choix.`,"storage.policy.lastRun":`Dernière exécution`,"storage.policy.lastRunDetail":`{count} supprimés · {size} libérés`,"storage.policy.nextRun":`Prochaine exécution`,"storage.policy.never":`Jamais`,"storage.policy.save":`Enregistrer`,"storage.policy.runNow":`Exécuter maintenant`,"storage.policy.running":`Exécution…`,"storage.policy.saved":`Politique enregistrée.`,"storage.policy.skippedDisabled":`La politique est désactivée — activez-la d’abord.`,"storage.policy.skippedUnder":`La taille des archives est inférieure au seuil — aucune action nécessaire.`,"storage.policy.skippedEmpty":`Aucune archive candidate ne correspond à l’objectif.`,"storage.policy.doneQuarantine":`La politique a mis {count} fichier(s) en quarantaine ({size}).`,"storage.policy.donePermanent":`La politique a supprimé définitivement {count} fichier(s) ({size}).`,"storage.policy.metadataSaveWarning":`L’exécution de la politique est terminée, mais ses métadonnées de planification n’ont pas pu être enregistrées.`,"modal.addNamed":`Ajouter : {label}`,"modal.add":`Ajouter un fournisseur`,"modal.search":`Rechercher des fournisseurs…`,"modal.logInWith":`Se connecter avec {label}`,"modal.waitingBrowser":`En attente du navigateur…`,"modal.providerName":`Nom du fournisseur`,"modal.adapter":`Adaptateur`,"modal.baseUrl":`URL de base`,"modal.endpoint":`Point de terminaison`,"modal.endpoint.tokenPlan":`Forfait de jetons`,"modal.endpoint.payAsYouGo":`Paiement à l’utilisation`,"modal.endpoint.custom":`Personnalisé`,"modal.defaultModel":`Modèle par défaut (facultatif)`,"modal.allowPrivateNetwork":`Autoriser le réseau local/privé`,"modal.allowPrivateNetworkHint":`À activer uniquement pour les fournisseurs volontairement auto-hébergés. Les points de terminaison de métadonnées restent bloqués.`,"modal.nameRequired":`Le nom du fournisseur est requis`,"modal.baseUrlRequired":`L’URL de base est requise`,"modal.networkError":`Erreur réseau — le proxy est-il en cours d’exécution ?`,"modal.loginFailStart":`Échec du lancement de la connexion`,"modal.waitingLogin":`En attente de la connexion dans le navigateur…`,"modal.loggingIn":`Connexion…`,"modal.loginTimeout":`Délai de connexion dépassé — réessayez.`,"modal.back":`Retour`,"modal.badge.oauth":`OAuth`,"modal.customProvider":`Fournisseur personnalisé`,"modal.failedStatus":`Échec ({status})`,"modal.loginError":`Erreur de connexion : {error}`,"modal.badge.codexLogin":`Connexion Codex`,"modal.badge.local":`Local`,"modal.badge.apiKey":`Clé API`,"modal.badge.direct":`Direct`,"modal.badge.pool":`Groupe`,"modal.badge.free":`Gratuit`,"modal.invalidPreset":`Ce préréglage de fournisseur intégré est incomplet. Redémarrez le proxy et réessayez.`,"modal.freeTierTitle":`Offre gratuite`,"modal.freeTierDefault":`Aucune clé API requise. Fonctionne immédiatement.`,"modal.tab.accounts":`Comptes`,"modal.tab.free":`Gratuit`,"modal.tab.paid":`Payant`,"modal.accountsHint":`Connectez-vous ici à ChatGPT/Codex, aux fournisseurs OAuth et aux comptes avec clé API. OpenAI est intégré : connectez-vous au lieu de l’ajouter de nouveau.`,"modal.accountsCodexAuthLink":`Codex Auth`,"modal.notListed":`Fournisseur absent de la liste ? Ajoutez-en un personnalisé`,"modal.catalogLoading":`Chargement du catalogue…`,"modal.accountLogin":`Se connecter`,"modal.accountLogout":`Se déconnecter`,"modal.accountAdd":`Ajouter un compte`,"modal.accountManage":`Gérer`,"modal.accountCodexPool":`Groupe de comptes ChatGPT`,"modal.accountLoggedIn":`Connecté`,"modal.accountLoggedOut":`Non connecté`,"quota.fiveHourLimit":`Limite sur 5 heures`,"quota.ageMinutes":`{n} min`,"quota.ageHours":`{n} h`,"quota.ageDays":`{n} j`,"quota.observedAgo":`Relevé il y a {age}`,"quota.observedHint":`Meta ne communique l'utilisation que pendant une réponse en streaming : il s'agit de la dernière valeur observée, pas d'une mesure en direct.`,"quota.weeklyLimit":`Limite hebdomadaire`,"quota.monthlyLimit":`Limite sur 30 jours`,"quota.cursorFirstParty":`Modèles propriétaires`,"quota.cursorApiUsage":`Utilisation de l’API`,"quota.totalSubscriptionCredits":`Total des crédits d’abonnement`,"quota.creditsBalance":`Solde de crédits`,"quota.creditsPeriodEnds":`La période de facturation se termine le {date}`,"quota.usedPercent":`{pct} % utilisés`,"quota.limitReached":`Limite atteinte`,"quota.resetsToday":`Réinitialisation aujourd’hui à {time}`,"quota.resetsTomorrow":`Réinitialisation demain à {time}`,"quota.resetsAt":`Réinitialisation {when}`,"quota.resetsRelativeMinutes":`Réinitialisation dans {n} min`,"quota.resetsRelativeHours":`Réinitialisation dans {n} h`,"pws.status.ready":`Prêt`,"pws.status.needsSetup":`Configuration requise`,"pws.status.needsAttention":`Attention requise`,"pws.auth.chatgptPassthrough":`Relais ChatGPT`,"pws.auth.noKey":`Aucune clé requise`,"pws.freeTitle":`Tarification gratuite (une clé peut néanmoins être requise)`,"pws.localTitle":`Environnement d’exécution local`,"pws.modelCountOne":`1 modèle`,"pws.modelCount":`{count} modèles`,"pws.rail.suffixDefault":` · par défaut`,"pws.rail.suffixLocal":` · local`,"pws.rail.suffixFree":` · gratuit`,"pws.rail.selectAria":`Sélectionner {name} — {status}{suffix}`,"pws.searchPlaceholder":`Rechercher des fournisseurs…`,"pws.filterAria":`Filtrer les fournisseurs`,"pws.providerFiltersAria":`Filtres des fournisseurs`,"pws.filters":`Filtres`,"pws.filterStatus":`État`,"pws.pricing":`Tarification`,"pws.paid":`Payant`,"pws.filterType":`Type`,"pws.type.cloud":`Cloud`,"pws.type.local":`Local`,"pws.type.selfHosted":`Auto-hébergé`,"pws.type.login":`Connexion`,"pws.sort":`Trier`,"pws.sortProvidersAria":`Trier les fournisseurs`,"pws.sort.az":`A–Z`,"pws.sort.za":`Z–A`,"pws.sort.freePaid":`Gratuits en premier`,"pws.sort.paidFree":`Payants en premier`,"pws.sort.accountsFirst":`Comptes en premier`,"pws.resetAll":`Tout réinitialiser`,"pws.providerList":`Liste des fournisseurs`,"pws.providersAria":`Fournisseurs`,"pws.groupReady":`Prêts ({count})`,"pws.groupNeedsSetup":`Configuration requise ({count})`,"pws.groupDisabled":`Désactivés ({count})`,"pws.noSearchResults":`Aucun fournisseur ne correspond à votre recherche.`,"pws.noMatchFilters":`Aucun fournisseur ne correspond aux filtres.`,"pws.noProvidersConfigured":`Aucun fournisseur configuré.`,"pws.workspaceMainAria":`Détails du fournisseur`,"pws.detailComingSoon":`La vue détaillée sera bientôt disponible — utilisez la vue classique pour gérer ce fournisseur.`,"pws.selectPrompt":`Sélectionnez un fournisseur dans la liste.`,"pws.connectFirst":`Connecter votre premier fournisseur`,"pws.empty.browseFree":`Parcourir les fournisseurs gratuits`,"pws.empty.browseFreeDesc":`Commencez sans abonnement`,"pws.empty.connectAccount":`Connecter un compte`,"pws.empty.connectAccountDesc":`Utilisez votre identifiant ChatGPT ou celui du fournisseur`,"pws.empty.addEndpoint":`Ajouter un point de terminaison`,"pws.empty.addEndpointDesc":`URL de base personnalisée et clé API`,"pws.tab.overview":`Vue d’ensemble`,"pws.tab.models":`Modèles`,"pws.tab.usage":`Utilisation`,"pws.tab.accounts":`Comptes`,"pws.tab.settings":`Paramètres`,"pws.connection":`Connexion`,"pws.status.connected":`Connecté`,"pws.attentionTitle":`Intervention requise`,"pws.attention.reauth":`Le compte actif doit être réauthentifié`,"pws.attention.reauthForward":`Le compte Codex actif doit être réauthentifié — ouvrez Comptes pour corriger le problème`,"pws.attention.missingCredentials":`Identifiants manquants`,"pws.cell.auth":`Authentification`,"pws.cell.note":`Note`,"pws.cell.defaultModel":`Modèle par défaut`,"pws.statsAria":`Statistiques du fournisseur`,"pws.statsTitle":`Statistiques`,"pws.stats.totalRequests":`Requêtes (30 j)`,"pws.stats.totalTokens":`Jetons (30 j)`,"pws.stats.quotaUpdated":`Quota mis à jour`,"pws.stats.quotaTracked":`Limites de débit suivies dans l’onglet Utilisation.`,"pws.stats.source":`Source`,"pws.usageLast30d":`Utilisation (30 derniers jours)`,"pws.estimatedCost":`Coût estimé`,"pws.costDisclaimer":`Estimation fondée sur le tarif public de l’API, et non montant réellement facturé.`,"pws.modelBreakdown":`Répartition par modèle`,"pws.col.model":`Modèle`,"pws.col.cost":`Coût est.`,"pws.col.tokens":`Jetons`,"pws.col.requests":`Req.`,"pws.col.share":`Part`,"pws.tokenInput":`Entrée`,"pws.tokenOutput":`Sortie`,"pws.metricRequests":`requêtes`,"pws.metricTokens":`jetons`,"pws.usageUnavailable":`Aucune utilisation enregistrée pour le moment.`,"pws.rateLimits":`Limites de débit`,"pws.quotaUnavailable":`Aucune donnée de quota pour ce fournisseur.`,"pws.accountQuotaUnavailable":`Données de limite de débit temporairement indisponibles ; affichage des dernières valeurs connues, le cas échéant.`,"pws.selected":`Sélectionné`,"pws.copyModelId":`Copier l’ID`,"pws.modelCopied":`Copié !`,"pws.modelsAvailable":`{count} disponibles`,"pws.modelSearchPlaceholder":`Filtrer les modèles…`,"pws.modelsLoading":`Chargement des modèles…`,"pws.modelsLoadFailed":`Impossible de charger les modèles.`,"pws.modelsNeedsReauth":`Le compte doit être reconnecté pour permettre la détection des modèles en direct. Affichage temporaire des modèles configurés.`,"pws.modelsConfiguredFallback":`Affichage des modèles configurés (détection en direct indisponible).`,"pws.modelsTruncated":`Affichage des {shown} premiers modèles sur {total}. Filtrez pour réduire la liste.`,"pws.retry":`Réessayer`,"pws.noModels":`Aucun modèle détecté pour ce fournisseur.`,"pws.noModelMatch":`Aucun modèle ne correspond au filtre.`,"pws.adapterBaseRequired":`L’adaptateur et l’URL de base sont requis.`,"pws.addAccount":`Ajouter un compte`,"pws.addKey":`Ajouter une clé API`,"pws.apiKeys":`Clés API`,"pws.authMode":`Mode d’authentification`,"pws.availableAccounts":`Comptes disponibles`,"pws.accountOrdinal":`Compte {count}`,"pws.accountsLoading":`Chargement des comptes…`,"pws.accountsLoadFailed":`Impossible de charger les comptes.`,"pws.retryAccounts":`Réessayer`,"pws.noAccounts":`Aucun compte connecté pour le moment.`,"pws.cockpitImportDescription":`Importez depuis cet appareil une exportation JSON Antigravity de Cockpit Tools. Le contenu du fichier n’est pas affiché.`,"pws.cockpitImportFileLabel":`Exportation JSON Antigravity de Cockpit Tools`,"pws.cockpitImportChooseFile":`Choisir un fichier JSON`,"pws.cockpitImporting":`Importation…`,"pws.cockpitImportInvalid":`Le fichier sélectionné n’est pas une exportation JSON valide ou est trop volumineux.`,"pws.cockpitImportFailed":`Impossible de terminer l’importation du compte.`,"pws.cockpitImportComplete":`Importation terminée : {imported} importés, {updated} mis à jour, {failed} en échec, {unsupported} non pris en charge.`,"pws.accountSwitching":`Changement…`,"pws.accountCurrent":`Compte actuel`,"pws.defaultModelNone":`Aucun (utiliser la valeur par défaut du fournisseur)`,"pws.discardSettings":`Abandonner les modifications`,"pws.jsonEditorDesc":`Modifiez la configuration JSON brute du fournisseur. Les modifications sont enregistrées immédiatement.`,"pws.jsonEditorTitle":`Éditeur JSON — {name}`,"pws.jsonRestore":`Restaurer`,"pws.jsonSave":`Enregistrer`,"pws.loggedInTitle":`Connecté`,"pws.notLoggedInTitle":`Non connecté`,"pws.note":`Note`,"pws.allowPrivateNetwork":`Autoriser le réseau local/privé`,"pws.liveModels":`Détecter les modèles auprès du fournisseur`,"pws.liveModelsDesc":`Récupérez le catalogue de modèles en direct du fournisseur. Désactivez cette option pour utiliser uniquement les modèles configurés/statiques.`,"pws.xaiResponsesOptIn":`Utiliser l’API Responses pour Grok 4.5 et 4.6`,"pws.xaiResponsesOptInDesc":`Achemine les deux modèles via openai-responses. Les autres modèles Grok et le comportement des tiers restent inchangés.`,"pws.xaiResponsesOptInMixed":`Activation partielle.`,"pws.cursorTransport":`Transport Cursor`,"pws.cursorTransportHttp2":`HTTP/2 (par défaut)`,"pws.cursorTransportHttp1":`HTTP/1.1 (compatibilité proxy)`,"pws.cursorTransportDesc":`Utilisez HTTP/1.1 si votre proxy ne transporte pas de façon fiable le flux HTTP/2 de Cursor.`,"pws.optionalPlaceholder":`Facultatif`,"pws.pacingTitle":`Cadencement des requêtes`,"pws.pacingDesc":`Répartissez uniformément le démarrage des requêtes sortantes pour ce fournisseur. Les réponses diffusées peuvent se chevaucher.`,"pws.pacingEnabled":`Activé`,"pws.pacingRpm":`Requêtes par minute`,"pws.pacingRpmUnit":`RPM`,"pws.pacingDelay":`Intervalle minimal (ms)`,"pws.pacingSlowerWins":`La limite la plus lente du fournisseur s’applique. Les remplacements par modèle ne peuvent qu’ajouter un délai.`,"pws.pacingQueued":`en attente`,"pws.pacingNextSlot":`avant le prochain créneau`,"pws.pacingLastModel":`dernier modèle`,"pws.pacingNone":`Aucun`,"pws.pacingModelOverrides":`Remplacements par modèle`,"pws.pacingModel":`Modèle`,"pws.pacingAdd":`Ajouter un remplacement`,"pws.pacingRemove":`Supprimer`,"pws.pacingRemoveModel":`Supprimer le remplacement du cadencement des requêtes pour {model}`,"pws.pacingRuleRequired":`Activez le cadencement des requêtes seulement après avoir défini une limite de fournisseur ou un remplacement par modèle.`,"pws.providerId":`ID du fournisseur`,"pws.reauth":`Réauthentification requise`,"pws.reauthenticate":`Réauthentifier`,"pws.copyDoctor":`Copier ocx doctor`,"pws.doctorCopied":`Copié`,"pws.doctorCopyUnavailable":`Presse-papiers indisponible`,"pws.healthCooldownHint":`Attendez la fin du délai de récupération. Ne sondez pas encore ce compte.`,"pws.healthLabel.rateLimited":`Débit limité`,"pws.healthLabel.quotaLimited":`Quota limité`,"pws.healthLabel.reauthRequired":`Réauthentification requise`,"pws.healthLabel.refreshFailed":`Actualisation échouée`,"pws.healthLabel.metadataMismatch":`Métadonnées incompatibles`,"pws.healthLabel.credentialConflict":`Conflit d’identifiants`,"pws.healthSummary.rateLimited":`{provider} {account} : débit limité jusqu’à {until}. Le routage de ce compte est suspendu jusque-là.`,"pws.healthSummary.quotaLimited":`{provider} {account} : quota limité jusqu’à {until}. Le routage de ce compte est suspendu jusque-là.`,"pws.healthSummary.reauthRequired":`{provider} {account} : réauthentification requise.`,"pws.healthSummary.credentialConflict":`{provider} {account} : conflit d’identifiants.`,"pws.healthSummary.metadataMismatch":`{provider} {account} : métadonnées incompatibles.`,"pws.healthSummary.staleCredentials":`{provider} {account} : identifiants incomplets.`,"pws.removeConfirm":`Supprimer`,"pws.removeConfirmBody":`Supprimer le fournisseur "{name}" ? Cette action est irréversible.`,"pws.removeDefaultConfirmBody":`Supprimer le fournisseur par défaut "{name}" ? "{defaultProvider}" deviendra le fournisseur par défaut. Cette action est irréversible.`,"pws.removeConfirmTitle":`Supprimer le fournisseur`,"pws.saveSettings":`Enregistrer`,"pws.saving":`Enregistrement…`,"pws.settingsSaved":`Paramètres enregistrés.`,"pws.accountModeSaved":`Mode de compte enregistré.`,"pws.accountModeFailed":`Impossible de changer de mode de compte.`,"pws.accountModeConfirm":`Changer le mode de compte OpenAI ? Les conversations en cours seront réaffectées à l’ensemble de comptes de l’autre mode, et l’utilisation du quota sera suivie selon le nouveau mode.`,"pws.settingsUnsavedBar":`Vous avez des modifications non enregistrées.`,"pws.unsavedLeaveBody":`Vous avez des modifications non enregistrées. Les enregistrer avant de quitter ?`,"pws.unsavedLeaveTitle":`Modifications non enregistrées`,"pws.attentionRequired":`Intervention requise`,"pws.attentionAria":`{name} : {reason}`,"pws.missingCredentials":`Identifiants manquants`,"pws.editJsonDesc":`Modifier la configuration brute du proxy au format JSON`,"pws.updatesUnavailable":`Les mises à jour du fournisseur ne sont pas disponibles.`,"pws.dashboard.title":`Vue d’ensemble des fournisseurs`,"pws.dashboard.subtitle":`Gérez tous vos fournisseurs de modèles au même endroit.`,"pws.dashboard.rateLimits":`LIMITES DE DÉBIT`,"pws.capacity.estimate":`Estimation du groupe pondérée selon la configuration`,"pws.capacity.currentAccount":`Compte effectif actuel`,"pws.capacity.nextRecovery":`Prochaine récupération de capacité`,"pws.capacity.recoveryShare":`+{percent}% de capacité du groupe`,"pws.capacity.incomplete":`Couverture incomplète : {excluded} compte(s) exclus`,"pws.capacity.uncalibratedPlan":`{count} compte(s) sur un forfait non calibré sont comptés au poids de siège de base ; cette estimation peut donc être prudente`,"pws.capacity.partial":`Couverture partielle des fenêtres : {count} compte(s) ne signalent pas toutes les fenêtres de limite affichées`,"pws.capacity.windowPartial":`Partielle`,"pws.capacity.windowPartialA11y":`{window} : couverture incomplète des comptes`,"pws.dashboard.recentlyUsed":`UTILISÉS RÉCEMMENT`,"pws.dashboard.requests":`{count} requêtes`,"pws.dashboard.checkedAgo":`Vérifié {time}`,"pws.dashboard.noQuota":`Aucune donnée de quota`,"pws.dashboard.noUsage":`Aucune donnée d’utilisation pour le moment`,"pws.dashboard.noRateLimits":`Aucune donnée de limite de débit pour le moment`,"pws.allProviders":`Vue d’ensemble des fournisseurs`,"pws.enabledLabel":`Activé`,"pws.testConnection":`Tester la connexion`,"pws.testing":`Test…`,"pws.connectionOk":`Connexion réussie`,"pws.connectionFailed":`Échec de la connexion`,"pws.connectionNotApplicable":`Sans objet — ce fournisseur utilise un catalogue de modèles statique.`,"pws.editSettings":`Modifier les paramètres`,"pws.viewUsage":`Afficher l’utilisation détaillée`,"pws.allSystemsOk":`Tous les systèmes sont opérationnels`,"pws.apiKeyConfigured":`Clé API configurée`,"pws.addApiKey":`Ajouter une clé API`,"pws.loggedInAs":`Connecté en tant que {email}`,"pws.notLoggedIn":`Non connecté`,"pws.passthrough":`Transfert direct Codex`,"pws.notes":`NOTES`,"pws.notePlaceholder":`Ajouter une note sur ce fournisseur…`,"pws.noteSaved":`Note enregistrée`,"pws.authSummary":`AUTHENTIFICATION`,"time.justNow":`À l’instant`,"time.notChecked":`Non vérifié`,"time.minutesAgo":`il y a {n} min`,"time.hoursAgo":`il y a {n} h`,"time.daysAgo":`il y a {n} j`,"modal.noMatch":`Aucun résultat.`,"modal.oauthDefaultNote":`Connectez-vous avec votre compte — aucune clé API requise.`,"modal.oauthComingSoon":`La connexion OAuth pour {label} sera disponible dans la prochaine mise à jour. Utilisez une clé API pour le moment.`,"modal.oauthComingSoonShort":`La connexion OAuth pour ce fournisseur sera disponible dans la prochaine mise à jour — utilisez une clé API pour le moment.`,"modal.useApiKeyInstead":`Utiliser plutôt une clé API`,"modal.setupGuide":`Guide de configuration`,"modal.setupStep1Prefix":`Accédez au`,"modal.setupDashboardLink":`tableau de bord {label}`,"modal.setupStep1Suffix":`et copiez votre clé API`,"modal.setupStep2":`Collez-la dans le champ de clé API ci-dessous`,"modal.setupStep3":`Cliquez sur Ajouter le fournisseur — les modèles sont détectés automatiquement`,"modal.namePlaceholder":`p. ex. openrouter`,"modal.duplicateWarn":`Le fournisseur "{name}" existe déjà et sera remplacé.`,"modal.forwardHintPrefix":`Aucune clé requise — le proxy transmet vos identifiants de`,"modal.forwardCredentials":`connexion Codex`,"modal.forwardHintSuffix":`à ce fournisseur.`,"modal.localHint":`Aucune clé API n’est stockée. Cette option ajoute le catalogue public statique de modèles Cursor pour Codex, mais le transport Cursor en direct et l’exécution native de fichiers/commandes restent désactivés jusqu’à leur audit.`,"modal.getApiKey":`Obtenir votre clé API {label}`,"modal.apiKey":`Clé API`,"modal.apiKeyTransport":`En-tête de clé API`,"modal.apiKeyTransportNative":`x-api-key (natif Anthropic)`,"modal.apiKeyTransportBearer":`Authorization: Bearer`,"modal.apiKeyPlaceholder":`sk-… (ou $ENV_VAR)`,"modal.defaultModelPlaceholder":`p. ex. gpt-5.5`,"modal.baseUrlPlaceholder":`https://...`,"modal.baseUrlPlaceholderError":`L’URL de base contient un {placeholder} non résolu. Remplacez-le par votre valeur réelle.`,"modal.baseUrlPlaceholderHint":`Remplacez le {placeholder} dans l’URL de base par votre ID de compte réel avant l’ajout.`,"modal.adding":`Ajout…`,"modal.useOauthLogin":`← Utiliser la connexion OAuth`,"nav.codexAuth":`Authentification Codex`,"nav.codexSet":`Réglages Codex`,"codexSet.tab.multiauth":`Multi-authentification`,"codexSet.tab.prompt":`Invite`,"codexSet.prompt.title":`Couches d'invite`,"codexSet.prompt.timing":`S'applique aux sessions nouvellement démarrées. Les sessions en cours conservent leurs réglages d'invite actuels.`,"codexSet.prompt.staleRevision":`La configuration a changé ailleurs. La liste a été rechargée.`,"codexSet.prompt.writeFailed":`La modification n'a pas pu être enregistrée.`,"codexSet.prompt.loadFailed":`Les couches d'invite n'ont pas pu être chargées.`,"codexSet.prompt.repair":`Réparer`,"codexSet.prompt.repairFailed":`La réparation n'a pas pu aboutir.`,"codexSet.drift.journalPresent":`Une écriture précédente ne s'est pas terminée. La récupération s'exécute automatiquement à la prochaine écriture.`,"codexSet.drift.projectionStale":`Les couches enregistrées et la valeur dans config.toml divergent. La réparation réécrit la valeur à partir de vos couches.`,"codexSet.drift.storeMissing":`Le fichier des couches a disparu alors que des instructions subsistent dans config.toml. La réparation crée d'abord une sauvegarde et conserve le texte en une seule couche.`,"codexSet.drift.ownedMalformed":`La ligne générée dans config.toml a été modifiée à la main ; la réécrire n'est plus sûr.`,"codexSet.custom.adoptUnsupported":`La valeur à {path} ligne {line} n'est pas une chaîne sur une seule ligne et ne peut pas être importée. Déplacez-la à la main pour la gérer ici.`,"codexSet.prompt.unreadable":`Le fichier de configuration Codex existe mais n'a pas pu être lu, les modifications sont donc refusées.`,"codexSet.layer.permissions":`Autorisations`,"codexSet.layer.collaboration":`Mode collaboration`,"codexSet.layer.environment":`Contexte d'environnement`,"codexSet.layer.apps":`Applications`,"codexSet.layer.skills":`Compétences`,"codexSet.prompt.extensionsUnknown":`Les extensions peuvent ajouter leurs propres couches. Codex ne les expose pas, elles ne peuvent donc pas être répertoriées ici.`,"codexSet.group.transition":`Avis de transition`,"codexSet.group.transitionDesc":`Ils signalent un changement au lieu de décrire un état : ils n'apparaissent donc qu'au passage en temps réel ou au changement de modèle.`,"codexSet.custom.slotNote":`Les couches personnalisées sont réunies en une seule section dans cet ordre.`,"codexSet.row.alwaysOn":`Toujours actif`,"codexSet.row.onChange":`Lors d'un changement`,"codexSet.row.featureGated":`Configuré sous [features]`,"codexSet.row.openFeatures":`Ouvrir les réglages`,"codexSet.dialog.setValue":`{value} (par défaut {fallback})`,"codexSet.dialog.copyKey":`Copier la clé`,"codexSet.dialog.unknownLayer":`Cette version ne contient pas de description pour cette couche. Elle provient d'un runtime Codex plus récent que le tableau de bord.`,"codexSet.custom.heading":`Couches personnalisées`,"codexSet.custom.add":`+ Ajouter une couche`,"codexSet.custom.newTitle":`Nouvelle couche`,"codexSet.custom.editTitle":`Modifier la couche`,"codexSet.custom.titleLabel":`Titre`,"codexSet.custom.bodyLabel":`Instructions`,"codexSet.custom.bodySize":`{bytes} octets sur {max}`,"codexSet.custom.normalized":`Les tabulations ont été remplacées par quatre espaces et les fins de ligne par LF.`,"codexSet.custom.titleRequired":`Saisissez un titre.`,"codexSet.custom.titleTooLong":`Le titre contient {count} caractères ; la limite est de {max}.`,"codexSet.custom.titleMultiline":`Le titre doit tenir sur une seule ligne.`,"codexSet.custom.bodyTooLarge":`Cette couche fait {bytes} octets ; la limite est de {max}.`,"codexSet.custom.composedTooLarge":`Les couches actives totaliseraient {bytes} octets, au-delà de la limite.`,"codexSet.custom.invalidCharacter":`Un caractère de contrôle à la position {position} ne peut pas être enregistré.`,"codexSet.custom.discardPrompt":`Abandonner vos modifications ?`,"codexSet.custom.keepEditing":`Continuer la modification`,"codexSet.custom.delete":`Supprimer {title}`,"codexSet.custom.deleteConfirm":`Supprimer cette couche ? Cette action est irréversible.`,"codexSet.custom.layerGone":`Cette couche a été supprimée ailleurs : l'éditeur a donc été fermé.`,"codexSet.custom.deleteConfirmNamed":`Supprimer « {title} » ? Cette action est irréversible.`,"codexSet.custom.moveUp":`Déplacer {title} vers le haut`,"codexSet.custom.prevLayer":`Couche précédente`,"codexSet.custom.nextLayer":`Couche suivante`,"codexSet.custom.navPosition":`{position} / {total}`,"codexSet.custom.moveDown":`Déplacer {title} vers le bas`,"codexSet.custom.limitReached":`Vous pouvez conserver jusqu’à {max} couches personnalisées.`,"codexSet.custom.notOwned":`developer_instructions a été écrit en dehors d’opencodex et ne peut donc pas être modifié ici. Importez-le pour le gérer comme une couche.`,"codexSet.custom.adopt":`Importer les instructions existantes`,"codexSet.custom.adoptConfirm":`Importer comme couche`,"codexSet.custom.adoptRefused":`La valeur existante n’a pas pu être importée.`,"codexSet.custom.baseReplaced":`model_instructions_file est défini sur {path} : un élément extérieur à opencodex a donc remplacé l’invite de base.`,"codexSet.lint.identity":`Ce texte revendique une identité différente de celle établie par Codex.`,"codexSet.lint.foreignTool":`Les outils proviennent du registre ; en nommer un ici ne le crée pas.`,"codexSet.lint.placeholder":`Aucun moteur de modèles ne traite les instructions ; ce texte sera donc envoyé tel quel.`,"codexSet.lint.applyPatch":`apply_patch est défini par le registre des outils, pas par les instructions.`,"codexSet.lint.approvalVocab":`Codex injecte son propre vocabulaire d’approbation ; ce texte peut le contredire.`,"codexSet.lint.environment":`Les informations d’environnement sont générées plus tard et peuvent contredire ce texte.`,"codexSet.lint.size":`Cette couche dépasse 8 KB. Elle peut être enregistrée, mais consomme des jetons à chaque requête.`,"codexSet.preset.blank":`Couche vide`,"codexSet.preset.concise.name":`Réponses concises`,"codexSet.preset.concise.description":`Réponses courtes, sans préambule et avec un minimum de mise en forme.`,"codexSet.preset.concise.provenance":`Adapté des consignes de concision de Claude Code. Formulation originale, pas une copie.`,"codexSet.preset.planFirst.name":`Planifier avant de modifier`,"codexSet.preset.planFirst.description":`Présenter le plan, puis effectuer la modification.`,"codexSet.preset.planFirst.provenance":`Adapté de l’approche de planification de Claude Code. Formulation originale, pas une copie.`,"codexSet.preset.explainWhy.name":`Expliquer le raisonnement`,"codexSet.preset.explainWhy.description":`Expliquer pourquoi, et pas seulement quoi.`,"codexSet.preset.explainWhy.provenance":`Adapté du style de confirmation de Grok Build. Formulation originale, pas une copie.`,"codexSet.preset.testFirst.name":`Tester d’abord`,"codexSet.preset.testFirst.description":`Écrire le test qui échoue avant d’apporter le correctif.`,"codexSet.preset.testFirst.provenance":`Adapté des pratiques courantes des agents. Formulation originale, pas une copie.`,"codexSet.preset.korean.name":`Réponses en coréen`,"codexSet.preset.korean.description":`Répondre en coréen, quelle que soit la langue de la requête.`,"codexSet.preset.korean.provenance":`Rédigé pour opencodex à partir d'une demande fréquente des utilisateurs. Formulation originale, pas une copie.`,"codexSet.dialog.class":`Type`,"codexSet.dialog.key":`Clé de configuration`,"codexSet.dialog.fileValue":`Valeur dans ce fichier`,"codexSet.dialog.absentDefault":`non défini (valeur par défaut : {value})`,"codexSet.dialog.noRenderedText":`Codex n’expose pas le texte assemblé d’une couche intégrée. Cette boîte de dialogue décrit donc la couche et indique sa clé au lieu d’afficher son contenu.`,"codexSet.dialog.sourceText":`Texte envoyé au modèle`,"codexSet.dialog.sourceBytes":`{bytes} octets`,"codexSet.dialog.notRendered":`Lors du tour que nous avons lu, cette couche n'a rien envoyé. Les sections ne sont renvoyées qu'en cas de changement : un seul échantillon peut donc ne pas la contenir.`,"codexSet.dialog.emptySource":`Le fichier {path} existe mais il est vide : cette couche n'envoie donc rien.`,"codexSet.dialog.notExposed":`L'invite de base circule en dehors de la liste de messages que Codex peut afficher ; elle ne peut donc pas être montrée ici. On peut la remplacer via model_instructions_file.`,"codexSet.dialog.textUnavailable":`L'invite Codex n'a pas pu être lue sur cette machine, le texte est donc indisponible.`,"codexSet.class.base":`Instructions de base`,"codexSet.class.config-toggle":`Modifiable ici`,"codexSet.class.feature-gated":`Contrôlé par une fonctionnalité`,"codexSet.class.runtime-conditional":`Conditionnel à l’exécution`,"codexSet.class.extension-unknown":`Couche d’extension`,"codexSet.layer.base-instructions":`Instructions de base`,"codexSet.layer.model-switch":`Avis de changement de modèle`,"codexSet.layer.personality":`Personnalité`,"codexSet.layer.context-window-guidance":`Conseils sur la fenêtre de contexte`,"codexSet.layer.realtime":`Temps réel`,"codexSet.layer.agents-md":`AGENTS.md`,"codexSet.layer.environments-instructions":`Environnements`,"codexSet.layer.plugins":`Plugins`,"codexSet.layer.tools":`Outils`,"codexSet.layer.multi-agent-mode":`Mode multi-agents`,"codexSet.layer.git-attribution":`Attribution des commits`,"codexSet.about.base-instructions":`Instructions propres à Codex. Elles accompagnent la requête elle-même et ne peuvent pas être désactivées.`,"codexSet.about.model-switch":`Ajouté lorsque le modèle change en cours de conversation.`,"codexSet.about.personality":`Consignes de ton et de style, régies par un indicateur de fonctionnalité.`,"codexSet.about.context-window-guidance":`Conseils sur le budget de contexte restant, régis par un indicateur de fonctionnalité.`,"codexSet.about.realtime":`Ajouté aux sessions en temps réel.`,"codexSet.about.agents-md":`Les fichiers AGENTS.md de votre projet. Cette page indique la couche, mais ne modifie jamais la documentation du projet.`,"codexSet.about.permissions":`Décrit les réglages actifs du bac à sable et des approbations.`,"codexSet.about.collaboration":`Décrit le mode collaboration actif.`,"codexSet.about.environment":`Répertoire de travail, plateforme et autres informations d’environnement.`,"codexSet.about.environments-instructions":`Consignes pour les environnements d’exécution différée, régies par un indicateur de fonctionnalité.`,"codexSet.about.apps":`Utilisation des applications connectées.`,"codexSet.about.plugins":`Ajouté lorsqu’un plugin est sélectionné ou qu’un plugin déclare une capacité.`,"codexSet.about.tools":`Descriptions différées des outils, régies par un indicateur de fonctionnalité.`,"codexSet.about.skills":`Liste des compétences disponibles.`,"codexSet.about.multi-agent-mode":`Instructions pour les sous-agents, régies par un indicateur de fonctionnalité.`,"codexSet.about.git-attribution":`Demande au modèle d’ajouter un trailer Co-authored-by: Codex aux commits qu’il écrit, et une ligne Generated with Codex. aux pull requests qu’il ouvre. Codex lit ce réglage depuis votre compte : il n’est modifiable ni ici ni dans [features]. Si votre compte le désactive, Codex envoie l’instruction inverse au lieu de ne rien envoyer.`,"codexSet.condition.model-switch":`Émis uniquement après un changement de modèle en cours de session.`,"codexSet.condition.realtime":`Émis uniquement dans une session en temps réel.`,"codexSet.condition.agents-md":`Émis lorsqu’un document de projet est trouvé pour le répertoire de travail.`,"codexSet.condition.plugins":`Émis lorsqu’un plugin est sélectionné ou qu’un plugin déclare une capacité.`,"codexSet.condition.git-attribution":`Défini par la politique d’attribution de votre compte.`,"codexSet.base.title":`Invite de base`,"codexSet.base.prev":`Option précédente`,"codexSet.base.next":`Option suivante`,"codexSet.base.position":`{position} / {total}`,"codexSet.base.swipeHint":`Balayez latéralement, utilisez les touches fléchées ou les boutons pour changer d’option. S’applique aux sessions démarrées ensuite.`,"codexSet.base.defaultTitle":`Invite de base propre à Codex`,"codexSet.base.defaultBody":`L’option par défaut n’est pas stockée ici : il n’y a donc rien à modifier ni à supprimer. La choisir retire simplement model_instructions_file de votre configuration, et Codex utilise l’invite qu’il fournit.`,"codexSet.base.variantTitle":`Nom`,"codexSet.base.variantBody":`Invite`,"codexSet.base.replacesWarning":`Ceci REMPLACE l’invite de base de Codex au lieu de s’y ajouter. Une invite courte ici donne un modèle avec des instructions courtes.`,"codexSet.base.use":`Utiliser celle-ci`,"codexSet.base.inUse":`Utilisée`,"codexSet.base.externalBlocked":`model_instructions_file pointe déjà vers {path}, une valeur qu’opencodex n’a pas écrite. Retirez-la vous-même avant de choisir ici.`,"nav.api":`API`,"nav.integrations":`Intégrations`,"nav.openMenu":`Ouvrir le menu`,"nav.closeMenu":`Fermer le menu`,"integrations.subtitle":`Connectez des clients à opencodex, gérez les identifiants et restaurez la configuration des clients.`,"integrations.tabsLabel":`Surfaces d’intégration`,"integrations.tab.overview":`Vue d’ensemble`,"integrations.tab.keys":`Clés API`,"integrations.tab.codex":`Codex`,"integrations.tab.claude":`Claude`,"integrations.tab.grok":`Grok Build`,"integrations.tab.opencode":`OpenCode`,"integrations.tab.pi":`Pi`,"integrations.tab.omp":`OMP`,"integrations.tab.hermes":`Hermes`,"integrations.tab.openclaw":`OpenClaw`,"integrations.tab.kimi":`Kimi Code`,"integrations.tab.gajae":`Gajae Code`,"integrations.tab.dsh":`DSH`,"integrations.tab.mcode":`MiniMax Code`,"integrations.tab.zcode":`ZCode`,"integrations.tab.prime":`Prime Agent`,"integrations.tab.aside":`Aside`,"integrations.codex.title":`Codex CLI`,"integrations.codex.body":`Le câblage de Codex est géré par le service proxy. Le démarrage d’opencodex l’applique ; l’arrêt du service rétablit le routage natif.`,"integrations.codex.openService":`Ouvrir les commandes du service`,"integrations.state.notInstalled":`Non installé`,"integrations.state.unknown":`Vérification…`,"integrations.detail.codexRouted":`Les requêtes Codex passent par ce proxy`,"integrations.detail.codexAbsent":`Codex ne passe pas encore par ce proxy`,"integrations.detail.keyCount":`{count} clé(s) émises`,"integrations.detail.keyNone":`Aucune clé émise`,"integrations.detail.keyChecking":`Vérification…`,"integrations.detail.keyUnavailable":`État des clés indisponible`,"integrations.detail.claudeOff":`La connexion est désactivée`,"integrations.detail.desktopCurrent":`Desktop exécute ce profil`,"integrations.detail.desktopStale":`Le fichier de profil a été modifié après son application`,"integrations.detail.desktopNotServed":`Le profil existe, mais Desktop en utilise un autre`,"integrations.detail.desktopAbsent":`Aucun profil appliqué`,"integrations.detail.desktopDesiredOff":`L’intégration Claude Desktop est désactivée`,"integrations.detail.desktopDesiredOffCleanupPending":`Claude Desktop utilise encore la passerelle ; le nettoyage est en attente`,"integrations.detail.desktopDesiredOnNotApplied":`L’intégration est activée, mais Desktop n’utilise pas le profil de passerelle`,"integrations.detail.desktopSelectedElsewhere":`Desktop utilise un autre profil`,"integrations.detail.desktopProfileDrift":`Le profil Desktop sélectionné a changé`,"integrations.detail.desktopObservedUnsafe":`Le profil Desktop sélectionné ne peut pas être modifié en toute sécurité`,"integrations.detail.desktopNotInstalled":`La bibliothèque de configuration de Claude Desktop n’est pas installée`,"integrations.detail.grokModels":`{count} modèle(s) câblés`,"integrations.detail.grokAbsent":`Aucun bloc opencodex dans la configuration`,"integrations.dialog.grok.title":`Désactiver l’intégration Grok Build ?`,"integrations.dialog.grok.changes":`Seul le bloc marqué par opencodex sera supprimé de {path}. Le contenu écrit en dehors du bloc restera inchangé.`,"integrations.dialog.grok.breakage":`La désactivation supprime les alias de modèles opencodex de Grok Build. Les modèles utilisés avec votre compte xAI restent disponibles.`,"integrations.dialog.grok.undo":`Si opencodex s’exécute sur une adresse de bouclage, la réactivation écrit un nouveau bloc à partir des modèles actuellement disponibles.`,"integrations.dialog.grok.confirm":`Désactiver`,"integrations.dialog.desktop.title":`Désactiver l’intégration Claude Desktop ?`,"integrations.dialog.desktop.changes":`Si {path} contient un profil de passerelle géré par opencodex, Desktop sélectionnera d’abord un nouveau profil standard sans identifiants, puis supprimera l’ancien profil et sa sauvegarde.`,"integrations.dialog.desktop.breakage":`Claude Desktop reviendra à Claude standard au lieu des modèles acheminés par opencodex.`,"integrations.dialog.desktop.undo":`La réactivation régénère le profil opencodex à partir de vos affectations de modèles enregistrées.`,"integrations.dialog.desktop.restart":`Claude Desktop lit cette configuration uniquement au lancement. Quittez-le complètement, puis rouvrez-le pour appliquer cette modification.`,"integrations.dialog.desktop.confirm":`Désactiver`,"integrations.native.msg.nonLoopbackRemoved":`Grok Build ne peut être enregistré automatiquement que lorsqu’opencodex s’exécute sur une adresse de bouclage. Le bloc précédent qui pointait vers le bouclage a été supprimé.`,"integrations.native.msg.nonLoopbackRemovedNoop":`Grok Build ne peut être enregistré automatiquement que lorsqu’opencodex s’exécute sur une adresse de bouclage. Aucun bloc précédent n’était à supprimer.`,"integrations.native.msg.nonLoopbackSuperseded":`Grok Build ne peut être enregistré automatiquement que lorsqu’opencodex s’exécute sur une adresse de bouclage. Un autre processus a écrit un nouveau bloc entre-temps ; le bloc désormais présent dans le fichier n’a donc pas été créé par cette requête.`,"integrations.native.error.orphanedMarker":`{path} contient un marqueur de début opencodex, mais aucun marqueur de fin. Le fichier est resté inchangé, car opencodex ne peut pas déterminer où se termine son bloc.`,"integrations.native.error.homeMismatch":`Le répertoire d’accueil du service installé ne correspond pas au répertoire actuel ; le fichier est donc resté inchangé.`,"integrations.native.error.notInstalled":`Grok Build n’est pas installé ; il n’y a donc rien à modifier.`,"integrations.native.error.configBusy":`La configuration est en cours d’enregistrement ailleurs et n’a pas pu être modifiée. Réessayez dans un instant.`,"integrations.native.error.desktopUnsafeMetadata":`Les métadonnées de Claude Desktop dans {path} n’ont pas pu être lues en toute sécurité ; sa bibliothèque n’a donc pas été modifiée.`,"integrations.native.error.desktopCleanupIncomplete":`Claude Desktop pointe vers le mode standard, mais d’anciens fichiers d’identifiants opencodex subsistent ici : {paths}.`,"integrations.native.msg.desktopDisabled":`Intégration Claude Desktop désactivée.`,"integrations.native.msg.desktopEnabled":`Intégration Claude Desktop activée.`,"integrations.state.absent":`Non appliqué`,"integrations.state.current":`Appliqué`,"integrations.state.stale":`Mise à jour requise`,"integrations.state.conflict":`Conflit`,"integrations.state.unsafe":`Vérification impossible`,"integrations.summary.detected":`Clients détectés`,"integrations.summary.applied":`Clients configurés`,"integrations.summary.stale":`Mise à jour requise`,"integrations.summary.lastChange":`Dernière modification`,"integrations.summary.disableAll":`Tout désactiver…`,"integrations.onboarding":`L’application écrit un bloc de fournisseur opencodex après avoir créé une sauvegarde. La désactivation supprime uniquement ce bloc, et un instantané conservé peut être restauré.`,"integrations.empty.title":`Aucun client installé n’a été détecté`,"integrations.empty.body":`Installez un client pris en charge, puis revenez ici pour appliquer opencodex.`,"integrations.action.apply":`Appliquer`,"integrations.action.disable":`Désactiver`,"integrations.action.refresh":`Mettre à jour`,"integrations.action.settings":`Paramètres`,"integrations.action.manageKeys":`Gérer les clés`,"integrations.action.restore":`Restaurer…`,"integrations.action.undo":`Annuler`,"integrations.action.restorePoint":`Restaurer ce point…`,"integrations.action.snapshotExpired":`Sauvegarde expirée`,"integrations.rollback.title":`Centre de restauration`,"integrations.rollback.empty":`Aucun historique d’application`,"integrations.rollback.emptyBody":`Chaque écriture réussie conserve d’abord un instantané antérieur.`,"integrations.catalog.title":`Clients`,"integrations.rollback.older":`Opérations antérieures`,"integrations.rollback.showMore":`Afficher {n} de plus`,"integrations.rollback.failed":`Impossible de charger l’historique des restaurations.`,"integrations.restore.title":`Restaurer cet instantané ?`,"integrations.restore.body":`Le fichier actuel est d’abord sauvegardé, puis remplacé par l’instantané sélectionné.`,"integrations.restore.driftTitle":`Des modifications plus récentes ont été détectées`,"integrations.restore.driftBody":`Les modifications apportées après cet instantané seront sauvegardées, puis le fichier sera remplacé.`,"integrations.restore.confirm":`Restaurer`,"integrations.restore.confirmDrift":`Sauvegarder les modifications récentes et restaurer`,"integrations.restore.pending":`Restauration…`,"integrations.restore.manual":`Échec de la restauration automatique : {reason}. Restaurez manuellement depuis {path}.`,"integrations.error.load":`Impossible de charger l’état de l’intégration.`,"integrations.error.stale":`La dernière actualisation a échoué. Les valeurs ci-dessous peuvent être obsolètes.`,"integrations.error.busy":`Une autre modification de ce client est toujours en cours. Réessayez dans un instant.`,"integrations.error.conflict":`La configuration a changé après son écriture par opencodex. Rien n’a été supprimé.`,"integrations.error.unsafe":`La configuration ne peut pas être modifiée en toute sécurité.`,"integrations.error.generic":`La modification de l’intégration a échoué. Votre état précédent a été conservé.`,"integrations.error.nonLoopback":`{client} ne peut atteindre qu’un proxy sur localhost — sa configuration ne permet pas d’ajouter l’en-tête d’admission requis par une liaison distante ; l’écrire manuellement ne servirait donc à rien. Donnez-lui plutôt un accès par bouclage, via un tunnel ou un redirecteur local.`,"integrations.status.installed":`Installé`,"integrations.status.notInstalled":`Non installé`,"integrations.status.appliedAt":`Appliqué`,"integrations.status.backup":`Sauvegarde`,"integrations.status.lastRestore":`Dernière restauration`,"integrations.status.unknown":`Inconnu`,"integrations.bulk.title":`Désactiver les intégrations client appliquées ?`,"integrations.bulk.body":`Seul le bloc appartenant à opencodex est supprimé. Un instantané antérieur est conservé pour chaque client.`,"integrations.bulk.partial":`Certains clients n’ont pas pu être désactivés : {clients}`,"integrations.bulk.success":`Les intégrations client appliquées ont été désactivées.`,"integrations.retention.degraded":`Le nettoyage des sauvegardes est en retard ; d’anciennes sauvegardes peuvent encore se trouver sur le disque.`,"integrations.error.residual":`Le fichier peut être dans un état intermédiaire : {message} Restaurez-le depuis {path}.`,"integrations.error.recover":`{message} Une sauvegarde se trouve dans {path}.`,"integrations.kind.apply":`Appliqué`,"integrations.kind.disable":`Désactivé`,"integrations.kind.refresh":`Mis à jour`,"integrations.kind.restore":`Restauré`,"integrations.kind.overwrite":`Écrasé`,"integrations.dialog.overwrite.title":`Remplacer le bloc dans cette configuration ?`,"integrations.dialog.overwrite.changesUnowned":`Dans {path}, un bloc que nous n'avons pas écrit occupe l'emplacement dont opencodex a besoin. L'application le remplace par le bloc qu'écrirait opencodex.`,"integrations.dialog.overwrite.changesForeign":`Votre modification dans le bloc opencodex de {path} sera abandonnée et remplacée par le bloc qu'écrirait opencodex.`,"integrations.dialog.overwrite.breakage":`Ce que configurait l'autre bloc cesse de s'appliquer. Le reste du fichier n'est pas touché.`,"integrations.dialog.overwrite.undo":`Un instantané est enregistré au préalable : cette opération apparaît dans la liste de restauration ci-dessous et peut être annulée.`,"integrations.dialog.overwrite.confirm":`Remplacer`,"integrations.action.overwrite":`Remplacer`,"integrations.semantics.opencode":`Uniquement pour les lancements directs depuis le disque ; l’injection d’environnement par ocx opencode est prioritaire.`,"integrations.semantics.pi":`S’applique aux nouvelles sessions.`,"integrations.semantics.omp":`Redémarrez OMP pour charger le catalogue.`,"integrations.semantics.hermes":`S’applique aux nouvelles sessions.`,"integrations.semantics.openclaw":`S’applique immédiatement à une passerelle en cours d’exécution.`,"integrations.semantics.kimi":`Redémarrez ou exécutez /reload pour l’appliquer (la v2 surveille le fichier).`,"integrations.semantics.gajae":`S’applique à une nouvelle session ou à l’ouverture de /model.`,"integrations.semantics.dsh":`OpenCodex gère uniquement llm-pi-ai.providers.opencodex dans $DSH_HOME/settings.yaml. DSH recharge ce fournisseur à chaud ; votre modèle par défaut et deepseek-official restent inchangés. Seule l’adresse de bouclage est actuellement prise en charge ; aucun identifiant réel n’est écrit.`,"integrations.semantics.mcode":`Gère uniquement custom_provider.opencodex. Votre modèle par défaut et votre connexion MiniMax restent inchangés.`,"integrations.semantics.zcode":`Gère uniquement provider.opencodex dans ~/.zcode/v2/config.json. Votre connexion Z.ai et les autres fournisseurs restent inchangés. Redémarrez ZCode après toute modification.`,"integrations.semantics.prime":`Gère uniquement providers.opencodex dans le models.json de Prime Agent — ~/.prime/agent, sauf si PRIME_AGENT_CODING_AGENT_DIR le redirige. Vos autres fournisseurs et surcharges de modèles restent inchangés. S'applique aux nouvelles sessions.`,"integrations.semantics.aside":`Gère uniquement providers.opencodex dans le models.json d'Aside pour le compte connecté (~/.aside/u/). Vos autres fournisseurs restent inchangés. Aside réécrit ce fichier pendant son exécution : quittez-le complètement et relancez-le après application.`,"codexAuth.mainAccount":`Compte principal`,"codexAuth.logLabel":`Libellé du journal`,"codexAuth.codexApp":`Application Codex`,"codexAuth.moreActions":`Afficher plus d’actions`,"codexAuth.copyId":`Copier l’ID du compte`,"codexAuth.appLogin":`Connexion à l’application`,"codexAuth.accountPool":`Groupe de comptes`,"codexAuth.accountModeTitle":`Mode de compte OpenAI`,"codexAuth.accountModePool":`Mode Groupe`,"codexAuth.accountModePoolDesc":`La connexion principale et les comptes ajoutés admissibles alternent ici.`,"codexAuth.accountModeDirect":`Mode Direct`,"codexAuth.accountModeDirectDesc":`Les requêtes utilisent uniquement la connexion principale ; les comptes ajoutés restent stockés pour le mode Groupe.`,"codexAuth.openaiMissing":`Le fournisseur OpenAI intégré n’est pas configuré.`,"codexAuth.openaiDisabled":`Le fournisseur OpenAI intégré est désactivé.`,"codexAuth.openaiUnavailableDesc":`Vos comptes OpenAI restent disponibles. Activez le fournisseur pour acheminer les requêtes Codex.`,"codexAuth.enableOpenai":`Activer OpenAI`,"codexAuth.enablingOpenai":`Activation…`,"codexAuth.enableOpenaiFailed":`Échec de l’activation du fournisseur OpenAI.`,"codexAuth.openaiPresetLoadFailed":`Échec du chargement du préréglage du fournisseur OpenAI.`,"codexAuth.openaiPresetUnavailable":`Le préréglage du fournisseur OpenAI est indisponible.`,"codexAuth.openProviders":`Ouvrir Fournisseurs`,"codexAuth.add":`Ajouter`,"codexAuth.sparkQuota":`Quota Codex Spark`,"codexAuth.sparkQuotaHint":`Affiche la fenêtre hebdomadaire GPT-5.3-Codex-Spark sur les cartes de compte. Masquée par défaut car elle ne concerne qu'un seul modèle.`,"codexAuth.sparkQuotaShown":`Quota Codex Spark affiché`,"codexAuth.sparkQuotaHidden":`Quota Codex Spark masqué`,"codexAuth.sparkQuotaFailed":`Impossible de modifier le réglage du quota Codex Spark`,"codexAuth.refreshQuota":`Actualiser les quotas`,"codexAuth.refreshingQuota":`Actualisation…`,"codexAuth.quotaRefreshed":`Quotas actualisés`,"codexAuth.quotaRefreshFailed":`Échec de l’actualisation des quotas`,"codexAuth.pauseExhausted":`Suspendre les comptes épuisés`,"codexAuth.pausingExhausted":`Vérification des quotas…`,"codexAuth.pauseExhaustedSucceeded":`Comptes à la limite suspendus : {count}`,"codexAuth.pauseExhaustedNone":`Aucun compte n’a une utilisation confirmée à 100 %.`,"codexAuth.pauseExhaustedFailed":`Échec de la vérification et de la suspension des comptes épuisés.`,"codexAuth.noPool":`Aucun compte ajouté au groupe pour le moment.`,"codexAuth.pause":`Suspendre`,"codexAuth.resume":`Reprendre`,"codexAuth.paused":`SUSPENDU`,"codexAuth.pauseSucceeded":`{email} est suspendu`,"codexAuth.resumeSucceeded":`{email} est de nouveau disponible dans le groupe`,"codexAuth.pauseFailed":`Impossible de suspendre {email}. Aucune modification apportée.`,"codexAuth.resumeFailed":`Impossible de réactiver {email}. Aucune modification apportée.`,"codexAuth.pausedHint":`Exclu du changement automatique, des nouvelles tentatives, de la récupération après délai et de la sélection manuelle jusqu’à sa réactivation.`,"codexAuth.pinned":`ÉPINGLÉ`,"codexAuth.pinnedHint":`Vous avez sélectionné ce compte manuellement ; un ordre de sélection supérieur ne le remplacera donc pas. L’épinglage dure jusqu’à l’épuisement de ce compte, la sélection d’un autre compte ou la modification d’un ordre de sélection.`,"codexAuth.fiveHour":`5 h`,"codexAuth.weekly":`Semaine`,"codexAuth.monthly":`30 j`,"codexAuth.resets":`réinitialisation`,"codexAuth.today":`Aujourd’hui`,"codexAuth.current":`ACTUEL`,"codexAuth.nextSession":`SÉLECTIONNÉ`,"codexAuth.poolPrepared":`PRÉPARÉ POUR LE GROUPE`,"codexAuth.preparePoolTitle":`Préparer ce compte pour le mode Groupe ?`,"codexAuth.preparePoolDesc":`Les requêtes directes continuent d’utiliser la connexion principale. Ce compte devient la sélection préparée du Groupe lorsque le mode Groupe est activé.`,"codexAuth.prepareForPool":`Préparer pour le Groupe`,"codexAuth.poolPreparedToast":`{email} est préparé pour le mode Groupe`,"codexAuth.switchTitle":`Changer de compte actif ?`,"codexAuth.switchDesc":`Prend effet immédiatement. Les fils liés à un compte et les requêtes déjà en cours conservent le compte capturé ; les requêtes nouvelles ou non liées utilisent le niveau d’ordre du compte sélectionné, et les comptes de même ordre continuent d’alterner.`,"codexAuth.cacheWarning":`Le cache des prompts est réinitialisé lors d’un changement de compte. La nouvelle session démarre avec un cache vide.`,"codexAuth.setAsNext":`Utiliser ensuite ce compte`,"codexAuth.cancel":`Annuler`,"codexAuth.switchBack":`Revenir au compte principal ?`,"codexAuth.switchBackDesc":`Prend effet immédiatement. Les fils liés à un compte et les requêtes déjà en cours conservent le compte capturé ; les requêtes nouvelles ou non liées utilisent le niveau d’ordre du compte de connexion à l’application, et les comptes de même ordre continuent d’alterner.`,"codexAuth.autoSwitch":`Changement proactif selon l’utilisation`,"codexAuth.autoSwitchQuotaDesc":`Quota : à partir de {threshold}% d’utilisation, la requête suivante peut passer à un compte admissible moins utilisé, y compris pour une tâche déjà liée ; Go/Free utilisent uniquement 30 j.`,"codexAuth.autoSwitchQuotaOffDesc":`Le changement proactif selon l’utilisation est désactivé. L’affectation nouvelle/non liée et la récupération après échec restent actives.`,"codexAuth.autoSwitchRoundRobinDesc":`L’affectation en rotation n’utilise pas ce seuil ; elle continue d’alterner les tâches nouvelles/non liées.`,"codexAuth.autoSwitchFillFirstDesc":`Remplissage prioritaire : {threshold}% est le seuil d’épuisement pour les tâches nouvelles/non liées ; les tâches liées saines conservent leur compte.`,"codexAuth.autoSwitchFillFirstOffDesc":`Le remplissage prioritaire n’a aucun seuil d’épuisement lié à l’utilisation pour les tâches nouvelles/non liées ; le délai de récupération, la réauthentification et la récupération après échec peuvent toujours modifier le routage.`,"codexAuth.failureRecoveryNote":`La récupération après échec est distincte : une requête rejetée avant toute sortie avec 429/402, un délai de récupération, une réauthentification, une exclusion ou un basculement transitoire configuré peut sélectionner un autre compte admissible.`,"codexAuth.autoSwitchThreshold":`Seuil d’utilisation`,"codexAuth.autoSwitchThresholdAria":`Seuil d’utilisation, en pourcentage`,"codexAuth.autoSwitchThresholdInc":`Augmenter le seuil d’utilisation`,"codexAuth.autoSwitchThresholdDec":`Diminuer le seuil d’utilisation`,"codexAuth.autoSwitchLoadFailed":`Impossible de charger le paramètre de changement selon l’utilisation.`,"codexAuth.autoSwitchThresholdInvalid":`Saisissez un nombre entier compris entre 1 et 100`,"codexAuth.autoSwitchUpdated":`Changement proactif selon l’utilisation mis à jour`,"codexAuth.autoSwitchUpdateFailed":`Impossible de confirmer la mise à jour du changement selon l’utilisation. La dernière valeur confirmée est affichée.`,"codexAuth.requestUserInput":`Demander une saisie en mode Default`,"codexAuth.requestUserInputDesc":`Permet à Codex de suspendre une session en mode Default et de vous poser des questions avec l’outil request_user_input.`,"codexAuth.requestUserInputUpdated":`Indicateur de fonctionnalité mis à jour — s’applique aux nouvelles sessions.`,"codexAuth.requestUserInputUpdatedRestart":`Indicateur de fonctionnalité mis à jour — s’applique aux nouvelles sessions. Redémarrez l’application Codex pour le prendre en compte.`,"codexAuth.requestUserInputUpdateFailed":`Impossible de mettre à jour l’indicateur de fonctionnalité. Aucune modification apportée.`,"codexAuth.requestUserInputLoadFailed":`Impossible de lire l’indicateur de fonctionnalité dans config.toml.`,"codexAuth.accountPickerTitle":`Cibler un compte Codex précis depuis le sélecteur de modèle`,"codexAuth.accountPickerOffDesc":`Lorsque cette option est activée, les lignes GPT ordinaires du sélecteur sont remplacées par une entrée pour chaque sélecteur de compte. Vous pouvez ainsi choisir le compte exact d’une conversation sans vous déconnecter. La désactivation ne supprime aucun compte.`,"codexAuth.accountPickerOnDesc":`Chaque sélecteur est un libellé public associé à un compte stocké. Le choisir verrouille la conversation sur le compte correspondant : elle n’alterne jamais et ne bascule pas vers un compte de secours, sans modifier le compte Groupe actif.`,"codexAuth.accountPickerCompatibility":`La connexion intégrée à l’application Codex possède son propre sélecteur ; les mappages générés l’appellent normalement main et utilisent au besoin un suffixe évitant les collisions, tel que main-2. Les comptes ajoutés reçoivent des libellés stables qui préservent la confidentialité, tandis que les libellés personnalisés restent inchangés. Le routage des conversations existantes et des sélections de modèles enregistrées se poursuit. La désactivation masque les entrées générées, mais préserve les sélecteurs et les routes exactes. Les ID de modèle GPT simples conservent leur comportement Groupe ou Direct.`,"codexAuth.accountPickerUpdated":`Ciblage des comptes mis à jour.`,"codexAuth.accountPickerUpdateFailed":`Impossible de mettre à jour le ciblage des comptes. Le dernier paramètre confirmé est affiché.`,"codexAuth.accountPickerLoadFailed":`Impossible de charger le paramètre de ciblage des comptes.`,"codexAuth.accountPickerRefreshFailed":`Impossible d’actualiser ce paramètre. La dernière valeur confirmée reste affichée.`,"codexAuth.advancedSettings":`Paramètres avancés`,"codexAuth.advancedSettingsAria":`Afficher ou masquer les paramètres avancés de l’authentification Codex`,"codexAuth.catalogRefreshPending":`La modification a été enregistrée, mais l’actualisation du catalogue de modèles Codex est en attente. Exécutez ocx sync pour réessayer.`,"anthropicPool.title":`Groupe de comptes Claude (expérimental)`,"anthropicPool.enabledDesc":`En cas de 429, met le compte en délai de récupération et bascule vers un autre. Les nouvelles sessions privilégient une utilisation inférieure à {threshold}% ({window}).`,"anthropicPool.enabledNoProactiveDesc":`En cas de 429, met le compte en délai de récupération et bascule. Le basculement proactif basé sur l'usage est désactivé au seuil 0, mais la sélection des nouvelles sessions et la récupération après 429 utilisent toujours la fenêtre {window}.`,"anthropicPool.disabledDesc":`Utilise uniquement le compte Claude actif. Activez cette option seulement si vous acceptez le routage expérimental.`,"anthropicPool.experimentalWarning":`Fonctionnalité expérimentale et peu éprouvée. Anthropic peut restreindre les comptes présentant une rotation multicomptes automatisée. Les comptes d’une même organisation peuvent partager un quota — leur mise en groupe n’apportera rien. Laissez cette option désactivée si vous n’en comprenez pas les risques.`,"anthropicPool.needTwoAccounts":`Ajoutez au moins deux comptes OAuth Claude avant d’activer le groupe.`,"anthropicPool.threshold":`Seuil d’utilisation des nouvelles sessions`,"anthropicPool.thresholdAria":`Seuil d’utilisation des nouvelles sessions, en pourcentage`,"anthropicPool.thresholdHelp":`0 désactive la sélection selon le quota (affinité + compte actif uniquement). Valeur par défaut : 80.`,"anthropicPool.thresholdInvalid":`Saisissez un nombre entier compris entre 0 et 100`,"anthropicPool.loadFailed":`Impossible de charger les paramètres du groupe Claude.`,"anthropicPool.saveFailed":`Impossible d’enregistrer les paramètres du groupe Claude.`,"anthropicPool.on":`Activé`,"anthropicPool.off":`Désactivé`,"accountPool.strategy":`Stratégie de rotation`,"accountPool.strategyDesc":`Méthode utilisée par OpenCodex pour affecter un compte à une tâche nouvelle/non liée.`,"accountPool.strategyQuota":`Quota`,"accountPool.strategyRoundRobin":`Rotation`,"accountPool.strategyFillFirst":`Remplissage prioritaire`,"accountPool.strategyHintQuota":`La stratégie Quota peut également relier une tâche existante à un autre compte lors de sa requête suivante, une fois le seuil d’utilisation franchi.`,"accountPool.strategyHintRoundRobin":`La rotation ne concerne que les tâches sans liaison active ; le seuil d’utilisation ne modifie pas la rotation normale.`,"accountPool.strategyHintFillFirst":`Le remplissage prioritaire utilise le seuil comme point d’épuisement pour les tâches non liées ; les tâches liées saines conservent leur affinité.`,"accountPool.unboundDefinition":`Une tâche nouvelle/non liée désigne une requête sans liaison actuelle à un compte ; une tâche existante visible peut devenir non liée après la réinitialisation du proxy ou de l’affinité.`,"accountPool.stickyLimit":`Affectations nouvelles/non liées avant rotation`,"accountPool.stickyLimitAria":`Affectations nouvelles/non liées avant rotation`,"accountPool.stickyLimitInc":`Augmenter la limite de persistance`,"accountPool.stickyLimitDec":`Diminuer la limite de persistance`,"accountPool.stickyLimitHelp":`Conservez le compte sélectionné pour ce nombre d’affectations de tâches nouvelles/non liées avant de passer au suivant ; le compteur augmente lorsque la tâche est liée, et non après la réussite en amont.`,"accountPool.stickyLimitInvalid":`Saisissez un nombre entier compris entre 1 et 100`,"accountPool.strategyLoadFailed":`Impossible de charger la stratégie de rotation.`,"accountPool.strategyUpdateFailed":`Impossible d’enregistrer la stratégie de rotation.`,"accountPool.quotaWindow":`Fenêtre de quota`,"accountPool.quotaWindowDesc":`Barre d’utilisation en cache qui régit la sélection des nouvelles sessions par quota, les seuils de remplissage prioritaire et les remplacements 429 admissibles.`,"accountPool.quotaWindowFiveHour":`Barre de 5 heures`,"accountPool.quotaWindowWeekly":`Barre hebdomadaire`,"accountPool.quotaWindowMaxUtilization":`Barre la plus haute`,"accountPool.quotaWindowHint":`La barre hebdomadaire ignore les comptes dont la barre de 5 heures est épuisée tant qu’un autre compte admissible reste disponible, mais y revient si aucun autre ne reste. Les égalités privilégient la plus faible utilisation sur 5 heures ; les barres hebdomadaires ne sont connues qu’après interrogation de la page Fournisseurs.`,"accountPool.quotaWindowInert":`Seule la stratégie Quota — ou le remplissage prioritaire avec un seuil supérieur à 0 — évalue une barre d’utilisation ; ce réglage ne change donc rien pour la stratégie de rotation actuelle.`,"accountPool.priority":`Ordre de sélection`,"accountPool.priorityAria":`Ordre de sélection de ce compte`,"accountPool.priorityHint":`Les nombres les plus élevés sont utilisés en premier. Le groupe ne passe à un nombre inférieur que lorsque tous les comptes de niveau supérieur sont épuisés ou indisponibles.`,"accountPool.priorityFirst":`Premier`,"accountPool.priorityEarlier":`Plus tôt`,"accountPool.priorityNormal":`Normal`,"accountPool.priorityLater":`Plus tard`,"accountPool.priorityLast":`Dernier`,"accountPool.priorityOption":`{name} ({value})`,"accountPool.priorityCustom":`Personnalisé`,"accountPool.priorityUpdated":`Ordre de sélection mis à jour pour {email}`,"accountPool.priorityUpdateFailed":`Impossible d’enregistrer l’ordre de sélection de {email}. La dernière valeur confirmée est affichée.`,"codexAuth.switched":`{email} est sélectionné pour la prochaine requête`,"codexAuth.loadFailed":`Impossible de charger les paramètres des comptes Codex.`,"codexAuth.switchFailed":`Impossible de changer de compte. Votre sélection précédente reste inchangée.`,"codexAuth.removeConfirm":`Supprimer {id} ?`,"codexAuth.removeFailed":`Impossible de supprimer le compte. Aucune modification apportée.`,"codexAuth.addTitle":`Ajouter un compte Codex`,"codexAuth.addIdLabel":`ID du compte (slug)`,"codexAuth.addIdPlaceholder":`codex-work, codex-alt, team...`,"codexAuth.resetCreditsAria":`{count} crédit(s) de réinitialisation`,"codexAuth.addJsonLabel":`Contenu de auth.json`,"codexAuth.addHelp":`Copiez-le depuis le fichier ~/.codex/auth.json d’une autre machine ou utilisez codex-auth export.`,"codexAuth.importBtn":`Importer`,"codexAuth.importInvalidJson":`JSON non valide`,"codexAuth.importMissingTokens":`access_token ou refresh_token absent du JSON`,"codexAuth.importMissingId":`L’identifiant du compte est requis`,"codexAuth.accountAdded":`Compte ajouté au groupe`,"codexAuth.addPickDesc":`Connectez-vous avec un autre compte ChatGPT pour l’ajouter au groupe.`,"codexAuth.oauthLogin":`Connexion OAuth`,"codexAuth.oauthDesc":`Ouvre la page de connexion ChatGPT dans le navigateur`,"codexAuth.deviceLogin":`Connexion par code d'appareil`,"codexAuth.deviceDesc":`Pour un proxy headless ou distant : saisissez un code court sur un autre appareil`,"codexAuth.importAuthJson":`Importer auth.json`,"codexAuth.importAuthJsonDesc":`Depuis une autre installation de Codex ou un export codex-auth`,"codexAuth.back":`Retour`,"codexAuth.oauthAlreadyInProgress":`Une connexion est déjà en cours. Terminez-la dans votre navigateur.`,"codexAuth.oauthWaiting":`En attente de la fin de la connexion à ChatGPT dans votre navigateur...`,"codexAuth.oauthSubmittingCode":`Envoi du code…`,"codexAuth.oauthCodeSubmitted":`Code envoyé — en attente de la fin de la connexion…`,"codexAuth.oauthStatusRetrying":`Erreur réseau ou de proxy lors de la vérification de l’état de la connexion — nouvelle tentative…`,"codexAuth.oauthCancelled":`La connexion a été annulée.`,"codexAuth.loginFailed":`Échec de la connexion`,"codexAuth.needsReauth":`Se reconnecter`,"codexAuth.reauthenticate":`Se réauthentifier`,"codexAuth.tokenExpired":`Jeton expiré — réauthentifiez ce compte`,"codexAuth.mainTokenExpired":`Jeton expiré — reconnectez-vous depuis l’application Codex`,"codexAuth.emailCollision":`Ce compte correspond à votre connexion Codex principale. Utilisez un autre compte.`,"codexAuth.resetCreditsTitle":`Crédits de réinitialisation`,"codexAuth.resetCreditsAvailable":`Vous disposez de {count} crédit(s) de réinitialisation.`,"codexAuth.resetCreditsDesc":`Chaque crédit réinitialise instantanément vos limites d’utilisation horaire et hebdomadaire actuelles.`,"codexAuth.noResetCredits":`Vous ne disposez d’aucun crédit de réinitialisation.`,"codexAuth.earnCreditsHint":`Les crédits sont accordés chaque mois et dans le cadre du programme de parrainage.`,"codexAuth.creditsExpireNote":`Les crédits expirent 30 jours après leur obtention.`,"codexAuth.useOneCredit":`Utiliser 1 crédit`,"codexAuth.confirmResetTitle":`Utiliser un crédit de réinitialisation?`,"codexAuth.confirmResetDesc":`Vos limites de débit actuelles seront réinitialisées immédiatement. Il vous reste {count} crédit(s).`,"codexAuth.irreversible":`Cette action est irréversible.`,"codexAuth.useCredit":`Utiliser le crédit`,"codexAuth.redeeming":`Réinitialisation...`,"codexAuth.resetSuccess":`Limites de débit réinitialisées! {remaining} crédit(s) restant(s).`,"codexAuth.resetSuccessGeneric":`Limites de débit réinitialisées!`,"codexAuth.resetAlreadyRedeemed":`Ce crédit a déjà été utilisé. Les crédits sont inchangés.`,"codexAuth.resetNothingToReset":`Aucune fenêtre de limite de débit ne doit être réinitialisée actuellement.`,"codexAuth.resetNoCredit":`Aucun crédit de réinitialisation disponible.`,"codexAuth.resetError":`Impossible d’utiliser le crédit de réinitialisation. Réessayez.`,"codexAuth.fifoNote":`Le crédit le plus ancien est utilisé en premier.`,"codexAuth.confirmWhichCredit":`Le crédit du {date} sera utilisé.`,"codexAuth.creditNext":`Prochain à utiliser`,"codexAuth.creditLabel":`Crédit nº {n}`,"codexAuth.creditNextBadge":`PROCHAIN`,"codexAuth.creditGranted":`Accordé le {date}`,"codexAuth.creditExpires":`Expire le {date} ({days} j restants)`,"api.title":`Accès à l’API`,"api.subtitle":`Utilisez les clés API générées pour accéder au proxy opencodex depuis des applications externes. Les clés s’authentifient au moyen de l’en-tête {authHeader}; consultez le tableau ci-dessous pour connaître les éléments acceptés par chaque point de terminaison.`,"api.baseUrl":`URL de base`,"api.responsesEndpoint":`API Responses`,"api.chatCompletionsEndpoint":`API Chat Completions`,"api.messagesEndpoint":`API Messages`,"api.modelsEndpoint":`API des modèles`,"api.endpointNote":`Utilisez l’URL de base avec les clients compatibles avec OpenAI. Responses et Chat Completions sont accessibles sous /v1.`,"api.endpointsTitle":`Points de terminaison`,"api.authTitle":`Authentification`,"api.authLoopback":`Les écoutes en boucle locale (127.0.0.1 ou ::1) contournent l’authentification. Les écoutes distantes nécessitent une clé ocx_ générée ou OPENCODEX_API_AUTH_TOKEN.`,"api.authBaseUrlNote":`Configurez les clients avec l’URL de base, puis choisissez ci-dessous le point de terminaison propre au protocole.`,"api.newKeyTitle":`Nouvelle clé créée`,"api.newKeyNote":`Copiez cette clé maintenant — elle ne sera plus affichée.`,"api.copy":`Copier`,"api.copied":`Copié`,"api.dismiss":`Fermer`,"api.generateTitle":`Générer une clé`,"api.keyNamePlaceholder":`Nom de la clé (facultatif)`,"api.generate":`Générer`,"api.generating":`Création…`,"api.activeKeys":`Clés actives ({count})`,"api.activeKeysLoading":`Clés actives`,"api.noKeys":`Aucune clé API pour le moment. Générez-en une ci-dessus.`,"api.workspace.sections":`Sections de l’API`,"api.section.keys":`Clés`,"api.section.connect":`Connexion`,"api.section.endpoints":`Points de terminaison`,"api.section.models":`Modèles`,"api.section.examples":`Exemples`,"api.workspace.details":`Détails de la clé API`,"api.workspace.keyDetails":`Détails de la clé`,"api.workspace.keyPrefix":`Préfixe de la clé`,"api.workspace.deleteKey":`Supprimer la clé`,"api.workspace.deleteConfirm":`Voulez-vous vraiment supprimer cette clé? Cette action est irréversible.`,"api.workspace.usageExamples":`Exemples d’utilisation`,"api.copyUrlHint":`Cliquer pour copier l’URL`,"api.urlCopied":`URL copiée`,"api.copyExampleHint":`Cliquer pour copier l’exemple`,"api.exampleCopied":`Exemple copié`,"api.colName":`Nom`,"api.colKey":`Clé`,"api.colCreated":`Création`,"api.confirm":`Confirmer`,"api.deleteAria":`Supprimer la clé API`,"api.modelsTitle":`Catalogue de modèles externes`,"api.modelsCount":`{count} appelable(s)`,"api.modelsLoading":`Chargement des modèles…`,"api.modelsSearch":`Rechercher des modèles`,"api.modelsSubtitle":`Utilisez ces identifiants de modèle exacts avec /v1/models et le protocole entrant de votre choix.`,"api.modelsEmpty":`Aucun modèle appelable de l’extérieur n’est encore disponible.`,"api.modelsNoMatch":`Aucun modèle ne correspond à « {query} ».`,"api.modelsLoadFailed":`Impossible de charger le catalogue de modèles externes.`,"api.colModel":`Modèle`,"api.colSource":`Source`,"api.colProtocols":`Protocoles`,"api.sourceNative":`Groupe ChatGPT`,"api.sourceCombo":`Route de combinaison`,"api.sourceCustom":`Personnalisé`,"api.protocolResponses":`Responses`,"api.protocolChatCompletions":`Chat Completions`,"api.protocolMessages":`Messages`,"api.copyModelId":`Copier l’identifiant`,"api.modelCopied":`Copié`,"api.testModel":`Tester`,"api.testingModel":`Test en cours…`,"api.testSucceeded":`OK`,"api.testFailed":`Échec`,"api.usageChatTitle":`Exemple Chat Completions`,"api.usageResponsesTitle":`Exemple Responses`,"api.usageMessagesTitle":`Exemple Messages`,"api.usageSampleInput":`Bonjour tout le monde!`,"api.clientConfig.title":`Configuration du client`,"api.clientConfig.rowsLabel":`Connecter un client`,"api.clientConfig.details":`Détails`,"api.clientConfig.detailsAria":`Détails de la configuration de {client}`,"api.clientConfig.copyAria":`Copier la configuration de {client}`,"api.clientConfig.downloadAria":`Télécharger la configuration de {client}`,"api.clientConfig.rowMeta":`{destination} · {count} modèle(s)`,"api.clientConfig.rowError":`Impossible de générer la configuration de {client}.`,"api.clientConfig.copiedAnnounceClient":`Configuration de {client} copiée dans le presse-papiers.`,"api.clientConfig.clientOpencode":`OpenCode`,"api.clientConfig.clientPi":`Pi`,"api.clientConfig.clientOmp":`OMP`,"api.clientConfig.clientHermes":`Hermes`,"api.clientConfig.clientOpenclaw":`OpenClaw`,"api.clientConfig.clientKimi":`Kimi Code`,"api.clientConfig.clientGajae":`Gajae Code`,"api.clientConfig.clientDsh":`DeepSeek Harness (DSH)`,"api.clientConfig.clientMcode":`MiniMax Code`,"api.clientConfig.clientZcode":`ZCode`,"api.clientConfig.clientPrime":`Prime Agent`,"api.clientConfig.clientAside":`Aside`,"api.clientConfig.copy":`Copier la configuration`,"api.clientConfig.download":`Télécharger`,"api.clientConfig.loading":`Génération de la configuration du client…`,"api.clientConfig.jsonLabel":`Configuration de {client}`,"api.clientConfig.destination":`Fichier de destination`,"api.clientConfig.envHint":`Définissez la clé avant le lancement`,"api.clientConfig.mergeWarning":`Fusionnez ceci dans le fichier de destination. Le remplacer supprimerait vos autres fournisseurs et paramètres MCP.`,"api.clientConfig.modelCount":`{count} modèle(s) exporté(s)`,"api.clientConfig.missingLimits":`{count} modèle(s) sur {total} sont fournis sans limite de contexte; le client applique ses propres valeurs par défaut.`,"api.clientConfig.noKeyYet":`Aucune clé ne se trouve encore derrière {env}. Générez une clé ci-dessus avant d’utiliser cette configuration hors de la boucle locale.`,"api.clientConfig.loadFailed":`Impossible de lire la liste des modèles; aucune configuration de client n’a donc été produite.`,"api.clientConfig.copiedAnnounce":`Configuration du client copiée dans le presse-papiers.`,"api.clientConfig.copyFailed":`Impossible de copier la configuration du client.`,"api.clientConfig.downloadedAnnounce":`{filename} téléchargé. Rien n’a encore changé — fusionnez-le vous-même dans {destination}.`,"api.clientConfig.whereDisclosure":`Emplacement de ce fichier`,"api.clientConfig.whereBody":`La destination ci-dessus est le chemin global. Un fichier de configuration local au projet dans le répertoire de travail a priorité sur celui-ci, et le client lit la clé depuis la variable d’environnement nommée dans la configuration — jamais depuis ce fichier.`,"api.keysLoadFailed":`Impossible de charger les clés API.`,"api.createFailed":`Impossible de créer la clé API.`,"api.deleteFailed":`Impossible de supprimer la clé API.`,"api.auth.endpoint":`Point de terminaison`,"api.auth.required":`Requis`,"api.auth.accepted":`Accepté`,"api.auth.rejected":`Non accepté`,"api.auth.testProtocol":`Tester {protocol}`,"api.auth.testNeedsFreshKey":`Générez une clé et conservez sa valeur à usage unique à l’écran pour exécuter un test authentifié.`,"api.key.name":`Nom de la clé`,"api.key.rename":`Renommer`,"api.key.saveName":`Enregistrer le nom`,"api.key.renaming":`Enregistrement…`,"api.key.renameFailed":`Impossible de renommer la clé. Votre brouillon a été conservé.`,"api.key.deleting":`Suppression…`,"api.rotation.title":`Rotation de la clé`,"api.rotation.description":`Crée une clé de remplacement tout en conservant brièvement la clé actuelle.`,"api.rotation.start":`Démarrer la rotation`,"api.rotation.starting":`Démarrage…`,"api.rotation.pending":`La rotation est en attente. Mettez à jour et vérifiez le client avant de la valider.`,"api.rotation.expires":`Fin du chevauchement :`,"api.rotation.secretOnce":`Clé de remplacement — affichée une seule fois. Copiez-la avant de fermer.`,"api.rotation.commit":`Valider la rotation`,"api.rotation.abort":`Annuler la rotation`,"api.rotation.failed":`L’action de rotation n’a pas abouti. Actualisez avant de réessayer.`,"api.rotation.startFailed":`Impossible de démarrer la rotation de la clé.`,"api.key.copyFailed":`Impossible de copier la clé. Sélectionnez-la et copiez-la manuellement avant de fermer ce panneau.`,"api.attribution.title":`Utilisation attribuée`,"api.attribution.requests7d":`Requêtes des 7 derniers jours`,"api.attribution.totalRequests":`Total des requêtes attribuées`,"api.attribution.totalRequestsAvailable":`Requêtes dans l’historique disponible`,"api.attribution.sinceAvailable":`Attribution disponible depuis le`,"api.attribution.lastUsed":`Dernière utilisation`,"api.attribution.since":`Attribution disponible depuis le`,"api.attribution.neverUsed":`Non utilisée depuis le début de l’attribution`,"api.attribution.unavailable":`Utilisation indisponible`,"api.attribution.unavailableDetail":`Aucune utilisation n’a encore été attribuée. Les requêtes enregistrées avant le début de l’attribution ne peuvent pas être attribuées rétroactivement.`,"api.attribution.ambiguous":`Deux clés partagent cet identifiant; l’utilisation ne peut donc pas être attribuée à l’une d’elles. Attribuez un identifiant unique à chaque clé dans le fichier de configuration.`,"api.attribution.railAmbiguous":`identifiant en double`,"claude.subtitle":`Utilisez GPT, Gemini et d’autres modèles dans Claude Code.`,"claude.pageTitle":`Claude Code`,"claude.workspace.settings":`Paramètres`,"claude.enabledLabel":`Connexion Claude`,"claude.enabledHint":`Lorsque cette option est désactivée, Claude Code ne peut pas utiliser ce proxy.`,"claude.authMode":`Mode d’authentification`,"claude.authModeHint":`L’abonnement nécessite un compte Claude; le proxy fonctionne sans compte Anthropic`,"claude.authModeSubscription":`Abonnement (compte Claude)`,"claude.authModeProxy":`Proxy (aucun compte requis)`,"claude.authModeAuto":`Automatique (détecter l’authentification Claude)`,"claude.effectiveMode.label":`Effectif au prochain lancement`,"claude.effectiveMode.manual":`Manuel : {mode}`,"claude.effectiveMode.autoPresent":`Automatique : abonnement — authentification Claude trouvée via {source}`,"claude.effectiveMode.autoAbsent":`Automatique : mode proxy — aucune authentification Claude trouvée`,"claude.effectiveMode.autoUnknown":`Automatique : abonnement — impossible de vérifier l’authentification`,"claude.effectiveMode.admissionKey":`La clé API de ce proxy est toujours envoyée.`,"claude.authSource.claude-json-oauth":`Compte Claude`,"claude.authSource.claude-credentials-file":`fichier d’identifiants`,"claude.authSource.macos-keychain":`Trousseau macOS`,"claude.authSource.exported-env":`variable d’environnement`,"claude.authSource.unknown":`un identifiant détecté`,"claude.systemEnv":`Connexion automatique`,"claude.systemEnvDesc":`Lorsque cette option est activée, l’exécution de claude dans n’importe quel terminal passe automatiquement par le proxy.`,"claude.systemEnvUnsupported":`La connexion automatique est disponible uniquement sous macOS. Sur ce système, démarrez Claude avec {cmd}.`,"claude.systemEnvWarn":`⚠ Vous devez quitter complètement votre application de terminal et la relancer pour appliquer ce changement. Non recommandé.`,"claude.fastMode":`Mode rapide (OpenAI)`,"claude.fastModeDesc":`Contrôle service_tier pour les modèles OpenAI. ACTIVÉ = priorité (plus rapide). DÉSACTIVÉ = valeur par défaut. Automatique = transmission directe (le client décide).`,"claude.fastAuto":`Automatique`,"claude.fastOn":`ACTIVÉ`,"claude.fastOff":`DÉSACTIVÉ`,"claude.autoContext":`Utiliser automatiquement le grand contexte`,"claude.autoContextDesc":`Contrôle l’étendue du marquage 1M. ACTIVÉ : tout modèle dont la fenêtre peut accueillir le seuil de compaction obtient une ligne de grand contexte. DÉSACTIVÉ : seuls les véritables modèles 1M en obtiennent une.`,"claude.autoContextInert":`Inactif, car une ancienne valeur de taille de contexte (maxContextTokens) existe dans le fichier de configuration. Supprimez-la dans ce fichier pour réactiver l’option.`,"claude.autoCompactWindow":`Seuil de résumé automatique`,"claude.autoCompactDefault":`{value} (par défaut)`,"claude.autoCompactWindowDesc":`Les anciens messages sont résumés lorsque la conversation atteint ce seuil. Celui-ci ne dépasse jamais la limite propre à chaque modèle; les modèles 200k ne sont donc pas affectés.`,"claude.autoCompactWindowWarn":`La modification de ce paramètre peut perturber les modèles GPT — s’il dépasse la limite réelle d’un modèle, les conversations échoueront avant le déclenchement du résumé.`,"claude.injectAgents":`Enregistrer automatiquement les sous-agents`,"claude.injectAgentsDesc":`Enregistre les modèles choisis dans l’onglet Sous-agents (ainsi que le modèle par défaut actuel) comme agents Claude Code délégables (ocx-*). S’applique à partir de la prochaine session.`,"claude.webSearchSidecar":`Remplacement du service auxiliaire de recherche Web`,"claude.webSearchSidecarHint":`Remplace le service auxiliaire principal de recherche Web pour les requêtes Claude Code.`,"claude.visionSidecar":`Remplacement du service auxiliaire de vision`,"claude.visionSidecarHint":`Remplace le service auxiliaire principal de vision pour les requêtes Claude Code.`,"claude.useMainSetting":`Utiliser le paramètre principal`,"claude.sidecarModelPlaceholder":`Modèle du paramètre principal`,"claude.quickstart":`Commencer`,"claude.quickstartHint":`{cmd} ouvre Claude Code via le proxy. Votre connexion à claude.ai reste active.`,"claude.manualEnv":`Configuration manuelle (avancé)`,"claude.smallFastModel":`Modèle auxiliaire en arrière-plan`,"claude.smallFastModelHint":`Le modèle utilisé par Claude Code pour les tâches en arrière-plan, comme le résumé des conversations et la détection des sujets. L’alias de sous-agent haiku l’utilise également. Vide = valeur par défaut de Claude (Haiku).`,"claude.smallFastModelAccurateHint":`Le modèle utilisé par Claude Code pour les tâches en arrière-plan, comme le résumé des conversations et la détection des sujets. L’alias de sous-agent haiku l’utilise également.`,"claude.smallFastModelUnsetOption":`Laisser Claude Code choisir (modèle natif)`,"claude.smallFastModelNativeWarning":`Lorsque ce champ est vide, OpenCodex ne définit aucun remplacement du modèle auxiliaire. Claude Code peut utiliser son modèle Sonnet natif, ce qui peut entraîner des frais auprès de votre fournisseur natif.`,"claude.slotUnset":`Utiliser la valeur par défaut de Claude`,"claude.modelMap":`Interception de modèles`,"claude.modelMapHint":`Intercepte les requêtes visant un modèle précis et les redirige vers celui que vous choisissez. Vide par défaut — rien ne se produit tant que vous n’ajoutez pas de règle.`,"claude.mapFrom":`Modèle d’origine (p. ex. claude-sonnet-4-5)`,"claude.mapTo":`Remplacer par (p. ex. gemini/gemini-3-pro)`,"claude.addMapping":`Ajouter une règle`,"claude.removeMapping":`Supprimer la règle`,"claude.aliases":`Modèles disponibles`,"claude.aliasesHint":`Modèles affichés dans le menu /model de Claude Code.`,"claude.aliasProviderOther":`Autre`,"claude.loading":`Chargement…`,"claude.loadFail":`Impossible de charger les paramètres de Claude`,"claude.saved":`Enregistré.`,"claude.saveFailed":`Échec de l’enregistrement`,"claude.networkError":`Erreur réseau — le proxy est-il en cours d’exécution?`,"claude.toggleAria":`Activer ou désactiver la connexion Claude`,"claude.none":`Aucun`,"cws.loading":`Chargement des combinaisons…`,"cws.loadFailed":`Impossible de charger les combinaisons.`,"cws.saveFailed":`Impossible d’enregistrer la combinaison.`,"cws.removeFailed":`Impossible de supprimer la combinaison.`,"cws.saved":`Combinaison enregistrée.`,"cws.created":`{model} créé.`,"cws.removed":`combo/{id} supprimé.`,"cws.renamed":`{from} renommé en {to}.`,"cws.add":`Ajouter une combinaison`,"cws.addTitle":`Ajouter une combinaison`,"cws.addSubtitle":`Créez un modèle virtuel couvrant plusieurs fournisseurs et choisissez le nom de modèle exact que les clients demanderont.`,"cws.create":`Créer la combinaison`,"cws.railAria":`Liste des combinaisons`,"cws.searchPlaceholder":`Rechercher des combinaisons ou des cibles…`,"cws.noSearchResults":`Aucune combinaison ne correspond à votre recherche.`,"cws.group.failover":`Basculement`,"cws.group.roundRobin":`Rotation`,"cws.group.other":`Autres stratégies`,"cws.targetCount":`{count} cibles`,"cws.targetCountOne":`1 cible`,"cws.overviewTitle":`Combinaisons`,"cws.overviewBlurb":`Modèles virtuels qui routent entre des cibles fournisseur/modèle par repli, rotation, aléatoire pondéré, moins utilisé ou réinitialisation de quota la plus proche.`,"cws.count.total":`Total`,"cws.count.failover":`Basculement`,"cws.count.roundRobin":`Rotation`,"cws.count.other":`Autres`,"cws.howTitle":`Fonctionnement`,"cws.howBody":`Demandez à Codex le nom de modèle public de la combinaison. Sans nom, combo/ est utilisé par défaut. OpenCodex sélectionne une cible et ne bascule qu’en cas d’échec réessayable en amont. Si aucune cible ne reste disponible, la requête est bloquée au lieu d’utiliser le fournisseur global par défaut.`,"cws.attentionTitle":`Intervention requise`,"cws.attention.empty":`Aucune cible configurée`,"cws.attention.few":`Une seule cible — le basculement n’a aucune autre destination`,"cws.attention.catalogOmitted":`Absent du catalogue de modèles — les capacités des membres sont incomplètes ou incompatibles (fenêtre de contexte ou métadonnées manquantes, ou intersection des modalités vide). Le routage par alias fonctionne toujours`,"cws.attention.allTargetsExhausted":`Toutes les cibles activées ont épuisé leur quota`,"cws.emptyTitle":`Créer votre première combinaison`,"cws.empty.createDesc":`Nommez un modèle virtuel et enchaînez au moins deux services principaux.`,"cws.backToAll":`Retour à toutes les combinaisons`,"cws.capability.imageInputUnavailable":`Indisponible tant que toutes les cibles sélectionnées ne prennent pas en charge les images.`,"cws.capability.imageInputHint":`Activé par défaut lorsque toutes les cibles prennent en charge les images. Désactivez cette option pour n’accepter que du texte.`,"cws.capability.imageInput":`Images / multimodal`,"cws.capability.adaptiveEffort":`Échelle de raisonnement adaptative`,"cws.capability.adaptiveEffortHint":`Désactivé : une cible sans réglage de raisonnement masque le sélecteur pour toute la combinaison. Activé : ces cibles restent utilisables et le sélecteur conserve les niveaux communs aux autres cibles.`,"cws.capabilities":`Capacités`,"cws.allCombos":`Toutes les combinaisons`,"cws.copyModel":`Copier l’identifiant`,"cws.copied":`Copié`,"cws.tabsLabel":`Sections des détails de la combinaison`,"cws.tab.config":`Configuration`,"cws.tab.about":`À propos`,"cws.strategy":`Stratégie`,"cws.strategy.failover":`Basculement`,"cws.strategy.roundRobin":`Rotation`,"cws.strategy.random":`Aléatoire`,"cws.strategy.leastUsed":`Moins utilisé`,"cws.strategy.resetWindow":`Fenêtre de réinitialisation`,"cws.strategy.failoverHint":`Essaie les cibles dans l’ordre. Si la première échoue avec une erreur réessayable (limite de débit, panne, restriction d’abonnement), passe à la suivante.`,"cws.strategy.roundRobinHint":`Répartit le trafic de manière déterministe selon les pondérations. Conserve chaque cible sélectionnée pendant un lot de requêtes réussies, puis passe à la suivante.`,"cws.strategy.randomHint":`Tire une cible éligible par requête, avec des probabilités proportionnelles au poids. Aucune adhérence entre requêtes.`,"cws.strategy.leastUsedHint":`Dirige chaque requête vers la cible éligible ayant le moins de succès enregistrés. Les compteurs redémarrent avec le proxy.`,"cws.strategy.resetWindowHint":`Préfère la cible éligible dont la fenêtre de quota se réinitialise le plus tôt. Sans données de quota, l’ordre de configuration s’applique.`,"cws.field.id":`Identifiant de la combinaison`,"cws.field.idHint":`Les clients demanderont {model}`,"cws.field.idInternalHint":`Identifiant interne de la combinaison. Vous pouvez le modifier après la création.`,"cws.field.idHintEdit":`Le renommage déplace la combinaison vers un nouvel identifiant. Les clients demandent {model}.`,"cws.field.alias":`Nom de modèle public`,"cws.field.aliasPlaceholder":`deepseek-v4-flash ou vendor/model`,"cws.field.aliasHint":`Facultatif. Utilisez un nom simple sans préfixe, un préfixe personnalisé comme vendor/model, ou laissez le champ vide pour utiliser combo/.`,"cws.field.nativeAlias":`Alias OpenAI natif`,"cws.field.nativeAliasHint":`Permet à cette combinaison de prendre en charge un identifiant de modèle OpenAI natif non qualifié compatible. Les routes OpenAI qualifiées par compte ou fournisseur restent distinctes.`,"cws.field.displayName":`Nom d’affichage`,"cws.field.displayNameHint":`Libellé de cette combinaison dans le sélecteur. Requis lorsque l’alias OpenAI natif est activé.`,"cws.field.stickyLimit":`Réussites persistantes avant rotation`,"cws.field.stickyLimitHint":`Conserve la cible sélectionnée pendant ce nombre de requêtes réussies avant que le sélecteur pondéré passe à la suivante.`,"cws.field.defaultEffort":`Raisonnement par défaut`,"cws.field.defaultEffortNone":`Aucun (valeur par défaut de la cible)`,"cws.field.defaultEffortHint":`Utilisé uniquement lorsque le client omet l’effort de raisonnement. Les options correspondent à l’intersection des efforts annoncés par les cibles sélectionnées; les cibles sans métadonnées d’effort dans le catalogue n’en proposent aucun.`,"cws.field.defaultEffortUnsupported":`Cet effort ne figure pas dans l’échelle commune des cibles — il sera ignoré ou ajusté lors de la requête.`,"cws.field.defaultEffortUnsupportedOption":`absent de l’intersection`,"cws.targets":`Cibles`,"cws.targets.failoverHint":`L’ordre est important — la première est la cible principale.`,"cws.targets.roundRobinHint":`Les pondérations contrôlent la sélection relative déterministe; l’ordre départage les égalités dans l’anneau de rotation.`,"cws.targets.randomHint":`Les pondérations contrôlent les chances de chaque tirage ; l’ordre n’a pas d’importance.`,"cws.targets.leastUsedHint":`L’ordre ne départage que les cibles également utilisées.`,"cws.targets.resetWindowHint":`L’ordre s’applique quand les données de quota manquent ou sont égales.`,"cws.target.provider":`Fournisseur`,"cws.target.model":`Modèle`,"cws.target.weight":`Pondération`,"cws.target.pickProvider":`Sélectionner un fournisseur…`,"cws.target.pickProviderFirst":`Sélectionner d’abord un fournisseur…`,"cws.target.pickModel":`Sélectionner un modèle…`,"cws.target.noModels":`Aucun modèle pour ce fournisseur`,"cws.target.modelPlaceholder":`identifiant du modèle`,"cws.target.add":`Ajouter une cible`,"cws.target.drag":`Faire glisser pour réorganiser`,"cws.target.moveUp":`Monter`,"cws.target.moveDown":`Descendre`,"cws.quota.available":`Disponible`,"cws.quota.exhausted":`Quota épuisé`,"cws.quota.unknown":`Quota inconnu`,"cws.quota.allExhausted":`Toutes les cibles activées ont épuisé leur quota. Choisissez une autre cible ou attendez le rétablissement du quota.`,"cws.aboutTitle":`Exécution`,"cws.aboutBody":`Les cibles en échec sont temporairement mises en attente conformément à Retry-After. Les erreurs de contexte ou de validité ne provoquent aucun basculement. Chaque cible adapte l’effort à ses propres capacités ; les combinaisons épuisées bloquent les requêtes. Les journaux et la section Utilisation conservent les tentatives physiques ordonnées et l’utilisation de chaque tentative.`,"cws.removeConfirmTitle":`Supprimer {model}?`,"cws.removeConfirmDesc":`Cette action supprime le modèle virtuel de la configuration et du catalogue Codex. Elle ne supprime aucun fournisseur.`,"cws.unsavedTitle":`Modifications non enregistrées`,"cws.unsavedDesc":`Abandonner les modifications de cette combinaison et continuer ?`,"cws.keepEditing":`Continuer la modification`,"cws.err.missingId":`L’identifiant de la combinaison est requis.`,"cws.err.invalidId":`L’identifiant doit commencer par une lettre ou un chiffre et contenir uniquement des lettres, des chiffres, des points, des traits de soulignement ou des traits d’union (64 caractères au maximum).`,"cws.err.duplicateId":`Une combinaison portant cet identifiant existe déjà.`,"cws.err.invalidAlias":`L’alias doit contenir des lettres, des chiffres, des points, des traits de soulignement ou des traits d’union, avec au plus un segment « / ».`,"cws.err.aliasReservedNamespace":`L’alias ne doit pas utiliser l’espace de noms réservé « combo/ ».`,"cws.err.aliasNativeFamily":`Les alias simples de la famille native OpenAI (gpt-*, o1-*, o3-*, o4-*, codex-*) ne sont pas autorisés.`,"cws.err.unsupportedNativeAlias":`L’alias natif doit être un identifiant simple de modèle OpenAI actuellement pris en charge.`,"cws.err.missingNativeAliasDisplayName":`Un nom d’affichage est requis pour les alias natifs.`,"cws.err.invalidDisplayName":`Le nom d’affichage doit comporter au plus 128 caractères et ne contenir aucun caractère de contrôle.`,"cws.err.duplicateAlias":`Une autre combinaison utilise déjà cet alias.`,"cws.err.noTargets":`Ajoutez au moins une cible.`,"cws.err.incompleteTarget":`Chaque cible nécessite un fournisseur et un modèle.`,"cws.target.disabled":`{name} (désactivé)`,"cws.err.reservedNamespace":`Un fournisseur physique nommé « combo » doit être renommé avant la création de combinaisons.`,"cws.err.providerCollision":`L’identifiant de la combinaison entre en conflit avec le nom d’un fournisseur configuré.`,"cws.err.unknownProvider":`Chaque cible doit utiliser un fournisseur configuré.`,"cws.err.duplicateTarget":`Une même cible fournisseur/modèle ne peut apparaître qu’une seule fois.`,"cws.err.invalidStickyLimit":`Le nombre de réussites persistantes doit être un entier compris entre 1 et 100.`,"cws.err.invalidWeight":`Chaque pondération de rotation doit être un entier compris entre 1 et 10000.`,"cws.err.noEnabledTarget":`Au moins une cible doit utiliser un fournisseur activé.`,"claude.tabsLabel":`Client Claude`,"claude.tabCode":`Code`,"claude.tabDesktop":`Desktop`,"claudeDesktop.title":`Claude Desktop`,"claudeDesktop.subtitle":`Acheminez chaque famille de modèles Claude vers un modèle disponible sur le port {port}.`,"claudeDesktop.importJson":`Importer le JSON`,"claudeDesktop.exportJson":`Exporter le JSON`,"claudeDesktop.loading":`Chargement du profil Claude Desktop…`,"claudeDesktop.loadFail":`Impossible de charger le profil Claude Desktop.`,"claudeDesktop.retry":`Réessayer`,"claudeDesktop.saveFailed":`Impossible d’enregistrer le profil Claude Desktop.`,"claudeDesktop.applyFailed":`Le profil a été enregistré, mais n’a pas pu être appliqué.`,"claudeDesktop.updateFailed":`Échec de la mise à jour de Claude Desktop.`,"claudeDesktop.savedApplied":`Profil enregistré et appliqué à Claude Desktop.`,"claudeDesktop.appliedMarkerUnsaved":`Appliqué à Claude Desktop, mais le marqueur d’application n’a pas été enregistré — l’état enregistré/appliqué ci-dessous peut être obsolète jusqu’à la prochaine application.`,"claudeDesktop.savedAppliedAnnounce":`Profil Claude Desktop enregistré et appliqué.`,"claudeDesktop.saved":`Profil enregistré.`,"claudeDesktop.savedAnnounce":`Profil Claude Desktop enregistré.`,"claudeDesktop.exported":`Profil exporté au format JSON.`,"claudeDesktop.importExpected":`Un profil Claude Desktop de version 1 était attendu.`,"claudeDesktop.importReady":`JSON importé. Vérifiez le brouillon, puis enregistrez-le et appliquez-le.`,"claudeDesktop.importedAnnounce":`JSON du profil importé. Les modifications non enregistrées sont prêtes à être vérifiées.`,"claudeDesktop.importInvalid":`Le fichier sélectionné n’est pas un profil valide.`,"claudeDesktop.importFailed":`Échec de l’importation. {error}`,"claudeDesktop.moved":`{route} déplacé vers {family}.`,"claudeDesktop.unsaved":`Modifications non enregistrées`,"claudeDesktop.upToDate":`Le profil est à jour`,"claudeDesktop.saving":`Enregistrement…`,"claudeDesktop.applying":`Application…`,"claudeDesktop.saveApply":`Enregistrer et appliquer`,"claudeDesktop.emptyTitle":`Aucun modèle disponible`,"claudeDesktop.emptyHint":`Ajoutez ou activez un fournisseur, puis revenez attribuer les routes Claude Desktop.`,"claudeDesktop.assignmentsLabel":`Attributions des familles de modèles Claude`,"claudeDesktop.family.opus":`Opus`,"claudeDesktop.family.fable":`Fable`,"claudeDesktop.family.sonnet":`Sonnet`,"claudeDesktop.family.haiku":`Haiku`,"claudeDesktop.modelCountOne":`{count} modèle`,"claudeDesktop.modelCountMany":`{count} modèles`,"claudeDesktop.chooseDefault":`Choisir une valeur par défaut`,"claudeDesktop.temporaryDefault":`Valeur par défaut temporaire`,"claudeDesktop.laneEmpty":`Déposez un modèle ici ou utilisez sa commande Déplacer.`,"claudeDesktop.laneNoMatch":`Aucun modèle de cette famille ne correspond à votre recherche.`,"nav.grok":`Grok`,"grok.title":`Grok Build`,"grok.subtitle":`Modèles qu’opencodex a enregistrés dans votre configuration Grok.`,"grok.loading":`Chargement de l’état de Grok…`,"grok.loadFail":`Impossible de lire la configuration Grok.`,"grok.notConfiguredTitle":`Grok Build n’est pas configuré`,"grok.notConfiguredHint":`Démarrez ou redémarrez le proxy avec Grok installé; opencodex écrit alors un bloc géré dans :`,"grok.endpoint":`Point de terminaison`,"grok.colModel":`Modèle`,"grok.colAlias":`Alias Grok`,"grok.colContext":`Contexte`,"grok.groupNative":`Modèles natifs`,"grok.groupRouted":`Modèles acheminés`,"grok.enabledCount":`{on} sur {total} enregistrés`,"grok.saved":`Sélection enregistrée.`,"grok.savedApplied":`Sélection enregistrée et écrite dans votre configuration Grok.`,"grok.saveFailed":`Impossible d’enregistrer la sélection Grok.`,"grok.applyFailed":`Sélection enregistrée, mais impossible de mettre à jour la configuration Grok.`,"grok.applySkipped":`Sélection enregistrée. La configuration Grok n’a pas été modifiée.`,"grok.saveApply":`Enregistrer et appliquer`,"grok.saving":`Enregistrement…`,"grok.applying":`Application…`,"grok.unsaved":`Modifications non enregistrées`,"grok.upToDate":`La sélection est à jour`,"grok.toggleModel":`Enregistrer {id} auprès de Grok`,"claudeDesktop.available":`Disponible`,"claudeDesktop.defaultBadge":`Par défaut`,"claudeDesktop.supports1m":`1M`,"claudeDesktop.unavailable":`Indisponible`,"claudeDesktop.contextM":`Contexte de {n}M`,"claudeDesktop.contextK":`Contexte de {n}k`,"claudeDesktop.contextUnknown":`contexte inconnu`,"claudeDesktop.alias":`Alias`,"claudeDesktop.useAsDefault":`Utiliser par défaut pour {family}`,"claudeDesktop.moveTo":`Déplacer vers`,"claudeDesktop.move":`Déplacer`,"claudeDesktop.status.applied":`Appliqué à Desktop`,"claudeDesktop.status.stale":`Configuration obsolète — appliquer de nouveau`,"claudeDesktop.status.notApplied":`Non appliqué`,"claudeDesktop.status.notActiveProfile":`Desktop utilise un autre profil — appliquer de nouveau`,"claudeDesktop.status.disabled":`L’intégration Claude Desktop est désactivée. Quittez complètement Desktop et rouvrez-le après l’avoir activée.`,"claudeDesktop.enableApply":`Activer et appliquer`,"claudeDesktop.health.lastRequest":`Dernière requête`,"claudeDesktop.health.stats":`{count} req. / {errors} err.`,"claudeDesktop.effort.supported":`effort`,"claudeDesktop.effort.displayOnly":`effort (affichage uniquement)`,"lab.title":`Laboratoire de compatibilité`,"lab.subtitle":`Matrice en lecture seule des verdicts de compatibilité issus des preuves de projection du laboratoire.`,"lab.loadFailed":`Impossible de charger les données du laboratoire de compatibilité`,"lab.projectionUnavailable":`La projection du laboratoire n’est pas disponible. Exécutez d’abord les tests de conformité ou les sondes en direct.`,"lab.projectionIncompatible":`Le schéma de projection du laboratoire est incompatible. Régénérez la projection.`,"lab.statusTitle":`État de la projection`,"lab.matrixTitle":`Matrice de compatibilité`,"lab.verdictsTitle":`Enregistrements de verdicts`,"lab.filter.layer":`Couche de preuve`,"lab.filter.verdict":`Verdict`,"lab.filter.subject":`Identifiant du sujet`,"lab.filter.all":`Tous`,"lab.col.subject":`Sujet`,"lab.col.layer":`Couche`,"lab.col.suite":`Suite`,"lab.col.verdict":`Verdict`,"lab.col.asOf":`En date du`,"lab.col.protocol":`Conformité du protocole`,"lab.col.live":`Compatibilité des routes en direct`,"lab.col.task":`Efficacité des tâches`,"lab.empty":`Aucun verdict de compatibilité dans la projection pour le moment.`,"lab.subjectKind":`Type`,"lab.observationCount":`Observations`,"lab.eventCount":`Événements`,"lab.verdictCount":`Verdicts`,"lab.subjectCount":`Sujets`,"lab.builtAt":`Générée le`,"lab.loading":`Chargement des preuves de compatibilité…`,"lab.loadMore":`Charger davantage`,"lab.detailTitle":`Détails du verdict`,"lab.detailClose":`Fermer`,"lab.detailSubject":`Sujet`,"lab.detailObservations":`Observations`,"lab.detailEvents":`Événements contributifs`,"lab.detailArtifacts":`Métadonnées des artefacts`,"lab.production.title":`Trafic de production observé`,"lab.production.notVerification":`Ne constitue pas une vérification du laboratoire`,"lab.production.attempts":`Tentatives`,"lab.production.successes":`Réussites`,"lab.production.routeErrors":`Erreurs de routage`,"lab.production.lastObserved":`Dernière observation`,"lab.detailLoadFailed":`Impossible de charger les détails du verdict`,"lab.refresh":`Actualiser`,"lab.verdict.UNKNOWN":`Inconnu`,"lab.verdict.CLAIMED":`Déclaré`,"lab.verdict.PROBED":`Sondé`,"lab.verdict.VERIFIED":`Vérifié`,"lab.verdict.DEGRADED":`Dégradé`,"lab.verdict.BLOCKED":`Bloqué`,"lab.verdict.UNSUPPORTED":`Non pris en charge`,"lab.layer.protocol_conformance":`Conformité du protocole`,"lab.layer.live_route_compatibility":`Compatibilité des routes en direct`,"lab.layer.task_effectiveness":`Efficacité des tâches`,"models.newPolicyGlobal":`Désactiver les nouveaux modèles par défaut`,"models.newPolicyProvider":`Politique des nouveaux modèles`,"models.newPolicy_inherit":`Hériter`,"models.newPolicy_off":`Désactivé`,"models.newPolicy_on":`Activé`,"models.newBadge":`NOUVEAU`,"models.newCount":`{count} nouveaux, désactivés`,"models.aliases":`Alias`,"models.aliasesTable":`Table des alias`,"models.aliasPrompt":`Alias du fournisseur (laisser vide pour effacer)`,"models.modelAliasPrompt":`Alias du modèle (laisser vide pour effacer)`,"models.aliasSaved":`Alias enregistré`,"models.aliasConflict":`Cet alias entre en conflit avec un nom existant`,"models.editProviderAlias":`Modifier l'alias du fournisseur`,"models.editModelAlias":`Modifier l'alias du modèle`,"models.useDefaultAliases":`Utiliser les alias par défaut`,"models.useDefaultAliasesGlobal":`Utiliser les alias par défaut partout`,"models.aliasAuto":`auto`,"models.aliasUser":`utilisateur`,"models.aliasStale":`obsolète`,"connection.discovering":`Détection des cibles locale et partagée…`,"connection.machineUnavailable":`Le plan machine local est indisponible. Les requêtes partagées n'ont pas été redirigées localement.`,"connection.disconnect":`Déconnecter du hub`,"connection.disconnectConfirm":`Déconnecter cette machine du hub et la redémarrer en mode autonome ?`,"connection.pairing.title":`Connecter ce tableau de bord au hub`,"connection.pairing.body":`Collez le code d'association à usage unique créé sur le hub.`,"connection.pairing.relayWarning":`Ce code passe par le relais fixe du hub. Le relais ne peut pas viser un autre hôte.`,"connection.pairing.code":`Code d'association à usage unique`,"connection.pairing.submit":`Connecter`,"connection.pairing.submitting":`Connexion…`,"connection.pairing.error":`Le code a été refusé ou a expiré. Il reste saisi pour vérification.`,"connection.machine.title":`Cette machine`,"connection.machine.shimHealthy":`Le shim Codex est opérationnel.`,"connection.machine.shimNeedsAttention":`Le shim Codex nécessite une intervention.`,"connection.machine.repairShim":`Réparer le shim`,"connection.machine.removeShim":`Supprimer le shim`,"connection.clients.title":`Clients connectés`,"connection.clients.none":`Aucun état client disponible`,"connection.clients.sync":`Synchroniser`,"connection.clients.syncing":`Synchronisation…`,"connection.sessionLogout":`Se déconnecter de la session distante`,"connection.sessionLoggingOut":`Déconnexion de la session distante…`,"connection.sessionLogoutFailed":`Impossible de fermer la session distante. La session actuelle a été conservée.`,"usage.source.connected":`Source : utilisation du hub`,"usage.source.local":`Source : usage.jsonl local`,"usage.scope.label":`Portée de l'utilisation`,"usage.scope.machine":`Cette machine`,"usage.scope.hub":`Tout le hub`,"usage.hubOffline":`L'utilisation du hub est indisponible. Les données locales n'ont pas été substituées.`,"integrations.tab.cursor":`Cursor`,"integrations.detail.cursorSeen":`Cursor a récemment envoyé une requête à ce proxy`,"integrations.detail.cursorNeverSeen":`Cursor Private Inference est installé ; aucune requête reçue pour le moment`,"integrations.detail.cursorAbsent":`Cursor Private Inference introuvable`,"integrations.cursor.title":`Cursor`,"integrations.cursor.intro":`Cursor Private Inference exécute son agent localement et communique avec opencodex via l’adresse de bouclage. La version standard de Cursor ne le peut pas : les serveurs de Cursor appellent le point de terminaison personnalisé, qui doit donc être accessible via une URL HTTPS publique. Cette page n’écrit jamais dans Cursor ; collez vous-même les valeurs ci-dessous dans Cursor.`,"integrations.cursor.loading":`Lecture de l’état de Cursor…`,"integrations.cursor.unavailable":`Impossible de lire l’état de Cursor depuis le proxy.`,"integrations.cursor.detection":`Versions installées`,"integrations.cursor.privateInference":`Cursor Private Inference`,"integrations.cursor.regular":`Cursor (version standard)`,"integrations.cursor.detected":`Détecté`,"integrations.cursor.notFound":`Introuvable`,"integrations.cursor.regularOnly":`Seule la version standard de Cursor a été trouvée. Ses requêtes vers les points de terminaison personnalisés passent par les serveurs de Cursor ; un proxy sur l’adresse de bouclage reste donc inaccessible sans tunnel public. Consultez le guide de Cursor Private Inference.`,"integrations.cursor.nothingFound":`Aucune installation de Cursor n’a été trouvée aux emplacements habituels. Si Cursor est installé ailleurs, les valeurs ci-dessous restent valables.`,"integrations.cursor.gateway":`Valeurs de la passerelle`,"integrations.cursor.gatewayHint":`Dans Cursor Private Inference, ouvrez Settings > Models > Gateway, collez ces deux valeurs, puis cliquez sur Refresh model list.`,"integrations.cursor.baseUrl":`Base URL`,"integrations.cursor.apiKey":`Clé API`,"integrations.cursor.apiKeyCredential":`L’une de vos clés API opencodex (cette liaison nécessite une authentification)`,"integrations.cursor.copy":`Copier`,"integrations.cursor.copied":`Copié`,"integrations.cursor.connection":`Connexion`,"integrations.cursor.seen":`Dernière requête de Cursor : {time} ({ua})`,"integrations.cursor.neverSeen":`Aucune requête de Cursor depuis le démarrage du proxy. Après avoir enregistré la passerelle, cliquez sur Refresh model list dans Cursor.`,"integrations.cursor.models":`Ce que Cursor affichera`,"integrations.cursor.modelsHint":`Cursor sélectionne le niveau de raisonnement dans sa propre table de modèles ; opencodex ne peut donc que le prévoir. La colonne Contexte indique la fenêtre par défaut et celle disponible en option (le Max Mode de Cursor).`,"integrations.cursor.ladderFromBundle":`Les niveaux de raisonnement sont lus dans le bundle Cursor Private Inference {version} installé. Cursor les décide ; opencodex ne fait que rapporter sa table.`,"integrations.cursor.ladderFromStatic":`Les niveaux de raisonnement sont un miroir statique de Cursor 3.18.25 (aucun bundle Private Inference lisible trouvé). La colonne Contexte indique la fenêtre par défaut et la fenêtre optionnelle.`,"integrations.cursor.unknownVersion":`version inconnue`,"integrations.cursor.noControl":`—`,"integrations.cursor.singleWindow":`fenêtre unique`,"integrations.cursor.noControlTitle":`Cet identifiant n'est pas dans la table d'effort intégrée de Cursor, donc Cursor n'affiche aucun contrôle de raisonnement.`,"integrations.cursor.effortRowsOne":`1 ligne d'effort publiée`,"integrations.cursor.effortRowsMany":`{n} lignes d'effort publiées`,"integrations.cursor.effortRowsOff":`aucune ligne d'effort`,"integrations.cursor.tableLessHint":`Les lignes marquées — n'ont pas de contrôle de raisonnement dans Cursor. Activez cursorEffortRows pour publier une entrée du sélecteur par effort (id--effort), ou définissez modelDefaultReasoningEfforts sur le fournisseur pour une valeur fixe.`,"integrations.cursor.colModel":`Modèle`,"integrations.cursor.colReasoning":`Raisonnement`,"integrations.cursor.colContext":`Contexte`,"integrations.cursor.guide":`Ouvrir le guide de Cursor Private Inference`},Ve={"nav.dashboard":`대시보드`,"uptime.day":`일`,"uptime.hour":`시간`,"uptime.minute":`분`,"uptime.second":`초`,"nav.startup":`시작 안전성`,"nav.providers":`프로바이더`,"nav.models":`모델`,"nav.combos":`콤보`,"nav.subagents":`서브에이전트`,"routing.title":`라우팅 인텔리전스 (beta)`,"routing.subtitle":`정책 프로필, 드라이런 평가, 소스 기반 라우팅 분석.`,"routing.loadFailed":`라우팅 데이터를 불러오지 못했습니다`,"routing.empty":"라우팅 프로필이 구성되지 않았습니다. config.json에 `routingProfiles`를 추가하세요.","routing.revision":`rev`,"routing.detail":`프로필`,"routing.createProfile":`프로필 만들기`,"routing.dryRunError":`드라이런 실패 (HTTP {status})`,"routing.removeConfirm":`프로필 {id}을(를) 제거할까요?`,"routing.unknownEvidence.allow":`허용`,"routing.unknownEvidence.penalize":`불이익`,"routing.unknownEvidence.exclude":`제외`,"routing.removeCandidate":`후보 {provider}/{model} 제거`,"routing.candidates":`후보`,"routing.require":`필수 요구사항`,"routing.optimize":`최적화 가중치`,"routing.limits":`제한`,"routing.unknownEvidence":`알 수 없는 증거 정책`,"routing.compatibility.title":`호환성 정책`,"routing.compatibility.enabled":`Compatibility Lab 증거 필요`,"routing.compatibility.requiredSuites":`필수 스위트`,"routing.compatibility.loadingCatalog":`Lab 카탈로그 로드 중…`,"routing.compatibility.catalogUnavailable":`Lab 카탈로그를 사용할 수 없습니다 — config.json에서 스위트 ID를 수동으로 입력하세요.`,"routing.compatibility.layer.protocol_conformance":`프로토콜 적합성`,"routing.compatibility.layer.live_route_compatibility":`라이브 라우트 호환성`,"routing.compatibility.minStatus":`최소 호환성 상태`,"routing.none":`없음`,"routing.unavailable":`–`,"routing.dryRun":`드라이런 평가`,"routing.dryRunContext":`요청 컨텍스트 창(토큰)`,"routing.dryRunTools":`요청에 도구 필요`,"routing.dryRunImage":`요청에 이미지 입력 필요`,"routing.dryRunStructured":`요청에 구조화된 출력 필요`,"routing.dryRunRun":`후보 평가`,"routing.candidate":`후보`,"routing.eligible":`적격`,"routing.exclusions":`제외`,"routing.costCap":`비용 상한`,"routing.capOutcome.satisfied":`한도 이내`,"routing.capOutcome.exceeded":`한도 초과`,"routing.capOutcome.unknown-allowed":`알 수 없음(허용)`,"routing.capOutcome.unknown-excluded":`알 수 없음(제외)`,"routing.exclusion.capability-unsatisfied":`기능 미충족`,"routing.exclusion.unknown-capability":`알 수 없는 기능`,"routing.exclusion.cost-limit":`비용 상한 초과`,"routing.exclusion.cost-limit-unknown":`상한 이하 비용 불명`,"routing.exclusion.cooldown":`쿨다운`,"routing.exclusion.unknown-health":`상태 불명`,"routing.exclusion.unknown-quota":`할당량 불명`,"routing.exclusion.unknown-price":`가격 불명`,"routing.exclusion.other":`제외: {code}`,"routing.score":`점수`,"routing.selected":`선택됨`,"routing.yes":`예`,"routing.no":`아니요`,"routing.analytics":`라우팅 분석`,"routing.analyticsTotal":`요청`,"routing.analyticsSuccessRate":`성공`,"routing.analyticsFallbackRate":`폴백`,"routing.analyticsP50":`p50`,"routing.analyticsP95":`p95`,"routing.analyticsP99":`p99`,"routing.analyticsCooldown":`쿨다운 실패`,"routing.analyticsConfidence":`신뢰도`,"routing.analyticsTruncated":`잘린 기록`,"routing.analyticsRequests":`요청`,"routing.analyticsEmpty":`분석이 아직 없습니다. 먼저 요청을 보내세요.`,"nav.logs":`로그&디버그`,"nav.usage":`사용량`,"common.github":`GitHub`,"sidebar.star":`GitHub에서 스타 누르기`,"sidebar.starred":`GitHub 스타 완료`,"sidebar.starUnauthenticated":`GitHub에서 스타 누르기 (gh CLI 로그인 안 됨)`,"sidebar.starFailed":`gh로 스타를 누르지 못했습니다. GitHub를 대신 엽니다.`,"sidebar.updateAvailable":`업데이트 있음: {version}`,"sidebar.checkUpdate":`업데이트 확인`,"common.save":`저장`,"common.saving":`저장 중…`,"common.cancel":`취소`,"common.discard":`버리기`,"common.delete":`삭제`,"common.remove":`삭제`,"common.loading":`불러오는 중…`,"common.retry":`재시도`,"auth.adminTokenTitle":`OpenCodex 관리자 토큰 (OPENCODEX_ADMIN_AUTH_TOKEN)`,"auth.adminAccountLabel":`계정`,"auth.adminTokenFieldLabel":`관리자 토큰`,"auth.adminTokenRejected":`관리자 토큰이 거부되었습니다. 확인한 후 다시 시도하세요.`,"auth.adminTokenUnavailable":`관리자 토큰을 확인할 수 없습니다. 다시 시도하세요.`,"theme.label":`테마`,"theme.light":`라이트`,"theme.dark":`다크`,"theme.system":`시스템`,"lang.label":`언어`,"lang.nativeName":`한국어`,"provider.name.commandCodeAuth":`Command Code - Auth`,"provider.name.commandCodeApi":`Command Code - API`,"provider.name.volcengine":`Volcengine Ark`,"provider.name.volcengineCodingPlan":`Volcengine Ark 코딩 플랜`,"provider.name.volcengineAgentPlan":`Volcengine Ark 에이전트 플랜`,"errorBoundary.title":`페이지를 불러오지 못했습니다`,"errorBoundary.message":`이 섹션을 렌더링하는 중 오류가 발생했습니다. 다시 불러와 재시도하세요.`,"errorBoundary.details":`오류`,"errorBoundary.reload":`다시 불러오기`,"startup.title":`시작 안전성`,"startup.subtitle":`재부팅 후 로컬 프록시 라우팅이 재연결 반복으로 이어지기 전에 Codex가 opencodex에 연결될 수 있는지 확인합니다.`,"startup.refresh":`새로고침`,"startup.backToDashboard":`대시보드로 돌아가기`,"startup.loading":`시작 보호 상태 확인 중…`,"startup.error":`시작 보호 상태를 읽지 못했습니다.`,"startup.staleData":`최신 시작 상태 확인에 실패했습니다. 아래 값은 이전 결과이며 보호 증거로 사용하면 안 됩니다.`,"startup.status.native":`네이티브 라우팅`,"startup.status.protected":`재부팅 보호됨`,"startup.status.atRisk":`조치 필요`,"startup.summary.native":`Codex가 로컬 프록시에 의존하지 않습니다`,"startup.summary.protected":`재부팅 후에도 opencodex가 자동으로 준비됩니다`,"startup.summary.atRisk":`재부팅 후 Codex 모델 연결이 끊길 수 있습니다`,"startup.riskDetail":`Codex는 로컬 프록시를 바라보지만 이를 다시 시작할 영구 서비스나 정상 launcher shim이 없습니다.`,"startup.riskDetailCustomLocal":`Codex가 사용자 지정 로컬 게이트웨이를 바라봅니다. opencodex는 해당 게이트웨이의 재시작 수명주기를 관리하거나 검증할 수 없습니다.`,"startup.riskDetailWindowsShim":`Launcher shim은 지원되는 CLI 스크립트만 보호하며 Windows의 Codex Desktop과 직접 codex.exe 실행은 이를 우회할 수 있습니다.`,"startup.safeDetail":`현재 라우팅과 시작 방식이 일치합니다. 재부팅 후 ocx start를 수동으로 실행할 필요가 없습니다.`,"startup.routing":`Codex 라우팅`,"startup.routing.proxy":`로컬 프록시`,"startup.routing.native":`OpenAI 네이티브`,"startup.routing.customLocal":`사용자 지정 로컬 게이트웨이`,"startup.routing.customRemote":`사용자 지정 원격 게이트웨이`,"startup.routing.unknown":`알 수 없거나 잘못된 라우팅`,"startup.restartProtection":`재부팅 보호`,"startup.preference":`필요 시 자동 시작`,"startup.enabled":`켜짐`,"startup.disabled":`꺼짐`,"startup.protection.service":`백그라운드 서비스`,"startup.protection.shim":`Launcher shim`,"startup.protection.none":`설치되지 않음`,"startup.details":`보호 상태 상세`,"startup.service":`백그라운드 서비스`,"startup.serviceHint":`로그인할 때 시작하고 프록시가 중단되면 다시 실행합니다.`,"startup.installed":`설치됨`,"startup.notInstalled":`설치되지 않음`,"startup.unsupported":`지원되지 않음`,"startup.shim":`Codex launcher shim`,"startup.shimHint":`지원되는 Codex 스크립트 런처가 시작될 때 ocx ensure를 실행합니다.`,"startup.healthy":`정상`,"startup.cliOnly":`CLI 전용`,"startup.stale":`업데이트 필요`,"startup.viable":`사용 가능`,"startup.unhealthy":`설치됐지만 비정상`,"startup.conflict":`서비스 충돌`,"startup.installedDisabled":`설치됐지만 꺼짐`,"startup.install":`설치하기`,"startup.installing":`설치 중…`,"startup.repair":`복구`,"startup.repairing":`복구 중…`,"startup.serviceInstalled":`백그라운드 서비스를 설치했습니다.`,"startup.serviceRepaired":`백그라운드 서비스를 복구했습니다.`,"startup.shimInstalled":`Codex launcher shim을 설치했습니다.`,"startup.shimRepaired":`Codex launcher shim을 복구했습니다.`,"startup.installFailed":`설치하지 못했습니다:`,"startup.tray.title":`Windows 시스템 트레이`,"startup.tray.hint":`로그인할 때 트레이 아이콘을 띄우고 프록시 시작·중지·재시작·대시보드·상태를 클릭으로 제어합니다.`,"startup.tray.login":`Windows 로그인 시 트레이 시작`,"startup.tray.notProtection":`트레이는 제어 화면이며 재부팅 보호 서비스가 아닙니다. 무인 복구에는 정상 백그라운드 서비스가 별도로 필요합니다.`,"startup.tray.running":`실행 중`,"startup.tray.stopped":`설치됨, 숨김`,"startup.tray.stale":`복구 필요`,"startup.tray.notInstalled":`설치되지 않음`,"startup.tray.loading":`확인 중…`,"startup.tray.unavailable":`상태 확인 불가`,"startup.tray.install":`트레이 설치 및 표시`,"startup.tray.start":`트레이 아이콘 표시`,"startup.tray.stop":`트레이 아이콘 종료`,"startup.tray.uninstall":`로그인 트레이 제거`,"startup.tray.error":`Windows 트레이 작업에 실패했습니다. ocx tray status에서 상세 내용을 확인하세요.`,"startup.recovery":`복구 방법`,"startup.recoveryHint":`위의 원클릭 설치를 사용하거나 수동 복구 명령을 복사할 수 있습니다. Codex Desktop과 Windows 실행 파일에는 백그라운드 서비스를 권장합니다.`,"startup.command.service":`권장: 영구 백그라운드 서비스`,"startup.command.shim":`대안: CLI launcher shim`,"startup.command.native":`안전 전환: Codex 네이티브 라우팅 복구`,"startup.copy":`복사`,"startup.copied":`복사됨`,"startup.recommended":`권장 복구 명령: {cmd}`,"startup.navRisk":`시작 보호 상태에 조치가 필요합니다`,"startup.codexRuntime.clampHidden":`OpenCodex가 Codex {version}을(를) 사용해 일부 reasoning effort 옵션이 숨겨졌습니다.`,"startup.codexRuntime.clampHiddenWithEfforts":`OpenCodex가 Codex {version}을(를) 사용해 일부 reasoning effort 옵션이 숨겨졌습니다(제거됨: {efforts}).`,"startup.codexRuntime.olderBinary":`OpenCodex가 더 오래된 Codex 바이너리({version})를 사용 중입니다. 더 새 설치를 사용할 수 있습니다.`,"dash.subtitle":`로컬 opencodex 프록시와 프로바이더, 그리고 Codex로 라우팅되는 모델의 실시간 상태입니다.`,"dash.workspace.overview":`개요`,"dash.workspace.sections":`섹션`,"dash.status":`상태`,"dash.online":`온라인`,"dash.offline":`오프라인`,"dash.version":`버전`,"dash.uptime":`가동 시간`,"dash.providers":`프로바이더`,"dash.tokens30d":`토큰 (30일)`,"dash.coverage":`커버리지 {pct}`,"dash.mem.title":`메모리 관찰`,"dash.mem.hint":`읽기 전용 런타임 진단. 관측 메모리는 max(RSS, external, ArrayBuffers)라 Windows working set trimming이 커밋된 보존 메모리를 숨기지 못합니다.`,"dash.mem.rss":`상주 메모리 (RSS)`,"dash.mem.jsHeap":`JS 힙 사용량`,"dash.mem.jsHeapArena":`아레나 {total}`,"dash.mem.pressure":`경고 임계값 대비`,"dash.mem.pressureOf":`임계값의 {pct}%`,"dash.mem.pressureUnknown":`임계값 정보 없음`,"dash.mem.jscHeap":`JSC 힙`,"dash.mem.external":`External`,"dash.mem.arrayBuffers":`ArrayBuffers`,"dash.mem.observed":`관측값`,"dash.mem.runtime":`런타임 카운터`,"dash.mem.growth":`시간당 관측 변화`,"dash.mem.perHour":`/시간`,"dash.mem.store":`연속 응답 저장소`,"dash.mem.storeHint":`프록시 previous_response_id 캐시. 힙이 증가하는 가운데 총 바이트가 늘면 런타임 할당기보다 대화 보존을 가리킵니다.`,"dash.mem.storeEntries":`항목`,"dash.mem.storeTotal":`합계`,"dash.mem.storeLargest":`최대`,"dash.mem.storeOldest":`가장 오래됨`,"dash.mem.threshold":`경고 임계값`,"dash.mem.lastWarn":`마지막 경고`,"dash.mem.never":`없음`,"dash.mem.details":`상세 정보`,"dash.mem.unavailable":`메모리 진단을 사용할 수 없음 (구버전 프록시).`,"dash.mem.inFlight":`진행 중 요청`,"dash.mem.restart":`작업 완료 후 재시작`,"dash.mem.restartConfirm":`진행 중 요청 {count}개가 끝날 때까지 기다린 뒤 재시작합니다(최대 {seconds}초; 시간이 지나면 남은 요청은 중단됩니다).`,"dash.mem.draining":`요청 {count}개 완료 대기 중… 끝나면 재시작`,"dash.mem.reconnecting":`프록시 재시작 중… 다시 연결하는 중`,"dash.mem.restartFailed":`작업 완료 후 재시작에 실패했습니다. 프록시가 실행 중인지 확인하세요.`,"dash.mem.restartNoSupervisor":`재시작 보호가 없습니다. 재시작 후 프록시가 자동으로 올라오지 않을 수 있습니다.`,"dash.activeProviders":`활성 프로바이더`,"dash.noProviders":`설정된 프로바이더가 없습니다. {cmd} 를 실행하세요.`,"dash.col.name":`이름`,"dash.col.adapter":`어댑터`,"dash.col.baseUrl":`Base URL`,"dash.col.model":`모델`,"dash.modelsNoResults":`검색과 일치하는 모델이 없습니다.`,"dash.availableModels":`사용 가능한 모델`,"dash.noModels":`모델을 찾을 수 없습니다. 프로바이더 API 키를 확인하세요.`,"dash.cannotConnect":`프록시에 연결할 수 없습니다. 실행 중인가요?`,"dash.runStart":`{cmd} 를 실행해 프록시를 시작하세요.`,"dash.stop":`프록시 중지`,"dash.stopConfirm":`프록시를 중지하고 Codex 원본 설정을 복원할까요?`,"dash.stopFailed":`프록시를 중지하지 못했습니다 (HTTP {status}).`,"dash.maSwitchFailed":`모드 전환에 실패했습니다 (HTTP {status}).`,"dash.maNetworkError":`네트워크 오류 — 프록시가 실행 중인지 확인하세요.`,"dash.stopping":`중지 중…`,"dash.actions":`프록시`,"dash.codexRestart":`Codex 모델 목록 새로고침`,"dash.codexRestarting":`종료하는 중…`,"dash.codexRestartConfirm":`Codex app-server를 종료해 모델 목록을 다시 읽게 할까요? 진행 중인 Codex 작업이 끊기고, Codex가 저절로 다시 뜨지는 않으니 끝나면 직접 열어야 합니다.`,"dash.codexRestartDone":`Codex app-server {count}개를 종료했습니다. Codex를 다시 열면 최신 모델 목록이 보입니다.`,"dash.codexRestartNothing":`실행 중인 Codex app-server가 없습니다. 다음 실행 때 최신 목록을 읽습니다.`,"dash.codexRestartUnknown":`프로세스 목록을 읽지 못해 아무것도 종료하지 않았습니다.`,"dash.codexRestartPartial":`app-server {count}개가 종료되지 않았습니다. 모델 목록이 최신 상태로 바뀌지 않으면 직접 종료하세요.`,"dash.codexRestartFailed":`Codex 모델 목록을 새로고침하지 못했습니다 (HTTP {status}).`,"dash.codexRestartUnreachable":`프록시에 연결하지 못했습니다.`,"dash.codexRestartMalformed":`프록시가 예상과 다른 응답을 보냈습니다.`,"dash.codexRestartTimeout":`프록시가 제때 응답하지 않았습니다. app-server를 계속 종료하는 중일 수 있습니다.`,"models.staleBanner":`Codex가 이 카탈로그보다 오래된 모델 목록을 보여주고 있습니다. Codex를 재시작하면 새로 읽습니다.`,"dash.codexAutoStart":`Codex 실행 시 opencodex 시작`,"dash.codexAutoStartHint":`설치된 launcher shim이 ocx ensure를 실행하도록 허용합니다. 이 설정은 재부팅 보호를 설치하지 않으므로 시작 안전성에서 실제 상태를 확인하세요.`,"dash.searchModel":`서치 사이드카 모델`,"dash.searchModelHint":`비-OpenAI 라우팅 모델의 web_search에 사용되는 모델입니다. ChatGPT 로그인 필요.`,"dash.searchReasoning":`서치 추론 강도`,"dash.visionModel":`비전 사이드카 모델`,"dash.visionModelHint":`텍스트 전용 라우팅 모델에 이미지를 설명하는 데 사용되는 모델입니다. ChatGPT 로그인 필요.`,"dash.webSearchSidecar":`웹 검색 사이드카`,"dash.webSearchSidecarHint":`라우팅 모델의 웹 검색에 쓸 백엔드와 모델을 고릅니다.`,"dash.webSearchStream":`응답 실시간 스트리밍`,"dash.webSearchStreamHint":`모델이 도구 호출을 결정할 때까지 앞부분 텍스트와 추론을 실시간 스트리밍합니다. 이후는 검색 가로채기를 위해 버퍼링됩니다. 검색 전 텍스트가 일부 반복될 수 있습니다.`,"dash.visionSidecar":`비전 사이드카`,"dash.visionSidecarHint":`텍스트 전용 라우팅 모델이 이미지를 읽을 때 쓸 백엔드와 모델을 고릅니다.`,"dash.visionOff":`끔`,"dash.shadowCallIntercept":`쉐도우 호출 가로채기`,"dash.shadowCallInterceptHint":`Codex 앱이 제목·커밋 메시지 생성에 쓰는 백그라운드 호출({models})을 가로채 선택한 모델로 바꿉니다.`,"dash.shadowCallWarning":`⚠ 활성화하면 {models} 요청이 모두 선택한 모델로 대체됩니다.`,"dash.shadowCallOriginal":`원본`,"dash.shadowCallModel":`대체 모델`,"dash.shadowCallTooltip":`Codex 앱은 스레드 제목 자동 생성, 커밋 메시지 생성, 스킬 오케스트레이션 같은 내부 작업을 백그라운드로 호출합니다. 이때 쓰는 모델은 클라이언트 버전마다 달라서 opencodex는 {models}를 모두 가로챕니다. 이 설정을 켜면 해당 호출이 선택한 모델로 넘어갑니다.`,"models.shadowCallIntercept":`쉐도우 호출 가로채기`,"models.shadowCallInterceptHint":`Codex 앱의 백그라운드 호출({models}, 제목·커밋 메시지)을 가로채 선택한 모델로 바꿉니다.`,"dash.sidecarBackend":`백엔드`,"dash.sidecarModel":`모델`,"dash.backendAuto":`자동`,"dash.backendOpenAI":`OpenAI`,"dash.backendAnthropic":`Anthropic`,"dash.sidecarSaved":`사이드카 설정이 저장됐습니다. 다음 요청부터 적용됩니다.`,"dash.sidecarSaveFailed":`사이드카 설정 저장에 실패했습니다.`,"dash.injectionLabel":`서브에이전트 위임`,"dash.injectionHint":`Codex가 서브에이전트에게 일을 넘길 때 쓸 모델을 고릅니다. 이 선택을 어디에 적용할지는 아래 두 스위치가 정합니다.`,"dash.injectionManage":`설정 열기`,"dash.syncCodexSubagentDefaults":`Codex 설정에도 기본값으로 저장`,"dash.syncCodexSubagentDefaultsHint":`켜면 위에서 고른 모델이 Codex 설정 파일에 저장돼, 새로 시작하는 작업도 처음부터 그 모델을 씁니다. 끄면 여기서만 기억합니다. 반영은 다음 동기화나 재시작 때이고, 직접 적어둔 [agents] 설정은 그대로 둡니다.`,"dash.multiAgentGuidance":`일 나누는 방법 알려주기`,"dash.multiAgentGuidanceHint":`Codex에게 "일을 이렇게 나눠 맡기면 된다"는 짧은 쪽지를 붙여 보냅니다. v2에서는 쓸 수 있는 모델 목록과 우선 모델을 알려주고, v1에서는 추론 강도가 max나 ultra일 때만 동작합니다. 끄면 아무 쪽지도 붙지 않습니다.`,"dash.injectionNone":`없음`,"dash.injectionEffortLabel":`추론 강도`,"dash.injectionEffortNone":`모델 기본값`,"dash.effortCapLabel":`V2 ultra 추론 강도 제한`,"dash.subagentEffortCapLabel":`V2 서브에이전트 추론 강도 제한`,"dash.effortCapHelp":`V2 ultra 모드 턴의 추론 강도를 제한합니다. 설정하면 ultra 모드에서 들어오는 max 요청이 선택한 수준으로 내려갑니다. 서브에이전트 제한은 스폰된 자식 에이전트에만 적용됩니다. 강도를 낮추기만 하고 올리지는 않습니다. 모델이 해당 수준을 지원하지 않으면 가장 가까운 지원 수준으로 내려갑니다.`,"dash.effortCapNone":`상한 없음`,"dash.maintenance":`유지보수`,"dash.maintenanceHint":`Codex 모델 카탈로그를 새로고침하거나 최신 opencodex 릴리스를 설치합니다.`,"dash.syncModels":`모델 동기화`,"dash.syncModelsHint":`연결해둔 프로바이더를 기준으로 Codex 모델 카탈로그를 다시 씁니다.`,"dash.syncRun":`지금 동기화`,"dash.syncing":`동기화 중…`,"dash.syncOk":`동기화 완료. {count}개 모델이 추가됐습니다.`,"dash.syncStaleHint":`Codex에 여전히 예전 목록이 보이면 오래 실행 중인 app-server를 재시작하세요 ({cmd}).`,"dash.syncFailed":`동기화 실패: {error}`,"dash.projectConfigTitle":`프로젝트 Codex 설정이 OpenCodex를 우회합니다`,"dash.projectConfigHint":`저장소 로컬 설정이 OpenCodex 프록시를 덮어씁니다(예: OpenCode Go로 직접 라우팅). 해당 프로젝트에서 ~/.codex/config.toml 프록시를 쓰려면 제거하세요.`,"dash.checkUpdate":`업데이트 확인`,"dash.updateTitle":`opencodex 업데이트`,"dash.updateDesc":`선택한 채널의 npm 최신 버전을 확인한 뒤, 설치 후 프록시를 재시작할지 선택합니다.`,"dash.updateChannel":`채널`,"dash.updateChecking":`업데이트 확인 중…`,"dash.updateInstalled":`설치됨`,"dash.updateLatest":`최신`,"dash.updateAvailable":`업데이트 가능`,"dash.updateCurrent":`최신 상태`,"dash.updateCommand":`명령`,"dash.updateSource":`현재는 소스 체크아웃입니다. 표시된 명령을 터미널에서 실행해 업데이트하세요.`,"dash.updateUnavailable":`npm에서 최신 버전을 읽지 못했습니다. 잠시 후 다시 시도하세요.`,"dash.updateRetry":`재시도`,"dash.updateRecheck":`다시 확인`,"dash.updateCannotAuto":`원클릭 업데이트를 사용할 수 없습니다 ({reason}).`,"dash.updateReason.source_checkout":`소스 체크아웃`,"dash.updateReason.latest_unavailable":`npm 레지스트리에 연결할 수 없음`,"dash.updateReason.already_latest":`이미 최신 버전`,"dash.updateReason.unknown":`업데이트 불가`,"dash.updateRestart":`업데이트 후 재시작`,"dash.updateRestartHint":`권장. 프록시를 재시작하기 전까지 현재 GUI는 이전 코드로 계속 실행됩니다.`,"dash.runUpdate":`업데이트`,"dash.updateReconnecting":`재시작된 프록시를 기다리는 중…`,"dash.updateStatus.running":`opencodex 업데이트 중입니다.`,"dash.updateStatus.restarting":`업데이트 설치 완료. 프록시를 재시작하는 중입니다.`,"dash.updateStatus.succeeded":`업데이트가 완료됐습니다.`,"dash.updateVersionTransition":`{currentVersion} -> {latestVersion}.`,"dash.updateStatus.failed":`업데이트에 실패했습니다.`,"prov.subtitle":`opencodex가 Codex로 라우팅하는 업스트림 프로바이더를 설정합니다. 계정으로 로그인하거나, 프로바이더를 추가하거나, 원본 설정을 편집하세요.`,"prov.add":`프로바이더 추가`,"prov.editJson":`JSON 편집`,"prov.accountLogin":`계정 로그인`,"prov.noOauth":`사용 가능한 OAuth 프로바이더가 없습니다.`,"prov.loggedIn":`로그인됨`,"prov.notLoggedIn":`로그인 안 됨`,"prov.logout":`로그아웃`,"prov.login":`로그인`,"prov.loginWith":`{provider} 로 로그인`,"prov.waitingBrowser":`브라우저 대기 중…`,"prov.didntOpen":`안 열렸나요? 여기를 클릭하세요`,"prov.copyLink":`링크 복사`,"prov.dontOpenBrowser":`프록시가 실행 중인 컴퓨터에서 브라우저를 열지 않기`,"prov.dontOpenBrowserHint":`다른 브라우저 프로필로 로그인하거나, 대시보드를 프록시와 다른 컴퓨터에서 쓸 때 유용합니다.`,"prov.linkCopied":`복사됨`,"prov.linkCopyUnavailable":`클립보드를 사용할 수 없음`,"prov.deviceCode":`기기 인증 코드`,"prov.copyCode":`코드 복사`,"prov.codeCopied":`코드 복사됨`,"prov.editAlias":`별칭 편집`,"prov.aliasPrompt":`표시 이름 (비우면 삭제)`,"prov.aliasSaved":`별칭이 저장되었습니다`,"prov.aliasSaveFailed":`별칭을 저장하지 못했습니다`,"prov.accountId":`ID`,"prov.pasteRedirect":`리다이렉트 URL 또는 코드 붙여넣기`,"prov.pasteRedirectHint":`브라우저에 localhost 오류가 표시되면, 주소창의 전체 URL을 복사해 여기에 붙여넣으세요(또는 인증 코드 붙여넣기).`,"prov.pasteSubmit":`제출`,"prov.pasteSubmitting":`제출 중…`,"prov.pasteOk":`코드를 제출했습니다 — 로그인 완료 중…`,"prov.pasteFail":`코드 제출 실패: {error}`,"prov.port":`포트`,"prov.default":`기본값`,"prov.loadingConfig":`불러오는 중…`,"prov.saved":`저장됨! 적용하려면 프록시를 재시작하세요.`,"prov.loadConfigFail":`설정을 불러오지 못했습니다`,"prov.invalidJson":`잘못된 JSON`,"prov.saveFailed":`저장 실패`,"prov.loginFailStart":`{provider} 로그인을 시작하지 못했습니다`,"prov.loginError":`{provider} 로그인 오류: {error}`,"prov.loginRequestFail":`{provider} 로그인 요청 실패`,"prov.loginCancelled":`{provider} 로그인이 취소되었습니다`,"prov.loginTimeout":`{provider} 로그인 시간 초과 — 브라우저를 닫았거나 완료되지 않았습니다. 다시 시도하세요.`,"prov.loginOk":`{provider} 에 로그인했습니다. 모델을 표시하려면 {cmd} 를 실행하세요(또는 실시간 적용됩니다).`,"prov.loginSameAccount":`같은 {provider} 계정입니다. 브라우저에서 계정을 전환한 뒤 계정 추가를 다시 시도하세요.`,"oauthTos.highTitle":`{provider}: 구독 OAuth 위험`,"oauthTos.elevatedTitle":`{provider}: 비공식 OAuth 브리지`,"oauthTos.anthropicBody":`Claude 구독 OAuth 토큰을 OpenCodex 같은 타사 프록시에서 직접 재사용하는 방식은 Anthropic이 지원하는 통합이 아니며 접근이 제한될 수 있습니다. Claude 구독을 사용하는 공식 Agent SDK 통합은 별도입니다.`,"oauthTos.highBody":`OpenCodex는 {provider}를 타사 OAuth 경로로 연결합니다. 지원되지 않는 사용 방식이면 접근이 제한되거나 정지될 수 있습니다.`,"oauthTos.elevatedBody":`OpenCodex는 {provider}를 비공식 OAuth 경로로 연결합니다. 가능하면 공식 클라이언트를 사용하세요. 비정상적이거나 자동화된 트래픽은 남용으로 간주되어 접근이 제한되거나 정지될 수 있습니다.`,"oauthTos.saferPath":`더 안전한 방법: OpenCodex에 API 키를 대신 설정하세요.`,"oauthTos.acknowledge":`위험을 이해했으며 OAuth로 계속 진행합니다.`,"oauthTos.continue":`OAuth로 계속`,"prov.logoutOk":`{provider} 에서 로그아웃했습니다.`,"prov.logoutFail":`{provider}에서 로그아웃하지 못했습니다. 계정 상태는 그대로입니다.`,"prov.removed":`"{name}" 을(를) 삭제했습니다.`,"prov.removedDefault":`"{name}"을(를) 삭제했습니다. 이제 기본 프로바이더는 "{defaultProvider}"입니다.`,"prov.removeFail":`"{name}" 삭제에 실패했습니다.`,"prov.removeLastProvider":`활성화된 다른 프로바이더가 기본이 될 수 없으면 이 프로바이더를 삭제할 수 없습니다.`,"prov.removeHasDependentCombos":`먼저 이 프로바이더를 사용하는 콤보를 삭제하거나 수정하세요: {combos}.`,"prov.setDefault":`기본으로 설정`,"prov.setDefaultSuccess":`"{name}"이(가) 기본 프로바이더로 설정되었습니다.`,"prov.setDefaultFail":`"{name}"을(를) 기본 프로바이더로 설정하지 못했습니다.`,"prov.defaultDisabled":`기본으로 설정하려면 먼저 이 프로바이더를 활성화하세요.`,"prov.updateFail":`이 프로바이더를 업데이트하지 못했습니다.`,"prov.networkError":`네트워크 오류입니다. 프록시가 실행 중인지 확인한 후 다시 시도하세요.`,"prov.added":`"{name}" 을(를) 추가했습니다. 지금 활성화됨 — Codex 모델 선택기에 표시하려면 {cmd} 를 실행하세요(또는 재시작).`,"prov.removeConfirm":`프로바이더 "{name}" 을(를) 삭제할까요? 해당 모델이 Codex 선택기에서 사라집니다.`,"prov.hasApiKey":`API 키 설정됨`,"prov.hasHeaders":`커스텀 헤더 설정됨`,"prov.accounts":`계정 ({n})`,"prov.accountsAria":`{name} 계정 목록 열기/닫기`,"prov.accountActive":`활성`,"prov.accountReauth":`재로그인`,"prov.reauthenticate":`재인증`,"prov.reauthAccountMissing":`로그인 후 선택한 계정을 찾을 수 없습니다`,"prov.reauthIdentityMismatch":`로그인한 계정이 선택한 계정과 일치하지 않습니다`,"prov.accountAdd":`계정 추가`,"prov.accountNoLabel":`계정 {id}`,"prov.accountSwitchTitle":`이 계정 사용`,"prov.accountSwitched":`{email} 계정으로 전환했습니다.`,"prov.accountSwitchFail":`계정 전환에 실패했습니다`,"prov.accountRemoved":`{email} 계정을 제거했습니다.`,"prov.accountRemoveFail":`{email} 계정을 제거하지 못했습니다. 계정은 그대로입니다.`,"prov.accountRemoveAria":`{email} 제거`,"prov.accountRemoveConfirm":`{email} 계정을 제거할까요? 이 프록시에서 로그인이 삭제됩니다.`,"prov.keyAdd":`API 키 추가`,"prov.keyAdded":`{name}에 API 키를 추가했습니다.`,"prov.keyAddFail":`API 키 추가에 실패했습니다`,"prov.keyPlaceholder":`API 키 붙여넣기`,"prov.keySwitchTitle":`이 키 사용`,"prov.keySwitched":`{key} 키로 전환했습니다.`,"prov.keySwitchFail":`키 전환에 실패했습니다`,"prov.keyRemoved":`{key} 키를 제거했습니다.`,"prov.keyRemoveAria":`{key} 키 제거`,"prov.keyRemoveConfirm":`API 키 {key}를 제거할까요? 이 프록시 설정에서 삭제됩니다.`,"prov.activeBadge":`활성`,"prov.disabledBadge":`비활성`,"prov.defaultBadge":`기본`,"prov.enable":`활성화`,"prov.disable":`비활성화`,"prov.enabled":`"{name}" 을(를) 활성화했습니다. 해당 모델을 다시 Codex에서 사용할 수 있습니다.`,"prov.disabled":`"{name}" 을(를) 비활성화했습니다. 설정은 유지되고 모델은 숨겨집니다.`,"prov.enableFail":`"{name}" 활성화에 실패했습니다.`,"prov.disableFail":`"{name}" 비활성화에 실패했습니다.`,"prov.enableAria":`{name} 프로바이더 활성화`,"prov.disableAria":`{name} 프로바이더 비활성화`,"prov.defaultCannotDisable":`기본 프로바이더는 비활성화할 수 없습니다`,"prov.openaiAccountMode":`Codex 계정 모드`,"prov.openaiModePool":`풀`,"prov.openaiModeDirect":`직접`,"prov.openaiPoolDesc":`기본값입니다. 메인 로그인과 추가 계정을 친화도, 할당량, 대기 시간, 장애 조치에 따라 순환합니다.`,"prov.openaiDirectDesc":`현재 메인 Codex 로그인만 사용합니다. 저장된 풀 계정은 읽거나 순환하지 않습니다.`,"prov.openaiModeSaved":`OpenAI 계정 모드를 {mode} 모드로 변경했습니다.`,"prov.openaiModeSaveFailed":`OpenAI 계정 모드를 변경하지 못했습니다.`,"prov.openaiApiDesc":`OpenAI API 키만 사용하며 Codex 계정 인증과 섞이지 않습니다.`,"prov.manageCodexAccounts":`Codex 계정 관리`,"prov.openaiApiMissing":`API 키 필요`,"prov.openaiApiSetup":`API 키 설정`,"models.tab.catalog":`모델`,"models.tab.combos":`콤보`,"models.tab.compatibility":`호환성`,"models.tab.routing":`라우팅 (beta)`,"models.tabsLabel":`모델 표면`,"models.subtitle.combos":`여러 모델을 하나의 id로 묶어 순서대로 응답하게 합니다. failover로 대상을 연결하거나 분산 전략으로 부하를 나눕니다.`,"models.subtitle.compatibility":`랩 프로젝션 증거의 읽기 전용 호환성 판정 행렬.`,"models.subtitle.routing":`정책 프로필, dry-run 평가, 그리고 근거가 남는 라우팅 분석입니다.`,"models.subtitle":`Codex가 보는 모델을 켜고 끕니다 — 네이티브 GPT passthrough와 라우팅된 모델을 프로바이더별로 묶어 보여줍니다(헤더를 클릭하면 접힘). 숨긴 모델은 카탈로그와 선택기에서 빠지지만 정확한 id로 직접 호출할 수 있습니다. 변경 사항은 다음 Codex 턴에 적용됩니다 — opencodex가 Codex의 5분 모델 캐시를 무효화하므로 재시작이 필요 없습니다.`,"models.nativeGroupLabel":`OpenAI 네이티브`,"models.nativeHint":"프로바이더에서 선택한 풀 또는 직접 계정 옵션으로 서빙되는 passthrough 모델입니다. 끄면 Codex 선택기에서 숨겨지고, 카탈로그 항목은 유지되므로 다시 켜면 그대로 복원됩니다. 여기서 모델을 추가하면 bare passthrough id가 아니라 라우팅된 `openai/` selector로 등록됩니다.","models.active":`{active}/{total} 표시`,"models.workspace.providers":`프로바이더`,"models.workspace.allProviders":`모든 프로바이더`,"models.workspace.mainAria":`모델 세부정보`,"models.allOn":`모두 켜기`,"models.allOff":`모두 끄기`,"models.presetLabel":`모델`,"models.presetMode_preset":`프리셋`,"models.presetMode_all":`전체`,"models.presetMode_custom":`커스텀`,"models.presetSummary":`{total}개 중 {count}개 표시 — 코어 프리셋 v{version}`,"models.presetUpdateAvailable":`프리셋 v{version} 사용 가능`,"models.presetAppliedToast":`{provider}: 프리셋 적용 — 모델 {count}개 선택`,"models.presetClearedToast":`{provider}: 모든 모델 표시`,"models.presetEmpty":`{provider}: 프리셋과 일치하는 모델이 없어 선택을 그대로 두었습니다`,"models.presetConfirmReplace":`선택한 목록을 {count}개짜리 프리셋으로 바꿀까요?`,"models.cap350k":`350k 제한`,"models.capApplied":`컨텍스트 제한 적용됨 — 다음 Codex 턴부터 반영됩니다.`,"models.capSaveFailed":`컨텍스트 제한 저장 실패`,"models.contextCapped":`350k 제한`,"models.contextCapLabel":`기본 창 / 상한`,"models.v2Label":`서브에이전트`,"models.shadowCallOriginal":`⚠ {models} →`,"models.v2Mode_v1":`v1`,"models.v2Mode_default":`base`,"models.v2Mode_v2":`v2`,"models.v2ModeDesc_v1":`전 모델 → v1 서피스`,"models.v2ModeDesc_default":`업스트림 기본값 (sol/terra=v2, luna=v1)`,"models.v2ModeDesc_v2":`전 모델 → v2 서피스`,"models.keepNativeOnV1":`ChatGPT는 v1 유지`,"models.keepNativeOnV1Hint":`ChatGPT 네이티브 부모는 v2 자식 작업을 암호화해서 Grok/Claude가 읽지 못합니다. Sol/Terra가 routed 모델을 spawn해야 하면 켜 두세요. routed 부모는 v2를 유지합니다.`,"models.v2Help":`모든 모델의 멀티에이전트 서피스를 제어합니다. - -v1: 단일 스레드 에이전트. 모든 모델이 v1 서피스를 사용합니다. -base: 업스트림 기본값 — sol/terra는 v2, luna는 v1, 나머지는 codex 플래그를 따릅니다. -v2: 멀티 스레드 에이전트(spawn_agent). 모든 모델이 v2 서피스를 사용합니다. - -v2에서 ChatGPT는 v1 유지를 켜면 Sol/Terra가 v1에 남아 Grok이나 Claude를 spawn할 수 있습니다. ChatGPT는 v2 자식 작업을 암호화하므로 routed 모델은 읽지 못합니다. routed 부모는 v2를 유지합니다. - -새 세션부터 적용됩니다.`,"models.v2DocsLink":`v1 / v2가 뭔가요?`,"dash.multiAgent":`서브에이전트`,"models.v2Conflict":`[agents] max_threads가 남아 있어 codex가 부팅을 거부합니다 — config.toml에서 제거하세요`,"models.v2Applied":`서브에이전트 모드 변경됨 — 새 세션부터 적용 (피커 갱신은 Codex 앱 재시작)`,"models.v2ThreadsLabel":`최대 스레드`,"models.v2ThreadsDefault":`기본값 (4)`,"models.v2ThreadsApplied":`스레드 한도 변경됨 — 새 세션부터 적용`,"models.v2ThreadsInvalid":`스레드 한도는 1 이상 정수여야 합니다`,"models.v2ThreadsApply":`적용`,"models.capValue":`기본 {value}`,"models.contextSettings":`사용자 지정 창`,"models.contextSettingsTitle":`사용자 지정 창 — {provider}`,"models.contextDefault":`프로바이더 기본값`,"models.contextModel":`모델`,"models.contextModelOverride":`모델별 재정의`,"models.contextHint":`이미 아는 경우 여기에 실제 Codex 컨텍스트 윈도우를 적습니다. 업스트림 값이 없으면 이 값을 쓰고, 더 큰 보고값만 낮추며, 더 작은 업스트림 컨텍스트 윈도우는 그대로 둡니다. 비우면 프로바이더의 「기본 창 / 상한」을 쓰고, 그 상한이 꺼져 있으면 128k입니다.`,"models.contextAutomatic":`자동 검색`,"models.contextSaved":`컨텍스트 윈도우가 업데이트되었습니다 — 다음 Codex 턴부터 적용됩니다.`,"models.contextUnchanged":`저장할 컨텍스트 윈도우 변경이 없습니다.`,"models.contextSaveFailed":`컨텍스트 윈도우를 저장하지 못했습니다`,"models.contextInvalid":`컨텍스트 윈도우는 양의 정수여야 합니다`,"models.contextCappedValue":`{value} 제한`,"models.setAll":`전체 적용`,"models.setAllHint":`라우팅된 모든 프로바이더에 {value} 기본 창을 켭니다. 중계가 context_window / context_length 를 주지 않으면 이 값이 실제 Codex 창이 됩니다. 모델 하나만 손으로 쓰려면 같은 줄의 「사용자 지정 창」을 쓰세요. 네이티브 프로바이더는 영향을 받지 않습니다.`,"models.collapseAll":`모두 접기`,"models.expandAll":`모두 펼치기`,"models.orderHint":`피커 순서: Subagents에서 지정한 순서 → 나머지 라우팅 모델(프로바이더, 모델 ID 순 알파벳 정렬) → 네이티브 모델. 노출 토글은 모델을 필터링할 뿐 이 순서를 바꾸지 않습니다.`,"models.custom":`직접 입력…`,"models.customApply":`적용`,"models.customPlaceholder":`토큰 (예: 420000)`,"models.customAdd":`커스텀 모델 추가`,"models.customAddTitle":`커스텀 모델 추가 — {provider}`,"models.customEditTitle":`커스텀 모델 편집 — {provider}`,"models.customAdded":`커스텀 모델 추가됨`,"models.customUpdated":`커스텀 모델 수정됨`,"models.customDeleted":`커스텀 모델 삭제됨`,"models.customSaveFailed":`커스텀 모델 저장 실패`,"models.customSaving":`저장 중…`,"models.customAddBtn":`추가`,"models.customEditBtn":`수정`,"models.customEdit":`편집`,"models.customDelete":`삭제`,"models.customDeleteConfirm":`{name} 모델을 삭제하시겠습니까?`,"models.customBadge":`커스텀`,"models.customSummary":`커스텀 {count}개`,"models.customFieldModelId":`모델 ID (엔드포인트 슬러그)`,"models.customFieldModelIdPlaceholder":`예: qwen4-max-preview`,"models.customFieldDisplayName":`표시명 (선택)`,"models.customFieldDisplayNamePlaceholder":`예: Qwen 4 Max Preview`,"models.customFieldContext":`컨텍스트 윈도우`,"models.customFieldModalities":`입력 모달리티`,"models.customFieldReasoning":`추론 노력`,"models.customFieldReasoningOverride":`추론 노력 재정의`,"models.reasoningEffort.none":`없음`,"models.reasoningEffort.minimal":`최소`,"models.reasoningEffort.low":`낮음`,"models.reasoningEffort.medium":`중간`,"models.reasoningEffort.high":`높음`,"models.reasoningEffort.xhigh":`매우 높음`,"models.reasoningEffort.max":`최대`,"models.tipProvider":`프로바이더`,"models.tipContext":`컨텍스트`,"models.tipModalities":`모달리티`,"models.tipStatus":`상태`,"models.tipActive":`활성`,"models.tipDisabled":`비활성`,"models.applied":`적용됨 — 다음 Codex 턴부터 반영됩니다.`,"models.saveFailed":`저장 실패`,"models.networkError":`네트워크 오류 — 프록시가 실행 중인가요?`,"models.loadFail":`모델을 불러오지 못했습니다 — 프록시가 실행 중인가요?`,"models.noRouted":`라우팅된 모델 없음`,"models.noRoutedHint":`먼저 프로바이더에 로그인하거나 추가하세요.`,"models.emptyDiscovery":`발견된 모델이 없습니다. 프로바이더 엔드포인트를 확인하거나 정적/사용자 모델을 추가하세요.`,"models.emptyDiscoveryDisabled":`실시간 모델 검색이 꺼져 있고 정적 모델도 설정되지 않았습니다.`,"models.discoveryFailedBadge":`검색 실패`,"models.discoveryFailedHttp":`모델 검색에 실패했습니다(HTTP {status}).`,"models.discoveryFailedBlocked":`대상 정책 때문에 모델 검색이 차단되었습니다.`,"models.discoveryFailedInvalidResponse":`모델 검색이 잘못된 응답을 받았습니다.`,"models.discoveryFailedNetwork":`네트워크 오류로 모델 검색에 실패했습니다.`,"models.discoveryFailedProvider":`프로바이더가 모델 검색 오류를 보고했습니다.`,"models.discoveryFailedGeneric":`모델 검색에 실패했습니다.`,"models.openProviderSettings":`프로바이더 설정 열기`,"models.loading":`불러오는 중…`,"models.search":`모델 검색…`,"models.showMore":`{n}개 더 보기`,"models.allowlistLabel":`선택만 노출`,"models.allowlistHint":`체크한 모델만 카탈로그에 노출돼요 (비우면 전체). 수천 개 모델을 노출하는 프로바이더에 유용해요.`,"models.selectedCount":`{n}개 선택`,"sub.subtitle":`Codex의 {cmd} 는 우선순위 상위 5개 모델만 오버라이드로 노출합니다. 여기서 최대 5개를 선택하면 — 네이티브 gpt 또는 라우팅된 모델 — opencodex가 카탈로그 우선순위를 설정해 정확히 이들이 앞에 옵니다. 다른 모델도 정확한 이름으로 호출할 수 있으며, 이 설정은 표시 항목만 제어합니다.`,"sub.featured":`추천`,"sub.advanced":`고급`,"sub.orderHintAria":`이 순서가 쓰이는 방식`,"sub.orderHint":`여기서 선택해 표시된 순서가 Codex 모델 피커 최상단 1~5위와 {cmd}의 기본 모델 후보를 결정합니다.`,"sub.noneSelected":`선택된 항목 없음 — 아래 목록에서 선택하세요.`,"sub.models":`모델`,"sub.search":`모델 검색(네이티브 gpt + 라우팅)…`,"sub.settings":`설정`,"sub.sections":`서브에이전트 구역`,"sub.delegation.model":`먼저 부를 모델`,"sub.delegation.modelHint":`Codex가 일을 나눠 맡길 때 가장 먼저 부를 모델입니다. 위 추천 목록이 부를 수 있는 후보라면, 여기서 고른 모델이 그중 1순위가 됩니다.`,"sub.noModels":`모델 없음 — 먼저 프로바이더에 로그인하거나 추가하세요.`,"sub.saved":`{n}개 모델을 저장했습니다. spawn_agent 오버라이드로 보려면 새 Codex 세션을 시작하거나 {cmd} 를 실행하세요.`,"sub.saveFailed":`저장 실패`,"sub.networkError":`네트워크 오류 — 프록시가 실행 중인가요?`,"sub.loadFail":`모델을 불러오지 못했습니다 — 프록시가 실행 중인가요?`,"sub.loading":`불러오는 중…`,"sub.moveUp":`{m} 위로 이동`,"sub.moveDown":`{m} 아래로 이동`,"sub.removeAria":`{m} 삭제`,"sub.workspace.addToFeatured":`{m}을(를) 추천에 추가`,"sub.workspace.allModels":`모든 모델`,"sub.workspace.featuredFull":`추천 목록이 가득 찼습니다 (최대 5개)`,"sub.workspace.mainAria":`서브에이전트 모델 세부 정보`,"sub.workspace.notFeatured":`추천되지 않음`,"sub.workspace.priority":`우선순위`,"sub.workspace.removeFromFeatured":`{m}을(를) 추천에서 제거`,"sub.workspace.selectModel":`모델 선택`,"sub.workspace.selectModelDesc":`목록에서 모델을 선택하여 세부 정보를 확인하고 spawn_agent에 추천하세요.`,"sub.workspace.selector":`공개 셀렉터`,"sub.ultraMode":`울트라 모드`,"sub.ultraModeHint":`모든 모델과 reasoning effort에서 Proactive 멀티에이전트 위임 정책을 켭니다 (reasoning effort 자체는 변경하지 않음). config.toml에 features.multi_agent_v2.multi_agent_mode_hint_text를 기록합니다.`,"sub.ultraModeV2Required":`v2 멀티에이전트 서피스가 필요합니다 — 먼저 multi_agent_v2를 켜고 서브에이전트 모드에서 v2를 선택하세요.`,"sub.ultraModeText":`울트라 모드 위임 텍스트`,"sub.ultraModePreset":`프리셋 복원`,"sub.ultraModeLoadFail":`울트라 모드 설정을 불러오지 못했습니다 — 프록시가 실행 중인가요?`,"sub.ultraModeSaveFail":`울트라 모드 설정 저장에 실패했습니다`,"sub.ultraModeSaved":`울트라 모드가 저장되었습니다. 새 Codex 세션부터 적용됩니다.`,"logs.title":`요청 로그`,"logs.tabLogs":`로그`,"logs.tabDebug":`디버그`,"logs.subtitle":`로컬 opencodex 프록시를 거친 최근 요청입니다. 최신순.`,"logs.autoRefresh":`자동 새로고침`,"logs.noRequests":`아직 요청이 없습니다.`,"logs.loadError":`요청 로그를 불러오지 못했습니다.`,"logs.filter.surface.label":`표면`,"logs.filter.surface.all":`전체`,"logs.filter.surface.claude":`Claude`,"logs.filter.surface.codex":`Codex`,"logs.filter.surface.grok":`Grok`,"logs.filter.interceptedHelpersOnly":`가로챈 헬퍼만`,"logs.badge.interceptedHelper":`I · {model}`,"logs.badge.interceptedHelperTitle":`가로챈 헬퍼 요청`,"logs.filter.conversation.label":`대화`,"logs.filter.conversation.placeholder":`대화 ID 붙여넣기`,"logs.filter.conversation.clear":`지우기`,"logs.filter.model.label":`모델`,"logs.filter.model.placeholder":`모델 또는 공급자로 거르기`,"logs.filter.conversation.apply":`로그 필터`,"logs.conversation.totals":`{requests}건 요청 · {tokens} 토큰 · {cost}`,"logs.conversation.scope":`합계는 현재 로드된 Logs 링만 포함합니다.`,"logs.conversation.excluded":`(~$에서 가격 없음 {unpriced}건, 미측정 {unmetered}건 제외)`,"logs.cost.approximate":`{amount}`,"logs.cost.lowerBound":`≥{amount}`,"logs.cost.unavailable":`사용 불가`,"logs.detail.conversation":`대화`,"logs.badge.claude":`Claude`,"logs.badge.grok":`Grok`,"logs.col.time":`시간`,"logs.col.request":`요청`,"logs.col.model":`모델`,"logs.col.effort":`추론 강도`,"logs.col.provider":`프로바이더`,"logs.col.status":`상태`,"logs.col.tokens":`토큰`,"logs.col.tokPerSec":`tok/s`,"logs.col.estimatedCost":`~$`,"logs.metric.tokPerSecTitle":`전체 요청 시간 기준 초당 출력 토큰`,"logs.metric.estimatedCostTitle":`API 정가 환산치이며 실제 청구액이 아닙니다. 가격 미매칭은 표시하지 않습니다.`,"usage.cost.total":`API 정가 환산치 (이 기간)`,"usage.cost.disclaimer":`결제 영수증이 아닙니다. 구독 사용량 또는 프로바이더 크레딧이 대신 적용될 수 있습니다.`,"usage.cost.unpricedNote":`비용 산정 불가 {count}건 제외`,"logs.detail.section.basic":`기본 정보`,"logs.detail.route.section":`라우팅 결정`,"logs.detail.route.kind":`라우팅 종류`,"logs.detail.route.profile":`프로필`,"logs.detail.route.selected":`선택됨`,"logs.detail.route.candidates":`후보`,"logs.detail.route.unknown":`이 요청에 대한 라우팅 추적이 기록되지 않았습니다(추적 이전 행).`,"logs.detail.section.performance":`성능`,"logs.detail.section.cost":`API 정가 환산치`,"logs.detail.section.attempts":`Combo 시도`,"logs.detail.section.usage":`원본 usage`,"logs.detail.ttft":`TTFT`,"logs.detail.costTotal":`정가 환산치`,"logs.detail.totalTokens":`전체 토큰`,"logs.detail.matchedKey":`매칭된 가격 키`,"logs.detail.priceSource":`가격 출처`,"logs.detail.unavailableReason":`표시 불가 사유`,"logs.detail.copyRequestId":`요청 ID 복사`,"logs.detail.copied":`복사됨`,"logs.detail.source.jawcode":`jawcode 카탈로그`,"logs.detail.source.expected":`expected 가격 오버레이`,"logs.detail.source.user":`프로바이더 구성 가격 오버레이`,"logs.detail.verification.verified":`검증됨`,"logs.detail.verification.derived":`기반 모델 유도`,"logs.detail.attempt.target":`프로바이더 / 모델`,"logs.detail.attempt.reason":`결과 / 사유`,"logs.detail.attempt.completed":`완료`,"logs.detail.attempt.e2eNote":`상위 tok/s는 전체 요청 기준이며 각 시도는 자체 소요 시간을 사용합니다.`,"logs.detail.attempt.recovery.transient5xx":`일시적 5xx 오류`,"logs.detail.attempt.recovery.connectionReset":`연결이 재설정됨`,"logs.detail.attempt.recovery.oauth401":`OAuth 재인증`,"logs.detail.attempt.recovery.key429":`키 요청 한도 초과 (429)`,"logs.detail.attempt.recovery.rateLimit429":`요청 한도 초과 (429)`,"logs.detail.attempt.recovery.anthropicOauth429":`Anthropic OAuth 요청 한도 초과 (429)`,"logs.detail.attempt.recovery.image413":`이미지 페이로드가 너무 큼 (413)`,"logs.detail.attempt.recovery.emptyCompletion":`빈 응답 재시도`,"logs.detail.attempt.recovery.unknown":`알 수 없는 복구 사유`,"logs.detail.reason.usage_missing":`usage가 보고되지 않았습니다.`,"logs.detail.reason.usage_unsupported":`이 프로바이더는 usage 보고를 지원하지 않습니다.`,"logs.detail.reason.output_missing":`양수 출력 토큰 수가 보고되지 않았습니다.`,"logs.detail.reason.invalid_duration":`요청 소요 시간이 유효하지 않습니다.`,"logs.detail.reason.price_unmatched":`매칭되는 가격을 찾지 못했습니다.`,"logs.detail.reason.invalid_cache_breakdown":`캐시 토큰 상세가 전체 입력 토큰과 모순됩니다.`,"logs.detail.reason.invalid_usage":`usage에 유효하지 않은 토큰 값이 있습니다.`,"logs.detail.reason.combo_attempt_unavailable":`하나 이상의 combo 시도 비용을 계산할 수 없습니다.`,"logs.detail.estimate.usage_estimated":`프로바이더 usage가 추정치입니다.`,"logs.detail.estimate.cache_detail_missing":`캐시 상세가 없어 입력 전액을 상한으로 추정했습니다.`,"logs.detail.estimate.expected_price_overlay":`검증된 expected 정가를 사용했습니다.`,"logs.detail.estimate.provider_cost_overlay":`프로바이더 구성 가격 오버레이를 사용했습니다.`,"logs.detail.estimate.priority_lower_bound":`확인된 Priority 가격을 사용할 수 없어 표시된 추정치는 알려진 하한입니다.`,"logs.col.error":`오류`,"logs.col.upstreamReason":`업스트림 원인`,"logs.col.duration":`소요 시간`,"logs.modelTooltip.model":`모델`,"logs.modelTooltip.resolvedModel":`해석된 모델`,"logs.modelTooltip.requestedTier":`요청 티어`,"logs.modelTooltip.configuredTier":`설정 티어`,"logs.modelTooltip.responseTier":`응답 티어`,"logs.modelTooltip.supportsTier":`티어 지원`,"logs.tokens.reported":`측정됨`,"logs.tokens.unreported":`미보고`,"logs.tokens.unsupported":`미지원`,"logs.tokens.estimated":`추정`,"logs.tokens.input":`입력`,"logs.tokens.output":`출력`,"logs.tokens.cacheRead":`캐시 히트 (c)`,"logs.tokens.cacheWrite":`캐시 생성 (w)`,"logs.tokens.reasoning":`추론`,"logs.tokens.noCache":`캐시 미보고`,"logs.tokens.contextTotal":`활성 컨텍스트`,"logs.tokens.noCacheNote":`이 프로바이더는 캐시 토큰 수치를 제공하지 않습니다`,"logs.tokens.noCacheCursor":`Cursor 캐시 상세 미보고`,"logs.tokens.noCacheCursorNote":`Cursor 프로토콜은 캐시 read/write 토큰 수치를 제공하지 않습니다. 캐시 미스가 확인됐다는 뜻은 아닙니다`,"logs.tokens.estimatedNote":`추정치 (프로바이더가 정확한 사용량을 제공하지 않음)`,"logs.details":`상세보기`,"logs.detailTitle":`요청 상세`,"logs.detailRaw":`원본 로그`,"debug.title":`디버그`,"debug.subtitle":`선택적 provider transport 및 usage 추출 진단. 요청 오류와 502는 로그 탭에 표시됩니다.`,"debug.debug":`Provider debug`,"debug.usage":`Usage 추출`,"debug.injection":`주입 로그`,"debug.claude":`Claude 인바운드`,"debug.claudeInbound.title":`Claude 인바운드 요청`,"debug.claudeInbound.sub":`Claude Code/Desktop이 실제로 보내는 값(thinking, effort, metadata)을 보여줍니다 — 프롬프트 원문은 저장하지 않습니다.`,"debug.claudeInbound.empty":`아직 캡처된 요청이 없습니다. 켜진 상태에서 Claude로 메시지를 보내보세요.`,"debug.claudeInbound.time":`시간`,"debug.claudeInbound.endpoint":`엔드포인트`,"debug.claudeInbound.model":`모델`,"debug.claudeInbound.none":`없음`,"debug.reset":`런타임 재정의 해제`,"debug.refresh":`새로고침`,"debug.follow":`Follow`,"debug.streamProvider":`Provider`,"debug.streamUsage":`Usage`,"debug.streamInjection":`Injection`,"debug.loading":`디버그 설정 로딩 중…`,"debug.loadFailed":`디버그 설정을 불러오지 못했습니다.`,"debug.emptyTitle":`디버그 로깅 꺼짐`,"debug.empty":`위 카드에서 Provider debug 또는 Usage extraction을 켜세요. 프록시로 요청을 보낸 뒤 라인이 표시됩니다.`,"debug.noLinesTitle":`라인 대기 중`,"debug.noLines.provider":`공급자 디버그는 켜져 있지만 전송 이상(드롭되거나 잘못된 프레임, Cursor dial/retry 이벤트)만 기록합니다. Anthropic 같은 공급자로의 정상 요청은 라인을 생성하지 않을 수 있습니다.`,"debug.noLines.usage":`사용량 추출은 켜져 있지만 아직 캡처된 항목이 없습니다. Codex로 요청을 보내면 여기에 표시됩니다.`,"debug.noLines.injection":`주입 로그는 켜져 있지만 아직 캡처된 항목이 없습니다. Collab 및 서브 에이전트 턴의 멀티 에이전트 가이던스 주입과 effort-cap 결정을 기록합니다.`,"usage.title":`사용량`,"usage.subtitle":`프록시의 로컬 토큰 집계입니다. 누락된 사용량은 0으로 표시하지 않습니다.`,"usage.loading":`사용량 데이터를 불러오는 중…`,"usage.empty":`아직 기록된 사용량이 없습니다. 프록시로 요청을 보내면 여기에 표시됩니다.`,"usage.loadError":`사용량 데이터를 불러오지 못했습니다.`,"usage.range.all":`전체`,"usage.range.available":`사용 가능한 기록`,"usage.historyTruncated":`이전 사용 기록을 불러오지 않아 합계는 사용 가능한 기록만 포함합니다.`,"usage.historyTruncatedWindow":`불러온 기록의 요청 시작 시각은 {start}부터 {end} 사이입니다. 읽기 한도 때문에 파일 앞부분의 기록이 빠졌으므로 선택한 기간이 완전하지 않을 수 있습니다.`,"usage.range.30d":`30일`,"usage.range.7d":`7일`,"usage.card.requests":`요청`,"usage.card.measured":`측정됨`,"usage.card.reported":`측정됨`,"usage.card.totalTokens":`총 토큰`,"usage.card.cachedTokens":`캐시 히트 토큰`,"usage.card.cachedTokensHint":`프로바이더 캐시에서 읽어온 프롬프트 토큰(히트)입니다. 캐시 생성(쓰기)은 아래에 별도 표시됩니다.`,"usage.card.cacheWriteTokens":`캐시 생성`,"usage.card.coverage":`커버리지`,"usage.card.activeDays":`활동일`,"usage.section.heatmap":`일별 활동`,"usage.section.overview":`개요`,"usage.section.models":`모델`,"usage.section.providers":`프로바이더`,"usage.section.coverage":`커버리지 상세`,"usage.workspace.report":`사용량 보고서`,"usage.workspace.sections":`사용량 섹션`,"usage.coverage.measured":`측정됨`,"usage.coverage.reported":`제공자 보고`,"usage.coverage.estimated":`추정`,"usage.coverage.note":`측정됨 항목은 제공자 보고와 추정 토큰 수치를 함께 포함합니다. 미보고/미지원 요청은 추적만 하고 0으로 환산하지 않습니다.`,"usage.search.models":`모델 검색…`,"usage.col.requests":`요청`,"usage.col.measured":`측정됨`,"usage.col.reported":`측정됨`,"usage.col.tokens":`토큰`,"usage.col.share":`비율`,"usage.heatmap.less":`적음`,"usage.heatmap.more":`많음`,"modal.addNamed":`추가: {label}`,"modal.add":`프로바이더 추가`,"modal.search":`프로바이더 검색…`,"modal.logInWith":`{label} 로 로그인`,"modal.waitingBrowser":`브라우저 대기 중…`,"modal.providerName":`프로바이더 이름`,"modal.adapter":`어댑터`,"modal.baseUrl":`Base URL`,"modal.endpoint":`엔드포인트`,"modal.endpoint.tokenPlan":`토큰 플랜`,"modal.endpoint.payAsYouGo":`종량제`,"modal.endpoint.custom":`사용자 지정`,"modal.defaultModel":`기본 모델(선택)`,"modal.allowPrivateNetwork":`로컬/사설 네트워크 허용`,"modal.allowPrivateNetworkHint":`의도적으로 자체 호스팅하는 프로바이더에만 활성화하세요. 메타데이터 엔드포인트는 계속 차단됩니다.`,"modal.nameRequired":`프로바이더 이름을 입력하세요`,"modal.baseUrlRequired":`Base URL을 입력하세요`,"modal.networkError":`네트워크 오류 — 프록시가 실행 중인가요?`,"modal.loginFailStart":`로그인을 시작하지 못했습니다`,"modal.waitingLogin":`브라우저 로그인 대기 중…`,"modal.loggingIn":`로그인 중…`,"modal.loginTimeout":`로그인 시간 초과 — 다시 시도하세요.`,"nav.api":`API`,"nav.integrations":`연동`,"nav.codexAuth":`Codex 인증`,"nav.codexSet":`Codex 설정`,"codexSet.tab.multiauth":`다중 인증`,"codexSet.tab.prompt":`프롬프트`,"codexSet.prompt.title":`프롬프트 레이어`,"codexSet.prompt.timing":`새 세션부터 적용됩니다. 실행 중인 세션은 현재 설정을 유지합니다.`,"codexSet.prompt.staleRevision":`다른 곳에서 설정이 바뀌어 목록을 다시 불러왔습니다.`,"codexSet.prompt.writeFailed":`변경 내용을 저장하지 못했습니다.`,"codexSet.prompt.loadFailed":`프롬프트 레이어를 불러오지 못했습니다.`,"codexSet.prompt.repair":`복구`,"codexSet.prompt.repairFailed":`복구를 완료하지 못했습니다.`,"codexSet.drift.journalPresent":`이전 쓰기가 끝나지 않았습니다. 다음 쓰기에서 자동으로 복구됩니다.`,"codexSet.drift.projectionStale":`저장된 레이어와 config.toml의 값이 서로 다릅니다. 복구하면 레이어 기준으로 값을 다시 씁니다.`,"codexSet.drift.storeMissing":`레이어 파일이 없는데 config.toml에는 지침이 남아 있습니다. 복구하면 백업을 먼저 만들고 그 내용을 레이어 하나로 보존합니다.`,"codexSet.drift.ownedMalformed":`config.toml에 생성된 줄이 직접 수정되어 다시 쓰기가 안전하지 않습니다.`,"codexSet.custom.adoptUnsupported":`{path} {line}번째 줄의 값이 한 줄 문자열이 아니어서 가져올 수 없습니다. 여기서 관리하려면 직접 옮기세요.`,"codexSet.prompt.unreadable":`Codex 설정 파일이 있지만 읽을 수 없어 변경을 거부했습니다.`,"codexSet.layer.permissions":`권한`,"codexSet.layer.collaboration":`협업 모드`,"codexSet.layer.environment":`환경 정보`,"codexSet.layer.apps":`앱`,"codexSet.layer.skills":`스킬`,"codexSet.prompt.extensionsUnknown":`확장 프로그램은 자체 레이어를 추가할 수 있습니다. Codex가 이를 공개하지 않아 여기에 표시할 수 없습니다.`,"codexSet.group.transition":`전환 알림`,"codexSet.group.transitionDesc":`상태를 설명하는 대신 변화를 알리는 항목이라, 세션이 실시간 모드로 바뀌거나 모델이 교체될 때만 나타납니다.`,"codexSet.custom.slotNote":`커스텀 레이어는 이 순서대로 이어져 하나의 섹션이 됩니다.`,"codexSet.row.alwaysOn":`항상 켜짐`,"codexSet.row.onChange":`변경 시 전달`,"codexSet.row.featureGated":`[features]에서 설정`,"codexSet.row.openFeatures":`설정 열기`,"codexSet.dialog.setValue":`{value}(기본값 {fallback})`,"codexSet.dialog.copyKey":`키 복사`,"codexSet.dialog.unknownLayer":`이 빌드에는 이 레이어에 대한 설명이 없습니다. 대시보드보다 최신 Codex 런타임에서 온 레이어입니다.`,"codexSet.custom.heading":`커스텀 레이어`,"codexSet.custom.add":`+ 레이어 추가`,"codexSet.custom.newTitle":`새 레이어`,"codexSet.custom.editTitle":`레이어 편집`,"codexSet.custom.titleLabel":`제목`,"codexSet.custom.bodyLabel":`지침`,"codexSet.custom.bodySize":`{max}바이트 중 {bytes}바이트`,"codexSet.custom.normalized":`탭을 공백 4개로, 줄바꿈을 LF로 바꿨습니다.`,"codexSet.custom.titleRequired":`제목을 입력하세요.`,"codexSet.custom.titleTooLong":`제목이 {count}자입니다. 최대 {max}자까지 입력할 수 있습니다.`,"codexSet.custom.titleMultiline":`제목은 한 줄이어야 합니다.`,"codexSet.custom.bodyTooLarge":`이 레이어는 {bytes}바이트입니다. 제한은 {max}바이트입니다.`,"codexSet.custom.composedTooLarge":`활성 레이어를 합치면 {bytes}바이트로 제한을 초과합니다.`,"codexSet.custom.invalidCharacter":`{position} 위치의 제어 문자는 저장할 수 없습니다.`,"codexSet.custom.discardPrompt":`변경 내용을 버리시겠습니까?`,"codexSet.custom.keepEditing":`계속 편집`,"codexSet.custom.delete":`{title} 삭제`,"codexSet.custom.deleteConfirm":`이 레이어를 삭제하시겠습니까? 되돌릴 수 없습니다.`,"codexSet.custom.layerGone":`다른 곳에서 해당 레이어가 삭제되어 편집기를 닫았습니다.`,"codexSet.custom.deleteConfirmNamed":`“{title}” 레이어를 삭제하시겠습니까? 되돌릴 수 없습니다.`,"codexSet.custom.moveUp":`{title} 위로 이동`,"codexSet.custom.prevLayer":`이전 레이어`,"codexSet.custom.nextLayer":`다음 레이어`,"codexSet.custom.navPosition":`{position} / {total}`,"codexSet.custom.moveDown":`{title} 아래로 이동`,"codexSet.custom.limitReached":`커스텀 레이어는 최대 {max}개까지 보관할 수 있습니다.`,"codexSet.custom.notOwned":`developer_instructions가 opencodex 외부에서 작성되어 여기서는 편집할 수 없습니다. 레이어로 관리하려면 가져오세요.`,"codexSet.custom.adopt":`기존 지침 가져오기`,"codexSet.custom.adoptConfirm":`레이어로 가져오기`,"codexSet.custom.adoptRefused":`기존 값을 가져오지 못했습니다.`,"codexSet.custom.baseReplaced":`model_instructions_file이 {path}(으)로 설정되어 opencodex 외부에서 기본 프롬프트를 교체했습니다.`,"codexSet.lint.identity":`Codex가 설정한 것과 다른 정체성을 주장합니다.`,"codexSet.lint.foreignTool":`도구는 레지스트리에서 제공됩니다. 여기서 이름을 지정해도 도구가 생성되지 않습니다.`,"codexSet.lint.placeholder":`지침에는 템플릿 엔진이 실행되지 않으므로 이 내용이 그대로 전달됩니다.`,"codexSet.lint.applyPatch":`apply_patch는 지침이 아니라 도구 레지스트리에서 정의됩니다.`,"codexSet.lint.approvalVocab":`Codex가 자체 승인 용어를 삽입하므로 이 내용과 충돌할 수 있습니다.`,"codexSet.lint.environment":`환경 정보는 나중에 생성되므로 이 내용과 충돌할 수 있습니다.`,"codexSet.lint.size":`이 레이어는 8 KB를 초과합니다. 저장은 가능하지만 요청마다 토큰을 사용합니다.`,"codexSet.preset.blank":`빈 레이어`,"codexSet.preset.concise.name":`간결한 출력`,"codexSet.preset.concise.description":`짧게 답하고, 서론과 불필요한 서식을 생략합니다.`,"codexSet.preset.concise.provenance":`Claude Code의 간결성 지침을 바탕으로 각색했습니다. 직접 작성한 문구이며 복사본이 아닙니다.`,"codexSet.preset.planFirst.name":`편집 전 계획`,"codexSet.preset.planFirst.description":`계획을 먼저 밝힌 뒤 변경합니다.`,"codexSet.preset.planFirst.provenance":`Claude Code의 계획 중심 방식을 바탕으로 각색했습니다. 직접 작성한 문구이며 복사본이 아닙니다.`,"codexSet.preset.explainWhy.name":`이유 설명`,"codexSet.preset.explainWhy.description":`무엇을 했는지만 말하지 말고 이유도 설명합니다.`,"codexSet.preset.explainWhy.provenance":`Grok Build의 확인 방식을 바탕으로 각색했습니다. 직접 작성한 문구이며 복사본이 아닙니다.`,"codexSet.preset.testFirst.name":`테스트 우선`,"codexSet.preset.testFirst.description":`수정 전에 실패하는 테스트부터 작성합니다.`,"codexSet.preset.testFirst.provenance":`일반적인 에이전트 작업 방식을 바탕으로 각색했습니다. 직접 작성한 문구이며 복사본이 아닙니다.`,"codexSet.preset.korean.name":`한국어 답변`,"codexSet.preset.korean.description":`요청 언어와 관계없이 한국어로 답합니다.`,"codexSet.preset.korean.provenance":`자주 요청되는 항목을 바탕으로 opencodex용으로 작성했습니다. 직접 작성한 문구이며 복사본이 아닙니다.`,"codexSet.dialog.class":`종류`,"codexSet.dialog.key":`설정 키`,"codexSet.dialog.fileValue":`이 파일의 값`,"codexSet.dialog.absentDefault":`설정되지 않음(기본값: {value})`,"codexSet.dialog.noRenderedText":`Codex는 기본 제공 레이어의 조합된 텍스트를 공개하지 않습니다. 따라서 이 대화상자에는 내용 대신 레이어 설명과 키를 표시합니다.`,"codexSet.dialog.sourceText":`모델에 전달되는 원문`,"codexSet.dialog.sourceBytes":`{bytes}바이트`,"codexSet.dialog.notRendered":`확인한 턴에서는 이 레이어가 아무것도 보내지 않았습니다. 각 섹션은 내용이 바뀔 때만 다시 전송되므로, 한 번 확인한 것만으로는 빠져 보일 수 있습니다.`,"codexSet.dialog.emptySource":`{path} 파일은 있지만 비어 있어서 이 레이어는 아무것도 보내지 않습니다.`,"codexSet.dialog.notExposed":`기본 프롬프트는 Codex가 출력하는 메시지 목록 밖으로 전달되어 여기서 보여줄 수 없습니다. 대신 model_instructions_file로 교체할 수 있습니다.`,"codexSet.dialog.textUnavailable":`이 컴퓨터에서 Codex 프롬프트를 읽지 못해 원문을 표시할 수 없습니다.`,"codexSet.class.base":`기본 지침`,"codexSet.class.config-toggle":`여기서 전환 가능`,"codexSet.class.feature-gated":`기능 플래그 적용`,"codexSet.class.runtime-conditional":`런타임 조건부`,"codexSet.class.extension-unknown":`확장 레이어`,"codexSet.layer.base-instructions":`기본 지침`,"codexSet.layer.model-switch":`모델 전환 알림`,"codexSet.layer.personality":`성격`,"codexSet.layer.context-window-guidance":`컨텍스트 창 안내`,"codexSet.layer.realtime":`실시간`,"codexSet.layer.agents-md":`AGENTS.md`,"codexSet.layer.environments-instructions":`실행 환경`,"codexSet.layer.plugins":`플러그인`,"codexSet.layer.tools":`도구`,"codexSet.layer.multi-agent-mode":`멀티 에이전트 모드`,"codexSet.layer.git-attribution":`커밋 어트리뷰션`,"codexSet.about.base-instructions":`Codex 자체 지침입니다. 요청에 포함되며 끌 수 없습니다.`,"codexSet.about.model-switch":`대화 도중 세션 모델이 바뀌면 추가됩니다.`,"codexSet.about.personality":`기능 플래그로 제어되는 어조와 말투 지침입니다.`,"codexSet.about.context-window-guidance":`기능 플래그로 제어되는 남은 컨텍스트 예산 안내입니다.`,"codexSet.about.realtime":`실시간 세션에 추가됩니다.`,"codexSet.about.agents-md":`프로젝트의 AGENTS.md 파일입니다. 이 페이지는 레이어만 표시하며 프로젝트 문서를 수정하지 않습니다.`,"codexSet.about.permissions":`현재 적용 중인 샌드박스와 승인 설정을 설명합니다.`,"codexSet.about.collaboration":`활성 협업 모드를 설명합니다.`,"codexSet.about.environment":`작업 디렉터리, 플랫폼 및 기타 환경 정보입니다.`,"codexSet.about.environments-instructions":`기능 플래그로 제어되는 지연 실행 환경 지침입니다.`,"codexSet.about.apps":`연결된 앱의 사용 방법입니다.`,"codexSet.about.plugins":`플러그인을 선택했거나 플러그인이 기능을 제공하면 추가됩니다.`,"codexSet.about.tools":`기능 플래그로 제어되는 지연 로드 도구 설명입니다.`,"codexSet.about.skills":`사용 가능한 스킬 목록입니다.`,"codexSet.about.multi-agent-mode":`기능 플래그로 제어되는 서브에이전트 지침입니다.`,"codexSet.about.git-attribution":`모델이 작성한 커밋에 Co-authored-by: Codex 트레일러를, 새로 여는 풀 리퀘스트에 Generated with Codex. 한 줄을 붙이게 합니다. Codex가 계정에서 이 값을 가져오기 때문에 여기서도 [features]에서도 바꿀 수 없습니다. 계정에서 꺼두면 아무것도 보내지 않는 대신 반대 지시를 보냅니다.`,"codexSet.condition.model-switch":`세션 도중 모델이 바뀐 뒤에만 포함됩니다.`,"codexSet.condition.realtime":`실시간 세션에만 포함됩니다.`,"codexSet.condition.agents-md":`작업 디렉터리에서 프로젝트 문서를 찾으면 포함됩니다.`,"codexSet.condition.plugins":`플러그인을 선택했거나 플러그인이 기능을 제공하면 포함됩니다.`,"codexSet.condition.git-attribution":`계정의 어트리뷰션 정책이 결정합니다.`,"codexSet.base.title":`기본 프롬프트`,"codexSet.base.prev":`이전 옵션`,"codexSet.base.next":`다음 옵션`,"codexSet.base.position":`{position} / {total}`,"codexSet.base.swipeHint":`좌우로 스와이프하거나 방향키 또는 화살표 버튼으로 옵션을 넘깁니다. 새로 시작하는 세션에 적용됩니다.`,"codexSet.base.defaultTitle":`Codex 자체 기본 프롬프트`,"codexSet.base.defaultBody":`기본값은 여기에 저장되지 않으므로 편집하거나 삭제할 것이 없습니다. 선택하면 설정에서 model_instructions_file을 지우고 Codex가 기본 제공 프롬프트를 씁니다.`,"codexSet.base.variantTitle":`이름`,"codexSet.base.variantBody":`프롬프트`,"codexSet.base.replacesWarning":`Codex 자체 기본 프롬프트에 덧붙이는 게 아니라 통째로 바꿉니다. 여기에 짧게 쓰면 모델도 그만큼 짧은 지시만 받습니다.`,"codexSet.base.use":`이걸로 쓰기`,"codexSet.base.inUse":`사용 중`,"codexSet.base.externalBlocked":`model_instructions_file이 이미 {path}를 가리키고 있고, opencodex가 쓴 값이 아닙니다. 직접 지운 뒤 여기서 선택하세요.`,"nav.openMenu":`메뉴 열기`,"nav.closeMenu":`메뉴 닫기`,"integrations.subtitle":`클라이언트를 opencodex에 연결하고 자격 증명과 설정 복원을 관리합니다.`,"integrations.tabsLabel":`연동 화면`,"integrations.tab.overview":`개요`,"integrations.tab.keys":`API 키`,"integrations.tab.codex":`Codex`,"integrations.tab.claude":`Claude`,"integrations.tab.grok":`Grok Build`,"integrations.tab.opencode":`OpenCode`,"integrations.tab.pi":`Pi`,"integrations.tab.omp":`OMP`,"integrations.tab.hermes":`Hermes`,"integrations.tab.openclaw":`OpenClaw`,"integrations.tab.kimi":`Kimi Code`,"integrations.tab.gajae":`Gajae Code`,"integrations.tab.dsh":`DSH`,"integrations.tab.mcode":`MiniMax Code`,"integrations.tab.zcode":`ZCode`,"integrations.tab.prime":`Prime Agent`,"integrations.tab.aside":`Aside`,"integrations.codex.title":`Codex CLI`,"integrations.codex.body":`Codex 연결은 프록시 서비스가 관리합니다. opencodex를 시작하면 적용되고 서비스를 중지하면 기본 라우팅으로 복원됩니다.`,"integrations.codex.openService":`서비스 제어 열기`,"integrations.state.notInstalled":`미설치`,"integrations.state.unknown":`확인 중`,"integrations.detail.codexRouted":`Codex 요청이 이 프록시를 지납니다`,"integrations.detail.codexAbsent":`Codex는 아직 이 프록시를 지나지 않습니다`,"integrations.detail.keyCount":`키 {count}개 발급됨`,"integrations.detail.keyNone":`발급된 키 없음`,"integrations.detail.keyChecking":`확인 중…`,"integrations.detail.keyUnavailable":`키 상태를 확인할 수 없음`,"integrations.detail.claudeOff":`연결이 꺼져 있습니다`,"integrations.detail.desktopCurrent":`Desktop이 이 프로필로 실행됩니다`,"integrations.detail.desktopStale":`적용 후 프로필 파일이 바뀌었습니다`,"integrations.detail.desktopNotServed":`프로필은 있지만 Desktop이 다른 것을 씁니다`,"integrations.detail.desktopAbsent":`적용된 프로필이 없습니다`,"integrations.detail.desktopDesiredOff":`Claude Desktop 통합이 꺼져 있습니다`,"integrations.detail.desktopDesiredOffCleanupPending":`Claude Desktop이 여전히 게이트웨이를 사용 중입니다. 정리 대기 중`,"integrations.detail.desktopDesiredOnNotApplied":`통합은 켜져 있지만 Desktop이 게이트웨이 프로필을 사용하지 않습니다`,"integrations.detail.desktopSelectedElsewhere":`Desktop이 다른 프로필을 사용 중입니다`,"integrations.detail.desktopProfileDrift":`선택된 Desktop 프로필이 변경되었습니다`,"integrations.detail.desktopObservedUnsafe":`선택된 Desktop 프로필은 안전하게 변경할 수 없습니다`,"integrations.detail.desktopNotInstalled":`Claude Desktop 구성 라이브러리가 설치되지 않았습니다`,"integrations.dialog.desktop.title":`Claude Desktop 통합을 끌까요?`,"integrations.dialog.desktop.changes":`{path}에 opencodex 게이트웨이 프로필이 있으면 먼저 자격 증명 없는 표준 프로필을 선택한 뒤 이전 프로필과 백업을 제거합니다.`,"integrations.dialog.desktop.breakage":`Claude Desktop은 opencodex를 통한 모델 대신 표준 Claude로 돌아갑니다.`,"integrations.dialog.desktop.undo":`다시 켜면 저장된 모델 할당으로 opencodex 프로필을 새로 만듭니다.`,"integrations.dialog.desktop.restart":`Claude Desktop은 시작할 때만 이 구성을 읽습니다. 변경하려면 완전히 종료한 뒤 다시 여세요.`,"integrations.dialog.desktop.confirm":`해제`,"integrations.native.error.desktopUnsafeMetadata":`{path}의 Claude Desktop 메타데이터를 안전하게 읽을 수 없어 라이브러리를 변경하지 않았습니다.`,"integrations.native.error.desktopCleanupIncomplete":`Claude Desktop은 표준 모드를 가리키지만 이전 opencodex 자격 증명 파일이 남아 있습니다: {paths}.`,"integrations.native.msg.desktopDisabled":`Claude Desktop 통합을 해제했습니다.`,"integrations.native.msg.desktopEnabled":`Claude Desktop 통합을 켰습니다.`,"integrations.detail.grokModels":`모델 {count}개 연결됨`,"integrations.detail.grokAbsent":`설정에 opencodex 블록이 없습니다`,"integrations.dialog.grok.title":`Grok Build 연동을 해제할까요?`,"integrations.dialog.grok.changes":`{path}에서 opencodex가 표시해 둔 블록만 제거합니다. 블록 바깥에 직접 쓴 내용은 그대로 둡니다.`,"integrations.dialog.grok.breakage":`해제하면 Grok Build에서 opencodex 모델 별칭이 사라집니다. xAI 계정으로 쓰던 모델은 그대로입니다.`,"integrations.dialog.grok.undo":`opencodex가 loopback 주소로 실행 중이면, 다시 켤 때 지금 쓸 수 있는 모델 목록으로 블록을 새로 씁니다.`,"integrations.dialog.grok.confirm":`해제`,"integrations.native.msg.nonLoopbackRemoved":`Grok Build은 opencodex가 loopback 주소로 실행 중일 때만 자동 등록할 수 있습니다. loopback 주소를 가리키던 이전 블록은 제거했습니다.`,"integrations.native.msg.nonLoopbackRemovedNoop":`Grok Build은 opencodex가 loopback 주소로 실행 중일 때만 자동 등록할 수 있습니다. 제거할 이전 블록은 없었습니다.`,"integrations.native.msg.nonLoopbackSuperseded":`Grok Build은 opencodex가 loopback 주소로 실행 중일 때만 자동 등록할 수 있습니다. 그 사이 다른 곳에서 설정에 블록이 새로 쓰여, 지금 파일에 있는 블록은 이 요청이 만든 것이 아닙니다.`,"integrations.native.error.orphanedMarker":`{path}에 opencodex 시작 표시는 있는데 끝 표시가 없습니다. 어디까지가 우리 블록인지 확신할 수 없어 파일을 건드리지 않았습니다.`,"integrations.native.error.homeMismatch":`설치된 서비스의 홈과 현재 홈이 일치하지 않아 파일을 건드리지 않았습니다.`,"integrations.native.error.notInstalled":`Grok Build가 설치되어 있지 않아 변경할 수 없습니다.`,"integrations.native.error.configBusy":`다른 곳에서 설정을 저장하는 중이라 변경하지 못했습니다. 잠시 후 다시 시도해 주세요.`,"integrations.state.absent":`미적용`,"integrations.state.current":`적용됨`,"integrations.state.stale":`업데이트 필요`,"integrations.state.conflict":`충돌`,"integrations.state.unsafe":`확인 불가`,"integrations.summary.detected":`감지된 클라이언트`,"integrations.summary.applied":`설정된 클라이언트`,"integrations.summary.stale":`업데이트 필요`,"integrations.summary.lastChange":`마지막 변경`,"integrations.summary.disableAll":`모두 해제…`,"integrations.onboarding":`적용하면 먼저 백업을 보관한 뒤 opencodex 제공자 블록 하나만 씁니다. 해제는 그 블록만 제거하며 보관된 스냅샷으로 복원할 수 있습니다.`,"integrations.empty.title":`설치된 클라이언트가 감지되지 않았습니다`,"integrations.empty.body":`지원 클라이언트를 설치한 뒤 돌아와 opencodex를 적용하세요.`,"integrations.action.apply":`적용`,"integrations.action.disable":`해제`,"integrations.action.refresh":`업데이트`,"integrations.action.settings":`설정`,"integrations.action.manageKeys":`키 관리`,"integrations.action.restore":`복원…`,"integrations.action.undo":`되돌리기`,"integrations.action.restorePoint":`이 시점으로 복원…`,"integrations.action.snapshotExpired":`백업 만료됨`,"integrations.rollback.title":`복원 센터`,"integrations.rollback.empty":`아직 적용 기록이 없습니다`,"integrations.rollback.emptyBody":`모든 쓰기는 먼저 변경 전 스냅샷을 보관합니다.`,"integrations.catalog.title":`클라이언트`,"integrations.rollback.older":`이전 작업`,"integrations.rollback.showMore":`{n}개 더 보기`,"integrations.rollback.failed":`롤백 기록을 불러오지 못했습니다.`,"integrations.restore.title":`이 스냅샷으로 복원할까요?`,"integrations.restore.body":`현재 파일을 먼저 백업한 뒤 선택한 스냅샷으로 교체합니다.`,"integrations.restore.driftTitle":`스냅샷 이후 변경이 감지되었습니다`,"integrations.restore.driftBody":`스냅샷 이후의 변경이 백업으로 보관되고 파일이 교체됩니다.`,"integrations.restore.confirm":`복원`,"integrations.restore.confirmDrift":`새 변경을 백업하고 복원`,"integrations.restore.pending":`복원 중…`,"integrations.restore.manual":`자동 복원에 실패했습니다: {reason}. {path}에서 직접 복원하세요.`,"integrations.error.load":`연동 상태를 불러오지 못했습니다.`,"integrations.error.stale":`최신 새로고침에 실패했습니다. 아래 값은 오래된 정보일 수 있습니다.`,"integrations.error.busy":`이 클라이언트의 다른 변경이 진행 중입니다. 잠시 후 다시 시도하세요.`,"integrations.error.conflict":`opencodex가 쓴 뒤 설정이 변경되었습니다. 아무 내용도 제거하지 않았습니다.`,"integrations.error.unsafe":`설정을 안전하게 변경할 수 없습니다.`,"integrations.error.generic":`연동 변경에 실패했습니다. 이전 상태는 유지되었습니다.`,"integrations.error.nonLoopback":`{client}은(는) localhost 프록시에만 연결할 수 있습니다. 원격 바인드에 필요한 인증 헤더를 넣을 자리가 설정 파일에 없어 직접 작성해도 마찬가지입니다. 터널이나 로컬 포워더로 loopback 경로를 열어주세요.`,"integrations.status.installed":`설치 감지됨`,"integrations.status.notInstalled":`설치되지 않음`,"integrations.status.appliedAt":`적용`,"integrations.status.backup":`백업`,"integrations.status.lastRestore":`마지막 복원`,"integrations.status.unknown":`알 수 없음`,"integrations.bulk.title":`적용된 클라이언트 연동을 해제할까요?`,"integrations.bulk.body":`opencodex가 소유한 블록만 제거합니다. 각 클라이언트의 변경 전 스냅샷을 보관합니다.`,"integrations.bulk.partial":`일부 클라이언트를 해제하지 못했습니다: {clients}`,"integrations.bulk.success":`적용된 클라이언트 연동을 해제했습니다.`,"integrations.retention.degraded":`백업 정리가 밀려 있습니다 — 오래된 백업이 남아 있을 수 있습니다.`,"integrations.error.residual":`파일이 중간 상태로 남았을 수 있습니다: {message} {path}에서 복원하세요.`,"integrations.error.recover":`{message} 백업 위치는 {path}입니다.`,"integrations.kind.apply":`적용`,"integrations.kind.disable":`해제`,"integrations.kind.refresh":`업데이트`,"integrations.kind.restore":`복원`,"integrations.kind.overwrite":`덮어씀`,"integrations.dialog.overwrite.title":`이 설정 파일의 블록을 덮어쓸까요?`,"integrations.dialog.overwrite.changesUnowned":`{path}에서 opencodex가 써야 하는 자리를 우리가 쓰지 않은 블록이 차지하고 있습니다. 적용하면 opencodex가 쓰는 블록으로 바꿉니다.`,"integrations.dialog.overwrite.changesForeign":`{path}의 opencodex 블록에 직접 넣은 수정은 버리고 opencodex가 쓰는 블록으로 바꿉니다.`,"integrations.dialog.overwrite.breakage":`그 블록이 설정하던 동작은 더 이상 적용되지 않습니다. 파일의 다른 부분은 그대로 둡니다.`,"integrations.dialog.overwrite.undo":`먼저 스냅숏을 저장하므로 아래 되돌리기 목록에 남고 다시 되돌릴 수 있습니다.`,"integrations.dialog.overwrite.confirm":`덮어쓰기`,"integrations.action.overwrite":`덮어쓰기`,"integrations.semantics.opencode":`디스크에서 직접 실행할 때만 적용됩니다. ocx opencode의 환경 주입이 우선합니다.`,"integrations.semantics.pi":`새 세션부터 적용됩니다.`,"integrations.semantics.omp":`카탈로그를 불러오려면 OMP를 재시작하세요.`,"integrations.semantics.hermes":`새 세션부터 적용됩니다.`,"integrations.semantics.openclaw":`실행 중인 게이트웨이에 즉시 반영됩니다.`,"integrations.semantics.kimi":`재시작 또는 /reload 시 적용됩니다 (v2는 파일 변경을 감지합니다).`,"integrations.semantics.gajae":`새 세션 또는 /model을 열 때 적용됩니다.`,"integrations.semantics.dsh":`OpenCodex는 $DSH_HOME/settings.yaml의 llm-pi-ai.providers.opencodex만 관리합니다. DSH는 이 provider를 hot reload하며 기본 model과 deepseek-official은 변경하지 않습니다. 현재 loopback 전용이며 실제 credential을 기록하지 않습니다.`,"integrations.semantics.mcode":`custom_provider.opencodex만 관리하며 기본 모델과 MiniMax 로그인은 변경하지 않습니다.`,"integrations.semantics.zcode":`~/.zcode/v2/config.json의 provider.opencodex만 관리하며 Z.ai 로그인과 다른 프로바이더는 변경하지 않습니다. 변경 후 ZCode를 재시작하세요.`,"integrations.semantics.prime":`Prime Agent의 models.json에서 providers.opencodex만 관리합니다. 위치는 ~/.prime/agent이며 PRIME_AGENT_CODING_AGENT_DIR가 설정되면 그쪽이 우선합니다. 다른 프로바이더와 모델 오버라이드는 변경하지 않습니다. 새 세션부터 적용됩니다.`,"integrations.semantics.aside":`로그인된 계정의 Aside models.json에서 providers.opencodex만 관리합니다. 위치는 ~/.aside/u/<계정>이며 다른 프로바이더는 변경하지 않습니다. Aside는 실행 중에 이 파일을 다시 쓰기 때문에 적용한 뒤 Aside를 완전히 종료하고 다시 여세요.`,"codexAuth.mainAccount":`메인 계정`,"codexAuth.logLabel":`로그 라벨`,"codexAuth.codexApp":`Codex App`,"codexAuth.moreActions":`추가 작업 표시`,"codexAuth.copyId":`계정 ID 복사`,"codexAuth.appLogin":`앱 로그인`,"codexAuth.accountPool":`계정 풀`,"codexAuth.accountModeTitle":`OpenAI 계정 모드`,"codexAuth.accountModePool":`풀 모드`,"codexAuth.accountModePoolDesc":`메인 로그인과 사용 가능한 추가 계정이 여기에서 순환됩니다.`,"codexAuth.accountModeDirect":`직접 모드`,"codexAuth.accountModeDirectDesc":`요청은 메인 로그인만 사용하며, 추가 계정은 풀 모드용으로 계속 저장됩니다.`,"codexAuth.openaiMissing":`내장 OpenAI 프로바이더가 설정되지 않았습니다.`,"codexAuth.openaiDisabled":`내장 OpenAI 프로바이더가 비활성화되어 있습니다.`,"codexAuth.openaiUnavailableDesc":`OpenAI 계정은 그대로 사용할 수 있습니다. Codex 요청을 라우팅하려면 프로바이더를 활성화하세요.`,"codexAuth.enableOpenai":`OpenAI 활성화`,"codexAuth.enablingOpenai":`활성화 중...`,"codexAuth.enableOpenaiFailed":`OpenAI 공급자를 활성화하지 못했습니다.`,"codexAuth.openaiPresetLoadFailed":`OpenAI 공급자 프리셋을 불러오지 못했습니다.`,"codexAuth.openaiPresetUnavailable":`OpenAI 공급자 프리셋을 사용할 수 없습니다.`,"codexAuth.openProviders":`프로바이더 열기`,"codexAuth.add":`추가`,"codexAuth.sparkQuota":`Codex Spark 할당량`,"codexAuth.sparkQuotaHint":`계정 카드에 GPT-5.3-Codex-Spark 주간 창을 표시합니다. 모델 하나에만 적용되므로 기본값은 숨김입니다.`,"codexAuth.sparkQuotaShown":`Codex Spark 할당량을 표시합니다`,"codexAuth.sparkQuotaHidden":`Codex Spark 할당량을 숨겼습니다`,"codexAuth.sparkQuotaFailed":`Codex Spark 할당량 설정을 바꾸지 못했습니다`,"codexAuth.refreshQuota":`할당량 새로고침`,"codexAuth.refreshingQuota":`새로고침 중...`,"codexAuth.quotaRefreshed":`할당량을 다시 조회했습니다`,"codexAuth.quotaRefreshFailed":`할당량 재조회에 실패했습니다`,"codexAuth.pauseExhausted":`한도 도달 계정 일시 중지`,"codexAuth.pausingExhausted":`할당량 확인 중...`,"codexAuth.pauseExhaustedSucceeded":`한도에 도달해 일시 중지된 계정: {count}`,"codexAuth.pauseExhaustedNone":`사용량 100%가 확인된 계정이 없습니다.`,"codexAuth.pauseExhaustedFailed":`한도 도달 계정을 확인하고 일시 중지하지 못했습니다.`,"codexAuth.noPool":`풀 계정이 아직 없습니다.`,"codexAuth.pause":`일시 중지`,"codexAuth.resume":`재개`,"codexAuth.paused":`일시 중지됨`,"codexAuth.pauseSucceeded":`{email} 계정을 일시 중지했습니다`,"codexAuth.resumeSucceeded":`{email} 계정을 풀에서 다시 사용할 수 있습니다`,"codexAuth.pauseFailed":`{email} 계정을 일시 중지하지 못했습니다. 변경 사항이 없습니다.`,"codexAuth.resumeFailed":`{email} 계정을 재개하지 못했습니다. 변경 사항이 없습니다.`,"codexAuth.pausedHint":`재개할 때까지 자동 전환, 재시도, 쿨다운 복구 및 수동 선택에서 제외됩니다.`,"codexAuth.pinned":`고정됨`,"codexAuth.pinnedHint":`직접 선택한 계정이므로 더 높은 선택 순서가 이 계정을 앞지르지 않습니다. 고정은 이 계정이 소진되거나, 다른 계정을 선택하거나, 어떤 계정이든 선택 순서를 변경할 때까지 유지됩니다.`,"codexAuth.fiveHour":`5시간`,"codexAuth.weekly":`주간`,"codexAuth.monthly":`30일`,"codexAuth.resets":`리셋`,"codexAuth.today":`오늘`,"codexAuth.current":`현재`,"codexAuth.nextSession":`선택됨`,"codexAuth.poolPrepared":`풀 모드 준비됨`,"codexAuth.preparePoolTitle":`이 계정을 풀 모드용으로 준비할까요?`,"codexAuth.preparePoolDesc":`직접 모드 요청은 계속 메인 로그인을 사용합니다. 풀 모드를 켜면 이 계정이 준비된 풀 선택으로 사용됩니다.`,"codexAuth.prepareForPool":`풀 모드용으로 준비`,"codexAuth.poolPreparedToast":`{email} 계정을 풀 모드용으로 준비했습니다`,"codexAuth.switchTitle":`활성 계정을 변경하시겠습니까?`,"codexAuth.switchDesc":`즉시 적용됩니다. 계정에 바인딩된 기존 스레드와 이미 진행 중인 요청은 기존 계정을 유지하고, 새 요청이나 바인딩 없는 요청은 선택한 계정의 순서 티어를 사용합니다. 같은 선택 순서의 계정은 계속 번갈아 사용됩니다.`,"codexAuth.cacheWarning":`계정 전환 시 프롬프트 캐시가 초기화됩니다.`,"codexAuth.setAsNext":`이 계정을 다음에 사용`,"codexAuth.cancel":`취소`,"codexAuth.switchBack":`메인 계정으로 돌아가시겠습니까?`,"codexAuth.switchBackDesc":`즉시 적용됩니다. 계정에 바인딩된 기존 스레드와 이미 진행 중인 요청은 기존 계정을 유지하고, 새 요청이나 바인딩 없는 요청은 앱 로그인 계정의 순서 티어를 사용합니다. 같은 선택 순서의 계정은 계속 번갈아 사용됩니다.`,"codexAuth.autoSwitch":`사용량 기반 선제 전환`,"codexAuth.autoSwitchQuotaDesc":`할당량: 사용량이 {threshold}% 이상이면 이미 바인딩된 작업을 포함해 다음 요청이 사용량이 더 낮은 적격 계정으로 이동할 수 있습니다. Go/Free는 30일만 봅니다.`,"codexAuth.autoSwitchQuotaOffDesc":`사용량 기반 선제 전환이 꺼져 있습니다. 새 작업/바인딩 없는 작업 배정과 실패 복구는 계속 적용됩니다.`,"codexAuth.autoSwitchRoundRobinDesc":`라운드로빈 배정은 이 임계값을 사용하지 않으며, 바인딩 없는 새 작업을 계속 순환합니다.`,"codexAuth.autoSwitchFillFirstDesc":`필 퍼스트: {threshold}%는 새 작업/바인딩 없는 작업의 소진 기준이며, 정상적인 바인딩 작업은 계정을 유지합니다.`,"codexAuth.autoSwitchFillFirstOffDesc":`필 퍼스트에는 새 작업/바인딩 없는 작업의 사용량 소진 기준이 없습니다. 쿨다운, 재인증, 실패 복구는 여전히 라우팅을 바꿀 수 있습니다.`,"codexAuth.failureRecoveryNote":`실패 복구는 별도입니다. 출력 전 429/402 거절, 쿨다운, 재인증, 제외 또는 설정된 일시적 장애 조치로 다른 적격 계정이 선택될 수 있습니다.`,"codexAuth.autoSwitchThreshold":`사용량 임계값`,"codexAuth.autoSwitchThresholdAria":`사용량 임계값(퍼센트)`,"codexAuth.autoSwitchThresholdInc":`사용량 임계값 증가`,"codexAuth.autoSwitchThresholdDec":`사용량 임계값 감소`,"codexAuth.autoSwitchLoadFailed":`사용량 기반 전환 설정을 불러오지 못했습니다.`,"codexAuth.autoSwitchThresholdInvalid":`1~100 사이의 정수를 입력하세요`,"codexAuth.autoSwitchUpdated":`사용량 기반 선제 전환 설정을 저장했습니다`,"codexAuth.autoSwitchUpdateFailed":`사용량 기반 전환 설정 변경을 확인하지 못했습니다. 마지막으로 확인된 값을 표시합니다.`,"codexAuth.requestUserInput":`Default 모드에서 입력 요청`,"codexAuth.requestUserInputDesc":`Default 모드 세션에서 Codex가 일시 중지하고 request_user_input 도구로 질문할 수 있게 합니다.`,"codexAuth.requestUserInputUpdated":`기능 플래그가 업데이트되었습니다 - 새 세션부터 적용됩니다.`,"codexAuth.requestUserInputUpdatedRestart":`기능 플래그가 업데이트되었습니다 - 새 세션부터 적용됩니다. Codex 앱을 다시 시작하세요.`,"codexAuth.requestUserInputUpdateFailed":`기능 플래그를 업데이트하지 못했습니다. 변경된 내용이 없습니다.`,"codexAuth.requestUserInputLoadFailed":`config.toml에서 기능 플래그를 읽지 못했습니다.`,"codexAuth.accountPickerTitle":`모델 선택기에서 사용할 Codex 계정 지정`,"codexAuth.accountPickerOffDesc":`활성화하면 일반 GPT 선택기 항목이 계정 선택기별 항목으로 대체되어 로그아웃하지 않고도 대화에 사용할 정확한 계정을 선택할 수 있습니다. 비활성화해도 계정은 삭제되지 않습니다.`,"codexAuth.accountPickerOnDesc":`각 선택기는 저장된 계정 하나를 나타내는 공개 레이블입니다. 선택하면 해당 대화가 그 계정에 고정되며 Pool 순환이나 대체가 일어나지 않고 활성 Pool 계정도 바뀌지 않습니다.`,"codexAuth.accountPickerCompatibility":`기본 Codex App 로그인에는 자체 선택기가 있습니다. 생성된 맵에서는 보통 main을 사용하며, 충돌 시 main-2 같은 안전한 접미사가 붙습니다. 추가 계정에는 안정적인 개인정보 보호 레이블이 부여되고 사용자 지정 선택기 이름은 유지됩니다. 기존 대화와 저장된 모델 선택은 계속 라우팅됩니다. 비활성화하면 생성된 항목만 숨기며 선택기와 정확한 경로는 보존됩니다. 일반 GPT 모델 ID는 기존 Pool 또는 Direct 동작을 유지합니다.`,"codexAuth.accountPickerUpdated":`계정 지정 설정을 업데이트했습니다.`,"codexAuth.accountPickerUpdateFailed":`계정 지정 설정을 업데이트하지 못했습니다. 마지막으로 확인된 설정을 표시합니다.`,"codexAuth.accountPickerLoadFailed":`계정 지정 설정을 불러오지 못했습니다.`,"codexAuth.accountPickerRefreshFailed":`이 설정을 새로 고치지 못했습니다. 마지막으로 확인된 값을 계속 표시합니다.`,"codexAuth.advancedSettings":`고급 설정`,"codexAuth.advancedSettingsAria":`고급 Codex 인증 설정 표시 또는 숨기기`,"codexAuth.catalogRefreshPending":`변경 사항은 저장되었지만 Codex 모델 카탈로그 새로 고침이 보류 중입니다. ocx sync를 실행해 다시 시도하세요.`,"anthropicPool.title":`Claude 계정 풀(실험적)`,"anthropicPool.enabledDesc":`429 시 계정을 쿨다운하고 장애 조치합니다. 새 세션은 {window}이 {threshold}% 미만인 계정을 우선합니다.`,"anthropicPool.enabledNoProactiveDesc":`429 시 계정을 쿨다운하고 장애 조치합니다. 임계값 0에서는 사용량 기반 사전 전환이 꺼지지만, 새 세션 선택과 429 복구는 여전히 {window} 창을 사용합니다.`,"anthropicPool.disabledDesc":`활성 Claude 계정만 사용합니다. 실험적 라우팅을 감수할 때만 켜세요.`,"anthropicPool.experimentalWarning":`실험적이며 충분히 검증되지 않았습니다. 자동 다중 계정 로테이션처럼 보이는 동작은 Anthropic이 계정을 제한할 수 있습니다. 같은 조직은 할당량을 공유할 수 있어 풀링이 도움이 되지 않을 수 있습니다. 위험을 이해하지 못하면 꺼 두세요.`,"anthropicPool.needTwoAccounts":`풀을 켜기 전에 Claude OAuth 계정을 두 개 이상 추가하세요.`,"anthropicPool.threshold":`새 세션 사용량 임계값`,"anthropicPool.thresholdAria":`새 세션 사용량 임계값(퍼센트)`,"anthropicPool.thresholdHelp":`0은 할당량 기반 선택을 끕니다(어피니티 + 활성 계정만). 기본값 80.`,"anthropicPool.thresholdInvalid":`0에서 100 사이의 정수를 입력하세요`,"anthropicPool.loadFailed":`Claude 풀 설정을 불러오지 못했습니다.`,"anthropicPool.saveFailed":`Claude 풀 설정을 저장하지 못했습니다.`,"anthropicPool.on":`켜짐`,"anthropicPool.off":`꺼짐`,"accountPool.strategy":`로테이션 전략`,"accountPool.strategyDesc":`OpenCodex가 새 작업/바인딩 없는 작업에 계정을 배정하는 방식입니다.`,"accountPool.strategyQuota":`할당량`,"accountPool.strategyRoundRobin":`라운드로빈`,"accountPool.strategyFillFirst":`필 퍼스트`,"accountPool.strategyHintQuota":`할당량 전략은 사용량 임계값을 넘으면 기존 작업의 다음 요청도 다른 계정에 다시 바인딩할 수 있습니다.`,"accountPool.strategyHintRoundRobin":`라운드로빈은 현재 바인딩이 없는 작업만 순환하며, 사용량 임계값은 기본 순환에 영향을 주지 않습니다.`,"accountPool.strategyHintFillFirst":`필 퍼스트는 임계값을 바인딩 없는 작업의 소진 기준으로 사용하며, 정상적인 바인딩 작업은 어피니티를 유지합니다.`,"accountPool.unboundDefinition":`새 작업/바인딩 없는 작업은 현재 계정 바인딩이 없는 요청입니다. 기존에 보이던 작업도 프록시나 어피니티 상태가 초기화되면 바인딩이 없어질 수 있습니다.`,"accountPool.stickyLimit":`회전 전 새 작업/바인딩 없는 작업 배정 횟수`,"accountPool.stickyLimitAria":`회전 전 새 작업/바인딩 없는 작업 배정 횟수`,"accountPool.stickyLimitInc":`스티키 한도 증가`,"accountPool.stickyLimitDec":`스티키 한도 감소`,"accountPool.stickyLimitHelp":`다음 계정으로 넘어가기 전에 이 횟수의 새 작업/바인딩 없는 작업을 선택 계정에 배정합니다. 카운터는 업스트림 성공 후가 아니라 작업을 바인딩할 때 증가합니다.`,"accountPool.stickyLimitInvalid":`1에서 100 사이의 정수를 입력하세요`,"accountPool.strategyLoadFailed":`로테이션 전략을 불러오지 못했습니다.`,"accountPool.strategyUpdateFailed":`로테이션 전략을 저장하지 못했습니다.`,"accountPool.quotaWindow":`할당량 기준 구간`,"accountPool.quotaWindowDesc":`할당량 기반 새 세션 선택, 필 퍼스트 임계값 판정, 가능한 429 대체 계정에 사용할 캐시 사용량 기준을 정합니다.`,"accountPool.quotaWindowFiveHour":`5시간 사용량`,"accountPool.quotaWindowWeekly":`주간 사용량`,"accountPool.quotaWindowMaxUtilization":`더 높은 사용량`,"accountPool.quotaWindowHint":`주간은 다른 사용 가능한 계정이 남아 있을 때만 5시간 사용량이 소진된 계정을 건너뛰고, 아무 계정도 남지 않으면 해당 계정으로 폴백합니다. 주간 사용량이 같으면 5시간 사용량이 더 낮은 계정을 고르며, 계정별 주간 사용량은 공급자 페이지에서 조회한 뒤에만 알 수 있습니다.`,"accountPool.quotaWindowInert":`할당량 전략, 또는 임계값이 0보다 큰 필 퍼스트만 사용량을 기준으로 점수를 매깁니다. 현재 로테이션 전략에서는 이 설정이 아무 영향을 주지 않습니다.`,"accountPool.priority":`선택 순서`,"accountPool.priorityAria":`이 계정의 선택 순서`,"accountPool.priorityHint":`숫자가 클수록 먼저 사용됩니다. 위에 있는 계정이 모두 소진되거나 사용할 수 없을 때에만 더 낮은 숫자로 넘어갑니다.`,"accountPool.priorityFirst":`가장 먼저`,"accountPool.priorityEarlier":`먼저`,"accountPool.priorityNormal":`기본`,"accountPool.priorityLater":`나중에`,"accountPool.priorityLast":`가장 마지막`,"accountPool.priorityOption":`{name} ({value})`,"accountPool.priorityCustom":`사용자 지정`,"accountPool.priorityUpdated":`{email}의 선택 순서를 업데이트했습니다`,"accountPool.priorityUpdateFailed":`{email}의 선택 순서를 저장하지 못했습니다. 마지막으로 확인된 값을 표시합니다.`,"codexAuth.switched":`다음 요청에 {email}을(를) 사용합니다`,"codexAuth.loadFailed":`Codex 계정 설정을 불러오지 못했습니다.`,"codexAuth.switchFailed":`계정을 전환하지 못했습니다. 이전 선택은 그대로 유지됩니다.`,"codexAuth.removeConfirm":`{id}을(를) 삭제하시겠습니까?`,"codexAuth.removeFailed":`계정을 제거하지 못했습니다. 변경된 내용은 없습니다.`,"codexAuth.addTitle":`Codex 계정 추가`,"codexAuth.addIdLabel":`계정 ID (슬러그)`,"codexAuth.addJsonLabel":`auth.json 내용`,"codexAuth.addHelp":`다른 머신의 ~/.codex/auth.json을 복사하거나, codex-auth export를 사용하세요.`,"codexAuth.importBtn":`가져오기`,"codexAuth.importInvalidJson":`유효하지 않은 JSON`,"codexAuth.importMissingTokens":`JSON에 access_token 또는 refresh_token이 없습니다`,"codexAuth.importMissingId":`계정 ID를 입력하세요`,"codexAuth.accountAdded":`풀에 계정이 추가되었습니다`,"codexAuth.addPickDesc":`다른 ChatGPT 계정으로 로그인하여 풀에 추가하세요.`,"codexAuth.oauthLogin":`OAuth 로그인`,"codexAuth.oauthDesc":`브라우저에서 ChatGPT 로그인 열기`,"codexAuth.deviceLogin":`기기 코드 로그인`,"codexAuth.deviceDesc":`헤드리스나 원격 프록시용. 다른 기기에서 짧은 코드를 입력합니다`,"codexAuth.importAuthJson":`auth.json 가져오기`,"codexAuth.importAuthJsonDesc":`다른 Codex 설치 또는 codex-auth export에서`,"codexAuth.back":`뒤로`,"codexAuth.oauthAlreadyInProgress":`로그인이 이미 진행 중입니다. 브라우저에서 완료하세요.`,"codexAuth.oauthWaiting":`브라우저에서 ChatGPT 로그인 완료를 기다리는 중...`,"codexAuth.oauthSubmittingCode":`코드를 제출 중…`,"codexAuth.oauthCodeSubmitted":`코드를 제출했습니다 — 로그인 완료를 기다리는 중입니다…`,"codexAuth.oauthStatusRetrying":`로그인 상태를 확인하는 중 네트워크 또는 프록시 오류가 발생했습니다 — 재시도 중…`,"codexAuth.oauthCancelled":`로그인이 취소되었습니다.`,"codexAuth.loginFailed":`로그인에 실패했습니다`,"codexAuth.needsReauth":`재로그인`,"codexAuth.reauthenticate":`Re-authenticate`,"codexAuth.tokenExpired":`토큰 만료 — 이 계정을 다시 인증하세요`,"codexAuth.mainTokenExpired":`토큰 만료 — Codex 앱 로그인으로 다시 로그인하세요`,"codexAuth.emailCollision":`이 계정은 메인 Codex 로그인과 동일합니다. 다른 계정을 사용하세요.`,"codexAuth.resetCreditsTitle":`리셋 크레딧`,"codexAuth.resetCreditsAvailable":`사용 가능한 리셋 크레딧이 {count}개 있습니다.`,"codexAuth.resetCreditsDesc":`크레딧 1개로 현재 시간/주간 사용량 제한을 즉시 초기화합니다.`,"codexAuth.noResetCredits":`사용 가능한 리셋 크레딧이 없습니다.`,"codexAuth.earnCreditsHint":`크레딧은 매월 자동 지급되며 추천 프로그램으로도 획득할 수 있습니다.`,"codexAuth.creditsExpireNote":`크레딧은 획득 후 30일 뒤 만료됩니다.`,"codexAuth.useOneCredit":`크레딧 1개 사용`,"codexAuth.confirmResetTitle":`리셋 크레딧을 사용하시겠습니까?`,"codexAuth.confirmResetDesc":`현재 사용량 제한이 즉시 초기화됩니다. 남은 크레딧: {count}개.`,"codexAuth.irreversible":`이 작업은 되돌릴 수 없습니다.`,"codexAuth.useCredit":`크레딧 사용`,"codexAuth.redeeming":`초기화 중...`,"codexAuth.resetSuccess":`사용량 제한이 초기화되었습니다! 남은 크레딧: {remaining}개.`,"codexAuth.resetSuccessGeneric":`사용량 제한이 초기화되었습니다!`,"codexAuth.resetAlreadyRedeemed":`이 크레딧은 이미 사용되었습니다. 크레딧은 변경되지 않았습니다.`,"codexAuth.resetNothingToReset":`현재 초기화할 사용량 윈도우가 없습니다.`,"codexAuth.resetNoCredit":`사용 가능한 리셋 크레딧이 없습니다.`,"codexAuth.resetError":`리셋 크레딧 사용에 실패했습니다. 다시 시도해 주세요.`,"codexAuth.fifoNote":`가장 오래된 크레딧부터 사용됩니다.`,"codexAuth.confirmWhichCredit":`{date}에 획득한 크레딧이 사용됩니다.`,"codexAuth.creditNext":`다음 사용 대상`,"codexAuth.creditLabel":`크레딧 #{n}`,"codexAuth.creditNextBadge":`NEXT`,"codexAuth.creditGranted":`획득 {date}`,"codexAuth.creditExpires":`만료 {date} ({days}일 남음)`,"api.title":`API 액세스`,"api.subtitle":`생성한 API 키로 외부 앱에서 opencodex 프록시에 접속합니다. 인증은 {authHeader} 헤더로 하며, 엔드포인트별로 받는 헤더는 아래 표에 있습니다.`,"api.endpointNote":`기본 URL을 OpenAI 호환 클라이언트에 사용하세요. Responses와 Chat Completions는 /v1 아래에 제공됩니다.`,"api.baseUrl":`기본 URL`,"api.responsesEndpoint":`Responses API`,"api.chatCompletionsEndpoint":`Chat Completions API`,"api.messagesEndpoint":`Messages API`,"api.modelsEndpoint":`Models API`,"api.endpointsTitle":`게이트웨이 엔드포인트`,"api.authBaseUrlNote":`클라이언트에는 기본 URL을 설정한 뒤 아래에서 프로토콜별 엔드포인트를 선택하세요.`,"api.authTitle":`인증`,"api.authLoopback":`루프백 바인드(127.0.0.1 또는 ::1)는 인증을 건너뜁니다. 원격 바인드는 생성된 ocx_ 키 또는 OPENCODEX_API_AUTH_TOKEN이 필요합니다.`,"api.modelsTitle":`외부 모델 카탈로그`,"api.modelsCount":`{count}개 호출 가능`,"api.modelsSearch":`모델 검색`,"api.modelsSubtitle":`이 정확한 모델 ID를 /v1/models와 선택한 인바운드 프로토콜과 함께 사용하세요.`,"api.modelsLoading":`모델 불러오는 중…`,"api.modelsEmpty":`아직 외부에서 호출 가능한 모델이 없습니다.`,"api.modelsNoMatch":`“{query}”와 일치하는 모델이 없습니다.`,"api.modelsLoadFailed":`외부 모델 카탈로그를 불러오지 못했습니다.`,"api.colModel":`모델`,"api.colSource":`출처`,"api.colProtocols":`프로토콜`,"api.copyModelId":`ID 복사`,"api.modelCopied":`복사됨`,"api.testModel":`테스트`,"api.testingModel":`테스트 중…`,"api.testSucceeded":`확인`,"api.testFailed":`실패`,"api.protocolResponses":`Responses`,"api.protocolChatCompletions":`Chat Completions`,"api.protocolMessages":`Messages`,"api.sourceNative":`ChatGPT 풀`,"api.sourceCombo":`콤보 경로`,"api.sourceCustom":`사용자 정의`,"api.usageResponsesTitle":`Responses 예시`,"api.usageChatTitle":`Chat Completions 예시`,"api.usageMessagesTitle":`Messages 예시`,"api.newKeyTitle":`새 키 생성됨`,"api.newKeyNote":`지금 키를 복사하세요. 다시 표시되지 않습니다.`,"api.copy":`복사`,"api.copied":`복사됨`,"api.dismiss":`닫기`,"api.generateTitle":`키 생성`,"api.keyNamePlaceholder":`키 이름 (선택)`,"api.generate":`생성`,"api.generating":`생성 중…`,"api.activeKeys":`활성 키 ({count})`,"api.activeKeysLoading":`활성 키`,"api.noKeys":`아직 API 키가 없습니다. 위에서 하나 생성하세요.`,"api.workspace.sections":`API 섹션`,"api.section.keys":`키`,"api.section.connect":`연결`,"api.section.endpoints":`엔드포인트`,"api.section.models":`모델`,"api.section.examples":`예제`,"api.workspace.details":`API 키 세부 정보`,"api.workspace.keyDetails":`키 세부 정보`,"api.workspace.keyPrefix":`키 접두사`,"api.workspace.deleteKey":`키 삭제`,"api.workspace.deleteConfirm":`이 키를 삭제하시겠습니까? 되돌릴 수 없습니다.`,"api.workspace.usageExamples":`사용 예제`,"api.copyUrlHint":`클릭하여 URL 복사`,"api.urlCopied":`URL 복사됨`,"api.copyExampleHint":`클릭하여 예제 복사`,"api.exampleCopied":`예제 복사됨`,"api.colName":`이름`,"api.colKey":`키`,"api.colCreated":`생성일`,"api.confirm":`확인`,"api.deleteAria":`API 키 삭제`,"api.usageSampleInput":`안녕하세요, 세계!`,"api.clientConfig.title":`클라이언트 설정`,"api.clientConfig.rowsLabel":`클라이언트 연결`,"api.clientConfig.details":`자세히`,"api.clientConfig.detailsAria":`{client} 설정 자세히 보기`,"api.clientConfig.copyAria":`{client} 설정 복사`,"api.clientConfig.downloadAria":`{client} 설정 다운로드`,"api.clientConfig.rowMeta":`{destination} · 모델 {count}개`,"api.clientConfig.rowError":`{client} 설정을 만들지 못했습니다.`,"api.clientConfig.copiedAnnounceClient":`{client} 설정을 클립보드에 복사했습니다.`,"api.clientConfig.clientOpencode":`OpenCode`,"api.clientConfig.clientPi":`Pi`,"api.clientConfig.clientOmp":`OMP`,"api.clientConfig.clientHermes":`Hermes`,"api.clientConfig.clientOpenclaw":`OpenClaw`,"api.clientConfig.clientKimi":`Kimi Code`,"api.clientConfig.clientGajae":`Gajae Code`,"api.clientConfig.clientDsh":`DeepSeek Harness (DSH)`,"api.clientConfig.clientMcode":`MiniMax Code`,"api.clientConfig.clientZcode":`ZCode`,"api.clientConfig.clientPrime":`Prime Agent`,"api.clientConfig.clientAside":`Aside`,"api.clientConfig.copy":`설정 복사`,"api.clientConfig.download":`다운로드`,"api.clientConfig.loading":`클라이언트 설정 생성 중…`,"api.clientConfig.jsonLabel":`{client} 설정`,"api.clientConfig.destination":`대상 파일`,"api.clientConfig.envHint":`실행 전 키 설정`,"api.clientConfig.mergeWarning":`대상 파일에 병합하세요. 덮어쓰면 기존 프로바이더와 MCP 설정이 사라집니다.`,"api.clientConfig.modelCount":`모델 {count}개 내보냄`,"api.clientConfig.missingLimits":`{total}개 중 {count}개 모델에 컨텍스트 한도가 없어 클라이언트 기본값이 적용됩니다.`,"api.clientConfig.noKeyYet":`{env}에 연결된 키가 아직 없습니다. 루프백 밖에서 쓰려면 위에서 키를 발급하세요.`,"api.clientConfig.loadFailed":`모델 목록을 읽지 못해 클라이언트 설정을 만들지 못했습니다.`,"api.clientConfig.copiedAnnounce":`클라이언트 설정을 클립보드에 복사했습니다.`,"api.clientConfig.copyFailed":`클라이언트 설정을 복사하지 못했습니다.`,"api.clientConfig.downloadedAnnounce":`{filename} 파일을 다운로드했습니다. 아직 아무것도 바뀌지 않았으니 {destination}에 직접 병합하세요.`,"api.clientConfig.whereDisclosure":`이 파일이 들어갈 위치`,"api.clientConfig.whereBody":`위 경로는 전역 설정 경로입니다. 작업 디렉터리의 프로젝트 설정 파일이 우선하며, 키는 설정에 적힌 환경 변수에서 읽고 이 파일에는 저장되지 않습니다.`,"api.keysLoadFailed":`API 키를 불러오지 못했습니다.`,"api.createFailed":`API 키를 만들지 못했습니다.`,"api.deleteFailed":`API 키를 삭제하지 못했습니다.`,"api.auth.endpoint":`엔드포인트`,"api.auth.required":`필수`,"api.auth.accepted":`가능`,"api.auth.rejected":`안 됨`,"api.auth.testProtocol":`{protocol} 테스트`,"api.auth.testNeedsFreshKey":`인증 테스트를 하려면 키를 새로 만들고 한 번만 보이는 값을 화면에 둔 채로 실행하세요.`,"api.key.name":`키 이름`,"api.key.rename":`이름 변경`,"api.key.saveName":`이름 저장`,"api.key.renaming":`저장 중…`,"api.key.renameFailed":`이름을 바꾸지 못했습니다. 입력한 내용은 그대로 뒀습니다.`,"api.key.deleting":`삭제 중…`,"api.rotation.title":`키 교체`,"api.rotation.description":`짧은 전환 시간 동안 기존 키를 유지한 채 새 키를 발급합니다.`,"api.rotation.start":`키 교체 시작`,"api.rotation.starting":`시작하는 중…`,"api.rotation.pending":`키 교체가 대기 중입니다. 클라이언트에 새 키를 적용하고 정상 연결을 확인한 뒤 확정하세요.`,"api.rotation.expires":`전환 가능 시간:`,"api.rotation.secretOnce":`새 키는 지금 한 번만 표시됩니다. 이 안내를 닫기 전에 복사하세요.`,"api.rotation.commit":`새 키로 확정`,"api.rotation.abort":`키 교체 취소`,"api.rotation.failed":`요청을 끝내지 못했습니다. 새로고침한 뒤 다시 시도하세요.`,"api.rotation.startFailed":`키 교체를 시작하지 못했습니다.`,"api.key.copyFailed":`키를 복사하지 못했습니다. 이 패널을 닫기 전에 직접 선택해서 복사하세요.`,"api.attribution.title":`키별 사용량`,"api.attribution.requests7d":`최근 7일 요청`,"api.attribution.totalRequests":`집계된 전체 요청`,"api.attribution.totalRequestsAvailable":`사용 가능한 기록의 요청`,"api.attribution.sinceAvailable":`사용 가능한 집계 시작일`,"api.attribution.lastUsed":`마지막 사용`,"api.attribution.since":`집계 시작`,"api.attribution.neverUsed":`집계 이후 사용 없음`,"api.attribution.unavailable":`사용량 없음`,"api.attribution.unavailableDetail":`아직 집계된 사용량이 없습니다. 집계가 시작되기 전 요청은 소급해서 배정할 수 없습니다.`,"api.attribution.ambiguous":`두 키가 같은 ID를 쓰고 있어 어느 쪽 사용량인지 가릴 수 없습니다. 설정 파일에서 키마다 다른 ID를 주세요.`,"api.attribution.railAmbiguous":`ID 중복`,"claude.subtitle":`Claude Code에서 GPT, Gemini 등 다른 모델도 쓸 수 있게 해줍니다.`,"claude.enabledLabel":`Claude 연결`,"claude.enabledHint":`끄면 Claude Code가 이 프록시를 사용할 수 없습니다.`,"claude.authMode":`인증 모드`,"claude.authModeHint":`subscription은 Claude 계정 필요, proxy는 opencodex 프록시만으로 사용 가능`,"claude.authModeSubscription":`Subscription (Claude 계정)`,"claude.authModeProxy":`Proxy (계정 불필요)`,"claude.authModeAuto":`자동 (Claude 인증 감지)`,"claude.effectiveMode.label":`다음 실행 시 적용`,"claude.effectiveMode.manual":`수동: {mode}`,"claude.effectiveMode.autoPresent":`자동: 구독 — {source}에서 Claude 인증을 찾았습니다`,"claude.effectiveMode.autoAbsent":`자동: 프록시 모드 — Claude 인증이 없습니다`,"claude.effectiveMode.autoUnknown":`자동: 구독 — 인증을 확인하지 못했습니다`,"claude.effectiveMode.admissionKey":`이 프록시의 API 키는 계속 전송됩니다.`,"claude.authSource.claude-json-oauth":`Claude 계정`,"claude.authSource.claude-credentials-file":`자격 증명 파일`,"claude.authSource.macos-keychain":`macOS 키체인`,"claude.authSource.exported-env":`환경 변수`,"claude.authSource.unknown":`감지된 자격 증명`,"claude.systemEnv":`자동 연결`,"claude.systemEnvDesc":`켜면 터미널에서 claude를 바로 실행해도 프록시를 거칩니다.`,"claude.systemEnvUnsupported":`자동 연결은 macOS에서만 지원됩니다. 이 시스템에서는 {cmd}로 Claude를 실행하세요.`,"claude.systemEnvWarn":`⚠ 터미널 앱을 완전히 종료했다 다시 열어야 적용됩니다. 사용을 권장하지 않습니다.`,"claude.fastMode":`Fast Mode (OpenAI)`,"claude.fastModeDesc":`OpenAI 모델의 추론 속도를 제어합니다. ON = 빠른 추론. OFF = 기본 속도. Auto = 클라이언트 설정 그대로.`,"claude.fastAuto":`Auto`,"claude.fastOn":`ON`,"claude.fastOff":`OFF`,"claude.autoContext":`큰 컨텍스트 자동 활용`,"claude.autoContextDesc":`1M 표기를 어디까지 붙일지 정합니다. 켜면 컴팩션 기준치를 담을 수 있는 창을 가진 모델에 큰 컨텍스트 행이 생기고, 끄면 진짜 1M 모델에만 생깁니다.`,"claude.autoContextInert":`설정 파일에 이전 방식의 컨텍스트 크기 값(maxContextTokens)이 있어 이 기능이 지금은 적용되지 않아요. 설정에서 그 값을 지우면 다시 켜집니다.`,"claude.autoCompactWindow":`자동 요약 지점`,"claude.autoCompactDefault":`{value} (기본값)`,"claude.autoCompactWindowDesc":`대화가 이 지점에 다다르면 오래된 내용을 자동 요약합니다. 모델마다 자기 한도를 넘지 않는 선에서만 적용되니 200k 모델은 영향받지 않아요.`,"claude.autoCompactWindowWarn":`값을 직접 바꾸면 GPT 모델들이 제대로 동작하지 않을 수 있어요 — 모델의 실제 한도보다 크게 잡으면 요약이 되기 전에 대화가 오류로 멈춥니다.`,"claude.injectAgents":`서브에이전트 자동 등록`,"claude.injectAgentsDesc":`위 '서브에이전트' 탭에서 고른 모델들(+현재 기본 모델)을 Claude Code의 파견 가능한 에이전트(ocx-*)로 자동 등록합니다. 새 세션부터 적용돼요.`,"claude.webSearchSidecar":`웹 검색 사이드카 덮어쓰기`,"claude.webSearchSidecarHint":`Claude Code 요청에만 메인 웹 검색 사이드카 대신 이 설정을 씁니다.`,"claude.visionSidecar":`비전 사이드카 덮어쓰기`,"claude.visionSidecarHint":`Claude Code 요청에만 메인 비전 사이드카 대신 이 설정을 씁니다.`,"claude.useMainSetting":`메인 설정 사용`,"claude.sidecarModelPlaceholder":`메인 설정의 모델`,"claude.quickstart":`시작하기`,"claude.quickstartHint":`{cmd} 을 실행하면 프록시를 거쳐 Claude Code가 열립니다. claude.ai 로그인은 그대로 유지됩니다.`,"claude.manualEnv":`직접 설정하기 (고급)`,"claude.smallFastModel":`백그라운드 보조 모델`,"claude.smallFastModelHint":`Claude Code가 대화 요약, 주제 감지 같은 배후 작업에 쓰는 모델입니다. 서브에이전트의 haiku 별칭도 이 모델을 씁니다. 비워두면 Claude 기본값(Haiku).`,"claude.smallFastModelAccurateHint":`Claude Code가 대화 요약, 주제 감지 같은 백그라운드 작업에 쓰는 모델입니다. 서브에이전트의 haiku 별칭도 이 모델을 사용합니다.`,"claude.smallFastModelUnsetOption":`Claude Code가 선택(네이티브 모델)`,"claude.smallFastModelNativeWarning":`비워 두면 OpenCodex가 보조 모델 환경 변수를 설정하지 않습니다. Claude Code가 네이티브 Sonnet 모델을 사용할 수 있으며, 네이티브 프로바이더 요금이 발생할 수 있습니다.`,"claude.slotUnset":`Claude 기본값 사용`,"claude.modelMap":`모델 가로채기`,"claude.modelMapHint":`Claude가 특정 모델을 요청하면 가로채서 지정한 모델로 보냅니다. 기본값은 비어 있어요 — 규칙을 추가할 때만 동작합니다.`,"claude.mapFrom":`원래 모델 (예: claude-sonnet-4-5)`,"claude.mapTo":`바꿀 모델 (예: gemini/gemini-3-pro)`,"claude.addMapping":`규칙 추가`,"claude.removeMapping":`규칙 삭제`,"claude.aliases":`사용 가능한 모델`,"claude.aliasesHint":`Claude Code의 /model 메뉴에 표시되는 모델 목록입니다.`,"claude.aliasProviderOther":`기타`,"claude.loading":`불러오는 중…`,"claude.loadFail":`Claude 설정을 불러오지 못했습니다`,"claude.saved":`저장되었습니다.`,"claude.saveFailed":`저장 실패`,"claude.networkError":`네트워크 오류 — 프록시가 실행 중인가요?`,"claude.toggleAria":`Claude 인바운드 켜기/끄기`,"claude.none":`없음`,"common.close":`닫기`,"common.ok":`확인`,"app.logoAria":`opencodex 로고`,"app.claudeOn":`Claude ON`,"app.claudeOff":`Claude OFF`,"usage.dayMon":`월`,"usage.dayWed":`수`,"usage.dayFri":`금`,"usage.heatmap.tooltipTokens":`{tokens} 토큰`,"usage.heatmap.tooltipRequests":`{requests} 요청`,"nav.storage":`저장소`,"storage.title":`저장소`,"storage.subtitle":`CODEX_HOME 사용량을 확인합니다. 정리는 활성 세션을 건드리지 않습니다.`,"storage.loading":`저장소 스캔 중…`,"storage.empty":`CODEX_HOME이 비어 있거나 없습니다 — 표시할 내용이 없습니다.`,"storage.error":`저장소 스캔에 실패했습니다. CODEX_HOME이 올바른 디렉터리를 가리키는지 확인하세요.`,"storage.refresh":`다시 스캔`,"storage.rescanned":`스캔이 완료되었습니다.`,"storage.card.total":`전체 크기`,"storage.card.files":`파일 수`,"storage.card.home":`CODEX_HOME`,"storage.snapshot.lastScan":`마지막 스캔`,"storage.snapshot.scanning":`스캔 중…`,"storage.snapshot.unavailable":`아직 스캔 없음.`,"storage.cleanupCard.title":`공간 확보`,"storage.cleanupCard.tabs":`정리 옵션`,"storage.cleanupCard.tab.policy":`정책`,"storage.cleanupCard.tab.quarantine":`격리`,"storage.cleanup.noArchives":`정리할 보관 세션이 없습니다.`,"storage.section.buckets":`버킷`,"storage.section.largest":`가장 큰 파일`,"storage.workspace.overview":`개요`,"storage.workspace.selectBucket":`목록에서 버킷을 선택하면 세부 내역을 볼 수 있습니다.`,"storage.col.bucket":`버킷`,"storage.col.size":`크기`,"storage.col.files":`파일`,"storage.col.oldest":`가장 오래됨`,"storage.col.newest":`가장 최근`,"storage.col.rows":`DB 행 수`,"storage.rows.unknown":`알 수 없음 (잠김)`,"storage.bucket.sessions":`활성 세션`,"storage.bucket.archived_sessions":`보관된 세션`,"storage.bucket.logs_db":`로그 데이터베이스`,"storage.bucket.state_db":`상태 데이터베이스`,"storage.bucket.attachments":`첨부 파일`,"storage.bucket.deletion_manifests":`삭제 매니페스트`,"storage.bucket.other":`기타`,"storage.cleanup.title":`보관 정리`,"storage.cleanup.help":`가장 오래된 보관 세션을 비율로 제거합니다. 활성 세션은 건드리지 않습니다. 기본은 격리이며 파일은 CODEX_HOME/.trash로 이동합니다.`,"storage.cleanup.slider":`오래된 보관 비율`,"storage.cleanup.percent":`{percent}%`,"storage.cleanup.preset":`{percent}`,"storage.cleanup.preview":`미리보기`,"storage.cleanup.confirmTitle":`보관 정리 확인`,"storage.cleanup.confirmBody":`보관 파일 {count}개(약 {size}), 오래된 {percent}%를 처리합니다.`,"storage.cleanup.moreFiles":`…외 {n}개`,"storage.cleanup.permanent":`영구 삭제(격리 건너뛰기)`,"storage.cleanup.permanentWarn":`영구 삭제는 되돌릴 수 없습니다.`,"storage.cleanup.quarantineNote":`파일은 CODEX_HOME 아래 .trash로 이동합니다. 격리 탭에서 복원할 수 있습니다.`,"storage.cleanup.cancel":`취소`,"storage.cleanup.confirmQuarantine":`격리`,"storage.cleanup.confirmPermanent":`영구 삭제`,"storage.cleanup.doneQuarantine":`파일 {count}개를 격리했습니다({size}).`,"storage.cleanup.donePermanent":`파일 {count}개를 영구 삭제했습니다({size}).`,"storage.cleanup.previewFailed":`미리보기에 실패했습니다.`,"storage.cleanup.cleanupFailed":`정리에 실패했습니다.`,"storage.cleanup.err.codex_busy":`Codex가 state.sqlite를 사용 중입니다 — Codex를 종료한 뒤 다시 시도하세요.`,"storage.cleanup.err.stale_preview":`미리보기 이후 보관 파일이 변경되었습니다 — 미리보기를 다시 실행하세요.`,"storage.cleanup.err.restore_pending_overlap":`선택한 보관 파일이 미완료 휴지통 복원과 겹칩니다 — 복원을 완료하거나 다시 시도하세요.`,"storage.cleanup.err.referenced_history":`선택한 보관본이 포크 또는 페이지 기록에서 아직 참조됩니다.`,"storage.cleanup.err.invalid_digest":`미리보기 digest가 없거나 잘못되었습니다.`,"storage.cleanup.err.invalid_mode":`모드는 quarantine 또는 permanent여야 합니다.`,"storage.cleanup.err.fs_failed":`파일 시스템 정리에 실패했습니다. 일부 변경이 이미 적용되었을 수 있습니다 — CODEX_HOME/.trash와 표시된 복구 경로를 확인하세요.`,"storage.cleanup.err.fs_failed_trash":`파일 시스템 정리에 실패했습니다. 일부 변경이 이미 적용되었을 수 있습니다 — {trashDir}와 manifest.json에서 복구 가능한 파일을 확인하세요.`,"storage.cleanup.err.db_reconcile_failed":`Codex 상태 데이터베이스를 업데이트할 수 없습니다.`,"storage.cleanup.err.cleanup_failed":`정리에 실패했습니다.`,"storage.trash.title":`격리`,"storage.trash.help":`CODEX_HOME/.trash로 옮긴 보관 세션입니다. 복원하면 JSONL과 스레드 행이 돌아갑니다.`,"storage.trash.empty":`격리된 항목이 없습니다.`,"storage.trash.loading":`격리 목록 불러오는 중…`,"storage.trash.col.when":`격리 시각`,"storage.trash.col.files":`파일`,"storage.trash.col.size":`크기`,"storage.trash.col.mode":`모드`,"storage.trash.col.id":`항목`,"storage.trash.restore":`복원`,"storage.trash.confirmTitle":`격리 항목을 복원할까요?`,"storage.trash.confirmBody":`{id}에서 파일 {count}개(약 {size})를 보관 세션으로 되돌립니다.`,"storage.trash.cancel":`취소`,"storage.trash.confirmRestore":`복원`,"storage.trash.done":`파일 {count}개를 복원했습니다({size}).`,"storage.trash.restoreFailed":`복원에 실패했습니다.`,"storage.trash.listFailed":`격리 목록을 불러오지 못했습니다.`,"storage.trash.mode.quarantine":`격리`,"storage.trash.mode.permanent":`영구(미완료)`,"storage.trash.err.codex_busy":`Codex가 state.sqlite를 사용 중입니다 — Codex를 종료한 뒤 다시 시도하세요.`,"storage.trash.err.invalid_trash":`격리 항목 ID가 없거나 잘못되었습니다.`,"storage.trash.err.missing_trash":`격리 항목을 찾을 수 없습니다.`,"storage.trash.err.dest_exists":`복원 대상이 이미 있습니다 — 보관 파일을 삭제하거나 이름을 바꾼 뒤 다시 시도하세요.`,"storage.trash.err.fs_failed":`파일 시스템 복원에 실패했습니다. 일부 파일이 이미 복원되었을 수 있습니다 — archived_sessions와 .trash를 확인하세요.`,"storage.trash.err.storage_mutation_busy":`다른 저장소 정리 또는 복원이 진행 중입니다 — 잠시 후 다시 시도하세요.`,"storage.trash.err.db_reconcile_failed":`Codex 상태 데이터베이스 행을 복원할 수 없습니다.`,"storage.trash.err.restore_failed":`복원에 실패했습니다.`,"storage.trash.err.restore_worker_timeout":`복원 시간이 너무 길어(10분 초과) 중단되었습니다.`,"storage.trash.err.restore_worker_aborted":`종료 중 복원이 취소되었습니다.`,"storage.trash.err.restore_worker_failed":`복원 워커가 충돌하거나 예기치 않게 실패했습니다.`,"storage.policy.title":`자동 정리 정책`,"storage.policy.help":`보관 세션이 임계값을 넘을 때 선택적으로 일괄 정리합니다. 기본은 꺼짐 — 자동으로 켜지지 않습니다.`,"storage.policy.loading":`정책을 불러오는 중…`,"storage.policy.loadFailed":`정리 정책을 불러오지 못했습니다.`,"storage.policy.saveFailed":`정리 정책을 저장하지 못했습니다.`,"storage.policy.runFailed":`정책 실행에 실패했습니다.`,"storage.policy.alreadyRunning":`정리 정책이 이미 실행 중입니다.`,"storage.policy.invalid":`정책 값이 올바르지 않습니다.`,"storage.policy.enabled":`자동 정리 사용`,"storage.policy.enabledHint":`기본은 꺼짐입니다. 켜면 선택한 일정(또는 지금 실행)에만 동작합니다.`,"storage.policy.threshold":`보관 용량이 초과하면 (GiB)`,"storage.policy.trigger":`트리거`,"storage.policy.target":`정리 목표`,"storage.policy.targetPercent":`가장 오래된 보관 제거 (%)`,"storage.policy.targetReduce":`보관 용량을 다음까지 줄이기 (GiB)`,"storage.policy.thresholdInc":`임계값 증가`,"storage.policy.thresholdDec":`임계값 감소`,"storage.policy.percentInc":`퍼센트 증가`,"storage.policy.percentDec":`퍼센트 감소`,"storage.policy.reduceInc":`축소 목표 증가`,"storage.policy.reduceDec":`축소 목표 감소`,"storage.policy.schedule":`일정`,"storage.policy.schedule.manual":`수동만`,"storage.policy.schedule.startup":`프록시 시작 시`,"storage.policy.schedule.daily":`매일`,"storage.policy.schedule.weekly":`매주`,"storage.policy.mode":`삭제 모드`,"storage.policy.mode.quarantine":`격리(기본)`,"storage.policy.mode.permanent":`영구 삭제`,"storage.policy.permanentWarn":`영구 모드는 되돌릴 수 없습니다. 확실하지 않으면 격리를 사용하세요.`,"storage.policy.lastRun":`마지막 실행`,"storage.policy.lastRunDetail":`{count}개 제거 · {size} 확보`,"storage.policy.nextRun":`다음 실행`,"storage.policy.never":`없음`,"storage.policy.save":`저장`,"storage.policy.runNow":`지금 실행`,"storage.policy.running":`실행 중…`,"storage.policy.saved":`정책을 저장했습니다.`,"storage.policy.skippedDisabled":`정책이 꺼져 있습니다 — 먼저 켜세요.`,"storage.policy.skippedUnder":`보관 용량이 임계값 미만입니다 — 할 일이 없습니다.`,"storage.policy.skippedEmpty":`목표에 맞는 보관 후보가 없습니다.`,"storage.policy.doneQuarantine":`정책이 파일 {count}개를 격리했습니다({size}).`,"storage.policy.donePermanent":`정책이 파일 {count}개를 영구 삭제했습니다({size}).`,"storage.policy.metadataSaveWarning":`정책 실행은 완료됐지만 일정 메타데이터를 저장하지 못했습니다.`,"modal.back":`뒤로`,"modal.badge.oauth":`OAuth`,"modal.customProvider":`사용자 지정 프로바이더`,"modal.failedStatus":`실패 ({status})`,"modal.loginError":`로그인 오류: {error}`,"modal.badge.codexLogin":`Codex 로그인`,"modal.badge.local":`로컬`,"modal.badge.apiKey":`API 키`,"modal.badge.direct":`Direct`,"modal.badge.pool":`풀`,"modal.badge.free":`무료`,"modal.invalidPreset":`내장 프로바이더 설정이 완전하지 않습니다. 프록시를 다시 시작한 뒤 재시도하세요.`,"modal.freeTierTitle":`무료 티어`,"modal.freeTierDefault":`API 키가 필요 없습니다. 바로 사용할 수 있습니다.`,"modal.tab.accounts":`계정`,"modal.tab.free":`무료`,"modal.tab.paid":`유료`,"modal.accountsHint":`여기서 ChatGPT/Codex, OAuth, API 키 계정에 로그인하세요. OpenAI는 기본 제공 — 다시 추가하지 말고 로그인하세요.`,"modal.accountsCodexAuthLink":`Codex 인증`,"modal.notListed":`찾는 프로바이더가 없나요? 직접 추가`,"modal.catalogLoading":`카탈로그 불러오는 중…`,"modal.accountLogin":`로그인`,"modal.accountLogout":`로그아웃`,"modal.accountAdd":`계정 추가`,"modal.accountManage":`관리`,"modal.accountCodexPool":`ChatGPT 계정 풀`,"modal.accountLoggedIn":`로그인됨`,"modal.accountLoggedOut":`로그인 안 됨`,"quota.fiveHourLimit":`5시간 한도`,"quota.ageMinutes":`{n}분`,"quota.ageHours":`{n}시간`,"quota.ageDays":`{n}일`,"quota.observedAgo":`{age} 전에 확인한 값`,"quota.observedHint":`Meta는 스트리밍 응답 중에만 사용량을 보고합니다. 실시간 수치가 아니라 마지막으로 확인된 값입니다.`,"quota.weeklyLimit":`주간 한도`,"quota.monthlyLimit":`30일 한도`,"quota.cursorFirstParty":`자사 모델`,"quota.cursorApiUsage":`API 사용량`,"quota.totalSubscriptionCredits":`전체 구독 크레딧`,"quota.creditsBalance":`크레딧 잔액`,"quota.creditsPeriodEnds":`청구 기간 종료: {date}`,"quota.usedPercent":`{pct}% 사용`,"quota.limitReached":`한도 도달`,"quota.resetsToday":`오늘 {time} 초기화`,"quota.resetsTomorrow":`내일 {time} 초기화`,"quota.resetsAt":`{when} 초기화`,"quota.resetsRelativeMinutes":`{n}분 후 초기화`,"quota.resetsRelativeHours":`{n}시간 후 초기화`,"pws.status.ready":`준비됨`,"pws.status.needsSetup":`설정 필요`,"pws.status.needsAttention":`확인 필요`,"pws.auth.chatgptPassthrough":`ChatGPT 패스스루`,"pws.auth.noKey":`키 불필요`,"pws.freeTitle":`무료 요금제 (키는 필요할 수 있음)`,"pws.localTitle":`로컬 런타임`,"pws.modelCountOne":`모델 1개`,"pws.modelCount":`모델 {count}개`,"pws.rail.suffixDefault":` · 기본`,"pws.rail.suffixLocal":` · 로컬`,"pws.rail.suffixFree":` · 무료`,"pws.rail.selectAria":`{name} 선택 — {status}{suffix}`,"pws.searchPlaceholder":`프로바이더 검색…`,"pws.filterAria":`프로바이더 필터`,"pws.providerFiltersAria":`프로바이더 필터`,"pws.filters":`필터`,"pws.filterStatus":`상태`,"pws.pricing":`요금`,"pws.paid":`유료`,"pws.filterType":`유형`,"pws.type.cloud":`클라우드`,"pws.type.local":`로컬`,"pws.type.selfHosted":`셀프 호스팅`,"pws.type.login":`로그인`,"pws.sort":`정렬`,"pws.sortProvidersAria":`프로바이더 정렬`,"pws.sort.az":`A–Z`,"pws.sort.za":`Z–A`,"pws.sort.freePaid":`무료 우선`,"pws.sort.paidFree":`유료 우선`,"pws.sort.accountsFirst":`계정 우선`,"pws.resetAll":`모두 초기화`,"pws.providerList":`프로바이더 목록`,"pws.providersAria":`프로바이더`,"pws.groupReady":`준비됨 ({count})`,"pws.groupNeedsSetup":`설정 필요 ({count})`,"pws.groupDisabled":`비활성화 ({count})`,"pws.noSearchResults":`검색과 일치하는 프로바이더가 없습니다.`,"pws.noMatchFilters":`필터와 일치하는 프로바이더가 없습니다.`,"pws.noProvidersConfigured":`설정된 프로바이더가 없습니다.`,"pws.workspaceMainAria":`프로바이더 상세`,"pws.detailComingSoon":`상세 보기는 준비 중입니다 — 클래식 보기에서 관리하세요.`,"pws.selectPrompt":`목록에서 프로바이더를 선택하세요.`,"pws.connectFirst":`첫 프로바이더를 연결하세요`,"pws.empty.browseFree":`무료 프로바이더 보기`,"pws.empty.browseFreeDesc":`구독 없이 시작`,"pws.empty.connectAccount":`계정 연결`,"pws.empty.connectAccountDesc":`ChatGPT 또는 프로바이더 로그인 사용`,"pws.empty.addEndpoint":`엔드포인트 추가`,"pws.empty.addEndpointDesc":`커스텀 base URL과 API 키`,"pws.tab.overview":`개요`,"pws.tab.models":`모델`,"pws.tab.usage":`사용량`,"pws.tab.accounts":`계정`,"pws.tab.settings":`설정`,"pws.connection":`연결`,"pws.status.connected":`연결됨`,"pws.attentionTitle":`확인 필요`,"pws.attention.reauth":`활성 계정 재인증이 필요합니다`,"pws.attention.reauthForward":`활성 Codex 계정 재인증이 필요합니다 — 계정에서 해결하세요`,"pws.attention.missingCredentials":`자격 증명 없음`,"pws.cell.auth":`인증`,"pws.cell.note":`메모`,"pws.cell.defaultModel":`기본 모델`,"pws.statsAria":`프로바이더 통계`,"pws.statsTitle":`통계`,"pws.stats.totalRequests":`요청 수 (30일)`,"pws.stats.totalTokens":`토큰 (30일)`,"pws.stats.quotaUpdated":`쿼터 갱신`,"pws.stats.quotaTracked":`사용량 탭에서 한도를 확인할 수 있습니다.`,"pws.stats.source":`출처`,"pws.usageLast30d":`사용량 (최근 30일)`,"pws.estimatedCost":`추정 비용`,"pws.costDisclaimer":`API 공시가 기준 추정치이며, 실제 청구 금액이 아닙니다.`,"pws.modelBreakdown":`모델별 사용량`,"pws.col.model":`모델`,"pws.col.cost":`추정 비용`,"pws.col.tokens":`토큰`,"pws.col.requests":`요청`,"pws.col.share":`점유율`,"pws.tokenInput":`입력`,"pws.tokenOutput":`출력`,"pws.metricRequests":`요청`,"pws.metricTokens":`토큰`,"pws.usageUnavailable":`아직 기록된 사용량이 없습니다.`,"pws.rateLimits":`요청 한도`,"pws.quotaUnavailable":`이 프로바이더의 쿼터 데이터가 없습니다.`,"pws.accountQuotaUnavailable":`요금 한도 데이터를 일시적으로 가져올 수 없습니다. 이전 값이 있으면 그대로 표시합니다.`,"pws.selected":`선택됨`,"pws.copyModelId":`ID 복사`,"pws.modelCopied":`복사됨!`,"pws.modelsAvailable":`{count}개 사용 가능`,"pws.modelSearchPlaceholder":`모델 필터…`,"pws.modelsLoading":`모델 불러오는 중…`,"pws.modelsLoadFailed":`모델을 불러오지 못했습니다.`,"pws.modelsNeedsReauth":`실시간 모델 목록을 받으려면 다시 로그인해야 합니다. 지금은 설정된 모델을 표시합니다.`,"pws.modelsConfiguredFallback":`설정된 모델을 표시합니다 (실시간 검색 불가).`,"pws.modelsTruncated":`{total}개 모델 중 처음 {shown}개를 표시합니다. 필터로 목록을 좁히세요.`,"pws.retry":`다시 시도`,"pws.noModels":`이 프로바이더에서 발견된 모델이 없습니다.`,"pws.noModelMatch":`필터와 일치하는 모델이 없습니다.`,"pws.adapterBaseRequired":`어댑터와 기본 URL은 필수입니다.`,"pws.addAccount":`계정 추가`,"pws.addKey":`API 키 추가`,"pws.apiKeys":`API 키`,"pws.authMode":`인증 방식`,"pws.availableAccounts":`사용 가능한 계정`,"pws.accountOrdinal":`계정 {count}`,"pws.accountsLoading":`계정 불러오는 중…`,"pws.accountsLoadFailed":`계정을 불러오지 못했습니다.`,"pws.retryAccounts":`다시 시도`,"pws.noAccounts":`연결된 계정이 아직 없습니다.`,"pws.cockpitImportDescription":`이 기기에서 Cockpit Tools Antigravity JSON 내보내기를 가져옵니다. 파일 내용은 표시되지 않습니다.`,"pws.cockpitImportFileLabel":`Cockpit Tools Antigravity JSON 내보내기`,"pws.cockpitImportChooseFile":`JSON 파일 선택`,"pws.cockpitImporting":`가져오는 중…`,"pws.cockpitImportInvalid":`선택한 파일은 유효한 JSON 내보내기가 아니거나 너무 큽니다.`,"pws.cockpitImportFailed":`계정 가져오기를 완료할 수 없습니다.`,"pws.cockpitImportComplete":`가져오기 완료: 가져옴 {imported}, 업데이트 {updated}, 실패 {failed}, 지원되지 않음 {unsupported}.`,"pws.accountSwitching":`전환 중…`,"pws.accountCurrent":`현재 계정`,"pws.defaultModelNone":`없음 (프로바이더 기본값 사용)`,"pws.discardSettings":`되돌리기`,"pws.jsonEditorDesc":`프로바이더 JSON 설정을 직접 편집합니다. 저장 즉시 반영됩니다.`,"pws.jsonEditorTitle":`JSON 편집기 — {name}`,"pws.jsonRestore":`복원`,"pws.jsonSave":`저장`,"pws.loggedInTitle":`로그인됨`,"pws.notLoggedInTitle":`로그인 필요`,"pws.note":`메모`,"pws.allowPrivateNetwork":`로컬/사설 네트워크 허용`,"pws.liveModels":`프로바이더에서 모델 검색`,"pws.liveModelsDesc":`프로바이더의 실시간 모델 카탈로그를 가져옵니다. 끄면 설정된 정적 모델만 사용합니다.`,"pws.xaiResponsesOptIn":`Grok 4.5와 4.6에 Responses API 사용`,"pws.xaiResponsesOptInDesc":`두 모델을 openai-responses로 라우팅합니다. 다른 Grok 모델과 티어 동작은 바뀌지 않습니다.`,"pws.xaiResponsesOptInMixed":`일부만 활성화됨.`,"pws.cursorTransport":`Cursor 전송`,"pws.cursorTransportHttp2":`HTTP/2 (기본값)`,"pws.cursorTransportHttp1":`HTTP/1.1 (프록시 호환)`,"pws.cursorTransportDesc":`프록시가 Cursor의 HTTP/2 스트림을 안정적으로 전달하지 못할 때 HTTP/1.1을 사용하세요.`,"pws.optionalPlaceholder":`선택사항`,"pws.providerId":`프로바이더 ID`,"pws.reauth":`재인증 필요`,"pws.reauthenticate":`재인증`,"pws.copyDoctor":`ocx doctor 복사`,"pws.doctorCopied":`복사됨`,"pws.healthCooldownHint":`쿨다운이 끝날 때까지 기다리세요. 지금은 이 계정을 프로브하지 마세요.`,"pws.doctorCopyUnavailable":`클립보드를 사용할 수 없음`,"pws.healthLabel.rateLimited":`요청 한도 초과`,"pws.healthLabel.quotaLimited":`할당량 제한`,"pws.healthLabel.reauthRequired":`재인증 필요`,"pws.healthLabel.refreshFailed":`새로고침 실패`,"pws.healthLabel.metadataMismatch":`메타데이터 불일치`,"pws.healthLabel.credentialConflict":`자격 증명 충돌`,"pws.healthSummary.rateLimited":`{provider} {account}: {until}까지 요청 한도 초과. 그전까지 이 계정 라우팅이 일시 중지됩니다.`,"pws.healthSummary.quotaLimited":`{provider} {account}: {until}까지 할당량 제한. 그전까지 이 계정 라우팅이 일시 중지됩니다.`,"pws.healthSummary.reauthRequired":`{provider} {account}: 재인증이 필요합니다.`,"pws.healthSummary.credentialConflict":`{provider} {account}: 자격 증명 충돌.`,"pws.healthSummary.metadataMismatch":`{provider} {account}: 메타데이터 불일치.`,"pws.healthSummary.staleCredentials":`{provider} {account}: 자격 증명이 불완전합니다.`,"pws.removeConfirm":`제거`,"pws.removeConfirmBody":`프로바이더 "{name}"을(를) 제거하시겠습니까? 되돌릴 수 없습니다.`,"pws.removeDefaultConfirmBody":`기본 프로바이더 "{name}"을(를) 제거하시겠습니까? "{defaultProvider}"이(가) 기본 프로바이더가 됩니다. 이 작업은 되돌릴 수 없습니다.`,"pws.removeConfirmTitle":`프로바이더 제거`,"pws.saveSettings":`저장`,"pws.pacingTitle":`요청 속도 조절`,"pws.pacingDesc":`이 프로바이더로 나가는 요청 시작을 일정 간격으로 지연합니다. 스트리밍 응답은 서로 겹칠 수 있습니다.`,"pws.pacingEnabled":`사용`,"pws.pacingRpm":`분당 요청 수`,"pws.pacingRpmUnit":`RPM`,"pws.pacingDelay":`최소 시작 간격(ms)`,"pws.pacingSlowerWins":`프로바이더의 더 느린 제한이 우선합니다. 모델별 설정은 지연을 더 늘릴 때만 적용됩니다.`,"pws.pacingQueued":`대기 중`,"pws.pacingNextSlot":`다음 슬롯까지`,"pws.pacingLastModel":`마지막 모델`,"pws.pacingNone":`없음`,"pws.pacingModelOverrides":`모델별 설정`,"pws.pacingModel":`모델`,"pws.pacingAdd":`모델 제한 추가`,"pws.pacingRemove":`제거`,"pws.pacingRemoveModel":`{model} 모델의 요청 속도 설정 제거`,"pws.pacingRuleRequired":`프로바이더 제한이나 모델별 제한을 하나 이상 설정한 뒤 요청 속도 조절을 켜세요.`,"pws.saving":`저장 중…`,"pws.settingsSaved":`설정이 저장되었습니다.`,"pws.accountModeSaved":`계정 모드가 저장되었습니다.`,"pws.accountModeFailed":`계정 모드를 전환할 수 없습니다.`,"pws.accountModeConfirm":`OpenAI 계정 모드를 전환할까요? 실행 중인 대화가 다른 모드의 계정 세트로 다시 연결되며, 할당량 사용량은 새 모드로 집계됩니다.`,"pws.settingsUnsavedBar":`저장하지 않은 변경사항이 있습니다.`,"pws.unsavedLeaveBody":`저장하지 않은 변경사항이 있습니다. 나가기 전에 저장하시겠습니까?`,"pws.unsavedLeaveTitle":`미저장 변경사항`,"pws.attentionRequired":`주의 필요`,"pws.attentionAria":`{name}: {reason}`,"pws.missingCredentials":`자격 증명 없음`,"pws.editJsonDesc":`프록시 설정을 JSON으로 편집`,"pws.updatesUnavailable":`프로바이더 업데이트를 사용할 수 없습니다.`,"pws.dashboard.title":`프로바이더 개요`,"pws.dashboard.subtitle":`모든 모델 프로바이더를 한곳에서 관리합니다.`,"pws.dashboard.rateLimits":`사용량 제한`,"pws.capacity.estimate":`설정 가중치 기반 풀 추정치`,"pws.capacity.currentAccount":`현재 유효 계정`,"pws.capacity.nextRecovery":`다음 용량 회복`,"pws.capacity.recoveryShare":`+{percent}% 풀 용량`,"pws.capacity.incomplete":`불완전한 범위: {excluded}개 계정 제외`,"pws.capacity.uncalibratedPlan":`보정되지 않은 요금제 {count}개는 기본 좌석 가중치로 계산되어, 이 추정치가 실제보다 낮을 수 있습니다`,"pws.capacity.partial":`일부 기간의 범위가 불완전합니다: {count}개 계정에서 표시된 모든 한도 기간을 확인할 수 없습니다`,"pws.capacity.windowPartial":`일부만`,"pws.capacity.windowPartialA11y":`{window}: 계정 범위가 불완전합니다`,"pws.dashboard.recentlyUsed":`최근 사용`,"pws.dashboard.requests":`{count}건 요청`,"pws.dashboard.checkedAgo":`{time} 확인`,"pws.dashboard.noQuota":`할당량 데이터 없음`,"pws.dashboard.noUsage":`아직 사용 데이터 없음`,"pws.dashboard.noRateLimits":`아직 한도 데이터 없음`,"pws.allProviders":`프로바이더 개요`,"pws.enabledLabel":`활성화`,"pws.testConnection":`연결 테스트`,"pws.testing":`테스트 중…`,"pws.connectionOk":`연결 성공`,"pws.connectionFailed":`연결 실패`,"pws.connectionNotApplicable":`해당 없음 — 이 프로바이더는 정적 모델 카탈로그를 사용합니다.`,"pws.editSettings":`설정 편집`,"pws.viewUsage":`사용량 상세 보기`,"pws.allSystemsOk":`모든 시스템 정상`,"pws.apiKeyConfigured":`API 키 설정됨`,"pws.addApiKey":`API 키 추가`,"pws.loggedInAs":`{email}으로 로그인됨`,"pws.notLoggedIn":`로그인되지 않음`,"pws.passthrough":`Codex 패스스루`,"pws.notes":`메모`,"pws.notePlaceholder":`이 프로바이더에 대한 메모를 추가하세요...`,"pws.noteSaved":`메모 저장됨`,"pws.authSummary":`인증`,"time.justNow":`방금 전`,"time.notChecked":`확인 안 됨`,"time.minutesAgo":`{n}분 전`,"time.hoursAgo":`{n}시간 전`,"time.daysAgo":`{n}일 전`,"modal.noMatch":`일치 항목 없음.`,"modal.oauthDefaultNote":`계정으로 로그인 — API 키 불필요.`,"modal.oauthComingSoon":`{label} OAuth 로그인은 다음 업데이트에 제공됩니다. 지금은 API 키를 사용하세요.`,"modal.oauthComingSoonShort":`이 프로바이더의 OAuth 로그인은 다음 업데이트에 제공됩니다 — 지금은 API 키를 사용하세요.`,"modal.useApiKeyInstead":`대신 API 키 사용`,"modal.setupGuide":`설정 안내`,"modal.setupStep1Prefix":`다음으로 이동:`,"modal.setupDashboardLink":`{label} 대시보드`,"modal.setupStep1Suffix":`에서 API 키를 복사하세요`,"modal.setupStep2":`아래 API 키 필드에 붙여넣으세요`,"modal.setupStep3":`프로바이더 추가를 클릭하세요 — 모델은 자동으로 검색됩니다`,"modal.namePlaceholder":`예: openrouter`,"modal.duplicateWarn":`프로바이더 "{name}"이(가) 이미 있어 덮어씁니다.`,"modal.forwardHintPrefix":`키 불필요 — 프록시가`,"modal.forwardCredentials":`codex login`,"modal.forwardHintSuffix":`자격 증명을 이 프로바이더로 전달합니다.`,"modal.localHint":`API 키는 저장되지 않습니다. Cursor의 공개 모델 카탈로그만 Codex에 추가되며, live Cursor 전송과 네이티브 파일/셸 실행은 검토 전까지 비활성입니다.`,"modal.getApiKey":`{label} API 키 받기`,"modal.apiKey":`API 키`,"modal.apiKeyTransport":`API 키 헤더`,"modal.apiKeyTransportNative":`x-api-key (Anthropic 기본)`,"modal.apiKeyTransportBearer":`Authorization: Bearer`,"modal.apiKeyPlaceholder":`sk-… (또는 $ENV_VAR)`,"modal.defaultModelPlaceholder":`예: gpt-5.5`,"modal.baseUrlPlaceholder":`https://...`,"modal.baseUrlPlaceholderError":`Base URL에 해결되지 않은 {placeholder}가 있습니다. 실제 값으로 교체하세요.`,"modal.baseUrlPlaceholderHint":`추가하기 전에 Base URL의 {placeholder}를 실제 Account ID로 교체하세요.`,"modal.adding":`추가 중…`,"modal.useOauthLogin":`← OAuth 로그인 사용`,"codexAuth.addIdPlaceholder":`codex-work, codex-alt, team…`,"codexAuth.resetCreditsAria":`리셋 크레딧 {count}개`,"claude.pageTitle":`Claude Code`,"claude.workspace.settings":`설정`,"cws.loading":`콤보 불러오는 중…`,"cws.loadFailed":`콤보를 불러오지 못했습니다.`,"cws.saveFailed":`콤보를 저장하지 못했습니다.`,"cws.removeFailed":`콤보를 삭제하지 못했습니다.`,"cws.saved":`콤보를 저장했습니다.`,"cws.created":`{model}을(를) 만들었습니다.`,"cws.removed":`combo/{id}을(를) 삭제했습니다.`,"cws.renamed":`{from}을(를) {to}(으)로 이름을 변경했습니다.`,"cws.add":`콤보 추가`,"cws.addTitle":`콤보 추가`,"cws.addSubtitle":`여러 프로바이더를 사용하는 가상 모델을 만들고 클라이언트가 요청할 정확한 모델 이름을 선택하세요.`,"cws.create":`콤보 만들기`,"cws.railAria":`콤보 목록`,"cws.searchPlaceholder":`콤보 또는 대상 검색…`,"cws.noSearchResults":`검색과 일치하는 콤보가 없습니다.`,"cws.group.failover":`장애 조치`,"cws.group.roundRobin":`라운드로빈`,"cws.group.other":`기타 전략`,"cws.targetCount":`대상 {count}개`,"cws.targetCountOne":`대상 1개`,"cws.overviewTitle":`콤보`,"cws.overviewBlurb":`프로바이더/모델 대상 사이를 장애 조치, 라운드로빈, 가중 랜덤, 최소 사용, 최단 쿼터 리셋으로 라우팅하는 가상 모델입니다.`,"cws.count.total":`전체`,"cws.count.failover":`장애 조치`,"cws.count.roundRobin":`라운드로빈`,"cws.count.other":`기타`,"cws.howTitle":`동작 방식`,"cws.howBody":`Codex에서 콤보의 공개 모델 이름을 요청하세요. 설정하지 않으면 combo/가 기본값입니다. OpenCodex는 재시도 가능한 업스트림 오류에서만 다음 대상으로 넘깁니다. 사용 가능한 대상이 없으면 전역 기본 프로바이더로 우회하지 않고 요청을 실패 처리합니다.`,"cws.attentionTitle":`확인 필요`,"cws.attention.empty":`구성된 대상 없음`,"cws.attention.few":`대상이 하나뿐 — 장애 조치할 곳이 없음`,"cws.attention.catalogOmitted":`모델 카탈로그에 없음 — 멤버 능력이 불완전하거나 호환되지 않음(context window/메타데이터 부족 또는 modality 교집합이 비어 있음). 별칭 라우팅은 계속 동작`,"cws.attention.allTargetsExhausted":`활성화된 모든 대상의 할당량이 소진됨`,"cws.emptyTitle":`첫 콤보 만들기`,"cws.empty.createDesc":`가상 모델 이름을 정하고 백엔드를 둘 이상 연결하세요.`,"cws.backToAll":`모든 콤보로`,"cws.allCombos":`모든 콤보`,"cws.copyModel":`ID 복사`,"cws.copied":`복사됨`,"cws.tabsLabel":`콤보 상세 섹션`,"cws.tab.config":`설정`,"cws.tab.about":`정보`,"cws.strategy":`전략`,"cws.strategy.failover":`장애 조치`,"cws.strategy.roundRobin":`라운드로빈`,"cws.strategy.random":`랜덤`,"cws.strategy.leastUsed":`최소 사용`,"cws.strategy.resetWindow":`리셋 윈도우`,"cws.strategy.failoverHint":`대상을 순서대로 시도합니다. 재시도 가능한 오류(한도, 장애, 구독 게이트)면 다음으로 넘어갑니다.`,"cws.strategy.roundRobinHint":`가중치에 따라 트래픽을 결정적으로 분배합니다. 선택된 대상을 성공 요청 묶음 동안 유지한 뒤 다음 대상으로 진행합니다.`,"cws.strategy.randomHint":`요청마다 가중치에 비례한 확률로 적합한 대상을 하나 뽑습니다. 요청 간 고정이 없습니다.`,"cws.strategy.leastUsedHint":`각 요청을 성공 횟수가 가장 적은 적합한 대상으로 보냅니다. 횟수는 프록시 재시작 시 초기화됩니다.`,"cws.strategy.resetWindowHint":`쿼터 윈도우가 가장 빨리 리셋되는 적합한 대상을 우선합니다. 쿼터 데이터가 없으면 설정 순서를 따릅니다.`,"cws.field.id":`콤보 ID`,"cws.field.idHintEdit":`ID를 변경하면 콤보 이름이 바뀝니다. 클라이언트는 {model}을(를) 요청합니다.`,"cws.field.alias":`공개 모델 이름`,"cws.field.aliasPlaceholder":`deepseek-v4-flash 또는 vendor/model`,"cws.field.aliasHint":`선택 사항입니다. 접두사 없는 이름, vendor/model 같은 사용자 지정 접두사를 사용하거나 비워 두어 combo/를 사용할 수 있습니다.`,"cws.field.nativeAlias":`네이티브 OpenAI 별칭`,"cws.field.nativeAliasHint":`이 콤보가 지원되는 비수식 OpenAI 네이티브 모델 ID를 사용합니다. 계정/프로바이더 수식 OpenAI 경로는 별도로 유지됩니다.`,"cws.field.displayName":`표시 이름`,"cws.field.displayNameHint":`모델 선택기에 표시할 이름입니다. 네이티브 OpenAI 별칭을 사용할 때 필수입니다.`,"cws.field.idHint":`클라이언트는 {model}을(를) 요청합니다`,"cws.field.idInternalHint":`내부 콤보 ID입니다. 생성 후에도 변경할 수 있습니다.`,"cws.field.stickyLimit":`회전 전 sticky 성공 횟수`,"cws.field.stickyLimitHint":`가중 선택기가 다음 대상으로 진행하기 전에 선택된 대상을 이 성공 요청 횟수만큼 유지합니다.`,"cws.field.defaultEffort":`기본 추론 수준`,"cws.field.defaultEffortNone":`없음 (대상 기본값)`,"cws.field.defaultEffortHint":`클라이언트가 추론 수준을 생략한 경우에만 사용합니다. 옵션은 선택한 대상이 광고하는 수준의 교집합입니다.`,"cws.capability.imageInputUnavailable":`선택한 모든 대상이 이미지 입력을 지원해야 사용할 수 있습니다.`,"cws.capability.imageInputHint":`모든 대상이 이미지를 지원하면 기본으로 켜집니다. 끄면 텍스트만 허용합니다.`,"cws.capability.imageInput":`이미지 / 멀티모달`,"cws.capability.adaptiveEffort":`적응형 추론 단계`,"cws.capability.adaptiveEffortHint":`끔: 추론 단계를 조절할 수 없는 대상이 하나라도 있으면 콤보 전체의 선택기가 사라집니다. 켬: 그런 대상도 그대로 쓰면서, 선택기에는 나머지 대상이 공통으로 지원하는 단계가 남습니다.`,"cws.capabilities":`기능`,"cws.field.defaultEffortUnsupported":`이 수준은 대상의 공통 사다리에 없습니다 — 요청 시 무시되거나 스냅됩니다.`,"cws.field.defaultEffortUnsupportedOption":`교집합에 없음`,"cws.targets":`대상`,"cws.targets.failoverHint":`순서가 중요합니다 — 첫 번째가 기본입니다.`,"cws.targets.roundRobinHint":`가중치는 결정적 상대 선택을 제어하고, 순서는 회전 고리의 동률을 결정합니다.`,"cws.targets.randomHint":`가중치가 각 추첨의 확률을 제어하며, 순서는 무관합니다.`,"cws.targets.leastUsedHint":`순서는 사용량이 같은 대상 간의 동률만 결정합니다.`,"cws.targets.resetWindowHint":`쿼터 데이터가 없거나 동률일 때 순서가 적용됩니다.`,"cws.target.provider":`프로바이더`,"cws.target.model":`모델`,"cws.target.weight":`가중치`,"cws.target.pickProvider":`프로바이더 선택…`,"cws.target.pickProviderFirst":`먼저 프로바이더를 선택하세요…`,"cws.target.pickModel":`모델 선택…`,"cws.target.noModels":`이 프로바이더에 모델 없음`,"cws.target.modelPlaceholder":`모델 ID`,"cws.target.add":`대상 추가`,"cws.target.drag":`드래그하여 순서 변경`,"cws.target.moveUp":`위로`,"cws.target.moveDown":`아래로`,"cws.quota.available":`사용 가능`,"cws.quota.exhausted":`할당량 소진`,"cws.quota.unknown":`할당량 알 수 없음`,"cws.quota.allExhausted":`활성화된 모든 대상의 할당량이 소진되었습니다. 다른 대상을 선택하거나 할당량이 복구될 때까지 기다리세요.`,"cws.aboutTitle":`런타임`,"cws.aboutBody":`실패한 대상은 Retry-After를 반영해 잠시 쿨다운됩니다. 잘못된 요청과 컨텍스트 오류는 다음 대상으로 넘기지 않습니다. 각 대상은 자체 기능에 맞게 추론 수준을 조정하며, 모든 대상 소진 시 우회 없이 실패합니다. 로그와 사용량에는 순서가 있는 실제 시도와 시도별 사용량이 남습니다.`,"cws.removeConfirmTitle":`{model}을(를) 삭제할까요?`,"cws.removeConfirmDesc":`설정과 Codex 카탈로그에서 가상 모델만 제거합니다. 프로바이더는 삭제되지 않습니다.`,"cws.unsavedTitle":`저장되지 않은 변경`,"cws.unsavedDesc":`이 콤보의 편집을 버리고 계속할까요?`,"cws.keepEditing":`계속 편집`,"cws.err.missingId":`콤보 ID가 필요합니다.`,"cws.err.invalidId":`ID는 문자/숫자로 시작하고 문자·숫자·점·밑줄·하이픈만 사용할 수 있습니다(최대 64).`,"cws.err.duplicateId":`같은 ID의 콤보가 이미 있습니다.`,"cws.err.invalidAlias":`별칭은 문자·숫자·점·밑줄·하이픈만 사용할 수 있으며 "/" 구분은 최대 한 번만 허용됩니다.`,"cws.err.aliasReservedNamespace":`별칭은 예약된 "combo/" 네임스페이스를 사용할 수 없습니다.`,"cws.err.aliasNativeFamily":`OpenAI 네이티브 계열(gpt-*, o1-*, o3-*, o4-*, codex-*)의 접두사 없는 별칭은 허용되지 않습니다.`,"cws.err.unsupportedNativeAlias":`네이티브 별칭은 현재 지원되는 OpenAI bare model id 중 하나여야 합니다.`,"cws.err.missingNativeAliasDisplayName":`네이티브 별칭에는 표시 이름이 필요합니다.`,"cws.err.invalidDisplayName":`표시 이름은 128자 이하여야 하며 제어 문자를 포함할 수 없습니다.`,"cws.err.duplicateAlias":`다른 콤보가 이미 이 별칭을 사용하고 있습니다.`,"cws.err.noTargets":`대상을 하나 이상 추가하세요.`,"cws.err.incompleteTarget":`각 대상에 프로바이더와 모델이 필요합니다.`,"cws.target.disabled":`{name} (비활성화됨)`,"cws.err.reservedNamespace":`콤보를 만들기 전에 combo라는 실제 프로바이더의 이름을 변경하세요.`,"cws.err.providerCollision":`콤보 ID가 설정된 프로바이더 이름과 충돌합니다.`,"cws.err.unknownProvider":`각 대상은 설정된 프로바이더를 사용해야 합니다.`,"cws.err.duplicateTarget":`같은 프로바이더/모델 대상은 한 번만 추가할 수 있습니다.`,"cws.err.invalidStickyLimit":`sticky 성공 횟수는 1~100의 정수여야 합니다.`,"cws.err.invalidWeight":`각 라운드로빈 가중치는 1~10000의 정수여야 합니다.`,"cws.err.noEnabledTarget":`하나 이상의 대상이 활성화된 프로바이더를 사용해야 합니다.`,"claude.tabsLabel":`Claude 클라이언트`,"claude.tabCode":`Code`,"claude.tabDesktop":`Desktop`,"claudeDesktop.title":`Claude Desktop`,"claudeDesktop.subtitle":`각 Claude 모델 패밀리를 포트 {port}의 사용 가능한 모델로 연결합니다.`,"claudeDesktop.importJson":`JSON 가져오기`,"claudeDesktop.exportJson":`JSON 내보내기`,"claudeDesktop.loading":`Claude Desktop 프로필을 불러오는 중…`,"claudeDesktop.loadFail":`Claude Desktop 프로필을 불러오지 못했습니다.`,"claudeDesktop.retry":`다시 시도`,"claudeDesktop.saveFailed":`Claude Desktop 프로필을 저장하지 못했습니다.`,"claudeDesktop.applyFailed":`프로필은 저장했지만 적용하지 못했습니다.`,"claudeDesktop.updateFailed":`Claude Desktop 업데이트에 실패했습니다.`,"claudeDesktop.savedApplied":`프로필을 저장하고 Claude Desktop에 적용했습니다.`,"claudeDesktop.appliedMarkerUnsaved":`Claude Desktop에는 적용했지만 적용 표시를 저장하지 못했습니다. 다시 적용하기 전까지 아래 저장/적용 상태가 실제와 다르게 보일 수 있습니다.`,"claudeDesktop.savedAppliedAnnounce":`Claude Desktop 프로필 저장과 적용을 마쳤습니다.`,"claudeDesktop.saved":`프로필을 저장했습니다.`,"claudeDesktop.savedAnnounce":`Claude Desktop 프로필을 저장했습니다.`,"claudeDesktop.exported":`프로필을 JSON으로 내보냈습니다.`,"claudeDesktop.importExpected":`버전 1 Claude Desktop 프로필이 필요합니다.`,"claudeDesktop.importReady":`JSON을 가져왔습니다. 초안을 검토한 뒤 저장하고 적용하세요.`,"claudeDesktop.importedAnnounce":`프로필 JSON을 가져왔습니다. 저장하지 않은 변경 사항을 검토할 수 있습니다.`,"claudeDesktop.importInvalid":`선택한 파일은 올바른 프로필이 아닙니다.`,"claudeDesktop.importFailed":`가져오기에 실패했습니다. {error}`,"claudeDesktop.moved":`{route} 모델을 {family}(으)로 옮겼습니다.`,"claudeDesktop.unsaved":`저장하지 않은 변경 사항`,"claudeDesktop.upToDate":`프로필이 최신 상태입니다`,"claudeDesktop.saving":`저장 중…`,"claudeDesktop.applying":`적용 중…`,"claudeDesktop.saveApply":`저장 및 적용`,"claudeDesktop.emptyTitle":`사용 가능한 모델이 없습니다`,"claudeDesktop.emptyHint":`프로바이더를 추가하거나 활성화한 뒤 Claude Desktop 경로를 할당하세요.`,"claudeDesktop.assignmentsLabel":`Claude 모델 패밀리 할당`,"claudeDesktop.family.opus":`Opus`,"claudeDesktop.family.fable":`Fable`,"claudeDesktop.family.sonnet":`Sonnet`,"claudeDesktop.family.haiku":`Haiku`,"claudeDesktop.modelCountOne":`모델 {count}개`,"claudeDesktop.modelCountMany":`모델 {count}개`,"claudeDesktop.chooseDefault":`기본 모델 선택`,"claudeDesktop.temporaryDefault":`임시 기본 모델`,"claudeDesktop.laneEmpty":`모델을 여기에 놓거나 이동 컨트롤을 사용하세요.`,"claudeDesktop.laneNoMatch":`검색어와 일치하는 모델이 이 계열에 없습니다.`,"nav.grok":`Grok`,"grok.title":`Grok Build`,"grok.subtitle":`opencodex가 Grok 설정에 등록한 모델입니다.`,"grok.loading":`Grok 상태를 불러오는 중…`,"grok.loadFail":`Grok 설정을 읽지 못했습니다.`,"grok.notConfiguredTitle":`Grok Build가 연결되지 않았습니다`,"grok.notConfiguredHint":`Grok을 설치한 뒤 프록시를 다시 시작하면 opencodex가 관리 블록을 다음 위치에 씁니다:`,"grok.endpoint":`엔드포인트`,"grok.colModel":`모델`,"grok.colAlias":`Grok 별칭`,"grok.colContext":`컨텍스트`,"grok.groupNative":`네이티브 모델`,"grok.groupRouted":`라우팅 모델`,"grok.enabledCount":`{total}개 중 {on}개 등록됨`,"grok.saved":`선택을 저장했습니다.`,"grok.savedApplied":`선택을 저장하고 Grok 설정에 반영했습니다.`,"grok.saveFailed":`Grok 선택을 저장하지 못했습니다.`,"grok.applyFailed":`선택은 저장했지만 Grok 설정을 갱신하지 못했습니다.`,"grok.applySkipped":`선택은 저장했지만 Grok 설정은 바뀌지 않았습니다.`,"grok.saveApply":`저장 및 적용`,"grok.saving":`저장 중…`,"grok.applying":`적용 중…`,"grok.unsaved":`저장되지 않은 변경`,"grok.upToDate":`선택이 최신 상태입니다`,"grok.toggleModel":`{id} 모델을 Grok에 등록`,"claudeDesktop.available":`사용 가능`,"claudeDesktop.defaultBadge":`기본`,"claudeDesktop.supports1m":`1M`,"claudeDesktop.unavailable":`사용 불가`,"claudeDesktop.contextM":`컨텍스트 {n}M`,"claudeDesktop.contextK":`컨텍스트 {n}k`,"claudeDesktop.contextUnknown":`컨텍스트 불명`,"claudeDesktop.alias":`별칭`,"claudeDesktop.useAsDefault":`{family} 기본 모델로 사용`,"claudeDesktop.moveTo":`이동 위치`,"claudeDesktop.move":`이동`,"claudeDesktop.status.applied":`Desktop에 적용됨`,"claudeDesktop.status.stale":`설정 변경됨 — 재적용 필요`,"claudeDesktop.status.notApplied":`미적용`,"claudeDesktop.status.notActiveProfile":`Desktop이 다른 프로필을 사용 중 — 재적용 필요`,"claudeDesktop.status.disabled":`Claude Desktop 통합이 꺼져 있습니다. 켠 뒤 Desktop을 완전히 종료하고 다시 여세요.`,"claudeDesktop.enableApply":`켜고 적용`,"claudeDesktop.health.lastRequest":`마지막 요청`,"claudeDesktop.health.stats":`{count} 요청 / {errors} 에러`,"claudeDesktop.effort.supported":`effort`,"claudeDesktop.effort.displayOnly":`effort (표시만)`,"lab.title":`Compatibility Lab`,"lab.subtitle":`Read-only compatibility verdict matrix from lab projection evidence.`,"lab.loadFailed":`Could not load compatibility lab data`,"lab.projectionUnavailable":`Lab projection is not available. Run conformance or live probes first.`,"lab.projectionIncompatible":`Lab projection schema is incompatible. Rebuild the projection.`,"lab.statusTitle":`Projection status`,"lab.matrixTitle":`Compatibility matrix`,"lab.verdictsTitle":`Verdict records`,"lab.filter.layer":`Evidence layer`,"lab.filter.verdict":`Verdict`,"lab.filter.subject":`Subject ID`,"lab.filter.all":`All`,"lab.col.subject":`Subject`,"lab.col.layer":`Layer`,"lab.col.suite":`Suite`,"lab.col.verdict":`Verdict`,"lab.col.asOf":`As of`,"lab.col.protocol":`Protocol conformance`,"lab.col.live":`Live route compatibility`,"lab.col.task":`Task effectiveness`,"lab.empty":`No compatibility verdicts in the projection yet.`,"lab.subjectKind":`Kind`,"lab.observationCount":`Observations`,"lab.eventCount":`Events`,"lab.verdictCount":`Verdicts`,"lab.subjectCount":`Subjects`,"lab.builtAt":`Built`,"lab.loading":`Loading compatibility evidence…`,"lab.loadMore":`Load more`,"lab.detailTitle":`Verdict detail`,"lab.detailClose":`Close`,"lab.detailSubject":`Subject`,"lab.detailObservations":`Observations`,"lab.detailEvents":`Contributing events`,"lab.detailArtifacts":`Artifact metadata`,"lab.production.title":`관측된 프로덕션 트래픽`,"lab.production.notVerification":`랩 검증 아님`,"lab.production.attempts":`시도`,"lab.production.successes":`성공`,"lab.production.routeErrors":`라우팅 오류`,"lab.production.lastObserved":`마지막 관측`,"lab.detailLoadFailed":`Could not load verdict detail`,"lab.refresh":`Refresh`,"lab.verdict.UNKNOWN":`Unknown`,"lab.verdict.CLAIMED":`Claimed`,"lab.verdict.PROBED":`Probed`,"lab.verdict.VERIFIED":`Verified`,"lab.verdict.DEGRADED":`Degraded`,"lab.verdict.BLOCKED":`Blocked`,"lab.verdict.UNSUPPORTED":`Unsupported`,"lab.layer.protocol_conformance":`Protocol conformance`,"lab.layer.live_route_compatibility":`Live route compatibility`,"lab.layer.task_effectiveness":`Task effectiveness`,"dash.visionAdvanced":`고급 설정`,"dash.visionMaxDescriptions":`턴당 최대 설명 수`,"dash.visionMaxDescriptionsInvalid":`양의 정수를 입력하세요.`,"dash.visionTimeout":`제한 시간`,"dash.visionTimeoutInvalid":`{min}에서 {max} 밀리초 사이의 정수를 입력하세요.`,"dash.visionAdvancedPopover":`고급 비전 설정`,"models.newPolicyGlobal":`새 모델을 비활성화 상태로 추가`,"models.newPolicyProvider":`새 모델 정책`,"models.newPolicy_inherit":`상속`,"models.newPolicy_off":`끔`,"models.newPolicy_on":`켬`,"models.newBadge":`신규`,"models.newCount":`신규 {count}개, 꺼짐`,"models.aliases":`별칭`,"models.aliasesTable":`별칭 표`,"models.aliasPrompt":`공급자 별칭 (비우면 해제)`,"models.modelAliasPrompt":`모델 별칭 (비우면 해제)`,"models.aliasSaved":`별칭을 저장했습니다`,"models.aliasConflict":`이 별칭은 기존 이름과 충돌합니다`,"models.editProviderAlias":`공급자 별칭 편집`,"models.editModelAlias":`모델 별칭 편집`,"models.useDefaultAliases":`기본 별칭 사용`,"models.useDefaultAliasesGlobal":`기본 별칭을 전체에 사용`,"models.aliasAuto":`자동`,"models.aliasUser":`사용자`,"models.aliasStale":`오래됨`,"connection.discovering":`로컬 및 공유 대상을 확인하는 중…`,"connection.machineUnavailable":`로컬 머신 연결을 사용할 수 없습니다. 공유 요청을 로컬로 우회하지 않았습니다.`,"connection.disconnect":`허브 연결 해제`,"connection.disconnectConfirm":`이 머신의 허브 연결을 해제하고 독립 실행 모드로 다시 시작할까요?`,"connection.pairing.title":`이 대시보드를 허브에 연결`,"connection.pairing.body":`허브에서 만든 일회용 페어링 코드를 붙여 넣으세요.`,"connection.pairing.relayWarning":`이 코드는 고정 허브 릴레이로 교환됩니다. 릴레이 목적지는 다른 호스트로 바꿀 수 없습니다.`,"connection.pairing.code":`일회용 페어링 코드`,"connection.pairing.submit":`연결`,"connection.pairing.submitting":`연결 중…`,"connection.pairing.error":`페어링 코드가 거부되었거나 만료되었습니다. 확인할 수 있도록 입력값은 유지했습니다.`,"connection.machine.title":`이 머신`,"connection.machine.shimHealthy":`Codex shim이 정상입니다.`,"connection.machine.shimNeedsAttention":`Codex shim을 확인해야 합니다.`,"connection.machine.repairShim":`shim 복구`,"connection.machine.removeShim":`shim 제거`,"connection.clients.title":`연결된 클라이언트`,"connection.clients.none":`클라이언트 상태 없음`,"connection.clients.sync":`지금 동기화`,"connection.clients.syncing":`동기화 중…`,"connection.sessionLogout":`원격 세션 로그아웃`,"connection.sessionLoggingOut":`원격 세션에서 로그아웃하는 중…`,"connection.sessionLogoutFailed":`원격 세션에서 로그아웃하지 못했습니다. 현재 세션은 그대로 유지했습니다.`,"usage.source.connected":`출처: 허브 사용량`,"usage.source.local":`출처: 로컬 usage.jsonl`,"usage.scope.label":`사용량 범위`,"usage.scope.machine":`이 머신`,"usage.scope.hub":`허브 전체`,"usage.hubOffline":`허브 사용량을 불러올 수 없습니다. 로컬 사용량으로 대체하지 않았습니다.`,"integrations.tab.cursor":`Cursor`,"integrations.detail.cursorSeen":`최근 Cursor가 이 프록시를 호출함`,"integrations.detail.cursorNeverSeen":`Private Inference 설치됨, 아직 요청 없음`,"integrations.detail.cursorAbsent":`Cursor Private Inference를 찾지 못함`,"integrations.cursor.title":`Cursor`,"integrations.cursor.intro":`Cursor Private Inference는 에이전트를 로컬에서 돌리고 loopback으로 opencodex와 통신합니다. 일반 Cursor는 백엔드가 커스텀 엔드포인트를 호출하므로 공개 HTTPS 주소가 필요합니다. 이 페이지는 Cursor에 아무것도 쓰지 않습니다. 아래 값을 직접 Cursor에 붙여넣으세요.`,"integrations.cursor.loading":`Cursor 상태 읽는 중…`,"integrations.cursor.unavailable":`프록시에서 Cursor 상태를 읽지 못했습니다.`,"integrations.cursor.detection":`설치된 빌드`,"integrations.cursor.privateInference":`Cursor Private Inference`,"integrations.cursor.regular":`Cursor (일반)`,"integrations.cursor.detected":`감지됨`,"integrations.cursor.notFound":`없음`,"integrations.cursor.regularOnly":`일반 Cursor만 발견됐습니다. 일반 빌드는 커스텀 엔드포인트를 Cursor 서버가 호출하므로 공개 터널 없이는 loopback 프록시에 닿을 수 없습니다. Private Inference 빌드는 가이드를 참고하세요.`,"integrations.cursor.nothingFound":`일반적인 위치에서 Cursor를 찾지 못했습니다. 다른 곳에 설치했다면 아래 값은 그대로 유효합니다.`,"integrations.cursor.gateway":`게이트웨이 값`,"integrations.cursor.gatewayHint":`Cursor Private Inference에서 Settings > Models > Gateway를 열고 아래 두 값을 붙여넣은 뒤 Refresh model list를 누르세요.`,"integrations.cursor.baseUrl":`Base URL`,"integrations.cursor.apiKey":`API 키`,"integrations.cursor.apiKeyCredential":`opencodex API 키 중 하나 (이 바인드는 자격 증명이 필요)`,"integrations.cursor.copy":`복사`,"integrations.cursor.copied":`복사됨`,"integrations.cursor.connection":`연결`,"integrations.cursor.seen":`Cursor의 마지막 요청: {time} ({ua})`,"integrations.cursor.neverSeen":`프록시 시작 후 Cursor 요청이 없습니다. 게이트웨이 저장 후 Cursor에서 Refresh model list를 누르세요.`,"integrations.cursor.models":`Cursor에 표시될 항목`,"integrations.cursor.modelsHint":`Reasoning 사다리는 Cursor 자체 모델 표가 정하므로 opencodex는 예측만 합니다. Context는 기본 창과 옵트인 창(Cursor의 Max Mode)입니다.`,"integrations.cursor.ladderFromBundle":`Reasoning 사다리는 설치된 Cursor Private Inference {version} 번들에서 읽었습니다. 사다리는 Cursor가 정하고 opencodex는 그 표를 보여줄 뿐입니다.`,"integrations.cursor.ladderFromStatic":`Reasoning 사다리는 Cursor 3.18.25의 정적 미러입니다(읽을 수 있는 Private Inference 번들을 찾지 못함). Context는 기본 창과 옵트인 창입니다.`,"integrations.cursor.unknownVersion":`버전 미상`,"integrations.cursor.noControl":`—`,"integrations.cursor.singleWindow":`단일 창`,"integrations.cursor.noControlTitle":`이 id는 Cursor 내장 effort 표에 없어서 Cursor가 Reasoning 컨트롤을 보여주지 않습니다.`,"integrations.cursor.effortRowsOne":`effort 행 1개 게시됨`,"integrations.cursor.effortRowsMany":`effort 행 {n}개 게시됨`,"integrations.cursor.effortRowsOff":`effort 행 없음`,"integrations.cursor.tableLessHint":`—로 표시된 행은 Cursor에서 Reasoning 컨트롤이 없습니다. cursorEffortRows를 켜면 effort마다 picker 항목(id--effort)을 하나씩 게시하고, 고정 기본값은 provider의 modelDefaultReasoningEfforts로 정합니다.`,"integrations.cursor.colModel":`모델`,"integrations.cursor.colReasoning":`추론`,"integrations.cursor.colContext":`컨텍스트`,"integrations.cursor.guide":`Cursor Private Inference 가이드 열기`},He={"nav.dashboard":`仪表盘`,"uptime.day":`天`,"uptime.hour":`小时`,"uptime.minute":`分钟`,"uptime.second":`秒`,"nav.startup":`启动安全`,"nav.providers":`提供方`,"nav.models":`模型`,"nav.combos":`组合`,"nav.subagents":`子代理`,"routing.title":`路由智能 (beta)`,"routing.subtitle":`策略配置文件、试运行评估以及基于来源的路由分析。`,"routing.loadFailed":`无法加载路由数据`,"routing.empty":"未配置路由策略。请在 config.json 中添加 `routingProfiles`。","routing.revision":`rev`,"routing.detail":`配置文件`,"routing.createProfile":`创建配置文件`,"routing.dryRunError":`试运行失败 (HTTP {status})`,"routing.removeConfirm":`删除配置文件 {id}?`,"routing.unknownEvidence.allow":`允许`,"routing.unknownEvidence.penalize":`惩罚`,"routing.unknownEvidence.exclude":`排除`,"routing.removeCandidate":`移除候选 {provider}/{model}`,"routing.candidates":`候选`,"routing.require":`硬性要求`,"routing.optimize":`优化权重`,"routing.limits":`限制`,"routing.unknownEvidence":`未知证据策略`,"routing.compatibility.title":`兼容性策略`,"routing.compatibility.enabled":`要求 Compatibility Lab 证据`,"routing.compatibility.requiredSuites":`必需套件`,"routing.compatibility.loadingCatalog":`正在加载 Lab 目录…`,"routing.compatibility.catalogUnavailable":`Lab 目录不可用 — 请在 config.json 中手动输入套件 ID。`,"routing.compatibility.layer.protocol_conformance":`协议一致性`,"routing.compatibility.layer.live_route_compatibility":`实时路由兼容性`,"routing.compatibility.minStatus":`最低兼容性状态`,"routing.none":`无`,"routing.unavailable":`–`,"routing.dryRun":`试运行评估`,"routing.dryRunContext":`请求上下文窗口(令牌)`,"routing.dryRunTools":`请求需要工具`,"routing.dryRunImage":`请求需要图像输入`,"routing.dryRunStructured":`请求需要结构化输出`,"routing.dryRunRun":`评估候选`,"routing.candidate":`候选`,"routing.eligible":`合格`,"routing.exclusions":`排除项`,"routing.costCap":`成本上限`,"routing.capOutcome.satisfied":`未超限`,"routing.capOutcome.exceeded":`已超限`,"routing.capOutcome.unknown-allowed":`未知(允许)`,"routing.capOutcome.unknown-excluded":`未知(排除)`,"routing.exclusion.capability-unsatisfied":`能力未满足`,"routing.exclusion.unknown-capability":`能力未知`,"routing.exclusion.cost-limit":`超出成本上限`,"routing.exclusion.cost-limit-unknown":`无法确认成本是否在上限内`,"routing.exclusion.cooldown":`冷却中`,"routing.exclusion.unknown-health":`健康状态未知`,"routing.exclusion.unknown-quota":`配额未知`,"routing.exclusion.unknown-price":`价格未知`,"routing.exclusion.other":`排除项:{code}`,"routing.score":`分数`,"routing.selected":`已选择`,"routing.yes":`是`,"routing.no":`否`,"routing.analytics":`路由分析`,"routing.analyticsTotal":`请求`,"routing.analyticsSuccessRate":`成功率`,"routing.analyticsFallbackRate":`回退`,"routing.analyticsP50":`p50`,"routing.analyticsP95":`p95`,"routing.analyticsP99":`p99`,"routing.analyticsCooldown":`冷却失败`,"routing.analyticsConfidence":`置信度`,"routing.analyticsTruncated":`历史已截断`,"routing.analyticsRequests":`请求`,"routing.analyticsEmpty":`暂无分析 — 请先发送一些请求。`,"nav.logs":`日志与调试`,"nav.usage":`用量`,"common.github":`GitHub`,"sidebar.star":`在 GitHub 上加星`,"sidebar.starred":`已在 GitHub 加星`,"sidebar.starUnauthenticated":`打开 GitHub 加星(gh CLI 未登录)`,"sidebar.starFailed":`无法通过 gh 加星,改为打开 GitHub。`,"sidebar.updateAvailable":`有可用更新:{version}`,"sidebar.checkUpdate":`检查更新`,"common.save":`保存`,"common.saving":`保存中…`,"common.cancel":`取消`,"common.discard":`丢弃`,"common.delete":`删除`,"common.remove":`移除`,"common.loading":`加载中…`,"common.retry":`重试`,"auth.adminTokenTitle":`OpenCodex 管理员令牌 (OPENCODEX_ADMIN_AUTH_TOKEN)`,"auth.adminAccountLabel":`账户`,"auth.adminTokenFieldLabel":`管理员令牌`,"auth.adminTokenRejected":`管理员令牌被拒绝。请检查后重试。`,"auth.adminTokenUnavailable":`无法验证管理员令牌。请重试。`,"theme.label":`主题`,"theme.light":`浅色`,"theme.dark":`深色`,"theme.system":`跟随系统`,"lang.label":`语言`,"lang.nativeName":`中文`,"provider.name.commandCodeAuth":`Command Code - Auth`,"provider.name.commandCodeApi":`Command Code - API`,"provider.name.volcengine":`火山方舟`,"provider.name.volcengineCodingPlan":`火山方舟编程套餐`,"provider.name.volcengineAgentPlan":`火山方舟智能体套餐`,"errorBoundary.title":`页面加载失败`,"errorBoundary.message":`此部分在渲染时发生错误。请重新加载后再试。`,"errorBoundary.details":`错误`,"errorBoundary.reload":`重新加载`,"startup.title":`启动安全`,"startup.subtitle":`检查重启后 Codex 是否仍能连接 opencodex,避免本地代理路由陷入重复重连。`,"startup.refresh":`刷新`,"startup.backToDashboard":`返回仪表盘`,"startup.loading":`正在检查启动保护…`,"startup.error":`无法读取启动保护状态。`,"startup.staleData":`最新启动检查失败。以下数据已过期,不得视为已受保护的证明。`,"startup.status.native":`原生路由`,"startup.status.protected":`重启已受保护`,"startup.status.atRisk":`需要处理`,"startup.summary.native":`Codex 不依赖本地代理`,"startup.summary.protected":`重启后 opencodex 会自动可用`,"startup.summary.atRisk":`重启后 Codex 可能无法访问模型`,"startup.riskDetail":`Codex 已指向本地代理,但没有持久服务或正常的 launcher shim 将其重新启动。`,"startup.riskDetailCustomLocal":`Codex 指向自定义本地网关。opencodex 无法管理或验证该网关的重启生命周期。`,"startup.riskDetailWindowsShim":`Launcher shim 仅保护受支持的 CLI 脚本;Windows 上的 Codex Desktop 和直接 codex.exe 启动可以绕过它。`,"startup.safeDetail":`当前路由与启动机制一致。重启后无需手动运行 ocx start。`,"startup.routing":`Codex 路由`,"startup.routing.proxy":`本地代理`,"startup.routing.native":`OpenAI 原生`,"startup.routing.customLocal":`自定义本地网关`,"startup.routing.customRemote":`自定义远程网关`,"startup.routing.unknown":`未知或无效的路由`,"startup.restartProtection":`重启保护`,"startup.preference":`按需启动`,"startup.enabled":`已启用`,"startup.disabled":`已禁用`,"startup.protection.service":`后台服务`,"startup.protection.shim":`Launcher shim`,"startup.protection.none":`未安装`,"startup.details":`保护详情`,"startup.service":`后台服务`,"startup.serviceHint":`登录时启动,并在代理崩溃后重新启动。`,"startup.installed":`已安装`,"startup.notInstalled":`未安装`,"startup.unsupported":`不支持`,"startup.shim":`Codex launcher shim`,"startup.shimHint":`支持的 Codex 脚本启动器运行时执行 ocx ensure。`,"startup.healthy":`正常`,"startup.cliOnly":`仅 CLI`,"startup.stale":`已过期`,"startup.viable":`可用`,"startup.unhealthy":`已安装但异常`,"startup.conflict":`服务冲突`,"startup.installedDisabled":`已安装但禁用`,"startup.install":`安装`,"startup.installing":`正在安装…`,"startup.repair":`修复`,"startup.repairing":`正在修复…`,"startup.serviceInstalled":`后台服务安装成功。`,"startup.serviceRepaired":`后台服务修复成功。`,"startup.shimInstalled":`Codex 启动器 shim 安装成功。`,"startup.shimRepaired":`Codex 启动器 shim 修复成功。`,"startup.installFailed":`安装失败:`,"startup.tray.title":`Windows 系统托盘`,"startup.tray.hint":`登录时启动托盘图标,一键控制代理启动、停止、重启、面板和状态。`,"startup.tray.login":`Windows 登录时启动托盘`,"startup.tray.notProtection":`托盘只是控制器,并非重启保护。无人值守恢复仍需要正常的后台服务。`,"startup.tray.running":`运行中`,"startup.tray.stopped":`已安装,未显示`,"startup.tray.stale":`需要修复`,"startup.tray.notInstalled":`未安装`,"startup.tray.loading":`正在检查…`,"startup.tray.unavailable":`状态不可用`,"startup.tray.install":`安装并显示托盘`,"startup.tray.start":`显示托盘图标`,"startup.tray.stop":`退出托盘图标`,"startup.tray.uninstall":`移除登录托盘`,"startup.tray.error":`Windows 托盘操作失败。请运行 ocx tray status 查看详情。`,"startup.recovery":`修复选项`,"startup.recoveryHint":`使用上方的一键安装,或复制命令进行手动修复。Codex Desktop 和 Windows 可执行文件建议使用后台服务。`,"startup.command.service":`推荐:持久后台服务`,"startup.command.shim":`备选:CLI launcher shim`,"startup.command.native":`安全恢复:还原 Codex 原生路由`,"startup.copy":`复制`,"startup.copied":`已复制`,"startup.recommended":`推荐修复:{cmd}`,"startup.navRisk":`启动保护需要处理`,"startup.codexRuntime.clampHidden":`部分推理强度选项已隐藏,因为 OpenCodex 正在使用 Codex {version}。`,"startup.codexRuntime.clampHiddenWithEfforts":`部分推理强度选项已隐藏,因为 OpenCodex 正在使用 Codex {version}(已移除:{efforts})。`,"startup.codexRuntime.olderBinary":`OpenCodex 正在使用较旧的 Codex 二进制文件({version})。检测到可用的较新安装。`,"dash.subtitle":`本地 opencodex 代理、其提供方以及路由到 Codex 的模型的实时状态。`,"dash.workspace.overview":`概览`,"dash.workspace.sections":`板块`,"dash.status":`状态`,"dash.online":`在线`,"dash.offline":`离线`,"dash.version":`版本`,"dash.uptime":`运行时间`,"dash.providers":`提供方`,"dash.tokens30d":`Token (30 天)`,"dash.coverage":`覆盖率 {pct}`,"dash.mem.title":`内存可观测性`,"dash.mem.hint":`只读运行时诊断。观测内存为 max(RSS, external, ArrayBuffers),避免 Windows working set trimming 隐藏已提交的保留内存。`,"dash.mem.rss":`常驻内存 (RSS)`,"dash.mem.jsHeap":`JS 堆已用`,"dash.mem.jsHeapArena":`堆区 {total}`,"dash.mem.pressure":`相对告警阈值`,"dash.mem.pressureOf":`阈值的 {pct}%`,"dash.mem.pressureUnknown":`未提供阈值`,"dash.mem.jscHeap":`JSC 堆`,"dash.mem.external":`External`,"dash.mem.arrayBuffers":`ArrayBuffers`,"dash.mem.observed":`观测值`,"dash.mem.runtime":`运行时计数器`,"dash.mem.growth":`每小时观测变化`,"dash.mem.perHour":`/小时`,"dash.mem.store":`延续存储`,"dash.mem.storeHint":`代理 previous_response_id 缓存。堆上升时总字节数增加,说明是对话保留而非运行时分配器。`,"dash.mem.storeEntries":`条目`,"dash.mem.storeTotal":`总计`,"dash.mem.storeLargest":`最大`,"dash.mem.storeOldest":`最旧`,"dash.mem.threshold":`告警阈值`,"dash.mem.lastWarn":`上次告警`,"dash.mem.never":`从不`,"dash.mem.details":`详情`,"dash.mem.unavailable":`内存诊断不可用(旧版代理)。`,"dash.mem.inFlight":`进行中的请求`,"dash.mem.restart":`排空并重启`,"dash.mem.restartConfirm":`等待 {count} 个进行中的请求结束后再重启(最多 {seconds} 秒;超时将中断剩余请求)。`,"dash.mem.draining":`正在等待 {count} 个请求完成… 完成后重启`,"dash.mem.reconnecting":`代理正在重启… 等待重新连接`,"dash.mem.restartFailed":`排空并重启失败。请确认代理正在运行。`,"dash.mem.restartNoSupervisor":`未检测到重启保护。重启后代理可能不会自动恢复,需手动启动。`,"dash.activeProviders":`活跃提供方`,"dash.noProviders":`尚未配置提供方。请运行 {cmd}。`,"dash.col.name":`名称`,"dash.col.adapter":`适配器`,"dash.col.baseUrl":`Base URL`,"dash.col.model":`模型`,"dash.modelsNoResults":`没有符合搜索的模型。`,"dash.availableModels":`可用模型`,"dash.noModels":`未找到模型。请检查提供方 API 密钥。`,"dash.cannotConnect":`无法连接到代理。它在运行吗?`,"dash.runStart":`运行 {cmd} 以启动代理。`,"dash.stop":`停止代理`,"dash.stopConfirm":`停止代理并恢复原生 Codex 配置?`,"dash.stopFailed":`无法停止代理 (HTTP {status})。`,"dash.maSwitchFailed":`模式切换失败 (HTTP {status})。`,"dash.maNetworkError":`网络错误 — 代理是否正在运行?`,"dash.stopping":`正在停止…`,"dash.actions":`代理`,"dash.codexRestart":`重新加载 Codex 模型列表`,"dash.codexRestarting":`正在停止…`,"dash.codexRestartConfirm":`停止 Codex app-server 以便重新读取模型列表?进行中的 Codex 任务会被中断,且 Codex 不会自动重启,请稍后自行重新打开。`,"dash.codexRestartDone":`已停止 {count} 个 Codex app-server。重新打开 Codex 即可加载最新模型列表。`,"dash.codexRestartNothing":`没有正在运行的 Codex app-server。下次启动会读取最新模型列表。`,"dash.codexRestartUnknown":`无法枚举进程,因此没有停止任何进程。`,"dash.codexRestartPartial":`有 {count} 个 app-server 未退出。若模型列表仍然过旧,请手动停止。`,"dash.codexRestartFailed":`无法重新加载 Codex 模型列表 (HTTP {status})。`,"dash.codexRestartUnreachable":`无法连接到代理。`,"dash.codexRestartMalformed":`代理返回了意外的响应。`,"dash.codexRestartTimeout":`代理未在超时前响应,可能仍在停止 app-server。`,"models.staleBanner":`Codex 显示的模型列表比当前目录旧。重启 Codex 即可重新读取。`,"dash.codexAutoStart":`随 Codex 启动 opencodex`,"dash.codexAutoStartHint":`允许已安装的 launcher shim 运行 ocx ensure。此设置不会安装重启保护;请在启动安全中检查实际状态。`,"dash.searchModel":`搜索附属模型`,"dash.searchModelHint":`用于非 OpenAI 路由模型的 web_search 的模型。需要 ChatGPT 登录。`,"dash.searchReasoning":`搜索推理强度`,"dash.visionModel":`视觉附属模型`,"dash.visionModelHint":`为纯文本路由模型描述图像的模型。需要 ChatGPT 登录。`,"dash.webSearchSidecar":`网页搜索附属服务`,"dash.webSearchSidecarHint":`选择路由模型进行网页搜索时使用的后端和模型。`,"dash.webSearchStream":`实时流式输出回答`,"dash.webSearchStreamHint":`实时流式输出开头的文本和推理,直到模型决定调用工具;其余部分为拦截搜索而保持缓冲。搜索前的文本可能会部分重复。`,"dash.visionSidecar":`视觉附属服务`,"dash.visionSidecarHint":`选择纯文本路由模型描述图像时使用的后端和模型。`,"dash.visionOff":`关闭`,"dash.shadowCallIntercept":`影子调用拦截`,"dash.shadowCallInterceptHint":`拦截 Codex 应用的后台辅助调用({models}:标题生成、提交消息)并重定向到所选模型。`,"dash.shadowCallWarning":`⚠ 启用后,所有对 {models} 的请求都将被替换为所选模型。`,"dash.shadowCallOriginal":`原始`,"dash.shadowCallModel":`替代模型`,"dash.shadowCallTooltip":`Codex 应用会在后台调用辅助模型来生成线程标题、提交消息以及进行技能编排。该模型随客户端版本变化,因此 opencodex 会同时拦截这些模型:{models}。启用此选项可将这些调用重定向到您选择的模型。`,"models.shadowCallIntercept":`影子调用拦截`,"models.shadowCallInterceptHint":`拦截 Codex 应用的后台辅助调用({models})并重定向到所选模型。`,"dash.sidecarBackend":`后端`,"dash.sidecarModel":`模型`,"dash.backendAuto":`自动`,"dash.backendOpenAI":`OpenAI`,"dash.backendAnthropic":`Anthropic`,"dash.sidecarSaved":`附属设置已保存。将在下一个请求时生效。`,"dash.sidecarSaveFailed":`保存附属设置失败。`,"dash.injectionLabel":`子代理委托`,"dash.injectionHint":`选择 Codex 把子任务交给谁来做的模型。这个选择用在哪里,由下面两个开关决定。`,"dash.syncCodexSubagentDefaults":`同时保存为 Codex 默认值`,"dash.syncCodexSubagentDefaultsHint":`打开后,上面选的模型会写进 Codex 自己的配置,新任务一开始也用它。关闭则只在这里记住。下次同步或重启后生效,你手写的 [agents] 设置不会被改动。`,"dash.multiAgentGuidance":`告诉 Codex 怎么分工`,"dash.multiAgentGuidanceHint":`给 Codex 附上一张短便条,说明怎么把活分给子代理。v2 会告诉它可用的模型和优先模型;v1 只在推理强度为 max 或 ultra 时才起作用。关闭则不附任何便条。`,"dash.injectionNone":`无`,"dash.injectionEffortLabel":`推理强度`,"dash.injectionEffortNone":`模型默认`,"dash.effortCapLabel":`V2 ultra 推理强度限制`,"dash.subagentEffortCapLabel":`V2 子代理推理强度限制`,"dash.effortCapHelp":`限制 V2 ultra 模式轮次的推理强度。设置后,来自 ultra 模式的 max 请求将被限制到所选级别。子代理限制仅适用于衍生的子代理。只会降低强度,不会提高。如果模型不支持所选级别,将自动降至最近的支持级别。`,"dash.effortCapNone":`无上限`,"dash.maintenance":`维护`,"dash.maintenanceHint":`刷新 Codex 模型目录,或安装新的 opencodex 版本。`,"dash.syncModels":`同步模型`,"dash.syncing":`同步中…`,"dash.syncOk":`同步完成。已追加 {count} 个模型。`,"dash.syncStaleHint":`如果 Codex 仍显示旧列表,请重启长期运行的 app-server({cmd})。`,"dash.syncFailed":`同步失败:{error}`,"dash.projectConfigTitle":`项目 Codex 配置绕过了 OpenCodex`,"dash.projectConfigHint":`这些仓库级设置会覆盖 OpenCodex 代理(例如直接走 OpenCode Go)。请移除它们,以便该项目使用 ~/.codex/config.toml 的代理路由。`,"dash.checkUpdate":`检查更新`,"dash.updateTitle":`更新 opencodex`,"dash.updateDesc":`检查所选 npm 频道的最新版本,然后选择安装后是否重启代理。`,"dash.updateChannel":`频道`,"dash.updateChecking":`正在检查更新…`,"dash.updateInstalled":`已安装`,"dash.updateLatest":`最新`,"dash.updateAvailable":`有可用更新`,"dash.updateCurrent":`已是最新`,"dash.updateCommand":`命令`,"dash.updateSource":`当前是源码检出。请在终端运行显示的命令进行更新。`,"dash.updateUnavailable":`无法从 npm 读取最新版本。请稍后重试。`,"dash.updateRetry":`重试`,"dash.updateRecheck":`重新检查`,"dash.updateCannotAuto":`无法一键更新({reason})。`,"dash.updateReason.source_checkout":`源码检出`,"dash.updateReason.latest_unavailable":`无法连接 npm 注册表`,"dash.updateReason.already_latest":`已是最新版本`,"dash.updateReason.unknown":`无法更新`,"dash.updateRestart":`更新后重启`,"dash.updateRestartHint":`推荐开启。代理重启前,当前 GUI 仍运行旧代码。`,"dash.runUpdate":`更新`,"dash.updateReconnecting":`正在等待重启后的代理…`,"dash.updateStatus.running":`正在更新 opencodex。`,"dash.updateStatus.restarting":`更新已安装。正在重启代理。`,"dash.updateStatus.succeeded":`更新完成。`,"dash.updateVersionTransition":`{currentVersion} -> {latestVersion}.`,"dash.updateStatus.failed":`更新失败。`,"prov.subtitle":`配置 opencodex 路由到 Codex 的上游提供方。使用账户登录、添加提供方,或编辑原始配置。`,"prov.add":`添加提供方`,"prov.editJson":`编辑 JSON`,"prov.accountLogin":`账户登录`,"prov.noOauth":`没有可用的 OAuth 提供方。`,"prov.loggedIn":`已登录`,"prov.notLoggedIn":`未登录`,"prov.logout":`退出登录`,"prov.login":`登录`,"prov.loginWith":`使用 {provider} 登录`,"prov.waitingBrowser":`等待浏览器…`,"prov.didntOpen":`没有打开?点击这里`,"prov.copyLink":`复制链接`,"prov.dontOpenBrowser":`不要在运行代理的机器上打开浏览器`,"prov.dontOpenBrowserHint":`适用于使用其他浏览器配置文件登录,或仪表板与代理不在同一台机器上。`,"prov.linkCopied":`已复制`,"prov.linkCopyUnavailable":`剪贴板不可用`,"prov.deviceCode":`设备验证码`,"prov.copyCode":`复制验证码`,"prov.codeCopied":`验证码已复制`,"prov.editAlias":`编辑别名`,"prov.aliasPrompt":`显示名称(留空以清除)`,"prov.aliasSaved":`别名已保存`,"prov.aliasSaveFailed":`无法保存别名`,"prov.accountId":`ID`,"prov.pasteRedirect":`粘贴重定向 URL 或授权码`,"prov.pasteRedirectHint":`如果浏览器显示 localhost 错误,请复制地址栏中的完整 URL 并粘贴到此处(或粘贴授权码)。`,"prov.pasteSubmit":`提交`,"prov.pasteSubmitting":`提交中…`,"prov.pasteOk":`已提交代码 — 正在完成登录…`,"prov.pasteFail":`无法提交代码:{error}`,"prov.port":`端口`,"prov.default":`默认`,"prov.loadingConfig":`加载中…`,"prov.saved":`已保存!重启代理以生效。`,"prov.loadConfigFail":`加载配置失败`,"prov.invalidJson":`无效的 JSON`,"prov.saveFailed":`保存失败`,"prov.loginFailStart":`{provider} 登录启动失败`,"prov.loginError":`{provider} 登录错误:{error}`,"prov.loginRequestFail":`{provider} 登录请求失败`,"prov.loginCancelled":`{provider} 登录已取消`,"prov.loginTimeout":`{provider} 登录超时 — 浏览器已关闭或未完成。请重试。`,"prov.loginOk":`已登录到 {provider}。运行 {cmd}(或实时生效)以列出其模型。`,"prov.loginSameAccount":`仍是同一个 {provider} 账户 — 请在浏览器中切换账户后再次尝试添加账户。`,"oauthTos.highTitle":`{provider}:订阅 OAuth 风险`,"oauthTos.elevatedTitle":`{provider}:非官方 OAuth 桥接`,"oauthTos.anthropicBody":`通过 OpenCodex 等第三方代理直接复用 Claude 订阅 OAuth 令牌,并非 Anthropic 支持的集成方式,可能导致访问受限。可使用 Claude 订阅的受支持 Agent SDK 集成属于另一种方式。`,"oauthTos.highBody":`OpenCodex 通过第三方 OAuth 路径连接 {provider}。如果该用法不受支持,访问可能会被限制或暂停。`,"oauthTos.elevatedBody":`OpenCodex 通过非官方 OAuth 路径连接 {provider}。请尽量使用官方客户端;异常或自动化流量可能被视为滥用,访问可能会被限制或暂停。`,"oauthTos.saferPath":`更安全的做法:改为在 OpenCodex 中配置 API 密钥。`,"oauthTos.acknowledge":`我了解风险,仍要继续使用 OAuth。`,"oauthTos.continue":`继续使用 OAuth`,"prov.logoutOk":`已退出 {provider}。`,"prov.logoutFail":`无法退出 {provider}。账户状态保持不变。`,"prov.removed":`已移除 "{name}"。`,"prov.removedDefault":`已移除 "{name}"。默认提供方现为 "{defaultProvider}"。`,"prov.removeFail":`移除 "{name}" 失败。`,"prov.removeLastProvider":`如果没有其他已启用的提供方可以成为默认,则无法移除此提供方。`,"prov.removeHasDependentCombos":`请先移除或更新依赖它的组合:{combos}。`,"prov.setDefault":`设为默认`,"prov.setDefaultSuccess":`"{name}" 已设为默认提供方。`,"prov.setDefaultFail":`无法将 "{name}" 设为默认提供方。`,"prov.defaultDisabled":`请先启用此提供方,再将其设为默认。`,"prov.updateFail":`无法更新此提供方。`,"prov.networkError":`网络错误。请确认代理正在运行后重试。`,"prov.added":`已添加 "{name}"。现已生效 — 运行 {cmd}(或重启)以在 Codex 选择器中列出其模型。`,"prov.removeConfirm":`移除提供方 "{name}"?其模型将从 Codex 选择器中消失。`,"prov.hasApiKey":`已配置 API 密钥`,"prov.hasHeaders":`已配置自定义请求头`,"prov.accounts":`账户({n})`,"prov.accountsAria":`展开/收起 {name} 账户`,"prov.accountActive":`使用中`,"prov.accountReauth":`需重新登录`,"prov.reauthenticate":`重新认证`,"prov.reauthAccountMissing":`登录后未找到所选账号`,"prov.reauthIdentityMismatch":`登录账号与所选账号不匹配`,"prov.accountAdd":`添加账户`,"prov.accountNoLabel":`账户 {id}`,"prov.accountSwitchTitle":`使用此账户`,"prov.accountSwitched":`已切换到 {email}。`,"prov.accountSwitchFail":`切换账户失败`,"prov.accountRemoved":`已移除 {email}。`,"prov.accountRemoveFail":`无法移除 {email}。账户保持不变。`,"prov.accountRemoveAria":`移除 {email}`,"prov.accountRemoveConfirm":`移除账户 {email}?其登录将从此代理中删除。`,"prov.keyAdd":`添加 API 密钥`,"prov.keyAdded":`已为 {name} 添加 API 密钥。`,"prov.keyAddFail":`添加 API 密钥失败`,"prov.keyPlaceholder":`粘贴 API 密钥`,"prov.keySwitchTitle":`使用此密钥`,"prov.keySwitched":`已切换到密钥 {key}。`,"prov.keySwitchFail":`切换密钥失败`,"prov.keyRemoved":`已移除密钥 {key}。`,"prov.keyRemoveAria":`移除密钥 {key}`,"prov.keyRemoveConfirm":`移除 API 密钥 {key}?它将从此代理的配置中删除。`,"prov.activeBadge":`已启用`,"prov.disabledBadge":`已禁用`,"prov.defaultBadge":`默认`,"prov.enable":`启用`,"prov.disable":`禁用`,"prov.enabled":`已启用 "{name}"。其模型可再次出现在 Codex 中。`,"prov.disabled":`已禁用 "{name}"。设置会保留,但模型会被隐藏。`,"prov.enableFail":`启用 "{name}" 失败。`,"prov.disableFail":`禁用 "{name}" 失败。`,"prov.enableAria":`启用提供方 {name}`,"prov.disableAria":`禁用提供方 {name}`,"prov.defaultCannotDisable":`默认提供方不能被禁用`,"prov.openaiAccountMode":`Codex 账户模式`,"prov.openaiModePool":`账户池`,"prov.openaiModeDirect":`直连`,"prov.openaiPoolDesc":`默认模式。根据会话关联、额度、冷却时间和故障转移,在主登录与已添加账户之间轮换。`,"prov.openaiDirectDesc":`仅使用当前主 Codex 登录。不会读取或轮换已存储的账户池账号。`,"prov.openaiModeSaved":`OpenAI 账户模式已更改为 {mode}。`,"prov.openaiModeSaveFailed":`无法更改 OpenAI 账户模式。`,"prov.openaiApiDesc":`仅使用 OpenAI API 密钥,不使用 Codex 账户凭据。`,"prov.manageCodexAccounts":`管理 Codex 账户`,"prov.openaiApiMissing":`需要 API 密钥`,"prov.openaiApiSetup":`设置 API 密钥`,"models.tab.catalog":`模型`,"models.tab.combos":`组合`,"models.tab.compatibility":`兼容性`,"models.tab.routing":`路由 (beta)`,"models.tabsLabel":`模型界面`,"models.subtitle.combos":`把多个模型合成一个 id 依次应答。用 failover 串联目标,或用均衡策略分摊负载。`,"models.subtitle.compatibility":`来自实验室投影证据的只读兼容性判定矩阵。`,"models.subtitle.routing":`策略配置、dry-run 评估,以及有据可查的路由分析。`,"models.subtitle":`开关 Codex 可见的模型 — 原生 GPT passthrough 与已路由模型按提供方分组(点击标题可折叠)。隐藏的模型不会出现在目录和模型选择器中,但仍可按精确 id 直接调用。更改在下一个 Codex 回合生效 — opencodex 会使 Codex 的 5 分钟模型缓存失效,因此无需重启。`,"models.nativeGroupLabel":`OpenAI 原生`,"models.nativeHint":"Passthrough 模型使用在提供方页面选择的账户池或直连选项。关闭后会从 Codex 选择器中隐藏(目录条目保留,重新开启即可完整恢复)。 在此添加模型将注册为路由的 `openai/` 选择器,而不是新的裸 passthrough id。","models.active":`{active}/{total} 可见`,"models.workspace.providers":`提供方`,"models.workspace.allProviders":`所有提供方`,"models.workspace.mainAria":`模型详情`,"models.allOn":`全部开启`,"models.allOff":`全部关闭`,"models.presetLabel":`模型`,"models.presetMode_preset":`预设`,"models.presetMode_all":`全部`,"models.presetMode_custom":`自定义`,"models.presetSummary":`显示 {count} / {total} — 核心预设 v{version}`,"models.presetUpdateAvailable":`预设 v{version} 可用`,"models.presetAppliedToast":`{provider}:已应用预设 — 选中 {count} 个模型`,"models.presetClearedToast":`{provider}:显示全部模型`,"models.presetEmpty":`{provider}:预设未匹配到模型,选择保持不变`,"models.presetConfirmReplace":`用包含 {count} 个模型的预设替换你的选择?`,"models.cap350k":`限制 350k`,"models.capApplied":`上下文限制已应用 — 将在下一个 Codex 回合生效。`,"models.capSaveFailed":`保存上下文限制失败`,"models.contextCapped":`350k 限制`,"models.contextCapLabel":`默认窗口 / 上限`,"models.v2Label":`子代理`,"models.shadowCallOriginal":`⚠ {models} →`,"models.v2Mode_v1":`v1`,"models.v2Mode_default":`base`,"models.v2Mode_v2":`v2`,"models.v2ModeDesc_v1":`所有模型 → v1 界面`,"models.v2ModeDesc_default":`上游默认值 (sol/terra=v2, luna=v1)`,"models.v2ModeDesc_v2":`所有模型 → v2 界面`,"models.keepNativeOnV1":`ChatGPT 保持 v1`,"models.keepNativeOnV1Hint":`仅当 ChatGPT 原生父代理仍留在 v2 时,才会加密 v2 子任务,Grok/Claude 无法读取。开启此选项可让 Sol/Terra 留在 v1,从而避免该加密。路由父代理仍使用 v2。`,"models.v2Help":`控制所有模型的多代理界面。 - -v1: 经典单线程代理。所有模型使用 v1 协作界面。 -base: 上游默认值 — sol/terra 使用 v2,luna 使用 v1,其余跟随 codex 功能标志。 -v2: 多线程代理(spawn_agent)。所有模型使用 v2 协作界面。 - -在 v2 下,「ChatGPT 保持 v1」会让 Sol/Terra 留在 v1,以便继续派发 Grok 或 Claude。ChatGPT 会加密 v2 子任务,路由模型无法读取;路由父代理仍留在 v2。 - -更改在新会话中生效。`,"models.v2DocsLink":`v1 / v2 是什么?`,"dash.multiAgent":`子代理`,"models.v2Conflict":`[agents] max_threads 仍存在 — codex 将拒绝启动,请从 config.toml 移除`,"models.v2Applied":`子代理模式已更新 — 新会话生效(重启 Codex 应用以刷新选择器)`,"models.v2ThreadsLabel":`最大线程`,"models.v2ThreadsDefault":`默认 (4)`,"models.v2ThreadsApplied":`线程上限已更新 — 新会话生效`,"models.v2ThreadsInvalid":`线程上限必须为 >= 1 的整数`,"models.v2ThreadsApply":`应用`,"models.capValue":`默认 {value}`,"models.contextSettings":`自定义窗口`,"models.contextSettingsTitle":`自定义窗口 — {provider}`,"models.contextDefault":`提供方默认值`,"models.contextModel":`模型`,"models.contextModelOverride":`模型覆盖值`,"models.contextHint":`已经知道窗口时,在这里手写 Codex 实际窗口。上游没报窗口就用这个值;上游报了更大窗口才压低。留空则使用提供方的「默认窗口 / 上限」;那个开关没开时才回退 128k。`,"models.contextAutomatic":`自动发现`,"models.contextSaved":`上下文窗口已更新 — 将在下一个 Codex 回合生效。`,"models.contextUnchanged":`没有需要保存的上下文窗口更改。`,"models.contextSaveFailed":`保存上下文窗口失败`,"models.contextInvalid":`上下文窗口必须为正整数`,"models.contextCappedValue":`{value} 限制`,"models.setAll":`全部设置`,"models.setAllHint":`给所有已路由提供方打开 {value} 默认窗口。中转站没报 context_window / context_length 时,这个值就是 Codex 实际窗口。要给单个模型手写,用同一行上的「自定义窗口」。原生提供方不受影响。`,"models.collapseAll":`全部折叠`,"models.expandAll":`全部展开`,"models.orderHint":`选择器顺序:Subagents 中的选择(按所选顺序)→ 其余已路由模型(依次按提供方、模型 ID 字母排序)→ 原生模型。可见性开关仅用于筛选,不会改变此顺序。`,"models.custom":`自定义…`,"models.customApply":`应用`,"models.customPlaceholder":`令牌 (例如 420000)`,"models.customAdd":`添加自定义模型`,"models.customAddTitle":`添加自定义模型 — {provider}`,"models.customEditTitle":`编辑自定义模型 — {provider}`,"models.customAdded":`已添加自定义模型`,"models.customUpdated":`已更新自定义模型`,"models.customDeleted":`已删除自定义模型`,"models.customSaveFailed":`保存自定义模型失败`,"models.customSaving":`正在保存…`,"models.customAddBtn":`添加`,"models.customEditBtn":`更新`,"models.customEdit":`编辑`,"models.customDelete":`删除`,"models.customDeleteConfirm":`要删除模型 {name} 吗?`,"models.customBadge":`自定义`,"models.customSummary":`{count} 个自定义模型`,"models.customFieldModelId":`模型 ID(端点标识)`,"models.customFieldModelIdPlaceholder":`例如 qwen4-max-preview`,"models.customFieldDisplayName":`显示名称(可选)`,"models.customFieldDisplayNamePlaceholder":`例如 Qwen 4 Max Preview`,"models.customFieldContext":`上下文窗口`,"models.customFieldModalities":`输入模态`,"models.customFieldReasoning":`推理强度`,"models.customFieldReasoningOverride":`覆盖推理强度`,"models.reasoningEffort.none":`无`,"models.reasoningEffort.minimal":`最低`,"models.reasoningEffort.low":`低`,"models.reasoningEffort.medium":`中`,"models.reasoningEffort.high":`高`,"models.reasoningEffort.xhigh":`极高`,"models.reasoningEffort.max":`最高`,"models.tipProvider":`提供方`,"models.tipContext":`上下文`,"models.tipModalities":`模态`,"models.tipStatus":`状态`,"models.tipActive":`已启用`,"models.tipDisabled":`已禁用`,"models.applied":`已应用 — 将在下一个 Codex 回合生效。`,"models.saveFailed":`保存失败`,"models.networkError":`网络错误 — 代理在运行吗?`,"models.loadFail":`加载模型失败 — 代理在运行吗?`,"models.noRouted":`没有已路由的模型`,"models.noRoutedHint":`请先登录提供方或添加一个。`,"models.emptyDiscovery":`未发现任何模型。请检查提供方端点,或添加静态/自定义模型。`,"models.emptyDiscoveryDisabled":`实时模型发现已关闭,且尚未配置静态模型。`,"models.discoveryFailedBadge":`发现失败`,"models.discoveryFailedHttp":`模型发现失败(HTTP {status})。`,"models.discoveryFailedBlocked":`模型发现被目标策略阻止。`,"models.discoveryFailedInvalidResponse":`模型发现返回了无效响应。`,"models.discoveryFailedNetwork":`由于网络错误,模型发现失败。`,"models.discoveryFailedProvider":`提供方报告了模型发现错误。`,"models.discoveryFailedGeneric":`模型发现失败。`,"models.openProviderSettings":`打开提供方设置`,"models.loading":`加载中…`,"models.search":`搜索模型…`,"models.showMore":`再显示 {n} 个`,"models.allowlistLabel":`仅所选`,"models.allowlistHint":`仅勾选的模型进入目录(留空 = 全部)。适用于暴露成千上万模型的提供商。`,"models.selectedCount":`已选 {n} 个`,"sub.subtitle":`Codex 的 {cmd} 仅将优先级最高的前 5 个模型作为覆盖项公开。在此最多选择 5 个 — 原生 gpt 或已路由模型 — opencodex 会设置它们的目录优先级,使其正好排在前面。其他模型仍可按确切名称调用;此设置仅控制显示项。`,"sub.featured":`精选`,"sub.advanced":`高级`,"sub.orderHintAria":`此顺序的用途`,"sub.orderHint":`此处所选并显示的顺序决定 Codex 模型选择器顶部第 1–5 位,以及 {cmd} 的默认模型候选。`,"sub.noneSelected":`未选择 — 请从下方列表选择。`,"sub.models":`模型`,"sub.search":`搜索模型(原生 gpt + 已路由)…`,"sub.noModels":`没有模型 — 请先登录提供方或添加一个。`,"sub.saved":`已保存 {n} 个模型。启动新的 Codex 会话(或运行 {cmd})以将它们作为 spawn_agent 覆盖项查看。`,"sub.saveFailed":`保存失败`,"sub.networkError":`网络错误 — 代理在运行吗?`,"sub.loadFail":`加载模型失败 — 代理在运行吗?`,"sub.loading":`加载中…`,"sub.moveUp":`上移 {m}`,"sub.moveDown":`下移 {m}`,"sub.removeAria":`移除 {m}`,"sub.workspace.addToFeatured":`将 {m} 添加到精选`,"sub.workspace.allModels":`所有模型`,"sub.workspace.featuredFull":`精选列表已满(最多 5 个)`,"sub.workspace.mainAria":`子代理模型详情`,"sub.workspace.notFeatured":`未设为精选`,"sub.workspace.priority":`优先级`,"sub.workspace.removeFromFeatured":`将 {m} 从精选中移除`,"sub.workspace.selectModel":`选择模型`,"sub.workspace.selectModelDesc":`从列表中选择一个模型以查看详情,并将其设为 spawn_agent 的精选模型。`,"sub.workspace.selector":`公开选择器`,"sub.ultraMode":`超级模式`,"sub.ultraModeHint":`为所有模型和推理力度启用主动多代理委派策略(不改变推理力度本身)。将 features.multi_agent_v2.multi_agent_mode_hint_text 写入 config.toml。`,"sub.ultraModeV2Required":`需要 v2 多代理表面 — 请先启用 multi_agent_v2,并在子代理模式控件中选择 v2。`,"sub.ultraModeText":`超级模式委派文本`,"sub.ultraModePreset":`恢复预设`,"sub.ultraModeLoadFail":`无法加载超级模式设置 — 代理是否在运行?`,"sub.ultraModeSaveFail":`保存超级模式设置失败`,"sub.ultraModeSaved":`超级模式已保存。适用于新的 Codex 会话。`,"logs.title":`请求日志`,"logs.tabLogs":`日志`,"logs.tabDebug":`调试`,"logs.subtitle":`经过本地 opencodex 代理的最近请求,最新在前。`,"logs.autoRefresh":`自动刷新`,"logs.noRequests":`暂无请求。`,"logs.loadError":`无法加载请求日志。`,"logs.filter.surface.label":`界面`,"logs.filter.surface.all":`全部`,"logs.filter.surface.claude":`Claude`,"logs.filter.surface.codex":`Codex`,"logs.filter.surface.grok":`Grok`,"logs.filter.interceptedHelpersOnly":`仅已拦截的辅助请求`,"logs.badge.interceptedHelper":`I · {model}`,"logs.badge.interceptedHelperTitle":`已拦截的辅助请求`,"logs.filter.conversation.label":`会话`,"logs.filter.conversation.placeholder":`粘贴会话 ID`,"logs.filter.conversation.clear":`清除`,"logs.filter.model.label":`模型`,"logs.filter.model.placeholder":`按模型或供应商筛选`,"logs.filter.conversation.apply":`筛选日志`,"logs.conversation.totals":`{requests} 次请求 · {tokens} tokens · {cost}`,"logs.conversation.scope":`合计仅覆盖当前已加载的 Logs 环形缓冲。`,"logs.conversation.excluded":`(~$ 已排除 {unpriced} 条无定价、{unmetered} 条无计量)`,"logs.cost.approximate":`{amount}`,"logs.cost.lowerBound":`≥{amount}`,"logs.cost.unavailable":`无法估算`,"logs.detail.conversation":`会话`,"logs.badge.claude":`Claude`,"logs.badge.grok":`Grok`,"logs.col.time":`时间`,"logs.col.request":`请求`,"logs.col.model":`模型`,"logs.col.effort":`推理强度`,"logs.col.provider":`提供方`,"logs.col.status":`状态`,"logs.col.tokens":`Token 数`,"logs.col.tokPerSec":`tok/s`,"logs.col.estimatedCost":`~$`,"logs.metric.tokPerSecTitle":`按完整请求耗时计算的每秒输出 token`,"logs.metric.estimatedCostTitle":`按 API 标价估算,并非实际扣费;价格无法匹配时不显示`,"usage.cost.total":`API 标价折算(当前范围)`,"usage.cost.disclaimer":`这不是账单或扣费凭证。实际可能计入订阅用量或消耗服务商额度。`,"usage.cost.unpricedNote":`已排除 {count} 个无法计费的请求`,"logs.detail.section.basic":`基本信息`,"logs.detail.route.section":`路由决策`,"logs.detail.route.kind":`路由类型`,"logs.detail.route.profile":`配置文件`,"logs.detail.route.selected":`已选择`,"logs.detail.route.candidates":`候选`,"logs.detail.route.unknown":`此请求未记录路由跟踪(跟踪之前的行)。`,"logs.detail.section.performance":`性能`,"logs.detail.section.cost":`API 标价折算`,"logs.detail.section.attempts":`Combo 尝试`,"logs.detail.section.usage":`原始 usage`,"logs.detail.ttft":`TTFT`,"logs.detail.costTotal":`标价折算`,"logs.detail.totalTokens":`Token 总数`,"logs.detail.matchedKey":`匹配的价格键`,"logs.detail.priceSource":`价格来源`,"logs.detail.unavailableReason":`不可用原因`,"logs.detail.copyRequestId":`复制请求 ID`,"logs.detail.copied":`已复制`,"logs.detail.source.jawcode":`jawcode 目录`,"logs.detail.source.expected":`Expected 价格覆盖`,"logs.detail.source.user":`用户配置的提供方价格覆盖`,"logs.detail.verification.verified":`已验证`,"logs.detail.verification.derived":`由基础模型推导`,"logs.detail.attempt.target":`提供方 / 模型`,"logs.detail.attempt.reason":`结果 / 原因`,"logs.detail.attempt.completed":`已完成`,"logs.detail.attempt.e2eNote":`顶层 tok/s 为端到端值;每次尝试使用各自耗时。`,"logs.detail.attempt.recovery.transient5xx":`临时 5xx 错误`,"logs.detail.attempt.recovery.connectionReset":`连接已重置`,"logs.detail.attempt.recovery.oauth401":`OAuth 重新认证`,"logs.detail.attempt.recovery.key429":`密钥被限流 (429)`,"logs.detail.attempt.recovery.rateLimit429":`被限流 (429)`,"logs.detail.attempt.recovery.anthropicOauth429":`Anthropic OAuth 被限流 (429)`,"logs.detail.attempt.recovery.image413":`图片载荷过大 (413)`,"logs.detail.attempt.recovery.emptyCompletion":`空完成重试`,"logs.detail.attempt.recovery.unknown":`未知的恢复原因`,"logs.detail.reason.usage_missing":`未上报 usage。`,"logs.detail.reason.usage_unsupported":`该提供方不支持上报 usage。`,"logs.detail.reason.output_missing":`未上报正数输出 token。`,"logs.detail.reason.invalid_duration":`请求耗时无效。`,"logs.detail.reason.price_unmatched":`未找到匹配的价格。`,"logs.detail.reason.invalid_cache_breakdown":`缓存 token 明细与输入 token 总数冲突。`,"logs.detail.reason.invalid_usage":`Usage 包含无效的 token 值。`,"logs.detail.reason.combo_attempt_unavailable":`至少一次 Combo 尝试无法计价。`,"logs.detail.estimate.usage_estimated":`提供方 usage 为估算值。`,"logs.detail.estimate.cache_detail_missing":`缺少缓存明细;输入费用按上限估算。`,"logs.detail.estimate.expected_price_overlay":`使用了已验证的 Expected 标价。`,"logs.detail.estimate.provider_cost_overlay":`使用了用户配置的提供方价格覆盖。`,"logs.detail.estimate.priority_lower_bound":`暂无已确认的 Priority 价格;当前显示的估算是已知下界。`,"logs.col.error":`错误`,"logs.col.upstreamReason":`上游原因`,"logs.col.duration":`耗时`,"logs.modelTooltip.model":`模型`,"logs.modelTooltip.resolvedModel":`解析后模型`,"logs.modelTooltip.requestedTier":`请求层级`,"logs.modelTooltip.configuredTier":`配置层级`,"logs.modelTooltip.responseTier":`响应层级`,"logs.modelTooltip.supportsTier":`支持层级`,"logs.tokens.reported":`已上报`,"logs.tokens.unreported":`未上报`,"logs.tokens.unsupported":`不支持`,"logs.tokens.estimated":`估算`,"logs.tokens.input":`输入`,"logs.tokens.output":`输出`,"logs.tokens.cacheRead":`缓存命中 (c)`,"logs.tokens.cacheWrite":`缓存写入 (w)`,"logs.tokens.reasoning":`推理`,"logs.tokens.noCache":`无缓存数据`,"logs.tokens.contextTotal":`活动上下文`,"logs.tokens.noCacheNote":`该提供商不报告缓存 token 数`,"logs.tokens.noCacheCursor":`Cursor 未报告缓存明细`,"logs.tokens.noCacheCursorNote":`Cursor 不提供缓存读写 token 数;这表示未知,并不代表已确认缓存未命中`,"logs.tokens.estimatedNote":`估算值(提供商不报告精确用量)`,"logs.details":`查看详情`,"logs.detailTitle":`请求详情`,"logs.detailRaw":`原始日志`,"debug.title":`调试`,"debug.subtitle":`可选的 provider transport 与 usage 提取诊断。请求错误和 502 在“日志”标签页显示。`,"debug.debug":`提供方调试`,"debug.usage":`用量提取`,"debug.injection":`注入日志`,"debug.claude":`Claude 入站`,"debug.claudeInbound.title":`Claude 入站请求`,"debug.claudeInbound.sub":`显示 Claude Code/Desktop 实际发送的内容(thinking、effort、metadata)— 不保存提示词原文。`,"debug.claudeInbound.empty":`尚未捕获任何请求。开启后从 Claude 发送一条消息试试。`,"debug.claudeInbound.time":`时间`,"debug.claudeInbound.endpoint":`端点`,"debug.claudeInbound.model":`模型`,"debug.claudeInbound.none":`无`,"debug.reset":`清除运行时覆盖`,"debug.refresh":`刷新`,"debug.follow":`跟随滚动`,"debug.streamProvider":`提供方`,"debug.streamUsage":`用量`,"debug.streamInjection":`注入`,"debug.loading":`正在加载调试设置…`,"debug.loadFailed":`无法加载调试设置。`,"debug.emptyTitle":`调试日志已关闭`,"debug.empty":`请在上方卡片中开启 Provider debug 或 Usage extraction。通过代理发送请求后,诊断行会显示在这里。`,"debug.noLinesTitle":`等待诊断行`,"debug.noLines.provider":`提供商调试已开启,但仅记录传输异常(丢弃或格式错误的帧,以及 Cursor dial/retry 事件)。通过 Anthropic 等提供商的正常请求可能不会产生任何行。`,"debug.noLines.usage":`用量提取已开启但尚未捕获任何内容。请通过 Codex 发送请求,随后会显示在此处。`,"debug.noLines.injection":`注入日志已开启但尚未捕获任何内容。它记录协作和子代理回合中的多代理指导注入与 effort-cap 决策。`,"usage.title":`用量`,"usage.subtitle":`代理本地的 Token 用量统计。缺失的用量不会显示为零。`,"usage.loading":`正在加载用量数据…`,"usage.empty":`尚无用量记录。通过代理发送请求后将在此显示。`,"usage.loadError":`无法加载用量数据。`,"usage.range.all":`全部`,"usage.range.available":`可用历史`,"usage.historyTruncated":`由于未加载较早的使用记录,合计仅涵盖可用历史。`,"usage.historyTruncatedWindow":`已加载记录的请求开始时间介于 {start} 到 {end} 之间。受读取上限限制,文件较前的条目已被省略,所选时间范围可能不完整。`,"usage.range.30d":`30 天`,"usage.range.7d":`7 天`,"usage.card.requests":`请求数`,"usage.card.measured":`已计量`,"usage.card.reported":`已上报`,"usage.card.totalTokens":`Token 总数`,"usage.card.cachedTokens":`缓存命中 Token`,"usage.card.cachedTokensHint":`从提供商缓存读取的提示 Token(命中)。缓存写入在下方单独显示。`,"usage.card.cacheWriteTokens":`缓存写入`,"usage.card.coverage":`覆盖率`,"usage.card.activeDays":`活跃天数`,"usage.section.heatmap":`每日活动`,"usage.section.overview":`概览`,"usage.section.models":`模型`,"usage.section.providers":`提供方`,"usage.section.coverage":`覆盖率明细`,"usage.workspace.report":`用量报告`,"usage.workspace.sections":`用量分区`,"usage.coverage.measured":`已计量`,"usage.coverage.reported":`提供方上报`,"usage.coverage.estimated":`估算`,"usage.coverage.note":`已计量包含提供方上报和估算的 Token 数。未上报 / 不支持请求仅做计数,不会被算作 0 Token。`,"usage.search.models":`搜索模型…`,"usage.col.requests":`请求数`,"usage.col.measured":`已计量`,"usage.col.reported":`已上报`,"usage.col.tokens":`Token 数`,"usage.col.share":`占比`,"usage.heatmap.less":`少`,"usage.heatmap.more":`多`,"modal.addNamed":`添加:{label}`,"modal.add":`添加提供方`,"modal.search":`搜索提供方…`,"modal.logInWith":`使用 {label} 登录`,"modal.waitingBrowser":`等待浏览器…`,"modal.providerName":`提供方名称`,"modal.adapter":`适配器`,"modal.baseUrl":`Base URL`,"modal.endpoint":`端点`,"modal.endpoint.tokenPlan":`Token 套餐`,"modal.endpoint.payAsYouGo":`按量付费`,"modal.endpoint.custom":`自定义`,"modal.defaultModel":`默认模型(可选)`,"modal.allowPrivateNetwork":`允许本地/私有网络`,"modal.allowPrivateNetworkHint":`仅为有意自托管的提供商启用。元数据端点仍被阻止。`,"modal.nameRequired":`提供方名称为必填项`,"modal.baseUrlRequired":`Base URL 为必填项`,"modal.networkError":`网络错误 — 代理在运行吗?`,"modal.loginFailStart":`登录启动失败`,"modal.waitingLogin":`等待浏览器登录…`,"modal.loggingIn":`登录中…`,"modal.loginTimeout":`登录超时 — 请重试。`,"nav.api":`API`,"nav.integrations":`集成`,"nav.codexAuth":`Codex 认证`,"nav.codexSet":`Codex 设置`,"codexSet.tab.multiauth":`多账号认证`,"codexSet.tab.prompt":`提示词`,"codexSet.prompt.title":`提示词层`,"codexSet.prompt.timing":`对新启动的会话生效。正在运行的会话保持当前的提示词设置。`,"codexSet.prompt.staleRevision":`配置已在别处更改,列表已重新加载。`,"codexSet.prompt.writeFailed":`无法保存更改。`,"codexSet.prompt.loadFailed":`无法加载提示词层。`,"codexSet.prompt.repair":`修复`,"codexSet.prompt.repairFailed":`无法完成修复。`,"codexSet.drift.journalPresent":`上一次写入未完成。下次写入时会自动恢复。`,"codexSet.drift.projectionStale":`已保存的层与 config.toml 中的值不一致。修复会按你的层重新写入该值。`,"codexSet.drift.storeMissing":`层文件已丢失,但 config.toml 中仍有指令。修复会先创建备份,并将该文本保留为一个层。`,"codexSet.drift.ownedMalformed":`config.toml 中生成的那一行被手动改动过,因此重写不再安全。`,"codexSet.custom.adoptUnsupported":`{path} 第 {line} 行的值不是单行字符串,无法导入。若要在此管理,请手动移动它。`,"codexSet.prompt.unreadable":`Codex 配置文件存在但无法读取,因此拒绝了更改。`,"codexSet.layer.permissions":`权限`,"codexSet.layer.collaboration":`协作模式`,"codexSet.layer.environment":`环境上下文`,"codexSet.layer.apps":`应用`,"codexSet.layer.skills":`技能`,"codexSet.prompt.extensionsUnknown":`扩展可添加自己的层。Codex 不会公开这些层,因此无法在此列出。`,"codexSet.group.transition":`变更通知`,"codexSet.group.transitionDesc":`它们通报变化而非描述状态,因此仅在会话切换到实时模式或更换模型时出现。`,"codexSet.custom.slotNote":`自定义层会按此顺序合并为一个部分。`,"codexSet.row.alwaysOn":`始终启用`,"codexSet.row.onChange":`变更时发送`,"codexSet.row.featureGated":`在 [features] 下配置`,"codexSet.row.openFeatures":`打开设置`,"codexSet.dialog.setValue":`{value}(默认 {fallback})`,"codexSet.dialog.copyKey":`复制配置键`,"codexSet.dialog.unknownLayer":`此版本没有该层的说明。它来自比仪表板更新的 Codex 运行时。`,"codexSet.custom.heading":`自定义层`,"codexSet.custom.add":`+ 添加层`,"codexSet.custom.newTitle":`新建层`,"codexSet.custom.editTitle":`编辑层`,"codexSet.custom.titleLabel":`标题`,"codexSet.custom.bodyLabel":`指令`,"codexSet.custom.bodySize":`{bytes}/{max} 字节`,"codexSet.custom.normalized":`制表符已转换为四个空格,换行符已转换为 LF。`,"codexSet.custom.titleRequired":`请输入标题。`,"codexSet.custom.titleTooLong":`标题有 {count} 个字符,上限为 {max} 个。`,"codexSet.custom.titleMultiline":`标题必须为单行。`,"codexSet.custom.bodyTooLarge":`此层为 {bytes} 字节,上限为 {max} 字节。`,"codexSet.custom.composedTooLarge":`启用的层合计将达到 {bytes} 字节,超过上限。`,"codexSet.custom.invalidCharacter":`无法保存位置 {position} 的控制字符。`,"codexSet.custom.discardPrompt":`要放弃更改吗?`,"codexSet.custom.keepEditing":`继续编辑`,"codexSet.custom.delete":`删除 {title}`,"codexSet.custom.deleteConfirm":`要删除此层吗?此操作无法撤销。`,"codexSet.custom.layerGone":`该层已在别处被删除,因此编辑器已关闭。`,"codexSet.custom.deleteConfirmNamed":`要删除“{title}”吗?此操作无法撤销。`,"codexSet.custom.moveUp":`上移 {title}`,"codexSet.custom.prevLayer":`上一层`,"codexSet.custom.nextLayer":`下一层`,"codexSet.custom.navPosition":`{position} / {total}`,"codexSet.custom.moveDown":`下移 {title}`,"codexSet.custom.limitReached":`最多可保留 {max} 个自定义层。`,"codexSet.custom.notOwned":`developer_instructions 是在 opencodex 外部写入的,因此无法在此编辑。请将其导入,以便作为层进行管理。`,"codexSet.custom.adopt":`导入现有指令`,"codexSet.custom.adoptConfirm":`导入为层`,"codexSet.custom.adoptRefused":`无法导入现有值。`,"codexSet.custom.baseReplaced":`model_instructions_file 已设置为 {path},因此 opencodex 外部的内容已替换基础提示词。`,"codexSet.lint.identity":`此内容声明了与 Codex 所设定身份不同的身份。`,"codexSet.lint.foreignTool":`工具由注册表提供;在此指定名称并不会创建工具。`,"codexSet.lint.placeholder":`指令不会经过模板引擎处理,因此此内容会按原样发送。`,"codexSet.lint.applyPatch":`apply_patch 由工具注册表定义,而不是由指令定义。`,"codexSet.lint.approvalVocab":`Codex 会注入自己的审批术语;此内容可能与其冲突。`,"codexSet.lint.environment":`环境信息稍后生成,可能与此内容冲突。`,"codexSet.lint.size":`此层超过 8 KB。仍可保存,但每次请求都会消耗令牌。`,"codexSet.preset.blank":`空白层`,"codexSet.preset.concise.name":`简洁输出`,"codexSet.preset.concise.description":`简短作答,不加开场白,尽量减少格式。`,"codexSet.preset.concise.provenance":`改编自 Claude Code 的简洁性指令。文案由我们原创,并非复制。`,"codexSet.preset.planFirst.name":`编辑前先规划`,"codexSet.preset.planFirst.description":`先说明计划,再进行更改。`,"codexSet.preset.planFirst.provenance":`改编自 Claude Code 的规划思路。文案由我们原创,并非复制。`,"codexSet.preset.explainWhy.name":`解释理由`,"codexSet.preset.explainWhy.description":`不仅说明做什么,也说明为什么。`,"codexSet.preset.explainWhy.provenance":`改编自 Grok Build 的确认风格。文案由我们原创,并非复制。`,"codexSet.preset.testFirst.name":`测试优先`,"codexSet.preset.testFirst.description":`修复前先编写会失败的测试。`,"codexSet.preset.testFirst.provenance":`改编自常见的代理实践。文案由我们原创,并非复制。`,"codexSet.preset.korean.name":`韩语回复`,"codexSet.preset.korean.description":`无论请求使用哪种语言,都用韩语回答。`,"codexSet.preset.korean.provenance":`根据常见的用户需求为 opencodex 编写。文案由我们原创,并非复制。`,"codexSet.dialog.class":`类型`,"codexSet.dialog.key":`配置键`,"codexSet.dialog.fileValue":`此文件中的值`,"codexSet.dialog.absentDefault":`未设置(默认为 {value})`,"codexSet.dialog.noRenderedText":`Codex 不会公开内置层组装后的文本,因此此对话框仅说明该层并列出其配置键,不显示具体内容。`,"codexSet.dialog.sourceText":`发送给模型的原文`,"codexSet.dialog.sourceBytes":`{bytes} 字节`,"codexSet.dialog.notRendered":`在我们读取的那一轮中,此层没有发送任何内容。各部分仅在内容变化时才会重新发送,因此单次采样可能看不到它。`,"codexSet.dialog.emptySource":`{path} 文件存在但为空,因此此层不会发送任何内容。`,"codexSet.dialog.notExposed":`基础提示词不在 Codex 可打印的消息列表中传递,因此无法在此显示。可以通过 model_instructions_file 替换它。`,"codexSet.dialog.textUnavailable":`本机无法读取 Codex 提示词,因此无法显示原文。`,"codexSet.class.base":`基础指令`,"codexSet.class.config-toggle":`可在此切换`,"codexSet.class.feature-gated":`功能开关控制`,"codexSet.class.runtime-conditional":`运行时条件控制`,"codexSet.class.extension-unknown":`扩展层`,"codexSet.layer.base-instructions":`基础指令`,"codexSet.layer.model-switch":`模型切换通知`,"codexSet.layer.personality":`个性`,"codexSet.layer.context-window-guidance":`上下文窗口指引`,"codexSet.layer.realtime":`实时会话`,"codexSet.layer.agents-md":`AGENTS.md`,"codexSet.layer.environments-instructions":`执行环境`,"codexSet.layer.plugins":`插件`,"codexSet.layer.tools":`工具`,"codexSet.layer.multi-agent-mode":`多代理模式`,"codexSet.layer.git-attribution":`提交署名`,"codexSet.about.base-instructions":`Codex 自身的指令。它们随请求一同发送,无法关闭。`,"codexSet.about.model-switch":`会话中途切换模型时添加。`,"codexSet.about.personality":`语气和表达风格指引,由功能开关控制。`,"codexSet.about.context-window-guidance":`剩余上下文预算的相关建议,由功能开关控制。`,"codexSet.about.realtime":`实时会话中添加。`,"codexSet.about.agents-md":`项目中的 AGENTS.md 文件。此页面只显示该层,绝不会编辑项目文档。`,"codexSet.about.permissions":`说明当前生效的沙箱和审批设置。`,"codexSet.about.collaboration":`说明当前启用的协作模式。`,"codexSet.about.environment":`工作目录、平台及其他环境信息。`,"codexSet.about.environments-instructions":`延迟执行环境的相关指引,由功能开关控制。`,"codexSet.about.apps":`已连接应用的使用方式。`,"codexSet.about.plugins":`选中插件或任一插件声明功能时添加。`,"codexSet.about.tools":`延迟加载的工具说明,由功能开关控制。`,"codexSet.about.skills":`可用技能列表。`,"codexSet.about.multi-agent-mode":`子代理指令,由功能开关控制。`,"codexSet.about.git-attribution":`让模型在它写的提交里加上 Co-authored-by: Codex 尾注,并在它开的拉取请求里加上 Generated with Codex. 这一行。Codex 从你的账号读取此项,所以这里和 [features] 都改不了。账号关闭时,Codex 会发送相反的指令,而不是什么都不发。`,"codexSet.condition.model-switch":`仅在会话中途切换模型后注入。`,"codexSet.condition.realtime":`仅在实时会话中注入。`,"codexSet.condition.agents-md":`找到适用于当前工作目录的项目文档时注入。`,"codexSet.condition.plugins":`选中插件或任一插件声明功能时注入。`,"codexSet.condition.git-attribution":`由你账号的署名策略决定。`,"codexSet.base.title":`基础提示词`,"codexSet.base.prev":`上一个选项`,"codexSet.base.next":`下一个选项`,"codexSet.base.position":`{position} / {total}`,"codexSet.base.swipeHint":`左右滑动、按方向键,或点箭头按钮切换选项。对新开始的会话生效。`,"codexSet.base.defaultTitle":`Codex 自带的基础提示词`,"codexSet.base.defaultBody":`默认项并不存在这里,所以没有可编辑或删除的内容:选它只是从配置里移除 model_instructions_file,让 Codex 用自带的提示词。`,"codexSet.base.variantTitle":`名称`,"codexSet.base.variantBody":`提示词`,"codexSet.base.replacesWarning":`这会整体替换 Codex 自带的基础提示词,而不是在其后追加。这里写得短,模型收到的指令就只有这么短。`,"codexSet.base.use":`用这一个`,"codexSet.base.inUse":`正在使用`,"codexSet.base.externalBlocked":`model_instructions_file 已指向 {path},且不是 opencodex 写的。请先自行清除,再在此处选择。`,"nav.openMenu":`打开菜单`,"nav.closeMenu":`关闭菜单`,"integrations.subtitle":`将客户端连接到 opencodex,管理凭据并恢复客户端配置。`,"integrations.tabsLabel":`集成页面`,"integrations.tab.overview":`概览`,"integrations.tab.keys":`API 密钥`,"integrations.tab.codex":`Codex`,"integrations.tab.claude":`Claude`,"integrations.tab.grok":`Grok Build`,"integrations.tab.opencode":`OpenCode`,"integrations.tab.pi":`Pi`,"integrations.tab.omp":`OMP`,"integrations.tab.hermes":`Hermes`,"integrations.tab.openclaw":`OpenClaw`,"integrations.tab.kimi":`Kimi Code`,"integrations.tab.gajae":`Gajae Code`,"integrations.tab.dsh":`DSH`,"integrations.tab.mcode":`MiniMax Code`,"integrations.tab.zcode":`ZCode`,"integrations.tab.prime":`Prime Agent`,"integrations.tab.aside":`Aside`,"integrations.codex.title":`Codex CLI`,"integrations.codex.body":`Codex 连接由代理服务管理。启动 opencodex 时应用该连接;停止服务时恢复原生路由。`,"integrations.codex.openService":`打开服务控制`,"integrations.state.notInstalled":`未安装`,"integrations.state.unknown":`检查中`,"integrations.detail.codexRouted":`Codex 请求经由此代理`,"integrations.detail.codexAbsent":`Codex 尚未经由此代理`,"integrations.detail.keyCount":`已签发 {count} 个密钥`,"integrations.detail.keyNone":`尚未签发密钥`,"integrations.detail.keyChecking":`检查中…`,"integrations.detail.keyUnavailable":`无法获取密钥状态`,"integrations.detail.claudeOff":`连接已关闭`,"integrations.detail.desktopCurrent":`Desktop 正在使用此配置`,"integrations.detail.desktopStale":`应用后配置文件已更改`,"integrations.detail.desktopNotServed":`配置存在,但 Desktop 使用的是另一个`,"integrations.detail.desktopAbsent":`未应用任何配置`,"integrations.detail.desktopDesiredOff":`Claude Desktop 集成已关闭`,"integrations.detail.desktopDesiredOffCleanupPending":`Claude Desktop 仍在使用网关,清理尚未完成`,"integrations.detail.desktopDesiredOnNotApplied":`集成已开启,但 Desktop 未使用网关配置`,"integrations.detail.desktopSelectedElsewhere":`Desktop 正在使用其他配置`,"integrations.detail.desktopProfileDrift":`选中的 Desktop 配置已更改`,"integrations.detail.desktopObservedUnsafe":`无法安全更改选中的 Desktop 配置`,"integrations.detail.desktopNotInstalled":`未安装 Claude Desktop 配置库`,"integrations.dialog.desktop.title":`要关闭 Claude Desktop 集成吗?`,"integrations.dialog.desktop.changes":`如果 {path} 包含由 opencodex 管理的网关配置,Desktop 会先选择新的无凭据标准配置,再移除旧配置及其备份。`,"integrations.dialog.desktop.breakage":`Claude Desktop 将不再使用经由 opencodex 路由的模型,而会恢复为标准 Claude。`,"integrations.dialog.desktop.undo":`重新开启后,会根据已保存的模型分配重新生成 opencodex 配置。`,"integrations.dialog.desktop.restart":`Claude Desktop 仅在启动时读取此配置。请完全退出并重新打开 Desktop 以使更改生效。`,"integrations.dialog.desktop.confirm":`停用`,"integrations.native.error.desktopUnsafeMetadata":`无法安全读取 {path} 中的 Claude Desktop 元数据,因此未更改其配置库。`,"integrations.native.error.desktopCleanupIncomplete":`Claude Desktop 已指向标准模式,但仍残留旧的 opencodex 凭据文件:{paths}。`,"integrations.native.msg.desktopDisabled":`Claude Desktop 集成已关闭。`,"integrations.native.msg.desktopEnabled":`Claude Desktop 集成已开启。`,"integrations.detail.grokModels":`已接入 {count} 个模型`,"integrations.detail.grokAbsent":`配置中没有 opencodex 区块`,"integrations.dialog.grok.title":`要停用 Grok Build 集成吗?`,"integrations.dialog.grok.changes":`只会从 {path} 中删除由 opencodex 标记的区块。区块之外手动写入的内容将保持不变。`,"integrations.dialog.grok.breakage":`停用后,Grok Build 中的 opencodex 模型别名将消失。通过 xAI 账号使用的模型不受影响。`,"integrations.dialog.grok.undo":`如果 opencodex 正在 loopback 地址上运行,再次启用时会根据当前可用的模型列表重新写入区块。`,"integrations.dialog.grok.confirm":`停用`,"integrations.native.msg.nonLoopbackRemoved":`只有当 opencodex 在 loopback 地址上运行时,才能自动注册 Grok Build。已删除之前指向 loopback 地址的区块。`,"integrations.native.msg.nonLoopbackRemovedNoop":`只有当 opencodex 在 loopback 地址上运行时,才能自动注册 Grok Build。没有需要删除的旧区块。`,"integrations.native.msg.nonLoopbackSuperseded":`只有当 opencodex 在 loopback 地址上运行时,才能自动注册 Grok Build。在此期间,其他进程向配置写入了新区块,因此文件中的当前区块并非由本次请求创建。`,"integrations.native.error.orphanedMarker":`{path} 中有 opencodex 开始标记,但没有结束标记。由于无法确定该区块的结束位置,因此未修改文件。`,"integrations.native.error.homeMismatch":`已安装服务的主目录与当前主目录不一致,因此未修改文件。`,"integrations.native.error.notInstalled":`尚未安装 Grok Build,因此没有可更改的内容。`,"integrations.native.error.configBusy":`其他进程正在保存配置,无法进行更改。请稍后重试。`,"integrations.state.absent":`未应用`,"integrations.state.current":`已应用`,"integrations.state.stale":`需要更新`,"integrations.state.conflict":`冲突`,"integrations.state.unsafe":`无法验证`,"integrations.summary.detected":`已检测客户端`,"integrations.summary.applied":`已配置客户端`,"integrations.summary.stale":`需要更新`,"integrations.summary.lastChange":`上次更改`,"integrations.summary.disableAll":`全部禁用…`,"integrations.onboarding":`应用时会先保存备份,再写入一个 opencodex 提供方配置块。禁用时只移除该配置块,并可从保留的快照恢复。`,"integrations.empty.title":`未检测到已安装的客户端`,"integrations.empty.body":`安装受支持的客户端,然后返回此处应用 opencodex。`,"integrations.action.apply":`应用`,"integrations.action.disable":`禁用`,"integrations.action.refresh":`更新`,"integrations.action.settings":`设置`,"integrations.action.manageKeys":`管理密钥`,"integrations.action.restore":`恢复…`,"integrations.action.undo":`撤销`,"integrations.action.restorePoint":`恢复到此时间点…`,"integrations.action.snapshotExpired":`备份已过期`,"integrations.rollback.title":`恢复中心`,"integrations.rollback.empty":`暂无应用记录`,"integrations.rollback.emptyBody":`每次成功写入前都会先保留一份写入前快照。`,"integrations.catalog.title":`客户端`,"integrations.rollback.older":`较早的操作`,"integrations.rollback.showMore":`再显示 {n} 个`,"integrations.rollback.failed":`无法加载回滚记录。`,"integrations.restore.title":`恢复此快照?`,"integrations.restore.body":`系统会先备份当前文件,再用所选快照替换它。`,"integrations.restore.driftTitle":`检测到较新的编辑`,"integrations.restore.driftBody":`此快照之后的更改将先备份,然后再替换文件。`,"integrations.restore.confirm":`恢复`,"integrations.restore.confirmDrift":`备份较新的编辑并恢复`,"integrations.restore.pending":`正在恢复…`,"integrations.restore.manual":`自动恢复失败:{reason}。请从 {path} 手动恢复。`,"integrations.error.load":`无法加载集成状态。`,"integrations.error.stale":`最近一次刷新失败。以下值可能已过期。`,"integrations.error.busy":`此客户端的另一项更改仍在进行中。请稍后重试。`,"integrations.error.conflict":`opencodex 写入后配置又发生了更改。未移除任何内容。`,"integrations.error.unsafe":`无法安全地更改配置。`,"integrations.error.generic":`集成更改失败。已保留之前的状态。`,"integrations.error.nonLoopback":`{client} 只能连接 localhost 上的代理:其配置没有位置放置远程绑定所需的准入标头,手动编写同样无效。请改用隧道或本地转发器提供 loopback 访问。`,"integrations.status.installed":`已安装`,"integrations.status.notInstalled":`未安装`,"integrations.status.appliedAt":`已应用`,"integrations.status.backup":`备份`,"integrations.status.lastRestore":`上次恢复`,"integrations.status.unknown":`未知`,"integrations.bulk.title":`禁用已应用的客户端集成?`,"integrations.bulk.body":`只会移除归 opencodex 所有的配置块。每个客户端都会先保留一份写入前快照。`,"integrations.bulk.partial":`部分客户端无法禁用:{clients}`,"integrations.bulk.success":`已禁用已应用的客户端集成。`,"integrations.retention.degraded":`备份清理进度滞后;磁盘上可能仍有较旧的备份。`,"integrations.error.residual":`文件可能处于中间状态:{message} 请从 {path} 恢复。`,"integrations.error.recover":`{message} 备份位于 {path}。`,"integrations.kind.apply":`已应用`,"integrations.kind.disable":`已停用`,"integrations.kind.refresh":`已更新`,"integrations.kind.restore":`已恢复`,"integrations.kind.overwrite":`已覆盖`,"integrations.dialog.overwrite.title":`替换该配置文件中的代码块?`,"integrations.dialog.overwrite.changesUnowned":`{path} 中 opencodex 需要写入的位置被一个并非我们写入的代码块占用。应用会将其替换为 opencodex 写入的代码块。`,"integrations.dialog.overwrite.changesForeign":`{path} 中 opencodex 代码块内你所做的修改会被丢弃,并替换为 opencodex 写入的代码块。`,"integrations.dialog.overwrite.breakage":`该代码块原本配置的内容将不再生效。文件其他位置保持不变。`,"integrations.dialog.overwrite.undo":`会先保存快照,因此这次操作会出现在下方的回滚列表中,可以撤销。`,"integrations.dialog.overwrite.confirm":`替换`,"integrations.action.overwrite":`替换`,"integrations.semantics.opencode":`仅适用于直接从磁盘启动;ocx opencode 的环境注入优先。`,"integrations.semantics.pi":`对新会话生效。`,"integrations.semantics.omp":`重启 OMP 以加载模型目录。`,"integrations.semantics.hermes":`对新会话生效。`,"integrations.semantics.openclaw":`立即应用到正在运行的网关。`,"integrations.semantics.kimi":`重启或运行 /reload 以应用(v2 会监视该文件)。`,"integrations.semantics.gajae":`在新会话中或打开 /model 时生效。`,"integrations.semantics.dsh":`OpenCodex 只管理 $DSH_HOME/settings.yaml 中的 llm-pi-ai.providers.opencodex。DSH 会热重载该 provider;你的默认模型和 deepseek-official 保持不变。目前仅支持环回地址,且不会写入真实凭据。`,"integrations.semantics.mcode":`仅管理 custom_provider.opencodex,不会更改默认模型或 MiniMax 登录状态。`,"integrations.semantics.zcode":`仅管理 ~/.zcode/v2/config.json 中的 provider.opencodex,不会更改 Z.ai 登录状态或其他提供商。更改后请重启 ZCode。`,"integrations.semantics.prime":`仅管理 Prime Agent 的 models.json 中的 providers.opencodex;默认位于 ~/.prime/agent,若设置 PRIME_AGENT_CODING_AGENT_DIR 则以其为准。不会更改其他提供商或模型覆盖设置。对新会话生效。`,"integrations.semantics.aside":`仅管理已登录账号的 Aside models.json 中的 providers.opencodex,位于 ~/.aside/u/<账号>。不会更改其他提供商。Aside 在运行时会重写该文件,因此应用后请完全退出并重新打开 Aside。`,"codexAuth.mainAccount":`主账号`,"codexAuth.logLabel":`日志标签`,"codexAuth.codexApp":`Codex App`,"codexAuth.moreActions":`显示更多操作`,"codexAuth.copyId":`复制账户 ID`,"codexAuth.appLogin":`应用登录`,"codexAuth.accountPool":`账号池`,"codexAuth.accountModeTitle":`OpenAI 账户模式`,"codexAuth.accountModePool":`账户池模式`,"codexAuth.accountModePoolDesc":`主登录与符合条件的已添加账户会在此轮换。`,"codexAuth.accountModeDirect":`直连模式`,"codexAuth.accountModeDirectDesc":`请求仅使用主登录;已添加账户会继续存储,供账户池模式使用。`,"codexAuth.openaiMissing":`未配置内置 OpenAI 提供方。`,"codexAuth.openaiDisabled":`内置 OpenAI 提供方已禁用。`,"codexAuth.openaiUnavailableDesc":`你的 OpenAI 账号仍然可用。启用提供方后即可路由 Codex 请求。`,"codexAuth.enableOpenai":`启用 OpenAI`,"codexAuth.enablingOpenai":`正在启用...`,"codexAuth.enableOpenaiFailed":`无法启用 OpenAI 提供方。`,"codexAuth.openaiPresetLoadFailed":`无法加载 OpenAI 提供方预设。`,"codexAuth.openaiPresetUnavailable":`OpenAI 提供方预设不可用。`,"codexAuth.openProviders":`打开提供商`,"codexAuth.add":`添加`,"codexAuth.sparkQuota":`Codex Spark 配额`,"codexAuth.sparkQuotaHint":`在账户卡片上显示 GPT-5.3-Codex-Spark 周窗口。默认隐藏,因为它只适用于一个模型。`,"codexAuth.sparkQuotaShown":`已显示 Codex Spark 配额`,"codexAuth.sparkQuotaHidden":`已隐藏 Codex Spark 配额`,"codexAuth.sparkQuotaFailed":`无法更改 Codex Spark 配额设置`,"codexAuth.refreshQuota":`刷新额度`,"codexAuth.refreshingQuota":`刷新中...`,"codexAuth.quotaRefreshed":`额度已刷新`,"codexAuth.quotaRefreshFailed":`额度刷新失败`,"codexAuth.pauseExhausted":`暂停已达上限账号`,"codexAuth.pausingExhausted":`正在检查额度...`,"codexAuth.pauseExhaustedSucceeded":`已暂停 {count} 个达到上限的账号`,"codexAuth.pauseExhaustedNone":`没有确认达到 100% 用量的账号。`,"codexAuth.pauseExhaustedFailed":`无法检查并暂停已达上限账号。`,"codexAuth.noPool":`尚未添加池账号。`,"codexAuth.pause":`暂停`,"codexAuth.resume":`恢复`,"codexAuth.paused":`已暂停`,"codexAuth.pauseSucceeded":`已暂停 {email}`,"codexAuth.resumeSucceeded":`{email} 已重新加入账号池`,"codexAuth.pauseFailed":`无法暂停 {email},未做任何更改。`,"codexAuth.resumeFailed":`无法恢复 {email},未做任何更改。`,"codexAuth.pausedHint":`恢复前不会参与自动切换、重试、冷却恢复或手动选择。`,"codexAuth.pinned":`已固定`,"codexAuth.pinnedHint":`这是你手动选择的账号,因此更高的选择顺序不会越过它。该固定会一直生效,直到此账号用尽、你改选其他账号,或你修改任一选择顺序。`,"codexAuth.fiveHour":`5 小时`,"codexAuth.weekly":`每周`,"codexAuth.monthly":`30天`,"codexAuth.resets":`重置`,"codexAuth.today":`今天`,"codexAuth.current":`当前`,"codexAuth.nextSession":`已选择`,"codexAuth.poolPrepared":`已为账户池准备`,"codexAuth.preparePoolTitle":`为账户池模式准备此账号?`,"codexAuth.preparePoolDesc":`直连请求仍使用主登录。启用账户池模式后,此账号会成为预先选择的池账号。`,"codexAuth.prepareForPool":`为账户池准备`,"codexAuth.poolPreparedToast":`已为账户池模式准备 {email}`,"codexAuth.switchTitle":`切换活跃账号?`,"codexAuth.switchDesc":`立即生效。已在进行中的请求保留原账号,其余都会切换到此账号;不过选择顺序相同的账号仍会轮换使用。`,"codexAuth.cacheWarning":`切换账号会重置提示缓存。新会话从空缓存开始。`,"codexAuth.setAsNext":`接下来使用此账号`,"codexAuth.cancel":`取消`,"codexAuth.switchBack":`切换回主账号?`,"codexAuth.switchBackDesc":`立即生效。已在进行中的请求保留原账号,其余都会切换到应用登录账号;不过选择顺序相同的账号仍会轮换使用。`,"codexAuth.autoSwitch":`基于用量的主动切换`,"codexAuth.autoSwitchQuotaDesc":`配额:使用率达到或超过 {threshold}% 时,包括已绑定任务在内的下一次请求可能转到用量更低的合格账号;Go/Free 仅使用 30 天窗口。`,"codexAuth.autoSwitchQuotaOffDesc":`基于用量的主动切换已关闭。新建/未绑定任务分配和故障恢复仍然生效。`,"codexAuth.autoSwitchRoundRobinDesc":`轮询分配不使用此阈值,并会继续轮换新建/未绑定任务。`,"codexAuth.autoSwitchFillFirstDesc":`填满优先:{threshold}% 是新建/未绑定任务的耗尽点;健康的已绑定任务继续使用原账号。`,"codexAuth.autoSwitchFillFirstOffDesc":`填满优先没有新建/未绑定任务的用量耗尽点;冷却、重新认证和故障恢复仍可能改变路由。`,"codexAuth.failureRecoveryNote":`故障恢复是独立机制:输出前的 429/402 拒绝、冷却、重新认证、排除或已配置的瞬时故障转移可能选择另一个合格账号。`,"codexAuth.autoSwitchThreshold":`用量阈值`,"codexAuth.autoSwitchThresholdAria":`用量阈值(百分比)`,"codexAuth.autoSwitchThresholdInc":`提高用量阈值`,"codexAuth.autoSwitchThresholdDec":`降低用量阈值`,"codexAuth.autoSwitchLoadFailed":`无法加载基于用量的切换设置。`,"codexAuth.autoSwitchThresholdInvalid":`请输入 1 到 100 之间的整数`,"codexAuth.autoSwitchUpdated":`基于用量的主动切换设置已更新`,"codexAuth.autoSwitchUpdateFailed":`无法确认基于用量的切换更新。当前显示最后一次确认的值。`,"codexAuth.requestUserInput":`在 Default 模式下请求输入`,"codexAuth.requestUserInputDesc":`允许 Codex 在 Default 模式会话中暂停,并通过 request_user_input 工具向你提问。`,"codexAuth.requestUserInputUpdated":`功能标志已更新 - 适用于新会话。`,"codexAuth.requestUserInputUpdatedRestart":`功能标志已更新 - 适用于新会话。请重启 Codex 应用。`,"codexAuth.requestUserInputUpdateFailed":`无法更新功能标志。未做任何更改。`,"codexAuth.requestUserInputLoadFailed":`无法从 config.toml 读取功能标志。`,"codexAuth.accountPickerTitle":`在模型选择器中指定 Codex 账号`,"codexAuth.accountPickerOffDesc":`启用后,普通 GPT 选择器条目会替换为每个账号选择器对应的条目,让你无需退出登录即可为对话明确选择账号。关闭此功能不会删除任何账号。`,"codexAuth.accountPickerOnDesc":`每个选择器都是一个已存储账号的公开标签。选择后,该对话会锁定到对应账号,不会参与 Pool 轮换或故障转移,也不会更改当前的 Pool 账号。`,"codexAuth.accountPickerCompatibility":`内置 Codex App 登录有自己的选择器;生成的映射通常使用 main,发生冲突时会使用 main-2 这类安全后缀。新增账号会获得稳定且保护隐私的标签,自定义选择器名称保持不变。现有对话和已保存的模型选择会继续路由。关闭后只隐藏生成的条目,选择器和精确路由仍会保留。普通 GPT 模型 ID 继续保持原有的 Pool 或 Direct 行为。`,"codexAuth.accountPickerUpdated":`账号指定设置已更新。`,"codexAuth.accountPickerUpdateFailed":`无法更新账号指定设置。当前显示的是最后一次确认的设置。`,"codexAuth.accountPickerLoadFailed":`无法加载账号指定设置。`,"codexAuth.accountPickerRefreshFailed":`无法刷新此设置。当前仍显示最后一次确认的值。`,"codexAuth.advancedSettings":`高级设置`,"codexAuth.advancedSettingsAria":`显示或隐藏高级 Codex 认证设置`,"codexAuth.catalogRefreshPending":`更改已保存,但 Codex 模型目录仍待刷新。请运行 ocx sync 重试。`,"anthropicPool.title":`Claude 账户池(实验性)`,"anthropicPool.enabledDesc":`遇到 429 时冷却该账户并故障转移。新会话优先使用{window}低于 {threshold}% 的账户。`,"anthropicPool.enabledNoProactiveDesc":`429 时冷却账号并切换。阈值为 0 时停用主动的用量切换,但新会话选择与 429 恢复仍会使用 {window} 窗口。`,"anthropicPool.disabledDesc":`仅使用当前活跃的 Claude 账户。仅在接受实验性路由时启用。`,"anthropicPool.experimentalWarning":`实验性功能,尚未充分验证。看起来像自动多账户轮换的行为可能导致 Anthropic 限制账户。同一组织可能共享配额——对这些账户做池化没有帮助。除非了解风险,否则请保持关闭。`,"anthropicPool.needTwoAccounts":`启用账户池前请至少添加两个 Claude OAuth 账户。`,"anthropicPool.threshold":`新会话用量阈值`,"anthropicPool.thresholdAria":`新会话用量阈值(百分比)`,"anthropicPool.thresholdHelp":`0 表示禁用基于配额的选择(仅亲和性 + 活跃账户)。默认 80。`,"anthropicPool.thresholdInvalid":`请输入 0 到 100 之间的整数`,"anthropicPool.loadFailed":`无法加载 Claude 账户池设置。`,"anthropicPool.saveFailed":`无法保存 Claude 账户池设置。`,"anthropicPool.on":`开`,"anthropicPool.off":`关`,"accountPool.strategy":`轮换策略`,"accountPool.strategyDesc":`OpenCodex 如何为新建/未绑定任务分配账号。`,"accountPool.strategyQuota":`配额`,"accountPool.strategyRoundRobin":`轮询`,"accountPool.strategyFillFirst":`填满优先`,"accountPool.strategyHintQuota":`配额策略在超过用量阈值后,也可以在现有任务的下一次请求中重新绑定账号。`,"accountPool.strategyHintRoundRobin":`轮询只轮换没有有效绑定的任务;用量阈值不会改变正常轮换。`,"accountPool.strategyHintFillFirst":`填满优先把阈值用作未绑定任务的耗尽点;健康的已绑定任务保持亲和性。`,"accountPool.unboundDefinition":`新建/未绑定任务是当前没有账号绑定的请求;已有的可见任务在代理或亲和性重置后也可能变为未绑定。`,"accountPool.stickyLimit":`轮换前的新建/未绑定任务分配数`,"accountPool.stickyLimitAria":`轮换前的新建/未绑定任务分配数`,"accountPool.stickyLimitInc":`提高粘性上限`,"accountPool.stickyLimitDec":`降低粘性上限`,"accountPool.stickyLimitHelp":`在推进到下一个账号之前,为所选账号分配这么多次新建/未绑定任务;计数在任务绑定时增加,而不是在上游成功后增加。`,"accountPool.stickyLimitInvalid":`请输入 1 到 100 之间的整数`,"accountPool.strategyLoadFailed":`无法加载轮换策略。`,"accountPool.strategyUpdateFailed":`无法保存轮换策略。`,"accountPool.quotaWindow":`配额统计窗口`,"accountPool.quotaWindowDesc":`指定按配额选择新会话、填满优先阈值判断以及可用 429 替代账户所使用的缓存用量。`,"accountPool.quotaWindowFiveHour":`5 小时用量`,"accountPool.quotaWindowWeekly":`每周用量`,"accountPool.quotaWindowMaxUtilization":`较高的用量`,"accountPool.quotaWindowHint":`每周用量会在仍有其他可用账户时跳过 5 小时用量已耗尽的账户;若没有其他账户,则回退使用这些账户。每周用量相同时优先选择 5 小时用量更低者;各账户的每周用量要等提供商页面轮询后才能获知。`,"accountPool.quotaWindowInert":`只有配额策略,或阈值大于 0 的填满优先策略,才会按用量打分;在当前轮换策略下这项设置不起作用。`,"accountPool.priority":`选择顺序`,"accountPool.priorityAria":`此账号的选择顺序`,"accountPool.priorityHint":`数字越大越先使用。只有当排在前面的账号全部用尽或不可用时,账号池才会转向更小的数字。`,"accountPool.priorityFirst":`最先`,"accountPool.priorityEarlier":`较先`,"accountPool.priorityNormal":`默认`,"accountPool.priorityLater":`较后`,"accountPool.priorityLast":`最后`,"accountPool.priorityOption":`{name}({value})`,"accountPool.priorityCustom":`自定义`,"accountPool.priorityUpdated":`已更新 {email} 的选择顺序`,"accountPool.priorityUpdateFailed":`无法保存 {email} 的选择顺序。当前显示最后一次确认的值。`,"codexAuth.switched":`下一次请求将使用 {email}`,"codexAuth.loadFailed":`无法加载 Codex 账号设置。`,"codexAuth.switchFailed":`无法切换账户。之前的选择保持不变。`,"codexAuth.removeConfirm":`删除 {id}?`,"codexAuth.removeFailed":`无法移除账户。未进行任何更改。`,"codexAuth.addTitle":`添加 Codex 账号`,"codexAuth.addIdLabel":`账号 ID(标识符)`,"codexAuth.addJsonLabel":`auth.json 内容`,"codexAuth.addHelp":`从另一台机器的 ~/.codex/auth.json 复制,或使用 codex-auth export。`,"codexAuth.importBtn":`导入`,"codexAuth.importInvalidJson":`无效的 JSON`,"codexAuth.importMissingTokens":`JSON 中缺少 access_token 或 refresh_token`,"codexAuth.importMissingId":`请输入账号 ID`,"codexAuth.accountAdded":`账号已添加到池中`,"codexAuth.addPickDesc":`使用另一个 ChatGPT 账号登录以添加到池中。`,"codexAuth.oauthLogin":`OAuth 登录`,"codexAuth.oauthDesc":`在浏览器中打开 ChatGPT 登录`,"codexAuth.deviceLogin":`设备码登录`,"codexAuth.deviceDesc":`适用于无头或远程代理:在另一台设备上输入短代码`,"codexAuth.importAuthJson":`导入 auth.json`,"codexAuth.importAuthJsonDesc":`从另一个 Codex 安装或 codex-auth 导出`,"codexAuth.back":`返回`,"codexAuth.oauthAlreadyInProgress":`登录已在进行中。请在浏览器中完成。`,"codexAuth.oauthWaiting":`等待浏览器中完成 ChatGPT 登录...`,"codexAuth.oauthSubmittingCode":`正在提交代码…`,"codexAuth.oauthCodeSubmitted":`代码已提交——正在等待登录完成…`,"codexAuth.oauthStatusRetrying":`检查登录状态时发生网络或代理错误——正在重试…`,"codexAuth.oauthCancelled":`登录已取消。`,"codexAuth.loginFailed":`登录失败`,"codexAuth.needsReauth":`重新登录`,"codexAuth.reauthenticate":`重新认证`,"codexAuth.tokenExpired":`令牌已过期 — 请重新认证此账号`,"codexAuth.mainTokenExpired":`令牌已过期 — 请通过 Codex 应用登录重新登录`,"codexAuth.emailCollision":`此账号与您的主 Codex 登录相同。请使用其他账号。`,"codexAuth.resetCreditsTitle":`重置额度`,"codexAuth.resetCreditsAvailable":`您有 {count} 个可用重置额度。`,"codexAuth.resetCreditsDesc":`每个额度可立即重置您当前的小时和每周使用限制。`,"codexAuth.noResetCredits":`没有可用的重置额度。`,"codexAuth.earnCreditsHint":`额度每月自动发放,也可通过推荐计划获得。`,"codexAuth.creditsExpireNote":`额度在获得后 30 天过期。`,"codexAuth.useOneCredit":`使用 1 个额度`,"codexAuth.confirmResetTitle":`使用重置额度?`,"codexAuth.confirmResetDesc":`这将立即重置您当前的使用限制。剩余额度:{count} 个。`,"codexAuth.irreversible":`此操作不可撤销。`,"codexAuth.useCredit":`使用额度`,"codexAuth.redeeming":`重置中...`,"codexAuth.resetSuccess":`使用限制已重置!剩余额度:{remaining} 个。`,"codexAuth.resetSuccessGeneric":`使用限制已重置!`,"codexAuth.resetAlreadyRedeemed":`该额度已兑换过,额度未变。`,"codexAuth.resetNothingToReset":`当前没有需要重置的使用窗口。`,"codexAuth.resetNoCredit":`没有可用的重置额度。`,"codexAuth.resetError":`重置额度使用失败,请重试。`,"codexAuth.fifoNote":`最早获得的额度优先使用。`,"codexAuth.confirmWhichCredit":`将使用 {date} 获得的额度。`,"codexAuth.creditNext":`即将使用`,"codexAuth.creditLabel":`额度 #{n}`,"codexAuth.creditNextBadge":`NEXT`,"codexAuth.creditGranted":`获得 {date}`,"codexAuth.creditExpires":`过期 {date}(剩余 {days} 天)`,"api.title":`API 访问`,"api.subtitle":`用生成的 API 密钥从外部应用访问 opencodex 代理。认证使用 {authHeader} 请求头;各端点接受哪些请求头见下表。`,"api.endpointNote":`请将基础 URL 用于 OpenAI 兼容客户端。Responses 与 Chat Completions 在 /v1 下提供。`,"api.baseUrl":`基础 URL`,"api.responsesEndpoint":`Responses API`,"api.chatCompletionsEndpoint":`Chat Completions API`,"api.messagesEndpoint":`Messages API`,"api.modelsEndpoint":`Models API`,"api.endpointsTitle":`网关端点`,"api.authBaseUrlNote":`客户端应使用基础 URL,然后选择下面的协议端点。`,"api.authTitle":`身份验证`,"api.authLoopback":`回环绑定(127.0.0.1 或 ::1)会跳过身份验证。远程绑定需要生成的 ocx_ 密钥或 OPENCODEX_API_AUTH_TOKEN。`,"api.modelsTitle":`外部模型目录`,"api.modelsCount":`{count} 个可调用`,"api.modelsSearch":`搜索模型`,"api.modelsSubtitle":`请使用这些精确的模型 ID 搭配 /v1/models 和你选择的入站协议。`,"api.modelsLoading":`正在加载模型…`,"api.modelsEmpty":`还没有可供外部调用的模型。`,"api.modelsNoMatch":`没有与“{query}”匹配的模型。`,"api.modelsLoadFailed":`无法加载外部模型目录。`,"api.colModel":`模型`,"api.colSource":`来源`,"api.colProtocols":`协议`,"api.copyModelId":`复制 ID`,"api.modelCopied":`已复制`,"api.testModel":`测试`,"api.testingModel":`测试中…`,"api.testSucceeded":`成功`,"api.testFailed":`失败`,"api.protocolResponses":`Responses`,"api.protocolChatCompletions":`Chat Completions`,"api.protocolMessages":`Messages`,"api.sourceNative":`ChatGPT 池`,"api.sourceCombo":`组合路由`,"api.sourceCustom":`自定义`,"api.usageResponsesTitle":`Responses 示例`,"api.usageChatTitle":`Chat Completions 示例`,"api.usageMessagesTitle":`Messages 示例`,"api.newKeyTitle":`已创建新密钥`,"api.newKeyNote":`请立即复制此密钥,它不会再次显示。`,"api.copy":`复制`,"api.copied":`已复制`,"api.dismiss":`关闭`,"api.generateTitle":`生成密钥`,"api.keyNamePlaceholder":`密钥名称(可选)`,"api.generate":`生成`,"api.generating":`创建中…`,"api.activeKeys":`活跃密钥({count})`,"api.activeKeysLoading":`有效密钥`,"api.noKeys":`还没有 API 密钥。请在上方生成一个。`,"api.workspace.sections":`API 分区`,"api.section.keys":`密钥`,"api.section.connect":`连接`,"api.section.endpoints":`端点`,"api.section.models":`模型`,"api.section.examples":`示例`,"api.workspace.details":`API 密钥详情`,"api.workspace.keyDetails":`密钥详情`,"api.workspace.keyPrefix":`密钥前缀`,"api.workspace.deleteKey":`删除密钥`,"api.workspace.deleteConfirm":`确定要删除此密钥吗?此操作无法撤销。`,"api.workspace.usageExamples":`用法示例`,"api.copyUrlHint":`点击复制 URL`,"api.urlCopied":`已复制 URL`,"api.copyExampleHint":`点击复制示例`,"api.exampleCopied":`已复制示例`,"api.colName":`名称`,"api.colKey":`密钥`,"api.colCreated":`创建时间`,"api.confirm":`确认`,"api.deleteAria":`删除 API 密钥`,"api.usageSampleInput":`你好,世界!`,"api.clientConfig.title":`客户端配置`,"api.clientConfig.rowsLabel":`连接客户端`,"api.clientConfig.details":`详情`,"api.clientConfig.detailsAria":`{client} 配置详情`,"api.clientConfig.copyAria":`复制 {client} 配置`,"api.clientConfig.downloadAria":`下载 {client} 配置`,"api.clientConfig.rowMeta":`{destination} · {count} 个模型`,"api.clientConfig.rowError":`无法生成 {client} 配置。`,"api.clientConfig.copiedAnnounceClient":`已将 {client} 配置复制到剪贴板。`,"api.clientConfig.clientOpencode":`OpenCode`,"api.clientConfig.clientPi":`Pi`,"api.clientConfig.clientOmp":`OMP`,"api.clientConfig.clientHermes":`Hermes`,"api.clientConfig.clientOpenclaw":`OpenClaw`,"api.clientConfig.clientKimi":`Kimi Code`,"api.clientConfig.clientGajae":`Gajae Code`,"api.clientConfig.clientDsh":`DeepSeek Harness (DSH)`,"api.clientConfig.clientMcode":`MiniMax Code`,"api.clientConfig.clientZcode":`ZCode`,"api.clientConfig.clientPrime":`Prime Agent`,"api.clientConfig.clientAside":`Aside`,"api.clientConfig.copy":`复制配置`,"api.clientConfig.download":`下载`,"api.clientConfig.loading":`正在生成客户端配置…`,"api.clientConfig.jsonLabel":`{client} 配置`,"api.clientConfig.destination":`目标文件`,"api.clientConfig.envHint":`启动前设置密钥`,"api.clientConfig.mergeWarning":`请合并到目标文件中。直接替换会丢失你已有的其他提供商和 MCP 配置。`,"api.clientConfig.modelCount":`已导出 {count} 个模型`,"api.clientConfig.missingLimits":`{total} 个模型中有 {count} 个没有上下文上限,客户端将使用自己的默认值。`,"api.clientConfig.noKeyYet":`{env} 目前还没有对应的密钥。离开回环地址使用前,请先在上方生成密钥。`,"api.clientConfig.loadFailed":`无法读取模型列表,因此没有生成客户端配置。`,"api.clientConfig.copiedAnnounce":`客户端配置已复制到剪贴板。`,"api.clientConfig.copyFailed":`无法复制客户端配置。`,"api.clientConfig.downloadedAnnounce":`已下载 {filename}。目前还没有任何改动,请自行将其合并到 {destination}。`,"api.clientConfig.whereDisclosure":`该文件应放在哪里`,"api.clientConfig.whereBody":`上面的路径是全局配置位置。工作目录中的项目级配置文件优先级更高;密钥由配置中指定的环境变量读取,不会写入该文件。`,"api.keysLoadFailed":`无法加载 API 密钥。`,"api.createFailed":`无法创建 API 密钥。`,"api.deleteFailed":`无法删除 API 密钥。`,"api.auth.endpoint":`端点`,"api.auth.required":`必需`,"api.auth.accepted":`可用`,"api.auth.rejected":`不接受`,"api.auth.testProtocol":`测试 {protocol}`,"api.auth.testNeedsFreshKey":`要运行带认证的测试,请先生成密钥,并让一次性显示的值保留在屏幕上。`,"api.key.name":`密钥名称`,"api.key.rename":`重命名`,"api.key.saveName":`保存名称`,"api.key.renaming":`保存中…`,"api.key.renameFailed":`无法重命名密钥,已保留你输入的内容。`,"api.key.deleting":`删除中…`,"api.rotation.title":`密钥轮换`,"api.rotation.description":`签发替换密钥,并在短暂过渡期内保留当前密钥。`,"api.rotation.start":`开始轮换`,"api.rotation.starting":`正在开始…`,"api.rotation.pending":`轮换待确认。请先更新并验证客户端,再提交轮换。`,"api.rotation.expires":`过渡期截止:`,"api.rotation.secretOnce":`替换密钥仅显示一次。关闭前请先复制。`,"api.rotation.commit":`提交轮换`,"api.rotation.abort":`中止轮换`,"api.rotation.failed":`轮换操作未完成。请刷新后重试。`,"api.rotation.startFailed":`无法开始密钥轮换。`,"api.key.copyFailed":`无法复制密钥。关闭此面板前请手动选中并复制。`,"api.attribution.title":`按密钥统计的用量`,"api.attribution.requests7d":`最近 7 天请求数`,"api.attribution.totalRequests":`已归属请求总数`,"api.attribution.totalRequestsAvailable":`可用历史中的请求`,"api.attribution.sinceAvailable":`可用归属记录起始时间`,"api.attribution.lastUsed":`最近使用`,"api.attribution.since":`统计起始`,"api.attribution.neverUsed":`统计开始后未使用`,"api.attribution.unavailable":`暂无用量`,"api.attribution.unavailableDetail":`尚未归属任何用量。统计开始之前的请求无法追溯归属。`,"api.attribution.ambiguous":`两个密钥共用同一个 ID,无法判断用量属于哪一个。请在配置文件中为每个密钥设置唯一 ID。`,"api.attribution.railAmbiguous":`ID 重复`,"claude.subtitle":`在 Claude Code 中使用 GPT、Gemini 等其他模型。`,"claude.enabledLabel":`Claude 连接`,"claude.enabledHint":`关闭后 Claude Code 无法使用此代理。`,"claude.authMode":`认证模式`,"claude.authModeHint":`Subscription 需要 Claude 账户,Proxy 无需 Anthropic 账户即可使用`,"claude.authModeSubscription":`Subscription(Claude 账户)`,"claude.authModeProxy":`Proxy(无需账户)`,"claude.authModeAuto":`自动(检测 Claude 认证)`,"claude.effectiveMode.label":`下次启动生效`,"claude.effectiveMode.manual":`手动:{mode}`,"claude.effectiveMode.autoPresent":`自动:订阅 — 已通过 {source} 找到 Claude 认证`,"claude.effectiveMode.autoAbsent":`自动:代理模式 — 未找到 Claude 认证`,"claude.effectiveMode.autoUnknown":`自动:订阅 — 无法确认认证`,"claude.effectiveMode.admissionKey":`此代理的 API 密钥仍会发送。`,"claude.authSource.claude-json-oauth":`Claude 账户`,"claude.authSource.claude-credentials-file":`凭据文件`,"claude.authSource.macos-keychain":`macOS 钥匙串`,"claude.authSource.exported-env":`环境变量`,"claude.authSource.unknown":`检测到的凭据`,"claude.systemEnv":`自动连接`,"claude.systemEnvDesc":`开启后,在任意终端运行 claude 会自动通过代理。`,"claude.systemEnvUnsupported":`自动连接仅在 macOS 上可用。在此系统上,请使用 {cmd} 启动 Claude。`,"claude.systemEnvWarn":`⚠ 需要完全退出并重新打开终端应用才能生效。不推荐使用。`,"claude.fastMode":`Fast Mode (OpenAI)`,"claude.fastModeDesc":`控制 OpenAI 模型的推理速度。ON = 优先级(更快)。OFF = 默认速度。Auto = 透传客户端设置。`,"claude.fastAuto":`Auto`,"claude.fastOn":`ON`,"claude.fastOff":`OFF`,"claude.autoContext":`自动利用大上下文`,"claude.autoContextDesc":`决定 1M 标记的范围。开:窗口能容纳压缩阈值的模型都有大上下文条目;关:仅真正的 1M 模型有。`,"claude.autoContextInert":`配置文件中存在旧式上下文大小值(maxContextTokens),此功能暂不生效。删除该值即可恢复。`,"claude.autoCompactWindow":`自动摘要触发点`,"claude.autoCompactDefault":`{value}(默认)`,"claude.autoCompactWindowDesc":`对话达到该点时自动摘要旧内容。不会超过各模型自身上限,因此 200k 模型不受影响。`,"claude.autoCompactWindowWarn":`修改该值可能导致 GPT 模型异常——若超过模型真实上限,会在摘要触发前报错。`,"claude.injectAgents":`自动注册子代理`,"claude.injectAgentsDesc":`将“子代理”页选中的模型(以及当前默认模型)注册为 Claude Code 可派遣的代理(ocx-*)。从下一个会话开始生效。`,"claude.webSearchSidecar":`网页搜索附属服务覆盖`,"claude.webSearchSidecarHint":`仅对 Claude Code 请求覆盖主网页搜索附属服务设置。`,"claude.visionSidecar":`视觉附属服务覆盖`,"claude.visionSidecarHint":`仅对 Claude Code 请求覆盖主视觉附属服务设置。`,"claude.useMainSetting":`使用主设置`,"claude.sidecarModelPlaceholder":`主设置中的模型`,"claude.quickstart":`开始使用`,"claude.quickstartHint":`{cmd} 通过代理打开 Claude Code。你的 claude.ai 登录保持不变。`,"claude.manualEnv":`手动配置(高级)`,"claude.smallFastModel":`后台辅助模型`,"claude.smallFastModelHint":`Claude Code 用于对话摘要、主题识别等后台工作的模型。子代理的 haiku 别名也使用它。留空 = Claude 默认(Haiku)。`,"claude.smallFastModelAccurateHint":`Claude Code 用于聊天摘要、主题识别等后台工作的模型。子代理的 haiku 别名也使用此模型。`,"claude.smallFastModelUnsetOption":`让 Claude Code 选择(原生模型)`,"claude.smallFastModelNativeWarning":`留空时,OpenCodex 不会设置辅助模型覆盖项。Claude Code 可能使用其原生 Sonnet 模型,并可能产生原生提供方费用。`,"claude.slotUnset":`使用 Claude 默认值`,"claude.modelMap":`模型拦截`,"claude.modelMapHint":`拦截对特定模型的请求并重定向到你指定的模型。默认为空——添加规则后才生效。`,"claude.mapFrom":`原始模型(如 claude-sonnet-4-5)`,"claude.mapTo":`替换为(如 gemini/gemini-3-pro)`,"claude.addMapping":`添加规则`,"claude.removeMapping":`删除规则`,"claude.aliases":`可用模型`,"claude.aliasesHint":`Claude Code 的 /model 菜单中显示的模型列表。`,"claude.aliasProviderOther":`其他`,"claude.loading":`加载中…`,"claude.loadFail":`加载 Claude 设置失败`,"claude.saved":`已保存。`,"claude.saveFailed":`保存失败`,"claude.networkError":`网络错误 — 代理是否在运行?`,"claude.toggleAria":`切换 Claude 连接`,"claude.none":`无`,"common.close":`关闭`,"common.ok":`确定`,"app.logoAria":`opencodex 徽标`,"app.claudeOn":`Claude 开`,"app.claudeOff":`Claude 关`,"usage.dayMon":`一`,"usage.dayWed":`三`,"usage.dayFri":`五`,"usage.heatmap.tooltipTokens":`{tokens} 令牌`,"usage.heatmap.tooltipRequests":`{requests} 请求`,"nav.storage":`存储`,"storage.title":`存储`,"storage.subtitle":`查看 CODEX_HOME 占用。清理不会动到活动会话。`,"storage.loading":`正在扫描存储…`,"storage.empty":`CODEX_HOME 为空或不存在——没有可显示的内容。`,"storage.error":`存储扫描失败。请检查 CODEX_HOME 是否指向有效目录。`,"storage.refresh":`重新扫描`,"storage.rescanned":`扫描完成。`,"storage.card.total":`总大小`,"storage.card.files":`文件数`,"storage.card.home":`CODEX_HOME`,"storage.snapshot.lastScan":`上次扫描`,"storage.snapshot.scanning":`扫描中…`,"storage.snapshot.unavailable":`尚无扫描。`,"storage.cleanupCard.title":`释放空间`,"storage.cleanupCard.tabs":`清理选项`,"storage.cleanupCard.tab.policy":`策略`,"storage.cleanupCard.tab.quarantine":`隔离区`,"storage.cleanup.noArchives":`没有可清理的归档会话。`,"storage.section.buckets":`分类`,"storage.section.largest":`最大文件`,"storage.workspace.overview":`概览`,"storage.workspace.selectBucket":`从列表中选择一个存储桶以查看明细。`,"storage.col.bucket":`分类`,"storage.col.size":`大小`,"storage.col.files":`文件`,"storage.col.oldest":`最旧`,"storage.col.newest":`最新`,"storage.col.rows":`数据库行数`,"storage.rows.unknown":`未知(已锁定)`,"storage.bucket.sessions":`活动会话`,"storage.bucket.archived_sessions":`已归档会话`,"storage.bucket.logs_db":`日志数据库`,"storage.bucket.state_db":`状态数据库`,"storage.bucket.attachments":`附件`,"storage.bucket.deletion_manifests":`删除清单`,"storage.bucket.other":`其他`,"storage.cleanup.title":`归档清理`,"storage.cleanup.help":`按百分比移除最旧的归档会话。不会触碰活动会话。默认隔离——文件移至 CODEX_HOME/.trash。`,"storage.cleanup.slider":`最旧归档百分比`,"storage.cleanup.percent":`{percent}%`,"storage.cleanup.preset":`{percent}`,"storage.cleanup.preview":`预览`,"storage.cleanup.confirmTitle":`确认归档清理`,"storage.cleanup.confirmBody":`将处理 {count} 个归档文件(约 {size}),即最旧的 {percent}%。`,"storage.cleanup.moreFiles":`…以及另外 {n} 个`,"storage.cleanup.permanent":`永久删除(跳过隔离)`,"storage.cleanup.permanentWarn":`永久删除无法撤销。`,"storage.cleanup.quarantineNote":`文件会移到 CODEX_HOME 下的 .trash。可在「隔离区」标签页恢复。`,"storage.cleanup.cancel":`取消`,"storage.cleanup.confirmQuarantine":`隔离`,"storage.cleanup.confirmPermanent":`永久删除`,"storage.cleanup.doneQuarantine":`已隔离 {count} 个文件({size})。`,"storage.cleanup.donePermanent":`已永久删除 {count} 个文件({size})。`,"storage.cleanup.previewFailed":`预览失败。`,"storage.cleanup.cleanupFailed":`清理失败。`,"storage.cleanup.err.codex_busy":`Codex 正在使用 state.sqlite — 请退出 Codex 后重试。`,"storage.cleanup.err.stale_preview":`预览后归档文件已变化 — 请重新预览。`,"storage.cleanup.err.restore_pending_overlap":`所选归档与未完成的隔离区恢复重叠 — 请先完成或重试恢复。`,"storage.cleanup.err.referenced_history":`所选归档仍被 fork 或分页历史引用。`,"storage.cleanup.err.invalid_digest":`预览摘要缺失或无效。`,"storage.cleanup.err.invalid_mode":`模式必须是 quarantine 或 permanent。`,"storage.cleanup.err.fs_failed":`文件系统清理失败。部分更改可能已生效 — 请检查 CODEX_HOME/.trash 及显示的恢复路径。`,"storage.cleanup.err.fs_failed_trash":`文件系统清理失败。部分更改可能已生效 — 请在 {trashDir} 和 manifest.json 中查找可恢复文件。`,"storage.cleanup.err.db_reconcile_failed":`无法更新 Codex 状态数据库。`,"storage.cleanup.err.cleanup_failed":`清理失败。`,"storage.trash.title":`隔离区`,"storage.trash.help":`已移至 CODEX_HOME/.trash 的归档会话。恢复会把 JSONL 与线程行写回。`,"storage.trash.empty":`没有隔离条目。`,"storage.trash.loading":`正在加载隔离区…`,"storage.trash.col.when":`隔离时间`,"storage.trash.col.files":`文件`,"storage.trash.col.size":`大小`,"storage.trash.col.mode":`模式`,"storage.trash.col.id":`条目`,"storage.trash.restore":`恢复`,"storage.trash.confirmTitle":`恢复隔离条目?`,"storage.trash.confirmBody":`将 {count} 个文件(约 {size})从 {id} 恢复到归档会话。`,"storage.trash.cancel":`取消`,"storage.trash.confirmRestore":`恢复`,"storage.trash.done":`已恢复 {count} 个文件({size})。`,"storage.trash.restoreFailed":`恢复失败。`,"storage.trash.listFailed":`无法列出隔离条目。`,"storage.trash.mode.quarantine":`隔离`,"storage.trash.mode.permanent":`永久(未完成)`,"storage.trash.err.codex_busy":`Codex 正在使用 state.sqlite — 请退出 Codex 后重试。`,"storage.trash.err.invalid_trash":`隔离条目 ID 缺失或无效。`,"storage.trash.err.missing_trash":`未找到隔离条目。`,"storage.trash.err.dest_exists":`恢复目标已存在 — 请删除或重命名归档文件后重试。`,"storage.trash.err.fs_failed":`文件系统恢复失败。部分文件可能已恢复 — 请检查 archived_sessions 与 .trash。`,"storage.trash.err.storage_mutation_busy":`另一项存储清理或恢复正在进行 — 请稍后再试。`,"storage.trash.err.db_reconcile_failed":`无法恢复 Codex 状态数据库行。`,"storage.trash.err.restore_failed":`恢复失败。`,"storage.trash.err.restore_worker_timeout":`恢复耗时过长(超过 10 分钟)已停止。`,"storage.trash.err.restore_worker_aborted":`关闭过程中恢复已取消。`,"storage.trash.err.restore_worker_failed":`恢复 worker 崩溃或意外失败。`,"storage.policy.title":`自动清理策略`,"storage.policy.help":`当归档大小超过阈值时可选批量清理。默认关闭——不会自动启用。`,"storage.policy.loading":`正在加载策略…`,"storage.policy.loadFailed":`无法加载清理策略。`,"storage.policy.saveFailed":`无法保存清理策略。`,"storage.policy.runFailed":`策略运行失败。`,"storage.policy.alreadyRunning":`清理策略已在运行中。`,"storage.policy.invalid":`策略值无效。`,"storage.policy.enabled":`启用自动清理`,"storage.policy.enabledHint":`默认关闭。启用后仅按所选计划(或立即运行)执行。`,"storage.policy.threshold":`当归档大小超过(GiB)`,"storage.policy.trigger":`触发条件`,"storage.policy.target":`清理目标`,"storage.policy.targetPercent":`删除最旧归档(%)`,"storage.policy.targetReduce":`将归档缩小至(GiB)`,"storage.policy.thresholdInc":`提高阈值`,"storage.policy.thresholdDec":`降低阈值`,"storage.policy.percentInc":`提高百分比`,"storage.policy.percentDec":`降低百分比`,"storage.policy.reduceInc":`提高缩减目标`,"storage.policy.reduceDec":`降低缩减目标`,"storage.policy.schedule":`计划`,"storage.policy.schedule.manual":`仅手动`,"storage.policy.schedule.startup":`代理启动时`,"storage.policy.schedule.daily":`每天`,"storage.policy.schedule.weekly":`每周`,"storage.policy.mode":`删除模式`,"storage.policy.mode.quarantine":`隔离(默认)`,"storage.policy.mode.permanent":`永久删除`,"storage.policy.permanentWarn":`永久模式无法撤销。不确定时请使用隔离。`,"storage.policy.lastRun":`上次运行`,"storage.policy.lastRunDetail":`已移除 {count} · 释放 {size}`,"storage.policy.nextRun":`下次运行`,"storage.policy.never":`从未`,"storage.policy.save":`保存`,"storage.policy.runNow":`立即运行`,"storage.policy.running":`运行中…`,"storage.policy.saved":`策略已保存。`,"storage.policy.skippedDisabled":`策略已禁用 — 请先启用。`,"storage.policy.skippedUnder":`归档大小低于阈值 — 无需操作。`,"storage.policy.skippedEmpty":`没有匹配目标的归档候选项。`,"storage.policy.doneQuarantine":`策略已隔离 {count} 个文件({size})。`,"storage.policy.donePermanent":`策略已永久删除 {count} 个文件({size})。`,"storage.policy.metadataSaveWarning":`策略运行已完成,但无法保存其调度元数据。`,"modal.back":`返回`,"modal.badge.oauth":`OAuth`,"modal.customProvider":`自定义提供方`,"modal.failedStatus":`失败 ({status})`,"modal.loginError":`登录错误:{error}`,"modal.badge.codexLogin":`Codex 登录`,"modal.badge.local":`本地`,"modal.badge.apiKey":`API 密钥`,"modal.badge.direct":`直连`,"modal.badge.pool":`账户池`,"modal.badge.free":`免费`,"modal.invalidPreset":`此内置提供方预设不完整。请重启代理后重试。`,"modal.freeTierTitle":`免费层级`,"modal.freeTierDefault":`无需 API 密钥,开箱即用。`,"modal.tab.accounts":`账户`,"modal.tab.free":`免费`,"modal.tab.paid":`付费`,"modal.accountsHint":`在此登录 ChatGPT/Codex、OAuth 与 API 密钥账户。OpenAI 为内置提供商 — 请登录,无需再次添加。`,"modal.accountsCodexAuthLink":`Codex 认证`,"modal.notListed":`没有你要的提供商?添加自定义`,"modal.catalogLoading":`正在加载目录…`,"modal.accountLogin":`登录`,"modal.accountLogout":`退出登录`,"modal.accountAdd":`添加账户`,"modal.accountManage":`管理`,"modal.accountCodexPool":`ChatGPT 账户池`,"modal.accountLoggedIn":`已登录`,"modal.accountLoggedOut":`未登录`,"quota.fiveHourLimit":`5 小时限额`,"quota.ageMinutes":`{n} 分钟`,"quota.ageHours":`{n} 小时`,"quota.ageDays":`{n} 天`,"quota.observedAgo":`{age}前获取`,"quota.observedHint":`Meta 仅在流式响应期间报告用量,因此这是最后一次获取的数值,而非实时读数。`,"quota.weeklyLimit":`每周限额`,"quota.monthlyLimit":`30 天限额`,"quota.cursorFirstParty":`官方模型`,"quota.cursorApiUsage":`API 用量`,"quota.totalSubscriptionCredits":`订阅总额度`,"quota.creditsBalance":`额度余额`,"quota.creditsPeriodEnds":`账单周期结束于 {date}`,"quota.usedPercent":`已用 {pct}%`,"quota.limitReached":`已达上限`,"quota.resetsToday":`今天 {time} 重置`,"quota.resetsTomorrow":`明天 {time} 重置`,"quota.resetsAt":`{when} 重置`,"quota.resetsRelativeMinutes":`{n} 分钟后重置`,"quota.resetsRelativeHours":`{n} 小时后重置`,"pws.status.ready":`就绪`,"pws.status.needsSetup":`需要设置`,"pws.status.needsAttention":`需要关注`,"pws.auth.chatgptPassthrough":`ChatGPT 直通`,"pws.auth.noKey":`无需密钥`,"pws.freeTitle":`免费定价(可能仍需密钥)`,"pws.localTitle":`本地运行时`,"pws.modelCountOne":`1 个模型`,"pws.modelCount":`{count} 个模型`,"pws.rail.suffixDefault":` · 默认`,"pws.rail.suffixLocal":` · 本地`,"pws.rail.suffixFree":` · 免费`,"pws.rail.selectAria":`选择 {name} — {status}{suffix}`,"pws.searchPlaceholder":`搜索提供商…`,"pws.filterAria":`筛选提供商`,"pws.providerFiltersAria":`提供商筛选`,"pws.filters":`筛选`,"pws.filterStatus":`状态`,"pws.pricing":`定价`,"pws.paid":`付费`,"pws.filterType":`类型`,"pws.type.cloud":`云端`,"pws.type.local":`本地`,"pws.type.selfHosted":`自托管`,"pws.type.login":`登录`,"pws.sort":`排序`,"pws.sortProvidersAria":`排序提供商`,"pws.sort.az":`A–Z`,"pws.sort.za":`Z–A`,"pws.sort.freePaid":`免费优先`,"pws.sort.paidFree":`付费优先`,"pws.sort.accountsFirst":`账户优先`,"pws.resetAll":`全部重置`,"pws.providerList":`提供商列表`,"pws.providersAria":`提供商`,"pws.groupReady":`就绪 ({count})`,"pws.groupNeedsSetup":`需要设置 ({count})`,"pws.groupDisabled":`已禁用 ({count})`,"pws.noSearchResults":`没有匹配搜索的提供商。`,"pws.noMatchFilters":`没有匹配筛选的提供商。`,"pws.noProvidersConfigured":`尚未配置提供商。`,"pws.workspaceMainAria":`提供商详情`,"pws.detailComingSoon":`详情视图即将推出 — 请在经典视图中管理。`,"pws.selectPrompt":`从列表中选择一个提供商。`,"pws.connectFirst":`连接你的第一个提供商`,"pws.empty.browseFree":`浏览免费提供商`,"pws.empty.browseFreeDesc":`无需订阅即可开始`,"pws.empty.connectAccount":`连接账户`,"pws.empty.connectAccountDesc":`使用 ChatGPT 或提供商登录`,"pws.empty.addEndpoint":`添加端点`,"pws.empty.addEndpointDesc":`自定义 base URL 和 API 密钥`,"pws.tab.overview":`概览`,"pws.tab.models":`模型`,"pws.tab.usage":`用量`,"pws.tab.accounts":`账户`,"pws.tab.settings":`设置`,"pws.connection":`连接`,"pws.status.connected":`已连接`,"pws.attentionTitle":`需要关注`,"pws.attention.reauth":`当前账号需要重新认证`,"pws.attention.reauthForward":`当前 Codex 账号需要重新认证 — 请到“账号”中处理`,"pws.attention.missingCredentials":`缺少凭证`,"pws.cell.auth":`认证`,"pws.cell.note":`备注`,"pws.cell.defaultModel":`默认模型`,"pws.statsAria":`提供商统计`,"pws.statsTitle":`统计`,"pws.stats.totalRequests":`请求数(30 天)`,"pws.stats.totalTokens":`令牌(30 天)`,"pws.stats.quotaUpdated":`配额更新`,"pws.stats.quotaTracked":`在用量标签查看限额。`,"pws.stats.source":`来源`,"pws.usageLast30d":`用量(最近 30 天)`,"pws.estimatedCost":`预估费用`,"pws.costDisclaimer":`基于 API 公示价格的预估值,非实际计费金额。`,"pws.modelBreakdown":`模型用量明细`,"pws.col.model":`模型`,"pws.col.cost":`预估费用`,"pws.col.tokens":`Token`,"pws.col.requests":`请求`,"pws.col.share":`占比`,"pws.tokenInput":`输入`,"pws.tokenOutput":`输出`,"pws.metricRequests":`请求`,"pws.metricTokens":`令牌`,"pws.usageUnavailable":`尚无用量记录。`,"pws.rateLimits":`速率限制`,"pws.quotaUnavailable":`此提供商暂无配额数据。`,"pws.accountQuotaUnavailable":`速率限制数据暂时不可用;若有上次已知值则继续显示。`,"pws.selected":`已选择`,"pws.copyModelId":`复制 ID`,"pws.modelCopied":`已复制!`,"pws.modelsAvailable":`{count} 个可用`,"pws.modelSearchPlaceholder":`筛选模型…`,"pws.modelsLoading":`正在加载模型…`,"pws.modelsLoadFailed":`无法加载模型。`,"pws.modelsNeedsReauth":`需要重新登录后才能获取实时模型列表。当前显示已配置的模型。`,"pws.modelsConfiguredFallback":`显示已配置的模型(实时发现不可用)。`,"pws.modelsTruncated":`显示 {total} 个模型中的前 {shown} 个。使用筛选以缩小列表。`,"pws.retry":`重试`,"pws.noModels":`未发现此提供商的模型。`,"pws.noModelMatch":`没有匹配筛选的模型。`,"pws.adapterBaseRequired":`适配器和基本 URL 为必填项。`,"pws.addAccount":`添加账户`,"pws.addKey":`添加 API 密钥`,"pws.apiKeys":`API 密钥`,"pws.authMode":`认证方式`,"pws.availableAccounts":`可用账户`,"pws.accountOrdinal":`账户 {count}`,"pws.accountsLoading":`正在加载账户…`,"pws.accountsLoadFailed":`无法加载账户。`,"pws.retryAccounts":`重试`,"pws.noAccounts":`尚未连接任何账户。`,"pws.cockpitImportDescription":`从此设备导入 Cockpit Tools Antigravity JSON 导出文件。不会显示文件内容。`,"pws.cockpitImportFileLabel":`Cockpit Tools Antigravity JSON 导出文件`,"pws.cockpitImportChooseFile":`选择 JSON 文件`,"pws.cockpitImporting":`正在导入…`,"pws.cockpitImportInvalid":`所选文件不是有效的 JSON 导出文件或文件过大。`,"pws.cockpitImportFailed":`无法完成账户导入。`,"pws.cockpitImportComplete":`导入完成:已导入 {imported},已更新 {updated},失败 {failed},不支持 {unsupported}。`,"pws.accountSwitching":`切换中…`,"pws.accountCurrent":`当前账户`,"pws.defaultModelNone":`无(使用提供商默认值)`,"pws.discardSettings":`放弃`,"pws.jsonEditorDesc":`直接编辑提供商 JSON 配置。更改将立即保存。`,"pws.jsonEditorTitle":`JSON 编辑器 — {name}`,"pws.jsonRestore":`恢复`,"pws.jsonSave":`保存`,"pws.loggedInTitle":`已登录`,"pws.notLoggedInTitle":`未登录`,"pws.note":`备注`,"pws.allowPrivateNetwork":`允许本地/私有网络`,"pws.liveModels":`从提供方发现模型`,"pws.liveModelsDesc":`获取提供方的实时模型目录。关闭后仅使用已配置的静态模型。`,"pws.xaiResponsesOptIn":`为 Grok 4.5 和 4.6 使用 Responses API`,"pws.xaiResponsesOptInDesc":`通过 openai-responses 路由这两个模型。其他 Grok 模型和层级行为不变。`,"pws.xaiResponsesOptInMixed":`已部分启用。`,"pws.cursorTransport":`Cursor 传输协议`,"pws.cursorTransportHttp2":`HTTP/2(默认)`,"pws.cursorTransportHttp1":`HTTP/1.1(代理兼容)`,"pws.cursorTransportDesc":`当代理无法稳定承载 Cursor 的 HTTP/2 流时,请使用 HTTP/1.1。`,"pws.optionalPlaceholder":`可选`,"pws.providerId":`提供商 ID`,"pws.reauth":`需要重新认证`,"pws.reauthenticate":`重新认证`,"pws.copyDoctor":`复制 ocx doctor`,"pws.doctorCopied":`已复制`,"pws.healthCooldownHint":`请等到冷却结束。暂时不要探测此账户。`,"pws.doctorCopyUnavailable":`剪贴板不可用`,"pws.healthLabel.rateLimited":`已限速`,"pws.healthLabel.quotaLimited":`配额受限`,"pws.healthLabel.reauthRequired":`需要重新认证`,"pws.healthLabel.refreshFailed":`刷新失败`,"pws.healthLabel.metadataMismatch":`元数据不匹配`,"pws.healthLabel.credentialConflict":`凭证冲突`,"pws.healthSummary.rateLimited":`{provider} {account}:限速至 {until}。在此之前将暂停该账户的路由。`,"pws.healthSummary.quotaLimited":`{provider} {account}:配额限制至 {until}。在此之前将暂停该账户的路由。`,"pws.healthSummary.reauthRequired":`{provider} {account}:需要重新认证。`,"pws.healthSummary.credentialConflict":`{provider} {account}:凭证冲突。`,"pws.healthSummary.metadataMismatch":`{provider} {account}:元数据不匹配。`,"pws.healthSummary.staleCredentials":`{provider} {account}:凭证不完整。`,"pws.removeConfirm":`移除`,"pws.removeConfirmBody":`移除提供商「{name}」?此操作无法撤销。`,"pws.removeDefaultConfirmBody":`移除默认提供方「{name}」?「{defaultProvider}」将成为默认提供方。此操作无法撤销。`,"pws.removeConfirmTitle":`移除提供商`,"pws.saveSettings":`保存`,"pws.pacingTitle":`请求节流`,"pws.pacingDesc":`均匀延迟发往此提供商的请求启动。流式响应可以重叠。`,"pws.pacingEnabled":`启用`,"pws.pacingRpm":`每分钟请求数`,"pws.pacingRpmUnit":`RPM`,"pws.pacingDelay":`最小间隔(毫秒)`,"pws.pacingSlowerWins":`以较慢的提供商限制为准,模型规则只能增加延迟。`,"pws.pacingQueued":`排队中`,"pws.pacingNextSlot":`距下个时隙`,"pws.pacingLastModel":`上个模型`,"pws.pacingNone":`无`,"pws.pacingModelOverrides":`模型规则`,"pws.pacingModel":`模型`,"pws.pacingAdd":`添加规则`,"pws.pacingRemove":`移除`,"pws.pacingRemoveModel":`移除 {model} 的请求节流规则`,"pws.pacingRuleRequired":`请先设置提供商限制或模型规则,再启用请求节流。`,"pws.saving":`保存中…`,"pws.settingsSaved":`设置已保存。`,"pws.accountModeSaved":`账户模式已保存。`,"pws.accountModeFailed":`无法切换账户模式。`,"pws.accountModeConfirm":`切换 OpenAI 账户模式?正在进行的对话将重新分配到另一种模式的账户集合,配额用量将按新模式计入。`,"pws.settingsUnsavedBar":`有未保存的更改。`,"pws.unsavedLeaveBody":`有未保存的更改。离开前保存吗?`,"pws.unsavedLeaveTitle":`未保存的更改`,"pws.attentionRequired":`需要关注`,"pws.attentionAria":`{name}:{reason}`,"pws.missingCredentials":`缺少凭证`,"pws.editJsonDesc":`以 JSON 编辑原始代理配置`,"pws.updatesUnavailable":`提供商更新不可用。`,"pws.dashboard.title":`提供商概览`,"pws.dashboard.subtitle":`在一个地方管理所有模型提供商。`,"pws.dashboard.rateLimits":`速率限制`,"pws.capacity.estimate":`按配置权重估算的账户池`,"pws.capacity.currentAccount":`当前有效账户`,"pws.capacity.nextRecovery":`下一次容量恢复`,"pws.capacity.recoveryShare":`+{percent}% 账户池容量`,"pws.capacity.incomplete":`覆盖不完整:已排除 {excluded} 个账户`,"pws.capacity.uncalibratedPlan":`{count} 个账户使用未校准套餐,按基准席位权重计入,因此该估算可能偏保守`,"pws.capacity.partial":`部分窗口覆盖不完整:{count} 个账户未报告所有显示的限额窗口`,"pws.capacity.windowPartial":`部分`,"pws.capacity.windowPartialA11y":`{window}:账户覆盖不完整`,"pws.dashboard.recentlyUsed":`最近使用`,"pws.dashboard.requests":`{count} 个请求`,"pws.dashboard.checkedAgo":`{time} 前检查`,"pws.dashboard.noQuota":`无配额数据`,"pws.dashboard.noUsage":`暂无使用数据`,"pws.dashboard.noRateLimits":`暂无速率限制数据`,"pws.allProviders":`提供商概览`,"pws.enabledLabel":`已启用`,"pws.testConnection":`测试连接`,"pws.testing":`测试中…`,"pws.connectionOk":`连接成功`,"pws.connectionFailed":`连接失败`,"pws.connectionNotApplicable":`不适用 — 此提供方使用静态模型目录。`,"pws.editSettings":`编辑设置`,"pws.viewUsage":`查看详细用量`,"pws.allSystemsOk":`所有系统正常运行`,"pws.apiKeyConfigured":`API 密钥已配置`,"pws.addApiKey":`添加 API 密钥`,"pws.loggedInAs":`已登录为 {email}`,"pws.notLoggedIn":`未登录`,"pws.passthrough":`Codex 透传`,"pws.notes":`备注`,"pws.notePlaceholder":`添加关于此提供商的备注...`,"pws.noteSaved":`备注已保存`,"pws.authSummary":`认证`,"time.justNow":`刚刚`,"time.notChecked":`未检查`,"time.minutesAgo":`{n} 分钟前`,"time.hoursAgo":`{n} 小时前`,"time.daysAgo":`{n} 天前`,"modal.noMatch":`无匹配。`,"modal.oauthDefaultNote":`使用账户登录 — 无需 API 密钥。`,"modal.oauthComingSoon":`{label} 的 OAuth 登录将在下次更新提供。请先使用 API 密钥。`,"modal.oauthComingSoonShort":`此提供方的 OAuth 登录将在下次更新提供 — 请先使用 API 密钥。`,"modal.useApiKeyInstead":`改用 API 密钥`,"modal.setupGuide":`设置指南`,"modal.setupStep1Prefix":`前往`,"modal.setupDashboardLink":`{label} 控制台`,"modal.setupStep1Suffix":`并复制 API 密钥`,"modal.setupStep2":`粘贴到下方的 API 密钥字段`,"modal.setupStep3":`点击添加提供方 — 模型会自动发现`,"modal.namePlaceholder":`例如 openrouter`,"modal.duplicateWarn":`提供方 "{name}" 已存在,将被覆盖。`,"modal.forwardHintPrefix":`无需密钥 — 代理会转发你的`,"modal.forwardCredentials":`codex login`,"modal.forwardHintSuffix":`凭据到此提供方。`,"modal.localHint":`不会存储 API 密钥。这会为 Codex 添加 Cursor 的公开模型目录,但在审计完成前,实时 Cursor 传输与原生文件/Shell 执行仍保持禁用。`,"modal.getApiKey":`获取 {label} API 密钥`,"modal.apiKey":`API 密钥`,"modal.apiKeyTransport":`API 密钥请求头`,"modal.apiKeyTransportNative":`x-api-key(Anthropic 原生)`,"modal.apiKeyTransportBearer":`Authorization: Bearer`,"modal.apiKeyPlaceholder":`sk-…(或 $ENV_VAR)`,"modal.defaultModelPlaceholder":`例如 gpt-5.5`,"modal.baseUrlPlaceholder":`https://...`,"modal.baseUrlPlaceholderError":`Base URL 包含未解析的 {placeholder},请替换为实际值。`,"modal.baseUrlPlaceholderHint":`请在添加前将 Base URL 中的 {placeholder} 替换为你的实际 Account ID。`,"modal.adding":`正在添加…`,"modal.useOauthLogin":`← 使用 OAuth 登录`,"codexAuth.addIdPlaceholder":`codex-work, codex-alt, team…`,"codexAuth.resetCreditsAria":`{count} 个重置额度`,"claude.pageTitle":`Claude Code`,"claude.workspace.settings":`设置`,"cws.loading":`正在加载组合…`,"cws.loadFailed":`无法加载组合。`,"cws.saveFailed":`无法保存组合。`,"cws.removeFailed":`无法删除组合。`,"cws.saved":`组合已保存。`,"cws.created":`已创建 {model}。`,"cws.removed":`已删除 combo/{id}。`,"cws.renamed":`已将 {from} 重命名为 {to}。`,"cws.add":`添加组合`,"cws.addTitle":`添加组合`,"cws.addSubtitle":`创建跨提供方的虚拟模型,并指定客户端实际请求的模型名称。`,"cws.create":`创建组合`,"cws.railAria":`组合列表`,"cws.searchPlaceholder":`搜索组合或目标…`,"cws.noSearchResults":`没有匹配的组合。`,"cws.group.failover":`故障转移`,"cws.group.roundRobin":`轮询`,"cws.group.other":`其他策略`,"cws.targetCount":`{count} 个目标`,"cws.targetCountOne":`1 个目标`,"cws.overviewTitle":`组合`,"cws.overviewBlurb":`在提供方/模型目标之间按故障转移、轮询、加权随机、最少使用或最早配额重置路由的虚拟模型。`,"cws.count.total":`总计`,"cws.count.failover":`故障转移`,"cws.count.roundRobin":`轮询`,"cws.count.other":`其他`,"cws.howTitle":`工作原理`,"cws.howBody":`在 Codex 中请求组合的公开模型名称;未设置时默认使用 combo/。OpenCodex 仅在可重试的上游错误时切换目标。若没有可用目标,请求会直接失败,不会回退到全局默认提供方。`,"cws.attentionTitle":`需要关注`,"cws.attention.empty":`未配置目标`,"cws.attention.few":`只有一个目标 — 故障转移无处可跳`,"cws.attention.catalogOmitted":`未出现在模型目录中 — 成员能力不完整或不兼容(缺少上下文窗口/元数据,或模态交集为空)。按别名路由仍可用`,"cws.attention.allTargetsExhausted":`所有已启用目标的额度均已用尽`,"cws.emptyTitle":`创建第一个组合`,"cws.empty.createDesc":`命名虚拟模型并串联两个或多个后端。`,"cws.backToAll":`返回全部组合`,"cws.allCombos":`全部组合`,"cws.copyModel":`复制 ID`,"cws.copied":`已复制`,"cws.tabsLabel":`组合详情分区`,"cws.tab.config":`配置`,"cws.tab.about":`关于`,"cws.strategy":`策略`,"cws.strategy.failover":`故障转移`,"cws.strategy.roundRobin":`轮询`,"cws.strategy.random":`随机`,"cws.strategy.leastUsed":`最少使用`,"cws.strategy.resetWindow":`重置窗口`,"cws.strategy.failoverHint":`按顺序尝试目标。若出现可重试错误(限流、故障、订阅门控),则跳到下一个。`,"cws.strategy.roundRobinHint":`按权重确定性地分配流量。将所选目标保留一批成功请求后,再推进到下一个目标。`,"cws.strategy.randomHint":`每个请求按权重比例随机抽取一个可用目标,请求之间不保持粘性。`,"cws.strategy.leastUsedHint":`把每个请求路由到成功次数最少的可用目标。计数随代理重启归零。`,"cws.strategy.resetWindowHint":`优先选择配额窗口最早重置的可用目标。缺少配额数据时回退到配置顺序。`,"cws.field.id":`组合 ID`,"cws.field.idHint":`客户端将请求 {model}`,"cws.field.idInternalHint":`组合的内部 ID,创建后仍可修改。`,"cws.field.idHintEdit":`修改 ID 即重命名组合。客户端将请求 {model}。`,"cws.field.alias":`公开模型名称`,"cws.field.aliasPlaceholder":`deepseek-v4-flash 或 vendor/model`,"cws.field.aliasHint":`可选。可填无前缀裸名称、自定义前缀(如 vendor/model),或留空使用 combo/。`,"cws.field.nativeAlias":`原生 OpenAI 别名`,"cws.field.nativeAliasHint":`允许此组合接管受支持的未限定 OpenAI 原生模型 ID。带账户或提供商限定的 OpenAI 路由仍保持独立。`,"cws.field.displayName":`显示名称`,"cws.field.displayNameHint":`模型选择器中的标签。启用原生 OpenAI 别名时必填。`,"cws.field.stickyLimit":`轮换前的粘性成功次数`,"cws.field.stickyLimitHint":`加权选择器推进前,将所选目标保留这么多次成功请求。`,"cws.field.defaultEffort":`默认推理级别`,"cws.field.defaultEffortNone":`无(使用目标默认)`,"cws.field.defaultEffortHint":`仅在客户端未指定推理级别时使用。选项为所选目标已公布努力级别的交集。`,"cws.capability.imageInputUnavailable":`所有已选目标均支持图片输入后才可用。`,"cws.capability.imageInputHint":`所有目标均支持图片时默认开启;关闭后仅接受文本。`,"cws.capability.imageInput":`图片 / 多模态`,"cws.capability.adaptiveEffort":`自适应推理档位`,"cws.capability.adaptiveEffortHint":`关闭:只要有一个目标不支持推理档位,整个组合的选择器都会消失。开启:这些目标仍可使用,选择器保留其余目标共有的档位。`,"cws.capabilities":`能力`,"cws.field.defaultEffortUnsupported":`该级别不在目标的公共阶梯中 — 请求时会被忽略或就近映射。`,"cws.field.defaultEffortUnsupportedOption":`不在交集中`,"cws.targets":`目标`,"cws.targets.failoverHint":`顺序很重要 — 第一个为主。`,"cws.targets.roundRobinHint":`权重控制确定性的相对选择;顺序用于打破轮换环中的平局。`,"cws.targets.randomHint":`权重控制每次抽取的概率,顺序无关紧要。`,"cws.targets.leastUsedHint":`顺序仅在使用量相同的目标之间打破平局。`,"cws.targets.resetWindowHint":`配额数据缺失或相同时按顺序处理。`,"cws.target.provider":`提供方`,"cws.target.model":`模型`,"cws.target.weight":`权重`,"cws.target.pickProvider":`选择提供方…`,"cws.target.pickProviderFirst":`请先选择提供方…`,"cws.target.pickModel":`选择模型…`,"cws.target.noModels":`该提供方没有模型`,"cws.target.modelPlaceholder":`模型 ID`,"cws.target.add":`添加目标`,"cws.target.drag":`拖动以重新排序`,"cws.target.moveUp":`上移`,"cws.target.moveDown":`下移`,"cws.quota.available":`可用`,"cws.quota.exhausted":`额度已用尽`,"cws.quota.unknown":`额度未知`,"cws.quota.allExhausted":`所有已启用目标的额度均已用尽。请选择其他目标,或等待额度恢复。`,"cws.aboutTitle":`运行时`,"cws.aboutBody":`失败目标会短暂冷却并遵循 Retry-After。无效请求与上下文错误不会切换。每个目标按自身能力调整推理级别;所有目标耗尽时直接失败。日志与用量会保留有序的实际尝试及每次尝试的用量。`,"cws.removeConfirmTitle":`删除 {model}?`,"cws.removeConfirmDesc":`从配置与 Codex 目录移除该虚拟模型,不会删除任何提供方。`,"cws.unsavedTitle":`未保存的更改`,"cws.unsavedDesc":`丢弃对此组合的编辑并继续?`,"cws.keepEditing":`继续编辑`,"cws.err.missingId":`需要组合 ID。`,"cws.err.invalidId":`ID 须以字母或数字开头,仅含字母、数字、点、下划线或连字符(最多 64)。`,"cws.err.duplicateId":`已存在相同 ID 的组合。`,"cws.err.invalidAlias":`别名仅可包含字母、数字、点、下划线或连字符,最多一个“/”分段。`,"cws.err.aliasReservedNamespace":`别名不得使用保留的“combo/”命名空间。`,"cws.err.aliasNativeFamily":`不允许使用 OpenAI 原生家族裸别名(gpt-*、o1-*、o3-*、o4-*、codex-*)。`,"cws.err.unsupportedNativeAlias":`原生别名必须是当前受支持的 OpenAI 裸 model id。`,"cws.err.missingNativeAliasDisplayName":`原生别名必须提供显示名称。`,"cws.err.invalidDisplayName":`显示名称最多 128 个字符,且不能包含控制字符。`,"cws.err.duplicateAlias":`另一个组合已使用该别名。`,"cws.err.noTargets":`至少添加一个目标。`,"cws.err.incompleteTarget":`每个目标都需要提供方和模型。`,"cws.target.disabled":`{name}(已禁用)`,"cws.err.reservedNamespace":`创建组合前,请先重命名名为 combo 的实体提供方。`,"cws.err.providerCollision":`组合 ID 与已配置的提供方名称冲突。`,"cws.err.unknownProvider":`每个目标都必须使用已配置的提供方。`,"cws.err.duplicateTarget":`同一提供方/模型目标只能出现一次。`,"cws.err.invalidStickyLimit":`粘性成功次数必须是 1 到 100 的整数。`,"cws.err.invalidWeight":`每个轮询权重必须是 1 到 10000 的整数。`,"cws.err.noEnabledTarget":`至少一个目标必须使用已启用的提供方。`,"claude.tabsLabel":`Claude 客户端`,"claude.tabCode":`Code`,"claude.tabDesktop":`Desktop`,"claudeDesktop.title":`Claude Desktop`,"claudeDesktop.subtitle":`将每个 Claude 模型系列路由到端口 {port} 上的可用模型。`,"claudeDesktop.importJson":`导入 JSON`,"claudeDesktop.exportJson":`导出 JSON`,"claudeDesktop.loading":`正在加载 Claude Desktop 配置…`,"claudeDesktop.loadFail":`无法加载 Claude Desktop 配置。`,"claudeDesktop.retry":`重试`,"claudeDesktop.saveFailed":`无法保存 Claude Desktop 配置。`,"claudeDesktop.applyFailed":`配置已保存,但无法应用。`,"claudeDesktop.updateFailed":`Claude Desktop 更新失败。`,"claudeDesktop.savedApplied":`配置已保存并应用到 Claude Desktop。`,"claudeDesktop.appliedMarkerUnsaved":`已应用到 Claude Desktop,但应用标记未能保存。在再次应用之前,下方的已保存/已应用状态可能显示不准确。`,"claudeDesktop.savedAppliedAnnounce":`Claude Desktop 配置已保存并应用。`,"claudeDesktop.saved":`配置已保存。`,"claudeDesktop.savedAnnounce":`Claude Desktop 配置已保存。`,"claudeDesktop.exported":`配置已导出为 JSON。`,"claudeDesktop.importExpected":`需要版本 1 的 Claude Desktop 配置。`,"claudeDesktop.importReady":`JSON 已导入。请检查草稿,然后保存并应用。`,"claudeDesktop.importedAnnounce":`配置 JSON 已导入。可检查尚未保存的更改。`,"claudeDesktop.importInvalid":`所选文件不是有效配置。`,"claudeDesktop.importFailed":`导入失败。{error}`,"claudeDesktop.moved":`已将 {route} 移动到 {family}。`,"claudeDesktop.unsaved":`有未保存的更改`,"claudeDesktop.upToDate":`配置已是最新`,"claudeDesktop.saving":`正在保存…`,"claudeDesktop.applying":`正在应用…`,"claudeDesktop.saveApply":`保存并应用`,"claudeDesktop.emptyTitle":`没有可用模型`,"claudeDesktop.emptyHint":`请添加或启用提供商,然后返回分配 Claude Desktop 路由。`,"claudeDesktop.assignmentsLabel":`Claude 模型系列分配`,"claudeDesktop.family.opus":`Opus`,"claudeDesktop.family.fable":`Fable`,"claudeDesktop.family.sonnet":`Sonnet`,"claudeDesktop.family.haiku":`Haiku`,"claudeDesktop.modelCountOne":`{count} 个模型`,"claudeDesktop.modelCountMany":`{count} 个模型`,"claudeDesktop.chooseDefault":`选择默认模型`,"claudeDesktop.temporaryDefault":`临时默认模型`,"claudeDesktop.laneEmpty":`将模型拖到这里,或使用移动控件。`,"claudeDesktop.laneNoMatch":`该系列中没有与搜索匹配的模型。`,"nav.grok":`Grok`,"grok.title":`Grok Build`,"grok.subtitle":`opencodex 已注册到你的 Grok 配置中的模型。`,"grok.loading":`正在加载 Grok 状态…`,"grok.loadFail":`无法读取 Grok 配置。`,"grok.notConfiguredTitle":`Grok Build 尚未接入`,"grok.notConfiguredHint":`安装 Grok 后重启代理,opencodex 会把托管块写入:`,"grok.endpoint":`端点`,"grok.colModel":`模型`,"grok.colAlias":`Grok 别名`,"grok.colContext":`上下文`,"grok.groupNative":`原生模型`,"grok.groupRouted":`路由模型`,"grok.enabledCount":`已注册 {on}/{total}`,"grok.saved":`选择已保存。`,"grok.savedApplied":`选择已保存并写入 Grok 配置。`,"grok.saveFailed":`无法保存 Grok 选择。`,"grok.applyFailed":`选择已保存,但无法更新 Grok 配置。`,"grok.applySkipped":`选择已保存,Grok 配置未更改。`,"grok.saveApply":`保存并应用`,"grok.saving":`保存中…`,"grok.applying":`应用中…`,"grok.unsaved":`未保存的更改`,"grok.upToDate":`选择已是最新`,"grok.toggleModel":`将 {id} 注册到 Grok`,"claudeDesktop.available":`可用`,"claudeDesktop.defaultBadge":`默认`,"claudeDesktop.supports1m":`1M`,"claudeDesktop.unavailable":`不可用`,"claudeDesktop.contextM":`{n}M 上下文`,"claudeDesktop.contextK":`{n}k 上下文`,"claudeDesktop.contextUnknown":`上下文未知`,"claudeDesktop.alias":`别名`,"claudeDesktop.useAsDefault":`设为 {family} 默认模型`,"claudeDesktop.moveTo":`移动到`,"claudeDesktop.move":`移动`,"claudeDesktop.status.applied":`已应用到 Desktop`,"claudeDesktop.status.stale":`配置已更改 — 需重新应用`,"claudeDesktop.status.notApplied":`未应用`,"claudeDesktop.status.notActiveProfile":`Desktop 正在使用其他配置 — 请重新应用`,"claudeDesktop.status.disabled":`Claude Desktop 集成已关闭。开启后请完全退出并重新打开 Desktop。`,"claudeDesktop.enableApply":`开启并应用`,"claudeDesktop.health.lastRequest":`最后请求`,"claudeDesktop.health.stats":`{count} 请求 / {errors} 错误`,"claudeDesktop.effort.supported":`effort`,"claudeDesktop.effort.displayOnly":`effort (仅显示)`,"dash.injectionManage":`打开设置`,"sub.settings":`设置`,"sub.sections":`子代理分区`,"sub.delegation.model":`优先调用的模型`,"sub.delegation.modelHint":`Codex 分派工作时最先调用的模型。上面的推荐是可调用的名单,这里选的是其中第一顺位。`,"dash.syncModelsHint":`按已连接的提供商重写 Codex 的模型目录。`,"dash.syncRun":`立即同步`,"lab.title":`Compatibility Lab`,"lab.subtitle":`Read-only compatibility verdict matrix from lab projection evidence.`,"lab.loadFailed":`Could not load compatibility lab data`,"lab.projectionUnavailable":`Lab projection is not available. Run conformance or live probes first.`,"lab.projectionIncompatible":`Lab projection schema is incompatible. Rebuild the projection.`,"lab.statusTitle":`Projection status`,"lab.matrixTitle":`Compatibility matrix`,"lab.verdictsTitle":`Verdict records`,"lab.filter.layer":`Evidence layer`,"lab.filter.verdict":`Verdict`,"lab.filter.subject":`Subject ID`,"lab.filter.all":`All`,"lab.col.subject":`Subject`,"lab.col.layer":`Layer`,"lab.col.suite":`Suite`,"lab.col.verdict":`Verdict`,"lab.col.asOf":`As of`,"lab.col.protocol":`Protocol conformance`,"lab.col.live":`Live route compatibility`,"lab.col.task":`Task effectiveness`,"lab.empty":`No compatibility verdicts in the projection yet.`,"lab.subjectKind":`Kind`,"lab.observationCount":`Observations`,"lab.eventCount":`Events`,"lab.verdictCount":`Verdicts`,"lab.subjectCount":`Subjects`,"lab.builtAt":`Built`,"lab.loading":`Loading compatibility evidence…`,"lab.loadMore":`Load more`,"lab.detailTitle":`Verdict detail`,"lab.detailClose":`Close`,"lab.detailSubject":`Subject`,"lab.detailObservations":`Observations`,"lab.detailEvents":`Contributing events`,"lab.detailArtifacts":`Artifact metadata`,"lab.production.title":`观测到的生产流量`,"lab.production.notVerification":`不是实验室验证`,"lab.production.attempts":`尝试`,"lab.production.successes":`成功`,"lab.production.routeErrors":`路由错误`,"lab.production.lastObserved":`最近观测`,"lab.detailLoadFailed":`Could not load verdict detail`,"lab.refresh":`Refresh`,"lab.verdict.UNKNOWN":`Unknown`,"lab.verdict.CLAIMED":`Claimed`,"lab.verdict.PROBED":`Probed`,"lab.verdict.VERIFIED":`Verified`,"lab.verdict.DEGRADED":`Degraded`,"lab.verdict.BLOCKED":`Blocked`,"lab.verdict.UNSUPPORTED":`Unsupported`,"lab.layer.protocol_conformance":`Protocol conformance`,"lab.layer.live_route_compatibility":`Live route compatibility`,"lab.layer.task_effectiveness":`Task effectiveness`,"dash.visionAdvanced":`高级设置`,"dash.visionMaxDescriptions":`每回合最大描述次数`,"dash.visionMaxDescriptionsInvalid":`请输入正整数。`,"dash.visionTimeout":`超时`,"dash.visionTimeoutInvalid":`请输入 {min} 到 {max} 毫秒之间的整数。`,"dash.visionAdvancedPopover":`高级视觉设置`,"models.newPolicyGlobal":`新模型默认停用`,"models.newPolicyProvider":`新模型策略`,"models.newPolicy_inherit":`继承`,"models.newPolicy_off":`关闭`,"models.newPolicy_on":`开启`,"models.newBadge":`新增`,"models.newCount":`{count} 个新增,已关闭`,"models.aliases":`别名`,"models.aliasesTable":`别名表`,"models.aliasPrompt":`服务商别名(留空即清除)`,"models.modelAliasPrompt":`模型别名(留空即清除)`,"models.aliasSaved":`别名已保存`,"models.aliasConflict":`该别名与现有名称冲突`,"models.editProviderAlias":`编辑服务商别名`,"models.editModelAlias":`编辑模型别名`,"models.useDefaultAliases":`使用默认别名`,"models.useDefaultAliasesGlobal":`全局使用默认别名`,"models.aliasAuto":`自动`,"models.aliasUser":`用户`,"models.aliasStale":`过期`,"connection.discovering":`Discovering local and shared targets…`,"connection.machineUnavailable":`The local machine plane is unavailable. Shared requests were not redirected locally.`,"connection.disconnect":`Disconnect from hub`,"connection.disconnectConfirm":`Disconnect this machine from the hub and restart it in standalone mode?`,"connection.pairing.title":`Connect this dashboard to the hub`,"connection.pairing.body":`Paste the one-time pairing code created on the hub.`,"connection.pairing.relayWarning":`This code is exchanged through the fixed hub relay. The relay cannot be redirected to another host.`,"connection.pairing.code":`One-time pairing code`,"connection.pairing.submit":`Connect`,"connection.pairing.submitting":`Connecting…`,"connection.pairing.error":`The pairing code was refused or expired. The code was left in place so you can check it.`,"connection.machine.title":`This machine`,"connection.machine.shimHealthy":`Codex shim is healthy.`,"connection.machine.shimNeedsAttention":`Codex shim needs attention.`,"connection.machine.repairShim":`Repair shim`,"connection.machine.removeShim":`Remove shim`,"connection.clients.title":`Connected clients`,"connection.clients.none":`No client status available`,"connection.clients.sync":`Sync now`,"connection.clients.syncing":`Syncing…`,"connection.sessionLogout":`退出远程会话`,"connection.sessionLoggingOut":`正在退出远程会话…`,"connection.sessionLogoutFailed":`无法退出远程会话,当前会话已保留。`,"usage.source.connected":`Source: hub usage`,"usage.source.local":`Source: local usage.jsonl`,"usage.scope.label":`Usage scope`,"usage.scope.machine":`This machine`,"usage.scope.hub":`Hub-wide`,"usage.hubOffline":`Hub usage is unavailable. Local usage was not substituted.`,"integrations.tab.cursor":`Cursor`,"integrations.detail.cursorSeen":`Cursor 最近调用了此代理`,"integrations.detail.cursorNeverSeen":`已安装 Private Inference;尚未收到请求`,"integrations.detail.cursorAbsent":`未找到 Cursor Private Inference`,"integrations.cursor.title":`Cursor`,"integrations.cursor.intro":`Cursor Private Inference 会在本地运行其智能体,并通过环回地址与 opencodex 通信。普通版 Cursor 无法如此工作:其后端会调用自定义端点,因此需要一个公开的 HTTPS URL。本页面不会向 Cursor 写入任何内容;请自行将以下值粘贴到 Cursor 中。`,"integrations.cursor.loading":`正在读取 Cursor 状态…`,"integrations.cursor.unavailable":`无法从代理读取 Cursor 状态。`,"integrations.cursor.detection":`已安装版本`,"integrations.cursor.privateInference":`Cursor Private Inference`,"integrations.cursor.regular":`Cursor(普通版)`,"integrations.cursor.detected":`已检测到`,"integrations.cursor.notFound":`未找到`,"integrations.cursor.regularOnly":`仅找到普通版 Cursor。它会通过 Cursor 的服务器路由自定义端点,因此如果没有公网隧道,便无法访问环回代理。有关 Private Inference 版本的信息,请参阅指南。`,"integrations.cursor.nothingFound":`在常用位置未找到 Cursor 安装。如果安装在其他位置,以下值仍然适用。`,"integrations.cursor.gateway":`网关参数`,"integrations.cursor.gatewayHint":`在 Cursor Private Inference 中打开 Settings > Models > Gateway,粘贴以下两个值,然后点击 Refresh model list。`,"integrations.cursor.baseUrl":`Base URL`,"integrations.cursor.apiKey":`API Key`,"integrations.cursor.apiKeyCredential":`你的任一 opencodex API 密钥(此绑定需要凭据)`,"integrations.cursor.copy":`复制`,"integrations.cursor.copied":`已复制`,"integrations.cursor.connection":`连接`,"integrations.cursor.seen":`Cursor 最近一次请求:{time}({ua})`,"integrations.cursor.neverSeen":`代理启动后尚未收到 Cursor 的请求。保存网关设置后,请在 Cursor 中点击 Refresh model list。`,"integrations.cursor.models":`Cursor 将显示的内容`,"integrations.cursor.modelsHint":`Cursor 会从自身的模型表中选择推理层级,因此 opencodex 只能进行预测。“上下文”列会列出默认窗口和可选窗口(Cursor 的 Max Mode)。`,"integrations.cursor.ladderFromBundle":`推理档位读取自已安装的 Cursor Private Inference {version} 包。档位由 Cursor 决定,opencodex 只是展示它的表。`,"integrations.cursor.ladderFromStatic":`推理档位是 Cursor 3.18.25 的静态镜像(未找到可读取的 Private Inference 包)。上下文列显示默认窗口和可选窗口。`,"integrations.cursor.unknownVersion":`未知版本`,"integrations.cursor.noControl":`—`,"integrations.cursor.singleWindow":`单一窗口`,"integrations.cursor.noControlTitle":`此 id 不在 Cursor 内置的 effort 表中,因此 Cursor 不显示推理控件。`,"integrations.cursor.effortRowsOne":`已发布 1 个 effort 行`,"integrations.cursor.effortRowsMany":`已发布 {n} 个 effort 行`,"integrations.cursor.effortRowsOff":`无 effort 行`,"integrations.cursor.tableLessHint":`标为 — 的行在 Cursor 中没有推理控件。开启 cursorEffortRows 可为每个 effort 发布一个选择器条目(id--effort),或在提供商上设置 modelDefaultReasoningEfforts 作为固定默认值。`,"integrations.cursor.colModel":`模型`,"integrations.cursor.colReasoning":`推理`,"integrations.cursor.colContext":`上下文`,"integrations.cursor.guide":`打开 Cursor Private Inference 指南`},Ue={"nav.dashboard":`儀表板`,"nav.startup":`啟動安全`,"nav.providers":`供應商`,"nav.models":`模型`,"nav.combos":`組合`,"nav.subagents":`子代理`,"nav.logs":`日誌與除錯`,"nav.usage":`用量`,"common.github":`GitHub`,"common.save":`儲存`,"common.saving":`儲存中…`,"common.cancel":`取消`,"common.discard":`捨棄`,"common.delete":`刪除`,"common.close":`關閉`,"common.ok":`確定`,"common.remove":`移除`,"common.loading":`載入中…`,"common.retry":`重試`,"app.logoAria":`opencodex 徽標`,"app.claudeOn":`Claude 開`,"app.claudeOff":`Claude 關`,"theme.label":`主題`,"theme.light":`淺色`,"theme.dark":`深色`,"theme.system":`跟隨系統`,"lang.label":`語言`,"errorBoundary.title":`頁面載入失敗`,"errorBoundary.message":`此部分在渲染時發生錯誤。請重新載入後再試。`,"errorBoundary.details":`錯誤`,"errorBoundary.reload":`重新載入`,"startup.title":`啟動安全`,"startup.subtitle":`檢查重新啟動後 Codex 是否仍能連線 opencodex,避免本機代理路由陷入重複重連。`,"startup.refresh":`重新整理`,"startup.loading":`正在檢查啟動保護…`,"startup.error":`無法讀取啟動保護狀態。`,"startup.staleData":`最新啟動檢查失敗。以下資料已過期,不能視為已受保護的證明。`,"startup.status.native":`原生路由`,"startup.status.protected":`已保護重新啟動`,"startup.status.atRisk":`需要處理`,"startup.summary.native":`Codex 不依賴本機代理`,"startup.summary.protected":`重新啟動後 opencodex 會自動可用`,"startup.summary.atRisk":`重新啟動後 Codex 可能無法存取模型`,"startup.riskDetail":`Codex 已指向本機代理,但沒有持久服務或正常的 launcher shim 將其重新啟動。`,"startup.riskDetailCustomLocal":`Codex 指向自訂本地閘道器。opencodex 無法管理或驗證該閘道器的重新啟動生命週期。`,"startup.riskDetailWindowsShim":`Launcher shim 僅保護受支援的 CLI 指令碼;Windows 上的 Codex Desktop 和直接 codex.exe 啟動可以繞過它。`,"startup.safeDetail":`當前路由與啟動機制一致。重新啟動後無需手動執行 ocx start。`,"startup.routing":`Codex 路由`,"startup.routing.proxy":`本機代理`,"startup.routing.native":`OpenAI 原生`,"startup.routing.customLocal":`自訂本地閘道器`,"startup.routing.customRemote":`自訂遠端閘道器`,"startup.routing.unknown":`未知或無效的路由`,"startup.restartProtection":`重新啟動保護`,"startup.preference":`按需啟動`,"startup.enabled":`已啟用`,"startup.disabled":`已停用`,"startup.protection.service":`背景服務`,"startup.protection.shim":`Launcher shim`,"startup.protection.none":`未安裝`,"startup.details":`保護詳細資料`,"startup.service":`背景服務`,"startup.serviceHint":`登入時啟動,並在代理崩潰後重新啟動。`,"startup.installed":`已安裝`,"startup.notInstalled":`未安裝`,"startup.unsupported":`不支援`,"startup.shim":`Codex launcher shim`,"startup.shimHint":`支援的 Codex 指令碼啟動器執行時執行 ocx ensure。`,"startup.healthy":`正常`,"startup.cliOnly":`僅 CLI`,"startup.stale":`已失效`,"startup.viable":`可用`,"startup.unhealthy":`已安裝但異常`,"startup.conflict":`服務衝突`,"startup.installedDisabled":`已安裝但停用`,"startup.install":`安裝`,"startup.installing":`正在安裝…`,"startup.serviceInstalled":`背景服務安裝成功。`,"startup.shimInstalled":`Codex 啟動器 shim 安裝成功。`,"startup.installFailed":`安裝失敗:`,"startup.tray.title":`Windows 系統托盤`,"startup.tray.hint":`登入時啟動托盤圖示,一鍵控制代理啟動、停止、重新啟動、面板和狀態。`,"startup.tray.login":`Windows 登入時啟動托盤`,"startup.tray.notProtection":`托盤只是控制器,並非重新啟動保護。無人值守恢復仍需要正常的背景服務。`,"startup.tray.running":`執行中`,"startup.tray.stopped":`已安裝,未顯示`,"startup.tray.stale":`需要修復`,"startup.tray.notInstalled":`未安裝`,"startup.tray.loading":`正在檢查…`,"startup.tray.unavailable":`狀態不可用`,"startup.tray.install":`安裝並顯示托盤`,"startup.tray.start":`顯示托盤圖示`,"startup.tray.stop":`退出托盤圖示`,"startup.tray.uninstall":`移除登入托盤`,"startup.tray.error":`Windows 托盤操作失敗。請執行 ocx tray status 檢視詳細資料。`,"startup.recovery":`修復選項`,"startup.recoveryHint":`使用上方的一鍵安裝,或複製命令進行手動修復。Codex Desktop 和 Windows 可執行檔案建議使用背景服務。`,"startup.command.service":`推薦:持久背景服務`,"startup.command.shim":`備選:CLI launcher shim`,"startup.command.native":`安全恢復:還原 Codex 原生路由`,"startup.copy":`複製`,"startup.copied":`已複製`,"startup.recommended":`推薦修復:{cmd}`,"startup.navRisk":`啟動保護需要處理`,"startup.codexRuntime.clampHidden":`部分推理強度選項已隱藏,因為 OpenCodex 正在使用 Codex {version}。`,"startup.codexRuntime.clampHiddenWithEfforts":`部分推理強度選項已隱藏,因為 OpenCodex 正在使用 Codex {version}(已移除:{efforts})。`,"startup.codexRuntime.olderBinary":`OpenCodex 正在使用較舊的 Codex 二進位制檔案({version})。檢測到可用的較新安裝。`,"dash.subtitle":`本地 opencodex 代理、其供應商以及路由到 Codex 的模型的即時狀態。`,"dash.workspace.overview":`總覽`,"dash.workspace.sections":`板塊`,"dash.status":`狀態`,"dash.online":`線上`,"dash.offline":`離線`,"dash.version":`版本`,"dash.uptime":`執行時間`,"dash.providers":`供應商`,"dash.tokens30d":`Token (30 天)`,"dash.coverage":`覆蓋率 {pct}`,"dash.mem.title":`記憶體可觀測性`,"dash.mem.hint":`只讀執行時診斷。觀測記憶體為 max(RSS, external, ArrayBuffers),避免 Windows working set trimming 隱藏已提交的保留記憶體。`,"dash.mem.rss":`常駐記憶體 (RSS)`,"dash.mem.jsHeap":`JS 堆(已用 / 總計)`,"dash.mem.jscHeap":`JSC 堆`,"dash.mem.external":`External`,"dash.mem.arrayBuffers":`ArrayBuffers`,"dash.mem.observed":`觀測值`,"dash.mem.runtime":`執行時計數器`,"dash.mem.growth":`每小時觀測變化`,"dash.mem.perHour":`/小時`,"dash.mem.store":`延續儲存`,"dash.mem.storeHint":`代理 previous_response_id 快取。堆上升時總位元組數增加,說明是對話保留而非執行時分配器。`,"dash.mem.storeEntries":`條目`,"dash.mem.storeTotal":`總計`,"dash.mem.storeLargest":`最大`,"dash.mem.storeOldest":`最舊`,"dash.mem.threshold":`告警閾值`,"dash.mem.lastWarn":`上次告警`,"dash.mem.never":`從不`,"dash.mem.details":`詳細資料`,"dash.mem.unavailable":`記憶體診斷不可用(舊版代理)。`,"dash.mem.inFlight":`進行中的請求`,"dash.mem.restart":`排空並重啟`,"dash.mem.restartConfirm":`等待 {count} 個進行中的請求結束後再重啟(最多 {seconds} 秒;超時將中斷剩餘請求)。`,"dash.mem.draining":`正在等待 {count} 個請求完成… 完成後重啟`,"dash.mem.reconnecting":`代理正在重啟… 等待重新連線`,"dash.mem.restartFailed":`排空並重啟失敗。請確認代理正在執行。`,"dash.mem.restartNoSupervisor":`未檢測到重啟保護。重啟後代理可能不會自動恢復,需手動啟動。`,"dash.activeProviders":`活躍供應商`,"dash.noProviders":`尚未配置供應商。請執行 {cmd}。`,"dash.col.name":`名稱`,"dash.col.adapter":`介面卡`,"dash.col.baseUrl":`Base URL`,"dash.col.model":`模型`,"dash.modelsNoResults":`沒有符合搜尋的模型。`,"dash.availableModels":`可用模型`,"dash.noModels":`未找到模型。請檢查供應商 API 金鑰。`,"dash.cannotConnect":`無法連線到代理。它在執行嗎?`,"dash.runStart":`執行 {cmd} 以啟動代理。`,"dash.stop":`停止代理`,"dash.stopConfirm":`停止代理並恢復原生 Codex 配置?`,"dash.stopFailed":`無法停止代理 (HTTP {status})。`,"dash.maSwitchFailed":`模式切換失敗 (HTTP {status})。`,"dash.maNetworkError":`網路錯誤 — 代理是否正在執行?`,"dash.stopping":`正在停止…`,"dash.actions":`代理`,"dash.codexRestart":`重新載入 Codex 模型清單`,"dash.codexRestarting":`正在停止…`,"dash.codexRestartConfirm":`停止 Codex app-server 以重新讀取模型清單?進行中的 Codex 工作會中斷,且 Codex 不會自動重啟,請稍後自行重新開啟。`,"dash.codexRestartDone":`已停止 {count} 個 Codex app-server。重新開啟 Codex 即可載入最新的模型清單。`,"dash.codexRestartNothing":`沒有執行中的 Codex app-server。下次啟動會讀取最新的模型清單。`,"dash.codexRestartUnknown":`無法列舉行程,因此沒有停止任何行程。`,"dash.codexRestartPartial":`有 {count} 個 app-server 未結束。若模型清單仍然過舊,請手動停止。`,"dash.codexRestartFailed":`無法重新載入 Codex 模型清單 (HTTP {status})。`,"dash.codexRestartUnreachable":`無法連線到代理。`,"dash.codexRestartMalformed":`代理回傳了非預期的回應。`,"dash.codexRestartTimeout":`代理未在時限內回應,可能仍在停止 app-server。`,"models.staleBanner":`Codex 顯示的模型清單比目前的目錄舊。重新啟動 Codex 即可重新讀取。`,"dash.codexAutoStart":`隨 Codex 啟動 opencodex`,"dash.codexAutoStartHint":`允許已安裝的 launcher shim 執行 ocx ensure。此設定不會安裝重新啟動保護;請在啟動安全中檢查實際狀態。`,"dash.searchModel":`搜尋附屬模型`,"dash.searchModelHint":`用於非 OpenAI 路由模型的 web_search 的模型。需要 ChatGPT 登入。`,"dash.searchReasoning":`搜尋推理強度`,"dash.visionModel":`視覺附屬模型`,"dash.visionModelHint":`為純文字路由模型描述圖像的模型。需要 ChatGPT 登入。`,"dash.webSearchSidecar":`網頁搜尋附屬服務`,"dash.webSearchSidecarHint":`選擇路由模型進行網頁搜尋時使用的後端和模型。`,"dash.webSearchStream":`即時串流輸出回答`,"dash.webSearchStreamHint":`即時串流輸出開頭的文字和推理,直到模型決定呼叫工具;其餘部分為攔截搜尋而保持緩衝。搜尋前的文字可能會部分重複。`,"dash.visionSidecar":`視覺附屬服務`,"dash.visionSidecarHint":`選擇純文字路由模型描述圖像時使用的後端和模型。`,"dash.visionOff":`關閉`,"dash.shadowCallIntercept":`影子呼叫攔截`,"dash.shadowCallInterceptHint":`攔截 Codex 應用的背景 helper 呼叫({models})以生成標題與提交訊息,並將它們重定向到您選擇的模型。`,"dash.shadowCallWarning":`⚠ 啟用後,{models} 的所有請求將被替換為所選模型。`,"dash.shadowCallOriginal":`原始`,"dash.shadowCallModel":`替代模型`,"dash.shadowCallTooltip":`Codex 應用會為執行緒標題生成、提交訊息生成與技能編排發出背景 helper 呼叫。helper 模型因客戶端版本而異,因此 opencodex 攔截此集合中的每個模型:{models}。啟用此選項可將這些呼叫重定向到您選擇的模型。`,"models.shadowCallIntercept":`影子呼叫攔截`,"models.shadowCallInterceptHint":`攔截 Codex 應用的背景 helper 呼叫({models})以生成標題與提交訊息,並將它們重定向到您選擇的模型。`,"dash.sidecarBackend":`後端`,"dash.sidecarModel":`模型`,"dash.backendAuto":`自動`,"dash.backendOpenAI":`OpenAI`,"dash.backendAnthropic":`Anthropic`,"dash.sidecarSaved":`附屬設定已儲存。將在下一個請求時生效。`,"dash.sidecarSaveFailed":`儲存附屬設定失敗。`,"dash.injectionLabel":`子代理委託`,"dash.injectionHint":`選擇供下方兩個控制元件共用的模型和可選推理強度。`,"dash.syncCodexSubagentDefaults":`用作原生 Codex 子代理預設值`,"dash.syncCodexSubagentDefaultsHint":`預設關閉。當 OpenCodex 管理 Codex 路由時,同步或重啟會將所選模型和推理強度應用為新 Codex 任務的原生 Codex [agents] 預設值。此設定本身不會觸發委託;現有的使用者自有 [agents] 預設值會保留而不會被覆蓋。`,"dash.multiAgentGuidance":`OpenCodex 多代理指引`,"dash.multiAgentGuidanceHint":`新增由 OpenCodex 編寫的委託指引。它與上方的原生 Codex 預設值相互獨立,不會更改 v1/v2 介面、子代理清單、路由或 effort 上限。`,"dash.injectionNone":`無`,"dash.injectionEffortLabel":`推理強度`,"dash.injectionEffortNone":`模型預設`,"dash.effortCapLabel":`V2 ultra 推理強度限制`,"dash.subagentEffortCapLabel":`V2 子代理推理強度限制`,"dash.effortCapHelp":`限制 V2 ultra 模式輪次的推理強度。設定後,來自 ultra 模式的 max 請求將被限制到所選級別。子代理限制僅適用於衍生的子代理。只會降低強度,不會提高。如果模型不支援所選級別,將自動降至最近的支援級別。`,"dash.effortCapNone":`無上限`,"dash.maintenance":`維護`,"dash.maintenanceHint":`重新整理 Codex 模型目錄,或安裝新的 opencodex 版本。`,"dash.syncModels":`同步模型`,"dash.syncing":`同步中…`,"dash.syncOk":`同步完成。已追加 {count} 個模型。`,"dash.syncStaleHint":`如果 Codex 仍顯示舊列表,請重啟長期執行的 app-server({cmd})。`,"dash.syncFailed":`同步失敗:{error}`,"dash.projectConfigTitle":`專案 Codex 配置繞過了 OpenCodex`,"dash.projectConfigHint":`這些儲存庫級設定會覆蓋 OpenCodex 代理(例如直接走 OpenCode Go)。請移除它們,以便該專案使用 ~/.codex/config.toml 的代理路由。`,"dash.checkUpdate":`檢查更新`,"dash.updateTitle":`更新 opencodex`,"dash.updateDesc":`檢查所選 npm 頻道的最新版本,然後選擇安裝後是否重新啟動代理。`,"dash.updateChannel":`頻道`,"dash.updateChecking":`正在檢查更新…`,"dash.updateInstalled":`已安裝`,"dash.updateLatest":`最新`,"dash.updateAvailable":`有可用更新`,"dash.updateCurrent":`已是最新`,"dash.updateCommand":`命令`,"dash.updateSource":`當前是原始碼檢出。請在終端執行顯示的命令進行更新。`,"dash.updateUnavailable":`無法從 npm 讀取最新版本。請稍後重試。`,"dash.updateRetry":`重試`,"dash.updateRecheck":`重新檢查`,"dash.updateCannotAuto":`無法一鍵更新({reason})。`,"dash.updateReason.source_checkout":`原始碼檢出`,"dash.updateReason.latest_unavailable":`無法連線 npm 登入檔`,"dash.updateReason.already_latest":`已是最新版本`,"dash.updateReason.unknown":`無法更新`,"dash.updateRestart":`更新後重新啟動`,"dash.updateRestartHint":`推薦開啟。代理重新啟動前,當前 GUI 仍執行舊程式碼。`,"dash.runUpdate":`更新`,"dash.updateReconnecting":`正在等待重新啟動後的代理…`,"dash.updateStatus.running":`正在更新 opencodex。`,"dash.updateStatus.restarting":`更新已安裝。正在重新啟動代理。`,"dash.updateStatus.succeeded":`更新完成。`,"dash.updateStatus.failed":`更新失敗。`,"prov.subtitle":`配置 opencodex 路由到 Codex 的上游供應商。使用帳號登入、新增供應商,或編輯原始配置。`,"prov.add":`新增供應商`,"prov.editJson":`編輯 JSON`,"prov.accountLogin":`帳號登入`,"prov.noOauth":`沒有可用的 OAuth 供應商。`,"prov.loggedIn":`已登入`,"prov.notLoggedIn":`未登入`,"prov.logout":`登出`,"prov.login":`登入`,"prov.loginWith":`使用 {provider} 登入`,"prov.waitingBrowser":`等待瀏覽器…`,"prov.didntOpen":`沒有開啟?點選這裡`,"prov.copyLink":`複製連結`,"prov.dontOpenBrowser":`不要在執行代理的機器上開啟瀏覽器`,"prov.dontOpenBrowserHint":`適用於使用其他瀏覽器設定檔登入,或儀表板與代理不在同一台機器上。`,"prov.linkCopied":`已複製`,"prov.linkCopyUnavailable":`剪貼簿不可用`,"prov.deviceCode":`裝置驗證碼`,"prov.copyCode":`複製驗證碼`,"prov.codeCopied":`驗證碼已複製`,"prov.editAlias":`編輯別名`,"prov.aliasPrompt":`顯示名稱(留空以清除)`,"prov.aliasSaved":`別名已儲存`,"prov.aliasSaveFailed":`無法儲存別名`,"prov.accountId":`ID`,"prov.pasteRedirect":`貼上重定向 URL 或授權碼`,"prov.pasteRedirectHint":`如果瀏覽器顯示 localhost 錯誤,請複製位址列中的完整 URL 並貼上到此處(或貼上授權碼)。`,"prov.pasteSubmit":`提交`,"prov.pasteSubmitting":`提交中…`,"prov.pasteOk":`已提交授權碼 — 正在完成登入…`,"prov.pasteFail":`無法提交授權碼:{error}`,"prov.port":`連接埠`,"prov.default":`預設`,"prov.loadingConfig":`載入中…`,"prov.saved":`已儲存!重新啟動代理以生效。`,"prov.loadConfigFail":`載入配置失敗`,"prov.invalidJson":`無效的 JSON`,"prov.removedDefault":`已移除「{name}」。預設供應商現在是「{defaultProvider}」。`,"prov.removeLastProvider":`沒有其他啟用的供應商可接手預設值時,無法移除此供應商。`,"prov.removeHasDependentCombos":`請先移除或更新這些相依的組合路由:{combos}。`,"prov.setDefault":`設為預設`,"prov.setDefaultSuccess":`「{name}」現在是預設供應商。`,"prov.setDefaultFail":`無法將「{name}」設為預設供應商。`,"prov.defaultDisabled":`設為預設前請先啟用此供應商。`,"prov.updateFail":`無法更新此供應商。`,"prov.networkError":`網路錯誤。請確認代理正在執行後再試一次。`,"prov.saveFailed":`儲存失敗`,"prov.loginFailStart":`{provider} 登入啟動失敗`,"prov.loginError":`{provider} 登入錯誤:{error}`,"prov.loginRequestFail":`{provider} 登入請求失敗`,"prov.loginCancelled":`{provider} 登入已取消`,"prov.loginTimeout":`{provider} 登入逾時 — 瀏覽器已關閉或未完成。請重試。`,"prov.loginOk":`已登入到 {provider}。執行 {cmd}(或即時生效)以列出其模型。`,"oauthTos.highTitle":`{provider}:訂閱 OAuth 風險`,"oauthTos.elevatedTitle":`{provider}:非官方 OAuth 橋接`,"oauthTos.anthropicBody":`透過 OpenCodex 等第三方代理直接複用 Claude 訂閱 OAuth 權杖,並非 Anthropic 支援的整合方式,可能導致存取受限。可使用 Claude 訂閱的受支援 Agent SDK 整合屬於另一種方式。`,"oauthTos.highBody":`OpenCodex 透過第三方 OAuth 路徑連線 {provider}。如果該用法不受支援,存取可能會被限制或暫停。`,"oauthTos.elevatedBody":`OpenCodex 透過非官方 OAuth 路徑連線 {provider}。請儘量使用官方客戶端;異常或自動化流量可能被視為濫用,存取可能會被限制或暫停。`,"oauthTos.saferPath":`更安全的做法:改為在 OpenCodex 中配置 API 金鑰。`,"oauthTos.acknowledge":`我瞭解風險,仍要繼續使用 OAuth。`,"oauthTos.continue":`繼續使用 OAuth`,"prov.logoutOk":`已登出 {provider}。`,"prov.logoutFail":`無法登出 {provider}。帳號狀態保持不變。`,"prov.removed":`已移除 "{name}"。`,"prov.removeFail":`移除 "{name}" 失敗。`,"prov.added":`已新增 "{name}"。現已生效 — 執行 {cmd}(或重新啟動)以在 Codex 選擇器中列出其模型。`,"prov.removeConfirm":`移除供應商 "{name}"?其模型將從 Codex 選擇器中消失。`,"prov.hasApiKey":`已配置 API 金鑰`,"prov.hasHeaders":`已配置自訂請求標頭`,"prov.accounts":`帳號({n})`,"prov.accountsAria":`展開/收起 {name} 帳號`,"prov.accountActive":`使用中`,"prov.accountReauth":`需重新登入`,"prov.reauthenticate":`重新認證`,"prov.reauthAccountMissing":`登入後未找到所選帳號`,"prov.reauthIdentityMismatch":`登入帳號與所選帳號不符合`,"prov.accountAdd":`新增帳號`,"prov.accountNoLabel":`帳號 {id}`,"prov.accountSwitchTitle":`使用此帳號`,"prov.accountSwitched":`已切換到 {email}。`,"prov.accountSwitchFail":`切換帳號失敗`,"prov.accountRemoved":`已移除 {email}。`,"prov.accountRemoveFail":`無法移除 {email}。帳號保持不變。`,"prov.accountRemoveAria":`移除 {email}`,"prov.accountRemoveConfirm":`移除帳號 {email}?其登入將從此代理中刪除。`,"prov.keyAdd":`新增 API 金鑰`,"prov.keyAdded":`已為 {name} 新增 API 金鑰。`,"prov.keyAddFail":`新增 API 金鑰失敗`,"prov.keyPlaceholder":`貼上 API 金鑰`,"prov.keySwitchTitle":`使用此金鑰`,"prov.keySwitched":`已切換到金鑰 {key}。`,"prov.keySwitchFail":`切換金鑰失敗`,"prov.keyRemoved":`已移除金鑰 {key}。`,"prov.keyRemoveAria":`移除金鑰 {key}`,"prov.keyRemoveConfirm":`移除 API 金鑰 {key}?它將從此代理的配置中刪除。`,"prov.activeBadge":`已啟用`,"prov.disabledBadge":`已停用`,"prov.defaultBadge":`預設`,"prov.enable":`啟用`,"prov.disable":`停用`,"prov.enabled":`已啟用 "{name}"。其模型可再次出現在 Codex 中。`,"prov.disabled":`已停用 "{name}"。設定會保留,但模型會被隱藏。`,"prov.enableFail":`啟用 "{name}" 失敗。`,"prov.disableFail":`停用 "{name}" 失敗。`,"prov.enableAria":`啟用供應商 {name}`,"prov.disableAria":`停用供應商 {name}`,"prov.defaultCannotDisable":`預設供應商不能被停用`,"prov.openaiAccountMode":`Codex 帳號模式`,"prov.openaiModePool":`帳號池`,"prov.openaiModeDirect":`直連`,"prov.openaiPoolDesc":`預設模式。根據會話關聯、額度、冷卻時間和容錯移轉,在主登入與已新增帳號之間輪換。`,"prov.openaiDirectDesc":`僅使用當前主 Codex 登入。不會讀取或輪換已儲存的帳號池帳號。`,"prov.openaiModeSaved":`OpenAI 帳號模式已更改為 {mode}。`,"prov.openaiModeSaveFailed":`無法更改 OpenAI 帳號模式。`,"prov.openaiApiDesc":`僅使用 OpenAI API 金鑰,不使用 Codex 帳號憑證。`,"prov.manageCodexAccounts":`管理 Codex 帳號`,"prov.openaiApiMissing":`需要 API 金鑰`,"prov.openaiApiSetup":`設定 API 金鑰`,"models.subtitle":`開關 Codex 可見的模型 — 原生 GPT passthrough 與已路由模型按供應商分組(點選標題可摺疊)。已停用的模型會從目錄和模型選擇器中隱藏。更改在下一個 Codex 回合生效 — opencodex 會使 Codex 的 5 分鐘模型快取失效,因此無需重新啟動。`,"models.nativeGroupLabel":`OpenAI 原生`,"models.nativeHint":"Passthrough 模型使用在供應商頁面選擇的帳號池或直連選項。關閉後會從 Codex 選擇器中隱藏(目錄條目保留,重新開啟即可完整恢復)。 在此新增模型會註冊為路由的 `openai/` 選擇器,而非新的裸 passthrough id。","models.active":`{active}/{total} 已啟用`,"models.workspace.providers":`供應商`,"models.workspace.allProviders":`所有供應商`,"models.workspace.mainAria":`模型詳細資料`,"models.allOn":`全部開啟`,"models.allOff":`全部關閉`,"models.presetLabel":`模型`,"models.presetMode_preset":`預設集`,"models.presetMode_all":`全部`,"models.presetMode_custom":`自訂`,"models.presetSummary":`顯示 {count} / {total} — 核心預設集 v{version}`,"models.presetUpdateAvailable":`預設集 v{version} 可用`,"models.presetAppliedToast":`{provider}:已套用預設集 — 選取 {count} 個模型`,"models.presetClearedToast":`{provider}:顯示全部模型`,"models.presetEmpty":`{provider}:預設集未比對到模型,選擇維持不變`,"models.presetConfirmReplace":`以包含 {count} 個模型的預設集取代你的選擇?`,"models.cap350k":`限制 350k`,"models.capApplied":`上下文限制已套用 — 將在下一個 Codex 回合生效。`,"models.capSaveFailed":`儲存上下文限制失敗`,"models.contextCapped":`350k 限制`,"models.contextCapLabel":`預設視窗 / 上限`,"models.v2Label":`子代理`,"models.shadowCallOriginal":`⚠ {models} →`,"models.v2DocsLink":`v1 / v2 是什麼?`,"models.v2Mode_v1":`v1`,"models.v2Mode_default":`base`,"models.v2Mode_v2":`v2`,"models.v2ModeDesc_v1":`所有模型 → v1 介面`,"models.v2ModeDesc_default":`上游預設值 (sol/terra=v2, luna=v1)`,"models.v2ModeDesc_v2":`所有模型 → v2 介面`,"models.keepNativeOnV1":`ChatGPT 維持 v1`,"models.keepNativeOnV1Hint":`ChatGPT 原生父代理會加密 v2 子任務,Grok/Claude 無法讀取。若 Sol/Terra 仍需派發路由模型,請保持開啟。路由父代理仍使用 v2。`,"models.v2Help":`控制所有模型的多代理介面。 - -v1: 經典單執行緒代理。所有模型使用 v1 協作介面。 -base: 上游預設值 — sol/terra 使用 v2,luna 使用 v1,其餘跟隨 codex 功能標誌。 -v2: 多執行緒代理(spawn_agent)。所有模型使用 v2 協作介面。 - -在 v2 下,「ChatGPT 維持 v1」會讓 Sol/Terra 留在 v1,以便繼續派發 Grok 或 Claude。ChatGPT 會加密 v2 子任務,路由模型無法讀取;路由父代理仍留在 v2。 - -更改在新會話中生效。`,"dash.multiAgent":`子代理`,"models.v2Conflict":`[agents] max_threads 仍存在 — codex 將拒絕啟動,請從 config.toml 移除`,"models.v2Applied":`子代理模式已更新 — 新會話生效(重新啟動 Codex 應用以重新整理選擇器)`,"models.v2ThreadsLabel":`最大執行緒`,"models.v2ThreadsDefault":`預設 (4)`,"models.v2ThreadsApplied":`執行緒上限已更新 — 新會話生效`,"models.v2ThreadsInvalid":`執行緒上限必須為 >= 1 的整數`,"models.v2ThreadsApply":`套用`,"models.capValue":`預設 {value}`,"models.contextCappedValue":`{value} 限制`,"models.setAll":`全部設定`,"models.setAllHint":`為所有已路由供應商打開 {value} 預設視窗。中繼站沒回報 context_window / context_length 時,這個值就是 Codex 實際視窗。要幫單一模型手寫,用同一列上的「自訂視窗」。原生供應商不受影響。`,"models.collapseAll":`全部摺疊`,"models.expandAll":`全部展開`,"models.orderHint":`選擇器順序:Subagents 中的選擇(按所選順序)→ 其餘已路由模型(依次按供應商、模型 ID 字母排序)→ 原生模型。可見性開關僅用於篩選,不會改變此順序。`,"models.custom":`自訂…`,"models.customApply":`套用`,"models.customPlaceholder":`tokens (例如 420000)`,"models.customAdd":`新增自訂模型`,"models.customAddTitle":`新增自訂模型 — {provider}`,"models.customEditTitle":`編輯自訂模型 — {provider}`,"models.customAdded":`已新增自訂模型`,"models.customUpdated":`已更新自訂模型`,"models.customDeleted":`已刪除自訂模型`,"models.customSaveFailed":`儲存自訂模型失敗`,"models.customSaving":`正在儲存…`,"models.customAddBtn":`新增`,"models.customEditBtn":`更新`,"models.customEdit":`編輯`,"models.customDelete":`刪除`,"models.customDeleteConfirm":`要刪除模型 {name} 嗎?`,"models.customBadge":`自訂`,"models.customSummary":`{count} 個自訂模型`,"models.customFieldModelId":`模型 ID(端點標識)`,"models.customFieldModelIdPlaceholder":`例如 qwen4-max-preview`,"models.customFieldDisplayName":`顯示名稱(可選)`,"models.customFieldDisplayNamePlaceholder":`例如 Qwen 4 Max Preview`,"models.customFieldContext":`上下文視窗`,"models.customFieldModalities":`輸入模態`,"models.customFieldReasoning":`推理強度`,"models.customFieldReasoningOverride":`覆寫推理強度`,"models.reasoningEffort.none":`無`,"models.reasoningEffort.minimal":`最低`,"models.reasoningEffort.low":`低`,"models.reasoningEffort.medium":`中`,"models.reasoningEffort.high":`高`,"models.reasoningEffort.xhigh":`極高`,"models.reasoningEffort.max":`最高`,"models.tipProvider":`供應商`,"models.tipContext":`上下文`,"models.tipModalities":`模態`,"models.tipStatus":`狀態`,"models.tipActive":`已啟用`,"models.tipDisabled":`已停用`,"models.applied":`已套用 — 將在下一個 Codex 回合生效。`,"models.saveFailed":`儲存失敗`,"models.networkError":`網路錯誤 — 代理在執行嗎?`,"models.loadFail":`載入模型失敗 — 代理在執行嗎?`,"models.noRouted":`沒有已路由的模型`,"models.noRoutedHint":`請先登入供應商或新增一個。`,"models.emptyDiscovery":`未發現任何模型。請檢查供應商端點,或新增靜態/自訂模型。`,"models.emptyDiscoveryDisabled":`即時模型發現已關閉,且尚未配置靜態模型。`,"models.discoveryFailedBadge":`發現失敗`,"models.discoveryFailedHttp":`模型發現失敗(HTTP {status})。`,"models.discoveryFailedBlocked":`模型發現被目標策略阻止。`,"models.discoveryFailedInvalidResponse":`模型發現返回了無效回應。`,"models.discoveryFailedNetwork":`由於網路錯誤,模型發現失敗。`,"models.discoveryFailedProvider":`供應商報告了模型發現錯誤。`,"models.discoveryFailedGeneric":`模型發現失敗。`,"models.openProviderSettings":`開啟供應商設定`,"models.loading":`載入中…`,"models.search":`搜尋模型…`,"models.showMore":`再顯示 {n} 個`,"models.allowlistLabel":`僅所選`,"models.allowlistHint":`僅勾選的模型進入目錄(留空 = 全部)。適用於暴露成千上萬模型的供應商。`,"models.selectedCount":`已選 {n} 個`,"sub.subtitle":`Codex 的 {cmd} 僅將優先順序最高的前 5 個模型作為覆蓋項公開。在此最多選擇 5 個 — 原生 gpt 或已路由模型 — opencodex 會設定它們的目錄優先順序,使其正好排在前面。其他模型仍可按確切名稱呼叫;此設定僅控制顯示項。`,"sub.featured":`精選`,"sub.advanced":`進階`,"sub.orderHintAria":`此順序的用途`,"sub.orderHint":`此處所選並顯示的順序決定 Codex 模型選擇器頂部第 1–5 位,以及 {cmd} 的預設模型候選。`,"sub.noneSelected":`未選擇 — 請從下方列表選擇。`,"sub.models":`模型`,"sub.search":`搜尋模型(原生 gpt + 已路由)…`,"sub.noModels":`沒有模型 — 請先登入供應商或新增一個。`,"sub.saved":`已儲存 {n} 個模型。啟動新的 Codex 會話(或執行 {cmd})以將它們作為 spawn_agent 覆蓋項檢視。`,"sub.saveFailed":`儲存失敗`,"sub.networkError":`網路錯誤 — 代理在執行嗎?`,"sub.loadFail":`載入模型失敗 — 代理在執行嗎?`,"sub.loading":`載入中…`,"sub.moveUp":`上移 {m}`,"sub.moveDown":`下移 {m}`,"sub.removeAria":`移除 {m}`,"sub.ultraMode":`超級模式`,"sub.ultraModeHint":`為所有模型和推理力度啟用主動多代理委派策略(不改變推理力度本身)。將 features.multi_agent_v2.multi_agent_mode_hint_text 寫入 config.toml。`,"sub.ultraModeV2Required":`需要 v2 多代理表面 — 請先啟用 multi_agent_v2,並在子代理模式控制項中選擇 v2。`,"sub.ultraModeText":`超級模式委派文字`,"sub.ultraModePreset":`還原預設`,"sub.ultraModeLoadFail":`無法載入超級模式設定 — 代理是否在執行?`,"sub.ultraModeSaveFail":`儲存超級模式設定失敗`,"sub.ultraModeSaved":`超級模式已儲存。適用於新的 Codex 會話。`,"logs.title":`請求日誌`,"logs.tabLogs":`日誌`,"logs.tabDebug":`除錯`,"logs.subtitle":`經過本地 opencodex 代理的最近請求,最新在前。`,"logs.autoRefresh":`自動重新整理`,"logs.noRequests":`暫無請求。`,"logs.loadError":`無法載入請求日誌。`,"logs.filter.surface.label":`介面`,"logs.filter.surface.all":`全部`,"logs.filter.surface.claude":`Claude`,"logs.filter.surface.codex":`Codex`,"logs.filter.surface.grok":`Grok`,"logs.filter.interceptedHelpersOnly":`僅已攔截的輔助請求`,"logs.badge.interceptedHelper":`I · {model}`,"logs.badge.interceptedHelperTitle":`已攔截的輔助請求`,"logs.filter.conversation.label":`對話`,"logs.filter.conversation.placeholder":`貼上對話 ID`,"logs.filter.conversation.clear":`清除`,"logs.filter.model.label":`模型`,"logs.filter.model.placeholder":`依模型或供應商篩選`,"logs.filter.conversation.apply":`篩選日誌`,"logs.conversation.totals":`{requests} 次請求 · {tokens} tokens · {cost}`,"logs.conversation.scope":`合計僅涵蓋目前已載入的 Logs 環形緩衝。`,"logs.conversation.excluded":`(~$ 已排除 {unpriced} 筆無定價、{unmetered} 筆無計量)`,"logs.cost.approximate":`{amount}`,"logs.cost.lowerBound":`≥{amount}`,"logs.cost.unavailable":`無法估算`,"logs.detail.conversation":`對話`,"logs.badge.claude":`Claude`,"logs.col.time":`時間`,"logs.col.request":`請求`,"logs.col.model":`模型`,"logs.col.effort":`推理強度`,"logs.col.provider":`供應商`,"logs.col.status":`狀態`,"logs.col.tokens":`Token 數`,"logs.col.tokPerSec":`tok/s`,"logs.col.estimatedCost":`~$`,"logs.metric.tokPerSecTitle":`按完整請求耗時計算的每秒輸出 token`,"logs.metric.estimatedCostTitle":`按 API 標價估算,並非實際扣費;價格無法符合時不顯示`,"usage.cost.total":`API 標價折算(當前範圍)`,"usage.cost.disclaimer":`這不是帳單或扣費憑證。實際可能計入訂閱用量或消耗服務商額度。`,"usage.cost.unpricedNote":`已排除 {count} 個無法計費的請求`,"logs.detail.section.basic":`基本資訊`,"logs.detail.section.performance":`效能`,"logs.detail.section.cost":`API 標價折算`,"logs.detail.section.attempts":`Combo 嘗試`,"logs.detail.section.usage":`原始 usage`,"logs.detail.ttft":`TTFT`,"logs.detail.costTotal":`標價折算`,"logs.detail.totalTokens":`Token 總數`,"logs.detail.matchedKey":`符合的 jawcode 鍵`,"logs.detail.priceSource":`價格來源`,"logs.detail.unavailableReason":`不可用原因`,"logs.detail.copyRequestId":`複製請求 ID`,"logs.detail.copied":`已複製`,"logs.detail.source.jawcode":`jawcode 目錄`,"logs.detail.source.expected":`Expected 價格覆蓋`,"logs.detail.verification.verified":`已驗證`,"logs.detail.verification.derived":`由基礎模型推導`,"logs.detail.attempt.target":`供應商 / 模型`,"logs.detail.attempt.reason":`結果 / 原因`,"logs.detail.attempt.completed":`已完成`,"logs.detail.attempt.e2eNote":`頂層 tok/s 為端到端值;每次嘗試使用各自耗時。`,"logs.detail.reason.usage_missing":`未上報 usage。`,"logs.detail.reason.usage_unsupported":`該供應商不支援上報 usage。`,"logs.detail.reason.output_missing":`未上報正數輸出 token。`,"logs.detail.reason.invalid_duration":`請求耗時無效。`,"logs.detail.reason.price_unmatched":`未找到符合的 jawcode 價格。`,"logs.detail.reason.invalid_cache_breakdown":`快取 token 明細與輸入 token 總數衝突。`,"logs.detail.reason.invalid_usage":`Usage 包含無效的 token 值。`,"logs.detail.reason.combo_attempt_unavailable":`至少一次 Combo 嘗試無法計價。`,"logs.detail.estimate.usage_estimated":`供應商 usage 為估算值。`,"logs.detail.estimate.cache_detail_missing":`缺少快取明細;輸入費用按上限估算。`,"logs.detail.estimate.expected_price_overlay":`使用了已驗證的 Expected 標價。`,"logs.col.error":`錯誤`,"logs.col.upstreamReason":`上游原因`,"logs.col.duration":`耗時`,"logs.modelTooltip.model":`模型`,"logs.modelTooltip.resolvedModel":`解析後模型`,"logs.modelTooltip.requestedTier":`請求層級`,"logs.modelTooltip.configuredTier":`設定層級`,"logs.modelTooltip.responseTier":`回應層級`,"logs.modelTooltip.supportsTier":`支援層級`,"logs.tokens.reported":`已上報`,"logs.tokens.unreported":`未上報`,"logs.tokens.unsupported":`不支援`,"logs.tokens.estimated":`估算`,"logs.tokens.input":`輸入`,"logs.tokens.output":`輸出`,"logs.tokens.cacheRead":`快取命中 (c)`,"logs.tokens.cacheWrite":`快取寫入 (w)`,"logs.tokens.reasoning":`推理`,"logs.tokens.noCache":`無快取資料`,"logs.tokens.noCacheNote":`該供應商不報告快取 token 數`,"logs.tokens.noCacheCursor":`Cursor 未報告快取明細`,"logs.tokens.noCacheCursorNote":`Cursor 不提供快取讀寫 token 數;這表示未知,並不代表已確認快取未命中`,"logs.tokens.estimatedNote":`估算值(供應商不報告精確用量)`,"logs.details":`檢視詳細資料`,"logs.detailTitle":`請求詳細資料`,"logs.detailRaw":`原始日誌`,"debug.title":`除錯`,"debug.subtitle":`可選的 provider transport 與 usage 提取診斷。請求錯誤和 502 在“日誌”分頁顯示。`,"debug.debug":`供應商除錯`,"debug.usage":`用量提取`,"debug.injection":`注入日誌`,"debug.claude":`Claude 入站`,"debug.claudeInbound.title":`Claude 入站請求`,"debug.claudeInbound.sub":`顯示 Claude Code/Desktop 實際傳送的內容(thinking、effort、metadata)— 不儲存提示詞原文。`,"debug.claudeInbound.empty":`尚未捕獲任何請求。開啟後從 Claude 傳送一條訊息試試。`,"debug.claudeInbound.time":`時間`,"debug.claudeInbound.endpoint":`端點`,"debug.claudeInbound.model":`模型`,"debug.claudeInbound.none":`無`,"debug.reset":`清除執行時覆蓋`,"debug.refresh":`重新整理`,"debug.follow":`跟隨捲動`,"debug.streamProvider":`供應商`,"debug.streamUsage":`用量`,"debug.streamInjection":`注入`,"debug.loading":`正在載入除錯設定…`,"debug.emptyTitle":`除錯日誌已關閉`,"debug.empty":`請在上方卡片中開啟供應商除錯或用量提取。透過代理傳送請求後,診斷行會顯示在這裡。`,"debug.noLinesTitle":`等待診斷行`,"debug.noLines.provider":`供應商除錯已開啟,但僅紀錄傳輸異常(捨棄或格式錯誤的幀,以及 Cursor dial/retry 事件)。透過 Anthropic 等供應商的正常請求可能不會產生任何行。`,"debug.noLines.usage":`用量提取已開啟但尚未捕獲任何內容。請透過 Codex 傳送請求,隨後會顯示在此處。`,"debug.noLines.injection":`注入日誌已開啟但尚未捕獲任何內容。它紀錄協作和子代理回合中的多代理指導注入與 effort-cap 決策。`,"usage.title":`用量`,"usage.subtitle":`代理本地的 Token 用量統計。缺失的用量不會顯示為零。`,"usage.loading":`正在載入用量資料…`,"usage.empty":`尚無用量紀錄。透過代理傳送請求後將在此顯示。`,"usage.loadError":`無法載入用量資料。`,"usage.range.all":`全部`,"usage.range.30d":`30 天`,"usage.range.7d":`7 天`,"usage.card.requests":`請求數`,"usage.card.measured":`已計量`,"usage.card.reported":`已上報`,"usage.card.totalTokens":`Token 總數`,"usage.card.cachedTokens":`快取命中 Token`,"usage.card.cachedTokensHint":`從供應商快取讀取的提示 Token(命中)。快取寫入在下方單獨顯示。`,"usage.card.cacheWriteTokens":`快取寫入`,"usage.card.coverage":`覆蓋率`,"usage.card.activeDays":`活躍天數`,"usage.section.heatmap":`每日活動`,"usage.section.overview":`總覽`,"usage.section.models":`模型`,"usage.section.providers":`供應商`,"usage.section.coverage":`覆蓋率明細`,"usage.coverage.measured":`已計量`,"usage.coverage.reported":`供應商上報`,"usage.coverage.estimated":`估算`,"usage.coverage.note":`已計量包含供應商上報和估算的 Token 數。未上報 / 不支援請求僅做計數,不會被算作 0 Token。`,"usage.search.models":`搜尋模型…`,"usage.col.requests":`請求數`,"usage.col.measured":`已計量`,"usage.col.reported":`已上報`,"usage.col.tokens":`Token 數`,"usage.col.share":`佔比`,"usage.heatmap.less":`少`,"usage.heatmap.more":`多`,"usage.dayMon":`一`,"usage.dayWed":`三`,"usage.dayFri":`五`,"usage.heatmap.tooltipTokens":`{tokens} Token`,"usage.heatmap.tooltipRequests":`{requests} 請求`,"nav.storage":`儲存`,"storage.title":`儲存`,"storage.subtitle":`CODEX_HOME 磁碟佔用診斷。下方歸檔清理可將最舊歸檔會話隔離或永久刪除——活動會話保持只讀。`,"storage.loading":`正在掃描儲存…`,"storage.empty":`CODEX_HOME 為空或不存在——沒有可顯示的內容。`,"storage.error":`儲存掃描失敗。請檢查 CODEX_HOME 是否指向有效目錄。`,"storage.refresh":`重新掃描`,"storage.card.total":`總大小`,"storage.card.files":`檔案數`,"storage.card.home":`CODEX_HOME`,"storage.section.buckets":`分類`,"storage.section.largest":`最大檔案`,"storage.col.bucket":`分類`,"storage.col.size":`大小`,"storage.col.files":`檔案`,"storage.col.oldest":`最舊`,"storage.col.newest":`最新`,"storage.col.rows":`資料庫行數`,"storage.rows.unknown":`未知(已鎖定)`,"storage.bucket.sessions":`活動會話`,"storage.bucket.archived_sessions":`已歸檔會話`,"storage.bucket.logs_db":`日誌資料庫`,"storage.bucket.state_db":`狀態資料庫`,"storage.bucket.attachments":`附件`,"storage.bucket.deletion_manifests":`刪除清單`,"storage.bucket.other":`其他`,"storage.cleanup.title":`歸檔清理`,"storage.cleanup.help":`按百分比移除最舊的歸檔會話。不會觸碰活動會話。預設隔離——檔案移至 CODEX_HOME/.trash。`,"storage.cleanup.slider":`最舊歸檔百分比`,"storage.cleanup.percent":`最舊 {percent}%`,"storage.cleanup.preset":`{percent}`,"storage.cleanup.preview":`預覽`,"storage.cleanup.confirmTitle":`確認歸檔清理`,"storage.cleanup.confirmBody":`將處理 {count} 個歸檔檔案(約 {size}),即最舊的 {percent}%。`,"storage.cleanup.moreFiles":`…以及另外 {n} 個`,"storage.cleanup.permanent":`永久刪除(跳過隔離)`,"storage.cleanup.permanentWarn":`永久刪除無法復原。`,"storage.cleanup.quarantineNote":`檔案會移到 CODEX_HOME 下的 .trash。可在下方「隔離區」恢復。`,"storage.cleanup.cancel":`取消`,"storage.cleanup.confirmQuarantine":`隔離`,"storage.cleanup.confirmPermanent":`永久刪除`,"storage.cleanup.doneQuarantine":`已隔離 {count} 個檔案({size})。`,"storage.cleanup.donePermanent":`已永久刪除 {count} 個檔案({size})。`,"storage.cleanup.previewFailed":`預覽失敗。`,"storage.cleanup.cleanupFailed":`清理失敗。`,"storage.cleanup.err.codex_busy":`Codex 正在使用 state.sqlite — 請退出 Codex 後重試。`,"storage.cleanup.err.stale_preview":`預覽後歸檔檔案已變化 — 請重新預覽。`,"storage.cleanup.err.restore_pending_overlap":`所選歸檔與未完成的隔離區恢復重疊 — 請先完成或重試恢復。`,"storage.cleanup.err.referenced_history":`所選歸檔仍被 fork 或分頁歷史引用。`,"storage.cleanup.err.invalid_digest":`預覽摘要缺失或無效。`,"storage.cleanup.err.invalid_mode":`模式必須是 quarantine 或 permanent。`,"storage.cleanup.err.fs_failed":`檔案系統清理失敗。部分更改可能已生效 — 請檢查 CODEX_HOME/.trash 及顯示的恢復路徑。`,"storage.cleanup.err.fs_failed_trash":`檔案系統清理失敗。部分更改可能已生效 — 請在 {trashDir} 和 manifest.json 中查詢可恢復檔案。`,"storage.cleanup.err.db_reconcile_failed":`無法更新 Codex 狀態資料庫。`,"storage.cleanup.err.cleanup_failed":`清理失敗。`,"storage.trash.title":`隔離區`,"storage.trash.help":`已移至 CODEX_HOME/.trash 的歸檔會話。恢復會把 JSONL 與執行緒行寫回。`,"storage.trash.empty":`沒有隔離條目。`,"storage.trash.loading":`正在載入隔離區…`,"storage.trash.col.when":`隔離時間`,"storage.trash.col.files":`檔案`,"storage.trash.col.size":`大小`,"storage.trash.col.mode":`模式`,"storage.trash.col.id":`條目`,"storage.trash.restore":`恢復`,"storage.trash.confirmTitle":`恢復隔離條目?`,"storage.trash.confirmBody":`將 {count} 個檔案(約 {size})從 {id} 恢復到歸檔會話。`,"storage.trash.cancel":`取消`,"storage.trash.confirmRestore":`恢復`,"storage.trash.done":`已恢復 {count} 個檔案({size})。`,"storage.trash.restoreFailed":`恢復失敗。`,"storage.trash.listFailed":`無法列出隔離條目。`,"storage.trash.mode.quarantine":`隔離`,"storage.trash.mode.permanent":`永久(未完成)`,"storage.trash.err.codex_busy":`Codex 正在使用 state.sqlite — 請退出 Codex 後重試。`,"storage.trash.err.invalid_trash":`隔離條目 ID 缺失或無效。`,"storage.trash.err.missing_trash":`未找到隔離條目。`,"storage.trash.err.dest_exists":`恢復目標已存在 — 請刪除或重新命名歸檔檔案後重試。`,"storage.trash.err.fs_failed":`檔案系統恢復失敗。部分檔案可能已恢復 — 請檢查 archived_sessions 與 .trash。`,"storage.trash.err.db_reconcile_failed":`無法恢復 Codex 狀態資料庫行。`,"storage.trash.err.storage_mutation_busy":`另一項儲存清理或恢復正在進行 — 請稍後再試。`,"storage.trash.err.restore_failed":`恢復失敗。`,"storage.trash.err.restore_worker_timeout":`恢復耗時過長(超過 10 分鐘)已停止。`,"storage.trash.err.restore_worker_aborted":`關閉過程中恢復已取消。`,"storage.trash.err.restore_worker_failed":`恢復 worker 崩潰或意外失敗。`,"storage.policy.title":`自動清理策略`,"storage.policy.help":`當歸檔大小超過閾值時可選批次清理。預設關閉——不會自動啟用。`,"storage.policy.loading":`正在載入策略…`,"storage.policy.loadFailed":`無法載入清理策略。`,"storage.policy.saveFailed":`無法儲存清理策略。`,"storage.policy.runFailed":`策略執行失敗。`,"storage.policy.alreadyRunning":`清理策略已在執行中。`,"storage.policy.invalid":`策略值無效。`,"storage.policy.enabled":`啟用自動清理`,"storage.policy.enabledHint":`預設關閉。啟用後僅按所選計劃(或立即執行)執行。`,"storage.policy.threshold":`歸檔大小超過時觸發(GiB)`,"storage.policy.target":`清理目標`,"storage.policy.targetPercent":`刪除最舊歸檔百分比`,"storage.policy.targetReduce":`將歸檔縮小至(GiB)`,"storage.policy.schedule":`計劃`,"storage.policy.schedule.manual":`僅手動`,"storage.policy.schedule.startup":`代理啟動時`,"storage.policy.schedule.daily":`每天`,"storage.policy.schedule.weekly":`每週`,"storage.policy.mode":`刪除模式`,"storage.policy.mode.quarantine":`隔離(預設)`,"storage.policy.mode.permanent":`永久刪除`,"storage.policy.permanentWarn":`永久模式無法復原。不確定時請使用隔離。`,"storage.policy.lastRun":`上次執行`,"storage.policy.lastRunDetail":`已移除 {count} · 釋放 {size}`,"storage.policy.nextRun":`下次執行`,"storage.policy.never":`從未`,"storage.policy.save":`儲存`,"storage.policy.runNow":`立即執行`,"storage.policy.running":`執行中…`,"storage.policy.saved":`策略已儲存。`,"storage.policy.skippedDisabled":`策略已禁用 — 請先啟用。`,"storage.policy.skippedUnder":`歸檔大小低於閾值 — 無需操作。`,"storage.policy.skippedEmpty":`沒有匹配目標的歸檔候選項。`,"storage.policy.doneQuarantine":`策略已隔離 {count} 個檔案({size})。`,"storage.policy.donePermanent":`策略已永久刪除 {count} 個檔案({size})。`,"storage.policy.metadataSaveWarning":`策略執行已完成,但無法儲存其排程中繼資料。`,"modal.addNamed":`新增:{label}`,"modal.add":`新增供應商`,"modal.search":`搜尋供應商…`,"modal.logInWith":`使用 {label} 登入`,"modal.waitingBrowser":`等待瀏覽器…`,"modal.providerName":`供應商名稱`,"modal.adapter":`介面卡`,"modal.baseUrl":`Base URL`,"modal.endpoint":`端點`,"modal.endpoint.tokenPlan":`Token 方案`,"modal.endpoint.payAsYouGo":`按量付費`,"modal.endpoint.custom":`自訂`,"modal.defaultModel":`預設模型(可選)`,"modal.allowPrivateNetwork":`允許本地/私有網路`,"modal.allowPrivateNetworkHint":`僅為有意自託管的供應商啟用。後設資料端點仍被阻止。`,"modal.nameRequired":`供應商名稱為必填項`,"modal.baseUrlRequired":`Base URL 為必填項`,"modal.networkError":`網路錯誤 — 代理在執行嗎?`,"modal.loginFailStart":`登入啟動失敗`,"modal.waitingLogin":`等待瀏覽器登入…`,"modal.loggingIn":`登入中…`,"modal.loginTimeout":`登入逾時 — 請重試。`,"modal.back":`返回`,"modal.badge.oauth":`OAuth`,"modal.customProvider":`自訂供應商`,"modal.failedStatus":`失敗 ({status})`,"modal.loginError":`登入錯誤:{error}`,"modal.badge.codexLogin":`Codex 登入`,"modal.badge.local":`本地`,"modal.badge.apiKey":`API 金鑰`,"modal.badge.direct":`Direct`,"modal.badge.pool":`帳號池`,"modal.badge.free":`免費`,"modal.invalidPreset":`此內建供應商預設不完整。請重新啟動代理後重試。`,"modal.freeTierTitle":`免費層級`,"modal.freeTierDefault":`無需 API 金鑰,開箱即用。`,"modal.tab.accounts":`帳號`,"modal.tab.free":`免費`,"modal.tab.paid":`付費`,"modal.accountsHint":`在此登入 ChatGPT/Codex、OAuth 與 API 金鑰帳號。OpenAI 為內建供應商 — 請登入,無需再次新增。`,"modal.accountsCodexAuthLink":`Codex 認證`,"modal.notListed":`沒有你要的供應商?新增自訂`,"modal.catalogLoading":`正在載入目錄…`,"modal.accountLogin":`登入`,"modal.accountLogout":`登出`,"modal.accountAdd":`新增帳號`,"modal.accountManage":`管理`,"modal.accountCodexPool":`ChatGPT 帳號池`,"modal.accountLoggedIn":`已登入`,"modal.accountLoggedOut":`未登入`,"quota.fiveHourLimit":`5 小時限額`,"quota.ageMinutes":`{n} 分鐘`,"quota.ageHours":`{n} 小時`,"quota.ageDays":`{n} 天`,"quota.observedAgo":`{age}前取得`,"quota.observedHint":`Meta 僅在串流回應期間回報用量,因此這是最後一次取得的數值,而非即時讀數。`,"quota.weeklyLimit":`每週限額`,"quota.monthlyLimit":`30 天限額`,"quota.cursorFirstParty":`官方模型`,"quota.cursorApiUsage":`API 用量`,"quota.totalSubscriptionCredits":`訂閱總額度`,"quota.creditsBalance":`額度餘額`,"quota.creditsPeriodEnds":`帳單週期結束於 {date}`,"quota.usedPercent":`已用 {pct}%`,"quota.limitReached":`已達上限`,"quota.resetsToday":`今天 {time} 重設`,"quota.resetsTomorrow":`明天 {time} 重設`,"quota.resetsAt":`{when} 重設`,"quota.resetsRelativeMinutes":`{n} 分鐘後重設`,"quota.resetsRelativeHours":`{n} 小時後重設`,"pws.status.ready":`就緒`,"pws.status.needsSetup":`需要設定`,"pws.status.needsAttention":`需要關注`,"pws.auth.chatgptPassthrough":`ChatGPT 直通`,"pws.auth.noKey":`無需金鑰`,"pws.freeTitle":`免費定價(可能仍需金鑰)`,"pws.localTitle":`本地執行時`,"pws.modelCountOne":`1 個模型`,"pws.modelCount":`{count} 個模型`,"pws.rail.suffixDefault":` · 預設`,"pws.rail.suffixLocal":` · 本地`,"pws.rail.suffixFree":` · 免費`,"pws.rail.selectAria":`選擇 {name} — {status}{suffix}`,"pws.searchPlaceholder":`搜尋供應商…`,"pws.filterAria":`篩選供應商`,"pws.providerFiltersAria":`供應商篩選`,"pws.filters":`篩選`,"pws.filterStatus":`狀態`,"pws.pricing":`定價`,"pws.paid":`付費`,"pws.filterType":`型別`,"pws.type.cloud":`雲端`,"pws.type.local":`本地`,"pws.type.selfHosted":`自託管`,"pws.type.login":`登入`,"pws.sort":`排序`,"pws.sortProvidersAria":`排序供應商`,"pws.sort.az":`A–Z`,"pws.sort.za":`Z–A`,"pws.sort.freePaid":`免費優先`,"pws.sort.paidFree":`付費優先`,"pws.sort.accountsFirst":`帳號優先`,"pws.resetAll":`全部重設`,"pws.providerList":`供應商列表`,"pws.providersAria":`供應商`,"pws.groupReady":`就緒 ({count})`,"pws.groupNeedsSetup":`需要設定 ({count})`,"pws.groupDisabled":`已停用 ({count})`,"pws.noSearchResults":`沒有符合搜尋的供應商。`,"pws.noMatchFilters":`沒有符合篩選的供應商。`,"pws.noProvidersConfigured":`尚未配置供應商。`,"pws.workspaceMainAria":`供應商詳細資料`,"pws.detailComingSoon":`詳細資料檢視即將推出 — 請在經典檢視中管理。`,"pws.selectPrompt":`從列表中選擇一個供應商。`,"pws.connectFirst":`連線你的第一個供應商`,"pws.empty.browseFree":`瀏覽免費供應商`,"pws.empty.browseFreeDesc":`無需訂閱即可開始`,"pws.empty.connectAccount":`連線帳號`,"pws.empty.connectAccountDesc":`使用 ChatGPT 或供應商登入`,"pws.empty.addEndpoint":`新增端點`,"pws.empty.addEndpointDesc":`自訂 base URL 和 API 金鑰`,"pws.tab.overview":`總覽`,"pws.tab.models":`模型`,"pws.tab.usage":`用量`,"pws.tab.accounts":`帳號`,"pws.tab.settings":`設定`,"pws.connection":`連線`,"pws.status.connected":`已連線`,"pws.attentionTitle":`需要關注`,"pws.attention.reauth":`當前帳號需要重新認證`,"pws.attention.reauthForward":`當前 Codex 帳號需要重新認證 — 請到“帳號”中處理`,"pws.attention.missingCredentials":`缺少憑證`,"pws.cell.auth":`認證`,"pws.cell.note":`備註`,"pws.cell.defaultModel":`預設模型`,"pws.statsAria":`供應商統計`,"pws.statsTitle":`統計`,"pws.stats.totalRequests":`請求數(30 天)`,"pws.stats.totalTokens":`tokens(30 天)`,"pws.stats.quotaUpdated":`配額更新`,"pws.stats.quotaTracked":`在用量標籤檢視限額。`,"pws.stats.source":`來源`,"pws.usageLast30d":`用量(最近 30 天)`,"pws.estimatedCost":`預估費用`,"pws.costDisclaimer":`基於 API 公示價格的預估值,非實際計費金額。`,"pws.modelBreakdown":`模型用量明細`,"pws.col.model":`模型`,"pws.col.cost":`預估費用`,"pws.col.tokens":`Token`,"pws.col.requests":`請求`,"pws.col.share":`佔比`,"pws.tokenInput":`輸入`,"pws.tokenOutput":`輸出`,"pws.metricRequests":`請求`,"pws.metricTokens":`Token`,"pws.usageUnavailable":`尚無用量紀錄。`,"pws.rateLimits":`速率限制`,"pws.quotaUnavailable":`此供應商暫無配額資料。`,"pws.accountQuotaUnavailable":`速率限制資料暫時不可用;若有上次已知值則繼續顯示。`,"pws.selected":`已選擇`,"pws.copyModelId":`複製 ID`,"pws.modelCopied":`已複製!`,"pws.modelsAvailable":`{count} 個可用`,"pws.modelSearchPlaceholder":`篩選模型…`,"pws.modelsLoading":`正在載入模型…`,"pws.modelsLoadFailed":`無法載入模型。`,"pws.modelsNeedsReauth":`需要重新登入後才能取得即時模型列表。當前顯示已配置的模型。`,"pws.modelsConfiguredFallback":`顯示已配置的模型(即時發現不可用)。`,"pws.modelsTruncated":`顯示 {total} 個模型中的前 {shown} 個。使用篩選以縮小列表。`,"pws.retry":`重試`,"pws.noModels":`未發現此供應商的模型。`,"pws.noModelMatch":`沒有符合篩選條件的模型。`,"pws.adapterBaseRequired":`介面卡和基本 URL 為必填項。`,"pws.addAccount":`新增帳號`,"pws.addKey":`新增 API 金鑰`,"pws.apiKeys":`API 金鑰`,"pws.authMode":`認證方式`,"pws.availableAccounts":`可用帳號`,"pws.accountOrdinal":`帳號 {count}`,"pws.accountsLoading":`正在載入帳號…`,"pws.accountsLoadFailed":`無法載入帳號。`,"pws.retryAccounts":`重試`,"pws.noAccounts":`尚未連線任何帳號。`,"pws.accountSwitching":`切換中…`,"pws.accountCurrent":`當前帳號`,"pws.defaultModelNone":`無(使用供應商預設值)`,"pws.discardSettings":`放棄`,"pws.jsonEditorDesc":`直接編輯供應商 JSON 配置。更改將立即儲存。`,"pws.jsonEditorTitle":`JSON 編輯器 — {name}`,"pws.jsonRestore":`恢復`,"pws.jsonSave":`儲存`,"pws.loggedInTitle":`已登入`,"pws.notLoggedInTitle":`未登入`,"pws.note":`備註`,"pws.allowPrivateNetwork":`允許本地/私有網路`,"pws.liveModels":`從供應商發現模型`,"pws.liveModelsDesc":`取得供應商的即時模型目錄。關閉後僅使用已配置的靜態模型。`,"pws.xaiResponsesOptIn":`讓 Grok 4.5 與 4.6 使用 Responses API`,"pws.xaiResponsesOptInDesc":`透過 openai-responses 路由這兩個模型。其他 Grok 模型與層級行為不變。`,"pws.xaiResponsesOptInMixed":`已部分啟用。`,"pws.cursorTransport":`Cursor 傳輸協定`,"pws.cursorTransportHttp2":`HTTP/2(預設)`,"pws.cursorTransportHttp1":`HTTP/1.1(代理相容)`,"pws.cursorTransportDesc":`當代理無法穩定承載 Cursor 的 HTTP/2 串流時,請使用 HTTP/1.1。`,"pws.optionalPlaceholder":`可選`,"pws.providerId":`供應商 ID`,"pws.reauth":`需要重新認證`,"pws.reauthenticate":`重新認證`,"pws.copyDoctor":`複製 ocx doctor`,"pws.doctorCopied":`已複製`,"pws.doctorCopyUnavailable":`剪貼簿不可用`,"pws.healthCooldownHint":`請等到冷卻結束。暫時不要探測此帳號。`,"pws.healthLabel.rateLimited":`已限速`,"pws.healthLabel.quotaLimited":`配額受限`,"pws.healthLabel.reauthRequired":`需要重新認證`,"pws.healthLabel.refreshFailed":`重新整理失敗`,"pws.healthLabel.metadataMismatch":`後設資料不符合`,"pws.healthLabel.credentialConflict":`憑證衝突`,"pws.healthSummary.rateLimited":`{provider} {account}:限速至 {until}。在此之前將暫停該帳號的路由。`,"pws.healthSummary.quotaLimited":`{provider} {account}:配額限制至 {until}。在此之前將暫停該帳號的路由。`,"pws.healthSummary.reauthRequired":`{provider} {account}:需要重新認證。`,"pws.healthSummary.credentialConflict":`{provider} {account}:憑證衝突。`,"pws.healthSummary.metadataMismatch":`{provider} {account}:後設資料不符合。`,"pws.healthSummary.staleCredentials":`{provider} {account}:憑證不完整。`,"pws.removeConfirm":`移除`,"pws.removeConfirmBody":`移除供應商「{name}」?此操作無法撤消。`,"pws.removeDefaultConfirmBody":`移除預設供應商「{name}」?「{defaultProvider}」將成為預設供應商。此操作無法撤消。`,"pws.removeConfirmTitle":`移除供應商`,"pws.saveSettings":`儲存`,"pws.pacingTitle":`請求節流`,"pws.pacingDesc":`均勻延遲送往此供應商的請求啟動。串流回應可以重疊。`,"pws.pacingEnabled":`啟用`,"pws.pacingRpm":`每分鐘請求數`,"pws.pacingRpmUnit":`次/分鐘`,"pws.pacingDelay":`最小間隔(毫秒)`,"pws.pacingSlowerWins":`以較慢的供應商限制為準,模型規則只能增加延遲。`,"pws.pacingQueued":`排隊中`,"pws.pacingNextSlot":`距下個時段`,"pws.pacingLastModel":`上個模型`,"pws.pacingNone":`無`,"pws.pacingModelOverrides":`模型規則`,"pws.pacingModel":`模型`,"pws.pacingAdd":`新增規則`,"pws.pacingRemove":`移除`,"pws.pacingRemoveModel":`移除 {model} 的請求節流規則`,"pws.pacingRuleRequired":`請先設定供應商限制或模型規則,再啟用請求節流。`,"pws.saving":`儲存中…`,"pws.settingsSaved":`設定已儲存。`,"pws.settingsUnsavedBar":`有未儲存的更改。`,"pws.unsavedLeaveBody":`有未儲存的更改。離開前儲存嗎?`,"pws.unsavedLeaveTitle":`未儲存的更改`,"pws.attentionRequired":`需要關注`,"pws.attentionAria":`{name}:{reason}`,"pws.missingCredentials":`缺少憑證`,"pws.editJsonDesc":`以 JSON 編輯原始代理配置`,"pws.updatesUnavailable":`供應商更新不可用。`,"pws.dashboard.title":`供應商總覽`,"pws.dashboard.subtitle":`在一個地方管理所有模型供應商。`,"pws.dashboard.rateLimits":`速率限制`,"pws.dashboard.recentlyUsed":`最近使用`,"pws.dashboard.requests":`{count} 個請求`,"pws.dashboard.checkedAgo":`{time} 前檢查`,"pws.dashboard.noQuota":`無配額資料`,"pws.dashboard.noUsage":`暫無使用資料`,"pws.allProviders":`供應商總覽`,"pws.enabledLabel":`已啟用`,"pws.testConnection":`測試連線`,"pws.testing":`測試中…`,"pws.connectionOk":`連線成功`,"pws.connectionFailed":`連線失敗`,"pws.editSettings":`編輯設定`,"pws.viewUsage":`檢視詳細用量`,"pws.allSystemsOk":`所有系統正常執行`,"pws.apiKeyConfigured":`API 金鑰已配置`,"pws.addApiKey":`新增 API 金鑰`,"pws.loggedInAs":`已登入為 {email}`,"pws.notLoggedIn":`未登入`,"pws.passthrough":`Codex 透傳`,"pws.notes":`備註`,"pws.notePlaceholder":`新增關於此供應商的備註...`,"pws.noteSaved":`備註已儲存`,"pws.authSummary":`認證`,"time.justNow":`剛剛`,"time.notChecked":`未檢查`,"time.minutesAgo":`{n} 分鐘前`,"time.hoursAgo":`{n} 小時前`,"time.daysAgo":`{n} 天前`,"modal.noMatch":`無符合。`,"modal.oauthDefaultNote":`使用帳號登入 — 無需 API 金鑰。`,"modal.oauthComingSoon":`{label} 的 OAuth 登入將在下次更新提供。請先使用 API 金鑰。`,"modal.oauthComingSoonShort":`此供應商的 OAuth 登入將在下次更新提供 — 請先使用 API 金鑰。`,"modal.useApiKeyInstead":`改用 API 金鑰`,"modal.setupGuide":`設定指南`,"modal.setupStep1Prefix":`前往`,"modal.setupDashboardLink":`{label} 控制檯`,"modal.setupStep1Suffix":`並複製 API 金鑰`,"modal.setupStep2":`貼上到下方的 API 金鑰欄位`,"modal.setupStep3":`點選新增供應商 — 模型會自動發現`,"modal.namePlaceholder":`例如 openrouter`,"modal.duplicateWarn":`供應商 "{name}" 已存在,將被覆蓋。`,"modal.forwardHintPrefix":`無需金鑰 — 代理會轉發你的`,"modal.forwardCredentials":`codex login`,"modal.forwardHintSuffix":`憑證到此供應商。`,"modal.localHint":`不會儲存 API 金鑰。這會為 Codex 新增 Cursor 的公開模型目錄,但在審計完成前,即時 Cursor 傳輸與原生檔案/Shell 執行仍保持停用。`,"modal.getApiKey":`取得 {label} API 金鑰`,"modal.apiKey":`API 金鑰`,"modal.apiKeyTransport":`API 金鑰標頭`,"modal.apiKeyTransportNative":`x-api-key(Anthropic 原生)`,"modal.apiKeyTransportBearer":`Authorization: Bearer`,"modal.apiKeyPlaceholder":`sk-…(或 $ENV_VAR)`,"modal.defaultModelPlaceholder":`例如 gpt-5.5`,"modal.baseUrlPlaceholder":`https://...`,"modal.baseUrlPlaceholderError":`Base URL 包含未解析的 {placeholder},請替換為實際值。`,"modal.baseUrlPlaceholderHint":`請在新增前將 Base URL 中的 {placeholder} 替換為你的實際 Account ID。`,"modal.adding":`正在新增…`,"modal.useOauthLogin":`← 使用 OAuth 登入`,"nav.codexAuth":`Codex 認證`,"nav.codexSet":`Codex 設定`,"codexSet.tab.multiauth":`多帳號認證`,"codexSet.tab.prompt":`提示詞`,"codexSet.prompt.title":`提示詞層`,"codexSet.prompt.timing":`對新啟動的工作階段生效。執行中的工作階段會保留目前的提示詞設定。`,"codexSet.prompt.staleRevision":`設定已在別處變更,清單已重新載入。`,"codexSet.prompt.writeFailed":`無法儲存變更。`,"codexSet.prompt.loadFailed":`無法載入提示詞層。`,"codexSet.prompt.repair":`修復`,"codexSet.prompt.repairFailed":`無法完成修復。`,"codexSet.drift.journalPresent":`上一次寫入未完成。下次寫入時會自動復原。`,"codexSet.drift.projectionStale":`已儲存的層與 config.toml 中的值不一致。修復會依你的層重新寫入該值。`,"codexSet.drift.storeMissing":`層檔案已遺失,但 config.toml 中仍有指示。修復會先建立備份,並將該文字保留為一個層。`,"codexSet.drift.ownedMalformed":`config.toml 中產生的該行曾被手動變更,因此重寫不再安全。`,"codexSet.custom.adoptUnsupported":`{path} 第 {line} 行的值不是單行字串,無法匯入。若要在此管理,請手動移動它。`,"codexSet.prompt.unreadable":`Codex 設定檔存在但無法讀取,因此拒絕了變更。`,"codexSet.layer.permissions":`權限`,"codexSet.layer.collaboration":`協作模式`,"codexSet.layer.environment":`環境內容`,"codexSet.layer.apps":`應用程式`,"codexSet.layer.skills":`技能`,"codexSet.prompt.extensionsUnknown":`擴充功能可新增自己的層。Codex 不會公開這些層,因此無法在此列出。`,"codexSet.group.transition":`變更通知`,"codexSet.group.transitionDesc":`它們通報變化而非描述狀態,因此僅在工作階段切換為即時模式或更換模型時出現。`,"codexSet.custom.slotNote":`自訂層會依此順序合併為一個區段。`,"codexSet.row.alwaysOn":`一律啟用`,"codexSet.row.onChange":`變更時傳送`,"codexSet.row.featureGated":`在 [features] 下設定`,"codexSet.row.openFeatures":`開啟設定`,"codexSet.dialog.setValue":`{value}(預設 {fallback})`,"codexSet.dialog.copyKey":`複製設定鍵`,"codexSet.dialog.unknownLayer":`此版本沒有該層的說明。它來自比儀表板更新的 Codex 執行環境。`,"codexSet.custom.heading":`自訂層`,"codexSet.custom.add":`+ 新增層`,"codexSet.custom.newTitle":`新增層`,"codexSet.custom.editTitle":`編輯層`,"codexSet.custom.titleLabel":`標題`,"codexSet.custom.bodyLabel":`指示`,"codexSet.custom.bodySize":`{bytes}/{max} 位元組`,"codexSet.custom.normalized":`定位字元已轉換為四個空格,換行符號已轉換為 LF。`,"codexSet.custom.titleRequired":`請輸入標題。`,"codexSet.custom.titleTooLong":`標題有 {count} 個字元,上限為 {max} 個。`,"codexSet.custom.titleMultiline":`標題必須為單行。`,"codexSet.custom.bodyTooLarge":`此層為 {bytes} 位元組,上限為 {max} 位元組。`,"codexSet.custom.composedTooLarge":`啟用的層合計將達到 {bytes} 位元組,超過上限。`,"codexSet.custom.invalidCharacter":`無法儲存位置 {position} 的控制字元。`,"codexSet.custom.discardPrompt":`要捨棄變更嗎?`,"codexSet.custom.keepEditing":`繼續編輯`,"codexSet.custom.delete":`刪除 {title}`,"codexSet.custom.deleteConfirm":`要刪除此層嗎?此操作無法復原。`,"codexSet.custom.layerGone":`該層已在別處被刪除,因此編輯器已關閉。`,"codexSet.custom.deleteConfirmNamed":`要刪除“{title}”嗎?此操作無法復原。`,"codexSet.custom.moveUp":`將 {title} 上移`,"codexSet.custom.prevLayer":`上一層`,"codexSet.custom.nextLayer":`下一層`,"codexSet.custom.navPosition":`{position} / {total}`,"codexSet.custom.moveDown":`將 {title} 下移`,"codexSet.custom.limitReached":`最多可保留 {max} 個自訂層。`,"codexSet.custom.notOwned":`developer_instructions 是在 opencodex 外部寫入的,因此無法在此編輯。請將其匯入,以便作為層管理。`,"codexSet.custom.adopt":`匯入現有指示`,"codexSet.custom.adoptConfirm":`匯入為層`,"codexSet.custom.adoptRefused":`無法匯入現有值。`,"codexSet.custom.baseReplaced":`model_instructions_file 已設為 {path},因此 opencodex 外部的內容已取代基礎提示詞。`,"codexSet.lint.identity":`此內容宣稱了與 Codex 所設定身分不同的身分。`,"codexSet.lint.foreignTool":`工具由登錄檔提供;在此指定名稱並不會建立工具。`,"codexSet.lint.placeholder":`指示不會經過範本引擎處理,因此此內容會依原樣傳送。`,"codexSet.lint.applyPatch":`apply_patch 由工具登錄檔定義,而不是由指示定義。`,"codexSet.lint.approvalVocab":`Codex 會注入自己的核准用語;此內容可能與其衝突。`,"codexSet.lint.environment":`環境資訊稍後才會產生,可能與此內容衝突。`,"codexSet.lint.size":`此層超過 8 KB。仍可儲存,但每次請求都會耗用權杖。`,"codexSet.preset.blank":`空白層`,"codexSet.preset.concise.name":`精簡輸出`,"codexSet.preset.concise.description":`簡短作答,不加開場白,儘量減少格式。`,"codexSet.preset.concise.provenance":`改編自 Claude Code 的精簡指示。文案由我們原創,並非複製。`,"codexSet.preset.planFirst.name":`編輯前先規劃`,"codexSet.preset.planFirst.description":`先說明計畫,再進行變更。`,"codexSet.preset.planFirst.provenance":`改編自 Claude Code 的規劃方式。文案由我們原創,並非複製。`,"codexSet.preset.explainWhy.name":`說明理由`,"codexSet.preset.explainWhy.description":`不只說明要做什麼,也說明原因。`,"codexSet.preset.explainWhy.provenance":`改編自 Grok Build 的確認風格。文案由我們原創,並非複製。`,"codexSet.preset.testFirst.name":`測試優先`,"codexSet.preset.testFirst.description":`修正前先撰寫會失敗的測試。`,"codexSet.preset.testFirst.provenance":`改編自常見的代理實務。文案由我們原創,並非複製。`,"codexSet.preset.korean.name":`韓文回覆`,"codexSet.preset.korean.description":`無論要求使用哪種語言,都以韓文回答。`,"codexSet.preset.korean.provenance":`根據常見的使用者需求為 opencodex 撰寫。文案由我們原創,並非複製。`,"codexSet.dialog.class":`類型`,"codexSet.dialog.key":`設定鍵`,"codexSet.dialog.fileValue":`此檔案中的值`,"codexSet.dialog.absentDefault":`未設定(預設為 {value})`,"codexSet.dialog.noRenderedText":`Codex 不會公開內建層組合後的文字,因此此對話框只說明該層並列出其設定鍵,不顯示具體內容。`,"codexSet.dialog.sourceText":`傳送給模型的原文`,"codexSet.dialog.sourceBytes":`{bytes} 位元組`,"codexSet.dialog.notRendered":`在我們讀取的那一輪中,此層沒有傳送任何內容。各區段只在內容變更時才會重新傳送,因此單次取樣可能看不到它。`,"codexSet.dialog.emptySource":`{path} 檔案存在但為空,因此此層不會傳送任何內容。`,"codexSet.dialog.notExposed":`基礎提示詞不在 Codex 可列印的訊息清單中傳遞,因此無法在此顯示。可以透過 model_instructions_file 取代它。`,"codexSet.dialog.textUnavailable":`本機無法讀取 Codex 提示詞,因此無法顯示原文。`,"codexSet.class.base":`基礎指令`,"codexSet.class.config-toggle":`可在此切換`,"codexSet.class.feature-gated":`功能開關控制`,"codexSet.class.runtime-conditional":`執行時條件控制`,"codexSet.class.extension-unknown":`擴充層`,"codexSet.layer.base-instructions":`基礎指令`,"codexSet.layer.model-switch":`模型切換通知`,"codexSet.layer.personality":`個性`,"codexSet.layer.context-window-guidance":`上下文視窗指引`,"codexSet.layer.realtime":`即時工作階段`,"codexSet.layer.agents-md":`AGENTS.md`,"codexSet.layer.environments-instructions":`執行環境`,"codexSet.layer.plugins":`外掛程式`,"codexSet.layer.tools":`工具`,"codexSet.layer.multi-agent-mode":`多代理模式`,"codexSet.layer.git-attribution":`提交署名`,"codexSet.about.base-instructions":`Codex 自身的指令。它們會隨請求一同傳送,無法關閉。`,"codexSet.about.model-switch":`工作階段中途切換模型時新增。`,"codexSet.about.personality":`語氣和表達風格指引,由功能開關控制。`,"codexSet.about.context-window-guidance":`剩餘上下文預算的相關建議,由功能開關控制。`,"codexSet.about.realtime":`即時工作階段中新增。`,"codexSet.about.agents-md":`專案中的 AGENTS.md 檔案。此頁面只顯示該層,絕不會編輯專案文件。`,"codexSet.about.permissions":`說明目前生效的沙箱和核准設定。`,"codexSet.about.collaboration":`說明目前啟用的協作模式。`,"codexSet.about.environment":`工作目錄、平台及其他環境資訊。`,"codexSet.about.environments-instructions":`延後執行環境的相關指引,由功能開關控制。`,"codexSet.about.apps":`已連接應用程式的使用方式。`,"codexSet.about.plugins":`選取外掛程式或任一外掛程式宣告功能時新增。`,"codexSet.about.tools":`延後載入的工具說明,由功能開關控制。`,"codexSet.about.skills":`可用技能清單。`,"codexSet.about.multi-agent-mode":`子代理指令,由功能開關控制。`,"codexSet.about.git-attribution":`讓模型在它寫的提交加上 Co-authored-by: Codex 尾註,並在它開的拉取請求加上 Generated with Codex. 這一行。Codex 從你的帳號讀取此項,所以這裡和 [features] 都改不了。帳號關閉時,Codex 會送出相反的指令,而不是什麼都不送。`,"codexSet.condition.model-switch":`僅在工作階段中途切換模型後插入。`,"codexSet.condition.realtime":`僅在即時工作階段中插入。`,"codexSet.condition.agents-md":`找到適用於目前工作目錄的專案文件時插入。`,"codexSet.condition.plugins":`選取外掛程式或任一外掛程式宣告功能時插入。`,"codexSet.condition.git-attribution":`由你帳號的署名政策決定。`,"codexSet.base.title":`基礎提示詞`,"codexSet.base.prev":`上一個選項`,"codexSet.base.next":`下一個選項`,"codexSet.base.position":`{position} / {total}`,"codexSet.base.swipeHint":`左右滑動、按方向鍵,或點箭頭按鈕切換選項。對新開始的工作階段生效。`,"codexSet.base.defaultTitle":`Codex 自帶的基礎提示詞`,"codexSet.base.defaultBody":`預設項並不存在這裡,所以沒有可編輯或刪除的內容:選它只是從設定移除 model_instructions_file,讓 Codex 用自帶的提示詞。`,"codexSet.base.variantTitle":`名稱`,"codexSet.base.variantBody":`提示詞`,"codexSet.base.replacesWarning":`這會整體取代 Codex 自帶的基礎提示詞,而不是在其後附加。這裡寫得短,模型收到的指令就只有這麼短。`,"codexSet.base.use":`用這一個`,"codexSet.base.inUse":`正在使用`,"codexSet.base.externalBlocked":`model_instructions_file 已指向 {path},且不是 opencodex 寫的。請先自行清除,再在此處選擇。`,"nav.api":`API`,"nav.openMenu":`開啟選單`,"nav.closeMenu":`關閉選單`,"codexAuth.mainAccount":`主帳號`,"codexAuth.codexApp":`Codex App`,"codexAuth.logLabel":`日誌標籤`,"codexAuth.moreActions":`顯示更多操作`,"codexAuth.copyId":`複製帳戶 ID`,"codexAuth.appLogin":`應用登入`,"codexAuth.accountPool":`帳號池`,"codexAuth.accountModeTitle":`OpenAI 帳號模式`,"codexAuth.accountModePool":`帳號池模式`,"codexAuth.accountModePoolDesc":`主登入與符合條件的已新增帳號會在此輪換。`,"codexAuth.accountModeDirect":`直連模式`,"codexAuth.accountModeDirectDesc":`請求僅使用主登入;已新增帳號會繼續儲存,供帳號池模式使用。`,"codexAuth.openaiMissing":`未配置內建 OpenAI 供應商。`,"codexAuth.openaiDisabled":`內建 OpenAI 供應商已停用。`,"codexAuth.openaiUnavailableDesc":`你的 OpenAI 帳號仍然可用。啟用供應商後即可路由 Codex 請求。`,"codexAuth.enableOpenai":`啟用 OpenAI`,"codexAuth.enablingOpenai":`正在啟用...`,"codexAuth.enableOpenaiFailed":`無法啟用 OpenAI 供應商。`,"codexAuth.openaiPresetLoadFailed":`無法載入 OpenAI 供應商預設。`,"codexAuth.openaiPresetUnavailable":`OpenAI 供應商預設不可用。`,"codexAuth.openProviders":`開啟供應商`,"codexAuth.add":`新增`,"codexAuth.sparkQuota":`Codex Spark 配額`,"codexAuth.sparkQuotaHint":`在帳號卡片上顯示 GPT-5.3-Codex-Spark 週視窗。預設隱藏,因為只適用於單一模型。`,"codexAuth.sparkQuotaShown":`已顯示 Codex Spark 配額`,"codexAuth.sparkQuotaHidden":`已隱藏 Codex Spark 配額`,"codexAuth.sparkQuotaFailed":`無法變更 Codex Spark 配額設定`,"codexAuth.refreshQuota":`重新整理額度`,"codexAuth.refreshingQuota":`重新整理中...`,"codexAuth.quotaRefreshed":`額度已重新整理`,"codexAuth.quotaRefreshFailed":`額度重新整理失敗`,"codexAuth.noPool":`尚未新增池帳號。`,"codexAuth.fiveHour":`5 小時`,"codexAuth.weekly":`每週`,"codexAuth.monthly":`30天`,"codexAuth.resets":`重設`,"codexAuth.today":`今天`,"codexAuth.current":`當前`,"codexAuth.nextSession":`已選擇`,"codexAuth.poolPrepared":`已為帳號池準備`,"codexAuth.preparePoolTitle":`為帳號池模式準備此帳號?`,"codexAuth.preparePoolDesc":`直連請求仍使用主登入。啟用帳號池模式後,此帳號會成為預先選擇的池帳號。`,"codexAuth.prepareForPool":`為帳號池準備`,"codexAuth.poolPreparedToast":`已為帳號池模式準備 {email}`,"codexAuth.switchTitle":`切換活躍帳號?`,"codexAuth.switchDesc":`從現有和新 Codex 會話的下一次請求開始生效。進行中的請求保留原帳號。`,"codexAuth.cacheWarning":`切換帳號會重設提示快取。新會話從空快取開始。`,"codexAuth.setAsNext":`選擇帳號`,"codexAuth.cancel":`取消`,"codexAuth.switchBack":`切換回主帳號?`,"codexAuth.switchBackDesc":`現有和新 Codex 會話的下一次請求將使用應用登入帳號。`,"codexAuth.autoSwitch":`自動切換帳號`,"codexAuth.autoSwitchThreshold":`切換閾值`,"codexAuth.autoSwitchThresholdAria":`切換閾值(百分比)`,"codexAuth.autoSwitchLoadFailed":`無法載入自動切換帳號設定。`,"codexAuth.autoSwitchThresholdInvalid":`請輸入 1 到 100 之間的整數`,"codexAuth.autoSwitchUpdated":`自動切換帳號設定已更新`,"codexAuth.autoSwitchUpdateFailed":`無法確認更新。當前顯示最後一次確認的值。`,"codexAuth.pauseExhausted":`暫停已達上限帳號`,"codexAuth.pausingExhausted":`正在檢查額度...`,"codexAuth.pauseExhaustedSucceeded":`已暫停 {count} 個達到上限的帳號`,"codexAuth.pauseExhaustedNone":`沒有確認達到 100% 用量的帳號。`,"codexAuth.pauseExhaustedFailed":`無法檢查並暫停已達上限帳號。`,"codexAuth.pause":`暫停`,"codexAuth.resume":`恢復`,"codexAuth.paused":`已暫停`,"codexAuth.pauseSucceeded":`已暫停 {email}`,"codexAuth.resumeSucceeded":`{email} 已重新加入帳號池`,"codexAuth.pauseFailed":`無法暫停 {email},未做任何變更。`,"codexAuth.resumeFailed":`無法恢復 {email},未做任何變更。`,"codexAuth.pausedHint":`恢復前不會參與自動切換、重試、冷卻恢復或手動選擇。`,"anthropicPool.title":`Claude 帳號池(實驗性)`,"anthropicPool.enabledDesc":`遇到 429 時冷卻該帳號並故障轉移。新會話優先使用{window}低於 {threshold}% 的帳號。`,"anthropicPool.enabledNoProactiveDesc":`429 時將帳號冷卻並切換。門檻為 0 時停用主動的用量切換,但新工作階段選擇與 429 復原仍會使用 {window} 視窗。`,"anthropicPool.disabledDesc":`僅使用當前活躍的 Claude 帳號。僅在接受實驗性路由時啟用。`,"anthropicPool.experimentalWarning":`實驗性功能,尚未充分驗證。看起來像自動多帳號輪換的行為可能導致 Anthropic 限制帳號。同一組織可能共享配額——對這些帳號做池化沒有幫助。除非瞭解風險,否則請保持關閉。`,"anthropicPool.needTwoAccounts":`啟用帳號池前請至少新增兩個 Claude OAuth 帳號。`,"anthropicPool.threshold":`新會話用量閾值`,"anthropicPool.thresholdAria":`新會話用量閾值(百分比)`,"anthropicPool.thresholdHelp":`0 表示禁用基於配額的選擇(僅親和性 + 活躍帳號)。預設 80。`,"anthropicPool.thresholdInvalid":`請輸入 0 到 100 之間的整數`,"anthropicPool.loadFailed":`無法載入 Claude 帳號池設定。`,"anthropicPool.saveFailed":`無法儲存 Claude 帳號池設定。`,"anthropicPool.on":`開`,"anthropicPool.off":`關`,"accountPool.strategy":`輪換策略`,"accountPool.strategyDesc":`新會話如何從帳號池中選擇帳號。`,"accountPool.strategyQuota":`配額`,"accountPool.strategyRoundRobin":`輪詢`,"accountPool.strategyFillFirst":`填滿優先`,"accountPool.stickyLimit":`輪換前的粘性成功次數`,"accountPool.stickyLimitAria":`輪換前的粘性成功次數`,"accountPool.stickyLimitHelp":`在推進到下一個帳號之前,將所選帳號保留這麼多次成功的新會話繫結。`,"accountPool.stickyLimitInvalid":`請輸入 1 到 100 之間的整數`,"accountPool.strategyLoadFailed":`無法載入輪換策略。`,"accountPool.strategyUpdateFailed":`無法儲存輪換策略。`,"accountPool.quotaWindow":`配額統計區間`,"accountPool.quotaWindowDesc":`指定依配額選擇新會話、填滿優先門檻判定,以及可用 429 替代帳號所採用的快取用量。`,"accountPool.quotaWindowFiveHour":`5 小時用量`,"accountPool.quotaWindowWeekly":`每週用量`,"accountPool.quotaWindowMaxUtilization":`較高的用量`,"accountPool.quotaWindowHint":`每週用量會在仍有其他可用帳號時略過 5 小時用量已用盡的帳號;若沒有其他帳號,則會退回使用這些帳號。每週用量相同時優先挑選 5 小時用量較低者;各帳號的每週用量要等供應商頁面輪詢後才會得知。`,"accountPool.quotaWindowInert":`只有配額策略,或門檻大於 0 的填滿優先策略,才會依用量計分;在目前的輪換策略下,這項設定不會有任何作用。`,"codexAuth.switched":`下一次請求將使用 {email}`,"codexAuth.loadFailed":`無法載入 Codex 帳號設定。`,"codexAuth.switchFailed":`無法切換帳號。之前的選擇保持不變。`,"codexAuth.removeConfirm":`刪除 {id}?`,"codexAuth.removeFailed":`無法移除帳號。未進行任何更改。`,"codexAuth.addTitle":`新增 Codex 帳號`,"codexAuth.addIdLabel":`帳號 ID(識別符號)`,"codexAuth.addIdPlaceholder":`codex-work, codex-alt, team…`,"codexAuth.resetCreditsAria":`{count} 個重設額度`,"codexAuth.addJsonLabel":`auth.json 內容`,"codexAuth.addHelp":`從另一臺機器的 ~/.codex/auth.json 複製,或使用 codex-auth export。`,"codexAuth.importBtn":`匯入`,"codexAuth.importInvalidJson":`無效的 JSON`,"codexAuth.importMissingTokens":`JSON 中缺少 access_token 或 refresh_token`,"codexAuth.importMissingId":`請輸入帳號 ID`,"codexAuth.accountAdded":`帳號已新增到池中`,"codexAuth.addPickDesc":`使用另一個 ChatGPT 帳號登入以新增到池中。`,"codexAuth.oauthLogin":`OAuth 登入`,"codexAuth.oauthDesc":`在瀏覽器中開啟 ChatGPT 登入`,"codexAuth.deviceLogin":`裝置碼登入`,"codexAuth.deviceDesc":`適用於無頭或遠端代理:在另一台裝置上輸入短代碼`,"codexAuth.importAuthJson":`匯入 auth.json`,"codexAuth.importAuthJsonDesc":`從另一個 Codex 安裝或 codex-auth 匯出`,"codexAuth.back":`返回`,"codexAuth.oauthAlreadyInProgress":`登入已在進行中。請在瀏覽器中完成。`,"codexAuth.oauthWaiting":`等待瀏覽器中完成 ChatGPT 登入...`,"codexAuth.oauthSubmittingCode":`正在提交程式碼…`,"codexAuth.oauthCodeSubmitted":`程式碼已提交——正在等待登入完成…`,"codexAuth.oauthStatusRetrying":`檢查登入狀態時發生網路或代理錯誤——正在重試…`,"codexAuth.oauthCancelled":`登入已取消。`,"codexAuth.loginFailed":`登入失敗`,"codexAuth.needsReauth":`重新登入`,"codexAuth.reauthenticate":`重新認證`,"codexAuth.tokenExpired":`權杖已過期 — 請重新認證此帳號`,"codexAuth.mainTokenExpired":`權杖已過期 — 請透過 Codex 應用登入重新登入`,"codexAuth.emailCollision":`此帳號與您的主 Codex 登入相同。請使用其他帳號。`,"codexAuth.resetCreditsTitle":`重設額度`,"codexAuth.resetCreditsAvailable":`您有 {count} 個可用重設額度。`,"codexAuth.resetCreditsDesc":`每個額度可立即重設您當前的小時和每週使用限制。`,"codexAuth.noResetCredits":`沒有可用的重設額度。`,"codexAuth.earnCreditsHint":`額度每月自動發放,也可透過推薦計劃獲得。`,"codexAuth.creditsExpireNote":`額度在獲得後 30 天過期。`,"codexAuth.useOneCredit":`使用 1 個額度`,"codexAuth.confirmResetTitle":`使用重設額度?`,"codexAuth.confirmResetDesc":`這將立即重設您當前的使用限制。剩餘額度:{count} 個。`,"codexAuth.irreversible":`此操作無法復原。`,"codexAuth.useCredit":`使用額度`,"codexAuth.redeeming":`重設中...`,"codexAuth.resetSuccess":`使用限制已重設!剩餘額度:{remaining} 個。`,"codexAuth.resetSuccessGeneric":`使用限制已重設!`,"codexAuth.resetAlreadyRedeemed":`該額度已兌換過,額度未變。`,"codexAuth.resetNothingToReset":`當前沒有需要重設的使用視窗。`,"codexAuth.resetNoCredit":`沒有可用的重設額度。`,"codexAuth.resetError":`重設額度使用失敗,請重試。`,"codexAuth.fifoNote":`最早獲得的額度優先使用。`,"codexAuth.confirmWhichCredit":`將使用 {date} 獲得的額度。`,"codexAuth.creditNext":`即將使用`,"codexAuth.creditLabel":`額度 #{n}`,"codexAuth.creditNextBadge":`NEXT`,"codexAuth.creditGranted":`獲得 {date}`,"codexAuth.creditExpires":`過期 {date}(剩餘 {days} 天)`,"api.title":`API 存取`,"api.subtitle":`使用生成的 API 金鑰從外部應用存取 opencodex 代理。金鑰透過 {authHeader} 標頭認證;下表顯示每個端點接受什麼。`,"api.baseUrl":`基礎 URL`,"api.responsesEndpoint":`Responses API`,"api.chatCompletionsEndpoint":`Chat Completions API`,"api.messagesEndpoint":`Messages API`,"api.modelsEndpoint":`Models API`,"api.endpointNote":`請將基礎 URL 用於 OpenAI 相容客戶端。Responses 與 Chat Completions 在 /v1 下提供。`,"api.endpointsTitle":`閘道器端點`,"api.authTitle":`身份驗證`,"api.authLoopback":`迴環繫結(127.0.0.1 或 ::1)會跳過身份驗證。遠端繫結需要生成的 ocx_ 金鑰或 OPENCODEX_API_AUTH_TOKEN。`,"api.authBaseUrlNote":`客戶端應使用基礎 URL,然後選擇下面的協議端點。`,"api.newKeyTitle":`已建立新金鑰`,"api.newKeyNote":`請立即複製此金鑰,它不會再次顯示。`,"api.copy":`複製`,"api.copied":`已複製`,"api.dismiss":`關閉`,"api.generateTitle":`生成金鑰`,"api.keyNamePlaceholder":`金鑰名稱(可選)`,"api.generate":`生成`,"api.generating":`建立中…`,"api.activeKeys":`活躍金鑰({count})`,"api.noKeys":`還沒有 API 金鑰。請在上方生成一個。`,"api.colName":`名稱`,"api.colKey":`金鑰`,"api.colCreated":`建立時間`,"api.confirm":`確認`,"api.deleteAria":`刪除 API 金鑰`,"api.modelsTitle":`外部模型目錄`,"api.modelsCount":`{count} 個可呼叫`,"api.modelsLoading":`正在載入模型…`,"api.modelsSearch":`搜尋模型`,"api.modelsSubtitle":`請使用這些精確的模型 ID 搭配 /v1/models 和你選擇的入站協議。`,"api.modelsEmpty":`還沒有可供外部呼叫的模型。`,"api.modelsNoMatch":`沒有模型符合「{query}」。`,"api.modelsLoadFailed":`無法載入外部模型目錄。`,"api.colModel":`模型`,"api.colSource":`來源`,"api.colProtocols":`協議`,"api.sourceNative":`ChatGPT 池`,"api.sourceCombo":`組合路由`,"api.sourceCustom":`自訂`,"api.protocolResponses":`Responses`,"api.protocolChatCompletions":`Chat Completions`,"api.protocolMessages":`Messages`,"api.copyModelId":`複製 ID`,"api.modelCopied":`已複製`,"api.testModel":`測試`,"api.testingModel":`測試中…`,"api.testSucceeded":`成功`,"api.testFailed":`失敗`,"api.usageChatTitle":`Chat Completions 範例`,"api.usageResponsesTitle":`Responses 範例`,"api.usageMessagesTitle":`Messages 範例`,"api.usageSampleInput":`你好,世界!`,"api.keysLoadFailed":`無法載入 API 金鑰。`,"api.createFailed":`無法建立 API 金鑰。`,"api.deleteFailed":`無法刪除 API 金鑰。`,"api.auth.endpoint":`端點`,"api.auth.required":`必要`,"api.auth.accepted":`已接受`,"api.auth.rejected":`未接受`,"api.auth.testProtocol":`測試 {protocol}`,"api.auth.testNeedsFreshKey":`生成一組金鑰並保留其一次性數值在畫面上,才能執行已認證的測試。`,"api.key.name":`金鑰名稱`,"api.key.rename":`重新命名`,"api.key.saveName":`儲存名稱`,"api.key.renaming":`儲存中…`,"api.key.renameFailed":`無法重新命名金鑰。你的草稿已保留。`,"api.key.deleting":`刪除中…`,"api.rotation.title":`金鑰輪替`,"api.rotation.description":`簽發替代金鑰,並在短暫轉換期間保留目前金鑰。`,"api.rotation.start":`開始輪替`,"api.rotation.starting":`正在開始…`,"api.rotation.pending":`輪替尚待確認。請先更新並驗證用戶端,再提交輪替。`,"api.rotation.expires":`轉換期間截止:`,"api.rotation.secretOnce":`替代金鑰只顯示一次。關閉前請先複製。`,"api.rotation.commit":`提交輪替`,"api.rotation.abort":`中止輪替`,"api.rotation.failed":`輪替操作未完成。請重新整理後再試。`,"api.rotation.startFailed":`無法開始金鑰輪替。`,"api.key.copyFailed":`無法複製金鑰。請手動選取並複製後再關閉此面板。`,"api.attribution.title":`已歸因用量`,"api.attribution.requests7d":`請求數,最近 7 天`,"api.attribution.totalRequests":`已歸因請求總數`,"api.attribution.lastUsed":`上次使用`,"api.attribution.since":`歸因功能自啟用時間起算`,"api.attribution.neverUsed":`自歸因啟用以來未曾使用`,"api.attribution.unavailable":`用量無法取得`,"api.attribution.unavailableDetail":`尚未歸因任何用量。歸因啟用前記錄的請求無法事後補歸。`,"api.attribution.ambiguous":`兩組金鑰共用此 ID,因此用量無法歸因到其中一組。請在設定檔中為每組金鑰指定唯一 ID。`,"api.attribution.railAmbiguous":`ID 重複`,"claude.subtitle":`在 Claude Code 中使用 GPT、Gemini 等其他模型。`,"claude.pageTitle":`Claude Code`,"claude.enabledLabel":`Claude 連線`,"claude.enabledHint":`關閉後 Claude Code 無法使用此代理。`,"claude.authMode":`認證模式`,"claude.authModeHint":`Subscription 需要 Claude 帳號,Proxy 無需 Anthropic 帳號即可使用`,"claude.authModeSubscription":`Subscription(Claude 帳號)`,"claude.authModeProxy":`Proxy(無需帳號)`,"claude.authModeAuto":`自動(檢測 Claude 認證)`,"claude.effectiveMode.label":`下次啟動生效`,"claude.effectiveMode.manual":`手動:{mode}`,"claude.effectiveMode.autoPresent":`自動:訂閱 — 已透過 {source} 找到 Claude 認證`,"claude.effectiveMode.autoAbsent":`自動:代理模式 — 未找到 Claude 認證`,"claude.effectiveMode.autoUnknown":`自動:訂閱 — 無法確認認證`,"claude.effectiveMode.admissionKey":`此代理的 API 金鑰仍會傳送。`,"claude.authSource.claude-json-oauth":`Claude 帳號`,"claude.authSource.claude-credentials-file":`憑證檔案`,"claude.authSource.macos-keychain":`macOS 鑰匙串`,"claude.authSource.exported-env":`環境變數`,"claude.authSource.unknown":`檢測到的憑證`,"claude.systemEnv":`自動連線`,"claude.systemEnvDesc":`開啟後,在任意終端執行 claude 會自動透過代理。`,"claude.systemEnvUnsupported":`自動連線僅在 macOS 上可用。在此系統上,請使用 {cmd} 啟動 Claude。`,"claude.systemEnvWarn":`⚠ 需要完全退出並重新開啟終端應用才能生效。不推薦使用。`,"claude.fastMode":`Fast Mode (OpenAI)`,"claude.fastModeDesc":`控制 OpenAI 模型的推理速度。ON = 優先順序(更快)。OFF = 預設速度。Auto = 透傳客戶端設定。`,"claude.fastAuto":`Auto`,"claude.fastOn":`ON`,"claude.fastOff":`OFF`,"claude.autoContext":`自動利用大上下文`,"claude.autoContextDesc":`決定 1M 標記的範圍。開:視窗能容納壓縮門檻的模型都有大上下文條目;關:僅真正的 1M 模型有。`,"claude.autoContextInert":`配置檔案中存在舊式上下文大小值(maxContextTokens),此功能暫不生效。刪除該值即可恢復。`,"claude.autoCompactWindow":`自動摘要觸發點`,"claude.autoCompactDefault":`{value}(預設)`,"claude.autoCompactWindowDesc":`對話達到該點時自動摘要舊內容。不會超過各模型自身上限,因此 200k 模型不受影響。`,"claude.autoCompactWindowWarn":`修改該值可能導致 GPT 模型異常——若超過模型真實上限,會在摘要觸發前報錯。`,"claude.injectAgents":`自動註冊子代理`,"claude.injectAgentsDesc":`將“子代理”頁選中的模型(以及當前預設模型)註冊為 Claude Code 可派遣的代理(ocx-*)。從下一個會話開始生效。`,"claude.webSearchSidecar":`網頁搜尋附屬服務覆蓋`,"claude.webSearchSidecarHint":`僅對 Claude Code 請求覆蓋主網頁搜尋附屬服務設定。`,"claude.visionSidecar":`視覺附屬服務覆蓋`,"claude.visionSidecarHint":`僅對 Claude Code 請求覆蓋主視覺附屬服務設定。`,"claude.useMainSetting":`使用主設定`,"claude.sidecarModelPlaceholder":`主設定中的模型`,"claude.quickstart":`開始使用`,"claude.quickstartHint":`{cmd} 透過代理開啟 Claude Code。你的 claude.ai 登入保持不變。`,"claude.manualEnv":`手動配置(高階)`,"claude.smallFastModel":`背景輔助模型`,"claude.smallFastModelHint":`Claude Code 用於對話摘要、主題識別等背景工作的模型。子代理的 haiku 別名也使用它。留空 = Claude 預設(Haiku)。`,"claude.smallFastModelAccurateHint":`Claude Code 用於聊天摘要、主題識別等背景工作的模型。子代理的 haiku 別名也使用此模型。`,"claude.smallFastModelUnsetOption":`讓 Claude Code 選擇(原生模型)`,"claude.smallFastModelNativeWarning":`留空時,OpenCodex 不會設定輔助模型覆蓋項。Claude Code 可能使用其原生 Sonnet 模型,並可能產生原生供應商費用。`,"claude.slotUnset":`使用 Claude 預設值`,"claude.modelMap":`模型攔截`,"claude.modelMapHint":`攔截對特定模型的請求並重定向到你指定的模型。預設為空——新增規則後才生效。`,"claude.mapFrom":`原始模型(如 claude-sonnet-4-5)`,"claude.mapTo":`替換為(如 gemini/gemini-3-pro)`,"claude.addMapping":`新增規則`,"claude.removeMapping":`刪除規則`,"claude.aliases":`可用模型`,"claude.aliasesHint":`Claude Code 的 /model 選單中顯示的模型列表。`,"claude.aliasProviderOther":`其他`,"claude.loading":`載入中…`,"claude.loadFail":`載入 Claude 設定失敗`,"claude.saved":`已儲存。`,"claude.saveFailed":`儲存失敗`,"claude.networkError":`網路錯誤 — 代理是否在執行?`,"claude.toggleAria":`切換 Claude 連線`,"claude.none":`無`,"cws.loading":`正在載入組合…`,"cws.loadFailed":`無法載入組合。`,"cws.saveFailed":`無法儲存組合。`,"cws.removeFailed":`無法刪除組合。`,"cws.saved":`組合已儲存。`,"cws.created":`已建立 {model}。`,"cws.removed":`已刪除 combo/{id}。`,"cws.renamed":`已將 {from} 重新命名為 {to}。`,"cws.add":`新增組合`,"cws.addTitle":`新增組合`,"cws.addSubtitle":`建立跨供應商的虛擬模型,並指定客戶端實際請求的模型名稱。`,"cws.create":`建立組合`,"cws.railAria":`組合列表`,"cws.searchPlaceholder":`搜尋組合或目標…`,"cws.noSearchResults":`沒有符合的組合。`,"cws.group.failover":`容錯移轉`,"cws.group.roundRobin":`輪詢`,"cws.group.other":`其他策略`,"cws.targetCount":`{count} 個目標`,"cws.targetCountOne":`1 個目標`,"cws.overviewTitle":`組合`,"cws.overviewBlurb":`在供應商/模型目標之間依容錯移轉、輪詢、加權隨機、最少使用或最早配額重置路由的虛擬模型。`,"cws.count.total":`總計`,"cws.count.failover":`容錯移轉`,"cws.count.roundRobin":`輪詢`,"cws.count.other":`其他`,"cws.howTitle":`工作原理`,"cws.howBody":`在 Codex 中請求組合的公開模型名稱;未設定時預設使用 combo/。OpenCodex 僅在可重試的上游錯誤時切換目標。若沒有可用目標,請求會直接失敗,不會回退到全域性預設供應商。`,"cws.attentionTitle":`需要關注`,"cws.attention.empty":`未配置目標`,"cws.attention.few":`只有一個目標 — 容錯移轉無處可跳`,"cws.attention.catalogOmitted":`未出現在模型目錄中 — 成員能力不完整或不相容(缺少上下文視窗/中繼資料,或模態交集為空)。依別名路由仍可用`,"cws.attention.allTargetsExhausted":`所有已啟用目標的額度均已用盡`,"cws.emptyTitle":`建立第一個組合`,"cws.empty.createDesc":`命名虛擬模型並串聯兩個或多個後端。`,"cws.backToAll":`返回全部組合`,"cws.allCombos":`全部組合`,"cws.copyModel":`複製 ID`,"cws.copied":`已複製`,"cws.tab.config":`配置`,"cws.tab.about":`關於`,"cws.strategy":`策略`,"cws.strategy.failover":`容錯移轉`,"cws.strategy.roundRobin":`輪詢`,"cws.strategy.random":`隨機`,"cws.strategy.leastUsed":`最少使用`,"cws.strategy.resetWindow":`重置視窗`,"cws.strategy.failoverHint":`按順序嘗試目標。若出現可重試錯誤(限流、故障、訂閱門控),則跳到下一個。`,"cws.strategy.roundRobinHint":`按權重確定性地分配流量。將所選目標保留一批成功請求後,再推進到下一個目標。`,"cws.strategy.randomHint":`每個請求按權重比例隨機抽取一個可用目標,請求之間不保持黏性。`,"cws.strategy.leastUsedHint":`將每個請求路由到成功次數最少的可用目標。計數隨代理重啟歸零。`,"cws.strategy.resetWindowHint":`優先選擇配額視窗最早重置的可用目標。缺少配額資料時回退到設定順序。`,"cws.field.id":`組合 ID`,"cws.field.idHint":`客戶端將請求 {model}`,"cws.field.idInternalHint":`組合的內部 ID,建立後仍可修改。`,"cws.field.idHintEdit":`修改 ID 即重新命名組合。客戶端將請求 {model}。`,"cws.field.alias":`公開模型名稱`,"cws.field.aliasPlaceholder":`deepseek-v4-flash 或 vendor/model`,"cws.field.aliasHint":`可選。可填無字首裸名稱、自訂字首(如 vendor/model),或留空使用 combo/。`,"cws.field.stickyLimit":`輪換前的粘性成功次數`,"cws.field.stickyLimitHint":`加權選擇器推進前,將所選目標保留這麼多次成功請求。`,"cws.field.defaultEffort":`預設推理級別`,"cws.field.defaultEffortNone":`無(使用目標預設)`,"cws.field.defaultEffortHint":`僅在客戶端未指定推理級別時使用。客戶端值優先,每個目標會按自身能力進行處理。`,"cws.capability.imageInputUnavailable":`所有已選目標都支援圖片輸入後才可使用。`,"cws.capability.imageInputHint":`所有目標都支援圖片時預設開啟;關閉後僅接受文字。`,"cws.capability.imageInput":`圖片 / 多模態`,"cws.capability.adaptiveEffort":`自適應推理層級`,"cws.capability.adaptiveEffortHint":`關閉:只要有一個目標不支援推理層級,整個組合的選擇器都會消失。開啟:這些目標仍可使用,選擇器保留其餘目標共有的層級。`,"cws.capabilities":`功能`,"cws.field.defaultEffortUnsupported":`此 effort 不在目標的共同階梯中 — 請求時會被忽略或就近對應。`,"cws.field.defaultEffortUnsupportedOption":`不在交集中`,"cws.targets":`目標`,"cws.targets.failoverHint":`順序很重要 — 第一個為主。`,"cws.targets.roundRobinHint":`權重控制確定性的相對選擇;順序用於打破輪換環中的平局。`,"cws.targets.randomHint":`權重控制每次抽取的機率,順序無關緊要。`,"cws.targets.leastUsedHint":`順序僅在使用量相同的目標之間打破平局。`,"cws.targets.resetWindowHint":`配額資料缺失或相同時依順序處理。`,"cws.target.provider":`供應商`,"cws.target.model":`模型`,"cws.target.weight":`權重`,"cws.target.pickProvider":`選擇供應商…`,"cws.target.pickProviderFirst":`請先選擇供應商…`,"cws.target.pickModel":`選擇模型…`,"cws.target.noModels":`該供應商沒有模型`,"cws.target.modelPlaceholder":`模型 ID`,"cws.target.add":`新增目標`,"cws.target.drag":`拖動以重新排序`,"cws.target.moveUp":`上移`,"cws.target.moveDown":`下移`,"cws.quota.available":`可用`,"cws.quota.exhausted":`額度已用盡`,"cws.quota.unknown":`額度未知`,"cws.quota.allExhausted":`所有已啟用目標的額度均已用盡。請選擇其他目標,或等待額度恢復。`,"cws.aboutTitle":`執行時`,"cws.aboutBody":`失敗目標會短暫冷卻並遵循 Retry-After。無效請求與上下文錯誤不會切換。每個目標按自身能力調整推理級別;所有目標耗盡時直接失敗。日誌與用量會保留有序的實際嘗試及每次嘗試的用量。`,"cws.removeConfirmTitle":`刪除 {model}?`,"cws.removeConfirmDesc":`從配置與 Codex 目錄移除該虛擬模型,不會刪除任何供應商。`,"cws.unsavedTitle":`未儲存的更改`,"cws.unsavedDesc":`捨棄對此組合的編輯並繼續?`,"cws.keepEditing":`繼續編輯`,"cws.err.missingId":`需要組合 ID。`,"cws.err.invalidId":`ID 須以字母或數字開頭,僅含字母、數字、點、下劃線或連字元(最多 64)。`,"cws.err.duplicateId":`已存在相同 ID 的組合。`,"cws.err.invalidAlias":`別名僅可包含字母、數字、點、下劃線或連字元,最多一個“/”分段。`,"cws.err.aliasReservedNamespace":`別名不得使用保留的“combo/”名稱空間。`,"cws.err.aliasNativeFamily":`不允許使用 OpenAI 原生家族裸別名(gpt-*、o1-*、o3-*、o4-*、codex-*)。`,"cws.err.duplicateAlias":`另一個組合已使用該別名。`,"cws.err.noTargets":`至少新增一個目標。`,"cws.err.incompleteTarget":`每個目標都需要供應商和模型。`,"cws.target.disabled":`{name}(已停用)`,"cws.err.reservedNamespace":`建立組合前,請先重新命名名為 combo 的實體供應商。`,"cws.err.providerCollision":`組合 ID 與已配置的供應商名稱衝突。`,"cws.err.unknownProvider":`每個目標都必須使用已配置的供應商。`,"cws.err.duplicateTarget":`同一供應商/模型目標只能出現一次。`,"cws.err.invalidStickyLimit":`粘性成功次數必須是 1 到 100 的整數。`,"cws.err.invalidWeight":`每個輪詢權重必須是 1 到 10000 的整數。`,"cws.err.noEnabledTarget":`至少一個目標必須使用已啟用的供應商。`,"claude.tabsLabel":`Claude 客戶端`,"claude.tabCode":`Code`,"claude.tabDesktop":`Desktop`,"claudeDesktop.title":`Claude Desktop`,"claudeDesktop.subtitle":`將每個 Claude 模型系列路由到埠 {port} 上的可用模型。`,"claudeDesktop.importJson":`匯入 JSON`,"claudeDesktop.exportJson":`匯出 JSON`,"claudeDesktop.loading":`正在載入 Claude Desktop 配置…`,"claudeDesktop.loadFail":`無法載入 Claude Desktop 配置。`,"claudeDesktop.retry":`重試`,"claudeDesktop.saveFailed":`無法儲存 Claude Desktop 配置。`,"claudeDesktop.applyFailed":`配置已儲存,但無法套用。`,"claudeDesktop.updateFailed":`Claude Desktop 更新失敗。`,"claudeDesktop.savedApplied":`配置已儲存並套用到 Claude Desktop。`,"claudeDesktop.savedAppliedAnnounce":`Claude Desktop 配置已儲存並套用。`,"claudeDesktop.saved":`配置已儲存。`,"claudeDesktop.savedAnnounce":`Claude Desktop 配置已儲存。`,"claudeDesktop.exported":`配置已匯出為 JSON。`,"claudeDesktop.importExpected":`需要版本 1 的 Claude Desktop 配置。`,"claudeDesktop.importReady":`JSON 已匯入。請檢查草稿,然後儲存並套用。`,"claudeDesktop.importedAnnounce":`配置 JSON 已匯入。可檢查尚未儲存的更改。`,"claudeDesktop.importInvalid":`所選檔案不是有效配置。`,"claudeDesktop.importFailed":`匯入失敗。{error}`,"claudeDesktop.moved":`已將 {route} 移動到 {family}。`,"claudeDesktop.unsaved":`有未儲存的更改`,"claudeDesktop.upToDate":`配置已是最新`,"claudeDesktop.saving":`正在儲存…`,"claudeDesktop.applying":`正在套用…`,"claudeDesktop.saveApply":`儲存並套用`,"claudeDesktop.emptyTitle":`沒有可用模型`,"claudeDesktop.emptyHint":`請新增或啟用供應商,然後返回分配 Claude Desktop 路由。`,"claudeDesktop.assignmentsLabel":`Claude 模型系列分配`,"claudeDesktop.family.opus":`Opus`,"claudeDesktop.family.fable":`Fable`,"claudeDesktop.family.sonnet":`Sonnet`,"claudeDesktop.family.haiku":`Haiku`,"claudeDesktop.modelCountOne":`{count} 個模型`,"claudeDesktop.modelCountMany":`{count} 個模型`,"claudeDesktop.chooseDefault":`選擇預設模型`,"claudeDesktop.temporaryDefault":`臨時預設模型`,"claudeDesktop.laneEmpty":`將模型拖到這裡,或使用移動控制元件。`,"claudeDesktop.laneNoMatch":`該系列中沒有與搜尋符合的模型。`,"nav.grok":`Grok`,"grok.title":`Grok Build`,"grok.subtitle":`opencodex 已註冊到你的 Grok 配置中的模型。`,"grok.loading":`正在載入 Grok 狀態…`,"grok.loadFail":`無法讀取 Grok 配置。`,"grok.notConfiguredTitle":`Grok Build 尚未接入`,"grok.notConfiguredHint":`安裝 Grok 後重新啟動代理,opencodex 會把託管塊寫入:`,"grok.endpoint":`端點`,"grok.colModel":`模型`,"grok.colAlias":`Grok 別名`,"grok.colContext":`上下文`,"grok.groupNative":`原生模型`,"grok.groupRouted":`路由模型`,"grok.enabledCount":`已註冊 {on}/{total}`,"grok.saved":`選擇已儲存。`,"grok.savedApplied":`選擇已儲存並寫入 Grok 配置。`,"grok.saveFailed":`無法儲存 Grok 選擇。`,"grok.applyFailed":`選擇已儲存,但無法更新 Grok 配置。`,"grok.applySkipped":`選擇已儲存,Grok 配置未更改。`,"grok.saveApply":`儲存並套用`,"grok.saving":`儲存中…`,"grok.applying":`套用中…`,"grok.unsaved":`未儲存的更改`,"grok.upToDate":`選擇已是最新`,"grok.toggleModel":`將 {id} 註冊到 Grok`,"claudeDesktop.available":`可用`,"claudeDesktop.defaultBadge":`預設`,"claudeDesktop.supports1m":`1M`,"claudeDesktop.unavailable":`不可用`,"claudeDesktop.contextM":`{n}M 上下文`,"claudeDesktop.contextK":`{n}k 上下文`,"claudeDesktop.contextUnknown":`上下文未知`,"claudeDesktop.alias":`別名`,"claudeDesktop.useAsDefault":`設為 {family} 預設模型`,"claudeDesktop.moveTo":`移動到`,"claudeDesktop.move":`移動`,"claudeDesktop.status.applied":`已套用到 Desktop`,"claudeDesktop.status.stale":`配置已更改 — 需重新套用`,"claudeDesktop.status.notApplied":`未套用`,"claudeDesktop.status.notActiveProfile":`Desktop 正在使用其他配置 — 請重新套用`,"claudeDesktop.health.lastRequest":`最後請求`,"claudeDesktop.health.stats":`{count} 請求 / {errors} 錯誤`,"claudeDesktop.effort.supported":`effort`,"claudeDesktop.effort.displayOnly":`effort (僅顯示)`,"startup.backToDashboard":`返回儀表板`,"startup.repair":`修復`,"startup.repairing":`正在修復…`,"startup.serviceRepaired":`背景服務修復成功。`,"startup.shimRepaired":`Codex 啟動器 shim 修復成功。`,"sub.workspace.addToFeatured":`將 {m} 加入精選`,"sub.workspace.allModels":`所有模型`,"sub.workspace.featuredFull":`精選列表已滿(最多 5 個)`,"sub.workspace.mainAria":`子代理模型詳情`,"sub.workspace.notFeatured":`未設為精選`,"sub.workspace.priority":`優先順序`,"sub.workspace.removeFromFeatured":`將 {m} 自精選移除`,"sub.workspace.selectModel":`選擇模型`,"sub.workspace.selectModelDesc":`從列表選擇模型以查看詳情,並設為 spawn_agent 的精選模型。`,"sub.workspace.selector":`公開選擇器`,"logs.badge.grok":`Grok`,"logs.tokens.contextTotal":`作用中上下文`,"usage.workspace.report":`用量報告`,"usage.workspace.sections":`用量分區`,"storage.rescanned":`掃描完成。`,"storage.snapshot.lastScan":`上次掃描`,"storage.snapshot.scanning":`掃描中…`,"storage.snapshot.unavailable":`尚無掃描。`,"storage.cleanupCard.title":`釋放空間`,"storage.cleanupCard.tabs":`清理選項`,"storage.cleanupCard.tab.policy":`原則`,"storage.cleanupCard.tab.quarantine":`隔離區`,"storage.cleanup.noArchives":`沒有可清理的封存工作階段。`,"storage.workspace.overview":`概覽`,"storage.workspace.selectBucket":`從列表選擇儲存區以查看明細。`,"storage.policy.trigger":`觸發條件`,"storage.policy.thresholdInc":`提高閾值`,"storage.policy.thresholdDec":`降低閾值`,"storage.policy.percentInc":`提高百分比`,"storage.policy.percentDec":`降低百分比`,"storage.policy.reduceInc":`提高縮減目標`,"storage.policy.reduceDec":`降低縮減目標`,"pws.dashboard.noRateLimits":`尚無速率限制資料`,"codexAuth.autoSwitchThresholdInc":`提高切換閾值`,"codexAuth.autoSwitchThresholdDec":`降低切換閾值`,"accountPool.stickyLimitInc":`提高黏性上限`,"accountPool.stickyLimitDec":`降低黏性上限`,"api.activeKeysLoading":`有效金鑰`,"api.workspace.details":`API 金鑰詳情`,"api.workspace.keyDetails":`金鑰詳情`,"api.workspace.keyPrefix":`金鑰前綴`,"api.workspace.deleteKey":`刪除金鑰`,"api.workspace.deleteConfirm":`確定要刪除此金鑰嗎?此操作無法復原。`,"api.workspace.usageExamples":`用法範例`,"api.copyUrlHint":`點擊以複製 URL`,"api.urlCopied":`已複製 URL`,"api.copyExampleHint":`點擊以複製範例`,"api.exampleCopied":`已複製範例`,"claude.workspace.settings":`設定`,"sidebar.star":`在 GitHub 上加星`,"sidebar.starred":`已在 GitHub 加星`,"sidebar.starUnauthenticated":`開啟 GitHub 加星(gh CLI 未登入)`,"sidebar.starFailed":`無法透過 gh 加星,改為開啟 GitHub。`,"sidebar.updateAvailable":`有可用更新:{version}`,"sidebar.checkUpdate":`檢查更新`,"dash.mem.jsHeapArena":`arena {total}`,"dash.mem.pressure":`相對於警告閾值`,"dash.mem.pressureOf":`警告閾值的 {pct}%`,"dash.mem.pressureUnknown":`未回報閾值`,"dash.injectionManage":`開啟設定`,"dash.syncModelsHint":`根據已連接的供應商重寫 Codex 的模型目錄。`,"dash.syncRun":`立即同步`,"sub.settings":`設定`,"sub.sections":`子代理分區`,"sub.delegation.model":`優先調用的模型`,"sub.delegation.modelHint":`Codex 分派工作時最先調用的模型。上面的推薦是可調用的名單,這裡選的是其中第一順位。`,"debug.loadFailed":`無法載入偵錯設定。`,"provider.name.volcengine":`Volcengine Ark`,"provider.name.volcengineCodingPlan":`Volcengine Ark Coding Plan`,"provider.name.volcengineAgentPlan":`Volcengine Ark Agent Plan`,"usage.range.available":`可用歷史紀錄`,"usage.historyTruncated":`總計僅涵蓋可用歷史紀錄,因為較舊的用量未被載入。`,"usage.historyTruncatedWindow":`已載入紀錄的請求開始時間介於 {start} 到 {end} 之間。受讀取上限限制,檔案較前的項目已被略過,所選期間可能不完整。`,"codexAuth.autoSwitchQuotaDesc":`配額:使用率達 {threshold}% 或以上時,下一個請求可能移至用量較低的合格帳號,包括已綁定的任務;Go/Free 僅使用 30 天。`,"codexAuth.autoSwitchQuotaOffDesc":`基於用量的主動切換已關閉。新增/未綁定分派與故障恢復仍然適用。`,"codexAuth.autoSwitchRoundRobinDesc":`輪詢分派不使用此閾值;它會繼續輪換新增/未綁定的任務。`,"codexAuth.autoSwitchFillFirstDesc":`優先填滿:{threshold}% 是新增/未綁定任務的耗盡點;健康的已綁定任務保留其帳號。`,"codexAuth.autoSwitchFillFirstOffDesc":`優先填滿對新增/未綁定任務沒有用量耗盡點;冷卻、重新驗證與故障恢復仍可改變路由。`,"codexAuth.failureRecoveryNote":`故障恢復是獨立的:請求在輸出前被拒絕(429/402)、冷卻、重新驗證、排除或已設定的暫時容錯移轉,可能選擇另一個合格帳號。`,"accountPool.strategyHintQuota":`配額也可以在跨越用量閾值後,於下次請求時重新綁定現有任務。`,"accountPool.strategyHintRoundRobin":`輪詢僅輪換沒有有效綁定的任務;用量閾值不會改變正常輪換。`,"accountPool.strategyHintFillFirst":`優先填滿將閾值用作未綁定任務的耗盡點;健康的已綁定任務保持親和性。`,"accountPool.unboundDefinition":`新增/未綁定任務表示沒有當前帳號綁定的請求;現有可見任務在代理或親和性重設後可能變成未綁定。`,"api.workspace.sections":`API 分區`,"api.section.keys":`金鑰`,"api.section.connect":`連接`,"api.section.endpoints":`端點`,"api.section.models":`模型`,"api.section.examples":`範例`,"api.clientConfig.title":`用戶端設定`,"api.clientConfig.rowsLabel":`連接用戶端`,"api.clientConfig.details":`詳情`,"api.clientConfig.detailsAria":`{client} 設定詳情`,"api.clientConfig.copyAria":`複製 {client} 設定 JSON`,"api.clientConfig.downloadAria":`下載 {client} 設定`,"api.clientConfig.rowMeta":`{destination} · {count} 個模型`,"api.clientConfig.rowError":`無法建構 {client} 設定。`,"api.clientConfig.copiedAnnounceClient":`{client} 設定 JSON 已複製到剪貼簿。`,"api.clientConfig.clientOpencode":`OpenCode`,"api.clientConfig.clientPi":`Pi`,"api.clientConfig.copy":`複製 JSON`,"api.clientConfig.download":`下載`,"api.clientConfig.loading":`正在建構用戶端設定…`,"api.clientConfig.jsonLabel":`{client} 設定 JSON`,"api.clientConfig.destination":`目標檔案`,"api.clientConfig.envHint":`啟動前設定金鑰`,"api.clientConfig.mergeWarning":`將此合併到目標檔案。替換它會丟失您的其他供應商和 MCP 設定。`,"api.clientConfig.modelCount":`已匯出 {count} 個模型`,"api.clientConfig.missingLimits":`{total} 個模型中有 {count} 個未附帶上下文限制;用戶端會套用自己的預設值。`,"api.clientConfig.noKeyYet":`{env} 背後尚無金鑰。在非回送環境使用此設定前,請先在上面產生金鑰。`,"api.clientConfig.loadFailed":`無法讀取模型清單,因此未產生用戶端設定。`,"api.clientConfig.copiedAnnounce":`用戶端設定 JSON 已複製到剪貼簿。`,"api.clientConfig.copyFailed":`無法複製用戶端設定 JSON。`,"api.clientConfig.downloadedAnnounce":`已下載 {filename}。目前尚未變更任何內容 — 請自行將其合併到 {destination}。`,"api.clientConfig.whereDisclosure":`此檔案的放置位置`,"api.clientConfig.whereBody":`上述目標是全域路徑。工作目錄中的專案本地設定檔優先於它,且用戶端從設定中指定的環境變數讀取金鑰 — 絕不從此檔案讀取。`,"api.attribution.totalRequestsAvailable":`可用歷史紀錄中的請求`,"api.attribution.sinceAvailable":`可用歸因起始自`,"uptime.day":`天`,"uptime.hour":`小時`,"uptime.minute":`分鐘`,"uptime.second":`秒`,"auth.adminTokenTitle":`OpenCodex 管理員金鑰 (OPENCODEX_ADMIN_AUTH_TOKEN)`,"auth.adminAccountLabel":`帳號`,"auth.adminTokenFieldLabel":`管理員金鑰`,"auth.adminTokenRejected":`該管理員金鑰被拒絕。請檢查後再試一次。`,"auth.adminTokenUnavailable":`無法驗證管理員金鑰。請再試一次。`,"lang.nativeName":`繁體中文`,"provider.name.commandCodeAuth":`Command Code - Auth`,"provider.name.commandCodeApi":`Command Code - API`,"routing.title":`路由智能 (beta)`,"routing.subtitle":`策略設定檔、試運行評估,以及有來源依據的路由分析。`,"routing.loadFailed":`無法載入路由資料`,"routing.empty":"尚未配置路由策略。請在 config.json 中新增 `routingProfiles`。","routing.revision":`rev`,"routing.detail":`設定檔`,"routing.createProfile":`建立設定檔`,"routing.dryRunError":`試運行失敗(HTTP {status})`,"routing.removeConfirm":`移除設定檔 {id}?`,"routing.unknownEvidence.allow":`允許`,"routing.unknownEvidence.penalize":`懲罰`,"routing.unknownEvidence.exclude":`排除`,"routing.removeCandidate":`移除候選 {provider}/{model}`,"routing.candidates":`候選`,"routing.require":`嚴格要求`,"routing.optimize":`最佳化權重`,"routing.limits":`限制`,"routing.unknownEvidence":`未知證據策略`,"routing.compatibility.title":`相容性策略`,"routing.compatibility.enabled":`要求 Compatibility Lab 證據`,"routing.compatibility.requiredSuites":`必要套件`,"routing.compatibility.loadingCatalog":`正在載入 Lab 目錄…`,"routing.compatibility.catalogUnavailable":`Lab 目錄不可用 — 請在 config.json 中手動輸入套件 ID。`,"routing.compatibility.layer.protocol_conformance":`協定一致性`,"routing.compatibility.layer.live_route_compatibility":`即時路由相容性`,"routing.compatibility.minStatus":`最低相容性狀態`,"routing.none":`無`,"routing.unavailable":`–`,"routing.dryRun":`試運行評估`,"routing.dryRunContext":`請求上下文視窗(tokens)`,"routing.dryRunTools":`請求需要工具`,"routing.dryRunImage":`請求需要圖片輸入`,"routing.dryRunStructured":`請求需要結構化輸出`,"routing.dryRunRun":`評估候選`,"routing.candidate":`候選`,"routing.eligible":`合格`,"routing.exclusions":`排除項目`,"routing.costCap":`成本上限`,"routing.capOutcome.satisfied":`未超過上限`,"routing.capOutcome.exceeded":`超過上限`,"routing.capOutcome.unknown-allowed":`未知(允許)`,"routing.capOutcome.unknown-excluded":`未知(排除)`,"routing.exclusion.capability-unsatisfied":`能力未滿足`,"routing.exclusion.unknown-capability":`能力未知`,"routing.exclusion.cost-limit":`超過成本上限`,"routing.exclusion.cost-limit-unknown":`成本未知(在上限內)`,"routing.exclusion.cooldown":`冷卻中`,"routing.exclusion.unknown-health":`健康狀態未知`,"routing.exclusion.unknown-quota":`額度未知`,"routing.exclusion.unknown-price":`價格未知`,"routing.exclusion.other":`排除:{code}`,"routing.score":`分數`,"routing.selected":`已選取`,"routing.yes":`是`,"routing.no":`否`,"routing.analytics":`路由分析`,"routing.analyticsTotal":`請求`,"routing.analyticsSuccessRate":`成功率`,"routing.analyticsFallbackRate":`備援`,"routing.analyticsP50":`p50`,"routing.analyticsP95":`p95`,"routing.analyticsP99":`p99`,"routing.analyticsCooldown":`冷卻失敗`,"routing.analyticsConfidence":`可信度`,"routing.analyticsTruncated":`已截斷的歷史`,"routing.analyticsRequests":`請求`,"routing.analyticsEmpty":`尚無分析資料 — 請先傳送一些請求。`,"dash.updateVersionTransition":`{currentVersion} -> {latestVersion}。`,"prov.loginSameAccount":`仍是同一個 {provider} 帳號 — 請在瀏覽器中切換帳號後,再試一次「新增帳號」。`,"models.tab.catalog":`模型`,"models.tab.combos":`組合`,"models.tab.compatibility":`相容性`,"models.tab.routing":`路由 (beta)`,"models.tabsLabel":`模型介面`,"models.subtitle.combos":`將多個模型合成一個 id 來回答。用容錯移轉串接目標,或用均衡策略分攤負載。`,"models.subtitle.compatibility":`來自實驗室投影證據的唯讀相容性判定矩陣。`,"models.subtitle.routing":`原則設定檔、dry-run 評估,以及有來源依據的路由分析。`,"models.contextSettings":`自訂視窗`,"models.contextSettingsTitle":`自訂視窗 — {provider}`,"models.contextDefault":`供應商預設`,"models.contextModel":`模型`,"models.contextModelOverride":`模型覆寫`,"models.contextHint":`已經知道視窗時,在這裡手寫 Codex 實際視窗。上游沒回報視窗就用這個值;上游回報更大視窗才壓低。留空則使用供應商的「預設視窗 / 上限」;那個開關沒開時才回退 128k。`,"models.contextAutomatic":`自動偵測`,"models.contextSaved":`上下文視窗已更新 — 將在下一個 Codex 回合生效。`,"models.contextUnchanged":`沒有需要儲存的上下文視窗變更。`,"models.contextSaveFailed":`儲存上下文視窗失敗`,"models.contextInvalid":`上下文視窗必須是正整數`,"logs.detail.route.section":`路由決策`,"logs.detail.route.kind":`路由類型`,"logs.detail.route.profile":`原則設定`,"logs.detail.route.selected":`已選取`,"logs.detail.route.candidates":`候選`,"logs.detail.route.unknown":`此請求沒有記錄路由追蹤(追蹤前的列)。`,"logs.detail.source.user":`供應商設定的價格覆蓋`,"logs.detail.attempt.recovery.transient5xx":`暫時性 5xx`,"logs.detail.attempt.recovery.connectionReset":`連線重設`,"logs.detail.attempt.recovery.oauth401":`OAuth 重新驗證`,"logs.detail.attempt.recovery.key429":`金鑰被限流 (429)`,"logs.detail.attempt.recovery.rateLimit429":`被限流 (429)`,"logs.detail.attempt.recovery.anthropicOauth429":`Anthropic OAuth 被限流 (429)`,"logs.detail.attempt.recovery.image413":`圖片承載過大 (413)`,"logs.detail.attempt.recovery.emptyCompletion":`空白完成重試`,"logs.detail.attempt.recovery.unknown":`未知的復原原因`,"logs.detail.estimate.provider_cost_overlay":`已使用供應商設定的價格覆蓋。`,"logs.detail.estimate.priority_lower_bound":`無法取得已確認的 Priority 價格;目前顯示的估算是已知下限。`,"pws.cockpitImportDescription":`從此裝置匯入 Cockpit Tools Antigravity JSON 匯出檔。不會顯示檔案內容。`,"pws.cockpitImportFileLabel":`Cockpit Tools Antigravity JSON 匯出檔`,"pws.cockpitImportChooseFile":`選擇 JSON 檔案`,"pws.cockpitImporting":`匯入中…`,"pws.cockpitImportInvalid":`選取的檔案不是有效的 JSON 匯出檔,或檔案過大。`,"pws.cockpitImportFailed":`無法完成帳號匯入。`,"pws.cockpitImportComplete":`匯入完成:已匯入 {imported} 個、已更新 {updated} 個、失敗 {failed} 個、不支援 {unsupported} 個。`,"pws.accountModeSaved":`帳號模式已儲存。`,"pws.accountModeFailed":`無法切換帳號模式。`,"pws.accountModeConfirm":`要切換 OpenAI 帳號模式嗎?進行中的對話將重新指派到另一種模式的帳號集合,配額用量將依新模式追蹤。`,"pws.capacity.estimate":`依設定權重的帳號池估算`,"pws.capacity.currentAccount":`目前有效帳號`,"pws.capacity.nextRecovery":`下一次容量復原`,"pws.capacity.recoveryShare":`+{percent}% 帳號池容量`,"pws.capacity.incomplete":`覆蓋不完整:已排除 {excluded} 個帳號`,"pws.capacity.uncalibratedPlan":`{count} 個帳號使用未校準方案,以基準席次權重計入,因此此估算可能偏保守`,"pws.capacity.partial":`部分視窗覆蓋:{count} 個帳號未回報所有顯示的限額視窗`,"pws.capacity.windowPartial":`部分`,"pws.capacity.windowPartialA11y":`{window}:帳號覆蓋不完整`,"pws.connectionNotApplicable":`不適用 — 此供應商使用靜態模型目錄。`,"nav.integrations":`整合`,"integrations.subtitle":`將客戶端連線到 opencodex、管理憑證,並還原客戶端設定。`,"integrations.tabsLabel":`整合表面`,"integrations.tab.overview":`總覽`,"integrations.tab.keys":`API 金鑰`,"integrations.tab.codex":`Codex`,"integrations.tab.claude":`Claude`,"integrations.tab.grok":`Grok Build`,"integrations.tab.opencode":`OpenCode`,"integrations.tab.pi":`Pi`,"integrations.tab.omp":`OMP`,"integrations.tab.hermes":`Hermes`,"integrations.tab.openclaw":`OpenClaw`,"integrations.tab.kimi":`Kimi Code`,"integrations.tab.gajae":`Gajae Code`,"integrations.tab.dsh":`DSH`,"integrations.tab.mcode":`MiniMax Code`,"integrations.tab.zcode":`ZCode`,"integrations.tab.prime":`Prime Agent`,"integrations.tab.aside":`Aside`,"integrations.codex.title":`Codex CLI`,"integrations.codex.body":`Codex 連線由代理服務管理。啟動 opencodex 時套用;停止服務時還原原生路由。`,"integrations.codex.openService":`開啟服務控制`,"integrations.state.notInstalled":`未安裝`,"integrations.state.unknown":`檢查中…`,"integrations.detail.codexRouted":`Codex 請求經由此代理`,"integrations.detail.codexAbsent":`Codex 尚未經由此代理路由`,"integrations.detail.keyCount":`已簽發 {count} 個金鑰`,"integrations.detail.keyNone":`尚未簽發金鑰`,"integrations.detail.keyChecking":`檢查中…`,"integrations.detail.keyUnavailable":`無法取得金鑰狀態`,"integrations.detail.claudeOff":`連線已關閉`,"integrations.detail.desktopCurrent":`Desktop 正在使用此設定檔`,"integrations.detail.desktopStale":`設定檔在套用後被變更`,"integrations.detail.desktopNotServed":`設定檔存在,但 Desktop 使用的是另一個`,"integrations.detail.desktopAbsent":`未套用任何設定檔`,"integrations.detail.desktopDesiredOff":`Claude Desktop 整合已關閉`,"integrations.detail.desktopDesiredOffCleanupPending":`Claude Desktop 仍在使用閘道,清理尚未完成`,"integrations.detail.desktopDesiredOnNotApplied":`整合已開啟,但 Desktop 未使用閘道設定檔`,"integrations.detail.desktopSelectedElsewhere":`Desktop 正在使用其他設定檔`,"integrations.detail.desktopProfileDrift":`選取的 Desktop 設定檔已變更`,"integrations.detail.desktopObservedUnsafe":`無法安全變更選取的 Desktop 設定檔`,"integrations.detail.desktopNotInstalled":`未安裝 Claude Desktop 設定程式庫`,"integrations.detail.grokModels":`已接入 {count} 個模型`,"integrations.detail.grokAbsent":`設定中沒有 opencodex 區塊`,"integrations.dialog.grok.title":`要停用 Grok Build 整合嗎?`,"integrations.dialog.grok.changes":`只會從 {path} 移除由 opencodex 標記的區塊。區塊之外寫入的內容將保持不變。`,"integrations.dialog.grok.breakage":`停用後,Grok Build 中的 opencodex 模型別名將消失。透過 xAI 帳號使用的模型不受影響。`,"integrations.dialog.grok.undo":`如果 opencodex 正在 loopback 位址上執行,重新啟用時會根據目前可用的模型寫入新的區塊。`,"integrations.dialog.grok.confirm":`停用`,"integrations.dialog.desktop.title":`要停用 Claude Desktop 整合嗎?`,"integrations.dialog.desktop.changes":`如果 {path} 包含 opencodex 管理的閘道設定檔,Desktop 會先選取新的免憑證標準設定檔,再移除舊設定檔與其備份。`,"integrations.dialog.desktop.breakage":`Claude Desktop 將恢復為標準 Claude,不再使用經由 opencodex 路由的模型。`,"integrations.dialog.desktop.undo":`重新啟用時,會根據你儲存的模型指派重新產生 opencodex 設定檔。`,"integrations.dialog.desktop.restart":`Claude Desktop 僅在啟動時讀取此設定。請完全結束並重新開啟 Desktop,變更才會生效。`,"integrations.dialog.desktop.confirm":`停用`,"integrations.native.msg.nonLoopbackRemoved":`只有當 opencodex 在 loopback 位址上執行時,才能自動註冊 Grok Build。先前指向 loopback 的區塊已移除。`,"integrations.native.msg.nonLoopbackRemovedNoop":`只有當 opencodex 在 loopback 位址上執行時,才能自動註冊 Grok Build。沒有需要移除的舊區塊。`,"integrations.native.msg.nonLoopbackSuperseded":`只有當 opencodex 在 loopback 位址上執行時,才能自動註冊 Grok Build。在此期間,另一個程序寫入了新的區塊,因此檔案中目前的區塊並非由本次請求建立。`,"integrations.native.error.orphanedMarker":`{path} 有 opencodex 開始標記但沒有結束標記。由於 opencodex 無法判斷其區塊的結束位置,因此未變更檔案。`,"integrations.native.error.homeMismatch":`已安裝服務的 home 與目前的 home 不符,因此未變更檔案。`,"integrations.native.error.notInstalled":`尚未安裝 Grok Build,因此沒有可變更的內容。`,"integrations.native.error.configBusy":`設定正在其他地方儲存中,無法變更。請稍後再試。`,"integrations.native.error.desktopUnsafeMetadata":`無法安全讀取 {path} 中的 Claude Desktop 中繼資料,因此未變更其設定庫。`,"integrations.native.error.desktopCleanupIncomplete":`Claude Desktop 已指向標準模式,但仍有舊的 opencodex 憑證檔案殘留於:{paths}。`,"integrations.native.msg.desktopDisabled":`Claude Desktop 整合已停用。`,"integrations.native.msg.desktopEnabled":`Claude Desktop 整合已啟用。`,"integrations.state.absent":`未套用`,"integrations.state.current":`已套用`,"integrations.state.stale":`需要更新`,"integrations.state.conflict":`衝突`,"integrations.state.unsafe":`無法驗證`,"integrations.summary.detected":`偵測到的用戶端`,"integrations.summary.applied":`已設定的用戶端`,"integrations.summary.stale":`需要更新`,"integrations.summary.lastChange":`上次變更`,"integrations.summary.disableAll":`全部停用…`,"integrations.onboarding":`套用時會先儲存備份,再寫入一個 opencodex 供應商區塊。停用只會移除該區塊,且可從保留的快照還原。`,"integrations.empty.title":`未偵測到已安裝的用戶端`,"integrations.empty.body":`安裝受支援的用戶端,然後返回此處套用 opencodex。`,"integrations.action.apply":`套用`,"integrations.action.disable":`停用`,"integrations.action.refresh":`更新`,"integrations.action.settings":`設定`,"integrations.action.manageKeys":`管理金鑰`,"integrations.action.restore":`還原…`,"integrations.action.undo":`復原`,"integrations.action.restorePoint":`還原到此時間點…`,"integrations.action.snapshotExpired":`備份已過期`,"integrations.rollback.title":`還原中心`,"integrations.rollback.empty":`尚無套用紀錄`,"integrations.rollback.emptyBody":`每次成功寫入前都會先保留一份寫入前快照。`,"integrations.catalog.title":`用戶端`,"integrations.rollback.older":`較早的操作`,"integrations.rollback.showMore":`再顯示 {n} 個`,"integrations.rollback.failed":`無法載入還原紀錄。`,"integrations.restore.title":`要還原此快照?`,"integrations.restore.body":`系統會先備份目前的檔案,再用所選快照取代它。`,"integrations.restore.driftTitle":`偵測到較新的編輯`,"integrations.restore.driftBody":`此快照之後的變更會先備份,然後再取代檔案。`,"integrations.restore.confirm":`還原`,"integrations.restore.confirmDrift":`備份較新的編輯並還原`,"integrations.restore.pending":`正在還原…`,"integrations.restore.manual":`自動還原失敗:{reason}。請從 {path} 手動還原。`,"integrations.error.load":`無法載入整合狀態。`,"integrations.error.stale":`最近的重新整理失敗。下列值可能已過期。`,"integrations.error.busy":`此用戶端的另一項變更仍在進行中。請稍後再試。`,"integrations.error.conflict":`opencodex 寫入後設定又變更了。未移除任何內容。`,"integrations.error.unsafe":`無法安全地變更設定。`,"integrations.error.generic":`整合變更失敗。已保留你先前的狀態。`,"integrations.error.nonLoopback":`{client} 只能連線到 localhost 上的 proxy——其設定沒有位置可放入遠端繫結所需的准入標頭,手動撰寫也不會有幫助。請改以隧道或本機轉發器提供 loopback 存取。`,"integrations.status.installed":`已安裝`,"integrations.status.notInstalled":`未安裝`,"integrations.status.appliedAt":`已套用`,"integrations.status.backup":`備份`,"integrations.status.lastRestore":`上次還原`,"integrations.status.unknown":`未知`,"integrations.bulk.title":`要停用已套用的用戶端整合?`,"integrations.bulk.body":`只會移除屬於 opencodex 的區塊。每個用戶端都會先保留一份寫入前快照。`,"integrations.bulk.partial":`部分用戶端無法停用:{clients}`,"integrations.bulk.success":`已套用的用戶端整合已停用。`,"integrations.retention.degraded":`備份清理進度落後;磁碟上可能仍有較舊的備份。`,"integrations.error.residual":`檔案可能處於中間狀態:{message} 請從 {path} 還原。`,"integrations.error.recover":`{message} 備份位於 {path}。`,"integrations.kind.apply":`已套用`,"integrations.kind.disable":`已停用`,"integrations.kind.refresh":`已更新`,"integrations.kind.restore":`已還原`,"integrations.kind.overwrite":`已覆寫`,"integrations.dialog.overwrite.title":`替換這個設定檔中的區塊?`,"integrations.dialog.overwrite.changesUnowned":`{path} 中 opencodex 需要寫入的位置被一個並非我們寫入的區塊佔用。套用會將其替換為 opencodex 寫入的區塊。`,"integrations.dialog.overwrite.changesForeign":`{path} 中 opencodex 區塊內你所做的修改會被捨棄,並替換為 opencodex 寫入的區塊。`,"integrations.dialog.overwrite.breakage":`該區塊原本設定的內容將不再生效。檔案其他位置保持不變。`,"integrations.dialog.overwrite.undo":`會先儲存快照,因此這次操作會出現在下方的還原清單中,可以復原。`,"integrations.dialog.overwrite.confirm":`替換`,"integrations.action.overwrite":`替換`,"integrations.semantics.opencode":`僅適用於直接從磁碟啟動;ocx opencode 的環境注入優先。`,"integrations.semantics.pi":`適用於新工作階段。`,"integrations.semantics.omp":`重新啟動 OMP 以載入模型目錄。`,"integrations.semantics.hermes":`適用於新工作階段。`,"integrations.semantics.openclaw":`立即套用到正在執行的閘道。`,"integrations.semantics.kimi":`重新啟動或執行 /reload 以套用(v2 會監視該檔案)。`,"integrations.semantics.gajae":`在新工作階段中或開啟 /model 時生效。`,"integrations.semantics.dsh":`OpenCodex 只管理 $DSH_HOME/settings.yaml 中的 llm-pi-ai.providers.opencodex。DSH 會熱重載該 provider;你的預設模型與 deepseek-official 維持不變。目前僅支援 loopback,且不會寫入真實憑證。`,"integrations.semantics.mcode":`僅管理 custom_provider.opencodex,不會變更預設模型或 MiniMax 登入狀態。`,"integrations.semantics.zcode":`僅管理 ~/.zcode/v2/config.json 中的 provider.opencodex,不會變更 Z.ai 登入狀態或其他供應商。變更後請重新啟動 ZCode。`,"integrations.semantics.prime":`僅管理 Prime Agent 的 models.json 中的 providers.opencodex;預設位於 ~/.prime/agent,若設定 PRIME_AGENT_CODING_AGENT_DIR 則以其為準。不會變更其他供應商或模型覆寫設定。對新工作階段生效。`,"integrations.semantics.aside":`僅管理已登入帳號的 Aside models.json 中的 providers.opencodex,位於 ~/.aside/u/<帳號>。不會變更其他供應商。Aside 在執行時會重寫該檔案,因此套用後請完全結束並重新開啟 Aside。`,"codexAuth.pinned":`已固定`,"codexAuth.pinnedHint":`你手動選取了此帳號,因此較高的選擇順序不會越過它。此固定會持續到該帳號用盡、你改選其他帳號,或你變更任一選擇順序為止。`,"codexAuth.requestUserInput":`在 Default 模式中要求輸入`,"codexAuth.requestUserInputDesc":`讓 Codex 暫停 Default 模式工作階段,並使用 request_user_input 工具向你提問。`,"codexAuth.requestUserInputUpdated":`功能旗標已更新 - 適用於新工作階段。`,"codexAuth.requestUserInputUpdatedRestart":`功能旗標已更新 - 適用於新工作階段。請重新啟動 Codex 應用程式以套用。`,"codexAuth.requestUserInputUpdateFailed":`無法更新功能旗標。未做任何變更。`,"codexAuth.requestUserInputLoadFailed":`無法從 config.toml 讀取功能旗標。`,"codexAuth.accountPickerTitle":`從模型選擇器指定特定的 Codex 帳號`,"codexAuth.accountPickerOffDesc":`啟用後,一般 GPT 選擇器選項會替換為每個帳號選擇器對應的選項,讓你可以不需登出就為對話指定確切帳號。關閉此功能不會移除任何帳號。`,"codexAuth.accountPickerOnDesc":`每個選擇器都是某個已儲存帳號的公開標籤。選擇後,該對話會鎖定到對應的帳號:永遠不會輪換或容錯移轉,也不會改變目前 Pool 的帳號。`,"codexAuth.accountPickerCompatibility":`內建的 Codex App 登入有自己的選擇器;產生的對應通常稱為 main,需要時會使用 main-2 這類避免衝突的後綴。新增的帳號會獲得穩定且保護隱私的標籤,而自訂選擇器名稱保持不變。現有的對話與已儲存的模型選擇會繼續路由。關閉此功能只會隱藏產生的選項,仍會保留選擇器與確切路由。一般 GPT 模型 ID 維持原有的 Pool 或 Direct 行為。`,"codexAuth.accountPickerUpdated":`帳號指定設定已更新。`,"codexAuth.accountPickerUpdateFailed":`無法更新帳號指定設定。目前顯示的是最後一次確認的設定。`,"codexAuth.accountPickerLoadFailed":`無法載入帳號指定設定。`,"codexAuth.accountPickerRefreshFailed":`無法重新整理此設定。目前仍顯示最後一次確認的值。`,"codexAuth.advancedSettings":`進階設定`,"codexAuth.advancedSettingsAria":`顯示或隱藏進階 Codex 認證設定`,"codexAuth.catalogRefreshPending":`變更已儲存,但 Codex 模型目錄仍在等待重新整理。請執行 ocx sync 重試。`,"accountPool.priority":`選擇順序`,"accountPool.priorityAria":`此帳號的選擇順序`,"accountPool.priorityHint":`數字越大越先使用。只有當排名在前的所有帳號都已耗盡或不可用時,帳號池才會轉向較小的數字。`,"accountPool.priorityFirst":`最先`,"accountPool.priorityEarlier":`較先`,"accountPool.priorityNormal":`一般`,"accountPool.priorityLater":`較後`,"accountPool.priorityLast":`最後`,"accountPool.priorityOption":`{name}({value})`,"accountPool.priorityCustom":`自訂`,"accountPool.priorityUpdated":`已更新 {email} 的選擇順序`,"accountPool.priorityUpdateFailed":`無法儲存 {email} 的選擇順序。目前顯示最後一次確認的值。`,"api.clientConfig.clientOmp":`OMP`,"api.clientConfig.clientHermes":`Hermes`,"api.clientConfig.clientOpenclaw":`OpenClaw`,"api.clientConfig.clientKimi":`Kimi Code`,"api.clientConfig.clientGajae":`Gajae Code`,"api.clientConfig.clientDsh":`DeepSeek Harness (DSH)`,"api.clientConfig.clientMcode":`MiniMax Code`,"api.clientConfig.clientZcode":`ZCode`,"api.clientConfig.clientPrime":`Prime Agent`,"api.clientConfig.clientAside":`Aside`,"cws.tabsLabel":`Combo 詳細區段`,"cws.field.nativeAlias":`原生 OpenAI 別名`,"cws.field.nativeAliasHint":`讓此 combo 擁有受支援的未限定原生 OpenAI 模型 ID。帶有帳號或供應商限定的 OpenAI 路由仍保持獨立。`,"cws.field.displayName":`顯示名稱`,"cws.field.displayNameHint":`此 combo 在模型選擇器中的標籤。啟用原生 OpenAI 別名時必填。`,"cws.err.unsupportedNativeAlias":`原生別名必須是目前受支援的裸 OpenAI 模型 ID。`,"cws.err.missingNativeAliasDisplayName":`原生別名必須提供顯示名稱。`,"cws.err.invalidDisplayName":`顯示名稱最多 128 個字元,且不能包含控制字元。`,"claudeDesktop.appliedMarkerUnsaved":`已套用到 Claude Desktop,但套用標記未能儲存 — 在你再次套用之前,下方的已儲存/已套用狀態可能顯示過時資訊。`,"claudeDesktop.status.disabled":`Claude Desktop 整合已關閉。啟用後請完全結束並重新開啟 Desktop。`,"claudeDesktop.enableApply":`啟用並套用`,"lab.title":`相容性實驗室`,"lab.subtitle":`以實驗室投影證據為基礎的唯讀相容性判定矩陣。`,"lab.loadFailed":`無法載入相容性實驗室資料`,"lab.projectionUnavailable":`實驗室投影不可用。請先執行 conformance 或 live 探測。`,"lab.projectionIncompatible":`實驗室投影 schema 不相容。請重新建立投影。`,"lab.statusTitle":`投影狀態`,"lab.matrixTitle":`相容性矩陣`,"lab.verdictsTitle":`判定記錄`,"lab.filter.layer":`證據層`,"lab.filter.verdict":`判定`,"lab.filter.subject":`主體 ID`,"lab.filter.all":`全部`,"lab.col.subject":`主體`,"lab.col.layer":`層級`,"lab.col.suite":`套件`,"lab.col.verdict":`判定`,"lab.col.asOf":`截至`,"lab.col.protocol":`協定符合度`,"lab.col.live":`即時路由相容性`,"lab.col.task":`任務效能`,"lab.empty":`投影中還沒有相容性判定。`,"lab.subjectKind":`類型`,"lab.observationCount":`觀察數`,"lab.eventCount":`事件數`,"lab.verdictCount":`判定數`,"lab.subjectCount":`主體數`,"lab.builtAt":`建立於`,"lab.loading":`載入相容性證據中…`,"lab.loadMore":`載入更多`,"lab.detailTitle":`判定詳細資料`,"lab.detailClose":`關閉`,"lab.detailSubject":`主體`,"lab.detailObservations":`觀察數`,"lab.detailEvents":`貢獻事件`,"lab.detailArtifacts":`產物中繼資料`,"lab.production.title":`觀測到的正式環境流量`,"lab.production.notVerification":`不是實驗室驗證`,"lab.production.attempts":`嘗試`,"lab.production.successes":`成功`,"lab.production.routeErrors":`路由錯誤`,"lab.production.lastObserved":`最近觀測`,"lab.detailLoadFailed":`無法載入判定詳細資料`,"lab.refresh":`重新整理`,"lab.verdict.UNKNOWN":`未知`,"lab.verdict.CLAIMED":`已宣稱`,"lab.verdict.PROBED":`已探測`,"lab.verdict.VERIFIED":`已驗證`,"lab.verdict.DEGRADED":`已降級`,"lab.verdict.BLOCKED":`已封鎖`,"lab.verdict.UNSUPPORTED":`不支援`,"lab.layer.protocol_conformance":`協定符合度`,"lab.layer.live_route_compatibility":`即時路由相容性`,"lab.layer.task_effectiveness":`任務效能`,"dash.visionAdvanced":`進階設定`,"dash.visionMaxDescriptions":`每回合最大描述次數`,"dash.visionMaxDescriptionsInvalid":`請輸入正整數。`,"dash.visionTimeout":`逾時`,"dash.visionTimeoutInvalid":`請輸入 {min} 到 {max} 毫秒之間的整數。`,"dash.visionAdvancedPopover":`進階視覺設定`,"models.newPolicyGlobal":`新模型預設停用`,"models.newPolicyProvider":`新模型策略`,"models.newPolicy_inherit":`繼承`,"models.newPolicy_off":`關閉`,"models.newPolicy_on":`開啟`,"models.newBadge":`新增`,"models.newCount":`{count} 個新增,已關閉`,"models.aliases":`別名`,"models.aliasesTable":`別名表`,"models.aliasPrompt":`供應商別名(留空即清除)`,"models.modelAliasPrompt":`模型別名(留空即清除)`,"models.aliasSaved":`別名已儲存`,"models.aliasConflict":`此別名與現有名稱衝突`,"models.editProviderAlias":`編輯供應商別名`,"models.editModelAlias":`編輯模型別名`,"models.useDefaultAliases":`使用預設別名`,"models.useDefaultAliasesGlobal":`全域使用預設別名`,"models.aliasAuto":`自動`,"models.aliasUser":`使用者`,"models.aliasStale":`過期`,"connection.discovering":`正在探索本機與共享目標…`,"connection.machineUnavailable":`本機機器平面無法使用。共享請求未改用本機資料。`,"connection.disconnect":`中斷 Hub 連線`,"connection.disconnectConfirm":`要中斷此機器與 Hub 的連線,並以獨立模式重新啟動嗎?`,"connection.pairing.title":`將此儀表板連接到 Hub`,"connection.pairing.body":`貼上在 Hub 建立的一次性配對碼。`,"connection.pairing.relayWarning":`此代碼透過固定 Hub 轉送交換,無法重新導向其他主機。`,"connection.pairing.code":`一次性配對碼`,"connection.pairing.submit":`連接`,"connection.pairing.submitting":`連接中…`,"connection.pairing.error":`配對碼遭拒或已過期。輸入內容已保留供檢查。`,"connection.machine.title":`此機器`,"connection.machine.shimHealthy":`Codex shim 狀態正常。`,"connection.machine.shimNeedsAttention":`Codex shim 需要處理。`,"connection.machine.repairShim":`修復 shim`,"connection.machine.removeShim":`移除 shim`,"connection.clients.title":`已連接的用戶端`,"connection.clients.none":`沒有用戶端狀態`,"connection.clients.sync":`立即同步`,"connection.clients.syncing":`同步中…`,"connection.sessionLogout":`登出遠端工作階段`,"connection.sessionLoggingOut":`正在登出遠端工作階段…`,"connection.sessionLogoutFailed":`無法登出遠端工作階段。目前的工作階段已保留。`,"usage.source.connected":`來源:Hub 使用量`,"usage.source.local":`來源:本機 usage.jsonl`,"usage.scope.label":`使用量範圍`,"usage.scope.machine":`此機器`,"usage.scope.hub":`整個 Hub`,"usage.hubOffline":`Hub 使用量無法使用,未以本機使用量替代。`,"integrations.tab.cursor":`Cursor`,"integrations.detail.cursorSeen":`Cursor 最近曾呼叫此代理`,"integrations.detail.cursorNeverSeen":`已安裝 Private Inference;尚未收到請求`,"integrations.detail.cursorAbsent":`找不到 Cursor Private Inference`,"integrations.cursor.title":`Cursor`,"integrations.cursor.intro":`Cursor Private Inference 在本機執行代理程式,並透過 loopback 與 opencodex 通訊。一般版 Cursor 做不到:它的後端會呼叫自訂端點,因此需要公開的 HTTPS 網址。此頁面絕不會寫入 Cursor;請自行把下方的值貼進 Cursor。`,"integrations.cursor.loading":`正在讀取 Cursor 狀態…`,"integrations.cursor.unavailable":`無法從代理讀取 Cursor 狀態。`,"integrations.cursor.detection":`已安裝的版本`,"integrations.cursor.privateInference":`Cursor Private Inference`,"integrations.cursor.regular":`Cursor(一般版)`,"integrations.cursor.detected":`已偵測到`,"integrations.cursor.notFound":`找不到`,"integrations.cursor.regularOnly":`只找到一般版 Cursor。它會把自訂端點導向 Cursor 伺服器,因此沒有公開通道就無法連到 loopback 代理。請參閱指南取得 Private Inference 版本。`,"integrations.cursor.nothingFound":`在常見位置找不到 Cursor。若安裝在其他地方,下方的值仍然適用。`,"integrations.cursor.gateway":`閘道設定值`,"integrations.cursor.gatewayHint":`在 Cursor Private Inference 開啟 Settings > Models > Gateway,貼上這兩個值,然後按 Refresh model list。`,"integrations.cursor.baseUrl":`Base URL`,"integrations.cursor.apiKey":`API 金鑰`,"integrations.cursor.apiKeyCredential":`你的其中一把 opencodex API 金鑰(此綁定需要憑證)`,"integrations.cursor.copy":`複製`,"integrations.cursor.copied":`已複製`,"integrations.cursor.connection":`連線`,"integrations.cursor.seen":`最近一次來自 Cursor 的請求:{time}({ua})`,"integrations.cursor.neverSeen":`代理啟動後尚未收到 Cursor 的請求。儲存閘道後,請在 Cursor 按 Refresh model list。`,"integrations.cursor.models":`Cursor 會顯示的內容`,"integrations.cursor.modelsHint":`Cursor 從自己的模型表決定 Reasoning 階梯,opencodex 只能預測。Context 列出預設與可選的視窗(Cursor 的 Max Mode)。`,"integrations.cursor.ladderFromBundle":`Reasoning 階梯讀取自已安裝的 Cursor Private Inference {version} bundle。階梯由 Cursor 決定,opencodex 只是呈現它的表。`,"integrations.cursor.ladderFromStatic":`Reasoning 階梯是 Cursor 3.18.25 的靜態鏡像(找不到可讀取的 Private Inference bundle)。Context 欄列出預設與可選的視窗。`,"integrations.cursor.unknownVersion":`版本不明`,"integrations.cursor.noControl":`—`,"integrations.cursor.singleWindow":`單一視窗`,"integrations.cursor.noControlTitle":`此 id 不在 Cursor 內建的 effort 表中,因此 Cursor 不會顯示 Reasoning 控制項。`,"integrations.cursor.effortRowsOne":`已發布 1 個 effort 列`,"integrations.cursor.effortRowsMany":`已發布 {n} 個 effort 列`,"integrations.cursor.effortRowsOff":`沒有 effort 列`,"integrations.cursor.tableLessHint":`標為 — 的列在 Cursor 中沒有 Reasoning 控制項。開啟 cursorEffortRows 可為每個 effort 發布一個選擇器項目(id--effort),或在 provider 上設定 modelDefaultReasoningEfforts 作為固定預設值。`,"integrations.cursor.colModel":`模型`,"integrations.cursor.colReasoning":`推理`,"integrations.cursor.colContext":`上下文`,"integrations.cursor.guide":`開啟 Cursor Private Inference 指南`},We={"nav.dashboard":`Дашборд`,"uptime.day":`д`,"uptime.hour":`ч`,"uptime.minute":`мин`,"uptime.second":`с`,"nav.startup":`Безопасность запуска`,"nav.providers":`Провайдеры`,"nav.models":`Модели`,"nav.combos":`Комбо`,"nav.subagents":`Подагенты`,"routing.title":`Интеллект маршрутизации (beta)`,"routing.subtitle":`Политики маршрутизации, пробная оценка и аналитика на основе источников.`,"routing.loadFailed":`Не удалось загрузить данные маршрутизации`,"routing.empty":"Профили маршрутизации не настроены. Добавьте `routingProfiles` в config.json.","routing.revision":`rev`,"routing.detail":`Профиль`,"routing.createProfile":`Создать профиль`,"routing.dryRunError":`Ошибка пробного запуска (HTTP {status})`,"routing.removeConfirm":`Удалить профиль {id}?`,"routing.unknownEvidence.allow":`разрешить`,"routing.unknownEvidence.penalize":`штрафовать`,"routing.unknownEvidence.exclude":`исключить`,"routing.removeCandidate":`Удалить кандидата {provider}/{model}`,"routing.candidates":`Кандидаты`,"routing.require":`Жёсткие требования`,"routing.optimize":`Веса оптимизации`,"routing.limits":`Лимиты`,"routing.unknownEvidence":`Политика неизвестных данных`,"routing.compatibility.title":`Политика совместимости`,"routing.compatibility.enabled":`Требовать доказательства Compatibility Lab`,"routing.compatibility.requiredSuites":`Обязательные наборы`,"routing.compatibility.loadingCatalog":`Загрузка каталога Lab…`,"routing.compatibility.catalogUnavailable":`Каталог Lab недоступен — укажите id наборов вручную в config.json.`,"routing.compatibility.layer.protocol_conformance":`Соответствие протоколу`,"routing.compatibility.layer.live_route_compatibility":`Совместимость живого маршрута`,"routing.compatibility.minStatus":`Минимальный статус совместимости`,"routing.none":`нет`,"routing.unavailable":`–`,"routing.dryRun":`Пробная оценка`,"routing.dryRunContext":`Контекстное окно запроса (токены)`,"routing.dryRunTools":`Запрос требует инструменты`,"routing.dryRunImage":`Запрос требует изображения`,"routing.dryRunStructured":`Запрос требует структурированный вывод`,"routing.dryRunRun":`Оценить кандидатов`,"routing.candidate":`Кандидат`,"routing.eligible":`Допустим`,"routing.exclusions":`Исключения`,"routing.costCap":`Лимит стоимости`,"routing.capOutcome.satisfied":`в пределах лимита`,"routing.capOutcome.exceeded":`сверх лимита`,"routing.capOutcome.unknown-allowed":`неизвестно (разрешено)`,"routing.capOutcome.unknown-excluded":`неизвестно (исключено)`,"routing.exclusion.capability-unsatisfied":`требование не выполнено`,"routing.exclusion.unknown-capability":`неизвестная возможность`,"routing.exclusion.cost-limit":`сверх лимита стоимости`,"routing.exclusion.cost-limit-unknown":`неизвестная стоимость при лимите`,"routing.exclusion.cooldown":`пауза`,"routing.exclusion.unknown-health":`неизвестное состояние`,"routing.exclusion.unknown-quota":`неизвестная квота`,"routing.exclusion.unknown-price":`неизвестная цена`,"routing.exclusion.other":`исключение: {code}`,"routing.score":`Оценка`,"routing.selected":`выбран`,"routing.yes":`да`,"routing.no":`нет`,"routing.analytics":`Аналитика маршрутизации`,"routing.analyticsTotal":`Запросы`,"routing.analyticsSuccessRate":`Успех`,"routing.analyticsFallbackRate":`Фолбэк`,"routing.analyticsP50":`p50`,"routing.analyticsP95":`p95`,"routing.analyticsP99":`p99`,"routing.analyticsCooldown":`Сбои кулдауна`,"routing.analyticsConfidence":`Доверие`,"routing.analyticsTruncated":`усечённая история`,"routing.analyticsRequests":`Запросы`,"routing.analyticsEmpty":`Аналитики пока нет — сначала отправьте несколько запросов.`,"nav.logs":`Логи и отладка`,"nav.usage":`Использование`,"common.github":`GitHub`,"sidebar.star":`Поставить звезду на GitHub`,"sidebar.starred":`Звезда на GitHub поставлена`,"sidebar.starUnauthenticated":`Открыть GitHub, чтобы поставить звезду (gh CLI не выполнил вход)`,"sidebar.starFailed":`Не удалось поставить звезду через gh. Открываем GitHub.`,"sidebar.updateAvailable":`Доступно обновление: {version}`,"sidebar.checkUpdate":`Проверить обновления`,"common.save":`Сохранить`,"common.saving":`Сохранение…`,"common.cancel":`Отмена`,"common.discard":`Отбросить`,"common.delete":`Удалить`,"common.close":`Закрыть`,"common.ok":`ОК`,"common.remove":`Удалить`,"common.loading":`Загрузка…`,"common.retry":`Повторить`,"auth.adminTokenTitle":`Токен администратора OpenCodex (OPENCODEX_ADMIN_AUTH_TOKEN)`,"auth.adminAccountLabel":`Учётная запись`,"auth.adminTokenFieldLabel":`Токен администратора`,"auth.adminTokenRejected":`Токен администратора отклонён. Проверьте его и повторите попытку.`,"auth.adminTokenUnavailable":`Не удалось проверить токен администратора. Повторите попытку.`,"app.logoAria":`Логотип opencodex`,"app.claudeOn":`Claude ВКЛ`,"app.claudeOff":`Claude ВЫКЛ`,"theme.label":`Тема`,"theme.light":`Светлая`,"theme.dark":`Тёмная`,"theme.system":`Системная`,"lang.label":`Язык`,"lang.nativeName":`Русский`,"provider.name.commandCodeAuth":`Command Code - Auth`,"provider.name.commandCodeApi":`Command Code - API`,"provider.name.volcengine":`Volcengine Ark`,"provider.name.volcengineCodingPlan":`Volcengine Ark — тариф Coding`,"provider.name.volcengineAgentPlan":`Volcengine Ark — тариф Agent`,"errorBoundary.title":`Не удалось загрузить страницу`,"errorBoundary.message":`При отображении этого раздела произошла ошибка. Перезагрузите его, чтобы повторить попытку.`,"errorBoundary.details":`Ошибка`,"errorBoundary.reload":`Перезагрузить`,"startup.title":`Безопасность запуска`,"startup.subtitle":`Проверьте, сможет ли Codex подключиться к opencodex после перезагрузки, прежде чем локальный прокси вызовет бесконечное переподключение.`,"startup.refresh":`Обновить`,"startup.backToDashboard":`Назад к панели`,"startup.loading":`Проверка защиты запуска…`,"startup.error":`Не удалось прочитать состояние защиты запуска.`,"startup.staleData":`Последняя проверка не удалась. Значения ниже устарели и не подтверждают защиту.`,"startup.status.native":`Нативная маршрутизация`,"startup.status.protected":`Перезапуск защищён`,"startup.status.atRisk":`Требуется действие`,"startup.summary.native":`Codex не зависит от локального прокси`,"startup.summary.protected":`opencodex будет доступен после перезагрузки`,"startup.summary.atRisk":`После перезагрузки Codex может потерять доступ к моделям`,"startup.riskDetail":`Codex направлен на локальный прокси, но постоянная служба или исправный launcher shim не запустят его снова.`,"startup.riskDetailCustomLocal":`Codex направлен на пользовательский локальный шлюз. opencodex не может управлять или проверять его перезапуск.`,"startup.riskDetailWindowsShim":`Launcher shim защищает поддерживаемые CLI-скрипты, но Codex Desktop и прямой запуск codex.exe в Windows могут обходить его.`,"startup.safeDetail":`Маршрутизация и механизм запуска согласованы. После перезагрузки ручной запуск ocx start не требуется.`,"startup.routing":`Маршрутизация Codex`,"startup.routing.proxy":`Локальный прокси`,"startup.routing.native":`Нативный OpenAI`,"startup.routing.customLocal":`Пользовательский локальный шлюз`,"startup.routing.customRemote":`Пользовательский удалённый шлюз`,"startup.routing.unknown":`Неизвестная или недопустимая маршрутизация`,"startup.restartProtection":`Защита перезапуска`,"startup.preference":`Запуск по требованию`,"startup.enabled":`Включён`,"startup.disabled":`Выключен`,"startup.protection.service":`Фоновая служба`,"startup.protection.shim":`Launcher shim`,"startup.protection.none":`Не установлен`,"startup.details":`Сведения о защите`,"startup.service":`Фоновая служба`,"startup.serviceHint":`Запускается при входе и перезапускает прокси после сбоя.`,"startup.installed":`Установлена`,"startup.notInstalled":`Не установлена`,"startup.unsupported":`Не поддерживается`,"startup.shim":`Codex launcher shim`,"startup.shimHint":`Запускает ocx ensure при запуске поддерживаемого скриптового лаунчера Codex.`,"startup.healthy":`Исправен`,"startup.cliOnly":`Только CLI`,"startup.stale":`Устарел`,"startup.viable":`Готов`,"startup.unhealthy":`Установлен, но неисправен`,"startup.conflict":`Конфликт служб`,"startup.installedDisabled":`Установлен, но отключён`,"startup.install":`Установить`,"startup.installing":`Установка…`,"startup.repair":`Исправить`,"startup.repairing":`Исправление…`,"startup.serviceInstalled":`Фоновая служба успешно установлена.`,"startup.serviceRepaired":`Фоновая служба успешно исправлена.`,"startup.shimInstalled":`Launcher shim Codex успешно установлен.`,"startup.shimRepaired":`Launcher shim Codex успешно исправлен.`,"startup.installFailed":`Не удалось установить:`,"startup.tray.title":`Системный трей Windows`,"startup.tray.hint":`Запускает значок при входе для управления запуском, остановкой, перезапуском, панелью и состоянием прокси.`,"startup.tray.login":`Запускать трей при входе в Windows`,"startup.tray.notProtection":`Трей — это контроллер, а не защита перезапуска. Для автоматического восстановления по-прежнему нужна исправная фоновая служба.`,"startup.tray.running":`Работает`,"startup.tray.stopped":`Установлен, скрыт`,"startup.tray.stale":`Требуется ремонт`,"startup.tray.notInstalled":`Не установлен`,"startup.tray.loading":`Проверка…`,"startup.tray.unavailable":`Статус недоступен`,"startup.tray.install":`Установить и показать трей`,"startup.tray.start":`Показать значок`,"startup.tray.stop":`Закрыть значок`,"startup.tray.uninstall":`Удалить трей входа`,"startup.tray.error":`Действие Windows tray завершилось ошибкой. Подробности: ocx tray status.`,"startup.recovery":`Варианты исправления`,"startup.recoveryHint":`Используйте установку в один клик выше или скопируйте команду для ручного восстановления. Для Codex Desktop и Windows рекомендуется фоновая служба.`,"startup.command.service":`Рекомендуется: постоянная фоновая служба`,"startup.command.shim":`Альтернатива: CLI launcher shim`,"startup.command.native":`Безопасный режим: восстановить нативную маршрутизацию Codex`,"startup.copy":`Копировать`,"startup.copied":`Скопировано`,"startup.recommended":`Рекомендуемое исправление: {cmd}`,"startup.navRisk":`Защита запуска требует внимания`,"startup.codexRuntime.clampHidden":`Некоторые уровни рассуждений скрыты, потому что OpenCodex использует Codex {version}.`,"startup.codexRuntime.clampHiddenWithEfforts":`Некоторые уровни рассуждений скрыты, потому что OpenCodex использует Codex {version} (удалены: {efforts}).`,"startup.codexRuntime.olderBinary":`OpenCodex использует более старый бинарник Codex ({version}). Доступна более новая установка.`,"dash.subtitle":`Актуальное состояние локального прокси opencodex, его провайдеров и моделей, маршрутизируемых в Codex.`,"dash.workspace.overview":`Обзор`,"dash.workspace.sections":`Разделы`,"dash.status":`Статус`,"dash.online":`В сети`,"dash.offline":`Не в сети`,"dash.version":`Версия`,"dash.uptime":`Время работы`,"dash.providers":`Провайдеры`,"dash.tokens30d":`Токены (30 дн.)`,"dash.coverage":`{pct} покрытия`,"dash.mem.title":`Наблюдение за памятью`,"dash.mem.hint":`Диагностика среды выполнения только для чтения. Наблюдаемая память — max(RSS, external, ArrayBuffers), чтобы trimming рабочего набора Windows не скрывал удержанную память.`,"dash.mem.rss":`Резидентная память (RSS)`,"dash.mem.jsHeap":`Куча JS занято`,"dash.mem.jsHeapArena":`арена {total}`,"dash.mem.pressure":`Относительно порога`,"dash.mem.pressureOf":`{pct}% от порога`,"dash.mem.pressureUnknown":`Порог не сообщён`,"dash.mem.jscHeap":`Куча JSC`,"dash.mem.external":`External`,"dash.mem.arrayBuffers":`ArrayBuffers`,"dash.mem.observed":`Наблюдаемая`,"dash.mem.runtime":`Счётчики среды`,"dash.mem.growth":`Изменение наблюдаемой / час`,"dash.mem.perHour":`/ч`,"dash.mem.store":`Хранилище продолжений`,"dash.mem.storeHint":`Кэш прокси previous_response_id. Рост общего числа байт при растущей куче указывает на удержание диалогов, а не на аллокатор среды.`,"dash.mem.storeEntries":`Записи`,"dash.mem.storeTotal":`Всего`,"dash.mem.storeLargest":`Наибольшая`,"dash.mem.storeOldest":`Старейшая`,"dash.mem.threshold":`Порог предупреждения`,"dash.mem.lastWarn":`Последнее предупреждение`,"dash.mem.never":`Никогда`,"dash.mem.details":`Подробности`,"dash.mem.unavailable":`Диагностика памяти недоступна (старая версия прокси).`,"dash.mem.inFlight":`Активные запросы`,"dash.mem.restart":`Дождаться и перезапустить`,"dash.mem.restartConfirm":`Дождаться завершения {count} активных запросов, затем перезапустить (до {seconds} с; оставшиеся при таймауте прервутся).`,"dash.mem.draining":`Ожидание {count} запрос(ов)… перезапуск после завершения`,"dash.mem.reconnecting":`Прокси перезапускается… ожидание подключения`,"dash.mem.restartFailed":`Не удалось дождаться и перезапустить. Проверьте, что прокси запущен.`,"dash.mem.restartNoSupervisor":`Защита перезапуска не обнаружена. После перезапуска прокси может остаться выключенным, пока вы не запустите его снова.`,"dash.activeProviders":`Активные провайдеры`,"dash.noProviders":`Провайдеры не настроены. Выполните {cmd}.`,"dash.col.name":`Название`,"dash.col.adapter":`Адаптер`,"dash.col.baseUrl":`Базовый URL`,"dash.col.model":`Модель`,"dash.modelsNoResults":`Нет моделей, соответствующих поиску.`,"dash.availableModels":`Доступные модели`,"dash.noModels":`Модели не найдены. Проверьте API-ключи провайдеров.`,"dash.cannotConnect":`Не удаётся подключиться к прокси. Он запущен?`,"dash.runStart":`Выполните {cmd}, чтобы запустить прокси.`,"dash.stop":`Остановить прокси`,"dash.stopConfirm":`Остановить прокси и восстановить нативный Codex?`,"dash.stopFailed":`Не удалось остановить прокси (HTTP {status}).`,"dash.maSwitchFailed":`Не удалось переключить режим (HTTP {status}).`,"dash.maNetworkError":`Ошибка сети — прокси запущен?`,"dash.stopping":`Остановка…`,"dash.actions":`Прокси`,"dash.codexRestart":`Обновить список моделей Codex`,"dash.codexRestarting":`Останавливается…`,"dash.codexRestartConfirm":`Остановить app-server'ы Codex, чтобы они перечитали список моделей? Текущий ход Codex будет прерван, и Codex не перезапустится сам — откройте его заново.`,"dash.codexRestartDone":`Остановлено app-server Codex: {count}. Откройте Codex заново, чтобы загрузить актуальный список моделей.`,"dash.codexRestartNothing":`Ни один app-server Codex не запущен. При следующем запуске будет прочитан актуальный список моделей.`,"dash.codexRestartUnknown":`Не удалось получить список процессов, поэтому ничего не остановлено.`,"dash.codexRestartPartial":`app-server не завершились: {count}. Остановите их вручную, если список моделей остаётся устаревшим.`,"dash.codexRestartFailed":`Не удалось обновить список моделей Codex (HTTP {status}).`,"dash.codexRestartUnreachable":`Не удалось связаться с прокси.`,"dash.codexRestartMalformed":`Прокси вернул неожиданный ответ.`,"dash.codexRestartTimeout":`Прокси не ответил вовремя. Возможно, он всё ещё останавливает app-server'ы.`,"models.staleBanner":`Codex показывает список моделей старее этого каталога. Перезапустите Codex, чтобы перечитать его.`,"dash.codexAutoStart":`Запускать opencodex вместе с Codex`,"dash.codexAutoStartHint":`Разрешает установленному launcher shim выполнять ocx ensure. Эта настройка не устанавливает защиту перезапуска; проверьте фактическое состояние в разделе безопасности запуска.`,"dash.searchModel":`Модель сайдкара поиска`,"dash.searchModelHint":`Модель, используемая для web_search на маршрутизируемых моделях, отличных от OpenAI. Требуется вход в аккаунт ChatGPT.`,"dash.searchReasoning":`Уровень рассуждений для поиска`,"dash.visionModel":`Модель сайдкара для изображений`,"dash.visionModelHint":`Модель, которая описывает изображения для маршрутизируемых моделей, работающих только с текстом. Требуется вход в аккаунт ChatGPT.`,"dash.webSearchSidecar":`Сайдкар веб-поиска`,"dash.webSearchSidecarHint":`Выберите бэкенд и модель, используемые для веб-поиска на маршрутизируемых моделях.`,"dash.webSearchStream":`Стримить ответы вживую`,"dash.webSearchStreamHint":`Транслировать начальный текст и рассуждения вживую, пока модель не решит вызвать инструмент; остальное буферизуется для перехвата поиска. Текст до поиска может частично повторяться.`,"dash.visionSidecar":`Сайдкар для изображений`,"dash.visionSidecarHint":`Выберите бэкенд и модель, которые описывают изображения для маршрутизируемых моделей, работающих только с текстом.`,"dash.visionOff":`Выкл`,"dash.shadowCallIntercept":`Перехват теневых вызовов`,"dash.shadowCallInterceptHint":`Перехватывает фоновые служебные вызовы Codex App ({models}: генерация заголовков, сообщений коммитов) и перенаправляет их на выбранную вами модель.`,"dash.shadowCallWarning":`⚠ Когда функция включена, ВСЕ запросы к {models} будут заменены выбранной моделью.`,"dash.shadowCallOriginal":`Оригинал`,"dash.shadowCallModel":`Модель-замена`,"dash.shadowCallTooltip":`Codex App в фоновом режиме вызывает служебную модель для генерации заголовков тредов, сообщений коммитов и оркестрации навыков. Эта модель менялась между версиями клиента, поэтому opencodex перехватывает весь набор: {models}. Включите функцию, чтобы перенаправлять такие вызовы на выбранную вами модель.`,"models.shadowCallIntercept":`Перехват теневых вызовов`,"models.shadowCallInterceptHint":`Перехватывает фоновые служебные вызовы Codex App ({models}: заголовки, сообщения коммитов) и перенаправляет их на выбранную вами модель.`,"dash.sidecarBackend":`Бэкенд`,"dash.sidecarModel":`Модель`,"dash.backendAuto":`Авто`,"dash.backendOpenAI":`OpenAI`,"dash.backendAnthropic":`Anthropic`,"dash.sidecarSaved":`Настройки сайдкара сохранены. Вступят в силу со следующего запроса.`,"dash.sidecarSaveFailed":`Не удалось сохранить настройки сайдкара.`,"dash.injectionLabel":`Делегирование подагентам`,"dash.injectionHint":`Выберите модель, которой Codex будет передавать работу подагентов. Где применяется этот выбор, решают два переключателя ниже.`,"dash.syncCodexSubagentDefaults":`Сохранить и как значение по умолчанию в Codex`,"dash.syncCodexSubagentDefaultsHint":`Если включено, выбранная выше модель записывается в собственную конфигурацию Codex, и новые задачи тоже начинаются с неё. Если выключено, выбор запоминается только здесь. Применится при следующей синхронизации или перезапуске, а ваши настройки [agents] останутся нетронутыми.`,"dash.multiAgentGuidance":`Подсказывать, как делить работу`,"dash.multiAgentGuidanceHint":`Отправляет Codex короткую записку о том, как передавать работу подагентам. На v2 она называет доступные модели и предпочтительную; на v1 работает только при усилии рассуждения max или ultra. Если выключено, записка не добавляется.`,"dash.injectionNone":`Нет`,"dash.injectionEffortLabel":`Уровень рассуждений`,"dash.injectionEffortNone":`По умолчанию для модели`,"dash.effortCapLabel":`Лимит рассуждений V2 ultra`,"dash.subagentEffortCapLabel":`Лимит рассуждений подагентов V2`,"dash.effortCapHelp":`Ограничивает уровень рассуждений для ходов V2 в режиме ultra. Когда лимит задан, входящие запросы с максимальным уровнем рассуждений (из режима ultra) снижаются до выбранного уровня. Лимит для подагентов действует только на порождённые дочерние агенты. Лимиты только понижают уровень рассуждений и никогда не повышают его. Если модель не поддерживает заданный лимитом уровень, он снижается до ближайшего поддерживаемого.`,"dash.effortCapNone":`Без лимита`,"dash.maintenance":`Обслуживание`,"dash.maintenanceHint":`Обновите каталог моделей Codex или установите более новую версию opencodex.`,"dash.syncModels":`Синхронизировать модели`,"dash.syncing":`Синхронизация…`,"dash.syncOk":`Синхронизация завершена. Добавлено моделей: {count}.`,"dash.syncStaleHint":`Если Codex всё ещё показывает старый список, перезапустите долгоживущий app-server ({cmd}).`,"dash.syncFailed":`Ошибка синхронизации: {error}`,"dash.projectConfigTitle":`Конфигурация Codex в проекте обходит OpenCodex`,"dash.projectConfigHint":`Эти локальные настройки репозитория переопределяют прокси OpenCodex (например, направляют запросы напрямую в OpenCode Go). Удалите их, чтобы в этом проекте действовала маршрутизация из ~/.codex/config.toml.`,"dash.checkUpdate":`Проверить обновления`,"dash.updateTitle":`Обновление opencodex`,"dash.updateDesc":`Проверьте npm для выбранного канала, затем решите, перезапускать ли прокси после установки.`,"dash.updateChannel":`Канал`,"dash.updateChecking":`Проверка обновлений…`,"dash.updateInstalled":`Установлена`,"dash.updateLatest":`Последняя`,"dash.updateAvailable":`Доступно обновление`,"dash.updateCurrent":`Актуальная версия`,"dash.updateCommand":`Команда`,"dash.updateSource":`Это рабочая копия из исходного кода. Обновите её в терминале с помощью показанной команды.`,"dash.updateUnavailable":`Не удалось получить сведения о последней версии из npm. Попробуйте позже.`,"dash.updateRetry":`Повторить`,"dash.updateRecheck":`Проверить снова`,"dash.updateCannotAuto":`Обновление в один клик недоступно ({reason}).`,"dash.updateReason.source_checkout":`установка из исходного кода`,"dash.updateReason.latest_unavailable":`реестр npm недоступен`,"dash.updateReason.already_latest":`уже установлена последняя версия`,"dash.updateReason.unknown":`обновление недоступно`,"dash.updateRestart":`Перезапустить после обновления`,"dash.updateRestartHint":`Рекомендуется. Текущий GUI продолжает работать на старом коде, пока прокси не перезапустится.`,"dash.runUpdate":`Обновить`,"dash.updateReconnecting":`Ожидание перезапущенного прокси…`,"dash.updateStatus.running":`Обновление opencodex.`,"dash.updateStatus.restarting":`Обновление установлено. Перезапуск прокси.`,"dash.updateStatus.succeeded":`Обновление завершено.`,"dash.updateVersionTransition":`{currentVersion} -> {latestVersion}.`,"dash.updateStatus.failed":`Обновление не удалось.`,"prov.subtitle":`Настройте вышестоящих провайдеров, которых opencodex маршрутизирует в Codex. Войдите в аккаунт, добавьте провайдера или отредактируйте конфигурацию вручную.`,"prov.add":`Добавить провайдера`,"prov.editJson":`Редактировать JSON`,"prov.accountLogin":`Вход в аккаунт`,"prov.noOauth":`Нет доступных OAuth-провайдеров.`,"prov.loggedIn":`вход выполнен`,"prov.notLoggedIn":`вход не выполнен`,"prov.logout":`Выйти`,"prov.login":`Войти`,"prov.loginWith":`Войти через {provider}`,"prov.waitingBrowser":`Ожидание браузера…`,"prov.didntOpen":`Не открылось? Нажмите здесь`,"prov.copyLink":`Копировать ссылку`,"prov.dontOpenBrowser":`Не открывать браузер на машине с прокси`,"prov.dontOpenBrowserHint":`Полезно для другого профиля браузера или когда панель управления не на машине с прокси.`,"prov.linkCopied":`Скопировано`,"prov.linkCopyUnavailable":`Буфер обмена недоступен`,"prov.deviceCode":`Код устройства`,"prov.copyCode":`Копировать код`,"prov.codeCopied":`Код скопирован`,"prov.editAlias":`Изменить псевдоним`,"prov.aliasPrompt":`Отображаемое имя (оставьте пустым для удаления)`,"prov.aliasSaved":`Псевдоним сохранен`,"prov.aliasSaveFailed":`Не удалось сохранить псевдоним`,"prov.accountId":`ID`,"prov.pasteRedirect":`Вставьте URL перенаправления или код`,"prov.pasteRedirectHint":`Если браузер показывает ошибку localhost, скопируйте полный URL из его адресной строки и вставьте сюда (или вставьте код авторизации).`,"prov.pasteSubmit":`Отправить`,"prov.pasteSubmitting":`Отправка…`,"prov.pasteOk":`Код отправлен — завершаем вход…`,"prov.pasteFail":`Не удалось отправить код: {error}`,"prov.port":`Порт`,"prov.default":`По умолчанию`,"prov.loadingConfig":`Загрузка…`,"prov.saved":`Сохранено! Перезапустите прокси, чтобы применить изменения.`,"prov.loadConfigFail":`Не удалось загрузить конфигурацию`,"prov.invalidJson":`Некорректный JSON`,"prov.saveFailed":`Не удалось сохранить`,"prov.loginFailStart":`Не удалось начать вход в {provider}`,"prov.loginError":`Ошибка входа в {provider}: {error}`,"prov.loginRequestFail":`Не удалось выполнить запрос на вход в {provider}`,"prov.loginCancelled":`Вход в {provider} отменён`,"prov.loginTimeout":`Время ожидания входа в {provider} истекло — браузер был закрыт или вход не был завершён. Попробуйте ещё раз.`,"prov.loginOk":`Выполнен вход в {provider}. Выполните {cmd} (или изменения применятся на лету), чтобы его модели появились в списке.`,"prov.loginSameAccount":`Это всё ещё тот же аккаунт {provider} — переключите аккаунт в браузере и снова нажмите «Добавить аккаунт».`,"oauthTos.highTitle":`{provider}: риск OAuth по подписке`,"oauthTos.elevatedTitle":`{provider}: неофициальный OAuth-мост`,"oauthTos.anthropicBody":`Прямое повторное использование OAuth-токенов подписки Claude через сторонний прокси, такой как OpenCodex, не является поддерживаемой интеграцией Anthropic и может привести к ограничению доступа. Поддерживаемые интеграции Agent SDK, использующие подписки Claude, — это отдельный механизм.`,"oauthTos.highBody":`OpenCodex подключает {provider} через сторонний механизм OAuth. Неподдерживаемое использование может привести к ограничению или приостановке доступа.`,"oauthTos.elevatedBody":`OpenCodex подключает {provider} через неофициальный механизм OAuth. По возможности используйте официальный клиент; нетипичный или автоматизированный трафик может быть расценён как злоупотребление, и доступ может быть ограничен или приостановлен.`,"oauthTos.saferPath":`Более безопасный вариант: вместо этого настройте API-ключ в OpenCodex.`,"oauthTos.acknowledge":`Я понимаю риск и всё равно хочу продолжить с OAuth.`,"oauthTos.continue":`Продолжить с OAuth`,"prov.logoutOk":`Выполнен выход из {provider}.`,"prov.logoutFail":`Не удалось выйти из {provider}. Состояние аккаунта не изменилось.`,"prov.removed":`Провайдер "{name}" удалён.`,"prov.removedDefault":`Провайдер "{name}" удалён. Провайдером по умолчанию теперь является "{defaultProvider}".`,"prov.removeFail":`Не удалось удалить "{name}".`,"prov.removeLastProvider":`Нельзя удалить этого провайдера, если ни один другой включённый провайдер не может стать провайдером по умолчанию.`,"prov.removeHasDependentCombos":`Сначала удалите или обновите зависимые комбо: {combos}.`,"prov.setDefault":`Сделать основным`,"prov.setDefaultSuccess":`"{name}" теперь провайдер по умолчанию.`,"prov.setDefaultFail":`Не удалось сделать "{name}" провайдером по умолчанию.`,"prov.defaultDisabled":`Сначала включите этого провайдера, затем сделайте его основным.`,"prov.updateFail":`Не удалось обновить этого провайдера.`,"prov.networkError":`Ошибка сети. Проверьте, что прокси запущен, и повторите попытку.`,"prov.added":`Провайдер "{name}" добавлен. Уже активен — выполните {cmd} (или перезапустите), чтобы его модели появились в селекторе моделей Codex.`,"prov.removeConfirm":`Удалить провайдера "{name}"? Его модели исчезнут из селектора моделей Codex.`,"prov.hasApiKey":`API-ключ настроен`,"prov.hasHeaders":`настроены пользовательские заголовки`,"prov.accounts":`Аккаунты ({n})`,"prov.accountsAria":`Показать или скрыть аккаунты {name}`,"prov.accountActive":`Активен`,"prov.accountReauth":`Повторный вход`,"prov.reauthenticate":`Переавторизоваться`,"prov.reauthAccountMissing":`Выбранный аккаунт не найден после входа`,"prov.reauthIdentityMismatch":`Аккаунт, в который выполнен вход, не совпадает с выбранным`,"prov.accountAdd":`Добавить аккаунт`,"prov.accountNoLabel":`аккаунт {id}`,"prov.accountSwitchTitle":`Использовать этот аккаунт`,"prov.accountSwitched":`Переключено на {email}.`,"prov.accountSwitchFail":`Не удалось переключить аккаунт`,"prov.accountRemoved":`Аккаунт {email} удалён.`,"prov.accountRemoveFail":`Не удалось удалить {email}. Аккаунт не изменён.`,"prov.accountRemoveAria":`Удалить {email}`,"prov.accountRemoveConfirm":`Удалить аккаунт {email}? Данные его входа будут удалены из этого прокси.`,"prov.keyAdd":`Добавить API-ключ`,"prov.keyAdded":`API-ключ добавлен для {name}.`,"prov.keyAddFail":`Не удалось добавить API-ключ`,"prov.keyPlaceholder":`Вставьте API-ключ`,"prov.keySwitchTitle":`Использовать этот ключ`,"prov.keySwitched":`Переключено на ключ {key}.`,"prov.keySwitchFail":`Не удалось переключить ключ`,"prov.keyRemoved":`Ключ {key} удалён.`,"prov.keyRemoveAria":`Удалить ключ {key}`,"prov.keyRemoveConfirm":`Удалить API-ключ {key}? Он будет удалён из конфигурации этого прокси.`,"prov.activeBadge":`Активен`,"prov.disabledBadge":`Отключён`,"prov.defaultBadge":`По умолчанию`,"prov.enable":`Включить`,"prov.disable":`Отключить`,"prov.enabled":`Провайдер "{name}" включён. Его модели снова могут появляться в Codex.`,"prov.disabled":`Провайдер "{name}" отключён. Настройки сохранены, но его модели скрыты.`,"prov.enableFail":`Не удалось включить "{name}".`,"prov.disableFail":`Не удалось отключить "{name}".`,"prov.enableAria":`Включить провайдера {name}`,"prov.disableAria":`Отключить провайдера {name}`,"prov.defaultCannotDisable":`Провайдера по умолчанию нельзя отключить`,"prov.openaiAccountMode":`Режим аккаунта Codex`,"prov.openaiModePool":`Пул`,"prov.openaiModeDirect":`Прямой`,"prov.openaiPoolDesc":`По умолчанию. Ротация основного входа и добавленных аккаунтов с учётом привязки, квот, периодов ожидания и отказоустойчивого переключения (failover).`,"prov.openaiDirectDesc":`Используется только текущий/основной вход Codex. Сохранённые аккаунты пула не читаются и не ротируются.`,"prov.openaiModeSaved":`Режим аккаунта OpenAI изменён на {mode}.`,"prov.openaiModeSaveFailed":`Не удалось изменить режим аккаунта OpenAI.`,"prov.openaiApiDesc":`Используется API-ключ OpenAI; учётные данные аккаунта Codex никогда не используются.`,"prov.manageCodexAccounts":`Управление аккаунтами Codex`,"prov.openaiApiMissing":`Требуется API-ключ`,"prov.openaiApiSetup":`Настроить API-ключ`,"models.tab.catalog":`Модели`,"models.tab.combos":`Комбо`,"models.tab.compatibility":`Совместимость`,"models.tab.routing":`Маршрутизация (beta)`,"models.tabsLabel":`Поверхности моделей`,"models.subtitle.combos":`Упорядоченные группы моделей, отвечающие под одним id. Связывайте цели через failover или распределяйте нагрузку стратегией балансировки.`,"models.subtitle.compatibility":`Матрица совместимости только для чтения из проекции лаборатории.`,"models.subtitle.routing":`Профили политик, оценка в режиме dry-run и аналитика маршрутизации с подтверждением источников.`,"models.subtitle":`Управляйте тем, какие модели видит Codex — нативные GPT (сквозной проброс) и модели маршрутизируемых провайдеров, сгруппированные по провайдеру (нажмите на заголовок, чтобы свернуть группу). Скрытые модели исчезают из каталога и селектора, но остаются вызываемыми по точному id. Изменения применяются на следующем ходе Codex — opencodex сбрасывает 5-минутный кэш моделей Codex, поэтому перезапуск не требуется.`,"models.nativeGroupLabel":`Нативные OpenAI`,"models.nativeHint":"Модели сквозного проброса используют режим аккаунта (пул или прямое подключение), выбранный на странице «Провайдеры». Отключение модели скрывает её из селектора Codex (запись в каталоге сохраняется, поэтому при повторном включении она восстанавливается в точности). Добавление модели здесь регистрирует маршрутизируемый селектор `openai/`, а не новый «голый» passthrough-идентификатор.","models.active":`{active}/{total} видимо`,"models.workspace.providers":`Провайдеры`,"models.workspace.allProviders":`Все провайдеры`,"models.workspace.mainAria":`Сведения о моделях`,"models.allOn":`Все вкл.`,"models.allOff":`Все выкл.`,"models.presetLabel":`Модели`,"models.presetMode_preset":`Пресет`,"models.presetMode_all":`Все`,"models.presetMode_custom":`Свои`,"models.presetSummary":`Показано {count} из {total} — базовый пресет v{version}`,"models.presetUpdateAvailable":`Доступен пресет v{version}`,"models.presetAppliedToast":`{provider}: пресет применён — выбрано моделей: {count}`,"models.presetClearedToast":`{provider}: показаны все модели`,"models.presetEmpty":`{provider}: пресет не совпал ни с одной моделью — выбор не изменён`,"models.presetConfirmReplace":`Заменить ваш выбор пресетом из {count} моделей?`,"models.cap350k":`Лимит 350k`,"models.capApplied":`Лимит контекста применён — вступит в силу на следующем ходе Codex.`,"models.capSaveFailed":`Не удалось сохранить лимит контекста`,"models.contextCapped":`Лимит 350k`,"models.contextCapLabel":`Окно по умолчанию / лимит`,"models.v2Label":`Подагент`,"models.shadowCallOriginal":`⚠ {models} →`,"models.v2DocsLink":`Что такое v1 / v2?`,"models.v2Mode_v1":`v1`,"models.v2Mode_default":`base`,"models.v2Mode_v2":`v2`,"models.v2ModeDesc_v1":`Все модели → поверхность v1`,"models.v2ModeDesc_default":`Вышестоящие значения по умолчанию (sol/terra=v2, luna=v1)`,"models.v2ModeDesc_v2":`Все модели → поверхность v2`,"models.keepNativeOnV1":`Оставить ChatGPT на v1`,"models.keepNativeOnV1Hint":`Нативные родители ChatGPT шифруют дочерние задачи v2 — Grok и Claude их не читают. Оставьте включённым, если Sol/Terra должны порождать routed-модели. Routed-родители остаются на v2.`,"models.v2Help":`Управляет мультиагентной поверхностью для всех моделей. - -v1: Классический однопоточный агент. Каждая модель использует поверхность взаимодействия v1. -base: Вышестоящие значения по умолчанию — sol/terra используют v2, luna использует v1, остальные следуют функциональному флагу codex. -v2: Многопоточный агент со spawn_agent. Каждая модель использует поверхность взаимодействия v2. - -На v2 «Оставить ChatGPT на v1» оставляет Sol/Terra на v1, чтобы они могли порождать Grok или Claude. ChatGPT шифрует дочерние задачи v2 — routed-модели их не читают. Routed-родители остаются на v2. - -Изменения применяются к новым сессиям.`,"dash.multiAgent":`Подагент`,"models.v2Conflict":`Задан [agents] max_threads — codex откажется запускаться; удалите его из config.toml`,"models.v2Applied":`Режим подагента обновлён — применяется к новым сессиям (перезапустите приложение Codex, чтобы обновить селектор моделей)`,"models.v2ThreadsLabel":`Макс. потоков`,"models.v2ThreadsDefault":`по умолчанию (4)`,"models.v2ThreadsApplied":`Лимит потоков обновлён — применяется к новым сессиям`,"models.v2ThreadsInvalid":`Лимит потоков должен быть целым числом >= 1`,"models.v2ThreadsApply":`Применить`,"models.capValue":`По умолч. {value}`,"models.contextSettings":`Пользовательские окна`,"models.contextSettingsTitle":`Пользовательские окна — {provider}`,"models.contextDefault":`Значение провайдера`,"models.contextModel":`Модель`,"models.contextModelOverride":`Переопределение модели`,"models.contextHint":`Если окно уже известно, запишите здесь реальное окно Codex. При отсутствии метаданных используется это значение; большее заявленное окно только ограничивается, меньшее сохраняется. Пустое поле берёт «Окно по умолчанию / лимит» провайдера или 128k, если лимит выключен.`,"models.contextAutomatic":`Автоматическое определение`,"models.contextSaved":`Контекстные окна обновлены — изменения вступят в силу на следующем ходе Codex.`,"models.contextUnchanged":`Нет изменений контекстных окон для сохранения.`,"models.contextSaveFailed":`Не удалось сохранить контекстные окна`,"models.contextInvalid":`Контекстные окна должны быть положительными целыми числами`,"models.contextCappedValue":`Лимит {value}`,"models.setAll":`Применить ко всем`,"models.setAllHint":`Включает окно по умолчанию {value} для всех маршрутизируемых провайдеров. Если релей не отдаёт context_window / context_length, это значение становится реальным окном Codex. Чтобы задать одну модель вручную, используйте «Пользовательские окна» в той же строке. Нативные провайдеры не затрагиваются.`,"models.collapseAll":`Свернуть все`,"models.expandAll":`Развернуть все`,"models.orderHint":`Порядок в селекторе: модели, выбранные на странице «Подагенты» (в заданном порядке) → остальные маршрутизируемые модели по алфавиту — сначала по провайдеру, затем по ID модели → нативные модели. Переключатели видимости лишь фильтруют модели и не меняют этот порядок.`,"models.custom":`Другое…`,"models.customApply":`Применить`,"models.customPlaceholder":`Токены (напр. 420000)`,"models.customAdd":`Добавить пользовательскую модель`,"models.customAddTitle":`Добавить пользовательскую модель — {provider}`,"models.customEditTitle":`Изменить пользовательскую модель — {provider}`,"models.customAdded":`Пользовательская модель добавлена`,"models.customUpdated":`Пользовательская модель обновлена`,"models.customDeleted":`Пользовательская модель удалена`,"models.customSaveFailed":`Не удалось сохранить пользовательскую модель`,"models.customSaving":`Сохранение…`,"models.customAddBtn":`Добавить`,"models.customEditBtn":`Обновить`,"models.customEdit":`Изменить`,"models.customDelete":`Удалить`,"models.customDeleteConfirm":`Удалить модель {name}?`,"models.customBadge":`Пользовательская`,"models.customSummary":`Пользовательских: {count}`,"models.customFieldModelId":`ID модели (slug эндпоинта)`,"models.customFieldModelIdPlaceholder":`например, qwen4-max-preview`,"models.customFieldDisplayName":`Отображаемое имя (необязательно)`,"models.customFieldDisplayNamePlaceholder":`например, Qwen 4 Max Preview`,"models.customFieldContext":`Контекстное окно`,"models.customFieldModalities":`Входные модальности`,"models.customFieldReasoning":`Уровень рассуждений`,"models.customFieldReasoningOverride":`Переопределить уровень рассуждений`,"models.reasoningEffort.none":`Нет`,"models.reasoningEffort.minimal":`Минимальный`,"models.reasoningEffort.low":`Низкий`,"models.reasoningEffort.medium":`Средний`,"models.reasoningEffort.high":`Высокий`,"models.reasoningEffort.xhigh":`Очень высокий`,"models.reasoningEffort.max":`Максимальный`,"models.tipProvider":`Провайдер`,"models.tipContext":`Контекст`,"models.tipModalities":`Модальности`,"models.tipStatus":`Статус`,"models.tipActive":`Активна`,"models.tipDisabled":`Отключена`,"models.applied":`Применено — вступит в силу на следующем ходе Codex.`,"models.saveFailed":`Не удалось сохранить`,"models.networkError":`Ошибка сети — запущен ли прокси?`,"models.loadFail":`Не удалось загрузить модели — запущен ли прокси?`,"models.noRouted":`Нет маршрутизируемых моделей`,"models.noRoutedHint":`Сначала войдите в провайдера или добавьте нового.`,"models.emptyDiscovery":`Модели не обнаружены. Проверьте адрес провайдера или добавьте статическую/пользовательскую модель.`,"models.emptyDiscoveryDisabled":`Автообнаружение моделей выключено, статические модели не настроены.`,"models.discoveryFailedBadge":`Ошибка обнаружения`,"models.discoveryFailedHttp":`Не удалось обнаружить модели (HTTP {status}).`,"models.discoveryFailedBlocked":`Обнаружение моделей заблокировано политикой назначения.`,"models.discoveryFailedInvalidResponse":`Обнаружение моделей вернуло недопустимый ответ.`,"models.discoveryFailedNetwork":`Обнаружение моделей не удалось из-за сетевой ошибки.`,"models.discoveryFailedProvider":`Провайдер сообщил об ошибке обнаружения моделей.`,"models.discoveryFailedGeneric":`Не удалось обнаружить модели.`,"models.openProviderSettings":`Открыть настройки провайдера`,"models.loading":`Загрузка…`,"models.search":`Поиск моделей…`,"models.showMore":`Показать ещё {n}`,"models.allowlistLabel":`Только выбранные`,"models.allowlistHint":`В каталог попадают только отмеченные модели (пусто = все). Полезно для провайдеров, предоставляющих тысячи моделей.`,"models.selectedCount":`Выбрано: {n}`,"sub.subtitle":`{cmd} в Codex объявляет как переопределения только первые 5 моделей (по приоритету). Выберите здесь до 5 моделей — нативные gpt или маршрутизируемые — и opencodex задаст им приоритет в каталоге так, чтобы именно они шли первыми. Любую другую модель по-прежнему можно вызвать по её точному имени; эта настройка управляет только тем, что отображается.`,"sub.featured":`Избранные`,"sub.advanced":`Дополнительно`,"sub.orderHintAria":`Как используется этот порядок`,"sub.orderHint":`Показанный здесь порядок задаёт позиции 1–5 в верхней части селектора моделей Codex и кандидатов в модели по умолчанию для {cmd}.`,"sub.noneSelected":`Ничего не выбрано — выберите из списка ниже.`,"sub.models":`Модели`,"sub.search":`Поиск моделей (нативные gpt + маршрутизируемые)…`,"sub.noModels":`Нет моделей — сначала войдите в провайдера или добавьте нового.`,"sub.saved":`Сохранено {n} моделей. Начните новую сессию Codex (или выполните {cmd}), чтобы увидеть их как переопределения spawn_agent.`,"sub.saveFailed":`Не удалось сохранить`,"sub.networkError":`Ошибка сети — запущен ли прокси?`,"sub.loadFail":`Не удалось загрузить модели — запущен ли прокси?`,"sub.loading":`Загрузка…`,"sub.moveUp":`Переместить {m} вверх`,"sub.moveDown":`Переместить {m} вниз`,"sub.removeAria":`Убрать {m}`,"sub.workspace.addToFeatured":`Добавить {m} в избранные`,"sub.workspace.allModels":`Все модели`,"sub.workspace.featuredFull":`Список избранных заполнен (макс. 5)`,"sub.workspace.mainAria":`Сведения о модели субагента`,"sub.workspace.notFeatured":`Не в избранных`,"sub.workspace.priority":`Приоритет`,"sub.workspace.removeFromFeatured":`Убрать {m} из избранных`,"sub.workspace.selectModel":`Выберите модель`,"sub.workspace.selectModelDesc":`Выберите модель из списка, чтобы увидеть детали и добавить её в избранные для spawn_agent.`,"sub.workspace.selector":`Публичный селектор`,"sub.ultraMode":`Ультра-режим`,"sub.ultraModeHint":`Включает политику упреждающего делегирования мультиагентов для всех моделей и уровней reasoning effort (сам reasoning effort не меняется). Записывает features.multi_agent_v2.multi_agent_mode_hint_text в config.toml.`,"sub.ultraModeV2Required":`Требуется мультиагентная поверхность v2 — сначала включите multi_agent_v2 и выберите v2 в переключателе режима субагентов.`,"sub.ultraModeText":`Текст делегирования ультра-режима`,"sub.ultraModePreset":`Восстановить пресет`,"sub.ultraModeLoadFail":`Не удалось загрузить настройки ультра-режима — работает ли прокси?`,"sub.ultraModeSaveFail":`Не удалось сохранить настройки ультра-режима`,"sub.ultraModeSaved":`Ультра-режим сохранён. Применяется к новым сеансам Codex.`,"logs.title":`Журнал запросов`,"logs.tabLogs":`Логи`,"logs.tabDebug":`Отладка`,"logs.subtitle":`Недавние запросы через локальный прокси opencodex, новые сверху.`,"logs.autoRefresh":`Автообновление`,"logs.noRequests":`Запросов пока нет.`,"logs.loadError":`Не удалось загрузить журнал запросов.`,"logs.filter.surface.label":`Источник`,"logs.filter.surface.all":`Все`,"logs.filter.surface.claude":`Claude`,"logs.filter.surface.codex":`Codex`,"logs.filter.surface.grok":`Grok`,"logs.filter.interceptedHelpersOnly":`Только перехваченные помощники`,"logs.badge.interceptedHelper":`I · {model}`,"logs.badge.interceptedHelperTitle":`Перехваченный запрос помощника`,"logs.filter.conversation.label":`Диалог`,"logs.filter.conversation.placeholder":`Вставьте ID диалога`,"logs.filter.conversation.clear":`Сбросить`,"logs.filter.model.label":`Модель`,"logs.filter.model.placeholder":`Фильтр по модели или провайдеру`,"logs.filter.conversation.apply":`Фильтровать логи`,"logs.conversation.totals":`{requests} запросов · {tokens} токенов · {cost}`,"logs.conversation.scope":`Итоги только по загруженному кольцу Logs.`,"logs.conversation.excluded":`(из ~$ исключены {unpriced} без цены, {unmetered} без учёта)`,"logs.cost.approximate":`{amount}`,"logs.cost.lowerBound":`≥{amount}`,"logs.cost.unavailable":`недоступно`,"logs.detail.conversation":`Диалог`,"logs.badge.claude":`Claude`,"logs.badge.grok":`Grok`,"logs.col.time":`Время`,"logs.col.request":`Запрос`,"logs.col.model":`Модель`,"logs.col.effort":`Уровень`,"logs.col.provider":`Провайдер`,"logs.col.status":`Статус`,"logs.col.tokens":`Токены`,"logs.col.tokPerSec":`tok/s`,"logs.col.estimatedCost":`~$`,"logs.metric.tokPerSecTitle":`Выходные токены в секунду за полную длительность запроса`,"logs.metric.estimatedCostTitle":`Эквивалент стоимости по прайс-листу API, а не фактическое списание; если цену не удалось сопоставить, значение недоступно`,"usage.cost.total":`Эквивалент стоимости по прайс-листу API (за этот период)`,"usage.cost.disclaimer":`Не является счётом. Расходы могут покрываться подпиской или кредитами провайдера.`,"usage.cost.unpricedNote":`Исключено {count} запросов (нет цены или данных использования)`,"logs.detail.section.basic":`Основная информация`,"logs.detail.route.section":`Решение о маршруте`,"logs.detail.route.kind":`Тип маршрута`,"logs.detail.route.profile":`Профиль`,"logs.detail.route.selected":`Выбрано`,"logs.detail.route.candidates":`Кандидаты`,"logs.detail.route.unknown":`Для этого запроса трасса маршрута не записана (строка до трассировки).`,"logs.detail.section.performance":`Производительность`,"logs.detail.section.cost":`Эквивалент стоимости по прайс-листу API`,"logs.detail.section.attempts":`Попытки комбо`,"logs.detail.section.usage":`Сырые данные использования`,"logs.detail.ttft":`TTFT`,"logs.detail.costTotal":`Эквивалент по прайс-листу`,"logs.detail.totalTokens":`Всего токенов`,"logs.detail.matchedKey":`Совпавший ключ цены`,"logs.detail.priceSource":`Источник цены`,"logs.detail.unavailableReason":`Причина недоступности`,"logs.detail.copyRequestId":`Копировать ID запроса`,"logs.detail.copied":`Скопировано`,"logs.detail.source.jawcode":`каталог jawcode`,"logs.detail.source.expected":`Оверлей ожидаемых цен`,"logs.detail.source.user":`Ценовой оверлей провайдера`,"logs.detail.verification.verified":`Подтверждено`,"logs.detail.verification.derived":`Выведено из базовой модели`,"logs.detail.attempt.target":`Провайдер / модель`,"logs.detail.attempt.reason":`Результат / причина`,"logs.detail.attempt.completed":`Завершено`,"logs.detail.attempt.e2eNote":`Общий tok/s — сквозной показатель; для каждой попытки используется её собственная длительность.`,"logs.detail.attempt.recovery.transient5xx":`Временная ошибка 5xx`,"logs.detail.attempt.recovery.connectionReset":`Соединение сброшено`,"logs.detail.attempt.recovery.oauth401":`Повторная авторизация OAuth`,"logs.detail.attempt.recovery.key429":`Ключ ограничен (429)`,"logs.detail.attempt.recovery.rateLimit429":`Ограничение частоты запросов (429)`,"logs.detail.attempt.recovery.anthropicOauth429":`Anthropic OAuth ограничен (429)`,"logs.detail.attempt.recovery.image413":`Слишком большой размер изображения (413)`,"logs.detail.attempt.recovery.emptyCompletion":`Повтор пустого завершения`,"logs.detail.attempt.recovery.unknown":`Неизвестная причина восстановления`,"logs.detail.reason.usage_missing":`Данные об использовании не были сообщены.`,"logs.detail.reason.usage_unsupported":`Этот провайдер не сообщает данные об использовании.`,"logs.detail.reason.output_missing":`Положительное число выходных токенов не было сообщено.`,"logs.detail.reason.invalid_duration":`Длительность запроса некорректна.`,"logs.detail.reason.price_unmatched":`Подходящая цена не найдена.`,"logs.detail.reason.invalid_cache_breakdown":`Детализация кэш-токенов противоречит общему числу входных токенов.`,"logs.detail.reason.invalid_usage":`В данных использования есть некорректное значение токенов.`,"logs.detail.reason.combo_attempt_unavailable":`Не удалось рассчитать стоимость как минимум одной попытки комбо.`,"logs.detail.estimate.usage_estimated":`Данные об использовании от провайдера — оценочные.`,"logs.detail.estimate.cache_detail_missing":`Детализация кэша недоступна; входные токены оценены по верхней границе.`,"logs.detail.estimate.expected_price_overlay":`Использована подтверждённая ожидаемая цена из прайс-листа.`,"logs.detail.estimate.provider_cost_overlay":`Использован ценовой оверлей провайдера.`,"logs.detail.estimate.priority_lower_bound":`Подтверждённая цена Priority недоступна; показанная оценка является известной нижней границей.`,"logs.col.error":`Ошибка`,"logs.col.upstreamReason":`Причина от провайдера`,"logs.col.duration":`Длительность`,"logs.modelTooltip.model":`модель`,"logs.modelTooltip.resolvedModel":`разрешённая модель`,"logs.modelTooltip.requestedTier":`запрошенный уровень`,"logs.modelTooltip.configuredTier":`настроенный уровень`,"logs.modelTooltip.responseTier":`уровень ответа`,"logs.modelTooltip.supportsTier":`поддержка уровня`,"logs.tokens.reported":`сообщено`,"logs.tokens.unreported":`не сообщено`,"logs.tokens.unsupported":`не поддерживается`,"logs.tokens.estimated":`оценка`,"logs.tokens.input":`вход`,"logs.tokens.output":`выход`,"logs.tokens.cacheRead":`чтение кэша (c)`,"logs.tokens.cacheWrite":`запись кэша (w)`,"logs.tokens.reasoning":`рассуждения`,"logs.tokens.noCache":`нет данных кэша`,"logs.tokens.contextTotal":`активный контекст`,"logs.tokens.noCacheNote":`этот провайдер не сообщает данные о кэш-токенах`,"logs.tokens.noCacheCursor":`детализация кэша Cursor не сообщается`,"logs.tokens.noCacheCursorNote":`Cursor не передает число токенов чтения/записи кэша; это неизвестно, а не подтвержденный промах кэша`,"logs.tokens.estimatedNote":`оценка (провайдер не сообщает точные данные использования)`,"logs.details":`Детали`,"logs.detailTitle":`Детали запроса`,"logs.detailRaw":`Сырая запись лога`,"debug.title":`Отладка`,"debug.subtitle":`Включаемая по желанию диагностика транспорта провайдеров и извлечения данных использования. Ошибки запросов и 502 остаются на вкладке «Логи».`,"debug.debug":`Отладка провайдера`,"debug.usage":`Извлечение данных использования`,"debug.injection":`Лог инъекций`,"debug.claude":`Входящие Claude`,"debug.claudeInbound.title":`Входящие запросы Claude`,"debug.claudeInbound.sub":`Что фактически отправляет Claude Code/Desktop (thinking, effort, метаданные) — текст промптов не сохраняется.`,"debug.claudeInbound.empty":`Запросы пока не зафиксированы. Отправьте сообщение из Claude, пока эта опция включена.`,"debug.claudeInbound.time":`Время`,"debug.claudeInbound.endpoint":`Конечная точка`,"debug.claudeInbound.model":`Модель`,"debug.claudeInbound.none":`нет`,"debug.reset":`Сбросить временные переопределения`,"debug.refresh":`Обновить`,"debug.follow":`Следить`,"debug.streamProvider":`Провайдер`,"debug.streamUsage":`Использование`,"debug.streamInjection":`Инъекции`,"debug.loading":`Загрузка настроек отладки…`,"debug.loadFailed":`Не удалось загрузить настройки отладки.`,"debug.emptyTitle":`Отладочное логирование выключено`,"debug.empty":`Включите «Отладка провайдера» или «Извлечение данных использования» в карточке выше. Строки появятся здесь после отправки запроса через прокси.`,"debug.noLinesTitle":`Ожидание строк`,"debug.noLines.provider":`Отладка провайдера включена, но записываются только аномалии транспорта (потерянные или повреждённые фреймы, а также события подключения и повторов Cursor). Успешный запрос через провайдера вроде Anthropic может не дать ни одной строки.`,"debug.noLines.usage":`Извлечение данных использования включено, но пока ничего не зафиксировано. Отправьте чат или запрос через Codex — и записи появятся здесь.`,"debug.noLines.injection":`Лог инъекций включён, но пока ничего не зафиксировано. Он записывает инъекции мультиагентных инструкций и решения об ограничении уровня рассуждений на ходах совместной работы (collab) и подагентов.`,"usage.title":`Использование`,"usage.subtitle":`Локальный учёт токенов вашего прокси. Отсутствующие данные никогда не показываются как ноль.`,"usage.loading":`Загрузка данных об использовании…`,"usage.empty":`Данных об использовании пока нет. Отправьте запрос через прокси, чтобы увидеть здесь активность.`,"usage.loadError":`Не удалось загрузить данные об использовании.`,"usage.range.all":`Все`,"usage.range.available":`Доступная история`,"usage.historyTruncated":`Итоги охватывают только доступную историю, поскольку старые данные не загружены.`,"usage.historyTruncatedWindow":`У загруженных записей время начала запроса находится в диапазоне от {start} до {end}. Более ранние записи файла пропущены из-за лимита чтения, поэтому выбранный период может быть неполным.`,"usage.range.30d":`30 дн.`,"usage.range.7d":`7 дн.`,"usage.card.requests":`Запросы`,"usage.card.measured":`Измерено`,"usage.card.reported":`Сообщено`,"usage.card.totalTokens":`Всего токенов`,"usage.card.cachedTokens":`Чтения из кэша`,"usage.card.cachedTokensHint":`Токены промпта, отданные из кэша провайдера (чтения). Записи в кэш показаны ниже, если они есть.`,"usage.card.cacheWriteTokens":`записи в кэш`,"usage.card.coverage":`Покрытие`,"usage.card.activeDays":`Активные дни`,"usage.section.heatmap":`Активность по дням`,"usage.section.overview":`Обзор`,"usage.section.models":`Модели`,"usage.section.providers":`Провайдеры`,"usage.section.coverage":`Детализация покрытия`,"usage.workspace.report":`Отчёт об использовании`,"usage.workspace.sections":`Разделы использования`,"usage.coverage.measured":`Измерено`,"usage.coverage.reported":`Сообщено провайдером`,"usage.coverage.estimated":`Оценено`,"usage.coverage.note":`Измеренные записи включают количество токенов, сообщённое провайдером, и оценочные значения. Запросы без отчёта и неподдерживаемые запросы учитываются, но никогда не показываются как ноль токенов.`,"usage.search.models":`Поиск моделей…`,"usage.col.requests":`Запросы`,"usage.col.measured":`Измерено`,"usage.col.reported":`Сообщено`,"usage.col.tokens":`Токены`,"usage.col.share":`Доля`,"usage.heatmap.less":`Меньше`,"usage.heatmap.more":`Больше`,"usage.dayMon":`Пн`,"usage.dayWed":`Ср`,"usage.dayFri":`Пт`,"usage.heatmap.tooltipTokens":`{tokens} токенов`,"usage.heatmap.tooltipRequests":`{requests} запросов`,"nav.storage":`Хранилище`,"storage.title":`Хранилище`,"storage.subtitle":`Смотрите, что занимает CODEX_HOME. Очистка не затрагивает активные сессии.`,"storage.loading":`Сканирование хранилища…`,"storage.empty":`CODEX_HOME пуст или отсутствует — показывать нечего.`,"storage.error":`Не удалось просканировать хранилище. Убедитесь, что CODEX_HOME указывает на корректный каталог.`,"storage.refresh":`Пересканировать`,"storage.rescanned":`Сканирование завершено.`,"storage.card.total":`Общий размер`,"storage.card.files":`Файлы`,"storage.card.home":`CODEX_HOME`,"storage.snapshot.lastScan":`Последний скан`,"storage.snapshot.scanning":`Сканирование…`,"storage.snapshot.unavailable":`Сканирования ещё не было.`,"storage.cleanupCard.title":`Освободить место`,"storage.cleanupCard.tabs":`Параметры очистки`,"storage.cleanupCard.tab.policy":`Политика`,"storage.cleanupCard.tab.quarantine":`Карантин`,"storage.cleanup.noArchives":`Нет архивных сессий для очистки.`,"storage.section.buckets":`Категории`,"storage.section.largest":`Крупнейшие файлы`,"storage.workspace.overview":`Обзор`,"storage.workspace.selectBucket":`Выберите сегмент в списке, чтобы увидеть разбивку.`,"storage.col.bucket":`Категория`,"storage.col.size":`Размер`,"storage.col.files":`Файлы`,"storage.col.oldest":`Старейший`,"storage.col.newest":`Новейший`,"storage.col.rows":`Строки БД`,"storage.rows.unknown":`неизвестно (заблокировано)`,"storage.bucket.sessions":`Активные сессии`,"storage.bucket.archived_sessions":`Архивные сессии`,"storage.bucket.logs_db":`База данных логов`,"storage.bucket.state_db":`База данных состояния`,"storage.bucket.attachments":`Вложения`,"storage.bucket.deletion_manifests":`Манифесты удаления`,"storage.bucket.other":`Прочее`,"storage.cleanup.title":`Очистка архива`,"storage.cleanup.help":`Удаляет самые старые архивные сессии по проценту. Активные сессии не затрагиваются. По умолчанию — карантин: файлы перемещаются в CODEX_HOME/.trash.`,"storage.cleanup.slider":`Доля самых старых архивов`,"storage.cleanup.percent":`{percent}%`,"storage.cleanup.preset":`{percent}`,"storage.cleanup.preview":`Предпросмотр`,"storage.cleanup.confirmTitle":`Подтвердить очистку архива`,"storage.cleanup.confirmBody":`Будет обработано {count} архивных файл(ов) (~{size}), самые старые {percent}%.`,"storage.cleanup.moreFiles":`…и ещё {n}`,"storage.cleanup.permanent":`Удалить навсегда (без карантина)`,"storage.cleanup.permanentWarn":`Безвозвратное удаление нельзя отменить.`,"storage.cleanup.quarantineNote":`Файлы перемещаются в .trash под CODEX_HOME. Восстановить можно на вкладке «Карантин».`,"storage.cleanup.cancel":`Отмена`,"storage.cleanup.confirmQuarantine":`В карантин`,"storage.cleanup.confirmPermanent":`Удалить навсегда`,"storage.cleanup.doneQuarantine":`В карантин: {count} файл(ов) ({size}).`,"storage.cleanup.donePermanent":`Удалено навсегда: {count} файл(ов) ({size}).`,"storage.cleanup.previewFailed":`Не удалось выполнить предпросмотр.`,"storage.cleanup.cleanupFailed":`Не удалось выполнить очистку.`,"storage.cleanup.err.codex_busy":`Codex использует state.sqlite — закройте Codex и повторите попытку.`,"storage.cleanup.err.stale_preview":`Архивы изменились после предпросмотра — выполните предпросмотр снова.`,"storage.cleanup.err.restore_pending_overlap":`Выбранные архивы пересекаются с незавершённым восстановлением из корзины — завершите или повторите восстановление.`,"storage.cleanup.err.referenced_history":`Выбранные архивы всё ещё ссылаются из forked или paginated history.`,"storage.cleanup.err.invalid_digest":`Digest предпросмотра отсутствует или недействителен.`,"storage.cleanup.err.invalid_mode":`Режим должен быть quarantine или permanent.`,"storage.cleanup.err.fs_failed":`Ошибка файловой очистки. Часть изменений могла уже примениться — проверьте CODEX_HOME/.trash и указанный путь восстановления.`,"storage.cleanup.err.fs_failed_trash":`Ошибка файловой очистки. Часть изменений могла уже примениться — проверьте {trashDir} и manifest.json на восстанавливаемые файлы.`,"storage.cleanup.err.db_reconcile_failed":`Не удалось обновить базу состояния Codex.`,"storage.cleanup.err.cleanup_failed":`Не удалось выполнить очистку.`,"storage.trash.title":`Карантин`,"storage.trash.help":`Архивные сессии в CODEX_HOME/.trash. Восстановление возвращает JSONL и строки потоков.`,"storage.trash.empty":`Нет записей в карантине.`,"storage.trash.loading":`Загрузка карантина…`,"storage.trash.col.when":`В карантине с`,"storage.trash.col.files":`Файлы`,"storage.trash.col.size":`Размер`,"storage.trash.col.mode":`Режим`,"storage.trash.col.id":`Запись`,"storage.trash.restore":`Восстановить`,"storage.trash.confirmTitle":`Восстановить запись карантина?`,"storage.trash.confirmBody":`Вернуть {count} файл(ов) (~{size}) из {id} в архивные сессии.`,"storage.trash.cancel":`Отмена`,"storage.trash.confirmRestore":`Восстановить`,"storage.trash.done":`Восстановлено {count} файл(ов) ({size}).`,"storage.trash.restoreFailed":`Не удалось восстановить.`,"storage.trash.listFailed":`Не удалось получить список карантина.`,"storage.trash.mode.quarantine":`карантин`,"storage.trash.mode.permanent":`permanent (незавершён)`,"storage.trash.err.codex_busy":`Codex использует state.sqlite — закройте Codex и повторите попытку.`,"storage.trash.err.invalid_trash":`Идентификатор записи корзины отсутствует или недействителен.`,"storage.trash.err.missing_trash":`Запись корзины не найдена.`,"storage.trash.err.dest_exists":`Цель восстановления уже существует — удалите или переименуйте архивный файл и повторите.`,"storage.trash.err.fs_failed":`Ошибка восстановления файлов. Часть файлов могла уже восстановиться — проверьте archived_sessions и .trash.`,"storage.trash.err.storage_mutation_busy":`Выполняется другая очистка или восстановление — повторите позже.`,"storage.trash.err.db_reconcile_failed":`Не удалось восстановить строки базы состояния Codex.`,"storage.trash.err.restore_failed":`Не удалось восстановить.`,"storage.trash.err.restore_worker_timeout":`Восстановление заняло слишком много времени (более 10 минут) и было остановлено.`,"storage.trash.err.restore_worker_aborted":`Восстановление отменено при завершении работы.`,"storage.trash.err.restore_worker_failed":`Worker восстановления завершился с ошибкой или аварийно.`,"storage.policy.title":`Политика автоочистки`,"storage.policy.help":`Необязательная пакетная очистка, когда архивные сессии превышают порог. По умолчанию выкл. — никогда не включается сама.`,"storage.policy.loading":`Загрузка политики…`,"storage.policy.loadFailed":`Не удалось загрузить политику очистки.`,"storage.policy.saveFailed":`Не удалось сохранить политику очистки.`,"storage.policy.runFailed":`Не удалось выполнить политику.`,"storage.policy.alreadyRunning":`Выполнение политики очистки уже выполняется.`,"storage.policy.invalid":`Недопустимые значения политики.`,"storage.policy.enabled":`Включить автоочистку`,"storage.policy.enabledHint":`По умолчанию выкл. При включении работает только по выбранному расписанию (или «Запустить сейчас»).`,"storage.policy.threshold":`Когда размер архива больше (ГиБ)`,"storage.policy.trigger":`Триггер`,"storage.policy.target":`Цель очистки`,"storage.policy.targetPercent":`Удалить самые старые архивы (%)`,"storage.policy.targetReduce":`Уменьшить архив до (ГиБ)`,"storage.policy.thresholdInc":`Увеличить порог`,"storage.policy.thresholdDec":`Уменьшить порог`,"storage.policy.percentInc":`Увеличить процент`,"storage.policy.percentDec":`Уменьшить процент`,"storage.policy.reduceInc":`Увеличить целевой размер`,"storage.policy.reduceDec":`Уменьшить целевой размер`,"storage.policy.schedule":`Расписание`,"storage.policy.schedule.manual":`Только вручную`,"storage.policy.schedule.startup":`При запуске прокси`,"storage.policy.schedule.daily":`Ежедневно`,"storage.policy.schedule.weekly":`Еженедельно`,"storage.policy.mode":`Режим удаления`,"storage.policy.mode.quarantine":`Карантин (по умолчанию)`,"storage.policy.mode.permanent":`Удалить навсегда`,"storage.policy.permanentWarn":`Постоянный режим нельзя отменить. Предпочитайте карантин, если не уверены.`,"storage.policy.lastRun":`Последний запуск`,"storage.policy.lastRunDetail":`Удалено {count} · освобождено {size}`,"storage.policy.nextRun":`Следующий запуск`,"storage.policy.never":`Никогда`,"storage.policy.save":`Сохранить`,"storage.policy.runNow":`Запустить сейчас`,"storage.policy.running":`Выполняется…`,"storage.policy.saved":`Политика сохранена.`,"storage.policy.skippedDisabled":`Политика отключена — сначала включите её.`,"storage.policy.skippedUnder":`Размер архива ниже порога — делать нечего.`,"storage.policy.skippedEmpty":`Нет архивных кандидатов под цель.`,"storage.policy.doneQuarantine":`Политика отправила в карантин {count} файл(ов) ({size}).`,"storage.policy.donePermanent":`Политика навсегда удалила {count} файл(ов) ({size}).`,"storage.policy.metadataSaveWarning":`Выполнение политики завершено, но не удалось сохранить метаданные расписания.`,"modal.addNamed":`Добавить: {label}`,"modal.add":`Добавить провайдера`,"modal.search":`Поиск провайдеров…`,"modal.logInWith":`Войти через {label}`,"modal.waitingBrowser":`Ожидание браузера…`,"modal.providerName":`Название провайдера`,"modal.adapter":`Адаптер`,"modal.baseUrl":`Базовый URL`,"modal.endpoint":`Конечная точка`,"modal.endpoint.tokenPlan":`Пакет токенов`,"modal.endpoint.payAsYouGo":`Оплата по факту`,"modal.endpoint.custom":`Своя`,"modal.defaultModel":`Модель по умолчанию (необязательно)`,"modal.allowPrivateNetwork":`Разрешить локальную/частную сеть`,"modal.allowPrivateNetworkHint":`Включайте только для провайдеров, намеренно развёрнутых у себя. Конечные точки метаданных остаются заблокированными.`,"modal.nameRequired":`Укажите название провайдера`,"modal.baseUrlRequired":`Укажите базовый URL`,"modal.networkError":`Ошибка сети — запущен ли прокси?`,"modal.loginFailStart":`Не удалось начать вход`,"modal.waitingLogin":`Ожидание входа в браузере…`,"modal.loggingIn":`Выполняется вход…`,"modal.loginTimeout":`Время входа истекло — попробуйте ещё раз.`,"modal.back":`Назад`,"modal.badge.oauth":`OAuth`,"modal.customProvider":`Свой провайдер`,"modal.failedStatus":`Ошибка ({status})`,"modal.loginError":`Ошибка входа: {error}`,"modal.badge.codexLogin":`Вход через Codex`,"modal.badge.local":`Локальный`,"modal.badge.apiKey":`API-ключ`,"modal.badge.direct":`Прямой`,"modal.badge.pool":`Пул`,"modal.badge.free":`Бесплатно`,"modal.invalidPreset":`Этот встроенный пресет провайдера неполный. Перезапустите прокси и попробуйте ещё раз.`,"modal.freeTierTitle":`Бесплатный тариф`,"modal.freeTierDefault":`API-ключ не нужен. Работает из коробки.`,"modal.tab.accounts":`Аккаунты`,"modal.tab.free":`Бесплатные`,"modal.tab.paid":`Платные`,"modal.accountsHint":`Здесь можно войти в аккаунты ChatGPT/Codex и OAuth-провайдеров, а также в аккаунты с API-ключами. Провайдер OpenAI уже встроен — просто войдите, а не добавляйте его заново.`,"modal.accountsCodexAuthLink":`Аутентификация Codex`,"modal.notListed":`Нет нужного провайдера? Добавьте свой`,"modal.catalogLoading":`Загрузка каталога…`,"modal.accountLogin":`Войти`,"modal.accountLogout":`Выйти`,"modal.accountAdd":`Добавить аккаунт`,"modal.accountManage":`Управление`,"modal.accountCodexPool":`Пул аккаунтов ChatGPT`,"modal.accountLoggedIn":`Вход выполнен`,"modal.accountLoggedOut":`Вход не выполнен`,"quota.fiveHourLimit":`5-часовой лимит`,"quota.ageMinutes":`{n} мин`,"quota.ageHours":`{n} ч`,"quota.ageDays":`{n} дн`,"quota.observedAgo":`Получено {age} назад`,"quota.observedHint":`Meta сообщает об использовании только во время потокового ответа, поэтому это последнее полученное значение, а не текущее.`,"quota.weeklyLimit":`Недельный лимит`,"quota.monthlyLimit":`30-дневный лимит`,"quota.cursorFirstParty":`Собственные модели`,"quota.cursorApiUsage":`Использование API`,"quota.totalSubscriptionCredits":`Всего кредитов подписки`,"quota.creditsBalance":`Остаток кредитов`,"quota.creditsPeriodEnds":`Расчётный период заканчивается {date}`,"quota.usedPercent":`Использовано {pct}%`,"quota.limitReached":`Лимит исчерпан`,"quota.resetsToday":`Сброс сегодня в {time}`,"quota.resetsTomorrow":`Сброс завтра в {time}`,"quota.resetsAt":`Сброс: {when}`,"quota.resetsRelativeMinutes":`Сброс через {n} мин`,"quota.resetsRelativeHours":`Сброс через {n} ч`,"pws.status.ready":`Готов`,"pws.status.needsSetup":`Требуется настройка`,"pws.status.needsAttention":`Требует внимания`,"pws.auth.chatgptPassthrough":`Сквозной режим ChatGPT`,"pws.auth.noKey":`Ключ не нужен`,"pws.freeTitle":`Бесплатный тариф (ключ всё же может потребоваться)`,"pws.localTitle":`Локальная среда выполнения`,"pws.modelCountOne":`1 модель`,"pws.modelCount":`{count} моделей`,"pws.rail.suffixDefault":` · по умолчанию`,"pws.rail.suffixLocal":` · локальный`,"pws.rail.suffixFree":` · бесплатный`,"pws.rail.selectAria":`Выбрать {name} — {status}{suffix}`,"pws.searchPlaceholder":`Поиск провайдеров…`,"pws.filterAria":`Фильтр провайдеров`,"pws.providerFiltersAria":`Фильтры провайдеров`,"pws.filters":`Фильтры`,"pws.filterStatus":`Статус`,"pws.pricing":`Тариф`,"pws.paid":`Платные`,"pws.filterType":`Тип`,"pws.type.cloud":`Облачные`,"pws.type.local":`Локальные`,"pws.type.selfHosted":`Свой хостинг`,"pws.type.login":`Вход`,"pws.sort":`Сортировка`,"pws.sortProvidersAria":`Сортировка провайдеров`,"pws.sort.az":`A–Z`,"pws.sort.za":`Z–A`,"pws.sort.freePaid":`Сначала бесплатные`,"pws.sort.paidFree":`Сначала платные`,"pws.sort.accountsFirst":`Сначала с аккаунтами`,"pws.resetAll":`Сбросить всё`,"pws.providerList":`Список провайдеров`,"pws.providersAria":`Провайдеры`,"pws.groupReady":`Готовы ({count})`,"pws.groupNeedsSetup":`Требуется настройка ({count})`,"pws.groupDisabled":`Отключены ({count})`,"pws.noSearchResults":`По вашему запросу провайдеры не найдены.`,"pws.noMatchFilters":`Нет провайдеров, соответствующих фильтрам.`,"pws.noProvidersConfigured":`Провайдеры не настроены.`,"pws.workspaceMainAria":`Сведения о провайдере`,"pws.detailComingSoon":`Подробный вид скоро появится — для управления этим провайдером используйте классический вид.`,"pws.selectPrompt":`Выберите провайдера из списка.`,"pws.connectFirst":`Подключите первого провайдера`,"pws.empty.browseFree":`Посмотреть бесплатных провайдеров`,"pws.empty.browseFreeDesc":`Начните без подписки`,"pws.empty.connectAccount":`Подключить аккаунт`,"pws.empty.connectAccountDesc":`Войдите через ChatGPT или аккаунт провайдера`,"pws.empty.addEndpoint":`Добавить конечную точку`,"pws.empty.addEndpointDesc":`Свой базовый URL и API-ключ`,"pws.tab.overview":`Обзор`,"pws.tab.models":`Модели`,"pws.tab.usage":`Использование`,"pws.tab.accounts":`Аккаунты`,"pws.tab.settings":`Настройки`,"pws.connection":`Подключение`,"pws.status.connected":`Подключено`,"pws.attentionTitle":`Требует внимания`,"pws.attention.reauth":`Активному аккаунту требуется повторная аутентификация`,"pws.attention.reauthForward":`Активному аккаунту Codex требуется повторная аутентификация — откройте вкладку «Аккаунты», чтобы исправить`,"pws.attention.missingCredentials":`Отсутствуют учётные данные`,"pws.cell.auth":`Аутентификация`,"pws.cell.note":`Заметка`,"pws.cell.defaultModel":`Модель по умолчанию`,"pws.statsAria":`Статистика провайдера`,"pws.statsTitle":`Статистика`,"pws.stats.totalRequests":`Запросы (30 дн.)`,"pws.stats.totalTokens":`Токены (30 дн.)`,"pws.stats.quotaUpdated":`Квота обновлена`,"pws.stats.quotaTracked":`Лимиты запросов отслеживаются на вкладке «Использование».`,"pws.stats.source":`Источник`,"pws.usageLast30d":`Использование (последние 30 дней)`,"pws.estimatedCost":`Ориентировочная стоимость`,"pws.costDisclaimer":`Оценка на основе публичных цен API, не фактический счёт.`,"pws.modelBreakdown":`Разбивка по моделям`,"pws.col.model":`Модель`,"pws.col.cost":`Ориент. стоимость`,"pws.col.tokens":`Токены`,"pws.col.requests":`Запр.`,"pws.col.share":`Доля`,"pws.tokenInput":`Вход`,"pws.tokenOutput":`Выход`,"pws.metricRequests":`запросов`,"pws.metricTokens":`токенов`,"pws.usageUnavailable":`Использование пока не зафиксировано.`,"pws.rateLimits":`Лимиты запросов`,"pws.quotaUnavailable":`Нет данных о квоте для этого провайдера.`,"pws.accountQuotaUnavailable":`Данные о лимитах временно недоступны; при наличии показываются последние известные значения.`,"pws.selected":`Выбрана`,"pws.copyModelId":`Копировать ID`,"pws.modelCopied":`Скопировано!`,"pws.modelsAvailable":`Доступно: {count}`,"pws.modelSearchPlaceholder":`Фильтр моделей…`,"pws.modelsLoading":`Загрузка моделей…`,"pws.modelsLoadFailed":`Не удалось загрузить модели.`,"pws.modelsNeedsReauth":`Для автоматического обнаружения моделей необходимо повторно войти в аккаунт. Пока отображаются настроенные модели.`,"pws.modelsConfiguredFallback":`Отображаются настроенные модели (автоматическое обнаружение недоступно).`,"pws.modelsTruncated":`Показаны первые {shown} моделей из {total}. Примените фильтр, чтобы сузить список.`,"pws.retry":`Повторить`,"pws.noModels":`Для этого провайдера модели не обнаружены.`,"pws.noModelMatch":`Нет моделей, соответствующих фильтру.`,"pws.adapterBaseRequired":`Укажите адаптер и базовый URL.`,"pws.addAccount":`Добавить аккаунт`,"pws.addKey":`Добавить API-ключ`,"pws.apiKeys":`API-ключи`,"pws.authMode":`Режим аутентификации`,"pws.availableAccounts":`Доступные аккаунты`,"pws.accountOrdinal":`Аккаунт {count}`,"pws.accountsLoading":`Загрузка аккаунтов…`,"pws.accountsLoadFailed":`Не удалось загрузить аккаунты.`,"pws.retryAccounts":`Повторить`,"pws.noAccounts":`Аккаунты пока не подключены.`,"pws.cockpitImportDescription":`Импортируйте JSON-экспорт Cockpit Tools Antigravity с этого устройства. Содержимое файла не показывается.`,"pws.cockpitImportFileLabel":`JSON-экспорт Cockpit Tools Antigravity`,"pws.cockpitImportChooseFile":`Выбрать JSON-файл`,"pws.cockpitImporting":`Импорт…`,"pws.cockpitImportInvalid":`Выбранный файл не является корректным JSON-экспортом или слишком велик.`,"pws.cockpitImportFailed":`Не удалось завершить импорт аккаунтов.`,"pws.cockpitImportComplete":`Импорт завершён: импортировано — {imported}, обновлено — {updated}, ошибок — {failed}, неподдерживаемых — {unsupported}.`,"pws.accountSwitching":`Переключение…`,"pws.accountCurrent":`Текущий аккаунт`,"pws.defaultModelNone":`Нет (использовать значение провайдера)`,"pws.discardSettings":`Не сохранять`,"pws.jsonEditorDesc":`Редактируйте исходную JSON-конфигурацию провайдера. Изменения сохраняются сразу.`,"pws.jsonEditorTitle":`Редактор JSON — {name}`,"pws.jsonRestore":`Восстановить`,"pws.jsonSave":`Сохранить`,"pws.loggedInTitle":`Вход выполнен`,"pws.notLoggedInTitle":`Вход не выполнен`,"pws.note":`Заметка`,"pws.allowPrivateNetwork":`Разрешить локальную/частную сеть`,"pws.liveModels":`Обнаруживать модели провайдера`,"pws.liveModelsDesc":`Загружать актуальный каталог моделей провайдера. Выключите, чтобы использовать только настроенные статические модели.`,"pws.xaiResponsesOptIn":`Использовать Responses API для Grok 4.5 и 4.6`,"pws.xaiResponsesOptInDesc":`Направляет обе модели через openai-responses. Другие модели Grok и поведение tier не меняются.`,"pws.xaiResponsesOptInMixed":`Включено частично.`,"pws.cursorTransport":`Транспорт Cursor`,"pws.cursorTransportHttp2":`HTTP/2 (по умолчанию)`,"pws.cursorTransportHttp1":`HTTP/1.1 (совместимость с прокси)`,"pws.cursorTransportDesc":`Используйте HTTP/1.1, если прокси нестабильно передаёт поток Cursor по HTTP/2.`,"pws.optionalPlaceholder":`Необязательно`,"pws.providerId":`ID провайдера`,"pws.reauth":`Нужна переавторизация`,"pws.reauthenticate":`Переавторизоваться`,"pws.copyDoctor":`Скопировать ocx doctor`,"pws.doctorCopied":`Скопировано`,"pws.healthCooldownHint":`Дождитесь окончания паузы. Пока не проверяйте эту учётную запись.`,"pws.doctorCopyUnavailable":`Буфер обмена недоступен`,"pws.healthLabel.rateLimited":`Ограничение частоты`,"pws.healthLabel.quotaLimited":`Ограничение квоты`,"pws.healthLabel.reauthRequired":`Требуется повторная аутентификация`,"pws.healthLabel.refreshFailed":`Ошибка обновления`,"pws.healthLabel.metadataMismatch":`Несоответствие метаданных`,"pws.healthLabel.credentialConflict":`Конфликт учётных данных`,"pws.healthSummary.rateLimited":`{provider} {account}: ограничение частоты до {until}. Маршрутизация этой учётной записи приостановлена до этого времени.`,"pws.healthSummary.quotaLimited":`{provider} {account}: квота ограничена до {until}. Маршрутизация этой учётной записи приостановлена до этого времени.`,"pws.healthSummary.reauthRequired":`{provider} {account}: требуется повторная аутентификация.`,"pws.healthSummary.credentialConflict":`{provider} {account}: конфликт учётных данных.`,"pws.healthSummary.metadataMismatch":`{provider} {account}: несоответствие метаданных.`,"pws.healthSummary.staleCredentials":`{provider} {account}: неполные учётные данные.`,"pws.removeConfirm":`Удалить`,"pws.removeConfirmBody":`Удалить провайдера "{name}"? Это действие нельзя отменить.`,"pws.removeDefaultConfirmBody":`Удалить провайдера по умолчанию "{name}"? "{defaultProvider}" станет провайдером по умолчанию. Это действие нельзя отменить.`,"pws.removeConfirmTitle":`Удалить провайдера`,"pws.saveSettings":`Сохранить`,"pws.pacingTitle":`Интервал запросов`,"pws.pacingDesc":`Равномерно задерживает начало исходящих запросов к провайдеру. Потоковые ответы могут пересекаться.`,"pws.pacingEnabled":`Включено`,"pws.pacingRpm":`Запросов в минуту`,"pws.pacingRpmUnit":`RPM`,"pws.pacingDelay":`Минимальный интервал (мс)`,"pws.pacingSlowerWins":`Действует более медленный лимит провайдера. Правила моделей могут только увеличить задержку.`,"pws.pacingQueued":`в очереди`,"pws.pacingNextSlot":`до следующего слота`,"pws.pacingLastModel":`последняя модель`,"pws.pacingNone":`Нет`,"pws.pacingModelOverrides":`Правила моделей`,"pws.pacingModel":`Модель`,"pws.pacingAdd":`Добавить правило`,"pws.pacingRemove":`Удалить`,"pws.pacingRemoveModel":`Удалить интервал запросов для {model}`,"pws.pacingRuleRequired":`Сначала задайте лимит провайдера или правило модели.`,"pws.saving":`Сохранение…`,"pws.settingsSaved":`Настройки сохранены.`,"pws.accountModeSaved":`Режим аккаунта сохранён.`,"pws.accountModeFailed":`Не удалось переключить режим аккаунта.`,"pws.accountModeConfirm":`Переключить режим аккаунта OpenAI? Текущие беседы будут перенаправлены на другой набор аккаунтов, а использование квоты будет учитываться в новом режиме.`,"pws.settingsUnsavedBar":`Есть несохранённые изменения.`,"pws.unsavedLeaveBody":`Есть несохранённые изменения. Сохранить их перед переходом?`,"pws.unsavedLeaveTitle":`Несохранённые изменения`,"pws.attentionRequired":`Требуется внимание`,"pws.attentionAria":`{name}: {reason}`,"pws.missingCredentials":`Отсутствуют учётные данные`,"pws.editJsonDesc":`Редактировать конфигурацию прокси в формате JSON`,"pws.updatesUnavailable":`Обновление провайдера недоступно.`,"pws.dashboard.title":`Обзор провайдеров`,"pws.dashboard.subtitle":`Управляйте всеми провайдерами моделей в одном месте.`,"pws.dashboard.rateLimits":`Лимиты запросов`,"pws.capacity.estimate":`Оценка пула по настроенным весам`,"pws.capacity.currentAccount":`Текущая активная учётная запись`,"pws.capacity.nextRecovery":`Следующее восстановление ёмкости`,"pws.capacity.recoveryShare":`+{percent}% ёмкости пула`,"pws.capacity.incomplete":`Неполное покрытие: исключено аккаунтов: {excluded}`,"pws.capacity.uncalibratedPlan":`Аккаунтов с некалиброванным планом: {count}. Они учитываются с базовым весом места, поэтому оценка может быть заниженной`,"pws.capacity.partial":`Частичное покрытие окон: для {count} аккаунтов доступны не все показанные окна лимитов`,"pws.capacity.windowPartial":`Частично`,"pws.capacity.windowPartialA11y":`{window}: неполное покрытие аккаунтов`,"pws.dashboard.recentlyUsed":`Недавно использованные`,"pws.dashboard.requests":`{count} запросов`,"pws.dashboard.checkedAgo":`Проверено {time}`,"pws.dashboard.noQuota":`Нет данных о квоте`,"pws.dashboard.noUsage":`Данных об использовании пока нет`,"pws.dashboard.noRateLimits":`Данных о лимитах пока нет`,"pws.allProviders":`Обзор провайдеров`,"pws.enabledLabel":`Включён`,"pws.testConnection":`Проверить подключение`,"pws.testing":`Проверка…`,"pws.connectionOk":`Подключение успешно`,"pws.connectionFailed":`Ошибка подключения`,"pws.connectionNotApplicable":`Не применимо — этот провайдер использует статический каталог моделей.`,"pws.editSettings":`Изменить настройки`,"pws.viewUsage":`Подробнее об использовании`,"pws.allSystemsOk":`Все системы работают штатно`,"pws.apiKeyConfigured":`API-ключ настроен`,"pws.addApiKey":`Добавить API-ключ`,"pws.loggedInAs":`Выполнен вход как {email}`,"pws.notLoggedIn":`Вход не выполнен`,"pws.passthrough":`Сквозной режим Codex`,"pws.notes":`Заметки`,"pws.notePlaceholder":`Добавьте заметку об этом провайдере...`,"pws.noteSaved":`Заметка сохранена`,"pws.authSummary":`Аутентификация`,"time.justNow":`Только что`,"time.notChecked":`Не проверялось`,"time.minutesAgo":`{n} мин назад`,"time.hoursAgo":`{n} ч назад`,"time.daysAgo":`{n} дн. назад`,"modal.noMatch":`Ничего не найдено.`,"modal.oauthDefaultNote":`Войдите со своим аккаунтом — API-ключ не нужен.`,"modal.oauthComingSoon":`Вход через OAuth для {label} появится в следующем обновлении. Пока используйте API-ключ.`,"modal.oauthComingSoonShort":`Вход через OAuth для этого провайдера появится в следующем обновлении — пока используйте API-ключ.`,"modal.useApiKeyInstead":`Использовать API-ключ`,"modal.setupGuide":`Инструкция по настройке`,"modal.setupStep1Prefix":`Откройте`,"modal.setupDashboardLink":`панель управления {label}`,"modal.setupStep1Suffix":`и скопируйте свой API-ключ`,"modal.setupStep2":`Вставьте его в поле «API-ключ» ниже`,"modal.setupStep3":`Нажмите «Добавить провайдера» — модели будут обнаружены автоматически`,"modal.namePlaceholder":`напр. openrouter`,"modal.duplicateWarn":`Провайдер "{name}" уже существует и будет перезаписан.`,"modal.forwardHintPrefix":`Ключ не нужен — прокси передаёт ваши учётные данные`,"modal.forwardCredentials":`codex login`,"modal.forwardHintSuffix":`этому провайдеру.`,"modal.localHint":`API-ключ не сохраняется. Будет добавлен статический публичный каталог моделей Cursor для Codex, но живой транспорт Cursor и нативное выполнение файловых и shell-операций остаются отключёнными до прохождения аудита.`,"modal.getApiKey":`Получить API-ключ {label}`,"modal.apiKey":`API-ключ`,"modal.apiKeyTransport":`Заголовок API-ключа`,"modal.apiKeyTransportNative":`x-api-key (нативный Anthropic)`,"modal.apiKeyTransportBearer":`Authorization: Bearer`,"modal.apiKeyPlaceholder":`sk-… (или $ENV_VAR)`,"modal.defaultModelPlaceholder":`напр. gpt-5.5`,"modal.baseUrlPlaceholder":`https://...`,"modal.baseUrlPlaceholderError":`Базовый URL содержит незаменённый {placeholder}. Замените его реальным значением.`,"modal.baseUrlPlaceholderHint":`Перед добавлением замените {placeholder} в базовом URL на реальный ID аккаунта.`,"modal.adding":`Добавление…`,"modal.useOauthLogin":`← Войти через OAuth`,"nav.codexAuth":`Аутентификация Codex`,"nav.codexSet":`Настройки Codex`,"codexSet.tab.multiauth":`Мультиаутентификация`,"codexSet.tab.prompt":`Промпт`,"codexSet.prompt.title":`Слои промпта`,"codexSet.prompt.timing":`Применяется к новым сессиям. Запущенные сессии сохраняют текущие настройки промпта.`,"codexSet.prompt.staleRevision":`Конфигурация изменилась в другом месте. Список перезагружен.`,"codexSet.prompt.writeFailed":`Не удалось сохранить изменение.`,"codexSet.prompt.loadFailed":`Не удалось загрузить слои промпта.`,"codexSet.prompt.repair":`Восстановить`,"codexSet.prompt.repairFailed":`Не удалось выполнить восстановление.`,"codexSet.drift.journalPresent":`Предыдущая запись не завершилась. Восстановление произойдёт автоматически при следующей записи.`,"codexSet.drift.projectionStale":`Сохранённые слои и значение в config.toml расходятся. Восстановление перезапишет значение из ваших слоёв.`,"codexSet.drift.storeMissing":`Файл слоёв отсутствует, но инструкции в config.toml остались. Восстановление сначала создаст резервную копию и сохранит текст одним слоем.`,"codexSet.drift.ownedMalformed":`Сгенерированная строка в config.toml была изменена вручную, поэтому перезаписывать её небезопасно.`,"codexSet.custom.adoptUnsupported":`Значение в {path}, строка {line}, не является однострочной строкой и не может быть импортировано. Перенесите его вручную, чтобы управлять им здесь.`,"codexSet.prompt.unreadable":`Файл конфигурации Codex существует, но его не удалось прочитать, поэтому изменения отклонены.`,"codexSet.layer.permissions":`Разрешения`,"codexSet.layer.collaboration":`Режим совместной работы`,"codexSet.layer.environment":`Контекст окружения`,"codexSet.layer.apps":`Приложения`,"codexSet.layer.skills":`Навыки`,"codexSet.prompt.extensionsUnknown":`Расширения могут добавлять собственные слои. Codex не раскрывает их, поэтому показать их здесь нельзя.`,"codexSet.group.transition":`Уведомления о переходе`,"codexSet.group.transitionDesc":`Они сообщают об изменении, а не описывают состояние, поэтому появляются только при переходе сессии в реальное время или смене модели.`,"codexSet.custom.slotNote":`Пользовательские слои объединяются в один раздел в этом порядке.`,"codexSet.row.alwaysOn":`Всегда включён`,"codexSet.row.onChange":`При изменении`,"codexSet.row.featureGated":`Настраивается в [features]`,"codexSet.row.openFeatures":`Открыть настройки`,"codexSet.dialog.setValue":`{value} (по умолчанию {fallback})`,"codexSet.dialog.copyKey":`Скопировать ключ`,"codexSet.dialog.unknownLayer":`В этой сборке нет описания этого слоя. Он получен из более новой среды выполнения Codex, чем панель.`,"codexSet.custom.heading":`Пользовательские слои`,"codexSet.custom.add":`+ Добавить слой`,"codexSet.custom.newTitle":`Новый слой`,"codexSet.custom.editTitle":`Изменить слой`,"codexSet.custom.titleLabel":`Название`,"codexSet.custom.bodyLabel":`Инструкции`,"codexSet.custom.bodySize":`{bytes} из {max} байт`,"codexSet.custom.normalized":`Табуляции заменены четырьмя пробелами, а окончания строк — на LF.`,"codexSet.custom.titleRequired":`Введите название.`,"codexSet.custom.titleTooLong":`В названии {count} символов; предел — {max}.`,"codexSet.custom.titleMultiline":`Название должно занимать одну строку.`,"codexSet.custom.bodyTooLarge":`Размер этого слоя — {bytes} байт; предел — {max}.`,"codexSet.custom.composedTooLarge":`Общий размер включённых слоёв составит {bytes} байт и превысит предел.`,"codexSet.custom.invalidCharacter":`Управляющий символ в позиции {position} невозможно сохранить.`,"codexSet.custom.discardPrompt":`Отменить изменения?`,"codexSet.custom.keepEditing":`Продолжить редактирование`,"codexSet.custom.delete":`Удалить {title}`,"codexSet.custom.deleteConfirm":`Удалить этот слой? Это действие нельзя отменить.`,"codexSet.custom.layerGone":`Этот слой был удалён в другом месте, поэтому редактор закрыт.`,"codexSet.custom.deleteConfirmNamed":`Удалить “{title}”? Это действие нельзя отменить.`,"codexSet.custom.moveUp":`Переместить {title} вверх`,"codexSet.custom.prevLayer":`Предыдущий слой`,"codexSet.custom.nextLayer":`Следующий слой`,"codexSet.custom.navPosition":`{position} / {total}`,"codexSet.custom.moveDown":`Переместить {title} вниз`,"codexSet.custom.limitReached":`Можно сохранить не более {max} пользовательских слоёв.`,"codexSet.custom.notOwned":`developer_instructions записан вне opencodex, поэтому его нельзя изменить здесь. Импортируйте его, чтобы управлять им как слоем.`,"codexSet.custom.adopt":`Импортировать существующие инструкции`,"codexSet.custom.adoptConfirm":`Импортировать как слой`,"codexSet.custom.adoptRefused":`Не удалось импортировать существующее значение.`,"codexSet.custom.baseReplaced":`Для model_instructions_file задан путь {path}, поэтому базовый промпт заменён за пределами opencodex.`,"codexSet.lint.identity":`Здесь заявлена личность, отличная от той, которую задаёт Codex.`,"codexSet.lint.foreignTool":`Инструменты предоставляет реестр; упоминание инструмента здесь не создаёт его.`,"codexSet.lint.placeholder":`Инструкции не обрабатываются шаблонизатором, поэтому этот текст будет отправлен буквально.`,"codexSet.lint.applyPatch":`apply_patch определяется реестром инструментов, а не инструкциями.`,"codexSet.lint.approvalVocab":`Codex добавляет собственную терминологию подтверждений; это может ей противоречить.`,"codexSet.lint.environment":`Данные среды создаются позже и могут этому противоречить.`,"codexSet.lint.size":`Размер этого слоя превышает 8 KB. Его можно сохранить, но он расходует токены при каждом запросе.`,"codexSet.preset.blank":`Пустой слой`,"codexSet.preset.concise.name":`Краткий ответ`,"codexSet.preset.concise.description":`Короткие ответы без вступлений и лишнего форматирования.`,"codexSet.preset.concise.provenance":`Адаптировано на основе указаний Claude Code о краткости. Формулировки наши, это не копия.`,"codexSet.preset.planFirst.name":`План перед правками`,"codexSet.preset.planFirst.description":`Сначала изложить план, затем внести изменения.`,"codexSet.preset.planFirst.provenance":`Адаптировано на основе подхода Claude Code к планированию. Формулировки наши, это не копия.`,"codexSet.preset.explainWhy.name":`Объяснять причины`,"codexSet.preset.explainWhy.description":`Объяснять не только что, но и почему.`,"codexSet.preset.explainWhy.provenance":`Адаптировано на основе стиля подтверждений Grok Build. Формулировки наши, это не копия.`,"codexSet.preset.testFirst.name":`Сначала тест`,"codexSet.preset.testFirst.description":`Перед исправлением написать тест, который завершается с ошибкой.`,"codexSet.preset.testFirst.provenance":`Адаптировано на основе распространённой практики работы агентов. Формулировки наши, это не копия.`,"codexSet.preset.korean.name":`Ответы на корейском`,"codexSet.preset.korean.description":`Отвечать на корейском независимо от языка запроса.`,"codexSet.preset.korean.provenance":`Написано для opencodex на основе частого пользовательского запроса. Формулировки наши, это не копия.`,"codexSet.dialog.class":`Тип`,"codexSet.dialog.key":`Ключ конфигурации`,"codexSet.dialog.fileValue":`Значение в этом файле`,"codexSet.dialog.absentDefault":`не задано (по умолчанию: {value})`,"codexSet.dialog.noRenderedText":`Codex не раскрывает собранный текст встроенного слоя, поэтому здесь приведены описание слоя и его ключ, а не содержимое.`,"codexSet.dialog.sourceText":`Текст, отправляемый модели`,"codexSet.dialog.sourceBytes":`{bytes} байт`,"codexSet.dialog.notRendered":`На прочитанном нами шаге этот слой ничего не отправил. Разделы пересылаются только при изменении, поэтому в одной выборке слой может отсутствовать.`,"codexSet.dialog.emptySource":`Файл {path} существует, но пуст, поэтому этот слой ничего не отправляет.`,"codexSet.dialog.notExposed":`Базовый промпт передаётся вне списка сообщений, который Codex может напечатать, поэтому показать его здесь нельзя. Заменить его можно через model_instructions_file.`,"codexSet.dialog.textUnavailable":`На этой машине не удалось прочитать промпт Codex, поэтому текст недоступен.`,"codexSet.class.base":`Базовые инструкции`,"codexSet.class.config-toggle":`Переключается здесь`,"codexSet.class.feature-gated":`Управляется флагом функции`,"codexSet.class.runtime-conditional":`Зависит от среды выполнения`,"codexSet.class.extension-unknown":`Слой расширения`,"codexSet.layer.base-instructions":`Базовые инструкции`,"codexSet.layer.model-switch":`Уведомление о смене модели`,"codexSet.layer.personality":`Стиль общения`,"codexSet.layer.context-window-guidance":`Рекомендации по контекстному окну`,"codexSet.layer.realtime":`Реальное время`,"codexSet.layer.agents-md":`AGENTS.md`,"codexSet.layer.environments-instructions":`Среды выполнения`,"codexSet.layer.plugins":`Плагины`,"codexSet.layer.tools":`Инструменты`,"codexSet.layer.multi-agent-mode":`Мультиагентный режим`,"codexSet.layer.git-attribution":`Атрибуция коммитов`,"codexSet.about.base-instructions":`Собственные инструкции Codex. Они передаются вместе с запросом, и отключить их нельзя.`,"codexSet.about.model-switch":`Добавляется при смене модели во время диалога.`,"codexSet.about.personality":`Указания по тону и стилю, управляемые флагом функции.`,"codexSet.about.context-window-guidance":`Рекомендации по оставшемуся бюджету контекста, управляемые флагом функции.`,"codexSet.about.realtime":`Добавляется для сеансов реального времени.`,"codexSet.about.agents-md":`Файлы AGENTS.md вашего проекта. Эта страница лишь показывает слой и никогда не изменяет документацию проекта.`,"codexSet.about.permissions":`Описывает действующие настройки песочницы и подтверждений.`,"codexSet.about.collaboration":`Описывает активный режим совместной работы.`,"codexSet.about.environment":`Рабочий каталог, платформа и другие сведения об окружении.`,"codexSet.about.environments-instructions":`Указания для сред отложенного выполнения, управляемые флагом функции.`,"codexSet.about.apps":`Правила использования подключённых приложений.`,"codexSet.about.plugins":`Добавляется, если выбран плагин или какой-либо плагин объявляет возможность.`,"codexSet.about.tools":`Описания отложенных инструментов, управляемые флагом функции.`,"codexSet.about.skills":`Список доступных навыков.`,"codexSet.about.multi-agent-mode":`Инструкции для подагентов, управляемые флагом функции.`,"codexSet.about.git-attribution":`Просит модель добавлять трейлер Co-authored-by: Codex в коммиты, которые она пишет, и строку Generated with Codex. в пул-реквесты, которые она открывает. Codex берёт это из вашей учётной записи, поэтому настройки нет ни здесь, ни в [features]. Если в учётной записи атрибуция отключена, Codex отправляет обратную инструкцию, а не молчит.`,"codexSet.condition.model-switch":`Добавляется только после смены модели во время сеанса.`,"codexSet.condition.realtime":`Добавляется только в сеансе реального времени.`,"codexSet.condition.agents-md":`Добавляется, если для рабочего каталога найден документ проекта.`,"codexSet.condition.plugins":`Добавляется, если выбран плагин или какой-либо плагин объявляет возможность.`,"codexSet.condition.git-attribution":`Определяется политикой атрибуции вашей учётной записи.`,"codexSet.base.title":`Базовый промпт`,"codexSet.base.prev":`Предыдущий вариант`,"codexSet.base.next":`Следующий вариант`,"codexSet.base.position":`{position} / {total}`,"codexSet.base.swipeHint":`Проведите в сторону, используйте клавиши-стрелки или кнопки со стрелками. Применяется к новым сессиям.`,"codexSet.base.defaultTitle":`Собственный базовый промпт Codex`,"codexSet.base.defaultBody":`Вариант по умолчанию здесь не хранится, поэтому его нечего править или удалять: выбор просто убирает model_instructions_file из конфигурации, и Codex берёт свой штатный промпт.`,"codexSet.base.variantTitle":`Название`,"codexSet.base.variantBody":`Промпт`,"codexSet.base.replacesWarning":`Это ЗАМЕНЯЕТ собственный базовый промпт Codex, а не дополняет его. Короткий текст здесь означает короткие инструкции для модели.`,"codexSet.base.use":`Использовать этот`,"codexSet.base.inUse":`Используется`,"codexSet.base.externalBlocked":`model_instructions_file уже указывает на {path}, и это значение записал не opencodex. Уберите его сами, прежде чем выбирать здесь.`,"nav.api":`API`,"nav.integrations":`Интеграции`,"nav.openMenu":`Открыть меню`,"nav.closeMenu":`Закрыть меню`,"integrations.subtitle":`Подключайте клиенты к opencodex, управляйте учётными данными и восстанавливайте конфигурацию клиентов.`,"integrations.tabsLabel":`Разделы интеграций`,"integrations.tab.overview":`Обзор`,"integrations.tab.keys":`Ключи API`,"integrations.tab.codex":`Codex`,"integrations.tab.claude":`Claude`,"integrations.tab.grok":`Grok Build`,"integrations.tab.opencode":`OpenCode`,"integrations.tab.pi":`Pi`,"integrations.tab.omp":`OMP`,"integrations.tab.hermes":`Hermes`,"integrations.tab.openclaw":`OpenClaw`,"integrations.tab.kimi":`Kimi Code`,"integrations.tab.gajae":`Gajae Code`,"integrations.tab.dsh":`DSH`,"integrations.tab.mcode":`MiniMax Code`,"integrations.tab.zcode":`ZCode`,"integrations.tab.prime":`Prime Agent`,"integrations.tab.aside":`Aside`,"integrations.codex.title":`Codex CLI`,"integrations.codex.body":`Подключением Codex управляет прокси-сервис. При запуске opencodex оно применяется, а при остановке сервиса восстанавливается нативная маршрутизация.`,"integrations.codex.openService":`Открыть управление сервисом`,"integrations.state.notInstalled":`Не установлен`,"integrations.state.unknown":`Проверка…`,"integrations.detail.codexRouted":`Запросы Codex идут через этот прокси`,"integrations.detail.codexAbsent":`Codex пока не идёт через этот прокси`,"integrations.detail.keyCount":`Выпущено ключей: {count}`,"integrations.detail.keyNone":`Ключи не выпущены`,"integrations.detail.keyChecking":`Проверка…`,"integrations.detail.keyUnavailable":`Статус ключей недоступен`,"integrations.detail.claudeOff":`Подключение выключено`,"integrations.detail.desktopCurrent":`Desktop работает с этим профилем`,"integrations.detail.desktopStale":`Файл профиля изменился после применения`,"integrations.detail.desktopNotServed":`Профиль есть, но Desktop использует другой`,"integrations.detail.desktopAbsent":`Профиль не применён`,"integrations.detail.desktopDesiredOff":`Интеграция Claude Desktop отключена`,"integrations.detail.desktopDesiredOffCleanupPending":`Claude Desktop всё ещё использует шлюз; очистка не завершена`,"integrations.detail.desktopDesiredOnNotApplied":`Интеграция включена, но Desktop не использует профиль шлюза`,"integrations.detail.desktopSelectedElsewhere":`Desktop использует другой профиль`,"integrations.detail.desktopProfileDrift":`Выбранный профиль Desktop был изменён`,"integrations.detail.desktopObservedUnsafe":`Выбранный профиль Desktop нельзя безопасно изменить`,"integrations.detail.desktopNotInstalled":`Библиотека конфигурации Claude Desktop не установлена`,"integrations.dialog.desktop.title":`Отключить интеграцию Claude Desktop?`,"integrations.dialog.desktop.changes":`Если {path} содержит профиль шлюза, управляемый opencodex, Desktop сначала выберет новый стандартный профиль без учётных данных, а затем удалит старый профиль и резервную копию.`,"integrations.dialog.desktop.breakage":`Claude Desktop вернётся к обычному Claude вместо моделей, маршрутизируемых через opencodex.`,"integrations.dialog.desktop.undo":`При повторном включении профиль opencodex будет создан заново из сохранённых назначений моделей.`,"integrations.dialog.desktop.restart":`Claude Desktop читает эту конфигурацию только при запуске. Полностью закройте и снова откройте Desktop, чтобы изменение вступило в силу.`,"integrations.dialog.desktop.confirm":`Отключить`,"integrations.native.error.desktopUnsafeMetadata":`Не удалось безопасно прочитать метаданные Claude Desktop в {path}, поэтому библиотека не изменялась.`,"integrations.native.error.desktopCleanupIncomplete":`Claude Desktop указывает на стандартный режим, но старые файлы учётных данных opencodex остались в: {paths}.`,"integrations.native.msg.desktopDisabled":`Интеграция Claude Desktop отключена.`,"integrations.native.msg.desktopEnabled":`Интеграция Claude Desktop включена.`,"integrations.detail.grokModels":`Подключено моделей: {count}`,"integrations.detail.grokAbsent":`В конфигурации нет блока opencodex`,"integrations.dialog.grok.title":`Отключить интеграцию Grok Build?`,"integrations.dialog.grok.changes":`Из {path} будет удалён только блок, отмеченный opencodex. Содержимое, добавленное вручную вне блока, останется без изменений.`,"integrations.dialog.grok.breakage":`После отключения псевдонимы моделей opencodex исчезнут из Grok Build. Модели, использовавшиеся с учётной записью xAI, останутся доступны.`,"integrations.dialog.grok.undo":`Если opencodex запущен на loopback-адресе, при повторном включении будет записан новый блок из списка доступных на тот момент моделей.`,"integrations.dialog.grok.confirm":`Отключить`,"integrations.native.msg.nonLoopbackRemoved":`Grok Build можно регистрировать автоматически, только когда opencodex запущен на loopback-адресе. Предыдущий блок, указывавший на loopback-адрес, удалён.`,"integrations.native.msg.nonLoopbackRemovedNoop":`Grok Build можно регистрировать автоматически, только когда opencodex запущен на loopback-адресе. Предыдущего блока для удаления не было.`,"integrations.native.msg.nonLoopbackSuperseded":`Grok Build можно регистрировать автоматически, только когда opencodex запущен на loopback-адресе. Тем временем другая программа записала в конфигурацию новый блок, поэтому текущий блок в файле создан не этим запросом.`,"integrations.native.error.orphanedMarker":`В {path} есть начальная метка opencodex, но нет конечной. Файл не изменён, потому что невозможно надёжно определить конец блока.`,"integrations.native.error.homeMismatch":`Домашний каталог установленного сервиса не совпадает с текущим, поэтому файл не изменён.`,"integrations.native.error.notInstalled":`Grok Build не установлен, поэтому изменять нечего.`,"integrations.native.error.configBusy":`Конфигурация сохраняется в другом месте, поэтому изменить её не удалось. Повторите попытку чуть позже.`,"integrations.state.absent":`Не применено`,"integrations.state.current":`Применено`,"integrations.state.stale":`Требуется обновление`,"integrations.state.conflict":`Конфликт`,"integrations.state.unsafe":`Невозможно проверить`,"integrations.summary.detected":`Клиентов найдено`,"integrations.summary.applied":`Настроено клиентов`,"integrations.summary.stale":`Требуется обновление`,"integrations.summary.lastChange":`Последнее изменение`,"integrations.summary.disableAll":`Отключить все…`,"integrations.onboarding":`При применении сначала сохраняется резервная копия, а затем записывается один блок провайдера opencodex. При отключении удаляется только этот блок, а сохранённый снимок можно восстановить.`,"integrations.empty.title":`Установленные клиенты не обнаружены`,"integrations.empty.body":`Установите поддерживаемый клиент, затем вернитесь сюда и примените opencodex.`,"integrations.action.apply":`Применить`,"integrations.action.disable":`Отключить`,"integrations.action.refresh":`Обновить`,"integrations.action.settings":`Настройки`,"integrations.action.manageKeys":`Управлять ключами`,"integrations.action.restore":`Восстановить…`,"integrations.action.undo":`Отменить`,"integrations.action.restorePoint":`Восстановить эту точку…`,"integrations.action.snapshotExpired":`Резервная копия устарела`,"integrations.rollback.title":`Центр восстановления`,"integrations.rollback.empty":`Истории применений пока нет`,"integrations.rollback.emptyBody":`Перед каждой успешной записью сначала сохраняется снимок исходного состояния.`,"integrations.catalog.title":`Клиенты`,"integrations.rollback.older":`Более ранние операции`,"integrations.rollback.showMore":`Показать ещё {n}`,"integrations.rollback.failed":`Не удалось загрузить историю откатов.`,"integrations.restore.title":`Восстановить этот снимок?`,"integrations.restore.body":`Сначала будет создана резервная копия текущего файла, затем выбранный снимок заменит его.`,"integrations.restore.driftTitle":`Обнаружены более новые изменения`,"integrations.restore.driftBody":`Изменения, сделанные после этого снимка, будут сохранены в резервной копии, а затем файл будет заменён.`,"integrations.restore.confirm":`Восстановить`,"integrations.restore.confirmDrift":`Сохранить новые изменения и восстановить`,"integrations.restore.pending":`Восстановление…`,"integrations.restore.manual":`Автоматическое восстановление не удалось: {reason}. Восстановите вручную из {path}.`,"integrations.error.load":`Не удалось загрузить состояние интеграции.`,"integrations.error.stale":`Последнее обновление не удалось. Значения ниже могут быть устаревшими.`,"integrations.error.busy":`Другое изменение для этого клиента ещё выполняется. Повторите попытку чуть позже.`,"integrations.error.conflict":`После записи opencodex конфигурация изменилась. Ничего не было удалено.`,"integrations.error.unsafe":`Конфигурацию нельзя изменить безопасно.`,"integrations.error.generic":`Изменить интеграцию не удалось. Предыдущее состояние сохранено.`,"integrations.error.nonLoopback":`{client} может обращаться только к прокси на localhost: в его конфигурации негде разместить заголовок, который требуется при удалённой привязке, поэтому ручная настройка тоже не поможет. Обеспечьте доступ через loopback — туннелем или локальным форвардером.`,"integrations.status.installed":`Установлен`,"integrations.status.notInstalled":`Не установлен`,"integrations.status.appliedAt":`Применено`,"integrations.status.backup":`Резервная копия`,"integrations.status.lastRestore":`Последнее восстановление`,"integrations.status.unknown":`Неизвестно`,"integrations.bulk.title":`Отключить применённые интеграции клиентов?`,"integrations.bulk.body":`Будет удалён только блок, принадлежащий opencodex. Для каждого клиента предварительно сохраняется снимок исходного состояния.`,"integrations.bulk.partial":`Не удалось отключить некоторые клиенты: {clients}`,"integrations.bulk.success":`Применённые интеграции клиентов отключены.`,"integrations.retention.degraded":`Очистка резервных копий отстаёт; старые копии могут всё ещё находиться на диске.`,"integrations.error.residual":`Файл может остаться в промежуточном состоянии: {message} Восстановите его из {path}.`,"integrations.error.recover":`{message} Резервная копия находится в {path}.`,"integrations.kind.apply":`Применено`,"integrations.kind.disable":`Отключено`,"integrations.kind.refresh":`Обновлено`,"integrations.kind.restore":`Восстановлено`,"integrations.kind.overwrite":`Перезаписано`,"integrations.dialog.overwrite.title":`Заменить блок в этом файле настроек?`,"integrations.dialog.overwrite.changesUnowned":`В {path} место, нужное opencodex, занято блоком, который писали не мы. Применение заменит его блоком, который пишет opencodex.`,"integrations.dialog.overwrite.changesForeign":`Ваша правка внутри блока opencodex в {path} будет отброшена и заменена блоком, который пишет opencodex.`,"integrations.dialog.overwrite.breakage":`То, что настраивал прежний блок, перестанет действовать. Остальная часть файла не меняется.`,"integrations.dialog.overwrite.undo":`Снимок сохраняется заранее, поэтому операция попадёт в список откатов ниже и её можно отменить.`,"integrations.dialog.overwrite.confirm":`Заменить`,"integrations.action.overwrite":`Заменить`,"integrations.semantics.opencode":`Действует только при прямом запуске с диска; внедрение окружения через ocx opencode имеет приоритет.`,"integrations.semantics.pi":`Применяется к новым сеансам.`,"integrations.semantics.omp":`Перезапустите OMP, чтобы загрузить каталог.`,"integrations.semantics.hermes":`Применяется к новым сеансам.`,"integrations.semantics.openclaw":`Немедленно применяется к работающему шлюзу.`,"integrations.semantics.kimi":`Чтобы применить, перезапустите клиент или выполните /reload (v2 отслеживает файл).`,"integrations.semantics.gajae":`Применяется в новом сеансе или при открытии /model.`,"integrations.semantics.dsh":`OpenCodex управляет только llm-pi-ai.providers.opencodex в $DSH_HOME/settings.yaml. DSH применяет этот провайдер горячей перезагрузкой; модель по умолчанию и deepseek-official остаются без изменений. Сейчас поддерживается только loopback; реальные учётные данные не записываются.`,"integrations.semantics.mcode":`Управляет только custom_provider.opencodex. Модель по умолчанию и вход MiniMax не меняются.`,"integrations.semantics.zcode":`Управляет только provider.opencodex в ~/.zcode/v2/config.json. Вход Z.ai и другие провайдеры не меняются. Перезапустите ZCode после изменений.`,"integrations.semantics.prime":`Управляет только providers.opencodex в models.json Prime Agent — ~/.prime/agent, если PRIME_AGENT_CODING_AGENT_DIR не переопределяет путь. Другие провайдеры и переопределения моделей не меняются. Применяется к новым сессиям.`,"integrations.semantics.aside":`Управляет только providers.opencodex в models.json Aside для выполнившего вход аккаунта (~/.aside/u/<аккаунт>). Другие провайдеры не меняются. Aside перезаписывает этот файл во время работы, поэтому после применения полностью закройте и снова откройте его.`,"codexAuth.mainAccount":`Основной аккаунт`,"codexAuth.logLabel":`Метка журнала`,"codexAuth.codexApp":`Codex App`,"codexAuth.moreActions":`Показать дополнительные действия`,"codexAuth.copyId":`Скопировать ID аккаунта`,"codexAuth.appLogin":`Вход через приложение`,"codexAuth.accountPool":`Пул аккаунтов`,"codexAuth.accountModeTitle":`Режим аккаунта OpenAI`,"codexAuth.accountModePool":`Режим пула`,"codexAuth.accountModePoolDesc":`Основной вход и подходящие добавленные аккаунты работают здесь в ротации.`,"codexAuth.accountModeDirect":`Прямой режим`,"codexAuth.accountModeDirectDesc":`Запросы используют только основной вход; добавленные аккаунты сохраняются для режима пула.`,"codexAuth.openaiMissing":`Встроенный провайдер OpenAI не настроен.`,"codexAuth.openaiDisabled":`Встроенный провайдер OpenAI отключён.`,"codexAuth.openaiUnavailableDesc":`Ваши аккаунты OpenAI по-прежнему доступны. Включите провайдера для маршрутизации запросов Codex.`,"codexAuth.enableOpenai":`Включить OpenAI`,"codexAuth.enablingOpenai":`Включение...`,"codexAuth.enableOpenaiFailed":`Не удалось включить провайдер OpenAI.`,"codexAuth.openaiPresetLoadFailed":`Не удалось загрузить пресет провайдера OpenAI.`,"codexAuth.openaiPresetUnavailable":`Пресет провайдера OpenAI недоступен.`,"codexAuth.openProviders":`Открыть провайдеров`,"codexAuth.add":`Добавить`,"codexAuth.sparkQuota":`Квота Codex Spark`,"codexAuth.sparkQuotaHint":`Показывать недельное окно GPT-5.3-Codex-Spark на карточках аккаунтов. По умолчанию скрыто: оно относится лишь к одной модели.`,"codexAuth.sparkQuotaShown":`Квота Codex Spark показана`,"codexAuth.sparkQuotaHidden":`Квота Codex Spark скрыта`,"codexAuth.sparkQuotaFailed":`Не удалось изменить настройку квоты Codex Spark`,"codexAuth.refreshQuota":`Обновить квоты`,"codexAuth.refreshingQuota":`Обновление...`,"codexAuth.quotaRefreshed":`Квоты обновлены`,"codexAuth.quotaRefreshFailed":`Не удалось обновить квоты`,"codexAuth.pauseExhausted":`Приостановить исчерпанные`,"codexAuth.pausingExhausted":`Проверка квот...`,"codexAuth.pauseExhaustedSucceeded":`Приостановлено аккаунтов на лимите: {count}`,"codexAuth.pauseExhaustedNone":`Нет аккаунтов с подтверждённым использованием 100%.`,"codexAuth.pauseExhaustedFailed":`Не удалось проверить и приостановить исчерпанные аккаунты.`,"codexAuth.noPool":`В пул ещё не добавлено ни одного аккаунта.`,"codexAuth.pause":`Приостановить`,"codexAuth.resume":`Возобновить`,"codexAuth.paused":`ПРИОСТАНОВЛЕН`,"codexAuth.pauseSucceeded":`Аккаунт {email} приостановлен`,"codexAuth.resumeSucceeded":`Аккаунт {email} снова доступен в пуле`,"codexAuth.pauseFailed":`Не удалось приостановить {email}. Изменений нет.`,"codexAuth.resumeFailed":`Не удалось возобновить {email}. Изменений нет.`,"codexAuth.pausedHint":`До возобновления исключён из автоматического переключения, повторов, восстановления после задержки и ручного выбора.`,"codexAuth.pinned":`ЗАКРЕПЛЁН`,"codexAuth.pinnedHint":`Этот аккаунт выбран вручную, поэтому более высокий порядок выбора не обойдёт его. Закрепление действует, пока этот аккаунт не будет исчерпан, пока вы не выберете другой или пока вы не измените порядок выбора любого аккаунта.`,"codexAuth.fiveHour":`5 ч`,"codexAuth.weekly":`Неделя`,"codexAuth.monthly":`30 дн.`,"codexAuth.resets":`сброс`,"codexAuth.today":`сегодня`,"codexAuth.current":`ТЕКУЩИЙ`,"codexAuth.nextSession":`ВЫБРАН`,"codexAuth.poolPrepared":`ГОТОВ ДЛЯ ПУЛА`,"codexAuth.preparePoolTitle":`Подготовить этот аккаунт для режима пула?`,"codexAuth.preparePoolDesc":`Прямые запросы продолжат использовать основной вход. Когда режим пула будет включён, этот аккаунт станет подготовленным выбором пула.`,"codexAuth.prepareForPool":`Подготовить для пула`,"codexAuth.poolPreparedToast":`{email} подготовлен для режима пула`,"codexAuth.switchTitle":`Сменить активный аккаунт?`,"codexAuth.switchDesc":`Применяется сразу. Существующие привязанные к аккаунту потоки и уже выполняющиеся запросы сохраняют прежний аккаунт; новые или непривязанные запросы используют порядковый уровень выбранного аккаунта. Аккаунты с тем же порядком выбора продолжают чередоваться.`,"codexAuth.cacheWarning":`Кэш промптов сбрасывается при смене аккаунта. Новая сессия начнётся с пустым кэшем.`,"codexAuth.setAsNext":`Использовать этот аккаунт следующим`,"codexAuth.cancel":`Отмена`,"codexAuth.switchBack":`Вернуться на основной аккаунт?`,"codexAuth.switchBackDesc":`Применяется сразу. Существующие привязанные к аккаунту потоки и уже выполняющиеся запросы сохраняют прежний аккаунт; новые или непривязанные запросы используют порядковый уровень аккаунта входа через приложение. Аккаунты с тем же порядком выбора продолжают чередоваться.`,"codexAuth.autoSwitch":`Проактивное переключение по использованию`,"codexAuth.autoSwitchQuotaDesc":`Квота: при использовании {threshold}% или выше следующий запрос может перейти на подходящий аккаунт с меньшим использованием, включая уже привязанную задачу; Go/Free используют только 30 дней.`,"codexAuth.autoSwitchQuotaOffDesc":`Проактивное переключение по использованию выключено. Назначение новых/непривязанных задач и восстановление после сбоев остаются активными.`,"codexAuth.autoSwitchRoundRobinDesc":`Round-robin не использует этот порог и продолжает ротировать новые/непривязанные задачи.`,"codexAuth.autoSwitchFillFirstDesc":`Fill-first: {threshold}% — порог исчерпания для новых/непривязанных задач; здоровые привязанные задачи сохраняют аккаунт.`,"codexAuth.autoSwitchFillFirstOffDesc":`У fill-first нет порога использования для новых/непривязанных задач; cooldown, повторная аутентификация и восстановление после сбоев всё ещё могут менять маршрутизацию.`,"codexAuth.failureRecoveryNote":`Восстановление после сбоев выполняется отдельно: отказ 429/402 до вывода, cooldown, повторная аутентификация, исключение или настроенный failover могут выбрать другой подходящий аккаунт.`,"codexAuth.autoSwitchThreshold":`Порог использования`,"codexAuth.autoSwitchThresholdAria":`Порог использования в процентах`,"codexAuth.autoSwitchThresholdInc":`Увеличить порог использования`,"codexAuth.autoSwitchThresholdDec":`Уменьшить порог использования`,"codexAuth.autoSwitchLoadFailed":`Не удалось загрузить настройку переключения по использованию.`,"codexAuth.autoSwitchThresholdInvalid":`Введите целое число от 1 до 100`,"codexAuth.autoSwitchUpdated":`Проактивное переключение по использованию обновлено`,"codexAuth.autoSwitchUpdateFailed":`Не удалось подтвердить обновление переключения по использованию. Показано последнее подтверждённое значение.`,"codexAuth.requestUserInput":`Запрашивать ввод в режиме Default`,"codexAuth.requestUserInputDesc":`Позволяет Codex ставить сеанс Default на паузу и задавать вопросы через инструмент request_user_input.`,"codexAuth.requestUserInputUpdated":`Флаг обновлён - применяется к новым сеансам.`,"codexAuth.requestUserInputUpdatedRestart":`Флаг обновлён - применяется к новым сеансам. Перезапустите приложение Codex.`,"codexAuth.requestUserInputUpdateFailed":`Не удалось обновить флаг. Ничего не изменено.`,"codexAuth.requestUserInputLoadFailed":`Не удалось прочитать флаг из config.toml.`,"codexAuth.accountPickerTitle":`Выбирать конкретный аккаунт Codex в списке моделей`,"codexAuth.accountPickerOffDesc":`После включения обычные пункты GPT в списке моделей заменяются отдельным пунктом для каждого селектора аккаунта, поэтому можно явно выбрать аккаунт для разговора без выхода из системы. Отключение не удаляет аккаунты.`,"codexAuth.accountPickerOnDesc":`Каждый селектор — публичная метка одного сохранённого аккаунта. Выбор закрепляет разговор за этим аккаунтом: без ротации и перехода на другой аккаунт, а активный аккаунт Pool не меняется.`,"codexAuth.accountPickerCompatibility":`Для встроенного входа Codex App используется отдельный селектор; в созданных map он обычно называется main, а при коллизии получает безопасный суффикс вроде main-2. Добавленные аккаунты получают стабильные метки, не раскрывающие личные данные, а пользовательские имена селекторов сохраняются. Существующие разговоры и сохранённые варианты моделей продолжают маршрутизироваться. Отключение скрывает созданные пункты, но сохраняет селекторы и точные маршруты. Обычные идентификаторы моделей GPT продолжают работать в режиме Pool или Direct.`,"codexAuth.accountPickerUpdated":`Выбор целевого аккаунта обновлён.`,"codexAuth.accountPickerUpdateFailed":`Не удалось обновить выбор целевого аккаунта. Показана последняя подтверждённая настройка.`,"codexAuth.accountPickerLoadFailed":`Не удалось загрузить настройку выбора аккаунта.`,"codexAuth.accountPickerRefreshFailed":`Не удалось обновить эту настройку. По-прежнему показано последнее подтверждённое значение.`,"codexAuth.advancedSettings":`Дополнительные настройки`,"codexAuth.advancedSettingsAria":`Показать или скрыть дополнительные настройки Codex Auth`,"codexAuth.catalogRefreshPending":`Изменение сохранено, но обновление каталога моделей Codex ещё не завершено. Выполните ocx sync, чтобы повторить попытку.`,"anthropicPool.title":`Пул аккаунтов Claude (экспериментально)`,"anthropicPool.enabledDesc":`При 429 аккаунт охлаждается и выполняется переключение. Новые сессии предпочитают использование ниже {threshold}% ({window}).`,"anthropicPool.enabledNoProactiveDesc":`При 429 аккаунт охлаждается и выполняется переключение. При пороге 0 упреждающее переключение по использованию отключено, но выбор новых сессий и восстановление после 429 по-прежнему используют окно {window}.`,"anthropicPool.disabledDesc":`Используется только активный аккаунт Claude. Включайте только если принимаете экспериментальную маршрутизацию.`,"anthropicPool.experimentalWarning":`Экспериментально и недостаточно проверено. Anthropic может ограничить аккаунты, похожие на автоматическую ротацию. Одна организация может делить квоту — пул таких аккаунтов не поможет. Оставляйте выключенным, если не понимаете риск.`,"anthropicPool.needTwoAccounts":`Перед включением пула добавьте минимум два OAuth-аккаунта Claude.`,"anthropicPool.threshold":`Порог использования для новых сессий`,"anthropicPool.thresholdAria":`Порог использования для новых сессий в процентах`,"anthropicPool.thresholdHelp":`0 отключает выбор по квоте (только аффинити + активный аккаунт). По умолчанию 80.`,"anthropicPool.thresholdInvalid":`Введите целое число от 0 до 100`,"anthropicPool.loadFailed":`Не удалось загрузить настройки пула Claude.`,"anthropicPool.saveFailed":`Не удалось сохранить настройки пула Claude.`,"anthropicPool.on":`Вкл`,"anthropicPool.off":`Выкл`,"accountPool.strategy":`Стратегия ротации`,"accountPool.strategyDesc":`Как OpenCodex назначает аккаунт новой/непривязанной задаче.`,"accountPool.strategyQuota":`Квота`,"accountPool.strategyRoundRobin":`Round-robin`,"accountPool.strategyFillFirst":`Fill-first`,"accountPool.strategyHintQuota":`Quota может перепривязать существующую задачу при следующем запросе после превышения порога использования.`,"accountPool.strategyHintRoundRobin":`Round-robin ротирует только задачи без действующей привязки; порог использования не меняет обычную ротацию.`,"accountPool.strategyHintFillFirst":`Fill-first использует порог как точку исчерпания для непривязанных задач; здоровые привязанные задачи сохраняют affinity.`,"accountPool.unboundDefinition":`Новая/непривязанная задача — запрос без текущей привязки к аккаунту; видимая существующая задача может стать непривязанной после сброса прокси или affinity.`,"accountPool.stickyLimit":`Назначений новых/непривязанных задач до ротации`,"accountPool.stickyLimitAria":`Назначений новых/непривязанных задач до ротации`,"accountPool.stickyLimitInc":`Увеличить sticky-лимит`,"accountPool.stickyLimitDec":`Уменьшить sticky-лимит`,"accountPool.stickyLimitHelp":`Назначить выбранному аккаунту столько новых/непривязанных задач перед переходом дальше; счётчик растёт при привязке задачи, а не после успеха upstream.`,"accountPool.stickyLimitInvalid":`Введите целое число от 1 до 100`,"accountPool.strategyLoadFailed":`Не удалось загрузить стратегию ротации.`,"accountPool.strategyUpdateFailed":`Не удалось сохранить стратегию ротации.`,"accountPool.quotaWindow":`Окно квоты`,"accountPool.quotaWindowDesc":`Какая кешированная полоса используется для выбора новых сессий по квоте, проверки порога Fill-first и допустимых замен после 429.`,"accountPool.quotaWindowFiveHour":`Полоса 5 часов`,"accountPool.quotaWindowWeekly":`Недельная полоса`,"accountPool.quotaWindowMaxUtilization":`Наибольшая полоса`,"accountPool.quotaWindowHint":`Недельная полоса пропускает аккаунты с исчерпанной полосой 5 часов, пока остаётся другой допустимый аккаунт, но возвращается к ним, если других нет. При равенстве выбирается меньшее использование за 5 часов; недельные полосы известны только после опроса на странице провайдеров.`,"accountPool.quotaWindowInert":`Полосу использования оценивает только «Квота» или Fill-first с порогом выше 0, поэтому для текущей стратегии ротации эта настройка ничего не меняет.`,"accountPool.priority":`Порядок выбора`,"accountPool.priorityAria":`Порядок выбора для этого аккаунта`,"accountPool.priorityHint":`Большие числа используются раньше. Пул переходит к меньшему числу только тогда, когда все аккаунты выше исчерпаны или недоступны.`,"accountPool.priorityFirst":`Первым`,"accountPool.priorityEarlier":`Раньше`,"accountPool.priorityNormal":`По умолчанию`,"accountPool.priorityLater":`Позже`,"accountPool.priorityLast":`Последним`,"accountPool.priorityOption":`{name} ({value})`,"accountPool.priorityCustom":`Своё значение`,"accountPool.priorityUpdated":`Порядок выбора для {email} обновлён`,"accountPool.priorityUpdateFailed":`Не удалось сохранить порядок выбора для {email}. Показано последнее подтверждённое значение.`,"codexAuth.switched":`{email} выбран для следующего запроса`,"codexAuth.loadFailed":`Не удалось загрузить настройки аккаунтов Codex.`,"codexAuth.switchFailed":`Не удалось переключить аккаунт. Ваш предыдущий выбор не изменён.`,"codexAuth.removeConfirm":`Удалить {id}?`,"codexAuth.removeFailed":`Не удалось удалить аккаунт. Ничего не изменено.`,"codexAuth.addTitle":`Добавить аккаунт Codex`,"codexAuth.addIdLabel":`ID аккаунта (slug)`,"codexAuth.addIdPlaceholder":`codex-work, codex-alt, team...`,"codexAuth.resetCreditsAria":`Кредитов сброса: {count}`,"codexAuth.addJsonLabel":`Содержимое auth.json`,"codexAuth.addHelp":`Скопируйте из ~/.codex/auth.json на другой машине или используйте codex-auth export.`,"codexAuth.importBtn":`Импортировать`,"codexAuth.importInvalidJson":`Некорректный JSON`,"codexAuth.importMissingTokens":`В JSON отсутствует access_token или refresh_token`,"codexAuth.importMissingId":`Укажите ID аккаунта`,"codexAuth.accountAdded":`Аккаунт добавлен в пул`,"codexAuth.addPickDesc":`Войдите в другой аккаунт ChatGPT, чтобы добавить его в пул.`,"codexAuth.oauthLogin":`Вход через OAuth`,"codexAuth.oauthDesc":`Открывает вход ChatGPT в браузере`,"codexAuth.deviceLogin":`Вход по коду устройства`,"codexAuth.deviceDesc":`Для headless или удалённого прокси: введите короткий код на другом устройстве`,"codexAuth.importAuthJson":`Импорт auth.json`,"codexAuth.importAuthJsonDesc":`Из другой установки Codex или через codex-auth export`,"codexAuth.back":`Назад`,"codexAuth.oauthAlreadyInProgress":`Вход уже выполняется. Завершите его в браузере.`,"codexAuth.oauthWaiting":`Ожидание завершения входа ChatGPT в браузере...`,"codexAuth.oauthSubmittingCode":`Отправка кода…`,"codexAuth.oauthCodeSubmitted":`Код отправлен — ждём завершения входа…`,"codexAuth.oauthStatusRetrying":`При проверке статуса входа возникла сетевая ошибка или ошибка прокси — повторяем…`,"codexAuth.oauthCancelled":`Вход отменён.`,"codexAuth.loginFailed":`Не удалось войти`,"codexAuth.needsReauth":`Повторный вход`,"codexAuth.reauthenticate":`Переавторизоваться`,"codexAuth.tokenExpired":`Токен истёк — переавторизуйте этот аккаунт`,"codexAuth.mainTokenExpired":`Токен истёк — повторите вход через приложение Codex`,"codexAuth.emailCollision":`Этот аккаунт совпадает с вашим основным входом Codex. Используйте другой аккаунт.`,"codexAuth.resetCreditsTitle":`Кредиты сброса`,"codexAuth.resetCreditsAvailable":`У вас доступно кредитов сброса: {count}.`,"codexAuth.resetCreditsDesc":`Каждый кредит мгновенно сбрасывает ваши текущие часовые и недельные лимиты использования.`,"codexAuth.noResetCredits":`У вас нет кредитов сброса.`,"codexAuth.earnCreditsHint":`Кредиты начисляются ежемесячно и по реферальной программе.`,"codexAuth.creditsExpireNote":`Кредиты истекают через 30 дней после начисления.`,"codexAuth.useOneCredit":`Использовать 1 кредит`,"codexAuth.confirmResetTitle":`Использовать кредит сброса?`,"codexAuth.confirmResetDesc":`Текущие лимиты запросов будут мгновенно сброшены. У вас осталось кредитов: {count}.`,"codexAuth.irreversible":`Это действие нельзя отменить.`,"codexAuth.useCredit":`Использовать кредит`,"codexAuth.redeeming":`Сброс...`,"codexAuth.resetSuccess":`Лимиты запросов сброшены! Осталось кредитов: {remaining}.`,"codexAuth.resetSuccessGeneric":`Лимиты запросов сброшены!`,"codexAuth.resetAlreadyRedeemed":`Этот кредит уже был использован. Количество кредитов не изменилось.`,"codexAuth.resetNothingToReset":`Сейчас ни одно окно лимитов не требует сброса.`,"codexAuth.resetNoCredit":`Нет доступных кредитов сброса.`,"codexAuth.resetError":`Не удалось использовать кредит сброса. Попробуйте ещё раз.`,"codexAuth.fifoNote":`Первым используется самый старый кредит.`,"codexAuth.confirmWhichCredit":`Будет использован кредит от {date}.`,"codexAuth.creditNext":`Следующий к использованию`,"codexAuth.creditLabel":`Кредит №{n}`,"codexAuth.creditNextBadge":`СЛЕД.`,"codexAuth.creditGranted":`Начислен {date}`,"codexAuth.creditExpires":`Истекает {date} (осталось {days} дн.)`,"api.title":`Доступ по API`,"api.subtitle":`Сгенерированные API-ключи дают внешним приложениям доступ к прокси opencodex. Аутентификация — через заголовок {authHeader}; какие заголовки принимает каждый эндпоинт, смотрите в таблице ниже.`,"api.endpointNote":`Используйте базовый URL с OpenAI-совместимыми клиентами. Responses и Chat Completions доступны под /v1.`,"api.baseUrl":`Базовый URL`,"api.responsesEndpoint":`Responses API`,"api.chatCompletionsEndpoint":`Chat Completions API`,"api.messagesEndpoint":`Messages API`,"api.modelsEndpoint":`Models API`,"api.endpointsTitle":`Конечные точки`,"api.authTitle":`Аутентификация`,"api.authBaseUrlNote":`Настройте клиентов с базовым URL, затем выберите нужный протокольный endpoint ниже.`,"api.authLoopback":`Loopback-привязки (127.0.0.1 или ::1) обходят аутентификацию. Для удалённых привязок нужен сгенерированный ocx_-ключ или OPENCODEX_API_AUTH_TOKEN.`,"api.modelsTitle":`Каталог внешних моделей`,"api.modelsCount":`{count} доступно`,"api.modelsSearch":`Поиск моделей`,"api.modelsSubtitle":`Используйте эти точные ID моделей с /v1/models и выбранным входящим протоколом.`,"api.modelsLoading":`Загрузка моделей…`,"api.modelsLoadFailed":`Не удалось загрузить каталог внешних моделей.`,"api.modelsEmpty":`Пока нет внешне доступных моделей.`,"api.modelsNoMatch":`Нет моделей, соответствующих «{query}».`,"api.colModel":`Модель`,"api.colSource":`Источник`,"api.colProtocols":`Протоколы`,"api.copyModelId":`Копировать ID`,"api.modelCopied":`Скопировано`,"api.testModel":`Тест`,"api.testingModel":`Тестирование…`,"api.testSucceeded":`OK`,"api.testFailed":`Ошибка`,"api.protocolResponses":`Responses`,"api.protocolChatCompletions":`Chat Completions`,"api.protocolMessages":`Messages`,"api.sourceNative":`Пул ChatGPT`,"api.sourceCombo":`Combo-маршрут`,"api.sourceCustom":`Пользовательская`,"api.usageResponsesTitle":`Пример Responses`,"api.usageChatTitle":`Пример Chat Completions`,"api.usageMessagesTitle":`Пример Messages`,"api.newKeyTitle":`Создан новый ключ`,"api.newKeyNote":`Скопируйте ключ сейчас — он больше не будет показан.`,"api.copy":`Копировать`,"api.copied":`Скопировано`,"api.dismiss":`Закрыть`,"api.generateTitle":`Сгенерировать ключ`,"api.keyNamePlaceholder":`Имя ключа (необязательно)`,"api.generate":`Сгенерировать`,"api.generating":`Создание…`,"api.activeKeys":`Активные ключи ({count})`,"api.activeKeysLoading":`Активные ключи`,"api.noKeys":`API-ключей пока нет. Сгенерируйте ключ выше.`,"api.workspace.sections":`Разделы API`,"api.section.keys":`Ключи`,"api.section.connect":`Подключение`,"api.section.endpoints":`Эндпоинты`,"api.section.models":`Модели`,"api.section.examples":`Примеры`,"api.workspace.details":`Сведения об API-ключе`,"api.workspace.keyDetails":`Сведения о ключе`,"api.workspace.keyPrefix":`Префикс ключа`,"api.workspace.deleteKey":`Удалить ключ`,"api.workspace.deleteConfirm":`Удалить этот ключ? Это действие нельзя отменить.`,"api.workspace.usageExamples":`Примеры использования`,"api.copyUrlHint":`Нажмите, чтобы скопировать URL`,"api.urlCopied":`URL скопирован`,"api.copyExampleHint":`Нажмите, чтобы скопировать пример`,"api.exampleCopied":`Пример скопирован`,"api.colName":`Имя`,"api.colKey":`Ключ`,"api.colCreated":`Создан`,"api.confirm":`Подтвердить`,"api.deleteAria":`Удалить API-ключ`,"api.usageSampleInput":`Привет, мир!`,"api.clientConfig.title":`Конфигурация клиента`,"api.clientConfig.rowsLabel":`Подключение клиента`,"api.clientConfig.details":`Подробнее`,"api.clientConfig.detailsAria":`Подробности конфигурации {client}`,"api.clientConfig.copyAria":`Скопировать конфигурацию {client}`,"api.clientConfig.downloadAria":`Скачать конфигурацию {client}`,"api.clientConfig.rowMeta":`{destination} · моделей: {count}`,"api.clientConfig.rowError":`Не удалось собрать конфигурацию {client}.`,"api.clientConfig.copiedAnnounceClient":`Конфигурация {client} скопирована в буфер обмена.`,"api.clientConfig.clientOpencode":`OpenCode`,"api.clientConfig.clientPi":`Pi`,"api.clientConfig.clientOmp":`OMP`,"api.clientConfig.clientHermes":`Hermes`,"api.clientConfig.clientOpenclaw":`OpenClaw`,"api.clientConfig.clientKimi":`Kimi Code`,"api.clientConfig.clientGajae":`Gajae Code`,"api.clientConfig.clientDsh":`DeepSeek Harness (DSH)`,"api.clientConfig.clientMcode":`MiniMax Code`,"api.clientConfig.clientZcode":`ZCode`,"api.clientConfig.clientPrime":`Prime Agent`,"api.clientConfig.clientAside":`Aside`,"api.clientConfig.copy":`Копировать конфигурацию`,"api.clientConfig.download":`Скачать`,"api.clientConfig.loading":`Формируется конфигурация клиента…`,"api.clientConfig.jsonLabel":`Конфигурация {client}`,"api.clientConfig.destination":`Целевой файл`,"api.clientConfig.envHint":`Задайте ключ перед запуском`,"api.clientConfig.mergeWarning":`Объедините это с целевым файлом. Замена удалит ваши другие провайдеры и настройки MCP.`,"api.clientConfig.modelCount":`Экспортировано моделей: {count}`,"api.clientConfig.missingLimits":`У {count} из {total} моделей нет лимита контекста; клиент применит свои значения по умолчанию.`,"api.clientConfig.noKeyYet":`Для {env} пока нет ключа. Создайте ключ выше, прежде чем использовать конфигурацию вне loopback.`,"api.clientConfig.loadFailed":`Не удалось прочитать список моделей, поэтому конфигурация клиента не создана.`,"api.clientConfig.copiedAnnounce":`Конфигурация клиента скопирована в буфер обмена.`,"api.clientConfig.copyFailed":`Не удалось скопировать конфигурацию клиента.`,"api.clientConfig.downloadedAnnounce":`Файл {filename} скачан. Пока ничего не изменилось — объедините его с {destination} самостоятельно.`,"api.clientConfig.whereDisclosure":`Куда положить этот файл`,"api.clientConfig.whereBody":`Путь выше — глобальное расположение. Файл конфигурации проекта в рабочем каталоге имеет приоритет, а ключ читается из переменной окружения, указанной в конфигурации, и никогда не хранится в этом файле.`,"api.keysLoadFailed":`Не удалось загрузить API-ключи.`,"api.createFailed":`Не удалось создать API-ключ.`,"api.deleteFailed":`Не удалось удалить API-ключ.`,"api.auth.endpoint":`Эндпоинт`,"api.auth.required":`Обязателен`,"api.auth.accepted":`Принимается`,"api.auth.rejected":`Не принимается`,"api.auth.testProtocol":`Проверить {protocol}`,"api.auth.testNeedsFreshKey":`Чтобы выполнить проверку с аутентификацией, создайте ключ и оставьте его одноразовое значение на экране.`,"api.key.name":`Имя ключа`,"api.key.rename":`Переименовать`,"api.key.saveName":`Сохранить имя`,"api.key.renaming":`Сохранение…`,"api.key.renameFailed":`Не удалось переименовать ключ. Введённое имя сохранено.`,"api.key.deleting":`Удаление…`,"api.rotation.title":`Ротация ключа`,"api.rotation.description":`Выпускает новый ключ, сохраняя текущий на короткий переходный период.`,"api.rotation.start":`Начать ротацию`,"api.rotation.starting":`Запуск…`,"api.rotation.pending":`Ротация ожидает завершения. Обновите и проверьте клиент перед подтверждением.`,"api.rotation.expires":`Переходный период завершится:`,"api.rotation.secretOnce":`Новый ключ показывается один раз. Скопируйте его перед закрытием.`,"api.rotation.commit":`Завершить ротацию`,"api.rotation.abort":`Отменить ротацию`,"api.rotation.failed":`Операция не завершилась. Обновите данные перед повторной попыткой.`,"api.rotation.startFailed":`Не удалось начать ротацию ключа.`,"api.key.copyFailed":`Не удалось скопировать ключ. Выделите и скопируйте его вручную, прежде чем закрыть панель.`,"api.attribution.title":`Использование по ключам`,"api.attribution.requests7d":`Запросы за 7 дней`,"api.attribution.totalRequests":`Всего учтённых запросов`,"api.attribution.totalRequestsAvailable":`Запросы в доступной истории`,"api.attribution.sinceAvailable":`Доступная атрибуция с`,"api.attribution.lastUsed":`Последнее использование`,"api.attribution.since":`Учёт ведётся с`,"api.attribution.neverUsed":`Не использовался с начала учёта`,"api.attribution.unavailable":`Нет данных`,"api.attribution.unavailableDetail":`Использование ещё не учтено. Запросы до начала учёта нельзя отнести к ключам задним числом.`,"api.attribution.ambiguous":`Два ключа используют один и тот же ID, поэтому нельзя определить, чьё это использование. Задайте каждому ключу уникальный ID в файле конфигурации.`,"api.attribution.railAmbiguous":`дубль ID`,"claude.subtitle":`Используйте GPT, Gemini и другие модели внутри Claude Code.`,"claude.pageTitle":`Claude Code`,"claude.workspace.settings":`Настройки`,"claude.enabledLabel":`Подключение Claude`,"claude.enabledHint":`Если выключено, Claude Code не сможет использовать этот прокси.`,"claude.authMode":`Режим аутентификации`,"claude.authModeHint":`«Подписка» требует аккаунт Claude, «Прокси» работает без аккаунта Anthropic`,"claude.authModeSubscription":`Подписка (аккаунт Claude)`,"claude.authModeProxy":`Прокси (аккаунт не нужен)`,"claude.authModeAuto":`Авто (определять вход в Claude)`,"claude.effectiveMode.label":`Применится при следующем запуске`,"claude.effectiveMode.manual":`Вручную: {mode}`,"claude.effectiveMode.autoPresent":`Авто: подписка — вход в Claude найден через {source}`,"claude.effectiveMode.autoAbsent":`Авто: режим прокси — вход в Claude не найден`,"claude.effectiveMode.autoUnknown":`Авто: подписка — не удалось проверить вход`,"claude.effectiveMode.admissionKey":`API-ключ этого прокси всё равно отправляется.`,"claude.authSource.claude-json-oauth":`аккаунт Claude`,"claude.authSource.claude-credentials-file":`файл учётных данных`,"claude.authSource.macos-keychain":`связку ключей macOS`,"claude.authSource.exported-env":`переменную окружения`,"claude.authSource.unknown":`обнаруженные учётные данные`,"claude.systemEnv":`Автоподключение`,"claude.systemEnvDesc":`Если включено, запуск claude в любом терминале автоматически идёт через прокси.`,"claude.systemEnvUnsupported":`Автоподключение доступно только в macOS. В этой системе запускайте Claude с помощью {cmd}.`,"claude.systemEnvWarn":`⚠ Чтобы изменение вступило в силу, необходимо полностью закрыть и заново запустить приложение терминала. Не рекомендуется.`,"claude.fastMode":`Быстрый режим (OpenAI)`,"claude.fastModeDesc":`Управляет service_tier для моделей OpenAI. ВКЛ = priority (быстрее). ВЫКЛ = default. Авто = сквозная передача (решает клиент).`,"claude.fastAuto":`Авто`,"claude.fastOn":`ВКЛ`,"claude.fastOff":`ВЫКЛ`,"claude.autoContext":`Автоматически использовать большой контекст`,"claude.autoContextDesc":`Определяет, как широко применяется пометка 1M. ВКЛ: строку с большим контекстом получает каждая модель, окно которой вмещает порог сжатия. ВЫКЛ: её получают только модели с настоящим контекстом 1M.`,"claude.autoContextInert":`Неактивно, поскольку в файле конфигурации задано устаревшее значение размера контекста (maxContextTokens). Удалите его там, чтобы снова включить эту настройку.`,"claude.autoCompactWindow":`Порог автосуммаризации`,"claude.autoCompactDefault":`{value} (по умолчанию)`,"claude.autoCompactWindowDesc":`Когда чат достигает этого порога, старые сообщения суммаризируются. Порог никогда не превышает собственный лимит модели, поэтому модели с контекстом 200k не затрагиваются.`,"claude.autoCompactWindowWarn":`Изменение этого значения может сломать модели GPT — если задать порог выше реального лимита модели, чаты будут выдавать ошибку ещё до срабатывания суммаризации.`,"claude.injectAgents":`Авторегистрация подагентов`,"claude.injectAgentsDesc":`Регистрирует модели, выбранные на вкладке «Подагенты» (плюс текущую модель по умолчанию), как доступных для вызова агентов Claude Code (ocx-*). Применяется со следующей сессии.`,"claude.webSearchSidecar":`Переопределение сайдкара веб-поиска`,"claude.webSearchSidecarHint":`Переопределяет основной сайдкар веб-поиска для запросов Claude Code.`,"claude.visionSidecar":`Переопределение сайдкара для изображений`,"claude.visionSidecarHint":`Переопределяет основной сайдкар для изображений в запросах Claude Code.`,"claude.useMainSetting":`Использовать основную настройку`,"claude.sidecarModelPlaceholder":`Модель из основной настройки`,"claude.quickstart":`Начало работы`,"claude.quickstartHint":`{cmd} запускает Claude Code через прокси. Ваш вход в claude.ai остаётся активным.`,"claude.manualEnv":`Ручная настройка (для продвинутых)`,"claude.smallFastModel":`Фоновая вспомогательная модель`,"claude.smallFastModelHint":`Модель, которую Claude Code использует для фоновых задач вроде суммаризации чатов и определения тем. Её также использует алиас подагента haiku. Пусто = значение Claude по умолчанию (Haiku).`,"claude.smallFastModelAccurateHint":`Модель, которую Claude Code использует для фоновых задач, например суммаризации чатов и определения тем. Её также использует алиас подагента haiku.`,"claude.smallFastModelUnsetOption":`Разрешить Claude Code выбрать нативную модель`,"claude.smallFastModelNativeWarning":`Если модель не выбрана, OpenCodex не задаёт переопределения вспомогательной модели. Claude Code может использовать нативную модель Sonnet, что может привести к расходам у вашего нативного провайдера.`,"claude.slotUnset":`Модель Claude по умолчанию`,"claude.modelMap":`Перехват моделей`,"claude.modelMapHint":`Перехватывает запросы к определённой модели и перенаправляет их на выбранную вами. По умолчанию список пуст — пока вы не добавите правило, ничего не происходит.`,"claude.mapFrom":`Исходная модель (напр. claude-sonnet-4-5)`,"claude.mapTo":`Заменить на (напр. gemini/gemini-3-pro)`,"claude.addMapping":`Добавить правило`,"claude.removeMapping":`Удалить правило`,"claude.aliases":`Доступные модели`,"claude.aliasesHint":`Модели, которые появляются в меню /model в Claude Code.`,"claude.aliasProviderOther":`Другое`,"claude.loading":`Загрузка…`,"claude.loadFail":`Не удалось загрузить настройки Claude`,"claude.saved":`Сохранено.`,"claude.saveFailed":`Не удалось сохранить`,"claude.networkError":`Ошибка сети — запущен ли прокси?`,"claude.toggleAria":`Переключить подключение Claude`,"claude.none":`Нет`,"claude.tabsLabel":`Клиент Claude`,"claude.tabCode":`Code`,"claude.tabDesktop":`Desktop`,"claudeDesktop.title":`Claude Desktop`,"claudeDesktop.subtitle":`Маршрутизируйте каждое семейство моделей Claude через доступную модель на порту {port}.`,"claudeDesktop.importJson":`Импорт JSON`,"claudeDesktop.exportJson":`Экспорт JSON`,"claudeDesktop.loading":`Загрузка профиля Claude Desktop…`,"claudeDesktop.loadFail":`Не удалось загрузить профиль Claude Desktop.`,"claudeDesktop.retry":`Повторить`,"claudeDesktop.saveFailed":`Не удалось сохранить профиль Claude Desktop.`,"claudeDesktop.applyFailed":`Профиль сохранён, но применить его не удалось.`,"claudeDesktop.updateFailed":`Не удалось обновить Claude Desktop.`,"claudeDesktop.savedApplied":`Профиль сохранён и применён к Claude Desktop.`,"claudeDesktop.appliedMarkerUnsaved":`Применено к Claude Desktop, но отметка о применении не сохранена — состояние ниже может показывать устаревшие данные до повторного применения.`,"claudeDesktop.savedAppliedAnnounce":`Профиль Claude Desktop сохранён и применён.`,"claudeDesktop.saved":`Профиль сохранён.`,"claudeDesktop.savedAnnounce":`Профиль Claude Desktop сохранён.`,"claudeDesktop.exported":`Профиль экспортирован в JSON.`,"claudeDesktop.importExpected":`Ожидается профиль Claude Desktop версии 1.`,"claudeDesktop.importReady":`JSON импортирован. Проверьте черновик, затем сохраните и примените.`,"claudeDesktop.importedAnnounce":`JSON профиля импортирован. Несохранённые изменения готовы к проверке.`,"claudeDesktop.importInvalid":`Выбранный файл не является допустимым профилем.`,"claudeDesktop.importFailed":`Импорт не удался. {error}`,"claudeDesktop.moved":`{route} перемещён в {family}.`,"claudeDesktop.unsaved":`Несохранённые изменения`,"claudeDesktop.upToDate":`Профиль актуален`,"claudeDesktop.saving":`Сохранение…`,"claudeDesktop.applying":`Применение…`,"claudeDesktop.saveApply":`Сохранить и применить`,"claudeDesktop.emptyTitle":`Нет доступных моделей`,"claudeDesktop.emptyHint":`Добавьте или включите провайдера, затем вернитесь для назначения маршрутов Claude Desktop.`,"claudeDesktop.assignmentsLabel":`Назначения семейств моделей Claude`,"claudeDesktop.family.opus":`Opus`,"claudeDesktop.family.fable":`Fable`,"claudeDesktop.family.sonnet":`Sonnet`,"claudeDesktop.family.haiku":`Haiku`,"claudeDesktop.modelCountOne":`{count} модель`,"claudeDesktop.modelCountMany":`{count} моделей`,"claudeDesktop.chooseDefault":`Выберите модель по умолчанию`,"claudeDesktop.temporaryDefault":`Временная модель по умолчанию`,"claudeDesktop.laneEmpty":`Перетащите модель сюда или используйте её элемент «Переместить».`,"claudeDesktop.laneNoMatch":`В этом семействе нет моделей, соответствующих запросу.`,"nav.grok":`Grok`,"grok.title":`Grok Build`,"grok.subtitle":`Модели, зарегистрированные opencodex в вашей конфигурации Grok.`,"grok.loading":`Загрузка состояния Grok…`,"grok.loadFail":`Не удалось прочитать конфигурацию Grok.`,"grok.notConfiguredTitle":`Grok Build не подключён`,"grok.notConfiguredHint":`Установите Grok и перезапустите прокси — opencodex запишет управляемый блок в:`,"grok.endpoint":`Точка входа`,"grok.colModel":`Модель`,"grok.colAlias":`Псевдоним Grok`,"grok.colContext":`Контекст`,"grok.groupNative":`Нативные модели`,"grok.groupRouted":`Маршрутизируемые модели`,"grok.enabledCount":`Зарегистрировано {on} из {total}`,"grok.saved":`Выбор сохранён.`,"grok.savedApplied":`Выбор сохранён и записан в конфиг Grok.`,"grok.saveFailed":`Не удалось сохранить выбор Grok.`,"grok.applyFailed":`Выбор сохранён, но конфиг Grok обновить не удалось.`,"grok.applySkipped":`Выбор сохранён. Конфиг Grok не изменён.`,"grok.saveApply":`Сохранить и применить`,"grok.saving":`Сохранение…`,"grok.applying":`Применение…`,"grok.unsaved":`Несохранённые изменения`,"grok.upToDate":`Выбор актуален`,"grok.toggleModel":`Зарегистрировать {id} в Grok`,"claudeDesktop.available":`Доступно`,"claudeDesktop.defaultBadge":`По умолчанию`,"claudeDesktop.supports1m":`1M`,"claudeDesktop.unavailable":`Недоступно`,"claudeDesktop.contextM":`Контекст {n}M`,"claudeDesktop.contextK":`Контекст {n}k`,"claudeDesktop.contextUnknown":`контекст неизвестен`,"claudeDesktop.alias":`Псевдоним`,"claudeDesktop.useAsDefault":`Сделать по умолчанию для {family}`,"claudeDesktop.moveTo":`Переместить в`,"claudeDesktop.move":`Переместить`,"claudeDesktop.status.applied":`Применено к Desktop`,"claudeDesktop.status.stale":`Конфигурация устарела — примените заново`,"claudeDesktop.status.notApplied":`Не применено`,"claudeDesktop.status.notActiveProfile":`Desktop использует другой профиль — примените заново`,"claudeDesktop.status.disabled":`Интеграция Claude Desktop отключена. После включения полностью закройте и снова откройте Desktop.`,"claudeDesktop.enableApply":`Включить и применить`,"claudeDesktop.health.lastRequest":`Последний запрос`,"claudeDesktop.health.stats":`{count} запр. / {errors} ошиб.`,"claudeDesktop.effort.supported":`effort`,"claudeDesktop.effort.displayOnly":`effort (только отображение)`,"cws.loading":`Загрузка комбо…`,"cws.loadFailed":`Не удалось загрузить комбо.`,"cws.saveFailed":`Не удалось сохранить комбо.`,"cws.removeFailed":`Не удалось удалить комбо.`,"cws.saved":`Комбо сохранено.`,"cws.created":`Создано: {model}.`,"cws.removed":`Удалено: combo/{id}.`,"cws.renamed":`Переименовано: {from} → {to}.`,"cws.add":`Добавить комбо`,"cws.addTitle":`Добавить комбо`,"cws.addSubtitle":`Создайте виртуальную модель для нескольких провайдеров и выберите точное имя, которое будут запрашивать клиенты.`,"cws.create":`Создать комбо`,"cws.railAria":`Список комбо`,"cws.searchPlaceholder":`Поиск комбо или целей…`,"cws.noSearchResults":`Нет комбо, соответствующих запросу.`,"cws.group.failover":`Failover`,"cws.group.roundRobin":`Round-robin`,"cws.group.other":`Другие стратегии`,"cws.targetCount":`{count} целей`,"cws.targetCountOne":`1 цель`,"cws.overviewTitle":`Комбо`,"cws.overviewBlurb":`Виртуальные модели, маршрутизирующие между целями провайдер/модель через failover, round-robin, взвешенный случайный выбор, наименее используемую цель или ближайший сброс квоты.`,"cws.count.total":`Всего`,"cws.count.failover":`Failover`,"cws.count.roundRobin":`Round-robin`,"cws.count.other":`Другие`,"cws.howTitle":`Как это работает`,"cws.howBody":`Запросите у Codex публичное имя модели комбо. Если оно не задано, используется combo/. OpenCodex выбирает цель и переключается на следующую только при сбоях вышестоящего провайдера, допускающих повтор. Если доступных целей не осталось, запрос завершается ошибкой, а не переходит на глобальный провайдер по умолчанию.`,"cws.attentionTitle":`Требует внимания`,"cws.attention.empty":`Цели не настроены`,"cws.attention.few":`Только одна цель — failover некуда переключаться`,"cws.attention.catalogOmitted":`Отсутствует в каталоге моделей — возможности участников неполны или несовместимы (нет context window / метаданных, или пустое пересечение modalities). Маршрутизация по alias всё ещё работает`,"cws.attention.allTargetsExhausted":`Квота исчерпана у всех включённых целей`,"cws.emptyTitle":`Создайте первое комбо`,"cws.empty.createDesc":`Задайте имя виртуальной модели и объедините в цепочку два и более бэкенда.`,"cws.backToAll":`Назад ко всем комбо`,"cws.allCombos":`Все комбо`,"cws.copyModel":`Копировать id`,"cws.copied":`Скопировано`,"cws.tabsLabel":`Разделы деталей комбо`,"cws.tab.config":`Конфигурация`,"cws.tab.about":`О комбо`,"cws.strategy":`Стратегия`,"cws.strategy.failover":`Failover`,"cws.strategy.roundRobin":`Round-robin`,"cws.strategy.random":`Случайный`,"cws.strategy.leastUsed":`Наименее используемый`,"cws.strategy.resetWindow":`Окно сброса`,"cws.strategy.failoverHint":`Цели перебираются по порядку. Если первая завершается ошибкой, допускающей повтор (лимит запросов, сбой, ограничение подписки), происходит переключение на следующую.`,"cws.strategy.roundRobinHint":`Детерминированное распределение трафика по весам. Выбранная цель удерживается на серию успешных запросов, затем селектор переходит к следующей.`,"cws.strategy.randomHint":`Для каждого запроса выбирается одна подходящая цель с вероятностью, пропорциональной весу. Между запросами привязки нет.`,"cws.strategy.leastUsedHint":`Каждый запрос направляется к подходящей цели с наименьшим числом успешных запросов. Счётчики обнуляются при перезапуске прокси.`,"cws.strategy.resetWindowHint":`Предпочитается подходящая цель, чьё окно квот сбрасывается раньше всех. Без данных о квотах действует порядок из конфигурации.`,"cws.field.id":`Id комбо`,"cws.field.idHint":`Клиенты будут запрашивать {model}`,"cws.field.idInternalHint":`Внутренний id комбо. Его можно изменить после создания.`,"cws.field.idHintEdit":`При переименовании комбо будет перенесено на новый id. Клиенты запрашивают {model}.`,"cws.field.alias":`Публичное имя модели`,"cws.field.aliasPlaceholder":`deepseek-v4-flash или vendor/model`,"cws.field.aliasHint":`Необязательно. Используйте имя без префикса, собственный префикс вроде vendor/model или оставьте поле пустым для combo/.`,"cws.field.nativeAlias":`Нативный псевдоним OpenAI`,"cws.field.nativeAliasHint":`Разрешает комбо занимать поддерживаемый неквалифицированный ID нативной модели OpenAI. Маршруты OpenAI с аккаунтом или провайдером остаются отдельными.`,"cws.field.displayName":`Отображаемое имя`,"cws.field.displayNameHint":`Подпись в списке моделей. Обязательна для нативного псевдонима OpenAI.`,"cws.field.stickyLimit":`Успешных запросов до ротации`,"cws.field.stickyLimitHint":`Выбранная цель удерживается на указанное число успешных запросов, прежде чем взвешенный селектор перейдёт к следующей.`,"cws.field.defaultEffort":`Рассуждения по умолчанию`,"cws.field.defaultEffortNone":`Нет (по умолчанию для цели)`,"cws.field.defaultEffortHint":`Используется, только если клиент не указал уровень рассуждений. Варианты — пересечение заявленных уровней выбранных целей.`,"cws.capability.imageInputUnavailable":`Доступно, когда все выбранные цели поддерживают ввод изображений.`,"cws.capability.imageInputHint":`Включено по умолчанию, если все цели поддерживают изображения. Выключите, чтобы принимать только текст.`,"cws.capability.imageInput":`Изображения / мультимодальность`,"cws.capability.adaptiveEffort":`Адаптивная шкала рассуждений`,"cws.capability.adaptiveEffortHint":`Выкл.: цель без настройки рассуждений скрывает выбор уровня для всей комбинации. Вкл.: такие цели остаются доступными, а в выборе сохраняются уровни, общие для остальных целей.`,"cws.capabilities":`Возможности`,"cws.field.defaultEffortUnsupported":`Этот уровень не входит в общую лестницу целей — при запросе он будет проигнорирован или снижен.`,"cws.field.defaultEffortUnsupportedOption":`нет в пересечении`,"cws.targets":`Цели`,"cws.targets.failoverHint":`Порядок важен — первая цель основная.`,"cws.targets.roundRobinHint":`Веса задают детерминированный относительный выбор; при равных весах порядок определяет очерёдность в кольце ротации.`,"cws.targets.randomHint":`Веса задают вероятности каждого выбора; порядок не важен.`,"cws.targets.leastUsedHint":`Порядок разрешает только равенство между одинаково используемыми целями.`,"cws.targets.resetWindowHint":`Порядок применяется, когда данных о квотах нет или они равны.`,"cws.target.provider":`Провайдер`,"cws.target.model":`Модель`,"cws.target.weight":`Вес`,"cws.target.pickProvider":`Выберите провайдера…`,"cws.target.pickProviderFirst":`Сначала выберите провайдера…`,"cws.target.pickModel":`Выберите модель…`,"cws.target.noModels":`Нет моделей для этого провайдера`,"cws.target.modelPlaceholder":`id модели`,"cws.target.add":`Добавить цель`,"cws.target.drag":`Перетащите, чтобы изменить порядок`,"cws.target.moveUp":`Переместить вверх`,"cws.target.moveDown":`Переместить вниз`,"cws.quota.available":`Доступно`,"cws.quota.exhausted":`Квота исчерпана`,"cws.quota.unknown":`Квота неизвестна`,"cws.quota.allExhausted":`Квота исчерпана у всех включённых целей. Выберите другую цель или дождитесь восстановления квоты.`,"cws.aboutTitle":`Поведение во время работы`,"cws.aboutBody":`После сбоя цель на короткое время выводится из ротации с учётом заголовка Retry-After. При ошибках валидации или превышения контекста переключение не выполняется. Каждая цель адаптирует уровень рассуждений к своим возможностям; полностью исчерпанное комбо завершает запрос ошибкой. Разделы «Логи» и «Использование» сохраняют упорядоченные физические попытки и расход по каждой попытке.`,"cws.removeConfirmTitle":`Удалить {model}?`,"cws.removeConfirmDesc":`Виртуальная модель будет удалена из конфигурации и каталога Codex. Провайдеры при этом не удаляются.`,"cws.unsavedTitle":`Несохранённые изменения`,"cws.unsavedDesc":`Отбросить изменения этого комбо и продолжить?`,"cws.keepEditing":`Продолжить редактирование`,"cws.err.missingId":`Необходимо указать id комбо.`,"cws.err.invalidId":`Id должен начинаться с буквы или цифры и содержать только буквы, цифры, точки, подчёркивания и дефисы (не более 64 символов).`,"cws.err.duplicateId":`Комбо с таким id уже существует.`,"cws.err.invalidAlias":`Алиас должен содержать только буквы, цифры, точки, подчёркивания и дефисы, максимум с одним сегментом "/".`,"cws.err.aliasReservedNamespace":`Алиас не должен использовать зарезервированное пространство имён "combo/".`,"cws.err.aliasNativeFamily":`Алиасы без префикса из нативного семейства OpenAI (gpt-*, o1-*, o3-*, o4-*, codex-*) недопустимы.`,"cws.err.unsupportedNativeAlias":`Нативный алиас должен быть одним из поддерживаемых сейчас неквалифицированных id моделей OpenAI.`,"cws.err.missingNativeAliasDisplayName":`Для нативного алиаса требуется отображаемое имя.`,"cws.err.invalidDisplayName":`Отображаемое имя должно содержать не более 128 символов и не иметь управляющих символов.`,"cws.err.duplicateAlias":`Другое комбо уже использует этот алиас.`,"cws.err.noTargets":`Добавьте хотя бы одну цель.`,"cws.err.incompleteTarget":`Для каждой цели нужно указать провайдера и модель.`,"cws.target.disabled":`{name} (отключён)`,"cws.err.reservedNamespace":`Прежде чем создавать комбо, необходимо переименовать физического провайдера с именем «combo».`,"cws.err.providerCollision":`Id комбо конфликтует с именем настроенного провайдера.`,"cws.err.unknownProvider":`Каждая цель должна использовать настроенного провайдера.`,"cws.err.duplicateTarget":`Одна и та же цель провайдер/модель может встречаться только один раз.`,"cws.err.invalidStickyLimit":`Число успешных запросов до ротации должно быть целым от 1 до 100.`,"cws.err.invalidWeight":`Каждый вес round-robin должен быть целым числом от 1 до 10000.`,"cws.err.noEnabledTarget":`Хотя бы одна цель должна использовать включённого провайдера.`,"dash.injectionManage":`Открыть настройки`,"sub.settings":`Настройки`,"sub.sections":`Разделы подагентов`,"sub.delegation.model":`Модель, которую вызывать первой`,"sub.delegation.modelHint":`Модель, к которой Codex обращается первой, когда передаёт работу. Список выше — кого он вообще может вызвать, а здесь выбирается первый в очереди.`,"dash.syncModelsHint":`Перезаписывает каталог моделей Codex по подключённым провайдерам.`,"dash.syncRun":`Синхронизировать`,"lab.title":`Compatibility Lab`,"lab.subtitle":`Read-only compatibility verdict matrix from lab projection evidence.`,"lab.loadFailed":`Could not load compatibility lab data`,"lab.projectionUnavailable":`Lab projection is not available. Run conformance or live probes first.`,"lab.projectionIncompatible":`Lab projection schema is incompatible. Rebuild the projection.`,"lab.statusTitle":`Projection status`,"lab.matrixTitle":`Compatibility matrix`,"lab.verdictsTitle":`Verdict records`,"lab.filter.layer":`Evidence layer`,"lab.filter.verdict":`Verdict`,"lab.filter.subject":`Subject ID`,"lab.filter.all":`All`,"lab.col.subject":`Subject`,"lab.col.layer":`Layer`,"lab.col.suite":`Suite`,"lab.col.verdict":`Verdict`,"lab.col.asOf":`As of`,"lab.col.protocol":`Protocol conformance`,"lab.col.live":`Live route compatibility`,"lab.col.task":`Task effectiveness`,"lab.empty":`No compatibility verdicts in the projection yet.`,"lab.subjectKind":`Kind`,"lab.observationCount":`Observations`,"lab.eventCount":`Events`,"lab.verdictCount":`Verdicts`,"lab.subjectCount":`Subjects`,"lab.builtAt":`Built`,"lab.loading":`Loading compatibility evidence…`,"lab.loadMore":`Load more`,"lab.detailTitle":`Verdict detail`,"lab.detailClose":`Close`,"lab.detailSubject":`Subject`,"lab.detailObservations":`Observations`,"lab.detailEvents":`Contributing events`,"lab.detailArtifacts":`Artifact metadata`,"lab.production.title":`Наблюдаемый производственный трафик`,"lab.production.notVerification":`Не является проверкой Lab`,"lab.production.attempts":`Попытки`,"lab.production.successes":`Успешные попытки`,"lab.production.routeErrors":`Ошибки маршрута`,"lab.production.lastObserved":`Последнее наблюдение`,"lab.detailLoadFailed":`Could not load verdict detail`,"lab.refresh":`Refresh`,"lab.verdict.UNKNOWN":`Unknown`,"lab.verdict.CLAIMED":`Claimed`,"lab.verdict.PROBED":`Probed`,"lab.verdict.VERIFIED":`Verified`,"lab.verdict.DEGRADED":`Degraded`,"lab.verdict.BLOCKED":`Blocked`,"lab.verdict.UNSUPPORTED":`Unsupported`,"lab.layer.protocol_conformance":`Protocol conformance`,"lab.layer.live_route_compatibility":`Live route compatibility`,"lab.layer.task_effectiveness":`Task effectiveness`,"dash.visionAdvanced":`Дополнительные настройки`,"dash.visionMaxDescriptions":`Максимум описаний за ход`,"dash.visionMaxDescriptionsInvalid":`Введите положительное целое число.`,"dash.visionTimeout":`Таймаут`,"dash.visionTimeoutInvalid":`Введите целое число от {min} до {max} миллисекунд.`,"dash.visionAdvancedPopover":`Дополнительные настройки изображений`,"models.newPolicyGlobal":`Добавлять новые модели выключенными`,"models.newPolicyProvider":`Политика новых моделей`,"models.newPolicy_inherit":`Наследовать`,"models.newPolicy_off":`Выкл.`,"models.newPolicy_on":`Вкл.`,"models.newBadge":`НОВАЯ`,"models.newCount":`Новых: {count}, выкл.`,"models.aliases":`Псевдонимы`,"models.aliasesTable":`Таблица псевдонимов`,"models.aliasPrompt":`Псевдоним провайдера (оставьте пустым, чтобы очистить)`,"models.modelAliasPrompt":`Псевдоним модели (оставьте пустым, чтобы очистить)`,"models.aliasSaved":`Псевдоним сохранён`,"models.aliasConflict":`Этот псевдоним конфликтует с существующим именем`,"models.editProviderAlias":`Изменить псевдоним провайдера`,"models.editModelAlias":`Изменить псевдоним модели`,"models.useDefaultAliases":`Использовать псевдонимы по умолчанию`,"models.useDefaultAliasesGlobal":`Использовать псевдонимы по умолчанию везде`,"models.aliasAuto":`авто`,"models.aliasUser":`пользователь`,"models.aliasStale":`устарел`,"connection.discovering":`Discovering local and shared targets…`,"connection.machineUnavailable":`The local machine plane is unavailable. Shared requests were not redirected locally.`,"connection.disconnect":`Disconnect from hub`,"connection.disconnectConfirm":`Disconnect this machine from the hub and restart it in standalone mode?`,"connection.pairing.title":`Connect this dashboard to the hub`,"connection.pairing.body":`Paste the one-time pairing code created on the hub.`,"connection.pairing.relayWarning":`This code is exchanged through the fixed hub relay. The relay cannot be redirected to another host.`,"connection.pairing.code":`One-time pairing code`,"connection.pairing.submit":`Connect`,"connection.pairing.submitting":`Connecting…`,"connection.pairing.error":`The pairing code was refused or expired. The code was left in place so you can check it.`,"connection.machine.title":`This machine`,"connection.machine.shimHealthy":`Codex shim is healthy.`,"connection.machine.shimNeedsAttention":`Codex shim needs attention.`,"connection.machine.repairShim":`Repair shim`,"connection.machine.removeShim":`Remove shim`,"connection.clients.title":`Connected clients`,"connection.clients.none":`No client status available`,"connection.clients.sync":`Sync now`,"connection.clients.syncing":`Syncing…`,"connection.sessionLogout":`Выйти из удалённой сессии`,"connection.sessionLoggingOut":`Выход из удалённой сессии…`,"connection.sessionLogoutFailed":`Не удалось выйти из удалённой сессии. Текущая сессия сохранена.`,"usage.source.connected":`Source: hub usage`,"usage.source.local":`Source: local usage.jsonl`,"usage.scope.label":`Usage scope`,"usage.scope.machine":`This machine`,"usage.scope.hub":`Hub-wide`,"usage.hubOffline":`Hub usage is unavailable. Local usage was not substituted.`,"integrations.tab.cursor":`Cursor`,"integrations.detail.cursorSeen":`Cursor недавно обращался к этому прокси`,"integrations.detail.cursorNeverSeen":`Cursor Private Inference установлен; запросов пока не было`,"integrations.detail.cursorAbsent":`Cursor Private Inference не найден`,"integrations.cursor.title":`Cursor`,"integrations.cursor.intro":`Cursor Private Inference запускает своего агента локально и обращается к opencodex через loopback. Обычный Cursor так не может: его серверная часть обращается к пользовательскому эндпоинту, для чего нужен публичный HTTPS-адрес. Эта страница ничего не записывает в Cursor; самостоятельно вставьте указанные ниже значения в Cursor.`,"integrations.cursor.loading":`Получение статуса Cursor…`,"integrations.cursor.unavailable":`Не удалось получить от прокси статус Cursor.`,"integrations.cursor.detection":`Установленные сборки`,"integrations.cursor.privateInference":`Cursor Private Inference`,"integrations.cursor.regular":`Cursor (обычная версия)`,"integrations.cursor.detected":`Обнаружено`,"integrations.cursor.notFound":`Не найдено`,"integrations.cursor.regularOnly":`Найден только обычный Cursor. Пользовательские эндпоинты он направляет через серверы Cursor, поэтому loopback-прокси недоступен без публичного туннеля. Сведения о сборке Cursor Private Inference см. в руководстве.`,"integrations.cursor.nothingFound":`Установка Cursor в обычных расположениях не обнаружена. Если Cursor установлен в другом месте, приведённые ниже значения всё равно подходят.`,"integrations.cursor.gateway":`Параметры шлюза`,"integrations.cursor.gatewayHint":`В Cursor Private Inference откройте Settings > Models > Gateway, вставьте эти два значения, затем нажмите Refresh model list.`,"integrations.cursor.baseUrl":`Базовый URL`,"integrations.cursor.apiKey":`API-ключ`,"integrations.cursor.apiKeyCredential":`Один из ваших API-ключей opencodex (для этой привязки требуются учётные данные)`,"integrations.cursor.copy":`Скопировать`,"integrations.cursor.copied":`Скопировано`,"integrations.cursor.connection":`Подключение`,"integrations.cursor.seen":`Последний запрос от Cursor: {time} ({ua})`,"integrations.cursor.neverSeen":`Запросов от Cursor не было с момента запуска прокси. После сохранения параметров шлюза нажмите Refresh model list в Cursor.`,"integrations.cursor.models":`Что будет отображаться в Cursor`,"integrations.cursor.modelsHint":`Cursor выбирает шкалу уровней рассуждений из собственной таблицы моделей, поэтому opencodex может только предсказать её. В столбце «Контекст» указаны окно по умолчанию и дополнительное окно, доступное при включении Max Mode в Cursor.`,"integrations.cursor.ladderFromBundle":`Уровни рассуждения прочитаны из установленного бандла Cursor Private Inference {version}. Их определяет Cursor; opencodex лишь показывает его таблицу.`,"integrations.cursor.ladderFromStatic":`Уровни рассуждения — статическая копия Cursor 3.18.25 (читаемый бандл Private Inference не найден). Столбец «Контекст» показывает окно по умолчанию и опциональное окно.`,"integrations.cursor.unknownVersion":`версия неизвестна`,"integrations.cursor.noControl":`—`,"integrations.cursor.singleWindow":`одно окно`,"integrations.cursor.noControlTitle":`Этого id нет во встроенной таблице усилий Cursor, поэтому Cursor не показывает управление рассуждением.`,"integrations.cursor.effortRowsOne":`опубликована 1 строка усилия`,"integrations.cursor.effortRowsMany":`опубликовано строк усилия: {n}`,"integrations.cursor.effortRowsOff":`строк усилия нет`,"integrations.cursor.tableLessHint":`Строки с — не получают управление рассуждением в Cursor. Включите cursorEffortRows, чтобы публиковать по одной записи выбора на каждое усилие (id--effort), или задайте modelDefaultReasoningEfforts у провайдера для фиксированного значения.`,"integrations.cursor.colModel":`Модель`,"integrations.cursor.colReasoning":`Рассуждения`,"integrations.cursor.colContext":`Контекст`,"integrations.cursor.guide":`Открыть руководство по Cursor Private Inference`},Ge={"nav.dashboard":`ダッシュボード`,"uptime.day":`日`,"uptime.hour":`時間`,"uptime.minute":`分`,"uptime.second":`秒`,"nav.startup":`起動安全性`,"nav.providers":`プロバイダー`,"nav.models":`モデル`,"nav.combos":`コンボ`,"nav.subagents":`サブエージェント`,"routing.title":`ルーティングインテリジェンス (beta)`,"routing.subtitle":`ポリシープロファイル、ドライラン評価、ソース連携のルーティング分析。`,"routing.loadFailed":`ルーティングデータを読み込めませんでした`,"routing.empty":"ルーティングプロファイルが設定されていません。config.json に `routingProfiles` を追加してください。","routing.revision":`rev`,"routing.detail":`プロファイル`,"routing.createProfile":`プロファイルを作成`,"routing.dryRunError":`ドライラン失敗 (HTTP {status})`,"routing.removeConfirm":`プロファイル {id} を削除しますか?`,"routing.unknownEvidence.allow":`許可`,"routing.unknownEvidence.penalize":`ペナルティ`,"routing.unknownEvidence.exclude":`除外`,"routing.removeCandidate":`候補 {provider}/{model} を削除`,"routing.candidates":`候補`,"routing.require":`必須要件`,"routing.optimize":`最適化ウェイト`,"routing.limits":`制限`,"routing.unknownEvidence":`不明なエビデンスのポリシー`,"routing.compatibility.title":`互換性ポリシー`,"routing.compatibility.enabled":`Compatibility Lab エビデンスを必須にする`,"routing.compatibility.requiredSuites":`必須スイート`,"routing.compatibility.loadingCatalog":`Lab カタログを読み込み中…`,"routing.compatibility.catalogUnavailable":`Lab カタログを利用できません — config.json でスイート ID を手動入力してください。`,"routing.compatibility.layer.protocol_conformance":`プロトコル適合`,"routing.compatibility.layer.live_route_compatibility":`ライブルート互換性`,"routing.compatibility.minStatus":`最低互換性ステータス`,"routing.none":`なし`,"routing.unavailable":`–`,"routing.dryRun":`ドライラン評価`,"routing.dryRunContext":`リクエストのコンテキストウィンドウ(トークン)`,"routing.dryRunTools":`リクエストにツールが必要`,"routing.dryRunImage":`リクエストに画像入力が必要`,"routing.dryRunStructured":`リクエストに構造化出力が必要`,"routing.dryRunRun":`候補を評価`,"routing.candidate":`候補`,"routing.eligible":`対象`,"routing.exclusions":`除外`,"routing.costCap":`コスト上限`,"routing.capOutcome.satisfied":`上限内`,"routing.capOutcome.exceeded":`上限超過`,"routing.capOutcome.unknown-allowed":`不明(許可)`,"routing.capOutcome.unknown-excluded":`不明(除外)`,"routing.exclusion.capability-unsatisfied":`能力要件未達`,"routing.exclusion.unknown-capability":`能力不明`,"routing.exclusion.cost-limit":`コスト上限超過`,"routing.exclusion.cost-limit-unknown":`上限下でコスト不明`,"routing.exclusion.cooldown":`クールダウン`,"routing.exclusion.unknown-health":`健全性不明`,"routing.exclusion.unknown-quota":`割当不明`,"routing.exclusion.unknown-price":`価格不明`,"routing.exclusion.other":`除外: {code}`,"routing.score":`スコア`,"routing.selected":`選択済み`,"routing.yes":`はい`,"routing.no":`いいえ`,"routing.analytics":`ルーティング分析`,"routing.analyticsTotal":`リクエスト`,"routing.analyticsSuccessRate":`成功率`,"routing.analyticsFallbackRate":`フォールバック`,"routing.analyticsP50":`p50`,"routing.analyticsP95":`p95`,"routing.analyticsP99":`p99`,"routing.analyticsCooldown":`クールダウン失敗`,"routing.analyticsConfidence":`信頼度`,"routing.analyticsTruncated":`切り捨て履歴`,"routing.analyticsRequests":`リクエスト`,"routing.analyticsEmpty":`分析はまだありません。まずリクエストを送信してください。`,"nav.logs":`ログ & デバッグ`,"nav.usage":`使用量`,"common.github":`GitHub`,"sidebar.star":`GitHub でスターを付ける`,"sidebar.starred":`GitHub でスター済み`,"sidebar.starUnauthenticated":`GitHub を開いてスターを付ける (gh CLI が未ログイン)`,"sidebar.starFailed":`gh でスターを付けられませんでした。代わりに GitHub を開きます。`,"sidebar.updateAvailable":`更新あり: {version}`,"sidebar.checkUpdate":`更新を確認`,"common.save":`保存`,"common.saving":`保存中…`,"common.cancel":`キャンセル`,"common.discard":`破棄`,"common.delete":`削除`,"common.close":`閉じる`,"common.ok":`OK`,"common.remove":`削除`,"common.loading":`読み込み中…`,"common.retry":`再試行`,"auth.adminTokenTitle":`OpenCodex 管理者トークン (OPENCODEX_ADMIN_AUTH_TOKEN)`,"auth.adminAccountLabel":`アカウント`,"auth.adminTokenFieldLabel":`管理者トークン`,"auth.adminTokenRejected":`管理者トークンが拒否されました。確認してもう一度お試しください。`,"auth.adminTokenUnavailable":`管理者トークンを確認できませんでした。もう一度お試しください。`,"app.logoAria":`opencodex ロゴ`,"app.claudeOn":`Claude オン`,"app.claudeOff":`Claude オフ`,"theme.label":`テーマ`,"theme.light":`ライト`,"theme.dark":`ダーク`,"theme.system":`システム`,"lang.label":`言語`,"lang.nativeName":`日本語`,"provider.name.commandCodeAuth":`Command Code - Auth`,"provider.name.commandCodeApi":`Command Code - API`,"provider.name.volcengine":`Volcengine Ark`,"provider.name.volcengineCodingPlan":`Volcengine Ark コーディングプラン`,"provider.name.volcengineAgentPlan":`Volcengine Ark エージェントプラン`,"errorBoundary.title":`ページを読み込めませんでした`,"errorBoundary.message":`このセクションの表示中にエラーが発生しました。再読み込みしてもう一度お試しください。`,"errorBoundary.details":`エラー`,"errorBoundary.reload":`再読み込み`,"startup.title":`起動安全性`,"startup.subtitle":`再起動後にローカルプロキシへの接続が再接続ループになる前に、Codex が opencodex へ到達できるか確認します。`,"startup.refresh":`更新`,"startup.backToDashboard":`ダッシュボードに戻る`,"startup.loading":`起動保護を確認中…`,"startup.error":`起動保護を読み取れませんでした。`,"startup.staleData":`最新の確認に失敗しました。以下は古い値であり、保護の証明にはなりません。`,"startup.status.native":`ネイティブルーティング`,"startup.status.protected":`再起動保護済み`,"startup.status.atRisk":`対応が必要`,"startup.summary.native":`Codex はローカルプロキシに依存していません`,"startup.summary.protected":`再起動後も opencodex を利用できます`,"startup.summary.atRisk":`再起動後に Codex がモデルへ接続できなくなる可能性があります`,"startup.riskDetail":`Codex はローカルプロキシを参照していますが、再起動する永続サービスまたは正常な launcher shim がありません。`,"startup.riskDetailCustomLocal":`Codex はカスタムローカルゲートウェイを参照しています。opencodex はその再起動ライフサイクルを管理・検証できません。`,"startup.riskDetailWindowsShim":`Launcher shim は対応する CLI スクリプトのみを保護し、Windows の Codex Desktop と codex.exe の直接起動はこれを迂回できます。`,"startup.safeDetail":`現在のルーティングと起動方式は整合しています。再起動後に ocx start を手動実行する必要はありません。`,"startup.routing":`Codex ルーティング`,"startup.routing.proxy":`ローカルプロキシ`,"startup.routing.native":`OpenAI ネイティブ`,"startup.routing.customLocal":`カスタムローカルゲートウェイ`,"startup.routing.customRemote":`カスタム遠隔ゲートウェイ`,"startup.routing.unknown":`不明または無効なルーティング`,"startup.restartProtection":`再起動保護`,"startup.preference":`オンデマンド起動`,"startup.enabled":`有効`,"startup.disabled":`無効`,"startup.protection.service":`バックグラウンドサービス`,"startup.protection.shim":`Launcher shim`,"startup.protection.none":`未インストール`,"startup.details":`保護の詳細`,"startup.service":`バックグラウンドサービス`,"startup.serviceHint":`ログイン時に起動し、クラッシュ後にプロキシを再起動します。`,"startup.installed":`インストール済み`,"startup.notInstalled":`未インストール`,"startup.unsupported":`未対応`,"startup.shim":`Codex launcher shim`,"startup.shimHint":`対応する Codex スクリプトランチャーの起動時に ocx ensure を実行します。`,"startup.healthy":`正常`,"startup.cliOnly":`CLI のみ`,"startup.stale":`要更新`,"startup.viable":`利用可能`,"startup.unhealthy":`インストール済み・異常`,"startup.conflict":`サービス競合`,"startup.installedDisabled":`インストール済み・無効`,"startup.install":`インストール`,"startup.installing":`インストール中…`,"startup.repair":`修復`,"startup.repairing":`修復中…`,"startup.serviceInstalled":`バックグラウンドサービスをインストールしました。`,"startup.serviceRepaired":`バックグラウンドサービスを修復しました。`,"startup.shimInstalled":`Codex ランチャー shim をインストールしました。`,"startup.shimRepaired":`Codex ランチャー shim を修復しました。`,"startup.installFailed":`インストールに失敗しました:`,"startup.tray.title":`Windows システムトレイ`,"startup.tray.hint":`ログイン時にトレイを起動し、プロキシの開始・停止・再起動・ダッシュボード・状態をクリックで操作します。`,"startup.tray.login":`Windows ログイン時にトレイを開始`,"startup.tray.notProtection":`トレイは操作画面であり再起動保護ではありません。無人復旧には正常なバックグラウンドサービスが必要です。`,"startup.tray.running":`実行中`,"startup.tray.stopped":`インストール済み・非表示`,"startup.tray.stale":`修復が必要`,"startup.tray.notInstalled":`未インストール`,"startup.tray.loading":`確認中…`,"startup.tray.unavailable":`状態を確認できません`,"startup.tray.install":`トレイをインストールして表示`,"startup.tray.start":`トレイアイコンを表示`,"startup.tray.stop":`トレイアイコンを終了`,"startup.tray.uninstall":`ログイントレイを削除`,"startup.tray.error":`Windows トレイ操作に失敗しました。ocx tray status で詳細を確認してください。`,"startup.recovery":`修復方法`,"startup.recoveryHint":`上のワンクリックインストールを使うか、手動修復用のコマンドをコピーできます。Codex Desktop と Windows 実行ファイルにはバックグラウンドサービスを推奨します。`,"startup.command.service":`推奨: 永続バックグラウンドサービス`,"startup.command.shim":`代替: CLI launcher shim`,"startup.command.native":`安全策: Codex ネイティブルーティングを復元`,"startup.copy":`コピー`,"startup.copied":`コピー済み`,"startup.recommended":`推奨修復: {cmd}`,"startup.navRisk":`起動保護に対応が必要です`,"startup.codexRuntime.clampHidden":`OpenCodex が Codex {version} を使用したため、一部の reasoning effort オプションが非表示になりました。`,"startup.codexRuntime.clampHiddenWithEfforts":`OpenCodex が Codex {version} を使用したため、一部の reasoning effort オプションが非表示になりました(削除: {efforts})。`,"startup.codexRuntime.olderBinary":`OpenCodex は古い Codex バイナリ({version})を使用しています。より新しいインストールが利用可能です。`,"dash.subtitle":`ローカル opencodex プロキシ、そのプロバイダー、Codex にルーティングされるモデルのライブ状態です。`,"dash.workspace.overview":`概要`,"dash.workspace.sections":`セクション`,"dash.status":`状態`,"dash.online":`オンライン`,"dash.offline":`オフライン`,"dash.version":`バージョン`,"dash.uptime":`稼働時間`,"dash.providers":`プロバイダー`,"dash.tokens30d":`トークン (30日)`,"dash.coverage":`{pct} カバレッジ`,"dash.mem.title":`メモリ可観測性`,"dash.mem.hint":`読み取り専用のランタイム診断。観測メモリは max(RSS, external, ArrayBuffers) で、Windows の working set trimming がコミット済み保持を隠さないようにします。`,"dash.mem.rss":`常駐メモリ (RSS)`,"dash.mem.jsHeap":`JS ヒープ使用量`,"dash.mem.jsHeapArena":`アリーナ {total}`,"dash.mem.pressure":`警告しきい値に対して`,"dash.mem.pressureOf":`しきい値の {pct}%`,"dash.mem.pressureUnknown":`しきい値の情報なし`,"dash.mem.jscHeap":`JSC ヒープ`,"dash.mem.external":`External`,"dash.mem.arrayBuffers":`ArrayBuffers`,"dash.mem.observed":`観測値`,"dash.mem.runtime":`ランタイムカウンター`,"dash.mem.growth":`1時間あたりの観測変化`,"dash.mem.perHour":`/時間`,"dash.mem.store":`継続ストア`,"dash.mem.storeHint":`プロキシの previous_response_id キャッシュ。ヒープ増加中に合計バイトが増える場合、ランタイムアロケータではなく会話保持を示します。`,"dash.mem.storeEntries":`エントリ`,"dash.mem.storeTotal":`合計`,"dash.mem.storeLargest":`最大`,"dash.mem.storeOldest":`最古`,"dash.mem.threshold":`警告しきい値`,"dash.mem.lastWarn":`最終警告`,"dash.mem.never":`なし`,"dash.mem.details":`詳細`,"dash.mem.unavailable":`メモリ診断は利用できません(旧バージョンのプロキシ)。`,"dash.mem.inFlight":`処理中のリクエスト`,"dash.mem.restart":`完了後に再起動`,"dash.mem.restartConfirm":`処理中のリクエスト {count} 件の完了を待ってから再起動します(最大 {seconds} 秒。タイムアウト時は残りを打ち切ります)。`,"dash.mem.draining":`リクエスト {count} 件の完了を待機中… 完了後に再起動`,"dash.mem.reconnecting":`プロキシを再起動中… 再接続を待機`,"dash.mem.restartFailed":`完了後の再起動に失敗しました。プロキシが起動しているか確認してください。`,"dash.mem.restartNoSupervisor":`再起動保護がありません。再起動後、プロキシが自動で戻らない可能性があります。`,"dash.activeProviders":`アクティブなプロバイダー`,"dash.noProviders":`プロバイダーが設定されていません。{cmd} を実行してください。`,"dash.col.name":`名前`,"dash.col.adapter":`アダプター`,"dash.col.baseUrl":`ベース URL`,"dash.col.model":`モデル`,"dash.modelsNoResults":`検索に一致するモデルはありません。`,"dash.availableModels":`利用可能なモデル`,"dash.noModels":`モデルが見つかりません。プロバイダーの API キーを確認してください。`,"dash.cannotConnect":`プロキシに接続できません。起動していますか?`,"dash.runStart":`{cmd} を実行してプロキシを起動してください。`,"dash.stop":`プロキシを停止`,"dash.stopConfirm":`プロキシを停止してネイティブの Codex に戻しますか?`,"dash.stopFailed":`プロキシを停止できませんでした (HTTP {status})。`,"dash.maSwitchFailed":`モードの切り替えに失敗しました (HTTP {status})。`,"dash.maNetworkError":`ネットワークエラー — プロキシは起動していますか?`,"dash.stopping":`停止中…`,"dash.actions":`プロキシ`,"dash.codexRestart":`Codex のモデル一覧を再読み込み`,"dash.codexRestarting":`停止中…`,"dash.codexRestartConfirm":`Codex app-server を停止してモデル一覧を読み直させますか? 進行中の Codex の処理は中断され、Codex は自動では再起動しないので後で開き直してください。`,"dash.codexRestartDone":`Codex app-server を {count} 個停止しました。Codex を開き直すと最新のモデル一覧が読み込まれます。`,"dash.codexRestartNothing":`実行中の Codex app-server はありません。次回起動時に最新のモデル一覧を読み込みます。`,"dash.codexRestartUnknown":`プロセスを列挙できなかったため、何も停止しませんでした。`,"dash.codexRestartPartial":`app-server が {count} 個終了しませんでした。モデル一覧が古いままなら手動で停止してください。`,"dash.codexRestartFailed":`Codex のモデル一覧を再読み込みできませんでした (HTTP {status})。`,"dash.codexRestartUnreachable":`プロキシに接続できませんでした。`,"dash.codexRestartMalformed":`プロキシが予期しない応答を返しました。`,"dash.codexRestartTimeout":`プロキシから時間内に応答がありませんでした。app-server の停止が続いている可能性があります。`,"models.staleBanner":`Codex はこのカタログより古いモデル一覧を表示しています。Codex を再起動すると読み直されます。`,"dash.codexAutoStart":`Codex と一緒に opencodex を起動`,"dash.codexAutoStartHint":`インストール済み launcher shim に ocx ensure の実行を許可します。この設定だけでは再起動保護はインストールされません。起動安全性で実際の状態を確認してください。`,"dash.searchModel":`検索サイドカーモデル`,"dash.searchModelHint":`非 OpenAI ルーティングモデルで web_search に使うモデル。ChatGPT ログインが必要です。`,"dash.searchReasoning":`検索の推論負荷`,"dash.visionModel":`ビジョンサイドカーモデル`,"dash.visionModelHint":`テキスト専用ルーティングモデルで画像を説明するために使うモデル。ChatGPT ログインが必要です。`,"dash.webSearchSidecar":`ウェブ検索サイドカー`,"dash.webSearchSidecarHint":`ルーティングモデルでウェブ検索に使うバックエンドとモデルを選択します。`,"dash.webSearchStream":`回答をライブ配信`,"dash.webSearchStreamHint":`モデルがツール呼び出しを決定するまで、先頭のテキストと推論をライブ配信します。以降は検索インターセプトのためバッファされます。検索前のテキストは一部繰り返される場合があります。`,"dash.visionSidecar":`ビジョンサイドカー`,"dash.visionSidecarHint":`テキスト専用ルーティングモデルで画像を説明するために使うバックエンドとモデルを選択します。`,"dash.visionOff":`オフ`,"dash.shadowCallIntercept":`シャドウコール傍受`,"dash.shadowCallInterceptHint":`Codex App のバックグラウンドヘルパー呼び出し({models}: タイトル生成、コミットメッセージ)を傍受し、選択したモデルにリダイレクトします。`,"dash.shadowCallWarning":`⚠ オンにすると、{models} へのリクエストがすべて選択したモデルに置き換えられます。`,"dash.shadowCallOriginal":`元のモデル`,"dash.shadowCallModel":`差し替えモデル`,"dash.shadowCallTooltip":`Codex App はスレッドタイトル生成、コミットメッセージ生成、スキルオーケストレーションをバックグラウンドで呼び出します。使われるモデルはクライアントのバージョンによって変わるため、opencodex は {models} をまとめて傍受します。これをオンにすると、それらの呼び出しを選択したモデルにリダイレクトします。`,"models.shadowCallIntercept":`シャドウコール傍受`,"models.shadowCallInterceptHint":`Codex App のバックグラウンドヘルパー呼び出し({models}: タイトル、コミットメッセージ)を傍受し、選択したモデルにリダイレクトします。`,"dash.sidecarBackend":`バックエンド`,"dash.sidecarModel":`モデル`,"dash.backendAuto":`自動`,"dash.backendOpenAI":`OpenAI`,"dash.backendAnthropic":`Anthropic`,"dash.sidecarSaved":`サイドカー設定を保存しました。次回リクエスト時に適用されます。`,"dash.sidecarSaveFailed":`サイドカー設定の保存に失敗しました。`,"dash.injectionLabel":`サブエージェント委任`,"dash.injectionHint":`Codex がサブエージェントに作業を渡すときのモデルを選びます。この選択をどこに適用するかは下の 2 つのスイッチが決めます。`,"dash.syncCodexSubagentDefaults":`Codex の既定値としても保存`,"dash.syncCodexSubagentDefaultsHint":`オンにすると、上で選んだモデルが Codex 自身の設定にも保存され、新しいタスクも最初からそのモデルを使います。オフならここだけで記憶します。反映は次回の同期または再起動時で、自分で書いた [agents] 設定はそのまま残ります。`,"dash.multiAgentGuidance":`作業の分け方を伝える`,"dash.multiAgentGuidanceHint":`「作業はこう分けて任せる」という短いメモを Codex に送ります。v2 では使えるモデルと優先モデルを伝え、v1 では推論強度が max か ultra のときだけ働きます。オフならメモは付きません。`,"dash.injectionNone":`なし`,"dash.injectionEffortLabel":`推論負荷`,"dash.injectionEffortNone":`モデル既定`,"dash.effortCapLabel":`V2 ultra 推論上限`,"dash.subagentEffortCapLabel":`V2 サブエージェント推論上限`,"dash.effortCapHelp":`V2 ultra モードのターンの推論負荷を制限します。設定すると、(ultra モードからの)最大負荷リクエストは選択したレベルに制限されます。サブエージェント上限は生成された子エージェントにのみ適用されます。上限は負荷を下げるだけで上げることはありません。モデルが制限レベルをサポートしない場合、最も近いサポートレベルに切り下げられます。`,"dash.effortCapNone":`上限なし`,"dash.maintenance":`メンテナンス`,"dash.maintenanceHint":`Codex のモデルカタログを更新するか、より新しい opencodex リリースをインストールします。`,"dash.syncModels":`モデルを同期`,"dash.syncing":`同期中…`,"dash.syncOk":`同期完了。{count} 個のモデルを追加しました。`,"dash.syncStaleHint":`Codex がまだ古いリストを表示する場合、長時間稼働の app-server を再起動してください({cmd})。`,"dash.syncFailed":`同期失敗: {error}`,"dash.projectConfigTitle":`プロジェクトの Codex 設定が OpenCodex をバイパスします`,"dash.projectConfigHint":`これらのリポジローカル設定は OpenCodex プロキシを上書きします(例: OpenCode Go に直接ルーティング)。~/.codex/config.toml のルーティングがそのプロジェクトで適用されるように削除してください。`,"dash.checkUpdate":`更新を確認`,"dash.updateTitle":`opencodex を更新`,"dash.updateDesc":`選択したチャンネルの npm を確認し、インストール後にプロキシを再起動するか選択します。`,"dash.updateChannel":`チャンネル`,"dash.updateChecking":`更新を確認中…`,"dash.updateInstalled":`インストール済み`,"dash.updateLatest":`最新`,"dash.updateAvailable":`更新があります`,"dash.updateCurrent":`最新です`,"dash.updateCommand":`コマンド`,"dash.updateSource":`これはソースチェックアウトです。表示されたコマンドでターミナルから更新してください。`,"dash.updateUnavailable":`npm から最新バージョンを読み取れませんでした。後でもう一度お試しください。`,"dash.updateRetry":`再試行`,"dash.updateRecheck":`再確認`,"dash.updateCannotAuto":`ワンクリック更新は利用できません({reason})。`,"dash.updateReason.source_checkout":`ソースチェックアウト`,"dash.updateReason.latest_unavailable":`npm レジストリに到達できません`,"dash.updateReason.already_latest":`最新です`,"dash.updateReason.unknown":`更新は利用できません`,"dash.updateRestart":`更新後に再起動`,"dash.updateRestartHint":`推奨。プロキシが再起動されるまで現在の GUI は古いコードを実行し続けます。`,"dash.runUpdate":`更新`,"dash.updateReconnecting":`再起動したプロキシを待機中…`,"dash.updateStatus.running":`opencodex を更新しています。`,"dash.updateStatus.restarting":`更新をインストールしました。プロキシを再起動中。`,"dash.updateStatus.succeeded":`更新が完了しました。`,"dash.updateVersionTransition":`{currentVersion} -> {latestVersion}.`,"dash.updateStatus.failed":`更新に失敗しました。`,"prov.subtitle":`opencodex が Codex にルーティングする上流プロバイダーを設定します。アカウントでログインするか、プロバイダーを追加、または生の設定を編集します。`,"prov.add":`プロバイダーを追加`,"prov.editJson":`JSON を編集`,"prov.accountLogin":`アカウントログイン`,"prov.noOauth":`利用可能な OAuth プロバイダーがありません。`,"prov.loggedIn":`ログイン済み`,"prov.notLoggedIn":`未ログイン`,"prov.logout":`ログアウト`,"prov.login":`ログイン`,"prov.loginWith":`{provider} でログイン`,"prov.waitingBrowser":`ブラウザを待機中…`,"prov.didntOpen":`開きませんか? ここをクリック`,"prov.copyLink":`リンクをコピー`,"prov.dontOpenBrowser":`プロキシのマシンでブラウザーを開かない`,"prov.dontOpenBrowserHint":`別のブラウザープロファイルでログインする場合や、ダッシュボードがプロキシと別のマシンにある場合に使います。`,"prov.linkCopied":`コピーしました`,"prov.linkCopyUnavailable":`クリップボードを使用できません`,"prov.deviceCode":`デバイスコード`,"prov.copyCode":`コードをコピー`,"prov.codeCopied":`コードをコピーしました`,"prov.pasteRedirect":`リダイレクト URL またはコードを貼り付け`,"prov.pasteRedirectHint":`ブラウザに localhost エラーが表示された場合、アドレスバーから URL 全体をコピーしてここに貼り付けてください(または認可コードを貼り付け)。`,"prov.pasteSubmit":`送信`,"prov.pasteSubmitting":`送信中…`,"prov.pasteOk":`コードを送信しました — ログインを完了しています…`,"prov.pasteFail":`コードを送信できませんでした: {error}`,"prov.port":`ポート`,"prov.default":`デフォルト`,"prov.loadingConfig":`読み込み中…`,"prov.saved":`保存しました! 適用にはプロキシを再起動してください。`,"prov.loadConfigFail":`設定の読み込みに失敗しました`,"prov.invalidJson":`無効な JSON です`,"prov.saveFailed":`保存に失敗しました`,"prov.loginFailStart":`{provider} ログインを開始できませんでした`,"prov.loginError":`{provider} ログインエラー: {error}`,"prov.loginRequestFail":`{provider} ログインリクエストに失敗しました`,"prov.loginCancelled":`{provider} ログインはキャンセルされました`,"prov.loginTimeout":`{provider} ログインがタイムアウトしました — ブラウザが閉じたか完了しませんでした。もう一度お試しください。`,"prov.loginOk":`{provider} にログインしました。{cmd} を実行(またはライブで適用)してモデルを一覧表示します。`,"prov.loginSameAccount":`同じ {provider} アカウントのままです。ブラウザでアカウントを切り替えてから、もう一度アカウント追加を試してください。`,"oauthTos.highTitle":`{provider}: サブスクリプション OAuth リスク`,"oauthTos.elevatedTitle":`{provider}: 非公式 OAuth ブリッジ`,"oauthTos.anthropicBody":`OpenCodex のような第三者プロキシ経由で Claude サブスクリプションの OAuth トークンを直接再利用することは、Anthropic がサポートする統合ではなく、アクセス制限につながる可能性があります。Claude サブスクリプションを使用するサポートされた Agent SDK 統合は別物です。`,"oauthTos.highBody":`OpenCodex は第三者 OAuth パス経由で {provider} に接続します。サポート外の利用はアクセス制限や停止につながる可能性があります。`,"oauthTos.elevatedBody":`OpenCodex は非公式 OAuth パス経由で {provider} に接続します。可能な場合は公式クライアントを使用してください。異常または自動化されたトラフィックは悪用とみなされ、アクセスが制限または停止される可能性があります。`,"oauthTos.saferPath":`より安全な選択肢: 代わりに OpenCodex で API キーを設定してください。`,"oauthTos.acknowledge":`リスクを理解した上で、OAuth を続行します。`,"oauthTos.continue":`OAuth で続行`,"prov.logoutOk":`{provider} からログアウトしました。`,"prov.logoutFail":`{provider} からログアウトできませんでした。アカウント状態は変更されていません。`,"prov.removed":`"{name}" を削除しました。`,"prov.removedDefault":`"{name}" を削除しました。既定のプロバイダーは "{defaultProvider}" になりました。`,"prov.removeFail":`"{name}" の削除に失敗しました。`,"prov.removeLastProvider":`このプロバイダーは、他に有効なプロバイダーを既定にできない場合は削除できません。`,"prov.removeHasDependentCombos":`先に依存するコンボを削除または更新してください: {combos}。`,"prov.setDefault":`既定に設定`,"prov.setDefaultSuccess":`"{name}" を既定のプロバイダーに設定しました。`,"prov.setDefaultFail":`"{name}" を既定のプロバイダーに設定できませんでした。`,"prov.defaultDisabled":`既定に設定する前に、このプロバイダーを有効にしてください。`,"prov.updateFail":`このプロバイダーを更新できませんでした。`,"prov.networkError":`ネットワークエラーです。プロキシが実行中であることを確認して、もう一度試してください。`,"prov.added":`"{name}" を追加しました。即時反映 — {cmd} を実行(または再起動)して Codex のピッカーにモデルを一覧表示します。`,"prov.removeConfirm":`プロバイダー "{name}" を削除しますか? そのモデルは Codex のピッカーから消えます。`,"prov.hasApiKey":`API キー設定済み`,"prov.hasHeaders":`カスタムヘッダー設定済み`,"prov.accounts":`アカウント ({n})`,"prov.accountsAria":`{name} のアカウントを切り替え`,"prov.accountActive":`アクティブ`,"prov.accountReauth":`再ログイン`,"prov.reauthenticate":`再認証`,"prov.reauthAccountMissing":`ログイン後に選択されたアカウントが見つかりませんでした`,"prov.reauthIdentityMismatch":`サインインしたアカウントが選択したアカウントと一致しませんでした`,"prov.accountAdd":`アカウントを追加`,"prov.accountNoLabel":`アカウント {id}`,"prov.accountSwitchTitle":`このアカウントを使用`,"prov.accountSwitched":`{email} に切り替えました。`,"prov.accountSwitchFail":`アカウントの切り替えに失敗しました`,"prov.accountRemoved":`{email} を削除しました。`,"prov.accountRemoveFail":`{email} を削除できませんでした。アカウントは変更されていません。`,"prov.accountRemoveAria":`{email} を削除`,"prov.accountRemoveConfirm":`アカウント {email} を削除しますか? そのログインはこのプロキシから削除されます。`,"prov.keyAdd":`API キーを追加`,"prov.keyAdded":`{name} に API キーを追加しました。`,"prov.keyAddFail":`API キーの追加に失敗しました`,"prov.keyPlaceholder":`API キーを貼り付け`,"prov.keySwitchTitle":`このキーを使用`,"prov.keySwitched":`キー {key} に切り替えました。`,"prov.keySwitchFail":`キーの切り替えに失敗しました`,"prov.keyRemoved":`キー {key} を削除しました。`,"prov.keyRemoveAria":`キー {key} を削除`,"prov.keyRemoveConfirm":`API キー {key} を削除しますか? このプロキシの設定から削除されます。`,"prov.activeBadge":`アクティブ`,"prov.disabledBadge":`無効`,"prov.defaultBadge":`デフォルト`,"prov.enable":`有効化`,"prov.disable":`無効化`,"prov.enabled":`"{name}" を有効にしました。そのモデルは再び Codex に表示できます。`,"prov.disabled":`"{name}" を無効にしました。設定は保持されますが、モデルは非表示になります。`,"prov.enableFail":`"{name}" の有効化に失敗しました。`,"prov.disableFail":`"{name}" の無効化に失敗しました。`,"prov.enableAria":`プロバイダー {name} を有効化`,"prov.disableAria":`プロバイダー {name} を無効化`,"prov.defaultCannotDisable":`デフォルトプロバイダーは無効化できません`,"prov.openaiAccountMode":`Codex アカウントモード`,"prov.openaiModePool":`プール`,"prov.openaiModeDirect":`ダイレクト`,"prov.openaiPoolDesc":`デフォルト。アフィニティ、クォータ、クールダウン、フェイルオーバーを使ってメインログインと追加アカウントをローテーションします。`,"prov.openaiDirectDesc":`現在/メインの Codex ログインのみを使用します。保存されたプールアカウントは読み込まれずローテーションもされません。`,"prov.openaiModeSaved":`OpenAI アカウントモードを {mode} に変更しました。`,"prov.openaiModeSaveFailed":`OpenAI アカウントモードを変更できませんでした。`,"prov.openaiApiDesc":`OpenAI API キーを使用し、Codex アカウントの資格情報は使用しません。`,"prov.manageCodexAccounts":`Codex アカウントを管理`,"prov.openaiApiMissing":`API キーが必要です`,"prov.openaiApiSetup":`API キーを設定`,"models.tab.catalog":`モデル`,"models.tab.combos":`コンボ`,"models.tab.compatibility":`互換性`,"models.tab.routing":`ルーティング (beta)`,"models.tabsLabel":`モデルサーフェス`,"models.subtitle.combos":`複数のモデルを 1 つの id にまとめ、順に応答させます。failover でターゲットを連鎖させるか、分散戦略で負荷を分散します。`,"models.subtitle.compatibility":`ラボ投影証拠の読み取り専用互換性判定マトリクス。`,"models.subtitle.routing":`ポリシープロファイル、dry-run 評価、そして根拠の残るルーティング分析です。`,"models.subtitle":`Codex に表示するモデルを切り替えます — ネイティブ GPT パススルーとルーティングプロバイダー、プロバイダー別(ヘッダーをクリックで折りたたみ)。非表示モデルはカタログとピッカーから外れますが、正確な id での直接呼び出しは可能です。変更は次回の Codex ターンで適用 — opencodex は Codex の 5 分間モデルキャッシュを無効化するので再起動は不要です。`,"models.nativeGroupLabel":`OpenAI ネイティブ`,"models.nativeHint":"パススルーモデルはプロバイダーで選択したプールまたはダイレクトアカウントオプションを使用します。一つオフにすると Codex ピッカーから隠します(カタログエントリは保持されるので、再有効化で正確に復元されます)。 ここでモデルを追加すると、bare passthrough id ではなくルーティングされた `openai/` セレクタとして登録されます。","models.active":`{active}/{total} 表示中`,"models.workspace.providers":`プロバイダー`,"models.workspace.allProviders":`すべてのプロバイダー`,"models.workspace.mainAria":`モデルの詳細`,"models.allOn":`すべてオン`,"models.allOff":`すべてオフ`,"models.presetLabel":`モデル`,"models.presetMode_preset":`プリセット`,"models.presetMode_all":`すべて`,"models.presetMode_custom":`カスタム`,"models.presetSummary":`{total} 件中 {count} 件を表示 — コアプリセット v{version}`,"models.presetUpdateAvailable":`プリセット v{version} が利用可能`,"models.presetAppliedToast":`{provider}: プリセットを適用 — {count} 件を選択`,"models.presetClearedToast":`{provider}: すべてのモデルを表示`,"models.presetEmpty":`{provider}: プリセットに一致するモデルがないため選択は変更していません`,"models.presetConfirmReplace":`選択中の一覧を {count} 件のプリセットで置き換えますか?`,"models.cap350k":`350k 上限`,"models.capApplied":`コンテキスト上限を適用しました — 次回の Codex ターンで有効になります。`,"models.capSaveFailed":`コンテキスト上限の保存に失敗しました`,"models.contextCapped":`350k 上限`,"models.contextCapLabel":`デフォルトウィンドウ / 上限`,"models.v2Label":`サブエージェント`,"models.shadowCallOriginal":`⚠ {models} →`,"models.v2DocsLink":`v1 / v2 とは?`,"models.v2Mode_v1":`v1`,"models.v2Mode_default":`ベース`,"models.v2Mode_v2":`v2`,"models.v2ModeDesc_v1":`すべてのモデル → v1 サーフェス`,"models.v2ModeDesc_default":`上流のデフォルト(sol/terra=v2、luna=v1)`,"models.v2ModeDesc_v2":`すべてのモデル → v2 サーフェス`,"models.keepNativeOnV1":`ChatGPT を v1 のまま`,"models.keepNativeOnV1Hint":`ChatGPT ネイティブの親は v2 子タスクを暗号化するため、Grok や Claude は読めません。Sol/Terra から routed モデルを spawn するならオンのまま。routed 親は v2 のままです。`,"models.v2Help":`すべてのモデルのマルチエージェントサーフェスを制御します。 - -v1: クラシックな単一スレッドエージェント。すべてのモデルが v1 コラボサーフェスを使います。 -ベース: 上流のデフォルト — sol/terra は v2、luna は v1、それ以外は codex のフィーチャーフラグに従います。 -v2: spawn_agent を備えたマルチスレッドエージェント。すべてのモデルが v2 コラボサーフェスを使います。 - -v2 では「ChatGPT を v1 のまま」にすると Sol/Terra が v1 に留まり、Grok や Claude を spawn できます。ChatGPT は v2 子タスクを暗号化するため routed モデルは読めません。routed 親は v2 のままです。 - -変更は新規セッションに適用されます。`,"dash.multiAgent":`サブエージェント`,"models.v2Conflict":`[agents] max_threads が設定されています — codex は起動を拒否します; config.toml から削除してください`,"models.v2Applied":`サブエージェントモードを更新しました — 新規セッションに適用(ピッカーを更新するには Codex アプリを再起動)`,"models.v2ThreadsLabel":`最大スレッド数`,"models.v2ThreadsDefault":`デフォルト (4)`,"models.v2ThreadsApplied":`スレッド上限を更新しました — 新規セッションに適用`,"models.v2ThreadsInvalid":`スレッド上限は 1 以上の整数にしてください`,"models.v2ThreadsApply":`適用`,"models.capValue":`デフォルト {value}`,"models.contextSettings":`カスタムウィンドウ`,"models.contextSettingsTitle":`カスタムウィンドウ — {provider}`,"models.contextDefault":`プロバイダーのデフォルト`,"models.contextModel":`モデル`,"models.contextModelOverride":`モデル別の上書き`,"models.contextHint":`すでに分かっている場合は、ここに Codex の実際のコンテキストウィンドウを書きます。上流の値がないときはこの値を使い、より大きな報告値だけ下げ、より小さな上流のコンテキストウィンドウはそのまま残します。空欄ならプロバイダーの「デフォルトウィンドウ / 上限」を使い、その上限がオフなら 128k です。`,"models.contextAutomatic":`自動検出`,"models.contextSaved":`コンテキストウィンドウを更新しました — 次回の Codex ターンから有効です。`,"models.contextUnchanged":`保存するコンテキストウィンドウの変更はありません。`,"models.contextSaveFailed":`コンテキストウィンドウを保存できませんでした`,"models.contextInvalid":`コンテキストウィンドウは正の整数で指定してください`,"models.contextCappedValue":`{value} 上限`,"models.setAll":`すべて設定`,"models.setAllHint":`すべてのルーティング済みプロバイダーに {value} のデフォルトウィンドウをオンにします。中継が context_window / context_length を返さない場合、この値が実際の Codex ウィンドウになります。1 モデルだけ手で書くときは同じ行の「カスタムウィンドウ」を使います。ネイティブプロバイダーには影響しません。`,"models.collapseAll":`すべて折りたたむ`,"models.expandAll":`すべて展開`,"models.orderHint":`ピッカーの順序: サブエージェントの選択(選択順) → 残りのルーティングモデルはプロバイダー別、次にモデル ID 別のアルファベット順 → ネイティブモデル。表示切り替えはモデルをフィルタするだけで、この順序は変更しません。`,"models.custom":`カスタム…`,"models.customApply":`適用`,"models.customPlaceholder":`トークン (例: 420000)`,"models.applied":`適用しました — 次回の Codex ターンで有効になります。`,"models.saveFailed":`保存に失敗しました`,"models.networkError":`ネットワークエラー — プロキシは起動していますか?`,"models.loadFail":`モデルの読み込みに失敗しました — プロキシは起動していますか?`,"models.noRouted":`ルーティングモデルがありません`,"models.noRoutedHint":`まずプロバイダーにログインするか追加してください。`,"models.emptyDiscovery":`モデルが見つかりませんでした。プロバイダーのエンドポイントを確認するか、静的/カスタムモデルを追加してください。`,"models.emptyDiscoveryDisabled":`ライブモデル検出がオフで、静的モデルも設定されていません。`,"models.discoveryFailedBadge":`検出に失敗`,"models.discoveryFailedHttp":`モデル検出に失敗しました(HTTP {status})。`,"models.discoveryFailedBlocked":`モデル検出は宛先ポリシーによりブロックされました。`,"models.discoveryFailedInvalidResponse":`モデル検出が無効な応答を返しました。`,"models.discoveryFailedNetwork":`ネットワークエラーによりモデル検出に失敗しました。`,"models.discoveryFailedProvider":`プロバイダーがモデル検出エラーを報告しました。`,"models.discoveryFailedGeneric":`モデル検出に失敗しました。`,"models.openProviderSettings":`プロバイダー設定を開く`,"models.loading":`読み込み中…`,"models.search":`モデルを検索…`,"models.showMore":`さらに {n} 件表示`,"models.allowlistLabel":`選択のみ`,"models.allowlistHint":`チェックしたモデルのみカタログに送信します(空 = すべて)。数千のモデルを公開するプロバイダーで有用です。`,"models.selectedCount":`{n} 件選択`,"sub.subtitle":`Codex の {cmd} は最初の 5 モデル(優先度順)のみをオーバーライドとして通知します。ここで最大 5 つを選んでください — ネイティブ gpt またはルーティング — opencodex がカタログ優先度を設定し、これらが先頭に来るようにします。他のモデルも正確な名前で呼び出し可能です; これは表示のみを制御します。`,"sub.featured":`おすすめ`,"sub.advanced":`詳細設定`,"sub.orderHintAria":`この順序の使われ方`,"sub.orderHint":`ここでの表示順が Codex モデルピッカーの上位 1〜5 番目の位置と {cmd} のデフォルトモデル候補を決定します。`,"sub.noneSelected":`未選択 — 以下のリストから選んでください。`,"sub.models":`モデル`,"sub.search":`モデルを検索(ネイティブ gpt + ルーティング)…`,"sub.noModels":`モデルがありません — まずプロバイダーにログインするか追加してください。`,"sub.saved":`{n} 件のモデルを保存しました。新規 Codex セッションを開始(または {cmd} を実行)して spawn_agent オーバーライドとして確認してください。`,"sub.saveFailed":`保存に失敗しました`,"sub.networkError":`ネットワークエラー — プロキシは起動していますか?`,"sub.loadFail":`モデルの読み込みに失敗しました — プロキシは起動していますか?`,"sub.loading":`読み込み中…`,"sub.moveUp":`{m} を上へ移動`,"sub.moveDown":`{m} を下へ移動`,"sub.removeAria":`{m} を削除`,"sub.workspace.addToFeatured":`{m} をおすすめに追加`,"sub.workspace.allModels":`すべてのモデル`,"sub.workspace.featuredFull":`おすすめリストがいっぱいです(最大 5)`,"sub.workspace.mainAria":`サブエージェントのモデル詳細`,"sub.workspace.notFeatured":`おすすめ未設定`,"sub.workspace.priority":`優先度`,"sub.workspace.removeFromFeatured":`{m} をおすすめから削除`,"sub.workspace.selectModel":`モデルを選択`,"sub.workspace.selectModelDesc":`一覧からモデルを選んで詳細を確認し、spawn_agent のおすすめに設定します。`,"sub.workspace.selector":`公開セレクター`,"sub.ultraMode":`ウルトラモード`,"sub.ultraModeHint":`すべてのモデルと reasoning effort で Proactive マルチエージェント委任ポリシーを有効にします(reasoning effort 自体は変更しません)。config.toml に features.multi_agent_v2.multi_agent_mode_hint_text を書き込みます。`,"sub.ultraModeV2Required":`v2 マルチエージェントサーフェスが必要です — 先に multi_agent_v2 を有効にし、サブエージェントモードで v2 を選択してください。`,"sub.ultraModeText":`ウルトラモード委任テキスト`,"sub.ultraModePreset":`プリセットを復元`,"sub.ultraModeLoadFail":`ウルトラモード設定を読み込めませんでした — プロキシは実行中ですか?`,"sub.ultraModeSaveFail":`ウルトラモード設定の保存に失敗しました`,"sub.ultraModeSaved":`ウルトラモードを保存しました。新しい Codex セッションから適用されます。`,"logs.title":`リクエストログ`,"logs.tabLogs":`ログ`,"logs.tabDebug":`デバッグ`,"logs.subtitle":`ローカル opencodex プロキシを経由した最近のリクエスト(新しい順)。`,"logs.autoRefresh":`自動更新`,"logs.noRequests":`まだリクエストがありません。`,"logs.loadError":`リクエストログを読み込めませんでした。`,"logs.filter.surface.label":`サーフェス`,"logs.filter.surface.all":`すべて`,"logs.filter.surface.claude":`Claude`,"logs.filter.surface.codex":`Codex`,"logs.filter.surface.grok":`Grok`,"logs.filter.interceptedHelpersOnly":`インターセプトされたヘルパーのみ`,"logs.badge.interceptedHelper":`I · {model}`,"logs.badge.interceptedHelperTitle":`インターセプトされたヘルパー要求`,"logs.filter.conversation.label":`会話`,"logs.filter.conversation.placeholder":`会話 ID を貼り付け`,"logs.filter.conversation.clear":`クリア`,"logs.filter.model.label":`モデル`,"logs.filter.model.placeholder":`モデルまたはプロバイダーで絞り込む`,"logs.filter.conversation.apply":`ログを絞り込み`,"logs.conversation.totals":`{requests} 件 · {tokens} トークン · {cost}`,"logs.conversation.scope":`合計は現在読み込まれている Logs リングのみです。`,"logs.conversation.excluded":`(~$ から価格なし {unpriced} / 未計測 {unmetered} を除外)`,"logs.cost.approximate":`{amount}`,"logs.cost.lowerBound":`≥{amount}`,"logs.cost.unavailable":`利用不可`,"logs.detail.conversation":`会話`,"logs.badge.claude":`Claude`,"logs.badge.grok":`Grok`,"logs.col.time":`時刻`,"logs.col.request":`リクエスト`,"logs.col.model":`モデル`,"logs.col.effort":`負荷`,"logs.col.provider":`プロバイダー`,"logs.col.status":`状態`,"logs.col.tokens":`トークン`,"logs.col.tokPerSec":`tok/s`,"logs.col.estimatedCost":`~$`,"logs.metric.tokPerSecTitle":`リクエスト全体の所要時間あたりの出力トークン数`,"logs.metric.estimatedCostTitle":`API 定価相当額(実際の請求ではありません); 未対応の価格は利用できません`,"usage.cost.total":`API 定価相当額(この期間)`,"usage.cost.disclaimer":`請求明細ではありません。サブスクリプション利用量やプロバイダークレジットが代わりに適用される場合があります。`,"usage.cost.unpricedNote":`{count} 件のリクエストを除外(価格または使用量なし)`,"logs.detail.section.basic":`基本情報`,"logs.detail.route.section":`ルート決定`,"logs.detail.route.kind":`ルート種別`,"logs.detail.route.profile":`プロファイル`,"logs.detail.route.selected":`選択済み`,"logs.detail.route.candidates":`候補`,"logs.detail.route.unknown":`このリクエストにはルートトレースが記録されていません(トレース前の行)。`,"logs.detail.section.performance":`パフォーマンス`,"logs.detail.section.cost":`API 定価相当額`,"logs.detail.section.attempts":`コンボの試行`,"logs.detail.section.usage":`生の使用量`,"logs.detail.ttft":`TTFT`,"logs.detail.costTotal":`定価相当額`,"logs.detail.totalTokens":`合計トークン`,"logs.detail.matchedKey":`一致した価格キー`,"logs.detail.priceSource":`価格ソース`,"logs.detail.unavailableReason":`利用不可の理由`,"logs.detail.copyRequestId":`リクエスト ID をコピー`,"logs.detail.copied":`コピーしました`,"logs.detail.source.jawcode":`jawcode カタログ`,"logs.detail.source.expected":`予想価格オーバーレイ`,"logs.detail.source.user":`プロバイダー設定の価格オーバーレイ`,"logs.detail.verification.verified":`検証済み`,"logs.detail.verification.derived":`ベースモデルから派生`,"logs.detail.attempt.target":`プロバイダー / モデル`,"logs.detail.attempt.reason":`結果 / 理由`,"logs.detail.attempt.completed":`完了`,"logs.detail.attempt.e2eNote":`トップレベルの tok/s はエンドツーエンドです; 各試行は自身の所要時間を使います。`,"logs.detail.attempt.recovery.transient5xx":`一時的な5xxエラー`,"logs.detail.attempt.recovery.connectionReset":`接続がリセットされました`,"logs.detail.attempt.recovery.oauth401":`OAuth 再認証`,"logs.detail.attempt.recovery.key429":`キーがレート制限 (429)`,"logs.detail.attempt.recovery.rateLimit429":`レート制限 (429)`,"logs.detail.attempt.recovery.anthropicOauth429":`Anthropic OAuth レート制限 (429)`,"logs.detail.attempt.recovery.image413":`画像ペイロードが大きすぎます (413)`,"logs.detail.attempt.recovery.emptyCompletion":`空の完了を再試行`,"logs.detail.attempt.recovery.unknown":`不明なリカバリ理由`,"logs.detail.reason.usage_missing":`使用量が報告されませんでした。`,"logs.detail.reason.usage_unsupported":`このプロバイダーは使用量を報告しません。`,"logs.detail.reason.output_missing":`正の出力トークン数が報告されませんでした。`,"logs.detail.reason.invalid_duration":`リクエストの所要時間が有効ではありません。`,"logs.detail.reason.price_unmatched":`一致する価格が見つかりませんでした。`,"logs.detail.reason.invalid_cache_breakdown":`キャッシュトークンの詳細が合計入力トークンと矛盾しています。`,"logs.detail.reason.invalid_usage":`使用量に無効なトークン値が含まれています。`,"logs.detail.reason.combo_attempt_unavailable":`少なくとも 1 つのコンボ試行に価格を設定できませんでした。`,"logs.detail.estimate.usage_estimated":`プロバイダーの使用量は推定です。`,"logs.detail.estimate.cache_detail_missing":`キャッシュの詳細が利用できませんでした; 入力は上限の推定です。`,"logs.detail.estimate.expected_price_overlay":`検証済みの予想定価が使用されました。`,"logs.detail.estimate.provider_cost_overlay":`プロバイダー設定の価格オーバーレイが使用されました。`,"logs.detail.estimate.priority_lower_bound":`確認済みの Priority 価格を利用できないため、表示される見積もりは既知の下限です。`,"logs.col.error":`エラー`,"logs.col.upstreamReason":`上流の理由`,"logs.col.duration":`所要時間`,"logs.modelTooltip.model":`モデル`,"logs.modelTooltip.resolvedModel":`解決後モデル`,"logs.modelTooltip.requestedTier":`要求ティア`,"logs.modelTooltip.configuredTier":`設定ティア`,"logs.modelTooltip.responseTier":`応答ティア`,"logs.modelTooltip.supportsTier":`ティア対応`,"logs.tokens.reported":`報告済み`,"logs.tokens.unreported":`未報告`,"logs.tokens.unsupported":`非対応`,"logs.tokens.estimated":`推定`,"logs.tokens.input":`入力`,"logs.tokens.output":`出力`,"logs.tokens.cacheRead":`キャッシュ読み取り (c)`,"logs.tokens.cacheWrite":`キャッシュ書き込み (w)`,"logs.tokens.reasoning":`推論`,"logs.tokens.noCache":`キャッシュデータなし`,"logs.tokens.contextTotal":`アクティブコンテキスト`,"logs.tokens.noCacheNote":`このプロバイダーはキャッシュトークンを報告しません`,"logs.tokens.noCacheCursor":`Cursor のキャッシュ詳細は未報告`,"logs.tokens.noCacheCursorNote":`Cursor はキャッシュ read/write トークン数を公開しません。これは不明という意味で、キャッシュミスの確定ではありません`,"logs.tokens.estimatedNote":`推定(プロバイダーは正確な使用量を報告しません)`,"logs.details":`詳細`,"logs.detailTitle":`リクエストの詳細`,"logs.detailRaw":`生のログエントリ`,"debug.title":`デバッグ`,"debug.subtitle":`オプトインのプロバイダートランスポートおよび使用量抽出診断です。リクエストエラーと 502 はログタブに残ります。`,"debug.debug":`プロバイダーデバッグ`,"debug.usage":`使用量抽出`,"debug.injection":`インジェクションログ`,"debug.claude":`Claude インバウンド`,"debug.claudeInbound.title":`Claude インバウンドリクエスト`,"debug.claudeInbound.sub":`Claude Code/Desktop が実際に送信する内容(thinking、effort、metadata) — プロンプトテキストは保存されません。`,"debug.claudeInbound.empty":`まだキャプチャされたリクエストはありません。これがオンの状態で Claude からメッセージを送信してください。`,"debug.claudeInbound.time":`時刻`,"debug.claudeInbound.endpoint":`エンドポイント`,"debug.claudeInbound.model":`モデル`,"debug.claudeInbound.none":`なし`,"debug.reset":`ランタイムオーバーライドをクリア`,"debug.refresh":`更新`,"debug.follow":`追従`,"debug.streamProvider":`プロバイダー`,"debug.streamUsage":`使用量`,"debug.streamInjection":`インジェクション`,"debug.loading":`デバッグ設定を読み込み中…`,"debug.loadFailed":`デバッグ設定を読み込めませんでした。`,"debug.emptyTitle":`デバッグログはオフです`,"debug.empty":`上のカードでプロバイダーデバッグまたは使用量抽出をオンにしてください。プロキシ経由でリクエストを送信すると、ここに行が表示されます。`,"debug.noLinesTitle":`行を待機中`,"debug.noLines.provider":`プロバイダーデバッグはオンですが、トランスポートの異常(欠落または不正なフレーム、Cursor のダイヤル/再試行イベント)のみを記録します。Anthropic のようなプロバイダーでの正常なリクエストは行を生成しないことがあります。`,"debug.noLines.usage":`使用量抽出はオンですが、まだ何もキャプチャされていません。Codex 経由でチャット/リクエストを送信するとここに表示されます。`,"debug.noLines.injection":`インジェクションログはオンですが、まだ何もキャプチャされていません。コラボおよびサブエージェントのターンでのマルチエージェントガイダンスインジェクションと負荷上限の決定を記録します。`,"usage.title":`使用量`,"usage.subtitle":`プロキシからのローカルトークン会計です。欠損した使用量はゼロとして表示されることはありません。`,"usage.loading":`使用量データを読み込み中…`,"usage.empty":`まだ使用量が記録されていません。プロキシ経由でリクエストを送信するとここにアクティビティが表示されます。`,"usage.loadError":`使用量データを読み込めませんでした。`,"usage.range.all":`すべて`,"usage.range.available":`利用可能な履歴`,"usage.historyTruncated":`古い利用履歴が読み込まれていないため、合計は利用可能な履歴のみを対象とします。`,"usage.historyTruncatedWindow":`読み込まれた行のリクエスト開始時刻は {start} から {end} の範囲です。読み取り上限によりファイル前方の記録が除外されているため、選択した期間は不完全な場合があります。`,"usage.range.30d":`30日`,"usage.range.7d":`7日`,"usage.card.requests":`リクエスト`,"usage.card.measured":`計測`,"usage.card.reported":`報告`,"usage.card.totalTokens":`合計トークン`,"usage.card.cachedTokens":`キャッシュ読み取り`,"usage.card.cachedTokensHint":`プロバイダーキャッシュから提供されたプロンプトトークン(読み取り)。キャッシュ書き込みは存在する場合、下に表示されます。`,"usage.card.cacheWriteTokens":`キャッシュ書き込み`,"usage.card.coverage":`カバレッジ`,"usage.card.activeDays":`アクティブ日数`,"usage.section.heatmap":`日のアクティビティ`,"usage.section.overview":`概要`,"usage.section.models":`モデル`,"usage.section.providers":`プロバイダー`,"usage.section.coverage":`カバレッジ内訳`,"usage.workspace.report":`使用量レポート`,"usage.workspace.sections":`使用量セクション`,"usage.coverage.measured":`計測`,"usage.coverage.reported":`プロバイダー報告`,"usage.coverage.estimated":`推定`,"usage.coverage.note":`計測エントリにはプロバイダー報告および推定のトークン数が含まれます。未報告および非対応のリクエストは追跡されますが、ゼロトークンに水増しされることはありません。`,"usage.search.models":`モデルを検索…`,"usage.col.requests":`リクエスト`,"usage.col.measured":`計測`,"usage.col.reported":`報告`,"usage.col.tokens":`トークン`,"usage.col.share":`割合`,"usage.heatmap.less":`少ない`,"usage.heatmap.more":`多い`,"usage.dayMon":`月`,"usage.dayWed":`水`,"usage.dayFri":`金`,"usage.heatmap.tooltipTokens":`{tokens} トークン`,"usage.heatmap.tooltipRequests":`{requests} リクエスト`,"nav.storage":`ストレージ`,"storage.title":`ストレージ`,"storage.subtitle":`CODEX_HOME の使用状況を確認。クリーンアップはアクティブセッションに触れません。`,"storage.loading":`ストレージをスキャン中…`,"storage.empty":`CODEX_HOME が空か存在しません — 報告するものはありません。`,"storage.error":`ストレージのスキャンに失敗しました。CODEX_HOME が有効なディレクトリを指しているか確認してください。`,"storage.refresh":`再スキャン`,"storage.rescanned":`スキャンが完了しました。`,"storage.card.total":`合計サイズ`,"storage.card.files":`ファイル`,"storage.card.home":`CODEX_HOME`,"storage.snapshot.lastScan":`最終スキャン`,"storage.snapshot.scanning":`スキャン中…`,"storage.snapshot.unavailable":`まだスキャンがありません。`,"storage.cleanupCard.title":`容量を空ける`,"storage.cleanupCard.tabs":`クリーンアップオプション`,"storage.cleanupCard.tab.policy":`ポリシー`,"storage.cleanupCard.tab.quarantine":`隔離`,"storage.cleanup.noArchives":`クリーンアップ対象のアーカイブセッションはありません。`,"storage.section.buckets":`バケット`,"storage.section.largest":`最大ファイル`,"storage.workspace.overview":`概要`,"storage.workspace.selectBucket":`一覧からバケットを選ぶと内訳が表示されます。`,"storage.col.bucket":`バケット`,"storage.col.size":`サイズ`,"storage.col.files":`ファイル`,"storage.col.oldest":`最古`,"storage.col.newest":`最新`,"storage.col.rows":`DB 行`,"storage.rows.unknown":`不明(ロック中)`,"storage.bucket.sessions":`アクティブセッション`,"storage.bucket.archived_sessions":`アーカイブ済みセッション`,"storage.bucket.logs_db":`ログデータベース`,"storage.bucket.state_db":`状態データベース`,"storage.bucket.attachments":`添付`,"storage.bucket.deletion_manifests":`削除マニフェスト`,"storage.bucket.other":`その他`,"storage.cleanup.title":`アーカイブのクリーンアップ`,"storage.cleanup.help":`古いアーカイブセッションを割合で削除します。アクティブセッションには触れません。既定は隔離で、ファイルは CODEX_HOME/.trash へ移動します。`,"storage.cleanup.slider":`古いアーカイブの割合`,"storage.cleanup.percent":`{percent}%`,"storage.cleanup.preset":`{percent}`,"storage.cleanup.preview":`プレビュー`,"storage.cleanup.confirmTitle":`アーカイブクリーンアップの確認`,"storage.cleanup.confirmBody":`アーカイブ {count} 件(約 {size})、古い {percent}% を処理します。`,"storage.cleanup.moreFiles":`…ほか {n} 件`,"storage.cleanup.permanent":`完全に削除する(隔離しない)`,"storage.cleanup.permanentWarn":`完全削除は元に戻せません。`,"storage.cleanup.quarantineNote":`ファイルは CODEX_HOME 下の .trash へ移動します。隔離タブから復元できます。`,"storage.cleanup.cancel":`キャンセル`,"storage.cleanup.confirmQuarantine":`隔離する`,"storage.cleanup.confirmPermanent":`完全に削除`,"storage.cleanup.doneQuarantine":`{count} 件を隔離しました({size})。`,"storage.cleanup.donePermanent":`{count} 件を完全削除しました({size})。`,"storage.cleanup.previewFailed":`プレビューに失敗しました。`,"storage.cleanup.cleanupFailed":`クリーンアップに失敗しました。`,"storage.cleanup.err.codex_busy":`Codex が state.sqlite を使用中です — Codex を終了して再試行してください。`,"storage.cleanup.err.stale_preview":`プレビュー以降にアーカイブが変わりました — プレビューをやり直してください。`,"storage.cleanup.err.restore_pending_overlap":`選択したアーカイブは未完了の隔離復元と重なっています — 復元を完了するか再試行してください。`,"storage.cleanup.err.referenced_history":`選択したアーカイブはフォークまたはページング履歴から参照されています。`,"storage.cleanup.err.invalid_digest":`プレビューのダイジェストが無い、または無効です。`,"storage.cleanup.err.invalid_mode":`モードは quarantine または permanent である必要があります。`,"storage.cleanup.err.fs_failed":`ファイルシステムのクリーンアップに失敗しました。一部の変更は既に適用されている可能性があります — CODEX_HOME/.trash と表示されたリカバリパスを確認してください。`,"storage.cleanup.err.fs_failed_trash":`ファイルシステムのクリーンアップに失敗しました。一部の変更は既に適用されている可能性があります — {trashDir} と manifest.json で復旧可能なファイルを確認してください。`,"storage.cleanup.err.db_reconcile_failed":`Codex の状態データベースを更新できませんでした。`,"storage.cleanup.err.cleanup_failed":`クリーンアップに失敗しました。`,"storage.trash.title":`隔離`,"storage.trash.help":`CODEX_HOME/.trash へ移したアーカイブセッションです。復元すると JSONL とスレッド行が戻ります。`,"storage.trash.empty":`隔離エントリはありません。`,"storage.trash.loading":`隔離を読み込み中…`,"storage.trash.col.when":`隔離日時`,"storage.trash.col.files":`ファイル`,"storage.trash.col.size":`サイズ`,"storage.trash.col.mode":`モード`,"storage.trash.col.id":`エントリ`,"storage.trash.restore":`復元`,"storage.trash.confirmTitle":`隔離エントリを復元しますか?`,"storage.trash.confirmBody":`{id} から {count} 件(約 {size})をアーカイブセッションへ戻します。`,"storage.trash.cancel":`キャンセル`,"storage.trash.confirmRestore":`復元`,"storage.trash.done":`{count} 件を復元しました({size})。`,"storage.trash.restoreFailed":`復元に失敗しました。`,"storage.trash.listFailed":`隔離一覧を取得できませんでした。`,"storage.trash.mode.quarantine":`隔離`,"storage.trash.mode.permanent":`完全削除(未完了)`,"storage.trash.err.codex_busy":`Codex が state.sqlite を使用中です — Codex を終了して再試行してください。`,"storage.trash.err.invalid_trash":`隔離エントリ ID が無い、または無効です。`,"storage.trash.err.missing_trash":`隔離エントリが見つかりません。`,"storage.trash.err.dest_exists":`復元先が既に存在します — アーカイブファイルを削除または改名して再試行してください。`,"storage.trash.err.fs_failed":`ファイルシステムの復元に失敗しました。一部は既に復元されている可能性があります — archived_sessions と .trash を確認してください。`,"storage.trash.err.storage_mutation_busy":`別のストレージクリーンアップまたは復元が進行中です — しばらくして再試行してください。`,"storage.trash.err.db_reconcile_failed":`Codex の状態データベース行を復元できませんでした。`,"storage.trash.err.restore_failed":`復元に失敗しました。`,"storage.trash.err.restore_worker_timeout":`復元が長時間(10 分超)かかったため停止しました。`,"storage.trash.err.restore_worker_aborted":`シャットダウン中に復元がキャンセルされました。`,"storage.trash.err.restore_worker_failed":`復元ワーカーがクラッシュまたは予期しないエラーで失敗しました。`,"storage.policy.title":`自動クリーンアップ方針`,"storage.policy.help":`アーカイブがしきい値を超えたときの任意の一括クリーンアップ。既定はオフ — 自動では有効になりません。`,"storage.policy.loading":`方針を読み込み中…`,"storage.policy.loadFailed":`クリーンアップ方針を読み込めませんでした。`,"storage.policy.saveFailed":`クリーンアップ方針を保存できませんでした。`,"storage.policy.runFailed":`方針の実行に失敗しました。`,"storage.policy.alreadyRunning":`クリーンアップ方針の実行が既に進行中です。`,"storage.policy.invalid":`方針の値が無効です。`,"storage.policy.enabled":`自動クリーンアップを有効化`,"storage.policy.enabledHint":`既定はオフです。有効にすると選択したスケジュール(または今すぐ実行)でのみ動きます。`,"storage.policy.threshold":`アーカイブサイズが超えたら(GiB)`,"storage.policy.trigger":`トリガー`,"storage.policy.target":`クリーンアップ目標`,"storage.policy.targetPercent":`古いアーカイブを削除(%)`,"storage.policy.targetReduce":`アーカイブを次のサイズまで縮小(GiB)`,"storage.policy.thresholdInc":`しきい値を上げる`,"storage.policy.thresholdDec":`しきい値を下げる`,"storage.policy.percentInc":`パーセントを上げる`,"storage.policy.percentDec":`パーセントを下げる`,"storage.policy.reduceInc":`削減目標を上げる`,"storage.policy.reduceDec":`削減目標を下げる`,"storage.policy.schedule":`スケジュール`,"storage.policy.schedule.manual":`手動のみ`,"storage.policy.schedule.startup":`プロキシ起動時`,"storage.policy.schedule.daily":`毎日`,"storage.policy.schedule.weekly":`毎週`,"storage.policy.mode":`削除モード`,"storage.policy.mode.quarantine":`隔離(既定)`,"storage.policy.mode.permanent":`完全削除`,"storage.policy.permanentWarn":`完全削除モードは元に戻せません。確信がなければ隔離を使ってください。`,"storage.policy.lastRun":`前回の実行`,"storage.policy.lastRunDetail":`{count} 件削除 · {size} 解放`,"storage.policy.nextRun":`次回の実行`,"storage.policy.never":`なし`,"storage.policy.save":`保存`,"storage.policy.runNow":`今すぐ実行`,"storage.policy.running":`実行中…`,"storage.policy.saved":`方針を保存しました。`,"storage.policy.skippedDisabled":`方針が無効です — 先に有効化してください。`,"storage.policy.skippedUnder":`アーカイブサイズがしきい値未満です — 作業はありません。`,"storage.policy.skippedEmpty":`目標に合うアーカイブ候補がありません。`,"storage.policy.doneQuarantine":`方針が {count} 件を隔離しました({size})。`,"storage.policy.donePermanent":`方針が {count} 件を完全削除しました({size})。`,"storage.policy.metadataSaveWarning":`方針の実行は完了しましたが、スケジュールのメタデータを保存できませんでした。`,"modal.addNamed":`追加: {label}`,"modal.add":`プロバイダーを追加`,"modal.search":`プロバイダーを検索…`,"modal.logInWith":`{label} でログイン`,"modal.waitingBrowser":`ブラウザを待機中…`,"modal.providerName":`プロバイダー名`,"modal.adapter":`アダプター`,"modal.baseUrl":`ベース URL`,"modal.endpoint":`エンドポイント`,"modal.endpoint.tokenPlan":`トークンプラン`,"modal.endpoint.payAsYouGo":`従量課金`,"modal.endpoint.custom":`カスタム`,"modal.defaultModel":`デフォルトモデル(任意)`,"modal.allowPrivateNetwork":`ローカル/プライベートネットワークを許可`,"modal.allowPrivateNetworkHint":`意図的にセルフホストしたプロバイダーに対してのみ有効化してください。メタデータエンドポイントはブロックされたままです。`,"modal.nameRequired":`プロバイダー名は必須です`,"modal.baseUrlRequired":`ベース URL は必須です`,"modal.networkError":`ネットワークエラー — プロキシは起動していますか?`,"modal.loginFailStart":`ログインを開始できませんでした`,"modal.waitingLogin":`ブラウザログインを待機中…`,"modal.loggingIn":`ログイン中…`,"modal.loginTimeout":`ログインがタイムアウトしました — もう一度お試しください。`,"modal.back":`戻る`,"modal.badge.oauth":`OAuth`,"modal.customProvider":`カスタムプロバイダー`,"modal.failedStatus":`失敗 ({status})`,"modal.loginError":`ログインエラー: {error}`,"modal.badge.codexLogin":`Codex ログイン`,"modal.badge.local":`ローカル`,"modal.badge.apiKey":`API キー`,"modal.badge.direct":`ダイレクト`,"modal.badge.pool":`プール`,"modal.badge.free":`無料`,"modal.invalidPreset":`この組み込みプロバイダープリセットは不完全です。プロキシを再起動してもう一度お試しください。`,"modal.freeTierTitle":`無料枠`,"modal.freeTierDefault":`API キー不要です。そのまま利用できます。`,"modal.tab.accounts":`アカウント`,"modal.tab.free":`無料`,"modal.tab.paid":`有料`,"modal.accountsHint":`ChatGPT/Codex、OAuth プロバイダー、API キーアカウントにここからサインインします。OpenAI は組み込み済み — 再度追加せずログインしてください。`,"modal.accountsCodexAuthLink":`Codex 認証`,"modal.notListed":`プロバイダーが載っていませんか? カスタムを追加`,"modal.catalogLoading":`カタログを読み込み中…`,"modal.accountLogin":`ログイン`,"modal.accountLogout":`ログアウト`,"modal.accountAdd":`アカウントを追加`,"modal.accountManage":`管理`,"modal.accountCodexPool":`ChatGPT アカウントプール`,"modal.accountLoggedIn":`ログイン済み`,"modal.accountLoggedOut":`未ログイン`,"quota.fiveHourLimit":`5 時間上限`,"quota.ageMinutes":`{n}分`,"quota.ageHours":`{n}時間`,"quota.ageDays":`{n}日`,"quota.observedAgo":`{age}前に取得`,"quota.observedHint":`Meta はストリーミング応答中にのみ使用量を報告します。リアルタイムの値ではなく、最後に取得した値です。`,"quota.weeklyLimit":`週間上限`,"quota.monthlyLimit":`30 日上限`,"quota.cursorFirstParty":`ファーストパーティモデル`,"quota.cursorApiUsage":`API 使用量`,"quota.totalSubscriptionCredits":`サブスクリプションクレジット合計`,"quota.creditsBalance":`クレジット残高`,"quota.creditsPeriodEnds":`請求期間終了日: {date}`,"quota.usedPercent":`{pct}% 使用`,"quota.limitReached":`上限に達しました`,"quota.resetsToday":`今日 {time} にリセット`,"quota.resetsTomorrow":`明日 {time} にリセット`,"quota.resetsAt":`{when} にリセット`,"quota.resetsRelativeMinutes":`{n} 分後にリセット`,"quota.resetsRelativeHours":`{n} 時間後にリセット`,"pws.status.ready":`準備完了`,"pws.status.needsSetup":`セットアップが必要`,"pws.status.needsAttention":`要対応`,"pws.auth.chatgptPassthrough":`ChatGPT パススルー`,"pws.auth.noKey":`キー不要`,"pws.freeTitle":`無料料金(キーが必要な場合もあります)`,"pws.localTitle":`ローカルランタイム`,"pws.modelCountOne":`1 モデル`,"pws.modelCount":`{count} モデル`,"pws.rail.suffixDefault":` · デフォルト`,"pws.rail.suffixLocal":` · ローカル`,"pws.rail.suffixFree":` · 無料`,"pws.rail.selectAria":`{name} を選択 — {status}{suffix}`,"pws.searchPlaceholder":`プロバイダーを検索…`,"pws.filterAria":`プロバイダーを絞り込み`,"pws.providerFiltersAria":`プロバイダーフィルタ`,"pws.filters":`フィルタ`,"pws.filterStatus":`状態`,"pws.pricing":`料金`,"pws.paid":`有料`,"pws.filterType":`タイプ`,"pws.type.cloud":`クラウド`,"pws.type.local":`ローカル`,"pws.type.selfHosted":`セルフホスト`,"pws.type.login":`ログイン`,"pws.sort":`並べ替え`,"pws.sortProvidersAria":`プロバイダーを並べ替え`,"pws.sort.az":`A–Z`,"pws.sort.za":`Z–A`,"pws.sort.freePaid":`無料優先`,"pws.sort.paidFree":`有料優先`,"pws.sort.accountsFirst":`アカウント優先`,"pws.resetAll":`すべてリセット`,"pws.providerList":`プロバイダー一覧`,"pws.providersAria":`プロバイダー`,"pws.groupReady":`準備完了 ({count})`,"pws.groupNeedsSetup":`セットアップが必要 ({count})`,"pws.groupDisabled":`無効 ({count})`,"pws.noSearchResults":`検索に一致するプロバイダーがありません。`,"pws.noMatchFilters":`フィルタに一致するプロバイダーがありません。`,"pws.noProvidersConfigured":`プロバイダーが設定されていません。`,"pws.workspaceMainAria":`プロバイダーの詳細`,"pws.detailComingSoon":`詳細ビューは近日対応 — このプロバイダーの管理にはクラシックビューを使用してください。`,"pws.selectPrompt":`リストからプロバイダーを選択してください。`,"pws.connectFirst":`最初のプロバイダーを接続`,"pws.empty.browseFree":`無料プロバイダーを見る`,"pws.empty.browseFreeDesc":`サブスクリプションなしで始める`,"pws.empty.connectAccount":`アカウントを接続`,"pws.empty.connectAccountDesc":`ChatGPT やプロバイダーのログインを使用`,"pws.empty.addEndpoint":`エンドポイントを追加`,"pws.empty.addEndpointDesc":`カスタムベース URL と API キー`,"pws.tab.overview":`概要`,"pws.tab.models":`モデル`,"pws.tab.usage":`使用量`,"pws.tab.accounts":`アカウント`,"pws.tab.settings":`設定`,"pws.connection":`接続`,"pws.status.connected":`接続済み`,"pws.attentionTitle":`要対応`,"pws.attention.reauth":`アクティブアカウントの再認証が必要です`,"pws.attention.reauthForward":`アクティブな Codex アカウントの再認証が必要です — アカウントタブを開いて修正してください`,"pws.attention.missingCredentials":`資格情報が不足しています`,"pws.cell.auth":`認証`,"pws.cell.note":`メモ`,"pws.cell.defaultModel":`デフォルトモデル`,"pws.statsAria":`プロバイダー統計`,"pws.statsTitle":`統計`,"pws.stats.totalRequests":`リクエスト (30日)`,"pws.stats.totalTokens":`トークン (30日)`,"pws.stats.quotaUpdated":`クォータを更新しました`,"pws.stats.quotaTracked":`レート制限は使用量タブで追跡されます。`,"pws.stats.source":`ソース`,"pws.usageLast30d":`使用量 (過去30日)`,"pws.metricRequests":`リクエスト`,"pws.metricTokens":`トークン`,"pws.usageUnavailable":`まだ使用量が記録されていません。`,"pws.rateLimits":`レート制限`,"pws.quotaUnavailable":`このプロバイダーのクォータデータがありません。`,"pws.accountQuotaUnavailable":`レート制限データを一時的に取得できません。前回の値がある場合はそれを表示します。`,"pws.selected":`選択中`,"pws.copyModelId":`ID をコピー`,"pws.modelCopied":`コピーしました!`,"pws.modelsAvailable":`{count} 件利用可能`,"pws.modelSearchPlaceholder":`モデルを絞り込み…`,"pws.modelsLoading":`モデルを読み込み中…`,"pws.modelsLoadFailed":`モデルを読み込めませんでした。`,"pws.modelsNeedsReauth":`ライブモデル検出が動作するには再ログインが必要です。今は設定済みモデルを表示しています。`,"pws.modelsConfiguredFallback":`設定済みモデルを表示中(ライブ検出は利用不可)。`,"pws.modelsTruncated":`最初の {shown} / {total} モデルを表示中。リストを絞り込んでください。`,"pws.retry":`再試行`,"pws.noModels":`このプロバイダーで検出されたモデルはありません。`,"pws.noModelMatch":`フィルタに一致するモデルがありません。`,"pws.adapterBaseRequired":`アダプターとベース URL は必須です。`,"pws.addAccount":`アカウントを追加`,"pws.addKey":`API キーを追加`,"pws.apiKeys":`API キー`,"pws.authMode":`認証モード`,"pws.availableAccounts":`利用可能なアカウント`,"pws.accountOrdinal":`アカウント {count}`,"pws.accountsLoading":`アカウントを読み込み中…`,"pws.accountsLoadFailed":`アカウントを読み込めませんでした。`,"pws.retryAccounts":`再試行`,"pws.noAccounts":`まだアカウントが接続されていません。`,"pws.cockpitImportDescription":`このデバイスから Cockpit Tools Antigravity JSON エクスポートをインポートします。ファイル内容は表示されません。`,"pws.cockpitImportFileLabel":`Cockpit Tools Antigravity JSON エクスポート`,"pws.cockpitImportChooseFile":`JSON ファイルを選択`,"pws.cockpitImporting":`インポート中…`,"pws.cockpitImportInvalid":`選択したファイルは有効な JSON エクスポートではないか、大きすぎます。`,"pws.cockpitImportFailed":`アカウントのインポートを完了できませんでした。`,"pws.cockpitImportComplete":`インポート完了: インポート {imported}、更新 {updated}、失敗 {failed}、未対応 {unsupported}。`,"pws.accountSwitching":`切り替え中…`,"pws.accountCurrent":`現在のアカウント`,"pws.defaultModelNone":`なし(プロバイダーのデフォルトを使用)`,"pws.discardSettings":`破棄`,"pws.jsonEditorDesc":`生のプロバイダー JSON 設定を編集します。変更はすぐに保存されます。`,"pws.jsonEditorTitle":`JSON エディタ — {name}`,"pws.jsonRestore":`復元`,"pws.jsonSave":`保存`,"pws.loggedInTitle":`ログイン済み`,"pws.notLoggedInTitle":`未ログイン`,"pws.note":`メモ`,"pws.allowPrivateNetwork":`ローカル/プライベートネットワークを許可`,"pws.liveModels":`プロバイダーからモデルを検出`,"pws.liveModelsDesc":`プロバイダーのライブモデルカタログを取得します。オフにすると設定済みの静的モデルのみを使用します。`,"pws.xaiResponsesOptIn":`Grok 4.5 と 4.6 で Responses API を使用`,"pws.xaiResponsesOptInDesc":`両モデルを openai-responses 経由でルーティングします。他の Grok モデルと tier 動作は変わりません。`,"pws.xaiResponsesOptInMixed":`一部のみ有効です。`,"pws.cursorTransport":`Cursor トランスポート`,"pws.cursorTransportHttp2":`HTTP/2(デフォルト)`,"pws.cursorTransportHttp1":`HTTP/1.1(プロキシ互換)`,"pws.cursorTransportDesc":`プロキシが Cursor の HTTP/2 ストリームを安定して転送できない場合は HTTP/1.1 を使用します。`,"pws.optionalPlaceholder":`任意`,"pws.providerId":`プロバイダー ID`,"pws.reauth":`再認証が必要`,"pws.reauthenticate":`再認証`,"pws.copyDoctor":`ocx doctor をコピー`,"pws.doctorCopied":`コピー済み`,"pws.healthCooldownHint":`クールダウンが終わるまで待ってください。まだこのアカウントをプローブしないでください。`,"pws.doctorCopyUnavailable":`クリップボードを利用できません`,"pws.healthLabel.rateLimited":`レート制限中`,"pws.healthLabel.quotaLimited":`クォータ制限中`,"pws.healthLabel.reauthRequired":`再認証が必要です`,"pws.healthLabel.refreshFailed":`更新に失敗しました`,"pws.healthLabel.metadataMismatch":`メタデータの不一致`,"pws.healthLabel.credentialConflict":`資格情報の競合`,"pws.healthSummary.rateLimited":`{provider} {account}: {until} までレート制限中です。それまでこのアカウントのルーティングは停止します。`,"pws.healthSummary.quotaLimited":`{provider} {account}: {until} までクォータ制限中です。それまでこのアカウントのルーティングは停止します。`,"pws.healthSummary.reauthRequired":`{provider} {account}: 再認証が必要です。`,"pws.healthSummary.credentialConflict":`{provider} {account}: 資格情報の競合があります。`,"pws.healthSummary.metadataMismatch":`{provider} {account}: メタデータが一致しません。`,"pws.healthSummary.staleCredentials":`{provider} {account}: 資格情報が不完全です。`,"pws.removeConfirm":`削除`,"pws.removeConfirmBody":`プロバイダー "{name}" を削除しますか? これは元に戻せません。`,"pws.removeDefaultConfirmBody":`既定のプロバイダー "{name}" を削除しますか? "{defaultProvider}" が既定のプロバイダーになります。この操作は元に戻せません。`,"pws.removeConfirmTitle":`プロバイダーを削除`,"pws.saveSettings":`保存`,"pws.pacingTitle":`リクエスト間隔調整`,"pws.pacingDesc":`このプロバイダーへの送信開始を均等に遅延します。ストリーミング応答は重複できます。`,"pws.pacingEnabled":`有効`,"pws.pacingRpm":`1分あたりのリクエスト数`,"pws.pacingRpmUnit":`RPM`,"pws.pacingDelay":`最小間隔 (ms)`,"pws.pacingSlowerWins":`より遅いプロバイダー制限が優先され、モデル設定は遅延を増やす場合のみ適用されます。`,"pws.pacingQueued":`待機中`,"pws.pacingNextSlot":`次のスロットまで`,"pws.pacingLastModel":`最後のモデル`,"pws.pacingNone":`なし`,"pws.pacingModelOverrides":`モデル別設定`,"pws.pacingModel":`モデル`,"pws.pacingAdd":`設定を追加`,"pws.pacingRemove":`削除`,"pws.pacingRemoveModel":`{model} のリクエスト間隔設定を削除`,"pws.pacingRuleRequired":`プロバイダー制限またはモデル別設定を追加してから有効にしてください。`,"pws.saving":`保存中…`,"pws.settingsSaved":`設定を保存しました。`,"pws.accountModeSaved":`アカウントモードを保存しました。`,"pws.accountModeFailed":`アカウントモードを切り替えられませんでした。`,"pws.accountModeConfirm":`OpenAI のアカウントモードを切り替えますか?実行中の会話はもう一方のモードのアカウントセットに再割り当てされ、クォータ使用量は新しいモードで計上されます。`,"pws.settingsUnsavedBar":`未保存の変更があります。`,"pws.unsavedLeaveBody":`未保存の変更があります。保存してから移動しますか?`,"pws.unsavedLeaveTitle":`未保存の変更`,"pws.attentionRequired":`要対応`,"pws.attentionAria":`{name}: {reason}`,"pws.missingCredentials":`資格情報が不足しています`,"pws.editJsonDesc":`生のプロキシ設定を JSON として編集`,"pws.updatesUnavailable":`プロバイダーの更新は利用できません。`,"pws.dashboard.title":`プロバイダー概要`,"pws.dashboard.subtitle":`すべてのモデルプロバイダーを一か所で管理します。`,"pws.dashboard.rateLimits":`レート制限`,"pws.capacity.estimate":`設定済み重みによるプール推定`,"pws.capacity.currentAccount":`現在の有効アカウント`,"pws.capacity.nextRecovery":`次の容量回復`,"pws.capacity.recoveryShare":`+{percent}% のプール容量`,"pws.capacity.incomplete":`対象範囲が不完全です: {excluded} 件を除外`,"pws.capacity.uncalibratedPlan":`未校正プランの {count} 件は基準シート重みで計上されるため、この推定値は控えめになる場合があります`,"pws.capacity.partial":`一部の期間の対象範囲が不完全です: {count} 件のアカウントでは表示中のすべての制限期間を取得できません`,"pws.capacity.windowPartial":`一部のみ`,"pws.capacity.windowPartialA11y":`{window}: アカウントの対象範囲が不完全です`,"pws.dashboard.recentlyUsed":`最近の使用`,"pws.dashboard.requests":`{count} リクエスト`,"pws.dashboard.checkedAgo":`{time} に確認`,"pws.dashboard.noQuota":`クォータデータなし`,"pws.dashboard.noUsage":`まだ使用量データはありません`,"pws.dashboard.noRateLimits":`まだレート制限データはありません`,"pws.allProviders":`プロバイダー概要`,"pws.enabledLabel":`有効`,"pws.testConnection":`接続テスト`,"pws.testing":`テスト中…`,"pws.connectionOk":`接続 OK`,"pws.connectionFailed":`接続失敗`,"pws.connectionNotApplicable":`対象外 — このプロバイダーは静的モデルカタログを使用します。`,"pws.editSettings":`設定を編集`,"pws.viewUsage":`詳細な使用量を表示`,"pws.allSystemsOk":`すべてのシステムが稼働中`,"pws.apiKeyConfigured":`API キー設定済み`,"pws.addApiKey":`API キーを追加`,"pws.loggedInAs":`{email} としてログイン中`,"pws.notLoggedIn":`未ログイン`,"pws.passthrough":`Codex パススルー`,"pws.notes":`メモ`,"pws.notePlaceholder":`このプロバイダーについてのメモを追加...`,"pws.noteSaved":`メモを保存しました`,"pws.authSummary":`認証`,"time.justNow":`たった今`,"time.notChecked":`未確認`,"time.minutesAgo":`{n}分前`,"time.hoursAgo":`{n}時間前`,"time.daysAgo":`{n}日前`,"modal.noMatch":`一致なし。`,"modal.oauthDefaultNote":`アカウントでログイン — API キー不要です。`,"modal.oauthComingSoon":`{label} の OAuth ログインは次回の更新で対応予定です。今は API キーをお使いください。`,"modal.oauthComingSoonShort":`このプロバイダーの OAuth ログインは次回の更新で対応予定です — 今は API キーをお使いください。`,"modal.useApiKeyInstead":`代わりに API キーを使用`,"modal.setupGuide":`セットアップガイド`,"modal.setupStep1Prefix":`にアクセスし`,"modal.setupDashboardLink":`{label} ダッシュボード`,"modal.setupStep1Suffix":`から API キーをコピー`,"modal.setupStep2":`下の API キー欄に貼り付け`,"modal.setupStep3":`プロバイダーを追加をクリック — モデルは自動検出されます`,"modal.namePlaceholder":`例: openrouter`,"modal.duplicateWarn":`プロバイダー "{name}" は既存で、上書きされます。`,"modal.forwardHintPrefix":`キー不要 — プロキシはあなたの`,"modal.forwardCredentials":`codex ログイン`,"modal.forwardHintSuffix":`資格情報をこのプロバイダーに転送します。`,"modal.localHint":`API キーは保存されません。Cursor の静的な公開モデルカタログを Codex に追加しますが、ライブの Cursor トランスポートとネイティブのファイル/シェル実行は監査されるまで無効のままです。`,"modal.getApiKey":`{label} の API キーを取得`,"modal.apiKey":`API キー`,"modal.apiKeyTransport":`API キーヘッダー`,"modal.apiKeyTransportNative":`x-api-key (Anthropic 標準)`,"modal.apiKeyTransportBearer":`Authorization: Bearer`,"modal.apiKeyPlaceholder":`sk-… (または $ENV_VAR)`,"modal.defaultModelPlaceholder":`例: gpt-5.5`,"modal.baseUrlPlaceholder":`https://...`,"modal.baseUrlPlaceholderError":`ベース URL に未解決の {placeholder} が含まれています。実際の値に置き換えてください。`,"modal.baseUrlPlaceholderHint":`プロバイダーを追加する前に、ベース URL の {placeholder} を実際の Account ID に置き換えてください。`,"modal.adding":`追加中…`,"modal.useOauthLogin":`← OAuth ログインを使用`,"nav.codexAuth":`Codex 認証`,"nav.codexSet":`Codex 設定`,"codexSet.tab.multiauth":`マルチ認証`,"codexSet.tab.prompt":`プロンプト`,"codexSet.prompt.title":`プロンプトレイヤー`,"codexSet.prompt.timing":`新しく開始したセッションから適用されます。実行中のセッションは現在の設定を保持します。`,"codexSet.prompt.staleRevision":`他の場所で設定が変更されたため、一覧を再読み込みしました。`,"codexSet.prompt.writeFailed":`変更を保存できませんでした。`,"codexSet.prompt.loadFailed":`プロンプトレイヤーを読み込めませんでした。`,"codexSet.prompt.repair":`修復`,"codexSet.prompt.repairFailed":`修復を完了できませんでした。`,"codexSet.drift.journalPresent":`前回の書き込みが完了していません。次の書き込み時に自動で復旧します。`,"codexSet.drift.projectionStale":`保存済みのレイヤーと config.toml の値が一致しません。修復するとレイヤーの内容で値を書き直します。`,"codexSet.drift.storeMissing":`レイヤーファイルがないのに config.toml に指示が残っています。修復するとバックアップを作成し、その内容を1つのレイヤーとして保持します。`,"codexSet.drift.ownedMalformed":`config.toml に生成された行が手動で変更されているため、安全に書き直せません。`,"codexSet.custom.adoptUnsupported":`{path} の {line} 行目の値が単一行の文字列ではないため、インポートできません。ここで管理するには手動で移動してください。`,"codexSet.prompt.unreadable":`Codex の設定ファイルは存在しますが読み取れないため、変更を拒否しました。`,"codexSet.layer.permissions":`権限`,"codexSet.layer.collaboration":`コラボレーションモード`,"codexSet.layer.environment":`環境コンテキスト`,"codexSet.layer.apps":`アプリ`,"codexSet.layer.skills":`スキル`,"codexSet.prompt.extensionsUnknown":`拡張機能は独自のレイヤーを追加できます。Codex が公開していないため、ここには表示できません。`,"codexSet.group.transition":`遷移通知`,"codexSet.group.transitionDesc":`状態を説明するのではなく変化を知らせる項目のため、セッションがリアルタイムに切り替わるかモデルが変わったときだけ現れます。`,"codexSet.custom.slotNote":`カスタムレイヤーはこの順序で連結され、1つのセクションになります。`,"codexSet.row.alwaysOn":`常に有効`,"codexSet.row.onChange":`変更時に送信`,"codexSet.row.featureGated":`[features] で設定`,"codexSet.row.openFeatures":`設定を開く`,"codexSet.dialog.setValue":`{value}(デフォルト {fallback})`,"codexSet.dialog.copyKey":`キーをコピー`,"codexSet.dialog.unknownLayer":`このビルドにはこのレイヤーの説明がありません。ダッシュボードより新しい Codex ランタイムのレイヤーです。`,"codexSet.custom.heading":`カスタムレイヤー`,"codexSet.custom.add":`+ レイヤーを追加`,"codexSet.custom.newTitle":`新しいレイヤー`,"codexSet.custom.editTitle":`レイヤーを編集`,"codexSet.custom.titleLabel":`タイトル`,"codexSet.custom.bodyLabel":`指示`,"codexSet.custom.bodySize":`{max} バイト中 {bytes} バイト`,"codexSet.custom.normalized":`タブを半角スペース4個に、改行コードを LF に変換しました。`,"codexSet.custom.titleRequired":`タイトルを入力してください。`,"codexSet.custom.titleTooLong":`タイトルは {count} 文字です。上限は {max} 文字です。`,"codexSet.custom.titleMultiline":`タイトルは1行で入力してください。`,"codexSet.custom.bodyTooLarge":`このレイヤーは {bytes} バイトです。上限は {max} バイトです。`,"codexSet.custom.composedTooLarge":`有効なレイヤーを合わせると {bytes} バイトとなり、上限を超えます。`,"codexSet.custom.invalidCharacter":`位置 {position} の制御文字は保存できません。`,"codexSet.custom.discardPrompt":`変更を破棄しますか?`,"codexSet.custom.keepEditing":`編集を続ける`,"codexSet.custom.delete":`{title} を削除`,"codexSet.custom.deleteConfirm":`このレイヤーを削除しますか?元に戻せません。`,"codexSet.custom.layerGone":`他の場所でそのレイヤーが削除されたため、エディターを閉じました。`,"codexSet.custom.deleteConfirmNamed":`“{title}” を削除しますか?元に戻せません。`,"codexSet.custom.moveUp":`{title} を上へ移動`,"codexSet.custom.prevLayer":`前のレイヤー`,"codexSet.custom.nextLayer":`次のレイヤー`,"codexSet.custom.navPosition":`{position} / {total}`,"codexSet.custom.moveDown":`{title} を下へ移動`,"codexSet.custom.limitReached":`カスタムレイヤーは最大 {max} 個まで保存できます。`,"codexSet.custom.notOwned":`developer_instructions は opencodex の外部で作成されたため、ここでは編集できません。レイヤーとして管理するにはインポートしてください。`,"codexSet.custom.adopt":`既存の指示をインポート`,"codexSet.custom.adoptConfirm":`レイヤーとしてインポート`,"codexSet.custom.adoptRefused":`既存の値をインポートできませんでした。`,"codexSet.custom.baseReplaced":`model_instructions_file が {path} に設定されているため、opencodex の外部で基本プロンプトが置き換えられています。`,"codexSet.lint.identity":`Codex が設定するものとは異なるアイデンティティを名乗っています。`,"codexSet.lint.foreignTool":`ツールはレジストリから提供されます。ここに名前を書いてもツールは作成されません。`,"codexSet.lint.placeholder":`指示にはテンプレートエンジンが適用されないため、このまま送信されます。`,"codexSet.lint.applyPatch":`apply_patch は指示ではなく、ツールレジストリで定義されます。`,"codexSet.lint.approvalVocab":`Codex は独自の承認用語を挿入するため、これと矛盾する可能性があります。`,"codexSet.lint.environment":`環境情報は後で生成されるため、これと矛盾する可能性があります。`,"codexSet.lint.size":`このレイヤーは 8 KB を超えています。保存はできますが、リクエストのたびにトークンを消費します。`,"codexSet.preset.blank":`空のレイヤー`,"codexSet.preset.concise.name":`簡潔な出力`,"codexSet.preset.concise.description":`短く答え、前置きと不要な書式を省きます。`,"codexSet.preset.concise.provenance":`Claude Code の簡潔さに関する指示をもとに翻案しました。独自の文言であり、複製ではありません。`,"codexSet.preset.planFirst.name":`編集前に計画`,"codexSet.preset.planFirst.description":`計画を示してから変更します。`,"codexSet.preset.planFirst.provenance":`Claude Code の計画重視の方針をもとに翻案しました。独自の文言であり、複製ではありません。`,"codexSet.preset.explainWhy.name":`理由を説明`,"codexSet.preset.explainWhy.description":`何をするかだけでなく、理由も説明します。`,"codexSet.preset.explainWhy.provenance":`Grok Build の確認スタイルをもとに翻案しました。独自の文言であり、複製ではありません。`,"codexSet.preset.testFirst.name":`テストを先に`,"codexSet.preset.testFirst.description":`修正前に、失敗するテストを作成します。`,"codexSet.preset.testFirst.provenance":`一般的なエージェントの実践をもとに翻案しました。独自の文言であり、複製ではありません。`,"codexSet.preset.korean.name":`韓国語で回答`,"codexSet.preset.korean.description":`リクエストの言語にかかわらず、韓国語で回答します。`,"codexSet.preset.korean.provenance":`よく要望される項目をもとに opencodex 向けに作成しました。独自の文言であり、複製ではありません。`,"codexSet.dialog.class":`種類`,"codexSet.dialog.key":`設定キー`,"codexSet.dialog.fileValue":`このファイルの値`,"codexSet.dialog.absentDefault":`未設定(デフォルト: {value})`,"codexSet.dialog.noRenderedText":`Codex は組み立て済みの組み込みレイヤー本文を公開していません。そのため、このダイアログには内容ではなくレイヤーの説明とキーを表示します。`,"codexSet.dialog.sourceText":`モデルに送られる原文`,"codexSet.dialog.sourceBytes":`{bytes} バイト`,"codexSet.dialog.notRendered":`確認したターンでは、このレイヤーは何も送信していません。各セクションは内容が変わったときだけ再送されるため、一度の確認では欠けて見えることがあります。`,"codexSet.dialog.emptySource":`{path} のファイルは存在しますが空のため、このレイヤーは何も送信しません。`,"codexSet.dialog.notExposed":`基本プロンプトは Codex が出力するメッセージ一覧の外を通るため、ここには表示できません。model_instructions_file で置き換えることは可能です。`,"codexSet.dialog.textUnavailable":`このマシンでは Codex のプロンプトを読み取れず、原文を表示できません。`,"codexSet.class.base":`基本指示`,"codexSet.class.config-toggle":`ここで切り替え可能`,"codexSet.class.feature-gated":`機能フラグ制御`,"codexSet.class.runtime-conditional":`ランタイム条件付き`,"codexSet.class.extension-unknown":`拡張レイヤー`,"codexSet.layer.base-instructions":`基本指示`,"codexSet.layer.model-switch":`モデル切り替え通知`,"codexSet.layer.personality":`パーソナリティ`,"codexSet.layer.context-window-guidance":`コンテキストウィンドウのガイダンス`,"codexSet.layer.realtime":`リアルタイム`,"codexSet.layer.agents-md":`AGENTS.md`,"codexSet.layer.environments-instructions":`実行環境`,"codexSet.layer.plugins":`プラグイン`,"codexSet.layer.tools":`ツール`,"codexSet.layer.multi-agent-mode":`マルチエージェントモード`,"codexSet.layer.git-attribution":`コミットの帰属表示`,"codexSet.about.base-instructions":`Codex 自体の指示です。リクエストに含まれ、無効にはできません。`,"codexSet.about.model-switch":`会話の途中でセッションのモデルが変わると追加されます。`,"codexSet.about.personality":`機能フラグで制御されるトーンと語調のガイダンスです。`,"codexSet.about.context-window-guidance":`機能フラグで制御される残りのコンテキスト予算のガイダンスです。`,"codexSet.about.realtime":`リアルタイムセッションに追加されます。`,"codexSet.about.agents-md":`プロジェクトの AGENTS.md ファイルです。このページはレイヤーを表示するだけで、プロジェクト文書は編集しません。`,"codexSet.about.permissions":`現在適用されているサンドボックスと承認設定を説明します。`,"codexSet.about.collaboration":`有効なコラボレーションモードを説明します。`,"codexSet.about.environment":`作業ディレクトリ、プラットフォーム、その他の環境情報です。`,"codexSet.about.environments-instructions":`機能フラグで制御される遅延実行環境向けのガイダンスです。`,"codexSet.about.apps":`接続済みアプリの使用方法です。`,"codexSet.about.plugins":`プラグインが選択されているか、プラグインが機能を提供すると追加されます。`,"codexSet.about.tools":`機能フラグで制御される遅延読み込みツールの説明です。`,"codexSet.about.skills":`利用可能なスキルの一覧です。`,"codexSet.about.multi-agent-mode":`機能フラグで制御されるサブエージェント向けの指示です。`,"codexSet.about.git-attribution":`モデルが書いたコミットに Co-authored-by: Codex トレーラーを、開いたプルリクエストに Generated with Codex. の行を追加させます。Codex がアカウントから取得するため、ここでも [features] でも変更できません。アカウント側で無効にすると、何も送らないのではなく逆の指示を送ります。`,"codexSet.condition.model-switch":`セッション中にモデルが変わった後のみ含まれます。`,"codexSet.condition.realtime":`リアルタイムセッションのみ含まれます。`,"codexSet.condition.agents-md":`作業ディレクトリ用のプロジェクト文書が見つかると含まれます。`,"codexSet.condition.plugins":`プラグインが選択されているか、プラグインが機能を提供すると含まれます。`,"codexSet.condition.git-attribution":`アカウントの帰属表示ポリシーで決まります。`,"codexSet.base.title":`ベースプロンプト`,"codexSet.base.prev":`前のオプション`,"codexSet.base.next":`次のオプション`,"codexSet.base.position":`{position} / {total}`,"codexSet.base.swipeHint":`左右にスワイプするか、矢印キーか矢印ボタンでオプションを切り替えます。新しく開始したセッションに適用されます。`,"codexSet.base.defaultTitle":`Codex 自身のベースプロンプト`,"codexSet.base.defaultBody":`デフォルトはここに保存されないため、編集も削除もできません。選ぶと設定から model_instructions_file が削除され、Codex は同梱のプロンプトを使います。`,"codexSet.base.variantTitle":`名前`,"codexSet.base.variantBody":`プロンプト`,"codexSet.base.replacesWarning":`Codex 自身のベースプロンプトに追記するのではなく、丸ごと置き換えます。ここに短く書けば、モデルへの指示もその分だけになります。`,"codexSet.base.use":`これを使う`,"codexSet.base.inUse":`使用中`,"codexSet.base.externalBlocked":`model_instructions_file はすでに {path} を指しており、opencodex が書いた値ではありません。自分で消してから選んでください。`,"nav.api":`API`,"nav.integrations":`連携`,"nav.openMenu":`メニューを開く`,"nav.closeMenu":`メニューを閉じる`,"integrations.subtitle":`クライアントを opencodex に接続し、認証情報の管理とクライアント設定の復元を行います。`,"integrations.tabsLabel":`連携画面`,"integrations.tab.overview":`概要`,"integrations.tab.keys":`API キー`,"integrations.tab.codex":`Codex`,"integrations.tab.claude":`Claude`,"integrations.tab.grok":`Grok Build`,"integrations.tab.opencode":`OpenCode`,"integrations.tab.pi":`Pi`,"integrations.tab.omp":`OMP`,"integrations.tab.hermes":`Hermes`,"integrations.tab.openclaw":`OpenClaw`,"integrations.tab.kimi":`Kimi Code`,"integrations.tab.gajae":`Gajae Code`,"integrations.tab.dsh":`DSH`,"integrations.tab.mcode":`MiniMax Code`,"integrations.tab.zcode":`ZCode`,"integrations.tab.prime":`Prime Agent`,"integrations.tab.aside":`Aside`,"integrations.codex.title":`Codex CLI`,"integrations.codex.body":`Codex の接続はプロキシサービスが管理します。opencodex を起動すると適用され、サービスを停止するとネイティブのルーティングに戻ります。`,"integrations.codex.openService":`サービス制御を開く`,"integrations.state.notInstalled":`未インストール`,"integrations.state.unknown":`確認中`,"integrations.detail.codexRouted":`Codex のリクエストはこのプロキシを経由します`,"integrations.detail.codexAbsent":`Codex はまだこのプロキシを経由していません`,"integrations.detail.keyCount":`キー {count} 個を発行済み`,"integrations.detail.keyNone":`発行済みのキーはありません`,"integrations.detail.keyChecking":`確認中…`,"integrations.detail.keyUnavailable":`キーの状態を取得できません`,"integrations.detail.claudeOff":`接続がオフです`,"integrations.detail.desktopCurrent":`Desktop はこのプロファイルで動作しています`,"integrations.detail.desktopStale":`適用後にプロファイルが変更されました`,"integrations.detail.desktopNotServed":`プロファイルはありますが Desktop は別のものを使用中です`,"integrations.detail.desktopAbsent":`適用されたプロファイルはありません`,"integrations.detail.desktopDesiredOff":`Claude Desktop 連携はオフです`,"integrations.detail.desktopDesiredOffCleanupPending":`Claude Desktop はまだゲートウェイを使用しています。クリーンアップ待ちです`,"integrations.detail.desktopDesiredOnNotApplied":`連携はオンですが、Desktop はゲートウェイプロファイルを使用していません`,"integrations.detail.desktopSelectedElsewhere":`Desktop は別のプロファイルを使用しています`,"integrations.detail.desktopProfileDrift":`選択された Desktop プロファイルが変更されました`,"integrations.detail.desktopObservedUnsafe":`選択された Desktop プロファイルは安全に変更できません`,"integrations.detail.desktopNotInstalled":`Claude Desktop の設定ライブラリがインストールされていません`,"integrations.dialog.desktop.title":`Claude Desktop 連携を無効にしますか?`,"integrations.dialog.desktop.changes":`{path} に opencodex 管理のゲートウェイプロファイルがある場合、Desktop は先に認証情報のない標準プロファイルを選択し、その後で古いプロファイルとバックアップを削除します。`,"integrations.dialog.desktop.breakage":`Claude Desktop は opencodex 経由のモデルではなく、標準の Claude に戻ります。`,"integrations.dialog.desktop.undo":`再度有効にすると、保存済みのモデル割り当てから opencodex プロファイルを再生成します。`,"integrations.dialog.desktop.restart":`Claude Desktop は起動時にのみこの設定を読み取ります。変更を反映するには完全に終了して再起動してください。`,"integrations.dialog.desktop.confirm":`無効にする`,"integrations.native.error.desktopUnsafeMetadata":`{path} の Claude Desktop メタデータを安全に読み取れなかったため、ライブラリは変更されませんでした。`,"integrations.native.error.desktopCleanupIncomplete":`Claude Desktop は標準モードを指していますが、古い opencodex 認証情報ファイルが残っています: {paths}。`,"integrations.native.msg.desktopDisabled":`Claude Desktop 連携を無効にしました。`,"integrations.native.msg.desktopEnabled":`Claude Desktop 連携を有効にしました。`,"integrations.detail.grokModels":`モデル {count} 個を接続済み`,"integrations.detail.grokAbsent":`設定に opencodex ブロックがありません`,"integrations.dialog.grok.title":`Grok Build 連携を解除しますか?`,"integrations.dialog.grok.changes":`{path} から、opencodex が印を付けたブロックだけを削除します。ブロック外に直接書いた内容はそのまま残します。`,"integrations.dialog.grok.breakage":`解除すると、Grok Build から opencodex のモデルエイリアスが消えます。xAI アカウントで使用していたモデルはそのままです。`,"integrations.dialog.grok.undo":`opencodex が loopback アドレスで実行中なら、再び有効にしたとき、現在利用できるモデル一覧からブロックを新しく書き込みます。`,"integrations.dialog.grok.confirm":`解除`,"integrations.native.msg.nonLoopbackRemoved":`Grok Build を自動登録できるのは、opencodex が loopback アドレスで実行中の場合だけです。loopback アドレスを指していた以前のブロックを削除しました。`,"integrations.native.msg.nonLoopbackRemovedNoop":`Grok Build を自動登録できるのは、opencodex が loopback アドレスで実行中の場合だけです。削除する以前のブロックはありませんでした。`,"integrations.native.msg.nonLoopbackSuperseded":`Grok Build を自動登録できるのは、opencodex が loopback アドレスで実行中の場合だけです。その間に別の場所から設定へ新しいブロックが書かれたため、現在ファイルにあるブロックはこのリクエストが作成したものではありません。`,"integrations.native.error.orphanedMarker":`{path} に opencodex の開始マーカーはありますが、終了マーカーがありません。ブロックの終端を特定できないため、ファイルは変更していません。`,"integrations.native.error.homeMismatch":`インストール済みサービスのホームと現在のホームが一致しないため、ファイルは変更していません。`,"integrations.native.error.notInstalled":`Grok Build がインストールされていないため、変更できません。`,"integrations.native.error.configBusy":`別の場所で設定を保存中のため、変更できませんでした。しばらくしてから再試行してください。`,"integrations.state.absent":`未適用`,"integrations.state.current":`適用済み`,"integrations.state.stale":`更新が必要`,"integrations.state.conflict":`競合`,"integrations.state.unsafe":`確認不可`,"integrations.summary.detected":`検出されたクライアント`,"integrations.summary.applied":`設定済みクライアント`,"integrations.summary.stale":`更新が必要`,"integrations.summary.lastChange":`最終変更`,"integrations.summary.disableAll":`すべて無効にする…`,"integrations.onboarding":`適用時は、先にバックアップを保存してから opencodex プロバイダーブロックを 1 つだけ書き込みます。無効化時はそのブロックだけを削除し、保存されたスナップショットから復元できます。`,"integrations.empty.title":`インストール済みのクライアントが検出されませんでした`,"integrations.empty.body":`対応クライアントをインストールしてから、ここに戻って opencodex を適用してください。`,"integrations.action.apply":`適用`,"integrations.action.disable":`無効にする`,"integrations.action.refresh":`更新`,"integrations.action.settings":`設定`,"integrations.action.manageKeys":`キーを管理`,"integrations.action.restore":`復元…`,"integrations.action.undo":`元に戻す`,"integrations.action.restorePoint":`この時点に復元…`,"integrations.action.snapshotExpired":`バックアップ期限切れ`,"integrations.rollback.title":`復元センター`,"integrations.rollback.empty":`適用履歴はまだありません`,"integrations.rollback.emptyBody":`書き込みが成功するたびに、変更前のスナップショットが先に保存されます。`,"integrations.catalog.title":`クライアント`,"integrations.rollback.older":`以前の操作`,"integrations.rollback.showMore":`さらに {n} 件表示`,"integrations.rollback.failed":`ロールバック履歴を読み込めませんでした。`,"integrations.restore.title":`このスナップショットを復元しますか?`,"integrations.restore.body":`現在のファイルを先にバックアップしてから、選択したスナップショットで置き換えます。`,"integrations.restore.driftTitle":`新しい編集が検出されました`,"integrations.restore.driftBody":`このスナップショット以降の変更をバックアップしてから、ファイルを置き換えます。`,"integrations.restore.confirm":`復元`,"integrations.restore.confirmDrift":`新しい編集をバックアップして復元`,"integrations.restore.pending":`復元中…`,"integrations.restore.manual":`自動復元に失敗しました: {reason}。{path} から手動で復元してください。`,"integrations.error.load":`連携状態を読み込めませんでした。`,"integrations.error.stale":`最新の更新に失敗しました。以下の値は古い可能性があります。`,"integrations.error.busy":`このクライアントに対する別の変更が実行中です。しばらくしてから再試行してください。`,"integrations.error.conflict":`opencodex の書き込み後に設定が変更されました。何も削除していません。`,"integrations.error.unsafe":`設定を安全に変更できません。`,"integrations.error.generic":`連携の変更に失敗しました。以前の状態は保持されています。`,"integrations.error.nonLoopback":`{client} は localhost のプロキシにしか接続できません。リモートバインドに必要な認証ヘッダーを置く場所が設定ファイルになく、手動で書いても同じです。トンネルやローカルフォワーダーで loopback 経路を用意してください。`,"integrations.status.installed":`インストール済み`,"integrations.status.notInstalled":`未インストール`,"integrations.status.appliedAt":`適用`,"integrations.status.backup":`バックアップ`,"integrations.status.lastRestore":`最終復元`,"integrations.status.unknown":`不明`,"integrations.bulk.title":`適用済みのクライアント連携を無効にしますか?`,"integrations.bulk.body":`opencodex が所有するブロックだけを削除します。各クライアントについて、変更前のスナップショットを保存します。`,"integrations.bulk.partial":`一部のクライアントを無効にできませんでした: {clients}`,"integrations.bulk.success":`適用済みのクライアント連携を無効にしました。`,"integrations.retention.degraded":`バックアップの整理が遅れています。古いバックアップがディスクに残っている可能性があります。`,"integrations.error.residual":`ファイルが中間状態のままの可能性があります: {message} {path} から復元してください。`,"integrations.error.recover":`{message} バックアップは {path} にあります。`,"integrations.kind.apply":`適用`,"integrations.kind.disable":`解除`,"integrations.kind.refresh":`更新`,"integrations.kind.restore":`復元`,"integrations.kind.overwrite":`上書き`,"integrations.dialog.overwrite.title":`この設定ファイルのブロックを置き換えますか?`,"integrations.dialog.overwrite.changesUnowned":`{path} で opencodex が必要とする位置を、opencodex が書いたのではないブロックが占めています。適用すると opencodex が書くブロックに置き換わります。`,"integrations.dialog.overwrite.changesForeign":`{path} の opencodex ブロックに加えた変更は破棄され、opencodex が書くブロックに置き換わります。`,"integrations.dialog.overwrite.breakage":`そのブロックが設定していた内容は効かなくなります。ファイルの他の部分はそのままです。`,"integrations.dialog.overwrite.undo":`先にスナップショットを保存するので、下のロールバック一覧に残り、元に戻せます。`,"integrations.dialog.overwrite.confirm":`置き換える`,"integrations.action.overwrite":`置き換える`,"integrations.semantics.opencode":`ディスクから直接起動した場合にのみ適用されます。ocx opencode の環境注入が優先されます。`,"integrations.semantics.pi":`新しいセッションから適用されます。`,"integrations.semantics.omp":`カタログを読み込むには OMP を再起動してください。`,"integrations.semantics.hermes":`新しいセッションから適用されます。`,"integrations.semantics.openclaw":`実行中のゲートウェイにすぐ適用されます。`,"integrations.semantics.kimi":`再起動するか /reload を実行すると適用されます(v2 はファイルを監視します)。`,"integrations.semantics.gajae":`新しいセッション、または /model を開いたときに適用されます。`,"integrations.semantics.dsh":`OpenCodex が管理するのは $DSH_HOME/settings.yaml 内の llm-pi-ai.providers.opencodex だけです。DSH はこのプロバイダーをホットリロードし、既定のモデルと deepseek-official は変更しません。現在はループバック専用で、実際の認証情報は書き込みません。`,"integrations.semantics.mcode":`custom_provider.opencodex のみを管理します。既定モデルと MiniMax ログインは変更しません。`,"integrations.semantics.zcode":`~/.zcode/v2/config.json の provider.opencodex のみを管理します。Z.ai ログインと他のプロバイダーは変更しません。変更後は ZCode を再起動してください。`,"integrations.semantics.prime":`Prime Agent の models.json 内の providers.opencodex のみを管理します。場所は ~/.prime/agent ですが、PRIME_AGENT_CODING_AGENT_DIR が設定されている場合はそちらが優先されます。他のプロバイダーとモデルオーバーライドは変更しません。新しいセッションから適用されます。`,"integrations.semantics.aside":`サインイン中のアカウントの Aside models.json 内の providers.opencodex のみを管理します。場所は ~/.aside/u/<アカウント> です。他のプロバイダーは変更しません。Aside は実行中にこのファイルを書き換えるため、適用後は Aside を完全に終了して再度開いてください。`,"codexAuth.mainAccount":`メインアカウント`,"codexAuth.logLabel":`ログラベル`,"codexAuth.codexApp":`Codex App`,"codexAuth.moreActions":`その他の操作を表示`,"codexAuth.copyId":`アカウント ID をコピー`,"codexAuth.appLogin":`アプリログイン`,"codexAuth.accountPool":`アカウントプール`,"codexAuth.accountModeTitle":`OpenAI アカウントモード`,"codexAuth.accountModePool":`プールモード`,"codexAuth.accountModePoolDesc":`メインログインと対象の追加アカウントがここでローテーションします。`,"codexAuth.accountModeDirect":`ダイレクトモード`,"codexAuth.accountModeDirectDesc":`リクエストはメインログインのみを使用します; 追加アカウントはプールモード用に保持されます。`,"codexAuth.openaiMissing":`組み込みの OpenAI プロバイダーが設定されていません。`,"codexAuth.openaiDisabled":`組み込みの OpenAI プロバイダーが無効です。`,"codexAuth.openaiUnavailableDesc":`OpenAI アカウントは引き続き利用できます。Codex リクエストをルーティングするにはプロバイダーを有効にしてください。`,"codexAuth.enableOpenai":`OpenAI を有効にする`,"codexAuth.enablingOpenai":`有効化中...`,"codexAuth.enableOpenaiFailed":`OpenAI プロバイダーを有効にできませんでした。`,"codexAuth.openaiPresetLoadFailed":`OpenAI プロバイダーのプリセットを読み込めませんでした。`,"codexAuth.openaiPresetUnavailable":`OpenAI プロバイダーのプリセットを利用できません。`,"codexAuth.openProviders":`プロバイダーを開く`,"codexAuth.add":`追加`,"codexAuth.sparkQuota":`Codex Spark 使用量`,"codexAuth.sparkQuotaHint":`アカウントカードに GPT-5.3-Codex-Spark の週次枠を表示します。対象が 1 モデルのみのため既定は非表示です。`,"codexAuth.sparkQuotaShown":`Codex Spark 使用量を表示しました`,"codexAuth.sparkQuotaHidden":`Codex Spark 使用量を非表示にしました`,"codexAuth.sparkQuotaFailed":`Codex Spark 使用量の設定を変更できませんでした`,"codexAuth.refreshQuota":`クォータを更新`,"codexAuth.refreshingQuota":`更新中...`,"codexAuth.quotaRefreshed":`クォータを更新しました`,"codexAuth.quotaRefreshFailed":`クォータの更新に失敗しました`,"codexAuth.pauseExhausted":`上限到達を一括停止`,"codexAuth.pausingExhausted":`クォータを確認中...`,"codexAuth.pauseExhaustedSucceeded":`上限に達したアカウントを停止しました: {count}`,"codexAuth.pauseExhaustedNone":`使用率 100% が確認されたアカウントはありません。`,"codexAuth.pauseExhaustedFailed":`上限到達アカウントの確認と停止に失敗しました。`,"codexAuth.noPool":`まだプールアカウントは追加されていません。`,"codexAuth.pause":`一時停止`,"codexAuth.resume":`再開`,"codexAuth.paused":`一時停止中`,"codexAuth.pauseSucceeded":`{email} を一時停止しました`,"codexAuth.resumeSucceeded":`{email} をアカウントプールに戻しました`,"codexAuth.pauseFailed":`{email} を一時停止できませんでした。変更はありません。`,"codexAuth.resumeFailed":`{email} を再開できませんでした。変更はありません。`,"codexAuth.pausedHint":`再開するまで、自動切り替え、再試行、クールダウン復旧、手動選択の対象外です。`,"codexAuth.pinned":`固定中`,"codexAuth.pinnedHint":`手動で選択したアカウントなので、これより高い選択順序が先に使われることはありません。固定はこのアカウントを使い切るか、別のアカウントを選ぶか、いずれかの選択順序を変更するまで続きます。`,"codexAuth.fiveHour":`5時間`,"codexAuth.weekly":`週`,"codexAuth.monthly":`30日`,"codexAuth.resets":`リセット`,"codexAuth.today":`今日`,"codexAuth.current":`現在`,"codexAuth.nextSession":`選択済み`,"codexAuth.poolPrepared":`プール準備済み`,"codexAuth.preparePoolTitle":`このアカウントをプールモード用に準備しますか?`,"codexAuth.preparePoolDesc":`ダイレクトリクエストはメインログインを使い続けます。このアカウントはプールモードが有効化された際の準備済みプール選択になります。`,"codexAuth.prepareForPool":`プール用に準備`,"codexAuth.poolPreparedToast":`{email} はプールモード用に準備されました`,"codexAuth.switchTitle":`アクティブアカウントを切り替えますか?`,"codexAuth.switchDesc":`すぐに反映されます。アカウントに紐付いた既存スレッドと処理中のリクエストは現在のアカウントを維持し、新規または未紐付けのリクエストは選択したアカウントの順序ティアを使います。同じ選択順序のアカウントは引き続き交代で使われます。`,"codexAuth.cacheWarning":`アカウント切り替えでプロンプトキャッシュはリセットされます。新規セッションは空のキャッシュで開始します。`,"codexAuth.setAsNext":`このアカウントを次に使う`,"codexAuth.cancel":`キャンセル`,"codexAuth.switchBack":`メインに戻しますか?`,"codexAuth.switchBackDesc":`すぐに反映されます。アカウントに紐付いた既存スレッドと処理中のリクエストは現在のアカウントを維持し、新規または未紐付けのリクエストはアプリログインアカウントの順序ティアを使います。同じ選択順序のアカウントは引き続き交代で使われます。`,"codexAuth.autoSwitch":`使用量ベースのプロアクティブ切り替え`,"codexAuth.autoSwitchQuotaDesc":`クォータ: 使用率が {threshold}% 以上になると、既に紐付いたタスクを含む次のリクエストが、使用率の低い適格アカウントへ移る場合があります。Go/Free は 30 日枠のみを使用します。`,"codexAuth.autoSwitchQuotaOffDesc":`使用量ベースのプロアクティブ切り替えはオフです。新規/未紐付けタスクの割り当てと障害回復は引き続き適用されます。`,"codexAuth.autoSwitchRoundRobinDesc":`ラウンドロビン割り当てはこのしきい値を使用せず、新規/未紐付けタスクを引き続きローテーションします。`,"codexAuth.autoSwitchFillFirstDesc":`フィルファースト: {threshold}% は新規/未紐付けタスクの使い切り基準です。正常な紐付け済みタスクはアカウントを維持します。`,"codexAuth.autoSwitchFillFirstOffDesc":`フィルファーストには新規/未紐付けタスクの使用量基準がありません。クールダウン、再認証、障害回復では引き続きルーティングが変わる場合があります。`,"codexAuth.failureRecoveryNote":`障害回復は別です。出力前の 429/402 拒否、クールダウン、再認証、除外、または設定済みの一時障害フェイルオーバーにより、別の適格アカウントが選ばれる場合があります。`,"codexAuth.autoSwitchThreshold":`使用量しきい値`,"codexAuth.autoSwitchThresholdAria":`使用量しきい値(パーセント)`,"codexAuth.autoSwitchThresholdInc":`使用量しきい値を上げる`,"codexAuth.autoSwitchThresholdDec":`使用量しきい値を下げる`,"codexAuth.autoSwitchLoadFailed":`使用量ベースの切り替え設定を読み込めませんでした。`,"codexAuth.autoSwitchThresholdInvalid":`1 から 100 までの整数を入力してください`,"codexAuth.autoSwitchUpdated":`使用量ベースのプロアクティブ切り替え設定を更新しました`,"codexAuth.autoSwitchUpdateFailed":`使用量ベースの切り替え更新を確認できませんでした。最後に確認された値を表示しています。`,"codexAuth.requestUserInput":`Default モードで入力を求める`,"codexAuth.requestUserInputDesc":`Default モードのセッションで Codex が一時停止し、request_user_input ツールで質問できるようにします。`,"codexAuth.requestUserInputUpdated":`機能フラグを更新しました - 新しいセッションから適用されます。`,"codexAuth.requestUserInputUpdatedRestart":`機能フラグを更新しました - 新しいセッションから適用されます。Codex アプリを再起動してください。`,"codexAuth.requestUserInputUpdateFailed":`機能フラグを更新できませんでした。変更はありません。`,"codexAuth.requestUserInputLoadFailed":`config.toml から機能フラグを読み込めませんでした。`,"codexAuth.accountPickerTitle":`モデルピッカーで使用する Codex アカウントを指定`,"codexAuth.accountPickerOffDesc":`有効にすると、通常の GPT ピッカー項目がアカウントセレクターごとの項目に置き換わり、ログアウトせずに会話で使うアカウントを明示的に選べます。無効にしてもアカウントは削除されません。`,"codexAuth.accountPickerOnDesc":`各セレクターは保存済みアカウント 1 つに対応する公開ラベルです。選択した会話はそのアカウントに固定され、Pool のローテーションやフォールバックは行われず、現在の Pool アカウントも変更されません。`,"codexAuth.accountPickerCompatibility":`組み込みの Codex App ログインには専用セレクターがあります。生成されたマップでは通常 main と呼ばれ、必要に応じて main-2 のような衝突を避けるサフィックスが使われます。追加アカウントには安定したプライバシー保護ラベルが割り当てられ、カスタムセレクター名は保持されます。既存の会話と保存済みのモデル選択は引き続きルーティングされます。無効にすると生成された項目だけが非表示になり、セレクターと完全一致ルートは保持されます。通常の GPT モデル ID は従来どおり Pool または Direct で動作します。`,"codexAuth.accountPickerUpdated":`アカウント指定を更新しました。`,"codexAuth.accountPickerUpdateFailed":`アカウント指定を更新できませんでした。最後に確認された設定を表示しています。`,"codexAuth.accountPickerLoadFailed":`アカウント指定の設定を読み込めませんでした。`,"codexAuth.accountPickerRefreshFailed":`この設定を更新できませんでした。最後に確認された値を引き続き表示しています。`,"codexAuth.advancedSettings":`詳細設定`,"codexAuth.advancedSettingsAria":`高度な Codex 認証設定を表示または非表示`,"codexAuth.catalogRefreshPending":`変更は保存されましたが、Codex モデルカタログの更新が保留中です。ocx sync を実行して再試行してください。`,"anthropicPool.title":`Claude アカウントプール(実験的)`,"anthropicPool.enabledDesc":`429 時にアカウントをクールダウンしてフェイルオーバーします。新規セッションは{window}の使用率が {threshold}% 未満のアカウントを優先します。`,"anthropicPool.enabledNoProactiveDesc":`429 時にアカウントをクールダウンしてフェイルオーバーします。しきい値 0 では使用量に基づく事前切り替えは無効ですが、新規セッション選択と 429 復旧では引き続き {window} ウィンドウを使用します。`,"anthropicPool.disabledDesc":`アクティブな Claude アカウントのみを使用します。実験的ルーティングを受け入れる場合のみ有効にしてください。`,"anthropicPool.experimentalWarning":`実験的で十分に検証されていません。自動的な複数アカウント回転に見える行為は Anthropic により制限される可能性があります。同一組織はクォータを共有することがあり、その場合プールしても効果がありません。リスクを理解していない場合はオフのままにしてください。`,"anthropicPool.needTwoAccounts":`プールを有効にする前に、Claude OAuth アカウントを 2 つ以上追加してください。`,"anthropicPool.threshold":`新規セッションの使用率しきい値`,"anthropicPool.thresholdAria":`新規セッションの使用率しきい値(パーセント)`,"anthropicPool.thresholdHelp":`0 はクォータに基づく選択を無効にします(アフィニティ + アクティブアカウントのみ)。デフォルト 80。`,"anthropicPool.thresholdInvalid":`0 から 100 までの整数を入力してください`,"anthropicPool.loadFailed":`Claude プール設定を読み込めませんでした。`,"anthropicPool.saveFailed":`Claude プール設定を保存できませんでした。`,"anthropicPool.on":`オン`,"anthropicPool.off":`オフ`,"accountPool.strategy":`ローテーション戦略`,"accountPool.strategyDesc":`OpenCodex が新規/未紐付けタスクへアカウントを割り当てる方法です。`,"accountPool.strategyQuota":`クォータ`,"accountPool.strategyRoundRobin":`ラウンドロビン`,"accountPool.strategyFillFirst":`フィルファースト`,"accountPool.strategyHintQuota":`クォータ戦略は使用量しきい値を超えると、既存タスクの次のリクエストも別アカウントへ再紐付けできます。`,"accountPool.strategyHintRoundRobin":`ラウンドロビンは有効な紐付けがないタスクだけをローテーションし、使用量しきい値は通常のローテーションを変えません。`,"accountPool.strategyHintFillFirst":`フィルファーストはしきい値を未紐付けタスクの使い切り基準として使用し、正常な紐付け済みタスクは親和性を維持します。`,"accountPool.unboundDefinition":`新規/未紐付けタスクとは、現在のアカウント紐付けがないリクエストです。既存の表示中タスクも、プロキシまたは親和性のリセット後は未紐付けになる場合があります。`,"accountPool.stickyLimit":`ローテーション前の新規/未紐付け割り当て数`,"accountPool.stickyLimitAria":`ローテーション前の新規/未紐付け割り当て数`,"accountPool.stickyLimitInc":`スティッキー上限を上げる`,"accountPool.stickyLimitDec":`スティッキー上限を下げる`,"accountPool.stickyLimitHelp":`次へ進む前に、この回数の新規/未紐付けタスクを選択アカウントへ割り当てます。カウンターは上流の成功後ではなく、タスクを紐付けた時点で増えます。`,"accountPool.stickyLimitInvalid":`1 から 100 までの整数を入力してください`,"accountPool.strategyLoadFailed":`ローテーション戦略を読み込めませんでした。`,"accountPool.strategyUpdateFailed":`ローテーション戦略を保存できませんでした。`,"accountPool.quotaWindow":`クォータ集計ウィンドウ`,"accountPool.quotaWindowDesc":`クォータに基づく新規セッション選択、フィルファーストのしきい値判定、対象となる 429 代替先で使うキャッシュ済み使用量バーを指定します。`,"accountPool.quotaWindowFiveHour":`5 時間バー`,"accountPool.quotaWindowWeekly":`週間バー`,"accountPool.quotaWindowMaxUtilization":`高い方のバー`,"accountPool.quotaWindowHint":`週間バーでは、他に対象アカウントが残る間だけ 5 時間バーを使い切ったアカウントをスキップし、残らない場合はそれらへフォールバックします。同点では 5 時間使用量が少ない方を優先します。アカウントごとの週間バーはプロバイダーページで取得した後にのみ判明します。`,"accountPool.quotaWindowInert":`使用量バーを評価するのはクォータ、またはしきい値が 0 を超えるフィルファーストだけです。現在のローテーション戦略では、この設定は何も変えません。`,"accountPool.priority":`選択順序`,"accountPool.priorityAria":`このアカウントの選択順序`,"accountPool.priorityHint":`数値が大きいほど先に使われます。上位のアカウントがすべて使い切られるか利用できなくなったときにのみ、より小さい数値へ移ります。`,"accountPool.priorityFirst":`最初`,"accountPool.priorityEarlier":`早め`,"accountPool.priorityNormal":`標準`,"accountPool.priorityLater":`遅め`,"accountPool.priorityLast":`最後`,"accountPool.priorityOption":`{name}({value})`,"accountPool.priorityCustom":`カスタム`,"accountPool.priorityUpdated":`{email} の選択順序を更新しました`,"accountPool.priorityUpdateFailed":`{email} の選択順序を保存できませんでした。最後に確認された値を表示しています。`,"codexAuth.switched":`次のリクエストでは {email} を使用します`,"codexAuth.loadFailed":`Codex アカウント設定を読み込めませんでした。`,"codexAuth.switchFailed":`アカウントを切り替えられませんでした。以前の選択はそのままです。`,"codexAuth.removeConfirm":`{id} を削除しますか?`,"codexAuth.removeFailed":`アカウントを削除できませんでした。何も変更されていません。`,"codexAuth.addTitle":`Codex アカウントを追加`,"codexAuth.addIdLabel":`アカウント ID (スラッグ)`,"codexAuth.addIdPlaceholder":`codex-work、codex-alt、team...`,"codexAuth.resetCreditsAria":`{count} 個のリセットクレジット`,"codexAuth.addJsonLabel":`auth.json の内容`,"codexAuth.addHelp":`別のマシンの ~/.codex/auth.json からコピー、または codex-auth export を使用。`,"codexAuth.importBtn":`インポート`,"codexAuth.importInvalidJson":`無効な JSON です`,"codexAuth.importMissingTokens":`JSON に access_token または refresh_token がありません`,"codexAuth.importMissingId":`アカウント ID は必須です`,"codexAuth.accountAdded":`アカウントをプールに追加しました`,"codexAuth.addPickDesc":`別の ChatGPT アカウントでログインしてプールに追加します。`,"codexAuth.oauthLogin":`OAuth ログイン`,"codexAuth.oauthDesc":`ブラウザで ChatGPT ログインを開きます`,"codexAuth.deviceLogin":`デバイスコードでログイン`,"codexAuth.deviceDesc":`ヘッドレスやリモートのプロキシ向け。別の端末で短いコードを入力します`,"codexAuth.importAuthJson":`auth.json をインポート`,"codexAuth.importAuthJsonDesc":`別の Codex インストールまたは codex-auth export から`,"codexAuth.back":`戻る`,"codexAuth.oauthAlreadyInProgress":`ログインは既に進行中です。ブラウザで完了してください。`,"codexAuth.oauthWaiting":`ブラウザで ChatGPT ログインが完了するのを待機中...`,"codexAuth.oauthSubmittingCode":`コードを送信中…`,"codexAuth.oauthCodeSubmitted":`コードを送信しました — ログイン完了を待っています…`,"codexAuth.oauthStatusRetrying":`ログイン状態の確認中にネットワークまたはプロキシ エラーが発生しました — 再試行中…`,"codexAuth.oauthCancelled":`ログインはキャンセルされました。`,"codexAuth.loginFailed":`ログインに失敗しました`,"codexAuth.needsReauth":`再ログイン`,"codexAuth.reauthenticate":`再認証`,"codexAuth.tokenExpired":`トークンが期限切れ — このアカウントを再認証してください`,"codexAuth.mainTokenExpired":`トークンが期限切れ — Codex アプリログインから再度サインインしてください`,"codexAuth.emailCollision":`このアカウントはメインの Codex ログインと一致します。別のアカウントを使用してください。`,"codexAuth.resetCreditsTitle":`リセットクレジット`,"codexAuth.resetCreditsAvailable":`{count} 個のリセットクレジットが利用可能です。`,"codexAuth.resetCreditsDesc":`各クレジットは現在の時間別・週間使用量上限を即座にリセットします。`,"codexAuth.noResetCredits":`リセットクレジットはありません。`,"codexAuth.earnCreditsHint":`クレジットは毎月および紹介プログラム経由で獲得できます。`,"codexAuth.creditsExpireNote":`クレジットは獲得から 30 日で失効します。`,"codexAuth.useOneCredit":`1 クレジットを使用`,"codexAuth.confirmResetTitle":`リセットクレジットを使用しますか?`,"codexAuth.confirmResetDesc":`現在のレート制限を即座にリセットします。残り {count} クレジットです。`,"codexAuth.irreversible":`この操作は元に戻せません。`,"codexAuth.useCredit":`クレジットを使用`,"codexAuth.redeeming":`リセット中...`,"codexAuth.resetSuccess":`レート制限をリセットしました! 残り {remaining} クレジット。`,"codexAuth.resetSuccessGeneric":`レート制限をリセットしました!`,"codexAuth.resetAlreadyRedeemed":`このクレジットは既に引き換え済みです。クレジットは変わりません。`,"codexAuth.resetNothingToReset":`今リセットが必要なレート制限枠はありません。`,"codexAuth.resetNoCredit":`利用可能なリセットクレジットはありません。`,"codexAuth.resetError":`リセットクレジットの引き換えに失敗しました。もう一度お試しください。`,"codexAuth.fifoNote":`最も古いクレジットが先に使用されます。`,"codexAuth.confirmWhichCredit":`{date} のクレジットが使用されます。`,"codexAuth.creditNext":`次に使用`,"codexAuth.creditLabel":`クレジット #{n}`,"codexAuth.creditNextBadge":`次`,"codexAuth.creditGranted":`付与 {date}`,"codexAuth.creditExpires":`失効 {date} (残り {days}日)`,"api.title":`API アクセス`,"api.subtitle":`生成した API キーで外部アプリから opencodex プロキシに接続します。認証は {authHeader} ヘッダーで行い、エンドポイントごとに受け付けるヘッダーは下の表のとおりです。`,"api.endpointNote":`ベース URL を OpenAI 互換クライアントで使ってください。Responses と Chat Completions は /v1 配下で公開されます。`,"api.endpointsTitle":`エンドポイント`,"api.baseUrl":`ベース URL`,"api.responsesEndpoint":`Responses API`,"api.chatCompletionsEndpoint":`Chat Completions API`,"api.messagesEndpoint":`Messages API`,"api.modelsEndpoint":`Models API`,"api.authTitle":`認証`,"api.authBaseUrlNote":`クライアントにはベース URL を設定し、下のプロトコル別エンドポイントを選んでください。`,"api.authLoopback":`ループバック (127.0.0.1 または ::1) は認証を省略します。リモートでは生成した ocx_ キーまたは OPENCODEX_API_AUTH_TOKEN が必要です。`,"api.modelsTitle":`外部モデルカタログ`,"api.modelsCount":`{count} 件が利用可能`,"api.modelsSearch":`モデルを検索`,"api.modelsSubtitle":`これらの ID を /v1/models と選択したプロトコルで使用してください。`,"api.modelsLoading":`モデルを読み込み中…`,"api.modelsLoadFailed":`外部モデルカタログを読み込めませんでした。`,"api.modelsEmpty":`外部から呼び出せるモデルはまだありません。`,"api.modelsNoMatch":`「{query}」に一致するモデルはありません。`,"api.colModel":`モデル`,"api.colSource":`ソース`,"api.colProtocols":`プロトコル`,"api.copyModelId":`ID をコピー`,"api.modelCopied":`コピーしました`,"api.testModel":`テスト`,"api.testingModel":`テスト中…`,"api.testSucceeded":`OK`,"api.testFailed":`失敗`,"api.protocolResponses":`Responses`,"api.protocolChatCompletions":`Chat Completions`,"api.protocolMessages":`Messages`,"api.sourceNative":`ChatGPT プール`,"api.sourceCombo":`コンボ`,"api.sourceCustom":`カスタム`,"api.usageResponsesTitle":`Responses の例`,"api.usageChatTitle":`Chat Completions の例`,"api.usageMessagesTitle":`Messages の例`,"api.newKeyTitle":`新しいキーを作成しました`,"api.newKeyNote":`今すぐこのキーをコピーしてください — 再表示されません。`,"api.copy":`コピー`,"api.copied":`コピーしました`,"api.dismiss":`閉じる`,"api.generateTitle":`キーを生成`,"api.keyNamePlaceholder":`キー名(任意)`,"api.generate":`生成`,"api.generating":`作成中…`,"api.activeKeys":`アクティブなキー ({count})`,"api.activeKeysLoading":`有効なキー`,"api.noKeys":`まだ API キーがありません。上で生成してください。`,"api.workspace.sections":`API セクション`,"api.section.keys":`キー`,"api.section.connect":`接続`,"api.section.endpoints":`エンドポイント`,"api.section.models":`モデル`,"api.section.examples":`例`,"api.workspace.details":`APIキーの詳細`,"api.workspace.keyDetails":`キーの詳細`,"api.workspace.keyPrefix":`キーのプレフィックス`,"api.workspace.deleteKey":`キーを削除`,"api.workspace.deleteConfirm":`このキーを削除しますか?この操作は元に戻せません。`,"api.workspace.usageExamples":`使用例`,"api.copyUrlHint":`クリックして URL をコピー`,"api.urlCopied":`URL をコピーしました`,"api.copyExampleHint":`クリックして例をコピー`,"api.exampleCopied":`例をコピーしました`,"api.colName":`名前`,"api.colKey":`キー`,"api.colCreated":`作成日`,"api.confirm":`確認`,"api.deleteAria":`API キーを削除`,"api.usageSampleInput":`こんにちは、世界!`,"api.clientConfig.title":`クライアント設定`,"api.clientConfig.rowsLabel":`クライアントを接続`,"api.clientConfig.details":`詳細`,"api.clientConfig.detailsAria":`{client} 設定の詳細`,"api.clientConfig.copyAria":`{client} 設定をコピー`,"api.clientConfig.downloadAria":`{client} 設定をダウンロード`,"api.clientConfig.rowMeta":`{destination} · モデル {count} 件`,"api.clientConfig.rowError":`{client} の設定を生成できませんでした。`,"api.clientConfig.copiedAnnounceClient":`{client} の設定をクリップボードにコピーしました。`,"api.clientConfig.clientOpencode":`OpenCode`,"api.clientConfig.clientPi":`Pi`,"api.clientConfig.clientOmp":`OMP`,"api.clientConfig.clientHermes":`Hermes`,"api.clientConfig.clientOpenclaw":`OpenClaw`,"api.clientConfig.clientKimi":`Kimi Code`,"api.clientConfig.clientGajae":`Gajae Code`,"api.clientConfig.clientDsh":`DeepSeek Harness (DSH)`,"api.clientConfig.clientMcode":`MiniMax Code`,"api.clientConfig.clientZcode":`ZCode`,"api.clientConfig.clientPrime":`Prime Agent`,"api.clientConfig.clientAside":`Aside`,"api.clientConfig.copy":`設定をコピー`,"api.clientConfig.download":`ダウンロード`,"api.clientConfig.loading":`クライアント設定を生成中…`,"api.clientConfig.jsonLabel":`{client} 設定`,"api.clientConfig.destination":`配置先ファイル`,"api.clientConfig.envHint":`起動前にキーを設定`,"api.clientConfig.mergeWarning":`配置先ファイルにマージしてください。置き換えると既存のプロバイダーや MCP 設定が失われます。`,"api.clientConfig.modelCount":`{count} 件のモデルを書き出しました`,"api.clientConfig.missingLimits":`{total} 件中 {count} 件のモデルにコンテキスト上限がないため、クライアント側の既定値が使われます。`,"api.clientConfig.noKeyYet":`{env} に対応するキーがまだありません。ループバック外で使う前に上でキーを発行してください。`,"api.clientConfig.loadFailed":`モデル一覧を読み取れなかったため、クライアント設定を生成できませんでした。`,"api.clientConfig.copiedAnnounce":`クライアント設定をクリップボードにコピーしました。`,"api.clientConfig.copyFailed":`クライアント設定をコピーできませんでした。`,"api.clientConfig.downloadedAnnounce":`{filename} をダウンロードしました。まだ何も変わっていません。{destination} に自分でマージしてください。`,"api.clientConfig.whereDisclosure":`このファイルの置き場所`,"api.clientConfig.whereBody":`上のパスはグローバル設定の場所です。作業ディレクトリのプロジェクト設定ファイルが優先され、キーは設定に書かれた環境変数から読み込まれ、このファイルには保存されません。`,"api.keysLoadFailed":`APIキーを読み込めませんでした。`,"api.createFailed":`APIキーを作成できませんでした。`,"api.deleteFailed":`APIキーを削除できませんでした。`,"api.auth.endpoint":`エンドポイント`,"api.auth.required":`必須`,"api.auth.accepted":`利用可`,"api.auth.rejected":`不可`,"api.auth.testProtocol":`{protocol} をテスト`,"api.auth.testNeedsFreshKey":`認証付きテストを実行するには、キーを新しく作成し、一度だけ表示される値を画面に残したままにしてください。`,"api.key.name":`キー名`,"api.key.rename":`名前を変更`,"api.key.saveName":`名前を保存`,"api.key.renaming":`保存中…`,"api.key.renameFailed":`名前を変更できませんでした。入力内容はそのまま残しています。`,"api.key.deleting":`削除中…`,"api.rotation.title":`キーのローテーション`,"api.rotation.description":`短い移行期間だけ現在のキーを有効にしたまま、置き換え用キーを発行します。`,"api.rotation.start":`ローテーションを開始`,"api.rotation.starting":`開始中…`,"api.rotation.pending":`ローテーションは保留中です。確定前にクライアントを更新して動作を確認してください。`,"api.rotation.expires":`移行期間の終了:`,"api.rotation.secretOnce":`置き換え用キー — 表示は一度だけです。閉じる前にコピーしてください。`,"api.rotation.commit":`ローテーションを確定`,"api.rotation.abort":`ローテーションを中止`,"api.rotation.failed":`操作を完了できませんでした。更新してから再試行してください。`,"api.rotation.startFailed":`キーのローテーションを開始できませんでした。`,"api.key.copyFailed":`キーをコピーできませんでした。このパネルを閉じる前に手動で選択してコピーしてください。`,"api.attribution.title":`キー別の使用状況`,"api.attribution.requests7d":`直近 7 日のリクエスト`,"api.attribution.totalRequests":`集計済みリクエスト総数`,"api.attribution.totalRequestsAvailable":`利用可能な履歴のリクエスト`,"api.attribution.sinceAvailable":`利用可能な集計開始日`,"api.attribution.lastUsed":`最終使用`,"api.attribution.since":`集計開始`,"api.attribution.neverUsed":`集計開始以降は未使用`,"api.attribution.unavailable":`使用状況なし`,"api.attribution.unavailableDetail":`まだ集計された使用状況がありません。集計開始前のリクエストは遡って割り当てられません。`,"api.attribution.ambiguous":`2 つのキーが同じ ID を共有しているため、どちらの使用状況か判別できません。設定ファイルでキーごとに一意の ID を指定してください。`,"api.attribution.railAmbiguous":`ID 重複`,"claude.subtitle":`Claude Code 内で GPT、Gemini などのモデルを使用します。`,"claude.pageTitle":`Claude Code`,"claude.workspace.settings":`設定`,"claude.enabledLabel":`Claude 接続`,"claude.enabledHint":`オフにすると Claude Code はこのプロキシを使用できません。`,"claude.authMode":`認証モード`,"claude.authModeHint":`サブスクリプションは Claude アカウントが必要、プロキシは Anthropic アカウント不要で動作します`,"claude.authModeSubscription":`サブスクリプション(Claude アカウント)`,"claude.authModeProxy":`プロキシ(アカウント不要)`,"claude.authModeAuto":`自動 (Claude 認証を検出)`,"claude.effectiveMode.label":`次回起動時に適用`,"claude.effectiveMode.manual":`手動: {mode}`,"claude.effectiveMode.autoPresent":`自動: サブスクリプション — {source} で Claude 認証を検出`,"claude.effectiveMode.autoAbsent":`自動: プロキシモード — Claude 認証が見つかりません`,"claude.effectiveMode.autoUnknown":`自動: サブスクリプション — 認証を確認できませんでした`,"claude.effectiveMode.admissionKey":`このプロキシの API キーは引き続き送信されます。`,"claude.authSource.claude-json-oauth":`Claude アカウント`,"claude.authSource.claude-credentials-file":`認証情報ファイル`,"claude.authSource.macos-keychain":`macOS キーチェーン`,"claude.authSource.exported-env":`環境変数`,"claude.authSource.unknown":`検出された認証情報`,"claude.systemEnv":`自動接続`,"claude.systemEnvDesc":`オンにすると、任意のターミナルで claude を実行すると自動的にプロキシ経由になります。`,"claude.systemEnvUnsupported":`自動接続は macOS でのみ利用できます。このシステムでは {cmd} で Claude を起動してください。`,"claude.systemEnvWarn":`⚠ これを有効化するにはターミナルアプリを完全に終了して再起動する必要があります。推奨されません。`,"claude.fastMode":`高速モード(OpenAI)`,"claude.fastModeDesc":`OpenAI モデルの service_tier を制御します。オン = 優先(高速)。オフ = デフォルト。自動 = パススルー(クライアントが決定)。`,"claude.fastAuto":`自動`,"claude.fastOn":`オン`,"claude.fastOff":`オフ`,"claude.autoContext":`大きなコンテキストを自動で使用`,"claude.autoContextDesc":`1M マーキングがどこまで及ぶかを制御します。オン: 圧縮しきい値を収められるウィンドウを持つモデルに大型コンテキスト行を付けます。オフ: 真の 1M モデルのみに付けます。`,"claude.autoContextInert":`設定ファイルにレガシーのコンテキストサイズ値(maxContextTokens)が存在するため無効です。再び有効化するにはそれを削除してください。`,"claude.autoCompactWindow":`自動要約ポイント`,"claude.autoCompactDefault":`{value}(デフォルト)`,"claude.autoCompactWindowDesc":`チャットがこのポイントに達すると古いメッセージが要約されます。各モデル自身の上限を超えることはないので、200k モデルは影響を受けません。`,"claude.autoCompactWindowWarn":`これを変更すると GPT モデルが壊れる可能性があります — モデルの実際の上限より高く設定すると、要約が働く前にチャットがエラーになります。`,"claude.injectAgents":`サブエージェントを自動登録`,"claude.injectAgentsDesc":`サブエージェントタブで選んだモデル(と現在のデフォルトモデル)をディスパッチ可能な Claude Code エージェント(ocx-*)として登録します。次回セッションから適用されます。`,"claude.webSearchSidecar":`ウェブ検索サイドカーの上書き`,"claude.webSearchSidecarHint":`Claude Code リクエストのメインウェブ検索サイドカーを上書きします。`,"claude.visionSidecar":`ビジョンサイドカーの上書き`,"claude.visionSidecarHint":`Claude Code リクエストのメインビジョンサイドカーを上書きします。`,"claude.useMainSetting":`メイン設定を使用`,"claude.sidecarModelPlaceholder":`メイン設定のモデル`,"claude.quickstart":`はじめる`,"claude.quickstartHint":`{cmd} はプロキシ経由で Claude Code を開きます。あなたの claude.ai ログインはそのまま有効です。`,"claude.manualEnv":`手動セットアップ(高度)`,"claude.smallFastModel":`バックグラウンドヘルパーモデル`,"claude.smallFastModelHint":`チャットの要約やトピック検出のようなバックグラウンド作業に Claude Code が使うモデルです。haiku サブエージェントエイリアスもこれを使います。空 = Claude デフォルト(Haiku)。`,"claude.smallFastModelAccurateHint":`チャットの要約やトピック検出など、Claude Code がバックグラウンド処理に使うモデルです。サブエージェントの haiku エイリアスもこのモデルを使います。`,"claude.smallFastModelUnsetOption":`Claude Code に選択させる(ネイティブモデル)`,"claude.smallFastModelNativeWarning":`未設定の場合、OpenCodex はヘルパーモデルの上書きを設定しません。Claude Code がネイティブの Sonnet モデルを使用し、ネイティブプロバイダーで料金が発生する可能性があります。`,"claude.slotUnset":`Claude デフォルトを使用`,"claude.modelMap":`モデルの傍受`,"claude.modelMapHint":`特定モデルへのリクエストを傍受し、選んだモデルに再ルーティングします。デフォルトは空 — ルールを追加するまで何も起きません。`,"claude.mapFrom":`元のモデル(例: claude-sonnet-4-5)`,"claude.mapTo":`差し替え先(例: gemini/gemini-3-pro)`,"claude.addMapping":`ルールを追加`,"claude.removeMapping":`ルールを削除`,"claude.aliases":`利用可能なモデル`,"claude.aliasesHint":`Claude Code の /model メニューに表示されるモデル。`,"claude.aliasProviderOther":`その他`,"claude.loading":`読み込み中…`,"claude.loadFail":`Claude 設定の読み込みに失敗しました`,"claude.saved":`保存しました。`,"claude.saveFailed":`保存に失敗しました`,"claude.networkError":`ネットワークエラー — プロキシは起動していますか?`,"claude.toggleAria":`Claude 接続を切り替え`,"claude.none":`なし`,"claude.tabsLabel":`Claude クライアント`,"claude.tabCode":`Code`,"claude.tabDesktop":`Desktop`,"claudeDesktop.title":`Claude Desktop`,"claudeDesktop.subtitle":`各 Claude モデルファミリーをポート {port} の利用可能なモデルへルーティングします。`,"claudeDesktop.importJson":`JSON をインポート`,"claudeDesktop.exportJson":`JSON をエクスポート`,"claudeDesktop.loading":`Claude Desktop プロファイルを読み込み中…`,"claudeDesktop.loadFail":`Claude Desktop プロファイルの読み込みに失敗しました。`,"claudeDesktop.retry":`再試行`,"claudeDesktop.saveFailed":`Claude Desktop プロファイルの保存に失敗しました。`,"claudeDesktop.applyFailed":`プロファイルは保存されましたが、適用できませんでした。`,"claudeDesktop.updateFailed":`Claude Desktop の更新に失敗しました。`,"claudeDesktop.savedApplied":`プロファイルを保存し、Claude Desktop に適用しました。`,"claudeDesktop.appliedMarkerUnsaved":`Claude Desktop への適用は完了しましたが、適用マーカーを保存できませんでした。再度適用するまで、下の保存済み/適用済み表示が実際と異なる場合があります。`,"claudeDesktop.savedAppliedAnnounce":`Claude Desktop プロファイルを保存して適用しました。`,"claudeDesktop.saved":`プロファイルを保存しました。`,"claudeDesktop.savedAnnounce":`Claude Desktop プロファイルを保存しました。`,"claudeDesktop.exported":`プロファイルを JSON としてエクスポートしました。`,"claudeDesktop.importExpected":`バージョン 1 の Claude Desktop プロファイルが必要です。`,"claudeDesktop.importReady":`JSON をインポートしました。ドラフトを確認して保存・適用してください。`,"claudeDesktop.importedAnnounce":`プロファイル JSON をインポートしました。未保存の変更を確認できます。`,"claudeDesktop.importInvalid":`選択されたファイルは有効なプロファイルではありません。`,"claudeDesktop.importFailed":`インポートに失敗しました。{error}`,"claudeDesktop.moved":`{route} を {family} に移動しました。`,"claudeDesktop.unsaved":`未保存の変更`,"claudeDesktop.upToDate":`プロファイルは最新です`,"claudeDesktop.saving":`保存中…`,"claudeDesktop.applying":`適用中…`,"claudeDesktop.saveApply":`保存して適用`,"claudeDesktop.emptyTitle":`利用可能なモデルがありません`,"claudeDesktop.emptyHint":`プロバイダーを追加または有効化してから、Claude Desktop ルートを割り当ててください。`,"claudeDesktop.assignmentsLabel":`Claude モデルファミリーの割り当て`,"claudeDesktop.family.opus":`Opus`,"claudeDesktop.family.fable":`Fable`,"claudeDesktop.family.sonnet":`Sonnet`,"claudeDesktop.family.haiku":`Haiku`,"claudeDesktop.modelCountOne":`{count} モデル`,"claudeDesktop.modelCountMany":`{count} モデル`,"claudeDesktop.chooseDefault":`デフォルトを選択`,"claudeDesktop.temporaryDefault":`一時的なデフォルト`,"claudeDesktop.laneEmpty":`ここにモデルをドロップするか、移動コントロールを使用してください。`,"claudeDesktop.laneNoMatch":`検索に一致するモデルはこのファミリーにありません。`,"nav.grok":`Grok`,"grok.title":`Grok Build`,"grok.subtitle":`opencodex が Grok 設定に登録したモデルです。`,"grok.loading":`Grok の状態を読み込み中…`,"grok.loadFail":`Grok 設定を読み取れませんでした。`,"grok.notConfiguredTitle":`Grok Build が未設定です`,"grok.notConfiguredHint":`Grok をインストールしてプロキシを再起動すると、opencodex が管理ブロックを次の場所に書き込みます:`,"grok.endpoint":`エンドポイント`,"grok.colModel":`モデル`,"grok.colAlias":`Grok エイリアス`,"grok.colContext":`コンテキスト`,"grok.groupNative":`ネイティブモデル`,"grok.groupRouted":`ルーティングモデル`,"grok.enabledCount":`{total} 件中 {on} 件を登録`,"grok.saved":`選択を保存しました。`,"grok.savedApplied":`選択を保存し、Grok 設定に反映しました。`,"grok.saveFailed":`Grok の選択を保存できませんでした。`,"grok.applyFailed":`選択は保存しましたが、Grok 設定を更新できませんでした。`,"grok.applySkipped":`選択は保存しましたが、Grok 設定は変更されませんでした。`,"grok.saveApply":`保存して適用`,"grok.saving":`保存中…`,"grok.applying":`適用中…`,"grok.unsaved":`未保存の変更`,"grok.upToDate":`選択は最新です`,"grok.toggleModel":`{id} を Grok に登録`,"claudeDesktop.available":`利用可能`,"claudeDesktop.defaultBadge":`既定`,"claudeDesktop.supports1m":`1M`,"claudeDesktop.unavailable":`利用不可`,"claudeDesktop.contextM":`{n}M コンテキスト`,"claudeDesktop.contextK":`{n}k コンテキスト`,"claudeDesktop.contextUnknown":`コンテキスト不明`,"claudeDesktop.alias":`エイリアス`,"claudeDesktop.useAsDefault":`{family} のデフォルトに設定`,"claudeDesktop.moveTo":`移動先`,"claudeDesktop.move":`移動`,"claudeDesktop.status.applied":`Desktop に適用済み`,"claudeDesktop.status.stale":`設定が古くなっています — 再適用してください`,"claudeDesktop.status.notApplied":`未適用`,"claudeDesktop.status.notActiveProfile":`Desktop は別のプロファイルを使用中 — 再適用してください`,"claudeDesktop.status.disabled":`Claude Desktop 連携はオフです。有効にした後、Desktop を完全に終了して再起動してください。`,"claudeDesktop.enableApply":`有効にして適用`,"claudeDesktop.health.lastRequest":`最終リクエスト`,"claudeDesktop.health.stats":`{count} リクエスト / {errors} エラー`,"claudeDesktop.effort.supported":`effort`,"claudeDesktop.effort.displayOnly":`effort (表示のみ)`,"cws.loading":`コンボを読み込み中…`,"cws.loadFailed":`コンボを読み込めませんでした。`,"cws.saveFailed":`コンボを保存できませんでした。`,"cws.removeFailed":`コンボを削除できませんでした。`,"cws.saved":`コンボを保存しました。`,"cws.created":`{model} を作成しました。`,"cws.removed":`combo/{id} を削除しました。`,"cws.add":`コンボを追加`,"cws.addTitle":`コンボを追加`,"cws.addSubtitle":`プロバイダー全体にファンアウトする仮想モデルを作成します。クライアントは combo/ をリクエストします。`,"cws.create":`コンボを作成`,"cws.railAria":`コンボ一覧`,"cws.searchPlaceholder":`コンボやターゲットを検索…`,"cws.noSearchResults":`検索に一致するコンボがありません。`,"cws.group.failover":`フェイルオーバー`,"cws.group.roundRobin":`ラウンドロビン`,"cws.group.other":`その他の戦略`,"cws.targetCount":`{count} ターゲット`,"cws.targetCountOne":`1 ターゲット`,"cws.overviewTitle":`コンボ`,"cws.overviewBlurb":`プロバイダー/モデルターゲット間を、フェイルオーバー、ラウンドロビン、重み付きランダム、最少使用、最短クォータリセットで振り分ける仮想モデル。`,"cws.count.total":`合計`,"cws.count.failover":`フェイルオーバー`,"cws.count.roundRobin":`ラウンドロビン`,"cws.count.other":`その他`,"cws.howTitle":`仕組み`,"cws.howBody":`Codex に combo/ を要求します。OpenCodex はターゲットを選び、再試行可能な上流の失敗時のみホップします。利用可能なターゲットが残っていない場合、グローバルなデフォルトプロバイダーを使わずにフェイルクローズします。`,"cws.attentionTitle":`要対応`,"cws.attention.empty":`ターゲットが設定されていません`,"cws.attention.few":`ターゲットが 1 つだけ — フェイルオーバーのホップ先がありません`,"cws.attention.catalogOmitted":`モデルカタログにありません — メンバー能力が不完全または非互換です(context window / メタデータ不足、または modality 交差が空)。エイリアス指定のルーティングは動作します`,"cws.attention.allTargetsExhausted":`有効なすべてのターゲットがクォータを使い切っています`,"cws.emptyTitle":`最初のコンボを作成`,"cws.empty.createDesc":`仮想モデルに名前を付け、2 つ以上のバックエンドをつなぎます。`,"cws.backToAll":`すべてのコンボに戻る`,"cws.allCombos":`すべてのコンボ`,"cws.copyModel":`ID をコピー`,"cws.copied":`コピーしました`,"cws.renamed":`{from} を {to} に変更しました。`,"cws.tabsLabel":`コンボ詳細セクション`,"cws.tab.config":`設定`,"cws.tab.about":`概要`,"cws.strategy":`ストラテジー`,"cws.strategy.failover":`フェイルオーバー`,"cws.strategy.roundRobin":`ラウンドロビン`,"cws.strategy.random":`ランダム`,"cws.strategy.leastUsed":`最少使用`,"cws.strategy.resetWindow":`リセットウィンドウ`,"cws.strategy.failoverHint":`ターゲットを順に試します。最初が再試行可能なエラー(レート制限、障害、サブスクリプションゲート)で失敗した場合、次へホップします。`,"cws.strategy.roundRobinHint":`重みで決定論的にトラフィックを分散します。選んだターゲットを成功リクエストのバッチ分保持し、次へ進みます。`,"cws.strategy.randomHint":`リクエストごとに適格なターゲットを 1 つ抽選します。確率は重みに比例し、リクエスト間でスティッキネスはありません。`,"cws.strategy.leastUsedHint":`各リクエストを、成功回数が最も少ない適格なターゲットへ振ります。カウントはプロキシの再起動でリセットされます。`,"cws.strategy.resetWindowHint":`クォータのウィンドウが最も早くリセットされる適格なターゲットを優先します。クォータデータがない場合は設定順に従います。`,"cws.field.id":`コンボ ID`,"cws.field.idHint":`クライアントは {model} をリクエストします`,"cws.field.idInternalHint":`コンボの内部 ID。作成後も変更できます。`,"cws.field.idHintEdit":`ID を変更するとコンボの名前が変更されます。クライアントは {model} をリクエストします。`,"cws.field.alias":`公開モデル名`,"cws.field.aliasPlaceholder":`deepseek-v4-flash または vendor/model`,"cws.field.aliasHint":`任意。プレフィックスなしの名前、vendor/model のようなカスタムプレフィックスを指定するか、空欄のままにすると combo/ を使用します。`,"cws.field.nativeAlias":`ネイティブ OpenAI エイリアス`,"cws.field.nativeAliasHint":`このコンボがサポート対象の修飾なし OpenAI ネイティブモデル ID を所有します。アカウント修飾・プロバイダー修飾ルートは別のままです。`,"cws.field.displayName":`表示名`,"cws.field.displayNameHint":`モデルピッカーに表示するラベルです。ネイティブ OpenAI エイリアスでは必須です。`,"cws.field.stickyLimit":`ローテーション前の固定成功数`,"cws.field.stickyLimitHint":`重み付きセレクタが進む前に、選んだターゲットをこの回数の成功リクエスト分保持します。`,"cws.field.defaultEffort":`デフォルトの推論`,"cws.field.defaultEffortNone":`なし(ターゲットのデフォルト)`,"cws.field.defaultEffortHint":`クライアントが推論負荷を省略した場合のみ使用されます。選択肢は選択ターゲットが広告する負荷の交差です。`,"cws.capability.imageInputUnavailable":`選択した全ターゲットが画像入力に対応すると有効になります。`,"cws.capability.imageInputHint":`全ターゲットが画像対応なら既定でオン。オフにするとテキストのみ。`,"cws.capability.imageInput":`画像 / マルチモーダル`,"cws.capability.adaptiveEffort":`適応的な推論レベル`,"cws.capability.adaptiveEffortHint":`オフ: 推論レベルを持たない対象があると、コンボ全体のセレクターが消えます。オン: その対象はそのまま使え、セレクターには残りの対象で共通するレベルが表示されます。`,"cws.capabilities":`能力`,"cws.field.defaultEffortUnsupported":`この負荷はターゲット共通の階段にありません — リクエスト時に無視またはスナップされます。`,"cws.field.defaultEffortUnsupportedOption":`交差に含まれない`,"cws.targets":`ターゲット`,"cws.targets.failoverHint":`順序が重要 — 最初がプライマリです。`,"cws.targets.roundRobinHint":`重みが決定論的な相対選択を制御し、順序がローテーションリングの同点を解消します。`,"cws.targets.randomHint":`重みが各抽選の確率を制御します。順序は影響しません。`,"cws.targets.leastUsedHint":`順序は同じ使用回数のターゲット間の同点のみを解消します。`,"cws.targets.resetWindowHint":`順序はクォータデータが欠落または同点のときに適用されます。`,"cws.target.provider":`プロバイダー`,"cws.target.model":`モデル`,"cws.target.weight":`重み`,"cws.target.pickProvider":`プロバイダーを選択…`,"cws.target.pickProviderFirst":`最初にプロバイダーを選択…`,"cws.target.pickModel":`モデルを選択…`,"cws.target.noModels":`このプロバイダーにモデルはありません`,"cws.target.modelPlaceholder":`モデル ID`,"cws.target.add":`ターゲットを追加`,"cws.target.drag":`ドラッグで並べ替え`,"cws.target.moveUp":`上へ移動`,"cws.target.moveDown":`下へ移動`,"cws.quota.available":`利用可能`,"cws.quota.exhausted":`クォータを使い切りました`,"cws.quota.unknown":`クォータ不明`,"cws.quota.allExhausted":`有効なすべてのターゲットがクォータを使い切っています。別のターゲットを選ぶか、クォータの回復を待ってください。`,"cws.aboutTitle":`ランタイム`,"cws.aboutBody":`失敗したターゲットは Retry-After を尊重して短時間クールダウンします。無効またはコンテキストエラーはホップしません。各ターゲットは自身の能力に推論負荷を適応させます; 枯渇したコンボはフェイルクローズします。ログと使用量は順序付きの物理試行と試行ごとの使用量を保持します。`,"cws.removeConfirmTitle":`{model} を削除しますか?`,"cws.removeConfirmDesc":`これで仮想モデルが設定と Codex カタログから削除されます。プロバイダーは削除されません。`,"cws.unsavedTitle":`未保存の変更`,"cws.unsavedDesc":`このコンボへの編集を破棄して続行しますか?`,"cws.keepEditing":`編集を続ける`,"cws.err.missingId":`コンボ ID は必須です。`,"cws.err.invalidId":`ID は英字または数字で始まり、英数字、ドット、アンダースコア、ハイフンのみ使用できます(最大 64)。`,"cws.err.duplicateId":`この ID のコンボはすでに存在します。`,"cws.err.invalidAlias":`エイリアスには英字、数字、ドット、アンダースコア、ハイフンを使用でき、スラッシュ区切りは 1 つまでです。`,"cws.err.aliasReservedNamespace":`エイリアスに予約済みの "combo/" 名前空間は使用できません。`,"cws.err.aliasNativeFamily":`OpenAI ネイティブファミリー(gpt-*、o1-*、o3-*、o4-*、codex-*)のプレフィックスなしエイリアスは使用できません。`,"cws.err.unsupportedNativeAlias":`ネイティブエイリアスには、現在サポートされている OpenAI の bare model id を指定してください。`,"cws.err.missingNativeAliasDisplayName":`ネイティブエイリアスには表示名が必要です。`,"cws.err.invalidDisplayName":`表示名は 128 文字以内で、制御文字を含めることはできません。`,"cws.err.duplicateAlias":`別のコンボがすでにこのエイリアスを使用しています。`,"cws.err.noTargets":`少なくとも 1 つのターゲットを追加してください。`,"cws.err.incompleteTarget":`各ターゲットにはプロバイダーとモデルが必要です。`,"cws.target.disabled":`{name}(無効)`,"cws.err.reservedNamespace":`combo という物理プロバイダーは、コンボ作成前に名前を変更する必要があります。`,"cws.err.providerCollision":`コンボ ID が設定されたプロバイダー名と衝突しています。`,"cws.err.unknownProvider":`各ターゲットは設定済みプロバイダーを使用する必要があります。`,"cws.err.duplicateTarget":`同じプロバイダー/モデルターゲットは一度しか使用できません。`,"cws.err.invalidStickyLimit":`固定成功数は 1 から 100 の整数にしてください。`,"cws.err.invalidWeight":`各ラウンドロビン重みは 1 から 10000 の整数にしてください。`,"cws.err.noEnabledTarget":`少なくとも 1 つのターゲットは有効なプロバイダーを使用する必要があります。`,"prov.editAlias":`Edit alias`,"prov.aliasPrompt":`Display name (leave empty to clear)`,"prov.aliasSaved":`Alias saved`,"prov.aliasSaveFailed":`Could not save alias`,"prov.accountId":`ID`,"models.customAdd":`Add custom model`,"models.customAddTitle":`Add custom model — {provider}`,"models.customEditTitle":`Edit custom model — {provider}`,"models.customAdded":`Custom model added`,"models.customUpdated":`Custom model updated`,"models.customDeleted":`Custom model deleted`,"models.customSaveFailed":`Failed to save custom model`,"models.customSaving":`Saving…`,"models.customAddBtn":`Add`,"models.customEditBtn":`Update`,"models.customEdit":`Edit`,"models.customDelete":`Delete`,"models.customDeleteConfirm":`Delete the {name} model?`,"models.customBadge":`Custom`,"models.customSummary":`{count} custom`,"models.customFieldModelId":`Model ID (endpoint slug)`,"models.customFieldModelIdPlaceholder":`e.g. qwen4-max-preview`,"models.customFieldDisplayName":`Display name (optional)`,"models.customFieldDisplayNamePlaceholder":`e.g. Qwen 4 Max Preview`,"models.customFieldContext":`Context window`,"models.customFieldModalities":`Input modalities`,"models.customFieldReasoning":`推論努力`,"models.customFieldReasoningOverride":`推論努力を上書き`,"models.reasoningEffort.none":`なし`,"models.reasoningEffort.minimal":`最小`,"models.reasoningEffort.low":`低`,"models.reasoningEffort.medium":`中`,"models.reasoningEffort.high":`高`,"models.reasoningEffort.xhigh":`非常に高`,"models.reasoningEffort.max":`最大`,"models.tipProvider":`Provider`,"models.tipContext":`Context`,"models.tipModalities":`Modalities`,"models.tipStatus":`Status`,"models.tipActive":`Active`,"models.tipDisabled":`Disabled`,"pws.estimatedCost":`Estimated cost`,"pws.costDisclaimer":`API list-price estimate, not an actual charge.`,"pws.modelBreakdown":`Model breakdown`,"pws.col.model":`Model`,"pws.col.cost":`Est. cost`,"pws.col.tokens":`Tokens`,"pws.col.requests":`Req.`,"pws.col.share":`Share`,"pws.tokenInput":`Input`,"pws.tokenOutput":`Output`,"dash.injectionManage":`設定を開く`,"sub.settings":`設定`,"sub.sections":`サブエージェントのセクション`,"sub.delegation.model":`最初に呼ぶモデル`,"sub.delegation.modelHint":`Codex が作業を任せるとき、最初に呼ぶモデルです。上のおすすめが呼べる候補で、ここで選んだものがその中の第一候補になります。`,"dash.syncModelsHint":`接続済みのプロバイダーをもとに Codex のモデルカタログを書き直します。`,"dash.syncRun":`今すぐ同期`,"lab.title":`Compatibility Lab`,"lab.subtitle":`Read-only compatibility verdict matrix from lab projection evidence.`,"lab.loadFailed":`Could not load compatibility lab data`,"lab.projectionUnavailable":`Lab projection is not available. Run conformance or live probes first.`,"lab.projectionIncompatible":`Lab projection schema is incompatible. Rebuild the projection.`,"lab.statusTitle":`Projection status`,"lab.matrixTitle":`Compatibility matrix`,"lab.verdictsTitle":`Verdict records`,"lab.filter.layer":`Evidence layer`,"lab.filter.verdict":`Verdict`,"lab.filter.subject":`Subject ID`,"lab.filter.all":`All`,"lab.col.subject":`Subject`,"lab.col.layer":`Layer`,"lab.col.suite":`Suite`,"lab.col.verdict":`Verdict`,"lab.col.asOf":`As of`,"lab.col.protocol":`Protocol conformance`,"lab.col.live":`Live route compatibility`,"lab.col.task":`Task effectiveness`,"lab.empty":`No compatibility verdicts in the projection yet.`,"lab.subjectKind":`Kind`,"lab.observationCount":`Observations`,"lab.eventCount":`Events`,"lab.verdictCount":`Verdicts`,"lab.subjectCount":`Subjects`,"lab.builtAt":`Built`,"lab.loading":`Loading compatibility evidence…`,"lab.loadMore":`Load more`,"lab.detailTitle":`Verdict detail`,"lab.detailClose":`Close`,"lab.detailSubject":`Subject`,"lab.detailObservations":`Observations`,"lab.detailEvents":`Contributing events`,"lab.detailArtifacts":`Artifact metadata`,"lab.production.title":`観測された本番トラフィック`,"lab.production.notVerification":`ラボ検証ではありません`,"lab.production.attempts":`試行`,"lab.production.successes":`成功`,"lab.production.routeErrors":`ルートエラー`,"lab.production.lastObserved":`最終観測`,"lab.detailLoadFailed":`Could not load verdict detail`,"lab.refresh":`Refresh`,"lab.verdict.UNKNOWN":`Unknown`,"lab.verdict.CLAIMED":`Claimed`,"lab.verdict.PROBED":`Probed`,"lab.verdict.VERIFIED":`Verified`,"lab.verdict.DEGRADED":`Degraded`,"lab.verdict.BLOCKED":`Blocked`,"lab.verdict.UNSUPPORTED":`Unsupported`,"lab.layer.protocol_conformance":`Protocol conformance`,"lab.layer.live_route_compatibility":`Live route compatibility`,"lab.layer.task_effectiveness":`Task effectiveness`,"dash.visionAdvanced":`詳細設定`,"dash.visionMaxDescriptions":`1 ターンあたりの最大説明数`,"dash.visionMaxDescriptionsInvalid":`正の整数を入力してください。`,"dash.visionTimeout":`タイムアウト`,"dash.visionTimeoutInvalid":`{min} から {max} ミリ秒の整数を入力してください。`,"dash.visionAdvancedPopover":`詳細なビジョン設定`,"models.newPolicyGlobal":`新しいモデルを無効で追加`,"models.newPolicyProvider":`新しいモデルのポリシー`,"models.newPolicy_inherit":`継承`,"models.newPolicy_off":`オフ`,"models.newPolicy_on":`オン`,"models.newBadge":`新着`,"models.newCount":`新着 {count} 件、オフ`,"models.aliases":`エイリアス`,"models.aliasesTable":`エイリアス一覧`,"models.aliasPrompt":`プロバイダーのエイリアス(空にすると解除)`,"models.modelAliasPrompt":`モデルのエイリアス(空にすると解除)`,"models.aliasSaved":`エイリアスを保存しました`,"models.aliasConflict":`このエイリアスは既存の名前と競合します`,"models.editProviderAlias":`プロバイダーのエイリアスを編集`,"models.editModelAlias":`モデルのエイリアスを編集`,"models.useDefaultAliases":`既定のエイリアスを使う`,"models.useDefaultAliasesGlobal":`既定のエイリアスを全体で使う`,"models.aliasAuto":`自動`,"models.aliasUser":`ユーザー`,"models.aliasStale":`古い`,"connection.discovering":`Discovering local and shared targets…`,"connection.machineUnavailable":`The local machine plane is unavailable. Shared requests were not redirected locally.`,"connection.disconnect":`Disconnect from hub`,"connection.disconnectConfirm":`Disconnect this machine from the hub and restart it in standalone mode?`,"connection.pairing.title":`Connect this dashboard to the hub`,"connection.pairing.body":`Paste the one-time pairing code created on the hub.`,"connection.pairing.relayWarning":`This code is exchanged through the fixed hub relay. The relay cannot be redirected to another host.`,"connection.pairing.code":`One-time pairing code`,"connection.pairing.submit":`Connect`,"connection.pairing.submitting":`Connecting…`,"connection.pairing.error":`The pairing code was refused or expired. The code was left in place so you can check it.`,"connection.machine.title":`This machine`,"connection.machine.shimHealthy":`Codex shim is healthy.`,"connection.machine.shimNeedsAttention":`Codex shim needs attention.`,"connection.machine.repairShim":`Repair shim`,"connection.machine.removeShim":`Remove shim`,"connection.clients.title":`Connected clients`,"connection.clients.none":`No client status available`,"connection.clients.sync":`Sync now`,"connection.clients.syncing":`Syncing…`,"connection.sessionLogout":`リモートセッションからログアウト`,"connection.sessionLoggingOut":`リモートセッションからログアウト中…`,"connection.sessionLogoutFailed":`リモートセッションからログアウトできませんでした。現在のセッションは維持されています。`,"usage.source.connected":`Source: hub usage`,"usage.source.local":`Source: local usage.jsonl`,"usage.scope.label":`Usage scope`,"usage.scope.machine":`This machine`,"usage.scope.hub":`Hub-wide`,"usage.hubOffline":`Hub usage is unavailable. Local usage was not substituted.`,"integrations.tab.cursor":`Cursor`,"integrations.detail.cursorSeen":`Cursor から最近このプロキシへのリクエストがありました`,"integrations.detail.cursorNeverSeen":`Private Inference はインストール済みですが、まだリクエストはありません`,"integrations.detail.cursorAbsent":`Cursor Private Inference が見つかりません`,"integrations.cursor.title":`Cursor`,"integrations.cursor.intro":`Cursor Private Inference はエージェントをローカルで実行し、loopback 経由で opencodex と通信します。通常版の Cursor では利用できません。バックエンドがカスタムエンドポイントを呼び出すため、公開 HTTPS URL が必要です。このページから Cursor への書き込みは行いません。以下の値を自分で Cursor に貼り付けてください。`,"integrations.cursor.loading":`Cursor の状態を読み込み中…`,"integrations.cursor.unavailable":`プロキシから Cursor の状態を読み取れませんでした。`,"integrations.cursor.detection":`インストール済みのビルド`,"integrations.cursor.privateInference":`Cursor Private Inference`,"integrations.cursor.regular":`Cursor(通常版)`,"integrations.cursor.detected":`検出済み`,"integrations.cursor.notFound":`見つかりません`,"integrations.cursor.regularOnly":`通常版の Cursor のみが見つかりました。カスタムエンドポイントは Cursor のサーバー経由でルーティングされるため、公開トンネルがなければ loopback プロキシには接続できません。Private Inference ビルドについてはガイドを参照してください。`,"integrations.cursor.nothingFound":`通常の場所に Cursor のインストールが見つかりませんでした。別の場所にインストールされている場合でも、以下の値を使用できます。`,"integrations.cursor.gateway":`ゲートウェイの値`,"integrations.cursor.gatewayHint":`Cursor Private Inference で Settings > Models > Gateway を開き、この 2 つの値を貼り付けてから、Refresh model list を押してください。`,"integrations.cursor.baseUrl":`Base URL`,"integrations.cursor.apiKey":`API Key`,"integrations.cursor.apiKeyCredential":`opencodex API キーのいずれか(このバインドには認証情報が必要です)`,"integrations.cursor.copy":`コピー`,"integrations.cursor.copied":`コピーしました`,"integrations.cursor.connection":`接続`,"integrations.cursor.seen":`Cursor からの最終リクエスト: {time} ({ua})`,"integrations.cursor.neverSeen":`プロキシの起動後、Cursor からのリクエストはありません。ゲートウェイを保存したら、Cursor で Refresh model list を押してください。`,"integrations.cursor.models":`Cursor に表示される内容`,"integrations.cursor.modelsHint":`Cursor は独自のモデルテーブルから推論レベルの段階を決めるため、opencodex が示せるのは予測のみです。コンテキスト欄にはデフォルトとオプトインのウィンドウ(Cursor の Max Mode)を表示します。`,"integrations.cursor.ladderFromBundle":`推論レベルの段階は、インストール済みの Cursor Private Inference {version} バンドルから読み取りました。決めるのは Cursor で、opencodex はその表を表示するだけです。`,"integrations.cursor.ladderFromStatic":`推論レベルの段階は Cursor 3.18.25 の静的ミラーです(読み取れる Private Inference のバンドルが見つかりません)。コンテキスト欄はデフォルトとオプトインのウィンドウを示します。`,"integrations.cursor.unknownVersion":`バージョン不明`,"integrations.cursor.noControl":`—`,"integrations.cursor.singleWindow":`単一ウィンドウ`,"integrations.cursor.noControlTitle":`この ID は Cursor 内蔵の effort 表にないため、Cursor は推論コントロールを表示しません。`,"integrations.cursor.effortRowsOne":`effort 行を 1 件公開`,"integrations.cursor.effortRowsMany":`effort 行を {n} 件公開`,"integrations.cursor.effortRowsOff":`effort 行なし`,"integrations.cursor.tableLessHint":`— の行は Cursor で推論コントロールが使えません。cursorEffortRows を有効にすると effort ごとにピッカー項目(id--effort)を公開できます。固定の既定値はプロバイダーの modelDefaultReasoningEfforts で設定します。`,"integrations.cursor.colModel":`モデル`,"integrations.cursor.colReasoning":`推論`,"integrations.cursor.colContext":`コンテキスト`,"integrations.cursor.guide":`Cursor Private Inference のガイドを開く`},Ke={"nav.dashboard":`Gösterge Paneli`,"uptime.day":` gün`,"uptime.hour":` saat`,"uptime.minute":` dk`,"uptime.second":` sn`,"nav.startup":`Başlatma Güvenliği`,"nav.providers":`Sağlayıcılar`,"nav.models":`Modeller`,"nav.combos":`Kombolar`,"nav.subagents":`Alt Ajanlar`,"nav.logs":`Günlükler & Hata Ayıklama`,"nav.usage":`Kullanım`,"common.github":`GitHub`,"sidebar.star":`GitHub'da Yıldız Ver`,"sidebar.starred":`GitHub'da Yıldız Verildi`,"sidebar.starUnauthenticated":`Yıldız vermek için GitHub'ı açın (gh CLI oturum açmamış)`,"sidebar.starFailed":`gh üzerinden yıldız verilemedi. Bunun yerine GitHub açılıyor.`,"sidebar.updateAvailable":`Güncelleme mevcut: {version}`,"sidebar.checkUpdate":`Güncellemeleri kontrol et`,"common.save":`Kaydet`,"common.saving":`Kaydediliyor…`,"common.cancel":`İptal`,"common.discard":`Vazgeç`,"common.delete":`Sil`,"common.close":`Kapat`,"common.ok":`Tamam`,"common.remove":`Kaldır`,"common.loading":`Yükleniyor…`,"common.retry":`Tekrar Dene`,"auth.adminTokenTitle":`OpenCodex yönetici jetonu (OPENCODEX_ADMIN_AUTH_TOKEN)`,"auth.adminAccountLabel":`Hesap`,"auth.adminTokenFieldLabel":`Yönetici jetonu`,"auth.adminTokenRejected":`Bu yönetici jetonu reddedildi. Kontrol edip tekrar deneyin.`,"auth.adminTokenUnavailable":`Yönetici jetonu doğrulanamadı. Tekrar deneyin.`,"app.logoAria":`opencodex logosu`,"app.claudeOn":`Claude AÇIK`,"app.claudeOff":`Claude KAPALI`,"theme.label":`Tema`,"theme.light":`Açık`,"theme.dark":`Koyu`,"theme.system":`Sistem`,"lang.label":`Dil`,"lang.nativeName":`Türkçe`,"provider.name.commandCodeAuth":`Command Code - Auth`,"provider.name.commandCodeApi":`Command Code - API`,"provider.name.volcengine":`Volcengine Ark`,"provider.name.volcengineCodingPlan":`Volcengine Ark Coding Plan`,"provider.name.volcengineAgentPlan":`Volcengine Ark Agent Plan`,"errorBoundary.title":`Sayfa yüklenemedi`,"errorBoundary.message":`Bu bölümde bir işleme hatası oluştu. Yeniden denemek için sayfayı yenileyin.`,"errorBoundary.details":`Hata`,"errorBoundary.reload":`Yeniden Yükle`,"routing.title":`Yönlendirme Zekası (beta)`,"routing.subtitle":`Politika profilleri, simülasyon değerlendirmesi ve kaynak destekli yönlendirme analitiği.`,"routing.loadFailed":`Yönlendirme verileri yüklenemedi`,"routing.empty":"Yapılandırılmış yönlendirme profili yok. config.json dosyasına `routingProfiles` ekleyin.","routing.revision":`revizyon`,"routing.detail":`Profil`,"routing.createProfile":`Profil oluştur`,"routing.dryRunError":`Simülasyon başarısız oldu (HTTP {status})`,"routing.removeConfirm":`{id} profili kaldırılsın mı?`,"routing.unknownEvidence.allow":`izin ver`,"routing.unknownEvidence.penalize":`cezalandır`,"routing.unknownEvidence.exclude":`hariç tut`,"routing.removeCandidate":`{provider}/{model} adayı kaldırılsın mı`,"routing.candidates":`Adaylar`,"routing.require":`Katı gereksinimler`,"routing.optimize":`Optimizasyon ağırlıkları`,"routing.limits":`Limitler`,"routing.unknownEvidence":`Bilinmeyen kanıt politikası`,"routing.compatibility.title":`Uyumluluk politikası`,"routing.compatibility.enabled":`Compatibility Lab kanıtı gerekli`,"routing.compatibility.requiredSuites":`Gerekli test süitleri`,"routing.compatibility.loadingCatalog":`Lab kataloğu yükleniyor…`,"routing.compatibility.catalogUnavailable":`Lab kataloğu kullanılamıyor — test süiti kimliklerini config.json içinde elle girin.`,"routing.compatibility.layer.protocol_conformance":`Protokol uyumu`,"routing.compatibility.layer.live_route_compatibility":`Canlı rota uyumluluğu`,"routing.compatibility.minStatus":`Minimum uyumluluk durumu`,"routing.none":`yok`,"routing.unavailable":`–`,"routing.dryRun":`Simülasyon değerlendirmesi`,"routing.dryRunContext":`İstek bağlam penceresi (jetonlar)`,"routing.dryRunTools":`İstek araç gerektiriyor`,"routing.dryRunImage":`İstek görsel girdisi gerektiriyor`,"routing.dryRunStructured":`İstek yapılandırılmış çıktı gerektiriyor`,"routing.dryRunRun":`Adayları değerlendir`,"routing.candidate":`Aday`,"routing.eligible":`Uygun`,"routing.exclusions":`Hariç Tutulanlar`,"routing.costCap":`Maliyet tavanı`,"routing.capOutcome.satisfied":`limit içinde`,"routing.capOutcome.exceeded":`limit aşıldı`,"routing.capOutcome.unknown-allowed":`bilinmiyor (izinli)`,"routing.capOutcome.unknown-excluded":`bilinmiyor (hariç tutuldu)`,"routing.exclusion.capability-unsatisfied":`yetenek karşılanmadı`,"routing.exclusion.unknown-capability":`bilinmeyen yetenek`,"routing.exclusion.cost-limit":`maliyet tavanı aşıldı`,"routing.exclusion.cost-limit-unknown":`maliyet tavanı doğrulanamadı`,"routing.exclusion.cooldown":`soğuma süresi`,"routing.exclusion.unknown-health":`bilinmeyen sağlık`,"routing.exclusion.unknown-quota":`bilinmeyen kota`,"routing.exclusion.unknown-price":`bilinmeyen fiyat`,"routing.exclusion.other":`hariç tutma: {code}`,"routing.score":`Puan`,"routing.selected":`seçildi`,"routing.yes":`evet`,"routing.no":`hayır`,"routing.analytics":`Yönlendirme analitiği`,"routing.analyticsTotal":`İstekler`,"routing.analyticsSuccessRate":`Başarı`,"routing.analyticsFallbackRate":`Yedekleme`,"routing.analyticsP50":`p50`,"routing.analyticsP95":`p95`,"routing.analyticsP99":`p99`,"routing.analyticsCooldown":`Soğuma süresi hataları`,"routing.analyticsConfidence":`Güvenilirlik`,"routing.analyticsTruncated":`kısaltılmış geçmiş`,"routing.analyticsRequests":`İstekler`,"routing.analyticsEmpty":`Henüz analitik verisi yok — önce birkaç istek gönderin.`,"startup.title":`Başlatma güvenliği`,"startup.subtitle":`Yerel proxy yönlendirmesi bir yeniden bağlanma döngüsüne girmeden önce Codex'in yeniden başlatmanın ardından opencodex'e erişebildiğini doğrulayın.`,"startup.refresh":`Yenile`,"startup.backToDashboard":`Gösterge Paneline Dön`,"startup.loading":`Başlatma koruması kontrol ediliyor…`,"startup.error":`Başlatma koruması okunamadı.`,"startup.staleData":`Son başlatma kontrolü başarısız oldu. Aşağıdaki değerler güncel değildir ve koruma kanıtı olarak kabul edilmemelidir.`,"startup.status.native":`Yerel yönlendirme`,"startup.status.protected":`Yeniden başlatma korumalı`,"startup.status.atRisk":`Eylem gerekiyor`,"startup.summary.native":`Codex yerel proxy'ye bağımlı değildir`,"startup.summary.protected":`opencodex yeniden başlatmanın ardından kullanılabilir olacaktır`,"startup.summary.atRisk":`Codex yeniden başlatmanın ardından model erişimini kaybedebilir`,"startup.riskDetail":`Codex yerel proxy'ye sabitlenmiş, ancak kalıcı bir servis veya sağlıklı başlatıcı shim bunu tekrar başlatmayacak.`,"startup.riskDetailCustomLocal":`Codex özel bir yerel ağ geçidine işaret ediyor. opencodex bu ağ geçidinin yeniden başlatma yaşam döngüsünü yönetemez veya doğrulayamaz.`,"startup.riskDetailWindowsShim":`Başlatıcı shim desteklenen CLI betiklerini korur, ancak Codex Desktop ve doğrudan codex.exe başlatmaları Windows'ta bunu atlayabilir.`,"startup.safeDetail":"Mevcut yönlendirme ve başlatma mekanizması tutarlıdır. Yeniden başlatmanın ardından manuel `ocx start` komutuna gerek yoktur.","startup.routing":`Codex yönlendirmesi`,"startup.routing.proxy":`Yerel proxy`,"startup.routing.native":`Yerel OpenAI`,"startup.routing.customLocal":`Özel yerel ağ geçidi`,"startup.routing.customRemote":`Özel uzak ağ geçidi`,"startup.routing.unknown":`Bilinmeyen veya geçersiz yönlendirme`,"startup.restartProtection":`Yeniden başlatma koruması`,"startup.preference":`İsteğe bağlı başlatma`,"startup.enabled":`Etkin`,"startup.disabled":`Devre dışı`,"startup.protection.service":`Arka plan servisi`,"startup.protection.shim":`Başlatıcı shim`,"startup.protection.none":`Yüklü değil`,"startup.details":`Koruma detayları`,"startup.service":`Arka plan servisi`,"startup.serviceHint":`Oturum açmada başlar ve çökmenin ardından proxy'yi yeniden başlatır.`,"startup.installed":`Yüklü`,"startup.notInstalled":`Yüklü değil`,"startup.unsupported":`Desteklenmiyor`,"startup.shim":`Codex başlatıcı shim`,"startup.shimHint":"Desteklenen bir Codex betik başlatıcısı başladığında `ocx ensure` çalıştırır.","startup.healthy":`Sağlıklı`,"startup.cliOnly":`Yalnızca CLI`,"startup.stale":`Eski`,"startup.viable":`Hazır`,"startup.unhealthy":`Yüklü ama sağlıksız`,"startup.conflict":`Servis çakışması`,"startup.installedDisabled":`Yüklü ama devre dışı`,"startup.install":`Yükle`,"startup.installing":`Yükleniyor…`,"startup.repair":`Onar`,"startup.repairing":`Onarılıyor…`,"startup.serviceInstalled":`Arka plan servisi başarıyla yüklendi.`,"startup.serviceRepaired":`Arka plan servisi başarıyla onarıldı.`,"startup.shimInstalled":`Codex başlatıcı shim başarıyla yüklendi.`,"startup.shimRepaired":`Codex başlatıcı shim başarıyla onarıldı.`,"startup.installFailed":`Yükleme başarısız oldu:`,"startup.tray.title":`Windows sistem tepsisi`,"startup.tray.hint":`Tek tıkla proxy başlatma, durdurma, yeniden başlatma, panel ve durum kontrolleri için oturum tepsisi simgesi yükleyin.`,"startup.tray.login":`Windows oturum açılışında tepsiyi başlat`,"startup.tray.notProtection":`Tepsi bir denetleyicidir, yeniden başlatma koruması değildir. İnsansız proxy kurtarma için hâlâ geçerli bir arka plan servisi gereklidir.`,"startup.tray.running":`Çalışıyor`,"startup.tray.stopped":`Yüklü, gizli`,"startup.tray.stale":`Onarım gerekiyor`,"startup.tray.notInstalled":`Yüklü değil`,"startup.tray.loading":`Kontrol ediliyor…`,"startup.tray.unavailable":`Durum kullanılamıyor`,"startup.tray.install":`Yükle ve tepsiyi göster`,"startup.tray.start":`Tepsi simgesini göster`,"startup.tray.stop":`Tepsi simgesinden çık`,"startup.tray.uninstall":`Oturum tepsisini kaldır`,"startup.tray.error":"Windows tepsi eylemi başarısız oldu. Detaylar için `ocx tray status` kontrol edin.","startup.recovery":`Onarım seçenekleri`,"startup.recoveryHint":`Yukarıdaki tek tıkla yükleyicileri kullanın veya manuel onarım için komutu kopyalayın. Codex Desktop ve Windows yürütülebilir dosyaları için arka plan servisi önerilir.`,"startup.command.service":`Önerilen: kalıcı arka plan servisi`,"startup.command.shim":`Alternatif: CLI başlatıcı shim`,"startup.command.native":`Güvenli mod: yerel Codex yönlendirmesini geri yükle`,"startup.copy":`Kopyala`,"startup.copied":`Kopyalandı`,"startup.recommended":`Önerilen onarım: {cmd}`,"startup.navRisk":`Başlatma koruması dikkat gerektiriyor`,"startup.codexRuntime.clampHidden":`OpenCodex, Codex {version} kullandığı için bazı akıl yürütme çabası seçenekleri gizlendi.`,"startup.codexRuntime.clampHiddenWithEfforts":`OpenCodex, Codex {version} kullandığı için bazı akıl yürütme çabası seçenekleri gizlendi (kaldırılanlar: {efforts}).`,"startup.codexRuntime.olderBinary":`OpenCodex eski bir Codex ikili dosyası ({version}) kullanıyor. Daha yeni bir kurulum mevcut.`,"dash.subtitle":`Yerel opencodex proxy'sinin, sağlayıcılarının ve Codex'e yönlendirilen modellerin canlı durumu.`,"dash.workspace.overview":`Genel Bakış`,"dash.workspace.sections":`Bölümler`,"dash.status":`Durum`,"dash.online":`Çevrimiçi`,"dash.offline":`Çevrimdışı`,"dash.version":`Sürüm`,"dash.uptime":`Çalışma Süresi`,"dash.providers":`Sağlayıcılar`,"dash.tokens30d":`Jetonlar (30 gün)`,"dash.coverage":`%{pct} kapsam`,"dash.mem.title":`Bellek izlenebilirliği`,"dash.mem.hint":`Salt okunur çalışma zamanı tanılamaları. Gözlemlenen bellek max(RSS, harici, ArrayBuffers) değeridir.`,"dash.mem.rss":`Yerleşik küme (RSS)`,"dash.mem.jsHeap":`Kullanımdaki JS yığını`,"dash.mem.jsHeapArena":`arena {total}`,"dash.mem.pressure":`Uyarı eşiğine göre`,"dash.mem.pressureOf":`Eşiğin %{pct}'si`,"dash.mem.pressureUnknown":`Bildirilen eşik yok`,"dash.mem.jscHeap":`JSC yığını`,"dash.mem.external":`Harici`,"dash.mem.arrayBuffers":`ArrayBuffers`,"dash.mem.observed":`Gözlemlenen`,"dash.mem.runtime":`Çalışma zamanı sayaçları`,"dash.mem.growth":`Saatlik gözlemlenen kayma`,"dash.mem.perHour":`/saat`,"dash.mem.store":`Devamlılık deposu`,"dash.mem.storeHint":`Proxy previous_response_id önbelleği.`,"dash.mem.storeEntries":`Girdiler`,"dash.mem.storeTotal":`Toplam`,"dash.mem.storeLargest":`En büyük`,"dash.mem.storeOldest":`En eski`,"dash.mem.threshold":`Uyarı eşiği`,"dash.mem.lastWarn":`Son uyarı`,"dash.mem.never":`Hiçbir zaman`,"dash.mem.details":`Detaylar`,"dash.mem.unavailable":`Bellek tanılaması kullanılamıyor (eski proxy).`,"dash.mem.inFlight":`İşlemdeki istekler`,"dash.mem.restart":`Boşalt & yeniden başlat`,"dash.mem.restartConfirm":`{count} işlemdeki isteğin tamamlanmasını bekleyin, ardından yeniden başlatın ({seconds} saniyeye kadar).`,"dash.mem.draining":`{count} istek boşaltılıyor… tamamlandığında yeniden başlatılacak`,"dash.mem.reconnecting":`Proxy yeniden başlatılıyor… yeniden bağlanması bekleniyor`,"dash.mem.restartFailed":`Boşaltma ve yeniden başlatma başarısız oldu. Proxy'nin çalıştığını kontrol edin.`,"dash.mem.restartNoSupervisor":`Yeniden başlatma koruması algılanmadı.`,"dash.activeProviders":`Aktif sağlayıcılar`,"dash.noProviders":`Yapılandırılmış sağlayıcı yok. {cmd} komutunu çalıştırın.`,"dash.col.name":`İsim`,"dash.col.adapter":`Adaptör`,"dash.col.baseUrl":`Taban URL`,"dash.col.model":`Model`,"dash.modelsNoResults":`Aramanızla eşleşen model bulunamadı.`,"dash.availableModels":`Kullanılabilir modeller`,"dash.noModels":`Model bulunamadı. Sağlayıcı API anahtarlarını kontrol edin.`,"dash.cannotConnect":`Proxy'ye bağlanılamıyor. Çalışıyor mu?`,"dash.runStart":`Proxy'yi başlatmak için {cmd} çalıştırın.`,"dash.stop":`Proxy'yi Durdur`,"dash.stopConfirm":`Proxy durdurulsun ve yerel Codex geri yüklensin mi?`,"dash.stopFailed":`Proxy durdurulamadı (HTTP {status}).`,"dash.maSwitchFailed":`Mod değiştirme başarısız oldu (HTTP {status}).`,"dash.maNetworkError":`Ağ hatası — proxy çalışıyor mu?`,"dash.stopping":`Durduruluyor…`,"dash.actions":`Proxy`,"dash.codexRestart":`Codex model listesini yenile`,"dash.codexRestarting":`Durduruluyor…`,"dash.codexRestartConfirm":`Model listesini yeniden okumaları için Codex app-server'ları durdurulsun mu? Süren bir Codex işlemi kesilir ve Codex kendiliğinden yeniden başlamaz — sonrasında yeniden açın.`,"dash.codexRestartDone":`{count} Codex app-server durduruldu. Güncel model listesi için Codex'i yeniden açın.`,"dash.codexRestartNothing":`Çalışan Codex app-server yok. Sonraki açılışta güncel model listesi okunur.`,"dash.codexRestartUnknown":`Süreçler listelenemedi, bu yüzden hiçbir şey durdurulmadı.`,"dash.codexRestartPartial":`{count} app-server kapanmadı. Model listesi eski kalırsa bunları elle durdurun.`,"dash.codexRestartFailed":`Codex model listesi yenilenemedi (HTTP {status}).`,"dash.codexRestartUnreachable":`Proxy'ye ulaşılamadı.`,"dash.codexRestartMalformed":`Proxy beklenmeyen bir yanıt döndürdü.`,"dash.codexRestartTimeout":`Proxy zamanında yanıt vermedi. App-server'ları durdurmaya devam ediyor olabilir.`,"models.staleBanner":`Codex, bu katalogdan daha eski bir model listesi gösteriyor. Yeniden okumak için Codex'i yeniden başlatın.`,"dash.codexAutoStart":`opencodex'i Codex ile başlat`,"dash.codexAutoStartHint":`Yüklü bir shim'in ocx ensure çalıştırmasına izin verir. Arka plan servisi veya yeniden başlatma koruması kurmaz; sistem durumu için Başlatma Güvenliği'ne bakın.`,"dash.searchModel":`Arama yan araç modeli`,"dash.searchModelHint":`OpenAI dışı yönlendirilen modellerde web_search için kullanılan model. ChatGPT girişi gerektirir.`,"dash.searchReasoning":`Arama akıl yürütme çabası`,"dash.visionModel":`Görsel yan araç modeli`,"dash.visionModelHint":`Salt metin yönlendirilen modeller için görselleri tanımlamakta kullanılan model. ChatGPT girişi gerektirir.`,"dash.webSearchSidecar":`Web arama yan aracı (sidecar)`,"dash.webSearchSidecarHint":`Yönlendirilen modellerde web araması için kullanılan arka ucu ve modeli seçin.`,"dash.webSearchStream":`Yanıtları canlı akıt`,"dash.webSearchStreamHint":`Model bir araç çağrısına karar verene kadar baştaki metni ve akıl yürütmeyi canlı akıtır; kalanı arama yakalama için arabelleğe alınır. Aramadan önce yazılan metin kısmen tekrarlanabilir.`,"dash.visionSidecar":`Görsel yan aracı (sidecar)`,"dash.visionSidecarHint":`Salt metin modeller için görselleri tanımlamakta kullanılan arka ucu ve modeli seçin.`,"dash.visionOff":`Kapalı`,"dash.shadowCallIntercept":`Gölge Çağrı Yakalama`,"dash.shadowCallInterceptHint":`Codex App'in arka plan yardımcı çağrılarını ({models}) başlık oluşturma ve commit mesajları için yakalar ve seçtiğiniz modele yönlendirir.`,"dash.shadowCallWarning":`⚠ Etkinleştirildiğinde, {models} için olan TÜM istekler seçilen modelle değiştirilecektir.`,"dash.shadowCallOriginal":`Orijinal`,"dash.shadowCallModel":`Yedek model`,"dash.shadowCallTooltip":`Codex App arka planda başlık ve commit mesajı oluşturmak için yardımcı çağrılar yapar (orijinal {models}). Etkinleştirerek bunları seçtiğiniz modele yönlendirebilirsiniz.`,"models.shadowCallIntercept":`Gölge Çağrı Yakalama`,"models.shadowCallInterceptHint":`Codex App'in başlıklar ve commit mesajları için yaptığı arka plan çağrılarını ({models}) yakalar ve seçtiğiniz modele yönlendirir.`,"dash.sidecarBackend":`Arka uç`,"dash.sidecarModel":`Model`,"dash.backendAuto":`Otomatik`,"dash.backendOpenAI":`OpenAI`,"dash.backendAnthropic":`Anthropic`,"dash.sidecarSaved":`Yan araç ayarları kaydedildi. Sonraki istekte uygulanacak.`,"dash.sidecarSaveFailed":`Yan araç ayarları kaydedilemedi.`,"dash.injectionLabel":`Alt ajan devri`,"dash.injectionHint":`Codex'in alt ajan işlerini devredeceği modeli seçin.`,"dash.injectionManage":`Ayarları aç`,"dash.syncCodexSubagentDefaults":`Ayrıca Codex varsayılanı olarak kaydet`,"dash.syncCodexSubagentDefaultsHint":`Açık olduğunda, yukarıdaki seçim Codex'in kendi yapılandırmasına yazılır.`,"dash.multiAgentGuidance":`Codex'e işi nasıl böleceğini söyle`,"dash.multiAgentGuidanceHint":`Codex'e işleri alt ajanlara nasıl devredeceğini söyleyen kısa bir not gönderir.`,"dash.injectionNone":`Yok`,"dash.injectionEffortLabel":`Akıl yürütme çabası`,"dash.injectionEffortNone":`Model varsayılanı`,"dash.effortCapLabel":`V2 ultra çaba limiti`,"dash.subagentEffortCapLabel":`V2 alt ajan çaba limiti`,"dash.effortCapHelp":`V2 ultra modu turları için akıl yürütme çabasını sınırlar. Ayarlandığında, gelen maksimum çaba istekleri (ultra modundan) seçilen seviye ile sınırlandırılır. Alt ajan limiti yalnızca türetilen çocuk ajanları etkiler. Limitler çabayı yalnızca düşürür, asla yükseltmez. Bir model sınırlandırılan seviyeyi desteklemiyorsa, desteklenen en yakın alt seviyeye iner.`,"dash.effortCapNone":`Limit yok`,"dash.maintenance":`Bakım`,"dash.maintenanceHint":`Codex'in model kataloğunu yenileyin veya daha yeni bir opencodex sürümü yükleyin.`,"dash.syncModels":`Modelleri senkronize et`,"dash.syncModelsHint":`Bağladığınız sağlayıcılardan Codex'in model kataloğunu yeniden yazın.`,"dash.syncRun":`Şimdi senkronize et`,"dash.syncing":`Senkronize ediliyor…`,"dash.syncOk":`Senkronizasyon tamamlandı. {count} model eklendi.`,"dash.syncStaleHint":`Codex hâlâ eski bir liste gösteriyorsa uygulama sunucusunu yeniden başlatın ({cmd}).`,"dash.syncFailed":`Senkronizasyon başarısız oldu: {error}`,"dash.projectConfigTitle":`Proje Codex konfigürasyonu OpenCodex'i atlıyor`,"dash.projectConfigHint":`Bu depoya özel ayarlar OpenCodex proxy'sini geçersiz kılar.`,"dash.checkUpdate":`Güncellemeyi kontrol et`,"dash.updateTitle":`opencodex'i güncelle`,"dash.updateDesc":`Seçilen kanal için npm'i kontrol edin.`,"dash.updateChannel":`Kanal`,"dash.updateChecking":`Güncellemeler kontrol ediliyor…`,"dash.updateInstalled":`Yüklü`,"dash.updateLatest":`En son`,"dash.updateAvailable":`Güncelleme mevcut`,"dash.updateCurrent":`Güncel`,"dash.updateCommand":`Komut`,"dash.updateSource":`Bu bir kaynak kod kopyasıdır. Terminalden güncelleyin.`,"dash.updateUnavailable":`npm'den en son sürüm okunamadı.`,"dash.updateRetry":`Tekrar dene`,"dash.updateRecheck":`Yeniden kontrol et`,"dash.updateCannotAuto":`Tek tıkla güncelleme kullanılamıyor ({reason}).`,"dash.updateReason.source_checkout":`kaynak kod kopyası`,"dash.updateReason.latest_unavailable":`npm sunucusuna ulaşılamıyor`,"dash.updateReason.already_latest":`zaten en son sürümde`,"dash.updateReason.unknown":`güncelleme kullanılamıyor`,"dash.updateRestart":`Güncellemeden sonra yeniden başlat`,"dash.updateRestartHint":`Önerilir. Proxy yeniden başlayana kadar mevcut GUI eski kodu çalıştırmaya devam eder.`,"dash.runUpdate":`Güncelle`,"dash.updateReconnecting":`Yeniden başlatılan proxy bekleniyor…`,"dash.updateStatus.running":`opencodex güncelleniyor.`,"dash.updateStatus.restarting":`Güncelleme yüklendi. Proxy yeniden başlatılıyor.`,"dash.updateStatus.succeeded":`Güncelleme tamamlandı.`,"dash.updateVersionTransition":`{currentVersion} -> {latestVersion}.`,"dash.updateStatus.failed":`Güncelleme başarısız oldu.`,"prov.subtitle":`opencodex'in Codex'e yönlendirdiği sağlayıcıları yapılandırın.`,"prov.add":`Sağlayıcı Ekle`,"prov.editJson":`JSON Düzenle`,"prov.accountLogin":`Hesap girişi`,"prov.noOauth":`Kullanılabilir OAuth sağlayıcısı yok.`,"prov.loggedIn":`giriş yapıldı`,"prov.notLoggedIn":`giriş yapılmadı`,"prov.logout":`Çıkış Yap`,"prov.login":`Giriş Yap`,"prov.loginWith":`{provider} ile Giriş Yap`,"prov.waitingBrowser":`Tarayıcı bekleniyor…`,"prov.didntOpen":`Açılmadı mı? Buraya tıklayın`,"prov.copyLink":`Bağlantıyı kopyala`,"prov.dontOpenBrowser":`Proxy makinesinde tarayıcı açma`,"prov.dontOpenBrowserHint":`Farklı bir tarayıcı profili için ya da pano proxy'nin makinesinde değilken kullanışlıdır.`,"prov.linkCopied":`Kopyalandı`,"prov.linkCopyUnavailable":`Pano kullanılamıyor`,"prov.deviceCode":`Cihaz kodu`,"prov.copyCode":`Kodu kopyala`,"prov.codeCopied":`Kod kopyalandı`,"prov.editAlias":`Takma adı düzenle`,"prov.aliasPrompt":`Görüntülenen ad (temizlemek için boş bırakın)`,"prov.aliasSaved":`Takma ad kaydedildi`,"prov.aliasSaveFailed":`Takma ad kaydedilemedi`,"prov.accountId":`ID`,"prov.pasteRedirect":`Yönlendirme URL'sini veya kodu yapıştırın`,"prov.pasteRedirectHint":`Tarayıcı localhost hatası gösterirse adres çubuğundaki URL'yi kopyalayıp buraya yapıştırın.`,"prov.pasteSubmit":`Gönder`,"prov.pasteSubmitting":`Gönderiliyor…`,"prov.pasteOk":`Kod gönderildi — giriş tamamlanıyor…`,"prov.pasteFail":`Kod gönderilemedi: {error}`,"prov.port":`Port`,"prov.default":`Varsayılan`,"prov.loadingConfig":`Yükleniyor…`,"prov.saved":`Kaydedildi! Uygulamak için proxy'yi yeniden başlatın.`,"prov.loadConfigFail":`Konfigürasyon yüklenemedi`,"prov.invalidJson":`Geçersiz JSON`,"prov.saveFailed":`Kaydetme başarısız`,"prov.loginFailStart":`{provider} girişi başlatılamadı`,"prov.loginError":`{provider} giriş hatası: {error}`,"prov.loginRequestFail":`{provider} giriş isteği başarısız oldu`,"prov.loginCancelled":`{provider} girişi iptal edildi`,"prov.loginTimeout":`{provider} girişi zaman aşımına uğradı.`,"prov.loginSameAccount":`Hâlâ aynı {provider} hesabı — tarayıcıda hesap değiştirin, ardından tekrar Hesap Ekle'yi deneyin.`,"prov.loginOk":`{provider} hesabına giriş yapıldı. Modellerini listelemek için {cmd} çalıştırın (veya canlı olarak uygulanır).`,"prov.added":`"{name}" eklendi. Modellerini listelemek için {cmd} çalıştırın (veya canlı olarak uygulanır).`,"oauthTos.highTitle":`{provider}: abonelik OAuth riski`,"oauthTos.elevatedTitle":`{provider}: gayri resmi OAuth köprüsü`,"oauthTos.anthropicBody":`Claude abonelik OAuth jetonlarının OpenCodex gibi üçüncü taraf bir proxy üzerinden doğrudan yeniden kullanılması desteklenen bir Anthropic entegrasyonu değildir ve erişim kısıtlamalarına yol açabilir. Claude aboneliklerini kullanan desteklenen Agent SDK entegrasyonları ayrıdır.`,"oauthTos.highBody":`OpenCodex, {provider} bağlantısını üçüncü taraf bir OAuth yolu üzerinden kurar. Desteklenmeyen kullanım, erişim kısıtlamalarına veya hesabın askıya alınmasına yol açabilir.`,"oauthTos.elevatedBody":`OpenCodex, {provider} bağlantısını resmi olmayan bir OAuth yolu üzerinden kurar. Mümkün olduğunda resmi istemciyi kullanın; sıra dışı veya otomatik trafik kötüye kullanım olarak değerlendirilebilir ve erişim kısıtlanabilir veya askıya alınabilir.`,"oauthTos.saferPath":`Daha güvenli seçenek: OpenCodex'te bir API anahtarı yapılandırın.`,"oauthTos.acknowledge":`Riski anlıyorum ve OAuth ile devam etmek istiyorum.`,"oauthTos.continue":`OAuth ile devam et`,"prov.logoutOk":`{provider} çıkışı yapıldı.`,"prov.logoutFail":`{provider} çıkışı yapılamadı.`,"prov.removed":`"{name}" kaldırıldı.`,"prov.removedDefault":`"{name}" kaldırıldı. Varsayılan sağlayıcı artık "{defaultProvider}".`,"prov.removeFail":`"{name}" kaldırılamadı.`,"prov.removeLastProvider":`Başka etkin sağlayıcı yokken bu sağlayıcıyı kaldıramazsınız.`,"prov.removeHasDependentCombos":`Önce bağımlı komboları kaldırın veya güncelleyin: {combos}.`,"prov.setDefault":`Varsayılan olarak ayarla`,"prov.setDefaultSuccess":`"{name}" artık varsayılan sağlayıcı.`,"prov.setDefaultFail":`"{name}" varsayılan yapılamadı.`,"prov.defaultDisabled":`Varsayılan yapmadan önce bu sağlayıcıyı etkinleştirin.`,"prov.updateFail":`Sağlayıcı güncellenemedi.`,"prov.networkError":`Ağ hatası. Proxy'nin çalıştığını kontrol edin.`,"prov.removeConfirm":`"{name}" sağlayıcısı kaldırılsın mı?`,"prov.hasApiKey":`API anahtarı yapılandırıldı`,"prov.hasHeaders":`özel başlıklar yapılandırıldı`,"prov.accounts":`Hesaplar ({n})`,"prov.accountsAria":`{name} hesaplarını aç/kapat`,"prov.accountActive":`Aktif`,"prov.accountReauth":`Tekrar giriş yap`,"prov.reauthenticate":`Yeniden doğrula`,"prov.reauthAccountMissing":`Seçilen hesap bulunamadı`,"prov.reauthIdentityMismatch":`Giriş yapılan hesap seçilen hesapla eşleşmedi`,"prov.accountAdd":`Hesap ekle`,"prov.accountNoLabel":`hesap {id}`,"prov.accountSwitchTitle":`Bu hesabı kullan`,"prov.accountSwitched":`{email} hesabına geçildi.`,"prov.accountSwitchFail":`Hesap değiştirilemedi`,"prov.accountRemoved":`{email} kaldırıldı.`,"prov.accountRemoveFail":`{email} kaldırılamadı.`,"prov.accountRemoveAria":`{email} hesabını kaldır`,"prov.accountRemoveConfirm":`{email} hesabı kaldırılsın mı?`,"prov.keyAdd":`API anahtarı ekle`,"prov.keyAdded":`{name} için API anahtarı eklendi.`,"prov.keyAddFail":`API anahtarı eklenemedi`,"prov.keyPlaceholder":`API anahtarını yapıştırın`,"prov.keySwitchTitle":`Bu anahtarı kullan`,"prov.keySwitched":`{key} anahtarına geçildi.`,"prov.keySwitchFail":`Anahtar değiştirilemedi`,"prov.keyRemoved":`{key} anahtarı kaldırıldı.`,"prov.keyRemoveAria":`{key} anahtarını kaldır`,"prov.keyRemoveConfirm":`{key} API anahtarı silinsin mi?`,"prov.activeBadge":`Aktif`,"prov.disabledBadge":`Devre Dışı`,"prov.defaultBadge":`Varsayılan`,"prov.enable":`Etkinleştir`,"prov.disable":`Devre Dışı Bırak`,"prov.enabled":`"{name}" etkinleştirildi.`,"prov.disabled":`"{name}" devre dışı bırakıldı.`,"prov.enableFail":`"{name}" etkinleştirilemedi.`,"prov.disableFail":`"{name}" devre dışı bırakılamadı.`,"prov.enableAria":`{name} sağlayıcısını etkinleştir`,"prov.disableAria":`{name} sağlayıcısını devre dışı bırak`,"prov.defaultCannotDisable":`Varsayılan sağlayıcı devre dışı bırakılamaz`,"prov.openaiAccountMode":`Codex hesap modu`,"prov.openaiModePool":`Havuz (Pool)`,"prov.openaiModeDirect":`Doğrudan (Direct)`,"prov.openaiPoolDesc":`Varsayılan. Ana girişi ve eklenen hesapları havuzda döndürür.`,"prov.openaiDirectDesc":`Yalnızca ana Codex girişini kullanır.`,"prov.openaiModeSaved":`OpenAI hesap modu {mode} olarak değiştirildi.`,"prov.openaiModeSaveFailed":`OpenAI hesap modu değiştirilemedi.`,"prov.openaiApiDesc":`OpenAI API anahtarı kullanır.`,"prov.manageCodexAccounts":`Codex hesaplarını yönet`,"prov.openaiApiMissing":`API anahtarı gerekli`,"prov.openaiApiSetup":`API anahtarı ayarla`,"models.tab.catalog":`Modeller`,"models.tab.combos":`Kombolar`,"models.tab.compatibility":`Uyumluluk`,"models.tab.routing":`Yönlendirme (beta)`,"models.tabsLabel":`Model yüzeyleri`,"models.subtitle.combos":`Tek bir kimlik olarak yanıt veren sıralı model grupları. Hedefleri failover ile zincirleyin veya yükü dengeleme stratejisiyle dağıtın.`,"models.subtitle.compatibility":`Lab projeksiyon kanıtından salt okunur uyumluluk matrisi.`,"models.subtitle.routing":`Politika profilleri ve simülasyon değerlendirmesi.`,"models.subtitle":`Codex'in göreceği modelleri açıp kapatın.`,"models.nativeGroupLabel":`Yerel OpenAI`,"models.nativeHint":"Doğrudan geçiş modelleri Sağlayıcılar bölümünde seçilen Havuz veya Doğrudan seçeneğini kullanır. Buradan model eklemek, yeni bir düz passthrough kimliği değil, yönlendirilmiş bir `openai/` seçici kaydeder.","models.active":`{active}/{total} görünür`,"models.workspace.providers":`Sağlayıcılar`,"models.workspace.allProviders":`Tüm sağlayıcılar`,"models.workspace.mainAria":`Model detayları`,"models.allOn":`Tümünü aç`,"models.allOff":`Tümünü kapat`,"models.presetLabel":`Modeller`,"models.presetMode_preset":`Ön ayar`,"models.presetMode_all":`Tümü`,"models.presetMode_custom":`Özel`,"models.presetSummary":`{total} modelden {count} tanesi gösteriliyor — çekirdek ön ayar v{version}`,"models.presetUpdateAvailable":`Ön ayar v{version} mevcut`,"models.presetAppliedToast":`{provider}: ön ayar uygulandı — {count} model seçildi`,"models.presetClearedToast":`{provider}: tüm modeller gösteriliyor`,"models.presetEmpty":`{provider}: ön ayar hiçbir modelle eşleşmedi — seçim değişmedi`,"models.presetConfirmReplace":`Seçiminiz {count} modellik ön ayarla değiştirilsin mi?`,"models.cap350k":`350k Sınırı`,"models.capApplied":`Bağlam sınırı uygulandı.`,"models.capSaveFailed":`Bağlam sınırı kaydedilemedi`,"models.contextCapped":`350k sınırı`,"models.contextCapLabel":`Varsayılan pencere / sınır`,"models.v2Label":`Alt Ajan`,"models.shadowCallOriginal":`⚠ {models} →`,"models.v2DocsLink":`v1 / v2 nedir?`,"models.v2Mode_v1":`v1`,"models.v2Mode_default":`taban`,"models.v2Mode_v2":`v2`,"models.v2ModeDesc_v1":`Tüm modeller → v1 yüzeyi`,"models.v2ModeDesc_default":`Yukarı akış varsayılanları`,"models.v2ModeDesc_v2":`Tüm modeller → v2 yüzeyi`,"models.keepNativeOnV1":`ChatGPT v1'de kalsın`,"models.keepNativeOnV1Hint":`ChatGPT yerel ebeveynleri v2 çocuk görevlerini şifreler; Grok ve Claude okuyamaz. Sol/Terra yönlendirilmiş modelleri spawn edecekse açık bırakın. Yönlendirilmiş ebeveynler v2'de kalır.`,"models.v2Help":`v1 alt ajanları birincil modelle sınırlandırır; taban standart ajan sınırlarını devralır; v2 tam çoklu ajan orkestrasyonunu etkinleştirir. - -v2'de ChatGPT v1'de kalsın, Sol/Terra'yı v1'de bırakır; böylece Grok veya Claude spawn edebilirler. ChatGPT v2 çocuk görevlerini şifreler; yönlendirilmiş modeller okuyamaz. Yönlendirilmiş ebeveynler v2'de kalır. - -Değişiklikler yeni oturumlara uygulanır.`,"dash.multiAgent":`Alt Ajan`,"models.v2Conflict":`[agents] max_threads ayarlanmış — config.toml dosyasından kaldırın`,"models.v2Applied":`Çoklu ajan modu güncellendi. Yeni oturumlar bu modu kullanacaktır; model seçiciyi yenilemek için Codex uygulamasını yeniden başlatın.`,"models.v2ThreadsLabel":`Maksimum iş parçacığı`,"models.v2ThreadsDefault":`varsayılan (4)`,"models.v2ThreadsApplied":`İş parçacığı limiti güncellendi`,"models.v2ThreadsInvalid":`İş parçacığı limiti >= 1 tamsayı olmalıdır`,"models.v2ThreadsApply":`Uygula`,"models.capValue":`Varsayılan {value}`,"models.contextSettings":`Özel pencereler`,"models.contextSettingsTitle":`Özel pencereler — {provider}`,"models.contextDefault":`Sağlayıcı varsayılanı`,"models.contextModel":`Model`,"models.contextModelOverride":`Model geçersiz kılma`,"models.contextHint":`Pencereyi biliyorsanız gerçek Codex penceresini buraya yazın. Üst akış değer yoksa bu kullanılır; daha büyük bildirilen pencere düşürülür, daha küçük olan korunur. Boş bırakırsanız sağlayıcının «Varsayılan pencere / sınır» değeri kullanılır; o sınır kapalıysa 128k olur.`,"models.contextAutomatic":`Otomatik keşif`,"models.contextSaved":`Bağlam pencereleri güncellendi.`,"models.contextUnchanged":`Kaydedilecek bağlam penceresi değişikliği yok.`,"models.contextSaveFailed":`Bağlam pencereleri kaydedilemedi`,"models.contextInvalid":`Bağlam pencereleri pozitif tam sayılar olmalıdır`,"models.contextCappedValue":`{value} sınırı`,"models.setAll":`Tümünü ayarla`,"models.setAllHint":`Her yönlendirilen sağlayıcıda {value} varsayılan pencereyi açar. Röle context_window / context_length vermezse bu değer gerçek Codex penceresi olur. Tek bir modeli elle yazmak için aynı satırdaki «Özel pencereler»i kullanın.`,"models.collapseAll":`Tümünü daralt`,"models.expandAll":`Tümünü genişlet`,"models.orderHint":`Seçici sırası: Alt ajan seçimleri → kalan modeller.`,"models.custom":`Özel…`,"models.customApply":`Uygula`,"models.customPlaceholder":`Jetonlar (örn. 420000)`,"models.customAdd":`Özel model ekle`,"models.customAddTitle":`Özel model ekle — {provider}`,"models.customEditTitle":`Özel modeli düzenle — {provider}`,"models.customAdded":`Özel model eklendi`,"models.customUpdated":`Özel model güncellendi`,"models.customDeleted":`Özel model silindi`,"models.customSaveFailed":`Özel model kaydedilemedi`,"models.customSaving":`Kaydediliyor…`,"models.customAddBtn":`Ekle`,"models.customEditBtn":`Güncelle`,"models.customEdit":`Düzenle`,"models.customDelete":`Sil`,"models.customDeleteConfirm":`{name} modeli silinsin mi?`,"models.customBadge":`Özel`,"models.customSummary":`{count} özel`,"models.customFieldModelId":`Model ID`,"models.customFieldModelIdPlaceholder":`örn. qwen4-max-preview`,"models.customFieldDisplayName":`Görüntülenen ad (isteğe bağlı)`,"models.customFieldDisplayNamePlaceholder":`örn. Qwen 4 Max Preview`,"models.customFieldContext":`Bağlam penceresi`,"models.customFieldModalities":`Girdi türleri`,"models.customFieldReasoning":`Akıl yürütme çabası`,"models.customFieldReasoningOverride":`Akıl yürütme çabasını geçersiz kıl`,"models.reasoningEffort.none":`Yok`,"models.reasoningEffort.minimal":`Minimal`,"models.reasoningEffort.low":`Düşük`,"models.reasoningEffort.medium":`Orta`,"models.reasoningEffort.high":`Yüksek`,"models.reasoningEffort.xhigh":`Çok yüksek`,"models.reasoningEffort.max":`Maksimum`,"models.tipProvider":`Sağlayıcı`,"models.tipContext":`Bağlam`,"models.tipModalities":`Girdi Türleri`,"models.tipStatus":`Durum`,"models.tipActive":`Aktif`,"models.tipDisabled":`Devre Dışı`,"models.applied":`Uygulandı.`,"models.saveFailed":`Kaydetme başarısız`,"models.networkError":`Ağ hatası — proxy çalışıyor mu?`,"models.loadFail":`Modeller yüklenemedi — proxy çalışıyor mu?`,"models.noRouted":`Yönlendirilen model yok`,"models.noRoutedHint":`Önce bir sağlayıcıya giriş yapın veya ekleyin.`,"models.emptyDiscovery":`Keşfedilen model yok.`,"models.emptyDiscoveryDisabled":`Canlı model keşfi kapalı.`,"models.discoveryFailedBadge":`Keşif başarısız`,"models.discoveryFailedHttp":`Model keşfi başarısız oldu (HTTP {status}).`,"models.discoveryFailedBlocked":`Model keşfi engellendi.`,"models.discoveryFailedInvalidResponse":`Model keşfi geçersiz bir yanıt döndürdü.`,"models.discoveryFailedNetwork":`Model keşfi ağ hatası nedeniyle başarısız oldu.`,"models.discoveryFailedProvider":`Sağlayıcı bir model keşfi hatası bildirdi.`,"models.discoveryFailedGeneric":`Model keşfi başarısız oldu.`,"models.openProviderSettings":`Sağlayıcı ayarlarını aç`,"models.loading":`Yükleniyor…`,"models.search":`Modellerde ara…`,"models.showMore":`{n} tane daha göster`,"models.allowlistLabel":`Sadece seçilenler`,"models.allowlistHint":`Sadece işaretli modeller kataloğa gönderilir.`,"models.selectedCount":`{n} seçildi`,"sub.subtitle":`Codex'in {cmd} komutu, geçersiz kılma olarak yalnızca ilk 5 modeli (önceliğe göre) sunar. Buradan 5 taneye kadar seçin (yerel gpt veya yönlendirilen) ve opencodex bunların katalog önceliğini tam olarak bunların liderlik edeceği şekilde ayarlar. Diğer herhangi bir model tam adıyla çağrılabilir kalır; bu yalnızca neyin gösterileceğini kontrol eder.`,"sub.featured":`Öne Çıkarılanlar`,"sub.advanced":`Gelişmiş`,"sub.orderHintAria":`Bu sıra nasıl kullanılır`,"sub.orderHint":`Burada gösterilen sıralama, Codex model seçicisinin üst kısmındaki 1-5 pozisyonlarını ve {cmd} için varsayılan model adaylarını belirler.`,"sub.noneSelected":`Hiçbiri seçilmedi — aşağıdaki listeden seçin.`,"sub.models":`Modeller`,"sub.search":`Modellerde ara…`,"sub.settings":`Ayarlar`,"sub.sections":`Alt ajan bölümleri`,"sub.delegation.model":`İlk çağrılacak model`,"sub.delegation.modelHint":`Codex'in iş devrederken ilk ulaştığı model.`,"sub.noModels":`Model yok — önce bir sağlayıcı ekleyin.`,"sub.saved":`{n} model kaydedildi. Bunları spawn_agent geçersiz kılmaları olarak görmek için yeni bir Codex oturumu başlatın (veya {cmd} çalıştırın).`,"sub.saveFailed":`Kaydetme başarısız`,"sub.networkError":`Ağ hatası — proxy çalışıyor mu?`,"sub.loadFail":`Modeller yüklenemedi`,"sub.loading":`Yükleniyor…`,"sub.moveUp":`{m} modelini yukarı taşı`,"sub.moveDown":`{m} modelini aşağı taşı`,"sub.removeAria":`{m} modelini kaldır`,"sub.workspace.addToFeatured":`{m} modelini öne çıkarılanlara ekle`,"sub.workspace.allModels":`Tüm modeller`,"sub.workspace.featuredFull":`Öne çıkarılanlar listesi dolu (maksimum 5)`,"sub.workspace.mainAria":`Alt ajan model detayları`,"sub.workspace.notFeatured":`Öne çıkarılmadı`,"sub.workspace.priority":`Öncelik`,"sub.workspace.removeFromFeatured":`{m} modelini öne çıkarılanlardan kaldır`,"sub.workspace.selectModel":`Bir model seçin`,"sub.workspace.selectModelDesc":`Detayları görmek için listeden bir model seçin.`,"sub.workspace.selector":`Genel seçici`,"sub.ultraMode":`Ultra modu`,"sub.ultraModeHint":`Tüm modeller ve reasoning effort için Proactive çoklu ajan delegasyon politikasını etkinleştirir (reasoning effort değerini değiştirmez). config.toml dosyasına features.multi_agent_v2.multi_agent_mode_hint_text yazar.`,"sub.ultraModeV2Required":`v2 çoklu ajan yüzeyi gerekir — önce multi_agent_v2'yi etkinleştirin ve alt ajan modu denetiminde v2'yi seçin.`,"sub.ultraModeText":`Ultra modu delegasyon metni`,"sub.ultraModePreset":`Ön ayarı geri yükle`,"sub.ultraModeLoadFail":`Ultra modu ayarları yüklenemedi — proxy çalışıyor mu?`,"sub.ultraModeSaveFail":`Ultra modu ayarları kaydedilemedi`,"sub.ultraModeSaved":`Ultra modu kaydedildi. Yeni Codex oturumlarına uygulanır.`,"logs.title":`İstek Günlükleri`,"logs.tabLogs":`Günlükler`,"logs.tabDebug":`Hata Ayıklama`,"logs.subtitle":`Proxy üzerinden yönlendirilen son istekler.`,"logs.autoRefresh":`Otomatik yenile`,"logs.noRequests":`Henüz istek yok.`,"logs.loadError":`İstek günlükleri yüklenemedi.`,"logs.filter.surface.label":`Yüzey`,"logs.filter.surface.all":`Tümü`,"logs.filter.surface.claude":`Claude`,"logs.filter.surface.codex":`Codex`,"logs.filter.surface.grok":`Grok`,"logs.filter.interceptedHelpersOnly":`Yalnizca yakalanan yardimcilar`,"logs.badge.interceptedHelper":`I · {model}`,"logs.badge.interceptedHelperTitle":`Yakalanan yardimci istegi`,"logs.filter.conversation.label":`Sohbet`,"logs.filter.conversation.placeholder":`Sohbet ID'sini yapıştırın`,"logs.filter.conversation.clear":`Temizle`,"logs.filter.model.label":`Model`,"logs.filter.model.placeholder":`Modele veya sağlayıcıya göre filtrele`,"logs.filter.conversation.apply":`Günlükleri filtrele`,"logs.conversation.totals":`{requests} istek · {tokens} jeton · {cost}`,"logs.conversation.scope":`Toplamlar yalnızca yüklü günlükleri kapsar.`,"logs.conversation.excluded":`({unpriced} fiyatlandırılmamış, {unmetered} ölçülmemiş hariç)`,"logs.cost.approximate":`{amount}`,"logs.cost.lowerBound":`≥{amount}`,"logs.cost.unavailable":`kullanılamıyor`,"logs.detail.conversation":`Sohbet`,"logs.badge.claude":`Claude`,"logs.badge.grok":`Grok`,"logs.col.time":`Zaman`,"logs.col.request":`İstek`,"logs.col.model":`Model`,"logs.col.effort":`Çaba`,"logs.col.provider":`Sağlayıcı`,"logs.col.status":`Durum`,"logs.col.tokens":`Jetonlar`,"logs.col.tokPerSec":`jeton/sn`,"logs.col.estimatedCost":`~$`,"logs.metric.tokPerSecTitle":`Çıktı jetonu / saniye`,"logs.metric.estimatedCostTitle":`Tahmini API liste fiyatı`,"usage.cost.total":`API liste fiyatı eşdeğeri`,"usage.cost.disclaimer":`Fatura makbuzu değildir.`,"usage.cost.unpricedNote":`{count} istek hariç tutuldu`,"logs.detail.section.basic":`Temel bilgiler`,"logs.detail.route.section":`Yönlendirme kararı`,"logs.detail.route.kind":`Yönlendirme türü`,"logs.detail.route.profile":`Profil`,"logs.detail.route.selected":`Seçilen`,"logs.detail.route.candidates":`Adaylar`,"logs.detail.route.unknown":`Kayıtlı yönlendirme izi yok.`,"logs.detail.section.performance":`Performans`,"logs.detail.section.cost":`Tahmini maliyet`,"logs.detail.section.attempts":`Kombo denemeleri`,"logs.detail.section.usage":`Ham kullanım`,"logs.detail.ttft":`TTFT`,"logs.detail.costTotal":`Liste fiyatı eşdeğeri`,"logs.detail.totalTokens":`Toplam jeton`,"logs.detail.matchedKey":`Eşleşen anahtar`,"logs.detail.priceSource":`Fiyat kaynağı`,"logs.detail.unavailableReason":`Kullanılamama nedeni`,"logs.detail.copyRequestId":`İstek ID kopyala`,"logs.detail.copied":`Kopyalandı`,"logs.detail.source.jawcode":`katalog`,"logs.detail.source.expected":`Beklenen fiyat`,"logs.detail.source.user":`Kullanıcı tarafından yapılandırılan sağlayıcı fiyat katmanı`,"logs.detail.verification.verified":`Doğrulandı`,"logs.detail.verification.derived":`Taban modelden türetildi`,"logs.detail.attempt.target":`Sağlayıcı / model`,"logs.detail.attempt.reason":`Sonuç / neden`,"logs.detail.attempt.completed":`Tamamlandı`,"logs.detail.attempt.e2eNote":`Süreç bilgisi`,"logs.detail.attempt.recovery.transient5xx":`Geçici 5xx`,"logs.detail.attempt.recovery.connectionReset":`Bağlantı sıfırlandı`,"logs.detail.attempt.recovery.oauth401":`OAuth yeniden doğrulaması`,"logs.detail.attempt.recovery.key429":`Anahtar oranı kısıtlandı (429)`,"logs.detail.attempt.recovery.rateLimit429":`Oran kısıtlandı (429)`,"logs.detail.attempt.recovery.anthropicOauth429":`Anthropic OAuth kısıtlandı (429)`,"logs.detail.attempt.recovery.image413":`Görsel boyutu çok büyük (413)`,"logs.detail.attempt.recovery.emptyCompletion":`Boş tamamlama yeniden denemesi`,"logs.detail.attempt.recovery.unknown":`Bilinmeyen kurtarma nedeni`,"logs.detail.reason.usage_missing":`Kullanım bildirilmedi.`,"logs.detail.reason.usage_unsupported":`Bu sağlayıcı kullanım bildirmeyebilir.`,"logs.detail.reason.output_missing":`Çıktı jeton sayısı bildirilmedi.`,"logs.detail.reason.invalid_duration":`İstek süresi geçersiz.`,"logs.detail.reason.price_unmatched":`Eşleşen fiyat bulunamadı.`,"logs.detail.reason.invalid_cache_breakdown":`Önbellek detayları çakışıyor.`,"logs.detail.reason.invalid_usage":`Kullanım geçersiz bir jeton değeri içeriyor.`,"logs.detail.reason.combo_attempt_unavailable":`Kombo denemesi fiyatlandırılamadı.`,"logs.detail.estimate.usage_estimated":`Sağlayıcı kullanımı tahminidir.`,"logs.detail.estimate.cache_detail_missing":`Önbellek detayları eksik.`,"logs.detail.estimate.expected_price_overlay":`Doğrulanmış liste fiyatı kullanıldı.`,"logs.detail.estimate.provider_cost_overlay":`Kullanıcı tarafından yapılandırılan bir sağlayıcı fiyat katmanı kullanıldı.`,"logs.detail.estimate.priority_lower_bound":`Doğrulanan Priority fiyatı kullanılamıyor; gösterilen tahmin bilinen bir alt sınırdır.`,"logs.col.error":`Hata`,"logs.col.upstreamReason":`Yukarı akış nedeni`,"logs.col.duration":`Süre`,"logs.modelTooltip.model":`model`,"logs.modelTooltip.resolvedModel":`çözümlenen model`,"logs.modelTooltip.requestedTier":`istenen katman`,"logs.modelTooltip.configuredTier":`yapılandırılan katman`,"logs.modelTooltip.responseTier":`yanıt katmanı`,"logs.modelTooltip.supportsTier":`katman desteği`,"logs.tokens.reported":`bildirilen`,"logs.tokens.unreported":`bildirilmeyen`,"logs.tokens.unsupported":`desteklenmeyen`,"logs.tokens.estimated":`tahmini`,"logs.tokens.input":`girdi`,"logs.tokens.output":`çıktı`,"logs.tokens.cacheRead":`önbellek okuma`,"logs.tokens.cacheWrite":`önbellek yazma`,"logs.tokens.reasoning":`akıl yürütme`,"logs.tokens.noCache":`önbellek verisi yok`,"logs.tokens.contextTotal":`aktif bağlam`,"logs.tokens.noCacheNote":`bu sağlayıcı önbellek jetonlarını bildirmiyor`,"logs.tokens.noCacheCursor":`Cursor önbellek detayı bildirilmedi`,"logs.tokens.noCacheCursorNote":`Cursor önbellek jeton sayılarını sunmaz`,"logs.tokens.estimatedNote":`tahmini kullanım`,"logs.details":`Detaylar`,"logs.detailTitle":`İstek detayları`,"logs.detailRaw":`Ham günlük kaydı`,"debug.title":`Hata Ayıklama`,"debug.subtitle":`Sağlayıcı taşıma ve kullanım çıkarma tanılamaları.`,"debug.debug":`Sağlayıcı hata ayıklama`,"debug.usage":`Kullanım çıkarma`,"debug.injection":`Enjeksiyon günlüğü`,"debug.claude":`Claude gelen istekler`,"debug.claudeInbound.title":`Claude gelen istekleri`,"debug.claudeInbound.sub":`Claude Code/Desktop tarafından gönderilen istekler.`,"debug.claudeInbound.empty":`Henüz istek yakalanmadı.`,"debug.claudeInbound.time":`Zaman`,"debug.claudeInbound.endpoint":`Uç nokta`,"debug.claudeInbound.model":`Model`,"debug.claudeInbound.none":`yok`,"debug.reset":`Sıfırla`,"debug.refresh":`Yenile`,"debug.follow":`Takip Et`,"debug.streamProvider":`Sağlayıcı`,"debug.streamUsage":`Kullanım`,"debug.streamInjection":`Enjeksiyon`,"debug.loading":`Ayarlar yükleniyor…`,"debug.loadFailed":`Ayarlar yüklenemedi.`,"debug.emptyTitle":`Hata ayıklama günlüğü kapalı`,"debug.empty":`Hata ayıklama seçeneğini açın.`,"debug.noLinesTitle":`Satırlar bekleniyor`,"debug.noLines.provider":`Sağlayıcı hata ayıklama açık.`,"debug.noLines.usage":`Kullanım çıkarma açık.`,"debug.noLines.injection":`Enjeksiyon günlüğü açık.`,"usage.title":`Kullanım`,"usage.subtitle":`Proxy'nizden yerel jeton muhasebesi.`,"usage.loading":`Kullanım verileri yükleniyor…`,"usage.empty":`Henüz kullanım kaydedilmedi.`,"usage.loadError":`Kullanım verileri yüklenemedi.`,"usage.range.all":`Tümü`,"usage.range.available":`Mevcut geçmiş`,"usage.historyTruncated":`Toplamlar yalnızca mevcut geçmişi kapsar.`,"usage.historyTruncatedWindow":`Yüklenen satırların istek başlangıç zamanları {start} ile {end} arasındadır. Dosyanın önceki kayıtları okuma sınırı nedeniyle atlandı, bu yüzden seçilen aralık eksik olabilir.`,"usage.range.30d":`30 gün`,"usage.range.7d":`7 gün`,"usage.card.requests":`İstekler`,"usage.card.measured":`Ölçülen`,"usage.card.reported":`Bildirilen`,"usage.card.totalTokens":`Toplam jeton`,"usage.card.cachedTokens":`Önbellek okuma`,"usage.card.cachedTokensHint":`Sağlayıcı önbelleğinden sunulan jetonlar.`,"usage.card.cacheWriteTokens":`önbellek yazma`,"usage.card.coverage":`Kapsam`,"usage.card.activeDays":`Aktif günler`,"usage.section.heatmap":`Günlük aktivite`,"usage.section.overview":`Genel bakış`,"usage.section.models":`Modeller`,"usage.section.providers":`Sağlayıcılar`,"usage.section.coverage":`Kapsam dağılımı`,"usage.workspace.report":`Kullanım raporu`,"usage.workspace.sections":`Kullanım bölümleri`,"usage.coverage.measured":`Ölçülen`,"usage.coverage.reported":`Bildirilen`,"usage.coverage.estimated":`Tahmin edilen`,"usage.coverage.note":`Ölçülen girdiler bildirilen ve tahmin edilen jeton sayılarını içerir.`,"usage.search.models":`Modellerde ara…`,"usage.col.requests":`İstekler`,"usage.col.measured":`Ölçülen`,"usage.col.reported":`Bildirilen`,"usage.col.tokens":`Jetonlar`,"usage.col.share":`Pay`,"usage.heatmap.less":`Daha az`,"usage.heatmap.more":`Daha fazla`,"usage.dayMon":`Pzt`,"usage.dayWed":`Çar`,"usage.dayFri":`Cum`,"usage.heatmap.tooltipTokens":`{tokens} jeton`,"usage.heatmap.tooltipRequests":`{requests} istek`,"nav.storage":`Depolama`,"storage.title":`Depolama`,"storage.subtitle":`CODEX_HOME dizinini nelerin kullandığını görün.`,"storage.loading":`Depolama taranıyor…`,"storage.empty":`CODEX_HOME boş veya eksik.`,"storage.error":`Depolama taraması başarısız oldu.`,"storage.refresh":`Yeniden tara`,"storage.rescanned":`Tarama tamamlandı.`,"storage.card.total":`Toplam boyut`,"storage.card.files":`Dosyalar`,"storage.card.home":`CODEX_HOME`,"storage.snapshot.lastScan":`Son tarama`,"storage.snapshot.scanning":`Taranıyor…`,"storage.snapshot.unavailable":`Henüz tarama yok.`,"storage.cleanupCard.title":`Alan açın`,"storage.cleanupCard.tabs":`Temizleme seçenekleri`,"storage.cleanupCard.tab.policy":`Politika`,"storage.cleanupCard.tab.quarantine":`Karantina`,"storage.cleanup.noArchives":`Temizlenecek arşivlenmiş oturum yok.`,"storage.section.buckets":`Kovalar`,"storage.section.largest":`En büyük dosyalar`,"storage.workspace.overview":`Genel Bakış`,"storage.workspace.selectBucket":`Ayrıntıları görmek için listeden bir kova seçin.`,"storage.col.bucket":`Kova`,"storage.col.size":`Boyut`,"storage.col.files":`Dosyalar`,"storage.col.oldest":`En eski`,"storage.col.newest":`En yeni`,"storage.col.rows":`DB satırları`,"storage.rows.unknown":`bilinmiyor (kilitli)`,"storage.bucket.sessions":`Aktif oturumlar`,"storage.bucket.archived_sessions":`Arşivlenmiş oturumlar`,"storage.bucket.logs_db":`Günlük veritabanı`,"storage.bucket.state_db":`Durum veritabanı`,"storage.bucket.attachments":`Eklentiler`,"storage.bucket.deletion_manifests":`Silme bildirimleri`,"storage.bucket.other":`Diğer`,"storage.cleanup.title":`Arşiv temizleme`,"storage.cleanup.help":`En eski arşivlenmiş oturumları yüzdeye göre kaldırın.`,"storage.cleanup.slider":`En eski arşivlenen yüzde`,"storage.cleanup.percent":`%{percent}`,"storage.cleanup.preset":`{percent}`,"storage.cleanup.preview":`Önizleme`,"storage.cleanup.confirmTitle":`Arşiv temizliğini onayla`,"storage.cleanup.confirmBody":`Toplam ~{size} tutan {count} arşivlenmiş oturum dosyası silinsin mi (%{percent} eşiği)?`,"storage.cleanup.moreFiles":`…ve {n} tane daha`,"storage.cleanup.permanent":`Kalıcı olarak sil (karantinayı atla)`,"storage.cleanup.permanentWarn":`Kalıcı silme geri alınamaz.`,"storage.cleanup.quarantineNote":`Dosyalar CODEX_HOME/.trash altına taşınır.`,"storage.cleanup.cancel":`İptal`,"storage.cleanup.confirmQuarantine":`Karantinaya Al`,"storage.cleanup.confirmPermanent":`Kalıcı Olarak Sil`,"storage.cleanup.doneQuarantine":`{count} dosya karantinaya alındı ({size}).`,"storage.cleanup.donePermanent":`{count} dosya kalıcı olarak silindi ({size}).`,"storage.cleanup.previewFailed":`Önizleme başarısız oldu.`,"storage.cleanup.cleanupFailed":`Temizleme başarısız oldu.`,"storage.cleanup.err.codex_busy":`Codex state.sqlite dosyasını kullanıyor.`,"storage.cleanup.err.stale_preview":`Önizlemeden sonra arşivlenmiş dosyalar değişti.`,"storage.cleanup.err.restore_pending_overlap":`Seçilen arşivler tamamlanmamış bir geri yükleme ile çakışıyor.`,"storage.cleanup.err.referenced_history":`Seçilen arşivler hâlâ geçmiş tarafından referans gösteriliyor.`,"storage.cleanup.err.invalid_digest":`Önizleme özeti eksik veya geçersiz.`,"storage.cleanup.err.invalid_mode":`Temizleme modu karantina veya kalıcı olmalıdır.`,"storage.cleanup.err.fs_failed":`Dosya sistemi temizliği başarısız oldu. Bazı dosyalar başarısızlıktan önce taşınmış olabilir; CODEX_HOME/.trash veya hedef dizini inceleyin.`,"storage.cleanup.err.fs_failed_trash":`{trashDir} için çöp kutusu işlemi başarısız oldu.`,"storage.cleanup.err.db_reconcile_failed":`Codex durum veritabanı güncellenemedi.`,"storage.cleanup.err.cleanup_failed":`Temizleme başarısız oldu.`,"storage.trash.title":`Karantina`,"storage.trash.help":`CODEX_HOME/.trash altına taşınan arşivlenmiş oturumlar.`,"storage.trash.empty":`Karantinaya alınmış öğe yok.`,"storage.trash.loading":`Karantina yükleniyor…`,"storage.trash.col.when":`Karantinaya Alındı`,"storage.trash.col.files":`Dosyalar`,"storage.trash.col.size":`Boyut`,"storage.trash.col.mode":`Mod`,"storage.trash.col.id":`Girdi`,"storage.trash.restore":`Geri Yükle`,"storage.trash.confirmTitle":`Karantina girdisi geri yüklensin mi?`,"storage.trash.confirmBody":`{id} kimliğinden {count} dosya (~{size}) geri yüklensin mi?`,"storage.trash.cancel":`İptal`,"storage.trash.confirmRestore":`Geri Yükle`,"storage.trash.done":`{count} dosya geri yüklendi ({size}).`,"storage.trash.restoreFailed":`Geri yükleme başarısız oldu.`,"storage.trash.listFailed":`Karantina girdileri listelenemedi.`,"storage.trash.mode.quarantine":`karantina`,"storage.trash.mode.permanent":`kalıcı (tamamlanmamış)`,"storage.trash.err.codex_busy":`Codex state.sqlite dosyasını kullanıyor.`,"storage.trash.err.invalid_trash":`Çöp girdisi ID'si eksik veya geçersiz.`,"storage.trash.err.missing_trash":`Çöp girdisi bulunamadı.`,"storage.trash.err.dest_exists":`Geri yükleme hedefi zaten mevcut.`,"storage.trash.err.fs_failed":`Dosya sistemi geri yüklemesi başarısız oldu.`,"storage.trash.err.db_reconcile_failed":`Veritabanı satırları geri yüklenemedi.`,"storage.trash.err.storage_mutation_busy":`Başka bir depolama eylemi devam ediyor.`,"storage.trash.err.restore_failed":`Geri yükleme başarısız oldu.`,"storage.trash.err.restore_worker_timeout":`Geri yükleme çok uzun sürdü.`,"storage.trash.err.restore_worker_aborted":`Geri yükleme iptal edildi.`,"storage.trash.err.restore_worker_failed":`Geri yükleme işleyicisi çöktü.`,"storage.policy.title":`Otomatik temizleme politikası`,"storage.policy.help":`İsteğe bağlı toplu temizleme politikası.`,"storage.policy.loading":`Politika yükleniyor…`,"storage.policy.loadFailed":`Temizleme politikası yüklenemedi.`,"storage.policy.saveFailed":`Temizleme politikası kaydedilemedi.`,"storage.policy.runFailed":`Politika çalıştırması başarısız oldu.`,"storage.policy.alreadyRunning":`Bir politika çalıştırması zaten devam ediyor.`,"storage.policy.invalid":`Geçersiz politika değerleri.`,"storage.policy.enabled":`Otomatik temizlemeyi etkinleştir`,"storage.policy.enabledHint":`Varsayılan olarak kapalıdır.`,"storage.policy.threshold":`Arşivlenen boyut aşıldığında (GiB)`,"storage.policy.trigger":`Tetikleyici`,"storage.policy.target":`Temizleme hedefi`,"storage.policy.targetPercent":`En eski arşivlenenleri kaldır (%)`,"storage.policy.targetReduce":`Arşiv boyutunu düşür (GiB)`,"storage.policy.thresholdInc":`Eşiği artır`,"storage.policy.thresholdDec":`Eşiği azalt`,"storage.policy.percentInc":`Yüzdeyi artır`,"storage.policy.percentDec":`Yüzdeyi azalt`,"storage.policy.reduceInc":`Hedef boyutu artır`,"storage.policy.reduceDec":`Hedef boyutu azalt`,"storage.policy.schedule":`Zamanlama`,"storage.policy.schedule.manual":`Yalnızca manuel`,"storage.policy.schedule.startup":`Proxy başlangıcında`,"storage.policy.schedule.daily":`Günlük`,"storage.policy.schedule.weekly":`Haftalık`,"storage.policy.mode":`Silme modu`,"storage.policy.mode.quarantine":`Karantina (varsayılan)`,"storage.policy.mode.permanent":`Kalıcı silme`,"storage.policy.permanentWarn":`Kalıcı mod geri alınamaz.`,"storage.policy.lastRun":`Son çalıştırma`,"storage.policy.lastRunDetail":`{count} kaldırıldı · {size} alan açıldı`,"storage.policy.nextRun":`Sonraki çalıştırma`,"storage.policy.never":`Hiçbir zaman`,"storage.policy.save":`Kaydet`,"storage.policy.runNow":`Şimdi çalıştır`,"storage.policy.running":`Çalıştırılıyor…`,"storage.policy.saved":`Politika kaydedildi.`,"storage.policy.skippedDisabled":`Politika devre dışı.`,"storage.policy.skippedUnder":`Arşiv boyutu eşiğin altında.`,"storage.policy.skippedEmpty":`Hedefle eşleşen aday yok.`,"storage.policy.doneQuarantine":`Politika {count} dosyayı karantinaya aldı ({size}).`,"storage.policy.donePermanent":`Politika {count} dosyayı kalıcı olarak sildi ({size}).`,"storage.policy.metadataSaveWarning":`Politika çalışması tamamlandı ancak zamanlama meta verileri kaydedilemedi.`,"modal.addNamed":`Ekle: {label}`,"modal.add":`Sağlayıcı ekle`,"modal.search":`Sağlayıcılarda ara…`,"modal.logInWith":`{label} ile giriş yap`,"modal.waitingBrowser":`Tarayıcı bekleniyor…`,"modal.providerName":`Sağlayıcı adı`,"modal.adapter":`Adaptör`,"modal.baseUrl":`Taban URL`,"modal.endpoint":`Uç nokta`,"modal.endpoint.tokenPlan":`Jeton planı`,"modal.endpoint.payAsYouGo":`Kullandıkça öde`,"modal.endpoint.custom":`Özel`,"modal.defaultModel":`Varsayılan model (isteğe bağlı)`,"modal.allowPrivateNetwork":`Yerel/özel ağa izin ver`,"modal.allowPrivateNetworkHint":`Yalnızca yerel barındırılan sağlayıcılar için etkinleştirin.`,"modal.nameRequired":`Sağlayıcı adı gereklidir`,"modal.baseUrlRequired":`Taban URL gereklidir`,"modal.networkError":`Ağ hatası — proxy çalışıyor mu?`,"modal.loginFailStart":`Giriş başlatılamadı`,"modal.waitingLogin":`Tarayıcı girişi bekleniyor…`,"modal.loggingIn":`Giriş yapılıyor…`,"modal.loginTimeout":`Giriş zaman aşılanına uğradı.`,"modal.back":`Geri`,"modal.badge.oauth":`OAuth`,"modal.customProvider":`Özel sağlayıcı`,"modal.failedStatus":`Başarısız ({status})`,"modal.loginError":`Giriş hatası: {error}`,"modal.badge.codexLogin":`Codex girişi`,"modal.badge.local":`Yerel`,"modal.badge.apiKey":`API anahtarı`,"modal.badge.direct":`Doğrudan`,"modal.badge.pool":`Havuz`,"modal.badge.free":`Ücretsiz`,"modal.invalidPreset":`Bu yerleşik sağlayıcı ayarı eksik.`,"modal.freeTierTitle":`Ücretsiz katman`,"modal.freeTierDefault":`API anahtarı gerekmez. Doğrudan çalışır.`,"modal.tab.accounts":`Hesaplar`,"modal.tab.free":`Ücretsiz`,"modal.tab.paid":`Ücretli`,"modal.accountsHint":`ChatGPT/Codex ve OAuth hesaplarına buradan giriş yapın.`,"modal.accountsCodexAuthLink":`Codex Kimlik Doğrulaması`,"modal.notListed":`Sağlayıcı listede yok mu? Özel sağlayıcı ekleyin`,"modal.catalogLoading":`Katalog yükleniyor…`,"modal.accountLogin":`Giriş yap`,"modal.accountLogout":`Çıkış yap`,"modal.accountAdd":`Hesap ekle`,"modal.accountManage":`Yönet`,"modal.accountCodexPool":`ChatGPT hesap havuzu`,"modal.accountLoggedIn":`Giriş yapıldı`,"modal.accountLoggedOut":`Giriş yapılmadı`,"quota.fiveHourLimit":`5 saatlik limit`,"quota.ageMinutes":`{n} dk`,"quota.ageHours":`{n} sa`,"quota.ageDays":`{n} g`,"quota.observedAgo":`{age} önce alındı`,"quota.observedHint":`Meta kullanımı yalnızca akış yanıtı sırasında bildirir; bu canlı bir ölçüm değil, en son alınan değerdir.`,"quota.weeklyLimit":`Haftalık limit`,"quota.monthlyLimit":`30 günlük limit`,"quota.cursorFirstParty":`Birinci taraf modeller`,"quota.cursorApiUsage":`API kullanımı`,"quota.totalSubscriptionCredits":`Toplam abonelik kredileri`,"quota.creditsBalance":`Kredi bakiyesi`,"quota.creditsPeriodEnds":`Faturalandırma dönemi {date} tarihinde sona erer`,"quota.usedPercent":`%{pct} kullanıldı`,"quota.limitReached":`Limite ulaşıldı`,"quota.resetsToday":`Bugün {time} saatinde sıfırlanır`,"quota.resetsTomorrow":`Yarın {time} saatinde sıfırlanır`,"quota.resetsAt":`{when} sıfırlanır`,"quota.resetsRelativeMinutes":`{n} dakika içinde sıfırlanır`,"quota.resetsRelativeHours":`{n} saat içinde sıfırlanır`,"pws.status.ready":`Hazır`,"pws.status.needsSetup":`Kurulum gerekiyor`,"pws.status.needsAttention":`Dikkat gerekiyor`,"pws.auth.chatgptPassthrough":`ChatGPT doğrudan geçiş`,"pws.auth.noKey":`Anahtar gerekmiyor`,"pws.freeTitle":`Ücretsiz fiyatlandırma`,"pws.localTitle":`Yerel çalışma zamanı`,"pws.modelCountOne":`1 model`,"pws.modelCount":`{count} model`,"pws.rail.suffixDefault":` · varsayılan`,"pws.rail.suffixLocal":` · yerel`,"pws.rail.suffixFree":` · ücretsiz`,"pws.rail.selectAria":`{name} seç — {status}{suffix}`,"pws.searchPlaceholder":`Sağlayıcılarda ara…`,"pws.filterAria":`Sağlayıcıları filtrele`,"pws.providerFiltersAria":`Sağlayıcı filtreleri`,"pws.filters":`Filtreler`,"pws.filterStatus":`Durum`,"pws.pricing":`Fiyatlandırma`,"pws.paid":`Ücretli`,"pws.filterType":`Tür`,"pws.type.cloud":`Bulut`,"pws.type.local":`Yerel`,"pws.type.selfHosted":`Kendi barındırdığı`,"pws.type.login":`Giriş yap`,"pws.sort":`Sırala`,"pws.sortProvidersAria":`Sağlayıcıları sırala`,"pws.sort.az":`A–Z`,"pws.sort.za":`Z–A`,"pws.sort.freePaid":`Önce ücretsizler`,"pws.sort.paidFree":`Önce ücretliler`,"pws.sort.accountsFirst":`Önce hesaplar`,"pws.resetAll":`Tümünü sıfırla`,"pws.providerList":`Sağlayıcı listesi`,"pws.providersAria":`Sağlayıcılar`,"pws.groupReady":`Hazır ({count})`,"pws.groupNeedsSetup":`Kurulum gerekiyor ({count})`,"pws.groupDisabled":`Devre dışı ({count})`,"pws.noSearchResults":`Aramanızla eşleşen sağlayıcı yok.`,"pws.noMatchFilters":`Filtrelerle eşleşen sağlayıcı yok.`,"pws.noProvidersConfigured":`Yapılandırılmış sağlayıcı yok.`,"pws.workspaceMainAria":`Sağlayıcı detayları`,"pws.detailComingSoon":`Detay görünümü yakında geliyor.`,"pws.selectPrompt":`Listeden bir sağlayıcı seçin.`,"pws.connectFirst":`İlk sağlayıcınızı bağlayın`,"pws.empty.browseFree":`Ücretsiz sağlayıcılara göz atın`,"pws.empty.browseFreeDesc":`Abonelik olmadan başlayın`,"pws.empty.connectAccount":`Bir hesap bağlayın`,"pws.empty.connectAccountDesc":`ChatGPT veya sağlayıcı girişinizi kullanın`,"pws.empty.addEndpoint":`Bir uç nokta ekleyin`,"pws.empty.addEndpointDesc":`Özel taban URL ve API anahtarı`,"pws.tab.overview":`Genel Bakış`,"pws.tab.models":`Modeller`,"pws.tab.usage":`Kullanım`,"pws.tab.accounts":`Hesaplar`,"pws.tab.settings":`Ayarlar`,"pws.connection":`Bağlantı`,"pws.status.connected":`Bağlandı`,"pws.attentionTitle":`Dikkat gerekiyor`,"pws.attention.reauth":`Aktif hesap yeniden doğrulama gerektiriyor`,"pws.attention.reauthForward":`Aktif Codex hesabı yeniden doğrulama gerektiriyor`,"pws.attention.missingCredentials":`Kimlik bilgileri eksik`,"pws.cell.auth":`Kimlik Doğrulama`,"pws.cell.note":`Not`,"pws.cell.defaultModel":`Varsayılan model`,"pws.statsAria":`Sağlayıcı istatistikleri`,"pws.statsTitle":`İstatistikler`,"pws.stats.totalRequests":`İstekler (30 gün)`,"pws.stats.totalTokens":`Jetonlar (30 gün)`,"pws.stats.quotaUpdated":`Kota güncellendi`,"pws.stats.quotaTracked":`Oran limitleri Kullanım sekmesinde takip edilir.`,"pws.stats.source":`Kaynak`,"pws.usageLast30d":`Kullanım (son 30 gün)`,"pws.estimatedCost":`Tahmini maliyet`,"pws.costDisclaimer":`API liste fiyatı tahminidir.`,"pws.modelBreakdown":`Model dağılımı`,"pws.col.model":`Model`,"pws.col.cost":`Tahm. maliyet`,"pws.col.tokens":`Jetonlar`,"pws.col.requests":`İst.`,"pws.col.share":`Pay`,"pws.tokenInput":`Girdi`,"pws.tokenOutput":`Çıktı`,"pws.metricRequests":`istek`,"pws.metricTokens":`jeton`,"pws.usageUnavailable":`Henüz kullanım kaydedilmedi.`,"pws.rateLimits":`Oran limitleri`,"pws.quotaUnavailable":`Bu sağlayıcı için kota verisi yok.`,"pws.accountQuotaUnavailable":`Oran limiti verisi geçici olarak kullanılamıyor.`,"pws.selected":`Seçilen`,"pws.copyModelId":`ID Kopyala`,"pws.modelCopied":`Kopyalandı!`,"pws.modelsAvailable":`{count} kullanılabilir`,"pws.modelSearchPlaceholder":`Modelleri filtrele…`,"pws.modelsLoading":`Modeller yükleniyor…`,"pws.modelsLoadFailed":`Modeller yüklenemedi.`,"pws.modelsNeedsReauth":`Hesabın yeniden giriş yapması gerekiyor.`,"pws.modelsConfiguredFallback":`Yapılandırılmış modeller gösteriliyor.`,"pws.modelsTruncated":`{total} modelden ilk {shown} tanesi gösteriliyor.`,"pws.retry":`Tekrar Dene`,"pws.noModels":`Bu sağlayıcı için keşfedilen model yok.`,"pws.noModelMatch":`Filtreyle eşleşen model yok.`,"pws.adapterBaseRequired":`Adaptör ve taban URL gereklidir.`,"pws.addAccount":`Hesap ekle`,"pws.addKey":`API anahtarı ekle`,"pws.apiKeys":`API Anahtarları`,"pws.authMode":`Kimlik doğrulama modu`,"pws.availableAccounts":`Mevcut hesaplar`,"pws.accountOrdinal":`Hesap {count}`,"pws.accountsLoading":`Hesaplar yükleniyor…`,"pws.accountsLoadFailed":`Hesaplar yüklenemedi.`,"pws.retryAccounts":`Tekrar Dene`,"pws.noAccounts":`Henüz bağlı hesap yok.`,"pws.cockpitImportDescription":`Bu cihazdan bir Cockpit Tools Antigravity JSON dışa aktarımını içe aktarın. Dosya içeriği gösterilmez.`,"pws.cockpitImportFileLabel":`Cockpit Tools Antigravity JSON dışa aktarımı`,"pws.cockpitImportChooseFile":`JSON dosyası seç`,"pws.cockpitImporting":`İçe aktarılıyor…`,"pws.cockpitImportInvalid":`Seçilen dosya geçerli bir JSON dışa aktarımı değil veya çok büyük.`,"pws.cockpitImportFailed":`Hesap içe aktarımı tamamlanamadı.`,"pws.cockpitImportComplete":`İçe aktarma tamamlandı: {imported} içe aktarıldı, {updated} güncellendi, {failed} başarısız, {unsupported} desteklenmiyor.`,"pws.accountSwitching":`Değiştiriliyor…`,"pws.accountCurrent":`Mevcut hesap`,"pws.defaultModelNone":`Yok (sağlayıcı varsayılanını kullan)`,"pws.discardSettings":`Vazgeç`,"pws.jsonEditorDesc":`Ham sağlayıcı JSON konfigürasyonunu düzenleyin.`,"pws.jsonEditorTitle":`JSON düzenleyici — {name}`,"pws.jsonRestore":`Geri Yükle`,"pws.jsonSave":`Kaydet`,"pws.loggedInTitle":`Giriş yapıldı`,"pws.notLoggedInTitle":`Giriş yapılmadı`,"pws.note":`Not`,"pws.allowPrivateNetwork":`Yerel/özel ağa izin ver`,"pws.liveModels":`Sağlayıcıdan canlı model keşfet`,"pws.liveModelsDesc":`Sağlayıcının canlı model kataloğunu çekin.`,"pws.xaiResponsesOptIn":`Grok 4.5 ve 4.6 için Responses API kullan`,"pws.xaiResponsesOptInDesc":`İki modeli de openai-responses üzerinden yönlendirir. Diğer Grok modelleri ve katman davranışı değişmez.`,"pws.xaiResponsesOptInMixed":`Kısmen etkin.`,"pws.cursorTransport":`Cursor aktarımı`,"pws.cursorTransportHttp2":`HTTP/2 (varsayılan)`,"pws.cursorTransportHttp1":`HTTP/1.1 (proxy uyumluluğu)`,"pws.cursorTransportDesc":`Proxy'niz Cursor'ın HTTP/2 akışını güvenilir biçimde taşıyamıyorsa HTTP/1.1 kullanın.`,"pws.optionalPlaceholder":`İsteğe bağlı`,"pws.providerId":`Sağlayıcı ID`,"pws.reauth":`Yeniden doğrulama gerekiyor`,"pws.reauthenticate":`Yeniden doğrula`,"pws.copyDoctor":`ocx doctor kopyala`,"pws.doctorCopied":`Kopyalandı`,"pws.doctorCopyUnavailable":`Pano kullanılamıyor`,"pws.healthCooldownHint":`Soğuma süresi bitene kadar bekleyin.`,"pws.healthLabel.rateLimited":`Oran kısıtlandı`,"pws.healthLabel.quotaLimited":`Kota kısıtlandı`,"pws.healthLabel.reauthRequired":`Yeniden doğrulama gerekli`,"pws.healthLabel.refreshFailed":`Yenileme başarısız oldu`,"pws.healthLabel.metadataMismatch":`Meta veri uyuşmazlığı`,"pws.healthLabel.credentialConflict":`Kimlik bilgisi çakışması`,"pws.healthSummary.rateLimited":`{provider} {account}: {until} tarihine kadar oran kısıtlandı.`,"pws.healthSummary.quotaLimited":`{provider} {account}: {until} tarihine kadar kota kısıtlandı.`,"pws.healthSummary.reauthRequired":`{provider} {account}: yeniden doğrulama gerekli.`,"pws.healthSummary.credentialConflict":`{provider} {account}: kimlik bilgisi çakışması.`,"pws.healthSummary.metadataMismatch":`{provider} {account}: meta veri uyuşmazlığı.`,"pws.healthSummary.staleCredentials":`{provider} {account}: eksik kimlik bilgileri.`,"pws.removeConfirm":`Kaldır`,"pws.removeConfirmBody":`"{name}" sağlayıcısı kaldırılsın mı?`,"pws.removeDefaultConfirmBody":`Varsayılan sağlayıcı "{name}" kaldırılsın mı? "{defaultProvider}" varsayılan sağlayıcı olacaktır. Bu işlem geri alınamaz.`,"pws.removeConfirmTitle":`Sağlayıcıyı kaldır`,"pws.saveSettings":`Kaydet`,"pws.pacingTitle":`İstek aralığı`,"pws.pacingDesc":`Bu sağlayıcıya giden istek başlangıçlarını eşit aralıklarla geciktirir. Akış yanıtları çakışabilir.`,"pws.pacingEnabled":`Etkin`,"pws.pacingRpm":`Dakikadaki istek`,"pws.pacingRpmUnit":`RPM`,"pws.pacingDelay":`En kısa aralık (ms)`,"pws.pacingSlowerWins":`Daha yavaş sağlayıcı sınırı geçerlidir. Model kuralları yalnızca ek gecikme getirir.`,"pws.pacingQueued":`kuyrukta`,"pws.pacingNextSlot":`sonraki aralığa`,"pws.pacingLastModel":`son model`,"pws.pacingNone":`Yok`,"pws.pacingModelOverrides":`Model kuralları`,"pws.pacingModel":`Model`,"pws.pacingAdd":`Kural ekle`,"pws.pacingRemove":`Kaldır`,"pws.pacingRemoveModel":`{model} için istek aralığı kuralını kaldır`,"pws.pacingRuleRequired":`Önce bir sağlayıcı sınırı veya model kuralı belirleyin.`,"pws.saving":`Kaydediliyor…`,"pws.settingsSaved":`Ayarlar kaydedildi.`,"pws.accountModeSaved":`Hesap modu kaydedildi.`,"pws.accountModeFailed":`Hesap modu değiştirilemedi.`,"pws.accountModeConfirm":`OpenAI hesap modu değiştirilsin mi? Aktif oturumlar hedef hesap havuzuna yönlendirilecek ve kota yeni mod altında birikecektir.`,"pws.settingsUnsavedBar":`Kaydedilmemiş değişiklikleriniz var.`,"pws.unsavedLeaveBody":`Kaydedilmemiş değişiklikleriniz var. Ayrılmadan önce kaydetmek ister misiniz?`,"pws.unsavedLeaveTitle":`Kaydedilmemiş değişiklikler`,"pws.attentionRequired":`Dikkat gerekiyor`,"pws.attentionAria":`{name}: {reason}`,"pws.missingCredentials":`Kimlik bilgileri eksik`,"pws.editJsonDesc":`Proxy konfigürasyonunu JSON olarak düzenleyin`,"pws.updatesUnavailable":`Sağlayıcı güncellemeleri mevcut değil.`,"pws.dashboard.title":`Sağlayıcılara genel bakış`,"pws.dashboard.subtitle":`Tüm model sağlayıcılarınızı tek bir yerden yönetin.`,"pws.dashboard.rateLimits":`ORAN LİMİTLERİ`,"pws.capacity.estimate":`Havuz kapasite tahmini`,"pws.capacity.currentAccount":`Mevcut geçerli hesap`,"pws.capacity.nextRecovery":`Sonraki kapasite yenilenmesi`,"pws.capacity.recoveryShare":`+%{percent} havuz kapasitesi`,"pws.capacity.incomplete":`Kısmi pencere kapsamı ({excluded} hariç tutuldu)`,"pws.capacity.uncalibratedPlan":`Kalibre edilmemiş plandaki {count} hesap temel koltuk ağırlığıyla sayılır; bu tahmin ihtiyatlı olabilir`,"pws.capacity.partial":`Kısmi ({count} hesap kota metriği bildiriyor)`,"pws.capacity.windowPartial":`Kısmi`,"pws.capacity.windowPartialA11y":`{window}: eksik hesap kapsamı`,"pws.dashboard.recentlyUsed":`SON KULLANILANLAR`,"pws.dashboard.requests":`{count} istek`,"pws.dashboard.checkedAgo":`{time} önce kontrol edildi`,"pws.dashboard.noQuota":`Kota verisi yok`,"pws.dashboard.noUsage":`Henüz kullanım verisi yok`,"pws.dashboard.noRateLimits":`Henüz oran limiti verisi yok`,"pws.allProviders":`Sağlayıcı Genel Bakışı`,"pws.enabledLabel":`Etkin`,"pws.testConnection":`Bağlantıyı test et`,"pws.testing":`Test ediliyor…`,"pws.connectionOk":`Bağlantı Başarılı`,"pws.connectionFailed":`Bağlantı Başarısız`,"pws.connectionNotApplicable":`Uygulanamaz — bu sağlayıcı statik katalog kullanıyor.`,"pws.editSettings":`Ayarları düzenle`,"pws.viewUsage":`Detaylı kullanımı gör`,"pws.allSystemsOk":`Tüm sistemler çalışır durumda`,"pws.apiKeyConfigured":`API anahtarı yapılandırıldı`,"pws.addApiKey":`API anahtarı ekle`,"pws.loggedInAs":`{email} olarak giriş yapıldı`,"pws.notLoggedIn":`Giriş yapılmadı`,"pws.passthrough":`Codex doğrudan geçiş`,"pws.notes":`NOTLAR`,"pws.notePlaceholder":`Bu sağlayıcı hakkında bir not ekleyin...`,"pws.noteSaved":`Not kaydedildi`,"pws.authSummary":`KİMLİK DOĞRULAMA`,"time.justNow":`Az önce`,"time.notChecked":`Kontrol edilmedi`,"time.minutesAgo":`{n}dk önce`,"time.hoursAgo":`{n}sa önce`,"time.daysAgo":`{n}gün önce`,"modal.noMatch":`Eşleşme yok.`,"modal.oauthDefaultNote":`Hesabınızla giriş yapın — API anahtarı gerekmez.`,"modal.oauthComingSoon":`{label} için OAuth girişi gelecektir. Şimdilik API anahtarı kullanın.`,"modal.oauthComingSoonShort":`Bu sağlayıcı için OAuth girişi gelecektir — şimdilik API anahtarı kullanın.`,"modal.useApiKeyInstead":`Bunun yerine API anahtarı kullanın`,"modal.setupGuide":`Kurulum rehberi`,"modal.setupStep1Prefix":`Adresine gidin:`,"modal.setupDashboardLink":`{label} paneli`,"modal.setupStep1Suffix":`ve API anahtarınızı kopyalayın`,"modal.setupStep2":`Aşağıdaki API anahtarı alanına yapıştırın`,"modal.setupStep3":`Sağlayıcı ekle'ye tıklayın — modeller otomatik keşfedilir`,"modal.namePlaceholder":`örn. openrouter`,"modal.duplicateWarn":`"{name}" sağlayıcısı mevcut ve üzerine yazılacak.`,"modal.forwardHintPrefix":`Anahtar gerekmez — proxy kimlik bilgilerinizi iletir`,"modal.forwardCredentials":`codex girişi`,"modal.forwardHintSuffix":`kimlik bilgilerini bu sağlayıcıya iletir.`,"modal.localHint":`Yerel mod, Cursor'ın statik kamuya açık kataloğunu kaydeder. Canlı taşıma, dosya ve kabuk (shell) araçları denetlenene kadar kapalı kalır.`,"modal.getApiKey":`{label} API anahtarınızı alın`,"modal.apiKey":`API anahtarı`,"modal.apiKeyTransport":`API anahtar başlığı`,"modal.apiKeyTransportNative":`x-api-key (Anthropic yerel)`,"modal.apiKeyTransportBearer":`Authorization: Bearer`,"modal.apiKeyPlaceholder":`sk-… (veya $ENV_VAR)`,"modal.defaultModelPlaceholder":`örn. gpt-5.5`,"modal.baseUrlPlaceholder":`https://...`,"modal.baseUrlPlaceholderError":`Taban URL çözümlenmemiş bir {placeholder} içeriyor.`,"modal.baseUrlPlaceholderHint":`Eklemeden önce {placeholder} değerini gerçek Hesap ID'nizle değiştirin.`,"modal.adding":`Ekleniyor…`,"modal.useOauthLogin":`← OAuth girişini kullan`,"nav.codexAuth":`Codex Kimlik Doğrulama`,"nav.codexSet":`Codex Ayarları`,"codexSet.tab.multiauth":`Çoklu kimlik doğrulama`,"codexSet.tab.prompt":`İstem`,"codexSet.prompt.title":`İstem katmanları`,"codexSet.prompt.timing":`Yeni başlatılan oturumlara uygulanır. Çalışan oturumlar mevcut istem ayarlarını korur.`,"codexSet.prompt.staleRevision":`Yapılandırma başka bir yerde değişti. Liste yeniden yüklendi.`,"codexSet.prompt.writeFailed":`Değişiklik kaydedilemedi.`,"codexSet.prompt.loadFailed":`İstem katmanları yüklenemedi.`,"codexSet.prompt.repair":`Onar`,"codexSet.prompt.repairFailed":`Onarım tamamlanamadı.`,"codexSet.drift.journalPresent":`Önceki yazma işlemi tamamlanmadı. Kurtarma bir sonraki yazma sırasında otomatik olarak çalışır.`,"codexSet.drift.projectionStale":`Kaydedilen katmanlar ile config.toml içindeki değer uyuşmuyor. Onarım, değeri katmanlarınıza göre yeniden yazar.`,"codexSet.drift.storeMissing":`Katman dosyası yok ancak config.toml içinde talimatlar duruyor. Onarım önce yedek alır ve metni tek bir katman olarak saklar.`,"codexSet.drift.ownedMalformed":`config.toml içinde oluşturulan satır elle değiştirilmiş, bu yüzden yeniden yazmak artık güvenli değil.`,"codexSet.custom.adoptUnsupported":`{path} dosyasının {line}. satırındaki değer tek satırlık bir dizge olmadığından içe aktarılamaz. Burada yönetmek için elle taşıyın.`,"codexSet.prompt.unreadable":`Codex yapılandırma dosyası var ancak okunamadı, bu yüzden değişiklikler reddedildi.`,"codexSet.layer.permissions":`İzinler`,"codexSet.layer.collaboration":`İş birliği modu`,"codexSet.layer.environment":`Ortam bağlamı`,"codexSet.layer.apps":`Uygulamalar`,"codexSet.layer.skills":`Beceriler`,"codexSet.prompt.extensionsUnknown":`Uzantılar kendi katmanlarını ekleyebilir. Codex bunları göstermediği için burada listelenemez.`,"codexSet.group.transition":`Geçiş bildirimleri`,"codexSet.group.transitionDesc":`Durumu anlatmak yerine bir değişikliği bildirirler; bu yüzden yalnızca oturum gerçek zamanlıya geçtiğinde veya model değiştiğinde görünürler.`,"codexSet.custom.slotNote":`Özel katmanlar bu sırayla birleştirilip tek bir bölüm olur.`,"codexSet.row.alwaysOn":`Her zaman açık`,"codexSet.row.onChange":`Değişimde gönderilir`,"codexSet.row.featureGated":`[features] altında yapılandırılır`,"codexSet.row.openFeatures":`Ayarları aç`,"codexSet.dialog.setValue":`{value} (varsayılan {fallback})`,"codexSet.dialog.copyKey":`Anahtarı kopyala`,"codexSet.dialog.unknownLayer":`Bu derlemede bu katman için açıklama yok. Panelden daha yeni bir Codex çalışma zamanından geliyor.`,"codexSet.custom.heading":`Özel katmanlar`,"codexSet.custom.add":`+ Katman ekle`,"codexSet.custom.newTitle":`Yeni katman`,"codexSet.custom.editTitle":`Katmanı düzenle`,"codexSet.custom.titleLabel":`Başlık`,"codexSet.custom.bodyLabel":`Talimatlar`,"codexSet.custom.bodySize":`{max} baytın {bytes} baytı`,"codexSet.custom.normalized":`Sekmeler dört boşluğa, satır sonları LF biçimine dönüştürüldü.`,"codexSet.custom.titleRequired":`Bir başlık girin.`,"codexSet.custom.titleTooLong":`Başlık {count} karakter; sınır {max}.`,"codexSet.custom.titleMultiline":`Başlık tek satır olmalıdır.`,"codexSet.custom.bodyTooLarge":`Bu katman {bytes} bayt; sınır {max}.`,"codexSet.custom.composedTooLarge":`Etkin katmanların toplamı {bytes} bayt olacak ve sınırı aşacak.`,"codexSet.custom.invalidCharacter":`{position} konumundaki denetim karakteri kaydedilemez.`,"codexSet.custom.discardPrompt":`Değişiklikler silinsin mi?`,"codexSet.custom.keepEditing":`Düzenlemeye devam et`,"codexSet.custom.delete":`{title} katmanını sil`,"codexSet.custom.deleteConfirm":`Bu katman silinsin mi? Bu işlem geri alınamaz.`,"codexSet.custom.layerGone":`Bu katman başka bir yerde silindiği için düzenleyici kapatıldı.`,"codexSet.custom.deleteConfirmNamed":`“{title}” silinsin mi? Bu işlem geri alınamaz.`,"codexSet.custom.moveUp":`{title} katmanını yukarı taşı`,"codexSet.custom.prevLayer":`Önceki katman`,"codexSet.custom.nextLayer":`Sonraki katman`,"codexSet.custom.navPosition":`{position} / {total}`,"codexSet.custom.moveDown":`{title} katmanını aşağı taşı`,"codexSet.custom.limitReached":`En fazla {max} özel katman saklayabilirsiniz.`,"codexSet.custom.notOwned":`developer_instructions opencodex dışında yazıldığı için buradan düzenlenemez. Katman olarak yönetmek için içe aktarın.`,"codexSet.custom.adopt":`Mevcut talimatları içe aktar`,"codexSet.custom.adoptConfirm":`Katman olarak içe aktar`,"codexSet.custom.adoptRefused":`Mevcut değer içe aktarılamadı.`,"codexSet.custom.baseReplaced":`model_instructions_file {path} olarak ayarlandığından, opencodex dışındaki bir öğe temel istemi değiştirmiş.`,"codexSet.lint.identity":`Bu, Codex tarafından belirlenenden farklı bir kimlik iddia ediyor.`,"codexSet.lint.foreignTool":`Araçlar kayıt defterinden gelir; burada bir aracın adını belirtmek onu oluşturmaz.`,"codexSet.lint.placeholder":`Talimatlar bir şablon motorundan geçmez, bu nedenle bu metin olduğu gibi gönderilir.`,"codexSet.lint.applyPatch":`apply_patch talimatlar tarafından değil, araç kayıt defteri tarafından tanımlanır.`,"codexSet.lint.approvalVocab":`Codex kendi onay terimlerini ekler; bu metin onlarla çelişebilir.`,"codexSet.lint.environment":`Ortam bilgileri daha sonra oluşturulur ve bu metinle çelişebilir.`,"codexSet.lint.size":`Bu katman 8 KB sınırını aşıyor. Yine de kaydedilir, ancak her istekte belirteç harcar.`,"codexSet.preset.blank":`Boş katman`,"codexSet.preset.concise.name":`Kısa çıktı`,"codexSet.preset.concise.description":`Kısa yanıtlar, giriş yok, en az biçimlendirme.`,"codexSet.preset.concise.provenance":`Claude Code'un kısa yanıt talimatlarından uyarlandı. İfade bize aittir, kopya değildir.`,"codexSet.preset.planFirst.name":`Düzenlemeden önce planla`,"codexSet.preset.planFirst.description":`Önce planı belirt, ardından değişikliği yap.`,"codexSet.preset.planFirst.provenance":`Claude Code'un planlama yaklaşımından uyarlandı. İfade bize aittir, kopya değildir.`,"codexSet.preset.explainWhy.name":`Gerekçeyi açıkla`,"codexSet.preset.explainWhy.description":`Yalnızca ne olduğunu değil, nedenini de belirt.`,"codexSet.preset.explainWhy.provenance":`Grok Build'in onaylama tarzından uyarlandı. İfade bize aittir, kopya değildir.`,"codexSet.preset.testFirst.name":`Önce test`,"codexSet.preset.testFirst.description":`Düzeltmeden önce başarısız olan testi yaz.`,"codexSet.preset.testFirst.provenance":`Yaygın ajan uygulamalarından uyarlandı. İfade bize aittir, kopya değildir.`,"codexSet.preset.korean.name":`Korece yanıtlar`,"codexSet.preset.korean.description":`İstek hangi dilde olursa olsun Korece yanıt ver.`,"codexSet.preset.korean.provenance":`Sık istenen bir ihtiyaçtan yola çıkarak opencodex için yazıldı. İfade bize aittir, kopya değildir.`,"codexSet.dialog.class":`Tür`,"codexSet.dialog.key":`Yapılandırma anahtarı`,"codexSet.dialog.fileValue":`Bu dosyadaki değer`,"codexSet.dialog.absentDefault":`ayarlanmamış (varsayılan: {value})`,"codexSet.dialog.noRenderedText":`Codex yerleşik bir katmanın birleştirilmiş metnini göstermez. Bu nedenle bu iletişim kutusu içeriği göstermek yerine katmanı açıklar ve anahtarını belirtir.`,"codexSet.dialog.sourceText":`Modele gönderilen metin`,"codexSet.dialog.sourceBytes":`{bytes} bayt`,"codexSet.dialog.notRendered":`Okuduğumuz turda bu katman hiçbir şey göndermedi. Bölümler yalnızca değiştiklerinde yeniden gönderilir, bu yüzden tek bir örnekte görünmeyebilir.`,"codexSet.dialog.emptySource":`{path} konumundaki dosya var ancak boş, bu yüzden bu katman hiçbir şey göndermiyor.`,"codexSet.dialog.notExposed":`Temel istem, Codex'in yazdırabildiği mesaj listesinin dışından geçtiği için burada gösterilemez. model_instructions_file ile değiştirilebilir.`,"codexSet.dialog.textUnavailable":`Bu makinede Codex istemi okunamadı, bu yüzden metin kullanılamıyor.`,"codexSet.class.base":`Temel talimatlar`,"codexSet.class.config-toggle":`Buradan değiştirilebilir`,"codexSet.class.feature-gated":`Özellik bayraklı`,"codexSet.class.runtime-conditional":`Çalışma zamanına bağlı`,"codexSet.class.extension-unknown":`Uzantı katmanı`,"codexSet.layer.base-instructions":`Temel talimatlar`,"codexSet.layer.model-switch":`Model değişikliği bildirimi`,"codexSet.layer.personality":`Kişilik`,"codexSet.layer.context-window-guidance":`Bağlam penceresi yönlendirmesi`,"codexSet.layer.realtime":`Gerçek zamanlı`,"codexSet.layer.agents-md":`AGENTS.md`,"codexSet.layer.environments-instructions":`Yürütme ortamları`,"codexSet.layer.plugins":`Eklentiler`,"codexSet.layer.tools":`Araçlar`,"codexSet.layer.multi-agent-mode":`Çoklu ajan modu`,"codexSet.layer.git-attribution":`Commit atıfları`,"codexSet.about.base-instructions":`Codex'in kendi talimatlarıdır. İstekle birlikte gönderilir ve kapatılamaz.`,"codexSet.about.model-switch":`Oturum sırasında model değiştiğinde eklenir.`,"codexSet.about.personality":`Bir özellik bayrağının yönettiği ton ve anlatım yönlendirmesi.`,"codexSet.about.context-window-guidance":`Bir özellik bayrağının yönettiği kalan bağlam bütçesi önerileri.`,"codexSet.about.realtime":`Gerçek zamanlı oturumlarda eklenir.`,"codexSet.about.agents-md":`Projenizin AGENTS.md dosyalarıdır. Bu sayfa katmanı gösterir, proje belgelerinizi değiştirmez.`,"codexSet.about.permissions":`Geçerli korumalı alan ve onay ayarlarını açıklar.`,"codexSet.about.collaboration":`Etkin iş birliği modunu açıklar.`,"codexSet.about.environment":`Çalışma dizini, platform ve diğer ortam bilgileri.`,"codexSet.about.environments-instructions":`Bir özellik bayrağının yönettiği ertelenmiş yürütme ortamı talimatları.`,"codexSet.about.apps":`Bağlı uygulamaların nasıl kullanılacağını açıklar.`,"codexSet.about.plugins":`Bir eklenti seçildiğinde veya herhangi bir eklenti bir yetenek bildirdiğinde eklenir.`,"codexSet.about.tools":`Bir özellik bayrağının yönettiği ertelenmiş araç açıklamaları.`,"codexSet.about.skills":`Kullanılabilir becerilerin listesi.`,"codexSet.about.multi-agent-mode":`Bir özellik bayrağının yönettiği alt ajan talimatları.`,"codexSet.about.git-attribution":`Modelin yazdığı commit’lere Co-authored-by: Codex trailer’ını, açtığı pull request’lere de Generated with Codex. satırını eklemesini söyler. Codex bunu hesabınızdan okur; ne burada ne de [features] altında değiştirilebilir. Hesabınız kapattığında Codex hiçbir şey göndermek yerine tersi yönde talimat gönderir.`,"codexSet.condition.model-switch":`Yalnızca oturum sırasında model değiştikten sonra eklenir.`,"codexSet.condition.realtime":`Yalnızca gerçek zamanlı oturumlarda eklenir.`,"codexSet.condition.agents-md":`Çalışma dizini için bir proje belgesi bulunduğunda eklenir.`,"codexSet.condition.plugins":`Bir eklenti seçildiğinde veya herhangi bir eklenti bir yetenek bildirdiğinde eklenir.`,"codexSet.condition.git-attribution":`Hesabınızın atıf politikası belirler.`,"codexSet.base.title":`Temel istem`,"codexSet.base.prev":`Önceki seçenek`,"codexSet.base.next":`Sonraki seçenek`,"codexSet.base.position":`{position} / {total}`,"codexSet.base.swipeHint":`Yana kaydırın, yön tuşlarını ya da ok düğmelerini kullanarak seçenekler arasında geçin. Yeni başlatılan oturumlarda geçerlidir.`,"codexSet.base.defaultTitle":`Codex’in kendi temel istemi`,"codexSet.base.defaultBody":`Varsayılan burada saklanmaz, dolayısıyla düzenlenecek veya silinecek bir şey yoktur: seçmek yapılandırmadan model_instructions_file satırını kaldırır ve Codex kendi istemini kullanır.`,"codexSet.base.variantTitle":`Ad`,"codexSet.base.variantBody":`İstem`,"codexSet.base.replacesWarning":`Bu, Codex’in temel istemine eklemek yerine onun YERİNE geçer. Buraya kısa yazarsanız modelin talimatları da o kadar kısa olur.`,"codexSet.base.use":`Bunu kullan`,"codexSet.base.inUse":`Kullanımda`,"codexSet.base.externalBlocked":`model_instructions_file zaten {path} yolunu gösteriyor ve bunu opencodex yazmadı. Buradan seçim yapmadan önce kendiniz temizleyin.`,"nav.api":`API`,"nav.integrations":`Entegrasyonlar`,"nav.openMenu":`Menüyü aç`,"nav.closeMenu":`Menüyü kapat`,"integrations.subtitle":`İstemcileri opencodex'e bağlayın, kimlik bilgilerini yönetin.`,"integrations.tabsLabel":`Entegrasyon yüzeyleri`,"integrations.tab.overview":`Genel Bakış`,"integrations.tab.keys":`API Anahtarları`,"integrations.tab.codex":`Codex`,"integrations.tab.claude":`Claude`,"integrations.tab.grok":`Grok Build`,"integrations.tab.opencode":`OpenCode`,"integrations.tab.pi":`Pi`,"integrations.tab.omp":`OMP`,"integrations.tab.hermes":`Hermes`,"integrations.tab.openclaw":`OpenClaw`,"integrations.tab.kimi":`Kimi Code`,"integrations.tab.gajae":`Gajae Code`,"integrations.tab.dsh":`DSH`,"integrations.tab.mcode":`MiniMax Code`,"integrations.tab.zcode":`ZCode`,"integrations.tab.prime":`Prime Agent`,"integrations.tab.aside":`Aside`,"integrations.codex.title":`Codex CLI`,"integrations.codex.body":`Codex bağlantısı proxy servisine aittir.`,"integrations.codex.openService":`Servis kontrollerini aç`,"integrations.state.notInstalled":`Yüklü değil`,"integrations.state.unknown":`Kontrol ediliyor…`,"integrations.detail.codexRouted":`Codex istekleri bu proxy üzerinden geçer`,"integrations.detail.codexAbsent":`Codex henüz bu proxy üzerinden yönlendirilmedi`,"integrations.detail.keyCount":`{count} anahtar oluşturuldu`,"integrations.detail.keyNone":`Oluşturulmuş anahtar yok`,"integrations.detail.keyChecking":`Kontrol ediliyor…`,"integrations.detail.keyUnavailable":`Anahtar durumu kullanılamıyor`,"integrations.detail.claudeOff":`Bağlantı kapalı`,"integrations.detail.desktopCurrent":`Desktop bu profili çalıştırıyor`,"integrations.detail.desktopStale":`Profil dosyası değiştirildi`,"integrations.detail.desktopNotServed":`Profil mevcut ancak Desktop başkasını kullanıyor`,"integrations.detail.desktopAbsent":`Uygulanan profil yok`,"integrations.detail.desktopDesiredOff":`Claude Desktop entegrasyonu kapalı`,"integrations.detail.desktopDesiredOffCleanupPending":`Claude Desktop hâlâ ağ geçidini kullanıyor; temizlik bekleniyor`,"integrations.detail.desktopDesiredOnNotApplied":`Entegrasyon açık ancak Desktop kullanmıyor`,"integrations.detail.desktopSelectedElsewhere":`Desktop başka bir profil kullanıyor`,"integrations.detail.desktopProfileDrift":`Seçilen Desktop profili değişti`,"integrations.detail.desktopObservedUnsafe":`Desktop profili güvenle değiştirilemiyor`,"integrations.detail.desktopNotInstalled":`Claude Desktop kütüphanesi yüklü değil`,"integrations.detail.grokModels":`{count} model bağlandı`,"integrations.detail.grokAbsent":`Konfigürasyonda opencodex bloğu yok`,"integrations.dialog.grok.title":`Grok Build entegrasyonu devre dışı bırakılsın mı?`,"integrations.dialog.grok.changes":`Yalnızca {path} dosyasında opencodex tarafından işaretlenen blok kaldırılacaktır. Blok dışında yazılan içerik değişmeden kalır.`,"integrations.dialog.grok.breakage":`Devre dışı bırakmak, opencodex model takma adlarını Grok Build'den kaldırır. xAI hesabınızla kullanılan modeller kullanılabilir kalır.`,"integrations.dialog.grok.undo":`opencodex bir geri döngü (loopback) adresinde çalışıyorsa, bunu tekrar açmak şu anda mevcut olan modellerden yeni bir blok yazar.`,"integrations.dialog.grok.confirm":`Devre Dışı Bırak`,"integrations.dialog.desktop.title":`Claude Desktop entegrasyonu devre dışı bırakılsın mı?`,"integrations.dialog.desktop.changes":`Eğer {path} opencodex tarafından yönetilen bir ağ geçidi profili içeriyorsa, Desktop önce kimlik bilgisiz yeni bir standart profil seçer, ardından eski profili ve yedeği kaldırır.`,"integrations.dialog.desktop.breakage":`Claude Desktop, opencodex üzerinden yönlendirilen modeller yerine standart Claude'a geri dönecektir.`,"integrations.dialog.desktop.undo":`Bunu tekrar açmak, kaydedilmiş model atamalarınızdan opencodex profilini yeniden oluşturur.`,"integrations.dialog.desktop.restart":`Claude Desktop bu yapılandırmayı yalnızca açılışta okur. Değişikliğin yürürlüğe girmesi için uygulamayı tamamen kapatıp yeniden açın.`,"integrations.dialog.desktop.confirm":`Devre Dışı Bırak`,"integrations.native.msg.nonLoopbackRemoved":`Grok Build yalnızca opencodex bir geri döngü (loopback) adresinde çalışırken otomatik olarak kaydolabilir. Loopback'i işaret eden önceki blok kaldırıldı.`,"integrations.native.msg.nonLoopbackRemovedNoop":`Grok Build yalnızca opencodex bir geri döngü (loopback) adresinde çalışırken otomatik olarak kaydolabilir. Kaldırılacak önceki bir blok yoktu.`,"integrations.native.msg.nonLoopbackSuperseded":`Grok Build yalnızca opencodex bir geri döngü adresi üzerinde çalışırken otomatik kaydedilebilir. Bu sırada başka bir işlem yeni bir blok yazdı, bu nedenle şu an dosyadaki blok bu istek tarafından oluşturulmadı.`,"integrations.native.error.orphanedMarker":`{path} bir opencodex başlangıç işaretine sahip ancak bitiş işareti yok. opencodex bloğunun nerede bittiğini belirleyemediği için dosya değiştirilmeden bırakıldı.`,"integrations.native.error.homeMismatch":`Yüklü servis dizini mevcut ev dizini ile uyuşmuyor, bu nedenle dosya değiştirilmeden bırakıldı.`,"integrations.native.error.notInstalled":`Grok Build yüklü değil, bu nedenle değiştirilecek bir şey yok.`,"integrations.native.error.configBusy":`Yapılandırma başka bir yerde kaydediliyor ve değiştirilemedi. Kısa süre sonra tekrar deneyin.`,"integrations.native.error.desktopUnsafeMetadata":`{path} konumundaki Claude Desktop meta verileri güvenle okunamadı, bu nedenle kütüphanesi değiştirilmedi.`,"integrations.native.error.desktopCleanupIncomplete":`Claude Desktop standart moda yönlendirildi, ancak eski opencodex kimlik bilgisi dosyaları şu konumda kalmaya devam ediyor: {paths}.`,"integrations.native.msg.desktopDisabled":`Claude Desktop entegrasyonu devre dışı bırakıldı.`,"integrations.native.msg.desktopEnabled":`Claude Desktop entegrasyonu etkinleştirildi.`,"integrations.state.absent":`Uygulanmadı`,"integrations.state.current":`Uygulandı`,"integrations.state.stale":`Güncelleme gerekiyor`,"integrations.state.conflict":`Çakışma`,"integrations.state.unsafe":`Doğrulanamadı`,"integrations.summary.detected":`Algılanan istemciler`,"integrations.summary.applied":`Yapılandırılan istemciler`,"integrations.summary.stale":`Güncelleme gerekiyor`,"integrations.summary.lastChange":`Son değişiklik`,"integrations.summary.disableAll":`Tümünü devre dışı bırak…`,"integrations.onboarding":`Uygulama işlemi bir yedek aldıktan sonra yazar.`,"integrations.empty.title":`Yüklü istemci algılanmadı`,"integrations.empty.body":`Desteklenen bir istemci yükleyin.`,"integrations.action.apply":`Uygula`,"integrations.action.disable":`Devre Dışı Bırak`,"integrations.action.refresh":`Güncelle`,"integrations.action.settings":`Ayarlar`,"integrations.action.manageKeys":`Anahtarları yönet`,"integrations.action.restore":`Geri Yükle…`,"integrations.action.undo":`Geri Al`,"integrations.action.restorePoint":`Bu noktayı geri yükle…`,"integrations.action.snapshotExpired":`Yedek süresi doldu`,"integrations.rollback.title":`Geri alma merkezi`,"integrations.rollback.empty":`Henüz uygulama geçmişi yok`,"integrations.rollback.emptyBody":`Her başarılı yazma bir anlık görüntü saklar.`,"integrations.catalog.title":`İstemciler`,"integrations.rollback.older":`Önceki işlemler`,"integrations.rollback.showMore":`{n} tane daha göster`,"integrations.rollback.failed":`Geri alma geçmişi yüklenemedi.`,"integrations.restore.title":`Bu anlık görüntü geri yüklensin mi?`,"integrations.restore.body":`Mevcut dosya önce yedeklenir.`,"integrations.restore.driftTitle":`Daha yeni düzenlemeler algılandı`,"integrations.restore.driftBody":`Bu anlık görüntüden sonra yapılan değişiklikler yedeklenecektir.`,"integrations.restore.confirm":`Geri Yükle`,"integrations.restore.confirmDrift":`Yedekle ve geri yükle`,"integrations.restore.pending":`Geri yükleniyor…`,"integrations.restore.manual":`{reason}: {path} dosyasını el ile geri yükleyin`,"integrations.error.load":`Entegrasyon durumu yüklenemedi.`,"integrations.error.stale":`Son yenileme başarısız oldu.`,"integrations.error.busy":`İşlem devam ediyor.`,"integrations.error.conflict":`Konfigürasyon yazıldıktan sonra değişti.`,"integrations.error.unsafe":`Konfigürasyon güvenle değiştirilemiyor.`,"integrations.error.generic":`Entegrasyon değişikliği başarısız oldu.`,"integrations.error.nonLoopback":`{client} istemcisi geri döngü (loopback) dışında bir adreste. Yerel entegrasyonlar yalnızca loopback (127.0.0.1 veya ::1) üzerinden düzenlenebilir. Bir API anahtarı ayarlayın veya yerel erişim kullanın.`,"integrations.status.installed":`Yüklü`,"integrations.status.notInstalled":`Yüklü değil`,"integrations.status.appliedAt":`Uygulandı`,"integrations.status.backup":`Yedek`,"integrations.status.lastRestore":`Son geri yükleme`,"integrations.status.unknown":`Bilinmiyor`,"integrations.bulk.title":`Uygulanan istemci entegrasyonları devre dışı bırakılsın mı?`,"integrations.bulk.body":`Yalnızca opencodex bloğu kaldırılır.`,"integrations.bulk.partial":`Bazı istemciler devre dışı bırakılamadı: {clients}`,"integrations.bulk.success":`Uygulanan istemci entegrasyonları devre dışı bırakıldı.`,"integrations.retention.degraded":`Yedek temizliği geride kaldı.`,"integrations.error.residual":`{path} konumunda {message}`,"integrations.error.recover":`{path} kurtarılırken {message}`,"integrations.kind.apply":`Uygulandı`,"integrations.kind.disable":`Devre Dışı Bırakıldı`,"integrations.kind.refresh":`Güncellendi`,"integrations.kind.restore":`Geri Yüklendi`,"integrations.kind.overwrite":`Üzerine yazıldı`,"integrations.dialog.overwrite.title":`Bu yapılandırmadaki blok değiştirilsin mi?`,"integrations.dialog.overwrite.changesUnowned":`{path} içinde opencodex'in ihtiyaç duyduğu yeri, bizim yazmadığımız bir blok tutuyor. Uygulamak onu opencodex'in yazacağı blokla değiştirir.`,"integrations.dialog.overwrite.changesForeign":`{path} içindeki opencodex bloğuna yaptığınız değişiklik atılacak ve opencodex'in yazacağı blokla değiştirilecek.`,"integrations.dialog.overwrite.breakage":`Diğer bloğun yaptığı ayarlar artık geçerli olmaz. Dosyanın kalanına dokunulmaz.`,"integrations.dialog.overwrite.undo":`Önce bir anlık görüntü kaydedilir; bu işlem aşağıdaki geri alma listesinde görünür ve geri alınabilir.`,"integrations.dialog.overwrite.confirm":`Değiştir`,"integrations.action.overwrite":`Değiştir`,"integrations.semantics.opencode":`Doğrudan disk başlatmaları.`,"integrations.semantics.pi":`Yeni oturumlara uygulanır.`,"integrations.semantics.hermes":`Yeni oturumlara uygulanır.`,"integrations.semantics.openclaw":`Anında uygulanır.`,"integrations.semantics.kimi":`Uygulamak için yeniden başlatın.`,"integrations.semantics.gajae":`Yeni oturuma uygulanır.`,"integrations.semantics.dsh":`OpenCodex yalnızca $DSH_HOME/settings.yaml içindeki llm-pi-ai.providers.opencodex bölümünü yönetir. DSH bu sağlayıcıyı çalışırken yeniden yükler; varsayılan modeliniz ve deepseek-official değişmez. Şimdilik yalnızca geri döngü desteklenir; gerçek kimlik bilgisi yazılmaz.`,"integrations.semantics.mcode":`Yalnızca custom_provider.opencodex bölümünü yönetir. Varsayılan model ve MiniMax oturumu değişmez.`,"integrations.semantics.zcode":`Yalnızca ~/.zcode/v2/config.json içindeki provider.opencodex bölümünü yönetir. Z.ai oturumu ve diğer sağlayıcılar değişmez. Değişikliklerden sonra ZCode'u yeniden başlatın.`,"integrations.semantics.prime":`Yalnızca Prime Agent'ın models.json dosyasındaki providers.opencodex bölümünü yönetir — PRIME_AGENT_CODING_AGENT_DIR ayarlı değilse ~/.prime/agent. Diğer sağlayıcılar ve model geçersiz kılmaları değişmez. Yeni oturumlarda geçerli olur.`,"integrations.semantics.aside":`Yalnızca oturum açmış hesabın Aside models.json dosyasındaki providers.opencodex bölümünü yönetir (~/.aside/u/). Diğer sağlayıcılar değişmez. Aside çalışırken bu dosyayı yeniden yazar; bu nedenle uyguladıktan sonra Aside'ı tamamen kapatıp yeniden açın.`,"integrations.semantics.omp":`Kataloğu yüklemek için OMP'yi yeniden başlatın.`,"codexAuth.mainAccount":`Ana Hesap`,"codexAuth.logLabel":`Günlük etiketi`,"codexAuth.codexApp":`Codex Uygulaması`,"codexAuth.moreActions":`Daha fazla işlem göster`,"codexAuth.copyId":`Hesap kimliğini kopyala`,"codexAuth.appLogin":`Uygulama girişi`,"codexAuth.accountPool":`Hesap Havuzu`,"codexAuth.accountModeTitle":`OpenAI hesap modu`,"codexAuth.accountModePool":`Havuz modu`,"codexAuth.accountModePoolDesc":`Ana giriş ve eklenen hesaplar burada döner.`,"codexAuth.accountModeDirect":`Doğrudan mod`,"codexAuth.accountModeDirectDesc":`İstekler yalnızca ana girişi kullanır.`,"codexAuth.accountPickerTitle":`Model seçiciden belirli bir Codex hesabını hedefleyin`,"codexAuth.accountPickerOffDesc":`Etkinleştirildiğinde, sıradan GPT seçici satırlarının yerini her hesap seçicisi için bir giriş alır, böylece çıkış yapmadan bir konuşma için tam hesabı seçebilirsiniz. Kapatılması hiçbir hesabı kaldırmaz.`,"codexAuth.accountPickerOnDesc":`Her seçici, kayıtlı bir hesap için genel bir etikettir. Bunu seçmek, konuşmayı eşlenen hesaba kilitler: asla dönmez veya yedek hesaba geçmez ve aktif Havuz hesabını değiştirmez.`,"codexAuth.accountPickerCompatibility":`Yerleşik Codex Uygulama girişinin kendi seçicisi vardır; oluşturulan haritalar normalde bunu main olarak adlandırır ve gerektiğinde main-2 gibi çakışmasız bir son ek kullanır. Eklenen hesaplar kararlı ve gizlilik açısından güvenli etiketler alırken, özel seçici etiketleri değişmeden kalır. Mevcut konuşmalar ve kaydedilmiş model seçimleri yönlendirilmeye devam eder. Bunu kapatmak oluşturulan girişleri gizler ancak seçicileri ve tam rotaları korur. Yalın GPT model kimlikleri Havuz veya Doğrudan davranışlarını sürdürür.`,"codexAuth.accountPickerUpdated":`Hesap hedefleme güncellendi.`,"codexAuth.accountPickerUpdateFailed":`Hesap hedefleme güncellenemedi. Son doğrulanan ayar gösteriliyor.`,"codexAuth.accountPickerLoadFailed":`Hesap hedefleme ayarı yüklenemedi.`,"codexAuth.accountPickerRefreshFailed":`Bu ayar yenilenemedi. Son doğrulanan değer hâlâ gösteriliyor.`,"codexAuth.advancedSettings":`Gelişmiş ayarlar`,"codexAuth.advancedSettingsAria":`Gelişmiş Codex Auth ayarlarını göster veya gizle`,"codexAuth.catalogRefreshPending":`Değişiklik kaydedildi ancak Codex model kataloğunun yenilenmesi bekleniyor. Yeniden denemek için ocx sync çalıştırın.`,"codexAuth.openaiMissing":`Yerleşik OpenAI sağlayıcısı yapılandırılmamış.`,"codexAuth.openaiDisabled":`Yerleşik OpenAI sağlayıcısı devre dışı.`,"codexAuth.openaiUnavailableDesc":`OpenAI hesaplarınız kullanılabilir.`,"codexAuth.enableOpenai":`OpenAI'yi Etkinleştir`,"codexAuth.enablingOpenai":`Etkinleştiriliyor...`,"codexAuth.enableOpenaiFailed":`OpenAI sağlayıcısı etkinleştirilemedi.`,"codexAuth.openaiPresetLoadFailed":`Ayar yüklenemedi.`,"codexAuth.openaiPresetUnavailable":`Ayar kullanılamıyor.`,"codexAuth.openProviders":`Sağlayıcıları Aç`,"codexAuth.add":`Ekle`,"codexAuth.sparkQuota":`Codex Spark kotası`,"codexAuth.sparkQuotaHint":`Hesap kartlarında GPT-5.3-Codex-Spark haftalık penceresini gösterir. Yalnızca tek bir modeli kapsadığı için varsayılan olarak gizlidir.`,"codexAuth.sparkQuotaShown":`Codex Spark kotası gösteriliyor`,"codexAuth.sparkQuotaHidden":`Codex Spark kotası gizlendi`,"codexAuth.sparkQuotaFailed":`Codex Spark kotası ayarı değiştirilemedi`,"codexAuth.refreshQuota":`Kotaları yenile`,"codexAuth.refreshingQuota":`Yenileniyor...`,"codexAuth.quotaRefreshed":`Kotalar yenilendi`,"codexAuth.quotaRefreshFailed":`Kotalar yenilenemedi`,"codexAuth.pauseExhausted":`Tükenenleri duraklat`,"codexAuth.pausingExhausted":`Kotalar kontrol ediliyor...`,"codexAuth.pauseExhaustedSucceeded":`Limitteki hesaplar duraklatıldı: {count}`,"codexAuth.pauseExhaustedNone":`%100 kullanımı doğrulanmış hesap yok.`,"codexAuth.pauseExhaustedFailed":`Tükenen hesaplar kontrol edilemedi.`,"codexAuth.noPool":`Henüz havuz hesabı eklenmedi.`,"codexAuth.pause":`Duraklat`,"codexAuth.resume":`Devam Ettir`,"codexAuth.paused":`DURAKLATILDI`,"codexAuth.pauseSucceeded":`{email} duraklatıldı`,"codexAuth.resumeSucceeded":`{email} tekrar havuza alındı`,"codexAuth.pauseFailed":`{email} duraklatılamadı.`,"codexAuth.resumeFailed":`{email} devam ettirilemedi.`,"codexAuth.pausedHint":`Devam ettirilene kadar otomatik seçimden hariç tutulur.`,"codexAuth.pinned":`SABİTLENDİ`,"codexAuth.pinnedHint":`Bu hesabı elle seçtiniz.`,"codexAuth.fiveHour":`5saat`,"codexAuth.weekly":`Hafta`,"codexAuth.monthly":`30gün`,"codexAuth.resets":`sıfırlanma`,"codexAuth.today":`Bugün`,"codexAuth.current":`MEVCUT`,"codexAuth.nextSession":`SEÇİLEN`,"codexAuth.poolPrepared":`HAVUZ İÇİN HAZIRLANDI`,"codexAuth.preparePoolTitle":`Bu hesap Havuz modu için hazırlansın mı?`,"codexAuth.preparePoolDesc":`Doğrudan istekler ana girişi kullanmaya devam eder.`,"codexAuth.prepareForPool":`Havuz İçin Hazırla`,"codexAuth.poolPreparedToast":`{email} Havuz modu için hazırlandı`,"codexAuth.switchTitle":`Aktif hesap değiştirilsin mi?`,"codexAuth.switchDesc":`Anında yürürlüğe girer. Mevcut hesaba bağlı iş parçacıkları ve işlenmekte olan istekler yakalanan hesaplarını korur; yeni veya bağımsız istekler seçilen hesabın sıra kademesini kullanır ve aynı seçim sırasındaki hesaplar sırayla görev almaya devam eder.`,"codexAuth.cacheWarning":`Hesap değiştirildiğinde önbellek sıfırlanır.`,"codexAuth.setAsNext":`Sonraki istekte bu hesabı kullan`,"codexAuth.cancel":`İptal`,"codexAuth.switchBack":`Ana hesaba geri dönülsün mü?`,"codexAuth.switchBackDesc":`Anında yürürlüğe girer. Mevcut hesaba bağlı iş parçacıkları ve işlenmekte olan istekler yakalanan hesaplarını korur; yeni veya bağımsız istekler Uygulama giriş hesabınızın sıra kademesini kullanır ve aynı seçim sırasındaki hesaplar sırayla görev almaya devam eder.`,"codexAuth.autoSwitch":`Kullanıma dayalı proaktif geçiş`,"codexAuth.autoSwitchQuotaDesc":`Kota: %{threshold} veya üzeri kullanımda sonraki istek daha az kullanılan bir hesaba geçebilir.`,"codexAuth.autoSwitchQuotaOffDesc":`Kullanıma dayalı proaktif geçiş kapalı.`,"codexAuth.autoSwitchRoundRobinDesc":`Round-robin bu eşiği kullanmaz.`,"codexAuth.autoSwitchFillFirstDesc":`Kullanım %{threshold} eşiğini aşana kadar hesabı doldurun, ardından sonraki kullanılabilir hesaba geçin.`,"codexAuth.autoSwitchFillFirstOffDesc":`İlk doldurma modunda kullanım boşaltma noktası yok.`,"codexAuth.failureRecoveryNote":`Hata kurtarma ayrıdır.`,"codexAuth.autoSwitchThreshold":`Kullanım eşiği`,"codexAuth.autoSwitchThresholdAria":`Kullanım eşiği, yüzde`,"codexAuth.autoSwitchThresholdInc":`Kullanım eşiğini artır`,"codexAuth.autoSwitchThresholdDec":`Kullanım eşiğini azalt`,"codexAuth.autoSwitchLoadFailed":`Ayarlar yüklenemedi.`,"codexAuth.autoSwitchThresholdInvalid":`1 ile 100 arasında bir tam sayı girin`,"codexAuth.autoSwitchUpdated":`Proaktif geçiş güncellendi`,"codexAuth.autoSwitchUpdateFailed":`Geçiş güncellemesi doğrulanamadı.`,"codexAuth.requestUserInput":`Varsayılan modda kullanıcı girdisi iste`,"codexAuth.requestUserInputDesc":`Codex'in soru sormasına izin verir.`,"codexAuth.requestUserInputUpdated":`Özellik güncellendi.`,"codexAuth.requestUserInputUpdatedRestart":`Özellik güncellendi. Codex uygulamasını yeniden başlatın.`,"codexAuth.requestUserInputUpdateFailed":`Özellik güncellenemedi.`,"codexAuth.requestUserInputLoadFailed":`Özellik okunamadı.`,"anthropicPool.title":`Claude hesap havuzu (deneysel)`,"anthropicPool.enabledDesc":`429 alındığında hesabı bekletir ve başka bir hesaba geçer. Yeni oturumlar {window} değerine göre %{threshold} altında kullanıma sahip hesapları tercih eder.`,"anthropicPool.enabledNoProactiveDesc":`429 alındığında hesabı bekletir ve başka bir hesaba geçer. Eşik 0 iken kullanıma dayalı öngörülü geçiş kapalıdır, ancak yeni oturum seçimi ve 429 kurtarma hâlâ {window} penceresini kullanır.`,"anthropicPool.disabledDesc":`Yalnızca aktif Claude hesabını kullanır.`,"anthropicPool.experimentalWarning":`Deneysel: Claude OAuth hesaplarını döndürmek desteklenmeyen bir kullanım yoludur ve Anthropic hesap kısıtlamalarına veya hesabın askıya alınmasına yol açabilir. Aynı kuruluşu paylaşan hesaplar oran limitlerini paylaşır ve döndürmeden ek kapasite kazanmaz. Riskleri anlamıyorsanız kapalı tutun.`,"anthropicPool.needTwoAccounts":`Havuzu etkinleştirmeden önce en az iki Claude OAuth hesabı ekleyin.`,"anthropicPool.threshold":`Yeni oturum kullanım eşiği`,"anthropicPool.thresholdAria":`Yeni oturum kullanım eşiği, yüzde`,"anthropicPool.thresholdHelp":`0 kota bazlı seçimi devre dışı bırakır. Varsayılan 80.`,"anthropicPool.thresholdInvalid":`0 ile 100 arasında bir tam sayı girin`,"anthropicPool.loadFailed":`Claude havuz ayarları yüklenemedi.`,"anthropicPool.saveFailed":`Claude havuz ayarları kaydedilemedi.`,"anthropicPool.on":`Açık`,"anthropicPool.off":`Kapalı`,"accountPool.strategy":`Rotasyon stratejisi`,"accountPool.strategyDesc":`OpenCodex'in yeni bir göreve nasıl hesap atayacağı.`,"accountPool.strategyQuota":`Kota`,"accountPool.strategyRoundRobin":`Round-robin`,"accountPool.strategyFillFirst":`İlk doldurma`,"accountPool.strategyHintQuota":`Kota kullanımı eşik aşıldığında hesabı değiştirebilir.`,"accountPool.strategyHintRoundRobin":`Round-robin yalnızca canlı bir hesap bağı olmayan yeni/bağımsız görevleri döndürür; mevcut görevler bağlı kalabilir ve kullanım eşiği normal rotasyonu değiştirmez.`,"accountPool.strategyHintFillFirst":`İlk doldurma eşiği boşaltma noktası olarak kullanır.`,"accountPool.unboundDefinition":`Bağlı olmayan yeni görev.`,"accountPool.stickyLimit":`Döndürmeden önceki sabit atamalar`,"accountPool.stickyLimitAria":`Döndürmeden önceki sabit atamalar`,"accountPool.stickyLimitInc":`Sabit limiti artır`,"accountPool.stickyLimitDec":`Sabit limiti azalt`,"accountPool.stickyLimitHelp":`Seçilen hesabı bu kadar yeni atama boyunca tutun.`,"accountPool.stickyLimitInvalid":`1 ile 100 arasında bir tam sayı girin`,"accountPool.strategyLoadFailed":`Strateji yüklenemedi.`,"accountPool.strategyUpdateFailed":`Strateji kaydedilemedi.`,"accountPool.quotaWindow":`Kota penceresi`,"accountPool.quotaWindowDesc":`Kotaya dayalı yeni oturum seçimi, İlk doldurma eşik kontrolleri ve uygun 429 yedekleri için hangi önbelleğe alınmış kullanım çubuğunun kullanılacağını belirler.`,"accountPool.quotaWindowFiveHour":`5 saatlik çubuk`,"accountPool.quotaWindowWeekly":`Haftalık çubuk`,"accountPool.quotaWindowMaxUtilization":`Daha yüksek çubuk`,"accountPool.quotaWindowHint":`Haftalık çubuk, başka uygun hesap kaldığı sürece 5 saatlik çubuğu tükenmiş hesapları atlar; hiçbiri kalmazsa bu hesaplara geri döner. Eşitlikte 5 saatlik kullanımı daha düşük olan seçilir; hesap başına haftalık çubuklar ancak Sağlayıcılar sayfası sorguladıktan sonra bilinir.`,"accountPool.quotaWindowInert":`Kullanım çubuğunu yalnızca Kota ya da eşiği 0'ın üzerinde olan İlk doldurma puanlar; bu yüzden geçerli rotasyon stratejisi için bu ayar hiçbir şeyi değiştirmez.`,"accountPool.priority":`Seçim sırası`,"accountPool.priorityAria":`Bu hesap için seçim sırası`,"accountPool.priorityHint":`Yüksek sayılar önce kullanılır.`,"accountPool.priorityFirst":`İlk`,"accountPool.priorityEarlier":`Daha önce`,"accountPool.priorityNormal":`Normal`,"accountPool.priorityLater":`Daha sonra`,"accountPool.priorityLast":`Son`,"accountPool.priorityOption":`{name} ({value})`,"accountPool.priorityCustom":`Özel`,"accountPool.priorityUpdated":`{email} için seçim sırası güncellendi`,"accountPool.priorityUpdateFailed":`{email} için öncelik güncellenemedi`,"codexAuth.switched":`Sonraki istek için {email} seçildi`,"codexAuth.loadFailed":`Codex hesap ayarları yüklenemedi.`,"codexAuth.switchFailed":`Hesap değiştirilemedi.`,"codexAuth.removeConfirm":`{id} kaldırılsın mı?`,"codexAuth.removeFailed":`Hesap kaldırılamadı.`,"codexAuth.addTitle":`Codex Hesabı Ekle`,"codexAuth.addIdLabel":`Hesap ID`,"codexAuth.addIdPlaceholder":`codex-is, codex-yedek...`,"codexAuth.resetCreditsAria":`{count} sıfırlama kredisi`,"codexAuth.addJsonLabel":`auth.json içeriği`,"codexAuth.addHelp":`Başka bir makineden kopyalayın.`,"codexAuth.importBtn":`İçe Aktar`,"codexAuth.importInvalidJson":`Geçersiz JSON`,"codexAuth.importMissingTokens":`Eksik jetonlar`,"codexAuth.importMissingId":`Hesap ID gereklidir`,"codexAuth.accountAdded":`Hesap havuza eklendi`,"codexAuth.addPickDesc":`Havuza eklemek için başka bir ChatGPT hesabı ile giriş yapın.`,"codexAuth.oauthLogin":`OAuth Girişi`,"codexAuth.oauthDesc":`Tarayıcıda ChatGPT girişini açar`,"codexAuth.deviceLogin":`Cihaz koduyla giriş`,"codexAuth.deviceDesc":`Başsız veya uzak proxy için: kısa kodu başka bir cihazda girin`,"codexAuth.importAuthJson":`auth.json İçe Aktar`,"codexAuth.importAuthJsonDesc":`Başka bir kurulumdan veya dışa aktarımdan`,"codexAuth.back":`Geri`,"codexAuth.oauthAlreadyInProgress":`Giriş zaten devam ediyor.`,"codexAuth.oauthWaiting":`Tarayıcıda girişin tamamlanması bekleniyor...`,"codexAuth.oauthSubmittingCode":`Kod gönderiliyor…`,"codexAuth.oauthCodeSubmitted":`Kod gönderildi — giriş bitmesi bekleniyor…`,"codexAuth.oauthStatusRetrying":`Durum kontrol edilirken hata — tekrar deneniyor…`,"codexAuth.oauthCancelled":`Giriş iptal edildi.`,"codexAuth.loginFailed":`Giriş başarısız oldu`,"codexAuth.needsReauth":`Tekrar Giriş Yap`,"codexAuth.reauthenticate":`Yeniden Doğrula`,"codexAuth.tokenExpired":`Jeton süresi doldu — hesabı yeniden doğrulayın`,"codexAuth.mainTokenExpired":`Jeton süresi doldu — tekrar giriş yapın`,"codexAuth.emailCollision":`Bu hesap ana girişinizle eşleşiyor.`,"codexAuth.resetCreditsTitle":`Kredileri Sıfırla`,"codexAuth.resetCreditsAvailable":`{count} sıfırlama krediniz var.`,"codexAuth.resetCreditsDesc":`Her kredi oran limitlerinizi anında sıfırlar.`,"codexAuth.noResetCredits":`Sıfırlama krediniz yok.`,"codexAuth.earnCreditsHint":`Krediler aylık ve tavsiye programı ile kazanılır.`,"codexAuth.creditsExpireNote":`Kredilerin süresi 30 gün içinde dolmaktadır.`,"codexAuth.useOneCredit":`1 Kredi Kullan`,"codexAuth.confirmResetTitle":`Sıfırlama Kredisi Kullanılsın mı?`,"codexAuth.confirmResetDesc":`Bu işlem oran limitlerinizi anında sıfırlayacaktır. {count} krediniz kaldı.`,"codexAuth.irreversible":`Bu işlem geri alınamaz.`,"codexAuth.useCredit":`Kredi Kullan`,"codexAuth.redeeming":`Sıfırlanıyor...`,"codexAuth.resetSuccess":`Oran limitleri sıfırlandı! Kalan kredi: {remaining}.`,"codexAuth.resetSuccessGeneric":`Oran limitleri sıfırlandı!`,"codexAuth.resetAlreadyRedeemed":`Bu kredi zaten kullanıldı.`,"codexAuth.resetNothingToReset":`Şu anda sıfırlanması gereken oran limiti yok.`,"codexAuth.resetNoCredit":`Kullanılabilir sıfırlama kredisi yok.`,"codexAuth.resetError":`Sıfırlama kredisi kullanılamadı.`,"codexAuth.fifoNote":`En eski kredi ilk önce kullanılır.`,"codexAuth.confirmWhichCredit":`{date} tarihli kredi kullanılacak.`,"codexAuth.creditNext":`Sonraki kullanılacak`,"codexAuth.creditLabel":`Kredi #{n}`,"codexAuth.creditNextBadge":`SONRAKİ`,"codexAuth.creditGranted":`Verildiği tarih {date}`,"codexAuth.creditExpires":`Son kullanma {date} ({days}gün kaldı)`,"api.title":`API Erişimi`,"api.subtitle":`Harici uygulamalardan opencodex proxy'sine erişmek için üretilen API anahtarlarını kullanın. Anahtarlar {authHeader} başlığı üzerinden kimlik doğrulaması yapar; her bir uç noktanın neleri kabul ettiğini görmek için aşağıdaki tabloya bakın.`,"api.baseUrl":`Taban URL`,"api.responsesEndpoint":`Responses API`,"api.chatCompletionsEndpoint":`Chat Completions API`,"api.messagesEndpoint":`Messages API`,"api.modelsEndpoint":`Models API`,"api.endpointNote":`OpenAI uyumlu istemcilerle taban URL'yi kullanın.`,"api.endpointsTitle":`Uç noktalar`,"api.authTitle":`Kimlik Doğrulama`,"api.authLoopback":`Geri döngü (loopback) bağlantıları (127.0.0.1 / ::1) kimlik doğrulamasını atlar. Harici/ağ istemcileri x-opencodex-api-key veya Authorization başlığında bir ocx_ API anahtarı ya da OPENCODEX_API_AUTH_TOKEN göndermelidir.`,"api.authBaseUrlNote":`İstemcileri taban URL ile yapılandırın.`,"api.newKeyTitle":`Yeni anahtar oluşturuldu`,"api.newKeyNote":`Bu anahtarı şimdi kopyalayın — tekrar gösterilmeyecektir.`,"api.copy":`Kopyala`,"api.copied":`Kopyalandı`,"api.dismiss":`Kapat`,"api.generateTitle":`Anahtar oluştur`,"api.keyNamePlaceholder":`Anahtar adı (isteğe bağlı)`,"api.generate":`Oluştur`,"api.generating":`Oluşturuluyor…`,"api.activeKeys":`Aktif anahtarlar ({count})`,"api.activeKeysLoading":`Aktif anahtarlar`,"api.noKeys":`Henüz API anahtarı yok. Yukarıdan bir tane oluşturun.`,"api.workspace.sections":`API bölümleri`,"api.section.keys":`Anahtarlar`,"api.section.connect":`Bağlan`,"api.section.endpoints":`Uç noktalar`,"api.section.models":`Modeller`,"api.section.examples":`Örnekler`,"api.workspace.details":`API anahtar detayları`,"api.workspace.keyDetails":`Anahtar detayları`,"api.workspace.keyPrefix":`Anahtar ön eki`,"api.workspace.deleteKey":`Anahtarı sil`,"api.workspace.deleteConfirm":`Bu API anahtarı silinsin mi? Bunu kullanan istemciler erişimi anında kaybedecektir. Bu işlem geri alınamaz.`,"api.workspace.usageExamples":`Kullanım örnekleri`,"api.copyUrlHint":`URL kopyalamak için tıklayın`,"api.urlCopied":`URL kopyalandı`,"api.copyExampleHint":`Örnek kopyalamak için tıklayın`,"api.exampleCopied":`Örnek kopyalandı`,"api.colName":`İsim`,"api.colKey":`Anahtar`,"api.colCreated":`Oluşturuldu`,"api.confirm":`Onayla`,"api.deleteAria":`API anahtarını sil`,"api.modelsTitle":`Harici model kataloğu`,"api.modelsCount":`{count} çağrılabilir`,"api.modelsLoading":`Modeller yükleniyor…`,"api.modelsSearch":`Modellerde ara`,"api.modelsSubtitle":`Uç noktalarınızla bu tam model ID'lerini kullanın.`,"api.modelsEmpty":`Henüz harici olarak çağrılabilir model yok.`,"api.modelsNoMatch":`“{query}” ile eşleşen model yok.`,"api.modelsLoadFailed":`Harici model kataloğu yüklenemedi.`,"api.colModel":`Model`,"api.colSource":`Kaynak`,"api.colProtocols":`Protokoller`,"api.sourceNative":`ChatGPT havuzu`,"api.sourceCombo":`Kombo rotası`,"api.sourceCustom":`Özel`,"api.protocolResponses":`Responses`,"api.protocolChatCompletions":`Chat Completions`,"api.protocolMessages":`Messages`,"api.copyModelId":`ID Kopyala`,"api.modelCopied":`Kopyalandı`,"api.testModel":`Test Et`,"api.testingModel":`Test ediliyor…`,"api.testSucceeded":`Tamam`,"api.testFailed":`Başarısız`,"api.usageChatTitle":`Chat Completions örneği`,"api.usageResponsesTitle":`Responses örneği`,"api.usageMessagesTitle":`Messages örneği`,"api.usageSampleInput":`Merhaba dünya!`,"api.clientConfig.title":`İstemci konfigürasyonu`,"api.clientConfig.rowsLabel":`Bir istemci bağlayın`,"api.clientConfig.details":`Detaylar`,"api.clientConfig.detailsAria":`{client} konfigürasyon detayları`,"api.clientConfig.copyAria":`{client} konfigürasyon JSON kopyala`,"api.clientConfig.downloadAria":`{client} konfigürasyonu indir`,"api.clientConfig.rowMeta":`{destination} · {count} model`,"api.clientConfig.rowError":`{client} konfigürasyonu oluşturulamadı.`,"api.clientConfig.copiedAnnounceClient":`{client} konfigürasyon JSON panoya kopyalandı.`,"api.clientConfig.clientOpencode":`OpenCode`,"api.clientConfig.clientPi":`Pi`,"api.clientConfig.clientHermes":`Hermes`,"api.clientConfig.clientOpenclaw":`OpenClaw`,"api.clientConfig.clientKimi":`Kimi Code`,"api.clientConfig.clientGajae":`Gajae Code`,"api.clientConfig.clientDsh":`DeepSeek Harness (DSH)`,"api.clientConfig.clientMcode":`MiniMax Code`,"api.clientConfig.clientZcode":`ZCode`,"api.clientConfig.clientPrime":`Prime Agent`,"api.clientConfig.clientAside":`Aside`,"api.clientConfig.copy":`JSON Kopyala`,"api.clientConfig.download":`İndir`,"api.clientConfig.loading":`İstemci konfigürasyonu oluşturuluyor…`,"api.clientConfig.jsonLabel":`{client} konfigürasyon JSON`,"api.clientConfig.destination":`Hedef dosya`,"api.clientConfig.envHint":`Başlatmadan önce anahtarı ayarlayın`,"api.clientConfig.mergeWarning":`Bunu hedef dosyaya birleştirin. Dosyanın üzerine yazıp değiştirmek, diğer sağlayıcılarınızı ve MCP ayarlarınızı silecektir.`,"api.clientConfig.modelCount":`{count} model dışa aktarıldı`,"api.clientConfig.missingLimits":`{total} modelden {count} tanesi bağlam sınırı olmadan gönderildi.`,"api.clientConfig.noKeyYet":`{env} arkasında henüz anahtar yok.`,"api.clientConfig.loadFailed":`Model listesi okunamadı.`,"api.clientConfig.copiedAnnounce":`İstemci konfigürasyonu JSON panoya kopyalandı.`,"api.clientConfig.copyFailed":`Kopyalanamadı.`,"api.clientConfig.downloadedAnnounce":`{filename} indirildi. Henüz hiçbir şey değişmedi — kendiniz {destination} konumuna birleştirin.`,"api.clientConfig.whereDisclosure":`Bu dosya nereye gidiyor`,"api.clientConfig.whereBody":`Yukarıdaki hedef genel (global) yoldur. Çalışma dizinindeki projeye özel bir konfigürasyon dosyası buna öncelik eder ve istemci anahtarı bu dosyadan değil, konfigürasyonda belirtilen çevre değişkeninden okur.`,"api.clientConfig.clientOmp":`OMP`,"api.keysLoadFailed":`API anahtarları yüklenemedi.`,"api.createFailed":`API anahtarı oluşturulamadı.`,"api.deleteFailed":`API anahtarı silinemedi.`,"api.auth.endpoint":`Uç nokta`,"api.auth.required":`Gerekli`,"api.auth.accepted":`Kabul edildi`,"api.auth.rejected":`Kabul edilmedi`,"api.auth.testProtocol":`{protocol} Test Et`,"api.auth.testNeedsFreshKey":`Test için bir anahtar oluşturun.`,"api.key.name":`Anahtar adı`,"api.key.rename":`Yeniden adlandır`,"api.key.saveName":`İsmi kaydet`,"api.key.renaming":`Kaydediliyor…`,"api.key.renameFailed":`Yeniden adlandırılamadı.`,"api.key.deleting":`Siliniyor…`,"api.rotation.title":`Anahtar döndürme`,"api.rotation.description":`Mevcut anahtarı kısa bir geçiş süresince geçerli tutarak yeni anahtar oluşturur.`,"api.rotation.start":`Döndürmeyi başlat`,"api.rotation.starting":`Başlatılıyor…`,"api.rotation.pending":`Döndürme bekliyor. Onaylamadan önce istemciyi güncelleyip doğrulayın.`,"api.rotation.expires":`Geçiş süresi sonu:`,"api.rotation.secretOnce":`Yeni anahtar yalnızca bir kez gösterilir. Kapatmadan önce kopyalayın.`,"api.rotation.commit":`Döndürmeyi onayla`,"api.rotation.abort":`Döndürmeyi iptal et`,"api.rotation.failed":`İşlem tamamlanmadı. Yeniden denemeden önce yenileyin.`,"api.rotation.startFailed":`Anahtar döndürme başlatılamadı.`,"api.key.copyFailed":`Otomatik kopyalanamadı. Kapatmadan önce anahtarı manuel olarak seçip kopyalayın — tekrar gösterilmeyecektir.`,"api.attribution.title":`Atfedilen kullanım`,"api.attribution.requests7d":`Son 7 gün istekleri`,"api.attribution.totalRequests":`Toplam atfedilen istekler`,"api.attribution.totalRequestsAvailable":`Mevcut geçmişteki istekler`,"api.attribution.sinceAvailable":`Mevcut atıf başlangıcı`,"api.attribution.lastUsed":`Son kullanım`,"api.attribution.since":`Atıf başlangıcı`,"api.attribution.neverUsed":`Henüz kullanılmadı`,"api.attribution.unavailable":`Kullanım mevcut değil`,"api.attribution.unavailableDetail":`Henüz kullanım atfedilmedi.`,"api.attribution.ambiguous":`İki anahtar aynı ID'yi paylaşıyor.`,"api.attribution.railAmbiguous":`mükerrer ID`,"claude.subtitle":`Claude Code içinde GPT, Gemini ve diğer modelleri kullanın.`,"claude.pageTitle":`Claude Code`,"claude.workspace.settings":`Ayarlar`,"claude.enabledLabel":`Claude bağlantısı`,"claude.enabledHint":`Kapalı olduğunda Claude Code bu proxy'yi kullanamaz.`,"claude.authMode":`Kimlik Doğrulama Modu`,"claude.authModeHint":`Abonelik Claude hesabı gerektirir, Proxy modunda gerekmez`,"claude.authModeSubscription":`Abonelik (Claude hesabı)`,"claude.authModeProxy":`Proxy (hesap gerekmez)`,"claude.authModeAuto":`Otomatik (Claude doğrulamasını algıla)`,"claude.effectiveMode.label":`Sonraki başlatmada geçerli`,"claude.effectiveMode.manual":`Manuel: {mode}`,"claude.effectiveMode.autoPresent":`Otomatik: abonelik — {source} üzerinden Claude kimliği bulundu`,"claude.effectiveMode.autoAbsent":`Otomatik: proxy modu — Claude kimliği bulunamadı`,"claude.effectiveMode.autoUnknown":`Otomatik: abonelik — doğrulanamadı`,"claude.effectiveMode.admissionKey":`Bu proxy'nin API anahtarı hâlâ gönderiliyor.`,"claude.authSource.claude-json-oauth":`Claude hesabı`,"claude.authSource.claude-credentials-file":`kimlik bilgileri dosyası`,"claude.authSource.macos-keychain":`macOS Keychain`,"claude.authSource.exported-env":`ortam değişkeni`,"claude.authSource.unknown":`algılanan bir kimlik bilgisi`,"claude.systemEnv":`Otomatik bağlan`,"claude.systemEnvDesc":`Açık olduğunda terminalde claude çalıştırmak proxy üzerinden geçer.`,"claude.systemEnvUnsupported":`Otomatik bağlanma yalnızca macOS üzerinde kullanılabilirdir. Bu sistemde Claude'u {cmd} ile başlatın.`,"claude.systemEnvWarn":`⚠ Terminal uygulamasını tamamen kapatıp yeniden açmalısınız.`,"claude.fastMode":`Hızlı Mod (OpenAI)`,"claude.fastModeDesc":`OpenAI modelleri için service_tier ayarını kontrol eder.`,"claude.fastAuto":`Otomatik`,"claude.fastOn":`AÇIK`,"claude.fastOff":`KAPALI`,"claude.autoContext":`Otomatik büyük bağlam kullan`,"claude.autoContextDesc":`1M işaretinin ne kadar ileri gideceğini kontrol eder. AÇIK: penceresi sıkıştırma eşiğini barındırabilen her model 1M aralığı kazanır. KAPALI: yalnızca gerçek 1M modelleri alır.`,"claude.autoContextInert":`Pasif durumdadır çünkü konfigürasyon dosyasında eski bir bağlam boyutu değeri (maxContextTokens) var. Yeniden etkinleştirmek için oradan kaldırın.`,"claude.autoCompactWindow":`Otomatik özetleme noktası`,"claude.autoCompactDefault":`{value} (varsayılan)`,"claude.autoCompactWindowDesc":`Sohbet bu noktaya ulaştığında eski mesajlar özetlenir. Her modelin kendi sınırını asla aşmaz, bu nedenle 200k modeller etkilenmez.`,"claude.autoCompactWindowWarn":`Bunu değiştirmek GPT modellerini bozabilir — bir modelin gerçek sınırından daha yüksek ayarlanırsa, sohbetler özet devreye girmeden önce hata verecektir.`,"claude.injectAgents":`Alt ajanları otomatik kaydet`,"claude.injectAgentsDesc":`Alt Ajanlar sekmesinde seçilen modelleri (ve mevcut varsayılan modeli) çağrılabilir Claude Code ajanları (ocx-*) olarak kaydeder. Sonraki oturumdan itibaren uygulanır.`,"claude.webSearchSidecar":`Web arama yan araç geçersiz kılması`,"claude.webSearchSidecarHint":`Claude Code istekleri için ana web arama sidecar'ını geçersiz kılın.`,"claude.visionSidecar":`Görsel yan araç geçersiz kılması`,"claude.visionSidecarHint":`Claude Code istekleri için ana görsel sidecar'ını geçersiz kılın.`,"claude.useMainSetting":`Ana ayarı kullan`,"claude.sidecarModelPlaceholder":`Ana ayar modeli`,"claude.quickstart":`Başlarken`,"claude.quickstartHint":`{cmd} Claude Code'u proxy üzerinden açar. claude.ai girişiniz aktif kalır.`,"claude.manualEnv":`Manuel kurulum (gelişmiş)`,"claude.smallFastModel":`Arka plan yardımcı modeli`,"claude.smallFastModelHint":`Claude Code'un sohbet özetleri ve konu tespiti gibi arka plan işleri için kullandığı model. haiku alt ajan takma adı da bunu kullanır. Boş = Claude varsayılanı (Haiku).`,"claude.smallFastModelAccurateHint":`Claude Code'un sohbet özetleri ve konu tespiti gibi arka plan işleri için kullandığı model. haiku alt ajan takma adı da bunu kullanır.`,"claude.smallFastModelUnsetOption":`Bırakın Claude Code seçsin (yerel model)`,"claude.smallFastModelNativeWarning":`Ayarlanmadığında, OpenCodex yardımcı model geçersiz kılmalarını boş bırakır. Claude Code kendi yerel Sonnet modelini kullanabilir ve bu durum yerel sağlayıcınızdan ücret alınmasına yol açabilir.`,"claude.slotUnset":`Claude varsayılanını kullan`,"claude.modelMap":`Model yakalama`,"claude.modelMapHint":`Belirli bir model isteklerini yakalar ve seçtiğiniz modele yönlendirir.`,"claude.mapFrom":`Orijinal model (örn. claude-sonnet-4-5)`,"claude.mapTo":`Değiştirilecek model (örn. gemini/gemini-3-pro)`,"claude.addMapping":`Kural ekle`,"claude.removeMapping":`Kuralı kaldır`,"claude.aliases":`Kullanılabilir modeller`,"claude.aliasesHint":`Claude Code'un /model menüsünde görünen modeller.`,"claude.aliasProviderOther":`Diğer`,"claude.loading":`Yükleniyor…`,"claude.loadFail":`Claude ayarları yüklenemedi`,"claude.saved":`Kaydedildi.`,"claude.saveFailed":`Kaydetme başarısız`,"claude.networkError":`Ağ hatası — proxy çalışıyor mu?`,"claude.toggleAria":`Claude bağlantısını değiştir`,"claude.none":`Yok`,"cws.loading":`Kombolar yükleniyor…`,"cws.loadFailed":`Kombolar yüklenemedi.`,"cws.saveFailed":`Kombo kaydedilemedi.`,"cws.removeFailed":`Kombo kaldırılamadı.`,"cws.saved":`Kombo kaydedildi.`,"cws.created":`{model} oluşturuldu.`,"cws.removed":`combo/{id} kaldırıldı.`,"cws.renamed":`{from}, {to} olarak yeniden adlandırıldı.`,"cws.add":`Kombo ekle`,"cws.addTitle":`Kombo ekle`,"cws.addSubtitle":`Sağlayıcılar arasında sanal bir model oluşturun.`,"cws.create":`Kombo oluştur`,"cws.railAria":`Kombo listesi`,"cws.searchPlaceholder":`Kombolarda veya hedeflerde ara…`,"cws.noSearchResults":`Aramanızla eşleşen kombo yok.`,"cws.group.failover":`Yedekli (Failover)`,"cws.group.roundRobin":`Round-robin`,"cws.group.other":`Diğer stratejiler`,"cws.targetCount":`{count} hedef`,"cws.targetCountOne":`1 hedef`,"cws.overviewTitle":`Kombolar`,"cws.overviewBlurb":`Sağlayıcı/model hedefleri arasında failover, round-robin, ağırlıklı rastgele, en az kullanılan veya en yakın kota sıfırlamasıyla yönlendiren sanal modeller.`,"cws.count.total":`Toplam`,"cws.count.failover":`Failover`,"cws.count.roundRobin":`Round-robin`,"cws.count.other":`Diğer`,"cws.howTitle":`Nasıl çalışır`,"cws.howBody":`Codex'ten kombonun kamuya açık model adını isteyin. Bir ad olmadan varsayılan combo/ şeklindedir. OpenCodex bir hedef seçer ve yalnızca yeniden denenebilir yukarı akış hatalarında atlar. Hiçbir hedef kullanılabilir kalmazsa, istek küresel varsayılan sağlayıcıyı kullanmak yerine kapalı olarak başarısız olur (fail closed).`,"cws.attentionTitle":`Dikkat gerekiyor`,"cws.attention.empty":`Yapılandırılmış hedef yok`,"cws.attention.few":`Yalnızca bir hedef — geçiş yapılacak yer yok`,"cws.attention.catalogOmitted":`Model kataloğunda eksik — üye yetenekleri eksik veya uyumsuz (eksik bağlam penceresi / meta veri veya boş modalite kesişimi). Takma ada göre yönlendirme hâlâ çalışır`,"cws.attention.allTargetsExhausted":`Etkin hedeflerin tüm kotaları tükendi`,"cws.emptyTitle":`İlk kombonuzu oluşturun`,"cws.empty.createDesc":`Sanal bir model adlandırın ve iki veya daha fazla arka ucu bağlayın.`,"cws.backToAll":`Tüm kombolara dön`,"cws.allCombos":`Tüm kombolar`,"cws.copyModel":`ID kopyala`,"cws.copied":`Kopyalandı`,"cws.tabsLabel":`Kombo detay bölümleri`,"cws.tab.config":`Konfigürasyon`,"cws.tab.about":`Hakkında`,"cws.strategy":`Strateji`,"cws.strategy.failover":`Yedekli (Failover)`,"cws.strategy.roundRobin":`Round-robin`,"cws.strategy.random":`Rastgele`,"cws.strategy.leastUsed":`En az kullanılan`,"cws.strategy.resetWindow":`Sıfırlama penceresi`,"cws.strategy.failoverHint":`Hedefleri sırayla deneyin. İlk hedef yeniden denenebilir bir hatayla (oran sınırı, kesinti, abonelik engeli) başarısız olursa sonraki hedefe atlayın.`,"cws.strategy.roundRobinHint":`Trafiği ağırlığa göre kararlı bir şekilde dengeleyin. Seçilen her hedefi bir dizi başarılı istek boyunca tutun, ardından ilerleyin.`,"cws.strategy.randomHint":`Her istek için ağırlığa orantılı olasılıkla bir uygun hedef çekilir. İstekler arasında yapışkanlık yoktur.`,"cws.strategy.leastUsedHint":`Her isteği, kayıtlı başarısı en az olan uygun hedefe yönlendirir. Sayaçlar proxy ile yeniden başlar.`,"cws.strategy.resetWindowHint":`Kota penceresi en yakında sıfırlanacak uygun hedefi tercih eder. Kota verisi yoksa yapılandırma sırasına döner.`,"cws.field.id":`Kombo ID`,"cws.field.idHint":`İstemciler {model} isteyecek`,"cws.field.idInternalHint":`Dahili kombo ID. Oluşturduktan sonra değiştirebilirsiniz.`,"cws.field.idHintEdit":`Yeniden adlandırmak komboyu yeni bir ID'ye taşır. İstemciler {model} ister.`,"cws.field.alias":`Genel model adı`,"cws.field.aliasPlaceholder":`deepseek-v4-flash veya üretici/model`,"cws.field.aliasHint":`İsteğe bağlı. Ön eki olmayan yalın bir ad, üretici/model gibi özel bir ön ek kullanın veya combo/ kullanmak için boş bırakın.`,"cws.field.nativeAlias":`Yerel OpenAI takma adı`,"cws.field.nativeAliasHint":`Bu kombonun desteklenen nitelemesiz yerel bir OpenAI model kimliğine sahip olmasına izin verin. Hesap ve sağlayıcı nitelikli OpenAI rotaları ayrı kalır.`,"cws.field.displayName":`Görünen ad`,"cws.field.displayNameHint":`Bu kombo için seçici etiketi. Yerel OpenAI takma adı etkinleştirildiğinde gereklidir.`,"cws.field.stickyLimit":`Döndürmeden önceki sabit başarılar`,"cws.field.stickyLimitHint":`Ağırlıklı seçici ilerlemeden önce seçilen hedefi bu kadar başarılı istek boyunca tutun.`,"cws.field.defaultEffort":`Varsayılan akıl yürütme`,"cws.field.defaultEffortNone":`Yok (hedef varsayılanı)`,"cws.field.defaultEffortHint":`Yalnızca istemci akıl yürütme çabasını belirtmediğinde (atladığında) kullanılır. Seçenekler, seçilen hedeflerin duyurulan çabalarının kesişimidir; katalog çaba meta verisi olmayan hedefler hiçbir seçenek sunmaz.`,"cws.capability.imageInputUnavailable":`Seçilen tüm hedefler görsel girişini destekleyene kadar kullanılamaz.`,"cws.capability.imageInputHint":`Tüm hedefler görselleri desteklediğinde varsayılan olarak açıktır. Yalnızca metin kabul etmek için kapatın.`,"cws.capability.imageInput":`Görsel / çok modlu`,"cws.capability.adaptiveEffort":`Uyarlanabilir akıl yürütme düzeyi`,"cws.capability.adaptiveEffortHint":`Kapalı: akıl yürütme denetimi olmayan bir hedef, tüm kombinasyonun seçicisini gizler. Açık: bu hedefler kullanılabilir kalır ve seçici, kalan hedeflerin ortak düzeylerini gösterir.`,"cws.capabilities":`Yetenekler`,"cws.field.defaultEffortUnsupported":`Bu çaba hedeflerin ortak merdiveninde yok — istek anında yok sayılacak veya uydurulacaktır.`,"cws.field.defaultEffortUnsupportedOption":`kesişimde değil`,"cws.targets":`Hedefler`,"cws.targets.failoverHint":`Sıralama önemlidir — birincil olan ilktir.`,"cws.targets.roundRobinHint":`Ağırlıklar kararlı bağıntılı seçimi kontrol eder; sıralama rotasyon halkasındaki eşitlikleri bozar.`,"cws.targets.randomHint":`Ağırlıklar her çekilişin olasılığını kontrol eder; sıralamanın önemi yoktur.`,"cws.targets.leastUsedHint":`Sıralama yalnızca eşit kullanımlı hedefler arasındaki eşitliği bozar.`,"cws.targets.resetWindowHint":`Kota verisi eksik veya eşitse sıralama uygulanır.`,"cws.target.provider":`Sağlayıcı`,"cws.target.model":`Model`,"cws.target.weight":`Ağırlık`,"cws.target.pickProvider":`Sağlayıcı seçin…`,"cws.target.pickProviderFirst":`Önce bir sağlayıcı seçin…`,"cws.target.pickModel":`Model seçin…`,"cws.target.noModels":`Bu sağlayıcı için model yok`,"cws.target.modelPlaceholder":`model ID`,"cws.target.add":`Hedef ekle`,"cws.target.drag":`Yeniden sıralamak için sürükleyin`,"cws.target.moveUp":`Yukarı taşı`,"cws.target.moveDown":`Aşağı taşı`,"cws.quota.available":`Kullanılabilir`,"cws.quota.exhausted":`Kota tükendi`,"cws.quota.unknown":`Kota bilinmiyor`,"cws.quota.allExhausted":`Etkin hedeflerin tüm kotaları tükendi. Başka bir hedef seçin veya kotanın yenilenmesini bekleyin.`,"cws.aboutTitle":`Çalışma zamanı`,"cws.aboutBody":`Başarısız hedefler Retry-After süresine uyarak kısa süreliğine soğumaya alınır. Geçersiz istek veya bağlam (context) hataları diğer hedefe atlamaz. Her hedef çabayı kendi yeteneklerine uyarlar; tükenen kombolar kapalı olarak başarısız olur (fail closed). Günlükler ve Kullanım bölümü sıralı fiziksel denemeleri ve deneme başına kullanımı saklar.`,"cws.removeConfirmTitle":`{model} kaldırılsın mı?`,"cws.removeConfirmDesc":`Bu işlem sanal modeli konfigürasyondan ve Codex kataloğundan kaldırır. Hiçbir sağlayıcıyı silmez.`,"cws.unsavedTitle":`Kaydedilmemiş değişiklikler`,"cws.unsavedDesc":`Düzenlemelerden vazgeçip devam edilsin mi?`,"cws.keepEditing":`Düzenlemeye devam et`,"cws.err.missingId":`Kombo ID gereklidir.`,"cws.err.invalidId":`ID harf veya sayı ile başlamalıdır.`,"cws.err.duplicateId":`Bu ID ile bir kombo zaten var.`,"cws.err.invalidAlias":`Takma ad geçerli karakterler içermelidir.`,"cws.err.aliasReservedNamespace":`Takma ad ayrılmış namespace kullanamaz.`,"cws.err.aliasNativeFamily":`Yerel OpenAI isimlerine izin verilmez.`,"cws.err.unsupportedNativeAlias":`Yerel takma ad, şu anda desteklenen yalın bir OpenAI model kimliği olmalıdır.`,"cws.err.missingNativeAliasDisplayName":`Yerel takma adlar için bir görünen ad gereklidir.`,"cws.err.invalidDisplayName":`Görünen ad en fazla 128 karakter olmalı ve kontrol karakteri içermemelidir.`,"cws.err.duplicateAlias":`Başka bir kombo bu takma adı zaten kullanıyor.`,"cws.err.noTargets":`En az bir hedef ekleyin.`,"cws.err.incompleteTarget":`Her hedefin bir sağlayıcısı ve modeli olmalıdır.`,"cws.target.disabled":`{name} (devre dışı)`,"cws.err.reservedNamespace":`Önce fiziksel sağlayıcı yeniden adlandırılmalıdır.`,"cws.err.providerCollision":`Kombo ID'si bir sağlayıcı adı ile çakışıyor.`,"cws.err.unknownProvider":`Her hedef yapılandırılmış bir sağlayıcı kullanmalıdır.`,"cws.err.duplicateTarget":`Aynı hedef yalnızca bir kez görünebilir.`,"cws.err.invalidStickyLimit":`Limit 1 ile 100 arasında bir tam sayı olmalıdır.`,"cws.err.invalidWeight":`Ağırlık 1 ile 10000 arasında olmalıdır.`,"cws.err.noEnabledTarget":`En az bir hedef etkin bir sağlayıcı kullanmalıdır.`,"claude.tabsLabel":`Claude istemcisi`,"claude.tabCode":`Code`,"claude.tabDesktop":`Desktop`,"claudeDesktop.title":`Claude Desktop`,"claudeDesktop.subtitle":`Claude model ailelerini {port} portundaki bir modele yönlendirin.`,"claudeDesktop.importJson":`JSON İçe Aktar`,"claudeDesktop.exportJson":`JSON Dışa Aktar`,"claudeDesktop.loading":`Claude Desktop profili yükleniyor…`,"claudeDesktop.loadFail":`Claude Desktop profili yüklenemedi.`,"claudeDesktop.retry":`Tekrar Dene`,"claudeDesktop.saveFailed":`Claude Desktop profili kaydedilemedi.`,"claudeDesktop.applyFailed":`Profil kaydedildi ancak uygulanamadı.`,"claudeDesktop.updateFailed":`Güncelleme başarısız oldu.`,"claudeDesktop.savedApplied":`Profil kaydedildi ve uygulandı.`,"claudeDesktop.appliedMarkerUnsaved":`Profil uygulandı ancak işaretçi kaydedilmedi.`,"claudeDesktop.savedAppliedAnnounce":`Profil kaydedildi ve uygulandı.`,"claudeDesktop.saved":`Profil kaydedildi.`,"claudeDesktop.savedAnnounce":`Profil kaydedildi.`,"claudeDesktop.exported":`Profil JSON olarak dışa aktarıldı.`,"claudeDesktop.importExpected":`Sürüm 1 profili bekleniyordu.`,"claudeDesktop.importReady":`JSON içe aktarıldı.`,"claudeDesktop.importedAnnounce":`Profil JSON içe aktarıldı.`,"claudeDesktop.importInvalid":`Seçilen dosya geçerli bir profil değil.`,"claudeDesktop.importFailed":`İçe aktarma başarısız oldu. {error}`,"claudeDesktop.moved":`{route}, {family} ailesine taşındı.`,"claudeDesktop.unsaved":`Kaydedilmemiş değişiklikler`,"claudeDesktop.upToDate":`Profil güncel`,"claudeDesktop.saving":`Kaydediliyor…`,"claudeDesktop.applying":`Uygulanıyor…`,"claudeDesktop.saveApply":`Kaydet & uygula`,"claudeDesktop.emptyTitle":`Kullanılabilir model yok`,"claudeDesktop.emptyHint":`Bir sağlayıcı ekleyin veya etkinleştirin.`,"claudeDesktop.assignmentsLabel":`Claude model ailesi atamaları`,"claudeDesktop.family.opus":`Opus`,"claudeDesktop.family.fable":`Fable`,"claudeDesktop.family.sonnet":`Sonnet`,"claudeDesktop.family.haiku":`Haiku`,"claudeDesktop.modelCountOne":`{count} model`,"claudeDesktop.modelCountMany":`{count} model`,"claudeDesktop.chooseDefault":`Bir varsayılan seçin`,"claudeDesktop.temporaryDefault":`Geçici varsayılan`,"claudeDesktop.laneEmpty":`Buraya bir model sürükleyin.`,"claudeDesktop.laneNoMatch":`Bu ailede aramanızla eşleşen model yok.`,"nav.grok":`Grok`,"grok.title":`Grok Build`,"grok.subtitle":`opencodex'in Grok konfigürasyonunuza kaydettiği modeller.`,"grok.loading":`Grok durumu yükleniyor…`,"grok.loadFail":`Grok konfigürasyonu okunamadı.`,"grok.notConfiguredTitle":`Grok Build henüz bağlanmadı`,"grok.notConfiguredHint":`Proxy'yi Grok yüklüyken başlatın.`,"grok.endpoint":`Uç nokta`,"grok.colModel":`Model`,"grok.colAlias":`Grok takma adı`,"grok.colContext":`Bağlam`,"grok.groupNative":`Yerel modeller`,"grok.groupRouted":`Yönlendirilen modeller`,"grok.enabledCount":`{total} modelden {on} tanesi kayıtlı`,"grok.saved":`Seçim kaydedildi.`,"grok.savedApplied":`Seçim kaydedildi ve Grok konfigürasyonuna yazıldı.`,"grok.saveFailed":`Grok seçimi kaydedilemedi.`,"grok.applyFailed":`Seçim kaydedildi ancak Grok güncellenemedi.`,"grok.applySkipped":`Seçim kaydedildi.`,"grok.saveApply":`Kaydet & uygula`,"grok.saving":`Kaydediliyor…`,"grok.applying":`Uygulanıyor…`,"grok.unsaved":`Kaydedilmemiş değişiklikler`,"grok.upToDate":`Seçim güncel`,"grok.toggleModel":`{id} modelini Grok ile kaydet`,"claudeDesktop.available":`Kullanılabilir`,"claudeDesktop.defaultBadge":`Varsayılan`,"claudeDesktop.supports1m":`1M`,"claudeDesktop.unavailable":`Kullanılamıyor`,"claudeDesktop.contextM":`{n}M bağlam`,"claudeDesktop.contextK":`{n}k bağlam`,"claudeDesktop.contextUnknown":`bağlam bilinmiyor`,"claudeDesktop.alias":`Takma Ad`,"claudeDesktop.useAsDefault":`{family} varsayılanı olarak kullan`,"claudeDesktop.moveTo":`Şuraya taşı`,"claudeDesktop.move":`Taşı`,"claudeDesktop.status.applied":`Desktop'a Uygulandı`,"claudeDesktop.status.stale":`Konfigürasyon eski — tekrar uygulayın`,"claudeDesktop.status.notApplied":`Uygulanmadı`,"claudeDesktop.status.notActiveProfile":`Desktop başka bir profil kullanıyor`,"claudeDesktop.status.disabled":`Claude Desktop entegrasyonu kapalı.`,"claudeDesktop.enableApply":`Etkinleştir ve uygula`,"claudeDesktop.health.lastRequest":`Son istek`,"claudeDesktop.health.stats":`{count} istek / {errors} hata`,"claudeDesktop.effort.supported":`çaba`,"claudeDesktop.effort.displayOnly":`çaba (yalnızca ekran)`,"lab.title":`Compatibility Lab`,"lab.subtitle":`Read-only compatibility verdict matrix from lab projection evidence.`,"lab.loadFailed":`Could not load compatibility lab data`,"lab.projectionUnavailable":`Lab projection is not available. Run conformance or live probes first.`,"lab.projectionIncompatible":`Lab projection schema is incompatible. Rebuild the projection.`,"lab.statusTitle":`Projection status`,"lab.matrixTitle":`Compatibility matrix`,"lab.verdictsTitle":`Verdict records`,"lab.filter.layer":`Evidence layer`,"lab.filter.verdict":`Verdict`,"lab.filter.subject":`Subject ID`,"lab.filter.all":`All`,"lab.col.subject":`Subject`,"lab.col.layer":`Layer`,"lab.col.suite":`Suite`,"lab.col.verdict":`Verdict`,"lab.col.asOf":`As of`,"lab.col.protocol":`Protocol conformance`,"lab.col.live":`Live route compatibility`,"lab.col.task":`Task effectiveness`,"lab.empty":`No compatibility verdicts in the projection yet.`,"lab.subjectKind":`Kind`,"lab.observationCount":`Observations`,"lab.eventCount":`Events`,"lab.verdictCount":`Verdicts`,"lab.subjectCount":`Subjects`,"lab.builtAt":`Built`,"lab.loading":`Loading compatibility evidence…`,"lab.loadMore":`Load more`,"lab.detailTitle":`Verdict detail`,"lab.detailClose":`Close`,"lab.detailSubject":`Subject`,"lab.detailObservations":`Observations`,"lab.detailEvents":`Contributing events`,"lab.detailArtifacts":`Artifact metadata`,"lab.production.title":`Gözlemlenen üretim trafiği`,"lab.production.notVerification":`Lab doğrulaması değildir`,"lab.production.attempts":`Denemeler`,"lab.production.successes":`Başarılı denemeler`,"lab.production.routeErrors":`Rota hataları`,"lab.production.lastObserved":`Son gözlem`,"lab.detailLoadFailed":`Could not load verdict detail`,"lab.refresh":`Refresh`,"lab.verdict.UNKNOWN":`Unknown`,"lab.verdict.CLAIMED":`Claimed`,"lab.verdict.PROBED":`Probed`,"lab.verdict.VERIFIED":`Verified`,"lab.verdict.DEGRADED":`Degraded`,"lab.verdict.BLOCKED":`Blocked`,"lab.verdict.UNSUPPORTED":`Unsupported`,"lab.layer.protocol_conformance":`Protocol conformance`,"lab.layer.live_route_compatibility":`Live route compatibility`,"lab.layer.task_effectiveness":`Task effectiveness`,"dash.visionAdvanced":`Gelişmiş ayarlar`,"dash.visionMaxDescriptions":`Tur başına en fazla açıklama`,"dash.visionMaxDescriptionsInvalid":`Pozitif bir tam sayı girin.`,"dash.visionTimeout":`Zaman aşımı`,"dash.visionTimeoutInvalid":`{min} ile {max} milisaniye arasında bir tam sayı girin.`,"dash.visionAdvancedPopover":`Gelişmiş görsel ayarları`,"models.newPolicyGlobal":`Yeni modeller devre dışı başlasın`,"models.newPolicyProvider":`Yeni model ilkesi`,"models.newPolicy_inherit":`Devral`,"models.newPolicy_off":`Kapalı`,"models.newPolicy_on":`Açık`,"models.newBadge":`YENİ`,"models.newCount":`{count} yeni, kapalı`,"models.aliases":`Takma adlar`,"models.aliasesTable":`Takma ad tablosu`,"models.aliasPrompt":`Sağlayıcı takma adı (temizlemek için boş bırakın)`,"models.modelAliasPrompt":`Model takma adı (temizlemek için boş bırakın)`,"models.aliasSaved":`Takma ad kaydedildi`,"models.aliasConflict":`Bu takma ad mevcut bir adla çakışıyor`,"models.editProviderAlias":`Sağlayıcı takma adını düzenle`,"models.editModelAlias":`Model takma adını düzenle`,"models.useDefaultAliases":`Varsayılan takma adları kullan`,"models.useDefaultAliasesGlobal":`Varsayılan takma adları her yerde kullan`,"models.aliasAuto":`otomatik`,"models.aliasUser":`kullanıcı`,"models.aliasStale":`eski`,"connection.discovering":`Discovering local and shared targets…`,"connection.machineUnavailable":`The local machine plane is unavailable. Shared requests were not redirected locally.`,"connection.disconnect":`Disconnect from hub`,"connection.disconnectConfirm":`Disconnect this machine from the hub and restart it in standalone mode?`,"connection.pairing.title":`Connect this dashboard to the hub`,"connection.pairing.body":`Paste the one-time pairing code created on the hub.`,"connection.pairing.relayWarning":`This code is exchanged through the fixed hub relay. The relay cannot be redirected to another host.`,"connection.pairing.code":`One-time pairing code`,"connection.pairing.submit":`Connect`,"connection.pairing.submitting":`Connecting…`,"connection.pairing.error":`The pairing code was refused or expired. The code was left in place so you can check it.`,"connection.machine.title":`This machine`,"connection.machine.shimHealthy":`Codex shim is healthy.`,"connection.machine.shimNeedsAttention":`Codex shim needs attention.`,"connection.machine.repairShim":`Repair shim`,"connection.machine.removeShim":`Remove shim`,"connection.clients.title":`Connected clients`,"connection.clients.none":`No client status available`,"connection.clients.sync":`Sync now`,"connection.clients.syncing":`Syncing…`,"connection.sessionLogout":`Uzak oturumdan çık`,"connection.sessionLoggingOut":`Uzak oturumdan çıkılıyor…`,"connection.sessionLogoutFailed":`Uzak oturumdan çıkılamadı. Mevcut oturum korundu.`,"usage.source.connected":`Source: hub usage`,"usage.source.local":`Source: local usage.jsonl`,"usage.scope.label":`Usage scope`,"usage.scope.machine":`This machine`,"usage.scope.hub":`Hub-wide`,"usage.hubOffline":`Hub usage is unavailable. Local usage was not substituted.`,"integrations.tab.cursor":`Cursor`,"integrations.detail.cursorSeen":`Cursor kısa süre önce bu proxy'ye istek gönderdi`,"integrations.detail.cursorNeverSeen":`Private Inference yüklü; henüz istek alınmadı`,"integrations.detail.cursorAbsent":`Cursor Private Inference bulunamadı`,"integrations.cursor.title":`Cursor`,"integrations.cursor.intro":`Cursor Private Inference, aracısını yerel olarak çalıştırır ve geri döngü üzerinden opencodex ile iletişim kurar. Normal Cursor bunu yapamaz: arka ucu özel uç noktayı çağırır ve herkese açık bir HTTPS URL'sine ihtiyaç duyar. Bu sayfa Cursor'a hiçbir zaman yazmaz; aşağıdaki değerleri Cursor'a kendiniz yapıştırın.`,"integrations.cursor.loading":`Cursor durumu okunuyor…`,"integrations.cursor.unavailable":`Cursor durumu proxy'den okunamadı.`,"integrations.cursor.detection":`Yüklü derlemeler`,"integrations.cursor.privateInference":`Cursor Private Inference`,"integrations.cursor.regular":`Cursor (normal)`,"integrations.cursor.detected":`Algılandı`,"integrations.cursor.notFound":`Bulunamadı`,"integrations.cursor.regularOnly":`Yalnızca normal Cursor bulundu. Özel uç noktaları Cursor sunucuları üzerinden yönlendirdiği için geri döngü proxy'sine herkese açık bir tünel olmadan erişilemez. Private Inference derlemesi için kılavuza bakın.`,"integrations.cursor.nothingFound":`Olağan konumlarda Cursor kurulumu bulunamadı. Başka bir yere yüklenmişse aşağıdaki değerler yine de geçerlidir.`,"integrations.cursor.gateway":`Ağ geçidi değerleri`,"integrations.cursor.gatewayHint":`Cursor Private Inference'da Settings > Models > Gateway bölümünü açın, bu iki değeri yapıştırın ve ardından Refresh model list düğmesine basın.`,"integrations.cursor.baseUrl":`Base URL`,"integrations.cursor.apiKey":`API Key`,"integrations.cursor.apiKeyCredential":`opencodex API anahtarlarınızdan biri (bu bağlantı için kimlik bilgisi gerekir)`,"integrations.cursor.copy":`Kopyala`,"integrations.cursor.copied":`Kopyalandı`,"integrations.cursor.connection":`Bağlantı`,"integrations.cursor.seen":`Cursor'dan gelen son istek: {time} ({ua})`,"integrations.cursor.neverSeen":`Proxy başlatıldığından beri Cursor'dan istek alınmadı. Ağ geçidini kaydettikten sonra Cursor'da Refresh model list düğmesine basın.`,"integrations.cursor.models":`Cursor'da gösterilecekler`,"integrations.cursor.modelsHint":`Cursor, akıl yürütme kademesini kendi model tablosundan seçtiği için opencodex bunu yalnızca tahmin edebilir. Bağlam sütunu varsayılan pencereyi ve isteğe bağlı pencereyi (Cursor'ın Max Mode'u) listeler.`,"integrations.cursor.ladderFromBundle":`Akıl yürütme kademeleri yüklü Cursor Private Inference {version} paketinden okundu. Bunlara Cursor karar verir; opencodex yalnızca tablosunu gösterir.`,"integrations.cursor.ladderFromStatic":`Akıl yürütme kademeleri Cursor 3.18.25'in statik bir kopyasıdır (okunabilir bir Private Inference paketi bulunamadı). Bağlam sütunu varsayılan ve isteğe bağlı pencereyi gösterir.`,"integrations.cursor.unknownVersion":`bilinmeyen sürüm`,"integrations.cursor.noControl":`—`,"integrations.cursor.singleWindow":`tek pencere`,"integrations.cursor.noControlTitle":`Bu kimlik Cursor'ın yerleşik çaba tablosunda yok, bu yüzden Cursor akıl yürütme denetimi göstermez.`,"integrations.cursor.effortRowsOne":`1 çaba satırı yayımlandı`,"integrations.cursor.effortRowsMany":`{n} çaba satırı yayımlandı`,"integrations.cursor.effortRowsOff":`çaba satırı yok`,"integrations.cursor.tableLessHint":`— ile işaretli satırlar Cursor'da akıl yürütme denetimi almaz. Her çaba için bir seçici girdisi (id--effort) yayımlamak üzere cursorEffortRows'u açın veya sabit bir varsayılan için sağlayıcıda modelDefaultReasoningEfforts ayarlayın.`,"integrations.cursor.colModel":`Model`,"integrations.cursor.colReasoning":`Akıl yürütme`,"integrations.cursor.colContext":`Bağlam`,"integrations.cursor.guide":`Cursor Private Inference kılavuzunu aç`},qe={en:{"lab.title":`Compatibility Lab`,"lab.subtitle":`Read-only compatibility verdict matrix from lab projection evidence.`,"lab.loadFailed":`Could not load compatibility lab data`,"lab.projectionUnavailable":`Lab projection is not available. Run conformance or live probes first.`,"lab.projectionIncompatible":`Lab projection schema is incompatible. Rebuild the projection.`,"lab.statusTitle":`Projection status`,"lab.matrixTitle":`Compatibility matrix`,"lab.verdictsTitle":`Verdict records`,"lab.filter.layer":`Evidence layer`,"lab.filter.verdict":`Verdict`,"lab.filter.subject":`Subject ID`,"lab.filter.all":`All`,"lab.col.subject":`Subject`,"lab.col.layer":`Layer`,"lab.col.suite":`Suite`,"lab.col.verdict":`Verdict`,"lab.col.asOf":`As of`,"lab.col.protocol":`Protocol conformance`,"lab.col.live":`Live route compatibility`,"lab.col.task":`Task effectiveness`,"lab.empty":`No compatibility verdicts in the projection yet.`,"lab.subjectKind":`Kind`,"lab.observationCount":`Observations`,"lab.eventCount":`Events`,"lab.verdictCount":`Verdicts`,"lab.subjectCount":`Subjects`,"lab.builtAt":`Built`,"lab.loading":`Loading compatibility evidence…`,"lab.loadMore":`Load more`,"lab.detailTitle":`Verdict detail`,"lab.detailClose":`Close`,"lab.detailSubject":`Subject`,"lab.detailObservations":`Observations`,"lab.detailEvents":`Evidence events`,"lab.detailArtifacts":`Artifact metadata`,"lab.detailLoadFailed":`Could not load verdict detail`,"lab.refresh":`Refresh`,"lab.verdict.UNKNOWN":`Unknown`,"lab.verdict.CLAIMED":`Claimed`,"lab.verdict.PROBED":`Probed`,"lab.verdict.VERIFIED":`Verified`,"lab.verdict.DEGRADED":`Degraded`,"lab.verdict.BLOCKED":`Blocked`,"lab.verdict.UNSUPPORTED":`Unsupported`,"lab.layer.protocol_conformance":`Protocol conformance`,"lab.layer.live_route_compatibility":`Live route compatibility`,"lab.layer.task_effectiveness":`Task effectiveness`},de:{"lab.title":`Kompatibilitäts-Labor`,"lab.subtitle":`Schreibgeschützte Kompatibilitätsmatrix aus den Evidenzen der Lab-Projektion.`,"lab.loadFailed":`Kompatibilitätsdaten konnten nicht geladen werden`,"lab.projectionUnavailable":`Die Lab-Projektion ist nicht verfügbar. Führe zuerst Konformitäts- oder Live-Probes aus.`,"lab.projectionIncompatible":`Das Schema der Lab-Projektion ist inkompatibel. Baue die Projektion neu auf.`,"lab.statusTitle":`Projektionsstatus`,"lab.matrixTitle":`Kompatibilitätsmatrix`,"lab.verdictsTitle":`Urteilsdatensätze`,"lab.filter.layer":`Evidenzschicht`,"lab.filter.verdict":`Urteil`,"lab.filter.subject":`Subjekt-ID`,"lab.filter.all":`Alle`,"lab.col.subject":`Subjekt`,"lab.col.layer":`Schicht`,"lab.col.suite":`Suite`,"lab.col.verdict":`Urteil`,"lab.col.asOf":`Stand`,"lab.col.protocol":`Protokollkonformität`,"lab.col.live":`Live-Route-Kompatibilität`,"lab.col.task":`Aufgabenwirksamkeit`,"lab.empty":`In der Projektion gibt es noch keine Kompatibilitätsurteile.`,"lab.subjectKind":`Art`,"lab.observationCount":`Beobachtungen`,"lab.eventCount":`Ereignisse`,"lab.verdictCount":`Urteile`,"lab.subjectCount":`Subjekte`,"lab.builtAt":`Erstellt`,"lab.loading":`Kompatibilitätsevidenz wird geladen…`,"lab.loadMore":`Mehr laden`,"lab.detailTitle":`Urteilsdetails`,"lab.detailClose":`Schließen`,"lab.detailSubject":`Subjekt`,"lab.detailObservations":`Beobachtungen`,"lab.detailEvents":`Evidenzereignisse`,"lab.detailArtifacts":`Artefakt-Metadaten`,"lab.detailLoadFailed":`Urteilsdetails konnten nicht geladen werden`,"lab.refresh":`Aktualisieren`,"lab.verdict.UNKNOWN":`Unbekannt`,"lab.verdict.CLAIMED":`Behauptet`,"lab.verdict.PROBED":`Geprüft`,"lab.verdict.VERIFIED":`Verifiziert`,"lab.verdict.DEGRADED":`Eingeschränkt`,"lab.verdict.BLOCKED":`Blockiert`,"lab.verdict.UNSUPPORTED":`Nicht unterstützt`,"lab.layer.protocol_conformance":`Protokollkonformität`,"lab.layer.live_route_compatibility":`Live-Route-Kompatibilität`,"lab.layer.task_effectiveness":`Aufgabenwirksamkeit`},fr:{"lab.title":`Laboratoire de compatibilité`,"lab.subtitle":`Matrice en lecture seule des verdicts de compatibilité fondée sur les preuves de la projection du laboratoire.`,"lab.loadFailed":`Impossible de charger les données du laboratoire de compatibilité`,"lab.projectionUnavailable":`La projection du laboratoire n’est pas disponible. Exécutez d’abord les sondes de conformité ou en conditions réelles.`,"lab.projectionIncompatible":`Le schéma de la projection du laboratoire est incompatible. Reconstruisez la projection.`,"lab.statusTitle":`État de la projection`,"lab.matrixTitle":`Matrice de compatibilité`,"lab.verdictsTitle":`Enregistrements des verdicts`,"lab.filter.layer":`Couche de preuves`,"lab.filter.verdict":`Verdict`,"lab.filter.subject":`ID du sujet`,"lab.filter.all":`Tous`,"lab.col.subject":`Sujet`,"lab.col.layer":`Couche`,"lab.col.suite":`Suite`,"lab.col.verdict":`Verdict`,"lab.col.asOf":`Établi le`,"lab.col.protocol":`Conformité au protocole`,"lab.col.live":`Compatibilité du routage en conditions réelles`,"lab.col.task":`Efficacité des tâches`,"lab.empty":`La projection ne contient encore aucun verdict de compatibilité.`,"lab.subjectKind":`Type`,"lab.observationCount":`Observations`,"lab.eventCount":`Événements`,"lab.verdictCount":`Verdicts`,"lab.subjectCount":`Sujets`,"lab.builtAt":`Générée le`,"lab.loading":`Chargement des preuves de compatibilité…`,"lab.loadMore":`Charger plus`,"lab.detailTitle":`Détails du verdict`,"lab.detailClose":`Fermer`,"lab.detailSubject":`Sujet`,"lab.detailObservations":`Observations`,"lab.detailEvents":`Événements probants`,"lab.detailArtifacts":`Métadonnées des artefacts`,"lab.detailLoadFailed":`Impossible de charger les détails du verdict`,"lab.refresh":`Actualiser`,"lab.verdict.UNKNOWN":`Inconnu`,"lab.verdict.CLAIMED":`Déclaré`,"lab.verdict.PROBED":`Sondé`,"lab.verdict.VERIFIED":`Vérifié`,"lab.verdict.DEGRADED":`Dégradé`,"lab.verdict.BLOCKED":`Bloqué`,"lab.verdict.UNSUPPORTED":`Non pris en charge`,"lab.layer.protocol_conformance":`Conformité au protocole`,"lab.layer.live_route_compatibility":`Compatibilité du routage en conditions réelles`,"lab.layer.task_effectiveness":`Efficacité des tâches`},ko:{"lab.title":`호환성 랩`,"lab.subtitle":`랩 프로젝션 증거를 기반으로 한 읽기 전용 호환성 판정 매트릭스입니다.`,"lab.loadFailed":`호환성 랩 데이터를 불러오지 못했습니다`,"lab.projectionUnavailable":`랩 프로젝션을 사용할 수 없습니다. 먼저 적합성 또는 라이브 프로브를 실행하세요.`,"lab.projectionIncompatible":`랩 프로젝션 스키마가 호환되지 않습니다. 프로젝션을 다시 빌드하세요.`,"lab.statusTitle":`프로젝션 상태`,"lab.matrixTitle":`호환성 매트릭스`,"lab.verdictsTitle":`판정 레코드`,"lab.filter.layer":`증거 레이어`,"lab.filter.verdict":`판정`,"lab.filter.subject":`대상 ID`,"lab.filter.all":`전체`,"lab.col.subject":`대상`,"lab.col.layer":`레이어`,"lab.col.suite":`스위트`,"lab.col.verdict":`판정`,"lab.col.asOf":`기준 시각`,"lab.col.protocol":`프로토콜 적합성`,"lab.col.live":`라이브 경로 호환성`,"lab.col.task":`작업 효과성`,"lab.empty":`프로젝션에 아직 호환성 판정이 없습니다.`,"lab.subjectKind":`종류`,"lab.observationCount":`관측`,"lab.eventCount":`이벤트`,"lab.verdictCount":`판정`,"lab.subjectCount":`대상`,"lab.builtAt":`빌드 시각`,"lab.loading":`호환성 증거를 불러오는 중…`,"lab.loadMore":`더 불러오기`,"lab.detailTitle":`판정 상세`,"lab.detailClose":`닫기`,"lab.detailSubject":`대상`,"lab.detailObservations":`관측`,"lab.detailEvents":`증거 이벤트`,"lab.detailArtifacts":`아티팩트 메타데이터`,"lab.detailLoadFailed":`판정 상세를 불러오지 못했습니다`,"lab.refresh":`새로고침`,"lab.verdict.UNKNOWN":`알 수 없음`,"lab.verdict.CLAIMED":`주장됨`,"lab.verdict.PROBED":`프로브됨`,"lab.verdict.VERIFIED":`검증됨`,"lab.verdict.DEGRADED":`저하됨`,"lab.verdict.BLOCKED":`차단됨`,"lab.verdict.UNSUPPORTED":`지원되지 않음`,"lab.layer.protocol_conformance":`프로토콜 적합성`,"lab.layer.live_route_compatibility":`라이브 경로 호환성`,"lab.layer.task_effectiveness":`작업 효과성`},zh:{"lab.title":`兼容性实验室`,"lab.subtitle":`基于实验室投影证据的只读兼容性判定矩阵。`,"lab.loadFailed":`无法加载兼容性实验室数据`,"lab.projectionUnavailable":`实验室投影不可用。请先运行一致性探测或实时探测。`,"lab.projectionIncompatible":`实验室投影架构不兼容。请重新构建投影。`,"lab.statusTitle":`投影状态`,"lab.matrixTitle":`兼容性矩阵`,"lab.verdictsTitle":`判定记录`,"lab.filter.layer":`证据层`,"lab.filter.verdict":`判定`,"lab.filter.subject":`主体 ID`,"lab.filter.all":`全部`,"lab.col.subject":`主体`,"lab.col.layer":`层`,"lab.col.suite":`测试套件`,"lab.col.verdict":`判定`,"lab.col.asOf":`截至`,"lab.col.protocol":`协议一致性`,"lab.col.live":`实时路由兼容性`,"lab.col.task":`任务有效性`,"lab.empty":`投影中还没有兼容性判定。`,"lab.subjectKind":`类型`,"lab.observationCount":`观测`,"lab.eventCount":`事件`,"lab.verdictCount":`判定`,"lab.subjectCount":`主体`,"lab.builtAt":`构建时间`,"lab.loading":`正在加载兼容性证据…`,"lab.loadMore":`加载更多`,"lab.detailTitle":`判定详情`,"lab.detailClose":`关闭`,"lab.detailSubject":`主体`,"lab.detailObservations":`观测`,"lab.detailEvents":`证据事件`,"lab.detailArtifacts":`制品元数据`,"lab.detailLoadFailed":`无法加载判定详情`,"lab.refresh":`刷新`,"lab.verdict.UNKNOWN":`未知`,"lab.verdict.CLAIMED":`已声明`,"lab.verdict.PROBED":`已探测`,"lab.verdict.VERIFIED":`已验证`,"lab.verdict.DEGRADED":`降级`,"lab.verdict.BLOCKED":`已阻止`,"lab.verdict.UNSUPPORTED":`不支持`,"lab.layer.protocol_conformance":`协议一致性`,"lab.layer.live_route_compatibility":`实时路由兼容性`,"lab.layer.task_effectiveness":`任务有效性`},"zh-TW":{"lab.title":`相容性實驗室`,"lab.subtitle":`基於實驗室投影證據的唯讀相容性判定矩陣。`,"lab.loadFailed":`無法載入相容性實驗室資料`,"lab.projectionUnavailable":`實驗室投影不可用。請先執行一致性探測或即時探測。`,"lab.projectionIncompatible":`實驗室投影架構不相容。請重新建置投影。`,"lab.statusTitle":`投影狀態`,"lab.matrixTitle":`相容性矩陣`,"lab.verdictsTitle":`判定記錄`,"lab.filter.layer":`證據層`,"lab.filter.verdict":`判定`,"lab.filter.subject":`主體 ID`,"lab.filter.all":`全部`,"lab.col.subject":`主體`,"lab.col.layer":`層`,"lab.col.suite":`測試套件`,"lab.col.verdict":`判定`,"lab.col.asOf":`截至`,"lab.col.protocol":`協定一致性`,"lab.col.live":`即時路由相容性`,"lab.col.task":`任務有效性`,"lab.empty":`投影中還沒有相容性判定。`,"lab.subjectKind":`類型`,"lab.observationCount":`觀測`,"lab.eventCount":`事件`,"lab.verdictCount":`判定`,"lab.subjectCount":`主體`,"lab.builtAt":`建置時間`,"lab.loading":`正在載入相容性證據…`,"lab.loadMore":`載入更多`,"lab.detailTitle":`判定詳情`,"lab.detailClose":`關閉`,"lab.detailSubject":`主體`,"lab.detailObservations":`觀測`,"lab.detailEvents":`證據事件`,"lab.detailArtifacts":`產物中繼資料`,"lab.detailLoadFailed":`無法載入判定詳情`,"lab.refresh":`重新整理`,"lab.verdict.UNKNOWN":`未知`,"lab.verdict.CLAIMED":`已聲明`,"lab.verdict.PROBED":`已探測`,"lab.verdict.VERIFIED":`已驗證`,"lab.verdict.DEGRADED":`降級`,"lab.verdict.BLOCKED":`已封鎖`,"lab.verdict.UNSUPPORTED":`不支援`,"lab.layer.protocol_conformance":`協定一致性`,"lab.layer.live_route_compatibility":`即時路由相容性`,"lab.layer.task_effectiveness":`任務有效性`},ru:{"lab.title":`Лаборатория совместимости`,"lab.subtitle":`Матрица вердиктов совместимости только для чтения на основе данных проекции лаборатории.`,"lab.loadFailed":`Не удалось загрузить данные лаборатории совместимости`,"lab.projectionUnavailable":`Проекция лаборатории недоступна. Сначала выполните проверки соответствия или live-проверки.`,"lab.projectionIncompatible":`Схема проекции лаборатории несовместима. Перестройте проекцию.`,"lab.statusTitle":`Состояние проекции`,"lab.matrixTitle":`Матрица совместимости`,"lab.verdictsTitle":`Записи вердиктов`,"lab.filter.layer":`Слой доказательств`,"lab.filter.verdict":`Вердикт`,"lab.filter.subject":`ID субъекта`,"lab.filter.all":`Все`,"lab.col.subject":`Субъект`,"lab.col.layer":`Слой`,"lab.col.suite":`Набор`,"lab.col.verdict":`Вердикт`,"lab.col.asOf":`По состоянию на`,"lab.col.protocol":`Соответствие протоколу`,"lab.col.live":`Совместимость live-маршрута`,"lab.col.task":`Эффективность задач`,"lab.empty":`В проекции пока нет вердиктов совместимости.`,"lab.subjectKind":`Тип`,"lab.observationCount":`Наблюдения`,"lab.eventCount":`События`,"lab.verdictCount":`Вердикты`,"lab.subjectCount":`Субъекты`,"lab.builtAt":`Собрано`,"lab.loading":`Загрузка доказательств совместимости…`,"lab.loadMore":`Загрузить ещё`,"lab.detailTitle":`Детали вердикта`,"lab.detailClose":`Закрыть`,"lab.detailSubject":`Субъект`,"lab.detailObservations":`Наблюдения`,"lab.detailEvents":`События доказательств`,"lab.detailArtifacts":`Метаданные артефактов`,"lab.detailLoadFailed":`Не удалось загрузить детали вердикта`,"lab.refresh":`Обновить`,"lab.verdict.UNKNOWN":`Неизвестно`,"lab.verdict.CLAIMED":`Заявлено`,"lab.verdict.PROBED":`Проверено пробой`,"lab.verdict.VERIFIED":`Подтверждено`,"lab.verdict.DEGRADED":`Ограничено`,"lab.verdict.BLOCKED":`Заблокировано`,"lab.verdict.UNSUPPORTED":`Не поддерживается`,"lab.layer.protocol_conformance":`Соответствие протоколу`,"lab.layer.live_route_compatibility":`Совместимость live-маршрута`,"lab.layer.task_effectiveness":`Эффективность задач`},ja:{"lab.title":`互換性ラボ`,"lab.subtitle":`ラボ投影の証拠に基づく読み取り専用の互換性判定マトリクスです。`,"lab.loadFailed":`互換性ラボのデータを読み込めませんでした`,"lab.projectionUnavailable":`ラボ投影を利用できません。先に適合性プローブまたはライブプローブを実行してください。`,"lab.projectionIncompatible":`ラボ投影のスキーマに互換性がありません。投影を再構築してください。`,"lab.statusTitle":`投影ステータス`,"lab.matrixTitle":`互換性マトリクス`,"lab.verdictsTitle":`判定レコード`,"lab.filter.layer":`証拠レイヤー`,"lab.filter.verdict":`判定`,"lab.filter.subject":`サブジェクト ID`,"lab.filter.all":`すべて`,"lab.col.subject":`サブジェクト`,"lab.col.layer":`レイヤー`,"lab.col.suite":`スイート`,"lab.col.verdict":`判定`,"lab.col.asOf":`時点`,"lab.col.protocol":`プロトコル適合性`,"lab.col.live":`ライブ経路互換性`,"lab.col.task":`タスク有効性`,"lab.empty":`投影にはまだ互換性判定がありません。`,"lab.subjectKind":`種類`,"lab.observationCount":`観測`,"lab.eventCount":`イベント`,"lab.verdictCount":`判定`,"lab.subjectCount":`サブジェクト`,"lab.builtAt":`構築日時`,"lab.loading":`互換性の証拠を読み込み中…`,"lab.loadMore":`さらに読み込む`,"lab.detailTitle":`判定の詳細`,"lab.detailClose":`閉じる`,"lab.detailSubject":`サブジェクト`,"lab.detailObservations":`観測`,"lab.detailEvents":`証拠イベント`,"lab.detailArtifacts":`アーティファクトのメタデータ`,"lab.detailLoadFailed":`判定の詳細を読み込めませんでした`,"lab.refresh":`更新`,"lab.verdict.UNKNOWN":`不明`,"lab.verdict.CLAIMED":`申告済み`,"lab.verdict.PROBED":`プローブ済み`,"lab.verdict.VERIFIED":`検証済み`,"lab.verdict.DEGRADED":`低下`,"lab.verdict.BLOCKED":`ブロック`,"lab.verdict.UNSUPPORTED":`未対応`,"lab.layer.protocol_conformance":`プロトコル適合性`,"lab.layer.live_route_compatibility":`ライブ経路互換性`,"lab.layer.task_effectiveness":`タスク有効性`},tr:{"lab.title":`Uyumluluk Laboratuvarı`,"lab.subtitle":`Laboratuvar projeksiyonu kanıtlarından oluşturulan salt okunur uyumluluk karar matrisi.`,"lab.loadFailed":`Uyumluluk laboratuvarı verileri yüklenemedi`,"lab.projectionUnavailable":`Laboratuvar projeksiyonu kullanılamıyor. Önce uygunluk veya canlı probları çalıştırın.`,"lab.projectionIncompatible":`Laboratuvar projeksiyonu şeması uyumsuz. Projeksiyonu yeniden oluşturun.`,"lab.statusTitle":`Projeksiyon durumu`,"lab.matrixTitle":`Uyumluluk matrisi`,"lab.verdictsTitle":`Karar kayıtları`,"lab.filter.layer":`Kanıt katmanı`,"lab.filter.verdict":`Karar`,"lab.filter.subject":`Özne kimliği`,"lab.filter.all":`Tümü`,"lab.col.subject":`Özne`,"lab.col.layer":`Katman`,"lab.col.suite":`Paket`,"lab.col.verdict":`Karar`,"lab.col.asOf":`Tarih`,"lab.col.protocol":`Protokol uygunluğu`,"lab.col.live":`Canlı rota uyumluluğu`,"lab.col.task":`Görev etkinliği`,"lab.empty":`Projeksiyonda henüz uyumluluk kararı yok.`,"lab.subjectKind":`Tür`,"lab.observationCount":`Gözlemler`,"lab.eventCount":`Olaylar`,"lab.verdictCount":`Kararlar`,"lab.subjectCount":`Özneler`,"lab.builtAt":`Oluşturulma`,"lab.loading":`Uyumluluk kanıtları yükleniyor…`,"lab.loadMore":`Daha fazla yükle`,"lab.detailTitle":`Karar ayrıntısı`,"lab.detailClose":`Kapat`,"lab.detailSubject":`Özne`,"lab.detailObservations":`Gözlemler`,"lab.detailEvents":`Kanıt olayları`,"lab.detailArtifacts":`Artefakt meta verileri`,"lab.detailLoadFailed":`Karar ayrıntısı yüklenemedi`,"lab.refresh":`Yenile`,"lab.verdict.UNKNOWN":`Bilinmiyor`,"lab.verdict.CLAIMED":`İddia edildi`,"lab.verdict.PROBED":`Problandı`,"lab.verdict.VERIFIED":`Doğrulandı`,"lab.verdict.DEGRADED":`Kısıtlı`,"lab.verdict.BLOCKED":`Engellendi`,"lab.verdict.UNSUPPORTED":`Desteklenmiyor`,"lab.layer.protocol_conformance":`Protokol uygunluğu`,"lab.layer.live_route_compatibility":`Canlı rota uyumluluğu`,"lab.layer.task_effectiveness":`Görev etkinliği`}},Je={en:{subjectKindUnknown:`Unknown`,"artifact.present":`Present`,"artifact.corrupt":`Corrupt`,"artifact.purged_unavailable":`Purged / unavailable`,selectVerdict:`View verdict for {subject}`,"community.title":`Community evidence`,"community.notLocalVerdict":`Untrusted read-only context. Not included in this local verdict.`,"community.bundles":`Bundles`,"community.activeRecords":`Active records`,"community.revokedRecords":`Revoked records`},de:{subjectKindUnknown:`Unbekannt`,"artifact.present":`Vorhanden`,"artifact.corrupt":`Beschädigt`,"artifact.purged_unavailable":`Gelöscht / nicht verfügbar`,selectVerdict:`Urteil für {subject} anzeigen`,"community.title":`Community-Evidenz`,"community.notLocalVerdict":`Nicht vertrauenswürdiger Nur-Lese-Kontext. Nicht Teil dieses lokalen Urteils.`,"community.bundles":`Pakete`,"community.activeRecords":`Aktive Einträge`,"community.revokedRecords":`Widerrufene Einträge`},fr:{subjectKindUnknown:`Inconnu`,"artifact.present":`Présent`,"artifact.corrupt":`Corrompu`,"artifact.purged_unavailable":`Purgé / indisponible`,selectVerdict:`Afficher le verdict pour {subject}`,"community.title":`Données de la communauté`,"community.notLocalVerdict":`Contexte non fiable en lecture seule. Non inclus dans ce verdict local.`,"community.bundles":`Lots`,"community.activeRecords":`Enregistrements actifs`,"community.revokedRecords":`Enregistrements révoqués`},ko:{subjectKindUnknown:`알 수 없음`,"artifact.present":`있음`,"artifact.corrupt":`손상됨`,"artifact.purged_unavailable":`삭제됨 / 사용할 수 없음`,selectVerdict:`{subject}의 판정 보기`,"community.title":`커뮤니티 증거`,"community.notLocalVerdict":`신뢰되지 않는 읽기 전용 컨텍스트입니다. 이 로컬 판정에는 포함되지 않습니다.`,"community.bundles":`번들`,"community.activeRecords":`활성 레코드`,"community.revokedRecords":`폐기된 레코드`},zh:{subjectKindUnknown:`未知`,"artifact.present":`存在`,"artifact.corrupt":`已损坏`,"artifact.purged_unavailable":`已清除 / 不可用`,selectVerdict:`查看 {subject} 的判定`,"community.title":`社区证据`,"community.notLocalVerdict":`不受信任的只读上下文。不计入此本地判定。`,"community.bundles":`证据包`,"community.activeRecords":`有效记录`,"community.revokedRecords":`已撤销记录`},"zh-TW":{subjectKindUnknown:`未知`,"artifact.present":`存在`,"artifact.corrupt":`已損壞`,"artifact.purged_unavailable":`已清除 / 不可用`,selectVerdict:`查看 {subject} 的判定`,"community.title":`社群證據`,"community.notLocalVerdict":`不受信任的唯讀脈絡。不計入此本地判定。`,"community.bundles":`證據包`,"community.activeRecords":`有效記錄`,"community.revokedRecords":`已撤銷記錄`},ru:{subjectKindUnknown:`Неизвестно`,"artifact.present":`Доступен`,"artifact.corrupt":`Повреждён`,"artifact.purged_unavailable":`Удалён / недоступен`,selectVerdict:`Открыть вердикт для {subject}`,"community.title":`Данные сообщества`,"community.notLocalVerdict":`Недоверенный контекст только для чтения. Не входит в этот локальный вердикт.`,"community.bundles":`Пакеты`,"community.activeRecords":`Активные записи`,"community.revokedRecords":`Отозванные записи`},ja:{subjectKindUnknown:`不明`,"artifact.present":`存在`,"artifact.corrupt":`破損`,"artifact.purged_unavailable":`削除済み / 利用不可`,selectVerdict:`{subject} の判定を表示`,"community.title":`コミュニティ証拠`,"community.notLocalVerdict":`信頼されていない読み取り専用コンテキストです。このローカル判定には含まれません。`,"community.bundles":`バンドル`,"community.activeRecords":`有効なレコード`,"community.revokedRecords":`取り消されたレコード`},tr:{subjectKindUnknown:`Bilinmiyor`,"artifact.present":`Mevcut`,"artifact.corrupt":`Bozuk`,"artifact.purged_unavailable":`Temizlenmiş / kullanılamıyor`,selectVerdict:`{subject} için kararı görüntüle`,"community.title":`Topluluk kanıtı`,"community.notLocalVerdict":`Güvenilmeyen salt okunur bağlam. Bu yerel karara dahil değildir.`,"community.bundles":`Paketler`,"community.activeRecords":`Etkin kayıtlar`,"community.revokedRecords":`Geri çekilen kayıtlar`}};function Ye(e,t,n){let r=Je[e][t];if(n)for(let[e,t]of Object.entries(n))r=r.split(`{${e}}`).join(String(t));return r}function Xe(e,t){return{...t,...qe[e]}}var Ze={en:Xe(`en`,Re),de:Xe(`de`,ze),fr:Xe(`fr`,Be),ko:Xe(`ko`,Ve),zh:Xe(`zh`,He),"zh-TW":Xe(`zh-TW`,Ue),ru:Xe(`ru`,We),ja:Xe(`ja`,Ge),tr:Xe(`tr`,Ke)};function Qe(e){return Ze[e][`lang.nativeName`]}function $e(e,t){return Ze[e][t]}var et=[{code:`en`,htmlLang:`en`},{code:`de`,htmlLang:`de`},{code:`fr`,htmlLang:`fr`},{code:`ko`,htmlLang:`ko`},{code:`zh`,htmlLang:`zh-CN`},{code:`zh-TW`,htmlLang:`zh-TW`},{code:`ru`,htmlLang:`ru`},{code:`ja`,htmlLang:`ja`},{code:`tr`,htmlLang:`tr`}],tt=`ocx-lang`,nt=null;function rt(){try{let e=localStorage.getItem(tt);if(e===`en`||e===`de`||e===`fr`||e===`ko`||e===`zh`||e===`zh-TW`||e===`ru`||e===`ja`||e===`tr`)return e}catch{}let e=typeof navigator<`u`&&navigator?.language?navigator.language.toLowerCase():`en`;return e.startsWith(`de`)?`de`:e.startsWith(`fr`)?`fr`:e.startsWith(`ko`)?`ko`:e.startsWith(`zh`)?e.includes(`tw`)||e.includes(`hk`)||e.includes(`mo`)||e.includes(`hant`)?`zh-TW`:`zh`:e.startsWith(`ru`)?`ru`:e.startsWith(`ja`)?`ja`:e.startsWith(`tr`)?`tr`:`en`}function it(){return nt??rt()}function at(e){nt=e}var ot=(0,_.createContext)(null);function st(e,t){if(!t)return e;let n=e;for(let e of Object.keys(t))n=n.split(`{${e}}`).join(String(t[e]));return n}function ct(){let e=(0,_.useContext)(ot);if(!e)throw Error(`useI18n must be used within LanguageProvider`);return e}function Q(){return ct().t}function lt({children:e}){let[t,n]=(0,_.useState)(()=>{let e=rt();return at(e),e}),r=(0,_.useCallback)(e=>{at(e),n(e)},[]);(0,_.useEffect)(()=>{let e=et.find(e=>e.code===t)??et[0];document.documentElement.lang=e.htmlLang;try{localStorage.setItem(`ocx-lang`,t)}catch{}},[t]);let i=(0,_.useCallback)((e,n)=>st(Ze[t][e]??Re[e]??e,n),[t]),a=(0,_.useMemo)(()=>({locale:t,setLocale:r,t:i}),[t,i]);return(0,J.jsx)(ot.Provider,{value:a,children:e})}function ut({k:e,cmd:t,vars:n}){let{t:r}=ct(),[i,a=``]=r(e,n).split(`{cmd}`);return(0,J.jsxs)(J.Fragment,{children:[i,(0,J.jsx)(`code`,{className:`chip`,children:t}),a]})}function dt(e){return e.replace(/^#\/?/,``)}function ft(e,t=window){let n=dt(e);if(dt(t.location.hash)===n)return;let r=`${t.location.pathname}${t.location.search}#${n}`;t.history.replaceState(t.history.state,``,r)}function pt(e,t=window){let n=dt(e);dt(t.location.hash)!==n&&(t.location.hash=n)}var mt=m(),ht=4,gt=8,_t=8,vt=280,yt=120,bt=160,xt=12;function St(){return typeof window<`u`?window.innerHeight:800}function Ct(){return typeof window<`u`?window.innerWidth:1024}function wt(e,{align:t,placement:n=`below`,menuHeight:r=vt}={}){let i=Math.min(Math.max(r,yt),vt),a=St(),o=Ct();if(n===`right`){let t=a-e.top-_t,n=e.top-_t,r=i+gt>t&&n>t,s=Math.max(_t,Math.min(e.right+xt,o-bt-_t));return r?{position:`fixed`,left:s,bottom:a-e.top+gt,minWidth:bt,maxHeight:Math.max(yt,Math.min(vt,e.top-_t-ht))}:{position:`fixed`,top:e.top,left:s,minWidth:bt,maxHeight:Math.max(yt,Math.min(vt,a-e.top-_t))}}let s=t??(e.right>o/2?`right`:`left`),c=Math.max(e.width,0),l=a-e.bottom-_t,u=e.top-_t;if(i+ht>l&&u>l){let t={position:`fixed`,bottom:a-e.top+gt,minWidth:c,maxHeight:Math.max(0,Math.min(vt,u-ht))};return s===`right`?t.right=Math.max(_t,o-e.right):t.left=Math.max(_t,Math.min(e.left,o-_t-c)),t}let d={position:`fixed`,top:e.bottom+ht,minWidth:c,maxHeight:Math.max(0,Math.min(vt,l-ht))};return s===`right`?d.right=Math.max(_t,o-e.right):d.left=Math.max(_t,Math.min(e.left,o-_t-c)),d}function Tt({on:e,mixed:t=!1,onClick:n,disabled:r,label:i,showLabel:a=!1,title:o}){let s=a&&!!i;return(0,J.jsxs)(`button`,{type:`button`,className:`switch${e?` on`:``}${t?` mixed`:``}${s?` switch-labeled`:``}`,onClick:n,disabled:r,"aria-pressed":t?`mixed`:e,"aria-label":s?void 0:i??(e?`enabled`:`disabled`),title:o,children:[(0,J.jsx)(`span`,{className:`knob`}),s?(0,J.jsx)(`span`,{className:`switch-labeled-text text-label muted`,children:i}):null]})}function $({tone:e,children:t}){return(0,J.jsxs)(`div`,{className:`notice ${e===`ok`?`notice-ok`:e===`warn`?`notice-warn`:`notice-err`}`,role:`status`,children:[e===`ok`?(0,J.jsx)(ue,{}):(0,J.jsx)(_e,{}),(0,J.jsx)(`span`,{children:t})]})}function Et({tone:e,children:t,onDismiss:n,dismissLabel:r}){return(0,mt.createPortal)((0,J.jsx)(`div`,{className:`toast-notice-host`,role:`presentation`,children:(0,J.jsxs)(`div`,{className:`toast-notice notice ${e===`ok`?`notice-ok`:e===`warn`?`notice-warn`:`notice-err`}`,role:`status`,"aria-live":`polite`,children:[e===`ok`?(0,J.jsx)(ue,{}):(0,J.jsx)(_e,{}),(0,J.jsx)(`span`,{className:`toast-notice-copy`,children:t}),n&&(0,J.jsx)(`button`,{type:`button`,className:`toast-notice-dismiss`,onClick:n,"aria-label":r,children:`×`})]})}),document.body)}function Dt({value:e,options:t,onChange:n,disabled:r,id:i,label:a,describedBy:o,title:s,style:c,align:l,placement:u,dropdownStyle:d,portal:f=!0}){let p=(0,_.useId)(),[m,h]=(0,_.useState)(!1),[g,v]=(0,_.useState)(null),[y,b]=(0,_.useState)(),x=(0,_.useRef)(null),S=(0,_.useRef)(null),C=(0,_.useRef)(null),w=(0,_.useCallback)(e=>`${p}-${e}`,[p]),T=t.find(t=>t.value===e),E=t.length===0?0:Math.max(0,t.findIndex(t=>t.value===e)),D=!m||t.length===0?E:Math.min(g??E,t.length-1),O=(0,_.useCallback)((e=!1)=>{h(!1),v(null),e&&S.current?.focus()},[]),k=(0,_.useCallback)(e=>{if(r||t.length===0)return;let n=Math.max(0,Math.min(t.length-1,e));v(n),h(!0)},[r,t.length]),A=(0,_.useCallback)(e=>{if(!f)return;let t=S.current;t&&b(wt(t.getBoundingClientRect(),{align:l,placement:u,menuHeight:e}))},[l,u,f]);(0,_.useEffect)(()=>{if(!m)return;let e=e=>{let t=e.target;x.current?.contains(t)||C.current?.contains(t)||O()};return document.addEventListener(`mousedown`,e),()=>document.removeEventListener(`mousedown`,e)},[O,m]),(0,_.useLayoutEffect)(()=>{if(!m||!f)return;A();let e=()=>A(C.current?.offsetHeight);return window.addEventListener(`resize`,e),window.addEventListener(`scroll`,e,!0),()=>{window.removeEventListener(`resize`,e),window.removeEventListener(`scroll`,e,!0)}},[m,t.length,f,A]),(0,_.useLayoutEffect)(()=>{if(!m||!f||!C.current||!S.current)return;let e=C.current.offsetHeight;if(!e)return;let t=wt(S.current.getBoundingClientRect(),{align:l,placement:u,menuHeight:e});b(e=>e?.top===t.top&&e?.bottom===t.bottom&&e?.maxHeight===t.maxHeight?e:t)},[l,m,t.length,u,f]),(0,_.useLayoutEffect)(()=>{!m||!C.current||C.current.querySelector(`[id="${w(D)}"]`)?.scrollIntoView({block:`nearest`})},[D,m,w]);let j=e=>{if(r)return;let i=t[e];i&&(n(i.value),O(!0))},M=e=>{if(!r)switch(e.key){case`ArrowDown`:e.preventDefault(),k(m?Math.min(t.length-1,D+1):E);break;case`ArrowUp`:e.preventDefault(),k(m?Math.max(0,D-1):E);break;case`Home`:e.preventDefault(),k(0);break;case`End`:e.preventDefault(),k(t.length-1);break;case`Enter`:case` `:e.preventDefault(),m?j(D):k(E);break;case`Escape`:m&&(e.preventDefault(),O(!0));break;case`Tab`:if(m){let e=t[D];e&&n(e.value),h(!1)}}},N=m&&t[D]?w(D):void 0,P=m&&!r?(0,J.jsx)(`div`,{ref:C,id:p,className:`select-dropdown${f?` select-dropdown-portal`:``}${!f&&l===`right`?` select-dropdown-right`:``}${!f&&u===`right`?` select-dropdown-beside`:``}`,role:`listbox`,"aria-label":a,style:f?{...y,zIndex:60,...d}:d,children:t.map((t,n)=>(0,J.jsx)(`button`,{id:w(n),type:`button`,role:`option`,tabIndex:-1,disabled:r,"aria-selected":t.value===e,className:`select-option${t.value===e?` active`:``}${n===D?` select-option-active`:``}`,onMouseEnter:()=>v(n),onClick:()=>j(n),children:t.label},t.value))}):null;return(0,J.jsxs)(`div`,{ref:x,className:`custom-select`,style:{position:`relative`,display:`inline-block`,...c},children:[(0,J.jsxs)(`button`,{ref:S,id:i,type:`button`,role:`combobox`,title:s,"aria-describedby":o,className:`select-trigger`,onClick:()=>{r||(m?O():k(E))},onKeyDown:M,disabled:r,"aria-haspopup":`listbox`,"aria-expanded":m,"aria-controls":m?p:void 0,"aria-activedescendant":N,"aria-label":a,children:[(0,J.jsx)(`span`,{children:T?.label??e}),(0,J.jsx)(Se,{style:{width:12,height:12,color:`var(--muted)`,transform:m?`rotate(90deg)`:`none`,transition:`transform .12s`}})]}),f?P&&(0,mt.createPortal)(P,document.body):P]})}function Ot({icon:e,title:t,children:n,className:r,style:i}){return(0,J.jsxs)(`div`,{className:r?`empty ${r}`:`empty`,style:i,children:[e,(0,J.jsx)(`div`,{className:`title`,children:t}),n&&(0,J.jsx)(`div`,{className:`text-control`,children:n})]})}function kt({content:e,children:t,side:n=`top`,maxWidth:r=280}){let[i,a]=(0,_.useState)(!1),o=(0,_.useId)(),s=(0,_.useRef)(null),c=()=>{s.current!==null&&window.clearTimeout(s.current),s.current=window.setTimeout(()=>a(!0),150)},l=()=>{s.current!==null&&(window.clearTimeout(s.current),s.current=null),a(!1)};return(0,_.useEffect)(()=>()=>{s.current!==null&&window.clearTimeout(s.current)},[]),(0,J.jsxs)(`button`,{type:`button`,className:`ocx-tooltip`,onMouseEnter:c,onMouseLeave:l,onFocus:c,onBlur:l,onKeyDown:e=>{e.key===`Escape`&&l()},"aria-describedby":i?o:void 0,style:{display:`inline`,border:0,background:`transparent`,padding:0,margin:0,color:`inherit`,font:`inherit`,cursor:`inherit`},children:[t,i&&(0,J.jsx)(`span`,{id:o,className:`ocx-tooltip-bubble ocx-tooltip-bubble--${n}`,role:`tooltip`,style:{maxWidth:r},children:e})]})}var At=45e3,jt=2147483647;async function Mt(e){if(e.status!==204){if(typeof e.text==`function`){let t=await e.text();return t.trim()?JSON.parse(t):void 0}return await e.json()}}function Nt(e,t){return typeof e.error==`string`&&e.error?e.error:typeof e.message==`string`&&e.message?e.message:t}async function Pt(e,t=`HTTP ${e.status}`){if(!e.ok){let n=t;try{n=Nt(await e.json(),t)}catch{}throw Error(n)}return Mt(e)}async function Ft(e){if(!e.ok)return null;try{return await Mt(e)}catch{return null}}var It=[`gpt-5.6-luna`];function Lt(e){let t=Array.isArray(e)?e.filter(e=>typeof e==`string`&&e.trim()!==``).map(e=>e.trim()):[];return t.length>0?t:It}function Rt(e){return Lt(e).join(`, `)}function zt(e){return Lt(e).map(e=>e.replace(/^gpt-/,``)).join(`, `)}var Bt=`dashboard/update`;function Vt(){let e=window.location.hash.replace(/^#\/?/,``);return e===`dashboard/providers`?`providers`:e===`dashboard/models`?`models`:`overview`}function Ht(){return window.location.hash.replace(/^#\/?/,``)===Bt}function Ut(e){return e===`overview`?`dashboard`:`dashboard/${e}`}async function Wt(e,t){let n=await Pt(e,t);if(n===void 0)throw Error(t??`empty response`);return n}var Gt=[`low`,`medium`,`high`,`xhigh`];function Kt(e){return e?.includes(`-preview.`)?`preview`:`latest`}function qt(e,t){switch(e){case`source_checkout`:return t(`dash.updateReason.source_checkout`);case`latest_unavailable`:return t(`dash.updateReason.latest_unavailable`);case`already_latest`:return t(`dash.updateReason.already_latest`);default:return t(`dash.updateReason.unknown`)}}function Jt(e,t){switch(e){case`running`:return t(`dash.updateStatus.running`);case`restarting`:return t(`dash.updateStatus.restarting`);case`succeeded`:return t(`dash.updateStatus.succeeded`);case`failed`:return t(`dash.updateStatus.failed`)}}function Yt(e,t){let n={...e};return t?.model!==void 0&&(n.model=t.model),t?.backend===null?delete n.backend:t?.backend!==void 0&&(n.backend=t.backend),t?.reasoning!==void 0&&(n.reasoning=t.reasoning),t?.streamRoutedModelOutput!==void 0&&(n.streamRoutedModelOutput=t.streamRoutedModelOutput),t?.enabled!==void 0&&(n.enabled=t.enabled),t?.maxDescriptionsPerTurn!==void 0&&(n.maxDescriptionsPerTurn=t.maxDescriptionsPerTurn),t?.timeoutMs!==void 0&&(n.timeoutMs=t.timeoutMs),n}function Xt(e){return{vision:{reasoning:e}}}function Zt(e){return{vision:{enabled:e}}}function Qt(e){return{vision:{maxDescriptionsPerTurn:e}}}function $t(e){return{vision:{timeoutMs:e}}}var en=At,tn=jt,nn=1;function rn(e){let t=e.trim();if(!/^[0-9]+$/.test(t))return;let n=Number(t);if(!(!Number.isSafeInteger(n)||n<=0))return n}function an(e){let t=rn(e);if(!(t===void 0||ttn))return t}var on=[`low`,`medium`,`high`,`xhigh`,`max`];function sn(e,t){let n=e.find(e=>e.id===t)?.reasoningEfforts;if(!n||n.length===0)return[...on];let r=on.filter(e=>n.includes(e));return r.length>0?r:[...on]}function cn(e,t){let n=ln(e,t);return e.includes(n)?e:[n,...e]}function ln(e,t){if(e.length===0||e.includes(t))return t;let n=on.indexOf(t),r=e[0],i=on.indexOf(r);for(let t of e){let e=on.indexOf(t);e<=n&&e>=i&&(r=t,i=e)}return r}function un(e){let t=[];for(let n of e)(n.provider===`openai`||n.provider===`anthropic`)&&t.push({value:n.id,label:`${n.provider}/${n.id}`});return t}function dn(e,t,n,r){if(e===void 0){let e=un(t);return n&&!e.some(e=>e.value===n)&&e.unshift({value:n,label:n,...r?{backend:r}:{},model:n}),e}let i=e.map(e=>({value:e.value,label:e.label,backend:e.backend,model:e.model}));return n&&!i.some(e=>e.value===n)&&i.unshift({value:n,label:n,...r?{backend:r}:{},model:n}),i}function fn(e,t,n,r){let i=e?e.map(e=>({value:e.value,label:e.label,backend:e.backend})):un(t);return n&&!i.some(e=>e.value===n)&&i.unshift({value:n,label:n,...r?{backend:r}:{}}),i}function pn(e,t,n){let r=Lt(n),i=r.flatMap(t=>{let n=e.find(e=>e.namespaced.startsWith(t))??e.find(e=>e.id.startsWith(t));return n?[{provider:n.provider,modelId:t}]:[]}),a=e.filter(e=>i.some(t=>e.provider===t.provider&&e.id.startsWith(t.modelId))),o=new Set([...r.flatMap(e=>[e,`openai/${e}`]),...a.flatMap(e=>[e.namespaced,`${e.provider}/${e.id}`])]),s=[{value:``,label:`—`},...e.filter(e=>!o.has(e.namespaced)).map(e=>({value:e.namespaced,label:e.namespaced}))];return t&&!o.has(t)&&!s.some(e=>e.value===t)&&s.push({value:t,label:t}),s}function mn(e,t){return e.find(e=>e.id===t)?.provider===`anthropic`?`anthropic`:`openai`}function hn(e,t,n){let r=t.find(e=>e.value===n);return{backend:r?.backend??mn(e,n),model:r?.model??n}}function gn(e,t,n){return t.find(e=>e.value===n)?.backend||(n.includes(`/`)?`routed`:mn(e,n))}var _n=!1;typeof window<`u`&&typeof window.addEventListener==`function`&&(window.addEventListener(`keydown`,()=>{_n=!0},{capture:!0,passive:!0}),window.addEventListener(`pointerdown`,()=>{_n=!1},{capture:!0,passive:!0}));function vn(e){if(e){if(_n){e.focus({preventScroll:!0});return}try{e.focus({preventScroll:!0,focusVisible:!1})}catch{e.focus({preventScroll:!0})}}}function yn(e,t){let n=(0,_.useRef)(null);return(0,_.useEffect)(()=>{let r=n.current;if(r){if(e){r.open||r.showModal();return}r.open&&r.close(),vn(t.current)}},[e,t]),(0,_.useEffect)(()=>()=>{let e=n.current;e?.open&&e.close(),vn(t.current)},[t]),n}function bn(e){let{t,updateOpen:n,closeUpdateDialog:r,updateDialogRef:i,updateChannel:a,changeUpdateChannel:o,updateLoading:s,updateError:c,updateCheck:l,fetchUpdateCheck:u,updateRestart:d,setUpdateRestart:f,runUpdate:p,maHelpOpen:m,setMaHelpOpen:h,maHelpDialogRef:g,effortCapHelpOpen:_,setEffortCapHelpOpen:v,effortCapHelpDialogRef:y,shadowCallHelpOpen:b,setShadowCallHelpOpen:x,shadowCallHelpDialogRef:S,shadowCall:C}=e;return(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)(`dialog`,{ref:i,id:`dashboard-update-dialog`,className:`modal-overlay`,style:{display:n?`flex`:`none`,border:`none`,margin:0,maxWidth:`none`,maxHeight:`none`,width:`100%`,height:`100%`},"aria-labelledby":`update-title`,onCancel:e=>{e.preventDefault(),r()},children:(0,J.jsxs)(`div`,{className:`modal-card`,children:[(0,J.jsxs)(`div`,{className:`modal-head`,children:[(0,J.jsx)(`h3`,{id:`update-title`,children:t(`dash.updateTitle`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-icon`,onClick:r,"aria-label":t(`common.cancel`),children:(0,J.jsx)(de,{})})]}),(0,J.jsx)(`div`,{className:`modal-desc`,children:t(`dash.updateDesc`)}),(0,J.jsxs)(`div`,{className:`update-row`,children:[(0,J.jsx)(`label`,{className:`field-label`,htmlFor:`update-channel`,children:t(`dash.updateChannel`)}),(0,J.jsx)(Dt,{value:a,options:[{value:`latest`,label:`latest`},{value:`preview`,label:`preview`}],onChange:e=>o(e),disabled:s,label:t(`dash.updateChannel`),portal:!1})]}),s&&(0,J.jsx)(Ot,{className:`update-empty`,icon:(0,J.jsx)(`span`,{className:`spin`}),title:t(`dash.updateChecking`)}),c&&(0,J.jsxs)(`div`,{className:`notice notice-err`,role:`status`,children:[(0,J.jsx)(_e,{}),(0,J.jsx)(`span`,{children:c})]}),l&&!s&&(0,J.jsxs)(`div`,{className:`update-box`,children:[(0,J.jsxs)(`div`,{className:`spread`,children:[(0,J.jsxs)(`div`,{children:[(0,J.jsx)(`div`,{className:`muted text-label`,children:t(`dash.updateInstalled`)}),(0,J.jsx)(`div`,{className:`mono`,children:l.currentVersion})]}),(0,J.jsxs)(`div`,{children:[(0,J.jsx)(`div`,{className:`muted text-label`,children:t(`dash.updateLatest`)}),(0,J.jsx)(`div`,{className:`mono`,children:l.latestVersion??`—`})]}),(0,J.jsx)(`span`,{className:`badge ${l.updateAvailable?`badge-green`:`badge-muted`}`,children:l.updateAvailable?t(`dash.updateAvailable`):t(`dash.updateCurrent`)})]}),(0,J.jsxs)(`div`,{className:`muted update-command`,children:[t(`dash.updateCommand`),` `,(0,J.jsx)(`code`,{className:`chip`,children:l.command})]}),l.reason===`source_checkout`&&(0,J.jsxs)(`div`,{className:`notice-warn`,role:`status`,children:[(0,J.jsx)(_e,{}),` `,t(`dash.updateSource`)]}),l.reason===`latest_unavailable`&&(0,J.jsxs)(`div`,{className:`notice-warn`,role:`status`,children:[(0,J.jsx)(_e,{}),` `,t(`dash.updateUnavailable`),(0,J.jsxs)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,disabled:s,onClick:()=>{u(a,!0)},style:{marginLeft:12},children:[(0,J.jsx)(pe,{}),` `,t(`dash.updateRetry`)]})]}),!l.canUpdate&&l.reason!==`latest_unavailable`&&l.reason!==`source_checkout`&&(0,J.jsxs)(`div`,{className:`update-recheck`,children:[(0,J.jsx)(`span`,{className:`muted update-recheck-reason`,children:t(`dash.updateCannotAuto`,{reason:qt(l.reason,t)})}),(0,J.jsxs)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,disabled:s,onClick:()=>{u(a,!0)},children:[(0,J.jsx)(pe,{}),` `,t(s?`dash.updateChecking`:`dash.updateRecheck`)]})]}),l.canUpdate&&(0,J.jsxs)(`div`,{className:`spread update-restart`,children:[(0,J.jsxs)(`div`,{children:[(0,J.jsx)(`div`,{className:`font-semibold`,children:t(`dash.updateRestart`)}),(0,J.jsx)(`div`,{className:`muted text-label`,children:t(`dash.updateRestartHint`)})]}),(0,J.jsx)(`button`,{type:`button`,className:`switch ${d?`on`:``}`,onClick:()=>f(e=>!e),"aria-label":t(`dash.updateRestart`),"aria-pressed":d,children:(0,J.jsx)(`span`,{className:`knob`})})]})]}),(0,J.jsxs)(`div`,{className:`modal-actions`,children:[(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost`,onClick:r,children:t(`common.cancel`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-primary`,onClick:p,disabled:!l?.canUpdate||s,children:t(`dash.runUpdate`)})]})]})}),(0,J.jsxs)(`dialog`,{ref:g,id:`multi-agent-help-dialog`,className:`modal-overlay`,style:{display:m?`flex`:`none`,border:`none`,margin:0,maxWidth:`none`,maxHeight:`none`,width:`100%`,height:`100%`},"aria-labelledby":`multi-agent-help-title`,onCancel:e=>{e.preventDefault(),h(!1)},children:[(0,J.jsx)(`button`,{type:`button`,className:`modal-backdrop-dismiss`,"aria-label":t(`common.close`),tabIndex:-1,onClick:()=>h(!1)}),(0,J.jsxs)(`div`,{className:`modal-card`,onClick:e=>e.stopPropagation(),children:[(0,J.jsxs)(`div`,{className:`modal-head`,children:[(0,J.jsx)(`h3`,{id:`multi-agent-help-title`,children:t(`dash.multiAgent`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-icon`,onClick:()=>h(!1),"aria-label":t(`common.close`),children:(0,J.jsx)(de,{})})]}),(0,J.jsx)(`div`,{className:`modal-desc leading-relaxed`,style:{whiteSpace:`pre-line`},children:t(`models.v2Help`)}),(0,J.jsx)(`div`,{style:{marginTop:12},children:(0,J.jsx)(`a`,{className:`text-control`,href:`https://opencodex.me/guides/sub-agent-surface/`,target:`_blank`,rel:`noreferrer`,style:{color:`var(--accent)`},children:t(`models.v2DocsLink`)})}),(0,J.jsx)(`div`,{className:`modal-actions`,children:(0,J.jsx)(`button`,{type:`button`,className:`btn btn-primary`,onClick:()=>h(!1),children:t(`common.ok`)})})]})]}),(0,J.jsxs)(`dialog`,{ref:y,id:`effort-cap-help-dialog`,className:`modal-overlay`,style:{display:_?`flex`:`none`,border:`none`,margin:0,maxWidth:`none`,maxHeight:`none`,width:`100%`,height:`100%`},"aria-labelledby":`effort-cap-help-title`,onCancel:e=>{e.preventDefault(),v(!1)},children:[(0,J.jsx)(`button`,{type:`button`,className:`modal-backdrop-dismiss`,"aria-label":t(`common.close`),tabIndex:-1,onClick:()=>v(!1)}),(0,J.jsxs)(`div`,{className:`modal-card`,onClick:e=>e.stopPropagation(),children:[(0,J.jsxs)(`div`,{className:`modal-head`,children:[(0,J.jsx)(`h3`,{id:`effort-cap-help-title`,children:t(`dash.effortCapLabel`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-icon`,onClick:()=>v(!1),"aria-label":t(`common.close`),children:(0,J.jsx)(de,{})})]}),(0,J.jsx)(`div`,{className:`modal-desc leading-relaxed`,style:{whiteSpace:`pre-line`},children:t(`dash.effortCapHelp`)}),(0,J.jsx)(`div`,{className:`modal-actions`,children:(0,J.jsx)(`button`,{type:`button`,className:`btn btn-primary`,onClick:()=>v(!1),children:t(`common.ok`)})})]})]}),(0,J.jsxs)(`dialog`,{ref:S,id:`shadow-call-help-dialog`,className:`modal-overlay`,style:{display:b?`flex`:`none`,border:`none`,margin:0,maxWidth:`none`,maxHeight:`none`,width:`100%`,height:`100%`},"aria-labelledby":`shadow-call-help-title`,onCancel:e=>{e.preventDefault(),x(!1)},children:[(0,J.jsx)(`button`,{type:`button`,className:`modal-backdrop-dismiss`,"aria-label":t(`common.close`),tabIndex:-1,onClick:()=>x(!1)}),(0,J.jsxs)(`div`,{className:`modal-card`,onClick:e=>e.stopPropagation(),children:[(0,J.jsxs)(`div`,{className:`modal-head`,children:[(0,J.jsx)(`h3`,{id:`shadow-call-help-title`,children:t(`dash.shadowCallIntercept`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-icon`,onClick:()=>x(!1),"aria-label":t(`common.close`),children:(0,J.jsx)(de,{})})]}),(0,J.jsx)(`div`,{className:`modal-desc leading-relaxed`,style:{whiteSpace:`pre-line`},children:t(`dash.shadowCallTooltip`,{models:Rt(C?.sourceModels)})}),(0,J.jsx)(`div`,{className:`modal-actions`,children:(0,J.jsx)(`button`,{type:`button`,className:`btn btn-primary`,onClick:()=>x(!1),children:t(`common.ok`)})})]})]})]})}var xn={anthropic:`claude-color.svg`,"anthropic-apikey":`claude-color.svg`,"azure-openai":`openai.svg`,chatgpt:`openai.svg`,"cloudflare-ai-gateway":`cloudflare-ai-gateway-color.svg`,"cloudflare-workers-ai":`cloudflare-ai-gateway-color.svg`,cline:`cline-color.svg`,"cline-pass":`cline-color.svg`,"command-code":`commandcode-color.svg`,commandcode:`commandcode-color.svg`,cursor:`cursor-color.svg`,deepseek:`deepseek-color.svg`,firepass:`firepass-color.svg`,fireworks:`fireworks-color.svg`,github:`github-copilot-color.svg`,"github-copilot":`copilot-color.svg`,"gitlab-duo":`gitlab-duo-color.svg`,google:`gemini-color.svg`,"google-antigravity":`antigravity-color.svg`,"google-vertex":`gemini-color.svg`,groq:`groq-color.svg`,huggingface:`huggingface-color.svg`,kimi:`kimi-color.svg`,"kimi-code":`kimi-color.svg`,kiro:`kiro-color.svg`,"lm-studio":`lm-studio-color.svg`,"meta-model":`meta.svg`,"meta-muse":`meta.svg`,mistral:`mistral-color.svg`,minimax:`minimax.svg`,"minimax-cn":`minimax.svg`,moonshot:`moonshot-color.svg`,nvidia:`nvidia-color.svg`,ollama:`ollama-color.svg`,"ollama-cloud":`ollama-color.svg`,openai:`openai.svg`,"openai-apikey":`openai.svg`,"opencode-free":`opencode.svg`,"opencode-go":`opencode.svg`,"opencode-zen":`opencode.svg`,openrouter:`openrouter-color.svg`,qianfan:`qianfan-color.svg`,alibaba:`alibaba-color.svg`,"alibaba-token-plan":`alibaba-color.svg`,"alibaba-token-plan-intl":`alibaba-color.svg`,baseten:`baseten.svg`,bizrouter:`bizrouter.svg`,cerebras:`cerebras.svg`,deepinfra:`deepinfra.svg`,digitalocean:`digitalocean.svg`,featherless:`featherless.svg`,hyperbolic:`hyperbolic.svg`,kilo:`kilo.svg`,nanogpt:`nanogpt.svg`,nebius:`nebius.svg`,neuralwatt:`neuralwatt.svg`,nous:`nous.svg`,novita:`novita.svg`,orcarouter:`orcarouter.svg`,parallel:`parallel.svg`,sambanova:`sambanova.svg`,scaleway:`scaleway.svg`,siliconflow:`siliconflow.svg`,synthetic:`synthetic.svg`,together:`together.svg`,umans:`umans.svg`,venice:`venice.svg`,vultr:`vultr.svg`,litellm:`litellm.svg`,zenmux:`zenmux.svg`,zai:`zai.svg`,"zhipu-bigmodel":`zai.svg`,"zhipu-bigmodel-coding":`zai.svg`,"qwen-cloud":`qwen-portal-color.svg`,"vercel-ai-gateway":`vercel-ai-gateway-color.svg`,vllm:`vllm-color.svg`,xai:`grok.svg`,"mimo-free":`xiaomi-color.svg`,mimo:`xiaomi-color.svg`,xiaomi:`xiaomi-color.svg`,"xiaomi-mimo":`xiaomi-color.svg`},Sn={anthropic:`Anthropic Claude`,"anthropic-apikey":`Anthropic Claude`,chatgpt:`ChatGPT`,openai:`OpenAI (Codex login)`,"openai-apikey":`OpenAI API`,"azure-openai":`Azure OpenAI`,"cloudflare-ai-gateway":`Cloudflare AI Gateway`,"cloudflare-workers-ai":`Cloudflare Workers AI`,cline:`Cline`,"cline-pass":`ClinePass`,nvidia:`NVIDIA NIM`,ollama:`Ollama`,"ollama-cloud":`Ollama Cloud`,xai:`xAI Grok`,"mimo-free":`MiMo Free`,xiaomi:`Xiaomi`,cursor:`Cursor`,deepseek:`DeepSeek`,github:`GitHub`,"github-copilot":`GitHub Copilot`,"gitlab-duo":`GitLab Duo`,openrouter:`OpenRouter`,"opencode-go":`OpenCode Go`,"opencode-free":`OpenCode Free`,"opencode-zen":`OpenCode Zen`,mistral:`Mistral`,groq:`Groq`,"meta-model":`Meta Model API`,"meta-muse":`Muse Code`,alibaba:`Alibaba Coding Plan`,"alibaba-token-plan":`Alibaba Token Plan`,"alibaba-token-plan-intl":`Alibaba Token Plan (Intl)`,kimi:`Kimi`,"kimi-code":`Kimi`,moonshot:`Moonshot`,google:`Google`,"google-vertex":`Google Vertex`,"lm-studio":`LM Studio`,huggingface:`Hugging Face`,"qwen-cloud":`Qwen Cloud`,siliconflow:`SiliconFlow`,"tencent-coding-plan":`Tencent Cloud Coding Plan`,"vercel-ai-gateway":`Vercel AI Gateway`,vllm:`vLLM`,litellm:`LiteLLM`},Cn={"command-code":`provider.name.commandCodeAuth`,commandcode:`provider.name.commandCodeApi`,volcengine:`provider.name.volcengine`,"volcengine-coding-plan":`provider.name.volcengineCodingPlan`,"volcengine-agent-plan":`provider.name.volcengineAgentPlan`},wn=new Set([...Object.keys(Sn),...Object.keys(Cn)]);function Tn(e){let t=e.toLowerCase();return Object.hasOwn(xn,t)?xn[t]:void 0}function En(e,t){let n=Tn(e);return n?`/provider-icons/${n}`:void 0}var Dn=new Set([`cerebras.svg`,`deepinfra.svg`,`neuralwatt.svg`,`nous.svg`,`novita.svg`,`siliconflow.svg`,`synthetic.svg`,`zenmux.svg`,`grok.svg`,`kimi-color.svg`,`ollama-color.svg`,`opencode.svg`,`vercel-ai-gateway-color.svg`]),On=new Set([`baseten.svg`,`kilo.svg`,`sambanova.svg`,`venice.svg`,`zai.svg`]),kn=new Set([`bizrouter.svg`,`featherless.svg`,`hyperbolic.svg`,`nebius.svg`,`parallel.svg`,`umans.svg`]);function An(e){if(!e)return`image`;let t=e.split(`/`).pop()??``;return Dn.has(t)?`mask`:On.has(t)?`plate`:kn.has(t)?`dark-plate`:`image`}function jn(e,t){let n=e.toLowerCase(),r=Object.hasOwn(Cn,n)?Cn[n]:void 0;return r?t(r):(Object.hasOwn(Sn,n)?Sn[n]:void 0)||(e===n&&/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(e)?e.split(`-`).map(e=>e&&e[0].toUpperCase()+e.slice(1)).join(` `):e)}function Mn(e){return wn.has(e.toLowerCase())}function Nn(e){return e===`command-code`?`commandcode-auth`:e===`commandcode`?`commandcode-api`:e}function Pn(e,t){let n=e.indexOf(`/`);if(n<=0)return e;let r=e.slice(0,n),i=e.slice(n+1);if(r===`command-code`||r===`commandcode`){let e=i.match(/^([a-z0-9]+)-([a-z0-9]+(?:-[a-z0-9]+)+)$/i);return e&&i.startsWith(`${e[1]}-${e[1]}-`)&&(i=i.slice(e[1].length+1)),`${r===`command-code`?`commandcode-auth`:`commandcode-api`}/${i}`}return e}function Fn({t:e,models:t,modelsLoading:n,modelQuery:r,setModelQuery:i,filteredGroups:a,expandedProviders:o,setExpandedProviders:s}){return(0,J.jsxs)(J.Fragment,{children:[(0,J.jsxs)(`div`,{className:`h-section`,children:[e(`dash.availableModels`),` `,(0,J.jsx)(`span`,{className:`count`,children:t.length}),n&&(0,J.jsx)(`span`,{className:`spin`,style:{marginLeft:4}})]}),t.length===0&&!n?(0,J.jsx)(Ot,{title:e(`dash.noModels`)}):(0,J.jsxs)(J.Fragment,{children:[(0,J.jsxs)(`div`,{className:`pws-search-wrap`,children:[(0,J.jsx)(ve,{className:`pws-search-icon`,width:14,height:14,"aria-hidden":`true`}),(0,J.jsx)(`input`,{type:`search`,className:`input pws-search-input`,placeholder:e(`models.search`),value:r,onChange:e=>i(e.target.value),"aria-label":e(`models.search`)})]}),a.length===0?(0,J.jsx)(`p`,{className:`muted text-control`,style:{margin:`4px 0`},children:e(`dash.modelsNoResults`)}):(0,J.jsx)(`div`,{className:`dash-model-acc`,children:a.map(([t,n])=>{let i=r.trim().toLowerCase()!==``||o.has(t);return(0,J.jsxs)(`div`,{className:`dash-model-group`,children:[(0,J.jsxs)(`button`,{type:`button`,className:`dash-model-head`,onClick:()=>s(e=>{let n=new Set(e);return n.has(t)?n.delete(t):n.add(t),n}),"aria-expanded":i,children:[(0,J.jsx)(Se,{width:12,height:12,style:{transform:i?`rotate(90deg)`:`none`,transition:`transform .12s`,color:`var(--muted)`},"aria-hidden":`true`}),(0,J.jsx)(`span`,{className:`font-semibold`,children:jn(t,e)}),(0,J.jsx)(`span`,{className:`count`,children:n.length})]}),i&&(0,J.jsx)(`div`,{className:`dash-model-chips`,children:n.map(e=>(0,J.jsx)(`code`,{className:`dash-model-chip`,children:e.id},`${e.provider}/${e.id}`))})]},t)})})]})]})}var In={ko:[{v:0x2386f26fc10000,s:`경`},{v:0xe8d4a51000,s:`조`},{v:1e8,s:`억`},{v:1e4,s:`만`}],zh:[{v:0x2386f26fc10000,s:`京`},{v:0xe8d4a51000,s:`兆`},{v:1e8,s:`亿`},{v:1e4,s:`万`}],"zh-TW":[{v:0x2386f26fc10000,s:`京`},{v:0xe8d4a51000,s:`兆`},{v:1e8,s:`億`},{v:1e4,s:`萬`}]};function Ln(e){return e.replace(/\.0+$/,``).replace(/(\.\d*?)0+$/,`$1`)}function Rn(e,t){let n=In[t];if(n){for(let t of n)if(e>=t.v)return`${Ln((e/t.v).toFixed(1))}${t.s}`;return String(e)}return e<1e4?String(e):e<1e6?`${Ln((e/1e3).toFixed(1))}K`:e<1e9?`${Ln((e/1e6).toFixed(1))}M`:e<0xe8d4a51000?`${Ln((e/1e9).toFixed(1))}B`:`${Ln((e/0xe8d4a51000).toFixed(1))}T`}function zn(e,t){let n=Math.max(0,Math.floor(e)),r=$e(t,`uptime.day`),i=$e(t,`uptime.hour`),a=$e(t,`uptime.minute`),o=$e(t,`uptime.second`);if(n<300)return`${n}${o}`;let s=Math.floor(n/60);if(s<60)return`${s}${a}`;let c=Math.floor(s/60);if(c<24){let e=s%60;return e>0?`${c}${i} ${e}${a}`:`${c}${i}`}let l=Math.floor(c/24),u=c%24;return u>0?`${l}${r} ${u}${i}`:`${l}${r}`}function Bn({locale:e,health:t,providers:n,usage30d:r,usageLoading:i,healthLoading:a,startupHealth:o,projectConfigWarnings:s,maMode:c,maBusy:l,maHelpTriggerRef:u,maHelpOpen:d,setMaHelpOpen:f,switchMaMode:p,maError:m}){let h=Q(),g=t?.status===`ok`;return(0,J.jsxs)(J.Fragment,{children:[(0,J.jsxs)(`div`,{className:`dash-overview-head`,children:[(0,J.jsxs)(`div`,{className:`stat-row`,children:[(0,J.jsxs)(`div`,{className:`stat`,children:[(0,J.jsxs)(`div`,{className:`label`,style:{display:`flex`,alignItems:`center`,gap:6},children:[h(`dash.multiAgent`),(0,J.jsx)(`button`,{ref:u,type:`button`,className:`btn btn-ghost btn-sm`,style:{width:24,height:24,minWidth:24,flex:`0 0 24px`,padding:0,borderRadius:`var(--radius-pill)`,color:`var(--muted)`},onClick:()=>f(!0),"aria-label":h(`dash.multiAgent`),"aria-haspopup":`dialog`,"aria-controls":`multi-agent-help-dialog`,"aria-expanded":d,children:(0,J.jsx)(Z,{width:14,height:14,"aria-hidden":`true`})})]}),(0,J.jsx)(`div`,{className:`value`,style:{display:`flex`,alignItems:`center`,justifyContent:`center`},children:(0,J.jsx)(`div`,{role:`radiogroup`,"aria-label":h(`dash.multiAgent`),style:{display:`inline-flex`,borderRadius:`var(--radius-pill)`,background:`var(--surface-soft, var(--raised))`,padding:3,gap:2},children:[`v1`,`default`,`v2`].map(e=>(0,J.jsx)(`button`,{type:`button`,role:`radio`,"aria-checked":c===e,className:`btn btn-sm text-caption${c===e?` btn-primary`:` btn-ghost`}`,style:{borderRadius:`var(--radius-pill)`,minWidth:36,padding:`5px 10px`,border:`none`,background:c===e?void 0:`transparent`,color:c===e?void 0:`var(--muted)`},disabled:l,onClick:()=>void p(e),children:h(`models.v2Mode_${e}`)},e))})}),m&&(0,J.jsx)(`div`,{role:`alert`,className:`text-caption`,style:{color:`var(--red)`,marginTop:4,textAlign:`center`,maxWidth:280,wordBreak:`break-word`},children:m})]}),(0,J.jsxs)(`div`,{className:`stat`,"aria-busy":a||void 0,children:[(0,J.jsx)(`div`,{className:`label`,children:h(`dash.status`)}),(0,J.jsxs)(`div`,{className:`value`,style:{display:`flex`,alignItems:`center`,gap:9,color:g?`var(--green)`:`var(--red)`},children:[(0,J.jsx)(`span`,{className:`dot ${g?`dot-green`:`dot-red`}`}),h(g?`dash.online`:`dash.offline`)]})]}),(0,J.jsxs)(`div`,{className:`stat`,"aria-busy":a||void 0,children:[(0,J.jsx)(`div`,{className:`label`,children:h(`dash.version`)}),(0,J.jsx)(`div`,{className:`value mono`,children:t?.version??`—`})]}),(0,J.jsxs)(`div`,{className:`stat`,"aria-busy":a||void 0,children:[(0,J.jsx)(`div`,{className:`label`,children:h(`dash.uptime`)}),(0,J.jsx)(`div`,{className:`value mono`,children:t?zn(t.uptime,e):`—`})]}),(0,J.jsxs)(`div`,{className:`stat`,"aria-busy":a||void 0,children:[(0,J.jsx)(`div`,{className:`label`,children:h(`dash.providers`)}),(0,J.jsx)(`div`,{className:`value`,children:n.length})]}),(0,J.jsxs)(`div`,{className:`stat`,"aria-busy":i||void 0,children:[(0,J.jsx)(`div`,{className:`label`,children:h(`dash.tokens30d`)}),(0,J.jsx)(`div`,{className:`value mono`,children:r&&r.summary.requests>0?Rn(r.summary.totalTokens,e):`—`}),(0,J.jsx)(`div`,{className:`muted text-label dash-stat-coverage`,children:r&&r.summary.requests>0?h(`dash.coverage`).replace(`{pct}`,`${Math.round(r.summary.coverageRatio*100)}%`):`\xA0`})]})]}),(0,J.jsx)(`div`,{className:`startup-health-slot`,"aria-live":`polite`,children:o?(0,J.jsxs)(`button`,{type:`button`,className:`startup-health-bar`,onClick:()=>pt(`startup`),children:[(0,J.jsx)(`span`,{className:`dot ${o===`error`?`dot-red`:o===`at-risk`?`dot-amber`:`dot-green`}`,"aria-hidden":`true`}),(0,J.jsx)(`span`,{className:`startup-health-bar__summary`,children:h(o===`error`?`startup.error`:o===`at-risk`?`startup.summary.atRisk`:o===`protected`?`startup.summary.protected`:`startup.summary.native`)})]}):(0,J.jsxs)(`div`,{className:`startup-health-bar startup-health-bar--pending`,"aria-hidden":`true`,children:[(0,J.jsx)(`span`,{className:`dot dot-amber`}),(0,J.jsx)(`span`,{className:`startup-health-bar__summary`,children:`\xA0`})]})})]}),s.length>0&&(0,J.jsxs)(`div`,{className:`notice notice-err maintenance-notice`,role:`alert`,children:[(0,J.jsx)(_e,{}),(0,J.jsxs)(`div`,{children:[(0,J.jsx)(`div`,{className:`font-semibold`,children:h(`dash.projectConfigTitle`)}),(0,J.jsx)(`div`,{className:`muted text-control`,style:{marginTop:4},children:h(`dash.projectConfigHint`)}),(0,J.jsx)(`ul`,{className:`text-control`,style:{margin:`10px 0 0`,paddingLeft:18},children:s.map(e=>(0,J.jsxs)(`li`,{style:{marginBottom:8},children:[(0,J.jsx)(`code`,{children:e.path}),` — `,e.issues.join(`, `),(0,J.jsx)(`div`,{className:`muted`,style:{marginTop:2},children:e.bypass})]},e.path))})]})]})]})}function Vn(e){let t=new AbortController;if(typeof AbortSignal<`u`&&typeof AbortSignal.any==`function`&&typeof AbortSignal.timeout==`function`)return{controller:t,signal:AbortSignal.any([t.signal,AbortSignal.timeout(e)]),clear:()=>void 0};let n=setTimeout(()=>t.abort(),e);return{controller:t,signal:t.signal,clear:()=>clearTimeout(n)}}function Hn(){return typeof document<`u`&&document.visibilityState===`hidden`}function Un(e,t){return typeof window<`u`&&typeof window.setInterval==`function`?window.setInterval(e,t):setInterval(e,t)}function Wn(e){if(typeof window<`u`&&typeof window.clearInterval==`function`){window.clearInterval(e);return}clearInterval(e)}function Gn(e,t,n){let r=n?.pauseWhenHidden!==!1,i=null,a=!1,o=()=>{try{e()}catch(e){console.error(`[visibility-poll]`,e)}},s=()=>{i!==null||a||(i=Un(o,t))},c=()=>{i!==null&&(Wn(i),i=null)},l=()=>{if(r){if(Hn()){c();return}o(),s()}};return r&&Hn()||s(),r&&typeof document<`u`&&document.addEventListener(`visibilitychange`,l),n?.immediate&&o(),()=>{a=!0,c(),r&&typeof document<`u`&&document.removeEventListener(`visibilitychange`,l)}}var Kn=new Map;function qn(e,t){let n=`${e}:${t}`,r=Kn.get(n);return r||(r=new Intl.NumberFormat(e,{minimumFractionDigits:t,maximumFractionDigits:t}),Kn.set(n,r)),r}var Jn=new Map;function Yn(e){let t=Jn.get(e);return t||(t=new Intl.NumberFormat(e),Jn.set(e,t)),t}function Xn(e,t){if(!Number.isFinite(e)||e<=0)return`0 B`;let n=[`B`,`KiB`,`MiB`,`GiB`,`TiB`],r=Math.min(Math.floor(Math.log(e)/Math.log(1024)),n.length-1),i=e/1024**r;return`${qn(t,r===0?0:1).format(i)} ${n[r]}`}function Zn(e,t){return!Number.isFinite(e)||e<0?`—`:zn(e/1e3,t)}function Qn(e){return typeof e.observedBytes==`number`?e.observedBytes:Math.max(e.rss,e.external??0,e.arrayBuffers??0)}function $n(e){if(e.observedMetric)return e.observedMetric;if(e.watchdog?.observedMetric)return e.watchdog.observedMetric;let t=[{metric:`rss`,bytes:e.rss},{metric:`external`,bytes:e.external??0},{metric:`arrayBuffers`,bytes:e.arrayBuffers??0}];return t.reduce((e,t)=>t.bytes>e.bytes?t:e,t[0]).metric}function er(e){if(e.length<2)return null;let t=e[0],n=e[e.length-1],r=n.at-t.at;return r<=0?null:(Qn(n)-Qn(t))/r*36e5}function tr({label:e,value:t,sub:n,tone:r}){return(0,J.jsxs)(`div`,{className:`stat`,children:[(0,J.jsx)(`div`,{className:`label`,children:e}),(0,J.jsx)(`div`,{className:`value mono${r?` value--${r}`:``}`,children:t}),n&&(0,J.jsx)(`div`,{className:`stat-sub mono`,children:n})]})}function nr({observedBytes:e,thresholdBytes:t,metric:n,locale:r,t:i}){let a=e!==null&&t!==null&&t>0?e/t:null,o=a===null?`unknown`:a>=1?`over`:a>=.75?`warn`:`ok`,s=a===null?null:Math.round(a*100);return(0,J.jsxs)(`div`,{className:`mem-pressure mem-pressure--${o}`,children:[(0,J.jsxs)(`div`,{className:`mem-pressure-head`,children:[(0,J.jsxs)(`span`,{className:`mem-pressure-label`,children:[i(`dash.mem.pressure`),n?(0,J.jsx)(`span`,{className:`mem-pressure-metric mono`,children:n}):null]}),(0,J.jsxs)(`span`,{className:`mem-pressure-figure mono`,children:[e===null?`—`:Xn(e,r),t!==null&&(0,J.jsxs)(`span`,{className:`mem-pressure-limit`,children:[` / `,Xn(t,r)]})]})]}),(0,J.jsx)(`div`,{className:`mem-pressure-track`,role:`presentation`,children:(0,J.jsx)(`span`,{className:`mem-pressure-fill`,style:{"--mem-scale":String(a===null?0:Math.min(1,Math.max(.01,a)))}})}),(0,J.jsx)(`div`,{className:`mem-pressure-foot`,children:s===null?i(`dash.mem.pressureUnknown`):i(`dash.mem.pressureOf`,{pct:s})})]})}var rr=60,ir=1500,ar=12e4;function or({apiBase:e}){let{locale:t,t:n}=ct(),[r,i]=(0,_.useState)(null),[a,o]=(0,_.useState)(!1),[s,c]=(0,_.useState)(`idle`),[l,u]=(0,_.useState)(null),[d,f]=(0,_.useState)(!1),[p,m]=(0,_.useState)(!1),[h,g]=(0,_.useState)(null);(0,_.useEffect)(()=>{let t=!1;return(async()=>{try{let n=await fetch(`${e}/api/startup-health`);if(!n.ok||t)return;let r=await n.json();t||f(r.protection===`none`)}catch{}})(),()=>{t=!0}},[e]),(0,_.useEffect)(()=>{let t=!1,n=!1,r=null,a=async()=>{if(n)return;n=!0;let a=Vn(1e4);r=a;try{let n=await fetch(`${e}/api/system/memory`,{signal:a.signal});if(!n.ok)throw Error(`memory unavailable`);let r=await n.json();if(t)return;i(r),o(!1),m(typeof r.activeTurnCount==`number`),r.isDraining&&s===`idle`&&c(`draining`),(s===`draining`||s===`reconnecting`)&&h!=null&&typeof r.pid==`number`&&r.pid!==h&&!r.isDraining&&(c(`idle`),g(null),u(null))}catch{if(t)return;s===`draining`||s===`reconnecting`?c(`reconnecting`):o(!0)}finally{a.clear(),r===a&&(r=null),n=!1}};a();let l=Gn(()=>void a(),5e3);return()=>{t=!0,r?.controller.abort(),r?.clear(),l()}},[e,s,h]),(0,_.useEffect)(()=>{if(s!==`reconnecting`)return;let t=!1,r=!1,i=null,a=Date.now(),o=()=>{if(r||t)return;r=!0;let o=Vn(5e3);i=o,fetch(`${e}/api/system/health`,{cache:`no-store`,signal:o.signal}).then(async e=>{if(t)return;if(!e.ok){Date.now()-a>=ar&&(c(`error`),u(n(`dash.mem.restartFailed`)));return}let r=h==null;if(h!=null)try{let t=await e.json();r=typeof t.pid==`number`&&t.pid!==h}catch{r=!0}if(!t){if(r){c(`idle`),g(null),u(null);return}Date.now()-a>=ar&&(c(`error`),u(n(`dash.mem.restartFailed`)))}}).catch(()=>{t||Date.now()-a>=ar&&(c(`error`),u(n(`dash.mem.restartFailed`)))}).finally(()=>{o.clear(),i===o&&(i=null),r=!1})};o();let l=setInterval(o,ir);return()=>{t=!0,i?.controller.abort(),i?.clear(),clearInterval(l)}},[e,s,h,n]);let v=()=>{let t=r?.activeTurnCount??0,i=[n(`dash.mem.restartConfirm`,{count:t,seconds:rr})];d&&i.push(n(`dash.mem.restartNoSupervisor`)),window.confirm(i.join(` - -`))&&(async()=>{u(null),g(typeof r?.pid==`number`?r.pid:null),c(`draining`);try{if(!(await fetch(`${e}/api/system/restart`,{method:`POST`})).ok)throw Error(`restart_failed`)}catch{c(`error`),g(null),u(n(`dash.mem.restartFailed`))}})()};if(a&&!r&&s===`idle`)return(0,J.jsxs)(`div`,{className:`panel`,style:{marginBottom:24},children:[(0,J.jsxs)(`div`,{className:`font-semibold`,style:{display:`flex`,alignItems:`center`,gap:8},children:[(0,J.jsx)(ce,{width:16,height:16,"aria-hidden":`true`}),n(`dash.mem.title`)]}),(0,J.jsx)(`div`,{className:`muted text-control`,style:{marginTop:8},children:n(`dash.mem.unavailable`)})]});let y=r?.watchdog?er(r.watchdog.samples):null,b=r?r.observedBytes??r.watchdog?.observedBytes??Qn(r):null,x=r?$n(r):null,S=(()=>{let e=r?.watchdog?.warnThresholdBytes;if(y===null||y<=0||b===null||!e)return;let t=e-b;if(t<=0)return`danger`;let n=t/y;if(n<=1)return`danger`;if(n<=8)return`warn`})(),C=r?.responseState,w=r?.activeTurnCount,T=s===`draining`||s===`reconnecting`;return(0,J.jsxs)(`div`,{className:`panel`,style:{marginBottom:24},children:[(0,J.jsxs)(`div`,{className:`mem-head`,children:[(0,J.jsxs)(`div`,{className:`font-semibold mem-head-title`,children:[(0,J.jsx)(ce,{width:16,height:16,"aria-hidden":`true`}),n(`dash.mem.title`)]}),p&&(0,J.jsxs)(`div`,{className:`mem-head-actions`,children:[(0,J.jsxs)(`span`,{className:`mem-inflight`,children:[(0,J.jsx)(`span`,{className:`mem-inflight-label`,children:n(`dash.mem.inFlight`)}),(0,J.jsx)(`span`,{className:`mem-inflight-value mono`,children:typeof w==`number`?Yn(t).format(w):`—`})]}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,disabled:T,onClick:v,children:n(`dash.mem.restart`)})]})]}),(0,J.jsx)(nr,{observedBytes:b,thresholdBytes:r?.watchdog?.warnThresholdBytes??null,metric:x,locale:t,t:n}),(0,J.jsxs)(`div`,{className:`stat-row mem-stats`,children:[(0,J.jsx)(tr,{label:n(`dash.mem.rss`),value:r?Xn(r.rss,t):`—`}),(0,J.jsx)(tr,{label:n(`dash.mem.jsHeap`),value:r?Xn(r.heapUsed,t):`—`,sub:r?n(`dash.mem.jsHeapArena`,{total:Xn(r.heapTotal,t)}):void 0}),(0,J.jsx)(tr,{label:n(`dash.mem.jscHeap`),value:r?.jscHeap?Xn(r.jscHeap.heapSize,t):`—`}),(0,J.jsx)(tr,{label:n(`dash.mem.growth`),value:y===null?`—`:`${y>=0?`+`:`−`}${Xn(Math.abs(y),t)}${n(`dash.mem.perHour`)}`,tone:S})]}),(0,J.jsxs)(`details`,{style:{marginTop:10},children:[(0,J.jsx)(`summary`,{className:`muted text-label`,style:{cursor:`pointer`,padding:`2px 2px`},children:n(`dash.mem.details`)}),(0,J.jsx)(`div`,{className:`muted text-control`,style:{margin:`8px 0 0`},children:n(`dash.mem.hint`)}),(0,J.jsx)(`div`,{className:`muted text-label`,style:{margin:`14px 0 6px`},children:n(`dash.mem.runtime`)}),(0,J.jsxs)(`div`,{className:`stat-row`,children:[(0,J.jsx)(tr,{label:n(`dash.mem.observed`),value:b===null?`—`:`${Xn(b,t)} (${x})`}),(0,J.jsx)(tr,{label:n(`dash.mem.external`),value:r?.external===void 0?`—`:Xn(r.external,t)}),(0,J.jsx)(tr,{label:n(`dash.mem.arrayBuffers`),value:r?.arrayBuffers===void 0?`—`:Xn(r.arrayBuffers,t)})]}),(0,J.jsx)(`div`,{className:`muted text-label`,style:{margin:`14px 0 6px`},children:n(`dash.mem.store`)}),(0,J.jsx)(`div`,{className:`muted text-control`,style:{marginBottom:10},children:n(`dash.mem.storeHint`)}),(0,J.jsxs)(`div`,{className:`stat-row`,children:[(0,J.jsx)(tr,{label:n(`dash.mem.storeEntries`),value:C?Yn(t).format(C.count):`—`}),(0,J.jsx)(tr,{label:n(`dash.mem.storeTotal`),value:C?Xn(C.totalBytes,t):`—`}),(0,J.jsx)(tr,{label:n(`dash.mem.storeLargest`),value:C?Xn(C.largestBytes,t):`—`}),(0,J.jsx)(tr,{label:n(`dash.mem.storeOldest`),value:C?C.count===0?`—`:Zn(C.oldestAgeMs,t):`—`})]}),r?.watchdog&&(0,J.jsxs)(`div`,{className:`stat-row`,style:{marginTop:16},children:[(0,J.jsx)(tr,{label:n(`dash.mem.threshold`),value:Xn(r.watchdog.warnThresholdBytes,t)}),(0,J.jsx)(tr,{label:n(`dash.mem.lastWarn`),value:r.watchdog.lastWarnAt?new Date(r.watchdog.lastWarnAt).toLocaleString(t):n(`dash.mem.never`)})]})]}),p&&(0,J.jsxs)(`div`,{className:`mem-status`,"aria-live":`polite`,children:[s===`draining`&&(0,J.jsx)(`span`,{className:`muted text-control`,children:n(`dash.mem.draining`,{count:typeof w==`number`?w:0})}),s===`reconnecting`&&(0,J.jsx)(`span`,{className:`muted text-control`,children:n(`dash.mem.reconnecting`)}),s===`error`&&l&&(0,J.jsx)(`span`,{className:`text-control`,style:{color:`var(--danger, #c44)`},children:l}),d&&s===`idle`&&(0,J.jsx)(`span`,{className:`muted text-control`,children:n(`dash.mem.restartNoSupervisor`)})]})]})}function sr({apiBase:e,d:t}){let{t:n,maMode:r,maModeResolved:i,effortCapHelpTriggerRef:a,effortCapHelpOpen:o,setEffortCapHelpOpen:s,effortCap:c,subagentEffortCap:l,effortCapSaving:u,setEffortCap:d,setSubagentEffortCap:f,setEffortCapSaving:p}=t;return!i||r===`v1`?null:(0,J.jsx)(`div`,{className:`panel`,children:(0,J.jsxs)(`div`,{className:`injection-head`,children:[(0,J.jsxs)(`span`,{className:`injection-label`,style:{display:`inline-flex`,alignItems:`center`,gap:6},children:[n(`dash.effortCapLabel`),(0,J.jsx)(`button`,{ref:a,type:`button`,className:`btn btn-ghost btn-sm`,style:{width:22,height:22,minWidth:22,padding:0,borderRadius:`var(--radius-pill)`,color:`var(--muted)`},onClick:()=>s(e=>!e),"aria-label":n(`dash.effortCapLabel`),"aria-expanded":o,"aria-haspopup":`dialog`,"aria-controls":`effort-cap-help-dialog`,children:(0,J.jsx)(Z,{width:13,height:13,"aria-hidden":`true`})})]}),(0,J.jsx)(Dt,{value:c,options:[{value:``,label:n(`dash.effortCapNone`)},...Gt.map(e=>({value:e,label:e}))],onChange:async t=>{if(!u){p(!0);try{let n=await Wt(await fetch(`${e}/api/effort-caps`,{method:`PUT`,headers:{"Content-Type":`application/json`},body:JSON.stringify({effortCap:t||null})}));d(n.effortCap??``),f(n.subagentEffortCap??``)}catch{}finally{p(!1)}}},disabled:u,label:n(`dash.effortCapLabel`),align:`right`}),(0,J.jsx)(Dt,{value:l,options:[{value:``,label:n(`dash.effortCapNone`)},...Gt.map(e=>({value:e,label:e}))],onChange:async t=>{if(!u){p(!0);try{let n=await Wt(await fetch(`${e}/api/effort-caps`,{method:`PUT`,headers:{"Content-Type":`application/json`},body:JSON.stringify({subagentEffortCap:t||null})}));d(n.effortCap??``),f(n.subagentEffortCap??``)}catch{}finally{p(!1)}}},disabled:u,label:n(`dash.subagentEffortCapLabel`),align:`right`})]})})}function cr({d:e}){let{t,injectionModel:n,injectionEffort:r,injectionEfforts:i,injectionAvailable:a,injectionSaving:o,saveInjection:s}=e;return(0,J.jsxs)(`div`,{className:`panel dash-delegation-summary`,children:[(0,J.jsx)(`div`,{className:`font-semibold`,children:t(`dash.injectionLabel`)}),(0,J.jsxs)(`div`,{className:`dash-delegation-controls`,children:[(0,J.jsx)(Dt,{value:n,options:[{value:``,label:t(`dash.injectionNone`)},...a.map(e=>({value:e.namespaced,label:Pn(`${e.provider}/${e.model}`,t)}))],onChange:e=>{s({model:e||null,effort:r||null})},disabled:o,label:t(`dash.injectionLabel`),align:`right`}),n&&i.length>0&&(0,J.jsx)(Dt,{value:r,options:[{value:``,label:t(`dash.injectionEffortNone`)},...i.map(e=>({value:e,label:e}))],onChange:e=>{s({model:n||null,effort:e||null})},disabled:o,label:t(`dash.injectionEffortLabel`),align:`right`}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:()=>pt(`#subagents`),children:t(`dash.injectionManage`)})]})]})}function lr({d:e}){let{t,runSync:n,syncing:r,updateTriggerRef:i,openUpdateDialog:a,updateLoading:o,updateOpen:s,syncResult:c,syncError:l,updateJob:u,reconnecting:d,clearSyncFeedback:f}=e,p=!!c&&(!!c.warning||!!c.nativeSubagentDefaultsWarning||!!c.staleAppServerHint),[m,h]=(0,_.useState)(!1),g=(0,_.useRef)(null);(0,_.useEffect)(()=>(g.current&&=(clearTimeout(g.current),null),(c||l)&&!p&&(g.current=setTimeout(()=>{g.current=null,h(!0),f()},l?8e3:6e3)),()=>{g.current&&clearTimeout(g.current)}),[c,l,p,f]);let v=()=>{h(!1),n()},y=()=>{h(!0),f()};return(0,J.jsxs)(J.Fragment,{children:[(0,J.jsxs)(`div`,{className:`panel maintenance-panel`,children:[(0,J.jsxs)(`div`,{className:`dash-sync-summary`,children:[(0,J.jsxs)(`div`,{className:`dash-sync-copy`,children:[(0,J.jsx)(`div`,{className:`font-semibold`,children:t(`dash.syncModels`)}),(0,J.jsx)(`div`,{className:`muted text-control dash-sync-hint`,children:t(`dash.syncModelsHint`)})]}),(0,J.jsxs)(`div`,{className:`maintenance-actions`,children:[(0,J.jsxs)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:v,disabled:r,children:[(0,J.jsx)(pe,{className:r?`spin-icon`:void 0}),` `,t(r?`dash.syncing`:`dash.syncRun`)]}),(0,J.jsx)(`button`,{ref:i,type:`button`,className:`maintenance-update-anchor`,onClick:a,disabled:o,"aria-haspopup":`dialog`,"aria-controls":`dashboard-update-dialog`,"aria-expanded":s,"aria-label":t(`dash.checkUpdate`),tabIndex:-1})]})]}),u&&(0,J.jsxs)(`div`,{className:`notice ${u.status===`failed`?`notice-err`:`notice-ok`} maintenance-notice`,role:`status`,children:[u.status===`failed`?(0,J.jsx)(_e,{}):(0,J.jsx)(pe,{}),(0,J.jsxs)(`span`,{children:[Jt(u.status,t),u.latestVersion?` ${t(`dash.updateVersionTransition`,{currentVersion:u.currentVersion,latestVersion:u.latestVersion})}`:``,d?` ${t(`dash.updateReconnecting`)}`:``,u.error?` ${u.error}`:``]})]})]}),!m&&c&&(0,J.jsxs)(`div`,{className:`action-toast notice ${p?`notice-warn`:`notice-ok`}`,role:`status`,"aria-live":`polite`,children:[p?(0,J.jsx)(_e,{}):(0,J.jsx)(ue,{}),(0,J.jsxs)(`span`,{children:[t(`dash.syncOk`,{count:c.added}),c.warning?` ${c.warning}`:``,c.nativeSubagentDefaultsWarning?` ${c.nativeSubagentDefaultsWarning}`:``,c.staleAppServerHint?(0,J.jsxs)(J.Fragment,{children:[` `,(0,J.jsx)(ut,{k:`dash.syncStaleHint`,cmd:`ocx sync --restart-codex`})]}):null]}),(0,J.jsx)(`button`,{type:`button`,className:`action-toast-dismiss`,onClick:y,"aria-label":t(`api.dismiss`),children:(0,J.jsx)(de,{width:13,height:13,"aria-hidden":`true`})})]}),!m&&l&&(0,J.jsxs)(`div`,{className:`action-toast notice notice-err`,role:`status`,"aria-live":`polite`,children:[(0,J.jsx)(_e,{}),(0,J.jsx)(`span`,{children:t(`dash.syncFailed`,{error:l})}),(0,J.jsx)(`button`,{type:`button`,className:`action-toast-dismiss`,onClick:y,"aria-label":t(`api.dismiss`),children:(0,J.jsx)(de,{width:13,height:13,"aria-hidden":`true`})})]})]})}function ur({t:e,open:t,triggerRef:n,onClose:r,maxValue:i,maxInvalid:a,timeoutValue:o,timeoutInvalid:s,disabled:c,setMaxDraft:l,setMaxInvalid:u,setTimeoutDraft:d,setTimeoutInvalid:f,commitMaxDescriptions:p,commitTimeout:m}){let h=(0,_.useRef)(null),g=(0,_.useRef)(null),[v,y]=(0,_.useState)(),b=(0,_.useCallback)(()=>{n.current&&y(wt(n.current.getBoundingClientRect(),{align:`right`,placement:`below`,menuHeight:h.current?.offsetHeight??180}))},[n]);return(0,_.useLayoutEffect)(()=>{if(!t)return;b();let e=()=>b();return window.addEventListener(`resize`,e),window.addEventListener(`scroll`,e,!0),()=>{window.removeEventListener(`resize`,e),window.removeEventListener(`scroll`,e,!0)}},[t,b,a,s]),(0,_.useEffect)(()=>{if(!t)return;let e=e=>{let t=e.target;if(h.current?.contains(t)||n.current?.contains(t))return;let i=document.activeElement;i&&h.current?.contains(i)&&i.blur(),r()};return document.addEventListener(`mousedown`,e),()=>document.removeEventListener(`mousedown`,e)},[t,r,n]),(0,_.useEffect)(()=>{if(!t)return;let e=e=>{e.key===`Escape`&&(e.preventDefault(),r(),n.current?.focus())};return document.addEventListener(`keydown`,e),()=>document.removeEventListener(`keydown`,e)},[t,r,n]),(0,_.useEffect)(()=>{t&&g.current?.focus()},[t]),t?(0,J.jsxs)(`div`,{ref:h,id:`dash-vision-advanced-popover`,className:`dash-vision-advanced-popover`,role:`dialog`,"aria-modal":`false`,"aria-label":e(`dash.visionAdvancedPopover`),style:{...v,zIndex:60},children:[(0,J.jsx)(`div`,{className:`dash-vision-advanced-popover-title`,children:e(`dash.visionAdvancedPopover`)}),(0,J.jsxs)(`label`,{className:`dash-vision-number`,children:[(0,J.jsx)(`span`,{className:`muted setting-hint`,id:`dash-vision-max-label`,children:e(`dash.visionMaxDescriptions`)}),(0,J.jsx)(`span`,{className:`codex-auto-switch-input-wrap`,children:(0,J.jsx)(`input`,{ref:g,className:`input mono codex-auto-switch-input`,type:`number`,min:1,step:1,inputMode:`numeric`,value:i,disabled:c,"aria-invalid":a||void 0,"aria-label":e(`dash.visionMaxDescriptions`),"aria-describedby":a?`dash-vision-max-error dash-vision-max-label`:`dash-vision-max-label`,onChange:e=>{u(!1),l(e.target.value)},onBlur:e=>p(e.currentTarget.value),onKeyDown:e=>{e.nativeEvent.isComposing||c||(e.key===`Enter`?(e.preventDefault(),p(e.currentTarget.value)):e.key===`Escape`&&(e.preventDefault(),l(null),u(!1)))}})}),a&&(0,J.jsx)(`span`,{id:`dash-vision-max-error`,className:`muted setting-hint`,role:`alert`,children:e(`dash.visionMaxDescriptionsInvalid`)})]}),(0,J.jsxs)(`label`,{className:`dash-vision-number`,children:[(0,J.jsx)(`span`,{className:`muted setting-hint`,id:`dash-vision-timeout-label`,children:e(`dash.visionTimeout`)}),(0,J.jsxs)(`span`,{className:`codex-auto-switch-input-wrap`,children:[(0,J.jsx)(`input`,{className:`input mono codex-auto-switch-input`,type:`number`,min:nn,max:tn,step:1e3,inputMode:`numeric`,value:o,disabled:c,"aria-invalid":s||void 0,"aria-label":e(`dash.visionTimeout`),"aria-describedby":s?`dash-vision-timeout-error dash-vision-timeout-label`:`dash-vision-timeout-label`,onChange:e=>{f(!1),d(e.target.value)},onBlur:e=>m(e.currentTarget.value),onKeyDown:e=>{e.nativeEvent.isComposing||c||(e.key===`Enter`?(e.preventDefault(),m(e.currentTarget.value)):e.key===`Escape`&&(e.preventDefault(),d(null),f(!1)))}}),(0,J.jsx)(`span`,{className:`codex-auto-switch-unit`,"aria-hidden":`true`,children:`ms`})]}),s&&(0,J.jsx)(`span`,{id:`dash-vision-timeout-error`,className:`muted setting-hint`,role:`alert`,children:e(`dash.visionTimeoutInvalid`,{min:nn,max:tn})})]})]}):null}function dr({d:e}){let{t,settings:n,settingsSaving:r,toggleCodexAutoStart:i,sidecar:a,sidecarSaving:o,sidecarModels:s,visionModels:c,models:l,saveSidecar:u,shadowCall:d,shadowCallSaving:f,shadowCallHelpTriggerRef:p,shadowCallHelpOpen:m,setShadowCallHelpOpen:h,saveShadowCall:g}=e,v=a?.vision.enabled!==!1,y=v?a?.vision.model??`gpt-5.4-mini`:``,b=a?.vision.reasoning??`low`,x=sn(l,y),S=ln(x,b),C=String(a?.vision.maxDescriptionsPerTurn??8),w=String(a?.vision.timeoutMs??en),[T,E]=(0,_.useState)(null),[D,O]=(0,_.useState)(null),[k,A]=(0,_.useState)(!1),[j,M]=(0,_.useState)(!1),[N,P]=(0,_.useState)(!1),F=(0,_.useRef)(null),I=T??C,L=D??w,R=(e=I)=>{let t=rn(e);if(t===void 0){E(e),A(!0);return}A(!1),E(null),t!==(a?.vision.maxDescriptionsPerTurn??8)&&u(Qt(t))},z=(e=L)=>{let t=an(e);if(t===void 0){O(e),M(!0);return}M(!1),O(null),t!==(a?.vision.timeoutMs??en)&&u($t(t))};return(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)(`div`,{className:`panel`,children:(0,J.jsxs)(`div`,{className:`spread`,children:[(0,J.jsxs)(`div`,{style:{flex:1,minWidth:0},children:[(0,J.jsx)(`div`,{className:`font-semibold`,children:t(`dash.codexAutoStart`)}),(0,J.jsx)(`div`,{className:`muted setting-hint`,children:t(`dash.codexAutoStartHint`)})]}),(0,J.jsx)(`button`,{type:`button`,className:`switch ${n?.codexAutoStart??!0?`on`:``}`,onClick:i,disabled:!n||r,"aria-label":t(`dash.codexAutoStart`),"aria-pressed":n?.codexAutoStart??!0,children:(0,J.jsx)(`span`,{className:`knob`})})]})}),(0,J.jsxs)(`div`,{className:`dash-sidecar-grid`,children:[(0,J.jsxs)(`div`,{className:`panel dash-delegation-summary dash-sidecar-row-card`,"aria-busy":!a||void 0,children:[(0,J.jsxs)(`div`,{className:`dash-sidecar-copy`,children:[(0,J.jsx)(`div`,{className:`font-semibold`,children:t(`dash.webSearchSidecar`)}),(0,J.jsx)(`div`,{className:`muted setting-hint`,children:t(`dash.webSearchSidecarHint`)})]}),(0,J.jsxs)(`div`,{className:`dash-delegation-controls`,children:[(0,J.jsx)(`div`,{className:`dash-sidecar-select-row`,children:(0,J.jsx)(Dt,{value:a?.webSearch.model??`gpt-5.6-luna`,options:s,onChange:e=>{u({webSearch:hn(l,s,e)})},disabled:!a||o,label:t(`dash.sidecarModel`),align:`right`})}),(0,J.jsxs)(`div`,{className:`dash-sidecar-trailing-row`,title:t(`dash.webSearchStreamHint`),children:[(0,J.jsx)(`span`,{className:`muted setting-hint dash-sidecar-toggle-label`,children:t(`dash.webSearchStream`)}),(0,J.jsx)(`button`,{type:`button`,className:`switch ${a?.webSearch.streamRoutedModelOutput?`on`:``}`,onClick:()=>{u({webSearch:{streamRoutedModelOutput:!a?.webSearch.streamRoutedModelOutput}})},disabled:!a||o,"aria-label":t(`dash.webSearchStream`),"aria-pressed":a?.webSearch.streamRoutedModelOutput===!0,children:(0,J.jsx)(`span`,{className:`knob`})})]})]})]}),(0,J.jsxs)(`div`,{className:`panel dash-delegation-summary dash-sidecar-row-card dash-vision-sidecar-card`,"aria-busy":!a||void 0,children:[(0,J.jsxs)(`div`,{className:`dash-sidecar-copy`,children:[(0,J.jsx)(`div`,{className:`font-semibold`,children:t(`dash.visionSidecar`)}),(0,J.jsx)(`div`,{className:`muted setting-hint`,children:t(`dash.visionSidecarHint`)})]}),(0,J.jsxs)(`div`,{className:`dash-delegation-controls`,children:[(0,J.jsxs)(`div`,{className:`dash-sidecar-select-row`,children:[(0,J.jsx)(Dt,{value:y,options:[{value:``,label:t(`dash.visionOff`)},...c],onChange:e=>{if(e===``){u(Zt(!1));return}let t=ln(sn(l,e),S),n={vision:{model:e,backend:gn(l,c,e),reasoning:t}};v||(n.vision={...n.vision,enabled:!0}),u(n)},disabled:!a||o,label:t(`dash.sidecarModel`)}),(0,J.jsx)(Dt,{value:S,options:cn(x,S).map(e=>({value:e,label:e})),onChange:e=>{u(Xt(e))},disabled:!v||!a||o,align:`right`,label:`${t(`dash.visionSidecar`)} — ${t(`dash.injectionEffortLabel`)}`})]}),(0,J.jsx)(`div`,{className:`dash-sidecar-trailing-row`,children:(0,J.jsxs)(`button`,{type:`button`,ref:F,className:`dash-vision-advanced-trigger`,onClick:()=>P(e=>!e),disabled:!v||!a||o,"aria-expanded":N,"aria-haspopup":`dialog`,"aria-controls":`dash-vision-advanced-popover`,children:[(0,J.jsx)(`span`,{children:t(`dash.visionAdvanced`)}),(0,J.jsx)(Se,{width:12,height:12,"aria-hidden":`true`,style:{transform:N?`rotate(90deg)`:`none`,transition:`transform .12s`}})]})})]}),(0,mt.createPortal)((0,J.jsx)(ur,{t,open:N,triggerRef:F,onClose:()=>P(!1),maxValue:I,maxInvalid:k,timeoutValue:L,timeoutInvalid:j,disabled:!v||!a||o,setMaxDraft:E,setMaxInvalid:A,setTimeoutDraft:O,setTimeoutInvalid:M,commitMaxDescriptions:R,commitTimeout:z}),document.body)]})]}),(0,J.jsx)(`div`,{className:`panel`,"aria-busy":!d||void 0,children:(0,J.jsxs)(`div`,{className:`spread`,style:{alignItems:`center`},children:[(0,J.jsxs)(`div`,{style:{display:`flex`,alignItems:`center`,gap:8},children:[(0,J.jsx)(`span`,{className:`font-semibold`,children:t(`dash.shadowCallIntercept`)}),(0,J.jsx)(`button`,{ref:p,type:`button`,className:`btn btn-ghost btn-sm`,style:{width:22,height:22,minWidth:22,padding:0,borderRadius:`var(--radius-pill)`,color:`var(--muted)`},onClick:()=>h(e=>!e),"aria-label":t(`dash.shadowCallIntercept`),"aria-expanded":m,"aria-haspopup":`dialog`,"aria-controls":`shadow-call-help-dialog`,children:(0,J.jsx)(Z,{width:13,height:13,"aria-hidden":`true`})}),(0,J.jsx)(`code`,{className:`muted text-caption`,children:`⚠ ${zt(d?.sourceModels)}`})]}),(0,J.jsxs)(`div`,{className:`setting-controls`,style:{display:`flex`,gap:8,alignItems:`center`},children:[(0,J.jsx)(`button`,{type:`button`,className:`switch ${d?.enabled?`on`:``}`,onClick:()=>g({enabled:!d?.enabled}),disabled:!d||f,"aria-label":t(`dash.shadowCallIntercept`),"aria-pressed":d?.enabled??!1,children:(0,J.jsx)(`span`,{className:`knob`})}),(0,J.jsx)(Dt,{value:d?.model??``,options:pn(l,d?.model,d?.sourceModels).map(e=>e.value===``?e:{...e,label:Pn(e.value,t)}),onChange:e=>{g({model:e})},disabled:!d||f||!d?.enabled,label:t(`dash.shadowCallModel`),align:`right`})]})]})})]})}function fr(e){return(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)(sr,{apiBase:e.apiBase,d:e}),(0,J.jsxs)(`div`,{className:`dash-overview-tools`,children:[(0,J.jsx)(cr,{apiBase:e.apiBase,d:e}),(0,J.jsx)(lr,{d:e})]}),(0,J.jsx)(dr,{d:e}),(0,J.jsx)(or,{apiBase:e.apiBase})]})}function pr(e){return(0,J.jsxs)(`div`,{className:`dash-overview-stack`,children:[(0,J.jsx)(Bn,{...e}),(0,J.jsx)(fr,{...e})]})}function mr({t:e,providers:t}){return(0,J.jsxs)(J.Fragment,{children:[(0,J.jsxs)(`div`,{className:`h-section`,children:[e(`dash.activeProviders`),` `,(0,J.jsx)(`span`,{className:`count`,children:t.length})]}),t.length===0?(0,J.jsx)(Ot,{title:(0,J.jsx)(ut,{k:`dash.noProviders`,cmd:`ocx init`})}):(0,J.jsx)(`div`,{className:`tbl-wrap`,children:(0,J.jsxs)(`table`,{className:`tbl`,children:[(0,J.jsx)(`thead`,{children:(0,J.jsxs)(`tr`,{children:[(0,J.jsx)(`th`,{children:e(`dash.col.name`)}),(0,J.jsx)(`th`,{children:e(`dash.col.adapter`)}),(0,J.jsx)(`th`,{children:e(`dash.col.baseUrl`)}),(0,J.jsx)(`th`,{children:e(`dash.col.model`)})]})}),(0,J.jsx)(`tbody`,{children:t.map(t=>(0,J.jsxs)(`tr`,{children:[(0,J.jsx)(`td`,{className:`font-semibold`,children:jn(t.name,e)}),(0,J.jsx)(`td`,{children:(0,J.jsx)(`span`,{className:`chip`,children:t.adapter})}),(0,J.jsx)(`td`,{className:`muted mono text-label`,children:t.baseUrl}),(0,J.jsx)(`td`,{className:`muted`,children:t.defaultModel??`—`})]},t.name))})]})})]})}var hr=`__ocxCachedAt`;function gr(e){try{let t=sessionStorage.getItem(e);if(!t)return null;let n=JSON.parse(t);return _r(n)?n.data:n}catch{return null}}function _r(e){return typeof e==`object`&&!!e&&typeof e[hr]==`number`&&`data`in e}function vr(e){try{let t=sessionStorage.getItem(e);if(!t)return null;let n=JSON.parse(t);return _r(n)?{data:n.data,cachedAt:n[hr]}:{data:n,cachedAt:null}}catch{return null}}function yr(e,t){try{sessionStorage.setItem(e,JSON.stringify({[hr]:Date.now(),data:t}))}catch{}}function br(e,t){try{sessionStorage.setItem(e,JSON.stringify(t))}catch{}}function xr(e){return e.routingKind===`custom-local`?`startup.riskDetailCustomLocal`:e.shimCoverage===`cli-only`?`startup.riskDetailWindowsShim`:`startup.riskDetail`}function Sr(e,t){return!t.mutationInFlight&&e.request===t.request&&e.mutation===t.mutation}function Cr(e,t){return{request:++e.current,mutation:t.current}}var wr=2e3;function Tr(e){let t=e.status;return t!==`native`&&t!==`protected`&&t!==`at-risk`?null:t}function Er(e){return e?e.stale&&e.status!==`error`:!1}function Dr(e,t){return!t||e!==null&&e!==`error`?e:t.status}var Or=3e4;function kr(e){return{multiAgentGuidanceEnabled:e.multiAgentGuidanceEnabled!==!1,syncCodexSubagentDefaults:e.syncCodexSubagentDefaults===!0,injectionModel:e.model??``,injectionEffort:e.effort??``}}function Ar(e,t){return t.aborted?!0:e instanceof Error&&e.name===`AbortError`}async function jr(e,t){try{let n=await fetch(`${e}/api/startup-health`,{signal:t});if(!n.ok)throw Error(`startup health unavailable`);let r=await n.json(),i=Tr(r);if(!i)throw Error(`invalid startup health response`);return{status:i,stale:r.diagnosticStale===!0}}catch(e){if(Ar(e,t))throw e;return{status:`error`,stale:!1}}}async function Mr(e,t){try{return(await Ft(await fetch(`${e}/api/diagnostics/project-config`,{signal:t})))?.grouped??[]}catch{return[]}}async function Nr(e,t){return Wt(await fetch(`${e}/api/models`,{signal:t}))}async function Pr(e,t){return Wt(await fetch(`${e}/api/usage?range=30d`,{signal:t}))}async function Fr(e,t,n){let{request:r,mutation:i}=Cr(n.shadowCallRequestEpochRef,n.shadowCallMutationEpochRef),[a,o]=await Promise.all([fetch(`${e}/api/sidecar-settings`,{signal:t}),fetch(`${e}/api/shadow-call-settings`,{signal:t})]),s=await Wt(a),c;try{if(o.ok){let e=await o.json();Sr({request:r,mutation:i},{request:n.shadowCallRequestEpochRef.current,mutation:n.shadowCallMutationEpochRef.current,mutationInFlight:n.shadowCallMutationInFlightRef.current})&&(c=e)}else Sr({request:r,mutation:i},{request:n.shadowCallRequestEpochRef.current,mutation:n.shadowCallMutationEpochRef.current,mutationInFlight:n.shadowCallMutationInFlightRef.current})&&(c=null)}catch{Sr({request:r,mutation:i},{request:n.shadowCallRequestEpochRef.current,mutation:n.shadowCallMutationEpochRef.current,mutationInFlight:n.shadowCallMutationInFlightRef.current})&&(c=null)}return{sidecar:s,shadowCall:c}}async function Ir(e,t,n){let{request:r,mutation:i}=Cr(n.settingsRequestEpochRef,n.settingsMutationEpochRef),a=await Wt(await fetch(`${e}/api/settings`,{signal:t})),o,s;return Sr({request:r,mutation:i},{request:n.settingsRequestEpochRef.current,mutation:n.settingsMutationEpochRef.current,mutationInFlight:n.settingsMutationInFlightRef.current})&&(o=a,s=a.startupHealth),{settings:o,startupHealthSeed:s}}async function Lr(e,t){try{let n=await fetch(`${e}/api/v2`,{signal:t});if(!n.ok)return{maMode:`default`};let r=await n.json();return r.multiAgentMode===`v1`||r.multiAgentMode===`v2`?{maMode:r.multiAgentMode}:{maMode:`default`}}catch(e){if(Ar(e,t))throw e;return{maMode:`default`}}}async function Rr(e,t){try{let[n,r]=await Promise.all([fetch(`${e}/api/system/health`,{signal:t}),fetch(`${e}/api/providers`,{signal:t})]);return{health:await Wt(n),providers:await Wt(r),error:!1}}catch{return{health:null,providers:[],error:!0}}}async function zr(e,t){let[n,r]=await Promise.all([fetch(`${e}/api/injection-model`,{signal:t}).catch(()=>null),fetch(`${e}/api/effort-caps`,{signal:t}).catch(()=>null)]),i;try{if(n?.ok){let e=await n.json();i={...kr(e),injectionEfforts:e.efforts??[],injectionAvailable:e.available??[]}}}catch{}let a;try{if(r?.ok){let e=await r.json();a={effortCap:e.effortCap??``,subagentEffortCap:e.subagentEffortCap??``}}}catch{}return{injection:i,effortCaps:a}}function Br(e,t=`all`){return t===`codex`?[`usage-summary-30d`,e,`codex`].join(`:`):[`usage-summary-30d`,e,`all`].join(`:`)}var Vr=`ocx.dash.controls.v1:`,Hr=`ocx.dash.overview.v1:`,Ur=`ocx.dash.usage30d.v1:`,Wr=`ocx.dash.startup.v1:`,Gr=`ocx.dash.maMode.v1:`;function Kr(e){let t=new Map;for(let n of e){let e=t.get(n.provider);e?e.push(n):t.set(n.provider,[n])}return[...t.entries()].sort(([e],[t])=>e.localeCompare(t))}function qr(e){return`${Vr}${e}`}function Jr(e){let{locale:t,t:n}=ct(),[r,i]=(0,_.useState)(Vt),[a,o]=(0,_.useState)(``),[s,c]=(0,_.useState)(new Set),l=(0,_.useMemo)(()=>gr(qr(e)),[e]),u=(0,_.useMemo)(()=>gr(`${Hr}${e}`),[e]),d=(0,_.useMemo)(()=>gr(`${Ur}${e}`),[e]),f=(0,_.useMemo)(()=>{let t=gr(`${Wr}${e}`);return t===`error`?null:t},[e]),p=(0,_.useMemo)(()=>gr(`${Gr}${e}`),[e]),[m,h]=(0,_.useState)(()=>u?.health??null),[g,v]=(0,_.useState)(()=>f),[y,b]=(0,_.useState)(()=>u?.providers??[]),[x,S]=(0,_.useState)([]),[C,w]=(0,_.useState)(()=>l?.settings??null),[T,E]=(0,_.useState)(()=>l?.sidecar??null),[D,O]=(0,_.useState)(()=>l?.shadowCall??null),[k,A]=(0,_.useState)(()=>d),[j,M]=(0,_.useState)(!1),[N,P]=(0,_.useState)(!1),[F,I]=(0,_.useState)(!1),[L,R]=(0,_.useState)(!1),[z,B]=(0,_.useState)(!1),[V,H]=(0,_.useState)(()=>p??`default`),[U,W]=(0,_.useState)(!1),[ee,K]=(0,_.useState)(null),[q,J]=(0,_.useState)(!1),[Y,te]=(0,_.useState)(!1),[ne,re]=(0,_.useState)(!1),[ie,ae]=(0,_.useState)(``),[oe,se]=(0,_.useState)(``),[ce,le]=(0,_.useState)([]),[ue,de]=(0,_.useState)([]),[fe,pe]=(0,_.useState)(!1),[X,me]=(0,_.useState)(!0),[he,ge]=(0,_.useState)(!1),[_e,Z]=(0,_.useState)(``),[ve,ye]=(0,_.useState)(``),[be,xe]=(0,_.useState)(!1),[Se,Ce]=(0,_.useState)(null),[we,Te]=(0,_.useState)(null),[Ee,De]=(0,_.useState)([]),[Oe,ke]=(0,_.useState)(!1),[Ae,je]=(0,_.useState)(`latest`),[Me,Ne]=(0,_.useState)(!0),[Pe,Fe]=(0,_.useState)(!1),Ie=(0,_.useRef)(0),Le=(0,_.useRef)(null),Re=(0,_.useRef)(0),ze=(0,_.useRef)(0),Be=(0,_.useRef)(0),Ve=(0,_.useRef)(!1),He=(0,_.useRef)(0),Ue=(0,_.useRef)(0),We=(0,_.useRef)(!1),[Ge,Ke]=(0,_.useState)(null),[qe,Je]=(0,_.useState)(null),[Ye,Xe]=(0,_.useState)(null),[Ze,Qe]=(0,_.useState)(!1),[$e,et]=(0,_.useState)(!1),tt=(0,_.useRef)(null),nt=(0,_.useRef)(null),rt=(0,_.useRef)(null),it=(0,_.useRef)(null),at=yn(Y,tt),ot=yn(Oe,nt),st=yn(q,rt),Q=yn(ne,it);(0,_.useEffect)(()=>{let e=()=>i(Vt());return window.addEventListener(`hashchange`,e),()=>window.removeEventListener(`hashchange`,e)},[]),(0,_.useEffect)(()=>()=>{Re.current+=1,Le.current!==null&&(window.clearTimeout(Le.current),Le.current=null)},[]);let lt=(0,_.useRef)(f),ut=(0,_.useRef)(0),dt=(0,_.useRef)({settingsRequestEpochRef:ze,settingsMutationEpochRef:Be,settingsMutationInFlightRef:Ve,shadowCallRequestEpochRef:He,shadowCallMutationEpochRef:Ue,shadowCallMutationInFlightRef:We}).current,pt=G(`dashboard-startup-health:${e}`,[e],t=>jr(e,t),{pollMs:3e4}),mt=Er(pt.data),ht=pt.refresh;(0,_.useEffect)(()=>{if(!mt)return;let e=window.setTimeout(()=>{ht()},wr);return()=>window.clearTimeout(e)},[mt,ht]);let gt=G(`dashboard-overview:${e}`,[e],t=>Rr(e,t),{pollMs:5e3}),_t=m!==null||gt.data!==void 0,vt=G(`dashboard-ma-mode:${e}`,[e],t=>Lr(e,t),{pollMs:5e3}),yt=G(`dashboard-sidecars:${e}`,[e],async t=>{let n=ut.current;return{...await Fr(e,t,dt),startupHealthGeneration:n}},{pollMs:5e3}),bt=G(`dashboard-settings:${e}`,[e],async t=>{let n=ut.current;return{...await Ir(e,t,dt),startupHealthGeneration:n}},{pollMs:5e3}),xt=G(`dashboard-multi-agent:${e}`,[e],t=>zr(e,t),{pollMs:5e3,enabled:_t}),St=G(Br(e),[e],t=>Pr(e,t),{enabled:_t,pollMs:6e4,deadlineMs:6e4}),Ct=G(`dashboard-diagnostics:${e}`,[e],t=>Mr(e,t),{pollMs:Or,enabled:_t}),wt=G(`dashboard-models:${e}`,[e,$e],t=>Nr(e,t),{enabled:_t&&!$e});(0,_.useEffect)(()=>{if(pt.data!==void 0){let t=pt.data;ut.current+=1,v(t.status),lt.current=t.status,t.status!==`error`&&!t.stale&&br(`${Wr}${e}`,t.status)}},[pt.data,e]),(0,_.useEffect)(()=>{let t=gt.data;t&&(t.health&&(h(t.health),b(t.providers),br(`${Hr}${e}`,{health:t.health,providers:t.providers})),et(t.error))},[gt.data,e]),(0,_.useEffect)(()=>{vt.data!==void 0&&(H(vt.data.maMode),br(`${Gr}${e}`,vt.data.maMode))},[vt.data,e]);let Tt=vt.data!==void 0||p!==null;(0,_.useEffect)(()=>{let e=xt.data;e&&(e.injection&&(me(e.injection.multiAgentGuidanceEnabled),ge(e.injection.syncCodexSubagentDefaults),ae(e.injection.injectionModel),se(e.injection.injectionEffort),le(e.injection.injectionEfforts),de(e.injection.injectionAvailable)),e.effortCaps&&(Z(e.effortCaps.effortCap),ye(e.effortCaps.subagentEffortCap)))},[xt.data]),(0,_.useEffect)(()=>{let t=yt.data;if(!t)return;E(t.sidecar),t.shadowCall!==void 0&&O(t.shadowCall);let n=gr(qr(e))??{};br(qr(e),{...n,sidecar:t.sidecar,...t.shadowCall===void 0?{}:{shadowCall:t.shadowCall}})},[yt.data,e]),(0,_.useEffect)(()=>{let t=bt.data;if(t){if(t.settings!==void 0&&w(t.settings),t.startupHealthSeed!==void 0&&t.startupHealthGeneration===ut.current){let n=Dr(lt.current,t.startupHealthSeed);v(n),lt.current=n,n&&br(`${Wr}${e}`,n)}if(t.settings!==void 0){let n=gr(qr(e))??{};br(qr(e),{...n,settings:t.settings})}}},[bt.data,e]),(0,_.useEffect)(()=>{St.data!==void 0&&(A(St.data),br(`${Ur}${e}`,St.data))},[St.data,e]),(0,_.useEffect)(()=>{Ct.data&&De(Ct.data)},[Ct.data]),(0,_.useEffect)(()=>{wt.data&&S(wt.data),I(wt.loading)},[wt.data,wt.loading]),(0,_.useEffect)(()=>()=>{ze.current+=1,He.current+=1},[]);let $=G(Ye?.id&&Ye.restart?`update-job:${e}:${Ye.id}`:`update-job:idle:${e}`,[e,Ye?.id,Ye?.restart,Ye?.latestVersion],async t=>{if(!Ye?.id||!Ye.restart)return{reconnecting:!1};let n=Ye.latestVersion;try{let r=await Wt(await fetch(`${e}/api/update/status?jobId=${encodeURIComponent(Ye.id)}`,{signal:t}));if(r.job){if(r.job.status===`failed`)return{job:r.job,reconnecting:!1};if(n)try{if((await Wt(await fetch(`${e}/healthz`,{cache:`no-store`,signal:t}))).version===n)return{job:r.job,reconnecting:!1,reload:!0}}catch{return{job:r.job,reconnecting:!0}}return{job:r.job,reconnecting:!1}}}catch{return{reconnecting:!0}}return{reconnecting:!1}},{pollMs:1500,enabled:!!(Ye?.id&&Ye.restart),pauseWhenHidden:!1});(0,_.useEffect)(()=>{let e=$.data;e&&(`job`in e&&e.job&&Xe(e.job),Qe(e.reconnecting),`reload`in e&&e.reload&&window.location.reload())},[$.data]);let Et=(0,_.useMemo)(()=>Kr(x),[x]),Dt=(0,_.useMemo)(()=>{let e=a.trim().toLowerCase();if(!e)return Et;let t=[];for(let[n,r]of Et){let i=r.filter(t=>t.id.toLowerCase().includes(e)||n.toLowerCase().includes(e));i.length>0&&t.push([n,i])}return t},[Et,a]),Ot=(0,_.useMemo)(()=>{let e=T?.webSearch.backend;return dn(T?.webSearchModels,x,T?.webSearch.model,e===`routed`?void 0:e)},[x,T?.webSearchModels,T?.webSearch]),kt=(0,_.useMemo)(()=>fn(T?.visionModels,x,T?.vision?.model,T?.vision?.backend),[T?.visionModels,x,T?.vision]),At=async t=>{if(!T||j)return;let n=T,r={webSearch:Yt(T.webSearch,t.webSearch),vision:Yt(T.vision,t.vision),...T.visionModels?{visionModels:T.visionModels}:{},...T.webSearchModels?{webSearchModels:T.webSearchModels}:{}};M(!0),E(r);try{let n=await Wt(await fetch(`${e}/api/sidecar-settings`,{method:`PUT`,headers:{"Content-Type":`application/json`},body:JSON.stringify(t)}),`save failed`);E({webSearch:n.webSearch,vision:n.vision,...n.visionModels?{visionModels:n.visionModels}:{},...n.webSearchModels?{webSearchModels:n.webSearchModels}:{}});let r=gr(qr(e))??{};br(qr(e),{...r,sidecar:{webSearch:n.webSearch,vision:n.vision,...n.visionModels?{visionModels:n.visionModels}:{},...n.webSearchModels?{webSearchModels:n.webSearchModels}:{}}})}catch{E(n)}finally{M(!1)}};async function jt(t){if(!D||N)return;let n=D,r={...D,...t};P(!0),We.current=!0,O(r);try{if(!(await fetch(`${e}/api/shadow-call-settings`,{method:`PUT`,headers:{"Content-Type":`application/json`},body:JSON.stringify(t)})).ok)throw Error(`shadow-call save failed`);Ue.current+=1}catch{O(n)}finally{We.current=!1,P(!1)}}let Mt=async t=>{if(!(U||V===t)){W(!0),K(null);try{let r=await fetch(`${e}/api/v2`,{method:`PUT`,headers:{"Content-Type":`application/json`},body:JSON.stringify({multiAgentMode:t})});if(r.ok)H(t),br(`${Gr}${e}`,t);else{let e=n(`dash.maSwitchFailed`,{status:String(r.status)});try{let t=await r.json();e=typeof t.error==`string`&&t.error||typeof t.message==`string`&&t.message||e}catch{}K(e)}}catch(e){K(e instanceof Error?e.message:n(`dash.maNetworkError`))}finally{W(!1)}}},Nt=async t=>{if(!fe){pe(!0);try{if(!(await fetch(`${e}/api/injection-model`,{method:`PUT`,headers:{"Content-Type":`application/json`},body:JSON.stringify(t)})).ok)throw Error(`injection save failed`);let n=await Wt(await fetch(`${e}/api/injection-model`)),r=kr(n);me(r.multiAgentGuidanceEnabled),ge(r.syncCodexSubagentDefaults),ae(r.injectionModel),se(r.injectionEffort),Array.isArray(n.efforts)&&le(n.efforts),Array.isArray(n.available)&&de(n.available)}catch{}finally{pe(!1)}}},Pt=async()=>{if(!C||L)return;let t=!C.codexAutoStart;R(!0),Ve.current=!0,w({...C,codexAutoStart:t});try{let n=await Wt(await fetch(`${e}/api/settings`,{method:`PUT`,headers:{"Content-Type":`application/json`},body:JSON.stringify({codexAutoStart:t})}),`save failed`);Be.current+=1,w(e=>e&&{...e,codexAutoStart:n.codexAutoStart,startupHealth:n.startupHealth??e.startupHealth})}catch{w(e=>e&&{...e,codexAutoStart:!t}),et(!0)}finally{Ve.current=!1,R(!1)}},Ft=(0,_.useCallback)(()=>{Ce(null),Te(null)},[]),It=async()=>{if(!z){B(!0),Ce(null),Te(null);try{let t=await Wt(await fetch(`${e}/api/sync`,{method:`POST`}),`sync failed`);Ce(t),t.projectConfigGrouped&&De(t.projectConfigGrouped)}catch(e){Te(e instanceof Error?e.message:String(e))}finally{B(!1)}}},Lt=async(t,n=!1)=>{n&&(Ie.current=0),Le.current!==null&&(window.clearTimeout(Le.current),Le.current=null);let r=++Re.current;Fe(!0),Je(null),Ke(null);try{let n=await Wt(await fetch(`${e}/api/update/check?tag=${t}`),`update check failed`);if(r!==Re.current)return;if(Ke(n),n.reason===`latest_unavailable`&&Ie.current<2){let e=++Ie.current;Le.current=window.setTimeout(()=>{r===Re.current&&(Le.current=null,Lt(t))},800*e);return}n.reason!==`latest_unavailable`&&(Ie.current=0),Fe(!1)}catch(e){if(r!==Re.current)return;Je(e instanceof Error?e.message:String(e)),Fe(!1)}},Rt=()=>{Re.current+=1,Le.current!==null&&(window.clearTimeout(Le.current),Le.current=null),Fe(!1),ke(!1)},zt=()=>{let e=Kt(m?.version);je(e),Ne(!0),ke(!0),Lt(e,!0)},Bt=e=>{je(e),Lt(e,!0)},Ut=(0,_.useRef)(zt);return(0,_.useEffect)(()=>{Ut.current=zt}),(0,_.useEffect)(()=>{let e=()=>{Ht()&&(ft(`dashboard`),Ut.current())},t=Ht()?window.setTimeout(e,0):null;return window.addEventListener(`hashchange`,e),()=>{t!==null&&window.clearTimeout(t),window.removeEventListener(`hashchange`,e)}},[]),{apiBase:e,locale:t,t:n,selectedSection:r,setSelectedSection:i,modelQuery:a,setModelQuery:o,expandedProviders:s,setExpandedProviders:c,health:m,startupHealth:g,providers:y,models:x,settings:C,sidecar:T,shadowCall:D,usage30d:k,usageLoading:St.loading&&!k,healthLoading:gt.loading&&!m,sidecarSaving:j,shadowCallSaving:N,modelsLoading:F,settingsSaving:L,syncing:z,maMode:V,maModeResolved:Tt,maBusy:U,setMaHelpOpen:J,maHelpOpen:q,maError:ee,effortCapHelpOpen:Y,setEffortCapHelpOpen:te,shadowCallHelpOpen:ne,setShadowCallHelpOpen:re,injectionModel:ie,injectionEffort:oe,injectionEfforts:ce,injectionAvailable:ue,injectionSaving:fe,multiAgentGuidanceEnabled:X,syncCodexSubagentDefaults:he,saveInjection:Nt,effortCap:_e,subagentEffortCap:ve,effortCapSaving:be,setEffortCap:Z,setSubagentEffortCap:ye,setEffortCapSaving:xe,syncResult:Se,syncError:we,projectConfigWarnings:Ee,updateOpen:Oe,updateChannel:Ae,setUpdateRestart:Ne,updateRestart:Me,updateLoading:Pe,updateCheck:Ge,updateError:qe,updateJob:Ye,reconnecting:Ze,error:$e,effortCapHelpTriggerRef:tt,updateTriggerRef:nt,maHelpTriggerRef:rt,shadowCallHelpTriggerRef:it,effortCapHelpDialogRef:at,updateDialogRef:ot,maHelpDialogRef:st,shadowCallHelpDialogRef:Q,filteredGroups:Dt,sidecarModels:Ot,visionModels:kt,saveSidecar:At,saveShadowCall:jt,switchMaMode:Mt,toggleCodexAutoStart:Pt,runSync:It,clearSyncFeedback:Ft,fetchUpdateCheck:Lt,closeUpdateDialog:Rt,openUpdateDialog:zt,changeUpdateChannel:Bt,runUpdate:async()=>{if(Ge?.canUpdate){Je(null);try{let t=await Wt(await fetch(`${e}/api/update/run`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({tag:Ae,restart:Me})}),`update failed to start`);if(!t.job)throw Error(`update failed to start`);Xe(t.job),Qe(!1),Rt()}catch(e){Je(e instanceof Error?e.message:String(e))}}}}}function Yr(e){pt(Ut(e))}function Xr({apiBase:e}){let t=Jr(e),{t:n,error:r,selectedSection:i,providers:a,models:o,modelsLoading:s,modelQuery:c,setModelQuery:l,filteredGroups:u,expandedProviders:d,setExpandedProviders:f}=t;if(r)return(0,J.jsx)(Ot,{style:{marginTop:40},icon:(0,J.jsx)(_e,{}),title:(0,J.jsx)(`span`,{style:{color:`var(--red)`},children:n(`dash.cannotConnect`)}),children:(0,J.jsx)(ut,{k:`dash.runStart`,cmd:`ocx start`})});let p=(0,J.jsx)(pr,{...t}),m=(0,J.jsx)(mr,{t:n,providers:a}),h=(0,J.jsx)(Fn,{t:n,models:o,modelsLoading:s,modelQuery:c,setModelQuery:l,filteredGroups:u,expandedProviders:d,setExpandedProviders:f}),g=(0,J.jsx)(bn,{...t}),_=[{id:`overview`,label:n(`dash.workspace.overview`),body:p},{id:`providers`,label:n(`dash.activeProviders`),body:m},{id:`models`,label:n(`dash.availableModels`),body:h}],v=_.find(e=>e.id===i)??_[0],y=Yr,b=e=>{let t=_.findIndex(e=>e.id===i),n=-1;if(e.key===`ArrowRight`?n=(t+1)%_.length:e.key===`ArrowLeft`?n=(t-1+_.length)%_.length:e.key===`Home`?n=0:e.key===`End`&&(n=_.length-1),n<0)return;e.preventDefault();let r=_[n];y(r.id),document.getElementById(`dashboard-tab-${r.id}`)?.focus()};return(0,J.jsxs)(`div`,{className:`dashboard-workspace-shell`,children:[(0,J.jsx)(`div`,{className:`page-head`,children:(0,J.jsx)(`h2`,{children:n(`nav.dashboard`)})}),(0,J.jsx)(`p`,{className:`page-sub`,children:n(`dash.subtitle`)}),(0,J.jsx)(`div`,{className:`page-tabs`,role:`tablist`,"aria-label":n(`dash.workspace.sections`),children:_.map(e=>(0,J.jsx)(`button`,{type:`button`,role:`tab`,id:`dashboard-tab-${e.id}`,"aria-selected":i===e.id,"aria-controls":`dashboard-panel-${e.id}`,tabIndex:i===e.id?0:-1,className:`page-tab${i===e.id?` page-tab--active`:``}`,onClick:()=>y(e.id),onKeyDown:b,children:e.label},e.id))}),(0,J.jsx)(`section`,{className:`dashboard-workspace-main`,role:`tabpanel`,id:`dashboard-panel-${v.id}`,"aria-labelledby":`dashboard-tab-${v.id}`,tabIndex:0,children:v.body}),g]})}var Zr=`https://chatgpt.com/backend-api/codex`,Qr=`openai`;function $r(e){try{let t=new URL(e.trim());if(t.username||t.password||t.search||t.hash)return;let n=t.pathname.replace(/\/+$/,``);return`${t.origin}${n}`}catch{return}}var ei=new URL(Zr).protocol;function ti(e,...t){return`${ei}//${e}/${t.join(`/`)}`}var ni={"cline-pass":{adapter:`openai-chat`,baseUrl:ti(`api.cline.bot`,`api`,`v1`)},"mimo-free":{adapter:`mimo-free`,baseUrl:ti(`api.xiaomimimo.com`,`api`,`free-ai`,`openai`,`chat`)}};function ri(e,t){let n=ni[e];return!n||t.adapter!==n.adapter||$r(t.baseUrl)!==$r(n.baseUrl)}function ii(e){try{let t=new URL(e).hostname.replace(/^\[|\]$/g,``).toLowerCase();return t===`localhost`||t===`127.0.0.1`||t===`::1`}catch{return!1}}function ai(e){return e.keyOptional===!0||e.authMode===`oauth`||e.authMode===`forward`||e.authMode===`local`||ii(e.baseUrl)||e.hasApiKey===!0}function oi(e){return e.adapter===`openai-responses`&&e.authMode===`forward`&&$r(e.baseUrl)===Zr}function si(e,t){return e===Qr&&oi(t)}function ci(e){return e.freeTier===!0||e.keyOptional===!0||e.authMode===`local`||ii(e.baseUrl)}function li(e,t){return si(e,t)?`accounts`:ci(t)?`free`:`paid`}function ui(e,t){let n=[...e],r=(e,t)=>e.name.localeCompare(t.name,void 0,{sensitivity:`base`}),i=e=>e.tier??li(e.name,e);switch(t){case`az`:return n.sort(r);case`za`:return n.sort((e,t)=>r(t,e));case`free-paid`:return n.sort((e,t)=>(i(e)===`free`?0:1)-(i(t)===`free`?0:1)||r(e,t));case`paid-free`:return n.sort((e,t)=>(i(e)===`free`)-+(i(t)===`free`)||r(e,t));case`accounts-first`:return n.sort((e,t)=>{let n=e=>{let t=i(e);return t===`accounts`?0:t===`free`?1:2};return n(e)-n(t)||r(e,t)});default:return n}}function di(e){let t=[],n=[],r=[];for(let[i,a]of Object.entries(e)){if(a.disabled){r.push({name:i,...a});continue}ai(a)?t.push({name:i,...a,tier:li(i,a)}):n.push({name:i,...a})}return{ready:t,needsSetup:n,disabled:r}}function fi(e,t){let n=new Set(Object.entries(t).filter(([,e])=>e).map(([e])=>e));if(n.size===0)return e;let r=e=>e.map(e=>n.has(e.name)?{...e,activeNeedsReauth:!0}:e);return{ready:r(e.ready),needsSetup:r(e.needsSetup),disabled:e.disabled}}function pi(e){return e.disabled?`disabled`:`activeNeedsReauth`in e&&e.activeNeedsReauth?`needs-setup`:ai(e)?`ready`:`needs-setup`}function mi(e){let t=e.openai,n=e.chatgpt;if(!t||!n||!si(`openai`,t)||!oi(n))return e;let r={...e};return delete r.chatgpt,r}function hi(e){return e.authMode===`local`||ii(e.baseUrl)}var gi=[`ollama`,`vllm`,`lm-studio`,`lmstudio`,`litellm`,`localai`];function _i(e){let t=(e.authMode??``).toLowerCase();if(t===`oauth`||t===`forward`)return`login`;if(hi(e))return`local`;let n=`${e.name??``} ${e.adapter} ${e.baseUrl}`.toLowerCase();return gi.some(e=>n.includes(e))?`selfHosted`:`cloud`}function vi(e){if(!e||typeof e!=`object`)return{};let t=e.available;if(!t||typeof t!=`object`||Array.isArray(t))return{};let n={};for(let[e,r]of Object.entries(t))Array.isArray(r)&&(n[e]=r.filter(e=>typeof e==`string`));return n}function yi(e){if(!e||typeof e!=`object`)return{};let t=e.liveModelCounts;if(!t||typeof t!=`object`||Array.isArray(t))return{};let n={};for(let[e,r]of Object.entries(t))typeof r!=`number`||!Number.isFinite(r)||r<0||(n[e]=Math.floor(r));return n}function bi(e){if(!e||typeof e!=`object`)return{};let t=e.selected;if(!t||typeof t!=`object`||Array.isArray(t))return{};let n={};for(let[e,r]of Object.entries(t))Array.isArray(r)&&(n[e]=r.filter(e=>typeof e==`string`));return n}function xi(e){let t={};for(let[n,r]of Object.entries(vi(e)))t[n]=r.length;return t}function Si(e){return Object.entries(e).filter(e=>typeof e[1].requests==`number`&&e[1].requests>0).map(([e,t])=>({name:e,...t,requests:t.requests})).sort((e,t)=>t.requests-e.requests||e.name.localeCompare(t.name))}var Ci={justNow:`Just now`,notChecked:`Not checked`,minutesAgo:e=>`${e}m ago`,hoursAgo:e=>`${e}h ago`,daysAgo:e=>`${e}d ago`};function wi(e,t,n){let r=typeof t==`object`&&t?t:Ci,i=typeof t==`number`?t:n??Date.now();if(e===void 0||!Number.isFinite(e))return r.notChecked;let a=Math.max(0,i-e),o=Math.floor(a/6e4);if(o<1)return r.justNow;if(o<60)return r.minutesAgo(o);let s=Math.floor(o/60);return s<24?r.hoursAgo(s):r.daysAgo(Math.floor(s/24))}function Ti(e){return{justNow:e(`time.justNow`),notChecked:e(`time.notChecked`),minutesAgo:t=>e(`time.minutesAgo`,{n:t}),hoursAgo:t=>e(`time.hoursAgo`,{n:t}),daysAgo:t=>e(`time.daysAgo`,{n:t})}}function Ei(e,t){let n=[];for(let r of e.ready)r.activeNeedsReauth&&n.push({name:r.name,reason:t[r.name]??`Active account needs re-authentication`});for(let r of e.needsSetup){let e=r.activeNeedsReauth?t[r.name]??`Active account needs re-authentication`:t[r.name]??`Missing credentials`;n.push({name:r.name,reason:e})}for(let r of e.disabled){let e=t[r.name];e&&n.push({name:r.name,reason:e})}return n}function Di(e){return e===`Active account needs re-authentication`?`reauth`:e===`Missing credentials`?`missing`:`custom`}function Oi(e,t=`en`){if(e===void 0)return`—`;if(t.toLowerCase().slice(0,2)===`de`){let t=e=>e.replace(/\.0+$/,``).replace(`.`,`,`);return e>=1e9?`${t((e/1e9).toFixed(2))} Mrd.`:e>=1e6?`${t((e/1e6).toFixed(1))} Mio.`:e>=1e3?`${t((e/1e3).toFixed(1))} Tsd.`:String(e)}return e>=1e9?`${(e/1e9).toFixed(2).replace(/\.?0+$/,``)}B`:e>=1e6?`${(e/1e6).toFixed(1)}M`:e>=1e3?`${(e/1e3).toFixed(1)}k`:String(e)}function ki(e,t=`en`){return Oi(e,t)}function Ai(e,t=`en`){return e==null||!Number.isFinite(e)||e<0?`—`:`~$${new Intl.NumberFormat(t,{minimumFractionDigits:4,maximumFractionDigits:4}).format(e)}`}function ji(e,t){let n=pi(e);return n===`disabled`?t(`prov.disabledBadge`):n===`ready`?t(`pws.status.ready`):`activeNeedsReauth`in e&&e.activeNeedsReauth?t(`pws.status.needsAttention`):t(`pws.status.needsSetup`)}function Mi(e,t){switch(e.authMode){case`oauth`:return t(`modal.badge.oauth`);case`forward`:return t(`pws.auth.chatgptPassthrough`);case`local`:return t(`modal.badge.local`);case`key`:return t(`modal.badge.apiKey`);default:return e.authMode??(e.keyOptional?t(`pws.auth.noKey`):t(`modal.badge.apiKey`))}}function Ni(e){let t=pi(e);return t===`disabled`?`providers-workspace-rail-status providers-workspace-rail-status--inactive`:t===`ready`?`providers-workspace-rail-status providers-workspace-rail-status--active`:`providers-workspace-rail-status providers-workspace-rail-status--warning`}function Pi({name:e,adapter:t,baseUrl:n,cls:r}){let i=Q(),a=En(e,{adapter:t,baseUrl:n}),o=An(a),s=o===`plate`?`${r} provider-icon--plate`:o===`dark-plate`?`${r} provider-icon--plate-dark`:r;return(0,J.jsx)(`span`,{className:s,children:a&&o===`mask`?(0,J.jsx)(`span`,{className:`provider-icon-mask`,style:{maskImage:`url(${a})`,WebkitMaskImage:`url(${a})`},"aria-hidden":`true`}):a?(0,J.jsx)(`img`,{src:a,alt:``,"aria-hidden":`true`}):(0,J.jsx)(Fi,{name:e,label:jn(e,i)})})}function Fi({name:e,label:t}){let n=[...e].reduce((e,t)=>e+t.charCodeAt(0),0)%360,r=(t.trim()[0]??e[0]??`?`).toUpperCase();return(0,J.jsx)(`span`,{className:`provider-icon-fallback`,style:{background:`hsl(${n} 55% 90%)`,color:`hsl(${n} 65% 32%)`},"aria-hidden":`true`,children:r})}function Ii({item:e,selected:t,tabbable:n,modelCount:r,isDefault:i,showConfigId:a,onClick:o,onFocus:s}){let c=Q(),l=ci(e),u=hi(e),d=ji(e,c),f=jn(e.name,c),p=a?`${f} (${e.name})`:f,m=`${i?c(`pws.rail.suffixDefault`):``}${u?c(`pws.rail.suffixLocal`):l?c(`pws.rail.suffixFree`):``}`,h=r!==void 0&&r>0?r===1?c(`pws.modelCountOne`):c(`pws.modelCount`,{count:r}):``,g=[a?e.name:``,h].filter(Boolean).join(` · `);return(0,J.jsxs)(`button`,{type:`button`,className:`providers-workspace-rail-row${t?` providers-workspace-rail-row--selected`:``}`,onClick:o,role:`option`,"aria-selected":t,tabIndex:n?0:-1,"aria-label":c(`pws.rail.selectAria`,{name:p,status:d,suffix:m}),title:p,onFocus:s,children:[(0,J.jsx)(Pi,{name:e.name,adapter:e.adapter,baseUrl:e.baseUrl,cls:`providers-workspace-rail-icon`}),(0,J.jsxs)(`span`,{className:`providers-workspace-rail-copy`,children:[(0,J.jsxs)(`span`,{className:`providers-workspace-rail-primary`,children:[(0,J.jsx)(`span`,{className:`providers-workspace-rail-name-label`,title:f,children:f}),u?(0,J.jsx)(`span`,{className:`pwi-rail-badge pwi-rail-badge--local`,title:c(`pws.localTitle`),children:c(`modal.badge.local`)}):l?(0,J.jsx)(`span`,{className:`pwi-rail-badge pwi-rail-badge--free`,title:c(`pws.freeTitle`),children:c(`modal.badge.free`)}):null]}),(0,J.jsx)(`span`,{className:`providers-workspace-rail-secondary`,title:g||void 0,children:g||`\xA0`})]}),(0,J.jsxs)(`span`,{className:`providers-workspace-rail-trail`,children:[i&&(0,J.jsx)(`span`,{className:`pwi-default-star`,title:c(`prov.defaultBadge`),"aria-label":c(`prov.defaultBadge`),children:(0,J.jsx)(Ie,{width:17,height:17,"aria-hidden":`true`})}),(0,J.jsx)(`span`,{className:Ni(e),title:d,"aria-hidden":`true`})]})]})}var Li=e=>typeof e==`number`&&Number.isFinite(e)?e:void 0,Ri=e=>{let t=Li(e);if(t===void 0)return;let n=t>1e10?t:t*1e3;return Number.isFinite(new Date(n).getTime())?t:void 0};function zi(e,t){if(!e||typeof e!=`object`||Array.isArray(e))return null;let n=e,r=Array.isArray(n.customWindows)?n.customWindows.flatMap(e=>{if(!e||typeof e!=`object`)return[];let t=e;return typeof t.label!=`string`||Li(t.percent)===void 0?[]:[{label:t.label,percent:t.percent,...Li(t.resetAt)===void 0?{}:{resetAt:t.resetAt}}]}):[],i=n.creditsUsd&&typeof n.creditsUsd==`object`&&!Array.isArray(n.creditsUsd)?n.creditsUsd:null,a=Li(i?.used),o=Li(i?.limit),s=Li(i?.remaining),c=Li(i?.percent),l=Ri(i?.expiresAt),u=a!==void 0&&o!==void 0&&s!==void 0&&c!==void 0?{used:a,limit:o,remaining:s,percent:c,...l===void 0?{}:{expiresAt:l},...typeof i?.unlimited==`boolean`?{unlimited:i.unlimited}:{}}:void 0,d={...Li(n.fiveHourPercent)===void 0?{}:{fiveHourPercent:n.fiveHourPercent},...Li(n.fiveHourResetAt)===void 0?{}:{fiveHourResetAt:n.fiveHourResetAt},...Li(n.weeklyPercent)===void 0?{}:{weeklyPercent:n.weeklyPercent},...Li(n.weeklyResetAt)===void 0?{}:{weeklyResetAt:n.weeklyResetAt},...Li(n.monthlyPercent)===void 0?{}:{monthlyPercent:n.monthlyPercent},...Li(n.monthlyResetAt)===void 0?{}:{monthlyResetAt:n.monthlyResetAt},...r.length>0?{customWindows:r}:{},...u?{creditsUsd:u}:{},updatedAt:Li(n.updatedAt)??t??Date.now()};return d.fiveHourPercent!==void 0||d.weeklyPercent!==void 0||d.monthlyPercent!==void 0||(d.customWindows?.length??0)>0||d.creditsUsd!==void 0?d:null}function Bi(e){return zi(e?.quota,e?.updatedAt)}function Vi(e){if(!e||typeof e!=`object`||Array.isArray(e))return;let t=e,n=Li(t.usedPercent);if(n!==void 0)return{usedPercent:n,...typeof t.incomplete==`boolean`?{incomplete:t.incomplete}:{},...Li(t.excludedAccounts)===void 0?{}:{excludedAccounts:t.excludedAccounts},...Li(t.nextRecoveryAt)===void 0?{}:{nextRecoveryAt:t.nextRecoveryAt},...Li(t.nextRecoveryPercent)===void 0?{}:{nextRecoveryPercent:t.nextRecoveryPercent}}}function Hi(e){let t=e?.aggregation;if(!t||typeof t!=`object`||Array.isArray(t))return null;let n=t;if(n.kind!==`capacity-weighted-v1`||n.scope!==`routable-known`)return null;let r=Li(n.excludedAccounts),i=Li(n.unknownPlanAccounts);if(r===void 0||i===void 0||typeof n.incomplete!=`boolean`)return null;let a=n.currentAccount&&typeof n.currentAccount==`object`&&!Array.isArray(n.currentAccount)?n.currentAccount:null,o=Array.isArray(n.customWindows)?n.customWindows.flatMap(e=>{if(!e||typeof e!=`object`||Array.isArray(e))return[];let t=e,n=Vi(t);return typeof t.label==`string`&&n?[{label:t.label,...n}]:[]}):[],s=Vi(n.fiveHour),c=Vi(n.weekly),l=Vi(n.monthly),u=!!s||!!c||!!l||o.length>0;return{presentation:n.presentation===`aggregate`||n.presentation===`effective-account-fallback`||n.presentation===`coverage-only`?n.presentation:u?`aggregate`:`coverage-only`,incomplete:n.incomplete,excludedAccounts:r,unknownPlanAccounts:i,partialWindowAccounts:Li(n.partialWindowAccounts)??0,...s?{fiveHour:s}:{},...c?{weekly:c}:{},...l?{monthly:l}:{},...o.length>0?{customWindows:o}:{},...a?{currentAccount:{...typeof a.plan==`string`||a.plan===null?{plan:a.plan}:{},quota:zi(a.quota)}}:{}}}function Ui(e){if(!e?.trim())return``;let[t,n]=e.split(`:`,2);return n?`${t} · ${n.replace(/-/g,` `)}`:e}function Wi(e,t,n,r,i,a){let o=r&&r.length>0?r:t?[t]:[],s=[...new Set([...a?e:o,...i])],c=n.trim().toLowerCase();return c?s.filter(e=>e.toLowerCase().includes(c)):s}function Gi(e){let t=e?.trim().toLowerCase();return t===`go`||t===`free`}function Ki(e,t){if(!e)return null;let n=e.shortPercent===void 0&&e.shortResetAt===void 0?e:{...e,fiveHourPercent:e.fiveHourPercent??e.shortPercent,fiveHourResetAt:e.fiveHourResetAt??e.shortResetAt};return Gi(t)?{...n.monthlyPercent===void 0?{}:{monthlyPercent:n.monthlyPercent},...n.monthlyResetAt===void 0?{}:{monthlyResetAt:n.monthlyResetAt},...n.creditsUsd===void 0?{}:{creditsUsd:n.creditsUsd},...n.resetCredits===void 0?{}:{resetCredits:n.resetCredits},updatedAt:n.updatedAt}:n}function qi(e){return e===`5h`?0:e===`First-party models`?2:e===`API usage`?3:5}function Ji(e,t){switch(e){case`First-party models`:return t(`quota.cursorFirstParty`);case`API usage`:return t(`quota.cursorApiUsage`);case`Total subscription credits`:return t(`quota.totalSubscriptionCredits`);default:return e}}function Yi(e,t,n){let r=Ki(e,t);if(!r)return[];let i=[];typeof r.fiveHourPercent==`number`&&i.push({rank:0,row:{windowKey:`fiveHour`,label:n(`codexAuth.fiveHour`),limitLabel:n(`quota.fiveHourLimit`),percent:r.fiveHourPercent,resetAt:r.fiveHourResetAt}}),typeof r.weeklyPercent==`number`&&i.push({rank:1,row:{windowKey:`weekly`,label:n(`codexAuth.weekly`),limitLabel:n(`quota.weeklyLimit`),percent:r.weeklyPercent,resetAt:r.weeklyResetAt}}),typeof r.monthlyPercent==`number`&&i.push({rank:4,row:{windowKey:`monthly`,label:n(`codexAuth.monthly`),limitLabel:n(`quota.monthlyLimit`),percent:r.monthlyPercent,resetAt:r.monthlyResetAt}});for(let e of r.customWindows??[]){let t=Ji(e.label,n);i.push({rank:qi(e.label),row:{customLabel:e.label,label:t,limitLabel:t,percent:e.percent,resetAt:e.resetAt}})}return i.sort((e,t)=>e.rank-t.rank).map(e=>e.row)}function Xi(e){if(!e)return-1;let t=[e.fiveHourPercent,e.weeklyPercent,e.monthlyPercent].filter(e=>typeof e==`number`);for(let n of e.customWindows??[])typeof n.percent==`number`&&t.push(n.percent);return t.length?Math.max(...t):-1}function Zi(e){switch(e){case`en`:return`en-GB`;case`de`:return`de-DE`;case`fr`:return`fr-FR`;case`ko`:return`ko-KR`;case`zh`:return`zh-CN`;case`zh-TW`:return`zh-TW`;case`ru`:return`ru-RU`;case`ja`:return`ja-JP`;case`tr`:return`tr-TR`;default:return e}}function Qi(e){return e>=99.5}function $i(e,t){return t>0&&e>=t}function ea(e,t){return $i(e,t)||Qi(e)?`bar-warn`:`bar-green`}function ta(e){let t=Math.max(0,Math.min(100,e));return t<=0?0:Math.max(4,Math.round(t))}function na(e){return{"--bar-scale":String(ta(e)/100)}}function ra(e,t,n=Date.now()){let r=n-e;return!Number.isFinite(r)||r<6e4?null:r<36e5?t(`quota.ageMinutes`).replace(`{n}`,String(Math.floor(r/6e4))):r<864e5?t(`quota.ageHours`).replace(`{n}`,String(Math.floor(r/36e5))):t(`quota.ageDays`).replace(`{n}`,String(Math.floor(r/864e5)))}function ia({quota:e,plan:t,threshold:n,t:r,className:i,layout:a=`compact`,pending:o=!1,incompleteWindowKeys:s,incompleteCustomWindowLabels:c,observedAt:l}){let{locale:u}=ct(),d=Yi(e,t,r),f=l===void 0?null:ra(l,r),p=f===null?null:(0,J.jsx)(`p`,{className:`quota-observed muted`,title:r(`quota.observedHint`),children:r(`quota.observedAgo`).replace(`{age}`,f)});return d.length===0?o?a===`stacked`?(0,J.jsxs)(`div`,{className:`quota-stacked quota-stacked--pending${i?` ${i}`:``}`,"aria-busy":`true`,role:`status`,children:[Array.from({length:2},(e,t)=>(0,J.jsxs)(`div`,{className:`quota-stacked-row quota-stacked-row--skeleton`,"aria-hidden":`true`,children:[(0,J.jsxs)(`div`,{className:`quota-stacked-head`,children:[(0,J.jsx)(`span`,{className:`quota-skel quota-skel--label`,style:{width:72}}),(0,J.jsx)(`span`,{className:`quota-skel quota-skel--time`,style:{width:64}})]}),(0,J.jsxs)(`div`,{className:`quota-stacked-bar-row`,children:[(0,J.jsx)(`span`,{className:`quota-skel quota-skel--bar`,style:{height:6,flex:1}}),(0,J.jsx)(`span`,{className:`quota-skel quota-skel--val`,style:{width:36}})]})]},t)),(0,J.jsx)(`span`,{className:`sr-only`,children:r(`common.loading`)})]}):(0,J.jsxs)(`div`,{className:`codex-account-quota-slot quota-compact quota-compact--pending${i?` ${i}`:``}`,"aria-busy":`true`,role:`status`,children:[(0,J.jsxs)(`div`,{className:`quota-row quota-row--skeleton`,"aria-hidden":`true`,children:[(0,J.jsx)(`span`,{className:`quota-skel quota-skel--label`}),(0,J.jsx)(`span`,{className:`quota-skel quota-skel--reset`}),(0,J.jsx)(`span`,{className:`quota-skel quota-skel--day`}),(0,J.jsx)(`span`,{className:`quota-skel quota-skel--time`}),(0,J.jsx)(`span`,{className:`quota-skel quota-skel--bar`}),(0,J.jsx)(`span`,{className:`quota-skel quota-skel--val`})]}),(0,J.jsx)(`span`,{className:`sr-only`,children:r(`common.loading`)})]}):null:a===`stacked`?(0,J.jsxs)(`div`,{className:`quota-stacked${i?` ${i}`:``}`,children:[p,d.map(e=>(0,J.jsx)(oa,{row:e,threshold:n,t:r,locale:u,incomplete:e.windowKey?s?.has(e.windowKey)===!0:e.customLabel!==void 0&&c?.has(e.customLabel)===!0},e.limitLabel))]}):(0,J.jsxs)(`div`,{className:`codex-account-quota-slot quota-compact${i?` ${i}`:``}`,children:[p,d.map(e=>(0,J.jsx)(aa,{label:e.label,percent:e.percent,resetAt:e.resetAt,threshold:n,t:r,locale:u},e.label))]})}function aa({label:e,percent:t,resetAt:n,threshold:r,t:i,locale:a}){let o=Qi(t),s=$i(t,r),c=ea(t,r),l=ca(n,i,a),u=l.day||l.time?`${i(`codexAuth.resets`)} ${l.day} ${l.time}`.replace(/\s+/g,` `).trim():void 0,d=l.day!==``||l.time!==``;return(0,J.jsxs)(`div`,{className:`quota-row${s?` quota-row--warn`:``}${o?` quota-row--exhausted`:``}`,children:[(0,J.jsx)(`span`,{className:`quota-label`,title:u,children:e}),(0,J.jsx)(`span`,{className:`quota-reset-label`,children:d?i(`codexAuth.resets`):``}),(0,J.jsx)(`span`,{className:`quota-reset-day`,children:l.day}),(0,J.jsx)(`span`,{className:`quota-reset-time`,children:l.time}),(0,J.jsx)(`div`,{className:`bar`,title:u,children:(0,J.jsx)(`div`,{className:`bar-fill ${c}`,style:na(t)})}),(0,J.jsxs)(`span`,{className:`quota-val${s?` quota-val--warn`:``}`,title:o?i(`quota.limitReached`):u,"aria-label":u,children:[s&&(0,J.jsx)(_e,{width:12,height:12,"aria-hidden":`true`}),Math.round(t),`%`,o?` · ${i(`quota.limitReached`)}`:``]})]})}function oa({row:e,threshold:t,t:n,locale:r,incomplete:i}){let a=Qi(e.percent),o=$i(e.percent,t),s=ea(e.percent,t),c=la(e.resetAt,n,r);return(0,J.jsxs)(`div`,{className:`quota-stacked-row${o?` quota-stacked-row--warn`:``}${a?` quota-stacked-row--exhausted`:``}`,children:[(0,J.jsxs)(`div`,{className:`quota-stacked-head`,children:[(0,J.jsxs)(`span`,{className:`quota-stacked-limit-group`,children:[(0,J.jsx)(`span`,{className:`quota-stacked-limit`,children:e.limitLabel}),i&&(0,J.jsx)(`span`,{className:`quota-window-partial`,role:`note`,"aria-label":n(`pws.capacity.windowPartialA11y`,{window:e.limitLabel}),title:n(`pws.capacity.windowPartialA11y`,{window:e.limitLabel}),children:n(`pws.capacity.windowPartial`)})]}),(0,J.jsx)(`span`,{className:`quota-stacked-reset muted`,children:c})]}),(0,J.jsxs)(`div`,{className:`quota-stacked-bar-row`,children:[(0,J.jsx)(`div`,{className:`bar quota-stacked-bar`,children:(0,J.jsx)(`div`,{className:`bar-fill ${s}`,style:na(e.percent)})}),(0,J.jsx)(`span`,{className:`quota-stacked-used${o?` quota-stacked-used--warn`:``}`,children:n(`quota.usedPercent`,{pct:Math.round(e.percent)})})]}),a&&(0,J.jsxs)(`div`,{className:`quota-stacked-limit-reached`,role:`status`,children:[(0,J.jsx)(_e,{width:12,height:12,"aria-hidden":`true`}),n(`quota.limitReached`)]})]})}function sa(e){if(typeof e!=`number`||!Number.isFinite(e))return null;let t=e<1e10?e*1e3:e,n=new Date(t);return Number.isFinite(n.getTime())?{date:n,ms:t}:null}function ca(e,t,n){let r=sa(e);if(!r)return{day:``,time:``};let{date:i}=r,a=new Date,o=Zi(n),s=new Intl.DateTimeFormat(o,{hour:`2-digit`,minute:`2-digit`,hour12:!1}).format(i);return i.getFullYear()===a.getFullYear()&&i.getMonth()===a.getMonth()&&i.getDate()===a.getDate()?{day:t(`codexAuth.today`),time:s}:{day:new Intl.DateTimeFormat(o,{day:`numeric`,month:`short`}).format(i),time:s}}function la(e,t,n=`en`,r=Date.now()){let i=sa(e);if(!i)return``;let{date:a,ms:o}=i,s=Zi(n),c=new Intl.DateTimeFormat(s,{hour:`2-digit`,minute:`2-digit`,hour12:!1}).format(a),l=new Date(r),u=new Date(l.getFullYear(),l.getMonth(),l.getDate()).getTime(),d=new Date(a.getFullYear(),a.getMonth(),a.getDate()).getTime(),f=Math.round((d-u)/864e5);if(f===1)return t(`quota.resetsTomorrow`,{time:c});let p=a.getFullYear()!==l.getFullYear(),m=new Intl.DateTimeFormat(s,{day:`numeric`,month:`short`,...p?{year:`numeric`}:{}}).format(a);if(o<=r)return t(`quota.resetsAt`,{date:m,time:c,when:`${m}, ${c}`});let h=Math.round((o-r)/6e4);if(h<60)return t(`quota.resetsRelativeMinutes`,{n:Math.max(1,h)});let g=Math.round(h/60);return g<12&&f===0?t(`quota.resetsRelativeHours`,{n:Math.max(1,g)}):f===0?t(`quota.resetsToday`,{time:c}):t(`quota.resetsAt`,{date:m,time:c,when:`${m}, ${c}`})}function ua(e){switch(e){case`en`:return`en-GB`;case`de`:return`de-DE`;case`fr`:return`fr-FR`;case`ko`:return`ko-KR`;case`zh`:return`zh-CN`;case`zh-TW`:return`zh-TW`;case`ru`:return`ru-RU`;case`ja`:return`ja-JP`;case`tr`:return`tr-TR`;default:return e}}function da(e){let t=new Date(e>1e10?e:e*1e3);return Number.isFinite(t.getTime())?t:null}function fa({report:e,pending:t}){let n=Q(),{locale:r}=ct(),i=Hi(e),a=Bi(e),o=a?.creditsUsd,s=i?.presentation===`aggregate`,c=new Set,l=new Set;if(s&&i){i.fiveHour?.incomplete&&c.add(`fiveHour`),i.weekly?.incomplete&&c.add(`weekly`),i.monthly?.incomplete&&c.add(`monthly`);for(let e of i.customWindows??[])e.incomplete&&l.add(e.label)}let u=s&&i?[...i.fiveHour?[{key:0,label:n(`codexAuth.fiveHour`),window:i.fiveHour}]:[],...i.weekly?[{key:1,label:n(`codexAuth.weekly`),window:i.weekly}]:[],...i.monthly?[{key:2,label:n(`codexAuth.monthly`),window:i.monthly}]:[],...(i.customWindows??[]).map((e,t)=>({key:t+3,label:e.label,window:e}))]:[],d=e=>new Intl.NumberFormat(r,{maximumFractionDigits:1}).format(e),f=e=>{let t=da(e);return t===null?null:new Intl.DateTimeFormat(r,{dateStyle:`medium`,timeStyle:`short`}).format(t)},p=ua(r),m=e=>new Intl.NumberFormat(p,{style:`currency`,currency:`USD`}).format(e),h=o?.expiresAt===void 0?null:(e=>{let t=da(e);return t===null?null:new Intl.DateTimeFormat(p,{dateStyle:`medium`}).format(t)})(o.expiresAt);return(0,J.jsxs)(J.Fragment,{children:[s&&(0,J.jsx)(`div`,{className:`pws-capacity-label`,children:n(`pws.capacity.estimate`)}),(a||t)&&(0,J.jsx)(ia,{quota:a,threshold:80,t:n,layout:`stacked`,pending:t,incompleteWindowKeys:s?c:void 0,incompleteCustomWindowLabels:s?l:void 0}),(o||i)&&(0,J.jsxs)(`div`,{className:`pws-capacity-details`,children:[o&&(0,J.jsxs)(`div`,{className:`pws-capacity-recovery`,children:[(0,J.jsx)(`span`,{children:n(`quota.creditsBalance`)}),(0,J.jsx)(`strong`,{children:m(o.remaining)})]}),h!==null&&(0,J.jsx)(`div`,{className:`pws-capacity-recovery`,children:(0,J.jsx)(`span`,{children:n(`quota.creditsPeriodEnds`,{date:h})})}),u.flatMap(({key:e,label:t,window:r})=>{let i=r.nextRecoveryAt===void 0?null:f(r.nextRecoveryAt);return i!==null&&r.nextRecoveryPercent!==void 0?[(0,J.jsxs)(`div`,{className:`pws-capacity-recovery`,children:[(0,J.jsxs)(`span`,{children:[n(`pws.capacity.nextRecovery`),` · `,t,` · `,i]}),(0,J.jsx)(`strong`,{children:n(`pws.capacity.recoveryShare`,{percent:d(r.nextRecoveryPercent)})})]},e)]:[]}),s&&i&&i.currentAccount?.quota&&(0,J.jsxs)(`div`,{className:`pws-capacity-current`,children:[(0,J.jsxs)(`span`,{className:`pws-capacity-label`,children:[n(`pws.capacity.currentAccount`),i.currentAccount.plan?` · ${i.currentAccount.plan}`:``]}),(0,J.jsx)(ia,{quota:i.currentAccount.quota,threshold:80,t:n,layout:`stacked`})]}),i&&i.incomplete&&i.excludedAccounts>0&&(0,J.jsx)(`div`,{className:`pws-capacity-incomplete`,children:n(`pws.capacity.incomplete`,{excluded:i.excludedAccounts})}),i&&i.unknownPlanAccounts>0&&(0,J.jsx)(`div`,{className:`pws-capacity-incomplete`,children:n(`pws.capacity.uncalibratedPlan`,{count:i.unknownPlanAccounts})}),i&&i.partialWindowAccounts>0&&(0,J.jsx)(`div`,{className:`pws-capacity-incomplete`,children:n(`pws.capacity.partial`,{count:i.partialWindowAccounts})})]})]})}function pa({sections:e,quotaReports:t,usageTotals:n,usageLoading:r=!1,quotasLoading:i=!1,onSelectProvider:a,onEditConfig:o}){let s=Q(),{locale:c}=ct(),l=Ti(s),u=(0,_.useMemo)(()=>[...e.ready,...e.needsSetup,...e.disabled],[e]),d=(0,_.useMemo)(()=>new Set(u.map(e=>e.name)),[u]),f=(0,_.useMemo)(()=>Ei(e,{}),[e]),p=f.length,m=(0,_.useMemo)(()=>e.ready.filter(e=>e.activeNeedsReauth).length,[e]),h=e.ready.length-m,g=e.needsSetup.length+m,v=(0,_.useMemo)(()=>{let e=[];for(let n of u){let r=t[n.name],i=r?Bi(r):null,a=r?Hi(r):null;r&&(i||a?.presentation===`coverage-only`)&&e.push({item:n,report:r,urgency:i?Xi(i):-1})}return e.sort((e,t)=>t.urgency-e.urgency||e.item.name.localeCompare(t.item.name))},[u,t]),y=(0,_.useMemo)(()=>{let e={};for(let[t,r]of Object.entries(n))d.has(t)&&(e[t]=r);return Si(e).slice(0,4)},[n,d]),b=e=>{let t=Di(e);return t===`reauth`?s(`pws.attention.reauth`):t===`missing`?s(`pws.attention.missingCredentials`):e};return(0,J.jsxs)(`div`,{className:`pws-dashboard`,children:[(0,J.jsxs)(`div`,{className:`pws-dashboard-header`,children:[(0,J.jsx)(`div`,{className:`pws-dashboard-header-text`,children:(0,J.jsx)(`h2`,{className:`pws-dashboard-title`,children:s(`pws.dashboard.title`)})}),o&&(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:o,children:s(`prov.editJson`)})]}),(0,J.jsxs)(`div`,{className:`pws-dashboard-summary`,children:[(0,J.jsx)(ma,{count:h,label:s(`pws.status.ready`),tone:`ok`}),(0,J.jsx)(ma,{count:g,label:s(m>0?`pws.status.needsAttention`:`pws.status.needsSetup`),tone:`warn`}),(0,J.jsx)(ma,{count:e.disabled.length,label:s(`prov.disabledBadge`),tone:`muted`})]}),p>0&&(0,J.jsxs)(`section`,{className:`pws-dashboard-section pws-dashboard-attention`,"aria-label":s(`pws.attentionTitle`),children:[(0,J.jsxs)(`h3`,{className:`pws-dashboard-section-title`,children:[(0,J.jsx)(_e,{style:{width:14,height:14},"aria-hidden":`true`}),s(`pws.attentionTitle`)]}),(0,J.jsx)(`div`,{className:`pws-dashboard-rows`,children:f.map(e=>(0,J.jsxs)(`button`,{type:`button`,className:`pws-dashboard-row pws-dashboard-row--attention`,onClick:()=>a(e.name),children:[(0,J.jsx)(Pi,{name:e.name,adapter:``,baseUrl:``,cls:`pws-dashboard-row-icon`}),(0,J.jsxs)(`div`,{className:`pws-dashboard-row-info`,children:[(0,J.jsx)(`span`,{className:`pws-dashboard-row-name`,children:jn(e.name,s)}),(0,J.jsx)(`span`,{className:`pws-dashboard-row-meta muted`,children:b(e.reason)})]}),(0,J.jsx)(Se,{className:`pws-dashboard-row-chevron`,"aria-hidden":`true`})]},`${e.name}:${e.reason}`))})]}),(0,J.jsxs)(`div`,{className:`pws-dashboard-columns`,children:[(0,J.jsxs)(`section`,{className:`pws-dashboard-section pws-dashboard-section--rate-limits`,"aria-label":s(`pws.dashboard.rateLimits`),"aria-busy":i||void 0,children:[(0,J.jsx)(`h3`,{className:`pws-dashboard-section-title`,children:s(`pws.dashboard.rateLimits`)}),v.length>0?(0,J.jsx)(`div`,{className:`pws-dashboard-rows`,children:v.map(({item:e,report:t})=>(0,J.jsxs)(`button`,{type:`button`,className:`pws-dashboard-row`,onClick:()=>a(e.name),children:[(0,J.jsx)(Pi,{name:e.name,adapter:e.adapter,baseUrl:e.baseUrl,cls:`pws-dashboard-row-icon`}),(0,J.jsxs)(`div`,{className:`pws-dashboard-row-info`,children:[(0,J.jsx)(`span`,{className:`pws-dashboard-row-name`,children:jn(e.name,s)}),(0,J.jsx)(`span`,{className:`pws-dashboard-row-meta muted`,children:s(`pws.dashboard.checkedAgo`,{time:wi(t.updatedAt,l)})})]}),(0,J.jsx)(Se,{className:`pws-dashboard-row-chevron`,"aria-hidden":`true`}),(0,J.jsx)(`div`,{className:`pws-dashboard-row-bars`,children:(0,J.jsx)(fa,{report:t,pending:i&&!t.quota})})]},e.name))}):i?(0,J.jsx)(`div`,{className:`pws-dashboard-rows pws-dashboard-rows--pending`,"aria-hidden":`true`,children:Array.from({length:3},(e,t)=>(0,J.jsxs)(`div`,{className:`pws-dashboard-row pws-dashboard-row--skeleton`,children:[(0,J.jsx)(`span`,{className:`pws-dashboard-row-icon pws-skel`}),(0,J.jsxs)(`div`,{className:`pws-dashboard-row-info`,children:[(0,J.jsx)(`span`,{className:`pws-skel pws-skel--name`}),(0,J.jsx)(`span`,{className:`pws-skel pws-skel--meta`})]}),(0,J.jsx)(`div`,{className:`pws-dashboard-row-bars`,children:(0,J.jsx)(ia,{quota:null,threshold:80,t:s,layout:`stacked`,pending:!0})})]},t))}):(0,J.jsx)(`p`,{className:`muted pws-dashboard-empty`,children:s(`pws.dashboard.noRateLimits`)})]}),(0,J.jsx)(`section`,{className:`pws-dashboard-section pws-dashboard-section--recent`,"aria-label":s(`pws.dashboard.recentlyUsed`),"aria-busy":r||void 0,children:(0,J.jsxs)(`details`,{className:`pws-dashboard-recent-details`,children:[(0,J.jsx)(`summary`,{className:`pws-dashboard-section-title`,children:s(`pws.dashboard.recentlyUsed`)}),y.length>0?(0,J.jsx)(`div`,{className:`pws-dashboard-rows`,children:y.map(e=>(0,J.jsxs)(`button`,{type:`button`,className:`pws-dashboard-row`,onClick:()=>a(e.name),children:[(0,J.jsx)(Pi,{name:e.name,adapter:``,baseUrl:``,cls:`pws-dashboard-row-icon`}),(0,J.jsx)(`span`,{className:`pws-dashboard-row-name`,children:jn(e.name,s)}),(0,J.jsx)(`span`,{className:`pws-dashboard-row-count muted`,children:s(`pws.dashboard.requests`,{count:Oi(e.requests,c)})}),(0,J.jsx)(Se,{className:`pws-dashboard-row-chevron`,"aria-hidden":`true`})]},e.name))}):r?(0,J.jsx)(`div`,{className:`pws-dashboard-rows pws-dashboard-rows--pending`,"aria-hidden":`true`,children:Array.from({length:3},(e,t)=>(0,J.jsxs)(`div`,{className:`pws-dashboard-row pws-dashboard-row--skeleton`,children:[(0,J.jsx)(`span`,{className:`pws-dashboard-row-icon pws-skel`}),(0,J.jsx)(`span`,{className:`pws-skel pws-skel--name`}),(0,J.jsx)(`span`,{className:`pws-skel pws-skel--count`})]},t))}):(0,J.jsx)(`p`,{className:`muted pws-dashboard-empty`,children:s(`pws.dashboard.noUsage`)})]})})]})]})}function ma({count:e,label:t,tone:n}){return(0,J.jsxs)(`div`,{className:`pws-dashboard-card pws-dashboard-card--${n}`,children:[(0,J.jsx)(`span`,{className:`pws-dashboard-card-count`,children:e}),(0,J.jsx)(`span`,{className:`pws-dashboard-card-label`,children:t})]})}function ha({editor:e,providerName:t,saving:n,onSave:r,message:i}){let a=Q(),o=(0,_.useRef)(null);return(0,_.useEffect)(()=>{e.open&&o.current?.focus()},[e.open]),e.open?(0,J.jsxs)(`div`,{className:`pwi-json-panel`,children:[(0,J.jsxs)(`div`,{className:`pwi-json-panel-header`,children:[(0,J.jsx)(`span`,{className:`pwi-json-panel-title`,children:a(`pws.jsonEditorTitle`,{name:t})}),(0,J.jsxs)(`div`,{className:`pwi-json-panel-actions`,children:[e.onRestore&&e.isDirty&&(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:e.onRestore,children:a(`pws.jsonRestore`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:e.onClose,children:a(`common.cancel`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-primary btn-sm`,onClick:r,disabled:n||!e.isDirty,children:a(n?`pws.saving`:`pws.jsonSave`)})]})]}),(0,J.jsx)(`p`,{className:`pwi-json-panel-desc muted`,children:a(`pws.jsonEditorDesc`)}),(0,J.jsx)(`textarea`,{ref:o,className:`input pwi-json-textarea`,value:e.draft,onChange:t=>e.onDraftChange(t.target.value),spellCheck:!1,rows:20,"aria-label":a(`pws.jsonEditorDesc`)}),i&&(0,J.jsx)(`div`,{className:i.ok?`pwi-settings-msg pwi-settings-msg--ok`:`pwi-settings-msg pwi-settings-msg--err`,children:i.text})]}):null}var ga=[{id:`az`,labelKey:`pws.sort.az`},{id:`za`,labelKey:`pws.sort.za`},{id:`free-paid`,labelKey:`pws.sort.freePaid`},{id:`paid-free`,labelKey:`pws.sort.paidFree`},{id:`accounts-first`,labelKey:`pws.sort.accountsFirst`}],_a=18e5;function va(e,t){if(!e||typeof e!=`object`||Array.isArray(e))return null;let n=e;return typeof n.updatedAt!=`number`||!Number.isFinite(n.updatedAt)||t-n.updatedAt>=_a||!(`quota`in n)||n.label!==void 0&&typeof n.label!=`string`||n.source!==void 0&&typeof n.source!=`string`?null:{...typeof n.label==`string`?{label:n.label}:{},...typeof n.source==`string`?{source:n.source}:{},updatedAt:n.updatedAt,quota:n.quota,...n.aggregation===void 0?{}:{aggregation:n.aggregation}}}function ya(e,t=Date.now()){if(!e||typeof e!=`object`||Array.isArray(e))return null;let n={};for(let[r,i]of Object.entries(e)){let e=va(i,t);r.trim()&&e&&(n[r]=e)}return n}function ba(e){return ya(gr(e))}function xa(e,t=Date.now()){if(!Array.isArray(e))return{};let n={};for(let r of e){if(!r||typeof r!=`object`||Array.isArray(r))continue;let e=r.provider,i=va(r,t);typeof e==`string`&&e.trim()&&i&&(n[e]=i)}return n}function Sa({providers:e,apiBase:t,defaultProvider:n,selectedName:r,onSelect:i,onRemoveProvider:a,onAddProvider:o,onEditConfig:s,jsonEditor:c,jsonSaving:l=!1,modelsRefreshToken:u=0,activeAccountNeedsReauth:d,quotaRefreshEpoch:f=0,quotaForceRefresh:p=!1,detail:m}){let h=Q(),[g,v]=(0,_.useState)(``),[y,b]=(0,_.useState)({ready:!0,needsSetup:!0,disabled:!0}),[x,S]=(0,_.useState)({free:!0,paid:!0}),[C,w]=(0,_.useState)({cloud:!0,local:!0,selfHosted:!0,login:!0}),[T,E]=(0,_.useState)(`az`),[D,O]=(0,_.useState)(!1),[k,A]=(0,_.useState)(null),[j,M]=(0,_.useState)({}),[N,P]=(0,_.useState)({}),[F,I]=(0,_.useState)({}),[L,R]=(0,_.useState)({}),[z,B]=(0,_.useState)(!1),[V,H]=(0,_.useState)(!1),U=`ocx.providers.quotas.v1:${t}`,W=`ocx.providers.usage.v1:${t}`,[ee,K]=(0,_.useState)(()=>gr(W)?.totals??{}),[q,Y]=(0,_.useState)(()=>gr(W)?.models??{}),[te,ne]=(0,_.useState)(()=>ba(U)??{}),[re,ie]=(0,_.useState)(()=>!gr(W)),[ae,oe]=(0,_.useState)(()=>{let e=ba(U);return!e||Object.keys(e).length===0}),[se,ce]=(0,_.useState)(0),le=(0,_.useRef)(null),ue=G(Br(t),[t],async e=>{let n=await fetch(t+`/api/usage?range=30d`,{signal:e});if(!n.ok)throw Error(String(n.status));return await n.json()},{deadlineMs:6e4}),de=(0,_.useMemo)(()=>fi(di(mi(e)),d??{}),[e,d]),fe=(0,_.useCallback)(()=>{ce(e=>e+1)},[]);(0,_.useEffect)(()=>{let e=!1,n=window.setTimeout(()=>{B(!0),(async()=>{try{let n=await Pt(await fetch(`${t}/api/selected-models`));if(e)return;M(xi(n)),P(vi(n)),I(yi(n)),R(bi(n)),H(!1)}catch{if(e)return;H(!0)}finally{e||B(!1)}})()},0);return()=>{e=!0,window.clearTimeout(n)}},[t,u,se]),(0,_.useEffect)(()=>{let e=!1,t=window.setTimeout(()=>{let t=ue.data;if(e)return;if(!t){ue.loading&&ie(!gr(W));return}let n={};for(let e of t.providers??[])n[e.provider]={requests:e.requests,totalTokens:e.totalTokens};K(n);let r={};for(let e of t.models??[]){let t=e.provider;r[t]||(r[t]=[]),r[t].push({model:e.model,...e.resolvedModel?{resolvedModel:e.resolvedModel}:{},requests:e.requests,totalTokens:e.totalTokens,inputTokens:e.inputTokens,outputTokens:e.outputTokens,shareRatio:e.shareRatio,...e.estimatedCostUsd===void 0?{}:{estimatedCostUsd:e.estimatedCostUsd}})}Y(r),br(W,{totals:n,models:r}),ie(!1)},0);return()=>{e=!0,window.clearTimeout(t)}},[t,W,ue.data,ue.loading]),(0,_.useEffect)(()=>{let e=!1,n=window.setTimeout(()=>{let n=ba(U);(!n||Object.keys(n).length===0)&&oe(!0),fetch(`${t}/api/provider-quotas${p?`?refresh=1`:``}`).then(e=>Ft(e)).then(t=>{if(e||!t)return;let n=xa(t.reports);ne(n),br(U,n)}).catch(()=>{e||ne(e=>{let t=ya(e)??{};return br(U,t),t})}).finally(()=>{e||oe(!1)})},0);return()=>{e=!0,window.clearTimeout(n)}},[t,f,p,U]),(0,_.useEffect)(()=>{if(!D)return;let e=e=>{le.current&&!le.current.contains(e.target)&&O(!1)},t=e=>{e.key===`Escape`&&O(!1)};return document.addEventListener(`mousedown`,e),window.addEventListener(`keydown`,t),()=>{document.removeEventListener(`mousedown`,e),window.removeEventListener(`keydown`,t)}},[D]);let pe=(0,_.useMemo)(()=>[...de.ready,...de.needsSetup,...de.disabled],[de]),X=(0,_.useMemo)(()=>pe.filter(ci).length,[pe]),me=pe.length-X,ge=(0,_.useMemo)(()=>{let e={cloud:0,local:0,selfHosted:0,login:0};for(let t of pe)e[_i(t)]+=1;return e},[pe]),_e=(0,_.useMemo)(()=>{let e=g.trim().toLowerCase(),t=t=>ui(t.filter(t=>{if(e&&!t.name.toLowerCase().includes(e)&&!t.adapter.toLowerCase().includes(e))return!1;let n=ci(t);return!(n&&!x.free||!n&&!x.paid||!C[_i(t)])}),T);return{ready:y.ready?t(de.ready):[],needsSetup:y.needsSetup?t(de.needsSetup):[],disabled:y.disabled?t(de.disabled):[]}},[de,g,y,x,C,T]),Z=!y.ready||!y.needsSetup||!y.disabled||!x.free||!x.paid||!C.cloud||!C.local||!C.selfHosted||!C.login||T!==`az`,ye=()=>{b({ready:!0,needsSetup:!0,disabled:!0}),S({free:!0,paid:!0}),w({cloud:!0,local:!0,selfHosted:!0,login:!0}),E(`az`)},be=(0,_.useMemo)(()=>r?pe.find(e=>e.name===r)??null:null,[r,pe]),xe=(0,_.useMemo)(()=>{let e=new Map;for(let t of pe){let n=jn(t.name,h);e.set(n,(e.get(n)??0)+1)}let t=new Set;for(let[n,r]of e.entries())r>1&&t.add(n);return t},[pe,h]);if(pe.length===0)return(0,J.jsx)(Ca,{onAddProvider:o});let Se=[{key:`ready`,label:h(`pws.status.ready`),count:de.ready.length},{key:`needsSetup`,label:h(`pws.status.needsSetup`),count:de.needsSetup.length},{key:`disabled`,label:h(`prov.disabledBadge`),count:de.disabled.length}],Ce=[{id:`ready`,label:h(`pws.status.ready`),count:_e.ready.length,ariaLabel:h(`pws.groupReady`,{count:_e.ready.length}),items:_e.ready},{id:`needs-setup`,label:h(`pws.status.needsSetup`),count:_e.needsSetup.length,ariaLabel:h(`pws.groupNeedsSetup`,{count:_e.needsSetup.length}),items:_e.needsSetup},{id:`disabled`,label:h(`prov.disabledBadge`),count:_e.disabled.length,ariaLabel:h(`pws.groupDisabled`,{count:_e.disabled.length}),items:_e.disabled}],we=Ce.flatMap(e=>e.items.map(e=>e.name)),Te=k&&we.includes(k)?k:r&&we.includes(r)?r:we[0]??null;return(0,J.jsx)(`div`,{className:`pws-shell-container`,children:(0,J.jsxs)(`div`,{className:`pws-root`,children:[(0,J.jsxs)(`aside`,{className:`pws-rail`,"aria-label":h(`pws.providerList`),children:[(0,J.jsxs)(`div`,{className:`pws-search-row`,children:[(0,J.jsxs)(`div`,{className:`pws-search-wrap`,children:[(0,J.jsx)(ve,{className:`pws-search-icon`,width:14,height:14,"aria-hidden":`true`}),(0,J.jsx)(`input`,{type:`search`,className:`input pws-search-input`,placeholder:h(`pws.searchPlaceholder`),value:g,onChange:e=>v(e.target.value),"aria-label":h(`pws.searchPlaceholder`)})]}),(0,J.jsxs)(`div`,{className:`pws-filter-wrap`,ref:le,children:[(0,J.jsxs)(`button`,{type:`button`,className:`pws-filter-btn${Z||D?` pws-filter-btn--active`:``}`,onClick:()=>O(e=>!e),"aria-label":h(`pws.filterAria`),"aria-expanded":D,"aria-controls":`pws-provider-filters`,children:[(0,J.jsx)(Le,{width:18,height:18,"aria-hidden":`true`}),Z&&(0,J.jsx)(`span`,{className:`pws-filter-dot`,"aria-hidden":`true`})]}),D&&(0,J.jsxs)(`div`,{id:`pws-provider-filters`,className:`pws-filter-menu`,role:`group`,"aria-label":h(`pws.providerFiltersAria`),children:[(0,J.jsx)(`div`,{className:`pws-filter-title`,children:h(`pws.filters`)}),(0,J.jsx)(`div`,{className:`pws-filter-head`,children:h(`pws.filterStatus`)}),Se.map(({key:e,label:t,count:n})=>(0,J.jsxs)(`label`,{className:`pws-filter-option`,children:[(0,J.jsx)(`input`,{type:`checkbox`,checked:y[e],onChange:()=>b(t=>({...t,[e]:!t[e]}))}),(0,J.jsx)(`span`,{className:`pws-filter-label`,children:t}),(0,J.jsx)(`span`,{className:`pws-filter-count`,children:n})]},e)),(0,J.jsx)(`div`,{className:`pws-filter-head`,children:h(`pws.pricing`)}),(0,J.jsxs)(`label`,{className:`pws-filter-option`,children:[(0,J.jsx)(`input`,{type:`checkbox`,checked:x.free,onChange:()=>S(e=>({...e,free:!e.free}))}),(0,J.jsx)(`span`,{className:`pws-filter-label`,children:h(`modal.badge.free`)}),(0,J.jsx)(`span`,{className:`pws-filter-count`,children:X})]}),(0,J.jsxs)(`label`,{className:`pws-filter-option`,children:[(0,J.jsx)(`input`,{type:`checkbox`,checked:x.paid,onChange:()=>S(e=>({...e,paid:!e.paid}))}),(0,J.jsx)(`span`,{className:`pws-filter-label`,children:h(`pws.paid`)}),(0,J.jsx)(`span`,{className:`pws-filter-count`,children:me})]}),(0,J.jsx)(`div`,{className:`pws-filter-head`,children:h(`pws.filterType`)}),[{key:`cloud`,label:h(`pws.type.cloud`),count:ge.cloud},{key:`local`,label:h(`pws.type.local`),count:ge.local},{key:`selfHosted`,label:h(`pws.type.selfHosted`),count:ge.selfHosted},{key:`login`,label:h(`pws.type.login`),count:ge.login}].map(({key:e,label:t,count:n})=>(0,J.jsxs)(`label`,{className:`pws-filter-option`,children:[(0,J.jsx)(`input`,{type:`checkbox`,checked:C[e],onChange:()=>w(t=>({...t,[e]:!t[e]}))}),(0,J.jsx)(`span`,{className:`pws-filter-label`,children:t}),(0,J.jsx)(`span`,{className:`pws-filter-count`,children:n})]},e)),(0,J.jsx)(`div`,{className:`pws-filter-head`,children:h(`pws.sort`)}),(0,J.jsx)(`div`,{className:`pws-sort-grid`,role:`group`,"aria-label":h(`pws.sortProvidersAria`),children:ga.map(e=>(0,J.jsx)(`button`,{type:`button`,className:`pws-sort-btn${T===e.id?` pws-sort-btn--active`:``}`,onClick:()=>E(e.id),"aria-pressed":T===e.id,children:h(e.labelKey)},e.id))}),(0,J.jsx)(`div`,{className:`pws-filter-footer`,children:(0,J.jsx)(`button`,{type:`button`,className:`link-btn`,onClick:ye,disabled:!Z,children:h(`pws.resetAll`)})})]})]})]}),(0,J.jsxs)(`div`,{className:`pws-rail-list`,role:`listbox`,"aria-label":h(`pws.providersAria`),onKeyDown:e=>{let t=Array.from(e.currentTarget.querySelectorAll(`[role="option"]`));if(t.length===0)return;let n=document.activeElement,r=t.findIndex(e=>e===n||e.contains(n));if(e.key===`ArrowDown`||e.key===`ArrowUp`){e.preventDefault();let n=e.key===`ArrowDown`?1:-1;t[r<0?n>0?0:t.length-1:(r+n+t.length)%t.length]?.focus();return}if(e.key===`Home`){e.preventDefault(),t[0]?.focus();return}e.key===`End`&&(e.preventDefault(),t[t.length-1]?.focus())},children:[Object.values(_e).every(e=>e.length===0)&&(0,J.jsx)(`span`,{className:`muted pws-rail-empty`,role:`status`,children:h(g?`pws.noSearchResults`:Z?`pws.noMatchFilters`:`pws.noProvidersConfigured`)}),Ce.map(({id:e,label:t,count:o,ariaLabel:s,items:c})=>c.length===0?null:(0,J.jsxs)(`div`,{className:`pws-rail-group`,role:`group`,"aria-label":s,children:[(0,J.jsxs)(`div`,{className:`pws-rail-group-head`,"aria-hidden":`true`,children:[(0,J.jsx)(`span`,{className:`pws-rail-group-label`,children:t}),(0,J.jsx)(`span`,{className:`pws-rail-group-count`,children:o})]}),c.map(e=>(0,J.jsxs)(`div`,{className:`pws-rail-row-wrap`,children:[(0,J.jsx)(Ii,{item:e,selected:r===e.name,tabbable:Te===e.name,modelCount:j[e.name],isDefault:n===e.name,showConfigId:xe.has(jn(e.name,h)),onClick:()=>i(e.name),onFocus:()=>A(e.name)}),a&&(0,J.jsx)(`button`,{type:`button`,className:`pws-rail-row-remove`,tabIndex:-1,"aria-hidden":`true`,onClick:t=>{t.stopPropagation(),a(e.name)},title:h(`pws.removeConfirmTitle`),children:(0,J.jsx)(he,{width:14,height:14})})]},e.name))]},e))]})]}),(0,J.jsx)(`main`,{className:`pws-main`,"aria-label":h(`pws.workspaceMainAria`),children:c?.open?(0,J.jsx)(ha,{editor:c,providerName:h(`nav.providers`),saving:l,onSave:()=>{c.onSave()}}):be?m?.(be,{usageTotals:ee[be.name],modelUsage:q[be.name],quotaReport:te[be.name],availableModels:N[be.name]??[],hasLiveModels:(F[be.name]??0)>0,selectedModels:L[be.name]??[],modelsLoading:z,modelsLoadFailed:V,onRetryModels:fe})??(0,J.jsxs)(`div`,{className:`pws-detail-placeholder`,children:[(0,J.jsx)(`h3`,{children:jn(be.name,h)}),(0,J.jsx)(`p`,{className:`muted`,children:h(`pws.detailComingSoon`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:()=>i(null),children:h(`modal.back`)})]}):(0,J.jsx)(pa,{sections:de,quotaReports:te,usageTotals:ee,usageLoading:re,quotasLoading:ae,onSelectProvider:e=>i(e),onEditConfig:s})})]})})}function Ca({onAddProvider:e}){let t=Q();return(0,J.jsx)(`div`,{className:`pws-empty-root`,children:(0,J.jsxs)(`div`,{className:`pws-empty-hero`,children:[(0,J.jsx)(`div`,{"aria-hidden":`true`,children:(0,J.jsx)(re,{style:{width:64,height:64}})}),(0,J.jsx)(`h2`,{children:t(`pws.connectFirst`)}),(0,J.jsxs)(`div`,{className:`pws-empty-tiles`,children:[(0,J.jsxs)(`button`,{type:`button`,className:`pws-empty-tile`,onClick:()=>e({tier:`free`}),children:[(0,J.jsx)(`span`,{"aria-hidden":`true`,children:(0,J.jsx)(Ne,{width:18,height:18})}),(0,J.jsx)(`span`,{className:`pws-empty-tile-label`,children:t(`pws.empty.browseFree`)}),(0,J.jsx)(`span`,{className:`pws-empty-tile-desc muted`,children:t(`pws.empty.browseFreeDesc`)})]}),(0,J.jsxs)(`button`,{type:`button`,className:`pws-empty-tile`,onClick:()=>e({tier:`accounts`}),children:[(0,J.jsx)(`span`,{"aria-hidden":`true`,children:(0,J.jsx)(De,{width:18,height:18})}),(0,J.jsx)(`span`,{className:`pws-empty-tile-label`,children:t(`pws.empty.connectAccount`)}),(0,J.jsx)(`span`,{className:`pws-empty-tile-desc muted`,children:t(`pws.empty.connectAccountDesc`)})]}),(0,J.jsxs)(`button`,{type:`button`,className:`pws-empty-tile`,onClick:()=>e({custom:!0}),children:[(0,J.jsx)(`span`,{"aria-hidden":`true`,children:(0,J.jsx)(Ee,{width:18,height:18})}),(0,J.jsx)(`span`,{className:`pws-empty-tile-label`,children:t(`pws.empty.addEndpoint`)}),(0,J.jsx)(`span`,{className:`pws-empty-tile-desc muted`,children:t(`pws.empty.addEndpointDesc`)})]})]})]})})}function wa(e){if(si(e.name,e))return`codex-accounts`;let t=(e.authMode??``).toLowerCase();if(t===`forward`||t===`local`||hi(e))return null;if(t===`oauth`)return`oauth-accounts`;let n=e.hasApiKey===!0;return!(t===`key`||n||t===``)||e.keyOptional===!0&&!n?null:`api-keys`}function Ta(e,t,n){let r=t.alias?.trim();if(r)return r;let i=t.email?.trim();if(i)return i;let a=e.findIndex(e=>e.id===t.id);return n(`pws.accountOrdinal`,{count:String(a>=0?a+1:1)})}function Ea({item:e,usageTotals:t,quotaReport:n,oauthEmail:r,oauth:i,apiBase:a,connectionIdentity:o,onEditSettings:s,onViewUsage:c,onUpdateProvider:l,onReauthenticate:u,onCancelLogin:d,reauthBusy:f=!1}){let p=Q(),{locale:m}=ct(),h=Ti(p),g=pi(e),v=!!e.activeNeedsReauth,y=p(g===`ready`?`pws.status.connected`:g===`needs-setup`?v?`pws.status.needsAttention`:`pws.status.needsSetup`:`prov.disabledBadge`),b=t?.requests,x=t?.totalTokens,S=Bi(n),C=JSON.stringify([a??null,e.name,e.adapter,e.baseUrl,e.authMode??null,e.apiKeyTransport??null,e.liveModels??null,e.disabled===!0,e.hasApiKey===!0,e.hasHeaders===!0,e.allowPrivateNetwork===!0,e.keyOptional===!0,e.activeNeedsReauth===!0,o??null]),[w,T]=(0,_.useState)(null),E=(0,_.useRef)(null),D=w?.key===C&&w.testing,O=w?.key===C?w.result:null;(0,_.useEffect)(()=>()=>{E.current?.key===C&&(E.current.controller.abort(),E.current=null)},[C]);let k=(0,_.useCallback)(async()=>{if(!a)return;E.current?.controller.abort();let t=new AbortController;E.current={key:C,controller:t},T({key:C,testing:!0,result:null});try{let n=await Pt(await fetch(`${a}/api/providers/test?name=${encodeURIComponent(e.name)}`,{method:`POST`,signal:t.signal}),p(`pws.connectionFailed`));if(!n)throw Error(p(`pws.connectionFailed`));t.signal.aborted||T({key:C,testing:!1,result:n})}catch(e){t.signal.aborted||T({key:C,testing:!1,result:{applicable:!0,ok:!1,error:e instanceof Error?e.message:p(`pws.connectionFailed`)}})}finally{E.current?.controller===t&&(E.current=null)}},[a,C,e.name,p]),A=O?.applicable===!1?`not-applicable`:O?.ok===!0?`ok`:`failed`,j=O?.applicable===!1?p(`pws.connectionNotApplicable`):O?.ok===!0?O.message||p(`pws.connectionOk`):O?.error||p(`pws.connectionFailed`);return(0,J.jsxs)(`div`,{className:`pws-overview-layout`,children:[(0,J.jsxs)(`div`,{className:`pws-overview-main`,children:[(0,J.jsxs)(`section`,{className:`pws-section`,"aria-label":p(`pws.connection`),children:[(0,J.jsx)(`h3`,{className:`pws-section-title`,children:p(`pws.connection`)}),(0,J.jsxs)(`dl`,{className:`pws-kv`,children:[(0,J.jsxs)(`div`,{className:`pws-kv-row`,children:[(0,J.jsx)(`dt`,{children:p(`dash.status`)}),(0,J.jsxs)(`dd`,{className:g===`ready`?`pws-status-ok`:`pws-status-warn`,children:[g===`ready`?(0,J.jsx)(ue,{style:{width:13,height:13},"aria-hidden":`true`}):(0,J.jsx)(_e,{style:{width:13,height:13},"aria-hidden":`true`}),y]})]}),(0,J.jsxs)(`div`,{className:`pws-kv-row`,children:[(0,J.jsx)(`dt`,{children:p(`modal.baseUrl`)}),(0,J.jsx)(`dd`,{children:(0,J.jsx)(`code`,{children:e.baseUrl?.trim()?e.baseUrl:`—`})})]}),(0,J.jsxs)(`div`,{className:`pws-kv-row`,children:[(0,J.jsx)(`dt`,{children:p(`pws.cell.auth`)}),(0,J.jsx)(`dd`,{children:r?`${Mi(e,p)} · ${r}`:Mi(e,p)})]}),(0,J.jsxs)(`div`,{className:`pws-kv-row`,children:[(0,J.jsx)(`dt`,{children:p(`modal.defaultModel`)}),(0,J.jsx)(`dd`,{children:e.defaultModel??(0,J.jsx)(`span`,{className:`muted`,children:`—`})})]}),e.note&&(0,J.jsxs)(`div`,{className:`pws-kv-row`,children:[(0,J.jsx)(`dt`,{children:p(`pws.cell.note`)}),(0,J.jsx)(`dd`,{className:`muted`,children:e.note})]})]}),a&&(0,J.jsxs)(`div`,{className:`row`,style:{marginTop:12,alignItems:`center`},children:[(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,disabled:D,onClick:()=>void k(),children:p(D?`pws.testing`:`pws.testConnection`)}),O&&(0,J.jsx)(`span`,{role:`status`,className:A===`ok`?`pws-status-ok`:A===`failed`?`pws-status-warn`:`muted`,"data-connection-test-state":A,children:j})]}),s&&(0,J.jsx)(`button`,{type:`button`,className:`link-btn pws-edit-settings-link`,onClick:s,children:p(`pws.editSettings`)})]}),n&&(0,J.jsxs)(`section`,{className:`pws-section`,"aria-label":p(`pws.rateLimits`),children:[(0,J.jsx)(`h3`,{className:`pws-section-title`,children:p(`pws.rateLimits`)}),(0,J.jsx)(fa,{report:n,pending:!1})]}),(0,J.jsxs)(`section`,{className:`pws-section`,"aria-label":p(`pws.authSummary`),children:[(0,J.jsx)(`h3`,{className:`pws-section-title`,children:p(`pws.authSummary`)}),v?(0,J.jsxs)(`div`,{className:`pws-auth-summary pws-auth-summary--warn`,role:`status`,children:[(0,J.jsx)(_e,{style:{width:14,height:14},"aria-hidden":`true`}),(0,J.jsxs)(`div`,{className:`pws-auth-summary-body`,children:[(0,J.jsxs)(`span`,{children:[(0,J.jsx)(`strong`,{children:p(`pws.status.needsAttention`)}),` — `,e.authMode===`forward`?p(`pws.attention.reauthForward`):p(`pws.attention.reauth`)]}),u&&(0,J.jsx)(`button`,{type:`button`,className:`btn btn-primary btn-sm`,disabled:f,onClick:()=>u(),children:p(f?`prov.waitingBrowser`:`pws.reauthenticate`)}),f&&d&&(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:()=>d(),children:p(`common.cancel`)})]})]}):(0,J.jsxs)(`div`,{className:`pws-auth-summary`,children:[(0,J.jsx)(`span`,{className:`pws-auth-dot`}),(0,J.jsx)(`span`,{children:e.authMode===`forward`?p(`pws.passthrough`):e.authMode===`oauth`?r?p(`pws.loggedInAs`,{email:r}):i?.loggedIn?p(`pws.loggedInTitle`):p(`pws.notLoggedIn`):e.hasApiKey?p(`pws.apiKeyConfigured`):Mi(e,p)})]})]})]}),(0,J.jsxs)(`aside`,{className:`pws-overview-sidebar`,children:[(0,J.jsxs)(`section`,{className:`pws-section`,"aria-label":p(`pws.statsAria`),children:[(0,J.jsx)(`h3`,{className:`pws-section-title`,children:p(`pws.statsTitle`)}),(0,J.jsxs)(`dl`,{className:`pws-kv`,children:[typeof b==`number`&&(0,J.jsxs)(`div`,{className:`pws-kv-row`,children:[(0,J.jsx)(`dt`,{children:p(`pws.stats.totalRequests`)}),(0,J.jsx)(`dd`,{className:`pws-kv-mono`,children:Oi(b,m)})]}),typeof x==`number`&&(0,J.jsxs)(`div`,{className:`pws-kv-row`,children:[(0,J.jsx)(`dt`,{children:p(`pws.stats.totalTokens`)}),(0,J.jsx)(`dd`,{className:`pws-kv-mono`,children:ki(x,m)})]}),n&&(0,J.jsxs)(`div`,{className:`pws-kv-row`,children:[(0,J.jsx)(`dt`,{children:p(`pws.stats.quotaUpdated`)}),(0,J.jsx)(`dd`,{className:`pws-kv-mono`,title:n.source?Ui(n.source):void 0,children:wi(n.updatedAt,h)})]}),typeof b!=`number`&&typeof x!=`number`&&!n&&(0,J.jsx)(`div`,{className:`muted`,children:p(`pws.usageUnavailable`)})]}),c&&(0,J.jsxs)(`button`,{type:`button`,className:`link-btn pws-view-usage-link`,onClick:c,children:[p(`pws.viewUsage`),` →`]}),S&&(0,J.jsx)(`div`,{className:`muted pws-stats-note`,children:p(`pws.stats.quotaTracked`)})]}),(0,J.jsx)(Da,{item:e,onUpdateProvider:l})]})]})}function Da({item:e,onUpdateProvider:t}){let n=Q(),[r,i]=(0,_.useState)(!1),[a,o]=(0,_.useState)(``),[s,c]=(0,_.useState)(!1),[l,u]=(0,_.useState)(``),d=(0,_.useRef)(null);(0,_.useEffect)(()=>{r&&d.current?.focus()},[r]);let f=(0,_.useCallback)(async()=>{if(s||!t)return;let r=a.trim();if(r===(e.note??``)){i(!1),u(``);return}c(!0);try{let a=await t(e.name,{note:r||void 0});if(!a.ok){u(a.error||n(`prov.saveFailed`));return}u(``),i(!1)}finally{c(!1)}},[a,e.name,e.note,t,s,n]);return r?(0,J.jsxs)(`section`,{className:`pws-section pws-notes-section`,"aria-label":n(`pws.notes`),children:[(0,J.jsx)(`h3`,{className:`pws-section-title`,children:n(`pws.notes`)}),(0,J.jsx)(`textarea`,{ref:d,className:`pws-notes-textarea`,value:a,onChange:e=>o(e.target.value),onBlur:()=>void f(),onKeyDown:t=>{t.key===`Escape`&&(o(e.note??``),u(``),i(!1))},placeholder:n(`pws.notePlaceholder`),rows:3,disabled:s}),l?(0,J.jsx)(`p`,{className:`pws-inline-error`,role:`alert`,children:l}):null]}):(0,J.jsxs)(`section`,{className:`pws-section pws-notes-section`,"aria-label":n(`pws.notes`),children:[(0,J.jsx)(`h3`,{className:`pws-section-title`,children:n(`pws.notes`)}),(0,J.jsx)(`button`,{type:`button`,className:`pws-notes-display`,onClick:()=>{t&&(o(e.note??``),u(``),i(!0))},disabled:!t,children:e.note||(0,J.jsx)(`span`,{className:`muted`,children:n(`pws.notePlaceholder`)})})]})}function Oa(e){return e.includes(`/`)?e.replaceAll(`/`,`-`):e}function ka(e,t){let n=Oa(e);for(let r of t)if(r!==e&&Oa(r)===n)return!0;return!1}function Aa({item:e,apiBase:t,availableModels:n,hasLiveModels:r,selectedModels:i,modelsLoading:a=!1,modelsLoadFailed:o=!1,needsReauth:s=!1,onRetryModels:c,onOpenAccounts:l}){let u=Q(),[d,f]=(0,_.useState)(``),[p,m]=(0,_.useState)(``),[h,g]=(0,_.useState)(!1),[v,y]=(0,_.useState)(``),[b,x]=(0,_.useState)(``),[S,C]=(0,_.useState)([]),[w,T]=(0,_.useState)(!1),[E,D]=(0,_.useState)(!1),[O,k]=(0,_.useState)(0),[A,j]=(0,_.useState)(null),M=(0,_.useRef)(null),N=(0,_.useMemo)(()=>new Set(i),[i]),P=(0,_.useMemo)(()=>e.models??[],[e.models]),F=p.trim(),I=[...n,...S,...P,...e.defaultModel?[e.defaultModel]:[]],L=!w||!F||n.includes(F)||S.includes(F)||P.includes(F)||e.defaultModel===F||ka(F,I),R=(0,_.useMemo)(()=>Wi(n,e.defaultModel,d,P,S,r),[n,e.defaultModel,d,P,S,r]);(0,_.useEffect)(()=>{let n=!0;return(async()=>{try{let r=await fetch(`${t}/api/custom-models`);if(!r.ok)throw Error();let i=await r.json();if(!Array.isArray(i))throw Error(`Invalid custom model list`);if(!n)return;C(i.flatMap(t=>{if(!t||typeof t!=`object`)return[];let n=t;return n.provider===e.name&&typeof n.modelId==`string`?[n.modelId]:[]})),D(!1),y(``),T(!0)}catch{if(!n)return;C([]),T(!1),D(!0),y(u(`models.networkError`))}})(),()=>{n=!1}},[t,e.name,u,O]);let z=()=>{T(!1),D(!1),y(``),k(e=>e+1)};(0,_.useEffect)(()=>()=>{M.current!=null&&window.clearTimeout(M.current)},[]);let B=async e=>{try{await navigator.clipboard.writeText(e),j(e),M.current!=null&&window.clearTimeout(M.current),M.current=window.setTimeout(()=>{j(t=>t===e?null:t),M.current=null},1200)}catch{}},V=async()=>{if(!(L||h)){g(!0),y(``),x(``);try{(await fetch(`${t}/api/custom-models`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({provider:e.name,modelId:F})})).ok?(C(e=>e.includes(F)?e:[...e,F]),m(``),x(u(`models.customAdded`)),c?.()):y(u(`models.customSaveFailed`))}catch{y(u(`models.networkError`))}finally{g(!1)}}},H=n.length===0&&P.length===0&&S.length===0&&!e.defaultModel,U=n.length===0&&P.length>0,W=R.length>300,ee=W?R.slice(0,300):R;return(0,J.jsxs)(`div`,{className:`pws-section`,children:[(0,J.jsxs)(`div`,{className:`pws-section-head`,children:[(0,J.jsx)(`h3`,{className:`pws-section-title`,children:u(`pws.tab.models`)}),R.length>0&&(0,J.jsx)(`span`,{className:`muted`,children:u(`pws.modelsAvailable`,{count:R.length})})]}),s&&(0,J.jsxs)(`div`,{className:`pws-inline-error`,role:`status`,children:[(0,J.jsx)(`span`,{children:u(`pws.modelsNeedsReauth`)}),l&&(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:l,children:u(`pws.tab.accounts`)})]}),U&&!s&&(0,J.jsx)(`p`,{className:`muted text-label`,style:{marginBottom:10},children:u(`pws.modelsConfiguredFallback`)}),(0,J.jsx)(`label`,{className:`text-label pws-custom-model-label`,htmlFor:`pws-custom-model-${e.name}`,children:u(`models.customAdd`)}),(0,J.jsxs)(`div`,{className:`row pws-custom-model-row`,children:[(0,J.jsx)(`input`,{id:`pws-custom-model-${e.name}`,className:`input`,value:p,onChange:e=>m(e.target.value),onKeyDown:e=>{e.key===`Enter`&&V()},placeholder:u(`models.customFieldModelIdPlaceholder`),"aria-label":u(`models.customAdd`),disabled:h}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-primary btn-sm`,onClick:()=>{V()},disabled:h||L,children:u(h?`models.customSaving`:`models.customAddBtn`)})]}),b&&(0,J.jsx)(`p`,{className:`muted text-label`,role:`status`,children:b}),v&&(0,J.jsxs)(`p`,{className:`pws-inline-error`,role:`alert`,children:[v,E&&(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:z,style:{marginLeft:8},children:u(`common.retry`)})]}),!H&&(0,J.jsx)(`input`,{type:`search`,className:`input pws-model-search`,placeholder:u(`pws.modelSearchPlaceholder`),value:d,onChange:e=>f(e.target.value),"aria-label":u(`pws.modelSearchPlaceholder`)}),a&&H?(0,J.jsx)(`p`,{className:`muted`,role:`status`,children:u(`pws.modelsLoading`)}):o&&H?(0,J.jsxs)(`div`,{role:`alert`,className:`pws-inline-error`,children:[(0,J.jsx)(`span`,{children:u(`pws.modelsLoadFailed`)}),c&&(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:c,children:u(`pws.retry`)})]}):H?(0,J.jsx)(`p`,{className:`muted`,children:u(`pws.noModels`)}):R.length===0?(0,J.jsx)(`p`,{className:`muted`,role:`status`,children:u(`pws.noModelMatch`)}):(0,J.jsx)(`ul`,{className:`pws-model-list`,children:ee.map(t=>{let n=t===e.defaultModel,r=N.has(t);return(0,J.jsxs)(`li`,{className:`pws-model-chip`,children:[(0,J.jsx)(`button`,{type:`button`,className:`pws-model-chip-main`,onClick:()=>{B(t)},title:t,"aria-label":u(A===t?`pws.modelCopied`:`pws.copyModelId`),children:(0,J.jsx)(`span`,{className:`pws-model-id`,children:t})}),n?(0,J.jsx)(`span`,{className:`badge badge-muted pws-model-flag`,children:u(`prov.defaultBadge`)}):null,r?(0,J.jsx)(`span`,{className:`badge badge-accent pws-model-flag`,children:u(`pws.selected`)}):null]},t)})}),W&&(0,J.jsx)(`p`,{className:`muted text-label`,style:{marginTop:10},children:u(`pws.modelsTruncated`,{shown:`300`,total:String(R.length)})})]})}function ja({item:e,usageTotals:t,quotaReport:n,modelUsage:r}){let i=Q(),{locale:a}=ct(),o=Ti(i),s=t?.requests!==void 0,c=Bi(n),[l,u]=(0,_.useState)(null),d=(0,_.useMemo)(()=>r?.length?r.toSorted((e,t)=>t.totalTokens-e.totalTokens):[],[r]),f=(0,_.useMemo)(()=>{if(!d.length)return;let e=0,t=!1;for(let n of d)n.estimatedCostUsd!==void 0&&(e+=n.estimatedCostUsd,t=!0);return t?e:void 0},[d]);return(0,J.jsxs)(`div`,{className:`pws-section`,children:[(0,J.jsxs)(`div`,{className:`pws-usage-block`,children:[(0,J.jsx)(`h3`,{className:`pws-section-title`,children:i(`pws.usageLast30d`)}),s?(0,J.jsxs)(J.Fragment,{children:[(0,J.jsxs)(`div`,{className:`pws-usage-metrics pws-usage-metrics-3`,role:`group`,"aria-label":i(`pws.usageLast30d`),children:[(0,J.jsxs)(`div`,{className:`pws-usage-metric`,children:[(0,J.jsx)(`span`,{className:`pws-usage-metric-value mono`,children:Ai(f,a)}),(0,J.jsx)(`span`,{className:`muted pws-usage-metric-label`,children:i(`pws.estimatedCost`)})]}),(0,J.jsxs)(`div`,{className:`pws-usage-metric`,children:[(0,J.jsx)(`span`,{className:`pws-usage-metric-value`,children:Oi(t?.requests,a)}),(0,J.jsx)(`span`,{className:`muted pws-usage-metric-label`,children:i(`pws.metricRequests`)})]}),(0,J.jsxs)(`div`,{className:`pws-usage-metric`,children:[(0,J.jsx)(`span`,{className:`pws-usage-metric-value`,children:ki(t?.totalTokens,a)}),(0,J.jsx)(`span`,{className:`muted pws-usage-metric-label`,children:i(`pws.metricTokens`)})]})]}),(0,J.jsx)(`p`,{className:`muted pws-cost-disclaimer`,children:i(`pws.costDisclaimer`)})]}):(0,J.jsx)(`p`,{className:`muted`,children:i(`pws.usageUnavailable`)})]}),d.length>0&&(0,J.jsxs)(`div`,{className:`pws-usage-block`,children:[(0,J.jsx)(`h3`,{className:`pws-section-title`,children:i(`pws.modelBreakdown`)}),(0,J.jsx)(`div`,{className:`tbl-wrap`,children:(0,J.jsxs)(`table`,{className:`pws-model-table`,children:[(0,J.jsx)(`thead`,{children:(0,J.jsxs)(`tr`,{children:[(0,J.jsx)(`th`,{children:i(`pws.col.model`)}),(0,J.jsx)(`th`,{className:`num`,children:i(`pws.col.cost`)}),(0,J.jsx)(`th`,{className:`num`,children:i(`pws.col.tokens`)}),(0,J.jsx)(`th`,{className:`num`,children:i(`pws.col.requests`)}),(0,J.jsx)(`th`,{children:i(`pws.col.share`)})]})}),(0,J.jsx)(`tbody`,{children:d.map(e=>{let t=e.model,n=l===t;return(0,J.jsxs)(_.Fragment,{children:[(0,J.jsxs)(`tr`,{className:`pws-model-row`,children:[(0,J.jsx)(`td`,{className:`mono`,children:(0,J.jsx)(`button`,{type:`button`,className:`pws-model-expand`,"aria-expanded":n,onClick:()=>u(n?null:t),children:e.model})}),(0,J.jsx)(`td`,{className:`num mono`,children:Ai(e.estimatedCostUsd,a)}),(0,J.jsx)(`td`,{className:`num mono`,children:ki(e.totalTokens,a)}),(0,J.jsx)(`td`,{className:`num`,children:e.requests}),(0,J.jsx)(`td`,{children:(0,J.jsx)(`div`,{className:`pws-share-bar`,children:(0,J.jsx)(`div`,{className:`pws-share-bar-fill`,style:{width:`${Math.round(e.shareRatio*100)}%`}})})})]}),n&&(0,J.jsx)(`tr`,{className:`pws-model-detail`,children:(0,J.jsx)(`td`,{colSpan:5,children:(0,J.jsxs)(`div`,{className:`pws-model-detail-grid`,children:[(0,J.jsxs)(`div`,{children:[(0,J.jsx)(`span`,{className:`muted`,children:i(`pws.tokenInput`)}),(0,J.jsxs)(`span`,{className:`mono`,children:[` `,ki(e.inputTokens,a)]})]}),(0,J.jsxs)(`div`,{children:[(0,J.jsx)(`span`,{className:`muted`,children:i(`pws.tokenOutput`)}),(0,J.jsxs)(`span`,{className:`mono`,children:[` `,ki(e.outputTokens,a)]})]})]})})})]},t)})})]})})]}),(0,J.jsxs)(`div`,{className:`pws-usage-block`,children:[(0,J.jsx)(`h3`,{className:`pws-section-title`,children:i(`pws.rateLimits`)}),c?(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)(ia,{quota:c,plan:null,threshold:80,t:i,layout:`stacked`}),(0,J.jsxs)(`dl`,{className:`pws-kv pws-usage-meta`,children:[n?.source?.trim()&&(0,J.jsxs)(`div`,{className:`pws-kv-row`,children:[(0,J.jsx)(`dt`,{children:i(`pws.stats.source`)}),(0,J.jsx)(`dd`,{children:Ui(n.source)})]}),(0,J.jsxs)(`div`,{className:`pws-kv-row`,children:[(0,J.jsx)(`dt`,{children:i(`pws.stats.quotaUpdated`)}),(0,J.jsx)(`dd`,{children:wi(n?.updatedAt,o)})]})]})]}):(0,J.jsx)(`p`,{className:`muted`,children:i(`pws.quotaUnavailable`)})]})]})}function Ma(e){if(!e)return null;let t=e.trim();return t?t.length<=4?`account-…`:`account-…${t.slice(-4)}`:null}function Na(e){return Ma(e)??`account-…`}function Pa(e){return e===`healthy`?`ok`:e===`cooldown`?`muted`:e===`reauth_required`||e===`warning`?`warn`:`muted`}function Fa(e){let t=Pa(e);return t===`ok`?`badge badge-green`:t===`warn`?`badge badge-amber`:`badge badge-muted`}function Ia(e){return e===`reauth_required`}function La(e){return!!e?.needsReauth||Ia(e?.health?.status)}function Ra(e){return e===`cooldown`}function za(e){return e===`warning`||e===`reauth_required`}function Ba(e){if(!e||e.status===`healthy`)return null;if(e.status===`cooldown`)return e.reason===`rate_limit`?`pws.healthLabel.rateLimited`:`pws.healthLabel.quotaLimited`;if(e.status===`reauth_required`)return e.reason===`refresh_failed`?`pws.healthLabel.refreshFailed`:`pws.healthLabel.reauthRequired`;switch(e.reason){case`refresh_conflict`:return`pws.healthLabel.credentialConflict`;case`metadata_mismatch`:return`pws.healthLabel.metadataMismatch`;case`stale_credentials`:return`pws.healthLabel.refreshFailed`;default:return`pws.healthLabel.reauthRequired`}}function Va(e,t){let n=Ba(t);return n?e(n):null}function Ha(e,t,n,r){if(!r||r.status===`healthy`)return null;let i=n===`__main__`?e(`codexAuth.mainAccount`):Na(n);if(r.status===`cooldown`){let n=r.until?new Date(r.until).toLocaleString():``;return e(r.reason===`rate_limit`?`pws.healthSummary.rateLimited`:`pws.healthSummary.quotaLimited`,{provider:t,account:i,until:n})}return r.status===`reauth_required`?e(`pws.healthSummary.reauthRequired`,{provider:t,account:i}):r.reason===`refresh_conflict`?e(`pws.healthSummary.credentialConflict`,{provider:t,account:i}):r.reason===`metadata_mismatch`?e(`pws.healthSummary.metadataMismatch`,{provider:t,account:i}):e(`pws.healthSummary.staleCredentials`,{provider:t,account:i})}async function Ua(e){let t=navigator.clipboard?.writeText?.bind(navigator.clipboard);if(t)try{return await t(e),!0}catch{}return Wa(e)}function Wa(e){if(typeof document>`u`||typeof document.execCommand!=`function`)return!1;let t=document.createElement(`textarea`);t.value=e,t.setAttribute(`readonly`,``),t.setAttribute(`aria-hidden`,`true`),t.style.position=`fixed`,t.style.top=`0`,t.style.opacity=`0`,document.body.appendChild(t);try{return t.select(),document.execCommand(`copy`)}catch{return!1}finally{t.remove()}}function Ga(e,t){return e(t?t===`copied`?`pws.doctorCopied`:`pws.doctorCopyUnavailable`:`pws.copyDoctor`)}var Ka=e=>({step:e?`oauth-waiting`:`pick`,id:``,error:``,authUrl:``,deviceCode:``,instructions:``,manualCode:``,manualCodeState:`idle`,statusNotice:``,statusTone:`ok`,flowId:null});function qa(e,t){switch(t.type){case`set-step`:return{...e,step:t.step};case`set-id`:return{...e,id:t.id};case`set-error`:return{...e,error:t.error};case`set-auth-url`:return{...e,authUrl:t.authUrl};case`set-login-hint`:return{...e,authUrl:t.authUrl,deviceCode:t.deviceCode??``,instructions:t.instructions??``};case`set-manual-code`:return{...e,manualCode:t.manualCode};case`set-manual-code-state`:return{...e,manualCodeState:t.manualCodeState};case`set-status-notice`:return{...e,statusNotice:t.statusNotice,statusTone:t.statusTone??e.statusTone};case`set-flow-id`:return{...e,flowId:t.flowId};case`clear-manual-code`:return{...e,manualCode:``,manualCodeState:`idle`,statusNotice:``,statusTone:`ok`};case`reset-oauth-start`:return{...e,error:``,statusNotice:``,statusTone:`ok`,flowId:null};case`oauth-code-submitted`:return{...e,error:``,manualCode:``,manualCodeState:`waiting`,statusTone:`ok`,statusNotice:``};default:return e}}function Ja({id:e,error:t,onIdChange:n,onStartOAuth:r,onStartDeviceOAuth:i,onClose:a}){let o=Q();return(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)(`h3`,{style:{marginBottom:4},children:o(`codexAuth.addTitle`)}),(0,J.jsx)(`p`,{className:`modal-desc`,children:o(`codexAuth.addPickDesc`)}),(0,J.jsx)(`label`,{className:`field-label`,htmlFor:`codex-account-id-input`,children:o(`codexAuth.addIdLabel`)}),(0,J.jsx)(`input`,{id:`codex-account-id-input`,className:`input`,placeholder:o(`codexAuth.addIdPlaceholder`),value:e,onChange:e=>n(e.target.value),style:{marginBottom:12}}),(0,J.jsx)(`button`,{type:`button`,className:`list-row`,onClick:r,style:{marginBottom:8},children:(0,J.jsxs)(`div`,{style:{display:`flex`,alignItems:`center`,gap:10},children:[(0,J.jsx)(Ne,{width:20}),(0,J.jsxs)(`div`,{children:[(0,J.jsx)(`div`,{className:`title`,children:o(`codexAuth.oauthLogin`)}),(0,J.jsx)(`div`,{className:`sub`,children:o(`codexAuth.oauthDesc`)})]})]})}),(0,J.jsx)(`button`,{type:`button`,className:`list-row`,onClick:i,style:{marginBottom:8},children:(0,J.jsxs)(`div`,{style:{display:`flex`,alignItems:`center`,gap:10},children:[(0,J.jsx)(ke,{width:20}),(0,J.jsxs)(`div`,{children:[(0,J.jsx)(`div`,{className:`title`,children:o(`codexAuth.deviceLogin`)}),(0,J.jsx)(`div`,{className:`sub`,children:o(`codexAuth.deviceDesc`)})]})]})}),t&&(0,J.jsx)(`div`,{className:`notice notice-err`,style:{marginTop:8},children:t}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost`,onClick:a,style:{width:`100%`},children:o(`codexAuth.cancel`)})]})}var Ya=2500;function Xa(){let[e,t]=(0,_.useState)(null),n=(0,_.useRef)(null),r=(0,_.useRef)(0),i=(0,_.useCallback)(()=>{n.current&&=(clearTimeout(n.current),null)},[]);return(0,_.useEffect)(()=>i,[i]),{outcomeFor:(0,_.useCallback)(t=>e&&Object.is(e.scope,t)?e.outcome:null,[e]),copy:(0,_.useCallback)((e,a)=>{let o=++r.current;Ua(e).then(e=>{r.current===o&&(i(),t({scope:a,outcome:e?`copied`:`unavailable`}),n.current=setTimeout(()=>{n.current=null,r.current===o&&t(null)},Ya))})},[i])}}function Za({url:e}){let t=Q(),{outcomeFor:n,copy:r}=Xa();if(!e)return null;let i=n(e),a=t(i===`copied`?`prov.linkCopied`:i===`unavailable`?`prov.linkCopyUnavailable`:`prov.copyLink`);return(0,J.jsxs)(`div`,{className:`login-url-block`,children:[(0,J.jsx)(`code`,{className:`login-url-block-text`,children:e}),(0,J.jsxs)(`div`,{className:`login-url-block-actions`,children:[(0,J.jsxs)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:()=>r(e,e),children:[(0,J.jsx)(ke,{style:{width:13,height:13},"aria-hidden":`true`}),(0,J.jsx)(`span`,{"aria-live":`polite`,children:a})]}),(0,J.jsxs)(`a`,{href:e,target:`_blank`,rel:`noreferrer`,className:`login-url-block-open`,children:[(0,J.jsx)(Te,{style:{width:13,height:13},"aria-hidden":`true`}),` `,t(`prov.didntOpen`)]})]})]})}function Qa({hint:e,paste:t}){let n=Q(),r=Xa(),i=e.deviceCode??``,a=e.url??``;if(!i&&!a&&!e.instructions&&!t)return null;let o=r.outcomeFor(i),s=n(o===`copied`?`prov.codeCopied`:o===`unavailable`?`prov.linkCopyUnavailable`:`prov.copyCode`);return(0,J.jsxs)(`div`,{className:`login-hint`,children:[i&&(0,J.jsxs)(`div`,{className:`login-hint-device pwi-device-code-wrap`,children:[(0,J.jsx)(`span`,{className:`text-label`,children:n(`prov.deviceCode`)}),(0,J.jsx)(`code`,{className:`login-hint-device-code pwi-device-code`,children:i}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-primary btn-sm`,onClick:()=>r.copy(i,i),children:(0,J.jsx)(`span`,{"aria-live":`polite`,children:s})})]}),(0,J.jsx)(Za,{url:a}),e.instructions&&(0,J.jsx)(`div`,{className:`muted text-label`,children:e.instructions}),t&&(0,J.jsxs)(`div`,{className:`login-hint-paste`,children:[(0,J.jsx)(`div`,{className:`muted text-label`,children:n(`prov.pasteRedirectHint`)}),(0,J.jsxs)(`div`,{className:`login-hint-paste-row`,children:[(0,J.jsx)(`input`,{type:`text`,autoComplete:`off`,spellCheck:!1,value:t.value,onChange:e=>t.onChange(e.target.value),onKeyDown:e=>{e.key===`Enter`&&(e.preventDefault(),t.onSubmit())},placeholder:n(`prov.pasteRedirect`),"aria-label":n(`prov.pasteRedirect`),disabled:t.busy,className:`input text-label login-hint-paste-input`}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost`,disabled:t.busy||t.disabled===!0||!t.value.trim(),onClick:t.onSubmit,children:t.busy?t.submittingLabel??n(`prov.pasteSubmitting`):n(`prov.pasteSubmit`)})]}),t.message&&(0,J.jsx)(`div`,{className:`text-label`,"aria-live":`polite`,style:{color:t.ok?`var(--accent-hover)`:`var(--amber)`},children:t.message})]})]})}function $a({reauthAccountId:e,authUrl:t,deviceCode:n,instructions:r,manualCode:i,manualCodeBusy:a,manualCodeWaiting:o,statusNotice:s,statusTone:c,flowId:l,error:u,onSwitchToDevice:d,onManualCodeChange:f,onSubmitManualCode:p,onClose:m}){let h=Q();return(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)(`h3`,{style:{marginBottom:4},children:h(e?`codexAuth.reauthenticate`:`codexAuth.oauthLogin`)}),(0,J.jsx)(`p`,{className:`modal-desc`,children:h(`codexAuth.oauthWaiting`)}),(0,J.jsx)(Qa,{hint:{url:t,deviceCode:n,instructions:r},paste:{value:i,busy:a,disabled:a||o||!i.trim()||!l,submittingLabel:h(`codexAuth.oauthSubmittingCode`),message:``,ok:!0,onChange:f,onSubmit:p}}),s&&(0,J.jsx)(`div`,{className:c===`warn`?`notice-warn`:`notice notice-ok`,role:`status`,"aria-live":`polite`,style:{marginTop:12},children:s}),u&&(0,J.jsx)(`div`,{className:`notice notice-err`,style:{marginTop:12},children:u}),d&&!n&&(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:d,style:{width:`100%`,marginTop:8},children:h(`codexAuth.deviceLogin`)}),(0,J.jsx)(`div`,{style:{textAlign:`center`,padding:`24px 0`},children:(0,J.jsx)(`span`,{className:`spin`,style:{width:24,height:24}})}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost`,onClick:m,style:{width:`100%`},children:h(`codexAuth.cancel`)})]})}var eo=`ocx.oauth.openBrowser`;function to(){try{let e=window.localStorage.getItem(eo);return e===`0`?!1:e===`1`||void 0}catch{return}}function no(){let e=to();return e===void 0?{}:{openBrowser:e}}function ro(e){try{window.localStorage.setItem(eo,e?`1`:`0`)}catch{}}function io(e){if(typeof e!=`object`||!e||Array.isArray(e))return{catalogRefreshPending:!1};let t=Object.getOwnPropertyDescriptor(e,`catalogRefreshPending`);return{catalogRefreshPending:t!==void 0&&`value`in t&&t.value===!0}}var ao=3e5,oo=96e4;function so({apiBase:e,reauthAccountId:t,ui:n,dispatch:r,t:i}){let a=(0,_.useRef)(!0),o=(0,_.useRef)(0),s=(0,_.useRef)(!1),c=(0,_.useRef)(null),l=(0,_.useRef)(null),u=(0,_.useRef)(null),d=(0,_.useRef)(null),f=(0,_.useRef)(n.manualCodeState),p=(0,_.useRef)(null),m=(0,_.useRef)(null),h=(0,_.useRef)(()=>{}),g=(0,_.useRef)(()=>{}),v=n.manualCodeState===`submitting`,y=n.manualCodeState===`waiting`;(0,_.useEffect)(()=>{f.current=n.manualCodeState},[n.manualCodeState]),(0,_.useEffect)(()=>{d.current=n.flowId},[n.flowId]);let b=(0,_.useCallback)(()=>{c.current&&=(c.current(),null),l.current&&=(clearTimeout(l.current),null),u.current?.abort(),u.current=null,s.current=!1},[]),x=(0,_.useCallback)(()=>{r({type:`clear-manual-code`}),o.current=0},[r]),S=(0,_.useCallback)(async()=>{x();let t=d.current;d.current=null,r({type:`set-flow-id`,flowId:null}),r({type:`set-login-hint`,authUrl:``}),b(),p.current?.abort(),p.current=null,t&&await fetch(`${e}/api/codex-auth/login/cancel`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({flowId:t})}).catch(()=>{})},[e,x,r,b]);(0,_.useEffect)(()=>(a.current=!0,()=>{x(),a.current=!1,m.current=null,p.current?.abort(),p.current=null;let t=d.current;d.current=null,r({type:`set-flow-id`,flowId:null}),b(),t&&fetch(`${e}/api/codex-auth/login/cancel`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({flowId:t})}).catch(()=>{})}),[e,x,r,b]);let C=(0,_.useCallback)((e,t)=>{h.current=e,g.current=t},[]),w=(0,_.useCallback)(()=>{n.step===`oauth-waiting`&&S(),g.current()},[n.step,S]),T=(0,_.useCallback)(async(n,m)=>{x(),d.current=null,r({type:`set-flow-id`,flowId:null});let _=new AbortController;p.current?.abort(),p.current=_,r({type:`reset-oauth-start`}),o.current=0;try{let p=t??n?.trim()??``,v=m?.device===!0,y=()=>fetch(`${e}/api/codex-auth/login`,{signal:_.signal,method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({...no(),...v?{device:!0}:{},...t?{id:t,reauth:!0}:p?{id:p}:{}})}),C=await y();if(!a.current)return;if(C.status===409){if(await fetch(`${e}/api/codex-auth/login/cancel`,{method:`POST`,headers:{"Content-Type":`application/json`},body:`{}`}),!a.current||_.signal.aborted)return;if(C=await y(),C.status===409){r({type:`set-error`,error:i(`codexAuth.oauthAlreadyInProgress`)});return}}let w=await Pt(C,i(`modal.networkError`));if(!a.current||!w)return;if(w.url){d.current=w.flowId??null,r({type:`set-flow-id`,flowId:w.flowId??null}),r({type:`set-login-hint`,authUrl:w.url,deviceCode:w.deviceCode,instructions:w.instructions}),r({type:`set-step`,step:`oauth-waiting`}),b();let n=w.flowId??``,m=t?`&reauth=1`:``,_=n?`${e}/api/codex-auth/login-status?flowId=${encodeURIComponent(n)}${p?`&accountId=${encodeURIComponent(p)}`:``}${m}`:`${e}/api/codex-auth/login-status`,y=new AbortController;u.current=y,c.current=Gn(async()=>{if(s.current||y.signal.aborted)return;s.current=!0;let e=AbortSignal.any([y.signal,AbortSignal.timeout(1e4)]);try{let n=await Ft(await fetch(_,{signal:e}));if(!a.current||y.signal.aborted)return;if(!n){o.current+=1,o.current>=3&&r({type:`set-status-notice`,statusNotice:i(`codexAuth.oauthStatusRetrying`),statusTone:`warn`});return}if(o.current=0,f.current===`waiting`?r({type:`set-status-notice`,statusNotice:i(`codexAuth.oauthCodeSubmitted`),statusTone:`ok`}):r({type:`set-status-notice`,statusNotice:``,statusTone:`ok`}),n.status===`done`){if(b(),x(),d.current=null,r({type:`set-flow-id`,flowId:null}),!a.current)return;h.current(io(n)),g.current()}else(n.status===`error`||n.status===`expired`)&&(b(),x(),d.current=null,r({type:`set-flow-id`,flowId:null}),a.current&&(t||r({type:`set-step`,step:`pick`}),r({type:`set-error`,error:n.error??i(`codexAuth.loginFailed`)})))}catch(e){if(!a.current||y.signal.aborted||e instanceof Error&&e.name===`AbortError`)return;o.current+=1,o.current>=3&&r({type:`set-status-notice`,statusNotice:i(`codexAuth.oauthStatusRetrying`),statusTone:`warn`})}finally{s.current=!1}},2e3),l.current=setTimeout(()=>{c.current&&(x(),S(),a.current&&(t||r({type:`set-step`,step:`pick`}),r({type:`set-error`,error:i(`modal.loginTimeout`)})))},v?oo:ao)}w.error&&!w.url&&r({type:`set-error`,error:w.error})}catch(e){a.current&&!(e instanceof Error&&e.name===`AbortError`)&&r({type:`set-error`,error:e instanceof Error?e.message:String(e)})}},[e,S,x,r,t,b,i]);return(0,_.useEffect)(()=>{if(!t){m.current=null;return}m.current!==t&&(m.current=t,T())},[t,T]),{manualCodeBusy:v,manualCodeWaiting:y,bindCallbacks:C,closeModal:w,startOAuth:T,submitManualCode:(0,_.useCallback)(async()=>{let t=d.current,s=n.manualCode.trim();if(!(!t||!s||v||y)){r({type:`set-manual-code-state`,manualCodeState:`submitting`}),r({type:`set-status-notice`,statusNotice:``,statusTone:`ok`});try{let n=await fetch(`${e}/api/codex-auth/login/code`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({flowId:t,input:s})});if(!a.current)return;if(!n.ok){r({type:`set-error`,error:i(`prov.pasteFail`,{error:(await n.json().catch(()=>({}))).error??n.statusText})}),r({type:`set-manual-code-state`,manualCodeState:`idle`});return}r({type:`oauth-code-submitted`}),r({type:`set-status-notice`,statusNotice:i(`codexAuth.oauthCodeSubmitted`),statusTone:`ok`}),o.current=0}catch{a.current&&(r({type:`set-error`,error:i(`modal.networkError`)}),r({type:`set-manual-code-state`,manualCodeState:`idle`}))}}},[e,r,v,y,i,n.manualCode])}}function co({apiBase:e,onClose:t,onAdded:n,reauthAccountId:r}){let i=Q(),[a,o]=(0,_.useReducer)(qa,r,Ka),s=(0,_.useRef)(null),c=(0,_.useRef)(null),{manualCodeBusy:l,manualCodeWaiting:u,bindCallbacks:d,closeModal:f,startOAuth:p,submitManualCode:m}=so({apiBase:e,reauthAccountId:r,ui:a,dispatch:o,t:i});(0,_.useEffect)(()=>{d(n,t)},[d,n,t]),(0,_.useEffect)(()=>{s.current=document.activeElement;let e=c.current;e&&!e.open&&e.showModal();let t=e?.querySelector(`input:not([disabled]), button:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex='-1'])`);return t&&t.focus(),()=>{s.current?.focus()}},[]);let h=(0,_.useCallback)(e=>{e.preventDefault(),f()},[f]),g=i(r?`codexAuth.reauthenticate`:`codexAuth.addTitle`);return(0,J.jsx)(`dialog`,{ref:c,"aria-label":g,className:`modal-overlay`,onCancel:h,children:(0,J.jsxs)(`div`,{className:`modal-card`,style:{maxWidth:440},children:[a.step===`pick`&&(0,J.jsx)(Ja,{id:a.id,error:a.error,onIdChange:e=>o({type:`set-id`,id:e}),onStartOAuth:()=>{p(a.id)},onStartDeviceOAuth:()=>{p(a.id,{device:!0})},onClose:f}),a.step===`oauth-waiting`&&(0,J.jsx)($a,{reauthAccountId:r,authUrl:a.authUrl,deviceCode:a.deviceCode,instructions:a.instructions,manualCode:a.manualCode,manualCodeBusy:l,manualCodeWaiting:u,statusNotice:a.statusNotice,statusTone:a.statusTone,flowId:a.flowId,error:a.error,onSwitchToDevice:()=>{p(a.id,{device:!0})},onManualCodeChange:e=>o({type:`set-manual-code`,manualCode:e}),onSubmitManualCode:()=>{m()},onClose:f})]})})}var lo=[2,1,0,-1,-2],uo=new Map([[2,`accountPool.priorityFirst`],[1,`accountPool.priorityEarlier`],[0,`accountPool.priorityNormal`],[-1,`accountPool.priorityLater`],[-2,`accountPool.priorityLast`]]);function fo(e){return typeof e==`number`&&Number.isInteger(e)&&e>=-100&&e<=100?e:0}function po(e){let t=fo(e);return t>0?`+${t}`:String(t)}function mo(e){return uo.has(e)}function ho(e){return uo.get(e)??null}function go(e,t){let n=fo(t);return e(`accountPool.priorityOption`,{name:e(ho(n)??`accountPool.priorityCustom`),value:po(n)})}var _o=1e4;function vo(e){return typeof e==`number`&&Number.isInteger(e)&&e>=0&&e<=100?e:80}function yo(e){let t=e.trim();if(!/^\d+$/.test(t))return null;let n=Number(t);return n>=1&&n<=100?n:null}function bo(e,t){return e>0?0:Number.isInteger(t)&&t>=1&&t<=100?t:80}function xo(e,t,n,r){return n===r?e||t?`defer`:`apply`:`ignore`}function So(e){return e&&typeof e==`object`&&e&&`autoSwitchThreshold`in e?e.autoSwitchThreshold:e}function Co(e,t,n){if(e<=0){let t=bo(e,n);return{threshold:t,lastEnabled:t}}return{threshold:0,lastEnabled:yo(t)??bo(0,n)}}async function wo(e,t,n=(e,t)=>fetch(e,t),r=_o){if(!Number.isInteger(t)||t<0||t>100)return!1;try{return(await n(`${e}/api/codex-auth/auto-switch`,{method:`PUT`,headers:{"Content-Type":`application/json`},body:JSON.stringify({threshold:t}),signal:AbortSignal.timeout(r)})).ok}catch{return!1}}var To=3e4,Eo=new Map;function Do(e,t=!0){let n=Eo.get(e),[r,i]=(0,_.useState)(()=>n?.accounts??[]),a=G(Br(e,`codex`),[e],async t=>{let n=await fetch(`${e}/api/usage?range=30d&surface=codex`,{signal:t});if(!n.ok)throw Error(`account usage load failed`);return n.json()},{enabled:t}),[o,s]=(0,_.useState)(()=>n?.activeId??null),[c,l]=(0,_.useState)(()=>n==null?`loading`:`ready`),[u,d]=(0,_.useState)(null),[f,p]=(0,_.useState)(null),[m,h]=(0,_.useState)(null),[g,v]=(0,_.useState)(!1),[y,b]=(0,_.useState)(null),[x,S]=(0,_.useState)(0),[C,w]=(0,_.useState)(()=>n!=null),[T,E]=(0,_.useState)(0),D=(0,_.useRef)(null);D.current===null&&(D.current=new Set);let O=(0,_.useRef)(null),k=(0,_.useRef)(0),A=(0,_.useRef)(null),j=(0,_.useRef)(null);j.current===null&&(j.current=new Set);let M=(0,_.useRef)(null),N=(0,_.useRef)(null),P=(0,_.useRef)(!!n?.accounts.length),F=(0,_.useRef)(n!=null),I=(0,_.useRef)(null),L=(0,_.useRef)(null),R=(0,_.useCallback)(e=>(j.current.add(e),()=>{j.current.delete(e)}),[]),z=(0,_.useCallback)(()=>So(M.current?.value),[]),B=(0,_.useCallback)(()=>M.current?.value,[]),V=(0,_.useCallback)(async(t=!1)=>{let n=++k.current,r=Vn(2e4);S(e=>e+1);try{let a=[...j.current],o=new Map;for(let e of a)o.set(e,e.beginActiveRead());!t&&!F.current&&l(`loading`);let c=null,u,d=(async()=>{try{let a=await fetch(`${e}/api/codex-auth/accounts${t?`?refresh=1`:``}`,{signal:r.signal});if(!a.ok)throw Error(`account load failed`);let o=await a.json();return k.current===n&&(c=(o.accounts??[]).map(e=>{let t=e.isMain?`main`:e.logLabel;return{...e,...t?{logLabel:t}:{},priority:fo(e.priority)}}),i(c),P.current=c.length>0,F.current=!0,l(`ready`)),!0}catch{return!1}})(),f=(async()=>{try{let t=await fetch(`${e}/api/codex-auth/active`,{signal:r.signal});if(!t.ok)throw Error(`active account load failed`);let i=await t.json();if(k.current===n){let e=i.activeCodexAccountId??null,t=A.current;t&&e!==t.id||(A.current=null,u=e,s(e)),M.current={value:i},b(typeof i.pinnedAccountId==`string`?i.pinnedAccountId:null);for(let e of a)e.acceptActiveRead(i,o.get(e))}return!0}catch{if(k.current===n)for(let e of a)e.rejectActiveRead();return!1}})(),[p,m]=await Promise.all([d,f]);if(k.current!==n)return!1;if(p){l(`ready`),F.current=!0;let t=Eo.get(e);return Eo.set(e,{accounts:c??t?.accounts??[],activeId:u===void 0?t?.activeId??null:u}),m}return F.current||l(`error`),!1}finally{r.clear(),S(e=>Math.max(0,e-1)),w(!0)}},[e]);(0,_.useEffect)(()=>{t&&O.current!==e&&(O.current=e,Promise.resolve().then(()=>{V()}))},[e,t,V]);let H=r.some(e=>e.hasCredential&&!e.quota);(0,_.useEffect)(()=>{if(!t||!H||T>0)return;let e=[350,900,2e3].map(e=>window.setTimeout(()=>{V(!1)},e));return()=>{for(let t of e)window.clearTimeout(t)}},[t,H,T,V]),(0,_.useEffect)(()=>{if(!(!t||T>0))return Gn(()=>{V()},To)},[t,V,T]);let U=(0,_.useCallback)(()=>{let e={};return D.current.add(e),E(D.current.size),e},[]),W=(0,_.useCallback)(e=>{D.current.delete(e)&&E(D.current.size)},[]),ee=(0,_.useCallback)(async t=>{if(N.current||L.current)return{ok:!1,reason:`busy`};N.current=t??`__main__`,d(t??`__main__`);try{let n=await fetch(`${e}/api/codex-auth/active`,{method:`PUT`,headers:{"Content-Type":`application/json`},body:JSON.stringify({accountId:t})});if(!n.ok)throw Error(`account switch failed`);let r=(await n.json().catch(()=>({}))).activeCodexAccountId??t;return A.current={id:r??null},s(r??null),b(r??`__main__`),V(),{ok:!0,activeId:r??null}}catch{return{ok:!1,reason:`request`}}finally{N.current=null,d(null)}},[e,V]),K=(0,_.useCallback)(async(t,n)=>{try{return(await fetch(`${e}/api/codex-auth/accounts/alias`,{method:`PUT`,headers:{"Content-Type":`application/json`},body:JSON.stringify({id:t,alias:n.trim()})})).ok?(await V(),{ok:!0}):{ok:!1,reason:`request`}}catch{return{ok:!1,reason:`request`}}},[e,V]),q=(0,_.useCallback)(async(t,n)=>{if(I.current)return{ok:!1,reason:`busy`};I.current={accountId:t},p(t);try{let r=await fetch(`${e}/api/codex-auth/accounts/pause`,{method:`PUT`,headers:{"Content-Type":`application/json`},body:JSON.stringify({id:t,paused:n})});if(!r.ok)return{ok:!1,reason:`request`};let a=await r.json().catch(()=>({})),o=a&&typeof a==`object`?a:{};if(i(e=>e.map(e=>e.id===t||t===`__main__`&&e.isMain?{...e,paused:n}:e)),Object.prototype.hasOwnProperty.call(o,`activeCodexAccountId`)){let e=o.activeCodexAccountId??null;A.current={id:e},s(e)}return n&&b(e=>e===t?null:e),V(),{ok:!0}}catch{return{ok:!1,reason:`request`}}finally{I.current=null,p(null)}},[e,V]),J=(0,_.useCallback)(async(t,n)=>{if(L.current||N.current)return{ok:!1,reason:`busy`};L.current={accountId:t},h(t);try{let r=await fetch(`${e}/api/codex-auth/accounts/priority`,{method:`PUT`,headers:{"Content-Type":`application/json`},body:JSON.stringify({id:t,priority:n})});if(!r.ok)return{ok:!1,reason:`request`};let a=await r.json().catch(()=>({})),o=fo((a&&typeof a==`object`?a:{}).priority??n);return i(e=>e.map(e=>e.id===t||t===`__main__`&&e.isMain?{...e,priority:o}:e)),b(null),A.current=null,V(),{ok:!0}}catch{return{ok:!1,reason:`request`}}finally{L.current=null,h(null)}},[e,V]),Y=(0,_.useCallback)(async()=>{if(I.current)return{ok:!1,reason:`busy`};I.current=`bulk`,v(!0);try{let t=await fetch(`${e}/api/codex-auth/accounts/pause-exhausted`,{method:`PUT`});if(!t.ok)return{ok:!1,reason:`request`};let n=await t.json().catch(()=>({})),r=n&&typeof n==`object`?n:{},a=new Set(r.pausedAccountIds??[]);if(i(e=>e.map(e=>a.has(e.id)||a.has(`__main__`)&&e.isMain?{...e,paused:!0}:e)),Object.prototype.hasOwnProperty.call(r,`activeCodexAccountId`)){let e=r.activeCodexAccountId??null;A.current={id:e},s(e)}return b(e=>e!==null&&a.has(e)?null:e),V(),{ok:!0,pausedCount:r.pausedCount??a.size}}catch{return{ok:!1,reason:`request`}}finally{I.current=null,v(!1)}},[e,V]),te=(0,_.useCallback)(async t=>{try{let n=await fetch(`${e}/api/codex-auth/accounts?id=${encodeURIComponent(t)}`,{method:`DELETE`});if(!n.ok)return{ok:!1,reason:`request`};let r=io(await n.json().catch(()=>({})));return await V(),{ok:!0,...r}}catch{return{ok:!1,reason:`request`}}},[e,V]),ne=(0,_.useCallback)(async()=>await V()?{ok:!0}:{ok:!1,reason:`reload`},[V]),re=o&&o!==`__main__`?r.find(e=>e.id===o):null,ie=r.find(e=>e.isMain),ae=re??ie,oe=!ae?.paused&&La(ae);return{accounts:(0,_.useMemo)(()=>{let e=new Map((a.data?.accounts??[]).map(e=>[e.accountLogLabel,e]));return r.map(t=>{let n=t.isMain?`main`:t.logLabel,r=n?e.get(n):void 0;return r?{...t,usage30d:r}:t})},[r,a.data]),activeId:o,loadState:c,refreshing:x>0,initialLoading:!C,switchingId:u,pauseUpdatingId:f,priorityUpdatingId:m,pausingExhausted:g,activeNeedsReauth:oe,activePinnedId:y,load:V,switchAccount:ee,setAccountPaused:q,setAccountPriority:J,pauseExhaustedAccounts:Y,saveAlias:K,removeAccount:te,syncAfterAccountAdded:ne,pauseRefresh:U,resumeRefresh:W,subscribeLoadObserver:R,readLastThreshold:z,readLastActive:B}}function Oo(e,t,n,r,i=1){let a=e.trim(),o=a===``?NaN:Number(a),s=Math.min(r,Math.max(n,(Number.isFinite(o)?o:n)+t));return String(i<1?Math.round(s*10)/10:Math.round(s))}function ko({disabled:e=!1,onIncrement:t,onDecrement:n,incrementLabel:r,decrementLabel:i}){return(0,J.jsxs)(`div`,{className:`ocx-stepper`,role:`group`,children:[(0,J.jsx)(`button`,{type:`button`,className:`ocx-stepper__btn`,disabled:e,"aria-label":r,onMouseDown:e=>e.preventDefault(),onClick:t,children:(0,J.jsx)(ye,{width:10,height:10,"aria-hidden":`true`})}),(0,J.jsx)(`button`,{type:`button`,className:`ocx-stepper__btn`,disabled:e,"aria-label":i,onMouseDown:e=>e.preventDefault(),onClick:n,children:(0,J.jsx)(be,{width:10,height:10,"aria-hidden":`true`})})]})}var Ao={quota:{on:`codexAuth.autoSwitchQuotaDesc`,off:`codexAuth.autoSwitchQuotaOffDesc`},"round-robin":{on:`codexAuth.autoSwitchRoundRobinDesc`,off:`codexAuth.autoSwitchRoundRobinDesc`},"fill-first":{on:`codexAuth.autoSwitchFillFirstDesc`,off:`codexAuth.autoSwitchFillFirstOffDesc`}};function jo({threshold:e,draft:t,strategy:n=`quota`,hydrated:r=!0,saving:i,loadError:a,feedback:o,onDraftChange:s,onEditingChange:c,onCommit:l,onCancel:u,onToggle:d,onRetry:f}){let p=Q(),m=(0,_.useRef)(!1),h=e>0,g=Ao[n][h?`on`:`off`],v=i||!r,y=i?p(`common.saving`):o?.message??``,b=i?`pending`:o?.tone,x=y?`codex-auto-switch-desc codex-auto-switch-feedback`:`codex-auto-switch-desc`;return(0,J.jsxs)(`div`,{className:`card card-row codex-auto-switch-card`,style:{marginTop:16},"aria-busy":i||!r&&!a||void 0,children:[(0,J.jsxs)(`div`,{className:`codex-auto-switch-copy`,children:[(0,J.jsx)(`strong`,{children:p(`codexAuth.autoSwitch`)}),(0,J.jsx)(`div`,{id:`codex-auto-switch-desc`,className:`card-sub`,role:a?`alert`:void 0,children:a?p(`codexAuth.autoSwitchLoadFailed`):p(g,{threshold:e})}),(0,J.jsx)(`div`,{className:`card-sub`,children:p(`codexAuth.failureRecoveryNote`)}),(0,J.jsx)(`div`,{className:`card-sub`,children:p(`codexAuth.cacheWarning`)})]}),(0,J.jsxs)(`div`,{className:`codex-auto-switch-controls`,onBlur:e=>{if(!e.currentTarget.contains(e.relatedTarget)){if(c(!1),m.current){m.current=!1;return}h&&!v&&l()}},children:[a&&(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:f,children:p(`pws.retryAccounts`)}),h&&(0,J.jsxs)(`label`,{className:`codex-auto-switch-threshold`,children:[(0,J.jsx)(`span`,{className:`field-label`,children:p(`codexAuth.autoSwitchThreshold`)}),(0,J.jsxs)(`span`,{className:`codex-auto-switch-input-wrap`,children:[(0,J.jsx)(`input`,{className:`input mono codex-auto-switch-input`,type:`number`,min:1,max:100,step:1,inputMode:`numeric`,value:t,readOnly:v,"aria-disabled":v,"aria-label":p(`codexAuth.autoSwitchThresholdAria`),"aria-describedby":x,onChange:e=>s(e.target.value),onFocus:()=>{v||c(!0)},onKeyDown:e=>{e.nativeEvent.isComposing||v||(e.key===`Enter`?(e.preventDefault(),l()):e.key===`Escape`&&(e.preventDefault(),u()))}}),(0,J.jsx)(`span`,{className:`codex-auto-switch-unit`,"aria-hidden":`true`,children:`%`}),(0,J.jsx)(ko,{disabled:v,incrementLabel:p(`codexAuth.autoSwitchThresholdInc`),decrementLabel:p(`codexAuth.autoSwitchThresholdDec`),onIncrement:()=>{c(!0),s(Oo(t,1,1,100))},onDecrement:()=>{c(!0),s(Oo(t,-1,1,100))}})]})]}),(0,J.jsx)(`span`,{className:`codex-auto-switch-toggle-slot`,children:(0,J.jsx)(`button`,{type:`button`,className:`toggle ${h?`on`:``}`,onPointerDownCapture:()=>{m.current=!0},onPointerUp:()=>{m.current=!1},onPointerCancel:()=>{m.current=!1},onClick:()=>{m.current=!1,d()},disabled:v,"aria-pressed":h,"aria-label":p(`codexAuth.autoSwitch`),"aria-describedby":x,title:p(`codexAuth.autoSwitch`),children:(0,J.jsx)(`span`,{className:`toggle-knob`})})})]}),y&&(0,J.jsx)(`div`,{id:`codex-auto-switch-feedback`,className:`codex-auto-switch-feedback${b===`err`?` is-error`:``}`,role:b===`err`?`alert`:`status`,"aria-atomic":`true`,children:y})]})}var Mo=[`quota`,`round-robin`,`fill-first`],No=[`five-hour`,`weekly`,`max-utilization`],Po=`quota`,Fo=`five-hour`,Io=new Set(Mo),Lo=new Set(No);function Ro(e){return typeof e==`string`&&Io.has(e)?e:Po}function zo(e){return typeof e==`string`&&Lo.has(e)?e:Fo}function Bo(e){return typeof e==`number`&&Number.isInteger(e)&&e>=1&&e<=100?e:1}function Vo(e){let t=e.trim();if(!/^\d+$/.test(t))return null;let n=Number(t);return n>=1&&n<=100?n:null}async function Ho(e,t,n=(e,t)=>fetch(e,t)){if(t.strategy===void 0&&t.stickyLimit===void 0)return{ok:!1};try{let r=await n(`${e}/api/codex-auth/pool-strategy`,{method:`PUT`,headers:{"Content-Type":`application/json`},body:JSON.stringify({...t.strategy===void 0?{}:{strategy:t.strategy},...t.stickyLimit===void 0?{}:{stickyLimit:t.stickyLimit}})});if(!r.ok)return{ok:!1};let i=await r.json();return{ok:!0,strategy:Ro(i.accountPoolStrategy??t.strategy),stickyLimit:Bo(i.accountPoolStickyLimit??t.stickyLimit)}}catch{return{ok:!1}}}var Uo={quota:`accountPool.strategyQuota`,"round-robin":`accountPool.strategyRoundRobin`,"fill-first":`accountPool.strategyFillFirst`},Wo={quota:`accountPool.strategyHintQuota`,"round-robin":`accountPool.strategyHintRoundRobin`,"fill-first":`accountPool.strategyHintFillFirst`};function Go({strategy:e,stickyDraft:t,disabled:n=!1,strategySelectId:r=`account-pool-strategy`,stickyInputId:i=`account-pool-sticky-limit`,onStrategyChange:a,onStickyDraftChange:o,onStickyCommit:s}){let c=Q(),l=Mo.map(e=>({value:e,label:c(Uo[e])}));return(0,J.jsxs)(`div`,{className:`account-pool-strategy-controls`,children:[(0,J.jsxs)(`div`,{className:`setting-row`,children:[(0,J.jsxs)(`div`,{className:`setting-label`,children:[(0,J.jsx)(`span`,{className:`title`,id:`${r}-label`,children:c(`accountPool.strategy`)}),(0,J.jsx)(`span`,{className:`desc`,children:c(`accountPool.strategyDesc`)}),(0,J.jsx)(`span`,{className:`desc`,children:c(Wo[e])}),(0,J.jsx)(`span`,{className:`desc`,children:c(`accountPool.unboundDefinition`)})]}),(0,J.jsx)(`div`,{className:`setting-controls`,children:(0,J.jsx)(Dt,{id:r,value:e,options:l,disabled:n,label:c(`accountPool.strategy`),onChange:e=>a(e)})})]}),e===`round-robin`&&(0,J.jsxs)(`div`,{className:`setting-row`,children:[(0,J.jsxs)(`label`,{className:`setting-label`,htmlFor:i,children:[(0,J.jsx)(`span`,{className:`title`,children:c(`accountPool.stickyLimit`)}),(0,J.jsx)(`span`,{className:`desc`,children:c(`accountPool.stickyLimitHelp`)})]}),(0,J.jsx)(`div`,{className:`setting-controls`,children:(0,J.jsxs)(`span`,{className:`codex-auto-switch-input-wrap`,children:[(0,J.jsx)(`input`,{id:i,className:`input mono codex-auto-switch-input`,type:`number`,min:1,max:100,step:1,inputMode:`numeric`,value:t,disabled:n,"aria-label":c(`accountPool.stickyLimitAria`),onChange:e=>o(e.target.value),onBlur:()=>s(),onKeyDown:e=>{e.nativeEvent.isComposing||n||e.key===`Enter`&&(e.preventDefault(),s())}}),(0,J.jsx)(ko,{disabled:n,incrementLabel:c(`accountPool.stickyLimitInc`),decrementLabel:c(`accountPool.stickyLimitDec`),onIncrement:()=>{let e=Oo(t,1,1,100);o(e),s(e)},onDecrement:()=>{let e=Oo(t,-1,1,100);o(e),s(e)}})]})})]})]})}function Ko(e){if(!e||typeof e!=`object`)return null;let t=e;return!(`accountPoolStrategy`in t)&&!(`accountPoolStickyLimit`in t)?null:{strategy:Ro(t.accountPoolStrategy),stickyLimit:Bo(t.accountPoolStickyLimit)}}function qo({apiBase:e,subscribeLoadObserver:t,readLastActive:n,onStrategyResolved:r}){let i=Q(),[a,o]=(0,_.useState)(Po),[s,c]=(0,_.useState)(1),[l,u]=(0,_.useState)(`1`),[d,f]=(0,_.useState)(!1),p=(0,_.useRef)(!1),[m,h]=(0,_.useState)(!1),g=(0,_.useRef)(!1),v=(0,_.useRef)(!1),y=(0,_.useRef)(0),[b,x]=(0,_.useState)(!1),[S,C]=(0,_.useState)(null),w=(0,_.useCallback)(e=>{let t=Ro(e.accountPoolStrategy),n=Bo(e.accountPoolStickyLimit);o(t),r?.(t),c(n),u(String(n)),p.current=!0,f(!0),x(!1),C(null)},[r]),T=(0,_.useCallback)(e=>{let t=Ko(e);t&&w({accountPoolStrategy:t.strategy,accountPoolStickyLimit:t.stickyLimit})},[w]),E=(0,_.useCallback)(async()=>{try{let t=await fetch(`${e}/api/codex-auth/active`);if(!t.ok)throw Error(`load`);let n=await t.json();if(g.current){v.current=!0;return}w(n)}catch{g.current||x(!0)}},[e,w]),D=(0,_.useCallback)(()=>{v.current&&(v.current=!1,queueMicrotask(()=>{if(g.current){v.current=!0;return}E()}))},[E]);(0,_.useEffect)(()=>{if(!t)return;let e=t({beginActiveRead:()=>y.current,acceptActiveRead:(e,t)=>{if(t===y.current){if(g.current){v.current=!0;return}T(e)}},rejectActiveRead:()=>{p.current||x(!0)}});return!g.current&&n&&T(n()),e},[t,T,n]),(0,_.useEffect)(()=>{!n||t||g.current||T(n())},[n,T,t]),(0,_.useEffect)(()=>{t||E()},[E,t]);let O=(0,_.useCallback)(async t=>{if(g.current)return;let n=a,l=s;t.strategy!==void 0&&(o(t.strategy),r?.(t.strategy)),t.stickyLimit!==void 0&&(c(t.stickyLimit),u(String(t.stickyLimit))),g.current=!0,h(!0),C(null),y.current+=1;let d=await Ho(e,t);y.current+=1,d.ok?(o(d.strategy),r?.(d.strategy),c(d.stickyLimit),u(String(d.stickyLimit)),p.current=!0,f(!0)):(C(i(`accountPool.strategyUpdateFailed`)),o(n),r?.(n),c(l),u(String(l))),g.current=!1,h(!1),D()},[e,r,D,s,a,i]),k=m||b||!d;return(0,J.jsxs)(`div`,{className:`card account-pool-strategy-card`,"aria-busy":m||!d&&!b,children:[b&&(0,J.jsx)(`div`,{className:`card-sub`,role:`alert`,children:i(`accountPool.strategyLoadFailed`)}),b&&(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm account-pool-strategy-card__retry`,onClick:()=>{E()},children:i(`common.retry`)}),!b&&(0,J.jsx)(Go,{strategy:a,stickyDraft:l,disabled:k,strategySelectId:`codex-pool-strategy`,stickyInputId:`codex-pool-sticky-limit`,onStrategyChange:e=>{k||e===a||O({strategy:e})},onStickyDraftChange:u,onStickyCommit:e=>{if(k)return;let t=Vo(e??l);if(t===null){u(String(s)),C(i(`accountPool.stickyLimitInvalid`));return}if(t===s){u(String(t));return}O({stickyLimit:t})}}),S&&(0,J.jsx)(`div`,{role:`alert`,className:`card-sub account-pool-strategy-card__error`,children:S})]})}function Jo({t:e,open:t,onToggle:n,children:r}){return(0,J.jsxs)(`section`,{className:`codex-auth-advanced`,children:[(0,J.jsxs)(`button`,{type:`button`,className:`codex-auth-advanced__toggle`,"aria-expanded":t,"aria-controls":`codex-auth-advanced-boxes`,onClick:n,children:[(0,J.jsx)(`span`,{children:e(`codexAuth.advancedSettings`)}),(0,J.jsx)(Se,{width:12,height:12,"aria-hidden":`true`,className:t?`codex-auth-advanced__chevron is-open`:`codex-auth-advanced__chevron`})]}),t&&(0,J.jsx)(`div`,{id:`codex-auth-advanced-boxes`,className:`codex-auth-advanced__boxes`,children:r})]})}function Yo(e,t){let[n,r]=(0,_.useState)(80),[i,a]=(0,_.useState)(`80`),[o,s]=(0,_.useState)(!1),[c,l]=(0,_.useState)(!1),[u,d]=(0,_.useState)(!1),[f,p]=(0,_.useState)(null),m=(0,_.useRef)(80),h=(0,_.useRef)(!1),g=(0,_.useRef)(80),v=(0,_.useRef)(!1),y=(0,_.useRef)(!1),b=(0,_.useRef)(!1),x=(0,_.useRef)(0),S=(0,_.useRef)(null),C=(0,_.useRef)(null),w=(0,_.useCallback)(e=>{m.current=e,h.current=!0,s(!0),r(e),e>0&&(g.current=e),a(String(e>0?e:g.current))},[]),T=(0,_.useCallback)(e=>{if(v.current||y.current){S.current=e;return}S.current=null,w(e)},[w]),E=(0,_.useCallback)(()=>{let e=S.current;return e!==null&&(S.current=null,w(e),!0)},[w]),D=(0,_.useCallback)(()=>{C.current!==null&&(window.clearTimeout(C.current),C.current=null),p(null)},[]),O=(0,_.useCallback)((e,t)=>{C.current!==null&&window.clearTimeout(C.current),p({tone:t?`err`:`ok`,message:e}),C.current=window.setTimeout(()=>{p(null),C.current=null},5e3)},[]);(0,_.useEffect)(()=>()=>{C.current!==null&&window.clearTimeout(C.current)},[]);let k=(0,_.useCallback)(()=>(h.current||l(!1),x.current),[]),A=(0,_.useCallback)((e,t)=>{l(!1);let n=So(e),r=xo(v.current,y.current,t,x.current);r===`defer`?S.current=vo(n):r===`apply`&&T(vo(n))},[T]),j=(0,_.useCallback)(e=>{h.current||v.current||y.current||(l(!1),w(vo(So(e))))},[w]),M=(0,_.useCallback)(()=>{h.current||l(!0)},[]),N=(0,_.useCallback)(async(n,r,i=!0)=>{if(y.current)return!1;y.current=!0,v.current=!1,D(),d(!0),x.current+=1;try{let a=await wo(e,n);return x.current+=1,a?(S.current=null,w(n),i&&O(t.updated,!1)):(E()||w(r),O(t.updateFailed,!0)),a}finally{y.current=!1,d(!1)}},[e,w,D,t.updateFailed,t.updated,E,O]),P=(0,_.useCallback)(()=>{v.current=!1;let e=m.current;E()||a(String(e>0?e:g.current)),O(t.invalid,!0)},[t.invalid,E,O]),F=(0,_.useCallback)(()=>{v.current=!1,b.current=!0,D();let e=m.current;E()||a(String(e>0?e:g.current))},[D,E]),I=(0,_.useCallback)(async()=>{if(b.current)return b.current=!1,!0;if(!h.current||y.current)return!1;let e=m.current;v.current=!1;let t=yo(i);return t===null?(P(),!1):t===e?(E()||a(String(t)),!0):N(t,e)},[i,E,P,N]),L=(0,_.useCallback)(async()=>{if(!h.current||y.current)return!1;let e=m.current;v.current=!1;let t=Co(e,i,g.current),n=await N(t.threshold,e);return n?(g.current=t.lastEnabled,t.threshold===0&&a(String(t.lastEnabled)),n):!1},[i,N]);return{threshold:n,draft:i,hydrated:o,saving:u,loadError:c,feedback:f,beginServerRead:k,acceptServerRead:A,hydrateServerValue:j,rejectServerRead:M,setDraft:(0,_.useCallback)(e=>{h.current&&(v.current=!0,b.current=!1,D(),a(e))},[D]),setEditing:(0,_.useCallback)(e=>{v.current=e},[]),commit:I,cancel:F,toggle:L,retry:(0,_.useCallback)(()=>{l(!1),D()},[D])}}function Xo({value:e,disabled:t=!1,selectId:n,onChange:r}){let i=Q(),a=fo(e),o=i(`accountPool.priorityHint`),s=`${n}-hint`,c=lo.map(e=>({value:String(e),label:go(i,e)})),l=mo(a)?c:[{value:String(a),label:go(i,a)},...c];return(0,J.jsxs)(`div`,{className:`codex-account-priority`,children:[(0,J.jsx)(`label`,{className:`codex-account-priority-label`,htmlFor:n,children:i(`accountPool.priority`)}),(0,J.jsx)(Dt,{id:n,value:String(a),options:l,disabled:t,label:i(`accountPool.priorityAria`),describedBy:s,title:o,onChange:e=>r(fo(Number.parseInt(e,10)))}),(0,J.jsx)(`span`,{id:s,className:`sr-only`,children:o})]})}function Zo({value:e}){let t=Q(),n=fo(e);return n===0?null:(0,J.jsx)(`span`,{className:`badge badge-muted`,children:go(t,n)})}function Qo(e,t){return`${e??``}\0${JSON.stringify(t)}`}var $o={month:`short`,day:`numeric`,year:`numeric`},es={month:`short`,day:`numeric`,year:`numeric`,hour:`2-digit`,minute:`2-digit`},ts=new Map;function ns(e,t){let n=Qo(e,t),r=ts.get(n);return r||(r=new Intl.DateTimeFormat(e,t),ts.set(n,r)),r}function rs(e,t){let n=new Date(e);return Number.isNaN(n.getTime())?`—`:ns(t,$o).format(n)}function is(e,t){let n=new Date(e);return Number.isNaN(n.getTime())?`—`:ns(t,es).format(n)}var as=new Intl.NumberFormat(`en-US`,{style:`currency`,currency:`USD`,currencyDisplay:`narrowSymbol`,minimumFractionDigits:4,maximumFractionDigits:4});function os(e,t){return!Number.isFinite(e)||e<0?`—`:`~${as.format(e)}`}function ss(e,t){return rs(e,t)}function cs(e,t){return is(e,t)}function ls(e){return Math.max(0,Math.ceil((new Date(e).getTime()-Date.now())/864e5))}function us({index:e,grantedAt:t,expiresAt:n,isNext:r,locale:i,t:a}){let o=ls(n),s=o<=7;return(0,J.jsxs)(`div`,{className:`credit-item${r?` credit-next`:``}`,children:[(0,J.jsxs)(`div`,{className:`credit-item-head`,children:[(0,J.jsx)(Oe,{width:13}),(0,J.jsx)(`span`,{className:`credit-item-label`,children:r?a(`codexAuth.creditNext`):a(`codexAuth.creditLabel`,{n:String(e+1)})}),r&&(0,J.jsx)(`span`,{className:`badge badge-amber text-micro`,style:{padding:`1px 6px`},children:a(`codexAuth.creditNextBadge`)})]}),(0,J.jsxs)(`div`,{className:`credit-item-dates`,children:[(0,J.jsx)(`span`,{children:a(`codexAuth.creditGranted`,{date:ss(t,i)})}),(0,J.jsx)(`span`,{className:s?`credit-urgent`:``,children:a(`codexAuth.creditExpires`,{date:cs(n,i),days:String(o)})})]})]})}function ds({account:e,onClick:t,t:n}){let r=e.quota?.resetCredits;return e.quota==null?(0,J.jsxs)(`span`,{className:`badge badge-muted codex-ticket-badge-slot`,"aria-hidden":`true`,children:[(0,J.jsx)(Oe,{width:12}),`0`]}):r===void 0?null:(0,J.jsxs)(`button`,{type:`button`,className:`badge ${typeof r==`number`&&r>0?`badge-amber`:`badge-muted`} badge-clickable`,onClick:e=>{e.stopPropagation(),t()},"aria-label":n(`codexAuth.resetCreditsAria`,{count:String(r)}),children:[(0,J.jsx)(Oe,{width:12}),r]})}function fs({t:e,paused:t,saving:n}){let r=e(`codexAuth.pause`),i=e(`codexAuth.resume`),a=e(`common.saving`),o=n?a:t?i:r,s=Math.max(r.length,i.length,a.length);return(0,J.jsx)(`span`,{className:`codex-auth-pause-label`,style:{minWidth:`${s}ch`},children:o})}function ps({pool:e,activeId:t,accountModeState:n,switchActionLabel:r,threshold:i,onOpenReset:a,onSwitch:o,onTogglePause:s,pauseUpdatingId:c,pauseBusy:l,onPriorityChange:u,priorityUpdatingId:d,switchingId:f,pinnedId:p=null,onReauth:m,onEditAlias:h,onRemove:g,onCopyDoctor:v,doctorCopyOutcomeFor:y}){let b=Q(),x=e=>!e.paused&&t===e.id,S=Xa(),[C,w]=(0,_.useState)(new Set);return(0,J.jsx)(J.Fragment,{children:e.map(e=>{let t=e.health?.status,_=!!e.needsReauth||Ia(t),T=Ra(t),E=Va(b,e.health),D=Ha(b,`codex`,e.id,e.health);return(0,J.jsxs)(`div`,{className:`card ${x(e)?`card-active`:``}`,style:{marginBottom:8},children:[(0,J.jsxs)(`div`,{className:`card-head`,children:[(0,J.jsx)(`span`,{className:`dot ${_?`dot-amber`:x(e)?`dot-blue`:`dot-muted`}`}),(0,J.jsx)(`strong`,{children:e.alias??e.email}),(0,J.jsxs)(`span`,{className:`card-badges`,children:[e.plan&&(0,J.jsx)(`span`,{className:`badge badge-green`,children:e.plan}),e.paused&&(0,J.jsx)(`span`,{className:`badge badge-muted`,title:b(`codexAuth.pausedHint`),children:b(`codexAuth.paused`)}),(0,J.jsx)(Zo,{value:e.priority}),e.id===p&&!e.paused&&(0,J.jsx)(`span`,{className:`badge badge-muted`,children:b(`codexAuth.pinned`)}),(0,J.jsx)(ds,{t:b,account:e,onClick:()=>a(e)}),E&&(0,J.jsx)(`span`,{className:Fa(t),children:E}),_&&!E&&(0,J.jsx)(`span`,{className:`badge badge-amber`,children:b(`codexAuth.needsReauth`)}),x(e)&&!_&&!T&&(0,J.jsx)(`span`,{className:`badge badge-primary`,children:b(n===`direct`?`codexAuth.poolPrepared`:`codexAuth.nextSession`)})]}),!e.paused&&(!x(e)||p!==e.id)&&!_&&!T&&(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm codex-account-switch`,onClick:()=>o(e),children:r}),_&&(0,J.jsx)(`button`,{type:`button`,className:`btn btn-primary btn-sm`,onClick:()=>m(e.id),children:b(`codexAuth.reauthenticate`)}),v&&za(t)&&(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm codex-auth-action-btn`,onClick:()=>v(e.id),children:(0,J.jsx)(`span`,{"aria-live":`polite`,children:Ga(b,y?.(e.id))})}),(0,J.jsxs)(`button`,{type:`button`,className:`btn btn-sm btn-ghost codex-auth-action-btn`,onClick:()=>s(e),disabled:l,title:e.paused?b(`codexAuth.pausedHint`):void 0,"aria-label":e.paused?`${b(`codexAuth.resume`)}. ${b(`codexAuth.pausedHint`)}`:b(`codexAuth.pause`),children:[e.paused?(0,J.jsx)(me,{width:14}):(0,J.jsx)(X,{width:14}),(0,J.jsx)(fs,{t:b,paused:e.paused,saving:c===e.id})]}),(0,J.jsxs)(`details`,{className:`codex-account-more card-right`,open:C.has(e.id),onToggle:t=>{let n=t.currentTarget.open;w(t=>{let r=new Set(t);return n?r.add(e.id):r.delete(e.id),r})},children:[(0,J.jsx)(`summary`,{className:`btn btn-ghost btn-sm`,"aria-label":`${b(`codexAuth.moreActions`)} — ${e.email}`,title:b(`codexAuth.moreActions`),children:`⋯`}),(0,J.jsxs)(`div`,{className:`codex-account-more-body`,children:[(0,J.jsxs)(`span`,{className:`mono text-caption muted`,children:[b(`prov.accountId`),`: `,Na(e.id)]}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:()=>S.copy(e.id,e.id),children:S.outcomeFor(e.id)===`copied`?b(`startup.copied`):b(`codexAuth.copyId`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:()=>void h(e),children:b(`prov.editAlias`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn-icon btn-icon-danger`,"aria-label":`${b(`common.remove`)} — ${e.email}`,title:`${b(`common.remove`)} — ${e.email}`,onClick:t=>{t.stopPropagation(),g(e.id)},children:(0,J.jsx)(de,{width:14})})]})]})]}),(0,J.jsxs)(`div`,{className:`codex-account-identity`,children:[(0,J.jsxs)(`div`,{className:`codex-account-identity-copy`,children:[e.email,e.plan?` · ${e.plan}`:``]}),(fo(e.priority)!==0||C.has(e.id))&&(0,J.jsx)(Xo,{value:e.priority,selectId:`codex-account-priority-${e.id}`,disabled:d!==null||f!==null,onChange:t=>u(e,t)})]}),D&&(0,J.jsx)(`div`,{className:`card-sub faint`,children:D}),T&&(0,J.jsx)(`div`,{className:`card-sub faint`,children:b(`pws.healthCooldownHint`)}),_?(0,J.jsx)(`div`,{className:`card-sub faint`,children:b(`codexAuth.tokenExpired`)}):!T&&(0,J.jsx)(ia,{quota:e.quota,plan:e.plan,threshold:i,t:b,pending:e.quota==null})]},e.id)})})}function ms({onReauth:e}){let t=Q();return(0,J.jsxs)(`div`,{className:`notice-warn`,style:{marginBottom:12,display:`flex`,alignItems:`center`,justifyContent:`space-between`,gap:12,flexWrap:`wrap`},children:[(0,J.jsxs)(`span`,{children:[(0,J.jsx)(_e,{width:14}),` `,t(`codexAuth.tokenExpired`)]}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-primary btn-sm`,onClick:e,children:t(`codexAuth.reauthenticate`)})]})}function hs({confirm:e,mainEmail:t,accountModeState:n,switchingId:r,orderBusy:i=!1,onCancel:a,onConfirm:o}){let s=Q(),c=(0,_.useRef)(null);(0,_.useEffect)(()=>{let e=c.current;e&&!e.open&&e.showModal()},[]);let l=(0,_.useCallback)(e=>{e.preventDefault(),a()},[a]);return(0,J.jsxs)(`dialog`,{ref:c,className:`modal-overlay`,"aria-labelledby":`codex-switch-title`,onCancel:l,children:[(0,J.jsx)(`button`,{type:`button`,className:`modal-backdrop-dismiss`,"aria-label":s(`common.close`),tabIndex:-1,onClick:a}),(0,J.jsxs)(`div`,{className:`modal-card`,onClick:e=>e.stopPropagation(),role:`document`,children:[(0,J.jsx)(`h3`,{id:`codex-switch-title`,children:n===`direct`?s(`codexAuth.preparePoolTitle`):e.id===`__main__`?s(`codexAuth.switchBack`):s(`codexAuth.switchTitle`)}),(0,J.jsx)(`p`,{className:`modal-desc`,children:n===`direct`?s(`codexAuth.preparePoolDesc`):e.id===`__main__`?s(`codexAuth.switchBackDesc`):s(`codexAuth.switchDesc`)}),(0,J.jsxs)(`div`,{className:`card`,style:{margin:`12px 0`},children:[(0,J.jsx)(`strong`,{children:e.id===`__main__`?t||s(`codexAuth.codexApp`):e.email}),e.plan&&(0,J.jsx)(`span`,{className:`badge badge-green`,style:{marginLeft:8},children:e.plan})]}),e.id!==`__main__`&&(0,J.jsxs)(`div`,{className:`notice-warn`,children:[(0,J.jsx)(_e,{width:14}),` `,s(`codexAuth.cacheWarning`)]}),(0,J.jsxs)(`div`,{className:`modal-actions`,children:[(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost`,onClick:a,children:s(`codexAuth.cancel`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-primary`,disabled:!!r||i,onClick:o,children:s(r?`pws.accountSwitching`:n===`direct`?`codexAuth.prepareForPool`:`codexAuth.setAsNext`)})]})]})]})}function gs({resetPopup:e,resetConfirm:t,creditDetails:n,creditDetailsLoading:r,redeeming:i,onClose:a,onShowConfirm:o,onCancelConfirm:s,onRedeem:c}){let{locale:l,t:u}=ct(),d=(0,_.useRef)(null);(0,_.useEffect)(()=>{let e=d.current;e&&!e.open&&e.showModal()},[]);let f=(0,_.useCallback)(e=>{e.preventDefault(),a()},[a]);return(0,J.jsxs)(`dialog`,{ref:d,className:`modal-overlay`,"aria-labelledby":`codex-reset-title`,onCancel:f,children:[(0,J.jsx)(`button`,{type:`button`,className:`modal-backdrop-dismiss`,"aria-label":u(`common.close`),tabIndex:-1,onClick:a}),(0,J.jsx)(`div`,{className:`modal-card`,onClick:e=>e.stopPropagation(),role:`document`,children:t?(0,J.jsxs)(J.Fragment,{children:[(0,J.jsxs)(`div`,{style:{textAlign:`center`,padding:`12px 0`},children:[(0,J.jsx)(`div`,{className:`confirm-icon`,children:(0,J.jsx)(_e,{width:22})}),(0,J.jsx)(`h3`,{id:`codex-reset-title`,children:u(`codexAuth.confirmResetTitle`)}),(0,J.jsx)(`p`,{className:`modal-desc`,children:u(`codexAuth.confirmResetDesc`,{count:String(e.quota?.resetCredits??0)})}),n&&n[0]&&(0,J.jsx)(`p`,{className:`faint text-label`,children:u(`codexAuth.confirmWhichCredit`,{date:ss(n[0].granted_at,l)})}),(0,J.jsx)(`p`,{className:`faint text-label`,children:u(`codexAuth.irreversible`)})]}),(0,J.jsxs)(`div`,{className:`modal-actions`,children:[(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost`,onClick:s,children:u(`codexAuth.cancel`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-primary`,onClick:c,disabled:i,children:u(i?`codexAuth.redeeming`:`codexAuth.useCredit`)})]})]}):(0,J.jsxs)(J.Fragment,{children:[(0,J.jsxs)(`h3`,{id:`codex-reset-title`,children:[(0,J.jsx)(Oe,{width:16}),` `,u(`codexAuth.resetCreditsTitle`)]}),(0,J.jsxs)(`div`,{className:`card-sub`,children:[e.email,e.plan?` · ${e.plan}`:``]}),(0,J.jsx)(`div`,{style:{margin:`16px 0`},children:(e.quota?.resetCredits??0)>0?(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)(`p`,{style:{marginBottom:12},children:u(`codexAuth.resetCreditsAvailable`,{count:String(e.quota?.resetCredits??0)})}),r&&(0,J.jsx)(`p`,{className:`faint text-label`,children:u(`common.loading`)}),n&&n.length>0&&(0,J.jsx)(`div`,{className:`credit-list`,children:n.map((e,t)=>(0,J.jsx)(us,{index:t,grantedAt:e.granted_at,expiresAt:e.expires_at,isNext:t===0,locale:l,t:u},`${e.granted_at}:${e.expires_at}`))}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-primary`,style:{marginTop:12,width:`100%`},onClick:o,disabled:i,children:u(`codexAuth.useOneCredit`)}),(0,J.jsx)(`p`,{className:`card-sub text-caption`,style:{marginTop:8,textAlign:`center`},children:u(`codexAuth.fifoNote`)})]}):(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)(`p`,{className:`faint`,children:u(`codexAuth.noResetCredits`)}),(0,J.jsx)(`p`,{className:`modal-desc`,children:u(`codexAuth.earnCreditsHint`)})]})})]})})]})}function _s({t:e,main:t,isMainActive:n,accountModeState:r,threshold:i,switchActionLabel:a,onSwitch:o,onTogglePause:s,pauseUpdatingId:c,pauseBusy:l,onPriorityChange:u,priorityUpdatingId:d,switchingId:f,pinnedId:p=null,onOpenReset:m,onCopyDoctor:h,doctorCopyOutcomeFor:g}){let _=e(`codexAuth.codexApp`),v=t?.id??`__main__`,y={id:`__main__`,email:t?.email||_,plan:t?.plan,isMain:!0,paused:t?.paused??!1,priority:t?.priority??0,hasCredential:!0,quota:t?.quota??null},b=!!t?.needsReauth||Ia(t?.health?.status),x=Ra(t?.health?.status),S=Va(e,t?.health),C=t?Ha(e,`codex`,v,t.health):null;return(0,J.jsxs)(`div`,{className:`card ${n?`card-active`:``}`,style:{marginBottom:12},children:[(0,J.jsxs)(`div`,{className:`card-head`,children:[(0,J.jsx)(`span`,{className:`dot ${b?`dot-amber`:`dot-green`}`}),(0,J.jsx)(`strong`,{children:e(`codexAuth.mainAccount`)}),(0,J.jsxs)(`span`,{className:`card-badges`,children:[t?.plan&&(0,J.jsx)(`span`,{className:`badge badge-green`,children:t.plan}),t?.paused&&(0,J.jsx)(`span`,{className:`badge badge-muted`,title:e(`codexAuth.pausedHint`),children:e(`codexAuth.paused`)}),(0,J.jsx)(Zo,{value:y.priority}),p===`__main__`&&!t?.paused&&(0,J.jsx)(`span`,{className:`badge badge-muted`,children:e(`codexAuth.pinned`)}),t&&(0,J.jsx)(ds,{t:e,account:{...t,id:`__main__`},onClick:()=>m({...t,id:`__main__`})}),S&&(0,J.jsx)(`span`,{className:Fa(t?.health?.status),children:S}),b&&!S&&(0,J.jsx)(`span`,{className:`badge badge-amber`,children:e(`codexAuth.needsReauth`)}),!t?.paused&&(0,J.jsx)(`span`,{className:`badge ${n?`badge-primary`:`badge-muted`}`,children:e(n?r===`direct`?`codexAuth.poolPrepared`:`codexAuth.nextSession`:`codexAuth.current`)})]}),!t?.paused&&(!n||p!==`__main__`)&&!b&&!x&&(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm codex-account-switch`,onClick:()=>o(y),children:a}),h&&za(t?.health?.status)&&(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm codex-auth-action-btn`,onClick:()=>h(v),children:(0,J.jsx)(`span`,{"aria-live":`polite`,children:Ga(e,g?.(v))})}),t&&(0,J.jsxs)(`button`,{type:`button`,className:`btn btn-sm btn-ghost codex-auth-action-btn`,onClick:()=>s(y),disabled:l,title:t.paused?e(`codexAuth.pausedHint`):void 0,"aria-label":t.paused?`${e(`codexAuth.resume`)}. ${e(`codexAuth.pausedHint`)}`:e(`codexAuth.pause`),children:[t.paused?(0,J.jsx)(me,{width:14}):(0,J.jsx)(X,{width:14}),(0,J.jsx)(fs,{t:e,paused:!!t.paused,saving:c===`__main__`})]}),(0,J.jsxs)(`span`,{className:`card-right`,children:[(0,J.jsx)(De,{width:14}),` `,e(`codexAuth.appLogin`)]})]}),(0,J.jsxs)(`div`,{className:`codex-account-identity`,children:[(0,J.jsxs)(`div`,{className:`codex-account-identity-copy`,children:[t?.email||e(`codexAuth.appLogin`),t?.plan?` · ${t.plan}`:``]}),t&&(0,J.jsx)(Xo,{value:y.priority,selectId:`codex-account-priority-${y.id}`,disabled:d!==null||f!==null,onChange:e=>u(y,e)})]}),C&&(0,J.jsx)(`div`,{className:`card-sub faint`,children:C}),x&&(0,J.jsx)(`div`,{className:`card-sub faint`,children:e(`pws.healthCooldownHint`)}),b?(0,J.jsx)(`div`,{className:`card-sub faint`,children:e(`codexAuth.mainTokenExpired`)}):!x&&(0,J.jsx)(ia,{quota:t?.quota??null,plan:t?.plan,threshold:i,t:e,pending:t!=null&&t.quota==null})]})}function vs({t:e,embedded:t,refreshingQuota:n,pausingExhausted:r,pauseBusy:i,actionFeedback:a,actionFeedbackTone:o,onRefresh:s,onPauseExhausted:c,sparkVisible:l,sparkBusy:u,onToggleSpark:d}){return(0,J.jsxs)(`div`,{className:t?`row`:`page-head codex-auth-page-head`,style:t?{justifyContent:`flex-end`,marginBottom:8}:void 0,children:[!t&&(0,J.jsx)(`h2`,{className:`page-title`,children:e(`nav.codexAuth`)}),(0,J.jsxs)(`div`,{className:t?`row`:`codex-auth-page-head__actions`,children:[(0,J.jsx)(`span`,{className:`codex-auth-page-head__feedback${o===`ok`?` is-ok`:``}${o===`warn`?` is-warn`:``}${o===`err`?` is-err`:``}`,role:`status`,"aria-live":`polite`,children:a??``}),l!==void 0&&d&&(0,J.jsxs)(`span`,{className:`codex-auth-spark-toggle`,children:[(0,J.jsx)(`span`,{className:`codex-auth-spark-toggle__label`,children:e(`codexAuth.sparkQuota`)}),(0,J.jsx)(`button`,{type:`button`,className:`toggle ${l?`on`:``}`,onClick:d,disabled:!!u,"aria-pressed":l,"aria-label":e(`codexAuth.sparkQuota`),title:e(`codexAuth.sparkQuotaHint`),children:(0,J.jsx)(`span`,{className:`toggle-knob`})})]}),(0,J.jsxs)(`button`,{type:`button`,className:`btn btn-sm btn-ghost codex-auth-action-btn`,onClick:c,disabled:n||r||!!i,children:[(0,J.jsx)(X,{width:14}),` `,e(r?`codexAuth.pausingExhausted`:`codexAuth.pauseExhausted`)]}),(0,J.jsxs)(`button`,{type:`button`,className:`btn btn-sm btn-ghost codex-auth-action-btn`,onClick:s,disabled:n||r||!!i,children:[(0,J.jsx)(pe,{width:14}),` `,e(n?`codexAuth.refreshingQuota`:`codexAuth.refreshQuota`)]})]})]})}function ys({t:e,loadState:t,accountsCount:n,onRetry:r}){return t===`loading`&&n===0?(0,J.jsxs)(`div`,{className:`codex-auth-load-skeleton`,role:`status`,"aria-live":`polite`,"aria-busy":`true`,children:[(0,J.jsxs)(`div`,{className:`card codex-auth-load-skeleton__main`,style:{marginBottom:12},"aria-hidden":`true`,children:[(0,J.jsxs)(`div`,{className:`card-head`,children:[(0,J.jsx)(`span`,{className:`dot dot-muted`}),(0,J.jsx)(`strong`,{children:e(`codexAuth.mainAccount`)}),(0,J.jsxs)(`span`,{className:`card-badges`,children:[(0,J.jsxs)(`span`,{className:`badge badge-muted codex-ticket-badge-slot`,"aria-hidden":`true`,children:[(0,J.jsx)(Oe,{width:12}),`0`]}),(0,J.jsx)(`span`,{className:`badge badge-primary`,children:e(`codexAuth.nextSession`)})]}),(0,J.jsxs)(`button`,{type:`button`,className:`btn btn-sm btn-ghost`,tabIndex:-1,disabled:!0,children:[(0,J.jsx)(X,{width:14}),` `,e(`codexAuth.pause`)]}),(0,J.jsxs)(`span`,{className:`card-right`,children:[(0,J.jsx)(De,{width:14}),` `,e(`codexAuth.appLogin`)]})]}),(0,J.jsxs)(`div`,{className:`card-sub`,children:[(0,J.jsx)(`span`,{className:`codex-auth-load-skeleton__strut`,children:e(`codexAuth.appLogin`)}),(0,J.jsx)(`span`,{className:`codex-auth-load-skeleton__line codex-auth-load-skeleton__line--sub`})]}),(0,J.jsx)(ia,{quota:null,threshold:0,t:e,pending:!0})]}),(0,J.jsxs)(`div`,{className:`section-sep`,"aria-hidden":`true`,children:[(0,J.jsx)(`span`,{className:`section-label`,children:e(`codexAuth.accountPool`)}),(0,J.jsx)(`div`,{className:`sep-line`}),(0,J.jsxs)(`button`,{type:`button`,className:`btn btn-sm btn-ghost`,tabIndex:-1,disabled:!0,children:[(0,J.jsx)(fe,{width:14}),` `,e(`codexAuth.add`)]})]}),(0,J.jsx)(`div`,{className:`empty codex-auth-pool-empty codex-auth-load-skeleton__empty`,"aria-hidden":`true`,children:(0,J.jsx)(`div`,{className:`title`,children:e(`codexAuth.noPool`)})}),(0,J.jsx)(`span`,{className:`sr-only`,children:e(`pws.accountsLoading`)})]}):t===`error`?(0,J.jsxs)(`div`,{className:`pwi-auth-state pwi-auth-state--error`,role:`alert`,children:[(0,J.jsx)(`span`,{children:e(`codexAuth.loadFailed`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:r,children:e(`pws.retryAccounts`)})]}):null}function bs(e,t){return t===void 0?e(`codexAuth.resetSuccessGeneric`):e(`codexAuth.resetSuccess`,{remaining:String(t)})}async function xs(e,t,n,r){try{let i=await Ft(await fetch(`${e}/api/codex-auth/reset-credits/consume`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({accountId:t})}));return i?i.code===`reset`||i.code===`already_redeemed`?(await r(!0),{ok:!0,close:!0,toast:bs(n,typeof i.remaining==`number`&&Number.isFinite(i.remaining)?Math.max(0,i.remaining):void 0)}):{ok:!1,close:!0,toast:n(i.code===`nothing_to_reset`?`codexAuth.resetNothingToReset`:i.code===`no_credit`?`codexAuth.resetNoCredit`:`codexAuth.resetError`)}:{ok:!1,toast:n(`codexAuth.resetError`)}}catch{return{ok:!1,toast:n(`codexAuth.resetError`)}}}var Ss=`ocx doctor`;function Cs({apiBase:e,accountModeState:t=null,banner:n=null,embedded:r=!1,onActiveNeedsReauthChange:i,controller:a,advancedExtras:o=null}){let s=Q(),c=Yo(e,{updated:s(`codexAuth.autoSwitchUpdated`),updateFailed:s(`codexAuth.autoSwitchUpdateFailed`),invalid:s(`codexAuth.autoSwitchThresholdInvalid`)}),[l,u]=(0,_.useState)(null),{beginServerRead:d,acceptServerRead:f,rejectServerRead:p,hydrateServerValue:m}=c,h=Do(e,!a),g=a??h,{accounts:v,activeId:y,loadState:b,switchingId:x,pauseUpdatingId:S,priorityUpdatingId:C,pausingExhausted:w,activePinnedId:T,load:E}=g,[D,O]=(0,_.useState)(null),[k,A]=(0,_.useState)(!1),[j,M]=(0,_.useState)(!1),[N,P]=(0,_.useState)(null),[F,I]=(0,_.useState)(null),[L,R]=(0,_.useState)(null),z=(0,_.useRef)(null),[B,V]=(0,_.useState)(!1),[H,U]=(0,_.useState)(void 0),[W,ee]=(0,_.useState)(!1),[G,K]=(0,_.useState)(null),[q,Y]=(0,_.useState)(!1),[te,ne]=(0,_.useState)(!1),[re,ie]=(0,_.useState)(null),[ae,oe]=(0,_.useState)(!1),se=Xa(),ce=(0,_.useCallback)((e,t=`ok`)=>{z.current&&clearTimeout(z.current),I(e),R(t),z.current=setTimeout(()=>{I(null),R(null),z.current=null},5e3)},[]);(0,_.useEffect)(()=>()=>{z.current&&clearTimeout(z.current)},[]);let le=(0,_.useCallback)(e=>{se.copy(Ss,e)},[se]),{subscribeLoadObserver:ue,readLastThreshold:de}=g;(0,_.useEffect)(()=>ue({beginActiveRead:d,acceptActiveRead:f,rejectActiveRead:p}),[ue,d,f,p]),(0,_.useEffect)(()=>{let e=de();e!==void 0&&m(e)},[de,m]),(0,_.useEffect)(()=>{if(!k)return;let e=g.pauseRefresh();return()=>g.resumeRefresh(e)},[g,k]);let pe=y&&y!==`__main__`?v.find(e=>e.id===y):null,X=!pe?.paused&&La(pe);(0,_.useEffect)(()=>{i?.(X)},[X,i]);let me=(0,_.useCallback)(e=>{P(e),A(!0)},[]),he=(0,_.useCallback)(()=>{A(!1),P(null)},[]),ge=(0,_.useCallback)(e=>{g.syncAfterAccountAdded(),ce(s(e.catalogRefreshPending?`codexAuth.catalogRefreshPending`:`codexAuth.accountAdded`),e.catalogRefreshPending?`warn`:`ok`),he()},[he,g,ce,s]),_e=async e=>{let n=await g.switchAccount(e);if(!n.ok){if(n.reason===`busy`)return;ce(s(`codexAuth.switchFailed`),`err`);return}O(null);let r=n.activeId,i=r&&r!==`__main__`?v.find(e=>e.id===r)?.email??s(`pws.accountOrdinal`,{count:`1`}):s(`codexAuth.mainAccount`);ce(s(t===`direct`?`codexAuth.poolPreparedToast`:`codexAuth.switched`,{email:i}))},Z=async e=>{let t=window.prompt(s(`prov.aliasPrompt`),e.alias??``);if(t===null)return;let n=await g.saveAlias(e.id,t);ce(s(n.ok?`prov.aliasSaved`:`prov.aliasSaveFailed`),n.ok?`ok`:`err`)},ve=async e=>{let t=!e.paused,n=await g.setAccountPaused(e.id,t);!n.ok&&n.reason===`busy`||(O(t=>t?.id===e.id?null:t),ce(s(n.ok?t?`codexAuth.pauseSucceeded`:`codexAuth.resumeSucceeded`:t?`codexAuth.pauseFailed`:`codexAuth.resumeFailed`,{email:e.alias??e.email}),n.ok?`ok`:`err`))},ye=async(e,t)=>{if(t===e.priority)return;let n=await g.setAccountPriority(e.id,t);!n.ok&&n.reason===`busy`||ce(s(n.ok?`accountPool.priorityUpdated`:`accountPool.priorityUpdateFailed`,{email:e.alias??e.email}),n.ok?`ok`:`err`)},be=async e=>{let t=v.find(t=>t.id===e)?.email??s(`pws.accountOrdinal`,{count:`1`});if(!window.confirm(s(`codexAuth.removeConfirm`,{id:t})))return;let n=await g.removeAccount(e);n.ok?n.catalogRefreshPending&&ce(s(`codexAuth.catalogRefreshPending`),`warn`):ce(s(`codexAuth.removeFailed`),`err`)},xe=async()=>{V(!0);try{let e=await E(!0);ce(s(e?`codexAuth.quotaRefreshed`:`codexAuth.quotaRefreshFailed`),e?`ok`:`err`)}finally{V(!1)}};(0,_.useEffect)(()=>{let t=new AbortController;return fetch(`${e}/api/settings`,{signal:t.signal}).then(e=>e.ok?e.json():null).then(e=>{t.signal.aborted||typeof e?.showCodexSparkQuota!=`boolean`||U(e.showCodexSparkQuota)}).catch(()=>{}),()=>{t.abort()}},[e]);let Se=async()=>{if(W||H===void 0)return;let t=!H;ee(!0),U(t);try{let n=await fetch(`${e}/api/settings`,{method:`PUT`,headers:{"content-type":`application/json`},body:JSON.stringify({showCodexSparkQuota:t})});if(!n.ok)throw Error(`save`);let r=await n.json(),i=typeof r.showCodexSparkQuota==`boolean`?r.showCodexSparkQuota:t;U(i),ce(s(i?`codexAuth.sparkQuotaShown`:`codexAuth.sparkQuotaHidden`),`ok`),await E(!0)}catch{U(!t),ce(s(`codexAuth.sparkQuotaFailed`),`err`)}finally{ee(!1)}},Ce=async()=>{let e=await g.pauseExhaustedAccounts();!e.ok&&e.reason===`busy`||ce(e.ok?e.pausedCount>0?s(`codexAuth.pauseExhaustedSucceeded`,{count:String(e.pausedCount)}):s(`codexAuth.pauseExhaustedNone`):s(`codexAuth.pauseExhaustedFailed`),e.ok?`ok`:`err`)},we=async t=>{K(t),Y(!1),ie(null),oe(!0);try{let n=await Ft(await fetch(`${e}/api/codex-auth/reset-credits?accountId=${encodeURIComponent(t.id)}`));if(n){let e=(n.credits??[]).sort((e,t)=>new Date(e.granted_at).getTime()-new Date(t.granted_at).getTime());ie(e)}}catch{}finally{oe(!1)}},Te=async t=>{ne(!0);try{let n=await xs(e,t,s,E);n.close&&(K(null),Y(!1)),n.toast&&ce(n.toast,n.ok?`ok`:`err`)}finally{ne(!1)}},Ee=v.find(e=>e.isMain),De=v.filter(e=>!e.isMain),Oe=!Ee?.paused&&(!y||y===`__main__`),ke=s(t===`direct`?`codexAuth.prepareForPool`:`codexAuth.setAsNext`),Ae=S!==null||w,je=c.threshold??0,Me=!r;return(0,J.jsxs)(`div`,{children:[(0,J.jsx)(vs,{t:s,embedded:r,refreshingQuota:B,actionFeedback:F,actionFeedbackTone:L,pausingExhausted:w,pauseBusy:Ae,onRefresh:()=>{xe()},onPauseExhausted:()=>{Ce()},sparkVisible:H,sparkBusy:W,onToggleSpark:()=>{Se()}}),n,(0,J.jsx)(ys,{t:s,loadState:b,accountsCount:v.length,onRetry:()=>{E()}}),(b!==`loading`||v.length!==0)&&(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)(_s,{t:s,main:Ee,isMainActive:Oe,accountModeState:t,threshold:je,switchActionLabel:ke,onSwitch:O,onTogglePause:ve,pauseUpdatingId:S,pauseBusy:Ae,onPriorityChange:(e,t)=>{ye(e,t)},priorityUpdatingId:C,switchingId:x,pinnedId:T,onOpenReset:we,onCopyDoctor:Me?le:void 0,doctorCopyOutcomeFor:Me?se.outcomeFor:void 0}),(0,J.jsxs)(`div`,{className:`section-sep`,children:[(0,J.jsx)(`span`,{className:`section-label`,children:s(`codexAuth.accountPool`)}),(0,J.jsx)(`div`,{className:`sep-line`}),(0,J.jsxs)(`button`,{type:`button`,className:`btn btn-sm btn-ghost`,onClick:()=>A(!0),children:[(0,J.jsx)(fe,{width:14}),` `,s(`codexAuth.add`)]})]}),X&&pe&&(0,J.jsx)(ms,{onReauth:()=>me(pe.id)}),De.length===0&&(0,J.jsx)(Ot,{title:s(`codexAuth.noPool`)}),(0,J.jsx)(ps,{pool:De,activeId:y,accountModeState:t,switchActionLabel:ke,threshold:je,onOpenReset:we,onSwitch:O,onTogglePause:ve,pauseUpdatingId:S,pauseBusy:Ae,onPriorityChange:(e,t)=>{ye(e,t)},priorityUpdatingId:C,switchingId:x,pinnedId:T,onReauth:me,onEditAlias:Z,onRemove:be,onCopyDoctor:Me?le:void 0,doctorCopyOutcomeFor:Me?se.outcomeFor:void 0})]}),(0,J.jsx)(qo,{apiBase:e,subscribeLoadObserver:g.subscribeLoadObserver,readLastActive:g.readLastActive,onStrategyResolved:u}),(0,J.jsxs)(Jo,{t:s,open:j,onToggle:()=>M(e=>!e),children:[l!==null&&(0,J.jsx)(jo,{threshold:c.threshold,draft:c.draft,strategy:l,hydrated:c.hydrated,saving:c.saving,loadError:c.loadError,feedback:c.feedback,onDraftChange:c.setDraft,onEditingChange:c.setEditing,onCommit:c.commit,onCancel:c.cancel,onToggle:c.toggle,onRetry:()=>{c.retry(),E()}}),o]}),D&&(0,J.jsx)(hs,{confirm:D,mainEmail:Ee?.email,accountModeState:t,switchingId:x,orderBusy:C!==null,onCancel:()=>O(null),onConfirm:()=>{_e(D.id===`__main__`?`__main__`:D.id)}}),G&&(0,J.jsx)(gs,{resetPopup:G,resetConfirm:q,creditDetails:re,creditDetailsLoading:ae,redeeming:te,onClose:()=>{K(null),Y(!1),ie(null)},onShowConfirm:()=>Y(!0),onCancelConfirm:()=>Y(!1),onRedeem:()=>{Te(G.id)}}),k&&(0,J.jsx)(co,{apiBase:e,reauthAccountId:N??void 0,onClose:he,onAdded:ge})]})}var ws={"five-hour":`accountPool.quotaWindowFiveHour`,weekly:`accountPool.quotaWindowWeekly`,"max-utilization":`accountPool.quotaWindowMaxUtilization`};function Ts({apiBase:e,accountCount:t}){let n=Q(),[r,i]=(0,_.useState)(null),[a,o]=(0,_.useState)(`80`),[s,c]=(0,_.useState)(`1`),[l,u]=(0,_.useState)(!1),[d,f]=(0,_.useState)(null),[p,m]=(0,_.useState)(!1);(0,_.useEffect)(()=>{let t=!1,n=new AbortController;return Promise.resolve().then(()=>fetch(`${e}/api/oauth/accounts/pool?provider=anthropic`,{signal:n.signal})).then(e=>{if(!e.ok)throw Error(`load`);return e.json()}).then(e=>{if(t)return;let n=typeof e.autoSwitchThreshold==`number`?e.autoSwitchThreshold:80,r=Bo(e.stickyLimit);i({enabled:e.enabled===!0,threshold:n,strategy:Ro(e.strategy),stickyLimit:r,quotaWindow:zo(e.quotaWindow)}),o(String(n)),c(String(r)),m(!1)}).catch(()=>{t||n.signal.aborted||m(!0)}),()=>{t=!0,n.abort()}},[e]);let h=(0,_.useCallback)(async t=>{let a=r;i({enabled:t.enabled,threshold:t.threshold,strategy:t.strategy,stickyLimit:t.stickyLimit,quotaWindow:t.quotaWindow}),u(!0),f(null);try{let n=await fetch(`${e}/api/oauth/accounts/pool`,{method:`PUT`,headers:{"content-type":`application/json`},body:JSON.stringify({provider:`anthropic`,enabled:t.enabled,autoSwitchThreshold:t.threshold,strategy:t.strategy,stickyLimit:t.stickyLimit,quotaWindow:t.quotaWindow})});if(!n.ok)throw Error(`save`);let r=await n.json().catch(()=>null),a=Ro(r?.strategy??t.strategy),s=Bo(r?.stickyLimit??t.stickyLimit),l=zo(r?.quotaWindow??t.quotaWindow);i({enabled:t.enabled,threshold:t.threshold,strategy:a,stickyLimit:s,quotaWindow:l}),o(String(t.threshold)),c(String(s))}catch{f(n(`anthropicPool.saveFailed`)),a&&(i(a),o(String(a.threshold)),c(String(a.stickyLimit)))}finally{u(!1)}},[e,r,n]),g=r?.enabled===!0,v=r?.threshold??80,y=r?.strategy??`quota`,b=r?.stickyLimit??1,x=r?.quotaWindow??`five-hour`,S=y===`round-robin`,C=r===null&&!p,w=C||l||p||!g&&t<2;return(0,J.jsxs)(`div`,{className:`card anthropic-pool-card`,"aria-busy":C||l,children:[(0,J.jsxs)(`div`,{className:`card-row`,style:{alignItems:`flex-start`,gap:12},children:[(0,J.jsxs)(`div`,{style:{flex:1},children:[(0,J.jsx)(`strong`,{children:n(`anthropicPool.title`)}),(0,J.jsx)(`div`,{className:`card-sub`,style:{marginTop:4},children:p?n(`anthropicPool.loadFailed`):C?n(`common.loading`):g?v===0?n(`anthropicPool.enabledNoProactiveDesc`,{window:n(ws[x])}):n(`anthropicPool.enabledDesc`,{threshold:v,window:n(ws[x])}):n(`anthropicPool.disabledDesc`)})]}),(0,J.jsx)(`button`,{type:`button`,className:`toggle ${g?`on`:``}`,disabled:w,"aria-pressed":g,"aria-label":n(`anthropicPool.title`),title:n(g?`anthropicPool.on`:`anthropicPool.off`),onClick:()=>{h({enabled:!g,threshold:v,strategy:y,stickyLimit:b,quotaWindow:x})},children:(0,J.jsx)(`span`,{className:`toggle-knob`})})]}),(0,J.jsx)(`div`,{role:`alert`,className:`card-sub anthropic-pool-card__notice`,children:n(`anthropicPool.experimentalWarning`)}),t<2&&(0,J.jsx)(`div`,{className:`card-sub`,style:{marginTop:8},children:n(`anthropicPool.needTwoAccounts`)}),g&&r&&(0,J.jsxs)(J.Fragment,{children:[(0,J.jsxs)(`label`,{className:`field anthropic-pool-card__field`,children:[(0,J.jsx)(`span`,{className:`field-label`,children:n(`anthropicPool.threshold`)}),(0,J.jsx)(`input`,{className:`input mono`,type:`number`,min:0,max:100,step:1,value:a,disabled:l,"aria-label":n(`anthropicPool.thresholdAria`),onChange:e=>o(e.target.value),onBlur:()=>{let e=Number(a);if(!Number.isInteger(e)||e<0||e>100){o(String(v)),f(n(`anthropicPool.thresholdInvalid`));return}e!==v&&h({enabled:!0,threshold:e,strategy:y,stickyLimit:b,quotaWindow:x})}}),(0,J.jsx)(`div`,{className:`card-sub`,style:{marginTop:4},children:n(`anthropicPool.thresholdHelp`)})]}),(0,J.jsx)(Go,{strategy:y,stickyDraft:s,disabled:l,strategySelectId:`anthropic-pool-strategy`,stickyInputId:`anthropic-pool-sticky-limit`,onStrategyChange:e=>{e!==y&&h({enabled:!0,threshold:v,strategy:e,stickyLimit:b,quotaWindow:x})},onStickyDraftChange:c,onStickyCommit:e=>{let t=Vo(e??s);if(t===null){c(String(b)),f(n(`accountPool.stickyLimitInvalid`));return}if(t===b){c(String(t));return}h({enabled:!0,threshold:v,strategy:y,stickyLimit:t,quotaWindow:x})}}),(0,J.jsxs)(`div`,{className:`field anthropic-pool-card__field anthropic-pool-card__field--quota-window`,children:[(0,J.jsx)(`span`,{className:`field-label`,children:n(`accountPool.quotaWindow`)}),(0,J.jsx)(Dt,{id:`anthropic-pool-quota-window`,value:x,options:No.map(e=>({value:e,label:n(ws[e])})),disabled:l||S,label:n(`accountPool.quotaWindow`),onChange:e=>{let t=zo(e);t!==x&&h({enabled:!0,threshold:v,strategy:y,stickyLimit:b,quotaWindow:t})}}),(0,J.jsx)(`div`,{className:`card-sub`,style:{marginTop:4},children:n(`accountPool.quotaWindowDesc`)}),(0,J.jsx)(`div`,{className:`card-sub`,style:{marginTop:4},children:n(S?`accountPool.quotaWindowInert`:`accountPool.quotaWindowHint`)})]})]}),d&&(0,J.jsx)(`div`,{role:`alert`,className:`card-sub`,style:{marginTop:8,color:`var(--danger, #c44)`},children:d})]})}function Es({serverDefault:e=!0}){let t=Q(),[n,r]=(0,_.useState)(to);return(0,J.jsxs)(`label`,{className:`open-browser-pref`,children:[(0,J.jsx)(`input`,{type:`checkbox`,checked:!(n??e),onChange:e=>{let t=!e.target.checked;r(t),ro(t)}}),(0,J.jsxs)(`span`,{className:`open-browser-pref-copy`,children:[(0,J.jsx)(`span`,{className:`text-label`,children:t(`prov.dontOpenBrowser`)}),(0,J.jsx)(`span`,{className:`muted text-label`,children:t(`prov.dontOpenBrowserHint`)})]})]})}var Ds=4e3,Os=262144,ks=[],As=[];function js({initialState:e,onUpdateProvider:t}){let n=Q(),[r,i]=(0,_.useState)(e),[a,o]=(0,_.useState)(e),[s,c]=(0,_.useState)(!1),[l,u]=(0,_.useState)(``);e!==a&&(o(e),i(e));let d=r===`mixed`,f=async()=>{if(!t||s)return;let e=r!==!0;c(!0),u(``);try{let r=await t(`xai`,{xaiResponsesOptIn:e});if(!r.ok){u(r.error??n(`prov.updateFail`));return}i(r.xaiResponsesOptInState??e)}catch{u(n(`prov.networkError`))}finally{c(!1)}};return(0,J.jsxs)(`div`,{className:`pwi-auth-optin-row`,children:[(0,J.jsxs)(`div`,{className:`pwi-auth-optin-copy`,children:[(0,J.jsx)(`span`,{className:`pwi-auth-optin-label`,children:n(`pws.xaiResponsesOptIn`)}),(0,J.jsxs)(`span`,{className:`pwi-auth-row-secondary`,children:[n(`pws.xaiResponsesOptInDesc`),d&&(0,J.jsxs)(`span`,{className:`pwi-auth-optin-mixed`,children:[` `,n(`pws.xaiResponsesOptInMixed`)]})]}),l&&(0,J.jsx)(`span`,{className:`pwi-auth-optin-error`,role:`alert`,children:l})]}),(0,J.jsx)(Tt,{on:r===!0,mixed:d,onClick:()=>{f()},disabled:!t||s,label:n(`pws.xaiResponsesOptIn`)})]})}var Ms=new Set([`totalCount`,`importedCount`,`updatedCount`,`failedCount`,`unsupportedCount`,`results`]),Ns=new Set([`imported`,`updated`,`failed`,`unsupported`]),Ps={imported:new Set([`imported`]),updated:new Set([`updated`]),failed:new Set([`invalid_record`,`credential_rejected`,`identity_mismatch`,`missing_project`,`persist_failed`]),unsupported:new Set([`unsupported_provider`,`unsupported_format`])};function Fs(e){if(!e||typeof e!=`object`||Array.isArray(e))return!1;let t=Object.getPrototypeOf(e);return t===Object.prototype||t===null}function Is(e){return typeof e==`number`&&Number.isSafeInteger(e)&&e>=0}function Ls(e){if(!Fs(e)||Object.keys(e).some(e=>!Ms.has(e)))return null;let{totalCount:t,importedCount:n,updatedCount:r,failedCount:i,unsupportedCount:a,results:o}=e;if(!Is(t)||!Is(n)||!Is(r)||!Is(i)||!Is(a)||!Array.isArray(o)||o.length!==t||n+r+i+a!==t)return null;let s={imported:0,updated:0,failed:0,unsupported:0};for(let[e,t]of o.entries()){if(!Fs(t)||Object.keys(t).some(e=>![`index`,`status`,`code`].includes(e)))return null;let n=String(t.status),r=String(t.code);if(t.index!==e||!Ns.has(n)||!Ps[n]?.has(r))return null;s[n]+=1}return s.imported!==n||s.updated!==r||s.failed!==i||s.unsupported!==a?null:{importedCount:n,updatedCount:r,failedCount:i,unsupportedCount:a}}function Rs({item:e,apiBase:t,oauth:n,accounts:r=ks,keys:i=As,accountLoadState:a=`ready`,switchingAccountId:o=null,busy:s=!1,loginHint:c,authHandlers:l,onCodexActiveNeedsReauthChange:u,codexController:d,onUpdateProvider:f}){let p=Q(),[m,h]=(0,_.useState)(!1),[g,v]=(0,_.useState)(``),[y,b]=(0,_.useState)(!1),[x,S]=(0,_.useState)(!1),[C,w]=(0,_.useState)(`idle`),[T,E]=(0,_.useState)(null),[D,O]=(0,_.useState)(!1),k=(0,_.useRef)(null),[A,j]=(0,_.useState)(``),[M,N]=(0,_.useState)(!1),[P,F]=(0,_.useState)(``),[I,L]=(0,_.useState)(!0);(0,_.useEffect)(()=>{if(r.length===0){O(!1);return}if(!r.some(e=>e.quota==null&&!e.quotaUnavailable)){O(!1);return}O(!0);let e=window.setTimeout(()=>O(!1),Ds);return()=>window.clearTimeout(e)},[r]);let R=wa({...e,hasApiKey:e.hasApiKey||i.length>0}),z=R===`oauth-accounts`,B=R===`api-keys`;if(R===`codex-accounts`)return(0,J.jsxs)(`section`,{className:`pwi-section pwi-auth-section`,"aria-label":p(`pws.availableAccounts`),children:[(0,J.jsx)(`h3`,{className:`pwi-section-title`,children:p(`pws.availableAccounts`)}),(0,J.jsx)(`div`,{className:`pwi-auth-body`,children:(0,J.jsx)(Cs,{apiBase:t,embedded:!0,controller:d,onActiveNeedsReauthChange:u})})]});if(!R||!l)return null;let V=c?.provider===e.name?c:null,H=async()=>{let n=A.trim();if(!(!n||M)){N(!0),F(``);try{let r=await fetch(`${t}/api/oauth/login/code`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({provider:e.name,input:n})});if(!r.ok){let e=await r.json().catch(()=>({}));L(!1),F(p(`prov.pasteFail`,{error:e.error||r.statusText}));return}j(``),L(!0),F(p(`prov.pasteOk`))}catch{L(!1),F(p(`modal.networkError`))}finally{N(!1)}}},U=r.length>0||n?.loggedIn===!0,W=r.find(e=>e.active&&e.needsReauth),ee=!!W,G=async()=>{let t=g.trim();if(t){b(!0);try{await l.onAddApiKey(e.name,t)&&(v(``),h(!1))}finally{b(!1)}}},K=async n=>{if(!(!n||x)){S(!0),w(`idle`),E(null);try{if(!n.name.toLowerCase().endsWith(`.json`)||n.size>Os){w(`invalid`);return}let r;try{r=JSON.parse(await n.text())}catch{w(`invalid`);return}let i=await fetch(`${t}/api/oauth/accounts/import`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({provider:`google-antigravity`,format:`cockpit-tools`,document:r})});if(!i.ok){w(`failed`);return}let a=Ls(await i.json().catch(()=>null));if(!a){w(`failed`);return}E(a),w(`complete`);try{await l.onRetryAccounts?.(e.name)}catch{}}catch{w(`failed`)}finally{k.current&&(k.current.value=``),S(!1)}}};return(0,J.jsxs)(`section`,{className:`pwi-section pwi-auth-section`,"aria-label":p(z?`pws.availableAccounts`:`pws.apiKeys`),children:[(0,J.jsx)(`h3`,{className:`pwi-section-title`,children:p(z?`pws.availableAccounts`:`pws.apiKeys`)}),(0,J.jsxs)(`div`,{className:`pwi-auth-body`,children:[e.name===`xai`&&(0,J.jsx)(js,{initialState:e.xaiResponsesOptInState??!1,onUpdateProvider:f}),z&&(0,J.jsxs)(J.Fragment,{children:[e.name===`anthropic`&&(0,J.jsx)(Ts,{apiBase:t,accountCount:r.length}),e.name===`google-antigravity`&&(0,J.jsxs)(`div`,{className:`pwi-auth-add-key`,children:[(0,J.jsxs)(`div`,{children:[(0,J.jsx)(`div`,{id:`cockpit-import-description`,className:`pwi-auth-row-secondary`,children:p(`pws.cockpitImportDescription`)}),(0,J.jsx)(`label`,{className:`sr-only`,htmlFor:`cockpit-import-file`,children:p(`pws.cockpitImportFileLabel`)}),(0,J.jsx)(`input`,{ref:k,id:`cockpit-import-file`,type:`file`,accept:`application/json,.json`,className:`sr-only`,"aria-describedby":`cockpit-import-description cockpit-import-status`,disabled:x,onChange:e=>{K(e.currentTarget.files?.[0])}})]}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,disabled:x,onClick:()=>k.current?.click(),children:p(x?`pws.cockpitImporting`:`pws.cockpitImportChooseFile`)}),(0,J.jsxs)(`div`,{id:`cockpit-import-status`,role:`status`,"aria-live":`polite`,children:[C===`invalid`&&p(`pws.cockpitImportInvalid`),C===`failed`&&p(`pws.cockpitImportFailed`),C===`complete`&&T&&p(`pws.cockpitImportComplete`,{imported:T.importedCount,updated:T.updatedCount,failed:T.failedCount,unsupported:T.unsupportedCount})]})]}),(0,J.jsxs)(`div`,{className:`pwi-auth-status-row`,children:[(0,J.jsx)(`span`,{className:`pwi-auth-dot ${ee?`pwi-auth-dot--warn`:U?`pwi-auth-dot--ok`:`pwi-auth-dot--off`}`,"aria-hidden":`true`}),(0,J.jsx)(`span`,{className:`pwi-auth-status-text`,children:U?r.length>0?p(`pws.loggedInTitle`):n?.email??p(`pws.loggedInTitle`):n?.error||p(`pws.notLoggedInTitle`)}),(0,J.jsxs)(`span`,{className:`pwi-auth-actions`,children:[W&&(0,J.jsx)(`button`,{type:`button`,className:`btn btn-primary btn-sm`,disabled:s,onClick:()=>void l.onReauth(e.name,W.id),children:p(`pws.reauthenticate`)}),U?(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:()=>void l.onLogout(e.name),children:p(`prov.logout`)}):(0,J.jsxs)(`button`,{type:`button`,className:`btn btn-primary btn-sm`,disabled:s,onClick:()=>void l.onLogin(e.name,!1),children:[s?(0,J.jsx)(`span`,{className:`pwi-spin-inline`,"aria-hidden":`true`}):(0,J.jsx)(De,{style:{width:13,height:13},"aria-hidden":`true`}),p(s?`prov.waitingBrowser`:`prov.login`)]})]})]}),!s&&(0,J.jsx)(Es,{}),s&&V&&(0,J.jsxs)(`div`,{className:`pwi-auth-wait`,children:[(0,J.jsx)(`span`,{className:`pwi-spin-inline`,"aria-hidden":`true`}),(0,J.jsxs)(`div`,{className:`pwi-auth-wait-copy`,children:[(0,J.jsx)(`div`,{className:`pwi-auth-wait-title`,children:p(`prov.waitingBrowser`)}),(0,J.jsx)(Qa,{hint:{url:V.url,deviceCode:V.deviceCode,instructions:V.instructions},paste:{value:A,busy:M,message:P,ok:I,onChange:j,onSubmit:()=>{H()}}}),l.onCancelLogin&&(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:()=>void l.onCancelLogin?.(e.name),children:p(`common.cancel`)})]})]}),a===`loading`&&r.length===0&&(0,J.jsxs)(`div`,{className:`pwi-auth-state`,role:`status`,children:[(0,J.jsx)(`span`,{className:`pwi-spin-inline`,"aria-hidden":`true`}),p(`pws.accountsLoading`)]}),a===`error`&&(0,J.jsxs)(`div`,{className:`pwi-auth-state pwi-auth-state--error`,role:`alert`,children:[(0,J.jsx)(`span`,{children:p(`pws.accountsLoadFailed`)}),l.onRetryAccounts&&(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:()=>void l.onRetryAccounts?.(e.name),children:p(`pws.retryAccounts`)})]}),r.length>0&&(0,J.jsx)(`ul`,{className:`pwi-auth-list`,children:r.map(t=>{let n=Ta(r,t,p),i=o===t.id,a=t.health?.status,c=!!t.needsReauth||Ia(a),u=Ra(a),d=Na(t.id),f=Va(p,t.health),m=Ha(p,e.name,t.id,t.health);return(0,J.jsxs)(`li`,{className:`pwi-auth-acct${t.active?` pwi-auth-acct--active`:``}`,children:[(0,J.jsxs)(`div`,{className:`pwi-auth-row${t.active?` pwi-auth-row--active`:``}`,children:[(0,J.jsxs)(`button`,{type:`button`,className:`pwi-auth-row-main`,onClick:()=>{!t.active&&!c&&!u&&!o&&l.onSwitchAccount(e.name,t)},"aria-current":t.active?`true`:void 0,"aria-label":`${n}${t.active?` — ${p(`pws.accountCurrent`)}`:``}`,disabled:!!(c||u||o&&!i),children:[(0,J.jsx)(`span`,{className:`pwi-auth-dot ${c?`pwi-auth-dot--warn`:t.active?`pwi-auth-dot--ok`:`pwi-auth-dot--off`}`,"aria-hidden":`true`}),(0,J.jsxs)(`span`,{className:`pwi-auth-row-copy`,children:[(0,J.jsx)(`span`,{className:`pwi-auth-row-label`,children:n}),(0,J.jsx)(`span`,{className:`pwi-auth-row-secondary`,children:[t.email,`${p(`prov.accountId`)}: ${d}`].filter(Boolean).join(` · `)}),m&&(0,J.jsx)(`span`,{className:`pwi-auth-row-secondary faint`,children:m}),u&&(0,J.jsx)(`span`,{className:`pwi-auth-row-secondary faint`,children:p(`pws.healthCooldownHint`)})]}),f&&(0,J.jsx)(`span`,{className:Fa(a),children:f}),c&&!f&&(0,J.jsx)(`span`,{className:`badge badge-amber`,children:p(`pws.reauth`)}),t.active&&(0,J.jsx)(`span`,{className:`badge badge-primary`,children:p(`prov.accountActive`)}),i&&(0,J.jsx)(`span`,{className:`badge badge-muted`,children:p(`pws.accountSwitching`)})]}),c&&(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,disabled:s||!!o,onClick:()=>void l.onReauth(e.name,t.id),children:p(`pws.reauthenticate`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:()=>void l.onEditAlias(e.name,`oauth`,t.id,t.alias),children:p(`prov.editAlias`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm pwi-auth-row-remove`,"aria-label":`${p(`common.remove`)} — ${n}`,title:`${p(`common.remove`)} — ${n}`,disabled:!!o,onClick:()=>void l.onRemoveAccount(e.name,t),children:(0,J.jsx)(he,{style:{width:13,height:13},"aria-hidden":`true`})})]}),(t.quota!=null||t.quotaUnavailable||D&&t.quota==null)&&(0,J.jsx)(`div`,{className:`pwi-auth-acct-quota`,children:t.quotaUnavailable?(0,J.jsx)(`p`,{className:`muted pwi-auth-acct-quota-stale`,children:p(`pws.accountQuotaUnavailable`)}):(0,J.jsx)(ia,{quota:t.quota??null,plan:null,threshold:80,t:p,layout:`stacked`,pending:t.quota==null,...e.name===`meta-muse`&&t.quota?{observedAt:t.quota.updatedAt}:{}})})]},t.id)})}),a===`ready`&&U&&r.length===0&&(0,J.jsx)(`div`,{className:`pwi-auth-state pwi-auth-state--empty`,children:p(`pws.noAccounts`)}),U&&(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,style:{marginTop:8},onClick:()=>void l.onLogin(e.name,!0),disabled:s||!!o,children:p(`pws.addAccount`)})]}),B&&(0,J.jsxs)(J.Fragment,{children:[i.length>0&&(0,J.jsx)(`ul`,{className:`pwi-auth-list`,children:i.map(t=>(0,J.jsxs)(`li`,{className:`pwi-auth-row${t.active?` pwi-auth-row--active`:``}`,children:[(0,J.jsxs)(`button`,{type:`button`,className:`pwi-auth-row-main`,onClick:()=>void l.onSwitchApiKey(e.name,t),disabled:t.active,children:[(0,J.jsx)(`span`,{className:`pwi-auth-dot ${t.active?`pwi-auth-dot--ok`:`pwi-auth-dot--off`}`,"aria-hidden":`true`}),(0,J.jsxs)(`span`,{className:`pwi-auth-row-copy`,children:[(0,J.jsx)(`span`,{className:`pwi-auth-row-label`,children:t.label??t.masked}),t.label&&(0,J.jsxs)(`code`,{className:`pwi-auth-row-secondary`,children:[t.masked,` · `,p(`prov.accountId`),`: `,t.id]})]}),t.active&&(0,J.jsx)(`span`,{className:`badge badge-primary`,children:p(`prov.accountActive`)})]}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:()=>void l.onEditAlias(e.name,`api-key`,t.id,t.label),children:p(`prov.editAlias`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm pwi-auth-row-remove`,"aria-label":`${p(`common.remove`)} — ${t.label??t.masked}`,title:`${p(`common.remove`)} — ${t.label??t.masked}`,onClick:()=>void l.onRemoveApiKey(e.name,t),children:(0,J.jsx)(he,{style:{width:13,height:13},"aria-hidden":`true`})})]},t.id))}),m?(0,J.jsxs)(`div`,{className:`pwi-auth-add-key`,children:[(0,J.jsx)(`input`,{className:`input`,type:`password`,value:g,onChange:e=>v(e.target.value),placeholder:p(`modal.apiKeyPlaceholder`),autoComplete:`off`,disabled:y}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-primary btn-sm`,onClick:()=>void G(),disabled:y||!g.trim(),children:p(y?`pws.saving`:`pws.addKey`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:()=>{h(!1),v(``)},children:p(`common.cancel`)})]}):(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,style:{marginTop:8},onClick:()=>h(!0),children:p(`pws.addKey`)})]})]})]})}function zs(e,t,n){let r=e?.find(e=>e.id===t);if(!r)return n;if(r.baseUrl)return r.baseUrl;let i=new Set((e??[]).map(e=>e.baseUrl?.trim().replace(/\/+$/,``)).filter(e=>!!e)),a=n.trim().replace(/\/+$/,``);return i.has(a)?``:n}function Bs(e,t){if(!e?.length)return`custom`;let n=t.trim().replace(/\/+$/,``);for(let t of e)if(t.baseUrl&&t.baseUrl.trim().replace(/\/+$/,``)===n)return t.id;return e.some(e=>e.id===`custom`)?`custom`:e[0].id}function Vs(e,t,n){let r=e?.find(e=>e.id===t);return r?.baseUrl?r.baseUrl.trim():n.trim()}var Hs=`https://chatgpt.com/backend-api/codex`;function Us(e){try{let t=new URL(e.trim());if(t.username||t.password||t.search||t.hash)return;let n=t.pathname.replace(/\/+$/,``);return`${t.origin}${n}`}catch{return}}function Ws(e){return[`openai`,...Object.entries(e).filter(([,e])=>e.authMode===`forward`).map(([e])=>e).filter(e=>e!==`openai`).sort((e,t)=>e.localeCompare(t))]}function Gs(e){return e?e.adapter!==`openai-responses`||e.authMode!==`forward`||typeof e.baseUrl!=`string`||Us(e.baseUrl)!==Hs?`invalid`:e.disabled===!0?`disabled`:`ready`:`absent`}function Ks(e){return e.id===`openai`}function qs(e){return e.id===`openai`?e.codexAccountMode===`direct`?`prov.openaiDirectDesc`:`prov.openaiPoolDesc`:null}function Js(e){let t={adapter:e.adapter.trim(),baseUrl:e.baseUrl.trim()};return e.responsesPath?.trim()&&(t.responsesPath=e.responsesPath.trim()),(e.authMode===`key`||e.authMode===`forward`)&&(t.authMode=e.authMode),e.authMode===`key`&&e.apiKey.trim()&&(t.apiKey=e.apiKey.trim()),e.adapter.trim()===`anthropic`&&e.authMode===`key`&&e.apiKeyTransport===`bearer`&&(t.apiKeyTransport=`bearer`),e.defaultModel.trim()&&(t.defaultModel=e.defaultModel.trim()),e.allowPrivateNetwork&&(t.allowPrivateNetwork=!0),t}function Ys(e,t){return Ks(e)?Xs(e):{name:t.name.trim(),provider:Js(t)}}function Xs(e){if(!e.provider)throw Error(`Missing canonical provider seed for ${e.id}`);return{name:e.id,provider:structuredClone(e.provider)}}var Zs=class extends Error{i18nKey;constructor(e){super(e),this.name=`OpenAiEnableError`,this.i18nKey=e}};async function Qs(e,t,n=fetch){if(t===`disabled`){if((await n(`${e}/api/providers?name=openai`,{method:`PATCH`,headers:{"Content-Type":`application/json`},body:JSON.stringify({disabled:!1})})).ok)return;throw new Zs(`codexAuth.enableOpenaiFailed`)}let r=await n(`${e}/api/provider-presets`);if(!r.ok)throw new Zs(`codexAuth.openaiPresetLoadFailed`);let i=(await r.json()).providers?.find(e=>e.id===`openai`);if(!i?.provider)throw new Zs(`codexAuth.openaiPresetUnavailable`);if(!(await n(`${e}/api/providers`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify(Xs(i))})).ok)throw new Zs(`codexAuth.enableOpenaiFailed`)}var $s=[`openai-responses`,`openai-chat`,`anthropic`,`google`,`azure-openai`,`cursor`],ec=[];function tc(e){return e===`http1.1`||e===`h1`?`http1.1`:`http2`}function nc(e){return e===void 0?``:String(e)}function rc(e){if(!e.trim())return;let t=Number(e);return Number.isFinite(t)&&t>=1/60?t:void 0}function ic(e){if(!e.trim())return;let t=Number(e);return Number.isSafeInteger(t)&&t>0?t:void 0}function ac(e){let t=Object.entries(e?.models??{}).sort(([e],[t])=>e.localeCompare(t)).map(([e,t])=>[e,t.requestsPerMinute??null,t.minIntervalMs??null]);return JSON.stringify([e?.enabled===!0,e?.requestsPerMinute??null,e?.minIntervalMs??null,t])}function oc({item:e,availableModels:t=ec,apiBase:n,onUpdateProvider:r,onDirtyChange:i,onRegisterSave:a}){let o=Q(),s=String(e.authMode??(e.keyOptional?`local`:`key`)),c=ri(e.name,e),l=c?e.liveModels!==!1:!1,u=tc(e.upstreamHttpVersion),[d,f]=(0,_.useState)(e.adapter),[p,m]=(0,_.useState)(e.baseUrl),[h,g]=(0,_.useState)(e.defaultModel??``),[v,y]=(0,_.useState)(s),[b,x]=(0,_.useState)(e.apiKeyTransport??`x-api-key`),[S,C]=(0,_.useState)(e.note??``),[w,T]=(0,_.useState)(e.allowPrivateNetwork??!1),[E,D]=(0,_.useState)(l),[O,k]=(0,_.useState)(u),[A,j]=(0,_.useState)(!1),[M,N]=(0,_.useState)(null),[P,F]=(0,_.useState)(e.codexAccountMode??`pool`),[I,L]=(0,_.useState)(!1),[R,z]=(0,_.useState)(null),[B,V]=(0,_.useState)(),[H,U]=(0,_.useState)(n?`loading`:`idle`),[W,ee]=(0,_.useState)(()=>`custom`),[G,K]=(0,_.useState)(e.requestPacing?.enabled===!0),[q,Y]=(0,_.useState)(()=>nc(e.requestPacing?.requestsPerMinute)),[te,ne]=(0,_.useState)(()=>nc(e.requestPacing?.minIntervalMs)),[re,ie]=(0,_.useState)(()=>({...e.requestPacing?.models??{}})),[ae,oe]=(0,_.useState)(``),[se,ce]=(0,_.useState)(``),[le,ue]=(0,_.useState)(``),[de,fe]=(0,_.useState)(null);(0,_.useEffect)(()=>{f(e.adapter),m(e.baseUrl),g(e.defaultModel??``),y(String(e.authMode??(e.keyOptional?`local`:`key`))),x(e.apiKeyTransport??`x-api-key`),C(e.note??``),T(e.allowPrivateNetwork??!1),D(l),k(u),K(e.requestPacing?.enabled===!0),Y(nc(e.requestPacing?.requestsPerMinute)),ne(nc(e.requestPacing?.minIntervalMs)),ie({...e.requestPacing?.models??{}}),N(null),z(null),queueMicrotask(()=>ee(Bs(B,e.baseUrl)))},[e.adapter,e.baseUrl,e.defaultModel,e.authMode,e.apiKeyTransport,e.keyOptional,e.note,e.allowPrivateNetwork,l,u,e.requestPacing,B]),(0,_.useEffect)(()=>{F(e.codexAccountMode??`pool`)},[e.codexAccountMode]),(0,_.useEffect)(()=>{if(!n)return;let t=!1,r=e.name,i=e.baseUrl;return fetch(`${n}/api/provider-presets`).then(e=>Ft(e)).then(e=>{if(t)return;if(!e){V(void 0),U(`error`);return}let n=(e.providers??[]).find(e=>e.id===r)?.baseUrlChoices;V(n),U(`ready`),ee(Bs(n,i))}).catch(()=>{t||(V(void 0),U(`error`))}),()=>{t=!0}},[n,e.name]),(0,_.useEffect)(()=>{if(!n)return;let t=!0,r=!1,i=()=>{if(r)return;r=!0;let i=Vn(1e4);fetch(`${n}/api/provider-request-pacing?name=${encodeURIComponent(e.name)}`,{signal:i.signal}).then(e=>Ft(e)).then(e=>{t&&e&&fe(e)}).catch(()=>void 0).finally(()=>{i.clear(),r=!1})};i();let a=Gn(i,2e3);return()=>{t=!1,a()}},[n,e.name]);let pe=(0,_.useMemo)(()=>({enabled:G,...rc(q)===void 0?{}:{requestsPerMinute:rc(q)},...ic(te)===void 0?{}:{minIntervalMs:ic(te)},...Object.keys(re).length>0?{models:re}:{}}),[te,G,re,q]),X=d.trim()!==e.adapter||p.trim()!==e.baseUrl||h.trim()!==(e.defaultModel??``)||v!==String(e.authMode??(e.keyOptional?`local`:`key`))||d.trim()===`anthropic`&&v===`key`&&b!==(e.apiKeyTransport??`x-api-key`)||S.trim()!==(e.note??``)||w!==(e.allowPrivateNetwork??!1)||E!==l||d.trim()===`cursor`&&O!==u,me=ac(pe)!==ac(e.requestPacing),he=X||me;(0,_.useEffect)(()=>(i?.(he),()=>i?.(!1)),[he,i]);let ge=(0,_.useMemo)(()=>{let n=new Set(t);return h.trim()&&n.add(h.trim()),e.defaultModel&&n.add(e.defaultModel),[...n].sort((e,t)=>e.localeCompare(t))},[t,h,e.defaultModel]),_e=(0,_.useMemo)(()=>{let e=[...$s];return d&&!e.includes(d)&&e.unshift(d),e},[d]),Z=Mn(e.name),ve=H===`ready`&&!!(B&&B.length>0),ye=d.trim()===`anthropic`&&v===`key`,be=e.name===`openai`?Gs(e):`invalid`,xe=be===`ready`||be===`disabled`,Se=Z&&H!==`error`,Ce=async()=>{if(!r)return N({ok:!1,text:o(`pws.updatesUnavailable`)}),!1;if(I)return!1;let t=ve?Vs(B,W,p):p.trim();if(!d.trim()||!t)return N({ok:!1,text:o(`pws.adapterBaseRequired`)}),!1;j(!0),N(null);try{if(G&&!pe.requestsPerMinute&&!pe.minIntervalMs&&!pe.models)return N({ok:!1,text:o(`pws.pacingRuleRequired`)}),!1;let n=me&&!X,i=n?{requestPacing:pe}:{adapter:d.trim(),baseUrl:t,defaultModel:h.trim(),authMode:v,note:S.trim(),allowPrivateNetwork:w,...me?{requestPacing:pe}:{}};n||(c&&E!==(e.liveModels!==!1)&&(i.liveModels=E),d.trim()===`cursor`&&O!==u&&(i.upstreamHttpVersion=O===`http1.1`?`http1.1`:null),ye?i.apiKeyTransport=b:e.apiKeyTransport!==void 0&&(i.apiKeyTransport=``));let a=await r(e.name,i);return N(a.ok?{ok:!0,text:o(`pws.settingsSaved`)}:{ok:!1,text:a.error||o(`prov.saveFailed`)}),a.ok}finally{j(!1)}},we=(0,_.useRef)(Ce);(0,_.useEffect)(()=>{we.current=Ce}),(0,_.useEffect)(()=>{if(a)return a(()=>we.current()),()=>a(null)},[a]);let Te=async e=>{if(!(I||A||e===P)){if(!r){z({ok:!1,text:o(`pws.updatesUnavailable`)});return}L(!0),z(null);try{let t=await r(`openai`,{codexAccountMode:e});t.ok?(F(e),z({ok:!0,text:o(`pws.accountModeSaved`)})):z({ok:!1,text:t.error||o(`pws.accountModeFailed`)})}catch{z({ok:!1,text:o(`pws.accountModeFailed`)})}finally{L(!1)}}},Ee=()=>{f(e.adapter),m(e.baseUrl),g(e.defaultModel??``),y(s),x(e.apiKeyTransport??`x-api-key`),C(e.note??``),T(e.allowPrivateNetwork??!1),D(l),k(u),N(null),K(e.requestPacing?.enabled===!0),Y(nc(e.requestPacing?.requestsPerMinute)),ne(nc(e.requestPacing?.minIntervalMs)),ie({...e.requestPacing?.models??{}}),ee(Bs(B,e.baseUrl))},Oe=(e,t)=>{switch(e){case`token-plan`:return o(`modal.endpoint.tokenPlan`);case`payg`:return o(`modal.endpoint.payAsYouGo`);case`custom`:return o(`modal.endpoint.custom`);default:return t}};return(0,J.jsxs)(`div`,{className:`pwi-settings-form`,children:[(0,J.jsxs)(`label`,{className:`pwi-settings-field`,children:[(0,J.jsxs)(`span`,{className:`pwi-settings-label`,children:[(0,J.jsx)(De,{style:{width:12,height:12}}),` `,o(`pws.providerId`)]}),(0,J.jsx)(`input`,{className:`input`,value:e.name,readOnly:!0,disabled:!0})]}),(0,J.jsxs)(`label`,{className:`pwi-settings-field`,children:[(0,J.jsx)(`span`,{className:`pwi-settings-label`,children:o(`modal.adapter`)}),Z?(0,J.jsx)(`input`,{className:`input`,value:d,readOnly:!0,disabled:!0}):(0,J.jsx)(`select`,{className:`input`,value:d,onChange:e=>f(e.target.value),children:_e.map(e=>(0,J.jsx)(`option`,{value:e,children:e},e))})]}),ve?(0,J.jsxs)(J.Fragment,{children:[(0,J.jsxs)(`label`,{className:`pwi-settings-field`,children:[(0,J.jsx)(`span`,{className:`pwi-settings-label`,children:o(`modal.endpoint`)}),(0,J.jsx)(`select`,{className:`input`,value:W,onChange:e=>{let t=e.target.value;ee(t),m(zs(B,t,p))},children:B.map(e=>(0,J.jsx)(`option`,{value:e.id,children:Oe(e.id,e.label)},e.id))})]}),W===`custom`&&(0,J.jsxs)(`label`,{className:`pwi-settings-field`,children:[(0,J.jsx)(`span`,{className:`pwi-settings-label`,children:o(`modal.baseUrl`)}),(0,J.jsx)(`input`,{className:`input`,value:p,onChange:e=>m(e.target.value),placeholder:o(`modal.baseUrlPlaceholder`)})]})]}):(0,J.jsxs)(`label`,{className:`pwi-settings-field`,children:[(0,J.jsx)(`span`,{className:`pwi-settings-label`,children:o(`modal.baseUrl`)}),(0,J.jsx)(`input`,{className:`input`,value:p,onChange:e=>m(e.target.value),readOnly:Se,disabled:Se})]}),d.trim()===`cursor`&&(0,J.jsxs)(`label`,{className:`pwi-settings-field`,children:[(0,J.jsx)(`span`,{className:`pwi-settings-label`,children:o(`pws.cursorTransport`)}),(0,J.jsxs)(`select`,{className:`input`,value:O,onChange:e=>k(e.target.value),children:[(0,J.jsx)(`option`,{value:`http2`,children:o(`pws.cursorTransportHttp2`)}),(0,J.jsx)(`option`,{value:`http1.1`,children:o(`pws.cursorTransportHttp1`)})]}),(0,J.jsx)(`span`,{className:`pwi-settings-hint`,children:o(`pws.cursorTransportDesc`)})]}),(0,J.jsxs)(`label`,{className:`pwi-settings-field`,children:[(0,J.jsx)(`span`,{className:`pwi-settings-label`,children:o(`pws.cell.defaultModel`)}),ge.length>0?(0,J.jsxs)(`select`,{className:`input`,value:h,onChange:e=>g(e.target.value),children:[(0,J.jsx)(`option`,{value:``,children:o(`pws.defaultModelNone`)}),ge.map(e=>(0,J.jsx)(`option`,{value:e,children:e},e))]}):(0,J.jsx)(`input`,{className:`input`,value:h,onChange:e=>g(e.target.value),placeholder:o(`pws.optionalPlaceholder`)})]}),(0,J.jsxs)(`label`,{className:`pwi-settings-field`,children:[(0,J.jsx)(`span`,{className:`pwi-settings-label`,children:o(`pws.authMode`)}),Z?(0,J.jsx)(`input`,{className:`input`,value:Mi(e,o),readOnly:!0,disabled:!0}):(0,J.jsxs)(`select`,{className:`input`,value:v,onChange:e=>y(e.target.value),children:[(0,J.jsx)(`option`,{value:`key`,children:o(`modal.badge.apiKey`)}),(0,J.jsx)(`option`,{value:`forward`,children:o(`pws.auth.chatgptPassthrough`)}),(0,J.jsx)(`option`,{value:`oauth`,children:o(`modal.badge.oauth`)}),(0,J.jsx)(`option`,{value:`local`,children:o(`modal.badge.local`)})]})]}),xe&&(0,J.jsxs)(`label`,{className:`pwi-settings-field`,children:[(0,J.jsx)(`span`,{className:`pwi-settings-label`,children:o(`codexAuth.accountModeTitle`)}),(0,J.jsxs)(`select`,{className:`input`,value:P,disabled:I||A,onChange:e=>{let t=e.target.value;if(t!==P){if(!window.confirm(o(`pws.accountModeConfirm`))){e.target.value=P;return}Te(t)}},children:[(0,J.jsx)(`option`,{value:`pool`,children:o(`codexAuth.accountModePool`)}),(0,J.jsx)(`option`,{value:`direct`,children:o(`codexAuth.accountModeDirect`)})]}),(0,J.jsx)(`span`,{className:`pwi-settings-hint`,children:o(P===`direct`?`codexAuth.accountModeDirectDesc`:`codexAuth.accountModePoolDesc`)}),I&&(0,J.jsx)(`span`,{className:`muted text-label`,children:o(`pws.accountSwitching`)}),R&&(0,J.jsx)(`span`,{role:R.ok?`status`:`alert`,className:R.ok?`pwi-settings-mode-msg pwi-settings-mode-msg--ok`:`pwi-settings-mode-msg pwi-settings-mode-msg--err`,children:R.text})]}),ye&&(0,J.jsxs)(`label`,{className:`pwi-settings-field`,children:[(0,J.jsx)(`span`,{className:`pwi-settings-label`,children:o(`modal.apiKeyTransport`)}),(0,J.jsxs)(`select`,{className:`input`,value:b,onChange:e=>x(e.target.value),children:[(0,J.jsx)(`option`,{value:`x-api-key`,children:o(`modal.apiKeyTransportNative`)}),(0,J.jsx)(`option`,{value:`bearer`,children:o(`modal.apiKeyTransportBearer`)})]})]}),(0,J.jsxs)(`label`,{className:`pwi-settings-field`,children:[(0,J.jsx)(`span`,{className:`pwi-settings-label`,children:o(`pws.note`)}),(0,J.jsx)(`textarea`,{className:`input pwi-settings-textarea`,value:S,onChange:e=>C(e.target.value),rows:2})]}),(0,J.jsxs)(`label`,{className:`pwi-settings-field`,style:{flexDirection:`row`,alignItems:`center`,gap:8},children:[(0,J.jsx)(`input`,{type:`checkbox`,checked:w,onChange:e=>T(e.target.checked)}),(0,J.jsx)(`span`,{className:`pwi-settings-label`,children:o(`pws.allowPrivateNetwork`)})]}),(0,J.jsxs)(`label`,{className:`pwi-settings-field`,style:{flexDirection:`row`,alignItems:`flex-start`,gap:8},children:[(0,J.jsx)(`input`,{type:`checkbox`,checked:E,disabled:!c,onChange:e=>D(e.target.checked)}),(0,J.jsxs)(`span`,{children:[(0,J.jsx)(`span`,{className:`pwi-settings-label`,children:o(`pws.liveModels`)}),(0,J.jsx)(`span`,{className:`muted text-label`,style:{display:`block`,marginTop:2},children:o(`pws.liveModelsDesc`)})]})]}),(0,J.jsxs)(`section`,{className:`pwi-pacing-card`,"aria-labelledby":`pwi-pacing-title`,children:[(0,J.jsxs)(`div`,{className:`pwi-pacing-head`,children:[(0,J.jsxs)(`div`,{children:[(0,J.jsx)(`h3`,{id:`pwi-pacing-title`,children:o(`pws.pacingTitle`)}),(0,J.jsx)(`p`,{children:o(`pws.pacingDesc`)})]}),(0,J.jsxs)(`label`,{className:`pwi-pacing-toggle`,children:[(0,J.jsx)(`input`,{type:`checkbox`,checked:G,onChange:e=>K(e.target.checked)}),` `,o(`pws.pacingEnabled`)]})]}),(0,J.jsxs)(`div`,{className:`pwi-pacing-grid`,children:[(0,J.jsxs)(`label`,{className:`pwi-settings-field`,children:[(0,J.jsx)(`span`,{className:`pwi-settings-label`,children:o(`pws.pacingRpm`)}),(0,J.jsx)(`input`,{className:`input`,type:`number`,min:`0.016667`,step:`any`,value:q,onChange:e=>Y(e.target.value),placeholder:`38`})]}),(0,J.jsxs)(`label`,{className:`pwi-settings-field`,children:[(0,J.jsx)(`span`,{className:`pwi-settings-label`,children:o(`pws.pacingDelay`)}),(0,J.jsx)(`input`,{className:`input`,type:`number`,min:`1`,step:`1`,value:te,onChange:e=>ne(e.target.value),placeholder:`1600`})]})]}),(0,J.jsx)(`p`,{className:`pwi-settings-hint`,children:o(`pws.pacingSlowerWins`)}),(0,J.jsxs)(`div`,{className:`pwi-pacing-status`,"aria-live":`polite`,children:[(0,J.jsxs)(`span`,{children:[(0,J.jsx)(`strong`,{children:de?.queued??0}),` `,o(`pws.pacingQueued`)]}),(0,J.jsxs)(`span`,{children:[(0,J.jsxs)(`strong`,{children:[de?.nextSlotInMs??0,` ms`]}),` `,o(`pws.pacingNextSlot`)]}),(0,J.jsxs)(`span`,{children:[(0,J.jsx)(`strong`,{children:de?.lastModelId??o(`pws.pacingNone`)}),` `,o(`pws.pacingLastModel`)]})]}),(0,J.jsx)(`h4`,{children:o(`pws.pacingModelOverrides`)}),(0,J.jsxs)(`div`,{className:`pwi-pacing-grid pwi-pacing-grid--model`,children:[(0,J.jsxs)(`label`,{className:`pwi-settings-field`,children:[(0,J.jsx)(`span`,{className:`pwi-settings-label`,children:o(`pws.pacingModel`)}),(0,J.jsx)(`input`,{className:`input`,list:`pacing-models-${e.name}`,value:ae,onChange:e=>oe(e.target.value)}),(0,J.jsx)(`datalist`,{id:`pacing-models-${e.name}`,children:t.map(e=>(0,J.jsx)(`option`,{value:e},e))})]}),(0,J.jsxs)(`label`,{className:`pwi-settings-field`,children:[(0,J.jsx)(`span`,{className:`pwi-settings-label`,children:o(`pws.pacingRpm`)}),(0,J.jsx)(`input`,{className:`input`,type:`number`,min:`0.016667`,step:`any`,value:se,onChange:e=>ce(e.target.value)})]}),(0,J.jsxs)(`label`,{className:`pwi-settings-field`,children:[(0,J.jsx)(`span`,{className:`pwi-settings-label`,children:o(`pws.pacingDelay`)}),(0,J.jsx)(`input`,{className:`input`,type:`number`,min:`1`,step:`1`,value:le,onChange:e=>ue(e.target.value)})]}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:()=>{let e=ae.trim(),t=rc(se),n=ic(le);!e||t===void 0&&n===void 0||(ie(r=>({...r,[e]:{...t===void 0?{}:{requestsPerMinute:t},...n===void 0?{}:{minIntervalMs:n}}})),oe(``),ce(``),ue(``))},children:o(`pws.pacingAdd`)})]}),Object.entries(re).length>0&&(0,J.jsx)(`div`,{className:`pwi-pacing-overrides`,children:Object.entries(re).map(([e,t])=>(0,J.jsxs)(`div`,{className:`pwi-pacing-row`,children:[(0,J.jsx)(`code`,{children:e}),(0,J.jsxs)(`span`,{children:[t.requestsPerMinute===void 0?``:`${t.requestsPerMinute} ${o(`pws.pacingRpmUnit`)}`,t.requestsPerMinute!==void 0&&t.minIntervalMs!==void 0?` · `:``,t.minIntervalMs===void 0?``:`${t.minIntervalMs} ms`]}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:()=>ie(t=>Object.fromEntries(Object.entries(t).filter(([t])=>t!==e))),"aria-label":o(`pws.pacingRemoveModel`,{model:e}),children:o(`pws.pacingRemove`)})]},e))})]}),he&&(0,J.jsxs)(`div`,{className:`pwi-settings-sticky-bar`,children:[(0,J.jsx)(`span`,{className:`muted`,children:o(`pws.settingsUnsavedBar`)}),(0,J.jsxs)(`div`,{className:`pwi-settings-sticky-bar-actions`,children:[(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:Ee,disabled:A,children:o(`pws.discardSettings`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-primary btn-sm`,onClick:()=>void Ce(),disabled:A||I,children:o(A?`pws.saving`:`pws.saveSettings`)})]})]}),M&&(0,J.jsx)(`div`,{role:M.ok?`status`:`alert`,className:M.ok?`pwi-settings-msg pwi-settings-msg--ok`:`pwi-settings-msg pwi-settings-msg--err`,children:M.text})]})}function sc({providerName:e,defaultProviderName:t,onConfirm:n,onCancel:r}){let i=Q();return(0,J.jsx)(`div`,{className:`dialog-backdrop`,onClick:r,children:(0,J.jsxs)(`div`,{className:`dialog`,role:`alertdialog`,"aria-label":i(`pws.removeConfirmTitle`),onClick:e=>e.stopPropagation(),children:[(0,J.jsx)(`h3`,{children:i(`pws.removeConfirmTitle`)}),(0,J.jsx)(`p`,{children:t?i(`pws.removeDefaultConfirmBody`,{name:e,defaultProvider:t}):i(`pws.removeConfirmBody`,{name:e})}),(0,J.jsxs)(`div`,{className:`dialog-actions`,children:[(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost`,onClick:r,children:i(`common.cancel`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-danger`,onClick:n,children:i(`pws.removeConfirm`)})]})]})})}function cc({onSave:e,onDiscard:t,onCancel:n,saving:r=!1}){let i=Q();return(0,J.jsx)(`div`,{className:`dialog-backdrop`,onClick:n,children:(0,J.jsxs)(`div`,{className:`dialog`,role:`alertdialog`,"aria-label":i(`pws.unsavedLeaveTitle`),onClick:e=>e.stopPropagation(),children:[(0,J.jsx)(`h3`,{children:i(`pws.unsavedLeaveTitle`)}),(0,J.jsx)(`p`,{children:i(`pws.unsavedLeaveBody`)}),(0,J.jsxs)(`div`,{className:`dialog-actions`,children:[(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost`,onClick:n,children:i(`common.cancel`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost`,onClick:t,children:i(`pws.discardSettings`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-primary`,onClick:e,disabled:r,children:i(r?`pws.saving`:`pws.saveSettings`)})]})]})})}function lc({item:e,usageTotals:t,modelUsage:n,quotaReport:r,availableModels:i,hasLiveModels:a,selectedModels:o,modelsLoading:s,modelsLoadFailed:c,onRetryModels:l,oauthEmail:u,onDeselect:d,apiBase:f,oauth:p,accounts:m,accountLoadState:h,accountsFocusToken:g=0,accountsFocusProvider:v=null,switchingAccountId:y,keys:b,busyProvider:x,loginHint:S,authHandlers:C,onCodexActiveNeedsReauthChange:w,codexController:T,onUpdateProvider:E,isDefault:D,onRemoveProvider:O,onSetDisabled:k,onSetDefault:A}){let j=Q(),[M,N]=(0,_.useState)(`overview`),[P,F]=(0,_.useState)(!1),[I,L]=(0,_.useState)(null),[R,z]=(0,_.useState)(!1),B=(0,_.useRef)(null),[V,H]=(0,_.useState)(0),U=(0,_.useCallback)(e=>{B.current=e},[]),W=e.disabled===!0,ee=(0,_.useMemo)(()=>ci(e),[e]),G=(0,_.useMemo)(()=>hi(e),[e]),K=(0,_.useMemo)(()=>wa(e),[e]),q=v===e.name?g:0,Y=JSON.stringify([T?.activeId??``,m?.find(e=>e.active)?.id??``,b?.find(e=>e.active)?.id??``,p?.loggedIn===void 0?``:String(p.loggedIn),p?.needsReauth===void 0?``:String(p.needsReauth),u??``]),te=(0,_.useMemo)(()=>[{id:`overview`,label:j(`pws.tab.overview`)},{id:`models`,label:j(`pws.tab.models`)},{id:`usage`,label:j(`pws.tab.usage`)},...K?[{id:`accounts`,label:j(K===`api-keys`?`pws.apiKeys`:`pws.tab.accounts`)}]:[],{id:`settings`,label:j(`pws.tab.settings`)}],[K,j]),ne=(0,_.useCallback)(e=>{if(P&&M===`settings`&&e!==`settings`){L(e);return}N(e)},[M,P]);q!==V&&!(q&&!K)&&(H(q),q&&K&&(P&&M===`settings`?L(`accounts`):N(`accounts`)));let re=(0,_.useCallback)(()=>{if(P&&M===`settings`){L(`deselect`);return}d()},[P,M,d]),ie=(0,_.useCallback)((e,t)=>{let n;if(e.key===`ArrowRight`)n=(t+1)%te.length;else if(e.key===`ArrowLeft`)n=(t-1+te.length)%te.length;else if(e.key===`Home`)n=0;else if(e.key===`End`)n=te.length-1;else return;e.preventDefault(),ne(te[n].id),e.currentTarget.parentElement?.querySelectorAll(`[role="tab"]`)[n]?.focus()},[ne,te]),ae=`pws-tab-${M}`,oe=`pws-panel-${M}`;return(0,J.jsxs)(`div`,{className:`pws-detail`,children:[(0,J.jsx)(`div`,{className:`pws-detail-head`,children:(0,J.jsxs)(`button`,{type:`button`,className:`pws-detail-back-link`,onClick:re,children:[(0,J.jsx)(Se,{className:`pws-detail-back-chevron`,"aria-hidden":`true`}),j(`pws.allProviders`)]})}),(0,J.jsxs)(`div`,{className:`pws-detail-head-main`,children:[(0,J.jsx)(Pi,{name:e.name,adapter:e.adapter,baseUrl:e.baseUrl,cls:`pws-detail-icon`}),(0,J.jsx)(`div`,{className:`pws-detail-title-wrap`,children:(0,J.jsxs)(`h2`,{className:`pws-detail-title`,children:[jn(e.name,j),G&&(0,J.jsx)(`span`,{className:`pwi-rail-badge pwi-rail-badge--local`,children:j(`modal.badge.local`)}),!G&&ee&&(0,J.jsx)(`span`,{className:`pwi-rail-badge pwi-rail-badge--free`,children:j(`modal.badge.free`)})]})}),(0,J.jsxs)(`div`,{className:`pws-detail-actions`,children:[!D&&!W&&A&&(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:()=>A(e.name),children:j(`prov.setDefault`)}),O&&(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm btn-icon-only`,onClick:()=>O(e.name),"aria-label":j(`pws.removeConfirmTitle`),title:j(`pws.removeConfirmTitle`),children:(0,J.jsx)(he,{style:{width:15,height:15},"aria-hidden":`true`})}),k&&(0,J.jsxs)(`div`,{className:`pws-detail-toggle`,children:[(0,J.jsx)(`span`,{className:`pws-detail-toggle-label`,children:j(`pws.enabledLabel`)}),(0,J.jsx)(Tt,{on:!W,onClick:()=>k(e.name,!W),disabled:D,label:j(`pws.enabledLabel`)})]})]})]}),(0,J.jsx)(`div`,{className:`pws-detail-tabs`,role:`tablist`,children:te.map((e,t)=>(0,J.jsx)(`button`,{type:`button`,role:`tab`,id:`pws-tab-${e.id}`,"aria-controls":`pws-panel-${e.id}`,"aria-selected":M===e.id,tabIndex:M===e.id?0:-1,className:`pws-detail-tab${M===e.id?` pws-detail-tab--active`:``}`,onClick:()=>ne(e.id),onKeyDown:e=>ie(e,t),children:e.label},e.id))}),(0,J.jsxs)(`div`,{className:`pws-detail-panel`,role:`tabpanel`,id:oe,"aria-labelledby":ae,tabIndex:0,children:[M===`overview`&&(0,J.jsx)(Ea,{item:e,apiBase:f,connectionIdentity:Y,usageTotals:t,quotaReport:r,oauthEmail:u,oauth:p,onEditSettings:()=>ne(`settings`),onViewUsage:()=>ne(`usage`),onUpdateProvider:E,reauthBusy:x===e.name,onCancelLogin:C?.onCancelLogin?()=>void C.onCancelLogin?.(e.name):void 0,onReauthenticate:e.activeNeedsReauth?()=>{if(e.authMode===`oauth`){let t=m??[],n=t.find(e=>e.active&&e.needsReauth)??t.find(e=>e.needsReauth);C?.onReauth(e.name,n?.id);return}ne(`accounts`)}:void 0}),M===`models`&&(0,J.jsx)(Aa,{item:e,apiBase:f,availableModels:i,hasLiveModels:a,selectedModels:o,modelsLoading:s,modelsLoadFailed:c,needsReauth:(m??[]).some(e=>e.active&&e.needsReauth)||p?.needsReauth===!0,onRetryModels:l,onOpenAccounts:K?()=>ne(`accounts`):void 0},e.name),M===`usage`&&(0,J.jsx)(ja,{item:e,usageTotals:t,quotaReport:r,modelUsage:n}),M===`accounts`&&(0,J.jsx)(Rs,{item:e,apiBase:f,oauth:p,accounts:m,keys:b,accountLoadState:h,switchingAccountId:y,busy:x===e.name,loginHint:S,authHandlers:C,onUpdateProvider:E,onCodexActiveNeedsReauthChange:w,codexController:T}),M===`settings`&&(0,J.jsx)(oc,{item:e,apiBase:f,availableModels:i,onUpdateProvider:E,onDirtyChange:F,onRegisterSave:U},e.name)]}),I&&(0,J.jsx)(cc,{saving:R,onCancel:()=>{R||L(null)},onDiscard:()=>{if(R)return;let e=I;L(null),F(!1),e===`deselect`?d():N(e)},onSave:()=>{(async()=>{if(!R){z(!0);try{if(!(await B.current?.()??!1))return;let e=I;L(null),F(!1),e===`deselect`?d():e&&N(e)}finally{z(!1)}}})()}})]})}var uc=new Set([`anthropic`,`google-antigravity`,`meta-muse`]),dc=new Set([`github-copilot`,`cursor`]);function fc(e){let t=e.trim().toLowerCase();return uc.has(t)?`high`:dc.has(t)?`elevated`:null}function pc(e){switch(e){case`high`:return`oauthTos.highTitle`;case`elevated`:return`oauthTos.elevatedTitle`;default:return e}}function mc(e){switch(e){case`high`:return`oauthTos.highBody`;case`elevated`:return`oauthTos.elevatedBody`;default:return e}}function hc(e,t=!1){let n={};for(let[t,r]of Object.entries(e))La(r.accounts.find(e=>e.active)??r.accounts.find(e=>e.id===r.activeAccountId))&&(n[t]=!0);return t&&(n.openai=!0),n}function gc(e){let{apiBase:t,t:n,config:r,aliveRef:i,notify:a,fetchConfig:o,fetchOauth:s,fetchProviderQuotas:c,codexActiveNeedsReauth:l}=e,[u,d]=(0,_.useState)({}),[f,p]=(0,_.useState)({}),[m,h]=(0,_.useState)(null),[g,v]=(0,_.useState)({}),[y,b]=(0,_.useState)({}),[x,S]=(0,_.useState)(null),[C,w]=(0,_.useState)(``),T=(0,_.useRef)({}),E=(0,_.useRef)(null),D=(0,_.useRef)(null),O=(0,_.useRef)(null),k=(0,_.useCallback)(async e=>{let n=[...new Set(e)];return p(e=>{let t={...e};for(let e of n)t[e]=`loading`;return t}),(await Promise.all(n.map(async e=>{let n=(T.current[e]??0)+1;T.current[e]=n;try{let r=await fetch(`${t}/api/oauth/accounts?provider=${encodeURIComponent(e)}`);if(!r.ok)throw Error(String(r.status));let a=await r.json();return!i.current||T.current[e]!==n||(d(t=>({...t,[e]:{activeAccountId:a.activeAccountId??null,accounts:a.accounts??[]}})),p(t=>({...t,[e]:`ready`})),(async()=>{try{let r=await fetch(`${t}/api/oauth/accounts?provider=${encodeURIComponent(e)}"a=1`);if(!r.ok)return;let o=await r.json();if(!i.current||T.current[e]!==n)return;d(t=>({...t,[e]:{activeAccountId:o.activeAccountId??a.activeAccountId??null,accounts:o.accounts??a.accounts??[]}}))}catch{}})(),!0)}catch{return!i.current||T.current[e]!==n||(p(t=>({...t,[e]:`error`})),!1)}}))).every(Boolean)},[i,t]),A=(0,_.useCallback)(async e=>{let n=await Promise.all(e.map(async e=>[e,(await fetch(`${t}/api/providers/keys?name=${encodeURIComponent(e)}`).then(async e=>{if(!e.ok)throw Error(String(e.status));return e.json()}).catch(()=>null))?.keys??[]]));b(Object.fromEntries(n))},[t]),j=async(e,r)=>{if(r.active||r.needsReauth||O.current)return;let o={provider:e,accountId:r.id};O.current=o,h(o);let l=Ta(u[e]?.accounts??[r],r,n);try{if(!(await fetch(`${t}/api/oauth/accounts/active`,{method:`PUT`,headers:{"Content-Type":`application/json`},body:JSON.stringify({provider:e,accountId:r.id})})).ok){a(n(`prov.accountSwitchFail`),!1);return}let i=await k([e]);if(await Promise.all([s(),c(!0)]),!i){a(n(`pws.accountsLoadFailed`),!1);return}a(n(`prov.accountSwitched`,{email:l}),!0)}catch{a(n(`prov.accountSwitchFail`),!1)}finally{O.current?.provider===o.provider&&O.current.accountId===o.accountId&&(O.current=null,i.current&&h(null))}},M=async(e,r)=>{if(r.active)return;let i=await fetch(`${t}/api/providers/keys/active`,{method:`PUT`,headers:{"Content-Type":`application/json`},body:JSON.stringify({name:e,id:r.id})});if(i.ok)a(n(`prov.keySwitched`,{key:r.label??r.masked}),!0),A(Object.keys(y)),c(!0);else{let e=await i.json().catch(()=>({}));a(e.error||n(`prov.keySwitchFail`),!1)}},N=async(e,r)=>{window.confirm(n(`prov.keyRemoveConfirm`,{key:r.label??r.masked}))&&(await fetch(`${t}/api/providers/keys?name=${encodeURIComponent(e)}&id=${encodeURIComponent(r.id)}`,{method:`DELETE`})).ok&&(a(n(`prov.keyRemoved`,{key:r.label??r.masked}),!0),A(Object.keys(y)),o(),c(!0))},P=async(e,r)=>{let i=r.trim();if(!i)return!1;try{let r=await fetch(`${t}/api/providers/keys`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({name:e,key:i})});if(!r.ok){let e=await r.json().catch(()=>({}));return a(e.error||n(`prov.keyAddFail`),!1),!1}return a(n(`prov.keyAdded`,{name:e}),!0),S(null),await Promise.all([A(Object.keys(y).includes(e)?Object.keys(y):[...Object.keys(y),e]),o(),c(!0)]),!0}catch{return a(n(`prov.keyAddFail`),!1),!1}},F=async e=>{await P(e,C)&&w(``)},I=async(e,r,i,o)=>{let s=window.prompt(n(`prov.aliasPrompt`),o??``);if(s===null)return;let c=s.trim(),l=await fetch(r===`oauth`?`${t}/api/oauth/accounts/alias`:`${t}/api/providers/keys/alias`,{method:`PUT`,headers:{"Content-Type":`application/json`},body:JSON.stringify(r===`oauth`?{provider:e,accountId:i,alias:c}:{name:e,id:i,alias:c})});if(!l.ok){let e=await l.json().catch(()=>({}));a(e.error||n(`prov.aliasSaveFailed`),!1);return}r===`oauth`?await k([e]):await A(Object.keys(y).includes(e)?Object.keys(y):[...Object.keys(y),e]),a(n(`prov.aliasSaved`),!0)},L=async(e,r)=>{let i=Ta(u[e]?.accounts??[r],r,n);if(window.confirm(n(`prov.accountRemoveConfirm`,{email:i})))try{if(!(await fetch(`${t}/api/oauth/accounts?provider=${encodeURIComponent(e)}&id=${encodeURIComponent(r.id)}`,{method:`DELETE`})).ok){a(n(`prov.accountRemoveFail`,{email:i}),!1);return}a(n(`prov.accountRemoved`,{email:i}),!0),await k([e]),await Promise.all([s(),c(!0)])}catch{a(n(`prov.accountRemoveFail`,{email:i}),!1)}},R=(0,_.useMemo)(()=>r?Object.entries(r.providers).filter(([,e])=>e.authMode===`oauth`).map(([e])=>e):[],[r]);(0,_.useEffect)(()=>{if(R.length===0)return;let e=R.join(`,`);E.current!==e&&(E.current=e,Promise.resolve().then(()=>{k(R)}))},[k,R]);let z=(0,_.useMemo)(()=>r?Object.entries(r.providers).filter(([,e])=>e.hasApiKey&&e.authMode!==`oauth`&&e.authMode!==`forward`).map(([e])=>e):[],[r]);return(0,_.useEffect)(()=>{if(z.length===0)return;let e=z.join(`,`);D.current!==e&&(D.current=e,Promise.resolve().then(()=>{A(z)}))},[A,z]),{accountSets:u,accountLoadStates:f,switchingAccount:m,openAccounts:g,keyPools:y,addingKeyFor:x,newKeyValue:C,setAccountSets:d,setAccountLoadStates:p,setSwitchingAccount:h,setOpenAccounts:v,setKeyPools:b,setAddingKeyFor:S,setNewKeyValue:w,fetchAccountSets:k,fetchKeyPools:A,switchAccount:j,switchApiKey:M,removeApiKey:N,addApiKeyValue:P,addApiKey:F,editCredentialAlias:I,removeAccount:L,oauthCardProviders:R,keyCardProviders:z,activeAccountNeedsReauth:(0,_.useMemo)(()=>hc(u,l),[u,l])}}var _c=new Set([`hasApiKey`,`hasHeaders`,`xaiResponsesOptInState`]);function vc(e){return{defaultProvider:e.defaultProvider,providers:Object.fromEntries(Object.entries(e.providers).map(([e,t])=>{let n={};for(let[e,r]of Object.entries(t))_c.has(e)||(n[e]=structuredClone(r));return[e,n]}))}}function yc(e){let{apiBase:t,config:n,notify:r,fetchConfig:i,fetchProviderQuotas:a,onSaved:o,t:s}=e,[c,l]=(0,_.useState)(!1),[u,d]=(0,_.useState)(``),[f,p]=(0,_.useState)(!1),[m,h]=(0,_.useState)(``),[g,v]=(0,_.useState)(!1),[y,b]=(0,_.useState)(!1),x=(0,_.useRef)(!1);(0,_.useEffect)(()=>{n&&!x.current&&d(JSON.stringify(vc(n),null,2))},[n]);let S=(0,_.useCallback)(async()=>{v(!0);let e;try{e=JSON.parse(u)}catch{return r(s(`prov.invalidJson`),!1),v(!1),!1}try{let n=JSON.parse(m),c=await fetch(`${t}/api/providers`,{method:`PUT`,headers:{"Content-Type":`application/json`},body:JSON.stringify({baseline:n,next:e})});if(!c.ok){let e=await c.json().catch(()=>({}));return r(e.error||s(`prov.saveFailed`),!1),!1}return r(s(`prov.saved`),!0),l(!1),p(!1),x.current=!1,b(!1),h(JSON.stringify(e,null,2)),i(),a(!0),o(),!0}catch{return r(s(`prov.saveFailed`),!1),!1}finally{v(!1)}},[t,u,i,a,m,r,o,s]),C=(0,_.useCallback)(()=>{let e=n?JSON.stringify(vc(n),null,2):u;h(e),d(e),b(!1),p(!0),x.current=!0},[n,u]),w=(0,_.useCallback)(()=>{b(!1),p(!1),x.current=!1;let e=n?JSON.stringify(vc(n),null,2):m;h(e),d(e)},[n,m]);return{editing:c,setEditing:l,draft:u,setDraft:d,jsonEditorOpen:f,jsonBaseline:m,jsonSaving:g,jsonLeaveOpen:y,jsonEditorOpenRef:x,saveConfig:S,openJsonEditor:C,discardJsonEditor:w,requestCloseJsonEditor:(0,_.useCallback)(()=>{if(f&&u!==m){b(!0);return}w()},[w,u,m,f]),restoreJsonEditor:(0,_.useCallback)(()=>{d(m)},[m]),jsonIsDirty:f&&u!==m,setJsonLeaveOpen:b}}var bc={xai:`xAI (Grok)`,anthropic:`Anthropic (Claude)`,kimi:`Kimi (Moonshot)`,"meta-muse":`Meta Muse Code (CLI)`,"google-antigravity":`Google Antigravity`,"github-copilot":`GitHub Copilot`,cursor:`Cursor`},xc=e=>bc[e]??e;function Sc({apiBase:e,t,aliveRef:n,accountSets:r,setAccountSets:i,setBusy:a,setStatus:o,setLoginInfo:s,setOauthStatus:c,notify:l,fetchConfig:u,fetchOauth:d,fetchAccountSets:f,fetchProviderQuotas:p,bumpModelsRefresh:m,onLoginSettled:h}){let g=(0,_.useRef)(null);g.current===null&&(g.current=new Map);let v=(0,_.useCallback)(e=>{let t=(g.current.get(e)??0)+1;return g.current.set(e,t),t},[]);return{cancelLoginOAuth:(0,_.useCallback)(async r=>{let i=v(r);try{await fetch(`${e}/api/oauth/login/cancel`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({provider:r})})}catch{}n.current&&(g.current.get(r)===i&&(a(e=>e===r?null:e),s(e=>e?.provider===r?null:e)),l(t(`prov.loginCancelled`,{provider:xc(r)}),!1))},[n,e,v,l,a,s,t]),loginOAuth:async(d,_=!1,y)=>{let b=v(d),x=y?.trim()||void 0;a(d),o(``),s(null);try{let a=await fetch(`${e}/api/oauth/login`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({provider:d,...no(),..._||x?{addAccount:!0}:{},...x?{accountId:x,reauth:!0}:{}})});if(g.current.get(d)!==b||!n.current)return;if(!a.ok){l((await a.json().catch(()=>({}))).error||t(`prov.loginFailStart`,{provider:xc(d)}),!1);return}let o=await a.json();(o.url||o.instructions||o.deviceCode)&&s({provider:d,url:o.url,instructions:o.instructions,deviceCode:o.deviceCode});let v=r[d]?.accounts.length??0,y=!1;for(let a=0;a<150&&n.current&&g.current.get(d)===b;a++){if(await new Promise(e=>setTimeout(e,2e3)),g.current.get(d)!==b||!n.current)return;let a=await fetch(`${e}/api/oauth/status?provider=${d}`).catch(()=>null),o=a?await Ft(a)??null:null;if(!o)continue;if(o.error){c(e=>({...e,[d]:o})),l(/cancel/i.test(o.error)?t(`prov.loginCancelled`,{provider:xc(d)}):t(`prov.loginError`,{provider:xc(d),error:o.error}),!1),s(null),y=!0;break}let S=o.accounts?.length??0;if(_||x?S>v||o.done===!0:o.loggedIn||o.done===!0){c(e=>({...e,[d]:o}));let e=x?o.accounts?.find(e=>e.id===x):o.accounts?.find(e=>e.active)??o.accounts?.find(e=>e.id===o.activeAccountId);if(x&&!e){l(t(`prov.loginError`,{provider:xc(d),error:t(`prov.reauthAccountMissing`)}),!1),s(null),y=!0;break}if(e?.needsReauth){l(t(`prov.loginError`,{provider:xc(d),error:t(`prov.reauthIdentityMismatch`)}),!1),s(null),y=!0;break}if(o.accounts){let e=o.accounts.find(e=>e.active)?.id??null;i(t=>({...t,[d]:{activeAccountId:o.activeAccountId??e,accounts:o.accounts}}))}s(null),h?.(d);let a=Object.keys(r);if(await f(new Set(a).has(d)?a:[...a,d]),!n.current||g.current.get(d)!==b)return;_&&!x&&S<=v?l(t(`prov.loginSameAccount`,{provider:xc(d)}),!1):l(t(`prov.loginOk`,{provider:xc(d),cmd:`ocx sync`}),!0),u(),p(!0),m(),y=!0;break}}!y&&g.current.get(d)===b&&n.current&&(await fetch(`${e}/api/oauth/login/cancel`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({provider:d})}).catch(()=>{}),l(t(`prov.loginTimeout`,{provider:xc(d)}),!1),s(null))}catch{g.current.get(d)===b&&l(t(`prov.loginRequestFail`,{provider:xc(d)}),!1)}finally{n.current&&g.current.get(d)===b&&a(null)}},logoutOAuth:async n=>{v(n),a(e=>e===n?null:e),s(e=>e?.provider===n?null:e);try{if(!(await fetch(`${e}/api/oauth/logout?provider=${encodeURIComponent(n)}`,{method:`POST`})).ok){l(t(`prov.logoutFail`,{provider:xc(n)}),!1);return}await Promise.all([f([n]),d(),u(),p(!0)]),m(),l(t(`prov.logoutOk`,{provider:xc(n)}),!0)}catch{l(t(`prov.logoutFail`,{provider:xc(n)}),!1)}}}}async function Cc(e,t){try{let t=await e.json();if(typeof t.error==`string`&&t.error.trim())return t.error.trim()}catch{}return t}function wc(e,t,n){switch(e.code){case`last_provider`:return t(`prov.removeLastProvider`);case`provider_has_dependent_combos`:return t(`prov.removeHasDependentCombos`,{combos:(Array.isArray(e.combos)?e.combos.filter(e=>typeof e==`string`).join(`, `):``)||`—`});case`default_provider_disabled`:return t(`prov.defaultDisabled`);default:return typeof e.error==`string`&&e.error.trim()?e.error.trim():n}}function Tc({apiBase:e,t,removeBusyRef:n,workspaceSelected:r,setWorkspaceSelected:i,setRemoveConfirmName:a,notify:o,fetchConfig:s,fetchOauth:c,fetchProviderQuotas:l,refreshCodexAccount:u}){let d=(0,_.useCallback)(async e=>{a(e)},[a]),f=(0,_.useCallback)(async u=>{let d=u;if(!d||n.current)return;n.current=!0,a(null);let f=t(`prov.removeFail`,{name:d});try{let n=await fetch(`${e}/api/providers?name=${encodeURIComponent(d)}`,{method:`DELETE`});if(n.ok){let e=await n.json().catch(()=>({})),a=typeof e.defaultProvider==`string`?e.defaultProvider:null;o(a?t(`prov.removedDefault`,{name:d,defaultProvider:a}):t(`prov.removed`,{name:d}),!0),r===d&&i(null),s(),c(),l(!0)}else o(wc(await n.json().catch(()=>({})),t,f),!1)}catch{o(f,!1)}finally{n.current=!1}},[e,s,c,l,o,n,a,i,t,r]),p=(0,_.useCallback)(async(n,r)=>{let i=await fetch(`${e}/api/providers?name=${encodeURIComponent(n)}`,{method:`PATCH`,headers:{"Content-Type":`application/json`},body:JSON.stringify({disabled:r})});if(!i.ok){o(await Cc(i,t(r?`prov.disableFail`:`prov.enableFail`,{name:n})),!1);return}o(t(r?`prov.disabled`:`prov.enabled`,{name:n}),!0),s(),c(),l(!0)},[e,s,c,l,o,t]),m=(0,_.useCallback)(async(n,r)=>{try{let i=await fetch(`${e}/api/providers?name=${encodeURIComponent(n)}`,{method:`PATCH`,headers:{"Content-Type":`application/json`},body:JSON.stringify(r)});if(!i.ok)return{ok:!1,error:await Cc(i,t(`prov.updateFail`))};let a=await i.json().catch(()=>({}));if(await s(),Object.hasOwn(r,`codexAccountMode`)){let e=[l(!0)];u&&e.push(Promise.resolve(u())),await Promise.all(e)}let o=a.xaiResponsesOptInState;return{ok:!0,...o===!0||o===!1||o===`mixed`?{xaiResponsesOptInState:o}:{}}}catch{return{ok:!1,error:t(`prov.networkError`)}}},[e,s,l,u,t]);return{removeProvider:d,confirmRemoveProvider:f,setProviderDisabled:p,setDefaultProvider:(0,_.useCallback)(async n=>{try{let r=await fetch(`${e}/api/providers?name=${encodeURIComponent(n)}`,{method:`PATCH`,headers:{"Content-Type":`application/json`},body:JSON.stringify({setDefault:!0})});return r.ok?(o(t(`prov.setDefaultSuccess`,{name:n}),!0),await s(),!0):(o(wc(await r.json().catch(()=>({})),t,t(`prov.setDefaultFail`,{name:n})),!1),!1)}catch{return o(t(`prov.setDefaultFail`,{name:n}),!1),!1}},[e,s,o,t]),updateProvider:m}}function Ec({apiBase:e,t,setConfig:n,setOauthProviders:r,setOauthStatus:i,notify:a,invalidateProviderQuotas:o,configCacheKey:s}){return{fetchConfig:(0,_.useCallback)(async()=>{try{let t=await Pt(await fetch(`${e}/api/config`));n(t??null),s&&t&&br(s,t)}catch{a(t(`prov.loadConfigFail`),!1)}},[e,s,a,n,t]),fetchOauth:(0,_.useCallback)(async()=>{try{let t=(await Pt(await fetch(`${e}/api/oauth/providers`)))?.providers??[];r(t);let n=await Promise.all(t.map(async t=>{let n=await fetch(`${e}/api/oauth/status?provider=${encodeURIComponent(t)}`).catch(()=>null);return[t,n?await Ft(n)??{loggedIn:!1}:{loggedIn:!1}]}));i(Object.fromEntries(n))}catch{}},[e,r,i]),fetchProviderQuotas:(0,_.useCallback)(async(e=!1)=>{o(e)},[o])}}function Dc({providerId:e,providerLabel:t,onCancel:n,onContinue:r}){let i=Q(),a=(0,_.useId)(),o=(0,_.useId)(),s=(0,_.useRef)(null),c=(0,_.useRef)(!1),[l,u]=(0,_.useState)(!1),[d,f]=(0,_.useState)(!1),p=fc(e);(0,_.useEffect)(()=>{let e=s.current;e&&!e.open&&e.showModal()},[]);let m=(0,_.useCallback)(e=>{e.preventDefault(),n()},[n]);if(!p)return null;let h=e.trim().toLowerCase(),g=h===`anthropic`?`oauthTos.anthropicBody`:mc(p),v=h===`anthropic`||h===`google-antigravity`;return(0,J.jsxs)(`dialog`,{ref:s,"aria-labelledby":a,"aria-describedby":o,className:`modal-overlay`,onCancel:m,children:[(0,J.jsx)(`button`,{type:`button`,className:`modal-backdrop-dismiss`,"aria-label":i(`common.close`),tabIndex:-1,onClick:n}),(0,J.jsxs)(`div`,{className:`modal-card`,onClick:e=>e.stopPropagation(),style:{maxWidth:460},children:[(0,J.jsx)(`h3`,{id:a,children:i(pc(p),{provider:t})}),(0,J.jsxs)(`div`,{id:o,className:`notice-warn`,style:{marginTop:12,display:`flex`,gap:8,alignItems:`flex-start`},children:[(0,J.jsx)(_e,{width:16,height:16,style:{flexShrink:0,marginTop:2},"aria-hidden":`true`}),(0,J.jsx)(`p`,{className:`modal-desc`,style:{margin:0},children:i(g,{provider:t})})]}),v&&(0,J.jsx)(`p`,{className:`muted text-label`,style:{marginTop:12},children:i(`oauthTos.saferPath`)}),(0,J.jsxs)(`label`,{className:`oauth-tos-ack`,style:{display:`flex`,gap:8,alignItems:`flex-start`,marginTop:14},children:[(0,J.jsx)(`input`,{type:`checkbox`,checked:l,onChange:e=>u(e.target.checked),style:{marginTop:3},"aria-required":`true`}),(0,J.jsx)(`span`,{className:`text-label`,children:i(`oauthTos.acknowledge`)})]}),(0,J.jsxs)(`div`,{className:`modal-actions`,children:[(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost`,onClick:n,children:i(`common.cancel`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-primary`,disabled:!l||d,onClick:()=>{!l||c.current||(c.current=!0,f(!0),r())},children:i(`oauthTos.continue`)})]})]})]})}function Oc(e){return{adapter:e.adapter,baseUrl:e.baseUrl,authMode:e.auth,freeTier:!!e.freeTier,keyOptional:!!e.keyOptional}}function kc(e){return li(e.id,Oc(e))}function Ac(e){let t={accounts:[],free:[],paid:[]};for(let n of e)t[kc(n)].push(n);return t}function jc(e,t){let n=t.trim().toLowerCase();return n?e.filter(e=>e.label.toLowerCase().includes(n)||e.id.toLowerCase().includes(n)):e}function Mc(e,t,n){return!n||e.kind!==`oauth`||t!==e.id?!1:n.provider===e.id}var Nc={},Pc=[],Fc={};function Ic({presets:e,usageRank:t=Nc,presetsLoading:n=!1,initialTier:r=`free`,onSelectPreset:i,onSelectCustom:a,accountRows:o=Pc,accountStatus:s=Fc,busyProvider:c=null,loginHint:l=null,paste:u,onLogin:d,onCancelLogin:f,onLogout:p,onManage:m}){let h=Q(),[g,v]=(0,_.useState)(r),[y,b]=(0,_.useState)(``),x=(0,_.useMemo)(()=>e.filter(e=>e.id!==`custom`),[e]),S=(0,_.useMemo)(()=>{let e=Object.keys(t).length>0;return x.toSorted((n,r)=>{if(e){let e=t[n.id]??0,i=t[r.id]??0;if(i!==e)return i-e}return n.label.localeCompare(r.label,void 0,{sensitivity:`base`})||n.id.localeCompare(r.id)})},[x,t]),C=(0,_.useMemo)(()=>Ac(S),[S])[g],w=(0,_.useMemo)(()=>jc(C,y),[C,y]),T=e=>{let t=e.codexAccountMode===`direct`?(0,J.jsx)(`span`,{className:`badge badge-green`,children:h(`modal.badge.direct`)}):e.codexAccountMode===`pool`?(0,J.jsx)(`span`,{className:`badge badge-accent`,children:h(`modal.badge.pool`)}):e.auth===`oauth`?(0,J.jsx)(`span`,{className:`badge badge-accent`,children:h(`modal.badge.oauth`)}):e.auth===`forward`?(0,J.jsx)(`span`,{className:`badge badge-green`,children:h(`modal.badge.codexLogin`)}):e.auth===`local`?(0,J.jsx)(`span`,{className:`badge badge-amber`,children:h(`modal.badge.local`)}):e.keyOptional?null:(0,J.jsx)(`span`,{className:`badge badge-muted`,children:h(`modal.badge.apiKey`)}),n=(e.freeTier||e.keyOptional)&&e.auth===`key`?(0,J.jsx)(`span`,{className:`badge badge-green`,children:h(`modal.badge.free`)}):null;return(0,J.jsxs)(J.Fragment,{children:[n,t]})};return(0,J.jsxs)(`div`,{className:`provider-catalog`,children:[(0,J.jsx)(`div`,{className:`provider-catalog-tabs`,role:`tablist`,children:[`accounts`,`free`,`paid`].map(e=>(0,J.jsx)(`button`,{type:`button`,role:`tab`,"aria-selected":g===e,className:`provider-catalog-tab${g===e?` active`:``}`,onClick:()=>{v(e),b(``)},children:h(e===`accounts`?`modal.tab.accounts`:e===`free`?`modal.tab.free`:`modal.tab.paid`)},e))}),g===`accounts`&&(0,J.jsx)(`div`,{className:`provider-catalog-accounts-hint muted text-label`,children:h(`modal.accountsHint`)}),(0,J.jsx)(`input`,{className:`input provider-catalog-search`,value:y,onChange:e=>b(e.target.value),placeholder:h(`modal.search`)}),(0,J.jsxs)(`div`,{className:`provider-catalog-rows`,children:[n&&w.length===0&&(0,J.jsx)(`div`,{className:`muted text-control provider-catalog-empty`,children:h(`modal.catalogLoading`)}),g!==`accounts`&&w.map(e=>(0,J.jsxs)(`button`,{type:`button`,className:`list-row`,onClick:()=>i(e),children:[(0,J.jsx)(Pi,{name:e.id,adapter:e.adapter,cls:`provider-icon provider-icon-sm`}),(0,J.jsxs)(`div`,{children:[(0,J.jsx)(`div`,{className:`title`,children:e.label}),(0,J.jsxs)(`div`,{className:`sub`,children:[(0,J.jsx)(`code`,{className:`chip`,children:e.adapter}),e.note?` · ${e.note}`:``]})]}),(0,J.jsx)(`div`,{className:`provider-catalog-badges`,children:T(e)})]},e.id)),g!==`accounts`&&!n&&w.length===0&&(0,J.jsx)(`div`,{className:`muted text-control provider-catalog-empty`,children:h(`modal.noMatch`)}),g===`accounts`&&o.map(e=>{let t=s[e.id],n=c===e.id,r=!!t?.loggedIn,i=r?t?.email??e.statusLabel??h(`modal.accountLoggedIn`):t?.error??e.statusLabel??h(`modal.accountLoggedOut`),a=Mc(e,c,l);return(0,J.jsxs)(`div`,{className:`list-row provider-catalog-account-row${a?` provider-catalog-account-row--waiting`:``}`,children:[(0,J.jsxs)(`div`,{className:`provider-catalog-account-row-head`,children:[(0,J.jsx)(Pi,{name:e.id,cls:`provider-icon provider-icon-sm`}),(0,J.jsxs)(`div`,{children:[(0,J.jsx)(`div`,{className:`title`,children:e.label}),(0,J.jsx)(`div`,{className:`sub`,children:i})]}),(0,J.jsx)(`div`,{className:`provider-catalog-badges`,children:e.kind===`key`?null:e.kind===`codex`?(0,J.jsxs)(J.Fragment,{children:[r&&(0,J.jsx)(`a`,{className:`btn btn-ghost`,href:e.href??`#codex-set`,children:h(`modal.accountManage`)}),d&&(0,J.jsx)(`button`,{type:`button`,className:r?`btn btn-ghost`:`btn btn-primary`,disabled:n,onClick:()=>{n||d(e.id)},children:h(n?`codexAuth.enablingOpenai`:r?`modal.accountAdd`:`modal.accountLogin`)})]}):r?(0,J.jsxs)(J.Fragment,{children:[m&&(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost`,onClick:()=>m(e.id),children:h(`modal.accountManage`)}),d&&(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost`,disabled:n,onClick:()=>{n||d(e.id,!0)},children:h(n?`prov.waitingBrowser`:`modal.accountAdd`)}),n&&f&&(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost`,onClick:()=>f(e.id),children:h(`common.cancel`)}),p&&!n&&(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost`,onClick:()=>p(e.id),children:h(`modal.accountLogout`)})]}):n?f&&(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost`,onClick:()=>f(e.id),children:h(`common.cancel`)}):d&&(0,J.jsx)(`button`,{type:`button`,className:`btn btn-primary`,onClick:()=>d(e.id),children:h(`modal.accountLogin`)})})]}),a&&l&&(0,J.jsx)(Qa,{hint:{url:l.url,deviceCode:l.deviceCode,instructions:l.instructions},...u?{paste:{value:u.value,busy:u.busy,message:u.message,ok:u.ok,onChange:u.onChange,onSubmit:()=>u.onSubmit(e.id)}}:{}})]},e.id)}),g===`accounts`&&o.length===0&&!n&&(0,J.jsx)(`div`,{className:`muted text-control provider-catalog-empty`,children:h(`modal.noMatch`)})]}),(0,J.jsxs)(`div`,{className:`provider-catalog-footer`,children:[(0,J.jsx)(`div`,{style:{flex:1}}),g!==`accounts`&&(0,J.jsx)(`button`,{type:`button`,className:`link-btn`,onClick:a,children:h(`modal.notListed`)})]})]})}function Lc({preset:e,oauthSupported:t,oauthBusy:n,oauthMsg:r,oauthMsgTone:i,oauthUrl:a,oauthDeviceCode:o,oauthInstructions:s,manualCode:c,manualCodeBusy:l,manualCodeMsg:u,manualCodeOk:d,onRequestLogin:f,onUseApiKeyInstead:p,onManualCodeChange:m,onSubmitManualCode:h,onBack:g}){let _=Q();return(0,J.jsxs)(`div`,{style:{display:`flex`,flexDirection:`column`,gap:14},children:[(0,J.jsx)(`div`,{className:`muted text-control`,children:e.note??_(`modal.oauthDefaultNote`)}),t.includes(e.oauthProvider??``)?(0,J.jsxs)(J.Fragment,{children:[(0,J.jsxs)(`button`,{type:`button`,className:`btn btn-primary`,onClick:()=>f(e.oauthProvider),disabled:n,style:{width:`100%`,padding:`12px 16px`},children:[(0,J.jsx)(De,{}),n?_(`modal.waitingBrowser`):_(`modal.logInWith`,{label:e.label})]}),!n&&(0,J.jsx)(Es,{})]}):(0,J.jsx)(`div`,{className:`text-control`,style:{color:`var(--amber)`,background:`var(--amber-soft)`,border:`1px solid var(--amber)`,borderRadius:`var(--radius-sm)`,padding:`10px 12px`},children:_(`modal.oauthComingSoon`,{label:e.label})}),r&&(0,J.jsx)(`div`,{className:`text-label`,style:{color:i===`warn`?`var(--amber)`:`var(--accent-hover)`},children:r}),n&&(0,J.jsx)(Qa,{hint:{url:a,deviceCode:o,instructions:s},paste:{value:c,busy:l,disabled:!e.oauthProvider,message:u,ok:d,onChange:m,onSubmit:()=>{e.oauthProvider&&h(e.oauthProvider)}}}),(0,J.jsxs)(`div`,{style:{display:`flex`,gap:8,alignItems:`center`,marginTop:2},children:[(0,J.jsx)(`button`,{type:`button`,className:`link-btn`,onClick:p,children:_(`modal.useApiKeyInstead`)}),(0,J.jsx)(`div`,{style:{flex:1}}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost`,onClick:g,children:_(`modal.back`)})]})]})}function Rc({label:e,children:t}){return(0,J.jsxs)(`label`,{style:{display:`block`},children:[(0,J.jsx)(`span`,{className:`field-label`,children:e}),t]})}function zc({preset:e,form:t,endpointChoice:n,error:r,saving:i,dup:a,isCustom:o,isLocal:s,isReservedForward:c,presetDescription:l,onFormChange:u,onEndpointChoiceChange:d,onSubmit:f,onUseOauthLogin:p,onBack:m}){let h=Q();return(0,J.jsxs)(`div`,{style:{display:`flex`,flexDirection:`column`,gap:10},children:[!c&&!o&&!s&&!e.keyOptional&&e.note&&(0,J.jsxs)(`details`,{className:`setup-guide`,children:[(0,J.jsx)(`summary`,{children:h(`modal.setupGuide`)}),(0,J.jsxs)(`ol`,{className:`text-label leading-relaxed`,style:{margin:`8px 0 0`,paddingLeft:18,color:`var(--muted)`},children:[(0,J.jsxs)(`li`,{children:[h(`modal.setupStep1Prefix`),` `,(0,J.jsx)(`a`,{href:e.dashboardUrl,target:`_blank`,rel:`noreferrer`,children:h(`modal.setupDashboardLink`,{label:e.label})}),` `,h(`modal.setupStep1Suffix`)]}),(0,J.jsx)(`li`,{children:h(`modal.setupStep2`)}),(0,J.jsx)(`li`,{children:h(`modal.setupStep3`)})]}),e.note&&(0,J.jsx)(`div`,{className:`text-label`,style:{color:`var(--muted)`,marginTop:6,fontStyle:`italic`},children:e.note}),/\{[^}]*\}/.test(t.baseUrl)&&(0,J.jsx)(`div`,{className:`text-label`,style:{color:`var(--amber)`,marginTop:6},children:h(`modal.baseUrlPlaceholderHint`)})]}),(0,J.jsx)(Rc,{label:h(`modal.providerName`),children:(0,J.jsx)(`input`,{className:`input`,value:t.name,readOnly:c,onChange:e=>u({...t,name:e.target.value}),placeholder:h(`modal.namePlaceholder`)})}),a&&(0,J.jsx)(`div`,{className:`text-label`,style:{color:`var(--amber)`},children:h(`modal.duplicateWarn`,{name:t.name.trim()})}),!c&&(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)(Rc,{label:h(`modal.adapter`),children:(0,J.jsx)(`select`,{className:`input`,value:t.adapter,onChange:e=>u({...t,adapter:e.target.value}),children:[`openai-responses`,`openai-chat`,`anthropic`,`google`,`azure-openai`,`cursor`].map(e=>(0,J.jsx)(`option`,{value:e,children:e},e))})}),e.baseUrlChoices&&e.baseUrlChoices.length>0?(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)(Rc,{label:h(`modal.endpoint`),children:(0,J.jsx)(`select`,{className:`input`,value:n,onChange:n=>{let r=n.target.value;d(r),u({...t,baseUrl:zs(e.baseUrlChoices,r,t.baseUrl)})},children:e.baseUrlChoices.map(e=>(0,J.jsx)(`option`,{value:e.id,children:e.id===`token-plan`?h(`modal.endpoint.tokenPlan`):e.id===`payg`?h(`modal.endpoint.payAsYouGo`):e.id===`custom`?h(`modal.endpoint.custom`):e.label},e.id))})}),n===`custom`&&(0,J.jsx)(Rc,{label:h(`modal.baseUrl`),children:(0,J.jsx)(`input`,{className:`input`,value:t.baseUrl,onChange:e=>u({...t,baseUrl:e.target.value}),placeholder:h(`modal.baseUrlPlaceholder`)})})]}):(0,J.jsx)(Rc,{label:h(`modal.baseUrl`),children:(0,J.jsx)(`input`,{className:`input`,value:t.baseUrl,onChange:e=>u({...t,baseUrl:e.target.value}),placeholder:h(`modal.baseUrlPlaceholder`)})}),!c&&(0,J.jsxs)(`label`,{className:`modal-field`,style:{flexDirection:`row`,alignItems:`center`,gap:8},children:[(0,J.jsx)(`input`,{type:`checkbox`,checked:t?.allowPrivateNetwork??!1,onChange:e=>u({...t,allowPrivateNetwork:e.target.checked})}),(0,J.jsx)(`span`,{className:`muted text-control`,children:h(`modal.allowPrivateNetwork`)})]}),!c&&(t?.allowPrivateNetwork??!1)&&(0,J.jsx)(`p`,{className:`muted text-hint`,children:h(`modal.allowPrivateNetworkHint`)})]}),t.authMode===`forward`?(0,J.jsx)(`div`,{className:`text-label`,style:{color:`var(--green)`,background:`var(--green-soft)`,border:`1px solid var(--green)`,borderRadius:`var(--radius-sm)`,padding:`8px 10px`},children:l(e)}):t.authMode===`local`?(0,J.jsx)(`div`,{className:`text-label leading-relaxed`,style:{color:`var(--amber)`,background:`var(--amber-soft)`,border:`1px solid var(--amber)`,borderRadius:`var(--radius-sm)`,padding:`8px 10px`},children:h(`modal.localHint`)}):e.keyOptional?(0,J.jsxs)(`div`,{className:`text-label leading-relaxed`,style:{color:`var(--green)`,background:`var(--green-soft)`,border:`1px solid var(--green)`,borderRadius:`var(--radius-sm)`,padding:`10px 12px`},children:[(0,J.jsx)(`strong`,{children:h(`modal.freeTierTitle`)}),` — `,e.note??h(`modal.freeTierDefault`)]}):(0,J.jsxs)(J.Fragment,{children:[e.dashboardUrl&&(0,J.jsxs)(`a`,{className:`text-label`,href:e.dashboardUrl,target:`_blank`,rel:`noreferrer`,style:{display:`inline-flex`,alignItems:`center`,gap:5},children:[(0,J.jsx)(Ee,{style:{width:14,height:14}}),h(`modal.getApiKey`,{label:e.label}),(0,J.jsx)(Te,{style:{width:13,height:13}})]}),(0,J.jsx)(Rc,{label:h(`modal.apiKey`),children:(0,J.jsx)(`input`,{className:`input`,type:`password`,value:t.apiKey,onChange:e=>u({...t,apiKey:e.target.value}),placeholder:h(`modal.apiKeyPlaceholder`)})}),t.adapter===`anthropic`&&t.authMode===`key`&&(0,J.jsx)(Rc,{label:h(`modal.apiKeyTransport`),children:(0,J.jsxs)(`select`,{className:`input`,value:t.apiKeyTransport??`x-api-key`,onChange:e=>u({...t,apiKeyTransport:e.target.value===`bearer`?`bearer`:void 0}),children:[(0,J.jsx)(`option`,{value:`x-api-key`,children:h(`modal.apiKeyTransportNative`)}),(0,J.jsx)(`option`,{value:`bearer`,children:h(`modal.apiKeyTransportBearer`)})]})})]}),!c&&(0,J.jsx)(Rc,{label:h(`modal.defaultModel`),children:(0,J.jsx)(`input`,{className:`input`,value:t.defaultModel,onChange:e=>u({...t,defaultModel:e.target.value}),placeholder:h(`modal.defaultModelPlaceholder`)})}),r&&(0,J.jsx)(`div`,{className:`text-control`,role:`alert`,style:{color:`var(--red)`},children:r}),(0,J.jsxs)(`div`,{style:{display:`flex`,gap:8,marginTop:4,alignItems:`center`},children:[(0,J.jsx)(`button`,{type:`button`,className:`btn btn-primary`,onClick:f,disabled:i,children:h(i?`modal.adding`:`modal.add`)}),e.auth===`oauth`&&(0,J.jsx)(`button`,{type:`button`,className:`link-btn`,onClick:p,children:h(`modal.useOauthLogin`)}),(0,J.jsx)(`div`,{style:{flex:1}}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost`,onClick:m,children:h(`modal.back`)})]})]})}var Bc=2e3;function Vc({apiBase:e,t,aliveRef:n,onAdded:r}){return{loginOAuth:(0,_.useCallback)(async(i,a)=>{let{setOauthBusy:o,setOauthMsg:s,setOauthMsgTone:c,setOauthUrl:l,setManualCode:u,setManualCodeMsg:d,setManualCodeOk:f}=a;o(!0),s(``),c(`ok`),l(``,i),u(``),d(``),f(!0);try{let a=await fetch(`${e}/api/oauth/login`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({provider:i,...no()})});if(!n.current)return;if(!a.ok){let e=await a.json().catch(()=>({}));c(`warn`),s(e.error===`unknown oauth provider`?t(`modal.oauthComingSoonShort`):e.error||t(`modal.loginFailStart`));return}let o=await a.json();l(o.url??``,i,o.deviceCode,o.instructions),o.url||o.deviceCode?s(t(`modal.waitingLogin`)):s(o.instructions||t(`modal.loggingIn`));for(let a=0;a<100;a++){if(await new Promise(e=>setTimeout(e,Bc)),!n.current)return;let a=await fetch(`${e}/api/oauth/status?provider=${i}`).catch(()=>null),o=a?await Ft(a):null;if(!n.current)return;if(o?.error){c(`warn`),s(t(`modal.loginError`,{error:o.error}));return}if(o?.loggedIn){r(i);return}}c(`warn`),s(t(`modal.loginTimeout`))}catch{n.current&&(c(`warn`),s(t(`modal.networkError`)))}finally{n.current&&o(!1)}},[n,e,r,t]),submitManualCode:(0,_.useCallback)(async(r,i,a,o)=>{let s=i.trim();if(!s||a)return;let{setManualCodeBusy:c,setManualCode:l,setManualCodeOk:u,setManualCodeMsg:d}=o;c(!0),d(``);try{let i=await fetch(`${e}/api/oauth/login/code`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({provider:r,input:s})});if(!n.current)return;if(!i.ok){let e=await i.json().catch(()=>({}));u(!1),d(t(`prov.pasteFail`,{error:e.error||i.statusText}));return}l(``),u(!0),d(t(`prov.pasteOk`))}catch{n.current&&(u(!1),d(t(`modal.networkError`)))}finally{n.current&&c(!1)}},[n,e,t])}}function Hc(e,t){return{preset:e?{id:`custom`,label:t,adapter:`openai-chat`,baseUrl:``,auth:`key`}:null,form:e?{name:``,adapter:`openai-chat`,baseUrl:``,authMode:`key`,apiKey:``,apiKeyTransport:void 0,defaultModel:``,allowPrivateNetwork:!1}:null,saving:!1,error:``,oauthBusy:!1,oauthMsg:``,oauthMsgTone:`ok`,oauthUrl:``,oauthDeviceCode:``,oauthInstructions:``,oauthUrlProvider:null,manualCode:``,manualCodeBusy:!1,manualCodeMsg:``,manualCodeOk:!0,endpointChoice:`custom`,oauthTosPending:null}}function Uc(e,t){switch(t.type){case`choose-preset`:return{...e,preset:t.preset,form:t.form,endpointChoice:t.endpointChoice,error:``,oauthMsg:``,oauthMsgTone:`ok`,oauthUrl:``,oauthDeviceCode:``,oauthInstructions:``,oauthUrlProvider:null,oauthBusy:!1,manualCode:``,manualCodeMsg:``,manualCodeOk:!0};case`back`:return{...e,preset:null,form:null,endpointChoice:`custom`,error:``,oauthMsg:``,oauthMsgTone:`ok`,oauthUrl:``,oauthDeviceCode:``,oauthInstructions:``,oauthUrlProvider:null,oauthBusy:!1,manualCode:``,manualCodeMsg:``,manualCodeOk:!0};case`set-form`:return{...e,form:t.form};case`set-endpoint-choice`:return{...e,endpointChoice:t.choice};case`set-saving`:return{...e,saving:t.saving};case`set-error`:return{...e,error:t.error};case`set-oauth-busy`:return{...e,oauthBusy:t.busy};case`set-oauth-msg`:return{...e,oauthMsg:t.msg,oauthMsgTone:t.tone??e.oauthMsgTone};case`set-oauth-tone`:return{...e,oauthMsgTone:t.tone};case`set-oauth-url`:return e.preset?.oauthProvider===t.providerId?{...e,oauthUrl:t.url,oauthDeviceCode:t.deviceCode??``,oauthInstructions:t.instructions??``,oauthUrlProvider:t.providerId}:e;case`set-manual-code`:return{...e,manualCode:t.code};case`set-manual-code-busy`:return{...e,manualCodeBusy:t.busy};case`set-manual-code-msg`:return{...e,manualCodeMsg:t.msg,manualCodeOk:t.ok??e.manualCodeOk};case`set-oauth-tos-pending`:return{...e,oauthTosPending:t.providerId};case`use-oauth-login`:return{...e,form:t.form,error:``,oauthUrl:``,oauthDeviceCode:``,oauthInstructions:``,oauthUrlProvider:null};case`use-api-key-instead`:return{...e,form:t.form,oauthMsg:``,oauthMsgTone:`ok`,oauthUrl:``,oauthDeviceCode:``,oauthInstructions:``,oauthUrlProvider:null,oauthBusy:!1,manualCode:``,manualCodeMsg:``};default:return e}}function Wc({apiBase:e,existingNames:t,onClose:n,onAdded:r,initialTier:i,initialCustom:a=!1,accountRows:o,accountStatus:s,accountBusy:c,accountLoginHint:l=null,onAccountLogin:u,onAccountCancelLogin:d,onAccountLogout:f,onAccountManage:p,onOpen:m}){let h=Q(),g=(0,_.useMemo)(()=>[{id:`custom`,label:h(`modal.customProvider`),adapter:`openai-chat`,baseUrl:``,auth:`key`}],[h]),[v,y]=(0,_.useReducer)(Uc,a,e=>Hc(e,h(`modal.customProvider`))),b=(0,_.useRef)(!0),x=(0,_.useRef)(null),S=(0,_.useRef)(null),C=G(`add-provider-oauth:${e}`,[e],async t=>{let n=await fetch(`${e}/api/oauth/providers`,{signal:t});return n.ok?(await n.json()).providers??[]:[]}),w=G(`add-provider-presets:${e}`,[e],async t=>{let n=await fetch(`${e}/api/provider-presets`,{signal:t});if(!n.ok)throw Error(String(n.status));let r=await n.json();return Array.isArray(r.providers)&&r.providers.length>0?r.providers:null}),T=G(Br(e),[e],async t=>{let n=await fetch(`${e}/api/usage?range=30d`,{signal:t});if(!n.ok)throw Error(String(n.status));return await n.json()},{deadlineMs:6e4}),E=C.data??[],D=w.data??g,O=w.loading,k=Object.fromEntries((T.data?.providers??[]).map(e=>[e.provider,e.requests])),{preset:A,form:j,saving:M,error:N,oauthBusy:P,oauthMsg:F,oauthMsgTone:I,oauthUrl:L,oauthUrlProvider:R,oauthDeviceCode:z,oauthInstructions:B,manualCode:V,manualCodeBusy:H,manualCodeMsg:U,manualCodeOk:W,endpointChoice:ee,oauthTosPending:K}=v;(0,_.useEffect)(()=>{b.current=!0,x.current=document.activeElement,m?.();let e=S.current;if(e){let t=e.querySelector(`input:not([disabled]), button:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex='-1'])`);t&&t.focus()}return()=>{b.current=!1,x.current?.focus()}},[]),(0,_.useEffect)(()=>{let e=e=>{e.key===`Escape`&&!K&&n()};return window.addEventListener(`keydown`,e),()=>window.removeEventListener(`keydown`,e)},[n,K]);let q=e=>{let t=qs(e);return t?h(t):e.note},Y=e=>{let t=Bs(e.baseUrlChoices,e.baseUrl);y({type:`choose-preset`,preset:e,endpointChoice:t,form:{name:e.id===`custom`?``:e.id,adapter:e.adapter,baseUrl:e.baseUrlChoices?.length?zs(e.baseUrlChoices,t,e.baseUrl):e.baseUrl,responsesPath:e.responsesPath,authMode:e.auth,apiKey:``,apiKeyTransport:void 0,defaultModel:e.defaultModel??``,allowPrivateNetwork:!1}})},te=async()=>{if(!j)return;let t=A?Ks(A):!1,n=A?.baseUrlChoices?.length?Vs(A.baseUrlChoices,ee,j.baseUrl):j.baseUrl.trim();if(!t&&!j.name.trim()){y({type:`set-error`,error:h(`modal.nameRequired`)});return}if(!t&&!n){y({type:`set-error`,error:h(`modal.baseUrlRequired`)});return}if(!t&&/\{[^}]*\}/.test(n)){y({type:`set-error`,error:h(`modal.baseUrlPlaceholderError`)});return}let i={...j,baseUrl:n},a;try{a=Ys(A??{id:`custom`},i)}catch{y({type:`set-error`,error:h(`modal.invalidPreset`)});return}y({type:`set-saving`,saving:!0}),y({type:`set-error`,error:``});try{let t=await fetch(`${e}/api/providers`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify(a)});if(!t.ok){let e=await t.json().catch(()=>({}));y({type:`set-error`,error:e.error||h(`modal.failedStatus`,{status:t.status})});return}r(a.name)}catch{y({type:`set-error`,error:h(`modal.networkError`)})}finally{y({type:`set-saving`,saving:!1})}},{loginOAuth:ne,submitManualCode:re}=Vc({apiBase:e,t:h,aliveRef:b,onAdded:r}),ie={setOauthBusy:e=>y({type:`set-oauth-busy`,busy:e}),setOauthMsg:e=>y({type:`set-oauth-msg`,msg:e}),setOauthMsgTone:e=>y({type:`set-oauth-tone`,tone:e}),setOauthUrl:(e,t,n,r)=>y({type:`set-oauth-url`,url:e,providerId:t,deviceCode:n,instructions:r}),setManualCode:e=>y({type:`set-manual-code`,code:e}),setManualCodeMsg:e=>y({type:`set-manual-code-msg`,msg:e}),setManualCodeOk:e=>y({type:`set-manual-code-msg`,msg:U,ok:e})},ae=j?t.includes(j.name.trim())&&j.name.trim()!==``:!1,oe=e=>{if(!P){if(fc(e)){y({type:`set-oauth-tos-pending`,providerId:e});return}ne(e,ie)}},se=e=>{re(e,V,H,{setManualCodeBusy:e=>y({type:`set-manual-code-busy`,busy:e}),setManualCode:e=>y({type:`set-manual-code`,code:e}),setManualCodeOk:e=>y({type:`set-manual-code-msg`,msg:U,ok:e}),setManualCodeMsg:e=>y({type:`set-manual-code-msg`,msg:e})})},ce=A?.id===`custom`,le=j?.authMode===`local`,ue=A?Ks(A):!1;return(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)(`div`,{role:`dialog`,"aria-modal":`true`,"aria-label":h(`modal.add`),className:`modal-overlay`,children:(0,J.jsxs)(`div`,{ref:S,className:`modal-card`,children:[(0,J.jsxs)(`div`,{className:`modal-head`,children:[(0,J.jsx)(`h3`,{children:A?h(`modal.addNamed`,{label:A.label}):h(`modal.add`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-icon`,"aria-label":h(`common.close`),onClick:n,children:(0,J.jsx)(de,{})})]}),A?j&&(A.auth===`oauth`&&j.authMode===`oauth`?(0,J.jsx)(Lc,{preset:A,oauthSupported:E,oauthBusy:P,oauthMsg:F,oauthMsgTone:I,oauthUrl:R===A.oauthProvider?L:``,oauthDeviceCode:R===A.oauthProvider?z:``,oauthInstructions:R===A.oauthProvider?B:``,manualCode:V,manualCodeBusy:H,manualCodeMsg:U,manualCodeOk:W,onRequestLogin:oe,onUseApiKeyInstead:()=>{y({type:`use-api-key-instead`,form:{...j,authMode:`key`}})},onManualCodeChange:e=>y({type:`set-manual-code`,code:e}),onSubmitManualCode:e=>{se(e)},onBack:()=>y({type:`back`})}):(0,J.jsx)(zc,{preset:A,form:j,endpointChoice:ee,error:N,saving:M,dup:ae,isCustom:ce,isLocal:le,isReservedForward:ue,presetDescription:q,onFormChange:e=>y({type:`set-form`,form:e}),onEndpointChoiceChange:e=>y({type:`set-endpoint-choice`,choice:e}),onSubmit:()=>{te()},onUseOauthLogin:()=>y({type:`use-oauth-login`,form:{...j,authMode:`oauth`}}),onBack:()=>y({type:`back`})})):(0,J.jsx)(Ic,{presets:D,usageRank:k,presetsLoading:O,initialTier:i,onSelectPreset:e=>Y(e),onSelectCustom:()=>Y(g[0]),accountRows:o,accountStatus:s,busyProvider:c,onLogin:u,onCancelLogin:d,onLogout:f,onManage:p,loginHint:l,paste:{value:V,busy:H,message:U,ok:W,onChange:e=>y({type:`set-manual-code`,code:e}),onSubmit:e=>{se(e)}}})]})}),K&&(0,J.jsx)(Dc,{providerId:K,providerLabel:A?.label??K,onCancel:()=>y({type:`set-oauth-tos-pending`,providerId:null}),onContinue:()=>{let e=K;e&&(y({type:`set-oauth-tos-pending`,providerId:null}),ne(e,ie))}},K)]})}function Gc({apiBase:e,config:t,adding:n,addIntent:r,busy:i,addModalAccountRows:a,accountLoginStatus:o,accountLoginHint:s,removeConfirmName:c,removeDefaultProvider:l,codexLoginOpen:u,jsonLeaveOpen:d,jsonSaving:f,oauthTosPending:p,onCloseAdd:m,onAdded:h,onAccountLogin:g,onAccountCancelLogin:_,onAccountLogout:v,onAccountManage:y,onOpenAdd:b,onCloseCodexLogin:x,onCodexAdded:S,onCancelRemove:C,onConfirmRemove:w,onCancelJsonLeave:T,onDiscardJson:E,onSaveJson:D,onCancelOauthTos:O,onContinueOauthTos:k}){return(0,J.jsxs)(J.Fragment,{children:[n&&(0,J.jsx)(Wc,{apiBase:e,existingNames:Object.keys(t.providers),initialTier:r?.tier,initialCustom:r?.custom,onClose:m,onAdded:h,accountRows:a,accountStatus:o,accountBusy:i,accountLoginHint:s??null,onAccountLogin:g,onAccountCancelLogin:_,onAccountLogout:v,onAccountManage:y,onOpen:b}),u&&(0,J.jsx)(co,{apiBase:e,onClose:x,onAdded:S}),c&&(0,J.jsx)(sc,{providerName:c,defaultProviderName:l,onCancel:C,onConfirm:w}),d&&T&&E&&D&&(0,J.jsx)(cc,{saving:f??!1,onCancel:T,onDiscard:E,onSave:D}),p&&(0,J.jsx)(Dc,{providerId:p.provider,providerLabel:xc(p.provider),onCancel:O,onContinue:k},`${p.provider}:${p.addAccount?`add`:`login`}`)]})}function Kc(e,t,n){return[...Ws(e.providers).map(e=>({id:e,label:jn(e,n),kind:`codex`,href:`#codex-set`})),...t.toSorted((e,t)=>e.localeCompare(t)).map(e=>({id:e,label:xc(e),kind:`oauth`}))]}function qc(e,t){let n={...t},r=t.openai;if(r)for(let[t,i]of Object.entries(e.providers))i.authMode===`forward`&&(n[t]=r);return n}function Jc({apiBase:e}){let t=Q(),n=`ocx.providers.config.v1:${e}`,[r,i]=(0,_.useState)(()=>gr(n)),[a,o]=(0,_.useState)(!1),[s,c]=(0,_.useState)(``),[l,u]=(0,_.useState)(!1),[d,f]=(0,_.useState)(`err`),[p,m]=(0,_.useState)(0),[h,g]=(0,_.useState)([]),[v,y]=(0,_.useState)({}),[b,x]=(0,_.useState)(null),[S,C]=(0,_.useState)(null),[w,T]=(0,_.useState)(null),[E,D]=(0,_.useState)(null),[O,k]=(0,_.useState)(null),[A,j]=(0,_.useState)(!1),[M,N]=(0,_.useState)(0),[P,F]=(0,_.useState)(null),[I,L]=(0,_.useState)({token:0,provider:null}),R=(0,_.useRef)(!0),z=(0,_.useRef)(null),B=(0,_.useRef)(!1),V=(0,_.useCallback)((e,t=!0)=>{c(e),u(t),f(t?`ok`:`err`),m(e=>e+1)},[]),H=(0,_.useCallback)(()=>{c(``),u(!1),f(`err`)},[]),U=(0,_.useCallback)(e=>{if(e.catalogRefreshPending){c(t(`codexAuth.catalogRefreshPending`)),u(!1),f(`warn`),m(e=>e+1);return}V(t(`codexAuth.accountAdded`),!0)},[V,t]);(0,_.useEffect)(()=>(R.current=!0,()=>{R.current=!1}),[]),(0,_.useEffect)(()=>{if(!s||!l)return;let e=window.setTimeout(H,4500);return()=>window.clearTimeout(e)},[s,l,p,H]);let W=(0,_.useCallback)(e=>{o(!1),D(null),T(e),L(t=>({token:t.token+1,provider:e}))},[]);G(`add-provider-presets:${e}`,[e],async t=>{let n=await fetch(`${e}/api/provider-presets`,{signal:t});if(!n.ok)throw Error(String(n.status));let r=await n.json();return Array.isArray(r.providers)&&r.providers.length>0?r.providers:null}),G(Br(e),[e],async t=>{let n=await fetch(`${e}/api/usage?range=30d`,{signal:t});if(!n.ok)throw Error(String(n.status));return await n.json()},{deadlineMs:6e4});let[ee,K]=(0,_.useState)({epoch:0,force:!1}),{fetchConfig:q,fetchOauth:Y,fetchProviderQuotas:te}=Ec({apiBase:e,t,setConfig:i,setOauthProviders:g,setOauthStatus:y,notify:V,invalidateProviderQuotas:(0,_.useCallback)((e=!1)=>{K(t=>({epoch:t.epoch+1,force:e}))},[]),configCacheKey:n}),ne=Do(e),re=ne.activeNeedsReauth,ie=(0,_.useMemo)(()=>{let e=ne.accounts;if(e.length===0&&ne.loadState===`loading`)return v;let t=e.find(e=>e.isMain)??e[0],n=!!t&&!!t.email&&t.email!==`Codex App login`,r=e.some(e=>!e.isMain&&(e.hasCredential||e.email)),i=n||r,a=n?t?.email:e.find(e=>!e.isMain&&e.email)?.email??void 0;return{...v,openai:{loggedIn:i,...a?{email:a}:{},...re?{needsReauth:!0}:{}}}},[v,ne.accounts,ne.loadState,re]),{accountSets:ae,setAccountSets:oe,accountLoadStates:se,switchingAccount:ce,keyPools:le,fetchAccountSets:ue,switchAccount:de,switchApiKey:pe,removeApiKey:X,addApiKeyValue:me,editCredentialAlias:he,removeAccount:ge,activeAccountNeedsReauth:_e}=gc({apiBase:e,t,config:r,oauthStatus:ie,aliveRef:R,notify:V,fetchConfig:q,fetchOauth:Y,fetchProviderQuotas:te,codexActiveNeedsReauth:re}),{draft:Z,setDraft:ve,jsonEditorOpen:ye,jsonSaving:be,jsonLeaveOpen:xe,saveConfig:Se,openJsonEditor:Ce,discardJsonEditor:we,requestCloseJsonEditor:Te,restoreJsonEditor:Ee,jsonIsDirty:De,setJsonLeaveOpen:Oe}=yc({apiBase:e,config:r,notify:V,fetchConfig:q,fetchProviderQuotas:te,onSaved:()=>N(e=>e+1),t});(0,_.useEffect)(()=>{z.current!==e&&(z.current=e,Promise.resolve().then(()=>{q(),Y()}))},[e,q,Y]);let ke=()=>N(e=>e+1),{cancelLoginOAuth:Ae,loginOAuth:je,logoutOAuth:Me}=Sc({apiBase:e,t,aliveRef:R,accountSets:ae,setAccountSets:oe,setBusy:x,setStatus:c,setLoginInfo:C,setOauthStatus:y,notify:V,fetchConfig:q,fetchOauth:Y,fetchAccountSets:ue,fetchProviderQuotas:te,bumpModelsRefresh:ke,onLoginSettled:W}),{removeProvider:Ne,confirmRemoveProvider:Pe,setProviderDisabled:Fe,setDefaultProvider:Ie,updateProvider:Le}=Tc({apiBase:e,t,removeBusyRef:B,workspaceSelected:w,setWorkspaceSelected:T,setRemoveConfirmName:k,notify:V,fetchConfig:q,fetchOauth:Y,fetchProviderQuotas:te,refreshCodexAccount:()=>ne.load(!0)}),Re=(e,t=!1,n)=>{if(b!==e){if(fc(e)){F({provider:e,addAccount:t,...n?{accountId:n}:{}});return}je(e,t,n)}};if(!r)return(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)(`div`,{className:`page-head`,children:(0,J.jsx)(`h2`,{children:t(`nav.providers`)})}),s?(0,J.jsx)(Et,{tone:d,onDismiss:H,dismissLabel:t(`common.close`),children:s}):(0,J.jsxs)(`div`,{className:`providers-workspace providers-workspace--boot`,"aria-busy":`true`,children:[(0,J.jsx)(`div`,{className:`providers-workspace-rail providers-workspace-rail--boot`,"aria-hidden":`true`}),(0,J.jsx)(`div`,{className:`providers-workspace-main`,children:(0,J.jsxs)(`p`,{className:`muted`,children:[(0,J.jsx)(`span`,{className:`spin`,"aria-hidden":`true`}),` `,t(`prov.loadingConfig`)]})})]})]});let ze=Kc(r,h,t),Be=qc(r,ie),Ve=e=>r.providers[e]?.authMode===`forward`;return(0,J.jsxs)(J.Fragment,{children:[(0,J.jsxs)(`div`,{className:`page-head`,children:[(0,J.jsx)(`h2`,{children:t(`nav.providers`)}),(0,J.jsx)(`div`,{className:`row`,children:(0,J.jsxs)(`button`,{type:`button`,className:`btn btn-primary`,onClick:()=>o(!0),children:[(0,J.jsx)(fe,{}),t(`prov.add`)]})})]}),s&&(0,J.jsx)(Et,{tone:d,onDismiss:H,dismissLabel:t(`common.close`),children:s}),(0,J.jsx)(Sa,{onRemoveProvider:Ne,providers:r.providers,apiBase:e,defaultProvider:r.defaultProvider,selectedName:w,onSelect:T,onAddProvider:e=>{D(e??null),o(!0)},onEditConfig:Ce,jsonEditor:{open:ye,draft:Z,isDirty:De,onDraftChange:ve,onSave:()=>Se(),onClose:Te,onRestore:Ee},jsonSaving:be,modelsRefreshToken:M,activeAccountNeedsReauth:_e,quotaRefreshEpoch:ee.epoch,quotaForceRefresh:ee.force,detail:(t,n)=>{let i=Be[t.name]??v[t.name];return(0,J.jsx)(lc,{item:t,usageTotals:n.usageTotals,modelUsage:n.modelUsage,quotaReport:n.quotaReport,availableModels:n.availableModels,hasLiveModels:n.hasLiveModels,selectedModels:n.selectedModels,modelsLoading:n.modelsLoading,modelsLoadFailed:n.modelsLoadFailed,onRetryModels:n.onRetryModels,oauthEmail:i?.email,onDeselect:()=>T(null),apiBase:e,oauth:i,accounts:ae[t.name]?.accounts??[],keys:le[t.name]??[],accountLoadState:se[t.name]??(t.authMode===`oauth`?`idle`:`ready`),accountsFocusToken:I.token,accountsFocusProvider:I.provider,switchingAccountId:ce?.provider===t.name?ce.accountId:null,busyProvider:b,loginHint:S,authHandlers:{onLogin:Re,onCancelLogin:Ae,onLogout:Me,onReauth:(e,t)=>Re(e,!0,t),onSwitchAccount:de,onRemoveAccount:ge,onRetryAccounts:async e=>{await ue([e])},onAddApiKey:me,onSwitchApiKey:pe,onRemoveApiKey:X,onEditAlias:he},isDefault:t.name===r.defaultProvider,onRemoveProvider:Ne,onSetDisabled:Fe,onSetDefault:e=>{Ie(e)},onUpdateProvider:Le,codexController:ne},t.name)}}),(0,J.jsx)(Gc,{apiBase:e,config:r,adding:a,addIntent:E,busy:b,addModalAccountRows:ze,accountLoginStatus:Be,accountLoginHint:S,removeConfirmName:O,removeDefaultProvider:O===r.defaultProvider?Object.entries(r.providers).find(([e,t])=>e!==O&&t.disabled!==!0)?.[0]??null:null,codexLoginOpen:A,jsonLeaveOpen:xe,jsonSaving:be,oauthTosPending:P,onCloseAdd:()=>{b&&Ae(b),o(!1),D(null)},onAdded:e=>{o(!1),D(null),V(t(`prov.added`,{name:e,cmd:`ocx sync`}),!0),q(),Y(),te(!0),ke()},onAccountLogin:async(n,i=!1)=>{if(n===`openai`){if(b===`openai`)return;let n=r.providers.openai,i=Gs(n);if(i===`invalid`){V(t(`codexAuth.openaiMissing`),!1);return}if(i===`absent`||i===`disabled`){x(`openai`);try{await Qs(e,i),await q()}catch(e){e instanceof Zs?V(t(e.i18nKey),!1):V(e instanceof Error?e.message:t(`prov.saveFailed`),!1);return}finally{R.current&&x(e=>e===`openai`?null:e)}}j(!0);return}if(Ve(n)){j(!0);return}(r.providers[n]?.authMode===`oauth`||h.includes(n))&&Re(n,i)},onAccountCancelLogin:e=>{Ae(e)},onAccountLogout:e=>{Me(e)},onAccountManage:e=>{W(e)},onOpenAdd:Y,onCloseCodexLogin:()=>j(!1),onCodexAdded:e=>{j(!1),U(e),q(),Y(),te(!0),ke()},onCancelRemove:()=>k(null),onConfirmRemove:()=>{Pe(O)},onCancelJsonLeave:()=>{be||Oe(!1)},onDiscardJson:we,onSaveJson:()=>{Se()},onCancelOauthTos:()=>F(null),onContinueOauthTos:()=>{let e=P;e&&(F(null),je(e.provider,e.addAccount,e.accountId))}})]})}function Yc(e){let{t}=ct();return e.state===`stale`?(0,J.jsxs)(`div`,{className:`codex-stale-banner`,role:`status`,children:[(0,J.jsx)(`span`,{className:`codex-stale-banner-text`,children:t(`models.staleBanner`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-sm`,disabled:e.controller.restarting,onClick:()=>{e.controller.restart()},children:e.controller.restarting?t(`dash.codexRestarting`):t(`dash.codexRestart`)})]}):null}var Xc=[`fresh`,`stale`,`not_running`,`unknown`],Zc=[`stopped`,`nothing_running`,`enumeration_unavailable`,`partially_stopped`];function Qc(e){return Array.isArray(e)&&e.every(e=>typeof e==`number`&&Number.isSafeInteger(e)&&e>0)}function $c(e){if(typeof e!=`object`||!e)return!1;let t=e;if(!(typeof t.success==`boolean`&&typeof t.synced==`boolean`&&typeof t.stateBefore==`string`&&Xc.includes(t.stateBefore)&&typeof t.code==`string`&&Zc.includes(t.code)&&Qc(t.requested)&&Qc(t.stopped)&&Qc(t.surviving)&&Qc(t.failed)))return!1;let n=t.success,r=t.code,i=t.surviving,a=t.failed,o=t.stopped;return!(n!==(r!==`partially_stopped`)||n&&(i.length>0||a.length>0)||!n&&i.length===0&&a.length===0||(r===`nothing_running`||r===`enumeration_unavailable`)&&o.length>0)}function el(e){if(typeof e!=`object`||!e)return!1;let t=e;return typeof t.state==`string`&&Xc.includes(t.state)&&typeof t.runningCount==`number`&&Number.isSafeInteger(t.runningCount)&&t.runningCount>=0}var tl={state:null,runningCount:0};async function nl(e,t={}){let n=t.fetchFn??fetch;try{let r=await n(`${e}/api/system/codex-app-server`,{signal:t.signal});if(!r.ok)return tl;let i=await r.json().catch(()=>null);return el(i)?{state:i.state,runningCount:i.runningCount}:tl}catch{return tl}}var rl=3e4;function il(e){return(e instanceof DOMException||e instanceof Error)&&(e.name===`AbortError`||e.name===`TimeoutError`)}async function al(e,t={}){let{fetchFn:n=fetch,timeoutMs:r=rl,formatFailure:i=e=>`Failed to restart Codex (HTTP ${e}).`,formatUnreachable:a=()=>`Could not reach the proxy.`,formatMalformed:o=()=>`The proxy returned an unexpected response.`,formatTimeout:s=()=>`The proxy did not answer in time. It may still be working.`}=t,c;try{c=await n(`${e}/api/system/codex-restart`,{method:`POST`,signal:AbortSignal.timeout(r)})}catch(e){return{ok:!1,message:il(e)?s():a()}}if(!c.ok)return{ok:!1,message:i(c.status)};let l;try{l=await c.json()}catch(e){return{ok:!1,message:il(e)?s():o()}}return $c(l)?{ok:!0,result:l}:{ok:!1,message:o()}}function ol(e){return e===`stopped`||e===`nothing_running`}function sl(e,t={}){let{t:n}=ct(),[r,i]=(0,_.useState)(!1),a=(0,_.useRef)(!0),o=(0,_.useRef)(t.onSettled);return(0,_.useEffect)(()=>{o.current=t.onSettled},[t.onSettled]),(0,_.useEffect)(()=>(a.current=!0,()=>{a.current=!1}),[]),{restarting:r,restart:(0,_.useCallback)(async()=>{if(!confirm(n(`dash.codexRestartConfirm`)))return null;i(!0);let t=await al(e,{formatFailure:e=>n(`dash.codexRestartFailed`,{status:String(e)}),formatUnreachable:()=>n(`dash.codexRestartUnreachable`),formatTimeout:()=>n(`dash.codexRestartTimeout`),formatMalformed:()=>n(`dash.codexRestartMalformed`)});if(a.current&&i(!1),!t.ok||!t.result)return alert(t.message),null;let r=t.result;return r.code===`stopped`?alert(n(`dash.codexRestartDone`,{count:String(r.stopped.length)})):r.code===`nothing_running`?alert(n(`dash.codexRestartNothing`)):r.code===`enumeration_unavailable`?alert(n(`dash.codexRestartUnknown`)):alert(n(`dash.codexRestartPartial`,{count:String(r.surviving.length)})),a.current&&ol(r.code)&&o.current?.(r.code),r.code},[e,n])}}var cl={"gpt-5.6-sol":Ae,"gpt-5.6-terra":Ne,"gpt-5.6-luna":je,"gpt-daybreak-blue-latest":Ae,"daybreak-blue-latest":Ae,"daybreak-red-latest":De},ll={width:14,height:14,flexShrink:0,verticalAlign:`text-bottom`};function ul(e){return e.slice(e.lastIndexOf(`/`)+1)}function dl(e){return cl[e]??cl[ul(e)]??null}function fl(e){let t=dl(e);return t?(0,_.createElement)(`span`,{className:`model-label`},(0,_.createElement)(t,{style:ll,"aria-hidden":!0}),e):e}function pl(e,t,n){if(!n)return{kind:`disabled`,data:void 0,error:void 0,showSkeleton:!1,refreshing:!1,showError:!1};let r=e.data!==void 0,i=!e.lastAttemptOk&&e.error!==void 0;return e.refreshing?r?{kind:`loading-with-stale-data`,data:e.data,error:e.error,showSkeleton:!1,refreshing:!0,showError:i}:{kind:i?`retrying-cold`:`cold`,data:void 0,error:i?e.error:void 0,showSkeleton:!0,refreshing:!0,showError:!1}:i?r?{kind:`failed-with-stale`,data:e.data,error:e.error,showSkeleton:!1,refreshing:!1,showError:!0}:{kind:`failed-cold`,data:void 0,error:e.error,showSkeleton:!1,refreshing:!1,showError:!0}:r?{kind:t(e.data)?`ready-empty`:`ready-populated`,data:e.data,error:void 0,showSkeleton:!1,refreshing:!1,showError:!1}:{kind:`cold`,data:void 0,error:void 0,showSkeleton:!0,refreshing:!1,showError:!1}}function ml(e,t,n,r){let{isEmpty:i,sessionCacheKey:a,...o}=r,s=(0,_.useMemo)(()=>a?vr(a):null,[a]),c=G(e,t,(0,_.useCallback)(async e=>{let t=await n(e);return a&&yr(a,t),t},[n,a]),{...o,...a?{initialData:o.initialData??s?.data,initialDataCachedAt:o.initialDataCachedAt??s?.cachedAt??null}:{}});return{...c,state:pl(c,i,r.enabled!==!1)}}function hl({className:e,style:t}){return(0,J.jsx)(`span`,{"aria-hidden":`true`,className:e?`data-surface-skeleton__block ${e}`:`data-surface-skeleton__block`,style:t})}function gl({label:e,rows:t=3,className:n}){let r=Math.max(1,Math.floor(t));return(0,J.jsxs)(`div`,{className:n?`data-surface-skeleton ${n}`:`data-surface-skeleton`,role:`status`,"aria-live":`polite`,"aria-atomic":`true`,"aria-busy":`true`,children:[(0,J.jsx)(`span`,{className:`sr-only`,children:e}),Array.from({length:r},(e,t)=>(0,J.jsx)(`div`,{className:`data-surface-skeleton__row`,"aria-hidden":`true`,children:(0,J.jsx)(hl,{})},t))]})}function _l({children:e,busy:t=!0,live:n=!0,className:r}){return(0,J.jsxs)(`div`,{className:r?`data-surface-status ${r}`:`data-surface-status`,role:n?`status`:void 0,"aria-live":n?`polite`:void 0,"aria-atomic":n?`true`:void 0,"aria-busy":t||void 0,children:[t&&(0,J.jsx)(`span`,{className:`spin`,"aria-hidden":`true`}),(0,J.jsx)(`span`,{children:e})]})}var vl=class extends _.Component{state={error:null};static getDerivedStateFromError(e){return{error:e instanceof Error?e:Error(String(e))}}reload=()=>{this.setState({error:null})};render(){return this.state.error?(0,J.jsxs)(`section`,{className:`card`,role:`alert`,style:{maxWidth:720,padding:`var(--space-6)`},children:[(0,J.jsxs)(`h2`,{style:{margin:`0 0 var(--space-2)`,fontSize:`var(--text-title)`},children:[this.props.pageName,`: `,this.props.title]}),(0,J.jsx)(`p`,{className:`muted`,style:{margin:`0 0 var(--space-4)`},children:this.props.message}),(0,J.jsxs)(`p`,{style:{margin:`0 0 var(--space-5)`,overflowWrap:`anywhere`},children:[(0,J.jsxs)(`strong`,{children:[this.props.detailsLabel,`:`]}),` `,this.state.error.message]}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-primary`,onClick:this.reload,children:this.props.reloadLabel})]}):this.props.children}},yl=`gpt-daybreak-blue-latest`,bl=`gpt-6-astra`,xl=Object.freeze({[yl]:`gpt-5.6-sol`});Object.freeze(Object.keys(xl)),Object.freeze({[yl]:{displayName:`Daybreak Blue`,description:`Frontier general-purpose model with safeguards for defensive cybersecurity work.`}});var Sl=new Set([`gpt-5.5`,`gpt-5.4`,`gpt-5.4-mini`,`gpt-5.3-codex-spark`,`gpt-5.6-sol`,`gpt-5.6-terra`,`gpt-5.6-luna`,yl,bl]),Cl=[`low`,`medium`,`high`,`xhigh`,`max`,`ultra`],wl=[`failover`,`round-robin`,`random`,`least-used`,`reset-window`],Tl={failover:`cws.strategy.failover`,"round-robin":`cws.strategy.roundRobin`,random:`cws.strategy.random`,"least-used":`cws.strategy.leastUsed`,"reset-window":`cws.strategy.resetWindow`},El={failover:`cws.strategy.failoverHint`,"round-robin":`cws.strategy.roundRobinHint`,random:`cws.strategy.randomHint`,"least-used":`cws.strategy.leastUsedHint`,"reset-window":`cws.strategy.resetWindowHint`},Dl={failover:`cws.targets.failoverHint`,"round-robin":`cws.targets.roundRobinHint`,random:`cws.targets.randomHint`,"least-used":`cws.targets.leastUsedHint`,"reset-window":`cws.targets.resetWindowHint`},Ol=new Set(wl);function kl(e,t,n=`strict`){let r=e.filter(e=>e.provider.trim()&&e.model.trim());if(r.length===0)return[...Cl];let i=new Set(Cl),a=null;for(let e of r){let r=`${e.provider.trim()}/${e.model.trim()}`,o=t.get(r);if(o===void 0||n===`adaptive`&&o.length===0)continue;let s=o.filter(e=>i.has(e));if(a===null)a=s;else{let e=new Set(s);a=a.filter(t=>e.has(t))}}if(a===null)return[...Cl];let o=new Set(a);return Cl.filter(e=>o.has(e))}var Al=0;function jl(e={}){return{provider:e.provider??``,model:e.model??``,...e.weight===void 0?{}:{weight:e.weight},clientKey:e.clientKey??`ct-${++Al}`}}function Ml(e){return e===`disabled`?`disabled`:`auto`}function Nl(e){return e===`adaptive`?`adaptive`:`strict`}var Pl=/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$/,Fl=/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}(\/[a-zA-Z0-9][a-zA-Z0-9._-]{0,63})?$/,Il=/^(?:gpt-|o1-|o3-|o4-|codex-)/;function Ll(e){return Pl.test(e.trim())}function Rl(e){return`combo/${e.trim()}`}function zl(e,t){return(typeof t==`string`?t.trim():``)||Rl(e)}function Bl(e,t){let n=t.trim(),r=e.nativeAlias&&(!n||n.includes(`/`)||!Il.test(n));return{...e,alias:n?t:null,model:zl(e.id,t),...r?{nativeAlias:!1,displayName:null}:{}}}function Vl(e){return typeof e==`string`&&e.trim()?e.trim():null}function Hl(e){return typeof e==`string`&&Ol.has(e)?e:`failover`}function Ul(e){return typeof e==`number`&&Number.isInteger(e)&&e>=1&&e<=100?e:1}function Wl(e){return typeof e==`string`&&Cl.includes(e)?e:null}function Gl(e){return typeof e==`number`&&Number.isInteger(e)&&e>=1&&e<=1e4?e:void 0}function Kl(e){if(!e||typeof e!=`object`)return[];let t=e.combos;if(!Array.isArray(t))return[];let n=[];for(let e of t){if(!e||typeof e!=`object`)continue;let t=e,r=typeof t.id==`string`?t.id.trim():``;if(!r)continue;let i=Array.isArray(t.targets)?t.targets:[],a=[];for(let e of i){if(!e||typeof e!=`object`)continue;let t=e,n=typeof t.provider==`string`?t.provider.trim():``,r=typeof t.model==`string`?t.model.trim():``;if(!n||!r)continue;let i=Gl(t.weight);a.push(jl(i===void 0?{provider:n,model:r}:{provider:n,model:r,weight:i}))}n.push({id:r,model:typeof t.model==`string`&&t.model.trim()?t.model.trim():zl(r,Vl(t.alias)),alias:Vl(t.alias),nativeAlias:t.nativeAlias===!0,displayName:Vl(t.displayName),strategy:Hl(t.strategy),stickyLimit:Ul(t.stickyLimit),defaultEffort:Wl(t.defaultEffort),imageInput:Ml(t.imageInput),reasoningEffortMode:Nl(t.reasoningEffortMode),targets:a})}return n.sort((e,t)=>e.id.localeCompare(t.id,void 0,{sensitivity:`base`}))}function ql(e){let t=[],n=[],r=[];for(let i of e)i.strategy===`failover`?t.push(i):i.strategy===`round-robin`?n.push(i):r.push(i);return{failover:t,roundRobin:n,other:r}}function Jl(e,t){let n=t.trim().toLowerCase();return n?e.filter(e=>e.id.toLowerCase().includes(n)||e.model.toLowerCase().includes(n)?!0:e.targets.some(e=>e.provider.toLowerCase().includes(n)||e.model.toLowerCase().includes(n))):e}function Yl(e){return e&&typeof e==`object`&&!Array.isArray(e)?e:null}function Xl(e){return typeof e==`number`&&Number.isFinite(e)?e:null}function Zl(e,t){let n=Xl(e);return n!==null&&t-n<18e5}function Ql(e){let t=Xl(e);return t!==null&&Number.isInteger(t)&&t>=0?t:null}function $l(e,t){let n=Yl(e),r=Xl(n?.usedPercent);return!!n&&r!==null&&r>=0&&Ql(n.includedAccounts)!==null&&(Ql(n.includedAccounts)??0)>0&&Ql(n.excludedAccounts)===0&&n.incomplete===!1&&Zl(n.updatedAt,t)}function eu(e,t){let n=Yl(e);if(!n||n.kind!==`capacity-weighted-v1`||n.scope!==`routable-known`||n.presentation!==`aggregate`||n.incomplete!==!1)return!1;for(let e of[`includedAccounts`,`excludedAccounts`,`unknownPlanAccounts`,`missingQuotaAccounts`,`pausedAccounts`,`reauthAccounts`,`staleQuotaAccounts`,`partialWindowAccounts`])if(Ql(n[e])===null)return!1;if((Ql(n.includedAccounts)??0)===0)return!1;for(let e of[`excludedAccounts`,`unknownPlanAccounts`,`missingQuotaAccounts`,`pausedAccounts`,`reauthAccounts`,`staleQuotaAccounts`,`partialWindowAccounts`])if(n[e]!==0)return!1;let r=!1;for(let e of[`fiveHour`,`weekly`,`monthly`])if(Object.hasOwn(n,e)){if(!$l(n[e],t))return!1;r=!0}if(Object.hasOwn(n,`customWindows`)){if(!Array.isArray(n.customWindows))return!1;for(let e of n.customWindows){let n=Yl(e);if(!n||typeof n.label!=`string`||!n.label.trim()||!$l(n,t))return!1;r=!0}}return r}function tu(e,t){if(!Zl(e.updatedAt,t))return`unknown`;let n=Yl(e.quota);if(!n||!Zl(n.updatedAt,t)||e.aggregation!==void 0&&!eu(e.aggregation,t))return`unknown`;let r=!1,i=!1;for(let e of[`fiveHourPercent`,`weeklyPercent`,`monthlyPercent`]){if(!Object.hasOwn(n,e))continue;let t=Xl(n[e]);if(t===null||t<0)return`unknown`;r=!0,t>=100&&(i=!0)}for(let e of[`fiveHourResetAt`,`weeklyResetAt`,`monthlyResetAt`])if(Object.hasOwn(n,e)&&Xl(n[e])===null)return`unknown`;if(Object.hasOwn(n,`customWindows`)){if(!Array.isArray(n.customWindows))return`unknown`;for(let e of n.customWindows){let t=Yl(e),n=Xl(t?.percent);if(!t||typeof t.label!=`string`||!t.label.trim()||n===null||n<0||Object.hasOwn(t,`resetAt`)&&Xl(t.resetAt)===null)return`unknown`;r=!0,n>=100&&(i=!0)}}if(Object.hasOwn(n,`creditsUsd`)){let e=Yl(n.creditsUsd);if(!e)return`unknown`;let t=Xl(e.used),a=Xl(e.limit),o=Xl(e.remaining),s=Xl(e.percent);if(t===null||t<0||a===null||a<0||o===null||s===null||s<0||e.unlimited!==void 0&&typeof e.unlimited!=`boolean`||Object.hasOwn(e,`expiresAt`)&&Xl(e.expiresAt)===null)return`unknown`;r=!0,e.unlimited!==!0&&o<=0&&(i=!0)}return r?i?`exhausted`:`available`:`unknown`}function nu(e,t=Date.now()){if(!Array.isArray(e))return{};let n={};for(let r of e){let e=Yl(r),i=typeof e?.provider==`string`?e.provider.trim():``;if(!e||!i)continue;let a=tu(e,t);n[i]=Object.hasOwn(n,i)&&n[i]!==a?`unknown`:a}return n}function ru(e,t,n){let r=e.flatMap(e=>{let t=e.provider.trim();return!t||!e.model.trim()||!Object.hasOwn(n,t)||n[t]?.disabled===!0?[]:[t]});if(r.length===0)return`unknown`;let i=!1;for(let e of r){let n=t[e]??`unknown`;if(n===`available`)return`available`;n===`unknown`&&(i=!0)}return i?`unknown`:`exhausted`}function iu(e,t={}){let n=[],r=t.cataloguedComboIds;for(let i of e)i.targets.length===0?n.push({id:i.id,model:i.model,reason:`empty-targets`}):i.targets.length<2&&n.push({id:i.id,model:i.model,reason:`few-targets`}),r&&i.targets.length>0&&!r.has(i.id)&&n.push({id:i.id,model:i.model,reason:`catalog-omitted`}),t.providerQuotaStates&&t.providers&&ru(i.targets,t.providerQuotaStates,t.providers)===`exhausted`&&n.push({id:i.id,model:i.model,reason:`all-targets-exhausted`});return n}function au(e,t){return e.id!==t.id||e.alias!==t.alias||e.nativeAlias!==t.nativeAlias||e.displayName!==t.displayName||e.strategy!==t.strategy||e.stickyLimit!==t.stickyLimit||e.defaultEffort!==t.defaultEffort||(e.imageInput??`auto`)!==(t.imageInput??`auto`)||(e.reasoningEffortMode??`strict`)!==(t.reasoningEffortMode??`strict`)||e.targets.length!==t.targets.length?!1:e.targets.every((e,n)=>{let r=t.targets[n];return e.provider===r.provider&&e.model===r.model&&(e.weight??1)===(r.weight??1)})}function ou(e,t={}){let n=e.strategy===`round-robin`||e.strategy===`random`;return{id:e.id.trim(),...t.renameFrom?{renameFrom:t.renameFrom}:{},combo:{targets:e.targets.map(e=>n?{provider:e.provider.trim(),model:e.model.trim(),weight:e.weight??1}:{provider:e.provider.trim(),model:e.model.trim()}),strategy:e.strategy,defaultEffort:e.defaultEffort,...e.imageInput===`disabled`?{imageInput:`disabled`}:{},...e.reasoningEffortMode===`adaptive`?{reasoningEffortMode:`adaptive`}:{},...e.strategy===`round-robin`?{stickyLimit:e.stickyLimit}:{},...e.alias&&e.alias.trim()?{alias:e.alias.trim()}:{},...e.nativeAlias?{nativeAlias:!0}:{},...e.displayName&&e.displayName.trim()?{displayName:e.displayName.trim()}:{}}}}function su(e,t){let n=e.id.trim();if(!n)return`missingId`;if(!Ll(n))return`invalidId`;if(t.existingIds.includes(n))return`duplicateId`;if(Object.hasOwn(t.providers,`combo`))return`reservedNamespace`;if(Object.hasOwn(t.providers,n))return`providerCollision`;let r=e.alias?.trim()??``,i=e.displayName?.trim()??``;if(r){if(!Fl.test(r))return`invalidAlias`;if(r===`combo`||r.startsWith(`combo/`))return`aliasReservedNamespace`;if(!r.includes(`/`)&&Il.test(r)&&!e.nativeAlias)return`aliasNativeFamily`;if((t.existingAliases??[]).includes(r))return`duplicateAlias`}let a=[...e.displayName??``].some(e=>{let t=e.codePointAt(0)??0;return t<=31||t===127});if(e.displayName!==null&&(i.length>128||a))return`invalidDisplayName`;if(e.nativeAlias&&!Sl.has(r))return`unsupportedNativeAlias`;if(e.nativeAlias&&!i)return`missingNativeAliasDisplayName`;if(e.targets.length<1)return`noTargets`;for(let n of e.targets){if(!n.provider.trim()||!n.model.trim())return`incompleteTarget`;if(!Object.hasOwn(t.providers,n.provider.trim()))return`unknownProvider`}let o=new Set;for(let t of e.targets){let e=`${t.provider.trim()}/${t.model.trim()}`;if(o.has(e))return`duplicateTarget`;o.add(e)}if(e.strategy===`round-robin`&&(!Number.isInteger(e.stickyLimit)||e.stickyLimit<1||e.stickyLimit>100))return`invalidStickyLimit`;if(e.strategy===`round-robin`||e.strategy===`random`)for(let t of e.targets){let e=t.weight??1;if(!Number.isInteger(e)||e<1||e>1e4)return`invalidWeight`}return e.targets.some(e=>t.providers[e.provider.trim()]?.disabled!==!0)?null:`noEnabledTarget`}function cu(e=``){return{id:e,model:e?Rl(e):`combo/`,alias:null,nativeAlias:!1,displayName:null,strategy:`failover`,stickyLimit:1,defaultEffort:null,imageInput:`auto`,reasoningEffortMode:`strict`,targets:[jl()]}}function lu(e,t){return e.length!==0&&e.every(e=>{let n=e.provider.trim(),r=e.model.trim();return!n||!r?!1:!!t.find(e=>e.provider===n&&e.id===r)?.inputModalities?.includes(`image`)})}function uu(e){return e.filter(e=>!e.disabled&&!e.hiddenFromPicker).sort((e,t)=>e.name.localeCompare(t.name))}function du(e,t,n){if(e===``)return;let r=Number(e);if(Number.isFinite(r))return Math.min(n,Math.max(t,r))}function fu(e){if(!e)return!1;let t=e.name.toLowerCase();if(t!==`openai`&&t!==`chatgpt`||(e.authMode??``).toLowerCase()!==`forward`||(e.adapter??``).toLowerCase()!==`openai-responses`)return!1;let n=(e.baseUrl??``).replace(/\/+$/,``);return!n||n.includes(`chatgpt.com/backend-api/codex`)}function pu(e,t,n){let r=new Set([t]),i=n.find(e=>e.name===t);(t.toLowerCase()===`chatgpt`||fu(i))&&r.add(`openai`);let a=[],o=new Set;for(let t of e)!r.has(t.provider)||!t.id||o.has(t.id)||(o.add(t.id),a.push(t.id));return a.toSorted((e,t)=>e.localeCompare(t))}function mu({value:e,onChange:t,disabled:n}){let r=Q();return(0,J.jsx)(`div`,{className:`cwi-strategy-seg`,role:`radiogroup`,"aria-label":r(`cws.strategy`),children:wl.map(i=>(0,J.jsx)(`button`,{type:`button`,role:`radio`,"aria-checked":e===i,className:`btn btn-sm${e===i?` btn-primary`:` btn-ghost`}`,disabled:n,onClick:()=>t(i),children:r(Tl[i])},i))})}function hu({id:e,value:t,onChange:n,disabled:r,allowedEfforts:i}){let a=Q(),o=i??Cl,s=t!==null&&!o.includes(t);return(0,J.jsxs)(J.Fragment,{children:[(0,J.jsxs)(`select`,{id:e,className:`input`,value:t??``,disabled:r,"aria-label":a(`cws.field.defaultEffort`),onChange:e=>n(e.target.value===``?null:e.target.value),children:[(0,J.jsx)(`option`,{value:``,children:a(`cws.field.defaultEffortNone`)}),s&&t?(0,J.jsxs)(`option`,{value:t,children:[t,` (`,a(`cws.field.defaultEffortUnsupportedOption`),`)`]}):null,o.map(e=>(0,J.jsx)(`option`,{value:e,children:e},e))]}),s?(0,J.jsx)(`p`,{className:`muted`,style:{fontSize:12,margin:`4px 0 0`,color:`var(--danger, #b42318)`},children:a(`cws.field.defaultEffortUnsupported`)}):null]})}function gu({targets:e,models:t,imageInput:n,reasoningEffortMode:r,disabled:i,onChange:a}){let o=Q(),s=lu(e,t),c=s&&n!==`disabled`;return(0,J.jsxs)(`section`,{className:`cwi-capabilities`,"aria-label":o(`cws.capabilities`),children:[(0,J.jsx)(`span`,{className:`field-label`,children:o(`cws.capabilities`)}),(0,J.jsxs)(`div`,{className:`cwi-capability-row`,children:[(0,J.jsxs)(`div`,{children:[(0,J.jsx)(`span`,{className:`cwi-capability-label`,children:o(`cws.capability.imageInput`)}),(0,J.jsx)(`p`,{className:`muted cwi-capability-hint`,children:o(s?`cws.capability.imageInputHint`:`cws.capability.imageInputUnavailable`)})]}),(0,J.jsx)(Tt,{on:c,onClick:()=>{s&&a({imageInput:n===`auto`?`disabled`:`auto`})},disabled:i||!s,label:o(`cws.capability.imageInput`)})]}),(0,J.jsxs)(`div`,{className:`cwi-capability-row`,children:[(0,J.jsxs)(`div`,{children:[(0,J.jsx)(`span`,{className:`cwi-capability-label`,children:o(`cws.capability.adaptiveEffort`)}),(0,J.jsx)(`p`,{className:`muted cwi-capability-hint`,children:o(`cws.capability.adaptiveEffortHint`)})]}),(0,J.jsx)(Tt,{on:r===`adaptive`,onClick:()=>{a({reasoningEffortMode:r===`adaptive`?`strict`:`adaptive`})},disabled:i,label:o(`cws.capability.adaptiveEffort`)})]})]})}function _u({targets:e,strategy:t,providers:n,models:r,providerQuotaStates:i,onChange:a}){let o=Q(),s=uu(n),[c,l]=(0,_.useState)(null),[u,d]=(0,_.useState)(null),f=(t,n)=>{a(e.map((e,r)=>r===t?{...e,...n}:e))},p=(t,n)=>{if(t===n||t<0||n<0||t>=e.length||n>=e.length)return;let r=[...e],[i]=r.splice(t,1);r.splice(n,0,i),a(r)};return(0,J.jsxs)(`div`,{className:`cwi-target-list`,children:[e.map((m,h)=>{let g=n.find(e=>e.name===m.provider),_=g&&!s.some(e=>e.name===m.provider)?[...s,g]:s,v=pu(r,m.provider,n),y=m.model&&!v.includes(m.model)?[m.model,...v]:v,b=!m.provider,x=c===h,S=u===h&&c!==null&&c!==h,C=i[m.provider.trim()]??`unknown`;return(0,J.jsxs)(`div`,{className:[`cwi-target-row`,t===`failover`?`cwi-target-row--failover`:``,x?`cwi-target-row--dragging`:``,S?`cwi-target-row--drop`:``].filter(Boolean).join(` `),onDragOver:e=>{c!==null&&(e.preventDefault(),e.dataTransfer.dropEffect=`move`,u!==h&&d(h))},onDrop:e=>{e.preventDefault(),c!==null&&p(c,h),l(null),d(null)},onDragEnd:()=>{l(null),d(null)},children:[(0,J.jsx)(`button`,{type:`button`,className:`cwi-target-grip`,draggable:!0,"aria-label":o(`cws.target.drag`),title:o(`cws.target.drag`),onDragStart:e=>{l(h),e.dataTransfer.effectAllowed=`move`,e.dataTransfer.setData(`text/plain`,String(h))},children:(0,J.jsx)(Fe,{width:14,height:14,"aria-hidden":`true`})}),(0,J.jsxs)(`div`,{className:`cwi-target-reorder`,children:[(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,disabled:h===0,"aria-label":o(`cws.target.moveUp`),onClick:()=>p(h,h-1),children:(0,J.jsx)(ye,{width:14,height:14,"aria-hidden":`true`})}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,disabled:h===e.length-1,"aria-label":o(`cws.target.moveDown`),onClick:()=>p(h,h+1),children:(0,J.jsx)(be,{width:14,height:14,"aria-hidden":`true`})})]}),(0,J.jsxs)(`select`,{className:`input`,value:m.provider,"aria-label":o(`cws.target.provider`),onChange:e=>{let t=e.target.value,i=pu(r,t,n)[0]??``;f(h,{provider:t,model:i})},children:[(0,J.jsx)(`option`,{value:``,children:o(`cws.target.pickProvider`)}),_.map(e=>(0,J.jsx)(`option`,{value:e.name,children:e.disabled?o(`cws.target.disabled`,{name:jn(e.name,o)}):jn(e.name,o)},e.name))]}),(0,J.jsxs)(`select`,{className:`input`,value:m.model,disabled:b,"aria-label":o(`cws.target.model`),onChange:e=>f(h,{model:e.target.value}),children:[(0,J.jsx)(`option`,{value:``,children:b?o(`cws.target.pickProviderFirst`):y.length===0?o(`cws.target.noModels`):o(`cws.target.pickModel`)}),y.map(e=>(0,J.jsx)(`option`,{value:e,children:e},e))]}),(t===`round-robin`||t===`random`)&&(0,J.jsx)(`input`,{className:`input mono`,type:`number`,min:1,max:1e4,value:m.weight??1,"aria-label":o(`cws.target.weight`),onChange:e=>{let t=du(e.target.value,1,1e4);t!==void 0&&f(h,{weight:t})}}),(0,J.jsx)(`span`,{className:`cwi-quota-badge cwi-quota-badge--${C}`,"aria-label":o(`cws.quota.${C}`),children:o(`cws.quota.${C}`)}),(0,J.jsx)(`div`,{className:`cwi-target-actions`,children:(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,disabled:e.length<=1,onClick:()=>a(e.filter((e,t)=>t!==h)),"aria-label":o(`common.remove`),children:(0,J.jsx)(he,{width:14,height:14})})})]},m.clientKey??`${m.provider}:${m.model}`)}),(0,J.jsxs)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,style:{alignSelf:`flex-start`},onClick:()=>a([...e,jl()]),children:[(0,J.jsx)(fe,{width:14,height:14}),` `,o(`cws.target.add`)]})]})}function vu({existingIds:e,existingAliases:t,providerMap:n,providerQuotaStates:r,providers:i,models:a,onClose:o,onSubmit:s}){let c=Q(),l=(0,_.useRef)(null),[u,d]=(0,_.useState)(()=>cu()),[f,p]=(0,_.useState)(!1),[m,h]=(0,_.useState)(``),g=(0,_.useMemo)(()=>{let e=new Map;for(let t of a)e.set(`${t.provider}/${t.id}`,t.reasoningEfforts);return e},[a]),v=(0,_.useMemo)(()=>kl(u.targets,g,u.reasoningEffortMode??`strict`),[u.targets,g,u.reasoningEffortMode]),y=ru(u.targets,r,n)===`exhausted`;(0,_.useEffect)(()=>{let e=l.current;e&&!e.open&&e.showModal()},[]);let b=(0,_.useCallback)(()=>{f||o()},[f,o]),x=(0,_.useCallback)(e=>{e.preventDefault(),b()},[b]),S=async()=>{let r=su(u,{existingIds:e,existingAliases:t,isCreate:!0,providers:n});if(r){h(c(`cws.err.${r}`));return}p(!0),h(``);let i=u.id.trim(),a=u.alias?.trim()||null;try{let e=await s({...u,id:i,alias:a,model:zl(i,a)});if(!e.ok){h(e.error||c(`cws.saveFailed`));return}}finally{p(!1)}};return(0,J.jsxs)(`dialog`,{ref:l,className:`modal-overlay`,"aria-labelledby":`cwi-add-title`,onCancel:x,children:[(0,J.jsx)(`button`,{type:`button`,className:`modal-backdrop-dismiss`,"aria-label":c(`common.close`),tabIndex:-1,onClick:b}),(0,J.jsxs)(`div`,{className:`modal-card`,style:{width:`min(560px, 94vw)`},onClick:e=>e.stopPropagation(),children:[(0,J.jsxs)(`div`,{className:`row`,style:{justifyContent:`space-between`,marginBottom:8},children:[(0,J.jsx)(`h3`,{id:`cwi-add-title`,style:{margin:0},children:c(`cws.addTitle`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:b,disabled:f,"aria-label":c(`common.close`),children:(0,J.jsx)(de,{width:16,height:16})})]}),(0,J.jsx)(`p`,{className:`muted`,style:{marginTop:0,maxWidth:`62ch`,overflowWrap:`anywhere`},children:c(`cws.addSubtitle`)}),m&&(0,J.jsx)($,{tone:`err`,children:m}),y&&(0,J.jsx)(`div`,{className:`cwi-quota-banner`,role:`status`,"aria-live":`polite`,children:c(`cws.quota.allExhausted`)}),(0,J.jsxs)(`div`,{className:`cwi-modal-form`,children:[(0,J.jsxs)(`div`,{className:`cwi-field`,children:[(0,J.jsx)(`label`,{htmlFor:`cwi-new-id`,children:c(`cws.field.id`)}),(0,J.jsx)(`input`,{id:`cwi-new-id`,className:`input mono`,value:u.id,disabled:f,onChange:e=>d(t=>({...t,id:e.target.value,model:zl(e.target.value,t.alias)}))}),(0,J.jsx)(`p`,{className:`muted`,style:{fontSize:12,margin:`8px 0 0`},children:c(`cws.field.idInternalHint`)})]}),(0,J.jsxs)(`div`,{className:`cwi-field`,children:[(0,J.jsx)(`label`,{htmlFor:`cwi-new-alias`,children:c(`cws.field.alias`)}),(0,J.jsx)(`input`,{id:`cwi-new-alias`,className:`input mono`,value:u.alias??``,placeholder:c(`cws.field.aliasPlaceholder`),disabled:f,onChange:e=>d(t=>({...t,alias:e.target.value.trim()?e.target.value:null,model:zl(t.id,e.target.value)}))}),(0,J.jsx)(`p`,{className:`muted`,style:{fontSize:12,margin:`8px 0 0`},children:c(`cws.field.aliasHint`)}),(0,J.jsx)(`p`,{className:`muted`,style:{fontSize:12,margin:`8px 0 0`},children:c(`cws.field.idHint`,{model:u.id.trim()?zl(u.id,u.alias):`…`})})]}),(0,J.jsxs)(`div`,{className:`cwi-field`,children:[(0,J.jsx)(`span`,{className:`field-label`,children:c(`cws.strategy`)}),(0,J.jsx)(mu,{value:u.strategy,disabled:f,onChange:e=>d(t=>({...t,strategy:e}))}),(0,J.jsx)(`p`,{className:`muted`,style:{fontSize:12,margin:`8px 0 0`},children:c(El[u.strategy])})]}),(0,J.jsxs)(`div`,{className:`cwi-field`,children:[(0,J.jsx)(`label`,{htmlFor:`cwi-new-effort`,children:c(`cws.field.defaultEffort`)}),(0,J.jsx)(hu,{id:`cwi-new-effort`,value:u.defaultEffort,disabled:f,allowedEfforts:v,onChange:e=>d(t=>({...t,defaultEffort:e}))}),(0,J.jsx)(`p`,{className:`muted`,style:{fontSize:12,margin:`8px 0 0`},children:c(`cws.field.defaultEffortHint`)})]}),u.strategy===`round-robin`&&(0,J.jsxs)(`div`,{className:`cwi-field`,children:[(0,J.jsx)(`label`,{htmlFor:`cwi-new-sticky`,children:c(`cws.field.stickyLimit`)}),(0,J.jsx)(`input`,{id:`cwi-new-sticky`,className:`input mono`,type:`number`,min:1,max:100,value:u.stickyLimit,disabled:f,onChange:e=>{let t=du(e.target.value,1,100);t!==void 0&&d(e=>({...e,stickyLimit:t}))}}),(0,J.jsx)(`p`,{className:`muted`,style:{fontSize:12,margin:`8px 0 0`},children:c(`cws.field.stickyLimitHint`)})]}),(0,J.jsxs)(`div`,{className:`cwi-field`,children:[(0,J.jsx)(`span`,{className:`field-label`,children:c(`cws.targets`)}),(0,J.jsx)(_u,{targets:u.targets,strategy:u.strategy,providers:i,models:a,providerQuotaStates:r,onChange:e=>d(t=>({...t,targets:e}))}),(0,J.jsx)(`p`,{className:`muted`,style:{fontSize:12,margin:`8px 0 0`},children:c(Dl[u.strategy])})]}),(0,J.jsx)(gu,{targets:u.targets,models:a,imageInput:u.imageInput??`auto`,reasoningEffortMode:u.reasoningEffortMode??`strict`,disabled:f,onChange:e=>d(t=>({...t,...e}))})]}),(0,J.jsxs)(`div`,{className:`cwi-modal-actions`,children:[(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost`,onClick:b,disabled:f,children:c(`common.cancel`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-primary`,onClick:()=>{S()},disabled:f||y,children:c(f?`common.saving`:`cws.create`)})]})]})]})}var yu=[`config`,`about`],bu=e=>`cws-detail-tab-${e}`,xu=e=>`cws-detail-panel-${e}`;function Su({baseline:e,isCreate:t=!1,otherIds:n,otherAliases:r,providerMap:i,providerQuotaStates:a,providers:o,models:s,onBack:c,onSaved:l,onRequestRemove:u,onSave:d,onDirtyChange:f}){let p=Q(),[m,h]=(0,_.useState)(`config`),g=(0,_.useCallback)((e,t)=>{let n;if(e.key===`ArrowRight`)n=(t+1)%yu.length;else if(e.key===`ArrowLeft`)n=(t-1+yu.length)%yu.length;else if(e.key===`Home`)n=0;else if(e.key===`End`)n=yu.length-1;else return;e.preventDefault(),h(yu[n]),e.currentTarget.parentElement?.querySelectorAll(`[role="tab"]`)[n]?.focus()},[]),[v,y]=(0,_.useState)(e),[b,x]=(0,_.useState)(!1),[S,C]=(0,_.useState)(null),[w,T]=(0,_.useState)(!1),E=!au(v,e),D=ru(v.targets,a,i)===`exhausted`,O=`${e.id}:${e.alias??``}:${e.nativeAlias}:${e.displayName??``}:${e.strategy}:${e.stickyLimit}:${e.defaultEffort}:${e.imageInput??`auto`}:${e.reasoningEffortMode??`strict`}:${e.targets.map(e=>`${e.provider}/${e.model}:${e.weight??1}`).join(`,`)}`,k=(0,_.useMemo)(()=>{let e=new Map;for(let t of s)e.set(`${t.provider}/${t.id}`,t.reasoningEfforts);return e},[s]),A=(0,_.useMemo)(()=>kl(v.targets,k,v.reasoningEffortMode??`strict`),[v.targets,k,v.reasoningEffortMode]),j=(0,_.useCallback)(t=>{let n=t(v);y(n),f(!au(n,e))},[v,e,f]);(0,_.useEffect)(()=>{let t=window.setTimeout(()=>{y(e),C(null),h(`config`),f(!1)},0);return()=>window.clearTimeout(t)},[O]);let M=async()=>{try{await navigator.clipboard.writeText(e.model),T(!0),window.setTimeout(()=>T(!1),1200)}catch{}},N=async()=>{let a=su(v,{existingIds:n,existingAliases:r,isCreate:t,providers:i});if(a){C({ok:!1,text:p(`cws.err.${a}`)});return}x(!0);let o=v.id.trim(),s=v.alias?.trim()||null,c=v.displayName?.trim()||null,u={...v,id:o,alias:s,displayName:c,model:zl(o,s)},f=!t&&o!==e.id?e.id:void 0;try{let e=await d(u,t,f);if(!e.ok){C({ok:!1,text:e.error||p(`cws.saveFailed`)});return}C({ok:!0,text:t?p(`cws.created`,{model:u.model}):p(`cws.saved`)}),l(u)}finally{x(!1)}},P=t?v.id.trim()?zl(v.id,v.alias):p(`cws.addTitle`):e.model;return(0,J.jsxs)(`div`,{className:`combos-workspace-detail`,children:[(0,J.jsxs)(`div`,{className:`combos-workspace-detail-head`,children:[c&&(0,J.jsxs)(`button`,{type:`button`,className:`btn btn-ghost btn-sm pwi-back-overview`,onClick:c,"aria-label":p(`cws.backToAll`),children:[(0,J.jsx)(Se,{style:{width:14,height:14,transform:`rotate(180deg)`},"aria-hidden":`true`}),p(`cws.allCombos`)]}),(0,J.jsx)(`h2`,{className:`combos-workspace-detail-title`,children:P}),!t&&(0,J.jsx)(`button`,{type:`button`,className:`chip cwi-copy-chip`,onClick:()=>{M()},title:p(`cws.copyModel`),children:p(w?`cws.copied`:`cws.copyModel`)}),(0,J.jsxs)(`div`,{className:`combos-workspace-detail-actions`,children:[!t&&u&&(0,J.jsxs)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:u,children:[(0,J.jsx)(he,{width:14,height:14}),` `,p(`common.remove`)]}),(0,J.jsx)(`button`,{id:t?`cwi-edit-create`:`cwi-edit-save`,type:`button`,className:`btn btn-primary btn-sm`,disabled:!t&&!E||b||D,onClick:()=>{N()},children:p(b?`common.saving`:t?`cws.create`:`common.save`)})]})]}),S&&(0,J.jsx)($,{tone:S.ok?`ok`:`err`,children:S.text}),D&&(0,J.jsx)(`div`,{className:`cwi-quota-banner`,role:`status`,"aria-live":`polite`,children:p(`cws.quota.allExhausted`)}),(0,J.jsx)(`div`,{className:`segmented combos-workspace-segmented`,role:`tablist`,"aria-label":p(`cws.tabsLabel`),children:yu.map((e,t)=>(0,J.jsx)(`button`,{type:`button`,role:`tab`,id:bu(e),"aria-selected":m===e,"aria-controls":xu(e),tabIndex:m===e?0:-1,className:`btn btn-sm ${m===e?`btn-primary`:`btn-ghost`}`,onClick:()=>h(e),onKeyDown:e=>g(e,t),children:p(e===`config`?`cws.tab.config`:`cws.tab.about`)},e))}),(0,J.jsx)(`div`,{className:`combos-workspace-tab-content`,role:`tabpanel`,id:xu(`config`),"aria-labelledby":bu(`config`),hidden:m!==`config`,children:(0,J.jsxs)(`div`,{className:`cwi-form-grid`,children:[(0,J.jsxs)(`div`,{className:`cwi-field`,children:[(0,J.jsx)(`label`,{htmlFor:`cwi-edit-id`,children:p(`cws.field.id`)}),(0,J.jsx)(`input`,{id:`cwi-edit-id`,className:`input mono`,value:v.id,disabled:b,onChange:e=>j(t=>({...t,id:e.target.value,model:zl(e.target.value,t.alias)}))}),(0,J.jsx)(`p`,{className:`muted`,style:{fontSize:12,margin:`8px 0 0`},children:t?p(`cws.field.idInternalHint`):p(`cws.field.idHintEdit`,{model:zl(v.id,v.alias)})})]}),(0,J.jsxs)(`div`,{className:`cwi-field`,children:[(0,J.jsx)(`label`,{htmlFor:`cwi-edit-alias`,children:p(`cws.field.alias`)}),(0,J.jsx)(`input`,{id:`cwi-edit-alias`,className:`input mono`,value:v.alias??``,placeholder:Rl(v.id.trim()||`…`),disabled:b,onChange:e=>j(t=>Bl(t,e.target.value))}),(0,J.jsx)(`p`,{className:`muted`,style:{fontSize:12,margin:`8px 0 0`},children:p(`cws.field.aliasHint`)})]}),(0,J.jsxs)(`div`,{className:`cwi-field`,children:[(0,J.jsxs)(`label`,{htmlFor:`cwi-edit-native-alias`,children:[(0,J.jsx)(`input`,{id:`cwi-edit-native-alias`,type:`checkbox`,checked:v.nativeAlias,disabled:b,onChange:e=>j(t=>({...t,nativeAlias:e.target.checked}))}),` `,p(`cws.field.nativeAlias`)]}),(0,J.jsx)(`p`,{className:`muted`,style:{fontSize:12,margin:`8px 0 0`},children:p(`cws.field.nativeAliasHint`)})]}),(0,J.jsxs)(`div`,{className:`cwi-field`,children:[(0,J.jsx)(`label`,{htmlFor:`cwi-edit-display-name`,children:p(`cws.field.displayName`)}),(0,J.jsx)(`input`,{id:`cwi-edit-display-name`,className:`input`,value:v.displayName??``,maxLength:128,disabled:b,onChange:e=>j(t=>({...t,displayName:e.target.value||null}))}),(0,J.jsx)(`p`,{className:`muted`,style:{fontSize:12,margin:`8px 0 0`},children:p(`cws.field.displayNameHint`)})]}),(0,J.jsxs)(`div`,{className:`cwi-field`,children:[(0,J.jsx)(`span`,{className:`field-label`,children:p(`cws.strategy`)}),(0,J.jsx)(mu,{value:v.strategy,disabled:b,onChange:e=>j(t=>({...t,strategy:e}))}),(0,J.jsx)(`p`,{className:`muted`,style:{fontSize:12,margin:`8px 0 0`},children:p(El[v.strategy])})]}),(0,J.jsxs)(`div`,{className:`cwi-field`,children:[(0,J.jsx)(`label`,{htmlFor:`cwi-effort`,children:p(`cws.field.defaultEffort`)}),(0,J.jsx)(hu,{id:`cwi-effort`,value:v.defaultEffort,disabled:b,allowedEfforts:A,onChange:e=>j(t=>({...t,defaultEffort:e}))}),(0,J.jsx)(`p`,{className:`muted`,style:{fontSize:12,margin:`8px 0 0`},children:p(`cws.field.defaultEffortHint`)})]}),v.strategy===`round-robin`&&(0,J.jsxs)(`div`,{className:`cwi-field`,children:[(0,J.jsx)(`label`,{htmlFor:`cwi-sticky`,children:p(`cws.field.stickyLimit`)}),(0,J.jsx)(`input`,{id:`cwi-sticky`,className:`input mono`,type:`number`,min:1,max:100,value:v.stickyLimit,disabled:b,onChange:e=>{let t=du(e.target.value,1,100);t!==void 0&&j(e=>({...e,stickyLimit:t}))}})]}),(0,J.jsxs)(`div`,{className:`cwi-field`,children:[(0,J.jsx)(`span`,{className:`field-label`,children:p(`cws.targets`)}),(0,J.jsx)(_u,{targets:v.targets,strategy:v.strategy,providers:o,models:s,providerQuotaStates:a,onChange:e=>j(t=>({...t,targets:e}))}),(0,J.jsx)(`p`,{className:`muted`,style:{fontSize:12,margin:`8px 0 0`},children:p(Dl[v.strategy])})]}),(0,J.jsx)(gu,{targets:v.targets,models:s,imageInput:v.imageInput??`auto`,reasoningEffortMode:v.reasoningEffortMode??`strict`,disabled:b,onChange:e=>j(t=>({...t,...e}))})]})}),(0,J.jsx)(`div`,{className:`combos-workspace-tab-content`,role:`tabpanel`,id:xu(`about`),"aria-labelledby":bu(`about`),hidden:m!==`about`,tabIndex:0,children:(0,J.jsxs)(`section`,{className:`pwi-section`,children:[(0,J.jsx)(`h3`,{className:`pwi-section-title`,children:p(`cws.aboutTitle`)}),(0,J.jsx)(`p`,{className:`muted`,style:{margin:0,maxWidth:`70ch`,overflowWrap:`anywhere`},children:p(`cws.aboutBody`)})]})})]})}function Cu({model:e,onCancel:t,onConfirm:n}){let r=Q(),i=(0,_.useRef)(null);(0,_.useEffect)(()=>{let e=i.current;e&&!e.open&&e.showModal()},[]);let a=(0,_.useCallback)(e=>{e.preventDefault(),t()},[t]);return(0,J.jsxs)(`dialog`,{ref:i,className:`modal-overlay`,"aria-labelledby":`cwi-remove-title`,onCancel:a,children:[(0,J.jsx)(`button`,{type:`button`,className:`modal-backdrop-dismiss`,"aria-label":r(`common.close`),tabIndex:-1,onClick:t}),(0,J.jsxs)(`div`,{className:`modal-card pwi-remove-confirm-card`,onClick:e=>e.stopPropagation(),children:[(0,J.jsx)(`h3`,{id:`cwi-remove-title`,className:`pwi-remove-confirm-title`,children:r(`cws.removeConfirmTitle`,{model:e})}),(0,J.jsx)(`p`,{className:`muted pwi-remove-confirm-desc`,children:r(`cws.removeConfirmDesc`)}),(0,J.jsxs)(`div`,{className:`pwi-remove-confirm-actions`,children:[(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost`,onClick:t,children:r(`common.cancel`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn pwi-remove-confirm-danger`,onClick:n,children:r(`common.remove`)})]})]})]})}function wu({onKeep:e,onDiscard:t}){let n=Q(),r=(0,_.useRef)(null);(0,_.useEffect)(()=>{let e=r.current;e&&!e.open&&e.showModal()},[]);let i=(0,_.useCallback)(t=>{t.preventDefault(),e()},[e]);return(0,J.jsxs)(`dialog`,{ref:r,className:`modal-overlay`,"aria-labelledby":`cwi-unsaved-title`,onCancel:i,children:[(0,J.jsx)(`button`,{type:`button`,className:`modal-backdrop-dismiss`,"aria-label":n(`common.close`),tabIndex:-1,onClick:e}),(0,J.jsxs)(`div`,{className:`modal-card pwi-json-unsaved-card`,onClick:e=>e.stopPropagation(),children:[(0,J.jsx)(`h3`,{id:`cwi-unsaved-title`,className:`pwi-json-unsaved-title`,children:n(`cws.unsavedTitle`)}),(0,J.jsx)(`p`,{className:`muted pwi-json-unsaved-desc`,children:n(`cws.unsavedDesc`)}),(0,J.jsxs)(`div`,{className:`pwi-json-unsaved-actions`,children:[(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost`,"data-testid":`cwi-unsaved-keep`,onClick:e,children:n(`cws.keepEditing`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-danger`,"data-testid":`cwi-unsaved-discard`,onClick:t,children:n(`common.discard`)})]})]})]})}function Tu(e,t){return t(e===`empty-targets`?`cws.attention.empty`:e===`catalog-omitted`?`cws.attention.catalogOmitted`:e===`all-targets-exhausted`?`cws.attention.allTargetsExhausted`:`cws.attention.few`)}function Eu({combos:e,cataloguedComboIds:t,providerMap:n,providerQuotaStates:r,onSelect:i,onAdd:a}){let o=Q(),s=ql(e),c=iu(e,{cataloguedComboIds:t,providers:n,providerQuotaStates:r});return(0,J.jsxs)(`div`,{className:`combos-workspace-overview`,children:[(0,J.jsxs)(`div`,{className:`combos-workspace-overview-head`,children:[(0,J.jsx)(`h2`,{className:`combos-workspace-overview-title`,children:o(`cws.overviewTitle`)}),(0,J.jsxs)(`button`,{type:`button`,className:`btn btn-primary btn-sm`,onClick:a,children:[(0,J.jsx)(fe,{width:14,height:14}),` `,o(`cws.add`)]})]}),(0,J.jsx)(`p`,{className:`muted`,style:{marginTop:0,maxWidth:`62ch`},children:o(`cws.overviewBlurb`)}),(0,J.jsxs)(`div`,{className:`cwi-count-strip`,children:[(0,J.jsxs)(`div`,{className:`cwi-count-pill`,children:[(0,J.jsx)(`strong`,{children:e.length}),(0,J.jsx)(`span`,{children:o(`cws.count.total`)})]}),(0,J.jsxs)(`div`,{className:`cwi-count-pill`,children:[(0,J.jsx)(`strong`,{children:s.failover.length}),(0,J.jsx)(`span`,{children:o(`cws.count.failover`)})]}),(0,J.jsxs)(`div`,{className:`cwi-count-pill`,children:[(0,J.jsx)(`strong`,{children:s.roundRobin.length}),(0,J.jsx)(`span`,{children:o(`cws.count.roundRobin`)})]}),(0,J.jsxs)(`div`,{className:`cwi-count-pill`,children:[(0,J.jsx)(`strong`,{children:s.other.length}),(0,J.jsx)(`span`,{children:o(`cws.count.other`)})]})]}),(0,J.jsxs)(`section`,{className:`pwi-section`,"aria-label":o(`cws.howTitle`),children:[(0,J.jsx)(`h3`,{className:`pwi-section-title`,children:o(`cws.howTitle`)}),(0,J.jsx)(`p`,{className:`muted`,style:{margin:0},children:o(`cws.howBody`)})]}),c.length>0&&(0,J.jsxs)(`section`,{className:`pwi-section`,"aria-label":o(`cws.attentionTitle`),children:[(0,J.jsx)(`h3`,{className:`pwi-section-title`,children:o(`cws.attentionTitle`)}),(0,J.jsx)(`div`,{className:`cwi-attention-list`,children:c.map(e=>(0,J.jsxs)(`button`,{type:`button`,className:`cwi-attention-row`,onClick:()=>i(e.id),children:[(0,J.jsx)(_e,{width:14,height:14,"aria-hidden":`true`}),(0,J.jsx)(`code`,{className:`chip`,children:e.model}),(0,J.jsx)(`span`,{className:`muted`,children:Tu(e.reason,o)}),(0,J.jsx)(Se,{width:14,height:14,style:{marginLeft:`auto`},"aria-hidden":`true`})]},`${e.id}:${e.reason}`))})]})]})}function Du({combos:e,providerQuotaStates:t,providers:n,models:r,cataloguedComboIds:i,loading:a,onRefresh:o,onSave:s,onRemove:c,onAdd:l,adding:u,onCloseAdd:d,onCreated:f}){let p=Q(),m=(0,_.useMemo)(()=>Object.fromEntries(n.map(e=>[e.name,{disabled:e.disabled}])),[n]),[h,g]=(0,_.useState)(``),[v,y]=(0,_.useState)(null),[b,x]=(0,_.useState)(void 0),[S,C]=(0,_.useState)(null),[w,T]=(0,_.useState)(null),E=(0,_.useMemo)(()=>cu(),[]),D=(0,_.useMemo)(()=>Jl(e,h),[e,h]),O=(0,_.useMemo)(()=>ql(D),[D]),k=(0,_.useMemo)(()=>e.flatMap(e=>e.alias?[e.alias]:[]),[e]),A=v&&e.some(e=>e.id===v)?v:null,j=e.find(e=>e.id===A)??null,M=j&&w?.id===j.id?w:j,[N,P]=(0,_.useState)(!1),F=[],I=[];if(M)for(let t of e)t.id!==M.id&&(F.push(t.id),t.alias&&I.push(t.alias));let L=(0,_.useCallback)(e=>{if(e!==A){if(!N){y(e),T(null);return}x(e)}},[A,N]),R=()=>{b!==void 0&&(y(b),T(null),P(!1),x(void 0))},z=()=>x(void 0),B=b!==void 0&&N,V=!a&&e.length===0;return(0,J.jsxs)(`div`,{className:`combos-workspace-root`,children:[(0,J.jsxs)(`aside`,{className:`combos-workspace-rail`,"aria-label":p(`cws.railAria`),children:[(0,J.jsxs)(`div`,{className:`combos-workspace-rail-header`,children:[(0,J.jsxs)(`div`,{children:[(0,J.jsx)(`div`,{className:`combos-workspace-rail-title`,children:p(`nav.combos`)}),(0,J.jsx)(`div`,{className:`combos-workspace-rail-count`,children:e.length})]}),(0,J.jsxs)(`button`,{type:`button`,className:`btn btn-primary btn-sm`,onClick:()=>{if(V){document.getElementById(`cwi-edit-id`)?.focus();return}l()},"aria-label":p(`cws.add`),children:[(0,J.jsx)(fe,{width:14,height:14}),` `,p(`cws.add`)]})]}),e.length>0&&(0,J.jsx)(`div`,{className:`cwi-search-row`,children:(0,J.jsxs)(`div`,{className:`cwi-search-wrap`,children:[(0,J.jsx)(ve,{className:`cwi-search-icon`,"aria-hidden":`true`}),(0,J.jsx)(`input`,{className:`input cwi-search-input`,value:h,onChange:e=>g(e.target.value),placeholder:p(`cws.searchPlaceholder`),"aria-label":p(`cws.searchPlaceholder`)})]})}),(0,J.jsx)(`div`,{className:`combos-workspace-rail-list`,children:D.length===0&&e.length>0?(0,J.jsx)(`p`,{className:`muted`,style:{padding:`16px`},children:p(`cws.noSearchResults`)}):(0,J.jsx)(J.Fragment,{children:[[`failover`,O.failover,`cws.group.failover`],[`round-robin`,O.roundRobin,`cws.group.roundRobin`],[`other`,O.other,`cws.group.other`]].map(([e,t,n])=>t.length>0?(0,J.jsxs)(`div`,{className:`combos-workspace-rail-group`,children:[(0,J.jsxs)(`div`,{className:`combos-workspace-rail-group-head`,children:[(0,J.jsx)(`span`,{className:`pwi-dot`,"aria-hidden":`true`}),p(n),(0,J.jsx)(`span`,{className:`combos-workspace-rail-count`,children:t.length})]}),t.map(e=>(0,J.jsxs)(`button`,{type:`button`,className:`combos-workspace-rail-row${A===e.id?` combos-workspace-rail-row--selected`:``}`,onClick:()=>L(e.id),"aria-current":A===e.id?`true`:void 0,children:[(0,J.jsx)(`span`,{className:`combos-workspace-rail-icon`,"aria-hidden":`true`,children:(0,J.jsx)(Pe,{width:16,height:16})}),(0,J.jsx)(`span`,{className:`combos-workspace-rail-name`,children:e.model}),(0,J.jsx)(`span`,{className:`combos-workspace-rail-meta`,children:e.targets.length===1?p(`cws.targetCountOne`):p(`cws.targetCount`,{count:e.targets.length})}),(0,J.jsx)(Se,{className:`combos-workspace-rail-chevron`,"aria-hidden":`true`})]},e.id))]},e):null)})})]}),(0,J.jsx)(`div`,{className:`combos-workspace-main`,children:M?(0,J.jsx)(Su,{baseline:M,otherIds:F,otherAliases:I,providerMap:m,providerQuotaStates:t,providers:n,models:r,onBack:()=>L(null),onSaved:e=>{P(!1),e.id===M.id?T(e):(y(e.id),T(null)),o()},onRequestRemove:()=>C(M.id),onSave:s,onDirtyChange:P},M.id):V?(0,J.jsx)(Su,{baseline:E,isCreate:!0,otherIds:[],otherAliases:[],providerMap:m,providerQuotaStates:t,providers:n,models:r,onSaved:e=>{P(!1),y(e.id),T(e),f(e.id)},onSave:s,onDirtyChange:P},`first-combo`):(0,J.jsx)(Eu,{combos:e,cataloguedComboIds:i,providerMap:m,providerQuotaStates:t,onSelect:e=>L(e),onAdd:l})}),u&&!V&&(0,J.jsx)(vu,{existingIds:e.map(e=>e.id),existingAliases:k,providerMap:m,providerQuotaStates:t,providers:n,models:r,onClose:d,onSubmit:async e=>{let t=await s(e,!0);return t.ok&&(d(),f(e.id),y(e.id),T(null)),t}}),S&&(0,J.jsx)(Cu,{model:e.find(e=>e.id===S)?.model??Rl(S),onCancel:()=>C(null),onConfirm:()=>{(async()=>{let e=await c(S);C(null),e.ok&&(A===S&&(y(null),T(null)),o())})()}}),B&&(0,J.jsx)(wu,{onKeep:z,onDiscard:R})]})}function Ou(e){if(!e||typeof e!=`object`||Array.isArray(e))return;let t=e.error;return typeof t==`string`&&t.trim()?t:void 0}function ku(e){return!!e&&typeof e==`object`&&!Array.isArray(e)&&e.success===!0}function Au(e){return vr(e)?.data??null}function ju(e){return vr(e)?.cachedAt??null}function Mu({apiBase:e,active:t=!0,onCountChange:n}){let r=Q(),i=`ocx.combos.workspace.v1:${e}`,a=(0,_.useMemo)(()=>Au(i),[i]),[o,s]=(0,_.useState)(a??null),[c,l]=(0,_.useState)(``),[u,d]=(0,_.useState)(!1),[f,p]=(0,_.useState)(!1),m=(e,t)=>{l(e),d(t)};(0,_.useEffect)(()=>{if(!c||!u)return;let e=window.setTimeout(()=>{l(``),d(!1)},5e3);return()=>window.clearTimeout(e)},[c,u]);let h=(0,_.useCallback)(async t=>{let[n,r,a]=await Promise.all([fetch(`${e}/api/combos`,{signal:t}),fetch(`${e}/api/config`,{signal:t}),fetch(`${e}/api/models`,{signal:t})]);if(!n.ok||!r.ok||!a.ok)throw Error(`combo workspace load failed`);let o=await n.json(),c=await r.json(),l=await a.json(),u=Array.isArray(l)?l:Array.isArray(l?.models)?l.models:[],d=Kl(o),f=c.providers??{},p=mi(f),m=Object.entries(f).map(([e,t])=>({name:e,disabled:!!t.disabled,hiddenFromPicker:!Object.hasOwn(p,e),authMode:t.authMode,adapter:t.adapter,baseUrl:t.baseUrl})),h=[],g=new Set;for(let e of u){if(!e||typeof e!=`object`)continue;let t=e;if(typeof t.provider!=`string`||typeof t.id!=`string`)continue;let n=t.provider.trim(),r=t.id.trim();if(!n||!r)continue;if(n===`combo`){g.add(r);continue}if(t.disabled===!0)continue;let i=Array.isArray(t.reasoningEfforts)?t.reasoningEfforts.filter(e=>typeof e==`string`):void 0,a=Array.isArray(t.inputModalities)?t.inputModalities.filter(e=>typeof e==`string`).map(e=>e.trim()).filter(Boolean):void 0;h.push({provider:n,id:r,namespaced:typeof t.namespaced==`string`?t.namespaced:void 0,...i?{reasoningEfforts:i}:{},...a&&a.length>0?{inputModalities:a}:{}})}for(let[e,t]of Object.entries(f)){let n=typeof t.defaultModel==`string`?t.defaultModel.trim():``;!n||t.disabled||h.some(t=>t.provider===e&&t.id===n)||h.push({provider:e,id:n,namespaced:`${e}/${n}`})}let _={combos:d,providers:m,models:h,cataloguedComboIds:[...g]};return yr(i,_),s(_),_},[e,i]),g=ml(i,[e],h,{isEmpty:()=>!1,initialData:a??void 0,initialDataCachedAt:ju(i),staleAfterMs:6e4,enabled:t}),{state:v}=g,y=(0,_.useCallback)(async t=>{let n=await fetch(`${e}/api/provider-quotas`,{signal:t});if(!n.ok)throw Error(`combo quota load failed`);let r=await n.json();return r&&typeof r==`object`&&!Array.isArray(r)?r:{}},[e]),b=ml(`ocx.combos.provider-quotas.v1:${e}`,[e],y,{isEmpty:()=>!1,pollMs:6e4,pauseWhenHidden:!0,enabled:t}),x=(0,_.useMemo)(()=>b.lastAttemptOk?nu(b.data?.reports):{},[b.data,b.lastAttemptOk]),S=v.data??o??void 0,C=S?.combos??[];(0,_.useEffect)(()=>{S&&n?.(C.length)},[C.length,S,n]);let w=S?.providers??[],T=S?.models??[],E=new Set(S?.cataloguedComboIds??[]),D=async(t,n,i)=>{try{let a=await fetch(`${e}/api/combos`,{method:`PUT`,headers:{"content-type":`application/json`},body:JSON.stringify(ou(t,i?{renameFrom:i}:{}))}),o=a.ok?await a.json():await a.json().catch(()=>null),s=Ou(o);if(!a.ok||s||!ku(o)){let e=s||r(`cws.saveFailed`);return m(e,!1),{ok:!1,error:e}}return g.refresh(),m(i?r(`cws.renamed`,{from:Rl(i),to:t.model}):n?r(`cws.created`,{model:t.model}):r(`cws.saved`),!0),{ok:!0}}catch{let e=r(`cws.saveFailed`);return m(e,!1),{ok:!1,error:e}}},O=async t=>{try{let n=await fetch(`${e}/api/combos?id=${encodeURIComponent(t)}`,{method:`DELETE`}),i=n.ok?await n.json():await n.json().catch(()=>null),a=Ou(i);if(!n.ok||a||!ku(i)){let e=a||r(`cws.removeFailed`);return m(e,!1),{ok:!1,error:e}}return g.refresh(),m(r(`cws.removed`,{id:t}),!0),{ok:!0}}catch{let e=r(`cws.removeFailed`);return m(e,!1),{ok:!1,error:e}}};if(v.showSkeleton&&!S)return(0,J.jsx)(gl,{label:r(`cws.loading`),rows:5});if(v.kind===`failed-cold`&&!S){let e=v.error instanceof Error?v.error.message:r(`cws.loadFailed`);return(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)($,{tone:`err`,children:e}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:()=>g.refresh(),children:r(`common.retry`)})]})}return(0,J.jsxs)(`div`,{className:`combos-workspace-shell`,children:[c&&(0,J.jsx)(`div`,{className:`combos-workspace-shell-banner`,children:(0,J.jsx)($,{tone:u?`ok`:`err`,children:c})}),v.showError&&(0,J.jsx)(`div`,{className:`combos-workspace-shell-banner`,children:(0,J.jsx)($,{tone:`err`,children:r(`cws.loadFailed`)})}),(0,J.jsxs)(`div`,{className:`combos-workspace-shell-body`,"aria-busy":v.refreshing,children:[(0,J.jsx)(`span`,{className:`sr-only`,role:`status`,"aria-live":`polite`,"aria-atomic":`true`,children:v.refreshing?r(`common.loading`):``}),(0,J.jsx)(Du,{combos:C,providerQuotaStates:x,providers:w,models:T,cataloguedComboIds:E,loading:!1,onRefresh:()=>g.refresh(),onSave:D,onRemove:O,onAdd:()=>p(!0),adding:f,onCloseAdd:()=>p(!1),onCreated:()=>g.refresh()})]})]})}var Nu=0;function Pu(){return Nu+=1,`candidate-${Nu}`}function Fu(e,t){return{provider:e,model:t,key:Pu()}}function Iu(e){if(!Array.isArray(e))return[];let t=[];for(let n of e){if(!n||typeof n!=`object`||Array.isArray(n))continue;let e=typeof n.suiteId==`string`?n.suiteId.trim():``,r=n.evidenceLayer;!e||r!==`protocol_conformance`&&r!==`live_route_compatibility`||t.push({suiteId:e,evidenceLayer:r})}return t}function Lu(e){if(typeof e!=`object`||!e||Array.isArray(e))return;let t=e,n={requiredSuites:Iu(t.requiredSuites)};return(t.minStatus===`PROBED`||t.minStatus===`VERIFIED`)&&(n.minStatus=t.minStatus),typeof t.maxEvidenceAgeMs==`number`&&Number.isFinite(t.maxEvidenceAgeMs)&&t.maxEvidenceAgeMs>=0&&(n.maxEvidenceAgeMs=t.maxEvidenceAgeMs),(t.unknownEvidence===`allow`||t.unknownEvidence===`penalize`||t.unknownEvidence===`exclude`)&&(n.unknownEvidence=t.unknownEvidence),(t.degradedEvidence===`allow`||t.degradedEvidence===`penalize`||t.degradedEvidence===`exclude`)&&(n.degradedEvidence=t.degradedEvidence),n}var Ru={latency:`0.55`,health:`0.25`,cost:`0.1`,quota:`0.1`},zu={capability:`exclude`,health:`penalize`,quota:`penalize`,cost:`penalize`};function Bu(e){return e===!0?`true`:e===!1?`false`:``}function Vu(e){return e===void 0?``:String(e)}function Hu(e=``,t=``){return{id:``,alias:``,candidates:[Fu(e,t)],require:{minContextWindow:``,minQuotaHeadroom:``,tools:``,imageInput:``,structuredOutput:``,reasoningEffort:``,serviceTier:``,localOnly:``,remoteAllowed:``,encryptedCodexTasks:``},optimize:{...Ru},limits:{maxEstimatedCostUsd:``,onUnknownCost:`allow`},unknownEvidence:{...zu},compatibility:{enabled:!1,requiredSuites:[],minStatus:``,maxEvidenceAgeMs:``,unknownEvidence:`exclude`,degradedEvidence:`penalize`}}}function Uu(e){let t=Lu(e.compatibility);return{id:e.id,alias:e.alias??``,candidates:e.candidates.map(e=>({...e,key:Pu()})),require:{minContextWindow:Vu(e.require.minContextWindow),minQuotaHeadroom:Vu(e.require.minQuotaHeadroom),tools:Bu(e.require.tools),imageInput:Bu(e.require.imageInput),structuredOutput:Bu(e.require.structuredOutput),reasoningEffort:e.require.reasoningEffort??``,serviceTier:e.require.serviceTier??``,localOnly:Bu(e.require.localOnly),remoteAllowed:Bu(e.require.remoteAllowed),encryptedCodexTasks:Bu(e.require.encryptedCodexTasks)},optimize:{latency:String(e.optimize.latency),health:String(e.optimize.health),cost:String(e.optimize.cost),quota:String(e.optimize.quota)},limits:{maxEstimatedCostUsd:Vu(e.limits.maxEstimatedCostUsd),onUnknownCost:e.limits.onUnknownCost===`exclude`?`exclude`:`allow`},unknownEvidence:{...e.unknownEvidence},compatibility:{enabled:!!t,requiredSuites:t?.requiredSuites??[],minStatus:t?.minStatus??``,maxEvidenceAgeMs:Vu(t?.maxEvidenceAgeMs),unknownEvidence:t?.unknownEvidence??`exclude`,degradedEvidence:t?.degradedEvidence??`penalize`}}}function Wu(e){let t=e.trim();return t?Number(t):void 0}function Gu(e){if(e===`true`)return!0;if(e===`false`)return!1}function Ku(e){return Object.fromEntries(Object.entries(e).filter(([,e])=>e!==void 0))}function qu(e,t,n){let r=Ku({minContextWindow:Wu(e.require.minContextWindow),minQuotaHeadroom:Wu(e.require.minQuotaHeadroom),tools:Gu(e.require.tools),imageInput:Gu(e.require.imageInput),structuredOutput:Gu(e.require.structuredOutput),reasoningEffort:e.require.reasoningEffort.trim()||void 0,serviceTier:e.require.serviceTier.trim()||void 0,localOnly:Gu(e.require.localOnly),remoteAllowed:Gu(e.require.remoteAllowed),encryptedCodexTasks:Gu(e.require.encryptedCodexTasks)}),i=Wu(e.limits.maxEstimatedCostUsd),a=e.compatibility.enabled?Ku({requiredSuites:e.compatibility.requiredSuites,minStatus:e.compatibility.minStatus||void 0,maxEvidenceAgeMs:Wu(e.compatibility.maxEvidenceAgeMs),unknownEvidence:e.compatibility.unknownEvidence,degradedEvidence:e.compatibility.degradedEvidence}):void 0,o=Ku({maxEstimatedCostUsd:i,onUnknownCost:e.limits.onUnknownCost===`exclude`?`exclude`:void 0});return{mode:t,id:e.id.trim(),...t===`update`&&n?{expectedRevision:n}:{},profile:{...e.alias.trim()?{alias:e.alias.trim()}:{},candidates:e.candidates.map(e=>({provider:e.provider.trim(),model:e.model.trim()})),...Object.keys(r).length>0?{require:r}:{},optimize:{latency:Number(e.optimize.latency),health:Number(e.optimize.health),cost:Number(e.optimize.cost),quota:Number(e.optimize.quota)},...Object.keys(o).length>0?{limits:o}:{},unknownEvidence:{...e.unknownEvidence},...a&&Object.keys(a).length>0?{compatibility:a}:{}}}}function Ju(e){if(!e||typeof e!=`object`||Array.isArray(e))return;let t=e.error;if(typeof t==`string`&&t.trim())return t;if(t&&typeof t==`object`&&!Array.isArray(t)){let e=t.message;if(typeof e==`string`&&e.trim())return e}}function Yu(e){return!!e&&typeof e==`object`&&!Array.isArray(e)&&e.success===!0}function Xu(e,t){return e.filter(e=>e.provider===t)}var Zu={en:{maxEvidenceAgeMs:`Maximum evidence age (ms)`,unknownEvidence:`Unknown evidence`,degradedEvidence:`Degraded evidence`},de:{maxEvidenceAgeMs:`Maximales Evidenzalter (ms)`,unknownEvidence:`Unbekannte Evidenz`,degradedEvidence:`Eingeschränkte Evidenz`},fr:{maxEvidenceAgeMs:`Âge maximal des preuves (ms)`,unknownEvidence:`Preuves inconnues`,degradedEvidence:`Preuves dégradées`},ko:{maxEvidenceAgeMs:`최대 증거 유효 기간 (ms)`,unknownEvidence:`알 수 없는 증거`,degradedEvidence:`저하된 증거`},zh:{maxEvidenceAgeMs:`证据最大有效期(毫秒)`,unknownEvidence:`未知证据`,degradedEvidence:`降级证据`},"zh-TW":{maxEvidenceAgeMs:`證據最大有效期限(毫秒)`,unknownEvidence:`未知證據`,degradedEvidence:`降級證據`},ru:{maxEvidenceAgeMs:`Максимальный возраст доказательств (мс)`,unknownEvidence:`Неизвестные доказательства`,degradedEvidence:`Ухудшенные доказательства`},ja:{maxEvidenceAgeMs:`エビデンスの最大有効期間 (ms)`,unknownEvidence:`不明なエビデンス`,degradedEvidence:`低下したエビデンス`},tr:{maxEvidenceAgeMs:`Maksimum kanıt yaşı (ms)`,unknownEvidence:`Bilinmeyen kanıt`,degradedEvidence:`Bozulmuş kanıt`}},Qu=[`tools`,`imageInput`,`structuredOutput`,`localOnly`,`remoteAllowed`,`encryptedCodexTasks`],$u=[`reasoningEffort`,`serviceTier`],ed={minContextWindow:{min:1,max:void 0,step:1},minQuotaHeadroom:{min:0,max:1,step:`any`}},td=Object.keys(ed),nd=[`latency`,`health`,`cost`,`quota`],rd=[`capability`,`health`,`quota`,`cost`],id=[`allow`,`penalize`,`exclude`],ad=[`allow`,`exclude`];function od(e){return`${e.evidenceLayer}:${e.suiteId}`}function sd(e){return!!e&&typeof e==`object`&&!Array.isArray(e)}function cd(e){let t=new Set,n=[];for(let r of e){if(!sd(r))continue;let e=typeof r.suiteId==`string`?r.suiteId.trim():``,i=r.evidenceLayer;if(!e||i!==`protocol_conformance`&&i!==`live_route_compatibility`)continue;let a={suiteId:e,evidenceLayer:i},o=od(a);t.has(o)||(t.add(o),n.push({...a,key:o}))}return n.sort((e,t)=>{let n=e.evidenceLayer.localeCompare(t.evidenceLayer);return n===0?e.suiteId.localeCompare(t.suiteId):n})}function ld(e,t){return e.some(e=>e.suiteId===t.suiteId&&e.evidenceLayer===t.evidenceLayer)}function ud(e,t){return e===void 0?t:`${Math.round(e)}ms`}function dd(e,t){return e==null?t:`${Math.round(e*100)}%`}function fd(e,t,n){switch(e){case`satisfied`:return t(`routing.capOutcome.satisfied`);case`exceeded`:return t(`routing.capOutcome.exceeded`);case`unknown-allowed`:return t(`routing.capOutcome.unknown-allowed`);case`unknown-excluded`:return t(`routing.capOutcome.unknown-excluded`);default:return n}}function pd(e,t){switch(e){case`capability-unsatisfied`:return t(`routing.exclusion.capability-unsatisfied`);case`unknown-capability`:return t(`routing.exclusion.unknown-capability`);case`cost-limit`:return t(`routing.exclusion.cost-limit`);case`cost-limit-unknown`:return t(`routing.exclusion.cost-limit-unknown`);case`cooldown`:return t(`routing.exclusion.cooldown`);case`unknown-health`:return t(`routing.exclusion.unknown-health`);case`unknown-quota`:return t(`routing.exclusion.unknown-quota`);case`unknown-price`:return t(`routing.exclusion.unknown-price`);default:return t(`routing.exclusion.other`,{code:e})}}function md(e){if(!e||typeof e!=`object`||Array.isArray(e))return[];let t=e.profiles;return Array.isArray(t)?t.filter(e=>sd(e)?typeof e.id==`string`&&typeof e.model==`string`&&typeof e.revision==`string`&&Array.isArray(e.candidates)&&sd(e.require)&&sd(e.optimize)&&sd(e.limits)&&sd(e.unknownEvidence):!1).map(e=>{let t=Lu(`compatibility`in e?e.compatibility:void 0),n={...e};return delete n.compatibility,{...n,alias:e.alias??null,...t?{compatibility:t}:{}}}):[]}function hd(e){let t=Array.isArray(e)?e:e&&typeof e==`object`&&Array.isArray(e.models)?e.models:[],n=new Set,r=[];for(let e of t){if(!e||typeof e!=`object`||Array.isArray(e))continue;let t=typeof e.provider==`string`?e.provider.trim():``,i=typeof e.id==`string`?e.id.trim():``;if(!t||!i||t===`combo`||t===`policy`||e.disabled===!0)continue;let a=JSON.stringify([t,i]);n.has(a)||(n.add(a),r.push({provider:t,id:i}))}return r}function gd(e,t,n){let r=n??t;if(r){let t=e.find(e=>e.id===r);if(t)return t}return e[0]??null}function _d({apiBase:e,active:t=!0,onCountChange:n}){let{locale:r,t:i}=ct(),a=Zu[r],o=i(`routing.unavailable`),[s,c]=(0,_.useState)([]),[l,u]=(0,_.useState)(null),[d,f]=(0,_.useState)([]),[p,m]=(0,_.useState)({}),[h,g]=(0,_.useState)([]),[v,y]=(0,_.useState)(``),[b,x]=(0,_.useState)(null),[S,C]=(0,_.useState)(null),[w,T]=(0,_.useState)(null),[E,D]=(0,_.useState)(!1),[O,k]=(0,_.useState)(``),[A,j]=(0,_.useState)(!1),[M,N]=(0,_.useState)(!1),[P,F]=(0,_.useState)(!1),[I,L]=(0,_.useState)(null),[R,z]=(0,_.useState)(``),[B,V]=(0,_.useState)(!1),[H,U]=(0,_.useState)([]),[W,ee]=(0,_.useState)(!1),G=(0,_.useRef)(null),K=(0,_.useRef)(0),q=(0,_.useRef)(null),Y=(0,_.useRef)(!0),te=(0,_.useCallback)(()=>{Y.current=!1,q.current?.abort(),K.current++},[]),ne=(0,_.useRef)(0),re=(0,_.useCallback)((e,t)=>{T({message:e,ok:t})},[]);(0,_.useEffect)(()=>{if(!w?.ok)return;let e=window.setTimeout(()=>T(null),5e3);return()=>window.clearTimeout(e)},[w]);let ie=(0,_.useCallback)(()=>{ne.current+=1,L(null),z(``),V(!1)},[]),ae=(0,_.useCallback)(e=>{G.current=e,x(e),C(e?Uu(e):null),T(null),ie()},[ie]),oe=(0,_.useCallback)(async t=>{if(!Y.current)return;q.current?.abort();let n=new AbortController;q.current=n;let{signal:r}=n,i=++K.current;y(``);try{let[n,a,o,s,l]=await Promise.all([fetch(`${e}/api/routing-profiles`,{signal:r}),fetch(`${e}/api/routing-analytics`,{signal:r}),fetch(`${e}/api/config`,{signal:r}),fetch(`${e}/api/models`,{signal:r}),fetch(`${e}/api/lab/catalog`,{signal:r})]);if(!n.ok)throw Error(`load-${n.status}`);let[d,p,h,_,v]=await Promise.all([n.json(),a.ok?a.json():Promise.resolve(null),o.ok?o.json():Promise.resolve({}),s.ok?s.json():Promise.resolve([]),l.ok?l.json().catch(()=>null):Promise.resolve(null)]);if(i!==K.current)return;let y=md(d),b=G.current,S=gd(y,b?.id??null,t),w=h.providers??{},T=Object.entries(w).filter(([,e])=>e.disabled!==!0).map(([e])=>e).sort((e,t)=>et)),E=Object.fromEntries(Object.entries(w).filter(([,e])=>e.disabled!==!0&&typeof e.defaultModel==`string`).map(([e,t])=>[e,t.defaultModel.trim()]));G.current=S,c(y),x(S),C(S?Uu(S):null),u(p),f(T),m(E),g(hd(_)),v&&Array.isArray(v.scenarios)?(U(cd(v.scenarios)),ee(!1)):(U([]),ee(!0)),(!b||!S||b.id!==S.id||b.revision!==S.revision)&&ie()}catch(e){if(i!==K.current||r.aborted)return;y(e instanceof Error?e.message:String(e))}finally{q.current===n&&(q.current=null)}},[e,ie]);(0,_.useEffect)(()=>{if(!t){te();return}Y.current=!0;let e=window.setTimeout(()=>void oe(),0);return()=>{window.clearTimeout(e),te()}},[t,te,oe]),(0,_.useEffect)(()=>{n?.(s.length)},[n,s.length]);let se=d[0]??``,ce=p[se]??Xu(h,se)[0]?.id??``,le=()=>{G.current=null,x(null),C(Hu(se,ce)),T(null),ie()},ue=()=>{if(b){C(Uu(b)),T(null);return}ae(s[0]??null)},de=async()=>{if(!(!S||E)){D(!0),T(null);try{let t=qu(S,b?`update`:`create`,b?.revision),n=await fetch(`${e}/api/routing-profiles`,{method:`PUT`,headers:{"content-type":`application/json`},body:JSON.stringify(t)}),r=await Ft(n);if(!n.ok){let e=await n.json().catch(()=>null);re(Ju(e)??i(`routing.loadFailed`),!1);return}if(!Yu(r)){re(Ju(r)??i(`routing.loadFailed`),!1);return}await oe(t.id),re(i(`common.ok`),!0)}catch(e){re(e instanceof Error?e.message:i(`routing.loadFailed`),!1)}finally{D(!1)}}},fe=async()=>{if(!(!b||E)&&window.confirm(i(`routing.removeConfirm`,{id:b.id}))){D(!0),T(null);try{let t=await fetch(`${e}/api/routing-profiles?id=${encodeURIComponent(b.id)}`,{method:`DELETE`}),n=await Ft(t);if(!t.ok){let e=await t.json().catch(()=>null);re(Ju(e)??i(`routing.loadFailed`),!1);return}if(!Yu(n)){re(Ju(n)??i(`routing.loadFailed`),!1);return}G.current=null,await oe(),re(i(`common.ok`),!0)}catch(e){re(e instanceof Error?e.message:i(`routing.loadFailed`),!1)}finally{D(!1)}}},pe=(e,t,n)=>{C(r=>{if(!r)return r;let i=r.candidates.map((r,i)=>i===e?t===`provider`?{...r,provider:n,model:p[n]??Xu(h,n)[0]?.id??``}:{...r,model:n}:r);return{...r,candidates:i}})},X=()=>{C(e=>e&&{...e,candidates:[...e.candidates,Fu(se,ce)]})},me=e=>{C(t=>t&&{...t,candidates:t.candidates.filter((t,n)=>n!==e)})},he=async()=>{if(!b)return;let t=++ne.current;V(!0),L(null),z(``);try{let n={},r=O.trim()?Number(O.trim()):NaN;Number.isFinite(r)&&r>0&&(n.contextWindow=r),A&&(n.toolsRequired=!0),M&&(n.imageInputRequired=!0),P&&(n.structuredOutputRequired=!0);let a=await fetch(`${e}/api/routing-profiles/dry-run`,{method:`POST`,headers:{"content-type":`application/json`},body:JSON.stringify({profile:b.id,evidence:n})});if(t!==ne.current)return;if(!a.ok){let e=await a.json().catch(()=>null);if(t!==ne.current)return;z(Ju(e)??i(`routing.dryRunError`,{status:a.status}));return}let o=await a.json();if(t!==ne.current)return;L(o)}catch(e){if(t!==ne.current)return;z(e instanceof Error?e.message:String(e))}finally{t===ne.current&&V(!1)}},ge=S?.candidates.map(e=>Xu(h,e.provider))??[];return(0,J.jsxs)(`div`,{className:`page`,"data-page":`routing`,children:[(0,J.jsxs)(`div`,{className:`row`,style:{display:`flex`,gap:8,justifyContent:`flex-end`,marginBottom:12},children:[(0,J.jsxs)(`button`,{type:`button`,className:`btn btn-primary btn-sm`,onClick:le,children:[(0,J.jsx)(`span`,{"aria-hidden":`true`,children:`+`}),` `,i(`routing.createProfile`)]}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:()=>void oe(),children:i(`common.retry`)})]}),v?(0,J.jsxs)($,{tone:`err`,children:[i(`routing.loadFailed`),`: `,v]}):null,w?(0,J.jsx)($,{tone:w.ok?`ok`:`err`,children:w.message}):null,s.length>0?(0,J.jsx)(`div`,{className:`panel`,style:{display:`flex`,flexDirection:`column`,gap:12},children:s.map(e=>(0,J.jsx)(`button`,{type:`button`,className:`model-card`,style:{textAlign:`left`,cursor:`pointer`},onClick:()=>ae(e),"aria-pressed":b?.id===e.id,children:(0,J.jsxs)(`div`,{className:`card-badges`,children:[(0,J.jsx)(`strong`,{children:e.id}),(0,J.jsx)(`span`,{className:`badge badge-muted`,children:e.model}),(0,J.jsxs)(`span`,{className:`badge badge-muted`,children:[i(`routing.revision`),`: `,e.revision]})]})},e.id))}):null,S?(0,J.jsxs)(`form`,{className:`panel`,style:{marginTop:14,display:`flex`,flexDirection:`column`,gap:16},onSubmit:e=>{e.preventDefault(),de()},children:[(0,J.jsxs)(`div`,{className:`page-head`,children:[(0,J.jsxs)(`h3`,{children:[i(`routing.detail`),`: `,b?.model??(0,J.jsx)(`code`,{children:`policy/…`})]}),b?(0,J.jsxs)(`span`,{className:`badge badge-muted`,children:[i(`routing.revision`),`: `,b.revision]}):null]}),(0,J.jsxs)(`div`,{className:`model-grid`,children:[(0,J.jsxs)(`label`,{className:`field-label`,children:[(0,J.jsx)(`code`,{children:`id`}),(0,J.jsx)(`input`,{className:`input`,required:!0,disabled:b!==null,value:S.id,onChange:e=>C(t=>t&&{...t,id:e.target.value})})]}),(0,J.jsxs)(`label`,{className:`field-label`,children:[(0,J.jsx)(`code`,{children:`alias`}),(0,J.jsx)(`input`,{className:`input`,value:S.alias,onChange:e=>C(t=>t&&{...t,alias:e.target.value})})]})]}),(0,J.jsxs)(`fieldset`,{style:{border:0,padding:0,margin:0},children:[(0,J.jsx)(`legend`,{className:`field-label`,children:i(`routing.candidates`)}),(0,J.jsxs)(`div`,{style:{display:`flex`,flexDirection:`column`,gap:10},children:[S.candidates.map((e,t)=>{let n=[...new Set([e.provider,...d])].filter(Boolean),r=`routing-model-options-${t}`;return(0,J.jsxs)(`div`,{className:`model-card`,children:[(0,J.jsxs)(`div`,{className:`model-grid`,children:[(0,J.jsxs)(`label`,{className:`field-label`,children:[(0,J.jsx)(`code`,{children:`provider`}),(0,J.jsxs)(`select`,{className:`input`,required:!0,value:e.provider,onChange:e=>pe(t,`provider`,e.target.value),children:[(0,J.jsx)(`option`,{value:``,disabled:!0,children:i(`routing.none`)}),n.map(e=>(0,J.jsx)(`option`,{value:e,children:e},e))]})]}),(0,J.jsxs)(`label`,{className:`field-label`,children:[(0,J.jsx)(`code`,{children:`model`}),(0,J.jsx)(`input`,{className:`input`,required:!0,list:r,value:e.model,onChange:e=>pe(t,`model`,e.target.value)}),(0,J.jsx)(`datalist`,{id:r,children:ge[t]?.map(e=>(0,J.jsx)(`option`,{value:e.id},e.id))})]})]}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,disabled:S.candidates.length===1,onClick:()=>me(t),"aria-label":i(`routing.removeCandidate`,{provider:e.provider,model:e.model}),children:i(`common.remove`)})]},e.key)}),(0,J.jsxs)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:X,children:[(0,J.jsx)(`span`,{"aria-hidden":`true`,children:`+`}),` `,i(`routing.candidate`)]})]})]}),(0,J.jsxs)(`fieldset`,{style:{border:0,padding:0,margin:0},children:[(0,J.jsx)(`legend`,{className:`field-label`,children:i(`routing.require`)}),(0,J.jsxs)(`div`,{className:`model-grid`,children:[td.map(e=>(0,J.jsxs)(`label`,{className:`field-label`,children:[(0,J.jsx)(`code`,{children:e}),(0,J.jsx)(`input`,{className:`input`,type:`number`,min:ed[e].min,max:ed[e].max,step:ed[e].step,value:S.require[e],onChange:t=>C(n=>n&&{...n,require:{...n.require,[e]:t.target.value}})})]},e)),$u.map(e=>(0,J.jsxs)(`label`,{className:`field-label`,children:[(0,J.jsx)(`code`,{children:e}),(0,J.jsx)(`input`,{className:`input`,value:S.require[e],onChange:t=>C(n=>n&&{...n,require:{...n.require,[e]:t.target.value}})})]},e)),Qu.map(e=>(0,J.jsxs)(`label`,{className:`field-label`,children:[(0,J.jsx)(`code`,{children:e}),(0,J.jsxs)(`select`,{className:`input`,value:S.require[e],onChange:t=>C(n=>n&&{...n,require:{...n.require,[e]:t.target.value}}),children:[(0,J.jsx)(`option`,{value:``,children:i(`routing.none`)}),(0,J.jsx)(`option`,{value:`true`,children:i(`routing.yes`)}),(0,J.jsx)(`option`,{value:`false`,children:i(`routing.no`)})]})]},e))]})]}),(0,J.jsxs)(`fieldset`,{style:{border:0,padding:0,margin:0},children:[(0,J.jsx)(`legend`,{className:`field-label`,children:i(`routing.optimize`)}),(0,J.jsx)(`div`,{className:`model-grid`,children:nd.map(e=>(0,J.jsxs)(`label`,{className:`field-label`,children:[(0,J.jsx)(`code`,{children:e}),(0,J.jsx)(`input`,{className:`input`,type:`number`,min:0,step:`any`,required:!0,value:S.optimize[e],onChange:t=>C(n=>n&&{...n,optimize:{...n.optimize,[e]:t.target.value}})})]},e))})]}),(0,J.jsxs)(`fieldset`,{style:{border:0,padding:0,margin:0},children:[(0,J.jsx)(`legend`,{className:`field-label`,children:i(`routing.limits`)}),(0,J.jsxs)(`label`,{className:`field-label`,children:[(0,J.jsx)(`code`,{children:`maxEstimatedCostUsd`}),(0,J.jsx)(`input`,{className:`input`,type:`number`,min:0,step:`any`,value:S.limits.maxEstimatedCostUsd,onChange:e=>C(t=>t&&{...t,limits:{...t.limits,maxEstimatedCostUsd:e.target.value}})})]}),(0,J.jsxs)(`label`,{className:`field-label`,children:[(0,J.jsx)(`code`,{children:`onUnknownCost`}),(0,J.jsx)(`select`,{className:`input`,value:S.limits.onUnknownCost,onChange:e=>C(t=>t&&{...t,limits:{...t.limits,onUnknownCost:e.target.value}}),children:ad.map(e=>(0,J.jsx)(`option`,{value:e,children:i(`routing.unknownEvidence.${e}`)},e))})]})]}),(0,J.jsxs)(`fieldset`,{style:{border:0,padding:0,margin:0},children:[(0,J.jsx)(`legend`,{className:`field-label`,children:i(`routing.unknownEvidence`)}),(0,J.jsx)(`div`,{className:`model-grid`,children:rd.map(e=>(0,J.jsxs)(`label`,{className:`field-label`,children:[(0,J.jsx)(`code`,{children:e}),(0,J.jsx)(`select`,{className:`input`,value:S.unknownEvidence[e],onChange:t=>C(n=>n&&{...n,unknownEvidence:{...n.unknownEvidence,[e]:t.target.value}}),children:id.map(e=>(0,J.jsx)(`option`,{value:e,children:i(`routing.unknownEvidence.${e}`)},e))})]},e))})]}),(0,J.jsxs)(`fieldset`,{style:{border:0,padding:0,margin:0},children:[(0,J.jsx)(`legend`,{className:`field-label`,children:i(`routing.compatibility.title`)}),(0,J.jsxs)(`label`,{className:`checkbox`,children:[(0,J.jsx)(`input`,{type:`checkbox`,checked:S.compatibility.enabled,onChange:e=>C(t=>t&&{...t,compatibility:{...t.compatibility,enabled:e.target.checked}})}),i(`routing.compatibility.enabled`)]}),S.compatibility.enabled?(0,J.jsxs)(`div`,{className:`model-grid`,style:{marginTop:10},children:[(0,J.jsxs)(`div`,{className:`field-label`,style:{gridColumn:`1 / -1`},children:[i(`routing.compatibility.requiredSuites`),W?(0,J.jsx)(`div`,{style:{marginTop:6},children:(0,J.jsx)($,{tone:`warn`,children:i(`routing.compatibility.catalogUnavailable`)})}):null,H.length>0?(0,J.jsx)(`div`,{style:{display:`flex`,flexDirection:`column`,gap:4,marginTop:6},children:H.map(e=>(0,J.jsxs)(`label`,{className:`checkbox`,children:[(0,J.jsx)(`input`,{type:`checkbox`,checked:ld(S.compatibility.requiredSuites,e),onChange:t=>C(n=>{if(!n)return n;let r=t.target.checked?[...n.compatibility.requiredSuites,{suiteId:e.suiteId,evidenceLayer:e.evidenceLayer}]:n.compatibility.requiredSuites.filter(t=>t.suiteId!==e.suiteId||t.evidenceLayer!==e.evidenceLayer);return{...n,compatibility:{...n.compatibility,requiredSuites:r}}})}),(0,J.jsxs)(`span`,{children:[e.suiteId,` `,(0,J.jsxs)(`span`,{className:`muted`,children:[`(`,i(`routing.compatibility.layer.${e.evidenceLayer}`),`)`]})]})]},e.key))}):null]}),(0,J.jsxs)(`label`,{className:`field-label`,children:[i(`routing.compatibility.minStatus`),(0,J.jsxs)(`select`,{className:`input`,value:S.compatibility.minStatus,onChange:e=>C(t=>t&&{...t,compatibility:{...t.compatibility,minStatus:e.target.value}}),children:[(0,J.jsx)(`option`,{value:``,children:i(`routing.none`)}),(0,J.jsx)(`option`,{value:`PROBED`,children:i(`lab.verdict.PROBED`)}),(0,J.jsx)(`option`,{value:`VERIFIED`,children:i(`lab.verdict.VERIFIED`)})]})]}),(0,J.jsxs)(`label`,{className:`field-label`,children:[a.maxEvidenceAgeMs,(0,J.jsx)(`input`,{className:`input`,type:`number`,min:0,value:S.compatibility.maxEvidenceAgeMs,onChange:e=>C(t=>t&&{...t,compatibility:{...t.compatibility,maxEvidenceAgeMs:e.target.value}})})]}),(0,J.jsxs)(`label`,{className:`field-label`,children:[a.unknownEvidence,(0,J.jsx)(`select`,{className:`input`,value:S.compatibility.unknownEvidence,onChange:e=>C(t=>t&&{...t,compatibility:{...t.compatibility,unknownEvidence:e.target.value}}),children:id.map(e=>(0,J.jsx)(`option`,{value:e,children:i(`routing.unknownEvidence.${e}`)},e))})]}),(0,J.jsxs)(`label`,{className:`field-label`,children:[a.degradedEvidence,(0,J.jsx)(`select`,{className:`input`,value:S.compatibility.degradedEvidence,onChange:e=>C(t=>t&&{...t,compatibility:{...t.compatibility,degradedEvidence:e.target.value}}),children:id.map(e=>(0,J.jsx)(`option`,{value:e,children:i(`routing.unknownEvidence.${e}`)},e))})]})]}):null]}),(0,J.jsxs)(`div`,{style:{display:`flex`,gap:8,flexWrap:`wrap`},children:[(0,J.jsx)(`button`,{type:`submit`,className:`btn btn-primary`,disabled:E,children:i(E?`common.saving`:`common.save`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost`,disabled:E,onClick:ue,children:i(`common.cancel`)}),b?(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost`,disabled:E,onClick:()=>void fe(),children:i(`common.remove`)}):null]})]}):null,b&&(0,J.jsxs)(`div`,{className:`panel`,style:{marginTop:14,display:`flex`,flexDirection:`column`,gap:10},children:[(0,J.jsx)(`h3`,{children:i(`routing.dryRun`)}),(0,J.jsxs)(`label`,{className:`field-label`,htmlFor:`routing-context`,children:[i(`routing.dryRunContext`),(0,J.jsx)(`input`,{id:`routing-context`,className:`input`,type:`number`,min:1,value:O,onChange:e=>{k(e.target.value),ie()}})]}),(0,J.jsxs)(`label`,{className:`checkbox`,children:[(0,J.jsx)(`input`,{type:`checkbox`,checked:A,onChange:e=>{j(e.target.checked),ie()}}),i(`routing.dryRunTools`)]}),(0,J.jsxs)(`label`,{className:`checkbox`,children:[(0,J.jsx)(`input`,{type:`checkbox`,checked:M,onChange:e=>{N(e.target.checked),ie()}}),i(`routing.dryRunImage`)]}),(0,J.jsxs)(`label`,{className:`checkbox`,children:[(0,J.jsx)(`input`,{type:`checkbox`,checked:P,onChange:e=>{F(e.target.checked),ie()}}),i(`routing.dryRunStructured`)]}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-primary`,disabled:!b||B,onClick:()=>void he(),children:i(`routing.dryRunRun`)}),R?(0,J.jsx)($,{tone:`err`,children:R}):null,I?(0,J.jsxs)(`table`,{className:`tbl`,children:[(0,J.jsx)(`thead`,{children:(0,J.jsxs)(`tr`,{children:[(0,J.jsx)(`th`,{children:i(`routing.candidate`)}),(0,J.jsx)(`th`,{children:i(`routing.eligible`)}),(0,J.jsx)(`th`,{children:i(`routing.exclusions`)}),(0,J.jsx)(`th`,{children:i(`routing.costCap`)}),(0,J.jsx)(`th`,{children:i(`routing.score`)})]})}),(0,J.jsx)(`tbody`,{children:I.candidates.map((e,t)=>(0,J.jsxs)(`tr`,{children:[(0,J.jsxs)(`td`,{children:[e.provider,`/`,e.model,t===I.selectedIndex?` ✓ (${i(`routing.selected`)})`:``]}),(0,J.jsx)(`td`,{children:e.eligible?i(`routing.yes`):i(`routing.no`)}),(0,J.jsx)(`td`,{children:e.exclusions.map(e=>pd(e.code,i)).join(`, `)||i(`routing.none`)}),(0,J.jsx)(`td`,{children:fd(e.cost?.capOutcome,i,o)}),(0,J.jsx)(`td`,{children:e.score?e.score.total.toFixed(3):o})]},`${e.provider}/${e.model}`))})]}):null]}),s.length>0&&(0,J.jsxs)(`div`,{className:`panel`,style:{marginTop:14,display:`flex`,flexDirection:`column`,gap:10},children:[(0,J.jsx)(`h3`,{children:i(`routing.analytics`)}),l?(0,J.jsxs)(J.Fragment,{children:[(0,J.jsxs)(`div`,{className:`card-badges`,children:[(0,J.jsxs)(`span`,{className:`badge badge-muted`,children:[i(`routing.analyticsTotal`),`: `,l.totalRequests]}),(0,J.jsxs)(`span`,{className:`badge badge-muted`,children:[i(`routing.analyticsSuccessRate`),`: `,dd(l.successRate,o)]}),(0,J.jsxs)(`span`,{className:`badge badge-muted`,children:[i(`routing.analyticsFallbackRate`),`: `,dd(l.fallbackRate,o)]}),(0,J.jsxs)(`span`,{className:`badge badge-muted`,children:[i(`routing.analyticsP50`),`: `,ud(l.durationMs.p50,o)]}),(0,J.jsxs)(`span`,{className:`badge badge-muted`,children:[i(`routing.analyticsP95`),`: `,ud(l.durationMs.p95,o)]}),(0,J.jsxs)(`span`,{className:`badge badge-muted`,children:[i(`routing.analyticsP99`),`: `,ud(l.durationMs.p99,o)]}),(0,J.jsxs)(`span`,{className:`badge badge-muted`,children:[i(`routing.analyticsCooldown`),`: `,l.cooldownTriggeringFailures]}),(0,J.jsxs)(`span`,{className:`badge badge-muted`,children:[i(`routing.analyticsConfidence`),`: `,l.confidence??o]}),l.historyTruncated?(0,J.jsx)(`span`,{className:`badge badge-muted`,children:i(`routing.analyticsTruncated`)}):null]}),(0,J.jsxs)(`table`,{className:`tbl`,children:[(0,J.jsx)(`thead`,{children:(0,J.jsxs)(`tr`,{children:[(0,J.jsx)(`th`,{children:i(`routing.candidate`)}),(0,J.jsx)(`th`,{children:i(`routing.analyticsRequests`)}),(0,J.jsx)(`th`,{children:i(`routing.analyticsSuccessRate`)}),(0,J.jsx)(`th`,{children:i(`routing.analyticsP50`)})]})}),(0,J.jsx)(`tbody`,{children:l.breakdown.map(e=>(0,J.jsxs)(`tr`,{children:[(0,J.jsxs)(`td`,{children:[e.provider,`/`,e.model]}),(0,J.jsx)(`td`,{children:e.requests}),(0,J.jsx)(`td`,{children:dd(e.successRate,o)}),(0,J.jsx)(`td`,{children:ud(e.p50DurationMs,o)})]},`${e.provider}/${e.model}`))})]})]}):(0,J.jsx)(`p`,{className:`muted`,children:i(`routing.analyticsEmpty`)})]})]})}var vd=[`protocol_conformance`,`live_route_compatibility`,`task_effectiveness`],yd=[`UNKNOWN`,`CLAIMED`,`PROBED`,`VERIFIED`,`DEGRADED`,`BLOCKED`,`UNSUPPORTED`],bd=[`present`,`corrupt`,`purged_unavailable`];function xd(e){return!!e&&typeof e==`object`&&!Array.isArray(e)}function Sd(e){return Array.isArray(e)&&e.every(e=>typeof e==`string`)}function Cd(e){return e===null||typeof e==`string`}function wd(e){return e===void 0||typeof e==`number`&&Number.isFinite(e)}function Td(e){if(!xd(e)||typeof e.projectionAvailable!=`boolean`||e.projectionIncompatible!==void 0&&typeof e.projectionIncompatible!=`boolean`)return null;for(let t of[`sqliteSchemaVersion`,`builtAtMs`,`eventCount`,`subjectCount`,`observationCount`,`claimCount`,`verdictCount`,`artifactCount`,`corruptionCount`])if(!wd(e[t]))return null;return e.projectionSpecVersion!==void 0&&typeof e.projectionSpecVersion!=`string`?null:e}function Ed(e){return!xd(e)||typeof e.projectionKey!=`string`||e.projectionKey.length===0||typeof e.subjectId!=`string`||e.subjectId.length===0||typeof e.evidenceLayer!=`string`||!vd.includes(e.evidenceLayer)||typeof e.suiteId!=`string`||e.suiteId.length===0||typeof e.suiteVersion!=`string`||typeof e.suiteManifestDigest!=`string`||typeof e.projectionSpecVersion!=`string`||typeof e.verdict!=`string`||!yd.includes(e.verdict)||typeof e.asOf!=`number`||!Number.isFinite(e.asOf)||!Sd(e.scenarioManifestDigests)||!Cd(e.claimSourceDigest)||!Sd(e.contributingEventIds)||!Sd(e.contradictingEventIds)||!Sd(e.notes)?null:e}function Dd(e){return!xd(e)||!Array.isArray(e.verdicts)?{verdicts:[],hasMore:!1}:{verdicts:e.verdicts.map(Ed).filter(e=>e!==null),hasMore:e.hasMore===!0,nextCursor:typeof e.nextCursor==`string`?e.nextCursor:void 0}}function Od(e){return!xd(e)||!Array.isArray(e.subjects)?{subjects:[],hasMore:!1}:{subjects:e.subjects.filter(e=>xd(e)&&typeof e.subjectId==`string`&&typeof e.subjectKind==`string`),hasMore:e.hasMore===!0,nextCursor:typeof e.nextCursor==`string`?e.nextCursor:void 0}}function kd(e){return!xd(e)||!xd(e.subject)||typeof e.subject.subjectKind!=`string`?null:e.subject}function Ad(e){return!xd(e)||typeof e.eventId!=`string`||typeof e.subjectId!=`string`||typeof e.evidenceLayer!=`string`||!vd.includes(e.evidenceLayer)||typeof e.suiteId!=`string`||typeof e.suiteVersion!=`string`||typeof e.suiteManifestDigest!=`string`||typeof e.scenarioId!=`string`||typeof e.scenarioVersion!=`string`||typeof e.scenarioManifestDigest!=`string`||typeof e.outcome!=`string`||typeof e.completedAt!=`number`||!Number.isFinite(e.completedAt)||typeof e.executionMode!=`string`||typeof e.excluded!=`boolean`||!Cd(e.exclusionReason)?null:e}function jd(e){return!xd(e)||!Array.isArray(e.observations)?{observations:[],hasMore:!1}:{observations:e.observations.map(Ad).filter(e=>e!==null),hasMore:e.hasMore===!0,nextCursor:typeof e.nextCursor==`string`?e.nextCursor:void 0}}function Md(e){if(!xd(e)||!xd(e.event))return null;let t={...e.event};return delete t.payload_json,typeof t.eventKind!=`string`||typeof t.eventId!=`string`||typeof t.recordedAt!=`number`||!Number.isFinite(t.recordedAt)||typeof t.producer!=`string`||typeof t.producerVersion!=`string`||typeof t.excluded!=`boolean`||!Cd(t.exclusionReason)?null:t}function Nd(e){if(!xd(e)||!xd(e.artifact))return null;let t=e.artifact;return typeof t.digest!=`string`||typeof t.status!=`string`||!bd.includes(t.status)||!Cd(t.artifactClass)||!Cd(t.mediaType)||t.byteCount!==null&&(typeof t.byteCount!=`number`||!Number.isFinite(t.byteCount))||!Cd(t.lastError)?null:t}function Pd(e){let t={},n=e.subjectQuery.trim();return e.layer&&(t.layer=e.layer),e.verdict&&(t.verdict=e.verdict),n&&(t.subjectId=n),e.suiteId.trim()&&(t.suiteId=e.suiteId.trim()),t}function Fd(){return{protocol_conformance:[],live_route_compatibility:[],task_effectiveness:[]}}function Id(e,t){let n=new Map(t.map(e=>[e.subjectId,e.subjectKind])),r=new Map;for(let t of e){let e=r.get(t.subjectId);e||(e={subjectId:t.subjectId,subjectKind:n.get(t.subjectId)??``,byLayer:Fd()},r.set(t.subjectId,e)),e.byLayer[t.evidenceLayer].push(t)}return[...r.values()].sort((e,t)=>e.subjectId.localeCompare(t.subjectId))}function Ld(e){return e.length<=16?e:`${e.slice(0,8)}.${e.slice(-6)}`}function Rd(e,t){return!Number.isFinite(e)||e<=0?`-`:new Date(e).toLocaleString(t)}function zd(e){let t=new Set;e.suiteManifestDigest&&t.add(e.suiteManifestDigest);for(let n of e.scenarioManifestDigests)n&&t.add(n);return e.claimSourceDigest&&t.add(e.claimSourceDigest),[...t]}var Bd=50,Vd=200,Hd=6,Ud=200;async function Wd(e,t,n){return Pt(await fetch(`${e}${t}`,{signal:n}))}function Gd(e,t){let n=new URLSearchParams({limit:String(Bd)});for(let[t,r]of Object.entries(e))r&&n.set(t,r);return t&&n.set(`cursor`,t),n.toString()}var Kd=class extends Error{};function qd(){return new Kd}function Jd(e,t,n){if(e.hasMore!==void 0&&typeof e.hasMore!=`boolean`||e.nextCursor!==void 0&&typeof e.nextCursor!=`string`||t&&!n)throw qd()}function Yd(e){if(!xd(e)||!Array.isArray(e.verdicts))throw qd();let t=Dd(e);if(t.verdicts.length!==e.verdicts.length)throw qd();return Jd(e,t.hasMore,t.nextCursor),t}function Xd(e){if(!xd(e)||!Array.isArray(e.subjects))throw qd();let t=Od(e);if(t.subjects.length!==e.subjects.length)throw qd();return Jd(e,t.hasMore,t.nextCursor),t}function Zd(e){if(!xd(e)||!Array.isArray(e.observations))throw qd();let t=jd(e);if(t.observations.length!==e.observations.length)throw qd();return Jd(e,t.hasMore,t.nextCursor),t}async function Qd(e,t){let n=Td(await Wd(e,`/api/lab/status`,t));if(!n)throw qd();return n}async function $d(e,t,n,r){return Yd(await Wd(e,`/api/lab/verdicts?${Gd({layer:t.layer,verdict:t.verdict,subjectId:t.subjectId,suiteId:t.suiteId},n)}`,r))}async function ef(e,t,n){return Xd(await Wd(e,`/api/lab/subjects?${Gd({},t)}`,n))}async function tf(e){let t=[],n=new Set,r;for(let i=0;i{let r=await ef(e,n,t);return{rows:r.subjects,hasMore:r.hasMore,nextCursor:r.nextCursor}})}async function rf(e,t,n){let r=kd(await Wd(e,`/api/lab/subjects/${encodeURIComponent(t)}`,n));if(!r)throw qd();return r}async function af(e,t,n,r){return Zd(await Wd(e,`/api/lab/observations?${Gd({subjectId:t.subjectId,layer:t.layer,suiteId:t.suiteId},n)}`,r))}async function of(e,t,n){return tf(async r=>{let i=await af(e,t,r,n);return{rows:i.observations,hasMore:i.hasMore,nextCursor:i.nextCursor}})}async function sf(e,t,n){let r=Md(await Wd(e,`/api/lab/events/${encodeURIComponent(t)}`,n));if(!r)throw qd();return r}async function cf(e,t,n){let r=Nd(await Wd(e,`/api/lab/artifacts/${encodeURIComponent(t)}`,n));if(!r)throw qd();return r}function lf(e){if(!xd(e)||e.verificationStatus!==`not_verification`||!xd(e.summary))throw qd();let t=e.summary;if(t.verificationStatus!==`not_verification`||typeof t.subjectId!=`string`||typeof t.recentProductionAttempts!=`number`||typeof t.recentSuccessfulAttempts!=`number`||typeof t.recentRouteErrorSignals!=`number`||t.lastObservedProductionAttempt!==void 0&&typeof t.lastObservedProductionAttempt!=`number`)throw qd();return{verificationStatus:`not_verification`,summary:t}}async function uf(e,t,n){return lf(await Wd(e,`/api/lab/production-signals?${Gd({subjectId:t})}`,n))}function df(e,t){let n=new Set(t);return Object.keys(e).every(e=>n.has(e))}function ff(e){return typeof e==`string`&&/^[0-9a-f]{64}$/.test(e)}function pf(e){return typeof e==`number`&&Number.isSafeInteger(e)&&e>=0}function mf(e){if(!xd(e)||!df(e,[`evidence`,`trustClass`,`locallyVerified`])||e.trustClass!==`community_untrusted_v1`||e.locallyVerified!==!1||!Array.isArray(e.evidence)||e.evidence.length>4096)return null;let t=[];for(let n of e.evidence){if(!xd(n)||!df(n,[`trustClass`,`status`,`bundleId`,`publisherKeyId`,`activeRecordCount`,`revokedRecordCount`])||n.trustClass!==`community_untrusted_v1`||n.status!==`cryptographically_valid`||!ff(n.bundleId)||!ff(n.publisherKeyId)||!pf(n.activeRecordCount)||!pf(n.revokedRecordCount))return null;t.push({trustClass:`community_untrusted_v1`,status:`cryptographically_valid`,bundleId:n.bundleId,publisherKeyId:n.publisherKeyId,activeRecordCount:n.activeRecordCount,revokedRecordCount:n.revokedRecordCount})}return{evidence:t,trustClass:`community_untrusted_v1`,locallyVerified:!1}}async function hf(e,t){let n=mf(await Wd(e,`/api/lab/public/community`,t));if(!n)throw qd();return n}async function gf(e,t,n){let[r,i]=await Promise.all([Qd(e,n),hf(e,n).catch(e=>{if(n.aborted)throw e;return null})]);if(!r.projectionAvailable)return{status:r,verdicts:[],subjects:[],subjectsTruncated:!1,hasMore:!1,community:i};let[a,o]=await Promise.all([$d(e,t,void 0,n),nf(e,n)]);return{status:r,verdicts:a.verdicts,subjects:o.rows,subjectsTruncated:o.truncated,hasMore:a.hasMore,nextCursor:a.nextCursor,community:i}}async function _f(e,t,n,r){return $d(e,t,n,r)}async function vf(e,t,n,r){let i=e.slice(0,Ud),a=[],o=0,s=async()=>{for(;;){if(n.aborted)throw new DOMException(`Aborted`,`AbortError`);let e=o++;if(e>=i.length)return;try{a.push(await r(i[e]))}catch(e){if(n.aborted)throw e}}},c=Math.min(t,i.length);return await Promise.all(Array.from({length:c},()=>s())),a}async function yf(e,t,n){let r=[...new Set([...t.contributingEventIds,...t.contradictingEventIds])],i=zd(t),a={subjectId:t.subjectId,layer:t.evidenceLayer,suiteId:t.suiteId},[o,s,c,l,u]=await Promise.all([rf(e,t.subjectId,n),of(e,a,n),vf(r,Hd,n,t=>sf(e,t,n)),vf(i,Hd,n,t=>cf(e,t,n)),uf(e,t.subjectId,n).catch(e=>{if(n.aborted)throw e;return null})]);return{subject:o,observations:s.rows,observationsTruncated:s.truncated,events:c,artifacts:l,production:u}}var bf={protocol_conformance:`lab.layer.protocol_conformance`,live_route_compatibility:`lab.layer.live_route_compatibility`,task_effectiveness:`lab.layer.task_effectiveness`},xf={protocol_conformance:`lab.col.protocol`,live_route_compatibility:`lab.col.live`,task_effectiveness:`lab.col.task`},Sf={UNKNOWN:`lab.verdict.UNKNOWN`,CLAIMED:`lab.verdict.CLAIMED`,PROBED:`lab.verdict.PROBED`,VERIFIED:`lab.verdict.VERIFIED`,DEGRADED:`lab.verdict.DEGRADED`,BLOCKED:`lab.verdict.BLOCKED`,UNSUPPORTED:`lab.verdict.UNSUPPORTED`},Cf={present:`artifact.present`,corrupt:`artifact.corrupt`,purged_unavailable:`artifact.purged_unavailable`};function wf(e,t){if(!(e instanceof Error))return t;let n=e.message;return n===`Failed to fetch`||n.includes(`NetworkError`)||n.includes(`network error`)?t:n||t}function Tf({verdict:e,caption:t,label:n,selected:r,onSelect:i}){let a=`lab-verdict-badge${r?` lab-verdict-badge--selected`:``}`;return i?(0,J.jsxs)(`button`,{type:`button`,className:a,"data-verdict":e,title:t,onClick:i,children:[(0,J.jsx)(`span`,{children:n}),(0,J.jsx)(`span`,{className:`suite`,children:t})]}):(0,J.jsxs)(`span`,{className:a,"data-verdict":e,title:t,children:[(0,J.jsx)(`span`,{children:n}),(0,J.jsx)(`span`,{className:`suite`,children:t})]})}function Ef({rows:e,t,selectedKey:n,onSelect:r}){return e.length===0?(0,J.jsx)(`span`,{className:`muted`,children:`-`}):(0,J.jsx)(`div`,{className:`lab-verdict-stack`,children:e.map(e=>(0,J.jsx)(Tf,{verdict:e.verdict,caption:e.suiteId,label:t(Sf[e.verdict]),selected:n===e.projectionKey,onSelect:()=>r(e)},e.projectionKey))})}function Df({data:e,t,locale:n}){let{status:r}=e,i=[{label:t(`lab.subjectCount`),value:String(r.subjectCount??0)},{label:t(`lab.verdictCount`),value:String(r.verdictCount??0)},{label:t(`lab.observationCount`),value:String(r.observationCount??0)},{label:t(`lab.eventCount`),value:String(r.eventCount??0)}];return r.builtAtMs&&i.push({label:t(`lab.builtAt`),value:Rd(r.builtAtMs,n)}),(0,J.jsx)(`div`,{className:`lab-status-grid`,"aria-label":t(`lab.statusTitle`),children:i.map(e=>(0,J.jsxs)(`div`,{className:`lab-status-card`,children:[(0,J.jsx)(`span`,{className:`label`,children:e.label}),(0,J.jsx)(`span`,{className:`value`,children:e.value})]},e.label))})}function Of({community:e,locale:t}){if(!e||e.evidence.length===0)return null;let n=e.evidence.reduce((e,t)=>e+t.activeRecordCount,0),r=e.evidence.reduce((e,t)=>e+t.revokedRecordCount,0);return(0,J.jsxs)(`section`,{className:`lab-matrix-block`,"data-testid":`lab-community-evidence`,children:[(0,J.jsx)(`h3`,{className:`lab-matrix-title`,children:Ye(t,`community.title`)}),(0,J.jsx)(`p`,{className:`muted`,children:Ye(t,`community.notLocalVerdict`)}),(0,J.jsxs)(`dl`,{className:`lab-detail-meta`,children:[(0,J.jsxs)(`div`,{children:[(0,J.jsx)(`dt`,{children:Ye(t,`community.bundles`)}),(0,J.jsx)(`dd`,{children:e.evidence.length})]}),(0,J.jsxs)(`div`,{children:[(0,J.jsx)(`dt`,{children:Ye(t,`community.activeRecords`)}),(0,J.jsx)(`dd`,{children:n})]}),(0,J.jsxs)(`div`,{children:[(0,J.jsx)(`dt`,{children:Ye(t,`community.revokedRecords`)}),(0,J.jsx)(`dd`,{children:r})]})]})]})}function kf({verdict:e,detail:t,loading:n,error:r,t:i,locale:a,onClose:o}){let s=new Set([...e.contributingEventIds,...e.contradictingEventIds]).size;return(0,J.jsxs)(`aside`,{className:`lab-detail-pane`,"aria-label":i(`lab.detailTitle`),children:[(0,J.jsxs)(`div`,{className:`lab-detail-head`,children:[(0,J.jsx)(`h3`,{children:Ld(e.subjectId)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:o,children:i(`lab.detailClose`)})]}),(0,J.jsxs)(`dl`,{className:`lab-detail-meta`,children:[(0,J.jsxs)(`div`,{children:[(0,J.jsx)(`dt`,{children:i(`lab.col.layer`)}),(0,J.jsx)(`dd`,{children:i(bf[e.evidenceLayer])})]}),(0,J.jsxs)(`div`,{children:[(0,J.jsx)(`dt`,{children:i(`lab.col.suite`)}),(0,J.jsx)(`dd`,{className:`mono`,children:e.suiteId})]}),(0,J.jsxs)(`div`,{children:[(0,J.jsx)(`dt`,{children:i(`lab.col.verdict`)}),(0,J.jsx)(`dd`,{children:i(Sf[e.verdict])})]}),(0,J.jsxs)(`div`,{children:[(0,J.jsx)(`dt`,{children:i(`lab.col.asOf`)}),(0,J.jsx)(`dd`,{children:Rd(e.asOf,a)})]})]}),n&&(0,J.jsx)(_l,{busy:!0,children:i(`common.loading`)}),r&&(0,J.jsx)($,{tone:`err`,children:r}),t&&(0,J.jsxs)(J.Fragment,{children:[(0,J.jsxs)(`section`,{className:`lab-detail-section`,children:[(0,J.jsx)(`h4`,{children:i(`lab.detailSubject`)}),(0,J.jsx)(`p`,{className:`mono`,children:t.subject.subjectKind})]}),t.production&&(0,J.jsxs)(`section`,{className:`lab-detail-section`,"data-testid":`lab-production-signals`,children:[(0,J.jsx)(`h4`,{children:i(`lab.production.title`)}),(0,J.jsx)(`p`,{className:`muted`,children:i(`lab.production.notVerification`)}),(0,J.jsxs)(`dl`,{className:`lab-detail-meta`,children:[(0,J.jsxs)(`div`,{children:[(0,J.jsx)(`dt`,{children:i(`lab.production.attempts`)}),(0,J.jsx)(`dd`,{children:t.production.summary.recentProductionAttempts})]}),(0,J.jsxs)(`div`,{children:[(0,J.jsx)(`dt`,{children:i(`lab.production.successes`)}),(0,J.jsx)(`dd`,{children:t.production.summary.recentSuccessfulAttempts})]}),(0,J.jsxs)(`div`,{children:[(0,J.jsx)(`dt`,{children:i(`lab.production.routeErrors`)}),(0,J.jsx)(`dd`,{children:t.production.summary.recentRouteErrorSignals})]}),t.production.summary.lastObservedProductionAttempt!==void 0&&(0,J.jsxs)(`div`,{children:[(0,J.jsx)(`dt`,{children:i(`lab.production.lastObserved`)}),(0,J.jsx)(`dd`,{children:Rd(t.production.summary.lastObservedProductionAttempt,a)})]})]})]}),t.observations.length>0&&(0,J.jsxs)(`section`,{className:`lab-detail-section`,children:[(0,J.jsx)(`h4`,{children:i(`lab.detailObservations`)}),(0,J.jsx)(`ul`,{className:`lab-detail-list`,children:t.observations.map(e=>(0,J.jsxs)(`li`,{children:[(0,J.jsx)(`span`,{className:`mono`,children:e.scenarioId}),(0,J.jsx)(`span`,{children:e.outcome}),(0,J.jsx)(`span`,{className:`muted`,children:Rd(e.completedAt,a)})]},e.eventId))})]}),(t.events.length>0||s>0)&&(0,J.jsxs)(`section`,{className:`lab-detail-section`,children:[(0,J.jsxs)(`h4`,{children:[i(`lab.detailEvents`),t.events.length(0,J.jsxs)(`li`,{children:[(0,J.jsx)(`span`,{className:`mono`,children:e.eventId}),(0,J.jsx)(`span`,{children:e.eventKind})]},e.eventId))})]}),t.artifacts.length>0&&(0,J.jsxs)(`section`,{className:`lab-detail-section`,children:[(0,J.jsx)(`h4`,{children:i(`lab.detailArtifacts`)}),(0,J.jsx)(`ul`,{className:`lab-detail-list`,children:t.artifacts.map(e=>(0,J.jsxs)(`li`,{children:[(0,J.jsx)(`span`,{className:`mono`,title:e.digest,children:Ld(e.digest)}),(0,J.jsx)(`span`,{children:e.artifactClass??`-`}),(0,J.jsx)(`span`,{children:Ye(a,Cf[e.status])})]},e.digest))})]})]})]})}function Af({apiBase:e,active:t=!0,onCountChange:n}){let{t:r,locale:i}=ct(),[a,o]=(0,_.useState)({layer:``,verdict:``,subjectQuery:``,suiteId:``}),[s,c]=(0,_.useState)(null),[l,u]=(0,_.useState)(null),[d,f]=(0,_.useState)(!1),[p,m]=(0,_.useState)(null),[h,g]=(0,_.useState)(null),[v,y]=(0,_.useState)(!1),[b,x]=(0,_.useState)(null),S=(0,_.useRef)(null),C=(0,_.useRef)(null),w=(0,_.useRef)(null),T=(0,_.useMemo)(()=>Pd(a),[a]),E=JSON.stringify(T),D=(0,_.useCallback)(t=>gf(e,T,t),[e,T]),O=ml(`lab-matrix:${e}:${E}`,[e,E],D,{isEmpty:e=>e.verdicts.length===0,pollMs:6e4,enabled:t,pauseWhenHidden:!0}),k=(0,_.useCallback)(()=>{S.current?.abort(),S.current=null,c(null),u(null),f(!1)},[]),A=(0,_.useCallback)(()=>{w.current=null,C.current?.abort(),C.current=null,m(null),g(null),x(null),y(!1)},[]);(0,_.useEffect)(()=>{S.current?.abort()},[O.data]),(0,_.useEffect)(()=>()=>{S.current?.abort(),C.current?.abort()},[]);let j=(0,_.useCallback)(e=>{A(),k(),o(e)},[A,k]),M=s!==null&&s.baseData===O.data&&s.queryKey===E?s:null,N=l!==null&&l.baseData===O.data&&l.queryKey===E?l.message:null,P=(0,_.useMemo)(()=>{if(!t||!O.data?.status.projectionAvailable)return null;let e=O.data.status.verdictCount;return typeof e==`number`?e:O.data.verdicts.length+(M?.verdicts.length??0)},[t,O.data,M]);(0,_.useEffect)(()=>{n?.(P)},[n,P]);let F=(0,_.useMemo)(()=>O.data?[...O.data.verdicts,...M?.verdicts??[]]:[],[O.data,M]),I=(0,_.useMemo)(()=>O.data?Id(F,O.data.subjects):[],[F,O.data]),L=(0,_.useCallback)(async()=>{let t=M?.nextCursor??O.data?.nextCursor,n=O.data;if(!t||!n||d)return;S.current?.abort();let i=new AbortController;S.current=i;let a=E;u(null),f(!0);try{let r=await _f(e,T,t,i.signal);if(i.signal.aborted)return;c(e=>{let t=e?.baseData===n&&e.queryKey===a?e.verdicts:[];return{baseData:n,queryKey:a,verdicts:[...t,...r.verdicts],nextCursor:r.nextCursor,hasMore:r.hasMore}})}catch(e){i.signal.aborted||u({baseData:n,queryKey:a,message:wf(e,r(`lab.loadFailed`))})}finally{S.current===i&&(S.current=null,f(!1))}},[e,d,T,E,O.data,r,M]),R=(0,_.useCallback)(async n=>{if(!t)return;C.current?.abort();let i=new AbortController;C.current=i,w.current=n.projectionKey,m(n),g(null),x(null),y(!0);try{let t=await yf(e,n,i.signal);!i.signal.aborted&&w.current===n.projectionKey&&g(t)}catch(e){!i.signal.aborted&&w.current===n.projectionKey&&x(wf(e,r(`lab.detailLoadFailed`)))}finally{C.current===i&&(C.current=null,!i.signal.aborted&&w.current===n.projectionKey&&y(!1))}},[t,e,r]),z=O.refresh,B=(0,_.useCallback)(()=>{A(),k(),z({forceLoading:!0})},[A,z,k]),V=t?p:null,H=M?M.hasMore:O.data?.hasMore??!1,U=[{value:``,label:r(`lab.filter.all`)},...vd.map(e=>({value:e,label:r(bf[e])}))],W=[{value:``,label:r(`lab.filter.all`)},...yd.map(e=>({value:e,label:r(Sf[e])}))],ee=[{value:``,label:r(`lab.filter.all`)},...(O.data?.subjects??[]).map(e=>({value:e.subjectId,label:`${Ld(e.subjectId)} · ${e.subjectKind}`}))];if(O.state.showSkeleton)return(0,J.jsx)(gl,{label:r(`lab.loading`),rows:5});let G=O.state.showError?wf(O.error,r(`lab.loadFailed`)):null,K=O.data?.status,q=K&&!K.projectionAvailable,Y=K?.projectionIncompatible===!0;return(0,J.jsxs)(`div`,{className:`lab-page`,children:[(0,J.jsx)(`div`,{className:`lab-toolbar`,children:(0,J.jsxs)(`button`,{type:`button`,className:`btn btn-ghost`,onClick:B,disabled:O.refreshing,children:[(0,J.jsx)(pe,{}),r(`lab.refresh`)]})}),O.refreshing&&!O.state.showSkeleton&&(0,J.jsx)(_l,{busy:!0,live:!G,children:r(`common.loading`)}),G&&(0,J.jsx)($,{tone:`err`,children:G}),Y&&(0,J.jsx)($,{tone:`err`,children:r(`lab.projectionIncompatible`)}),q&&!Y&&(0,J.jsx)(Ot,{title:r(`lab.projectionUnavailable`)}),O.data&&(0,J.jsx)(Of,{community:O.data.community,locale:i}),O.data&&K?.projectionAvailable&&!Y&&(0,J.jsxs)(`div`,{className:`lab-layout`,children:[(0,J.jsxs)(`div`,{className:`lab-main`,children:[(0,J.jsx)(Df,{data:O.data,t:r,locale:i}),(0,J.jsxs)(`div`,{className:`lab-filters`,children:[(0,J.jsxs)(`div`,{className:`lab-filter-field`,children:[(0,J.jsx)(`label`,{htmlFor:`lab-filter-layer`,children:r(`lab.filter.layer`)}),(0,J.jsx)(Dt,{id:`lab-filter-layer`,value:a.layer,options:U,onChange:e=>j(t=>({...t,layer:e})),label:r(`lab.filter.layer`),portal:!1})]}),(0,J.jsxs)(`div`,{className:`lab-filter-field`,children:[(0,J.jsx)(`label`,{htmlFor:`lab-filter-verdict`,children:r(`lab.filter.verdict`)}),(0,J.jsx)(Dt,{id:`lab-filter-verdict`,value:a.verdict,options:W,onChange:e=>j(t=>({...t,verdict:e})),label:r(`lab.filter.verdict`),portal:!1})]}),(0,J.jsxs)(`div`,{className:`lab-filter-field`,children:[(0,J.jsx)(`label`,{htmlFor:`lab-filter-subject`,children:r(`lab.filter.subject`)}),(0,J.jsx)(Dt,{id:`lab-filter-subject`,value:a.subjectQuery,options:ee,onChange:e=>j(t=>({...t,subjectQuery:e})),label:r(`lab.filter.subject`),portal:!1})]})]}),I.length===0?(0,J.jsx)(Ot,{title:r(`lab.empty`)}):(0,J.jsxs)(J.Fragment,{children:[(0,J.jsxs)(`div`,{className:`lab-matrix-block`,children:[(0,J.jsx)(`h3`,{className:`lab-matrix-title`,children:r(`lab.matrixTitle`)}),(0,J.jsx)(`div`,{className:`lab-matrix-scroll`,children:(0,J.jsxs)(`table`,{className:`lab-matrix`,children:[(0,J.jsx)(`thead`,{children:(0,J.jsxs)(`tr`,{children:[(0,J.jsx)(`th`,{children:r(`lab.col.subject`)}),(0,J.jsx)(`th`,{children:r(`lab.subjectKind`)}),vd.map(e=>(0,J.jsx)(`th`,{children:r(xf[e])},e))]})}),(0,J.jsx)(`tbody`,{children:I.map(e=>(0,J.jsxs)(`tr`,{children:[(0,J.jsx)(`td`,{className:`subject`,title:e.subjectId,children:Ld(e.subjectId)}),(0,J.jsx)(`td`,{className:`kind`,children:e.subjectKind||Ye(i,`subjectKindUnknown`)}),vd.map(t=>(0,J.jsx)(`td`,{children:(0,J.jsx)(Ef,{rows:e.byLayer[t],t:r,selectedKey:V?.projectionKey??null,onSelect:e=>{R(e)}})},t))]},e.subjectId))})]})})]}),(0,J.jsxs)(`div`,{className:`lab-matrix-block`,children:[(0,J.jsx)(`h3`,{className:`lab-matrix-title`,children:r(`lab.verdictsTitle`)}),(0,J.jsx)(`div`,{className:`lab-matrix-scroll`,children:(0,J.jsxs)(`table`,{className:`lab-detail-table`,children:[(0,J.jsx)(`thead`,{children:(0,J.jsxs)(`tr`,{children:[(0,J.jsx)(`th`,{children:r(`lab.col.subject`)}),(0,J.jsx)(`th`,{children:r(`lab.col.layer`)}),(0,J.jsx)(`th`,{children:r(`lab.col.suite`)}),(0,J.jsx)(`th`,{children:r(`lab.col.verdict`)}),(0,J.jsx)(`th`,{children:r(`lab.col.asOf`)})]})}),(0,J.jsx)(`tbody`,{children:F.map(e=>{let t=V?.projectionKey===e.projectionKey;return(0,J.jsxs)(`tr`,{className:t?`selected`:``,children:[(0,J.jsx)(`td`,{className:`mono`,title:e.subjectId,children:(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,"data-verdict-detail":e.projectionKey,"aria-pressed":t,"aria-label":Ye(i,`selectVerdict`,{subject:Ld(e.subjectId)}),onClick:()=>{R(e)},children:Ld(e.subjectId)})}),(0,J.jsx)(`td`,{children:r(bf[e.evidenceLayer])}),(0,J.jsx)(`td`,{className:`mono`,children:e.suiteId}),(0,J.jsx)(`td`,{children:(0,J.jsx)(Tf,{verdict:e.verdict,caption:e.suiteId,label:r(Sf[e.verdict])})}),(0,J.jsx)(`td`,{children:Rd(e.asOf,i)})]},e.projectionKey)})})]})})]}),N&&(0,J.jsx)($,{tone:`err`,children:N}),H&&(0,J.jsx)(`div`,{className:`lab-load-more`,children:(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost`,disabled:d,onClick:()=>{L()},children:r(d?`common.loading`:`lab.loadMore`)})})]})]}),V&&(0,J.jsx)(kf,{verdict:V,detail:h,loading:v,error:b,t:r,locale:i,onClose:A})]})]})}var jf=[`catalog`,`combos`,`routing`,`compatibility`];function Mf(e){return e===`catalog`?`models`:`models/${e}`}function Nf(e=window.location.hash){let t=dt(e);return t===`models/combos`||t===`combos`||t.startsWith(`combos/`)?`combos`:t===`models/routing`||t===`routing`||t.startsWith(`routing/`)?`routing`:t===`models/compatibility`||t===`lab`||t.startsWith(`lab/`)?`compatibility`:`catalog`}function Pf(e){pt(Mf(e))}function Ff(e){return`models-tab-${e}`}function If(e){return`models-panel-${e}`}var Lf={catalog:`models.tab.catalog`,combos:`models.tab.combos`,routing:`models.tab.routing`,compatibility:`models.tab.compatibility`};function Rf({tab:e,onSelect:t,meta:n}){let r=Q(),i=(0,_.useRef)(null);i.current===null&&(i.current=new Map);let a=e=>{t(e),window.requestAnimationFrame(()=>{i.current.get(e)?.focus({preventScroll:!0})})},o=t=>{let n=jf.indexOf(e),r=null;t.key===`ArrowLeft`?r=(n-1+jf.length)%jf.length:t.key===`ArrowRight`?r=(n+1)%jf.length:t.key===`Home`?r=0:t.key===`End`&&(r=jf.length-1),r!==null&&(t.preventDefault(),a(jf[r]))};return(0,J.jsx)(`div`,{className:`page-tabs`,role:`tablist`,"aria-label":r(`models.tabsLabel`),children:jf.map(t=>{let s=t===e,c=n?.[t];return(0,J.jsxs)(`button`,{ref:e=>{e?i.current.set(t,e):i.current.delete(t)},type:`button`,role:`tab`,id:Ff(t),"aria-selected":s,"aria-controls":If(t),tabIndex:s?0:-1,className:`page-tab${s?` page-tab--active`:``}`,onClick:()=>a(t),onKeyDown:o,children:[r(Lf[t]),c?(0,J.jsx)(`span`,{className:`section-tab-meta`,children:c}):null]},t)})})}function zf(e,t){let n=new Map;for(let t of e){let e=n.get(t.provider);e?e.push(t):n.set(t.provider,[t])}let r=new Map(t.map(e=>[e.name,e]));for(let e of t){if(e.disabled===!0){n.delete(e.name);continue}e.authMode!==`forward`&&(n.has(e.name)||n.set(e.name,[]))}return[...n.entries()].map(([e,t])=>{let n=r.get(e);return{provider:e,rows:t,native:t.length>0&&t.every(e=>e.native===!0),nativeProviderGroup:t.some(e=>e.native===!0),liveModels:n?.liveModels!==!1,configuredModels:n?.models??[],contextWindow:n?.contextWindow,modelContextWindows:n?.modelContextWindows,discovery:n?.discovery,entitlement:n?.entitlement}}).sort((e,t)=>e.nativeProviderGroup===t.nativeProviderGroup?e.provider.localeCompare(t.provider):e.nativeProviderGroup?-1:1)}function Bf(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function Vf(e){if(!Array.isArray(e)||e.some(e=>typeof e!=`string`))throw Error(`invalid model list`);return[...new Set(e)]}function Hf(e){if(!Bf(e))throw Error(`invalid selected models response`);let t=e.selected;if(!Bf(t))throw Error(`invalid selected models response`);return Object.fromEntries(Object.entries(t).map(([e,t])=>[e,Vf(t)]))}async function Uf(e,t=fetch,n){let r=await t(`${e}/api/selected-models`,n?{signal:n}:void 0);if(!r.ok)throw Error(`selected models HTTP ${r.status}`);return Hf(await r.json())}function Wf(e,t,n,r=!1){if(r)return!0;let i=e[t];return!i||i.length===0||i.includes(n)}function Gf(e,t,n,r,i){return Wf(e,t,n,r)&&!i}async function Kf(e,t,n,r,i,a=fetch){return a(`${e}/api/model-visibility`,{method:`PUT`,headers:{"Content-Type":`application/json`},body:JSON.stringify({scope:t,provider:n,targets:r,enabled:i})})}function qf(e,t){return e===t}function Jf(e,t){switch(t.reason){case`http`:return e(`models.discoveryFailedHttp`,{status:t.httpStatus});case`blocked`:return e(`models.discoveryFailedBlocked`);case`invalid_response`:return e(`models.discoveryFailedInvalidResponse`);case`network`:return e(`models.discoveryFailedNetwork`);case`provider`:return e(`models.discoveryFailedProvider`);default:return e(`models.discoveryFailedGeneric`)}}var Yf=[`none`,`minimal`,`low`,`medium`,`high`,`xhigh`,`max`],Xf=Array.from({length:18},(e,t)=>1e5+t*5e4),Zf=new Set(Xf),Qf=272e3,$f=922e3,ep=[Qf,372e3,$f],tp=new Set(ep),np=`custom`,rp=[4,8,16,32,64,128,256,500,1e3],ip=new Set(rp),ap=`ocx-models-collapsed:v2`;function op(e){return!Number.isFinite(e)||e<=0?String(e):e%1e3==0?e>=1e6?Number((e/1e6).toFixed(2))+`M`:`${e/1e3}k`:e.toLocaleString()}function sp(e){let t=new Set;for(let n of e)n.disabled&&t.add(n.namespaced);return t}function cp(e,t,n,r){let i=[];for(let a of e){let e=t.has(a.id)||t.has(a.namespaced);Gf(n,a.provider,a.id,a.native===!0,e)&&i.push({value:a.namespaced,label:r?Pn(a.namespaced,r):a.namespaced})}return i}function lp(e=localStorage){try{let t=e.getItem(ap);if(t===null)return null;let n=JSON.parse(t);return Array.isArray(n)?new Set(n.filter(e=>typeof e==`string`)):null}catch{return null}}function up(e,t=localStorage){try{t.setItem(ap,JSON.stringify([...e]))}catch{}}function dp({liveModels:e,discovery:t,showFailureBadge:n=!0}){let r=Q(),i=e&&t?.status===`failed`?t:void 0;return(0,J.jsxs)(`div`,{className:`row muted text-label leading-body`,role:`status`,style:{alignItems:`flex-start`,gap:8,padding:`6px 0`},children:[(0,J.jsx)(Z,{width:15,height:15,"aria-hidden":`true`,style:{flexShrink:0,marginTop:2}}),(0,J.jsxs)(`span`,{children:[i&&n&&(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)(`span`,{className:`badge badge-amber`,children:r(`models.discoveryFailedBadge`)}),` `]}),i?`${Jf(r,i)} `:`${r(e?`models.emptyDiscovery`:`models.emptyDiscoveryDisabled`)} `,(0,J.jsx)(`button`,{type:`button`,className:`link-btn`,onClick:()=>pt(`providers`),children:r(`models.openProviderSettings`)})]})]})}var fp={catalog:`models.subtitle`,combos:`models.subtitle.combos`,routing:`models.subtitle.routing`,compatibility:`models.subtitle.compatibility`};function pp(e){let t=e.replace(/[_,\s]/g,``);if(!t)return null;let n=Number(t);return Number.isSafeInteger(n)&&n>0?n:void 0}function mp({apiBase:e,restartEpoch:t=0}){let[n,r]=(0,_.useState)(null),i=(0,_.useRef)(!0);(0,_.useEffect)(()=>(i.current=!0,()=>{i.current=!1}),[]);let a=(0,_.useCallback)(t=>{nl(e,{signal:t}).then(e=>{t?.aborted||!i.current||r(e.state)})},[e]),{restarting:o,restart:s}=sl(e,{onSettled:()=>a()});(0,_.useEffect)(()=>{let e=new AbortController;return a(e.signal),()=>e.abort()},[a,t]);let[c,l]=(0,_.useState)(Nf),[u,d]=(0,_.useState)(()=>new Set([Nf()])),f=(0,_.useCallback)(e=>{l(e),d(t=>t.has(e)?t:new Set([...t,e]))},[]);(0,_.useEffect)(()=>{let e=()=>f(Nf());return window.addEventListener(`hashchange`,e),window.addEventListener(`popstate`,e),()=>{window.removeEventListener(`hashchange`,e),window.removeEventListener(`popstate`,e)}},[f]);let p=(0,_.useCallback)(e=>{Pf(e),f(e)},[f]),m=c===`catalog`,[h,g]=(0,_.useState)(null),[v,y]=(0,_.useState)(null),[b,x]=(0,_.useState)(null),S=Q(),C=`ocx.models.catalog.v1:${e}`,w=(0,_.useMemo)(()=>gr(C),[C]),[T,E]=(0,_.useState)(()=>w?.models??[]),[D,O]=(0,_.useState)(()=>w?.providers??[]),[k,A]=(0,_.useState)(()=>new Set(w?.disabled??[])),[j,M]=(0,_.useState)(()=>w?.selectedModels??null),[N,P]=(0,_.useState)({}),[F,I]=(0,_.useState)({}),[L,R]=(0,_.useState)(()=>w?.contextCaps??{}),[z,B]=(0,_.useState)(()=>w?.contextCapValue??35e4),[V,H]=(0,_.useState)(``),[U,W]=(0,_.useState)(!1),[ee,G]=(0,_.useState)({}),[q,Y]=(0,_.useState)({}),te=lp(),[ne,ie]=(0,_.useState)(()=>te??new Set),ae=(0,_.useRef)(te===null),[oe,se]=(0,_.useState)(``),[ce,le]=(0,_.useState)(!1),[de,fe]=(0,_.useState)(0),X=(e,t)=>{le(e),se(t),fe(e=>e+1)};(0,_.useEffect)(()=>{if(!oe)return;let e=setTimeout(()=>se(``),ce?6e3:8e3);return()=>clearTimeout(e)},[oe,ce,de]);let[me,he]=(0,_.useState)(!1),ve=(0,_.useRef)(!1),ye=(0,_.useRef)(0),be=(0,_.useRef)(!1),[xe,Ce]=(0,_.useState)(null),[we,Te]=(0,_.useState)({}),[Ee,De]=(0,_.useState)(null),[Oe,ke]=(0,_.useState)({providers:{},models:{},defaults:{global:!1,providers:{}}}),[Ae,je]=(0,_.useState)(!1),[Me,Ne]=(0,_.useState)(null),[Pe,Fe]=(0,_.useState)(!0),[Ie,Le]=(0,_.useState)(!1),[Re,ze]=(0,_.useState)(``),Be=(0,_.useRef)(!1),[Ve,He]=(0,_.useState)(``),[Ue,We]=(0,_.useState)(!1),[Ge,Ke]=(0,_.useState)(!1),[qe,Je]=(0,_.useState)(!1),Ye=(0,_.useCallback)(async t=>{let n=await Ft(await fetch(`${e}/api/aliases`,{signal:t}));n&&!t?.aborted&&ke(n)},[e]);(0,_.useEffect)(()=>{let e=new AbortController;return Ye(e.signal),()=>e.abort()},[Ye]);let Xe=async t=>{let n=window.prompt(S(`models.aliasPrompt`),Oe.providers[t]??``);if(n!==null){if(!(await fetch(`${e}/api/providers/${encodeURIComponent(t)}/alias`,{method:`PUT`,headers:{"content-type":`application/json`},body:JSON.stringify({alias:n.trim()||null})})).ok){X(!1,S(`models.aliasConflict`));return}await Ye(),X(!0,S(`models.aliasSaved`))}},Ze=async(t,n)=>{let r=Oe.models[t]?.[n]?.alias??``,i=window.prompt(S(`models.modelAliasPrompt`),r);if(i===null)return;let a=i.trim()?{set:{[n]:i.trim()}}:{remove:[n]};if(!(await fetch(`${e}/api/providers/${encodeURIComponent(t)}/model-aliases`,{method:`PUT`,headers:{"content-type":`application/json`},body:JSON.stringify(a)})).ok){X(!1,S(`models.aliasConflict`));return}await Ye(),X(!0,S(`models.aliasSaved`))},Qe=async(t,n)=>{(await fetch(`${e}/api/default-aliases`,{method:`PUT`,headers:{"content-type":`application/json`},body:JSON.stringify({enabled:t,...n?{provider:n}:{}})})).ok&&await Ye()},[$e,et]=(0,_.useState)(`add`),[tt,nt]=(0,_.useState)(``),[rt,it]=(0,_.useState)(``),[at,ot]=(0,_.useState)(``),[st,ct]=(0,_.useState)(``),[lt,ut]=(0,_.useState)(``),[dt,ft]=(0,_.useState)(!1),[pt,mt]=(0,_.useState)([`text`]),[ht,gt]=(0,_.useState)(!1),[_t,vt]=(0,_.useState)([]),yt=(0,_.useRef)(!1),[bt,xt]=(0,_.useState)(!1),[St,Ct]=(0,_.useState)(``),[wt,Et]=(0,_.useState)(null),[At,jt]=(0,_.useState)([]),[Mt,Nt]=(0,_.useState)(``),[It,Lt]=(0,_.useState)(``),[Bt,Vt]=(0,_.useState)({}),[Ht,Ut]=(0,_.useState)({contextWindow:null,modelContextWindows:{}}),[Wt,Gt]=(0,_.useState)(new Set),[Kt,qt]=(0,_.useState)(!1),[Jt,Yt]=(0,_.useState)(!1),[Xt,Zt]=(0,_.useState)(``),[Qt,$t]=(0,_.useState)(null),en=(0,_.useRef)(null),[tn,nn]=(0,_.useState)(null),[rn,an]=(0,_.useState)(!1),[on,sn]=(0,_.useState)(null);(0,_.useEffect)(()=>()=>{en.current&&clearTimeout(en.current)},[]);let cn=(0,_.useMemo)(()=>cp(T,k,j??{},S),[T,k,j,S]),ln=(0,_.useMemo)(()=>{let e=new Set(cn.map(e=>e.value));return pn(T.filter(t=>e.has(t.namespaced)),tn?.model,tn?.sourceModels)},[T,tn?.model,tn?.sourceModels,cn]),un=(0,_.useCallback)(async()=>{let t=Vn(15e3);try{let n=await Ft(await fetch(`${e}/api/shadow-call-settings`,{signal:t.signal}));n&&nn(n)}catch{}finally{t.clear()}},[e]),dn=(0,_.useCallback)(async()=>{if(Be.current)return;let t=Vn(15e3);try{let n=await fetch(`${e}/api/v2`,{signal:t.signal});if(!(n.headers.get(`content-type`)??``).includes(`application/json`)){Ce(null);return}let r=await Ft(n);if(!r||typeof r.enabled!=`boolean`){Ce(null);return}Ce({enabled:r.enabled,agentsMaxThreadsConflict:r.agentsMaxThreadsConflict===!0,maxConcurrentThreadsPerSession:typeof r.maxConcurrentThreadsPerSession==`number`?r.maxConcurrentThreadsPerSession:null,multiAgentMode:r.multiAgentMode===`v1`||r.multiAgentMode===`v2`?r.multiAgentMode:`default`,keepNativeChatGptOnV1:r.keepNativeChatGptOnV1===!0})}catch{Ce(null)}finally{t.clear(),Fe(!1)}},[e]),fn=(0,_.useCallback)(async t=>{let[n,r,i,a]=await Promise.all([fetch(`${e}/api/models`,{signal:t}),fetch(`${e}/api/provider-context-caps`,{signal:t}),fetch(`${e}/api/providers`,{signal:t}),Uf(e,fetch,t)]),[o,s,c]=await Promise.all([Pt(n),Pt(r),Pt(i)]);if(o===void 0||s===void 0||c===void 0)throw Error(`models payload missing`);if(t.aborted)throw Error(`models request aborted`);let l=sp(o),u=typeof s.value==`number`&&Number.isFinite(s.value)&&s.value>0?s.value:typeof s.cap==`number`&&Number.isFinite(s.cap)&&s.cap>0?s.cap:void 0,d=u===void 0?35e4:u,f={models:o,providers:c,selectedModels:a,disabled:[...l],contextCaps:s.caps??{},contextCapValue:d};return br(C,f),f},[e,C]),mn=(0,_.useCallback)(e=>{let t=zf(e.models,e.providers);sn(e=>e!==null&&!t.some(t=>t.provider===e)?null:e),E(e.models),O(e.providers),A(new Set(e.disabled)),M(e.selectedModels),B(e.contextCapValue),R(e.contextCaps)},[]),hn=ml(C,[e],async e=>{let t=await fn(e);if(e.aborted)throw Error(`models request aborted`);return mn(t),t},{isEmpty:()=>!1,pollMs:1e4,initialData:w??void 0,enabled:m,deadlineMs:6e4}),gn=hn.state,_n=(0,_.useCallback)(async(e=!1)=>{if(be.current&&!e)return!1;be.current=!0;let t=++ye.current;try{let e=await fn(new AbortController().signal);return qf(t,ye.current)?(mn(e),K(C,e),!0):!1}catch{return!1}finally{qf(t,ye.current)&&(be.current=!1)}},[mn,C,fn]);(0,_.useEffect)(()=>{if(!m)return;let e=window.setTimeout(()=>{un(),dn(),Wn(),Kn()},0),t=Gn(()=>{Be.current||dn()},1e4);return()=>{window.clearTimeout(e),t()}},[m,un,dn]);let vn=(0,_.useMemo)(()=>zf(T,D),[T,D]),yn=T.length>0||gn.data!==void 0,bn=e=>{let t=[...new Set([...e.rows.map(e=>e.id),...e.configuredModels,...Object.keys(e.modelContextWindows??{})])].sort(),n=t[0]??``;Et(e.provider),jt(t),Nt(n);let r=e.contextWindow?String(e.contextWindow):``,i=Object.fromEntries(Object.entries(e.modelContextWindows??{}).map(([e,t])=>[e,String(t)]));Lt(r),Vt(i),Ut({contextWindow:e.contextWindow??null,modelContextWindows:Object.fromEntries(Object.entries(e.modelContextWindows??{}).map(([e,t])=>[e,t]))}),Gt(new Set),qt(!1),Zt(``)},xn=e=>{Nt(e)},Sn=async()=>{if(!wt)return;let t=pp(It);if(!vn.find(e=>e.provider===wt)){Zt(S(`models.contextSaveFailed`));return}if(Kt&&t===void 0){Zt(S(`models.contextInvalid`));return}let n={};for(let e of Wt){let t=pp(Bt[e]??``);if(t===void 0){Zt(S(`models.contextInvalid`));return}t!==(Ht.modelContextWindows[e]??null)&&(n[e]=t)}let r=Kt&&t!==Ht.contextWindow;if(!r&&Object.keys(n).length===0){Et(null),X(!0,S(`models.contextUnchanged`));return}Yt(!0),Zt(``);try{let i={};r&&(i.contextWindow=t),Object.keys(n).length>0&&(i.modelContextWindows=n),await Pt(await fetch(`${e}/api/providers?name=${encodeURIComponent(wt)}`,{method:`PATCH`,headers:{"Content-Type":`application/json`},body:JSON.stringify(i)}),S(`models.contextSaveFailed`))}catch(e){Zt(e instanceof Error?e.message:S(`models.contextSaveFailed`));return}finally{Yt(!1)}Et(null),X(!0,S(`models.contextSaved`)),await _n(!0)};(0,_.useEffect)(()=>{if(!ae.current||vn.length===0)return;ae.current=!1;let e=new Set(vn.map(e=>e.provider));ie(e),up(e)},[vn]);let Cn=(0,_.useMemo)(()=>j?T.filter(e=>Gf(j,e.provider,e.id,e.native===!0,k.has(e.namespaced))).length:0,[k,T,j]),wn=(0,_.useMemo)(()=>({catalog:yn?S(`models.active`,{active:Cn,total:T.length}):void 0,combos:h===null?void 0:String(h),routing:v===null?void 0:String(v),compatibility:b===null?void 0:String(b)}),[yn,h,b,Cn,T.length,v,S]),Tn=async(t,n,r,i)=>{++ye.current,he(!0),ve.current=!0,se(``);let a=null;try{(await Kf(e,t,n,r,i)).ok||(a=`models.saveFailed`)}catch{a=`models.networkError`}finally{let e=await _n(!0);a?(le(!1),se(S(a))):e&&(le(!0),se(S(`models.applied`))),he(!1),ve.current=!1}},En=async(t,n=!1)=>{he(!0),ve.current=!0,se(``);let r=L[t]===void 0;try{let i=await fetch(`${e}/api/provider-context-caps`,{method:`PUT`,headers:{"Content-Type":`application/json`},body:JSON.stringify(r&&n?{provider:t,enabled:r,value:$f}:{provider:t,enabled:r})});try{let e=await Pt(i,S(`models.capSaveFailed`));R(e?.caps??{}),le(!0),se(S(`models.capApplied`)),await _n(!0)}catch(e){le(!1),se(e instanceof Error?e.message:S(`models.capSaveFailed`))}}catch{le(!1),se(S(`models.networkError`))}finally{he(!1),ve.current=!1}},Dn=e=>{ie(t=>{let n=new Set(t);return n.has(e)?n.delete(e):n.add(e),up(n),n})},On=e=>{ie(()=>{let t=e?new Set(vn.map(e=>e.provider)):new Set;return up(t),t})},kn=async t=>{he(!0),ve.current=!0,se(``);try{let n=await fetch(`${e}/api/provider-context-caps`,{method:`PUT`,headers:{"Content-Type":`application/json`},body:JSON.stringify(t)});try{let e=await Pt(n,S(`models.capSaveFailed`));typeof e?.value==`number`&&Number.isFinite(e.value)&&e.value>0&&B(e.value),R(e?.caps??{}),le(!0),se(S(`models.capApplied`)),await _n(!0)}catch(e){le(!1),se(e instanceof Error?e.message:S(`models.capSaveFailed`))}}catch{le(!1),se(S(`models.networkError`))}finally{he(!1),ve.current=!1}},An=(0,_.useMemo)(()=>{let e=vn.filter(e=>!e.native);return e.length>0&&e.every(e=>L[e.provider]===z)&&Object.keys(L).every(e=>L[e]===z)},[vn,L,z]),Mn=e=>{!Number.isSafeInteger(e)||e<=0||kn(An?{value:e,setAll:!0}:{value:e})},Fn=(e,t)=>{if(t===`custom`){G(t=>({...t,[e]:!0})),Y(t=>({...t,[e]:String(L[e]??z)}));return}G(t=>({...t,[e]:!1}));let n=Number(t);Number.isSafeInteger(n)&&n>0&&n!==L[e]&&kn({provider:e,enabled:!0,value:n})},In=e=>{let t=Number((q[e]??``).replace(/[_,\s]/g,``));if(!Number.isSafeInteger(t)||t<=0){X(!1,S(`models.capSaveFailed`));return}G(t=>({...t,[e]:!1})),kn({provider:e,enabled:!0,value:t})},Ln=e=>{if(e===`custom`){W(!0),H(String(z));return}W(!1);let t=Number(e);Number.isSafeInteger(t)&&t>0&&t!==z&&Mn(t)},Rn=()=>{let e=Number(V.replace(/[_,\s]/g,``));if(!Number.isSafeInteger(e)||e<=0){X(!1,S(`models.capSaveFailed`));return}W(!1),Mn(e)},zn=()=>{kn({setAll:!An})},Bn=async t=>{if(!(!tn||rn)){an(!0),nn({...tn,...t});try{await fetch(`${e}/api/shadow-call-settings`,{method:`PUT`,headers:{"Content-Type":`application/json`},body:JSON.stringify(t)})}finally{an(!1)}}},Hn=async t=>{if(!(!xe||Be.current)){Le(!0),Be.current=!0,ze(``),se(``);try{let n=await fetch(`${e}/api/v2`,{method:`PUT`,headers:{"Content-Type":`application/json`},body:JSON.stringify(t)});try{let e=await Pt(n,S(`models.saveFailed`));if(!e||typeof e.enabled!=`boolean`){le(!1),se(S(`models.saveFailed`));return}Ce({enabled:e.enabled,agentsMaxThreadsConflict:e.agentsMaxThreadsConflict===!0,maxConcurrentThreadsPerSession:typeof e.maxConcurrentThreadsPerSession==`number`?e.maxConcurrentThreadsPerSession:null,multiAgentMode:e.multiAgentMode===`v1`||e.multiAgentMode===`v2`?e.multiAgentMode:`default`,keepNativeChatGptOnV1:e.keepNativeChatGptOnV1===!0}),le(!0),se(S(`models.v2Applied`)),ze((e.warnings??[]).join(` `))}catch(e){le(!1),se(e instanceof Error?e.message:S(`models.saveFailed`))}}catch{le(!1),se(S(`models.networkError`))}finally{Le(!1),Be.current=!1}}},Un=async e=>{!xe||xe.multiAgentMode===e||await Hn({multiAgentMode:e})},Wn=async()=>{try{let t=Vn(15e3),n=await Ft(await fetch(`${e}/api/model-presets`,{signal:t.signal}));Te(n?.providers??{})}catch{Te({})}},Kn=async()=>{try{let t=await fetch(`${e}/api/model-discovery`);De(await Ft(t)??null)}catch{De(null)}},qn=async(t,n)=>{await Ft(await fetch(`${e}/api/model-discovery`,{method:`PUT`,headers:{"content-type":`application/json`},body:JSON.stringify({policy:t,provider:n??null})})),await Promise.all([Kn(),_n()])},Jn=async(t,n)=>{if(!Me){Ne(t);try{let r=Vn(3e4),i=await Ft(await fetch(`${e}/api/model-presets`,{method:`PUT`,headers:{"content-type":`application/json`},body:JSON.stringify({provider:t,mode:n}),signal:r.signal}))??{};i.fallback===`preset-empty`?X(!1,S(`models.presetEmpty`,{provider:t})):X(!0,n===`all`?S(`models.presetClearedToast`,{provider:t}):S(`models.presetAppliedToast`,{provider:t,count:String(i.selected?.length??0)})),await Promise.all([Wn(),_n()])}catch(e){X(!1,e instanceof Error?e.message:String(e))}finally{Ne(null)}}},Yn=async e=>{!xe||xe.keepNativeChatGptOnV1===e||await Hn({keepNativeChatGptOnV1:e})},Xn=async t=>{if(!(!xe||Be.current)){if(!Number.isInteger(t)||t<1){X(!1,S(`models.v2ThreadsInvalid`));return}if(xe.maxConcurrentThreadsPerSession!==t){Le(!0),Be.current=!0,ze(``),se(``);try{let n=await fetch(`${e}/api/v2`,{method:`PUT`,headers:{"Content-Type":`application/json`},body:JSON.stringify({maxConcurrentThreadsPerSession:t})});try{let e=await Pt(n,S(`models.saveFailed`));if(!e||typeof e.enabled!=`boolean`){le(!1),se(S(`models.saveFailed`));return}Ce({enabled:e.enabled,agentsMaxThreadsConflict:e.agentsMaxThreadsConflict===!0,maxConcurrentThreadsPerSession:typeof e.maxConcurrentThreadsPerSession==`number`?e.maxConcurrentThreadsPerSession:null,multiAgentMode:e.multiAgentMode===`v1`||e.multiAgentMode===`v2`?e.multiAgentMode:`default`,keepNativeChatGptOnV1:e.keepNativeChatGptOnV1===!0}),le(!0),se(S(`models.v2ThreadsApplied`)),We(!1)}catch(e){le(!1),se(e instanceof Error?e.message:S(`models.saveFailed`))}}catch{le(!1),se(S(`models.networkError`))}finally{Le(!1),Be.current=!1}}}},Zn=e=>{if(e===`custom`){We(!0),He(String(xe?.maxConcurrentThreadsPerSession??``));return}We(!1),Xn(Number(e))},Qn=(e,t)=>{en.current&&clearTimeout(en.current),en.current=setTimeout(()=>{$t({namespaced:e,rect:t.getBoundingClientRect()})},300)},$n=(e,t)=>{en.current&&clearTimeout(en.current),$t({namespaced:e,rect:t.getBoundingClientRect()})},er=()=>{en.current&&clearTimeout(en.current),en.current=setTimeout(()=>$t(null),120)},tr=()=>{en.current&&clearTimeout(en.current)},nr=async(t,n,r,i,a,o)=>{xt(!0),Ct(``);try{let s=await fetch(`${e}/api/custom-models`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({provider:t,modelId:n,displayName:r,contextWindow:i,inputModalities:a,reasoningEfforts:o})});try{await Pt(s,S(`models.customSaveFailed`)),Je(!1),X(!0,S(`models.customAdded`)),await _n(!0)}catch(e){Ct(e instanceof Error?e.message:S(`models.customSaveFailed`))}}catch{Ct(S(`models.networkError`))}finally{xt(!1)}},rr=async(t,n)=>{xt(!0),Ct(``);try{let r=await fetch(`${e}/api/custom-models/${encodeURIComponent(t)}`,{method:`PUT`,headers:{"Content-Type":`application/json`},body:JSON.stringify(n)});try{await Pt(r,S(`models.customSaveFailed`)),Je(!1),X(!0,S(`models.customUpdated`)),await _n(!0)}catch(e){Ct(e instanceof Error?e.message:S(`models.customSaveFailed`))}}catch{Ct(S(`models.networkError`))}finally{xt(!1)}},ir=async t=>{try{(await fetch(`${e}/api/custom-models/${encodeURIComponent(t)}`,{method:`DELETE`})).ok?(X(!0,S(`models.customDeleted`)),await _n(!0)):X(!1,S(`models.customSaveFailed`))}catch{X(!1,S(`models.networkError`))}},ar=gn.data??w,or=gn.kind===`failed-cold`?gn.error instanceof Error?gn.error.message:S(`models.loadFail`):null,sr=gn.showSkeleton&&!ar,cr=j??{},lr=e=>{let{provider:t,rows:n,nativeProviderGroup:r,liveModels:i,discovery:a}=e,o=ne.has(t),s=e=>Gf(cr,t,e.id,e.native===!0,k.has(e.namespaced)),c=n.filter(s).length,l=Ee?.recentArrivals[t]??[],u=new Set(l.map(e=>e.id)),d=L[t]!==void 0,f=L[t]??z,p=n.reduce((e,t)=>{let n=typeof t.contextWindow==`number`&&t.contextWindow>0?t.contextWindow:void 0;return n===void 0?e:e===void 0||n>e?n:e},void 0),m=d?f:r?Qf:p??f,h=e.nativeProviderGroup?ep:Xf,g=e.nativeProviderGroup?tp:Zf,_=i&&a?.status===`failed`?a:void 0,v=(N[t]??``).trim().toLowerCase(),y=v?n.filter(e=>e.id.toLowerCase().includes(v)):n,b=y.toSorted((e,t)=>Number(!s(e))-Number(!s(t))),x=F[t]??60,C=b.slice(0,x),w=y.length-C.length,T=n.length>0,E=!T||n.every(s),D=!T||n.every(e=>!s(e)),O=e=>{T&&Tn(`provider`,t,n.map(e=>({id:e.id,native:e.native===!0})),e)};return(0,J.jsxs)(`div`,{className:`card models-provider-card`,children:[(0,J.jsxs)(`div`,{className:`row group-head models-provider-head${o?``:` open`}`,children:[(0,J.jsxs)(`button`,{type:`button`,className:`row models-provider-toggle`,onClick:()=>Dn(t),"aria-expanded":!o,style:{flex:`1 1 auto`,border:0,background:`transparent`,padding:0,color:`inherit`,cursor:`pointer`,textAlign:`left`},children:[(0,J.jsx)(Se,{style:{width:14,height:14,color:`var(--muted)`,transform:o?`none`:`rotate(90deg)`,transition:`transform .12s`}}),(0,J.jsx)(`span`,{className:`text-body font-semibold`,style:{whiteSpace:`nowrap`},children:Nn(t)}),Oe.providers[t]&&(0,J.jsx)(`span`,{className:`models-chip mono text-caption`,children:Oe.providers[t]}),r&&(0,J.jsx)(`span`,{className:`models-chip muted mono text-caption`,children:S(`models.nativeGroupLabel`)}),_&&(0,J.jsx)(`span`,{className:`badge badge-amber`,role:`status`,title:Jf(S,_),children:S(`models.discoveryFailedBadge`)}),(0,J.jsx)(`span`,{className:`muted mono text-label`,children:S(`models.active`,{active:c,total:n.length})}),l.length>0&&(0,J.jsx)(`span`,{className:`models-chip mono text-caption`,children:S(`models.newCount`,{count:l.length})})]}),(0,J.jsxs)(`div`,{className:`row models-provider-actions`,children:[(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm models-alias-edit`,"aria-label":S(`models.editProviderAlias`),title:S(`models.editProviderAlias`),onClick:()=>void Xe(t),children:(0,J.jsx)(ge,{style:{width:14,height:14}})}),(0,J.jsx)(Tt,{on:Oe.defaults.providers[t]??Oe.defaults.global,onClick:()=>void Qe(!(Oe.defaults.providers[t]??Oe.defaults.global),t),label:S(`models.useDefaultAliases`),showLabel:!0}),(0,J.jsxs)(`button`,{type:`button`,className:`btn btn-ghost btn-sm text-caption`,onClick:e=>{e.stopPropagation(),et(`add`),nt(t),it(``),ot(``),ct(``),ut(``),ft(!1),mt([`text`]),gt(!1),vt([]),yt.current=!1,Ct(``),Je(!0)},"aria-haspopup":`dialog`,children:[(0,J.jsx)(`span`,{"aria-hidden":`true`,children:`+`}),` `,S(`models.customAdd`)]}),(()=>{let e=we[t];if(!e)return null;let n=Me===t,r=e.mode===`custom`&&e.appliedVersion!==void 0&&e.appliedVersion(0,J.jsx)(`button`,{type:`button`,role:`radio`,"aria-checked":e.mode===r,className:`btn btn-sm${e.mode===r?` btn-primary`:` btn-ghost`}`,style:{background:e.mode===r?void 0:`transparent`,color:e.mode===r?void 0:`var(--muted)`},disabled:me||n,onClick:n=>{n.stopPropagation(),!(r===`preset`&&e.mode===`custom`&&!confirm(S(`models.presetConfirmReplace`,{count:String(e.presetCount)})))&&Jn(t,r)},children:S(`models.presetMode_${r}`)},r)),e.mode===`custom`&&(0,J.jsx)(`button`,{type:`button`,role:`radio`,"aria-checked":!0,className:`btn btn-sm btn-primary`,disabled:!0,children:S(`models.presetMode_custom`)})]}),e.mode===`preset`&&(0,J.jsx)(`span`,{className:`muted mono text-label`,children:S(`models.presetSummary`,{count:String(e.presetCount),total:String(e.totalCount),version:String(e.availableVersion)})}),r&&(0,J.jsx)(`span`,{className:`badge badge-amber`,role:`status`,children:S(`models.presetUpdateAvailable`,{version:String(e.availableVersion)})})]})})(),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm text-caption`,disabled:me||E,onClick:()=>O(!0),children:S(`models.allOn`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm text-caption`,disabled:me||D,onClick:()=>O(!1),children:S(`models.allOff`)}),(0,J.jsxs)(`div`,{className:`models-cap-cluster`,children:[(0,J.jsx)(Tt,{on:d,onClick:()=>En(t,r),disabled:me,label:S(`models.contextCapLabel`),showLabel:!0}),(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)(Dt,{value:ee[t]?np:String(m),options:[...!g.has(m)&&!ee[t]?[{value:String(m),label:op(m)}]:[],...h.map(e=>({value:String(e),label:op(e)})),{value:np,label:S(`models.custom`)}],onChange:e=>Fn(t,e),disabled:me||!d,label:S(`models.capValue`,{value:op(m)}),title:S(`models.contextCapLabel`)}),d&&ee[t]&&(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)(`input`,{className:`input`,style:{width:120},inputMode:`numeric`,placeholder:S(`models.customPlaceholder`),value:q[t]??``,onChange:e=>Y(n=>({...n,[t]:e.target.value})),onKeyDown:e=>{e.key===`Enter`&&In(t)},disabled:me,"aria-label":S(`models.customPlaceholder`)}),(0,J.jsx)(`button`,{type:`button`,onClick:()=>In(t),disabled:me,className:`btn btn-ghost btn-sm`,children:S(`models.customApply`)})]})]}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm text-caption`,onClick:()=>bn(e),"aria-haspopup":`dialog`,children:S(`models.contextSettings`)})]})]})]}),!o&&(0,J.jsxs)(`div`,{className:`models-provider-body`,children:[r&&(0,J.jsx)(`p`,{className:`muted text-label models-provider-hint`,children:S(`models.nativeHint`)}),!r&&Ee&&(0,J.jsxs)(`div`,{className:`row models-provider-hint`,children:[(0,J.jsx)(`span`,{className:`muted text-label`,children:S(`models.newPolicyProvider`)}),(0,J.jsx)(`div`,{className:`segmented models-segmented`,role:`radiogroup`,"aria-label":S(`models.newPolicyProvider`),children:[`off`,`on`].map(e=>(0,J.jsx)(`button`,{type:`button`,role:`radio`,"aria-checked":(Ee.providers[t]??`inherit`)===e,className:`btn btn-sm${(Ee.providers[t]??`inherit`)===e?` btn-primary`:` btn-ghost`}`,onClick:()=>void qn(e,t),children:S(`models.newPolicy_${e}`)},e))})]}),n.length===0&&(0,J.jsx)(dp,{liveModels:i,discovery:a,showFailureBadge:!1}),n.length>30&&(0,J.jsx)(`input`,{className:`input`,placeholder:S(`models.search`),value:N[t]??``,onChange:e=>P(n=>({...n,[t]:e.target.value})),"aria-label":S(`models.search`)}),C.map(e=>{let n=!s(e);return(0,J.jsxs)(`div`,{className:`model-row-wrap`,onMouseEnter:t=>Qn(e.namespaced,t.currentTarget),onMouseLeave:er,onFocus:t=>$n(e.namespaced,t.currentTarget),onBlur:e=>{e.currentTarget.contains(e.relatedTarget)||$t(null)},children:[(0,J.jsxs)(`div`,{className:`row models-model-row`,children:[(0,J.jsx)(Tt,{on:!n,onClick:()=>void Tn(`models`,t,[{id:e.id,native:e.native===!0}],n),disabled:me,label:e.native?e.id:e.namespaced}),Oe.models[t]?.[e.id]&&(0,J.jsx)(`strong`,{className:`mono text-control`,children:Oe.models[t][e.id].alias}),(0,J.jsx)(`code`,{className:`mono text-control`,style:{color:n?`var(--faint)`:`var(--text)`,textDecoration:n?`line-through`:`none`},children:e.native?fl(e.id):Pn(e.namespaced,S)}),Oe.models[t]?.[e.id]?.source===`builtin`&&(0,J.jsx)(`span`,{className:`models-chip muted text-caption`,children:S(`models.aliasAuto`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,"aria-label":S(`models.editModelAlias`),title:S(`models.editModelAlias`),onClick:()=>void Ze(t,e.id),children:(0,J.jsx)(ge,{style:{width:13,height:13}})}),e.custom&&(0,J.jsx)(`span`,{className:`models-chip muted mono text-caption`,children:S(`models.customBadge`)}),!e.custom&&u.has(e.id)&&(0,J.jsx)(`span`,{className:`badge badge-amber`,children:S(`models.newBadge`)}),e.contextCapped&&(0,J.jsx)(`span`,{className:`models-chip muted mono text-caption`,children:S(`models.contextCappedValue`,{value:op(e.contextCap??z)})})]}),Qt?.namespaced===e.namespaced&&(()=>{let t=Qt.rect,r=t.bottom+4,i=r+360>window.innerHeight;return(0,J.jsxs)(`div`,{className:`model-tip${e.custom?` has-actions`:``}${i?` flip-up`:``}`,role:`tooltip`,style:{position:`fixed`,left:t.left+24,...i?{bottom:window.innerHeight-t.top+4}:{top:r}},onMouseEnter:tr,onMouseLeave:er,children:[(0,J.jsx)(`div`,{className:`model-tip-id`,children:e.native?e.id:e.namespaced}),e.displayName&&(0,J.jsx)(`div`,{className:`model-tip-display`,children:e.displayName}),e.custom&&(0,J.jsx)(`span`,{className:`models-chip models-chip--tip muted mono text-caption`,children:S(`models.customBadge`)}),(0,J.jsxs)(`div`,{className:`model-tip-grid`,children:[(0,J.jsx)(`span`,{className:`model-tip-key`,children:S(`models.tipProvider`)}),(0,J.jsx)(`span`,{className:`model-tip-val`,children:jn(e.provider,S)}),(e.contextWindow||e.contextCap)&&(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)(`span`,{className:`model-tip-key`,children:S(`models.tipContext`)}),(0,J.jsx)(`span`,{className:`model-tip-val`,children:op(e.contextWindow??e.contextCap??0)})]}),e.inputModalities&&e.inputModalities.length>0&&(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)(`span`,{className:`model-tip-key`,children:S(`models.tipModalities`)}),(0,J.jsx)(`span`,{className:`model-tip-val`,children:e.inputModalities.join(`, `)})]}),(0,J.jsx)(`span`,{className:`model-tip-key`,children:S(`models.tipStatus`)}),(0,J.jsx)(`span`,{className:`model-tip-val`,children:S(n?`models.tipDisabled`:`models.tipActive`)})]}),e.custom&&e.customId&&(0,J.jsxs)(`div`,{className:`model-tip-actions`,children:[(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm text-caption`,onClick:()=>{et(`edit`),nt(e.provider),it(e.customId),ot(e.id),ct(e.displayName??``),ut(e.contextWindow?String(e.contextWindow):``),ft(!1),mt(e.inputModalities??[`text`]),gt(Array.isArray(e.reasoningEfforts)),vt(e.reasoningEfforts??[]),yt.current=Array.isArray(e.reasoningEfforts),Ct(``),Je(!0),$t(null)},children:S(`models.customEdit`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm text-caption`,style:{color:`var(--red)`},onClick:()=>{window.confirm(S(`models.customDeleteConfirm`,{name:e.displayName??e.id}))&&ir(e.customId),$t(null)},children:S(`models.customDelete`)})]})]})})()]},e.namespaced)}),w>0&&(0,J.jsx)(`button`,{type:`button`,onClick:()=>I(e=>({...e,[t]:x+60})),className:`btn btn-ghost btn-sm models-show-more`,children:S(`models.showMore`,{n:w})})]})]},t)},ur=on?vn.filter(e=>e.provider===on):vn,dr=(0,J.jsxs)(J.Fragment,{children:[(0,J.jsxs)(`div`,{className:`models-control-top-row`,children:[Ee&&(0,J.jsxs)(`div`,{className:`models-shadow-row row muted text-control`,children:[(0,J.jsx)(`span`,{className:`models-shadow-label`,children:S(`models.newPolicyGlobal`)}),(0,J.jsx)(Tt,{on:Ee.policy===`off`,onClick:()=>void qn(Ee.policy===`off`?`on`:`off`),label:S(`models.newPolicyGlobal`)})]}),(0,J.jsxs)(`div`,{className:`row`,children:[(0,J.jsx)(Tt,{on:Oe.defaults.global,onClick:()=>void Qe(!Oe.defaults.global),label:S(`models.useDefaultAliasesGlobal`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:()=>je(e=>!e),children:S(`models.aliases`)})]}),(0,J.jsxs)(`div`,{className:`models-shadow-row row muted text-control`,"aria-busy":!tn||void 0,children:[(0,J.jsxs)(`span`,{className:`models-shadow-label`,children:[S(`models.shadowCallIntercept`),` `,(0,J.jsx)(kt,{content:S(`models.shadowCallInterceptHint`,{models:Rt(tn?.sourceModels)}),side:`top`,maxWidth:320,children:(0,J.jsx)(`span`,{style:{cursor:`help`},"aria-label":S(`models.shadowCallInterceptHint`,{models:Rt(tn?.sourceModels)}),children:`ⓘ`})})]}),(0,J.jsx)(`code`,{className:`text-caption models-shadow-warning`,style:{opacity:.6},children:S(`models.shadowCallOriginal`,{models:zt(tn?.sourceModels)})}),(0,J.jsx)(Tt,{on:tn?.enabled??!1,onClick:()=>void Bn({enabled:!tn?.enabled}),disabled:!tn||rn,label:S(`models.shadowCallIntercept`)}),(0,J.jsx)(`div`,{className:`models-shadow-model-slot`,children:(0,J.jsx)(Dt,{value:tn?.model??``,options:ln,onChange:e=>{nn(t=>t&&{...t,model:e}),Bn({model:e})},disabled:!tn||rn||!tn.enabled,label:S(`models.shadowCallIntercept`)})})]}),(Pe||xe)&&(0,J.jsxs)(`div`,{className:`models-v2-mode-row row`,children:[(0,J.jsx)(`span`,{className:`muted text-control`,children:S(`models.v2Label`)}),(0,J.jsx)(`div`,{className:`segmented models-segmented`,role:`radiogroup`,"aria-label":S(`models.v2Label`),children:[`v1`,`default`,`v2`].map(e=>(0,J.jsx)(`button`,{type:`button`,role:`radio`,"aria-checked":(xe?.multiAgentMode??`default`)===e,className:`btn btn-sm${(xe?.multiAgentMode??`default`)===e?` btn-primary`:` btn-ghost`}`,style:{background:(xe?.multiAgentMode??`default`)===e?void 0:`transparent`,color:(xe?.multiAgentMode??`default`)===e?void 0:`var(--muted)`},disabled:!xe||Ie,onClick:()=>void Un(e),children:S(`models.v2Mode_${e}`)},e))}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,style:{width:24,height:24,minWidth:24,flex:`0 0 24px`,padding:0,borderRadius:`var(--radius-pill)`,color:`var(--muted)`},disabled:!xe,onClick:()=>Ke(!0),"aria-label":S(`models.v2Label`),"aria-haspopup":`dialog`,children:(0,J.jsx)(Z,{width:14,height:14,"aria-hidden":`true`})})]}),xe&&xe.multiAgentMode===`v2`&&(0,J.jsx)(`div`,{className:`models-v2-keep-native-row`,children:(0,J.jsxs)(`div`,{className:`models-v2-keep-native`,children:[(0,J.jsx)(`span`,{className:`models-v2-keep-native-label text-caption`,children:S(`models.keepNativeOnV1`)}),(0,J.jsx)(Tt,{on:xe.keepNativeChatGptOnV1===!0,onClick:()=>void Yn(!xe.keepNativeChatGptOnV1),disabled:Ie,label:S(`models.keepNativeOnV1`)}),(0,J.jsx)(kt,{content:S(`models.keepNativeOnV1Hint`),side:`top`,maxWidth:360,children:(0,J.jsx)(`span`,{className:`models-v2-keep-native-info`,"aria-label":S(`models.keepNativeOnV1Hint`),children:(0,J.jsx)(Z,{width:13,height:13,"aria-hidden":`true`})})})]})})]}),xe&&(xe.enabled||xe.agentsMaxThreadsConflict||Re)&&(0,J.jsxs)(`div`,{className:`models-v2-detail-row row`,children:[xe.enabled&&(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)(`span`,{className:`muted text-control`,children:S(`models.v2ThreadsLabel`)}),(0,J.jsx)(Dt,{value:Ue?`custom`:xe.maxConcurrentThreadsPerSession!==null&&xe.maxConcurrentThreadsPerSession!==void 0?ip.has(xe.maxConcurrentThreadsPerSession)?String(xe.maxConcurrentThreadsPerSession):`custom`:``,options:[...xe.maxConcurrentThreadsPerSession===null||xe.maxConcurrentThreadsPerSession===void 0?[{value:``,label:S(`models.v2ThreadsDefault`)}]:[],...xe.maxConcurrentThreadsPerSession!==null&&xe.maxConcurrentThreadsPerSession!==void 0&&!ip.has(xe.maxConcurrentThreadsPerSession)&&!Ue?[{value:`custom`,label:String(xe.maxConcurrentThreadsPerSession)}]:[],...rp.map(e=>({value:String(e),label:String(e)})),{value:`custom`,label:S(`models.custom`)}],onChange:e=>Zn(e),disabled:Ie,label:S(`models.v2ThreadsLabel`)}),Ue&&(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)(`input`,{className:`input`,style:{width:100},inputMode:`numeric`,value:Ve,onChange:e=>He(e.target.value),onKeyDown:e=>{e.key===`Enter`&&Xn(Number(Ve.replace(/[_,\s]/g,``)))},disabled:Ie,"aria-label":S(`models.v2ThreadsLabel`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-sm`,disabled:Ie,onClick:()=>{Xn(Number(Ve.replace(/[_,\s]/g,``)))},children:S(`models.v2ThreadsApply`)})]})]}),xe.enabled&&xe.agentsMaxThreadsConflict&&(0,J.jsx)(`span`,{className:`mono text-label`,style:{color:`var(--err, #e5484d)`},children:S(`models.v2Conflict`)}),Re&&(0,J.jsx)(`span`,{className:`muted text-label`,children:Re})]}),(0,J.jsxs)(`div`,{className:`row models-cap-row`,children:[(0,J.jsx)(`span`,{className:`muted text-control`,children:S(`models.contextCapLabel`)}),(0,J.jsx)(Dt,{value:U?np:String(z),options:[...!Zf.has(z)&&!U?[{value:String(z),label:op(z)}]:[],...Xf.map(e=>({value:String(e),label:op(e)})),{value:np,label:S(`models.custom`)}],onChange:e=>Ln(e),disabled:me,label:S(`models.contextCapLabel`)}),U&&(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)(`input`,{className:`input`,style:{width:160},inputMode:`numeric`,placeholder:S(`models.customPlaceholder`),value:V,onChange:e=>H(e.target.value),onKeyDown:e=>{e.key===`Enter`&&Rn()},disabled:me,"aria-label":S(`models.customPlaceholder`)}),(0,J.jsx)(`button`,{type:`button`,onClick:Rn,disabled:me,className:`btn btn-ghost btn-sm`,children:S(`models.customApply`)})]}),(0,J.jsx)(Tt,{on:An,onClick:zn,disabled:me,label:S(`models.setAll`)}),(0,J.jsx)(`span`,{className:`muted text-label leading-body`,children:S(`models.setAllHint`,{value:op(z)})})]}),(()=>{let e=T.filter(e=>e.custom).length;return e===0?null:(0,J.jsx)(`div`,{className:`row muted text-label models-custom-summary`,children:(0,J.jsx)(`span`,{className:`models-chip mono text-caption`,children:S(`models.customSummary`,{count:e})})})})(),(0,J.jsxs)(`div`,{className:`row muted text-label leading-body models-order-hint`,children:[(0,J.jsx)(Z,{width:15,height:15,"aria-hidden":`true`}),(0,J.jsx)(`span`,{children:S(`models.orderHint`)})]})]}),fr=(0,J.jsxs)(`div`,{className:`row models-collapse-controls`,children:[(0,J.jsxs)(`button`,{type:`button`,className:`btn btn-ghost btn-sm text-caption`,onClick:()=>On(!0),disabled:me,children:[(0,J.jsx)(Se,{width:12,height:12,"aria-hidden":`true`}),` `,S(`models.collapseAll`)]}),(0,J.jsxs)(`button`,{type:`button`,className:`btn btn-ghost btn-sm text-caption`,onClick:()=>On(!1),disabled:me,children:[(0,J.jsx)(Se,{width:12,height:12,"aria-hidden":`true`,style:{transform:`rotate(90deg)`}}),` `,S(`models.expandAll`)]})]}),pr=(0,J.jsx)(J.Fragment,{children:vn.length===0&&(0,J.jsx)(Ot,{icon:(0,J.jsx)(re,{}),title:S(`models.noRouted`),children:S(`models.noRoutedHint`)})}),mr=(0,J.jsxs)(J.Fragment,{children:[Ge&&(0,J.jsx)(`div`,{className:`modal-overlay`,role:`dialog`,"aria-modal":`true`,"aria-label":S(`models.v2Label`),onClick:()=>Ke(!1),onKeyDown:e=>{e.key===`Escape`&&Ke(!1)},children:(0,J.jsxs)(`div`,{className:`modal-card`,onClick:e=>e.stopPropagation(),children:[(0,J.jsxs)(`div`,{className:`modal-head`,children:[(0,J.jsx)(`h3`,{children:S(`models.v2Label`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:()=>Ke(!1),"aria-label":S(`common.close`),children:`×`})]}),(0,J.jsx)(`div`,{className:`modal-desc leading-relaxed`,style:{whiteSpace:`pre-line`},children:S(`models.v2Help`)}),(0,J.jsx)(`div`,{className:`models-help-link`,children:(0,J.jsx)(`a`,{className:`text-control`,href:`https://opencodex.me/guides/sub-agent-surface/`,target:`_blank`,rel:`noreferrer`,style:{color:`var(--accent)`},children:S(`models.v2DocsLink`)})}),(0,J.jsx)(`div`,{className:`modal-actions`,children:(0,J.jsx)(`button`,{type:`button`,className:`btn btn-primary`,onClick:()=>Ke(!1),children:S(`common.ok`)})})]})}),wt&&(0,J.jsx)(`div`,{className:`modal-overlay`,role:`dialog`,"aria-modal":`true`,"aria-label":S(`models.contextSettings`),onClick:()=>{Jt||Et(null)},onKeyDown:e=>{e.key===`Escape`&&!Jt&&Et(null)},children:(0,J.jsxs)(`div`,{className:`modal-card`,onClick:e=>e.stopPropagation(),children:[(0,J.jsxs)(`div`,{className:`modal-head`,children:[(0,J.jsx)(`h3`,{children:S(`models.contextSettingsTitle`,{provider:jn(wt,S)})}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:()=>Et(null),disabled:Jt,"aria-label":S(`common.close`),children:`×`})]}),Xt&&(0,J.jsx)($,{tone:`err`,children:Xt}),(0,J.jsx)(`p`,{className:`modal-desc leading-relaxed`,children:S(`models.contextHint`)}),(0,J.jsxs)(`div`,{className:`models-context-fields`,children:[(0,J.jsxs)(`label`,{className:`text-label models-field`,children:[S(`models.contextDefault`),(0,J.jsx)(`input`,{className:`input`,inputMode:`numeric`,value:It,onChange:e=>{Lt(e.target.value),qt(!0)},disabled:Jt,placeholder:S(`models.contextAutomatic`),autoFocus:!0})]}),At.length>0&&(0,J.jsxs)(J.Fragment,{children:[(0,J.jsxs)(`div`,{className:`text-label models-field`,children:[S(`models.contextModel`),(0,J.jsx)(Dt,{value:Mt,options:At.map(e=>({value:e,label:e})),onChange:xn,disabled:Jt,label:S(`models.contextModel`)})]}),(0,J.jsxs)(`label`,{className:`text-label models-field`,children:[S(`models.contextModelOverride`),(0,J.jsx)(`input`,{className:`input`,inputMode:`numeric`,value:Bt[Mt]??``,onChange:e=>{Vt(t=>({...t,[Mt]:e.target.value})),Gt(e=>new Set(e).add(Mt))},disabled:Jt,placeholder:S(`models.contextAutomatic`)})]})]})]}),(0,J.jsxs)(`div`,{className:`modal-actions`,children:[(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost`,onClick:()=>Et(null),disabled:Jt,children:S(`common.cancel`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-primary`,onClick:()=>void Sn(),disabled:Jt,children:S(Jt?`models.customSaving`:`models.customApply`)})]})]})}),qe&&(0,J.jsx)(`div`,{className:`modal-overlay`,role:`dialog`,"aria-modal":`true`,"aria-label":S(`models.customAdd`),onClick:()=>{bt||Je(!1)},onKeyDown:e=>{e.key===`Escape`&&!bt&&Je(!1)},children:(0,J.jsxs)(`div`,{className:`modal-card`,onClick:e=>e.stopPropagation(),children:[(0,J.jsxs)(`div`,{className:`modal-head`,children:[(0,J.jsx)(`h3`,{children:S($e===`add`?`models.customAddTitle`:`models.customEditTitle`,{provider:jn(tt,S)})}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:()=>Je(!1),disabled:bt,"aria-label":S(`common.close`),children:`×`})]}),St&&(0,J.jsx)($,{tone:`err`,children:St}),(0,J.jsxs)(`div`,{className:`models-field-stack`,children:[(0,J.jsxs)(`label`,{className:`text-label models-field`,children:[S(`models.customFieldModelId`),(0,J.jsx)(`input`,{className:`input`,value:at,onChange:e=>ot(e.target.value),disabled:bt,placeholder:S(`models.customFieldModelIdPlaceholder`),autoFocus:!0})]}),(0,J.jsxs)(`label`,{className:`text-label models-field`,children:[S(`models.customFieldDisplayName`),(0,J.jsx)(`input`,{className:`input`,value:st,onChange:e=>ct(e.target.value),disabled:bt,placeholder:S(`models.customFieldDisplayNamePlaceholder`)})]}),(0,J.jsxs)(`label`,{className:`text-label models-field`,children:[S(`models.customFieldContext`),(0,J.jsxs)(`div`,{className:`row models-field-row`,children:[(0,J.jsx)(Dt,{value:dt?`custom`:lt,options:[{value:``,label:`—`},{value:`100000`,label:`100k`},{value:`128000`,label:`128k`},{value:`200000`,label:`200k`},{value:`256000`,label:`256k`},{value:`352000`,label:`352k`},{value:`500000`,label:`500k`},{value:`1000000`,label:`1M`},{value:`custom`,label:S(`models.custom`)}],onChange:e=>{if(e===`custom`){ft(!0);return}ft(!1),ut(e)},disabled:bt,label:S(`models.customFieldContext`)}),dt&&(0,J.jsx)(`input`,{className:`input`,style:{width:120},inputMode:`numeric`,value:lt,onChange:e=>ut(e.target.value),disabled:bt,placeholder:S(`models.customPlaceholder`),"aria-label":S(`models.customFieldContext`)})]})]}),(0,J.jsxs)(`div`,{className:`text-label models-field`,children:[S(`models.customFieldModalities`),(0,J.jsx)(`div`,{className:`row models-field-row`,children:[`text`,`image`,`audio`].map(e=>(0,J.jsxs)(`label`,{className:`row models-modality-option`,children:[(0,J.jsx)(`input`,{type:`checkbox`,checked:pt.includes(e),onChange:t=>{mt(n=>t.target.checked?[...n,e]:n.filter(t=>t!==e))},disabled:bt}),(0,J.jsx)(`span`,{className:`text-control`,children:e})]},e))})]}),(0,J.jsxs)(`div`,{className:`text-label models-field`,children:[S(`models.customFieldReasoning`),(0,J.jsx)(`div`,{className:`row models-field-row`,children:(0,J.jsxs)(`label`,{className:`row models-modality-option`,children:[(0,J.jsx)(`input`,{type:`checkbox`,checked:ht,onChange:e=>{if(gt(e.target.checked),e.target.checked&&!yt.current){yt.current=!0;let e=T.find(e=>e.provider===tt&&e.id===at),t=Array.isArray(e?.reasoningEfforts)?e.reasoningEfforts:void 0;vt(t??[...Yf])}},disabled:bt}),(0,J.jsx)(`span`,{className:`text-control`,children:S(`models.customFieldReasoningOverride`)})]})}),ht&&(0,J.jsx)(`div`,{className:`row models-field-row`,style:{flexWrap:`wrap`},children:Yf.map(e=>(0,J.jsxs)(`label`,{className:`row models-modality-option`,children:[(0,J.jsx)(`input`,{type:`checkbox`,checked:_t.includes(e),onChange:t=>{vt(n=>t.target.checked?[...n,e]:n.filter(t=>t!==e))},disabled:bt}),(0,J.jsx)(`span`,{className:`text-control`,children:S(`models.reasoningEffort.${e}`)})]},e))})]})]}),(0,J.jsxs)(`div`,{className:`modal-actions`,children:[(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost`,onClick:()=>Je(!1),disabled:bt,children:S(`common.cancel`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-primary`,disabled:bt||!at.trim(),onClick:()=>{let e=at.trim(),t=st.trim(),n=lt?Number(lt.replace(/[_,\s]/g,``)):void 0,r=n&&n>0?Math.floor(n):void 0;if($e===`add`){let n=ht?_t:void 0;nr(tt,e,t||void 0,r,pt.length>0?pt:void 0,n)}else rr(rt,{modelId:e,displayName:t,contextWindow:r??null,inputModalities:pt,reasoningEfforts:ht?_t:null})},children:S(bt?`models.customSaving`:$e===`add`?`models.customAddBtn`:`models.customEditBtn`)})]})]})})]}),hr=(0,J.jsxs)(`div`,{className:`models-workspace-shell`,children:[oe&&(0,J.jsxs)(`div`,{className:`action-toast notice ${ce?`notice-ok`:`notice-err`}`,role:`status`,"aria-live":`polite`,children:[ce?(0,J.jsx)(ue,{}):(0,J.jsx)(_e,{}),(0,J.jsx)(`span`,{children:oe})]}),gn.showError&&(0,J.jsx)($,{tone:`err`,children:S(`models.loadFail`)}),(0,J.jsxs)(`div`,{className:`models-workspace-root`,"aria-busy":gn.refreshing||void 0,children:[(0,J.jsxs)(`aside`,{className:`models-workspace-rail`,"aria-label":S(`nav.models`),children:[(0,J.jsxs)(`div`,{className:`models-workspace-rail-header`,children:[(0,J.jsx)(`span`,{className:`models-workspace-rail-title`,children:S(`models.workspace.providers`)}),(0,J.jsx)(`span`,{className:`models-workspace-rail-count`,children:vn.length})]}),(0,J.jsxs)(`div`,{className:`models-workspace-rail-list`,children:[(0,J.jsxs)(`button`,{type:`button`,className:`models-workspace-rail-row${on===null?` models-workspace-rail-row--selected`:``}`,onClick:()=>sn(null),"aria-current":on===null?`true`:void 0,children:[(0,J.jsx)(`span`,{className:`models-workspace-rail-name`,children:S(`models.workspace.allProviders`)}),(0,J.jsx)(`span`,{className:`models-workspace-rail-meta`,children:S(`models.active`,{active:Cn,total:T.length})})]}),vn.map(e=>{let{provider:t,rows:n}=e,r=n.filter(e=>Gf(cr,t,e.id,e.native===!0,k.has(e.namespaced))).length;return(0,J.jsxs)(`button`,{type:`button`,className:`models-workspace-rail-row${on===t?` models-workspace-rail-row--selected`:``}`,onClick:()=>sn(t),"aria-current":on===t?`true`:void 0,children:[(0,J.jsx)(`span`,{className:`models-workspace-rail-name`,children:jn(t,S)}),(0,J.jsx)(`span`,{className:`models-workspace-rail-meta`,children:S(`models.active`,{active:r,total:n.length})})]},t)})]})]}),(0,J.jsxs)(`section`,{className:`models-workspace-main`,"aria-label":S(`models.workspace.mainAria`),children:[dr,fr,Ae&&(0,J.jsxs)(`div`,{className:`card`,"aria-label":S(`models.aliasesTable`),children:[(0,J.jsx)(`div`,{className:`row group-head`,children:(0,J.jsx)(`strong`,{children:S(`models.aliases`)})}),Object.entries(Oe.models).flatMap(([e,t])=>Object.entries(t).map(([t,n])=>(0,J.jsxs)(`div`,{className:`row models-model-row`,children:[(0,J.jsxs)(`code`,{className:`mono text-caption`,style:{flex:1},children:[e,`/`,t]}),(0,J.jsx)(`strong`,{className:`mono text-control`,children:n.alias}),(0,J.jsx)(`span`,{className:`models-chip muted text-caption`,children:n.source===`builtin`?S(`models.aliasAuto`):S(`models.aliasUser`)}),n.stale&&(0,J.jsx)(`span`,{className:`badge badge-amber`,children:S(`models.aliasStale`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,"aria-label":S(`models.editModelAlias`),onClick:()=>void Ze(e,t),children:(0,J.jsx)(ge,{style:{width:13,height:13}})})]},`${e}/${t}`)))]}),(0,J.jsx)(`div`,{className:`models-provider-list`,children:ur.map(e=>lr(e))}),vn.length===0&&pr]})]}),mr]});return(0,J.jsxs)(J.Fragment,{children:[(0,J.jsxs)(`div`,{className:`page-head`,children:[(0,J.jsx)(`h2`,{children:S(`nav.models`)}),(0,J.jsx)(`div`,{className:`page-head-actions`,children:(0,J.jsx)(`button`,{type:`button`,className:`sidebar-orb`,onClick:()=>{s()},disabled:o,"aria-label":S(o?`dash.codexRestarting`:`dash.codexRestart`),title:S(o?`dash.codexRestarting`:`dash.codexRestart`),children:(0,J.jsx)(pe,{})})})]}),(0,J.jsx)(Yc,{state:n,controller:{restarting:o,restart:s}}),(0,J.jsx)(Rf,{tab:c,onSelect:p,meta:wn}),(0,J.jsx)(`p`,{className:`page-sub`,children:S(fp[c])}),(0,J.jsx)(`div`,{className:`models-tab-panel`,role:`tabpanel`,id:If(`catalog`),"aria-labelledby":Ff(`catalog`),hidden:c!==`catalog`,children:(0,J.jsx)(vl,{pageName:S(`models.tab.catalog`),title:S(`errorBoundary.title`),message:S(`errorBoundary.message`),detailsLabel:S(`errorBoundary.details`),reloadLabel:S(`errorBoundary.reload`),children:sr?(0,J.jsx)(gl,{label:S(`models.loading`),rows:5}):or===null?hr:(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)($,{tone:`err`,children:or}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:()=>hn.refresh(),children:S(`common.retry`)})]})})}),(0,J.jsx)(`div`,{className:`models-tab-panel models-tab-panel--fill`,role:`tabpanel`,id:If(`combos`),"aria-labelledby":Ff(`combos`),hidden:c!==`combos`,children:u.has(`combos`)&&(0,J.jsx)(vl,{pageName:S(`models.tab.combos`),title:S(`errorBoundary.title`),message:S(`errorBoundary.message`),detailsLabel:S(`errorBoundary.details`),reloadLabel:S(`errorBoundary.reload`),children:(0,J.jsx)(Mu,{apiBase:e,active:c===`combos`,onCountChange:g})})}),(0,J.jsx)(`div`,{className:`models-tab-panel`,role:`tabpanel`,id:If(`routing`),"aria-labelledby":Ff(`routing`),hidden:c!==`routing`,children:u.has(`routing`)&&(0,J.jsx)(vl,{pageName:S(`models.tab.routing`),title:S(`errorBoundary.title`),message:S(`errorBoundary.message`),detailsLabel:S(`errorBoundary.details`),reloadLabel:S(`errorBoundary.reload`),children:(0,J.jsx)(_d,{apiBase:e,active:c===`routing`,onCountChange:y})})}),(0,J.jsx)(`div`,{className:`models-tab-panel`,role:`tabpanel`,id:If(`compatibility`),"aria-labelledby":Ff(`compatibility`),hidden:c!==`compatibility`,children:u.has(`compatibility`)&&(0,J.jsx)(vl,{pageName:S(`models.tab.compatibility`),title:S(`errorBoundary.title`),message:S(`errorBoundary.message`),detailsLabel:S(`errorBoundary.details`),reloadLabel:S(`errorBoundary.reload`),children:(0,J.jsx)(Af,{apiBase:e,active:c===`compatibility`,onCountChange:x})})})]})}var hp=`section`;function gp(e,t){return[e,hp,t].join(`-`)}function _p(e){return[e,hp,``].join(`-`)}var vp=1200;function yp({scope:e,items:t,ariaLabel:n}){let[r,i]=(0,_.useState)(t[0]?.id??``),a=(0,_.useRef)(null),o=(0,_.useRef)(null),s=(0,_.useCallback)(()=>{a.current=null,o.current!==null&&(clearTimeout(o.current),o.current=null)},[]),c=(0,_.useCallback)(()=>{s();let n=null,r=1/0;for(let i of t){let t=document.getElementById(gp(e,i.id));if(!t)continue;let a=Math.abs(t.getBoundingClientRect().top-72);a()=>s(),[s]),(0,_.useEffect)(()=>{if(typeof IntersectionObserver>`u`)return;let n=t.map(t=>document.getElementById(gp(e,t.id))).filter(e=>e!==null);if(n.length===0)return;let r=new IntersectionObserver(t=>{let n=a.current;if(n){let r=document.getElementById(gp(e,n));t.some(e=>e.isIntersecting&&e.target===r)&&(s(),i(n));return}let r=t.filter(e=>e.isIntersecting).sort((e,t)=>e.boundingClientRect.top-t.boundingClientRect.top)[0];if(!r)return;let o=r.target.id.slice(_p(e).length);i(e=>e===o?e:o)},{rootMargin:`-72px 0px -60% 0px`,threshold:0});for(let e of n)r.observe(e);return()=>r.disconnect()},[s,t,e]);let l=t=>{let n=document.getElementById(gp(e,t));n&&(a.current=t,o.current!==null&&clearTimeout(o.current),o.current=setTimeout(c,vp),i(t),n.scrollIntoView({behavior:`smooth`,block:`start`}))};return(0,J.jsx)(`div`,{className:`page-tabs section-tabs`,role:`tablist`,"aria-label":n,children:t.map(t=>(0,J.jsxs)(`button`,{type:`button`,role:`tab`,"aria-selected":r===t.id,"aria-controls":gp(e,t.id),tabIndex:r===t.id?0:-1,className:`page-tab${r===t.id?` page-tab--active`:``}`,onClick:()=>l(t.id),children:[t.label,t.meta?(0,J.jsx)(`span`,{className:`section-tab-meta`,children:t.meta}):null]},t.id))})}function bp({model:e,effort:t,efforts:n,available:r,guidanceEnabled:i,syncCodexDefaults:a,saving:o,onSave:s,ultraMode:c,ultraSaving:l,onUltraModeSave:u,ultraLoadFailed:d,onUltraModeRetry:f}){let p=Q(),m=(c.hintText??``).trim().length>0;return(0,J.jsxs)(`div`,{className:`swi-delegation`,children:[d&&(0,J.jsxs)(`div`,{className:`swi-delegation-row`,children:[(0,J.jsxs)(`div`,{className:`setting-copy`,children:[(0,J.jsx)(`div`,{className:`font-semibold`,children:p(`sub.ultraMode`)}),(0,J.jsx)(`div`,{className:`muted setting-hint`,children:p(`sub.ultraModeLoadFail`)})]}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:f,children:p(`common.retry`)})]}),(0,J.jsxs)(`div`,{className:`swi-delegation-row`,children:[(0,J.jsxs)(`div`,{className:`setting-copy`,children:[(0,J.jsx)(`div`,{className:`font-semibold`,children:p(`sub.delegation.model`)}),(0,J.jsx)(`div`,{className:`muted setting-hint`,children:p(`sub.delegation.modelHint`)})]}),(0,J.jsxs)(`div`,{className:`swi-delegation-controls`,children:[(0,J.jsx)(Dt,{value:e,options:[{value:``,label:p(`dash.injectionNone`)},...r.map(e=>({value:e.namespaced,label:Pn(`${e.provider}/${e.model}`,p)}))],onChange:e=>s({model:e||null,effort:t||null}),disabled:o,label:p(`dash.injectionLabel`),align:`right`}),e&&n.length>0&&(0,J.jsx)(Dt,{value:t,options:[{value:``,label:p(`dash.injectionEffortNone`)},...n.map(e=>({value:e,label:e}))],onChange:t=>s({model:e||null,effort:t||null}),disabled:o,label:p(`dash.injectionEffortLabel`),align:`right`})]})]}),(0,J.jsxs)(`div`,{className:`swi-delegation-row`,children:[(0,J.jsxs)(`div`,{className:`setting-copy`,children:[(0,J.jsx)(`div`,{className:`font-semibold`,children:p(`dash.syncCodexSubagentDefaults`)}),(0,J.jsx)(`div`,{className:`muted setting-hint`,children:p(`dash.syncCodexSubagentDefaultsHint`)})]}),(0,J.jsx)(`button`,{type:`button`,className:`switch ${a?`on`:``}`,onClick:()=>s({syncCodexSubagentDefaults:!a}),disabled:o||!e,"aria-label":p(`dash.syncCodexSubagentDefaults`),"aria-pressed":a,children:(0,J.jsx)(`span`,{className:`knob`})})]}),(0,J.jsxs)(`details`,{className:`swi-advanced`,children:[(0,J.jsx)(`summary`,{className:`muted text-label`,children:p(`sub.advanced`)}),(0,J.jsxs)(`div`,{className:`swi-delegation-row`,children:[(0,J.jsxs)(`div`,{className:`setting-copy`,children:[(0,J.jsxs)(`div`,{className:`font-semibold`,style:{display:`inline-flex`,alignItems:`center`,gap:6},children:[p(`models.v2Label`),(0,J.jsxs)(kt,{content:p(`models.v2Help`),side:`top`,maxWidth:380,children:[(0,J.jsx)(Z,{width:13,height:13,"aria-hidden":`true`}),(0,J.jsx)(`span`,{className:`sr-only`,children:p(`models.v2Label`)})]})]}),(0,J.jsx)(`div`,{className:`muted setting-hint`,children:(0,J.jsx)(`a`,{className:`text-control`,href:`https://opencodex.me/guides/sub-agent-surface/`,target:`_blank`,rel:`noreferrer`,style:{color:`var(--accent)`},children:p(`models.v2DocsLink`)})})]}),(0,J.jsx)(`div`,{className:`swi-delegation-controls`,children:(0,J.jsx)(`div`,{className:`segmented models-segmented`,role:`radiogroup`,"aria-label":p(`models.v2Label`),children:[`v1`,`default`,`v2`].map(e=>(0,J.jsx)(`button`,{type:`button`,role:`radio`,"aria-checked":c.multiAgentMode===e,className:`btn btn-sm${c.multiAgentMode===e?` btn-primary`:` btn-ghost`}`,style:{background:c.multiAgentMode===e?void 0:`transparent`,color:c.multiAgentMode===e?void 0:`var(--muted)`},disabled:l||d,onClick:()=>{c.multiAgentMode!==e&&u({multiAgentMode:e})},children:p(`models.v2Mode_${e}`)},e))})})]}),(0,J.jsxs)(`div`,{className:`swi-delegation-row`,children:[(0,J.jsxs)(`div`,{className:`setting-copy`,children:[(0,J.jsx)(`div`,{className:`font-semibold`,children:p(`dash.multiAgentGuidance`)}),(0,J.jsx)(`div`,{className:`muted setting-hint`,children:p(`dash.multiAgentGuidanceHint`)})]}),(0,J.jsx)(`button`,{type:`button`,className:`switch ${i?`on`:``}`,onClick:()=>s({multiAgentGuidanceEnabled:!i}),disabled:o,"aria-label":p(`dash.multiAgentGuidance`),"aria-pressed":i,children:(0,J.jsx)(`span`,{className:`knob`})})]}),(0,J.jsxs)(`div`,{className:`swi-delegation-row`,children:[(0,J.jsxs)(`div`,{className:`setting-copy`,children:[(0,J.jsx)(`div`,{className:`font-semibold`,children:p(`sub.ultraMode`)}),(0,J.jsx)(`div`,{className:`muted setting-hint`,children:p(`sub.ultraModeHint`)})]}),(0,J.jsx)(`button`,{type:`button`,className:`switch ${m?`on`:``}`,onClick:()=>u({multiAgentModeHintText:m?null:Sp}),disabled:o||l||!m&&!c.multiAgentV2Enabled,"aria-label":p(`sub.ultraMode`),"aria-pressed":m,children:(0,J.jsx)(`span`,{className:`knob`})}),!c.multiAgentV2Enabled&&(0,J.jsx)(`div`,{className:`muted setting-hint`,children:p(`sub.ultraModeV2Required`)})]}),m&&(0,J.jsx)(`div`,{className:`swi-delegation-row swi-ultra-mode-editor`,children:(0,J.jsx)(xp,{initialHint:c.hintText??``,disabled:o||l,onSave:u,preset:`Proactive multi-agent delegation is active. Any earlier instruction requiring an explicit user request before spawning sub-agents no longer applies. Delegate independent sub-tasks to sub-agents whenever parallel work would materially improve speed or quality — do not serialize work that can run concurrently. Each sub-agent runs in its own context and can use all available tools; prefer spawning specialists over doing everything yourself. This mode remains active until a later multi-agent mode developer message changes it.`,labels:{text:p(`sub.ultraModeText`),preset:p(`sub.ultraModePreset`),save:p(`common.save`)}},c.hintText)})]})]})}function xp({initialHint:e,disabled:t,onSave:n,preset:r,labels:i}){let[a,o]=(0,_.useState)(e);return(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)(`textarea`,{className:`input swi-ultra-mode-textarea`,value:a,onChange:e=>o(e.target.value),disabled:t,rows:4,"aria-label":i.text}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:()=>o(r),disabled:t,children:i.preset}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-primary btn-sm`,onClick:()=>{a.trim().length!==0&&n({multiAgentModeHintText:a})},disabled:t||a.trim().length===0,children:i.save})]})}var Sp=`Proactive multi-agent delegation is active. Any earlier instruction requiring an explicit user request before spawning sub-agents no longer applies. Delegate independent sub-tasks to sub-agents whenever parallel work would materially improve speed or quality — do not serialize work that can run concurrently. Each sub-agent runs in its own context and can use all available tools; prefer spawning specialists over doing everything yourself. This mode remains active until a later multi-agent mode developer message changes it.`;function Cp({available:e,chosen:t,busy:n=!1,onToggle:r,onMove:i,onSave:a,delegation:o}){let s=Q(),[c,l]=(0,_.useState)(``),u=(0,_.useMemo)(()=>new Set(t),[t]),d=t.length>=5,f=(0,_.useMemo)(()=>{let t=c.trim().toLowerCase();return e.filter(e=>!t||e.toLowerCase().includes(t))},[e,c]),p=(0,_.useMemo)(()=>[{id:`featured`,label:s(`sub.featured`),meta:`${t.length}/5`},{id:`models`,label:s(`sub.models`),meta:String(f.length)},{id:`settings`,label:s(`sub.settings`)}],[s,t.length,f.length]);return(0,J.jsxs)(`div`,{className:`subagents-workspace-shell`,children:[(0,J.jsx)(yp,{scope:`subagents`,items:p,ariaLabel:s(`sub.sections`)}),(0,J.jsxs)(`div`,{className:`subagents-workspace-root`,children:[(0,J.jsxs)(`section`,{id:gp(`subagents`,`featured`),className:`subagents-workspace-section`,"aria-label":s(`sub.featured`),children:[(0,J.jsxs)(`div`,{className:`swi-featured-head`,children:[(0,J.jsx)(`h2`,{className:`swi-featured-title`,children:s(`sub.featured`)}),(0,J.jsxs)(`span`,{className:`swi-featured-count`,children:[t.length,`/`,5]}),(0,J.jsxs)(kt,{content:(0,J.jsx)(ut,{k:`sub.orderHint`,cmd:`spawn_agent`}),side:`bottom`,maxWidth:380,children:[(0,J.jsx)(Z,{width:14,height:14,"aria-hidden":`true`}),(0,J.jsx)(`span`,{className:`sr-only`,children:s(`sub.orderHintAria`)})]})]}),t.length===0?(0,J.jsx)(`div`,{className:`swi-featured-empty`,children:s(`sub.noneSelected`)}):(0,J.jsx)(`div`,{className:`swi-featured-list`,children:t.map((e,a)=>(0,J.jsxs)(`div`,{className:`swi-featured-row`,children:[(0,J.jsx)(`span`,{className:`swi-featured-pos`,children:a+1}),(0,J.jsx)(`span`,{className:`swi-featured-name`,children:fl(e)}),(0,J.jsxs)(`span`,{className:`swi-featured-actions`,children:[(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-icon btn-sm`,onClick:()=>i(a,-1),disabled:n||a===0,"aria-label":s(`sub.moveUp`,{m:e}),children:(0,J.jsx)(ye,{})}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-icon btn-sm`,onClick:()=>i(a,1),disabled:n||a===t.length-1,"aria-label":s(`sub.moveDown`,{m:e}),children:(0,J.jsx)(be,{})}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-icon btn-sm`,onClick:()=>r(e),disabled:n,"aria-label":s(`sub.removeAria`,{m:e}),style:{color:`var(--red)`},children:(0,J.jsx)(de,{})})]})]},e))}),(0,J.jsx)(`div`,{className:`swi-save-row`,children:(0,J.jsx)(`button`,{type:`button`,className:`btn btn-primary`,onClick:a,disabled:n,children:s(`common.save`)})})]}),(0,J.jsxs)(`section`,{id:gp(`subagents`,`models`),className:`subagents-workspace-section`,"aria-label":s(`sub.models`),children:[(0,J.jsxs)(`div`,{className:`swi-featured-head`,children:[(0,J.jsx)(`h2`,{className:`swi-featured-title`,children:s(`sub.models`)}),(0,J.jsx)(`span`,{className:`swi-featured-count`,children:f.length})]}),(0,J.jsxs)(`div`,{className:`swi-picker-box`,children:[(0,J.jsx)(`div`,{className:`subagents-workspace-rail-search`,children:(0,J.jsx)(`input`,{className:`input`,value:c,onChange:e=>l(e.target.value),placeholder:s(`sub.search`),"aria-label":s(`sub.search`)})}),(0,J.jsx)(`div`,{className:`subagents-workspace-rail-list`,children:f.length===0?(0,J.jsx)(`span`,{className:`subagents-workspace-rail-empty`,children:s(`sub.noModels`)}):f.map(e=>{let i=u.has(e),a=i?t.indexOf(e)+1:null,o=!i&&(d||n);return(0,J.jsxs)(`div`,{className:`subagents-workspace-rail-row${i?` subagents-workspace-rail-row--selected`:``}`,children:[(0,J.jsxs)(`span`,{className:`subagents-workspace-rail-row-main`,children:[(0,J.jsx)(`span`,{className:`swi-rail-priority`,children:a??``}),(0,J.jsx)(ie,{className:`swi-rail-icon`,"aria-hidden":`true`}),(0,J.jsx)(`span`,{className:`subagents-workspace-rail-name`,children:fl(e)})]}),(0,J.jsx)(`button`,{type:`button`,className:`subagents-workspace-rail-toggle${i?` subagents-workspace-rail-toggle--on`:``}${o?` subagents-workspace-rail-toggle--disabled`:``}`,onClick:()=>{o||r(e)},disabled:o,"aria-pressed":i,"aria-label":s(i?`sub.workspace.removeFromFeatured`:`sub.workspace.addToFeatured`,{m:e}),title:i?s(`sub.workspace.removeFromFeatured`,{m:e}):d?s(`sub.workspace.featuredFull`):s(`sub.workspace.addToFeatured`,{m:e}),children:i?(0,J.jsx)(ue,{style:{width:14,height:14}}):(0,J.jsx)(fe,{style:{width:14,height:14}})})]},e)})})]})]}),(0,J.jsxs)(`section`,{id:gp(`subagents`,`settings`),className:`subagents-workspace-section`,"aria-label":s(`sub.settings`),children:[(0,J.jsx)(`div`,{className:`swi-featured-head`,children:(0,J.jsx)(`h2`,{className:`swi-featured-title`,children:s(`sub.settings`)})}),(0,J.jsx)(bp,{model:o.model,effort:o.effort,efforts:o.efforts,available:o.available,guidanceEnabled:o.guidanceEnabled,syncCodexDefaults:o.syncCodexDefaults,saving:o.saving,onSave:o.onSave,ultraMode:o.ultraMode,ultraSaving:o.ultraSaving,onUltraModeSave:o.onUltraModeSave,ultraLoadFailed:o.ultraLoadFailed,onUltraModeRetry:o.onUltraModeRetry})]})]})]})}function wp(e){let[t,n]=(0,_.useState)(!1),[r,i]=(0,_.useState)(!1),[a,o]=(0,_.useState)(``),[s,c]=(0,_.useState)(``),[l,u]=(0,_.useState)([]),[d,f]=(0,_.useState)([]),[p,m]=(0,_.useState)(!0),[h,g]=(0,_.useState)(!1),v=(0,_.useCallback)(e=>{let t=kr(e);m(t.multiAgentGuidanceEnabled),g(t.syncCodexSubagentDefaults),o(t.injectionModel),c(t.injectionEffort),Array.isArray(e.efforts)&&u(e.efforts),Array.isArray(e.available)&&f(e.available)},[]);return(0,_.useEffect)(()=>{let t=!1;return(async()=>{try{let n=await Wt(await fetch(`${e}/api/injection-model`));if(t)return;v(n)}catch{}finally{t||n(!0)}})(),()=>{t=!0}},[e,v]),{loaded:t,saving:r,model:a,effort:s,efforts:l,available:d,guidanceEnabled:p,syncCodexDefaults:h,save:(0,_.useCallback)(async t=>{if(!r){i(!0);try{if(!(await fetch(`${e}/api/injection-model`,{method:`PUT`,headers:{"Content-Type":`application/json`},body:JSON.stringify(t)})).ok)throw Error(`injection save failed`);let n=await fetch(`${e}/api/injection-model`);v(await Wt(n))}catch{}finally{i(!1)}}},[e,v,r])}}function Tp(e){return gr(e)}function Ep({apiBase:e}){let t=Q(),n=`ocx.subagents.v1:${e}`,r=Tp(n),[i,a]=(0,_.useState)(()=>r?.chosen??[]),[o,s]=(0,_.useState)(``),[c,l]=(0,_.useState)(!1),[u,d]=(0,_.useState)(!1),f=(0,_.useRef)(!1),p=wp(e),[m,h]=(0,_.useState)({enabled:!1,hintText:null,multiAgentV2Enabled:!1,multiAgentMode:`default`}),[g,v]=(0,_.useState)(!1),[y,b]=(0,_.useState)(!1),x=(0,_.useRef)(0),S=(0,_.useRef)(e);(0,_.useEffect)(()=>{S.current=e,x.current++},[e]);let C=(0,_.useCallback)(async n=>{if(S.current!==e)return!1;let r=++x.current,i=await Pt(await fetch(`${e}/api/v2`,{signal:n}),t(`sub.ultraModeLoadFail`));return!i||n?.aborted||r!==x.current||S.current!==e?!1:(b(!1),h({enabled:i.enabled??!1,hintText:i.multiAgentModeHintText??null,multiAgentV2Enabled:i.enabled===!0&&i.multiAgentMode===`v2`,multiAgentMode:i.multiAgentMode??`default`}),!0)},[e,t]);(0,_.useEffect)(()=>{let e=new AbortController;return(async()=>{await C(e.signal)})().catch(()=>{e.signal.aborted||(l(!1),b(!0),s(t(`sub.ultraModeLoadFail`)))}),()=>{e.abort()}},[C,t]);let w=async n=>{if(g)return;let r=e;v(!0),s(``);try{if(await Pt(await fetch(`${e}/api/v2`,{method:`PUT`,headers:{"Content-Type":`application/json`},body:JSON.stringify(n)}),t(`sub.ultraModeSaveFail`)),S.current!==r||!await C())return;l(!0),s(t(`sub.ultraModeSaved`))}catch(e){if(S.current!==r)return;l(!1),s(e instanceof Error&&e.message?e.message:t(`sub.networkError`))}finally{v(!1)}},T=(0,_.useCallback)(async()=>{try{if(!await C())return;l(!1),s(e=>e===t(`sub.ultraModeLoadFail`)?``:e)}catch{l(!1),b(!0),s(t(`sub.ultraModeLoadFail`))}},[C,t]),E=(0,_.useCallback)(async r=>{let i=await Pt(await fetch(`${e}/api/subagent-models`,{signal:r}),t(`sub.loadFail`));if(!i)throw Error(t(`sub.loadFail`));let o=i.available??[],s=new Set(o),c={available:o,chosen:(i.chosen??[]).filter(e=>s.has(e))};return a(c.chosen),br(n,c),c},[e,n,t]),D=ml(n,[e],E,{isEmpty:()=>!1,initialData:r??void 0}),{state:O}=D,k=D.refresh,A=O.data??r,j=A?.available??[],M=e=>{u||(s(``),a(t=>t.includes(e)?t.filter(t=>t!==e):t.length>=5?t:[...t,e]))},N=(e,t)=>{u||a(n=>{let r=[...n],i=e+t;return i<0||i>=r.length?n:([r[e],r[i]]=[r[i],r[e]],r)})},P=async()=>{if(!(u||f.current)){f.current=!0,d(!0),s(``);try{let r=await Pt(await fetch(`${e}/api/subagent-models`,{method:`PUT`,headers:{"Content-Type":`application/json`},body:JSON.stringify({models:i})}),t(`sub.saveFailed`)),o=r?.applied??i;r?.applied&&a(r.applied),br(n,{available:j,chosen:o}),l(!0),s(t(`sub.saved`,{n:o.length,cmd:`ocx sync`}))}catch(e){l(!1),s(e instanceof Error&&e.message?e.message:t(`sub.networkError`))}finally{f.current=!1,d(!1)}}};if(O.showSkeleton&&!A)return(0,J.jsx)(gl,{label:t(`sub.loading`),rows:4});if(O.kind===`failed-cold`){let e=O.error instanceof Error?O.error.message:t(`sub.loadFail`);return(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)($,{tone:`err`,children:e}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:()=>k(),children:t(`common.retry`)})]})}return(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)(`div`,{className:`page-head`,children:(0,J.jsx)(`h2`,{children:t(`nav.subagents`)})}),o&&(0,J.jsx)($,{tone:c?`ok`:`err`,children:o}),O.showError&&(0,J.jsx)($,{tone:`err`,children:t(`sub.loadFail`)}),(0,J.jsx)(Cp,{available:j,chosen:i,busy:u,onToggle:M,onMove:N,onSave:()=>{P()},delegation:{model:p.model,effort:p.effort,efforts:p.efforts,available:p.available,guidanceEnabled:p.guidanceEnabled,syncCodexDefaults:p.syncCodexDefaults,saving:p.saving,onSave:e=>{p.save(e)},ultraMode:m,ultraSaving:g,onUltraModeSave:e=>{w(e)},ultraLoadFailed:y,onUltraModeRetry:()=>{T()}}})]})}function Dp(e,t,n){let r=Array(e);return new Proxy(r,{get(r,i,a){if(typeof i==`string`){let a=i.charCodeAt(0);if(a>=48&&a<=57){let a=+i;if(Number.isInteger(a)&&a>=0&&ar[t]!==e)?(r=o,i=t(...o),n?.onChange&&!(a&&n.skipInitialOnChange)&&n.onChange(i),a=!1,i):i}return o.updateDeps=e=>{r=e},o}function kp(e,t){if(e===void 0)throw Error(`Unexpected undefined${t?`: ${t}`:``}`);return e}var Ap=(e,t)=>Math.abs(e-t)<1.01,jp=(e,t,n)=>{let r;return function(...i){e.clearTimeout(r),r=e.setTimeout(()=>t.apply(this,i),n)}},Mp,Np=()=>{if(Mp!==void 0)return Mp;if(typeof navigator>`u`)return Mp=!1;if(/iP(hone|od|ad)/.test(navigator.userAgent))return Mp=!0;let e=navigator.maxTouchPoints;return Mp=navigator.platform===`MacIntel`&&e!==void 0&&e>0},Pp=e=>{let{offsetWidth:t,offsetHeight:n}=e;return{width:t,height:n}},Fp=e=>e,Ip=e=>{let t=Math.max(e.startIndex-e.overscan,0),n=Math.min(e.endIndex+e.overscan,e.count-1)-t+1,r=Array(n);for(let e=0;e{let n=e.scrollElement;if(!n)return;let r=e.targetWindow;if(!r)return;let i=e=>{let{width:n,height:r}=e;t({width:Math.round(n),height:Math.round(r)})};if(i(Pp(n)),!r.ResizeObserver)return()=>{};let a=new r.ResizeObserver(t=>{let r=()=>{let e=t[0];if(e?.borderBoxSize){let t=e.borderBoxSize[0];if(t){i({width:t.inlineSize,height:t.blockSize});return}}i(Pp(n))};e.options.useAnimationFrameWithResizeObserver?requestAnimationFrame(r):r()});return a.observe(n,{box:`border-box`}),()=>{a.unobserve(n)}},Rp={passive:!0},zp=typeof window>`u`||`onscrollend`in window,Bp=(e,t,n)=>{let r=e.scrollElement;if(!r)return;let i=e.targetWindow;if(!i)return;let a=e.options.useScrollendEvent&&zp,o=0,s=a?null:jp(i,()=>t(o,!1),e.options.isScrollingResetDelay),c=e=>()=>{o=n(r),s?.(),t(o,e)},l=c(!0),u=c(!1);return r.addEventListener(`scroll`,l,Rp),a&&r.addEventListener(`scrollend`,u,Rp),()=>{r.removeEventListener(`scroll`,l),a&&r.removeEventListener(`scrollend`,u)}},Vp=(e,t)=>Bp(e,t,t=>{let{horizontal:n,isRtl:r}=e.options;return n?t.scrollLeft*(r&&-1||1):t.scrollTop}),Hp=(e,t,n)=>{if(n.options.useCachedMeasurements){let t=n.indexFromElement(e),r=n.options.getItemKey(t);return n.itemSizeCache.get(r)??n.options.estimateSize(t)}if(t?.borderBoxSize){let e=t.borderBoxSize[0];if(e)return Math.round(e[n.options.horizontal?`inlineSize`:`blockSize`])}if(!t){let t=n.indexFromElement(e),r=n.options.getItemKey(t),i=n.itemSizeCache.get(r);if(i!==void 0)return i}return e[n.options.horizontal?`offsetWidth`:`offsetHeight`]},Up=(e,{adjustments:t=0,behavior:n},r)=>{var i,a;(a=(i=r.scrollElement)?.scrollTo)==null||a.call(i,{[r.options.horizontal?`left`:`top`]:e+t,behavior:n})},Wp=class{constructor(e){this.unsubs=[],this.scrollElement=null,this.targetWindow=null,this.isScrolling=!1,this.scrollState=null,this.measurementsCache=[],this._flatMeasurements=null,this.itemSizeCache=new Map,this.itemSizeCacheVersion=0,this.laneAssignments=new Map,this.pendingMin=null,this.prevLanes=void 0,this.lanesChangedFlag=!1,this.lanesSettling=!1,this.pendingScrollAnchor=null,this.scrollRect=null,this.scrollOffset=null,this.scrollDirection=null,this.scrollAdjustments=0,this._iosDeferredAdjustment=0,this._iosTouching=!1,this._iosJustTouchEnded=!1,this._iosTouchEndTimerId=null,this._intendedScrollOffset=null,this.elementsCache=new Map,this.now=()=>{var e;return((e=this.targetWindow?.performance)?.now)?.call(e)??Date.now()},this.observer=(()=>{let e=null,t=()=>e||(!this.targetWindow||!this.targetWindow.ResizeObserver?null:e=new this.targetWindow.ResizeObserver(e=>{e.forEach(e=>{let t=()=>{let t=e.target,n=this.indexFromElement(t);if(!t.isConnected){this.observer.unobserve(t);for(let[e,n]of this.elementsCache)if(n===t){this.elementsCache.delete(e);break}return}this.shouldMeasureDuringScroll(n)&&this.resizeItem(n,this.options.measureElement(t,e,this))};this.options.useAnimationFrameWithResizeObserver?requestAnimationFrame(t):t()})}));return{disconnect:()=>{var n;(n=t())==null||n.disconnect(),e=null},observe:e=>t()?.observe(e,{box:`border-box`}),unobserve:e=>t()?.unobserve(e)}})(),this.range=null,this.setOptions=e=>{let t={debug:!1,initialOffset:0,overscan:1,paddingStart:0,paddingEnd:0,scrollPaddingStart:0,scrollPaddingEnd:0,horizontal:!1,getItemKey:Fp,rangeExtractor:Ip,onChange:()=>{},measureElement:Hp,initialRect:{width:0,height:0},scrollMargin:0,gap:0,indexAttribute:`data-index`,initialMeasurementsCache:[],lanes:1,anchorTo:`start`,followOnAppend:!1,scrollEndThreshold:1,isScrollingResetDelay:150,enabled:!0,isRtl:!1,useScrollendEvent:!1,useAnimationFrameWithResizeObserver:!1,laneAssignmentMode:`estimate`,useCachedMeasurements:!1};for(let n in e){let r=e[n];r!==void 0&&(t[n]=r)}let n=this.options,r=null,i=null,a=!1;if(n!==void 0&&n.enabled&&t.enabled&&t.anchorTo===`end`&&this.scrollElement!==null){let e=n.count,o=t.count,s=this.getMeasurements(),c=e>0?s[0]?.key??n.getItemKey(0):null,l=e>0?s[e-1]?.key??n.getItemKey(e-1):null;if(o!==e||e>0&&o>0&&(t.getItemKey(0)!==c||t.getItemKey(o-1)!==l)){a=!0;let c=e>0?this.getVirtualItemForOffset(this.getScrollOffset())??s[0]:null;c&&(r=[c.key,this.getScrollOffset()-c.start]);let u=t.followOnAppend===!0?`auto`:t.followOnAppend||null;u&&o>e&&this.isAtEnd(n.scrollEndThreshold)&&(e===0||t.getItemKey(o-1)!==l)&&(i=u)}}this.options=t,a&&(this.pendingMin=0,this.itemSizeCacheVersion++);let o=!1,s=0;if(r&&this.scrollOffset!==null){let[e,t]=r,n=this.getMeasurements(),{count:i,getItemKey:a}=this.options,c=0;for(;c{var t,n;(n=(t=this.options).onChange)==null||n.call(t,this,e)},this.maybeNotify=Op(()=>(this.calculateRange(),[this.isScrolling,this.range?this.range.startIndex:null,this.range?this.range.endIndex:null]),e=>{this.notify(e)},{key:!1,debug:()=>this.options.debug,initialDeps:[this.isScrolling,this.range?this.range.startIndex:null,this.range?this.range.endIndex:null]}),this.cleanup=()=>{this.unsubs.filter(Boolean).forEach(e=>e()),this.unsubs=[],this.observer.disconnect(),this.rafId!=null&&this.targetWindow&&(this.targetWindow.cancelAnimationFrame(this.rafId),this.rafId=null),this.scrollState=null,this._iosDeferredAdjustment=0,this._iosTouching=!1,this._iosJustTouchEnded=!1,this.scrollElement=null,this.targetWindow=null},this._didMount=()=>()=>{this.cleanup()},this._willUpdate=()=>{let e=this.options.enabled?this.options.getScrollElement():null;if(this.scrollElement!==e){if(this.cleanup(),!e){this.maybeNotify();return}if(this.scrollElement=e,this.targetWindow=this.scrollElement&&`ownerDocument`in this.scrollElement?this.scrollElement.ownerDocument.defaultView:this.scrollElement?.window??null,this.elementsCache.forEach(e=>{this.observer.observe(e)}),this.unsubs.push(this.options.observeElementRect(this,e=>{this.scrollRect=e,this.maybeNotify()})),this.unsubs.push(this.options.observeElementOffset(this,(e,t)=>{if(t&&this._intendedScrollOffset===null&&e===this.scrollOffset)return;this._intendedScrollOffset!==null&&Math.abs(e-this._intendedScrollOffset)<1.5&&(e=this._intendedScrollOffset),this._intendedScrollOffset=null,this.scrollAdjustments=0;let n=this.getScrollOffset();this.scrollDirection=t?n===e?this.scrollDirection:n{this._iosTouching=!0,this._iosJustTouchEnded=!1,this._iosTouchEndTimerId!==null&&this.targetWindow!=null&&(this.targetWindow.clearTimeout(this._iosTouchEndTimerId),this._iosTouchEndTimerId=null)},n=()=>{this._iosTouching=!1,!(!Np()||this.targetWindow==null)&&(this._iosJustTouchEnded=!0,this._iosTouchEndTimerId=this.targetWindow.setTimeout(()=>{this._iosJustTouchEnded=!1,this._iosTouchEndTimerId=null,this._flushIosDeferredIfReady()},150))};e.addEventListener(`touchstart`,t,Rp),e.addEventListener(`touchend`,n,Rp),this.unsubs.push(()=>{e.removeEventListener(`touchstart`,t),e.removeEventListener(`touchend`,n),this._iosTouchEndTimerId!==null&&this.targetWindow!=null&&(this.targetWindow.clearTimeout(this._iosTouchEndTimerId),this._iosTouchEndTimerId=null)})}this._scrollToOffset(this.getScrollOffset(),{adjustments:void 0,behavior:void 0})}let t=this.pendingScrollAnchor;if(this.pendingScrollAnchor=null,t&&this.scrollElement&&this.options.enabled){let[e,n,r,i]=t;e!==null&&!r&&(Np()&&(this.isScrolling||this._iosTouching||this._iosJustTouchEnded)?i!==0&&(this._iosDeferredAdjustment+=i):this._scrollToOffset(this.getScrollOffset(),{adjustments:void 0,behavior:void 0})),r&&this.scrollToEnd({behavior:r})}},this._flushIosDeferredIfReady=()=>{if(this._iosDeferredAdjustment===0||this.isScrolling||this._iosTouching||this._iosJustTouchEnded)return;let e=this.getScrollOffset(),t=this.getMaxScrollOffset();if(e<0||e>t)return;if(this._iosDeferredAdjustment<0&&e>=t-1){this._iosDeferredAdjustment=0;return}let n=this._iosDeferredAdjustment;this._iosDeferredAdjustment=0,this._scrollToOffset(e,{adjustments:this.scrollAdjustments+=n,behavior:void 0})},this.rafId=null,this.getSize=()=>this.options.enabled?(this.scrollRect=this.scrollRect??this.options.initialRect,this.scrollRect[this.options.horizontal?`width`:`height`]):(this.scrollRect=null,0),this.getScrollOffset=()=>this.options.enabled?(this.scrollOffset=this.scrollOffset??(typeof this.options.initialOffset==`function`?this.options.initialOffset():this.options.initialOffset),this.scrollOffset):(this.scrollOffset=null,0),this.getMeasurementOptions=Op(()=>[this.options.count,this.options.paddingStart,this.options.scrollMargin,this.options.getItemKey,this.options.enabled,this.options.lanes,this.options.laneAssignmentMode,this.options.gap],(e,t,n,r,i,a,o,s)=>(this.prevLanes!==void 0&&this.prevLanes!==a&&(this.lanesChangedFlag=!0),this.prevLanes=a,this.pendingMin=null,{count:e,paddingStart:t,scrollMargin:n,getItemKey:r,enabled:i,lanes:a,laneAssignmentMode:o,gap:s}),{key:!1}),this.getMeasurements=Op(()=>[this.getMeasurementOptions(),this.itemSizeCacheVersion],({count:e,paddingStart:t,scrollMargin:n,getItemKey:r,enabled:i,lanes:a,laneAssignmentMode:o,gap:s},c)=>{let l=this.itemSizeCache;if(!i)return this.measurementsCache=[],this.itemSizeCache.clear(),this.laneAssignments.clear(),[];if(this.laneAssignments.size>e)for(let t of this.laneAssignments.keys())t>=e&&this.laneAssignments.delete(t);this.lanesChangedFlag&&(this.lanesChangedFlag=!1,this.lanesSettling=!0,this.measurementsCache=[],this.itemSizeCache.clear(),this.laneAssignments.clear(),this.pendingMin=null),this.measurementsCache.length===0&&!this.lanesSettling&&(this.measurementsCache=this.options.initialMeasurementsCache,this.measurementsCache.forEach(e=>{this.itemSizeCache.set(e.key,e.size)}));let u=this.lanesSettling?0:this.pendingMin??0;if(this.pendingMin=null,this.lanesSettling&&this.measurementsCache.length===e&&(this.lanesSettling=!1),a===1){let i=e*2,a=this._flatMeasurements;if(!a||a.length0&&e.set(a.subarray(0,u*2)),a=e,this._flatMeasurements=a}let o;if(u===0)o=t+n;else{let e=u-1;o=a[e*2]+a[e*2+1]+s}for(let t=u;t1){u=c;let e=f[u],r=e===void 0?void 0:d[e];h=r?r.end+s:t+n}else if(m===a){let e=0,t=p[0],n=f[0];for(let r=1;rthis.options.debug}),this.calculateRange=Op(()=>[this.getMeasurements(),this.getSize(),this.getScrollOffset(),this.options.lanes],(e,t,n,r)=>e.length===0||t===0?(this.range=null,null):(this.range=qp(e,t,n,r,r===1&&this._flatMeasurements!=null?this._flatMeasurements:null),this.range),{key:!1,debug:()=>this.options.debug}),this.getVirtualIndexes=Op(()=>{let e=null,t=null,n=this.calculateRange();return n&&(e=n.startIndex,t=n.endIndex),this.maybeNotify.updateDeps([this.isScrolling,e,t]),[this.options.rangeExtractor,this.options.overscan,this.options.count,e,t]},(e,t,n,r,i)=>r===null||i===null?[]:e({startIndex:r,endIndex:i,overscan:t,count:n}),{key:!1,debug:()=>this.options.debug}),this.indexFromElement=e=>{let t=this.options.indexAttribute,n=e.getAttribute(t);return n?parseInt(n,10):(console.warn(`Missing attribute name '${t}={index}' on measured element.`),-1)},this.shouldMeasureDuringScroll=e=>{if(!this.scrollState||this.scrollState.behavior!==`smooth`)return!0;let t=this.scrollState.index??this.getVirtualItemForOffset(this.scrollState.lastTargetOffset)?.index;if(t!==void 0&&this.range){let n=Math.max(this.options.overscan,Math.ceil((this.range.endIndex-this.range.startIndex)/2)),r=Math.max(0,t-n),i=Math.min(this.options.count-1,t+n);return e>=r&&e<=i}return!0},this.measureElement=e=>{if(!e){this.elementsCache.forEach((e,t)=>{e.isConnected||(this.observer.unobserve(e),this.elementsCache.delete(t))});return}let t=this.indexFromElement(e),n=this.options.getItemKey(t),r=this.elementsCache.get(n);r!==e&&(r&&this.observer.unobserve(r),this.observer.observe(e),this.elementsCache.set(n,e)),(!this.isScrolling||this.scrollState)&&this.shouldMeasureDuringScroll(t)&&this.resizeItem(t,this.options.measureElement(e,void 0,this))},this.resizeItem=(e,t)=>{if(e<0||e>=this.options.count)return;let n,r,i,a=this._flatMeasurements;if(this.options.lanes===1&&a!==null)i=this.options.getItemKey(e),r=a[e*2],n=a[e*2+1];else{let t=this.measurementsCache[e];if(!t)return;i=t.key,r=t.start,n=t.size}let o=this.itemSizeCache.get(i)??n,s=t-o;if(s!==0){let a=this.options.anchorTo===`end`&&this.scrollState?.behavior!==`smooth`&&this.getVirtualDistanceFromEnd()<=this.options.scrollEndThreshold,c=a?this.getTotalSize():0,l=this.getScrollOffset()+this.scrollAdjustments,u=this.itemSizeCache.has(i)?r+o<=l&&this.scrollDirection!==`backward`:r[this.getVirtualIndexes(),this.getMeasurements()],(e,t)=>{let n=[];for(let r=0,i=e.length;rthis.options.debug}),this.getVirtualItemForOffset=e=>{let t=this.getMeasurements();if(t.length===0)return;let n=this._flatMeasurements,r=this.options.lanes===1&&n!=null;return kp(t[Gp(0,t.length-1,r?e=>n[e*2]:e=>kp(t[e]).start,e)])},this.getMaxScrollOffset=()=>{if(!this.scrollElement)return 0;if(`scrollHeight`in this.scrollElement)return this.options.horizontal?this.scrollElement.scrollWidth-this.scrollElement.clientWidth:this.scrollElement.scrollHeight-this.scrollElement.clientHeight;{let e=this.scrollElement.document.documentElement;return this.options.horizontal?e.scrollWidth-this.scrollElement.innerWidth:e.scrollHeight-this.scrollElement.innerHeight}},this.getVirtualDistanceFromEnd=()=>Math.max(this.getTotalSize()-this.getSize()-this.getScrollOffset(),0),this.getDistanceFromEnd=()=>Math.max(this.getMaxScrollOffset()-this.getScrollOffset(),0),this.isAtEnd=(e=this.options.scrollEndThreshold)=>this.getDistanceFromEnd()<=e,this.getOffsetForAlignment=(e,t,n=0)=>{if(!this.scrollElement)return 0;let r=this.getSize(),i=this.getScrollOffset();t===`auto`&&(t=e>=i+r?`end`:`start`),t===`center`?e+=(n-r)/2:t===`end`&&(e-=r);let a=this.getMaxScrollOffset();return Math.max(Math.min(a,e),0)},this.getOffsetForIndex=(e,t=`auto`)=>{e=Math.max(0,Math.min(e,this.options.count-1));let n=this.getSize(),r=this.getScrollOffset(),i=this.measurementsCache[e];if(!i)return;if(t===`auto`){if(i.end>=r+n-this.options.scrollPaddingEnd)t=`end`;else if(i.start<=r+this.options.scrollPaddingStart)t=`start`;else return[r,t]}if(t===`end`&&e===this.options.count-1)return[this.getMaxScrollOffset(),t];let a=t===`end`?i.end+this.options.scrollPaddingEnd:i.start-this.options.scrollPaddingStart;return[this.getOffsetForAlignment(a,t,i.size),t]},this.scrollToOffset=(e,{align:t=`start`,behavior:n=`auto`}={})=>{this._iosDeferredAdjustment=0;let r=this.getOffsetForAlignment(e,t),i=this.now();this.scrollState={index:null,align:t,behavior:n,startedAt:i,lastTargetOffset:r,stableFrames:0},this._scrollToOffset(r,{adjustments:void 0,behavior:n}),this.scheduleScrollReconcile()},this.scrollToIndex=(e,{align:t=`auto`,behavior:n=`auto`}={})=>{this._iosDeferredAdjustment=0,e=Math.max(0,Math.min(e,this.options.count-1));let r=this.getOffsetForIndex(e,t);if(!r)return;let[i,a]=r,o=this.now();this.scrollState={index:e,align:a,behavior:n,startedAt:o,lastTargetOffset:i,stableFrames:0},this._scrollToOffset(i,{adjustments:void 0,behavior:n}),this.scheduleScrollReconcile()},this.scrollBy=(e,{behavior:t=`auto`}={})=>{let n=this.getScrollOffset()+e,r=this.now();this.scrollState={index:null,align:`start`,behavior:t,startedAt:r,lastTargetOffset:n,stableFrames:0},this._scrollToOffset(n,{adjustments:void 0,behavior:t}),this.scheduleScrollReconcile()},this.scrollToEnd=({behavior:e=`auto`}={})=>{if(this.options.count>0){this.scrollToIndex(this.options.count-1,{align:`end`,behavior:e});return}this.scrollToOffset(Math.max(this.getTotalSize()-this.getSize(),0),{behavior:e})},this.getTotalSize=()=>{let e=this.getMeasurements(),t;if(e.length===0)t=this.options.paddingStart;else if(this.options.lanes===1){let n=e.length-1,r=this._flatMeasurements;t=r==null?e[n]?.end??0:r[n*2]+r[n*2+1]}else{let n=Array(this.options.lanes).fill(null),r=e.length-1;for(;r>=0&&n.some(e=>e===null);){let t=e[r];n[t.lane]===null&&(n[t.lane]=t.end),r--}t=Math.max(...n.filter(e=>e!==null))}return Math.max(t-this.options.scrollMargin+this.options.paddingEnd,0)},this.takeSnapshot=()=>{let e=[];if(this.itemSizeCache.size===0)return e;let t=this.getMeasurements();for(let n of t)n&&this.itemSizeCache.has(n.key)&&e.push({index:n.index,key:n.key,start:n.start,size:n.size,end:n.end,lane:n.lane});return e},this._scrollToOffset=(e,{adjustments:t,behavior:n})=>{this._intendedScrollOffset=e+(t??0),this.options.scrollToFn(e,{behavior:n,adjustments:t},this)},this.measure=()=>{this.pendingMin=null,this.itemSizeCache.clear(),this.laneAssignments.clear(),this.itemSizeCacheVersion++,this.notify(!1)},this.setOptions(e)}applyScrollAdjustment(e,t){return e===0?!1:Np()&&(this.isScrolling||this._iosTouching||this._iosJustTouchEnded)?(this._iosDeferredAdjustment+=e,!1):(this._scrollToOffset(this.getScrollOffset(),{adjustments:this.scrollAdjustments+=e,behavior:t}),this.scrollOffset!==null&&(this.scrollOffset+=this.scrollAdjustments,this.scrollOffset<0&&(this.scrollOffset=0),this.scrollAdjustments=0),!0)}scheduleScrollReconcile(){if(!this.targetWindow){this.scrollState=null;return}this.rafId??=this.targetWindow.requestAnimationFrame(()=>{this.rafId=null,this.reconcileScroll()})}reconcileScroll(){if(!this.scrollState||!this.scrollElement)return;if(this.now()-this.scrollState.startedAt>5e3){this.scrollState=null;return}let e=this.scrollState.index==null?void 0:this.getOffsetForIndex(this.scrollState.index,this.scrollState.align),t=e?e[0]:this.scrollState.lastTargetOffset,n=t!==this.scrollState.lastTargetOffset;if(!n&&Ap(t,this.getScrollOffset())){if(this.scrollState.stableFrames++,this.scrollState.stableFrames>=1){this.getScrollOffset()!==t&&this._scrollToOffset(t,{adjustments:void 0,behavior:`auto`}),this.scrollState=null;return}}else if(this.scrollState.stableFrames=0,n){let e=this.getSize()||600,n=Math.abs(t-this.getScrollOffset()),r=this.scrollState.behavior===`smooth`&&n>e;this.scrollState.lastTargetOffset=t,r||(this.scrollState.behavior=`auto`),this._scrollToOffset(t,{adjustments:void 0,behavior:r?`smooth`:`auto`})}this.scheduleScrollReconcile()}},Gp=(e,t,n,r)=>{for(;e<=t;){let i=(e+t)/2|0,a=n(i);if(ar)t=i-1;else return i}return e>0?e-1:0};function Kp(e,t,n){let r=0;for(;r<=t;){let i=(r+t)/2|0,a=e[i*2];if(an)t=i-1;else return i}return r>0?r-1:0}function qp(e,t,n,r,i){let a=e.length-1;if(e.length<=r)return{startIndex:0,endIndex:a};if(r===1&&i!==null){let e=Kp(i,a,n),r=e,o=n+t;for(;re[t].start,n),s=o;if(r===1)for(;s1){let i=Array(r).fill(0);for(;se=0&&c.some(e=>e>=n);){let t=e[o];c[t.lane]=t.start,o--}o=Math.max(0,o-o%r),s=Math.min(a,s+(r-1-s%r))}return{startIndex:o,endIndex:s}}var Jp=typeof document<`u`?_.useLayoutEffect:_.useEffect;function Yp({useFlushSync:e=!0,directDomUpdates:t=!1,directDomUpdatesMode:n=`transform`,...r}){let i=_.useReducer(e=>e+1,0)[1],a=_.useRef({enabled:t,mode:n,container:null,lastSize:null,lastPositions:new WeakMap,prevRange:null});a.current.enabled=t,a.current.mode=n;let o=e=>{let t=a.current;if(!t.enabled||!t.container)return;let n=e.getTotalSize();if(n!==t.lastSize){t.lastSize=n;let r=e.options.horizontal?`width`:`height`;t.container.style[r]=`${n}px`}},s=e=>{let t=a.current;if(!t.enabled||!t.container)return;o(e);let n=!!e.options.horizontal,r=t.mode===`transform`,i=n?`left`:`top`,s=e.options.scrollMargin,c=e.getVirtualItems();for(let a of c){let o=a.start-s,c=e.elementsCache.get(a.key);c&&t.lastPositions.get(c)!==o&&(t.lastPositions.set(c,o),r?c.style.transform=n?`translate3d(${o}px, 0, 0)`:`translate3d(0, ${o}px, 0)`:c.style[i]=`${o}px`)}},c={...r,onChange:(t,n)=>{var o;let c=a.current,l=!0;if(c.enabled){s(t);let e=t.range,n=c.prevRange;l=!n||n.isScrolling!==t.isScrolling||n.startIndex!==e?.startIndex||n.endIndex!==e?.endIndex,l&&(c.prevRange=e?{startIndex:e.startIndex,endIndex:e.endIndex,isScrolling:t.isScrolling}:null)}l&&(e&&n?(0,mt.flushSync)(i):i()),(o=r.onChange)==null||o.call(r,t,n)}},[l]=_.useState(()=>{let e=new Wp(c);return Object.assign(e,{containerRef:t=>{let n=a.current;if(n.container=t,n.lastSize=null,t&&n.enabled){let r=e.getTotalSize();n.lastSize=r;let i=e.options.horizontal?`width`:`height`;t.style[i]=`${r}px`}}})});return l.setOptions(c),Jp(()=>l._didMount(),[]),Jp(()=>(o(l),l._willUpdate())),Jp(()=>{s(l)}),l}function Xp(e){return Yp({observeElementRect:Lp,observeElementOffset:Vp,scrollToFn:Up,...e})}var Zp=32;function Qp(e){return[...new Uint8Array(e)].map(e=>e.toString(16).padStart(2,`0`)).join(``)}function $p(e){for(let t=0;t4096))return Qp(await crypto.subtle.digest(`SHA-256`,new TextEncoder().encode(t))).slice(0,Zp)}function tm(e,t,n){if(!e)return!1;let r=t.trim();return r?e===r||n!==void 0&&e===n:!1}var nm={400:{en:{label:`Bad request`,description:`The proxy could not understand the request. Check the model, message shape, headers, and JSON body before retrying.`},fr:{label:`Requête incorrecte`,description:`Le proxy n’a pas pu comprendre la requête. Vérifiez le modèle, la structure des messages, les en-têtes et le corps JSON avant de réessayer.`},ko:{label:`잘못된 요청`,description:`프록시가 요청을 이해할 수 없습니다. 재시도 전에 모델, 메시지 형식, 헤더, JSON 본문을 확인해야 합니다.`},zh:{label:`错误请求`,description:`代理无法理解该请求。重试前请检查模型、消息结构、标头和 JSON 正文。`},"zh-TW":{label:`錯誤請求`,description:`代理無法理解該請求。重試前請檢查模型、訊息結構、標頭和 JSON 本文。`},de:{label:`Ungültige Anfrage`,description:`Der Proxy konnte die Anfrage nicht verstehen. Prüfe Modell, Nachrichtenformat, Header und JSON-Body vor einem erneuten Versuch.`},ru:{label:`Некорректный запрос`,description:`Прокси не смог интерпретировать запрос. Перед повторной попыткой проверьте модель, формат сообщений, заголовки и тело JSON.`},ja:{label:`不正なリクエスト`,description:`プロキシがリクエストを解釈できませんでした。再試行前にモデル、メッセージ形式、ヘッダー、JSON 本文を確認してください。`},tr:{label:`Hatalı istek`,description:`Proxy isteği anlayamadı. Yeniden denemeden önce modeli, mesaj yapısını, başlıkları ve JSON gövdesini kontrol edin.`}},401:{en:{label:`Unauthorized`,description:`Credentials are missing, expired, or invalid. Re-login or refresh the account/provider credentials used by opencodex.`},fr:{label:`Non autorisé`,description:`Les identifiants sont absents, expirés ou non valides. Reconnectez-vous ou actualisez les identifiants du compte ou du fournisseur utilisés par opencodex.`},ko:{label:`인증 필요`,description:`자격 증명이 없거나 만료되었거나 유효하지 않습니다. opencodex에서 사용하는 계정 또는 제공자 자격 증명을 다시 로그인하거나 갱신해야 합니다.`},zh:{label:`未授权`,description:`凭据缺失、已过期或无效。请重新登录,或刷新 opencodex 使用的账号/提供商凭据。`},"zh-TW":{label:`未授權`,description:`憑證缺失、已過期或無效。請重新登入,或重新整理 opencodex 使用的帳號/供應商憑證。`},de:{label:`Nicht autorisiert`,description:`Anmeldedaten fehlen, sind abgelaufen oder ungültig. Melde dich erneut an oder aktualisiere die von opencodex genutzten Konto-/Anbieter-Zugangsdaten.`},ru:{label:`Не авторизован`,description:`Учётные данные отсутствуют, истекли или недействительны. Войдите заново или обновите учётные данные аккаунта или провайдера, которые использует opencodex.`},ja:{label:`認証が必要`,description:`認証情報が不在・期限切れ・無効です。opencodex が使用するアカウントまたはプロバイダー認証情報を再ログインまたは更新してください。`},tr:{label:`Yetkisiz erişim`,description:`Kimlik bilgileri eksik, süresi dolmuş veya geçersiz. opencodex tarafından kullanılan hesap veya sağlayıcı kimlik bilgilerini yeniden doğrulayın.`}},402:{en:{label:`Payment required`,description:`The upstream provider rejected the request because billing, credits, or plan access is not available. Add credits, update billing, or switch provider.`},fr:{label:`Paiement requis`,description:`Le fournisseur en amont a rejeté la requête, car la facturation, les crédits ou l’accès à l’offre ne sont pas disponibles. Ajoutez des crédits, mettez à jour la facturation ou changez de fournisseur.`},ko:{label:`결제 필요`,description:`청구, 크레딧, 플랜 접근 권한 문제로 업스트림 제공자가 요청을 거부했습니다. 크레딧 추가, 결제 정보 갱신, 제공자 전환이 필요합니다.`},zh:{label:`需要付款`,description:`上游提供商因账单、额度或套餐权限不可用而拒绝了请求。请充值、更新账单信息或切换提供商。`},"zh-TW":{label:`需要付款`,description:`上游供應商因帳單、額度或方案許可權不可用而拒絕了請求。請儲值、更新帳單資訊或切換供應商。`},de:{label:`Zahlung erforderlich`,description:`Der Upstream-Anbieter hat die Anfrage abgelehnt, weil Abrechnung, Guthaben oder Planzugriff nicht verfügbar ist. Guthaben aufladen, Abrechnung aktualisieren oder Anbieter wechseln.`},ru:{label:`Требуется оплата`,description:`Вышестоящий провайдер отклонил запрос из-за проблем с оплатой, кредитами или доступом по тарифному плану. Пополните баланс, обновите платёжные данные или переключитесь на другого провайдера.`},ja:{label:`支払いが必要`,description:`課金、クレジット、プランアクセスが利用できないため上流プロバイダーがリクエストを拒否しました。クレジット追加、支払い情報更新、プロバイダー切替が必要です。`},tr:{label:`Ödeme gerekli`,description:`Yukarı akış sağlayıcısı faturalandırma, kredi veya plan erişimi bulunmadığından isteği reddetti. Kredi ekleyin, ödeme bilgilerini güncelleyin veya sağlayıcı değiştirin.`}},403:{en:{label:`Forbidden`,description:`The account is authenticated but not allowed to use this model or operation. Often a plan/subscription gate (e.g. Ollama Cloud Pro), org policy, or model permission — not necessarily a bad API key.`},fr:{label:`Accès interdit`,description:`Le compte est authentifié, mais n’est pas autorisé à utiliser ce modèle ou cette opération. Il s’agit souvent d’une restriction liée à l’offre ou à l’abonnement (p. ex. Ollama Cloud Pro), à la politique de l’organisation ou aux autorisations du modèle — pas nécessairement d’une clé API incorrecte.`},ko:{label:`권한 없음`,description:`계정 인증은 되었지만 이 모델 또는 작업을 사용할 권한이 없습니다. 플랜/구독 제한(예: Ollama Cloud Pro), 조직 정책, 모델 권한 문제인 경우가 많으며 API 키가 잘못된 것은 아닐 수 있습니다.`},zh:{label:`禁止访问`,description:`账号已认证,但无权使用此模型或操作。常见原因是套餐/订阅限制(例如 Ollama Cloud Pro)、组织策略或模型权限——不一定是 API 密钥无效。`},"zh-TW":{label:`禁止存取`,description:`帳號已認證,但無權使用此模型或操作。常見原因是方案/訂閱限制(例如 Ollama Cloud Pro)、組織策略或模型許可權——不一定是 API 金鑰無效。`},de:{label:`Verboten`,description:`Das Konto ist authentifiziert, darf dieses Modell oder diese Operation aber nicht nutzen. Oft Plan-/Abo-Sperre (z. B. Ollama Cloud Pro), Organisationsrichtlinie oder Modellrecht — nicht zwingend ein ungültiger API-Key.`},ru:{label:`Доступ запрещён`,description:`Аккаунт аутентифицирован, но не имеет права использовать эту модель или операцию. Часто причина — ограничение тарифа или подписки (например, Ollama Cloud Pro), политика организации или права доступа к модели, а не обязательно неверный API-ключ.`},ja:{label:`アクセス禁止`,description:`アカウントは認証済みですがこのモデルや操作の使用が許可されていません。多くはプラン/サブスクリプション制限(例: Ollama Cloud Pro)、組織ポリシー、モデル権限であり、API キーが不正とは限りません。`},tr:{label:`Erişim yasaklandı`,description:`Hesabın kimliği doğrulandı ancak bu modeli veya işlemi kullanma izni yok. Genellikle plan/abonelik sınırı (örn. Ollama Cloud Pro), organizasyon politikası veya model izni kaynaklıdır.`}},404:{en:{label:`Not found`,description:`The requested route, model, account, or upstream resource was not found. Verify the model name and opencodex provider configuration.`},fr:{label:`Introuvable`,description:`La route, le modèle, le compte ou la ressource en amont demandés sont introuvables. Vérifiez le nom du modèle et la configuration du fournisseur opencodex.`},ko:{label:`찾을 수 없음`,description:`요청한 경로, 모델, 계정 또는 업스트림 리소스를 찾을 수 없습니다. 모델 이름과 opencodex 제공자 설정을 확인해야 합니다.`},zh:{label:`未找到`,description:`找不到请求的路由、模型、账号或上游资源。请确认模型名称和 opencodex 提供商配置。`},"zh-TW":{label:`未找到`,description:`找不到請求的路由、模型、帳號或上游資源。請確認模型名稱和 opencodex 供應商配置。`},de:{label:`Nicht gefunden`,description:`Die angeforderte Route, das Modell, das Konto oder die Upstream-Ressource wurde nicht gefunden. Prüfe Modellname und opencodex-Anbieterkonfiguration.`},ru:{label:`Не найдено`,description:`Запрошенный маршрут, модель, аккаунт или вышестоящий ресурс не найден. Проверьте имя модели и конфигурацию провайдера в opencodex.`},ja:{label:`見つかりません`,description:`要求されたルート、モデル、アカウント、上流リソースが見つかりませんでした。モデル名と opencodex プロバイダー設定を確認してください。`},tr:{label:`Bulunamadı`,description:`İstenen rota, model, hesap veya yukarı akış kaynağı bulunamadı. Model adını ve opencodex sağlayıcı yapılandırmasını doğrulayın.`}},408:{en:{label:`Request timeout`,description:`The request took too long before the proxy or upstream provider could complete it. Retry with a smaller request or a different provider.`},fr:{label:`Délai d’attente de la requête dépassé`,description:`La requête a pris trop de temps pour que le proxy ou le fournisseur en amont puisse la traiter. Réessayez avec une requête plus petite ou un autre fournisseur.`},ko:{label:`요청 시간 초과`,description:`프록시 또는 업스트림 제공자가 요청을 완료하기 전에 시간이 초과되었습니다. 더 작은 요청으로 재시도하거나 다른 제공자로 전환해야 합니다.`},zh:{label:`请求超时`,description:`代理或上游提供商未能在限定时间内完成请求。请缩小请求后重试,或切换提供商。`},"zh-TW":{label:`請求逾時`,description:`代理或上游供應商未能在限定時間內完成請求。請縮小請求後重試,或切換供應商。`},de:{label:`Anfrage-Timeout`,description:`Die Anfrage dauerte zu lange, bevor Proxy oder Upstream-Anbieter sie abschließen konnten. Mit kleinerer Anfrage oder anderem Anbieter erneut versuchen.`},ru:{label:`Тайм-аут запроса`,description:`Обработка запроса заняла слишком много времени, и прокси или вышестоящий провайдер не успел её завершить. Повторите попытку с меньшим запросом или через другого провайдера.`},ja:{label:`リクエストタイムアウト`,description:`プロキシまたは上流プロバイダーがリクエストを完了する前に時間切れになりました。より小さいリクエストで再試行するか、別のプロバイダーに切り替えてください。`},tr:{label:`İstek zaman aşımı`,description:`Proxy veya yukarı akış sağlayıcısı isteği tamamlayamadan zaman aşımına uğradı. Daha küçük bir istek veya farklı bir sağlayıcı ile tekrar deneyin.`}},409:{en:{label:`Conflict`,description:`The request conflicts with the current account, session, or provider state. Refresh the session or retry after the active operation finishes.`},fr:{label:`Conflit`,description:`La requête entre en conflit avec l’état actuel du compte, de la session ou du fournisseur. Actualisez la session ou réessayez une fois l’opération en cours terminée.`},ko:{label:`상태 충돌`,description:`요청이 현재 계정, 세션 또는 제공자 상태와 충돌합니다. 세션을 갱신하거나 진행 중인 작업이 끝난 뒤 재시도해야 합니다.`},zh:{label:`状态冲突`,description:`请求与当前账号、会话或提供商状态冲突。请刷新会话,或等待当前操作完成后重试。`},"zh-TW":{label:`狀態衝突`,description:`請求與當前帳號、會話或供應商狀態衝突。請重新整理會話,或等待當前操作完成後重試。`},de:{label:`Konflikt`,description:`Die Anfrage kollidiert mit dem aktuellen Konto-, Sitzungs- oder Anbieterstatus. Sitzung aktualisieren oder nach Abschluss der laufenden Operation erneut versuchen.`},ru:{label:`Конфликт`,description:`Запрос конфликтует с текущим состоянием аккаунта, сессии или провайдера. Обновите сессию или повторите попытку после завершения текущей операции.`},ja:{label:`状態の衝突`,description:`リクエストが現在のアカウント、セッション、プロバイダー状態と衝突しています。セッションを更新するか、進行中の操作が終わった後に再試行してください。`},tr:{label:`Durum çakışması`,description:`İstek mevcut hesap, oturum veya sağlayıcı durumuyla çakışıyor. Oturumu yenileyin veya aktif işlem bittikten sonra tekrar deneyin.`}},413:{en:{label:`Request too large`,description:`The prompt, attachments, or generated payload exceeds a proxy or upstream limit. Reduce tokens, file size, or conversation history.`},fr:{label:`Requête trop volumineuse`,description:`L’invite, les pièces jointes ou la charge utile générée dépassent une limite du proxy ou du fournisseur en amont. Réduisez le nombre de jetons, la taille des fichiers ou l’historique de la conversation.`},ko:{label:`요청 과대`,description:`프롬프트, 첨부 파일 또는 생성 페이로드가 프록시나 업스트림 한도를 초과했습니다. 토큰, 파일 크기, 대화 기록을 줄여야 합니다.`},zh:{label:`请求过大`,description:`提示、附件或生成的负载超过了代理或上游限制。请减少 token、文件大小或对话历史。`},"zh-TW":{label:`請求過大`,description:`提示、附件或生成的負載超過了代理或上游限制。請減少 token、檔案大小或對話歷史。`},de:{label:`Anfrage zu groß`,description:`Prompt, Anhänge oder generierte Nutzlast überschreiten ein Proxy- oder Upstream-Limit. Tokens, Dateigröße oder Verlauf reduzieren.`},ru:{label:`Слишком большой запрос`,description:`Промпт, вложения или сформированная полезная нагрузка превышают лимит прокси или вышестоящего провайдера. Сократите количество токенов, размер файлов или историю диалога.`},ja:{label:`リクエストが大きすぎます`,description:`プロンプト、添付ファイル、生成ペイロードがプロキシまたは上流の制限を超えました。トークン、ファイルサイズ、会話履歴を減らしてください。`},tr:{label:`İstek çok büyük`,description:`İstemi, ekler veya oluşturulan veri proxy ya da yukarı akış sınırını aşıyor. Jeton sayısını, dosya boyutunu veya sohbet geçmişini azaltın.`}},422:{en:{label:`Invalid content`,description:`The provider accepted the request format but rejected its contents. Check model options, tool definitions, message roles, and unsupported fields.`},fr:{label:`Contenu non valide`,description:`Le fournisseur a accepté le format de la requête, mais en a rejeté le contenu. Vérifiez les options du modèle, les définitions des outils, les rôles des messages et les champs non pris en charge.`},ko:{label:`내용 검증 실패`,description:`제공자가 요청 형식은 받았지만 내용을 거부했습니다. 모델 옵션, 도구 정의, 메시지 역할, 지원되지 않는 필드를 확인해야 합니다.`},zh:{label:`内容无效`,description:`提供商接受了请求格式,但拒绝了其中的内容。请检查模型选项、工具定义、消息角色和不支持的字段。`},"zh-TW":{label:`內容無效`,description:`供應商接受了請求格式,但拒絕了其中的內容。請檢查模型選項、工具定義、訊息角色和不支援的欄位。`},de:{label:`Ungültiger Inhalt`,description:`Der Anbieter akzeptierte das Anfrageformat, lehnte den Inhalt aber ab. Prüfe Modelloptionen, Tool-Definitionen, Nachrichtenrollen und nicht unterstützte Felder.`},ru:{label:`Недопустимое содержимое`,description:`Провайдер принял формат запроса, но отклонил его содержимое. Проверьте параметры модели, определения инструментов, роли сообщений и неподдерживаемые поля.`},ja:{label:`内容の検証失敗`,description:`プロバイダーはリクエスト形式を受け付けましたが内容を拒否しました。モデルオプション、ツール定義、メッセージロール、未サポートのフィールドを確認してください。`},tr:{label:`Geçersiz içerik`,description:`Sağlayıcı istek formatını kabul etti ancak içeriğini reddetti. Model seçeneklerini, araç tanımlarını, mesaj rollerini ve desteklenmeyen alanları kontrol edin.`}},424:{en:{label:`Provider dependency failed`,description:`A required upstream dependency failed while opencodex was routing the request. Retry later or switch to another configured provider.`},fr:{label:`Échec d’une dépendance du fournisseur`,description:`Une dépendance en amont requise a échoué pendant le routage de la requête par opencodex. Réessayez plus tard ou sélectionnez un autre fournisseur configuré.`},ko:{label:`제공자 의존성 실패`,description:`opencodex가 요청을 라우팅하는 동안 필요한 업스트림 의존성이 실패했습니다. 나중에 재시도하거나 다른 설정된 제공자로 전환해야 합니다.`},zh:{label:`提供商依赖失败`,description:`opencodex 路由请求时,必需的上游依赖失败。请稍后重试,或切换到另一个已配置的提供商。`},"zh-TW":{label:`供應商依賴失敗`,description:`opencodex 路由請求時,必需的上游依賴失敗。請稍後重試,或切換到另一個已配置的供應商。`},de:{label:`Anbieter-Abhängigkeit fehlgeschlagen`,description:`Eine erforderliche Upstream-Abhängigkeit ist fehlgeschlagen, während opencodex die Anfrage geroutet hat. Später erneut versuchen oder zu einem anderen Anbieter wechseln.`},ru:{label:`Сбой зависимости провайдера`,description:`Необходимая вышестоящая зависимость дала сбой, пока opencodex маршрутизировал запрос. Повторите попытку позже или переключитесь на другого настроенного провайдера.`},ja:{label:`プロバイダー依存の失敗`,description:`opencodex がリクエストをルーティング中に必要な上流依存が失敗しました。後で再試行するか、別の設定済みプロバイダーに切り替えてください。`},tr:{label:`Sağlayıcı bağımlılığı başarısız`,description:`opencodex isteği yönlendirirken gerekli bir yukarı akış bağımlılığı başarısız oldu. Daha sonra tekrar deneyin veya başka bir sağlayıcıya geçin.`}},429:{en:{label:`Rate limited`,description:`The upstream provider rate or quota limit has been reached. Wait for the quota window to reset or switch account/provider.`},fr:{label:`Limite de débit atteinte`,description:`La limite de débit ou de quota du fournisseur en amont a été atteinte. Attendez la réinitialisation de la fenêtre de quota ou changez de compte ou de fournisseur.`},ko:{label:`한도 초과`,description:`업스트림 제공자의 속도 또는 할당량 한도에 도달했습니다. 한도 창이 초기화될 때까지 기다리거나 계정/제공자를 전환해야 합니다.`},zh:{label:`限流`,description:`已达到上游提供商的速率或额度限制。请等待额度窗口重置,或切换账号/提供商。`},"zh-TW":{label:`限流`,description:`已達到上游供應商的速率或額度限制。請等待額度視窗重設,或切換帳號/供應商。`},de:{label:`Ratenlimit erreicht`,description:`Das Raten- oder Kontingentlimit des Upstream-Anbieters ist erreicht. Auf Reset des Kontingentfensters warten oder Konto/Anbieter wechseln.`},ru:{label:`Превышен лимит запросов`,description:`Достигнут лимит скорости или квота вышестоящего провайдера. Дождитесь сброса окна квоты или переключитесь на другой аккаунт или провайдера.`},ja:{label:`レート制限`,description:`上流プロバイダーのレートまたはクォータ制限に達しました。クォータウィンドウがリセットされるまで待つか、アカウント/プロバイダーを切り替えてください。`},tr:{label:`Oran sınırı aşıldı`,description:`Yukarı akış sağlayıcısının hız veya kota sınırına ulaşıldı. Kota penceresinin sıfırlanmasını bekleyin ya da hesap/sağlayıcı değiştirin.`}},499:{en:{label:`Client closed request`,description:`The client disconnected or canceled the request before opencodex finished routing it. Retry if the cancellation was accidental.`},fr:{label:`Requête fermée par le client`,description:`Le client s’est déconnecté ou a annulé la requête avant la fin de son routage par opencodex. Réessayez si l’annulation était involontaire.`},ko:{label:`클라이언트 취소`,description:`opencodex가 라우팅을 끝내기 전에 클라이언트 연결이 끊기거나 요청이 취소되었습니다. 의도한 취소가 아니면 다시 시도해야 합니다.`},zh:{label:`客户端已取消`,description:`opencodex 完成路由前,客户端已断开连接或取消请求。如果不是有意取消,请重试。`},"zh-TW":{label:`客戶端已取消`,description:`opencodex 完成路由前,客戶端已斷開連線或取消請求。如果不是有意取消,請重試。`},de:{label:`Client hat Anfrage geschlossen`,description:`Der Client hat die Verbindung getrennt oder die Anfrage abgebrochen, bevor opencodex das Routing abgeschlossen hat. Bei versehentlichem Abbruch erneut versuchen.`},ru:{label:`Запрос закрыт клиентом`,description:`Клиент отключился или отменил запрос до того, как opencodex завершил его маршрутизацию. Если отмена была случайной, повторите попытку.`},ja:{label:`クライアントがリクエストをクローズ`,description:`opencodex がルーティングを終える前にクライアントが切断またはキャンセルしました。意図しないキャンセルなら再試行してください。`},tr:{label:`İstemci isteği kapattı`,description:`opencodex yönlendirmeyi bitirmeden önce istemci bağlantıyı kesti veya isteği iptal etti. İptal kazara yapıldıysa tekrar deneyin.`}},500:{en:{label:`Proxy error`,description:`opencodex hit an internal error while handling the request. Retry once, then check proxy logs if it repeats.`},fr:{label:`Erreur du proxy`,description:`opencodex a rencontré une erreur interne lors du traitement de la requête. Réessayez une fois, puis consultez les journaux du proxy si l’erreur se reproduit.`},ko:{label:`프록시 오류`,description:`opencodex가 요청을 처리하는 동안 내부 오류가 발생했습니다. 한 번 재시도하고 반복되면 프록시 로그를 확인해야 합니다.`},zh:{label:`代理错误`,description:`opencodex 处理请求时发生内部错误。请先重试一次;如果重复出现,请检查代理日志。`},"zh-TW":{label:`代理錯誤`,description:`opencodex 處理請求時發生內部錯誤。請先重試一次;如果重複出現,請檢查代理日誌。`},de:{label:`Proxy-Fehler`,description:`opencodex ist bei der Anfragebearbeitung auf einen internen Fehler gestoßen. Einmal erneut versuchen, bei Wiederholung Proxy-Logs prüfen.`},ru:{label:`Ошибка прокси`,description:`В opencodex произошла внутренняя ошибка при обработке запроса. Повторите попытку один раз; если ошибка повторяется, проверьте логи прокси.`},ja:{label:`プロキシエラー`,description:`opencodex がリクエスト処理中に内部エラーに遭遇しました。1 回再試行し、繰り返す場合はプロキシログを確認してください。`},tr:{label:`Proxy hatası`,description:`opencodex isteği işlerken dahili bir hatayla karşılaştı. Bir kez tekrar deneyin, tekrarlarsa proxy günlüklerini kontrol edin.`}},502:{en:{label:`Bad upstream response`,description:`The upstream provider returned an invalid or failed response through the proxy. Retry or route the request to another provider.`},fr:{label:`Réponse incorrecte du fournisseur en amont`,description:`Le fournisseur en amont a renvoyé une réponse non valide ou en échec par l’intermédiaire du proxy. Réessayez ou acheminez la requête vers un autre fournisseur.`},ko:{label:`업스트림 응답 오류`,description:`업스트림 제공자가 프록시를 통해 유효하지 않거나 실패한 응답을 반환했습니다. 재시도하거나 다른 제공자로 라우팅해야 합니다.`},zh:{label:`上游响应错误`,description:`上游提供商通过代理返回了无效或失败的响应。请重试,或将请求路由到其他提供商。`},"zh-TW":{label:`上游回應錯誤`,description:`上游供應商透過代理返回了無效或失敗的回應。請重試,或將請求路由到其他供應商。`},de:{label:`Ungültige Upstream-Antwort`,description:`Der Upstream-Anbieter lieferte über den Proxy eine ungültige oder fehlgeschlagene Antwort. Erneut versuchen oder zu einem anderen Anbieter routen.`},ru:{label:`Некорректный ответ провайдера`,description:`Вышестоящий провайдер вернул через прокси недействительный или ошибочный ответ. Повторите попытку или направьте запрос другому провайдеру.`},ja:{label:`上流レスポンス不良`,description:`上流プロバイダーがプロキシ経由で無効または失敗したレスポンスを返しました。再試行するか、リクエストを別のプロバイダーにルーティングしてください。`},tr:{label:`Kötü yukarı akış yanıtı`,description:`Yukarı akış sağlayıcısı proxy üzerinden geçersiz veya başarısız bir yanıt döndürdü. Tekrar deneyin veya isteği başka bir sağlayıcıya yönlendirin.`}},503:{en:{label:`Provider unavailable`,description:`The proxy or upstream provider is temporarily unavailable or overloaded. Wait briefly, then retry or switch provider.`},fr:{label:`Fournisseur indisponible`,description:`Le proxy ou le fournisseur en amont est temporairement indisponible ou surchargé. Patientez un instant, puis réessayez ou changez de fournisseur.`},ko:{label:`제공자 사용 불가`,description:`프록시 또는 업스트림 제공자가 일시적으로 사용할 수 없거나 과부하 상태입니다. 잠시 기다린 뒤 재시도하거나 제공자를 전환해야 합니다.`},zh:{label:`提供商不可用`,description:`代理或上游提供商暂时不可用或过载。请稍后重试,或切换提供商。`},"zh-TW":{label:`供應商不可用`,description:`代理或上游供應商暫時不可用或過載。請稍後重試,或切換供應商。`},de:{label:`Anbieter nicht verfügbar`,description:`Proxy oder Upstream-Anbieter ist vorübergehend nicht verfügbar oder überlastet. Kurz warten, dann erneut versuchen oder Anbieter wechseln.`},ru:{label:`Провайдер недоступен`,description:`Прокси или вышестоящий провайдер временно недоступен или перегружен. Немного подождите, затем повторите попытку или смените провайдера.`},ja:{label:`プロバイダー利用不可`,description:`プロキシまたは上流プロバイダーが一時的に利用不可または過負荷です。少し待ってから再試行するか、プロバイダーを切り替えてください。`},tr:{label:`Sağlayıcı kullanılamıyor`,description:`Proxy veya yukarı akış sağlayıcısı geçici olarak kullanılamıyor veya aşırı yüklü. Kısa bir süre bekleyip tekrar deneyin ya da sağlayıcı değiştirin.`}},504:{en:{label:`Upstream timeout`,description:`The upstream provider did not respond before the proxy timeout. Retry with a smaller request or choose a faster provider.`},fr:{label:`Délai d’attente du fournisseur en amont dépassé`,description:`Le fournisseur en amont n’a pas répondu avant l’expiration du délai du proxy. Réessayez avec une requête plus petite ou choisissez un fournisseur plus rapide.`},ko:{label:`업스트림 시간 초과`,description:`프록시 시간 제한 전에 업스트림 제공자가 응답하지 않았습니다. 더 작은 요청으로 재시도하거나 더 빠른 제공자를 선택해야 합니다.`},zh:{label:`上游超时`,description:`上游提供商未在代理超时前响应。请缩小请求后重试,或选择响应更快的提供商。`},"zh-TW":{label:`上游逾時`,description:`上游供應商未在代理逾時前回應。請縮小請求後重試,或選擇回應更快的供應商。`},de:{label:`Upstream-Timeout`,description:`Der Upstream-Anbieter antwortete nicht vor dem Proxy-Timeout. Mit kleinerer Anfrage erneut versuchen oder schnelleren Anbieter wählen.`},ru:{label:`Тайм-аут вышестоящего провайдера`,description:`Вышестоящий провайдер не ответил до истечения тайм-аута прокси. Повторите попытку с меньшим запросом или выберите более быстрого провайдера.`},ja:{label:`上流タイムアウト`,description:`上流プロバイダーがプロキシタイムアウト前に応答しませんでした。より小さいリクエストで再試行するか、より速いプロバイダーを選んでください。`},tr:{label:`Yukarı akış zaman aşımı`,description:`Yukarı akış sağlayıcısı proxy zaman aşımı süresinden önce yanıt vermedi. Daha küçük bir istekle tekrar deneyin veya daha hızlı bir sağlayıcı seçin.`}},529:{en:{label:`Provider overloaded`,description:`The upstream provider is overloaded or capacity-limited. Wait and retry, or switch to another account/provider.`},fr:{label:`Fournisseur surchargé`,description:`Le fournisseur en amont est surchargé ou sa capacité est limitée. Patientez et réessayez, ou changez de compte ou de fournisseur.`},ko:{label:`제공자 과부하`,description:`업스트림 제공자가 과부하 상태이거나 처리 용량이 제한되었습니다. 기다렸다가 재시도하거나 다른 계정/제공자로 전환해야 합니다.`},zh:{label:`提供商过载`,description:`上游提供商过载或容量受限。请等待后重试,或切换到其他账号/提供商。`},"zh-TW":{label:`供應商過載`,description:`上游供應商過載或容量受限。請等待後重試,或切換到其他帳號/供應商。`},de:{label:`Anbieter überlastet`,description:`Der Upstream-Anbieter ist überlastet oder kapazitätsbegrenzt. Warten und erneut versuchen oder anderes Konto/Anbieter nutzen.`},ru:{label:`Провайдер перегружен`,description:`Вышестоящий провайдер перегружен или ограничен по мощности. Подождите и повторите попытку либо переключитесь на другой аккаунт или провайдера.`},ja:{label:`プロバイダー過負荷`,description:`上流プロバイダーが過負荷または容量制限されています。待ってから再試行するか、別のアカウント/プロバイダーに切り替えてください。`},tr:{label:`Sağlayıcı aşırı yüklü`,description:`Yukarı akış sağlayıcısı aşırı yüklü veya kapasitesi sınırlı. Bekleyip tekrar deneyin veya başka bir hesap/sağlayıcıya geçin.`}}},rm={client:{en:{label:`Request error`,description:`The proxy or upstream provider rejected the request. Check the request shape, credentials, model name, and provider configuration.`},fr:{label:`Erreur de requête`,description:`Le proxy ou le fournisseur en amont a rejeté la requête. Vérifiez sa structure, les identifiants, le nom du modèle et la configuration du fournisseur.`},ko:{label:`요청 오류`,description:`프록시 또는 업스트림 제공자가 요청을 거부했습니다. 요청 형식, 자격 증명, 모델 이름, 제공자 설정을 확인해야 합니다.`},zh:{label:`请求错误`,description:`代理或上游提供商拒绝了该请求。请检查请求结构、凭据、模型名称和提供商配置。`},"zh-TW":{label:`請求錯誤`,description:`代理或上游供應商拒絕了該請求。請檢查請求結構、憑證、模型名稱和供應商配置。`},de:{label:`Anfragefehler`,description:`Der Proxy oder Upstream-Anbieter hat die Anfrage abgelehnt. Prüfe Anfrageformat, Anmeldedaten, Modellname und Anbieterkonfiguration.`},ru:{label:`Ошибка запроса`,description:`Прокси или вышестоящий провайдер отклонил запрос. Проверьте структуру запроса, учётные данные, имя модели и конфигурацию провайдера.`},ja:{label:`リクエストエラー`,description:`プロキシまたは上流プロバイダーがリクエストを拒否しました。リクエスト形式、認証情報、モデル名、プロバイダー設定を確認してください。`},tr:{label:`İstek hatası`,description:`Proxy veya yukarı akış sağlayıcısı isteği reddetti. İstek yapısını, kimlik bilgilerini, model adını ve sağlayıcı yapılandırmasını kontrol edin.`}},server:{en:{label:`Server or upstream error`,description:`opencodex or an upstream provider failed while processing the request. Retry later or route the request to another provider.`},fr:{label:`Erreur du serveur ou du fournisseur en amont`,description:`opencodex ou un fournisseur en amont a échoué lors du traitement de la requête. Réessayez plus tard ou acheminez la requête vers un autre fournisseur.`},ko:{label:`서버 또는 업스트림 오류`,description:`opencodex 또는 업스트림 제공자가 요청 처리 중 실패했습니다. 나중에 재시도하거나 다른 제공자로 라우팅해야 합니다.`},zh:{label:`服务器或上游错误`,description:`opencodex 或上游提供商处理请求时失败。请稍后重试,或将请求路由到其他提供商。`},"zh-TW":{label:`伺服器或上游錯誤`,description:`opencodex 或上游供應商處理請求時失敗。請稍後重試,或將請求路由到其他供應商。`},de:{label:`Server- oder Upstream-Fehler`,description:`opencodex oder ein Upstream-Anbieter ist bei der Anfragebearbeitung fehlgeschlagen. Später erneut versuchen oder zu einem anderen Anbieter routen.`},ru:{label:`Ошибка сервера или провайдера`,description:`opencodex или вышестоящий провайдер завершил обработку запроса с ошибкой. Повторите попытку позже или направьте запрос другому провайдеру.`},ja:{label:`サーバーまたは上流エラー`,description:`opencodex または上流プロバイダーがリクエスト処理中に失敗しました。後で再試行するか、リクエストを別のプロバイダーにルーティングしてください。`},tr:{label:`Sunucu veya yukarı akış hatası`,description:`opencodex veya bir yukarı akış sağlayıcısı isteği işlerken başarısız oldu. Daha sonra tekrar deneyin veya isteği başka bir sağlayıcıya yönlendirin.`}}};function im(e){return e.toLowerCase().startsWith(`fr`)?`fr`:e===`de`||e===`ko`||e===`zh`||e===`zh-TW`||e===`ru`||e===`ja`||e===`tr`?e:`en`}function am(e,t){if(e<400)return null;let n=im(t);return(nm[Math.trunc(e)]??(e<500?rm.client:rm.server))[n]}var om=[`provider`,`usage`,`injection`];function sm(e){return e>0?`[${new Date(e).toLocaleTimeString()}] `:``}function cm(e){return new Date(e).toLocaleTimeString()}function lm(e,t){return t===`provider`?!!e?.enabled:t===`usage`?!!e?.usage:!!e?.injection}function um(e,t){return t===`debug`?e.enabled:t===`usage`?e.usage:t===`injection`?e.injection:e.claude}function dm({entries:e}){let{t}=ct();return(0,J.jsxs)(`div`,{className:`card`,style:{marginBottom:16,padding:`12px 14px`},children:[(0,J.jsx)(`div`,{className:`font-semibold`,style:{marginBottom:4},children:t(`debug.claudeInbound.title`)}),(0,J.jsx)(`div`,{className:`muted text-control`,style:{marginBottom:10},children:t(`debug.claudeInbound.sub`)}),e.length===0?(0,J.jsx)(`div`,{className:`muted text-control`,children:t(`debug.claudeInbound.empty`)}):(0,J.jsx)(`div`,{style:{overflowX:`auto`},children:(0,J.jsxs)(`table`,{className:`table text-label`,children:[(0,J.jsx)(`thead`,{children:(0,J.jsxs)(`tr`,{children:[(0,J.jsx)(`th`,{children:t(`debug.claudeInbound.time`)}),(0,J.jsx)(`th`,{children:t(`debug.claudeInbound.endpoint`)}),(0,J.jsx)(`th`,{children:t(`debug.claudeInbound.model`)}),(0,J.jsx)(`th`,{children:`thinking`}),(0,J.jsx)(`th`,{children:`effort`}),(0,J.jsx)(`th`,{children:`beta`}),(0,J.jsx)(`th`,{children:`metadata`}),(0,J.jsx)(`th`,{children:`system`})]})}),(0,J.jsx)(`tbody`,{children:e.map(e=>(0,J.jsxs)(`tr`,{children:[(0,J.jsx)(`td`,{className:`muted mono`,children:cm(e.at)}),(0,J.jsx)(`td`,{className:`mono`,children:e.endpoint}),(0,J.jsxs)(`td`,{className:`mono`,title:e.resolvedModel,children:[e.model,e.resolvedModel&&e.resolvedModel!==e.model&&(0,J.jsxs)(`span`,{className:`muted`,children:[` → `,e.resolvedModel]})]}),(0,J.jsxs)(`td`,{className:`mono`,children:[e.thinkingType??`-`,e.thinkingBudgetTokens!==void 0&&(0,J.jsxs)(`span`,{className:`muted`,children:[` (`,e.thinkingBudgetTokens,`)`]})]}),(0,J.jsx)(`td`,{className:`mono`,children:e.outputConfigEffort??`-`}),(0,J.jsx)(`td`,{className:`mono`,title:e.anthropicBeta,style:{maxWidth:160,overflow:`hidden`,textOverflow:`ellipsis`,whiteSpace:`nowrap`},children:e.anthropicBeta??`-`}),(0,J.jsx)(`td`,{className:`mono`,title:e.metadataKeys?.join(`, `),children:e.hasMetadataUserId?`user_id ${e.userIdTag??``}`:t(`debug.claudeInbound.none`)}),(0,J.jsx)(`td`,{className:`mono`,children:e.hasSystem?e.systemTag??`yes`:t(`debug.claudeInbound.none`)})]},e.id))})]})})]})}function fm({debug:e,stream:t,streamEnabled:n,entries:r,scrollContainerRef:i,lineVirtualizer:a}){let{t:o}=ct();return e?n?r.length===0?(0,J.jsxs)(`div`,{className:`empty`,children:[(0,J.jsx)(`div`,{className:`font-semibold`,style:{marginBottom:6},children:o(`debug.noLinesTitle`)}),(0,J.jsx)(`div`,{className:`muted text-control`,style:{maxWidth:560,marginInline:`auto`},children:o(`debug.noLines.${t}`)})]}):(0,J.jsx)(`div`,{ref:i,className:`log-detail-json`,style:{maxHeight:`calc(100vh - 280px)`,overflow:`auto`},children:(0,J.jsx)(`div`,{style:{position:`relative`,height:a.getTotalSize(),width:`100%`},children:a.getVirtualItems().map(e=>(0,J.jsx)(`div`,{ref:a.measureElement,"data-index":e.index,style:{position:`absolute`,top:0,left:0,width:`100%`,transform:`translateY(${e.start}px)`},children:`${sm(r[e.index].at)}${r[e.index].line}`},e.key))})}):(0,J.jsxs)(`div`,{className:`empty`,children:[(0,J.jsx)(`div`,{className:`font-semibold`,style:{marginBottom:6},children:o(`debug.emptyTitle`)}),(0,J.jsx)(`div`,{className:`muted text-control`,style:{maxWidth:560,marginInline:`auto`},children:o(`debug.empty`)})]}):null}function pm({debug:e,debugBusy:t,stream:n,onSetFlag:r,onReset:i,onStreamChange:a}){let{t:o}=ct();return(0,J.jsxs)(`div`,{className:`card`,style:{marginBottom:16,padding:`12px 14px`},children:[(0,J.jsxs)(`div`,{style:{display:`flex`,alignItems:`center`,justifyContent:`space-between`,gap:12,flexWrap:`wrap`},children:[(0,J.jsx)(`div`,{style:{display:`flex`,flexWrap:`wrap`,gap:16},children:[`debug`,`usage`,`injection`,`claude`].map(n=>{let i=um(e,n);return(0,J.jsxs)(`div`,{style:{display:`inline-flex`,alignItems:`center`,gap:10,minWidth:220},children:[(0,J.jsx)(Tt,{on:i,disabled:t,label:o(`debug.${n}`),onClick:()=>r(n,!i)}),(0,J.jsx)(`span`,{className:`text-control`,children:o(`debug.${n}`)})]},n)})}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,disabled:t,onClick:i,children:o(`debug.reset`)})]}),(e.enabled||e.usage||e.injection)&&(0,J.jsxs)(`div`,{style:{display:`inline-flex`,gap:6,marginTop:12},children:[e.enabled&&(0,J.jsx)(`button`,{type:`button`,className:`btn btn-sm${n===`provider`?` btn-primary`:` btn-ghost`}`,onClick:()=>a(`provider`),children:o(`debug.streamProvider`)}),e.usage&&(0,J.jsx)(`button`,{type:`button`,className:`btn btn-sm${n===`usage`?` btn-primary`:` btn-ghost`}`,onClick:()=>a(`usage`),children:o(`debug.streamUsage`)}),e.injection&&(0,J.jsx)(`button`,{type:`button`,className:`btn btn-sm${n===`injection`?` btn-primary`:` btn-ghost`}`,onClick:()=>a(`injection`),children:o(`debug.streamInjection`)})]})]})}function mm({embedded:e,refreshing:t,streamEnabled:n,follow:r,onRefresh:i,onFollowChange:a}){let{t:o}=ct();return(0,J.jsxs)(J.Fragment,{children:[(0,J.jsxs)(`div`,{className:e?`row`:`page-head`,style:e?{justifyContent:`flex-end`,marginBottom:4}:void 0,children:[!e&&(0,J.jsx)(`h2`,{children:o(`debug.title`)}),(0,J.jsxs)(`div`,{style:{display:`inline-flex`,alignItems:`center`,gap:12},children:[(0,J.jsxs)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,disabled:t||!n,onClick:i,children:[(0,J.jsx)(pe,{}),` `,o(`debug.refresh`)]}),(0,J.jsxs)(`label`,{className:`muted text-control`,style:{cursor:`pointer`,display:`inline-flex`,alignItems:`center`,gap:6},children:[(0,J.jsx)(`input`,{type:`checkbox`,checked:r,onChange:e=>a(e.target.checked)}),o(`debug.follow`)]})]})]}),(0,J.jsx)(`p`,{className:`page-sub`,children:o(`debug.subtitle`)})]})}function hm(e){return`debug-settings:${e}`}function gm({apiBase:e,embedded:t,active:n=!0}){let{t:r}=ct(),i=`ocx.debug.settings.v1:${e}`,a=gr(i),o=hm(e),[s,c]=(0,_.useState)(!1),[l,u]=(0,_.useState)(`provider`),[d,f]=(0,_.useState)([]),[p,m]=(0,_.useState)(!0),[h,g]=(0,_.useState)(!1),v=(0,_.useRef)(0),y=(0,_.useRef)(0),b=(0,_.useRef)(0),x=(0,_.useRef)(null),S=(0,_.useRef)(null),C=(0,_.useRef)(null),w=ml(o,[e],async t=>{let n=await fetch(`${e}/api/debug`,{signal:t});if(!n.ok)throw Error(String(n.status));let r=await n.json();return br(i,r),r},{pollMs:2e3,enabled:n,isEmpty:()=>!1,initialData:a??void 0}),T=w.state,E=w.data??a??null,D=G(`debug-claude-inbound:${e}`,[e,E?.claude],async t=>{let n=await fetch(`${e}/api/claude/inbound-debug`,{signal:t});if(!n.ok)return[];let r=await n.json();return Array.isArray(r.entries)?r.entries:[]},{pollMs:2e3,enabled:n&&!!E?.claude}).data??[],O=Xp({count:d.length,getScrollElement:()=>S.current,estimateSize:()=>20,overscan:30,getItemKey:e=>d[e].seq}),k=(0,_.useCallback)(e=>lm(E,e),[E]);(0,_.useEffect)(()=>{if(!E||k(l))return;let e=om.find(k);if(!e)return;let t=window.setTimeout(()=>u(e),0);return()=>window.clearTimeout(t)},[E,l,k]);let A=k(l),j=l===`provider`?`${e}/api/debug/logs`:l===`usage`?`${e}/api/debug/usage-logs`:`${e}/api/debug/injection-logs`,M=(0,_.useCallback)(async(e,t)=>{let n=++b.current;if(!A){n===b.current&&(f([]),v.current=0);return}g(!0);try{let r=new URLSearchParams({limit:`500`});!e&&v.current>0&&r.set(`after`,String(v.current));let i=await fetch(`${j}?${r}`,{signal:t});if(!i.ok||t?.aborted||n!==b.current)return;let a=await i.json();if(t?.aborted||n!==b.current||a.length===0)return;f(t=>(e?a:[...t,...a]).slice(-2e3)),v.current=a[a.length-1].seq}catch{}finally{n===b.current&&g(!1)}},[j,A]);(0,_.useEffect)(()=>{if(!n)return;let t=`${e}:${l}:${A}`,r=C.current!==t;if(C.current=t,!r&&d.length>0)return;v.current=0;let i=new AbortController,a=window.setTimeout(()=>{r&&f([]),M(!0,i.signal)},0);return()=>{window.clearTimeout(a),b.current+=1,i.abort()}},[n,e,l,A]);let N=(0,_.useRef)(!1),P=(0,_.useEffectEvent)(()=>{if(N.current)return;N.current=!0;let e=Vn(1e4);M(!1,e.signal).finally(()=>{e.clear(),N.current=!1})});(0,_.useEffect)(()=>{if(!(!n||!p||!A))return Gn(()=>P(),1e3)},[n,p,A]),(0,_.useEffect)(()=>{p&&d.length>0&&O.scrollToIndex(d.length-1,{align:`end`})},[d,p,O]);let F=async t=>{let n=++y.current;c(!0);let r=async()=>{try{let r=await fetch(`${e}/api/debug`,{method:`PUT`,headers:{"content-type":`application/json`},body:JSON.stringify(t)});if(!r.ok)return;let a=await r.json();if(n!==y.current)return;br(i,a),K(o,a)}catch{}},a=(x.current??Promise.resolve()).then(r,r);x.current=a.then(()=>void 0,()=>void 0);try{await a}finally{n===y.current&&c(!1)}},I=async(e,t)=>{await F({[e]:t})},L=async()=>{await F({reset:!0})};return(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)(mm,{embedded:t,refreshing:h,streamEnabled:A,follow:p,onRefresh:()=>void M(!0),onFollowChange:m}),!E&&T.showError?(0,J.jsxs)(`div`,{className:`notice notice-err`,role:`alert`,children:[(0,J.jsx)(`span`,{children:r(`debug.loadFailed`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:()=>w.refresh(),children:r(`common.retry`)})]}):T.showSkeleton&&!E?(0,J.jsx)(gl,{label:r(`debug.loading`),rows:3}):E?(0,J.jsx)(pm,{debug:E,debugBusy:s,stream:l,onSetFlag:(e,t)=>{I(e,t)},onReset:()=>{L()},onStreamChange:u}):null,E&&T.showError&&(0,J.jsx)($,{tone:`err`,children:r(`debug.loadFailed`)}),E?.claude&&(0,J.jsx)(dm,{entries:D}),(0,J.jsx)(fm,{debug:!!E,stream:l,streamEnabled:A,entries:d,scrollContainerRef:S,lineVirtualizer:O})]})}function _m(){return window.location.hash.replace(/^#\/?/,``)===`logs/debug`?`debug`:`logs`}function vm(e){window.location.hash=e===`debug`?`logs/debug`:`logs`}function ym(e){e.key===`ArrowLeft`||e.key===`Home`?(e.preventDefault(),vm(`logs`),document.getElementById(`logs-tab-logs`)?.focus()):(e.key===`ArrowRight`||e.key===`End`)&&(e.preventDefault(),vm(`debug`),document.getElementById(`logs-tab-debug`)?.focus())}function bm(e,t){return[`${t(`logs.modelTooltip.model`)}=${e.model}`,e.resolvedModel?`${t(`logs.modelTooltip.resolvedModel`)}=${e.resolvedModel}`:void 0,e.requestedServiceTier?`${t(`logs.modelTooltip.requestedTier`)}=${e.requestedServiceTier}`:void 0,e.configuredServiceTier?`${t(`logs.modelTooltip.configuredTier`)}=${e.configuredServiceTier}`:void 0,e.responseServiceTier?`${t(`logs.modelTooltip.responseTier`)}=${e.responseServiceTier}`:void 0,e.modelSupportsServiceTier===void 0?void 0:`${t(`logs.modelTooltip.supportsTier`)}=${e.modelSupportsServiceTier}`].filter(Boolean).join(` · `)}function xm(e){return e.requestedSpeedLabel||void 0}var Sm=new Intl.NumberFormat(`en-US`,{style:`currency`,currency:`USD`,currencyDisplay:`narrowSymbol`,minimumFractionDigits:4,maximumFractionDigits:4});function Cm(e,t,n,r=!1){if(!Number.isFinite(e)||e<0)return t(`logs.cost.unavailable`);let i=Sm.format(e);return t(r?`logs.cost.lowerBound`:`logs.cost.approximate`,{amount:i})}function wm(e,t,n){return!e||e.kind===`unavailable`?t(`logs.cost.unavailable`):Cm(e.estimate.cost.total,t,n,e.estimate.priorityLowerBound)}function Tm(e){let t=0,n=!0,r=0,i=0,a=0;for(let o of e){if(o.usageStatus===`unsupported`){a+=1;continue}let e=o.displayMetrics?.cost;if(e?.kind===`value`){let i=e.estimate.cost.total;if(Number.isFinite(i)&&i>=0){t+=i,r+=1,n&&=e.estimate.priorityLowerBound===!0;continue}}i+=1}return{estimatedCostUsd:t,priorityLowerBound:r>0&&n,unpricedRequests:i,unmeteredRequests:a}}function Em(e){return e===`cursor`||e.startsWith(`cursor-`)}function Dm(e){let t=e.usage;if(!t)return{};let n=typeof t.cacheCreationInputTokens==`number`?t.cacheCreationInputTokens:void 0;return{read:typeof t.cacheReadInputTokens==`number`?t.cacheReadInputTokens:typeof t.cachedInputTokens==`number`&&n!==void 0?Math.max(0,t.cachedInputTokens-n):t.cachedInputTokens,write:n}}function Om(e,t){if(!e.usage)return;let n=Dm(e),r=[`${t(`logs.tokens.input`)}=${e.usage.inputTokens}`,`${t(`logs.tokens.output`)}=${e.usage.outputTokens}`];return n.read!==void 0&&r.push(`${t(`logs.tokens.cacheRead`)}=${n.read}`),n.write!==void 0&&r.push(`${t(`logs.tokens.cacheWrite`)}=${n.write}`),typeof e.usage.contextTotalTokens==`number`&&r.push(`${t(`logs.tokens.contextTotal`)}=${e.usage.contextTotalTokens}`),typeof e.usage.reasoningOutputTokens==`number`&&r.push(`${t(`logs.tokens.reasoning`)}=${e.usage.reasoningOutputTokens}`),e.usageStatus===`estimated`&&r.push(t(`logs.tokens.estimatedNote`)),e.usageStatus===`estimated`&&n.read===void 0&&n.write===void 0&&r.push(t(Em(e.provider)?`logs.tokens.noCacheCursorNote`:`logs.tokens.noCacheNote`)),r.join(` · `)}function km(e,t){return t===`all`?!0:t===`claude`?e.surface===`claude`||e.surface===`claude-desktop`:t===`grok`?e.surface===`grok`:e.surface===void 0}function Am(e,t){let n=t.trim().toLowerCase();if(!n)return!0;let r=Array.isArray(e.attempts)?e.attempts.flatMap(e=>e&&typeof e==`object`?[e.provider,e.model]:[]):[];return[e.model,e.resolvedModel,e.provider,...r].some(e=>typeof e==`string`&&e.toLowerCase().includes(n))}function jm(e){return e===void 0||typeof e==`string`}function Mm(e){if(e===void 0)return!0;if(!e||typeof e!=`object`||!jm(e.routeKind)||e.profile!==void 0&&(!e.profile||typeof e.profile!=`object`||!jm(e.profile.id)||!jm(e.profile.revision))||e.selected!==void 0&&(!e.selected||typeof e.selected!=`object`||!jm(e.selected.provider)||!jm(e.selected.model)||!jm(e.selected.reason)))return!1;if(e.candidates===void 0)return!0;if(!Array.isArray(e.candidates))return!1;for(let t of e.candidates){if(!t||typeof t!=`object`||!jm(t.provider)||!jm(t.model)||t.eligible!==void 0&&typeof t.eligible!=`boolean`)return!1;if(t.exclusions!==void 0){if(!Array.isArray(t.exclusions))return!1;for(let e of t.exclusions)if(!e||typeof e!=`object`||!jm(e.code))return!1}}return!0}function Nm(e){if(e.routeDecision===void 0||Mm(e.routeDecision))return e;let t={...e};return delete t.routeDecision,t}function Pm(e){return`ocx.logs.list.v1:${e}`}function Fm(e){if(!Array.isArray(e))return null;for(let t of e)if(!t||typeof t!=`object`||typeof t.timestamp!=`number`||typeof t.model!=`string`||typeof t.provider!=`string`||typeof t.status!=`number`||typeof t.durationMs!=`number`||t.shadowCallRewrittenFrom!==void 0&&typeof t.shadowCallRewrittenFrom!=`string`||!Mm(t.routeDecision))return null;return e}function Im(e){if(!e.usage)return typeof e.totalTokens==`number`?e.totalTokens:void 0;let t=e.usage.inputTokens+e.usage.outputTokens,n=e.usage.totalTokens??e.totalTokens;return typeof n==`number`?Math.max(n,t):t}function Lm(e){let t=Im(e),n=e.usage?.contextTotalTokens;return typeof n==`number`?Math.max(t??0,n)||void 0:t}function Rm(e){let t=e.requestedEffort?.replace(/\s*->\s*/g,` → `),n=e.effectiveEffort;return t?!n||t===n||t.split(` → `).at(-1)===n?t:`${t} → ${n}`:n??`-`}function zm(e){if(!(!e.reasoningWireField||e.reasoningWireValue===void 0))return`${e.reasoningWireField}=${e.reasoningWireValue}`}function Bm(e,t){if(!e||e.kind===`unavailable`||!Number.isFinite(e.value)||e.value<=0)return`—`;let n=e.value>=100?0:1,r=new Intl.NumberFormat(t,{minimumFractionDigits:n,maximumFractionDigits:n}).format(e.value);return`${e.estimated?`~`:``}${r}`}var Vm=2e3,Hm=4,Um=3,Wm={usage_missing:`logs.detail.reason.usage_missing`,usage_unsupported:`logs.detail.reason.usage_unsupported`,output_missing:`logs.detail.reason.output_missing`,invalid_duration:`logs.detail.reason.invalid_duration`,price_unmatched:`logs.detail.reason.price_unmatched`,invalid_cache_breakdown:`logs.detail.reason.invalid_cache_breakdown`,invalid_usage:`logs.detail.reason.invalid_usage`,combo_attempt_unavailable:`logs.detail.reason.combo_attempt_unavailable`},Gm={usage_estimated:`logs.detail.estimate.usage_estimated`,cache_detail_missing:`logs.detail.estimate.cache_detail_missing`,expected_price_overlay:`logs.detail.estimate.expected_price_overlay`,provider_cost_overlay:`logs.detail.estimate.provider_cost_overlay`,priority_lower_bound:`logs.detail.estimate.priority_lower_bound`},Km={"transient-5xx":`logs.detail.attempt.recovery.transient5xx`,"connection-reset":`logs.detail.attempt.recovery.connectionReset`,"oauth-401":`logs.detail.attempt.recovery.oauth401`,"key-429":`logs.detail.attempt.recovery.key429`,"rate-limit-429":`logs.detail.attempt.recovery.rateLimit429`,"anthropic-oauth-429":`logs.detail.attempt.recovery.anthropicOauth429`,"image-413":`logs.detail.attempt.recovery.image413`,"empty-completion":`logs.detail.attempt.recovery.emptyCompletion`};function qm(e){return Wm[e]}function Jm(e){return Gm[e]}function Ym(e){return Km[e]??`logs.detail.attempt.recovery.unknown`}function Xm(e){return e===`verified`?`logs.detail.verification.verified`:`logs.detail.verification.derived`}function Zm(e){return e>=200&&e<300?`var(--green)`:e>=400?`var(--red)`:`var(--amber)`}function Qm(e,t,n){let r=n?{timeZone:n}:void 0;try{return{date:new Date(e).toLocaleDateString(t,r),time:new Date(e).toLocaleTimeString(t,r)}}catch{return{date:new Date(e).toLocaleDateString(t),time:new Date(e).toLocaleTimeString(t)}}}function $m(e,t,n){let{date:r,time:i}=Qm(e,t,n);return`${r} ${i}`}function eh(e){let t=0;for(let n of e){let e=Im(n);e!==void 0&&(t+=e)}return{requests:e.length,totalTokens:t,...Tm(e)}}function th({apiBase:e}){let{t,locale:n}=ct(),r=Pm(e),i=Fm(gr(r)),[a,o]=(0,_.useState)(!0),[s,c]=(0,_.useState)({error:null,count:0}),[l,u]=(0,_.useState)(null),[d,f]=(0,_.useState)(`all`),[p,m]=(0,_.useState)(!1),[h,g]=(0,_.useState)(``),[v,y]=(0,_.useState)(``),[b,x]=(0,_.useState)(),S=(0,_.useRef)(null),C=(0,_.useRef)({key:r,failures:0,nextAttemptAt:0,error:null}),w=et.find(e=>e.code===n)?.htmlLang,[T,E]=(0,_.useState)();(0,_.useEffect)(()=>{let t=new AbortController,n=!1;return fetch(`${e}/api/settings`,{signal:t.signal}).then(e=>e.ok?e.json():null).then(e=>{n||!e||typeof e.timeZone==`string`&&e.timeZone.trim()&&E(e.timeZone.trim())}).catch(()=>{}),()=>{n=!0,t.abort()}},[e]);let[D,O]=(0,_.useState)(_m),[k,A]=(0,_.useState)(()=>_m()===`debug`);(0,_.useEffect)(()=>{let e=()=>O(_m());return window.addEventListener(`hashchange`,e),()=>window.removeEventListener(`hashchange`,e)},[]),(0,_.useEffect)(()=>{D===`debug`&&A(!0)},[D]);let j=vm,M=(0,_.useCallback)(async t=>{let n=C.current;if(n.key!==r&&(n={key:r,failures:0,nextAttemptAt:0,error:null},C.current=n),n.failures>0&&Date.now()e.length===0,enabled:D===`logs`,pollMs:a?Vm:void 0,initialData:i??void 0}),P=N.state,F=P.data??i??[],I=N.refresh,L=(0,_.useCallback)(()=>{C.current={key:r,failures:0,nextAttemptAt:0,error:null},I({forceLoading:!0})},[I,r]),R=!N.refreshing&&P.showError;!N.refreshing&&!P.showError&&P.data!==void 0&&s.count!==0?c({error:null,count:0}):R&&s.error!==P.error&&c(e=>({error:P.error,count:e.count+1}));let z=s.count>=Um||!a&&R,B=l?am(l.status,n):null,V=h.trim();(0,_.useEffect)(()=>{let e=!1;if(!V){x(void 0);return}return em(V).then(t=>{e||x(t)}),()=>{e=!0}},[V]);let H=F.filter(e=>km(e,d)&&(!p||!!e.shadowCallRewrittenFrom)&&Am(e,v)&&(!V||tm(e.conversationId,V,b))),U=V?eh(H):null,W=Xp({count:H.length,getScrollElement:()=>S.current,estimateSize:()=>92,overscan:15,getItemKey:e=>{let t=H[H.length-1-e];return t.requestId??`${t.timestamp}:${t.model}:${t.provider}`}}),ee=W.getVirtualItems(),G=ee.length>0?ee[0].start:0,K=ee.length>0?W.getTotalSize()-ee[ee.length-1].end:0;return(0,J.jsxs)(`div`,{className:`logs-page`,children:[(0,J.jsxs)(`div`,{className:`page-head`,children:[(0,J.jsx)(`h2`,{children:t(`nav.logs`)}),D===`logs`&&(0,J.jsxs)(`label`,{className:`muted text-control logs-auto-refresh`,children:[(0,J.jsx)(`input`,{type:`checkbox`,checked:a,onChange:e=>o(e.target.checked)}),t(`logs.autoRefresh`)]})]}),(0,J.jsxs)(`div`,{className:`page-tabs`,role:`tablist`,"aria-label":t(`nav.logs`),children:[(0,J.jsx)(`button`,{type:`button`,role:`tab`,id:`logs-tab-logs`,"aria-selected":D===`logs`,"aria-controls":`logs-panel-logs`,tabIndex:D===`logs`?0:-1,className:`page-tab${D===`logs`?` page-tab--active`:``}`,onClick:()=>j(`logs`),onKeyDown:ym,children:t(`logs.tabLogs`)}),(0,J.jsx)(`button`,{type:`button`,role:`tab`,id:`logs-tab-debug`,"aria-selected":D===`debug`,"aria-controls":`logs-panel-debug`,tabIndex:D===`debug`?0:-1,className:`page-tab${D===`debug`?` page-tab--active`:``}`,onClick:()=>j(`debug`),onKeyDown:ym,children:t(`logs.tabDebug`)})]}),k&&(0,J.jsx)(`div`,{role:`tabpanel`,id:`logs-panel-debug`,"aria-labelledby":`logs-tab-debug`,hidden:D!==`debug`,children:(0,J.jsx)(gm,{apiBase:e,embedded:!0,active:D===`debug`})}),(0,J.jsxs)(`div`,{role:`tabpanel`,id:`logs-panel-logs`,"aria-labelledby":`logs-tab-logs`,hidden:D!==`logs`,children:[(0,J.jsxs)(`div`,{className:`logs-toolbar`,children:[(0,J.jsx)(`span`,{className:`muted text-control`,children:t(`logs.filter.surface.label`)}),(0,J.jsx)(`div`,{className:`segmented logs-segmented`,role:`radiogroup`,"aria-label":t(`logs.filter.surface.label`),children:[`all`,`claude`,`codex`,`grok`].map(e=>(0,J.jsx)(`button`,{type:`button`,role:`radio`,"aria-checked":d===e,className:`btn btn-sm${d===e?` btn-primary`:` btn-ghost`}`,style:{background:d===e?void 0:`transparent`,color:d===e?void 0:`var(--muted)`},onClick:()=>f(e),children:t(`logs.filter.surface.${e}`)},e))}),(0,J.jsxs)(`label`,{className:`muted text-control logs-filter-field`,children:[(0,J.jsx)(`input`,{type:`checkbox`,checked:p,onChange:e=>m(e.target.checked)}),t(`logs.filter.interceptedHelpersOnly`)]}),(0,J.jsxs)(`label`,{className:`muted text-control logs-filter-field`,children:[t(`logs.filter.conversation.label`),(0,J.jsx)(`input`,{type:`search`,className:`input mono`,value:h,onChange:e=>g(e.target.value),placeholder:t(`logs.filter.conversation.placeholder`),"aria-label":t(`logs.filter.conversation.label`)})]}),(0,J.jsxs)(`label`,{className:`muted text-control logs-filter-field`,children:[t(`logs.filter.model.label`),(0,J.jsx)(`input`,{type:`search`,className:`input mono`,value:v,onChange:e=>y(e.target.value),placeholder:t(`logs.filter.model.placeholder`),"aria-label":t(`logs.filter.model.label`)})]}),V&&(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:()=>g(``),children:t(`logs.filter.conversation.clear`)})]}),U&&(0,J.jsx)(`div`,{className:`logs-conversation-totals`,children:(0,J.jsxs)($,{tone:`ok`,children:[t(`logs.conversation.totals`,{requests:U.requests,tokens:Rn(U.totalTokens,w??n),cost:Cm(U.estimatedCostUsd,t,w,U.priorityLowerBound)}),` `,(0,J.jsxs)(`span`,{className:`muted`,children:[t(`logs.conversation.scope`),U.unpricedRequests+U.unmeteredRequests>0?` ${t(`logs.conversation.excluded`,{unpriced:U.unpricedRequests,unmetered:U.unmeteredRequests})}`:``]})]})}),P.kind===`failed-cold`&&(0,J.jsxs)($,{tone:`err`,children:[P.error instanceof Error?`${t(`logs.loadError`)} ${P.error.message}`:t(`logs.loadError`),` `,(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:L,disabled:P.refreshing,children:t(`common.retry`)})]}),z&&F.length>0&&(0,J.jsxs)($,{tone:`err`,children:[t(`logs.loadError`),` `,(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:L,disabled:N.refreshing,children:t(`common.retry`)})]}),P.kind===`failed-cold`?null:P.showSkeleton&&F.length===0?(0,J.jsx)(gl,{label:t(`common.loading`),rows:6}):H.length===0?(0,J.jsx)(Ot,{title:t(`logs.noRequests`)}):(0,J.jsx)(J.Fragment,{children:(0,J.jsx)(`div`,{ref:S,className:`tbl-wrap logs-table-wrap`,children:(0,J.jsxs)(`table`,{className:`tbl logs-table`,children:[(0,J.jsxs)(`colgroup`,{children:[(0,J.jsx)(`col`,{className:`logs-col-time`}),(0,J.jsx)(`col`,{className:`logs-col-tokens`}),(0,J.jsx)(`col`,{className:`logs-col-rate`}),(0,J.jsx)(`col`,{className:`logs-col-cost`}),(0,J.jsx)(`col`,{className:`logs-col-model`}),(0,J.jsx)(`col`,{className:`logs-col-effort`}),(0,J.jsx)(`col`,{className:`logs-col-provider`}),(0,J.jsx)(`col`,{className:`logs-col-status`}),(0,J.jsx)(`col`,{className:`logs-col-request`}),(0,J.jsx)(`col`,{className:`logs-col-duration`})]}),(0,J.jsx)(`thead`,{children:(0,J.jsxs)(`tr`,{children:[(0,J.jsx)(`th`,{children:t(`logs.col.time`)}),(0,J.jsx)(`th`,{className:`num log-col-tokens`,children:t(`logs.col.tokens`)}),(0,J.jsx)(`th`,{className:`num log-col-rate`,title:t(`logs.metric.tokPerSecTitle`),children:t(`logs.col.tokPerSec`)}),(0,J.jsx)(`th`,{className:`num log-col-cost`,title:t(`logs.metric.estimatedCostTitle`),children:t(`logs.col.estimatedCost`)}),(0,J.jsx)(`th`,{className:`log-col-model`,children:t(`logs.col.model`)}),(0,J.jsx)(`th`,{children:t(`logs.col.effort`)}),(0,J.jsx)(`th`,{children:t(`logs.col.provider`)}),(0,J.jsx)(`th`,{children:t(`logs.col.status`)}),(0,J.jsx)(`th`,{children:t(`logs.col.request`)}),(0,J.jsx)(`th`,{className:`num log-col-duration`,children:t(`logs.col.duration`)})]})}),(0,J.jsxs)(`tbody`,{children:[G>0&&(0,J.jsx)(`tr`,{children:(0,J.jsx)(`td`,{colSpan:10,className:`logs-virtual-spacer`,style:{height:G}})}),ee.map(e=>{let r=H[H.length-1-e.index],i=zm(r),a=Qm(r.timestamp,w,T);return(0,J.jsxs)(`tr`,{"data-index":e.index,ref:W.measureElement,children:[(0,J.jsx)(`td`,{className:`muted mono log-col-time`,children:(0,J.jsxs)(`span`,{className:`logs-stack-start`,children:[(0,J.jsx)(`span`,{children:a.date}),(0,J.jsx)(`span`,{children:a.time})]})}),(0,J.jsx)(`td`,{className:`num mono log-col-tokens`,title:Om(r,t),children:(()=>{let e=Lm(r),{read:i,write:a}=Dm(r);return e===void 0?(0,J.jsx)(`span`,{className:`muted`,children:t(`logs.tokens.${r.usageStatus??`unreported`}`)}):(0,J.jsxs)(`span`,{className:`logs-stack-end`,children:[(0,J.jsxs)(`span`,{children:[r.usageStatus===`estimated`?`~`:``,Rn(e,n)]}),i!==void 0&&i>0&&(0,J.jsxs)(`span`,{className:`muted text-caption leading-tight`,children:[`c `,Rn(i,n)]}),a!==void 0&&a>0&&(0,J.jsxs)(`span`,{className:`muted text-caption leading-tight`,children:[`w `,Rn(a,n)]}),r.usageStatus===`estimated`&&i===void 0&&a===void 0&&(0,J.jsx)(`span`,{className:`muted text-caption leading-tight`,children:t(Em(r.provider)?`logs.tokens.noCacheCursor`:`logs.tokens.noCache`)})]})})()}),(0,J.jsx)(`td`,{className:`num mono log-col-rate`,children:Bm(r.displayMetrics?.tokPerSecond,w)}),(0,J.jsx)(`td`,{className:`num mono log-col-cost`,children:wm(r.displayMetrics?.cost,t,w)}),(0,J.jsx)(`td`,{className:`mono log-col-model`,title:bm(r,t),children:(0,J.jsxs)(`span`,{className:`logs-model-cell`,children:[(0,J.jsx)(`span`,{children:fl(r.resolvedModel??r.model)}),r.shadowCallRewrittenFrom&&(0,J.jsx)(`span`,{className:`badge badge-muted`,style:{whiteSpace:`nowrap`},title:t(`logs.badge.interceptedHelperTitle`),children:t(`logs.badge.interceptedHelper`,{model:r.shadowCallRewrittenFrom})}),(r.surface===`claude`||r.surface===`claude-desktop`)&&(0,J.jsx)(`span`,{className:`badge badge-accent`,children:t(`logs.badge.claude`)}),r.surface===`grok`&&(0,J.jsx)(`span`,{className:`badge badge-accent`,children:t(`logs.badge.grok`)}),xm(r)&&(0,J.jsx)(`span`,{className:`badge badge-amber`,children:xm(r)})]})}),(0,J.jsx)(`td`,{className:`mono log-reasoning-cell`,title:i,children:Rm(r)}),(0,J.jsx)(`td`,{className:`muted`,children:jn(r.provider,t)}),(0,J.jsx)(`td`,{children:(0,J.jsxs)(`span`,{className:`log-status-cell`,children:[(0,J.jsx)(`span`,{className:`mono font-semibold`,style:{color:Zm(r.status)},children:r.status}),(0,J.jsx)(`button`,{type:`button`,className:`log-detail-btn`,onClick:()=>u(r),"aria-label":`${t(`logs.details`)}: ${r.requestId??r.status}`,children:t(`logs.details`)})]})}),(0,J.jsx)(`td`,{className:`muted mono`,children:(0,J.jsx)(`span`,{className:`log-reqid`,title:r.requestId,children:r.requestId??`-`})}),(0,J.jsxs)(`td`,{className:`num log-col-duration`,children:[r.durationMs,`ms`]})]},e.key)}),K>0&&(0,J.jsx)(`tr`,{children:(0,J.jsx)(`td`,{colSpan:10,className:`logs-virtual-spacer`,style:{height:K}})})]})]})})}),l&&(0,J.jsx)(rh,{detail:l,detailInfo:B,localeCode:n,localeTag:w,serverTimeZone:T,t,onClose:()=>u(null),onFilterConversation:e=>{g(e),u(null)}})]})]})}function nh(e){let t=(0,_.useRef)(null);return(0,_.useEffect)(()=>{let n=t.current;n&&(e&&!n.open?n.showModal():!e&&n.open&&n.close())},[e]),t}function rh({detail:e,detailInfo:t,localeCode:n,localeTag:r,serverTimeZone:i,t:a,onClose:o,onFilterConversation:s}){let c=nh(!0),[l,u]=(0,_.useState)(!1),d=Dm(e),f=e.displayMetrics?.cost,p=zm(e),m=async()=>{if(e.requestId)try{await navigator.clipboard.writeText(e.requestId),u(!0),window.setTimeout(()=>u(!1),1200)}catch{}};return(0,J.jsxs)(`dialog`,{ref:c,className:`modal-overlay`,"aria-labelledby":`log-detail-title`,onCancel:e=>{e.preventDefault(),o()},children:[(0,J.jsx)(`button`,{type:`button`,className:`modal-backdrop-dismiss`,"aria-label":a(`common.close`),tabIndex:-1,onClick:o}),(0,J.jsxs)(`div`,{className:`modal-card log-detail-card`,onClick:e=>e.stopPropagation(),role:`document`,children:[(0,J.jsxs)(`div`,{className:`modal-head`,children:[(0,J.jsxs)(`h3`,{id:`log-detail-title`,children:[(0,J.jsx)(`span`,{className:`mono`,style:{color:Zm(e.status)},children:e.status}),t&&(0,J.jsx)(`span`,{className:`logs-detail-info`,children:t.label})]}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:o,"aria-label":a(`common.cancel`),children:(0,J.jsx)(de,{})})]}),t&&(0,J.jsx)(`p`,{className:`modal-desc`,children:t.description}),(0,J.jsxs)(`section`,{className:`log-detail-section`,"aria-labelledby":`log-detail-basic`,children:[(0,J.jsx)(`h4`,{id:`log-detail-basic`,className:`log-detail-section-title`,children:a(`logs.detail.section.basic`)}),(0,J.jsxs)(`div`,{className:`log-detail-grid`,children:[(0,J.jsx)(`span`,{className:`muted`,children:a(`logs.col.time`)}),(0,J.jsx)(`span`,{className:`mono`,children:$m(e.timestamp,r,i)}),(0,J.jsx)(`span`,{className:`muted`,children:a(`logs.col.request`)}),(0,J.jsxs)(`span`,{className:`log-detail-request-row`,children:[(0,J.jsx)(`span`,{className:`mono log-detail-break`,children:e.requestId??`—`}),e.requestId&&(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:()=>void m(),children:a(l?`logs.detail.copied`:`logs.detail.copyRequestId`)})]}),e.conversationId&&(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)(`span`,{className:`muted`,children:a(`logs.detail.conversation`)}),(0,J.jsxs)(`span`,{className:`log-detail-request-row`,children:[(0,J.jsx)(`span`,{className:`mono log-detail-break`,children:e.conversationId}),s&&(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:()=>s(e.conversationId),children:a(`logs.filter.conversation.apply`)})]})]}),(0,J.jsx)(`span`,{className:`muted`,children:a(`logs.col.model`)}),(0,J.jsx)(`span`,{className:`mono`,children:fl(e.resolvedModel??e.model)}),(0,J.jsx)(`span`,{className:`muted`,children:a(`logs.col.provider`)}),(0,J.jsx)(`span`,{children:jn(e.provider,a)}),(e.requestedEffort||e.effectiveEffort)&&(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)(`span`,{className:`muted`,children:a(`logs.col.effort`)}),(0,J.jsxs)(`span`,{className:`mono`,children:[Rm(e),p?` (${p})`:``]})]}),e.errorCode&&(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)(`span`,{className:`muted`,children:a(`logs.col.error`)}),(0,J.jsx)(`span`,{className:`mono`,children:e.errorCode})]}),e.upstreamError&&(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)(`span`,{className:`muted`,children:a(`logs.col.upstreamReason`)}),(0,J.jsx)(`span`,{className:`mono log-detail-break`,children:e.upstreamError})]})]})]}),(0,J.jsxs)(`section`,{className:`log-detail-section`,"aria-labelledby":`log-detail-route`,children:[(0,J.jsx)(`h4`,{id:`log-detail-route`,className:`log-detail-section-title`,children:a(`logs.detail.route.section`)}),e.routeDecision?(0,J.jsxs)(`div`,{className:`log-detail-grid`,children:[(0,J.jsx)(`span`,{className:`muted`,children:a(`logs.detail.route.kind`)}),(0,J.jsx)(`span`,{className:`mono`,children:e.routeDecision.routeKind??`–`}),e.routeDecision.profile?.id&&(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)(`span`,{className:`muted`,children:a(`logs.detail.route.profile`)}),(0,J.jsxs)(`span`,{className:`mono`,children:[e.routeDecision.profile.id,` (`,e.routeDecision.profile.revision,`)`]})]}),e.routeDecision.selected?.provider&&(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)(`span`,{className:`muted`,children:a(`logs.detail.route.selected`)}),(0,J.jsxs)(`span`,{className:`mono`,children:[e.routeDecision.selected.provider,`/`,e.routeDecision.selected.model,e.routeDecision.selected.reason?` — ${e.routeDecision.selected.reason}`:``]})]}),(0,J.jsx)(`span`,{className:`muted`,children:a(`logs.detail.route.candidates`)}),(0,J.jsx)(`span`,{className:`mono`,children:(e.routeDecision.candidates??[]).map(e=>`${typeof e.provider==`string`&&e.provider.length>0?e.provider:`–`}/${typeof e.model==`string`&&e.model.length>0?e.model:`–`}${e.eligible===!0?` ✓`:e.eligible===!1?` ✗`:` ?`}`).join(` `)||`–`})]}):(0,J.jsx)(`p`,{className:`log-detail-notes-line muted`,children:a(`logs.detail.route.unknown`)})]}),(0,J.jsxs)(`section`,{className:`log-detail-section`,"aria-labelledby":`log-detail-performance`,children:[(0,J.jsx)(`h4`,{id:`log-detail-performance`,className:`log-detail-section-title`,children:a(`logs.detail.section.performance`)}),(0,J.jsxs)(`div`,{className:`log-detail-grid`,children:[(0,J.jsx)(`span`,{className:`muted`,children:a(`logs.col.duration`)}),(0,J.jsxs)(`span`,{className:`mono`,children:[e.durationMs,`ms`]}),(0,J.jsx)(`span`,{className:`muted`,children:a(`logs.col.tokPerSec`)}),(0,J.jsx)(`span`,{className:`mono`,children:Bm(e.displayMetrics?.tokPerSecond,r)}),e.firstOutputMs!==void 0&&(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)(`span`,{className:`muted`,children:a(`logs.detail.ttft`)}),(0,J.jsxs)(`span`,{className:`mono`,children:[e.firstOutputMs,`ms`]})]})]}),e.displayMetrics?.tokPerSecond.kind===`unavailable`&&(0,J.jsx)(`p`,{className:`log-detail-notes-line muted`,children:a(qm(e.displayMetrics.tokPerSecond.reason))})]}),(0,J.jsxs)(`section`,{className:`log-detail-section`,"aria-labelledby":`log-detail-cost`,children:[(0,J.jsx)(`h4`,{id:`log-detail-cost`,className:`log-detail-section-title`,children:a(`logs.detail.section.cost`)}),(0,J.jsx)(`p`,{className:`log-detail-notes-line muted`,children:a(`usage.cost.disclaimer`)}),f?.kind===`value`?(0,J.jsxs)(J.Fragment,{children:[(0,J.jsxs)(`div`,{className:`log-detail-grid`,children:[(0,J.jsx)(`span`,{className:`muted`,children:a(`logs.detail.costTotal`)}),(0,J.jsx)(`span`,{className:`mono`,children:Cm(f.estimate.cost.total,a,r,f.estimate.priorityLowerBound)}),(0,J.jsx)(`span`,{className:`muted`,children:a(`logs.tokens.input`)}),(0,J.jsx)(`span`,{className:`mono`,children:Cm(f.estimate.cost.input,a,r,f.estimate.priorityLowerBound)}),(0,J.jsx)(`span`,{className:`muted`,children:a(`logs.tokens.cacheRead`)}),(0,J.jsx)(`span`,{className:`mono`,children:Cm(f.estimate.cost.cacheRead,a,r,f.estimate.priorityLowerBound)}),(0,J.jsx)(`span`,{className:`muted`,children:a(`logs.tokens.cacheWrite`)}),(0,J.jsx)(`span`,{className:`mono`,children:Cm(f.estimate.cost.cacheWrite,a,r,f.estimate.priorityLowerBound)}),(0,J.jsx)(`span`,{className:`muted`,children:a(`logs.tokens.output`)}),(0,J.jsx)(`span`,{className:`mono`,children:Cm(f.estimate.cost.output,a,r,f.estimate.priorityLowerBound)}),f.estimate.price&&(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)(`span`,{className:`muted`,children:a(`logs.detail.matchedKey`)}),(0,J.jsxs)(`span`,{className:`mono log-detail-break`,children:[f.estimate.price.jawcodeProvider??f.estimate.price.provider,`/`,f.estimate.price.modelId]}),(0,J.jsx)(`span`,{className:`muted`,children:a(`logs.detail.priceSource`)}),(0,J.jsxs)(`span`,{children:[a(`logs.detail.source.${f.estimate.price.source}`),` · `,a(Xm(f.estimate.price.status))]})]})]}),f.estimateReasons.length>0&&(0,J.jsx)(`ul`,{className:`log-detail-notes`,children:f.estimateReasons.map(e=>(0,J.jsx)(`li`,{children:a(Jm(e))},e))})]}):(0,J.jsxs)(`div`,{className:`log-detail-grid`,children:[(0,J.jsx)(`span`,{className:`muted`,children:a(`logs.detail.costTotal`)}),(0,J.jsx)(`span`,{className:`mono`,children:a(`logs.cost.unavailable`)}),(0,J.jsx)(`span`,{className:`muted`,children:a(`logs.detail.unavailableReason`)}),(0,J.jsx)(`span`,{children:f?.kind===`unavailable`?a(qm(f.reason)):a(`logs.detail.reason.usage_missing`)})]})]}),e.attempts?.length?(0,J.jsxs)(`section`,{className:`log-detail-section`,"aria-labelledby":`log-detail-attempts`,children:[(0,J.jsx)(`h4`,{id:`log-detail-attempts`,className:`log-detail-section-title`,children:a(`logs.detail.section.attempts`)}),(0,J.jsx)(`p`,{className:`log-detail-notes-line muted`,children:a(`logs.detail.attempt.e2eNote`)}),(0,J.jsx)(`div`,{className:`log-detail-attempts-wrap`,children:(0,J.jsxs)(`table`,{className:`tbl log-detail-attempts`,children:[(0,J.jsx)(`thead`,{children:(0,J.jsxs)(`tr`,{children:[(0,J.jsx)(`th`,{className:`num`,children:`#`}),(0,J.jsx)(`th`,{children:a(`logs.detail.attempt.target`)}),(0,J.jsx)(`th`,{className:`num`,children:a(`logs.col.duration`)}),(0,J.jsx)(`th`,{className:`num`,children:a(`logs.col.tokPerSec`)}),(0,J.jsx)(`th`,{className:`num`,children:a(`logs.col.estimatedCost`)}),(0,J.jsx)(`th`,{children:a(`logs.detail.attempt.reason`)})]})}),(0,J.jsx)(`tbody`,{children:e.attempts.toSorted((e,t)=>e.ordinal-t.ordinal).map(e=>{let t=e.displayMetrics?.cost,n=zm(e),i=t?.kind===`value`?t.estimate.price:void 0,o=e.errorCode??(e.recoveryKinds.length?e.recoveryKinds.map(e=>a(Ym(e))).join(`, `):void 0)??(t?.kind===`unavailable`?a(qm(t.reason)):a(`logs.detail.attempt.completed`));return(0,J.jsxs)(`tr`,{children:[(0,J.jsx)(`td`,{className:`num mono`,children:e.ordinal}),(0,J.jsxs)(`td`,{children:[(0,J.jsx)(`span`,{children:jn(e.provider,a)}),(0,J.jsx)(`br`,{}),(0,J.jsx)(`span`,{className:`mono muted log-detail-break`,children:e.model}),(e.requestedEffort||e.effectiveEffort)&&(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)(`br`,{}),(0,J.jsxs)(`span`,{className:`mono muted text-caption log-detail-break`,children:[Rm(e),n?` (${n})`:``]})]}),i&&(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)(`br`,{}),(0,J.jsxs)(`span`,{className:`muted text-caption log-detail-break`,children:[i.jawcodeProvider??i.provider,`/`,i.modelId,` · `,a(`logs.detail.source.${i.source}`),` · `,a(Xm(i.status))]})]})]}),(0,J.jsxs)(`td`,{className:`num mono`,children:[e.durationMs,`ms`]}),(0,J.jsx)(`td`,{className:`num mono`,children:Bm(e.displayMetrics?.tokPerSecond,r)}),(0,J.jsx)(`td`,{className:`num mono`,children:wm(t,a,r)}),(0,J.jsx)(`td`,{className:`log-detail-break`,children:o})]},`${e.ordinal}-${e.provider}-${e.model}`)})})]})})]}):null,(0,J.jsxs)(`section`,{className:`log-detail-section`,"aria-labelledby":`log-detail-usage`,children:[(0,J.jsx)(`h4`,{id:`log-detail-usage`,className:`log-detail-section-title`,children:a(`logs.detail.section.usage`)}),(0,J.jsxs)(`div`,{className:`log-detail-grid`,children:[(0,J.jsx)(`span`,{className:`muted`,children:a(`logs.tokens.input`)}),(0,J.jsx)(`span`,{className:`mono`,children:e.usage?Rn(e.usage.inputTokens,n):`—`}),(0,J.jsx)(`span`,{className:`muted`,children:a(`logs.tokens.output`)}),(0,J.jsx)(`span`,{className:`mono`,children:e.usage?Rn(e.usage.outputTokens,n):`—`}),(0,J.jsx)(`span`,{className:`muted`,children:a(`logs.tokens.cacheRead`)}),(0,J.jsx)(`span`,{className:`mono`,children:d.read===void 0?`—`:Rn(d.read,n)}),(0,J.jsx)(`span`,{className:`muted`,children:a(`logs.tokens.cacheWrite`)}),(0,J.jsx)(`span`,{className:`mono`,children:d.write===void 0?`—`:Rn(d.write,n)}),(0,J.jsx)(`span`,{className:`muted`,children:a(`logs.tokens.reasoning`)}),(0,J.jsx)(`span`,{className:`mono`,children:e.usage?.reasoningOutputTokens===void 0?`—`:Rn(e.usage.reasoningOutputTokens,n)}),(0,J.jsx)(`span`,{className:`muted`,children:a(`logs.detail.totalTokens`)}),(0,J.jsx)(`span`,{className:`mono`,children:Lm(e)===void 0?`—`:Rn(Lm(e),n)}),e.usage?.contextTotalTokens!==void 0&&(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)(`span`,{className:`muted`,children:a(`logs.tokens.contextTotal`)}),(0,J.jsx)(`span`,{className:`mono`,children:Rn(e.usage.contextTotalTokens,n)})]})]}),e.usageStatus===`estimated`&&(0,J.jsx)(`p`,{className:`log-detail-notes-line muted`,children:a(`logs.tokens.estimatedNote`)})]}),(0,J.jsxs)(`details`,{className:`log-detail-raw`,children:[(0,J.jsx)(`summary`,{children:a(`logs.detailRaw`)}),(0,J.jsx)(`pre`,{className:`log-detail-json`,children:JSON.stringify(e,null,2)})]})]})]})}function ih(e){return`${Math.round(e*100)}%`}function ah(e,t){let n=`${t}/${e}`,r=0;for(let e=0;e>>0;return`hsl(${r%360} 55% 55%)`}function oh(e){let t=new Map(e.map(e=>[e.date,e])),n=[],r=new Date;r.setHours(0,0,0,0),r.setDate(r.getDate()-6);for(let e=0;e<7;e++){let e=`${r.getFullYear()}-${String(r.getMonth()+1).padStart(2,`0`)}-${String(r.getDate()).padStart(2,`0`)}`,i=t.get(e);n.push({date:e,requests:i?.requests??0,measuredRequests:i?.measuredRequests??0,reportedRequests:i?.reportedRequests??0,totalTokens:i?.totalTokens??0,models:i?.models??[]}),r.setDate(r.getDate()+1)}return n}function sh(e){let t=e.filter(e=>e>0).sort((e,t)=>e-t);if(t.length===0)return[0,0,0,0];let n=e=>t[Math.min(t.length-1,Math.floor(e*t.length))];return[n(.25),n(.5),n(.75),n(.95)]}function ch(e,t){return e<=0?0:e<=t[0]?1:e<=t[1]?2:e<=t[2]?3:4}function lh(e){let t=sh(e.map(e=>e.totalTokens)),n=new Map(e.map(e=>[e.date,e])),r=new Date;r.setHours(0,0,0,0);let i=new Date(r);i.setDate(i.getDate()-364),i.setDate(i.getDate()-i.getDay());let a=[],o=[],s=[`Jan`,`Feb`,`Mar`,`Apr`,`May`,`Jun`,`Jul`,`Aug`,`Sep`,`Oct`,`Nov`,`Dec`],c=-4,l=-1,u=[],d=new Date(i);for(;d<=r;){let e=`${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,`0`)}-${String(d.getDate()).padStart(2,`0`)}`,r=d.getMonth();d.getDay()===0&&r!==l&&a.length-c>=4&&(o.push({label:s[r],col:a.length}),c=a.length,l=r);let i=n.get(e);u.push({date:e,requests:i?.requests??0,totalTokens:i?.totalTokens??0,level:i?ch(i.totalTokens,t):0,dayOfWeek:d.getDay()}),d.getDay()===6&&(a.push(u),u=[]),d.setDate(d.getDate()+1)}if(u.length>0){for(;u.length<7;)u.push({date:``,requests:0,totalTokens:0,level:0,dayOfWeek:u.length});a.push(u)}return{weeks:a,months:o,buckets:t}}function uh({surface:e,range:t,onSurface:n,onRange:r,t:i}){return(0,J.jsxs)(`div`,{className:`usage-filters`,children:[(0,J.jsx)(`div`,{className:`usage-segmented`,role:`group`,"aria-label":i(`logs.filter.surface.label`),children:[`all`,`codex`,`claude`,`grok`].map(t=>{let r=i(`logs.filter.surface.${t}`);return(0,J.jsxs)(`button`,{type:`button`,className:`usage-segmented-btn usage-source-btn${e===t?` active`:``}`,"aria-label":r,"aria-pressed":e===t,onClick:()=>n(t),children:[t===`codex`&&(0,J.jsx)(`img`,{className:`usage-source-mark`,src:`/provider-icons/openai.svg`,alt:``,"aria-hidden":`true`}),t===`claude`&&(0,J.jsx)(`img`,{className:`usage-source-mark`,src:`/provider-icons/claude-color.svg`,alt:``,"aria-hidden":`true`}),t===`grok`&&(0,J.jsx)(`img`,{className:`usage-source-mark usage-source-mark--mono`,src:`/provider-icons/grok.svg`,alt:``,"aria-hidden":`true`}),(0,J.jsx)(`span`,{className:t===`all`?`usage-source-label`:`usage-source-label usage-source-label-collapsible`,children:r})]},t)})}),(0,J.jsx)(`div`,{className:`usage-segmented`,role:`group`,"aria-label":i(`usage.title`),children:[`all`,`30d`,`7d`].map(e=>{let n=i(e===`all`?`usage.range.available`:`usage.range.${e}`);return(0,J.jsx)(`button`,{type:`button`,className:`usage-segmented-btn${t===e?` active`:``}`,"aria-label":n,"aria-pressed":t===e,onClick:()=>r(e),children:n},e)})})]})}function dh({summary:e,activeDays:t,locale:n,t:r}){return(0,J.jsxs)(J.Fragment,{children:[(0,J.jsxs)(`div`,{className:`usage-cards usage-cards-3x2`,role:`group`,"aria-label":r(`usage.title`),children:[(0,J.jsxs)(`div`,{className:`stat`,children:[(0,J.jsx)(`div`,{className:`muted`,children:r(`usage.card.requests`)}),(0,J.jsx)(`div`,{className:`stat-value`,children:e.requests})]}),(0,J.jsxs)(`div`,{className:`stat`,children:[(0,J.jsx)(`div`,{className:`muted`,children:r(`usage.card.measured`)}),(0,J.jsx)(`div`,{className:`stat-value`,children:e.measuredRequests})]}),(0,J.jsxs)(`div`,{className:`stat`,children:[(0,J.jsx)(`div`,{className:`muted`,children:r(`usage.card.totalTokens`)}),(0,J.jsx)(`div`,{className:`stat-value`,children:Rn(e.totalTokens,n)})]}),(0,J.jsxs)(`div`,{className:`stat`,title:r(`usage.card.cachedTokensHint`),children:[(0,J.jsx)(`div`,{className:`muted`,children:r(`usage.card.cachedTokens`)}),(0,J.jsx)(`div`,{className:`stat-value`,children:Rn(e.cacheReadInputTokens??e.cachedInputTokens,n)}),(e.cacheCreationInputTokens??0)>0&&(0,J.jsxs)(`div`,{className:`muted text-caption`,children:[r(`usage.card.cacheWriteTokens`),`: `,Rn(e.cacheCreationInputTokens??0,n)]})]}),(0,J.jsxs)(`div`,{className:`stat`,children:[(0,J.jsx)(`div`,{className:`muted`,children:r(`usage.card.coverage`)}),(0,J.jsx)(`div`,{className:`stat-value`,children:ih(e.coverageRatio)})]}),(0,J.jsxs)(`div`,{className:`stat`,children:[(0,J.jsx)(`div`,{className:`muted`,children:r(`usage.card.activeDays`)}),(0,J.jsx)(`div`,{className:`stat-value`,children:t})]})]}),e.estimatedCostUsd!==void 0&&(0,J.jsxs)(`div`,{className:`usage-cost-row`,role:`note`,children:[(0,J.jsx)(`span`,{className:`muted`,children:r(`usage.cost.total`)}),(0,J.jsx)(`span`,{className:`stat-value mono usage-cost-value`,children:os(e.estimatedCostUsd,n)}),(0,J.jsx)(`span`,{className:`muted text-caption`,children:r(`usage.cost.disclaimer`)}),(e.unpricedRequests??0)+(e.unmeteredRequests??0)>0&&(0,J.jsx)(`span`,{className:`muted text-caption`,children:r(`usage.cost.unpricedNote`).replace(`{count}`,String((e.unpricedRequests??0)+(e.unmeteredRequests??0)))})]})]})}function fh({weekBars:e,locale:t,t:n}){let[r,i]=(0,_.useState)(null),a=Math.max(1,...e.map(e=>e.totalTokens));return(0,J.jsx)(`div`,{className:`daybars`,role:`img`,"aria-label":n(`usage.section.heatmap`),children:e.map(e=>{let n=Math.round(e.totalTokens/a*100),o=e.date.slice(5);return(0,J.jsxs)(`div`,{className:`daybar`,onMouseEnter:()=>i(e.date),onMouseLeave:()=>i(t=>t===e.date?null:t),children:[(0,J.jsx)(`div`,{className:`daybar-track`,children:(0,J.jsxs)(`div`,{className:`daybar-stack`,style:{"--daybar-scale":String(Math.max(0,Math.min(1,n/100)))},children:[e.models.map(e=>(0,J.jsx)(`div`,{className:`daybar-seg`,style:{flexGrow:e.totalTokens,background:ah(e.model,e.provider)}},`${e.provider}/${e.model}`)),e.models.length===0&&e.totalTokens>0&&(0,J.jsx)(`div`,{className:`daybar-seg`,style:{flexGrow:1,background:`var(--green)`}})]})}),r===e.date&&e.totalTokens>0&&(0,J.jsxs)(`div`,{className:`daybar-tip`,role:`tooltip`,children:[(0,J.jsx)(`div`,{className:`daybar-tip-date`,children:e.date}),e.models.slice(0,8).map(e=>(0,J.jsxs)(`div`,{className:`daybar-tip-row`,children:[(0,J.jsx)(`span`,{className:`daybar-tip-swatch`,style:{background:ah(e.model,e.provider)}}),(0,J.jsx)(`span`,{className:`daybar-tip-name`,children:fl(e.model)}),(0,J.jsx)(`span`,{className:`daybar-tip-val`,children:Rn(e.totalTokens,t)})]},`${e.provider}/${e.model}`))]}),(0,J.jsx)(`span`,{className:`daybar-count`,children:Rn(e.totalTokens,t)}),(0,J.jsx)(`span`,{className:`daybar-label muted`,children:o})]},e.date)})})}function ph({range:e,heatmap:t,weekBars:n,locale:r,t:i}){let a=(0,_.useRef)(null),[o,s]=(0,_.useState)(null);return(0,_.useEffect)(()=>{let e=a.current;if(!e)return;let t=()=>{e.scrollLeft=e.scrollWidth};t();let n=new ResizeObserver(t);return n.observe(e),()=>n.disconnect()},[t,e]),(0,J.jsxs)(`section`,{className:`panel`,style:{marginTop:16},"aria-labelledby":`usage-heatmap-title`,children:[(0,J.jsx)(`h3`,{id:`usage-heatmap-title`,className:`panel-title`,children:i(`usage.section.heatmap`)}),e===`7d`?(0,J.jsx)(fh,{weekBars:n,locale:r,t:i}):(0,J.jsxs)(`div`,{className:`heatmap`,ref:a,role:`img`,"aria-labelledby":`usage-heatmap-title`,children:[(0,J.jsxs)(`div`,{className:`heatmap-months`,style:{gridTemplateColumns:`28px repeat(${t.weeks.length}, calc(var(--hm-cell) + var(--hm-gap)))`},children:[(0,J.jsx)(`span`,{className:`heatmap-day-spacer`}),t.months.map(e=>(0,J.jsx)(`span`,{className:`heatmap-month`,style:{gridColumn:e.col+2},children:e.label},`${e.label}-${e.col}`))]}),(0,J.jsxs)(`div`,{className:`heatmap-body`,children:[(0,J.jsxs)(`div`,{className:`heatmap-days`,children:[(0,J.jsx)(`span`,{}),(0,J.jsx)(`span`,{children:i(`usage.dayMon`)}),(0,J.jsx)(`span`,{}),(0,J.jsx)(`span`,{children:i(`usage.dayWed`)}),(0,J.jsx)(`span`,{}),(0,J.jsx)(`span`,{children:i(`usage.dayFri`)}),(0,J.jsx)(`span`,{})]}),(0,J.jsx)(`div`,{className:`heatmap-grid`,style:{gridTemplateColumns:`repeat(${t.weeks.length}, var(--hm-cell))`},children:t.weeks.map((e,t)=>(0,J.jsx)(`div`,{className:`heatmap-week`,children:e.map((e,n)=>(0,J.jsx)(`div`,{className:`heatmap-cell heatmap-cell-${e.level}`,onMouseEnter:r=>{if(!e.date)return;let i=r.currentTarget.getBoundingClientRect();s({weekIndex:t,dayIndex:n,x:i.left+i.width/2,y:i.top})},onMouseLeave:()=>s(e=>e?.weekIndex===t&&e.dayIndex===n?null:e)},e.date||`pad-${t}-${n}`))},e[0]?.date||`week-${t}`))})]}),o&&(()=>{let e=t.weeks[o.weekIndex]?.[o.dayIndex];return e?.date?(0,J.jsxs)(`div`,{className:`heatmap-tip`,role:`tooltip`,style:{left:o.x,top:o.y},children:[(0,J.jsx)(`div`,{className:`heatmap-tip-date`,children:e.date}),(0,J.jsx)(`div`,{className:`heatmap-tip-val`,children:i(`usage.heatmap.tooltipTokens`,{tokens:Rn(e.totalTokens,r)})}),(0,J.jsx)(`div`,{className:`heatmap-tip-req muted`,children:i(`usage.heatmap.tooltipRequests`,{requests:e.requests})})]}):null})(),(0,J.jsxs)(`div`,{className:`heatmap-legend muted`,children:[(0,J.jsx)(`span`,{children:i(`usage.heatmap.less`)}),[0,1,2,3,4].map(e=>(0,J.jsx)(`span`,{className:`heatmap-cell heatmap-cell-${e}`},e)),(0,J.jsx)(`span`,{children:i(`usage.heatmap.more`)})]})]})]})}function mh({title:e,titleId:t,children:n}){return(0,J.jsxs)(`section`,{className:`usw-section`,"aria-labelledby":t,children:[(0,J.jsx)(`h3`,{id:t,className:`h-section`,children:e}),n]})}function hh({models:e,modelQuery:t,onModelQuery:n,locale:r,t:i,workspace:a=!1}){let o=i(`usage.search.models`),s=i(`usage.section.models`),c=`usage-models-title`,l=(0,J.jsx)(`input`,{className:`input`,"aria-label":o,placeholder:o,value:t,onChange:e=>n(e.target.value)}),u=(0,J.jsx)(`div`,{className:`tbl-wrap`,children:(0,J.jsxs)(`table`,{className:`tbl`,children:[(0,J.jsx)(`thead`,{children:(0,J.jsxs)(`tr`,{children:[(0,J.jsx)(`th`,{children:i(`logs.col.model`)}),(0,J.jsx)(`th`,{children:i(`logs.col.provider`)}),(0,J.jsx)(`th`,{className:`num`,children:i(`usage.col.requests`)}),(0,J.jsx)(`th`,{className:`num`,children:i(`usage.col.measured`)}),(0,J.jsx)(`th`,{className:`num`,children:i(`usage.col.tokens`)}),(0,J.jsx)(`th`,{children:i(`usage.col.share`)})]})}),(0,J.jsx)(`tbody`,{children:e.map(e=>(0,J.jsxs)(`tr`,{children:[(0,J.jsx)(`td`,{className:`mono`,children:fl(e.model)}),(0,J.jsx)(`td`,{className:`muted`,children:jn(e.provider,i)}),(0,J.jsx)(`td`,{className:`num`,children:e.requests}),(0,J.jsx)(`td`,{className:`num`,children:e.measuredRequests}),(0,J.jsx)(`td`,{className:`num mono`,children:Rn(e.totalTokens,r)}),(0,J.jsx)(`td`,{children:(0,J.jsx)(`div`,{className:`usage-bar`,children:(0,J.jsx)(`div`,{className:`usage-bar-fill`,style:{width:`${Math.round(e.shareRatio*100)}%`}})})})]},`${e.provider}/${e.model}`))})]})});return a?(0,J.jsxs)(mh,{title:s,titleId:c,children:[(0,J.jsx)(`div`,{className:`usw-section-toolbar`,children:l}),u]}):(0,J.jsxs)(`section`,{className:`panel`,style:{marginTop:16},"aria-labelledby":c,children:[(0,J.jsxs)(`div`,{className:`panel-head`,children:[(0,J.jsx)(`h3`,{id:c,className:`panel-title`,children:s}),l]}),u]})}function gh({providers:e,locale:t,t:n,workspace:r=!1}){let i=n(`usage.section.providers`),a=`usage-providers-title`,o=(0,J.jsx)(`div`,{className:`tbl-wrap`,children:(0,J.jsxs)(`table`,{className:`tbl`,children:[(0,J.jsx)(`thead`,{children:(0,J.jsxs)(`tr`,{children:[(0,J.jsx)(`th`,{children:n(`logs.col.provider`)}),(0,J.jsx)(`th`,{className:`num`,children:n(`usage.col.requests`)}),(0,J.jsx)(`th`,{className:`num`,children:n(`usage.col.measured`)}),(0,J.jsx)(`th`,{className:`num`,children:n(`usage.col.tokens`)}),(0,J.jsx)(`th`,{children:n(`usage.col.share`)})]})}),(0,J.jsx)(`tbody`,{children:e.map(e=>(0,J.jsxs)(`tr`,{children:[(0,J.jsx)(`td`,{className:`mono`,children:jn(e.provider,n)}),(0,J.jsx)(`td`,{className:`num`,children:e.requests}),(0,J.jsx)(`td`,{className:`num`,children:e.measuredRequests}),(0,J.jsx)(`td`,{className:`num mono`,children:Rn(e.totalTokens,t)}),(0,J.jsx)(`td`,{children:(0,J.jsx)(`div`,{className:`usage-bar`,children:(0,J.jsx)(`div`,{className:`usage-bar-fill`,style:{width:`${Math.round(e.shareRatio*100)}%`}})})})]},e.provider))})]})});return r?(0,J.jsx)(mh,{title:i,titleId:a,children:o}):(0,J.jsxs)(`section`,{className:`panel`,style:{marginTop:16},"aria-labelledby":a,children:[(0,J.jsx)(`h3`,{id:a,className:`panel-title`,children:i}),o]})}function _h({summary:e,t,workspace:n=!1}){let r=t(`usage.section.coverage`),i=`usage-coverage-title`,a=(0,J.jsxs)(J.Fragment,{children:[(0,J.jsxs)(`div`,{className:`usage-cards usage-cards-3x2`,children:[(0,J.jsxs)(`div`,{className:`stat`,children:[(0,J.jsx)(`div`,{className:`muted`,children:t(`usage.coverage.measured`)}),(0,J.jsx)(`div`,{className:`stat-value`,children:e.measuredRequests})]}),(0,J.jsxs)(`div`,{className:`stat`,children:[(0,J.jsx)(`div`,{className:`muted`,children:t(`usage.coverage.reported`)}),(0,J.jsx)(`div`,{className:`stat-value`,children:e.reportedRequests})]}),(0,J.jsxs)(`div`,{className:`stat`,children:[(0,J.jsx)(`div`,{className:`muted`,children:t(`usage.coverage.estimated`)}),(0,J.jsx)(`div`,{className:`stat-value`,children:e.estimatedRequests})]}),(0,J.jsxs)(`div`,{className:`stat`,children:[(0,J.jsx)(`div`,{className:`muted`,children:t(`logs.tokens.unreported`)}),(0,J.jsx)(`div`,{className:`stat-value`,children:e.unreportedRequests})]}),(0,J.jsxs)(`div`,{className:`stat`,children:[(0,J.jsx)(`div`,{className:`muted`,children:t(`logs.tokens.unsupported`)}),(0,J.jsx)(`div`,{className:`stat-value`,children:e.unsupportedRequests})]})]}),(0,J.jsx)(`p`,{className:`muted text-control`,style:{marginTop:12},children:t(`usage.coverage.note`)})]});return n?(0,J.jsx)(mh,{title:r,titleId:i,children:a}):(0,J.jsxs)(`section`,{className:`panel`,style:{marginTop:16},"aria-labelledby":i,children:[(0,J.jsx)(`h3`,{id:i,className:`panel-title`,children:r}),a]})}function vh({data:e,heatmap:t,weekBars:n,activeDays:r,filteredModels:i,modelQuery:a,onModelQuery:o,sortedProviders:s,range:c,locale:l,t:u}){let d=!!e&&e.summary.requests===0,f=[{id:`overview`,label:u(`usage.section.overview`),meta:e?`${e.summary.requests}`:`—`,body:e?(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)(dh,{summary:e.summary,activeDays:r,locale:l,t:u}),(0,J.jsx)(ph,{range:c,heatmap:t,weekBars:n,locale:l,t:u})]}):null},{id:`models`,label:u(`usage.section.models`),meta:e?`${e.models.length}`:`—`,body:e?(0,J.jsx)(hh,{models:i,modelQuery:a,onModelQuery:o,locale:l,t:u,workspace:!0}):null},{id:`providers`,label:u(`usage.section.providers`),meta:e?`${e.providers.length}`:`—`,body:e?(0,J.jsx)(gh,{providers:s,locale:l,t:u,workspace:!0}):null},{id:`coverage`,label:u(`usage.section.coverage`),meta:e?ih(e.summary.coverageRatio):`—`,body:e?(0,J.jsx)(_h,{summary:e.summary,t:u,workspace:!0}):null}];return(0,J.jsx)(`div`,{className:`usage-workspace-shell`,children:(0,J.jsxs)(`div`,{className:`usage-workspace-root`,children:[(0,J.jsx)(yp,{scope:`usage`,ariaLabel:u(`usage.workspace.sections`),items:f.map(e=>({id:e.id,label:e.label,meta:e.meta}))}),(0,J.jsx)(`section`,{className:`usage-workspace-main`,"aria-label":u(`usage.workspace.report`),children:d?(0,J.jsx)(Ot,{title:u(`usage.empty`)}):f.map(e=>(0,J.jsx)(`div`,{id:gp(`usage`,e.id),className:`usw-body usw-section-block`,children:e.body},e.id))})]})})}var yh=new Map;function bh(e,t,n,r,i,a){return`ocx.usage.v2:${e}:${r?`connected`:`standalone`}:${i}:${a??``}:${t}:${n}`}function xh(e,t,n,r,i,a){let o=bh(e,t,n,r,i,a);return yh.get(o)??gr(o)}function Sh(e,t,n,r,i,a,o){let s=bh(e,t,n,r,i,a);yh.set(s,o),br(s,o)}function Ch({apiBase:e,connected:t=!1,apiKeyId:n}){let{t:r,locale:i}=ct(),[a,o]=(0,_.useState)(`30d`),[s,c]=(0,_.useState)(`all`),[l,u]=(0,_.useState)(`machine`),[d,f]=(0,_.useState)(``),p=(0,_.useCallback)(async r=>{let i=new URLSearchParams({range:a,surface:s});t&&l===`machine`&&n&&i.set(`apiKeyId`,n);let o=await fetch(`${e}/api/usage?${i}`,{signal:r});if(!o.ok)throw Error(`${o.status} ${o.statusText}`.trim());let c=await o.json();return Sh(e,a,s,t,l,n,c),c},[e,n,t,a,l,s]),m=bh(e,a,s,t,l,n),h=xh(e,a,s,t,l,n),g=ml(m,[e,n,t,a,l,s],p,{isEmpty:()=>!1,initialData:h??void 0}),{state:v}=g,y=v.data??h??null,b=(0,_.useMemo)(()=>lh(y?.days??[]),[y?.days]),x=(0,_.useMemo)(()=>oh(y?.days??[]),[y?.days]),S=(0,_.useMemo)(()=>(y?.days??[]).filter(e=>e.requests>0).length,[y?.days]),C=(0,_.useMemo)(()=>{let e=d.trim().toLowerCase(),t=(y?.models??[]).toSorted((e,t)=>t.totalTokens-e.totalTokens);return e?t.filter(t=>t.model.toLowerCase().includes(e)||t.provider.toLowerCase().includes(e)||(t.resolvedModel??``).toLowerCase().includes(e)).slice(0,100):t.slice(0,100)},[y?.models,d]),w=(0,_.useMemo)(()=>(y?.providers??[]).toSorted((e,t)=>t.totalTokens-e.totalTokens),[y?.providers]);return(0,J.jsxs)(J.Fragment,{children:[(0,J.jsxs)(`div`,{className:`page-head usage-head`,children:[(0,J.jsx)(`h2`,{id:`usage-page-title`,children:r(`usage.title`)}),(0,J.jsx)(uh,{surface:s,range:a,onSurface:c,onRange:o,t:r})]}),(0,J.jsx)(`p`,{className:`page-sub`,children:r(`usage.subtitle`)}),t&&(0,J.jsxs)(`div`,{className:`usage-source-row`,children:[(0,J.jsx)(`span`,{children:r(`usage.source.connected`)}),(0,J.jsxs)(`div`,{className:`usage-scope-control`,role:`group`,"aria-label":r(`usage.scope.label`),children:[(0,J.jsx)(`button`,{type:`button`,className:`btn btn-sm${l===`machine`?` btn-primary`:` btn-ghost`}`,"aria-pressed":l===`machine`,onClick:()=>u(`machine`),children:r(`usage.scope.machine`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-sm${l===`hub`?` btn-primary`:` btn-ghost`}`,"aria-pressed":l===`hub`,onClick:()=>u(`hub`),children:r(`usage.scope.hub`)})]})]}),v.showSkeleton&&!y?(0,J.jsx)(gl,{label:r(`usage.loading`),rows:5}):v.kind===`failed-cold`?(0,J.jsxs)($,{tone:`err`,children:[t?r(`usage.hubOffline`):v.error instanceof Error?`${r(`usage.loadError`)} ${v.error.message}`:r(`usage.loadError`),` `,(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:()=>g.refresh(),children:r(`common.retry`)})]}):(0,J.jsxs)(J.Fragment,{children:[v.showError&&(0,J.jsx)($,{tone:`err`,children:r(t?`usage.hubOffline`:`usage.loadError`)}),y?.historyTruncated&&(0,J.jsx)($,{tone:`warn`,children:(()=>{let e=wh(y.snapshotWindowStart),t=wh(y.snapshotWindowEnd);return e!==null&&t!==null?r(`usage.historyTruncatedWindow`,{start:e,end:t}):r(`usage.historyTruncated`)})()}),(0,J.jsx)(vh,{data:y,heatmap:b,weekBars:x,activeDays:S,filteredModels:C,modelQuery:d,onModelQuery:f,sortedProviders:w,range:a,locale:i,t:r})]})]})}function wh(e){if(typeof e!=`number`||!Number.isFinite(e))return null;let t=new Date(e);return Number.isFinite(t.getTime())?t.toLocaleString():null}function Th(e,t){if(e<1024)return`${e} B`;let n=[`KiB`,`MiB`,`GiB`,`TiB`],r=e,i=-1;do r/=1024,i++;while(r>=1024&&i0,[f,p]=(0,_.useState)(!1);return(0,J.jsxs)(`div`,{className:`stw-section`,"data-testid":`codex-log-guard`,children:[(0,J.jsx)(`h3`,{className:`stw-section-title`,children:n(`storage.bucket.logs_db`)}),(0,J.jsxs)(`dl`,{className:`stw-kv`,children:[(0,J.jsxs)(`div`,{className:`stw-kv-row`,children:[(0,J.jsx)(`dt`,{children:n(`dash.status`)}),(0,J.jsxs)(`dd`,{className:`stw-kv-mono`,children:[(0,J.jsx)(`code`,{children:jh(t,e.schema.state)}),c&&e.schema.state===`unsupported`?(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)(`span`,{"aria-hidden":`true`,children:` · `}),(0,J.jsx)(`code`,{children:Dh(t,`inspectionOnly`)})]}):null]})]}),(0,J.jsxs)(`div`,{className:`stw-kv-row`,children:[(0,J.jsx)(`dt`,{children:n(`storage.bucket.logs_db`)}),(0,J.jsx)(`dd`,{className:`stw-kv-mono`,children:Th(e.files.databaseBytes,t)})]}),(0,J.jsxs)(`div`,{className:`stw-kv-row`,children:[(0,J.jsx)(`dt`,{children:(0,J.jsx)(`code`,{children:`WAL`})}),(0,J.jsx)(`dd`,{className:`stw-kv-mono`,children:Th(e.files.walBytes,t)})]}),(0,J.jsxs)(`div`,{className:`stw-kv-row`,children:[(0,J.jsx)(`dt`,{children:(0,J.jsx)(`code`,{children:`SHM`})}),(0,J.jsx)(`dd`,{className:`stw-kv-mono`,children:Th(e.files.shmBytes,t)})]}),s&&(0,J.jsxs)(J.Fragment,{children:[(0,J.jsxs)(`div`,{className:`stw-kv-row`,children:[(0,J.jsx)(`dt`,{children:n(`storage.col.rows`)}),(0,J.jsx)(`dd`,{className:`stw-kv-mono`,children:s.totalRows.toLocaleString(t)})]}),(0,J.jsxs)(`div`,{className:`stw-kv-row`,children:[(0,J.jsx)(`dt`,{children:(0,J.jsx)(`code`,{children:`TRACE`})}),(0,J.jsxs)(`dd`,{className:`stw-kv-mono`,children:[(s.traceShare*100).toFixed(1),`%`]})]}),(0,J.jsxs)(`div`,{className:`stw-kv-row`,children:[(0,J.jsx)(`dt`,{children:(0,J.jsx)(`code`,{children:`freelist`})}),(0,J.jsx)(`dd`,{className:`stw-kv-mono`,children:Th(s.reclaimableBytes,t)})]})]}),!s&&e.metricsSkipped&&(0,J.jsxs)(`div`,{className:`stw-kv-row`,"data-testid":`log-guard-metrics-skipped`,children:[(0,J.jsx)(`dt`,{children:n(`storage.col.rows`)}),(0,J.jsx)(`dd`,{className:`muted`,children:Dh(t,`metricsSkippedLarge`).replace(`{threshold}`,Th(e.metricsSkipped.thresholdBytes,t))})]}),(0,J.jsxs)(`div`,{className:`stw-kv-row`,children:[(0,J.jsx)(`dt`,{children:(0,J.jsx)(`code`,{children:`sqlite_home`})}),(0,J.jsx)(`dd`,{className:`stw-kv-mono`,children:(0,J.jsx)(`code`,{children:e.externalSqliteHome?Dh(t,`externalSqliteHome`):`CODEX_HOME`})})]})]}),l&&(0,J.jsxs)(`div`,{className:`stw-section`,"data-testid":`log-guard-protection`,children:[(0,J.jsx)(`h4`,{className:`stw-section-title`,children:Dh(t,`protection`)}),(0,J.jsxs)(`div`,{className:`stw-kv-row`,children:[(0,J.jsx)(`span`,{className:`muted`,children:(0,J.jsx)(`code`,{children:Mh(t,l.state)})}),(0,J.jsxs)(`span`,{className:`stw-kv-mono`,children:[(0,J.jsx)(`code`,{children:Nh(t,l.desiredMode)}),l.observedMode===l.desiredMode?null:(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)(`span`,{"aria-hidden":`true`,children:` · `}),(0,J.jsx)(`code`,{children:Nh(t,l.observedMode)})]})]})]}),(0,J.jsxs)(`div`,{className:`storage-policy-actions`,children:[(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,"data-testid":`log-guard-protect-compat`,disabled:u,"aria-pressed":l.desiredMode===`compat`,onClick:()=>o({action:`protect`,mode:`compat`}),children:Dh(t,`compat`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,"data-testid":`log-guard-protect-quiet`,disabled:u,"aria-pressed":l.desiredMode===`quiet`,onClick:()=>o({action:`protect`,mode:`quiet`}),children:Dh(t,`quiet`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,"data-testid":`log-guard-unprotect`,disabled:u||l.desiredMode===`off`,onClick:()=>o({action:`unprotect`}),children:Dh(t,`disable`)}),l.state===`drifted`&&(0,J.jsx)(`button`,{type:`button`,className:`btn btn-sm`,"data-testid":`log-guard-repair`,disabled:u,onClick:()=>o({action:`repair`}),children:Dh(t,`repair`)}),r&&(0,J.jsx)(`span`,{className:`muted`,role:`status`,children:kh(t,`applying`)})]}),i&&(0,J.jsx)(`p`,{className:`err`,role:`alert`,children:i})]}),d&&(0,J.jsx)(`div`,{className:`stw-section`,"data-testid":`log-guard-reclaim`,children:(0,J.jsx)(`div`,{className:`storage-policy-actions`,children:f?(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)(`button`,{type:`button`,className:`btn btn-sm`,"data-testid":`log-guard-compact-confirm`,disabled:r,onClick:()=>{p(!1),o({action:`compact`})},children:Dh(t,`confirmCompact`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,disabled:r,onClick:()=>p(!1),children:Dh(t,`cancel`)})]}):(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,"data-testid":`log-guard-compact`,disabled:r,onClick:()=>p(!0),children:Dh(t,`compact`)})})}),a&&(0,J.jsx)(`div`,{className:`stw-section`,children:(0,J.jsx)(`p`,{className:`stw-kv-mono`,role:`status`,"data-testid":`log-guard-compact-result`,children:a})}),s&&s.topTargets.length>0&&(0,J.jsxs)(`div`,{className:`stw-section`,children:[(0,J.jsx)(`h4`,{className:`stw-section-title`,children:(0,J.jsx)(`code`,{children:`target`})}),s.topTargets.slice(0,5).map(e=>(0,J.jsxs)(`div`,{className:`stw-file-row`,children:[(0,J.jsx)(`span`,{className:`stw-file-path`,title:e.target,children:(0,J.jsx)(`code`,{children:e.target})}),(0,J.jsx)(`span`,{className:`stw-file-size`,children:e.rows.toLocaleString(t)})]},e.target))]}),(0,J.jsx)(`p`,{className:`stw-hint`,children:(0,J.jsxs)(`code`,{children:[`immutable=1 · snapshot=`,e.snapshot]})})]})}function Bh({locale:e,t}){return(0,J.jsxs)(`div`,{className:`stw-section`,"data-testid":`codex-log-guard-unavailable`,children:[(0,J.jsx)(`h3`,{className:`stw-section-title`,children:t(`storage.bucket.logs_db`)}),(0,J.jsx)(`p`,{className:`stw-hint`,children:Dh(e,`inspectionUnavailable`)})]})}function Vh({report:e,locale:t,apiBase:n=``,logGuardBusy:r=!1,onLogGuardAction:i}){let a=Q(),[o,s]=(0,_.useState)(null),[c,l]=(0,_.useState)(null),[u,d]=(0,_.useState)(!1),[f,p]=(0,_.useState)(null),[m,h]=(0,_.useState)(null),g=(0,_.useMemo)(()=>e.buckets.toSorted((e,t)=>t.bytes-e.bytes),[e.buckets]),v=g.find(e=>e.key===o)??null,y=c?.generation===e.generatedAt?c.report:e.codexLogs??null,b=f?.generation===e.generatedAt?f.message:null,x=m?.generation===e.generatedAt?m.summary:null,S=r||u,C=(0,_.useMemo)(()=>{let t=[];for(let n of e.buckets)for(let e of n.largest??[])t.push({...e,bucketKey:n.key});return t.sort((e,t)=>t.bytes-e.bytes).slice(0,10)},[e.buckets]),w=(0,_.useMemo)(()=>new Map(e.buckets.map(e=>[e.key,e])),[e.buckets]);return(0,J.jsxs)(`div`,{className:`storage-workspace-root`,children:[(0,J.jsxs)(`aside`,{className:`storage-workspace-rail`,"aria-label":a(`storage.section.buckets`),children:[(0,J.jsxs)(`div`,{className:`storage-workspace-rail-header`,children:[(0,J.jsx)(`span`,{className:`storage-workspace-rail-title`,children:a(`storage.section.buckets`)}),(0,J.jsx)(`span`,{className:`storage-workspace-rail-count`,children:g.length})]}),(0,J.jsx)(`div`,{className:`storage-workspace-rail-list`,children:g.length===0?(0,J.jsx)(`span`,{className:`storage-workspace-rail-empty`,children:a(`storage.empty`)}):g.map(e=>(0,J.jsxs)(`button`,{type:`button`,className:`storage-workspace-rail-row${o===e.key?` storage-workspace-rail-row--selected`:``}`,onClick:()=>s(t=>t===e.key?null:e.key),"aria-current":o===e.key?`true`:void 0,children:[(0,J.jsxs)(`span`,{className:`storage-workspace-rail-primary`,children:[(0,J.jsx)(`span`,{className:`storage-workspace-rail-name`,children:Fh(e,a)}),(0,J.jsx)(`span`,{className:`storage-workspace-rail-size`,children:Th(e.bytes,t)})]}),(0,J.jsxs)(`span`,{className:`storage-workspace-rail-meta`,children:[e.fileCount.toLocaleString(t),` `,a(`storage.col.files`).toLowerCase()]})]},e.key))})]}),(0,J.jsx)(`section`,{className:`storage-workspace-main`,"aria-label":v?Fh(v,a):a(`storage.section.largest`),children:v?(0,J.jsxs)(`div`,{className:`stw-detail`,children:[(0,J.jsx)(`div`,{className:`stw-detail-toolbar`,children:(0,J.jsxs)(`button`,{type:`button`,className:`stw-detail-back`,onClick:()=>s(null),children:[(0,J.jsx)(Se,{className:`stw-detail-back-chevron`,"aria-hidden":`true`}),a(`modal.back`)]})}),(0,J.jsxs)(`div`,{className:`stw-detail-body`,children:[(0,J.jsx)(`h2`,{className:`stw-detail-title`,children:Fh(v,a)}),(0,J.jsxs)(`dl`,{className:`stw-kv`,children:[(0,J.jsxs)(`div`,{className:`stw-kv-row`,children:[(0,J.jsx)(`dt`,{children:a(`storage.col.size`)}),(0,J.jsx)(`dd`,{className:`stw-kv-mono`,children:Th(v.bytes,t)})]}),(0,J.jsxs)(`div`,{className:`stw-kv-row`,children:[(0,J.jsx)(`dt`,{children:a(`storage.col.files`)}),(0,J.jsx)(`dd`,{className:`stw-kv-mono`,children:v.fileCount.toLocaleString(t)})]}),(0,J.jsxs)(`div`,{className:`stw-kv-row`,children:[(0,J.jsx)(`dt`,{children:a(`storage.col.oldest`)}),(0,J.jsx)(`dd`,{children:Ih(v.oldest,t)})]}),(0,J.jsxs)(`div`,{className:`stw-kv-row`,children:[(0,J.jsx)(`dt`,{children:a(`storage.col.newest`)}),(0,J.jsx)(`dd`,{children:Ih(v.newest,t)})]}),(0,J.jsxs)(`div`,{className:`stw-kv-row`,children:[(0,J.jsx)(`dt`,{children:a(`storage.col.rows`)}),(0,J.jsx)(`dd`,{className:`stw-kv-mono`,children:Lh(v,t,a)})]})]}),(v.largest?.length??0)>0&&(0,J.jsxs)(`div`,{className:`stw-section`,children:[(0,J.jsx)(`h3`,{className:`stw-section-title`,children:a(`storage.section.largest`)}),v.largest.map(e=>(0,J.jsxs)(`div`,{className:`stw-file-row`,children:[(0,J.jsx)(`span`,{className:`stw-file-path`,title:e.path,children:e.path}),(0,J.jsx)(`span`,{className:`stw-file-size`,children:Th(e.bytes,t)})]},e.path))]})]})]}):(0,J.jsxs)(`div`,{className:`stw-overview`,children:[(0,J.jsxs)(`div`,{className:`stw-summary`,children:[(0,J.jsxs)(`div`,{className:`stw-summary-card`,children:[(0,J.jsx)(`div`,{className:`stw-summary-label`,children:a(`storage.card.total`)}),(0,J.jsx)(`div`,{className:`stw-summary-value`,children:Th(e.total.bytes,t)})]}),(0,J.jsxs)(`div`,{className:`stw-summary-card`,children:[(0,J.jsx)(`div`,{className:`stw-summary-label`,children:a(`storage.card.files`)}),(0,J.jsx)(`div`,{className:`stw-summary-value`,children:e.total.fileCount.toLocaleString(t)})]}),(0,J.jsxs)(`div`,{className:`stw-summary-card`,children:[(0,J.jsx)(`div`,{className:`stw-summary-label`,children:a(`storage.card.home`)}),(0,J.jsx)(`div`,{className:`stw-summary-value mono stw-home-path`,title:e.codexHome,children:e.codexHome})]})]}),y?(0,J.jsx)(zh,{report:y,locale:t,t:a,busy:S,error:b,compaction:x,onAction:r=>{if(i){i(r);return}if(u)return;let a=e.generatedAt;(async()=>{d(!0),p(null),r.action===`compact`&&h(null);try{let e=r.action===`protect`?`protect`:r.action,i={method:`POST`,...r.action===`protect`?{headers:{"content-type":`application/json`},body:JSON.stringify({mode:r.mode})}:{}},o=await fetch(`${n}/api/storage/codex-logs/${e}`,i);if(!o.ok){let e=await o.json().catch(()=>({}));p({generation:a,message:Rh(t,e.error)});return}if(r.action===`compact`){let e=(await o.json().catch(()=>null))?.report;if(e){let n=Th(e.logicalBytesReclaimed??0,t),r=Th(e.physicalDatabaseBytesReclaimed??0,t),i=e.complete?Dh(t,`compactComplete`):Dh(t,`compactPartial`),o=e.pagesReclaimed??0,s=!e.complete&&e.stopReason?` (${e.stopReason})`:``;h({generation:a,summary:[`${i}${s}`,`${o.toLocaleString(t)} ${Dh(t,`pagesUnit`)}`,`${n} / ${r}`].join(` — `)})}try{let e=await fetch(`${n}/api/storage/codex-logs`);if(e.ok){let t=await e.json();l({generation:a,report:t})}}catch{}return}let s=await o.json();l({generation:a,report:s})}catch{p({generation:a,message:kh(t,`error.generic`)})}finally{d(!1)}})()}}):e.codexLogsError===`inspect_failed`?(0,J.jsx)(Bh,{locale:t,t:a}):null,C.length>0?(0,J.jsxs)(`div`,{className:`stw-section`,children:[(0,J.jsx)(`h3`,{className:`stw-section-title`,children:a(`storage.section.largest`)}),C.map(e=>{let n=w.get(e.bucketKey);return(0,J.jsxs)(`div`,{className:`stw-file-row`,children:[(0,J.jsx)(`span`,{className:`stw-file-path`,title:e.path,children:e.path}),n&&(0,J.jsx)(`span`,{className:`stw-file-bucket`,children:Fh(n,a)}),(0,J.jsx)(`span`,{className:`stw-file-size`,children:Th(e.bytes,t)})]},`${e.bucketKey}:${e.path}`)})]}):(0,J.jsxs)(`p`,{className:`stw-hint`,children:[(0,J.jsx)(le,{style:{width:14,height:14,verticalAlign:`text-bottom`,marginRight:6},"aria-hidden":`true`}),a(`storage.workspace.selectBucket`)]})]})})]})}var Hh=1024**3,Uh=[10,25,50],Wh=(e,t)=>{if(!(e instanceof Error))return t;let n=e.message;return n===`Failed to fetch`||n.includes(`NetworkError`)||n.includes(`network error`)||n.includes(`JSON`)||n.includes(`Unexpected end of`)?t:n||t};function Gh({apiBase:e,locale:t,t:n,onDone:r}){let[i,a]=(0,_.useState)(25),[o,s]=(0,_.useState)(null),[c,l]=(0,_.useState)(!1),[u,d]=(0,_.useState)(!1),[f,p]=(0,_.useState)(!1),[m,h]=(0,_.useState)(null),[g,v]=(0,_.useState)(null),y=(0,_.useRef)(null),b=(0,_.useRef)(null),x=(0,_.useRef)(!1),S=(0,_.useCallback)((e=!1)=>{l(!1),d(!1),e&&s(null)},[]);(0,_.useEffect)(()=>{x.current=f},[f]),(0,_.useEffect)(()=>{if(!c)return;b.current=document.activeElement,y.current?.focus();let e=e=>{e.key===`Escape`&&!x.current&&S()};return window.addEventListener(`keydown`,e),()=>{window.removeEventListener(`keydown`,e),b.current?.focus()}},[c,S]);let C=(e,t,r)=>{switch(e){case`codex_busy`:return n(`storage.cleanup.err.codex_busy`);case`stale_preview`:return n(`storage.cleanup.err.stale_preview`);case`restore_pending_overlap`:return n(`storage.cleanup.err.restore_pending_overlap`);case`referenced_history`:return n(`storage.cleanup.err.referenced_history`);case`invalid_digest`:return n(`storage.cleanup.err.invalid_digest`);case`invalid_mode`:return n(`storage.cleanup.err.invalid_mode`);case`fs_failed`:return r?n(`storage.cleanup.err.fs_failed_trash`,{trashDir:r}):n(`storage.cleanup.err.fs_failed`);case`db_reconcile_failed`:return n(`storage.cleanup.err.db_reconcile_failed`);case`cleanup_failed`:return n(`storage.cleanup.err.cleanup_failed`);default:return t??n(`storage.cleanup.cleanupFailed`)}},w=e=>n(`storage.cleanup.preset`,{percent:new Intl.NumberFormat(t,{style:`percent`,maximumFractionDigits:0}).format(e/100)}),T=async()=>{p(!0),v(null),h(null);try{let t=await fetch(`${e}/api/storage/cleanup/preview`,{method:`POST`,headers:{"content-type":`application/json`},body:JSON.stringify({percent:i})});if(!t.ok){let e=await t.json().catch(()=>({}));throw Error(C(e.error,n(`storage.cleanup.previewFailed`)))}let r=await t.json();s(r),l(!0)}catch(e){v(Wh(e,n(`storage.cleanup.previewFailed`)))}finally{p(!1)}},E=async()=>{if(o){p(!0),v(null);try{let i=await fetch(`${e}/api/storage/cleanup`,{method:`POST`,headers:{"content-type":`application/json`},body:JSON.stringify({percent:o.percent,mode:u?`permanent`:`quarantine`,digest:o.digest})});if(!i.ok){let e=await i.json().catch(()=>({}));throw e.error===`stale_preview`&&S(!0),Error(C(e.error,e.message,e.trashDir))}let a=await i.json();if(!a.ok)throw a.error===`stale_preview`&&S(!0),Error(C(a.error,a.message,a.trashDir));S(!0),h(n(u?`storage.cleanup.donePermanent`:`storage.cleanup.doneQuarantine`,{count:String(a.count),size:Th(a.bytes,t)})),r()}catch(e){v(Wh(e,n(`storage.cleanup.cleanupFailed`)))}finally{p(!1)}}};return(0,J.jsxs)(`section`,{className:`storage-cleanup-pane`,children:[(0,J.jsx)(`p`,{className:`muted storage-manual-panel__help`,children:n(`storage.cleanup.help`)}),(0,J.jsxs)(`div`,{className:`storage-manual-panel__controls`,children:[(0,J.jsxs)(`label`,{className:`storage-manual-panel__slider`,children:[(0,J.jsx)(`span`,{className:`muted mono`,style:{minWidth:`3.5rem`,fontVariantNumeric:`tabular-nums`},children:n(`storage.cleanup.percent`,{percent:String(i)})}),(0,J.jsx)(`input`,{type:`range`,min:1,max:100,value:i,onChange:e=>a(Number(e.target.value)),disabled:f,style:{flex:1,minWidth:0},"aria-label":n(`storage.cleanup.slider`)})]}),(0,J.jsx)(`div`,{className:`storage-manual-panel__presets`,children:Uh.map(e=>(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm${i===e?` active`:``}`,disabled:f,onClick:()=>a(e),children:w(e)},e))}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-sm`,disabled:f,onClick:()=>void T(),children:n(`storage.cleanup.preview`)})]}),m&&(0,J.jsx)(`p`,{className:`muted storage-manual-panel__status`,children:m}),g&&!c&&(0,J.jsx)(`p`,{className:`storage-manual-panel__status`,style:{color:`var(--red)`},children:g}),c&&o&&(0,J.jsx)(`div`,{className:`modal-overlay`,role:`dialog`,"aria-modal":`true`,"aria-labelledby":`storage-cleanup-confirm-title`,onClick:()=>!f&&S(),children:(0,J.jsxs)(`div`,{className:`modal-card`,onClick:e=>e.stopPropagation(),children:[(0,J.jsx)(`h3`,{id:`storage-cleanup-confirm-title`,children:n(`storage.cleanup.confirmTitle`)}),(0,J.jsx)(`p`,{children:n(`storage.cleanup.confirmBody`,{count:String(o.count),size:Th(o.bytes,t),percent:String(o.percent)})}),o.candidates.length>0&&(0,J.jsxs)(`ul`,{className:`mono muted`,style:{maxHeight:160,overflow:`auto`,fontSize:`var(--text-caption)`},children:[o.candidates.slice(0,8).map(e=>(0,J.jsx)(`li`,{children:e.relPath},e.relPath)),o.count>8&&(0,J.jsx)(`li`,{children:n(`storage.cleanup.moreFiles`,{n:String(Math.max(0,o.count-8))})})]}),(0,J.jsxs)(`label`,{style:{display:`flex`,gap:8,alignItems:`center`,marginTop:12},children:[(0,J.jsx)(`input`,{type:`checkbox`,checked:u,disabled:f,onChange:e=>d(e.target.checked)}),(0,J.jsx)(`span`,{children:n(`storage.cleanup.permanent`)})]}),(0,J.jsx)(`p`,{className:`muted`,style:{marginTop:8,fontSize:`var(--text-caption)`},children:n(u?`storage.cleanup.permanentWarn`:`storage.cleanup.quarantineNote`)}),g&&(0,J.jsx)(`p`,{style:{marginTop:12,color:`var(--red)`},children:g}),(0,J.jsxs)(`div`,{className:`dialog-actions`,style:{marginTop:16},children:[(0,J.jsx)(`button`,{ref:y,type:`button`,className:`btn btn-ghost`,disabled:f,onClick:()=>S(),children:n(`storage.cleanup.cancel`)}),(0,J.jsx)(`button`,{type:`button`,className:u?`btn btn-danger`:`btn`,disabled:f||o.count===0,onClick:()=>void E(),children:n(u?`storage.cleanup.confirmPermanent`:`storage.cleanup.confirmQuarantine`)})]})]})})]})}function Kh({apiBase:e,locale:t,t:n,onDone:r,reloadToken:i,onEntriesChange:a}){let[o,s]=(0,_.useState)(!1),[c,l]=(0,_.useState)(null),[u,d]=(0,_.useState)(null),[f,p]=(0,_.useState)(null),m=(0,_.useRef)(null),h=(0,_.useRef)(null),g=(0,_.useRef)(!1);(0,_.useEffect)(()=>{g.current=o},[o]);let v=(0,_.useCallback)(()=>l(null),[]);(0,_.useEffect)(()=>{if(!c)return;h.current=document.activeElement,m.current?.focus();let e=e=>{e.key===`Escape`&&!g.current&&v()};return window.addEventListener(`keydown`,e),()=>{window.removeEventListener(`keydown`,e),h.current?.focus()}},[c,v]);let y=(0,_.useCallback)(async t=>{let r=await fetch(`${e}/api/storage/trash`,{signal:t});if(!r.ok)throw Error(n(`storage.trash.listFailed`));let i=await r.json(),o=Array.isArray(i.entries)?i.entries:[];return a?.(o),o},[e,a,n]),b=ml(`storage-trash:${e}`,[e,i],y,{isEmpty:e=>e.length===0}).state,x=b.data??[],S=(e,t)=>{switch(e){case`codex_busy`:return n(`storage.trash.err.codex_busy`);case`invalid_trash`:return n(`storage.trash.err.invalid_trash`);case`missing_trash`:return n(`storage.trash.err.missing_trash`);case`dest_exists`:return n(`storage.trash.err.dest_exists`);case`fs_failed`:return n(`storage.trash.err.fs_failed`);case`db_reconcile_failed`:return n(`storage.trash.err.db_reconcile_failed`);case`storage_mutation_busy`:return n(`storage.trash.err.storage_mutation_busy`);case`restore_failed`:return n(`storage.trash.err.restore_failed`);case`restore_worker_timeout`:return n(`storage.trash.err.restore_worker_timeout`);case`restore_worker_aborted`:return n(`storage.trash.err.restore_worker_aborted`);case`restore_worker_failed`:return t??n(`storage.trash.err.restore_worker_failed`);default:return t??n(`storage.trash.restoreFailed`)}},C=async()=>{if(c){s(!0),p(null);try{let i=await fetch(`${e}/api/storage/trash/restore`,{method:`POST`,headers:{"content-type":`application/json`},body:JSON.stringify({id:c.id})});if(!i.ok){let e=await i.json().catch(()=>({}));throw Error(S(e.error,e.message))}let a=await i.json();if(!a.ok)throw Error(S(a.error,a.message));v(),d(n(`storage.trash.done`,{count:String(a.count),size:Th(a.bytes,t)})),r()}catch(e){p(Wh(e,n(`storage.trash.restoreFailed`)))}finally{s(!1)}}},w=e=>{let n=e.quarantinedAt??Number(e.epoch.split(`-`)[0]);return!Number.isFinite(n)||n<=0?`—`:new Date(n).toLocaleString(t)},T=e=>e===`permanent`?n(`storage.trash.mode.permanent`):e===`quarantine`?n(`storage.trash.mode.quarantine`):`—`;return(0,J.jsxs)(`section`,{className:`storage-cleanup-pane storage-quarantine-pane`,children:[(0,J.jsx)(`p`,{className:`muted storage-manual-panel__help`,children:n(`storage.trash.help`)}),u&&(0,J.jsx)(`p`,{className:`muted storage-manual-panel__status`,children:u}),f&&!c&&(0,J.jsx)(`p`,{className:`storage-manual-panel__status`,style:{color:`var(--red)`},role:`alert`,children:f}),b.showError&&!c&&(0,J.jsx)(`p`,{className:`storage-manual-panel__status`,style:{color:`var(--red)`},role:`alert`,children:b.error instanceof Error?b.error.message:n(`storage.trash.listFailed`)}),b.refreshing&&!b.showSkeleton&&(0,J.jsx)(_l,{live:!b.showError,children:n(`storage.trash.loading`)}),b.showSkeleton?(0,J.jsx)(gl,{label:n(`storage.trash.loading`),rows:2}):x.length===0?(0,J.jsx)(`p`,{className:`muted storage-manual-panel__status`,children:n(`storage.trash.empty`)}):(0,J.jsx)(`div`,{className:`tbl-wrap storage-manual-panel__table`,children:(0,J.jsxs)(`table`,{className:`tbl`,children:[(0,J.jsx)(`thead`,{children:(0,J.jsxs)(`tr`,{children:[(0,J.jsx)(`th`,{children:n(`storage.trash.col.when`)}),(0,J.jsx)(`th`,{className:`num`,children:n(`storage.trash.col.files`)}),(0,J.jsx)(`th`,{className:`num`,children:n(`storage.trash.col.size`)}),(0,J.jsx)(`th`,{children:n(`storage.trash.col.mode`)}),(0,J.jsx)(`th`,{children:n(`storage.trash.col.id`)}),(0,J.jsx)(`th`,{})]})}),(0,J.jsx)(`tbody`,{children:x.map(e=>(0,J.jsxs)(`tr`,{children:[(0,J.jsx)(`td`,{className:`muted`,children:w(e)}),(0,J.jsx)(`td`,{className:`num`,children:e.fileCount}),(0,J.jsx)(`td`,{className:`num mono`,children:Th(e.bytes,t)}),(0,J.jsx)(`td`,{className:`muted`,children:T(e.mode)}),(0,J.jsx)(`td`,{className:`mono`,style:{fontSize:`var(--text-caption)`},children:e.id}),(0,J.jsx)(`td`,{children:(0,J.jsx)(`button`,{type:`button`,className:`btn btn-sm`,disabled:o,onClick:()=>{p(null),l(e)},children:n(`storage.trash.restore`)})})]},e.id))})]})}),c&&(0,J.jsx)(`div`,{className:`modal-overlay`,role:`dialog`,"aria-modal":`true`,"aria-labelledby":`storage-trash-confirm-title`,onClick:()=>!o&&v(),children:(0,J.jsxs)(`div`,{className:`modal-card`,onClick:e=>e.stopPropagation(),children:[(0,J.jsx)(`h3`,{id:`storage-trash-confirm-title`,children:n(`storage.trash.confirmTitle`)}),(0,J.jsx)(`p`,{children:n(`storage.trash.confirmBody`,{count:String(c.fileCount),size:Th(c.bytes,t),id:c.id})}),f&&(0,J.jsx)(`p`,{style:{marginTop:12,color:`var(--red)`},children:f}),(0,J.jsxs)(`div`,{className:`dialog-actions`,style:{marginTop:16},children:[(0,J.jsx)(`button`,{ref:m,type:`button`,className:`btn btn-ghost`,disabled:o,onClick:()=>v(),children:n(`storage.trash.cancel`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn`,disabled:o,onClick:()=>void C(),children:n(`storage.trash.confirmRestore`)})]})]})})]})}function qh(e){let{job:t,...n}=e;return n}function Jh(e){let t=String(Math.max(0,Math.round(e.trigger.archivedBytesOver/Hh*100)/100));return e.target.reduceToBytes===void 0?{policy:qh(e),thresholdGb:t,targetMode:`percent`,percent:String(Math.min(100,Math.max(1,Math.floor(e.target.removeOldestPercent??25)))),reduceGb:`4`}:{policy:qh(e),thresholdGb:t,targetMode:`reduce`,percent:`25`,reduceGb:String(Math.max(0,Math.round(e.target.reduceToBytes/Hh*100)/100))}}async function Yh(e){await new Promise(t=>window.setTimeout(t,e))}function Xh({apiBase:e,locale:t,t:n,onDone:r}){let i=`ocx.storage.cleanup-policy.v1:${e}`,a=gr(i),o=(0,_.useRef)(!!a),[s,c]=(0,_.useState)(()=>a?.policy??null),[l,u]=(0,_.useState)(()=>!a),[d,f]=(0,_.useState)(!1),[p,m]=(0,_.useState)(!1),[h,g]=(0,_.useState)(null),[v,y]=(0,_.useState)(null),[b,x]=(0,_.useState)(()=>a?.targetMode??`percent`),[S,C]=(0,_.useState)(()=>a?.percent??`25`),[w,T]=(0,_.useState)(()=>a?.reduceGb??`4`),[E,D]=(0,_.useState)(()=>a?.thresholdGb??`5`),O=(0,_.useRef)(null),k=(0,_.useRef)(!1),A=(0,_.useRef)(!1),j=(0,_.useRef)(0),M=(0,_.useCallback)(e=>{let t=Jh(e);c(t.policy),D(t.thresholdGb),x(t.targetMode),C(t.percent),T(t.reduceGb),o.current=!0,br(i,t),k.current=!1},[i]),N=(0,_.useCallback)(()=>{k.current=!0},[]),P=(0,_.useCallback)(e=>{A.current=e},[]),F=(0,_.useCallback)(async t=>{let r=++j.current;o.current||u(!0),y(null);try{let n=await fetch(`${e}/api/storage/cleanup-policy`,{signal:t});if(!n.ok)throw Error(`load_failed`);let i=await n.json();if(t?.aborted||r!==j.current||k.current||A.current)return;M(i)}catch{if(t?.aborted||r!==j.current)return;o.current||(c(null),y(n(`storage.policy.loadFailed`)))}finally{!t?.aborted&&r===j.current&&u(!1)}},[e,M,n]);(0,_.useEffect)(()=>{let e=new AbortController,t=window.setTimeout(()=>{F(e.signal)},0);return()=>{window.clearTimeout(t),j.current+=1,e.abort()}},[F]),(0,_.useEffect)(()=>()=>{O.current?.abort(),O.current=null},[]);let I=()=>{if(!s)return null;let e=E.trim();if(e===``)return null;let t=Number(e);if(!Number.isFinite(t)||t<0)return null;let n;if(b===`reduce`){let e=w.trim();if(e===``)return null;let t=Number(e);if(!Number.isFinite(t)||t<0)return null;n={reduceToBytes:Math.floor(t*Hh)}}else{let e=Number(S);if(!Number.isFinite(e)||e<1||e>100)return null;n={removeOldestPercent:Math.min(100,Math.max(1,Math.floor(e)))}}return{enabled:s.enabled,trigger:{archivedBytesOver:Math.floor(t*Hh)},target:n,schedule:s.schedule,mode:s.mode}},L=async t=>{let r=I();if(!r){y(n(`storage.policy.invalid`));return}let i={...r,...t};f(!0),y(null),g(null);try{let t=await fetch(`${e}/api/storage/cleanup-policy`,{method:`PUT`,headers:{"content-type":`application/json`},body:JSON.stringify(i)});if(!t.ok){y(n(`storage.policy.saveFailed`));return}let r=await t.json();if(!r.policy){y(n(`storage.policy.saveFailed`));return}M(r.policy),g(n(`storage.policy.saved`))}catch{y(n(`storage.policy.saveFailed`))}finally{f(!1)}},R=async()=>{O.current?.abort();let i=new AbortController;O.current=i;let{signal:a}=i;m(!0),y(null),g(null);try{let i=I();if(!i){y(n(`storage.policy.invalid`));return}let o=await fetch(`${e}/api/storage/cleanup-policy`,{method:`PUT`,headers:{"content-type":`application/json`},body:JSON.stringify(i),signal:a});if(a.aborted)return;if(!o.ok){y(n(`storage.policy.saveFailed`));return}let s=await o.json();if(a.aborted)return;if(!s.policy){y(n(`storage.policy.saveFailed`));return}M(s.policy);let c=await fetch(`${e}/api/storage/cleanup-policy/run`,{method:`POST`,signal:a});if(a.aborted)return;if(c.status===409){let e=await c.json().catch(()=>({}));if(a.aborted)return;e.policy&&M(e.policy),y(n(`storage.policy.alreadyRunning`));return}if(!c.ok){let e=await c.json().catch(()=>({}));if(a.aborted)return;if(e.policy&&M(e.policy),e.error===`already_running`){y(n(`storage.policy.alreadyRunning`));return}y(n(`storage.policy.runFailed`));return}let l=await c.json();if(a.aborted)return;if(l.policy&&M(l.policy),l.error===`already_running`){y(n(`storage.policy.alreadyRunning`));return}if(!l.started||!l.job?.startedAt){y(n(`storage.policy.runFailed`));return}let u=l.job.startedAt,d=Date.now()+12e4,f,p;for(;Date.now()=u&&r.lastOutcome){f=r.lastOutcome;break}}}if(a.aborted)return;if(p&&M(p),!f){y(n(`storage.policy.runFailed`));return}f.skipped===`disabled`?g(n(`storage.policy.skippedDisabled`)):f.ok&&f.metadataPersistenceError?(y(n(`storage.policy.metadataSaveWarning`)),f.removed!==void 0&&r()):f.skipped===`under_threshold`?g(n(`storage.policy.skippedUnder`)):f.skipped===`nothing_selected`?g(n(`storage.policy.skippedEmpty`)):f.deferred===`codex_busy`||f.error===`codex_busy`?y(n(`storage.cleanup.err.codex_busy`)):f.ok?(g(f.mode===`permanent`?n(`storage.policy.donePermanent`,{count:String(f.removed??0),size:Th(f.freedBytes??0,t)}):n(`storage.policy.doneQuarantine`,{count:String(f.removed??0),size:Th(f.freedBytes??0,t)})),r()):y(n(`storage.policy.runFailed`))}catch(e){if(a.aborted||e instanceof DOMException&&e.name===`AbortError`)return;y(n(`storage.policy.runFailed`))}finally{O.current===i&&(O.current=null),a.aborted||m(!1)}},z=e=>e===void 0?n(`storage.policy.never`):new Date(e).toLocaleString(t);return l&&!s?(0,J.jsx)(`section`,{className:`storage-cleanup-pane`,children:(0,J.jsx)(`p`,{className:`muted storage-policy-help`,children:n(`storage.policy.loading`)})}):s?(0,J.jsxs)(`section`,{className:`storage-cleanup-pane`,children:[(0,J.jsx)(`p`,{className:`muted storage-policy-help`,children:n(`storage.policy.help`)}),(0,J.jsx)(`div`,{className:`storage-policy-enable`,children:(0,J.jsxs)(`div`,{className:`storage-policy-enable-row`,children:[(0,J.jsx)(`button`,{type:`button`,className:`toggle${s.enabled?` on`:``}`,disabled:d||p,"aria-pressed":s.enabled,"aria-label":n(`storage.policy.enabled`),title:n(`storage.policy.enabledHint`),onClick:()=>void L({enabled:!s.enabled}),children:(0,J.jsx)(`span`,{className:`toggle-knob`})}),(0,J.jsx)(`span`,{children:n(`storage.policy.enabled`)})]})}),(0,J.jsxs)(`div`,{className:`storage-policy-fields`,children:[(0,J.jsxs)(`div`,{className:`field storage-policy-trigger`,children:[(0,J.jsx)(`label`,{className:`field-label`,htmlFor:`storage-policy-threshold`,children:n(`storage.policy.trigger`)}),(0,J.jsxs)(`div`,{className:`storage-policy-trigger-row`,children:[(0,J.jsx)(`span`,{className:`storage-policy-trigger-hint`,children:n(`storage.policy.threshold`)}),(0,J.jsxs)(`span`,{className:`codex-auto-switch-input-wrap`,onBlur:e=>{e.currentTarget.contains(e.relatedTarget)||(P(!1),L())},children:[(0,J.jsx)(`input`,{id:`storage-policy-threshold`,className:`input mono codex-auto-switch-input`,type:`number`,min:0,step:.1,inputMode:`decimal`,value:E,disabled:d||p,"aria-label":n(`storage.policy.threshold`),onFocus:()=>P(!0),onChange:e=>{N(),D(e.target.value)},onKeyDown:e=>{e.nativeEvent.isComposing||d||p||e.key===`Enter`&&(e.preventDefault(),L())}}),(0,J.jsx)(`span`,{className:`codex-auto-switch-unit`,"aria-hidden":`true`,children:`GiB`}),(0,J.jsx)(ko,{disabled:d||p,incrementLabel:n(`storage.policy.thresholdInc`),decrementLabel:n(`storage.policy.thresholdDec`),onIncrement:()=>{N(),D(Oo(E,.1,0,1e4,.1))},onDecrement:()=>{N(),D(Oo(E,-.1,0,1e4,.1))}})]})]})]}),(0,J.jsxs)(`fieldset`,{className:`field storage-policy-target`,children:[(0,J.jsx)(`legend`,{className:`field-label`,children:n(`storage.policy.target`)}),(0,J.jsxs)(`label`,{className:`storage-policy-target-row`,children:[(0,J.jsx)(`input`,{type:`radio`,name:`storage-policy-target`,checked:b===`percent`,disabled:d||p,onChange:()=>{N(),x(`percent`)}}),(0,J.jsx)(`span`,{className:`storage-policy-target-label`,children:n(`storage.policy.targetPercent`)}),b===`percent`&&(0,J.jsxs)(`span`,{className:`codex-auto-switch-input-wrap`,onBlur:e=>{e.currentTarget.contains(e.relatedTarget)||(P(!1),L())},children:[(0,J.jsx)(`input`,{id:`storage-policy-percent`,className:`input mono codex-auto-switch-input`,type:`number`,min:1,max:100,step:1,inputMode:`numeric`,value:S,disabled:d||p,"aria-label":n(`storage.policy.targetPercent`),onFocus:()=>P(!0),onChange:e=>{N(),C(e.target.value)},onKeyDown:e=>{e.nativeEvent.isComposing||d||p||e.key===`Enter`&&(e.preventDefault(),L())}}),(0,J.jsx)(`span`,{className:`codex-auto-switch-unit`,"aria-hidden":`true`,children:`%`}),(0,J.jsx)(ko,{disabled:d||p,incrementLabel:n(`storage.policy.percentInc`),decrementLabel:n(`storage.policy.percentDec`),onIncrement:()=>{N(),C(Oo(S,1,1,100))},onDecrement:()=>{N(),C(Oo(S,-1,1,100))}})]})]}),(0,J.jsxs)(`label`,{className:`storage-policy-target-row`,children:[(0,J.jsx)(`input`,{type:`radio`,name:`storage-policy-target`,checked:b===`reduce`,disabled:d||p,onChange:()=>{N(),x(`reduce`)}}),(0,J.jsx)(`span`,{className:`storage-policy-target-label`,children:n(`storage.policy.targetReduce`)}),b===`reduce`&&(0,J.jsxs)(`span`,{className:`codex-auto-switch-input-wrap`,onBlur:e=>{e.currentTarget.contains(e.relatedTarget)||(P(!1),L())},children:[(0,J.jsx)(`input`,{id:`storage-policy-reduce`,className:`input mono codex-auto-switch-input`,type:`number`,min:0,step:.1,inputMode:`decimal`,value:w,disabled:d||p,"aria-label":n(`storage.policy.targetReduce`),onFocus:()=>P(!0),onChange:e=>{N(),T(e.target.value)},onKeyDown:e=>{e.nativeEvent.isComposing||d||p||e.key===`Enter`&&(e.preventDefault(),L())}}),(0,J.jsx)(`span`,{className:`codex-auto-switch-unit`,"aria-hidden":`true`,children:`GiB`}),(0,J.jsx)(ko,{disabled:d||p,incrementLabel:n(`storage.policy.reduceInc`),decrementLabel:n(`storage.policy.reduceDec`),onIncrement:()=>{N(),T(Oo(w,.1,0,1e4,.1))},onDecrement:()=>{N(),T(Oo(w,-.1,0,1e4,.1))}})]})]})]}),(0,J.jsxs)(`div`,{className:`storage-policy-selects`,children:[(0,J.jsxs)(`label`,{className:`field`,htmlFor:`storage-policy-schedule`,children:[(0,J.jsx)(`span`,{className:`field-label`,children:n(`storage.policy.schedule`)}),(0,J.jsxs)(`select`,{id:`storage-policy-schedule`,className:`input`,value:s.schedule,disabled:d||p,onChange:e=>{let t=e.target.value;L({schedule:t})},children:[(0,J.jsx)(`option`,{value:`manual`,children:n(`storage.policy.schedule.manual`)}),(0,J.jsx)(`option`,{value:`startup`,children:n(`storage.policy.schedule.startup`)}),(0,J.jsx)(`option`,{value:`daily`,children:n(`storage.policy.schedule.daily`)}),(0,J.jsx)(`option`,{value:`weekly`,children:n(`storage.policy.schedule.weekly`)})]})]}),(0,J.jsxs)(`label`,{className:`field`,htmlFor:`storage-policy-mode`,children:[(0,J.jsx)(`span`,{className:`field-label`,children:n(`storage.policy.mode`)}),(0,J.jsxs)(`select`,{id:`storage-policy-mode`,className:`input`,value:s.mode,disabled:d||p,onChange:e=>{let t=e.target.value;L({mode:t})},children:[(0,J.jsx)(`option`,{value:`quarantine`,children:n(`storage.policy.mode.quarantine`)}),(0,J.jsx)(`option`,{value:`permanent`,children:n(`storage.policy.mode.permanent`)})]})]})]}),s.mode===`permanent`&&(0,J.jsx)(`p`,{className:`err storage-policy-warn`,role:`status`,children:n(`storage.policy.permanentWarn`)})]}),(0,J.jsxs)(`div`,{className:`storage-policy-meta`,children:[(0,J.jsxs)(`div`,{className:`storage-policy-meta-item`,children:[(0,J.jsx)(`span`,{className:`muted`,children:n(`storage.policy.lastRun`)}),(0,J.jsxs)(`span`,{className:`storage-policy-meta-value`,children:[z(s.lastRun?.at),s.lastRun?` · ${n(`storage.policy.lastRunDetail`,{count:String(s.lastRun.removed),size:Th(s.lastRun.freedBytes,t)})}`:``]})]}),(0,J.jsxs)(`div`,{className:`storage-policy-meta-item`,children:[(0,J.jsx)(`span`,{className:`muted`,children:n(`storage.policy.nextRun`)}),(0,J.jsx)(`span`,{className:`storage-policy-meta-value`,children:z(s.nextRun)})]})]}),(0,J.jsxs)(`div`,{className:`storage-policy-actions`,children:[(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,disabled:d||p,onClick:()=>void L(),children:n(`storage.policy.save`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-sm`,disabled:d||p,onClick:()=>void R(),children:n(p?`storage.policy.running`:`storage.policy.runNow`)}),(0,J.jsx)(`span`,{className:`storage-policy-actions__status${v?` is-error`:``}`,role:v?`alert`:`status`,"aria-live":`polite`,children:v??h??``})]})]}):(0,J.jsx)(`section`,{className:`storage-cleanup-pane`,children:v&&(0,J.jsx)(`p`,{className:`err`,role:`alert`,children:v})})}function Zh({apiBase:e,locale:t,t:n,archivedCount:r,showQuarantine:i,trashReloadToken:a,onDone:o,onTrashEntriesChange:s}){let[c,l]=(0,_.useState)(`policy`),u=(0,_.useRef)(null),d=(0,_.useRef)(null),f=[{id:`policy`,label:n(`storage.cleanupCard.tab.policy`),ref:u},{id:`quarantine`,label:n(`storage.cleanupCard.tab.quarantine`),ref:d}],p=e=>{l(e),window.requestAnimationFrame(()=>(e===`policy`?u:d).current?.focus())},m=e=>{e.key===`ArrowLeft`||e.key===`ArrowRight`?(e.preventDefault(),p(c===`policy`?`quarantine`:`policy`)):e.key===`Home`?(e.preventDefault(),p(`policy`)):e.key===`End`&&(e.preventDefault(),p(`quarantine`))};return(0,J.jsxs)(`section`,{className:`panel storage-cleanup-card`,"aria-labelledby":`storage-cleanup-card-title`,children:[(0,J.jsx)(`div`,{className:`page-tabs storage-cleanup-card__tabs`,role:`tablist`,"aria-label":n(`storage.cleanupCard.tabs`),children:f.map(({id:e,label:t,ref:n})=>(0,J.jsx)(`button`,{type:`button`,role:`tab`,ref:n,id:`storage-cleanup-tab-${e}`,"aria-selected":c===e,"aria-controls":`storage-cleanup-panel-${e}`,tabIndex:c===e?0:-1,className:`page-tab${c===e?` page-tab--active`:``}`,onKeyDown:m,onClick:()=>p(e),children:t},e))}),(0,J.jsx)(`h3`,{id:`storage-cleanup-card-title`,className:`panel-title`,children:n(`storage.cleanupCard.title`)}),(0,J.jsxs)(`div`,{className:`storage-cleanup-card__stack`,children:[(0,J.jsxs)(`div`,{id:`storage-cleanup-panel-policy`,role:`tabpanel`,"aria-labelledby":`storage-cleanup-tab-policy`,className:`storage-cleanup-card__body storage-cleanup-policy-split`,"data-active":c===`policy`?`true`:`false`,"aria-hidden":c!==`policy`,...c===`policy`?{}:{inert:!0},children:[(0,J.jsx)(Xh,{apiBase:e,locale:t,t:n,onDone:o}),(0,J.jsxs)(`aside`,{className:`storage-cleanup-manual`,"aria-labelledby":`storage-cleanup-manual-title`,children:[(0,J.jsx)(`h4`,{id:`storage-cleanup-manual-title`,className:`storage-cleanup-manual__title`,children:n(`storage.cleanup.title`)}),r>0?(0,J.jsx)(Gh,{apiBase:e,locale:t,t:n,onDone:o}):(0,J.jsx)(`p`,{className:`muted storage-manual-panel__status`,children:n(`storage.cleanup.noArchives`)})]})]}),(0,J.jsx)(`div`,{id:`storage-cleanup-panel-quarantine`,role:`tabpanel`,"aria-labelledby":`storage-cleanup-tab-quarantine`,className:`storage-cleanup-card__body`,"data-active":c===`quarantine`?`true`:`false`,"aria-hidden":c!==`quarantine`,...c===`quarantine`?{}:{inert:!0},children:i?(0,J.jsx)(Kh,{apiBase:e,locale:t,t:n,onDone:o,reloadToken:a,onEntriesChange:s}):(0,J.jsx)(`p`,{className:`muted storage-manual-panel__status`,children:n(`storage.trash.empty`)})})]})]})}function Qh({apiBase:e}){let{t,locale:n}=ct(),r=`ocx.storage.report.v1:${e}`,i=gr(r),[a,o]=(0,_.useState)(null),[s,c]=(0,_.useState)(0),l=(0,_.useRef)(!1),[u,d]=(0,_.useState)({apiBase:e,settled:!1,hasEntries:!1}),f=(0,_.useCallback)(async n=>{try{let i=await fetch(`${e}/api/storage`,{signal:n});if(!i.ok)throw Error(t(`storage.error`));let a=await i.json();return br(r,a),l.current&&(l.current=!1,o(t(`storage.rescanned`))),a}catch(e){throw l.current&&(l.current=!1,o(t(`storage.error`))),n.aborted?e:Error(t(`storage.error`),{cause:e})}},[e,r,t]),p=ml(`storage-report:${e}`,[e],f,{isEmpty:e=>e.total.fileCount===0&&e.error===void 0}),m=p.state,h=m.data??i,g=m.refreshing||m.showSkeleton&&!h,v=p.refresh,y=(0,_.useCallback)(()=>{o(null),l.current=!0,v(),c(e=>e+1)},[v]),b=(0,_.useCallback)(t=>{d({apiBase:e,settled:!0,hasEntries:t.length>0})},[e]),x=u.apiBase===e&&u.settled,S=u.apiBase===e&&u.hasEntries,C=h?.error!==void 0,w=!g&&!m.showError&&!C&&h.total.fileCount===0&&x&&!S,T=h?.buckets.find(e=>e.key===`archived_sessions`)?.fileCount??0,E=!!h&&!C,D=E&&(h.total.fileCount>0||!x||S);return(0,J.jsxs)(J.Fragment,{children:[(0,J.jsxs)(`div`,{className:`page-head`,children:[(0,J.jsx)(`h2`,{id:`storage-page-title`,children:t(`storage.title`)}),(0,J.jsxs)(`div`,{className:`storage-page-head-actions`,children:[(0,J.jsx)(`span`,{className:`storage-page-head-feedback`,role:`status`,"aria-live":`polite`,children:a??``}),(0,J.jsxs)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,disabled:g,onClick:()=>void y(),children:[(0,J.jsx)(pe,{}),` `,t(`storage.refresh`)]})]})]}),(0,J.jsx)(`p`,{className:`page-sub`,children:t(`storage.subtitle`)}),h&&h.error===void 0&&(0,J.jsxs)(`p`,{className:`storage-page-meta`,children:[(0,J.jsx)(`code`,{className:`mono storage-page-meta__home`,title:h.codexHome,children:h.codexHome}),(0,J.jsx)(`span`,{className:`storage-page-meta__sep`,"aria-hidden":`true`,children:`·`}),(0,J.jsxs)(`span`,{children:[t(`storage.snapshot.lastScan`),`:`,` `,new Date(h.generatedAt).toLocaleString(n)]})]}),m.showSkeleton&&!h?(0,J.jsx)(gl,{label:t(`storage.loading`),rows:5}):m.kind===`failed-cold`&&!h?(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)(`div`,{className:`alert alert-err`,role:`alert`,children:m.error instanceof Error?m.error.message:t(`storage.error`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:()=>v(),children:t(`common.retry`)})]}):C?(0,J.jsx)(J.Fragment,{children:(0,J.jsx)(`div`,{className:`alert alert-err`,role:`alert`,children:t(`storage.error`)})}):(0,J.jsxs)(J.Fragment,{children:[m.showError&&(0,J.jsx)(`div`,{className:`alert alert-err`,role:`alert`,children:t(`storage.error`)}),w?(0,J.jsx)(Ot,{title:t(`storage.empty`)}):h&&h.total.fileCount>0&&(0,J.jsx)(Vh,{report:h,locale:n,apiBase:e})]}),h&&h.error===void 0&&m.refreshing&&!m.showSkeleton&&(0,J.jsx)(_l,{live:!m.showError,children:t(`storage.loading`)}),E&&(0,J.jsx)(Zh,{apiBase:e,locale:n,t,archivedCount:T,showQuarantine:D,trashReloadToken:s,onDone:()=>void y(),onTrashEntriesChange:b})]})}var $h=`/api/codex-auth/features/default-mode-request-user-input`;function eg({apiBase:e}){let t=Q(),[n,r]=(0,_.useState)(!1),[i,a]=(0,_.useState)(!1),[o,s]=(0,_.useState)(!1),[c,l]=(0,_.useState)(!1),[u,d]=(0,_.useState)(null),f=(0,_.useRef)(!1),p=(0,_.useRef)(!1),m=(0,_.useRef)(0),h=(0,_.useCallback)(async()=>{if(f.current)return;let t=++m.current,n=Vn(15e3);try{let i=await fetch(`${e}${$h}`,{signal:n.signal});if(!i.ok)throw Error(`load`);let o=await i.json();if(f.current||t!==m.current)return;p.current=o.enabled===!0,r(p.current),a(!0),l(!1)}catch{!f.current&&t===m.current&&l(!0)}finally{n.clear()}},[e]);(0,_.useEffect)(()=>{let e=window.setTimeout(()=>{h()},0),t=Gn(()=>{h()},3e4);return()=>{window.clearTimeout(e),t()}},[h]);let g=(0,_.useCallback)(async()=>{if(f.current||!i||c)return;let n=!p.current,o=p.current;p.current=n,r(n),f.current=!0,s(!0),d(null),m.current++;try{let i=await fetch(`${e}${$h}`,{method:`PUT`,headers:{"content-type":`application/json`},body:JSON.stringify({enabled:n})}),o=await Pt(i)??{};if(o.ok!==!0)throw Error(String(i.status));p.current=o.enabled===!0,r(p.current),a(!0),d({tone:`ok`,message:t(o.changed===!0?`codexAuth.requestUserInputUpdatedRestart`:`codexAuth.requestUserInputUpdated`)})}catch(e){p.current=o,r(o);let n=e instanceof Error&&e.message&&!/^HTTP \d{3}$/.test(e.message)?e.message:t(`codexAuth.requestUserInputUpdateFailed`);d({tone:`err`,message:n})}finally{f.current=!1,s(!1)}},[e,i,c,t]),v=o||!i||c;return(0,J.jsxs)(`div`,{className:`card card-row codex-request-user-input-card`,style:{marginTop:16},"aria-busy":o||!i&&!c||void 0,children:[(0,J.jsxs)(`div`,{className:`codex-request-user-input-copy`,children:[(0,J.jsx)(`strong`,{children:t(`codexAuth.requestUserInput`)}),(0,J.jsx)(`div`,{className:`card-sub`,role:c?`alert`:void 0,children:t(c?`codexAuth.requestUserInputLoadFailed`:`codexAuth.requestUserInputDesc`)}),(0,J.jsx)(`code`,{className:`mono codex-request-user-input-config`,children:`[features] -default_mode_request_user_input = true`})]}),(0,J.jsxs)(`div`,{className:`codex-request-user-input-controls`,children:[c&&(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:()=>{h()},children:t(`common.retry`)}),(0,J.jsx)(`button`,{type:`button`,className:`toggle ${n?`on`:``}`,onClick:()=>{g()},disabled:v,"aria-pressed":n,"aria-label":t(`codexAuth.requestUserInput`),title:t(`codexAuth.requestUserInput`),children:(0,J.jsx)(`span`,{className:`toggle-knob`})})]}),u&&(0,J.jsx)(`div`,{className:`codex-request-user-input-feedback${u.tone===`err`?` is-error`:``}`,role:u.tone===`err`?`alert`:`status`,"aria-atomic":`true`,children:u.message})]})}function tg({apiBase:e}){let t=Q(),[n,r]=(0,_.useState)(!1),[i,a]=(0,_.useState)(!1),[o,s]=(0,_.useState)(!1),[c,l]=(0,_.useState)(!1),[u,d]=(0,_.useState)(null),f=(0,_.useRef)(!1),p=(0,_.useRef)(!1),m=(0,_.useRef)(0),h=(0,_.useCallback)(async()=>{if(p.current)return;let t=++m.current,n=Vn(15e3);try{let i=await fetch(`${e}/api/settings`,{signal:n.signal});if(!i.ok)throw Error(`load`);let o=await i.json();if(p.current||t!==m.current)return;if(typeof o.codexAccountPickerEnabled!=`boolean`)throw Error(`shape`);f.current=o.codexAccountPickerEnabled,r(o.codexAccountPickerEnabled),a(!0),l(!1)}catch{!p.current&&t===m.current&&l(!0)}finally{n.clear()}},[e]);(0,_.useEffect)(()=>{let e=window.setTimeout(()=>{h()},0),t=Gn(()=>{h()},3e4);return()=>{window.clearTimeout(e),t()}},[h]);let g=(0,_.useCallback)(async()=>{if(p.current||!i)return;let n=f.current,o=!n;f.current=o,r(o),p.current=!0,s(!0),d(null),m.current+=1;try{let n=await Pt(await fetch(`${e}/api/settings`,{method:`PUT`,headers:{"content-type":`application/json`},body:JSON.stringify({codexAccountPickerEnabled:o})}))??{};if(n.ok!==!0||typeof n.codexAccountPickerEnabled!=`boolean`)throw Error(`unconfirmed`);f.current=n.codexAccountPickerEnabled,r(n.codexAccountPickerEnabled),a(!0),l(!1),d(n.catalogRefreshPending===!0?{tone:`warn`,message:t(`codexAuth.catalogRefreshPending`)}:{tone:`ok`,message:t(`codexAuth.accountPickerUpdated`)})}catch{f.current=n,r(n),d({tone:`err`,message:t(`codexAuth.accountPickerUpdateFailed`)})}finally{p.current=!1,s(!1)}},[e,i,t]),v=c&&!i;return(0,J.jsxs)(`div`,{className:`card card-row codex-account-picker-card`,"aria-busy":o||!i&&!v||void 0,children:[(0,J.jsxs)(`div`,{className:`codex-account-picker-copy`,children:[(0,J.jsx)(`strong`,{children:t(`codexAuth.accountPickerTitle`)}),(0,J.jsx)(`div`,{className:`card-sub`,role:v?`status`:void 0,children:t(v?`codexAuth.accountPickerLoadFailed`:i?n?`codexAuth.accountPickerOnDesc`:`codexAuth.accountPickerOffDesc`:`common.loading`)}),i&&n&&(0,J.jsx)(`div`,{className:`card-sub faint`,children:t(`codexAuth.accountPickerCompatibility`)}),i&&c&&(0,J.jsx)(`div`,{className:`card-sub faint`,role:`status`,children:t(`codexAuth.accountPickerRefreshFailed`)})]}),(0,J.jsxs)(`div`,{className:`codex-account-picker-controls`,children:[c&&(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:()=>{h()},disabled:o,children:t(`common.retry`)}),i&&(0,J.jsx)(`button`,{type:`button`,className:`toggle ${n?`on`:``}`,onClick:()=>{g()},disabled:o,"aria-pressed":n,"aria-label":t(`codexAuth.accountPickerTitle`),title:t(`codexAuth.accountPickerTitle`),children:(0,J.jsx)(`span`,{className:`toggle-knob`})})]}),u&&(0,J.jsx)(`div`,{className:`codex-account-picker-feedback is-${u.tone}`,role:u.tone===`err`?`alert`:`status`,"aria-atomic":`true`,children:u.message})]})}function ng(e){if(!e||typeof e!=`object`)return`absent`;let t=e.providers;if(!t||typeof t!=`object`||Array.isArray(t)||!Object.hasOwn(t,`openai`))return`absent`;let n=t.openai;if(!n||typeof n!=`object`||Array.isArray(n))return`absent`;let r=n;return r.disabled===!0?`disabled`:r.codexAccountMode===`direct`?`direct`:r.codexAccountMode===void 0||r.codexAccountMode===`pool`?`pool`:`absent`}function rg({state:e,busy:t,onEnable:n}){let r=Q();return e===null?null:(0,J.jsxs)(`div`,{className:`panel openai-account-mode-banner`,style:{marginBottom:16},children:[(0,J.jsxs)(`div`,{className:`row`,children:[(0,J.jsx)(`strong`,{children:r(`codexAuth.accountModeTitle`)}),e===`pool`?(0,J.jsx)(`span`,{className:`badge badge-accent openai-account-mode-banner__badge-slot`,children:r(`codexAuth.accountModePool`)}):e===`direct`?(0,J.jsx)(`span`,{className:`badge badge-green openai-account-mode-banner__badge-slot`,children:r(`codexAuth.accountModeDirect`)}):null]}),e===`pool`&&(0,J.jsx)(`p`,{className:`card-sub openai-account-mode-banner__desc`,children:r(`codexAuth.accountModePoolDesc`)}),e===`direct`&&(0,J.jsxs)(`p`,{className:`card-sub openai-account-mode-banner__desc`,children:[r(`codexAuth.accountModeDirectDesc`),` `,(0,J.jsx)(`button`,{type:`button`,className:`link-btn`,onClick:()=>pt(`providers`),children:r(`codexAuth.openProviders`)})]}),(e===`absent`||e===`disabled`)&&(0,J.jsxs)(`div`,{className:`row`,style:{alignItems:`center`,marginTop:8},children:[(0,J.jsx)(`p`,{className:`card-sub`,style:{flex:1,margin:0},children:r(`codexAuth.openaiUnavailableDesc`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-primary btn-sm`,disabled:t,onClick:n,children:r(t?`codexAuth.enablingOpenai`:`codexAuth.enableOpenai`)})]}),e===`invalid`&&(0,J.jsxs)(`p`,{className:`card-sub openai-account-mode-banner__desc`,children:[r(`codexAuth.openaiMissing`),` `,(0,J.jsx)(`button`,{type:`button`,className:`link-btn`,onClick:()=>pt(`providers`),children:r(`codexAuth.openProviders`)})]})]})}function ig(e){if(!e||typeof e!=`object`)return;let t=e.providers;if(!t||typeof t!=`object`||Array.isArray(t)||!Object.hasOwn(t,`openai`))return;let n=t.openai;if(!(!n||typeof n!=`object`||Array.isArray(n)))return n}function ag({apiBase:e}){let t=Q(),n=`ocx.codex-auth.config.v1:${e}`,r=gr(n),[i,a]=(0,_.useState)(()=>r?.bannerState??null),[o,s]=(0,_.useState)(()=>r?.accountModeState??null),c=(0,_.useRef)(null),[l,u]=(0,_.useState)(!1),[d,f]=(0,_.useState)(``),p=(0,_.useCallback)(async()=>{let t=Vn(15e3);try{let r=await fetch(`${e}/api/config`,{signal:t.signal});if(!r.ok)throw Error(String(r.status));let i=await r.json(),o=Gs(ig(i));if(o===`absent`||o===`disabled`||o===`invalid`){a(o);let e=o===`disabled`?`disabled`:`absent`;s(e),br(n,{bannerState:o,accountModeState:e});return}let c=ng(i);a(c),s(c),br(n,{bannerState:c,accountModeState:c})}catch{}finally{t.clear()}},[e,n]);(0,_.useEffect)(()=>{c.current!==e&&(c.current=e,Promise.resolve().then(()=>{p()}));let t=Gn(()=>{p()},3e4);return()=>{t()}},[e,p]);let m=async()=>{u(!0),f(``);try{if(i!==`absent`&&i!==`disabled`)return;await Qs(e,i),await p()}catch(e){e instanceof Zs?f(t(e.i18nKey)):f(e instanceof Error?e.message:t(`prov.saveFailed`))}finally{u(!1)}};return(0,J.jsx)(J.Fragment,{children:(0,J.jsx)(Cs,{apiBase:e,accountModeState:o,banner:(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)(rg,{state:i,busy:l,onEnable:()=>{m()}}),d&&(0,J.jsx)(`div`,{className:`notice notice-err`,role:`alert`,children:d})]}),advancedExtras:(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)(tg,{apiBase:e}),(0,J.jsx)(eg,{apiBase:e})]})})})}var og={"base-instructions":`codexSet.layer.base-instructions`,"model-switch":`codexSet.layer.model-switch`,personality:`codexSet.layer.personality`,"context-window-guidance":`codexSet.layer.context-window-guidance`,realtime:`codexSet.layer.realtime`,"agents-md":`codexSet.layer.agents-md`,permissions:`codexSet.layer.permissions`,collaboration:`codexSet.layer.collaboration`,environment:`codexSet.layer.environment`,"environments-instructions":`codexSet.layer.environments-instructions`,apps:`codexSet.layer.apps`,plugins:`codexSet.layer.plugins`,tools:`codexSet.layer.tools`,skills:`codexSet.layer.skills`,"multi-agent-mode":`codexSet.layer.multi-agent-mode`,"git-attribution":`codexSet.layer.git-attribution`},sg={"base-instructions":`codexSet.about.base-instructions`,"model-switch":`codexSet.about.model-switch`,personality:`codexSet.about.personality`,"context-window-guidance":`codexSet.about.context-window-guidance`,realtime:`codexSet.about.realtime`,"agents-md":`codexSet.about.agents-md`,permissions:`codexSet.about.permissions`,collaboration:`codexSet.about.collaboration`,environment:`codexSet.about.environment`,"environments-instructions":`codexSet.about.environments-instructions`,apps:`codexSet.about.apps`,plugins:`codexSet.about.plugins`,tools:`codexSet.about.tools`,skills:`codexSet.about.skills`,"multi-agent-mode":`codexSet.about.multi-agent-mode`,"git-attribution":`codexSet.about.git-attribution`},cg={"model-switch":`codexSet.condition.model-switch`,realtime:`codexSet.condition.realtime`,"agents-md":`codexSet.condition.agents-md`,plugins:`codexSet.condition.plugins`,"git-attribution":`codexSet.condition.git-attribution`},lg={base:`codexSet.class.base`,"config-toggle":`codexSet.class.config-toggle`,"feature-gated":`codexSet.class.feature-gated`,"runtime-conditional":`codexSet.class.runtime-conditional`,"extension-unknown":`codexSet.class.extension-unknown`};function ug({descriptor:e,toggle:t,bytes:n,transitionOnly:r=!1,busy:i,writesRefused:a,onToggle:o,onSelectBase:s,baseSelection:c,onOpen:l}){let u=Q(),d=og[e.id],f=d?u(d):e.id,p=cg[e.id],m=t?.defaultedUserValue??e.default??!0,h=c?.kind==="default";return(0,J.jsxs)(`li`,{className:`codex-set-prompt__row`,"data-layer-id":e.id,"data-layer-class":e.class,children:[(0,J.jsx)(`span`,{className:`codex-set-prompt__pos`,"aria-hidden":`true`,children:e.order===null?`·`:e.order+1}),(0,J.jsx)(`button`,{type:`button`,className:`link-btn codex-set-prompt__name`,onClick:()=>l(e.id),children:f}),e.key&&(0,J.jsx)(`code`,{className:`codex-set-prompt__key`,children:e.key}),n!==null&&n>0&&(0,J.jsx)(`span`,{className:`codex-set-prompt__bytes`,title:u(`codexSet.dialog.sourceBytes`,{bytes:n}),children:n>=1024?Math.round(n/1024)+` KB`:n+` B`}),e.class===`base`&&s?(0,J.jsx)(`button`,{type:`button`,role:`switch`,className:`toggle ${h?`on`:``}`,"aria-checked":h,"aria-label":f,disabled:i||a||c?.kind===`external`,onClick:()=>{s(!h)},children:(0,J.jsx)(`span`,{className:`toggle-knob`})}):e.class===`config-toggle`?(0,J.jsx)(`button`,{type:`button`,role:`switch`,className:`toggle ${m?`on`:``}`,"aria-checked":m,"aria-label":f,disabled:i||a,onClick:()=>{o(e.id,!m)},children:(0,J.jsx)(`span`,{className:`toggle-knob`})}):e.class===`feature-gated`?(0,J.jsxs)(`span`,{className:`codex-set-prompt__note`,children:[u(`codexSet.row.featureGated`),` `,(0,J.jsx)(`button`,{type:`button`,className:`link-btn`,onClick:()=>pt(`integrations/codex`),children:u(`codexSet.row.openFeatures`)})]}):(0,J.jsx)(`span`,{className:`codex-set-prompt__note codex-set-prompt__note--locked`,children:u(r?`codexSet.row.onChange`:p||`codexSet.row.alwaysOn`)})]})}function dg({descriptor:e,toggle:t,text:n,busy:r,onToggle:i,onClose:a}){let o=Q(),s=(0,_.useRef)(null),c=`codex-set-layer-dialog-`+e.id;(0,_.useEffect)(()=>{let e=s.current,t=document.activeElement;return e&&!e.open&&e.showModal(),()=>{e?.open&&e.close(),t&&typeof t.focus==`function`&&t.focus()}},[]);let l=(0,_.useCallback)(e=>{e.preventDefault(),a()},[a]),u=lg[e.class],d=og[e.id],f=sg[e.id],p=cg[e.id];return(0,J.jsxs)(`dialog`,{ref:s,className:`modal-overlay`,"aria-labelledby":c,onCancel:l,children:[(0,J.jsx)(`button`,{type:`button`,className:`modal-backdrop-dismiss`,"aria-label":o(`common.close`),tabIndex:-1,onClick:a}),(0,J.jsxs)(`div`,{className:`modal-card codex-set-layer-dialog`,onClick:e=>e.stopPropagation(),role:`document`,children:[(0,J.jsxs)(`div`,{className:`modal-head`,children:[(0,J.jsx)(`h3`,{id:c,children:d?o(d):e.id}),e.class===`config-toggle`&&t&&(0,J.jsx)(`button`,{type:`button`,role:`switch`,className:`toggle ${t.defaultedUserValue?`on`:``}`,"aria-checked":t.defaultedUserValue,"aria-label":d?o(d):e.id,disabled:r,onClick:()=>{i(e.id,!t.defaultedUserValue)},children:(0,J.jsx)(`span`,{className:`toggle-knob`})}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:a,children:o(`common.close`)})]}),(0,J.jsx)(`p`,{className:`muted small`,children:o(f||`codexSet.dialog.unknownLayer`)}),(0,J.jsxs)(`div`,{className:`codex-set-layer-dialog__line`,children:[(0,J.jsx)(`span`,{className:`muted text-label`,children:o(`codexSet.dialog.class`)}),(0,J.jsx)(`span`,{children:o(u)})]}),e.key&&(0,J.jsxs)(`div`,{className:`codex-set-layer-dialog__line`,children:[(0,J.jsx)(`span`,{className:`muted text-label`,children:o(`codexSet.dialog.key`)}),(0,J.jsx)(`code`,{className:`api-code`,children:e.key}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:()=>{navigator.clipboard?.writeText(e.key??``)},children:o(`codexSet.dialog.copyKey`)})]}),e.class===`config-toggle`&&t&&(0,J.jsxs)(`div`,{className:`codex-set-layer-dialog__line`,children:[(0,J.jsx)(`span`,{className:`muted text-label`,children:o(`codexSet.dialog.fileValue`)}),(0,J.jsx)(`span`,{children:t.userFileValue===null?o(`codexSet.dialog.absentDefault`,{value:String(t.default)}):o(`codexSet.dialog.setValue`,{value:String(t.userFileValue),fallback:String(t.default)})})]}),e.class===`runtime-conditional`&&p&&(0,J.jsx)(`p`,{className:`muted small`,children:o(p)}),n?.reason===`ok`&&n.text?(0,J.jsxs)(J.Fragment,{children:[(0,J.jsxs)(`div`,{className:`codex-set-layer-dialog__line`,children:[(0,J.jsx)(`span`,{className:`muted text-label`,children:o(`codexSet.dialog.sourceText`)}),(0,J.jsx)(`span`,{className:`muted small`,children:o(`codexSet.dialog.sourceBytes`,{bytes:n.bytes})})]}),(0,J.jsx)(`pre`,{className:`api-code codex-set-layer-dialog__text`,children:n.text})]}):(0,J.jsx)(`p`,{className:`muted small codex-set-layer-dialog__no-text`,children:n?.reason===`empty-source`?o(`codexSet.dialog.emptySource`,{path:n.sourcePath??``}):n?.reason===`not-rendered`?o(`codexSet.dialog.notRendered`):n?.reason===`not-exposed`?o(`codexSet.dialog.notExposed`):o(`codexSet.dialog.textUnavailable`)})]})]})}function fg({layer:e,index:t,total:n,busy:r,onToggle:i,onEdit:a,onDelete:o,onMove:s}){let c=Q();return(0,J.jsxs)(`li`,{className:`codex-set-prompt__row codex-set-custom__row`,"data-custom-id":e.id,onKeyDown:i=>{!i.altKey||r||(i.key===`ArrowUp`&&t>0?(i.preventDefault(),s(e.id,-1)):i.key===`ArrowDown`&&ta(e.id),children:e.title}),(0,J.jsxs)(`span`,{className:`codex-set-custom__reorder`,children:[(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,"aria-label":c(`codexSet.custom.moveUp`,{title:e.title}),disabled:t===0||r,onClick:()=>s(e.id,-1),children:`↑`}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,"aria-label":c(`codexSet.custom.moveDown`,{title:e.title}),disabled:t===n-1||r,onClick:()=>s(e.id,1),children:`↓`})]}),(0,J.jsx)(`button`,{type:`button`,role:`switch`,className:`toggle ${e.enabled?`on`:``}`,"aria-checked":e.enabled,"aria-label":e.title,disabled:r,onClick:()=>i(e.id,!e.enabled),children:(0,J.jsx)(`span`,{className:`toggle-knob`})}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm codex-set-custom__delete`,"aria-label":c(`codexSet.custom.delete`,{title:e.title}),disabled:r,onClick:()=>o(e.id),children:`×`})]})}var pg=8192,mg=[{rule:`identity`,level:`warn`,messageKey:`codexSet.lint.identity`,pattern:/you\s+are\s+(claude|grok|gemini|gpt-|chatgpt)/gi},{rule:`foreign-tool`,level:`warn`,messageKey:`codexSet.lint.foreignTool`,pattern:/\b(Read|Edit|Write|Bash|Glob|Grep)\s+tool\b/g},{rule:`placeholder`,level:`warn`,messageKey:`codexSet.lint.placeholder`,pattern:/\$\{\{[\s\S]*?\}\}/g},{rule:`apply-patch`,level:`warn`,messageKey:`codexSet.lint.applyPatch`,pattern:/apply_patch\s+(?:is|must|should|means|works)/gi},{rule:`approval-vocab`,level:`warn`,messageKey:`codexSet.lint.approvalVocab`,pattern:/\b(always-approve|ask mode|acceptEdits)\b/gi},{rule:`environment`,level:`warn`,messageKey:`codexSet.lint.environment`,pattern:/\b(your (?:cwd|working directory) is|today's date is|you have no network access|you are running on (?:macos|linux|windows))/gi}];function hg(e){let t=0;for(let n of e){let e=n.codePointAt(0);t+=e<128?1:e<2048?2:e<65536?3:4}return t}function gg(e){let t=[];for(let n of mg){let r=new RegExp(n.pattern.source,n.pattern.flags);for(let i=r.exec(e);i!==null;i=r.exec(e))t.push({level:n.level,rule:n.rule,messageKey:n.messageKey,span:[i.index,i.index+i[0].length]}),i[0].length===0&&(r.lastIndex+=1)}return hg(e)>pg&&t.push({level:`info`,rule:`size`,messageKey:`codexSet.lint.size`}),t.sort((e,t)=>(e.span?.[0]??1/0)-(t.span?.[0]??1/0))}var _g=65536;function vg(e){let t=0;for(let n of e){let e=n.codePointAt(0);t+=e<128?1:e<2048?2:e<65536?3:4}return t}function yg(e){return e.replace(/\r\n/g,` -`).replace(/\r/g,` -`).replace(/\t/g,` `)}function bg(e){let t=0;for(let n of e){let e=n.codePointAt(0);if(n!==` -`&&(e<32||e===127)||e>=55296&&e<=57343)return{position:t};t+=1}return null}function xg(e,t){let n=e.title;if(n.trim().length===0)return{kind:`title-empty`};if(n.length>80)return{kind:`title-too-long`,length:n.length};if(/[\r\n]/.test(n))return{kind:`title-multiline`};let r=yg(e.body),i=vg(r);if(i>65536)return{kind:`body-too-large`,bytes:i};let a=bg(r);if(a)return{kind:`invalid-character`,position:a.position};let o=vg([...t.filter(t=>t.enabled&&t.id!==e.id).map(e=>e.body),...e.enabled?[r]:[]].join(` - -`));return o>131072?{kind:`composed-too-large`,bytes:o}:null}function Sg(e){let t=new Set(e.map(e=>e.id));for(;;){let e=``;for(let t=0;t<6;t+=1)e+=`abcdefghijklmnopqrstuvwxyz0123456789`[Math.floor(Math.random()*36)];if(!t.has(e))return e}}function Cg(e,t,n){let r=[...e],i=r.findIndex(e=>e.id===t);if(i===-1)return r;let a=i+n;if(a<0||a>=r.length)return r;let[o]=r.splice(i,1);return r.splice(a,0,o),r}function wg({layer:e,seed:t,others:n,busy:r,navigation:i,onSave:a,onClose:o}){let s=Q(),c=(0,_.useRef)(null),[l,u]=(0,_.useState)(e?.title??t?.title??``),[d,f]=(0,_.useState)(e?.body??t?.body??``),p=(0,_.useRef)(new Map),m=e?.id??null,h=(0,_.useRef)(m),g=(0,_.useRef)({title:l,body:d});(0,_.useEffect)(()=>{g.current={title:l,body:d}},[l,d]),(0,_.useEffect)(()=>{if(h.current===m)return;h.current!==null&&p.current.set(h.current,g.current),h.current=m;let t=m===null?void 0:p.current.get(m);u(t?.title??e?.title??``),f(t?.body??e?.body??``)},[m,e]);let[v,y]=(0,_.useState)(!1),b=`codex-set-custom-dialog`,x=e?.title??t?.title??``,S=e?.body??t?.body??``,C=l!==x||d!==S;(0,_.useEffect)(()=>{let e=c.current,t=document.activeElement;return e&&!e.open&&e.showModal(),()=>{e?.open&&e.close(),t&&typeof t.focus==`function`&&t.focus()}},[]);let w=(0,_.useCallback)(()=>{if(C){y(!0);return}o()},[C,o]),T=(0,_.useCallback)(e=>{e.preventDefault(),w()},[w]),E={id:e?.id??null,title:l,body:d,enabled:e?.enabled??!0},D=xg(E,n),O=yg(d),k=O!==d,A=(0,_.useMemo)(()=>gg(O),[O]),j=vg(O),M=D?D.kind===`title-empty`?s(`codexSet.custom.titleRequired`):D.kind===`title-too-long`?s(`codexSet.custom.titleTooLong`,{count:D.length,max:80}):D.kind===`title-multiline`?s(`codexSet.custom.titleMultiline`):D.kind===`body-too-large`?s(`codexSet.custom.bodyTooLarge`,{bytes:D.bytes,max:_g}):D.kind===`composed-too-large`?s(`codexSet.custom.composedTooLarge`,{bytes:D.bytes}):s(`codexSet.custom.invalidCharacter`,{position:D.position}):null;return(0,J.jsxs)(`dialog`,{ref:c,className:`modal-overlay`,"aria-labelledby":b,onCancel:T,children:[(0,J.jsx)(`button`,{type:`button`,className:`modal-backdrop-dismiss`,"aria-label":s(`common.close`),tabIndex:-1,onClick:w}),(0,J.jsxs)(`div`,{className:`modal-card codex-set-custom-dialog`,onClick:e=>e.stopPropagation(),role:`document`,children:[(0,J.jsxs)(`div`,{className:`modal-head`,children:[(0,J.jsx)(`h3`,{id:b,children:s(e?`codexSet.custom.editTitle`:`codexSet.custom.newTitle`)}),i&&(0,J.jsxs)(`span`,{className:`codex-set-custom-dialog__nav`,children:[(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,"aria-label":s(`codexSet.custom.prevLayer`),disabled:i.position<=1||r,onClick:i.onPrev,children:`←`}),(0,J.jsx)(`span`,{className:`codex-set-custom-dialog__nav-pos`,children:s(`codexSet.custom.navPosition`,{position:i.position,total:i.total})}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,"aria-label":s(`codexSet.custom.nextLayer`),disabled:i.position>=i.total||r,onClick:i.onNext,children:`→`})]})]}),(0,J.jsxs)(`label`,{className:`field`,children:[(0,J.jsx)(`span`,{className:`muted text-label`,children:s(`codexSet.custom.titleLabel`)}),(0,J.jsx)(`input`,{type:`text`,value:l,maxLength:100,onChange:e=>u(e.target.value)})]}),(0,J.jsxs)(`label`,{className:`field`,children:[(0,J.jsx)(`span`,{className:`muted text-label`,children:s(`codexSet.custom.bodyLabel`)}),(0,J.jsx)(`textarea`,{rows:10,value:d,onChange:e=>f(e.target.value)})]}),(0,J.jsx)(`p`,{className:`muted small`,children:s(`codexSet.custom.bodySize`,{bytes:j,max:_g})}),k&&(0,J.jsx)(`p`,{className:`muted small codex-set-custom-dialog__normalized`,children:s(`codexSet.custom.normalized`)}),M&&(0,J.jsx)(`div`,{className:`notice notice-err`,role:`alert`,children:M}),A.length>0&&(0,J.jsx)(`ul`,{className:`codex-set-custom-dialog__lint`,children:A.map((e,t)=>(0,J.jsxs)(`li`,{"data-lint-rule":e.rule,"data-lint-level":e.level,children:[s(e.messageKey),e.span&&(0,J.jsx)(`code`,{className:`codex-set-custom-dialog__span`,children:O.slice(e.span[0],e.span[1])})]},e.rule+`:`+t))}),v?(0,J.jsxs)(`div`,{className:`modal-actions codex-set-custom-dialog__discard`,role:`alertdialog`,"aria-labelledby":`codex-set-custom-dialog-discard`,children:[(0,J.jsx)(`span`,{id:`codex-set-custom-dialog-discard`,className:`muted small`,children:s(`codexSet.custom.discardPrompt`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-sm`,onClick:()=>y(!1),children:s(`codexSet.custom.keepEditing`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-danger btn-sm`,onClick:o,children:s(`common.discard`)})]}):(0,J.jsxs)(`div`,{className:`modal-actions`,children:[(0,J.jsx)(`button`,{type:`button`,className:`btn btn-primary btn-sm`,disabled:D!==null||r,onClick:()=>a({...E,body:O}),children:s(`common.save`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-sm`,onClick:w,children:s(`common.cancel`)})]})]})]})}var Tg=Object.freeze([{id:`concise`,nameKey:`codexSet.preset.concise.name`,descriptionKey:`codexSet.preset.concise.description`,provenanceKey:`codexSet.preset.concise.provenance`,body:[`Answer directly. Skip preamble, restatement of the question, and summaries of the work about to be done.`,`Prefer a short paragraph over a list, and a list over a table, unless the structure carries real meaning.`,`When the answer is a single fact, give the fact and stop.`].join(` -`)},{id:`plan-first`,nameKey:`codexSet.preset.planFirst.name`,descriptionKey:`codexSet.preset.planFirst.description`,provenanceKey:`codexSet.preset.planFirst.provenance`,body:[`Before changing anything non-trivial, state the plan in two or three sentences: what will change, where, and how it will be verified.`,`If the plan turns out to be wrong mid-way, say so and revise it rather than continuing quietly.`].join(` -`)},{id:`explain-why`,nameKey:`codexSet.preset.explainWhy.name`,descriptionKey:`codexSet.preset.explainWhy.description`,provenanceKey:`codexSet.preset.explainWhy.provenance`,body:[`When a choice had alternatives, name the alternative and why it lost.`,`Explain reasoning where it changes what the reader should do, not as a narration of every step.`,`State uncertainty plainly instead of presenting a guess as a conclusion.`].join(` -`)},{id:`test-first`,nameKey:`codexSet.preset.testFirst.name`,descriptionKey:`codexSet.preset.testFirst.description`,provenanceKey:`codexSet.preset.testFirst.provenance`,body:[`For a behavior change, write the failing test first and show that it fails for the expected reason.`,`A test that passes before the change is not evidence; say so rather than counting it.`].join(` -`)},{id:`korean`,nameKey:`codexSet.preset.korean.name`,descriptionKey:`codexSet.preset.korean.description`,provenanceKey:`codexSet.preset.korean.provenance`,body:[`Reply in Korean regardless of the language of the request, unless explicitly asked for another language.`,`Keep code, identifiers, file paths, and command output unchanged.`,`Write plain Korean: no translationese, one consistent register throughout.`].join(` -`)}]);function Eg({onBlank:e,onPreset:t,disabled:n,presets:r=Tg}){let i=Q(),[a,o]=(0,_.useState)(!1),s=(0,_.useRef)(null),c=a&&!n;(0,_.useEffect)(()=>{if(!c)return;let e=e=>{s.current?.contains(e.target)||o(!1)},t=e=>{e.key===`Escape`&&o(!1)};return document.addEventListener(`mousedown`,e),document.addEventListener(`keydown`,t),()=>{document.removeEventListener(`mousedown`,e),document.removeEventListener(`keydown`,t)}},[c]);let l=e=>{o(!1),e()};return r.length===0?(0,J.jsx)(`button`,{type:`button`,className:`btn btn-sm codex-set-custom__add`,disabled:n,onClick:e,children:i(`codexSet.custom.add`)}):(0,J.jsxs)(`div`,{className:`codex-set-preset`,ref:s,children:[(0,J.jsx)(`button`,{type:`button`,className:`btn btn-sm codex-set-custom__add`,"aria-expanded":c,disabled:n,onClick:()=>o(e=>!e),children:i(`codexSet.custom.add`)}),c&&(0,J.jsxs)(`div`,{className:`codex-set-preset__menu`,children:[(0,J.jsx)(`button`,{type:`button`,className:`codex-set-preset__item`,onClick:()=>l(e),children:(0,J.jsx)(`span`,{className:`codex-set-preset__name`,children:i(`codexSet.preset.blank`)})}),r.map(e=>(0,J.jsxs)(`button`,{type:`button`,className:`codex-set-preset__item`,"data-preset-id":e.id,onClick:()=>l(()=>t(e.body,i(e.nameKey))),children:[(0,J.jsx)(`span`,{className:`codex-set-preset__name`,children:i(e.nameKey)}),(0,J.jsx)(`span`,{className:`codex-set-preset__desc`,children:i(e.descriptionKey)}),(0,J.jsx)(`span`,{className:`codex-set-preset__provenance`,children:i(e.provenanceKey)}),(0,J.jsx)(`span`,{className:`codex-set-preset__preview`,children:e.body})]},e.id))]})]})}var Dg=48;function Og({variants:e,selection:t,maxVariants:n,busy:r,onSelect:i,onSave:a,onDelete:o,onClose:s}){let c=Q(),l=(0,_.useRef)(null),u=[{kind:`default`},...e.map(e=>({kind:`variant`,variant:e})),...e.lengthe.variant?.id===t.id)):0,[f,p]=(0,_.useState)(d),m=u[Math.min(f,u.length-1)],[h,g]=(0,_.useState)(m.variant?.title??``),[v,y]=(0,_.useState)(m.variant?.body??``),[b,x]=(0,_.useState)(m.variant?.id??null);(m.variant?.id??null)!==b&&(x(m.variant?.id??null),g(m.variant?.title??``),y(m.variant?.body??``));let S=(0,_.useCallback)(e=>{p(t=>{let n=t+e;return n<0?u.length-1:n>=u.length?0:n})},[u.length]);(0,_.useEffect)(()=>{let e=l.current;e&&!e.open&&e.showModal()},[]);let C=(0,_.useRef)(null),w=e=>{C.current={x:e.clientX,y:e.clientY}},T=e=>{let t=C.current;if(C.current=null,!t||r)return;let n=e.clientX-t.x,i=e.clientY-t.y;Math.abs(n){if(r)return;let t=e.target.tagName;t!==`TEXTAREA`&&t!==`INPUT`&&(e.key===`ArrowLeft`&&(e.preventDefault(),S(-1)),e.key===`ArrowRight`&&(e.preventDefault(),S(1)))},D=m.kind==="default"?t.kind==="default":m.kind===`variant`&&t.kind===`variant`&&t.id===m.variant.id,O=t.kind===`external`;return(0,J.jsx)(`dialog`,{ref:l,className:`modal-overlay codex-set-base-dialog`,"aria-label":c(`codexSet.base.title`),onClose:s,onKeyDown:E,onPointerDown:w,onPointerUp:T,children:(0,J.jsxs)(`div`,{className:`modal-card`,children:[(0,J.jsxs)(`div`,{className:`row`,children:[(0,J.jsx)(`strong`,{children:c(`codexSet.base.title`)}),(0,J.jsxs)(`span`,{className:`codex-set-base-dialog__nav`,children:[(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,"aria-label":c(`codexSet.base.prev`),disabled:r||u.length<2,onClick:()=>S(-1),children:`←`}),(0,J.jsx)(`span`,{className:`codex-set-base-dialog__pos`,"data-slot-kind":m.kind,children:c(`codexSet.base.position`,{position:f+1,total:u.length})}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,"aria-label":c(`codexSet.base.next`),disabled:r||u.length<2,onClick:()=>S(1),children:`→`})]})]}),(0,J.jsx)(`p`,{className:`card-sub`,children:c(`codexSet.base.swipeHint`)}),u.length>1&&(0,J.jsx)(`div`,{className:`codex-set-base-dialog__dots`,"aria-hidden":`true`,children:u.map((e,t)=>(0,J.jsx)(`span`,{className:`codex-set-base-dialog__dot${t===f?` active`:``}`},t))}),O&&(0,J.jsx)(`div`,{className:`notice notice-err`,role:`alert`,children:c(`codexSet.base.externalBlocked`,{path:t.path})}),m.kind==="default"?(0,J.jsxs)(`div`,{className:`codex-set-base-dialog__default`,children:[(0,J.jsx)(`strong`,{children:c(`codexSet.base.defaultTitle`)}),(0,J.jsx)(`p`,{className:`muted small`,children:c(`codexSet.base.defaultBody`)})]}):(0,J.jsxs)(J.Fragment,{children:[(0,J.jsxs)(`label`,{className:`field`,children:[(0,J.jsx)(`span`,{children:c(`codexSet.base.variantTitle`)}),(0,J.jsx)(`input`,{type:`text`,value:h,disabled:r||O,onChange:e=>g(e.target.value)})]}),(0,J.jsxs)(`label`,{className:`field`,children:[(0,J.jsx)(`span`,{children:c(`codexSet.base.variantBody`)}),(0,J.jsx)(`textarea`,{rows:12,value:v,disabled:r||O,onChange:e=>y(e.target.value)})]}),(0,J.jsx)(`p`,{className:`muted small`,children:c(`codexSet.base.replacesWarning`)})]}),(0,J.jsxs)(`div`,{className:`modal-actions`,children:[m.kind!=="default"&&(0,J.jsx)(`button`,{type:`button`,className:`btn btn-primary btn-sm`,disabled:r||O||v.trim().length===0,onClick:()=>a({id:m.variant?.id??null,title:h,body:v}),children:c(`common.save`)}),!D&&m.kind!==`new`&&(0,J.jsx)(`button`,{type:`button`,className:`btn btn-sm`,disabled:r||O,onClick:()=>i(m.kind==="default"?{kind:`default`}:{kind:`variant`,id:m.variant.id}),children:c(`codexSet.base.use`)}),D&&(0,J.jsx)(`span`,{className:`pill`,children:c(`codexSet.base.inUse`)}),m.kind===`variant`&&(0,J.jsx)(`button`,{type:`button`,className:`btn btn-danger btn-sm`,disabled:r||O,onClick:()=>o(m.variant.id),children:c(`common.delete`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-sm`,onClick:s,children:c(`common.close`)})]})]})})}function kg(e){return`codex-prompt:`+e}var Ag={"journal-present":`codexSet.drift.journalPresent`,"projection-stale":`codexSet.drift.projectionStale`,"store-missing":`codexSet.drift.storeMissing`,"owned-malformed":`codexSet.drift.ownedMalformed`};function jg({apiBase:e}){let t=Q(),n=kg(e),[r,i]=(0,_.useState)(``),[a,o]=(0,_.useState)(null),[s,c]=(0,_.useState)(null),[l,u]=(0,_.useState)(null),[d,f]=(0,_.useState)(null),[p,m]=(0,_.useState)(null),[h,g]=(0,_.useState)(null),[v,y]=(0,_.useState)(!1),[b,x]=(0,_.useState)(null),[S,C]=(0,_.useState)(null),w=(0,_.useCallback)(()=>{C(null)},[]),T=(0,_.useCallback)(async t=>{let n=await fetch(e+`/api/codex-prompt`,{signal:t});if(!n.ok)throw Error(String(n.status));return await n.json()},[e]),E=ml(n,[e],T,{isEmpty:e=>e.inventory.length===0}),D=E.data,O=E.state,k=async(r,a)=>{if(D){o(r),i(``);try{let o=await fetch(e+`/api/codex-prompt/toggle`,{method:`PUT`,headers:{"content-type":`application/json`},body:JSON.stringify({id:r,enabled:a,revision:D.revision})}),s=await o.json();if(!o.ok||!s.ok||!s.snapshot){if(s.code===`stale_revision`){E.refresh(),i(t(`codexSet.prompt.staleRevision`));return}i(s.message??t(`codexSet.prompt.writeFailed`)),E.refresh();return}K(n,s.snapshot),w(),C(null)}catch{i(t(`codexSet.prompt.writeFailed`)),E.refresh()}finally{o(null)}}},A=async(r,s)=>{if(!(!D||a!==null)){o(`base`),i(``);try{let a=await fetch(e+r,{method:`PUT`,headers:{"content-type":`application/json`},body:JSON.stringify({...s,revision:D.revision})}),o=await a.json();if(!a.ok||!o.ok||!o.snapshot){if(o.code===`stale_revision`){E.refresh(),i(t(`codexSet.prompt.staleRevision`));return}i(o.message??t(`codexSet.prompt.writeFailed`)),E.refresh();return}K(n,o.snapshot),w(),C(null)}catch{i(t(`codexSet.prompt.writeFailed`)),E.refresh()}finally{o(null)}}},j=async(r,s)=>{if(!D||a!==null)return!1;o(s),i(``);let c=D.custom;try{let a=await fetch(e+`/api/codex-prompt/custom`,{method:`PUT`,headers:{"content-type":`application/json`},body:JSON.stringify({layers:r,revision:D.revision})}),o=await a.json();return!a.ok||!o.ok||!o.snapshot?o.code===`stale_revision`?(E.refresh(),i(t(`codexSet.prompt.staleRevision`)),!1):(i(o.message??t(`codexSet.prompt.writeFailed`)),K(n,{...D,custom:c}),E.refresh(),!1):(K(n,o.snapshot),w(),!0)}catch{return i(t(`codexSet.prompt.writeFailed`)),E.refresh(),!1}finally{o(null)}},M=async e=>{if(!D)return;let t=D.custom,n=e.id===null?[...t,{id:Sg(t),title:e.title,body:e.body,enabled:!0}]:t.map(t=>t.id===e.id?{...t,title:e.title,body:e.body}:t);await j(n,e.id??`new`)&&u(null)},N=async r=>{if(D){o(`adopt`),i(``);try{let a=await fetch(e+`/api/codex-prompt/adopt`,{method:`POST`,headers:{"content-type":`application/json`},body:JSON.stringify(r?{confirm:!0,revision:D.revision}:{confirm:!1})}),o=await a.json();if(!a.ok||!o.ok){i(o.message??t(`codexSet.custom.adoptRefused`)),m(null),g(o.code===`adopt_unsupported_form`?{path:o.path,line:o.line,rawLine:o.rawLine}:null);return}if(o.snapshot){K(n,o.snapshot),w(),m(null);return}m(o.preview??null)}catch{i(t(`codexSet.prompt.writeFailed`))}finally{o(null)}}},P=async r=>{if(!(!D||D.drift===null)){y(!0),i(``);try{let a=await fetch(e+`/api/codex-prompt/repair`,{method:`POST`,headers:{"content-type":`application/json`},body:JSON.stringify(r?{confirm:!0,revision:D.revision}:{confirm:!1})}),o=await a.json();if(!a.ok||!o.ok){i(o.message??t(`codexSet.prompt.repairFailed`));return}o.snapshot?K(n,o.snapshot):E.refresh(),w()}catch{i(t(`codexSet.prompt.repairFailed`))}finally{y(!1)}}},F=[...D?.inventory??[]].filter(e=>e.class!==`extension-unknown`).sort((e,t)=>(e.order??2**53-1)-(t.order??2**53-1)),I=new Set([`realtime`,`model-switch`]),L=F.filter(e=>!I.has(e.id)),R=F.filter(e=>I.has(e.id)),z=F.find(e=>e.id===s)??null,B=l===null||l===`new`?-1:D?.custom.findIndex(e=>e.id===l)??-1,V=B>=0?D.custom[B]:null,H=l!==null&&l!==`new`&&D!==void 0&&B<0;return(0,_.useEffect)(()=>{if(S!==null)return;let t=new AbortController,n=!1;return(async()=>{try{let r=await fetch(e+`/api/codex-prompt/text`,{signal:t.signal});if(!r.ok){n||C({ok:!1});return}let i=await r.json();n||C(i)}catch{n||C({ok:!1})}})(),()=>{n=!0,t.abort()}},[S,e]),(0,J.jsxs)(`div`,{className:`panel codex-set-prompt`,children:[(0,J.jsx)(`div`,{className:`row`,children:(0,J.jsx)(`strong`,{children:t(`codexSet.prompt.title`)})}),(0,J.jsx)(`p`,{className:`card-sub`,children:t(`codexSet.prompt.timing`)}),O.refreshing&&(0,J.jsx)(_l,{live:!O.showError,children:t(`common.loading`)}),O.showSkeleton&&(0,J.jsx)(gl,{label:t(`common.loading`),rows:5}),D&&!D.readable&&(0,J.jsx)(`div`,{className:`notice notice-err`,role:`alert`,children:t(`codexSet.prompt.unreadable`)}),O.showError&&(0,J.jsx)(`div`,{className:`notice notice-err`,role:`alert`,children:t(`codexSet.prompt.loadFailed`)}),(r||H)&&(0,J.jsx)(`div`,{className:`notice notice-err`,role:`alert`,children:H?t(`codexSet.custom.layerGone`):r}),D?.drift&&(0,J.jsxs)(`div`,{className:`notice codex-set-prompt__drift`,role:`alert`,"data-drift":D.drift,children:[(0,J.jsx)(`span`,{children:t(Ag[D.drift])}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-sm`,disabled:v,onClick:()=>{P(!0)},children:t(`codexSet.prompt.repair`)})]}),(0,J.jsx)(`ul`,{className:`codex-set-prompt__rows`,children:L.map(e=>(0,J.jsx)(ug,{descriptor:e,toggle:D?.toggles.find(t=>t.id===e.id),bytes:S?.layers?.[e.id]?.bytes??null,busy:a===e.id,writesRefused:D?.readable===!1,onToggle:(e,t)=>{k(e,t)},onSelectBase:e.class===`base`&&D?(t=>{if(t){A(`/api/codex-prompt/base/select`,{kind:`default`});return}let n=D.baseVariants[0];if(!n){c(e.id);return}A(`/api/codex-prompt/base/select`,{kind:`variant`,id:n.id})}):void 0,baseSelection:D?.baseSelection,onOpen:c},e.id))}),R.length>0&&(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)(`div`,{className:`row codex-set-prompt__group`,children:(0,J.jsx)(`strong`,{children:t(`codexSet.group.transition`)})}),(0,J.jsx)(`p`,{className:`muted small`,children:t(`codexSet.group.transitionDesc`)}),(0,J.jsx)(`ul`,{className:`codex-set-prompt__rows`,children:R.map(e=>(0,J.jsx)(ug,{descriptor:e,toggle:D?.toggles.find(t=>t.id===e.id),bytes:S?.layers?.[e.id]?.bytes??null,transitionOnly:!0,busy:a===e.id,writesRefused:D?.readable===!1,onToggle:(e,t)=>{k(e,t)},onOpen:c},e.id))})]}),D&&!D.extensionLayersEnumerable&&(0,J.jsx)(`p`,{className:`muted small codex-set-prompt__extensions`,children:t(`codexSet.prompt.extensionsUnknown`)}),z&&(z.class===`base`&&D?(0,J.jsx)(Og,{variants:D.baseVariants,selection:D.baseSelection,maxVariants:D.maxBaseVariants,busy:a!==null||!D.readable,onSelect:e=>{A(`/api/codex-prompt/base/select`,e)},onSave:e=>{A(`/api/codex-prompt/base`,e)},onDelete:e=>{A(`/api/codex-prompt/base`,{id:e,delete:!0})},onClose:()=>c(null)}):(0,J.jsx)(dg,{descriptor:z,toggle:D?.toggles.find(e=>e.id===z.id),text:S?.layers?.[z.id],busy:a!==null,onToggle:(e,t)=>{k(e,t)},onClose:()=>c(null)})),D&&(0,J.jsxs)(`section`,{className:`codex-set-custom`,children:[(0,J.jsxs)(`div`,{className:`row`,children:[(0,J.jsx)(`strong`,{children:t(`codexSet.custom.heading`)}),D.developerInstructionsState===`external`?null:(0,J.jsx)(Eg,{disabled:D.custom.length>=32||a!==null||!D.readable,onBlank:()=>{x(null),u(`new`)},onPreset:(e,t)=>{x({body:e,title:t}),u(`new`)}})]}),D.custom.length>=32&&(0,J.jsx)(`p`,{className:`muted small`,children:t(`codexSet.custom.limitReached`,{max:32})}),D.developerInstructionsState===`external`&&D.modelInstructionsFile===null&&(0,J.jsxs)(`div`,{className:`codex-set-custom__adopt`,children:[(0,J.jsx)(`p`,{className:`muted small`,children:t(`codexSet.custom.notOwned`)}),h&&(0,J.jsx)(`p`,{className:`muted small codex-set-custom__adopt-refusal`,children:t(`codexSet.custom.adoptUnsupported`,{path:h.path??``,line:h.line??0})}),p?(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)(`pre`,{className:`api-code codex-set-custom__adopt-preview`,children:p.decodedBody}),(0,J.jsxs)(`div`,{className:`modal-actions`,children:[(0,J.jsx)(`button`,{type:`button`,className:`btn btn-primary btn-sm`,disabled:a!==null,onClick:()=>{N(!0)},children:t(`codexSet.custom.adoptConfirm`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-sm`,onClick:()=>m(null),children:t(`common.cancel`)})]})]}):(0,J.jsx)(`button`,{type:`button`,className:`btn btn-sm`,disabled:a!==null,onClick:()=>{N(!1)},children:t(`codexSet.custom.adopt`)})]}),D.modelInstructionsFile!==null&&(0,J.jsx)(`p`,{className:`muted small codex-set-custom__replaced`,children:t(`codexSet.custom.baseReplaced`,{path:D.modelInstructionsFile})}),(0,J.jsx)(`ul`,{className:`codex-set-prompt__rows`,children:D.custom.map((e,t)=>(0,J.jsx)(fg,{layer:e,index:t,total:D.custom.length,busy:a!==null||!D.readable,onToggle:(e,t)=>{j(D.custom.map(n=>n.id===e?{...n,enabled:t}:n),e)},onEdit:u,onDelete:f,onMove:(e,t)=>{j(Cg(D.custom,e,t),e)}},e.id))}),d&&(0,J.jsxs)(`div`,{className:`notice codex-set-custom__confirm`,role:`alertdialog`,"aria-labelledby":`codex-set-delete-confirm`,children:[(0,J.jsx)(`span`,{id:`codex-set-delete-confirm`,children:t(`codexSet.custom.deleteConfirmNamed`,{title:D.custom.find(e=>e.id===d)?.title??``})}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-danger btn-sm`,onClick:()=>{let e=d;f(null),j(D.custom.filter(t=>t.id!==e),e)},children:t(`common.delete`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-sm`,onClick:()=>f(null),children:t(`common.cancel`)})]})]}),l&&D&&!H&&(0,J.jsx)(wg,{layer:l===`new`?null:V,seed:l===`new`?b:null,others:D.custom,busy:a!==null,navigation:B>=0&&D.custom.length>1?{position:B+1,total:D.custom.length,onPrev:()=>{B>0&&u(D.custom[B-1].id)},onNext:()=>{B{u(null),x(null)}})]})}function Mg(){return window.location.hash.replace(/^#\/?/,``)===`codex-set/prompt`?`prompt`:`multiauth`}function Ng(e){window.location.hash=e===`prompt`?`codex-set/prompt`:`codex-set`}function Pg(e){e.key===`ArrowLeft`||e.key===`Home`?(e.preventDefault(),Ng(`multiauth`),document.getElementById(`codex-set-tab-multiauth`)?.focus()):(e.key===`ArrowRight`||e.key===`End`)&&(e.preventDefault(),Ng(`prompt`),document.getElementById(`codex-set-tab-prompt`)?.focus())}function Fg({apiBase:e}){let t=Q(),[n,r]=(0,_.useState)(Mg),[i,a]=(0,_.useState)(()=>Mg()===`prompt`),[o,s]=(0,_.useState)(()=>Mg()===`multiauth`);(0,_.useEffect)(()=>{let e=()=>r(Mg());return window.addEventListener(`hashchange`,e),()=>window.removeEventListener(`hashchange`,e)},[]);let c=i||n===`prompt`,l=o||n===`multiauth`;return c!==i&&a(!0),l!==o&&s(!0),(0,J.jsxs)(J.Fragment,{children:[(0,J.jsxs)(`div`,{className:`page-tabs`,role:`tablist`,"aria-label":t(`nav.codexSet`),children:[(0,J.jsx)(`button`,{type:`button`,role:`tab`,id:`codex-set-tab-multiauth`,"aria-selected":n===`multiauth`,"aria-controls":`codex-set-panel-multiauth`,tabIndex:n===`multiauth`?0:-1,className:`page-tab${n===`multiauth`?` page-tab--active`:``}`,onClick:()=>Ng(`multiauth`),onKeyDown:Pg,children:t(`codexSet.tab.multiauth`)}),(0,J.jsx)(`button`,{type:`button`,role:`tab`,id:`codex-set-tab-prompt`,"aria-selected":n===`prompt`,"aria-controls":`codex-set-panel-prompt`,tabIndex:n===`prompt`?0:-1,className:`page-tab${n===`prompt`?` page-tab--active`:``}`,onClick:()=>Ng(`prompt`),onKeyDown:Pg,children:t(`codexSet.tab.prompt`)})]}),c&&(0,J.jsx)(`div`,{role:`tabpanel`,id:`codex-set-panel-prompt`,"aria-labelledby":`codex-set-tab-prompt`,hidden:n!==`prompt`,children:(0,J.jsx)(jg,{apiBase:e})}),l&&(0,J.jsx)(`div`,{role:`tabpanel`,id:`codex-set-panel-multiauth`,"aria-labelledby":`codex-set-tab-multiauth`,hidden:n!==`multiauth`,children:(0,J.jsx)(ag,{apiBase:e})})]})}var Ig=[`opencode`,`pi`,`omp`,`hermes`,`openclaw`,`kimi`,`gajae`,`dsh`,`mcode`,`zcode`,`prime`,`aside`],Lg={opencode:`api.clientConfig.clientOpencode`,pi:`api.clientConfig.clientPi`,omp:`api.clientConfig.clientOmp`,hermes:`api.clientConfig.clientHermes`,openclaw:`api.clientConfig.clientOpenclaw`,kimi:`api.clientConfig.clientKimi`,gajae:`api.clientConfig.clientGajae`,dsh:`api.clientConfig.clientDsh`,mcode:`api.clientConfig.clientMcode`,zcode:`api.clientConfig.clientZcode`,prime:`api.clientConfig.clientPrime`,aside:`api.clientConfig.clientAside`},Rg={opencode:`/provider-icons/opencode.svg`,pi:`/provider-icons/pi.svg`,omp:`/provider-icons/oh-my-pi.svg`,hermes:`/provider-icons/hermes-agent.svg`,openclaw:`/provider-icons/openclaw.svg`,kimi:`/provider-icons/kimi-color.svg`,gajae:`/provider-icons/gajae-code.svg`,dsh:`/provider-icons/deepseek-harness.svg`,mcode:`/provider-icons/minimax.svg`,zcode:`/provider-icons/zcode.svg`,prime:`/provider-icons/prime-agent.svg`,aside:`/provider-icons/aside.svg`},zg=new Set([`opencode`,`kimi`,`prime`,`aside`,`hermes`]),Bg={codex:`/provider-icons/openai.svg`,claude:`/provider-icons/claude-color.svg`,claudeDesktop:`/provider-icons/claude-color.svg`,grok:`/provider-icons/grok.svg`,cursor:`/provider-icons/cursor-color.svg`},Vg={...Bg,opencode:Rg.opencode??null,pi:Rg.pi??null,omp:Rg.omp??null,hermes:Rg.hermes??null,openclaw:Rg.openclaw??null,kimi:Rg.kimi??null,gajae:Rg.gajae??null,dsh:Rg.dsh??null,mcode:Rg.mcode??null,zcode:Rg.zcode??null,prime:Rg.prime??null,aside:Rg.aside??null},Hg=[Bg.grok],Ug=new Set([...[...zg].map(e=>Rg[e]).filter(e=>e!==void 0),...Hg]);function Wg(e){return Vg[e]}function Gg({src:e,label:t,size:n=20,className:r}){let i=r?`client-mark ${r}`:`client-mark`,a={"--client-mark-size":String(n)+`px`};return e?Ug.has(e)?(0,J.jsx)(`span`,{className:`${i} client-mark--mask`,style:{...a,maskImage:`url(${e})`,WebkitMaskImage:`url(${e})`},"aria-hidden":`true`}):(0,J.jsx)(`span`,{className:`${i} client-mark--img`,style:a,"aria-hidden":`true`,children:(0,J.jsx)(`img`,{src:e,alt:``,width:n,height:n})}):(0,J.jsx)(`span`,{className:`${i} client-mark--monogram`,style:a,"aria-hidden":`true`,children:t.slice(0,1)})}function Kg(){return typeof document>`u`?null:document.querySelector(`meta[name="opencodex-runtime-role"]`)?.getAttribute(`content`)?.trim()||null}function qg(){return Kg()===`client`}function Jg(e){return e.replace(/\/+$/,``)}function Yg(e){return new URL(e||`/`,window.location.href)}function Xg(e){try{let t=new URL(e);return t.protocol!==`http:`&&t.protocol!==`https:`||t.username||t.password||t.pathname!==`/`||t.search||t.hash?null:t.origin}catch{return null}}function Zg(e,t,n,r){let i=Jg(t);return{id:e,baseUrl:i,serverOrigin:n,bootstrapPath:`${i}/opencodex-session`,transport:r}}function Qg(e){let t=Yg(e),n=Jg(e);return{connected:!1,machine:Zg(`machine`,n,t.origin,`same-origin`),shared:Zg(`shared`,n,t.origin,`same-origin`)}}function $g(e){if(!e||typeof e!=`object`||Array.isArray(e))return!1;let t=e;return t.mode===`client`&&t.connected===!0&&t.protocolVersion===1&&(t.managementTransport===`direct`||t.managementTransport===`relay`)&&typeof t.machineBase==`string`&&typeof t.sharedBase==`string`&&typeof t.sharedServerOrigin==`string`&&typeof t.apiKeyId==`string`&&t.apiKeyId.trim().length>0&&typeof t.connectedAt==`string`}function e_(e,t){if(!$g(t))throw TypeError(`machine status response is invalid`);let n=Qg(e),r=Xg(t.machineBase),i=Xg(t.sharedServerOrigin);if(!r||r!==n.machine.serverOrigin||!i)throw TypeError(`machine status target origins are invalid`);let a;try{a=new URL(t.sharedBase)}catch{throw TypeError(`machine status shared target is invalid`)}if(a.username||a.password||a.search||a.hash)throw TypeError(`machine status shared target is invalid`);if(t.managementTransport===`direct`){if(a.origin!==i||a.pathname!==`/`)throw TypeError(`machine status direct target is inconsistent`)}else if(a.origin!==r||a.pathname!==`/api/machine/hub-relay`)throw TypeError(`machine status relay target is inconsistent`);return{connected:!0,machine:Zg(`machine`,Jg(e),r,`same-origin`),shared:t.managementTransport===`relay`?Zg(`shared`,`${Jg(e)}/api/machine/hub-relay`,i,`relay`):Zg(`shared`,i,i,`direct`),apiKeyId:t.apiKeyId}}function t_(e,t){return t[e].baseUrl}async function n_(e,t){let n=Qg(e);if(Kg()!==`client`)return n;let r;try{r=await fetch(`${n.machine.baseUrl}/api/machine/status`,{signal:t,cache:`no-store`})}catch(e){throw Error(`local machine plane unavailable`,{cause:e})}if(r.status===404)return n;if(!r.ok)throw Error(`local machine plane refused discovery (${r.status})`);let i=await r.json().catch(()=>null);if(!$g(i))throw Error(`local machine plane returned invalid status`);return e_(e,i)}function r_(e){return e?[`responses`,`chat`,`messages`]:[`responses`,`chat`]}function i_(e){let t=e.id.indexOf(`/`),n=typeof e.owned_by==`string`&&e.owned_by.trim()?e.owned_by.trim():void 0,r=e.is_combo===!0?`combo`:t>0?e.id.slice(0,t):n??`openai`,i=t<0&&r===`openai`,a=r!==`openai`&&r!==`combo`;return{id:e.id,displayName:e.id,provider:r,native:i,custom:a}}function a_(e){return e.id}function o_(e){if(!e||typeof e!=`object`)return!1;let t=e;return t.ambiguous===!0||!(typeof t.requests7d!=`number`||!Number.isFinite(t.requests7d)||typeof t.totalRequests!=`number`||!Number.isFinite(t.totalRequests)||t.lastUsedAt!==void 0&&(typeof t.lastUsedAt!=`string`||Number.isNaN(new Date(t.lastUsedAt).getTime())))}var s_=new Set([`required`,`accepted`,`rejected`]);function c_(e){return Array.isArray(e)&&e.length>0&&e.every(e=>{if(!e||typeof e!=`object`)return!1;let t=e;return typeof t.endpoint==`string`&&s_.has(t.bearer)&&s_.has(t.dedicated)&&s_.has(t.xApiKey)})}var l_={baseUrl:`http://127.0.0.1:10100/v1`,responses:`http://127.0.0.1:10100/v1/responses`,chatCompletions:`http://127.0.0.1:10100/v1/chat/completions`,messages:`http://127.0.0.1:10100/v1/messages`,models:`http://127.0.0.1:10100/v1/models`};function u_(e){let t=e||l_.responses,n=t.match(/^(.*)\/v1\/responses\/?$/),r=n?`${n[1]}/v1`:t.replace(/\/responses\/?$/,``);return{baseUrl:r,responses:t,chatCompletions:`${r}/chat/completions`,messages:`${r}/messages`,models:`${r}/models`}}function d_(e,t){let n=new Date(e);return!e||Number.isNaN(n.getTime())?`—`:n.toLocaleDateString(t)}function f_({url:e}){return(0,J.jsx)(p_,{text:e,hintKey:`api.copyUrlHint`,copiedKey:`api.urlCopied`,className:`api-endpoint-url-btn`,children:(0,J.jsx)(`code`,{className:`api-code api-code-inline api-endpoint-url`,children:e})})}function p_({text:e,hintKey:t,copiedKey:n,className:r,children:i}){let{t:a}=ct(),o=(0,_.useId)(),s=(0,_.useRef)(null),[c,l]=(0,_.useState)(!1),[u,d]=(0,_.useState)(!1),[f,p]=(0,_.useState)(null),m=(0,_.useRef)(null),h=c||u;(0,_.useEffect)(()=>()=>{m.current!==null&&window.clearTimeout(m.current)},[]),(0,_.useLayoutEffect)(()=>{if(!h)return;let e=()=>{let e=s.current;if(!e)return;let t=e.getBoundingClientRect();p({top:Math.max(8,t.top-8),left:t.left+t.width/2})};return e(),window.addEventListener(`scroll`,e,!0),window.addEventListener(`resize`,e),()=>{window.removeEventListener(`scroll`,e,!0),window.removeEventListener(`resize`,e)}},[h]);let g=()=>l(!0),v=()=>l(!1),y=async()=>{try{await navigator.clipboard.writeText(e),d(!0),m.current!==null&&window.clearTimeout(m.current),m.current=window.setTimeout(()=>d(!1),1500)}catch{d(!1)}},b=h&&f?(0,mt.createPortal)((0,J.jsx)(`span`,{id:o,className:`ocx-tooltip-bubble api-copy-tip-fixed`,role:`tooltip`,style:{top:f.top,left:f.left},children:a(u?n:t)}),document.body):null,x={ref:s,className:`ocx-tooltip ${r}`,onMouseEnter:g,onMouseLeave:v,onFocus:g,onBlur:v,onClick:e=>{if(typeof window<`u`&&window.getSelection()?.toString())return;let t=e.target;if(t instanceof HTMLElement&&t!==e.currentTarget){let n=t.getBoundingClientRect();if(t.scrollHeight>t.clientHeight+1&&e.clientX>=n.right-16||t.scrollWidth>t.clientWidth+1&&e.clientY>=n.bottom-16)return}y()},onKeyDown:e=>{e.key===`Escape`&&v()},"aria-label":a(t),"aria-describedby":h?o:void 0};return(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)(`button`,{type:`button`,...x,children:i}),b]})}function m_({text:e}){return(0,J.jsx)(p_,{text:e,hintKey:`api.copyExampleHint`,copiedKey:`api.exampleCopied`,className:`api-example-copy-btn`,children:(0,J.jsx)(`code`,{className:`api-code api-example-pre`,children:e})})}function h_(e,t){return t(e===`required`?`api.auth.required`:e===`accepted`?`api.auth.accepted`:`api.auth.rejected`)}function g_({endpoints:e,claudeCodeEnabled:t,authMatrix:n}){let{t:r}=ct();return(0,J.jsxs)(`div`,{className:`panel api-panel`,children:[(0,J.jsx)(`h3`,{className:`panel-title`,children:r(`api.endpointsTitle`)}),(0,J.jsxs)(`div`,{className:`api-endpoints`,children:[(0,J.jsxs)(`div`,{children:[(0,J.jsx)(`span`,{className:`muted small`,children:r(`api.baseUrl`)}),(0,J.jsx)(f_,{url:e.baseUrl})]}),(0,J.jsxs)(`div`,{children:[(0,J.jsx)(`span`,{className:`muted small`,children:r(`api.responsesEndpoint`)}),(0,J.jsx)(f_,{url:e.responses})]}),(0,J.jsxs)(`div`,{children:[(0,J.jsx)(`span`,{className:`muted small`,children:r(`api.chatCompletionsEndpoint`)}),(0,J.jsx)(f_,{url:e.chatCompletions})]}),t&&(0,J.jsxs)(`div`,{children:[(0,J.jsx)(`span`,{className:`muted small`,children:r(`api.messagesEndpoint`)}),(0,J.jsx)(f_,{url:e.messages})]}),(0,J.jsxs)(`div`,{children:[(0,J.jsx)(`span`,{className:`muted small`,children:r(`api.modelsEndpoint`)}),(0,J.jsx)(f_,{url:e.models})]})]}),(0,J.jsx)(`p`,{className:`muted small`,children:r(`api.endpointNote`)}),(0,J.jsxs)(`div`,{className:`api-auth-matrix-block`,children:[(0,J.jsx)(`h4`,{className:`api-auth-matrix-title`,children:r(`api.authTitle`)}),(0,J.jsx)(`div`,{className:`api-auth-matrix-scroll`,children:(0,J.jsxs)(`table`,{className:`api-auth-matrix`,children:[(0,J.jsx)(`thead`,{children:(0,J.jsxs)(`tr`,{children:[(0,J.jsx)(`th`,{children:r(`api.auth.endpoint`)}),(0,J.jsx)(`th`,{children:(0,J.jsx)(`code`,{children:`Authorization: Bearer`})}),(0,J.jsx)(`th`,{children:(0,J.jsx)(`code`,{children:`x-opencodex-api-key`})}),(0,J.jsx)(`th`,{children:(0,J.jsx)(`code`,{children:`x-api-key`})})]})}),(0,J.jsx)(`tbody`,{children:n.map(e=>(0,J.jsxs)(`tr`,{children:[(0,J.jsx)(`td`,{children:(0,J.jsx)(`code`,{children:e.endpoint})}),(0,J.jsx)(`td`,{children:h_(e.bearer,r)}),(0,J.jsx)(`td`,{children:h_(e.dedicated,r)}),(0,J.jsx)(`td`,{children:h_(e.xApiKey,r)})]},e.endpoint))})]})}),(0,J.jsx)(`p`,{className:`muted small`,children:r(`api.authLoopback`)}),(0,J.jsx)(`p`,{className:`muted small`,children:r(`api.authBaseUrlNote`)})]})]})}function __({keys:e,keysLoading:t=!1,keysLoadFailed:n,newName:r,creating:i,newKey:a,copied:o,confirmDelete:s,localeTag:c,showKeyList:l=!0,onNewNameChange:u,onCreate:d,onDismissNewKey:f,onCopyKey:p,onConfirmDelete:m,onCancelDelete:h,onDelete:g}){let{t:_}=ct();return(0,J.jsxs)(J.Fragment,{children:[a&&(0,J.jsxs)(`div`,{className:`panel api-panel panel-accent api-newkey-panel`,children:[(0,J.jsx)(`h3`,{className:`panel-title`,children:_(`api.newKeyTitle`)}),(0,J.jsx)(`p`,{className:`muted small`,children:_(`api.newKeyNote`)}),(0,J.jsxs)(`div`,{className:`api-form-row`,children:[(0,J.jsx)(`code`,{className:`api-code`,style:{flex:1,wordBreak:`break-all`},children:a}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-sm btn-ghost`,onClick:p,children:o?(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)(ue,{}),` `,_(`api.copied`)]}):_(`api.copy`)})]}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-sm btn-ghost`,style:{alignSelf:`flex-start`},onClick:f,children:_(`api.dismiss`)})]}),(0,J.jsxs)(`div`,{className:`panel api-panel api-generate-panel`,children:[(0,J.jsx)(`h3`,{className:`panel-title`,children:_(`api.generateTitle`)}),(0,J.jsxs)(`div`,{className:`api-form-row`,children:[(0,J.jsx)(`input`,{id:`api-key-name`,type:`text`,placeholder:_(`api.keyNamePlaceholder`),"aria-label":_(`api.keyNamePlaceholder`),value:r,maxLength:64,onChange:e=>u(e.target.value),className:`input`}),(0,J.jsxs)(`button`,{type:`button`,className:`btn btn-primary`,onClick:d,disabled:i,children:[(0,J.jsx)(fe,{}),` `,_(i?`api.generating`:`api.generate`)]})]})]}),l&&(0,J.jsxs)(`div`,{className:`panel api-panel`,style:{marginTop:`1rem`},"aria-busy":t,children:[(0,J.jsx)(`h3`,{className:`panel-title`,children:t?_(`api.activeKeysLoading`):_(`api.activeKeys`,{count:e.length})}),t?(0,J.jsx)(`div`,{className:`api-active-keys-skeleton`,role:`status`,"aria-label":_(`common.loading`)}):e.length>0?(0,J.jsx)(`div`,{className:`tbl-wrap`,children:(0,J.jsxs)(`table`,{className:`tbl`,children:[(0,J.jsx)(`thead`,{children:(0,J.jsxs)(`tr`,{children:[(0,J.jsx)(`th`,{children:_(`api.colName`)}),(0,J.jsx)(`th`,{children:_(`api.colKey`)}),(0,J.jsx)(`th`,{children:_(`api.colCreated`)}),(0,J.jsx)(`th`,{})]})}),(0,J.jsx)(`tbody`,{children:e.map(e=>(0,J.jsxs)(`tr`,{children:[(0,J.jsx)(`td`,{children:e.name}),(0,J.jsx)(`td`,{children:(0,J.jsx)(`code`,{children:e.prefix})}),(0,J.jsx)(`td`,{children:d_(e.createdAt,c)}),(0,J.jsx)(`td`,{children:s===e.id?(0,J.jsxs)(`span`,{className:`api-actions`,children:[(0,J.jsx)(`button`,{type:`button`,className:`btn btn-sm btn-danger`,onClick:()=>g(e.id),children:_(`api.confirm`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-sm btn-ghost`,onClick:h,children:_(`common.cancel`)})]}):(0,J.jsx)(`button`,{type:`button`,className:`btn btn-sm btn-ghost`,"aria-label":_(`api.deleteAria`),onClick:()=>m(e.id),children:(0,J.jsx)(de,{})})})]},e.id))})]})}):n?(0,J.jsx)(`p`,{className:`muted`,children:_(`api.keysLoadFailed`)}):(0,J.jsx)(`p`,{className:`muted`,children:_(`api.noKeys`)})]})]})}function v_({filteredModels:e,modelsLoading:t,modelsRefreshing:n=!1,modelsLoadFailed:r,modelCount:i,hasModelData:a,modelQuery:o,copiedModelId:s,modelTests:c,claudeCodeEnabled:l,onModelQueryChange:u,onCopyModelId:d,onTestModel:f,onRetryModels:p,canTestModels:m,sourceLabel:h,protocolLabel:g}){let{t:_}=ct();return(0,J.jsxs)(`div`,{className:`panel api-panel api-models-panel`,children:[(0,J.jsxs)(`div`,{className:`api-panel-head`,children:[(0,J.jsx)(`h3`,{className:`panel-title`,children:_(`api.modelsTitle`)}),(0,J.jsx)(`span`,{className:`muted mono text-label`,children:_(`api.modelsCount`,{count:e.length})})]}),(0,J.jsx)(`p`,{className:`muted small`,children:_(`api.modelsSubtitle`)}),(0,J.jsx)(`input`,{type:`search`,className:`input`,value:o,onChange:e=>u(e.target.value),placeholder:_(`api.modelsSearch`),"aria-label":_(`api.modelsSearch`)}),r&&(0,J.jsxs)(`div`,{className:`api-models-error`,children:[(0,J.jsx)(`p`,{className:`muted small`,role:`alert`,children:_(`api.modelsLoadFailed`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:p,children:_(`common.retry`)})]}),n&&!t&&(0,J.jsx)(`p`,{className:`muted small`,"aria-live":`polite`,children:_(`api.modelsLoading`)}),t?(0,J.jsx)(gl,{label:_(`api.modelsLoading`),rows:3}):a?e.length===0?(0,J.jsx)(`p`,{className:`muted small api-models-empty`,children:i===0?_(`api.modelsEmpty`):_(`api.modelsNoMatch`,{query:o.trim()})}):(0,J.jsx)(`div`,{className:`api-models-scroll`,children:(0,J.jsxs)(`table`,{className:`tbl`,children:[(0,J.jsx)(`thead`,{children:(0,J.jsxs)(`tr`,{children:[(0,J.jsx)(`th`,{children:_(`api.colModel`)}),(0,J.jsx)(`th`,{children:_(`api.colSource`)}),(0,J.jsx)(`th`,{children:_(`api.colProtocols`)})]})}),(0,J.jsx)(`tbody`,{children:e.map(e=>{let t=a_(e),n=r_(l);return(0,J.jsxs)(`tr`,{children:[(0,J.jsx)(`td`,{children:(0,J.jsxs)(`div`,{className:`api-model-cell`,children:[(0,J.jsx)(`code`,{children:t}),e.displayName!==e.id&&(0,J.jsx)(`span`,{className:`muted small`,children:e.displayName})]})}),(0,J.jsx)(`td`,{children:h(e)}),(0,J.jsx)(`td`,{children:(0,J.jsxs)(`div`,{className:`api-model-actions`,children:[(0,J.jsx)(`button`,{type:`button`,className:`btn btn-sm btn-ghost`,onClick:()=>{d(t)},children:_(s===t?`api.modelCopied`:`api.copyModelId`)}),n.map(n=>{let r=c[t]?.[n],i=r?.state??`idle`;return(0,J.jsxs)(`span`,{className:`api-model-test-chip`,children:[(0,J.jsx)(`button`,{type:`button`,className:`btn btn-sm btn-ghost`,disabled:i===`testing`||!m,title:m?void 0:_(`api.auth.testNeedsFreshKey`),onClick:()=>{f(e,n)},children:_(`api.auth.testProtocol`,{protocol:g(n)})}),i!==`idle`&&(0,J.jsx)(`span`,{className:`api-test-note api-test-note--${i}`,role:`status`,"aria-live":`polite`,"aria-atomic":`true`,children:i===`testing`?_(`api.testingModel`):i===`ok`?_(`api.testSucceeded`):r?.detail??_(`api.testFailed`)})]},n)})]})})]},t)})})]})}):null]})}function y_({endpoints:e,claudeCodeEnabled:t}){let{t:n}=ct(),r=JSON.stringify(n(`api.usageSampleInput`)),i=`curl ${e.chatCompletions} \\ - -H "x-opencodex-api-key: ocx_YOUR_KEY_HERE" \\ - -H "Content-Type: application/json" \\ - -d '{ - "model": "gpt-5.4", - "messages": [{"role": "user", "content": ${r}}] - }'`,a=`curl ${e.responses} \\ - -H "x-opencodex-api-key: ocx_YOUR_KEY_HERE" \\ - -H "Content-Type: application/json" \\ - -d '{ - "model": "gpt-5.4", - "input": ${r} - }'`,o=`curl ${e.messages} \\ - -H "x-opencodex-api-key: ocx_YOUR_KEY_HERE" \\ - -H "Content-Type: application/json" \\ - -d '{ - "model": "claude-sonnet-4-6", - "max_tokens": 64, - "messages": [{"role": "user", "content": ${r}}] - }'`;return(0,J.jsxs)(`section`,{className:`panel api-panel awi-usage-panel`,children:[(0,J.jsx)(`h3`,{className:`panel-title`,children:n(`api.workspace.usageExamples`)}),(0,J.jsxs)(`div`,{className:`awi-usage-panel-body`,children:[(0,J.jsxs)(`div`,{className:`awi-usage-example`,children:[(0,J.jsx)(`h4`,{className:`awi-usage-example-title`,children:n(`api.usageChatTitle`)}),(0,J.jsx)(m_,{text:i})]}),(0,J.jsxs)(`div`,{className:`awi-usage-example`,children:[(0,J.jsx)(`h4`,{className:`awi-usage-example-title`,children:n(`api.usageResponsesTitle`)}),(0,J.jsx)(m_,{text:a})]}),t&&(0,J.jsxs)(`div`,{className:`awi-usage-example`,children:[(0,J.jsx)(`h4`,{className:`awi-usage-example-title`,children:n(`api.usageMessagesTitle`)}),(0,J.jsx)(m_,{text:o})]})]})]})}function b_({client:e,apiBase:t,onOpenDetails:n,onCopy:r,onDownload:i}){let a=Q(),[o,s]=(0,_.useState)(0),c=[t,e,String(o)].join(`|`),[l,u]=(0,_.useState)(null);(0,_.useEffect)(()=>{let n=new AbortController,r=!1;return(async()=>{try{let i=await fetch(`${t}/api/client-config?client=${encodeURIComponent(e)}`,{signal:n.signal});if(!i.ok)throw Error(String(i.status));let a=await i.json();if(r)return;u({key:c,data:a,failed:!1})}catch{if(r)return;u({key:c,data:null,failed:!0})}})(),()=>{r=!0,n.abort()}},[t,e,c]);let d=l!==null&&l.key===c?l:null,f=d?.data??null,p=d?.failed??!1,m=d===null,h=a(Lg[e]),g=Rg[e],v=f?.text??``,y=(0,_.useCallback)(t=>{f&&n(e,f,v,t.currentTarget)},[e,f,v,n]);return(0,J.jsxs)(`li`,{className:`awi-clientconfig-row`,children:[(0,J.jsx)(`span`,{className:`awi-clientconfig-mark`,children:(0,J.jsx)(Gg,{src:g??null,label:h,size:20})}),(0,J.jsxs)(`span`,{className:`awi-clientconfig-identity`,children:[(0,J.jsx)(`span`,{className:`awi-clientconfig-name`,children:h}),(0,J.jsx)(`span`,{className:`muted text-label awi-clientconfig-meta`,children:m?a(`api.clientConfig.loading`):p||!f?(0,J.jsx)(`span`,{role:`alert`,children:a(`api.clientConfig.rowError`,{client:h})}):a(`api.clientConfig.rowMeta`,{destination:f.destination,count:f.modelCount})})]}),(0,J.jsx)(`span`,{className:`awi-clientconfig-row-actions`,children:p&&!m?(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:()=>s(e=>e+1),children:a(`common.retry`)}):(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)(`button`,{type:`button`,className:`btn btn-primary btn-sm`,"aria-label":a(`api.clientConfig.copyAria`,{client:h}),disabled:!f,onClick:()=>{f&&r(e,v)},children:a(`api.clientConfig.copy`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-sm`,"aria-label":a(`api.clientConfig.downloadAria`,{client:h}),disabled:!f,onClick:()=>{f&&i(e,f,v)},children:a(`api.clientConfig.download`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,"aria-label":a(`api.clientConfig.detailsAria`,{client:h}),disabled:!f,onClick:y,children:a(`api.clientConfig.details`)})]})})]})}function x_({client:e,envelope:t,json:n,hasKeys:r,onClose:i,onCopy:a,onDownload:o}){let s=Q(),c=(0,_.useRef)(null),l=`awi-clientconfig-dialog-${e}`;(0,_.useEffect)(()=>{let e=c.current;return e&&!e.open&&e.showModal(),()=>{e?.open&&e.close()}},[]);let u=(0,_.useCallback)(e=>{e.preventDefault(),i()},[i]);return(0,J.jsxs)(`dialog`,{ref:c,className:`modal-overlay`,"aria-labelledby":l,onCancel:u,children:[(0,J.jsx)(`button`,{type:`button`,className:`modal-backdrop-dismiss`,"aria-label":s(`common.close`),tabIndex:-1,onClick:i}),(0,J.jsxs)(`div`,{className:`modal-card awi-clientconfig-dialog`,onClick:e=>e.stopPropagation(),role:`document`,children:[(0,J.jsxs)(`div`,{className:`modal-head`,children:[(0,J.jsx)(`h3`,{id:l,children:s(Lg[e])}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:i,children:s(`common.close`)})]}),(0,J.jsx)(`pre`,{className:`api-code api-example-pre awi-clientconfig-json`,tabIndex:0,role:`group`,"aria-label":s(`api.clientConfig.jsonLabel`,{client:s(Lg[e])}),children:n}),(0,J.jsx)(`p`,{className:`muted small awi-clientconfig-count`,children:s(`api.clientConfig.modelCount`,{count:t.modelCount})}),t.modelsWithoutLimits>0&&(0,J.jsx)(`p`,{className:`muted small awi-clientconfig-degraded`,children:s(`api.clientConfig.missingLimits`,{count:t.modelsWithoutLimits,total:t.modelCount})}),!r&&(0,J.jsx)(`p`,{className:`muted small awi-clientconfig-nokey`,children:s(`api.clientConfig.noKeyYet`,{env:t.apiKeyEnv})}),(0,J.jsxs)(`div`,{className:`awi-clientconfig-line`,children:[(0,J.jsx)(`span`,{className:`muted text-label`,children:s(`api.clientConfig.destination`)}),(0,J.jsx)(m_,{text:t.destination})]}),(0,J.jsxs)(`div`,{className:`awi-clientconfig-line`,children:[(0,J.jsx)(`span`,{className:`muted text-label`,children:s(`api.clientConfig.envHint`)}),(0,J.jsx)(m_,{text:t.exportHint})]}),(0,J.jsx)(`p`,{className:`muted small awi-clientconfig-merge`,children:s(`api.clientConfig.mergeWarning`)}),(0,J.jsx)(`p`,{className:`muted text-label awi-clientconfig-where-title`,children:s(`api.clientConfig.whereDisclosure`)}),(0,J.jsx)(`p`,{className:`muted small`,children:s(`api.clientConfig.whereBody`)}),(0,J.jsxs)(`div`,{className:`modal-actions`,children:[(0,J.jsx)(`button`,{type:`button`,className:`btn btn-primary btn-sm`,onClick:a,children:s(`api.clientConfig.copy`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-sm`,onClick:o,children:s(`api.clientConfig.download`)})]})]})]})}function S_({apiBase:e,baseUrl:t,hasKeys:n}){let r=Q(),[i,a]=(0,_.useState)(null),[o,s]=(0,_.useState)(``),c=(0,_.useRef)(null);(0,_.useEffect)(()=>{if(i!==null)return;let e=c.current;e&&(c.current=null,e.isConnected&&e.focus())},[i]);let l=(0,_.useCallback)(async(e,t,n)=>{try{await navigator.clipboard.writeText(t),s(n?r(`api.clientConfig.copiedAnnounceClient`,{client:r(Lg[e])}):r(`api.clientConfig.copiedAnnounce`))}catch{s(r(`api.clientConfig.copyFailed`))}},[r]),u=(0,_.useCallback)((e,t,n)=>{let i=URL.createObjectURL(new Blob([n],{type:t.mediaType})),a=document.createElement(`a`);a.href=i,a.download=t.filename,a.click(),URL.revokeObjectURL(i),s(r(`api.clientConfig.downloadedAnnounce`,{filename:t.filename,destination:t.destination}))},[r]),d=(0,_.useCallback)(()=>a(null),[]),f=(0,_.useCallback)((e,t,n,r)=>{c.current=r,a({client:e,envelope:t,json:n})},[]);return(0,J.jsxs)(`section`,{className:`panel api-panel awi-clientconfig-panel`,children:[(0,J.jsx)(`div`,{className:`api-panel-head awi-clientconfig-head`,children:(0,J.jsx)(`h3`,{className:`panel-title`,children:r(`api.clientConfig.title`)})}),(0,J.jsx)(`ul`,{className:`awi-clientconfig-rows`,"aria-label":r(`api.clientConfig.rowsLabel`),children:Ig.map(t=>(0,J.jsx)(b_,{client:t,apiBase:e,onOpenDetails:f,onCopy:(e,t)=>{l(e,t,!0)},onDownload:u},t))}),(0,J.jsxs)(`div`,{className:`awi-clientconfig-line`,children:[(0,J.jsx)(`span`,{className:`muted text-label`,children:r(`api.baseUrl`)}),(0,J.jsx)(m_,{text:t})]}),(0,J.jsx)(`div`,{className:`sr-only`,"aria-live":`polite`,"aria-atomic":`true`,children:o}),i&&(0,J.jsx)(x_,{client:i.client,envelope:i.envelope,json:i.json,hasKeys:n,onClose:d,onCopy:()=>{l(i.client,i.json,!1)},onDownload:()=>u(i.client,i.envelope,i.json)})]})}function C_({keys:e,keysLoading:t,keysLoadFailed:n,attributionSince:r,localeTag:i,busy:a,onSelect:o}){let s=Q();return(0,J.jsxs)(`div`,{className:`panel api-panel awi-keylist-panel`,"aria-busy":t,children:[(0,J.jsx)(`div`,{className:`api-panel-head`,children:(0,J.jsx)(`h3`,{className:`panel-title`,children:t?s(`api.activeKeysLoading`):s(`api.activeKeys`,{count:e.length})})}),t?(0,J.jsx)(`div`,{className:`api-active-keys-skeleton`,role:`status`,"aria-label":s(`common.loading`)}):e.length===0?(0,J.jsx)(`p`,{className:`muted small`,children:s(n?`api.keysLoadFailed`:`api.noKeys`)}):(0,J.jsx)(`div`,{className:`tbl-wrap`,children:(0,J.jsxs)(`table`,{className:`tbl awi-keylist-table`,children:[(0,J.jsx)(`thead`,{children:(0,J.jsxs)(`tr`,{children:[(0,J.jsx)(`th`,{children:s(`api.colName`)}),(0,J.jsx)(`th`,{children:s(`api.colKey`)}),(0,J.jsx)(`th`,{children:s(`api.attribution.requests7d`)}),(0,J.jsx)(`th`,{children:s(`api.attribution.lastUsed`)})]})}),(0,J.jsx)(`tbody`,{children:e.map(e=>(0,J.jsxs)(`tr`,{children:[(0,J.jsx)(`td`,{children:(0,J.jsx)(`button`,{type:`button`,className:`awi-keylist-name`,disabled:a,onClick:()=>o(e.id),children:e.name})}),(0,J.jsx)(`td`,{children:(0,J.jsx)(`code`,{children:e.prefix})}),(0,J.jsx)(`td`,{children:r?e.usage.ambiguous?s(`api.attribution.railAmbiguous`):e.usage.requests7d.toLocaleString(i):s(`api.attribution.unavailable`)}),(0,J.jsx)(`td`,{children:!r||e.usage.ambiguous?`—`:e.usage.lastUsedAt?d_(e.usage.lastUsedAt,i):s(`api.attribution.neverUsed`)})]},e.id))})]})})]})}function w_({keys:e,apiBase:t,attributionSince:n,historyTruncated:r,authMatrix:i,keysLoading:a,keysLoadFailed:o,endpoints:s,claudeCodeEnabled:c,localeTag:l,newName:u,creating:d,newKey:f,copied:p,rotationSecret:m=null,rotationCopied:h=!1,filteredModels:g,modelsLoading:v,modelsRefreshing:y=!1,modelsLoadFailed:b,modelCount:x,hasModelData:S,modelQuery:C,copiedModelId:w,modelTests:T,canTestModels:E,onNewNameChange:D,onCreate:O,onDismissNewKey:k,onCopyKey:A,onDelete:j,onRename:M,onRotationStart:N,onRotationCommit:P,onRotationAbort:F,onCopyRotationSecret:I,onDismissRotationSecret:L,onModelQueryChange:R,onCopyModelId:z,onTestModel:B,onRetryModels:V,sourceLabel:H,protocolLabel:U}){let W=Q(),[ee,G]=(0,_.useState)(null),[K,q]=(0,_.useState)(!1),[Y,te]=(0,_.useState)(!1),[ne,re]=(0,_.useState)(!1),[ie,ae]=(0,_.useState)(!1),[oe,se]=(0,_.useState)(``),[ce,le]=(0,_.useState)(!1),[ue,de]=(0,_.useState)(!1),[fe,pe]=(0,_.useState)(!1),[X,me]=(0,_.useState)(!1),[ge,_e]=(0,_.useState)(!1),Z=ee?e.find(e=>e.id===ee)??null:null,ve=Z?m?.id===Z.id?m.rotationId:Z.pendingRotation?.id:void 0,ye=ne||ce||X,be=async e=>{if(!(!Z||X)){me(!0),_e(!1);try{let t=ve;(e===`start`?await N?.(Z.id)??!1:t&&(await(e===`commit`?P:F)?.(Z.id,t)??!1))||_e(!0)}finally{me(!1)}}},xe=(0,_.useMemo)(()=>[{id:`keys`,label:W(`api.section.keys`),meta:a?void 0:String(e.length)},{id:`connect`,label:W(`api.section.connect`)},{id:`endpoints`,label:W(`api.section.endpoints`)},{id:`models`,label:W(`api.section.models`),meta:String(x)},{id:`examples`,label:W(`api.section.examples`)}],[W,e.length,a,x]),Ce=()=>{q(!1),te(!1)},we=()=>{G(null),Ce(),ae(!1),de(!1),pe(!1)};(0,_.useEffect)(()=>{if(!K)return;let e=window.setTimeout(()=>te(!0),300);return()=>window.clearTimeout(e)},[K]);let Te=()=>{Z&&q(!0)},Ee=async()=>{if(!(!Z||!Y||ne)){re(!0),pe(!1);try{await j(Z.id)?(Ce(),G(null)):pe(!0)}finally{re(!1)}}},De=()=>{Z&&(se(Z.name),de(!1),ae(!0))},Oe=async()=>{if(!Z||ce)return;let e=oe.trim();if(!e||e===Z.name){ae(!1);return}le(!0),de(!1);try{await M(Z.id,e)?ae(!1):de(!0)}finally{le(!1)}};return(0,J.jsxs)(`div`,{className:`apikeys-workspace-shell`,children:[!Z&&(0,J.jsx)(yp,{scope:`api`,items:xe,ariaLabel:W(`api.workspace.sections`)}),(0,J.jsx)(`div`,{className:`apikeys-workspace-root`,children:(0,J.jsx)(`section`,{className:`apikeys-workspace-main`,"aria-label":W(`api.workspace.details`),children:Z?(0,J.jsxs)(`div`,{className:`awi-detail`,children:[(0,J.jsx)(`div`,{className:`awi-detail-toolbar`,children:(0,J.jsxs)(`button`,{type:`button`,className:`awi-back`,onClick:we,disabled:ye,children:[(0,J.jsx)(Se,{className:`awi-back-chevron`,"aria-hidden":`true`}),W(`modal.back`)]})}),(0,J.jsxs)(`div`,{className:`awi-detail-body`,children:[(0,J.jsxs)(`div`,{className:`awi-detail-head`,children:[(0,J.jsx)(`h2`,{className:`awi-detail-title`,children:Z.name}),(0,J.jsx)(`span`,{className:`awi-detail-actions`,children:K?(0,J.jsxs)(J.Fragment,{children:[(0,J.jsxs)(`button`,{type:`button`,className:`btn btn-danger btn-sm awi-confirm-delete`,onClick:()=>{Ee()},disabled:!Y||ne,children:[(0,J.jsx)(he,{}),` `,W(ne?`api.key.deleting`:`api.confirm`)]},`confirm-delete`),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:Ce,disabled:ne,children:W(`common.cancel`)})]}):(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:De,disabled:ie,children:W(`api.key.rename`)},`rename`),(0,J.jsxs)(`button`,{type:`button`,className:`btn btn-danger btn-sm`,onClick:Te,"aria-label":W(`api.deleteAria`),children:[(0,J.jsx)(he,{}),` `,W(`api.workspace.deleteKey`)]},`request-delete`)]})})]}),K&&(0,J.jsx)(`p`,{className:`muted awi-delete-hint`,children:W(`api.workspace.deleteConfirm`)}),fe&&(0,J.jsx)(`p`,{className:`awi-delete-error`,role:`alert`,children:W(`api.deleteFailed`)}),ie&&(0,J.jsxs)(`div`,{className:`awi-rename`,children:[(0,J.jsx)(`label`,{className:`awi-rename-label`,htmlFor:`awi-key-name`,children:W(`api.key.name`)}),(0,J.jsx)(`input`,{id:`awi-key-name`,className:`input`,type:`text`,value:oe,maxLength:64,disabled:ce,onChange:e=>se(e.target.value),onKeyDown:e=>{e.key===`Enter`&&(e.preventDefault(),Oe())}}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-sm`,onClick:()=>{Oe()},disabled:ce,children:W(ce?`api.key.renaming`:`api.key.saveName`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:()=>ae(!1),disabled:ce,children:W(`common.cancel`)}),ue&&(0,J.jsx)(`p`,{className:`awi-rename-error`,role:`alert`,children:W(`api.key.renameFailed`)})]}),(0,J.jsxs)(`div`,{className:`awi-section`,children:[(0,J.jsx)(`h3`,{className:`awi-section-title`,children:W(`api.workspace.keyDetails`)}),(0,J.jsxs)(`dl`,{className:`awi-kv`,children:[(0,J.jsxs)(`div`,{className:`awi-kv-row`,children:[(0,J.jsx)(`dt`,{children:W(`api.workspace.keyPrefix`)}),(0,J.jsx)(`dd`,{children:(0,J.jsx)(`code`,{children:Z.prefix})})]}),(0,J.jsxs)(`div`,{className:`awi-kv-row`,children:[(0,J.jsx)(`dt`,{children:W(`api.colCreated`)}),(0,J.jsx)(`dd`,{children:d_(Z.createdAt,l)})]})]})]}),(0,J.jsxs)(`div`,{className:`awi-section`,"aria-live":`polite`,children:[(0,J.jsx)(`h3`,{className:`awi-section-title`,children:W(`api.rotation.title`)}),ve?(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)(`p`,{className:`muted`,children:W(`api.rotation.pending`)}),Z.pendingRotation&&(0,J.jsxs)(`p`,{className:`muted`,children:[W(`api.rotation.expires`),` `,d_(Z.pendingRotation.expiresAt,l)]}),m?.id===Z.id&&(0,J.jsxs)(`div`,{className:`api-key-reveal`,role:`status`,children:[(0,J.jsx)(`p`,{children:W(`api.rotation.secretOnce`)}),(0,J.jsx)(`code`,{children:m.key}),(0,J.jsxs)(`span`,{children:[(0,J.jsx)(`button`,{type:`button`,className:`btn btn-sm`,onClick:I,children:W(h?`api.copied`:`api.copy`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:L,children:W(`common.close`)})]})]}),(0,J.jsxs)(`div`,{className:`awi-detail-actions`,children:[(0,J.jsx)(`button`,{type:`button`,className:`btn btn-sm`,disabled:X,onClick:()=>{be(`commit`)},children:W(`api.rotation.commit`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,disabled:X,onClick:()=>{be(`abort`)},children:W(`api.rotation.abort`)})]})]}):(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)(`p`,{className:`muted`,children:W(`api.rotation.description`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,disabled:X,onClick:()=>{be(`start`)},children:W(X?`api.rotation.starting`:`api.rotation.start`)})]}),ge&&(0,J.jsx)(`p`,{className:`awi-delete-error`,role:`alert`,children:W(`api.rotation.failed`)})]}),(0,J.jsxs)(`div`,{className:`awi-section`,children:[(0,J.jsx)(`h3`,{className:`awi-section-title`,children:W(`api.attribution.title`)}),n?Z.usage.ambiguous?(0,J.jsx)(`p`,{className:`muted`,children:W(`api.attribution.ambiguous`)}):(0,J.jsxs)(`dl`,{className:`awi-kv`,children:[(0,J.jsxs)(`div`,{className:`awi-kv-row`,children:[(0,J.jsx)(`dt`,{children:W(`api.attribution.requests7d`)}),(0,J.jsx)(`dd`,{children:Z.usage.requests7d.toLocaleString(l)})]}),(0,J.jsxs)(`div`,{className:`awi-kv-row`,children:[(0,J.jsx)(`dt`,{children:W(r?`api.attribution.totalRequestsAvailable`:`api.attribution.totalRequests`)}),(0,J.jsx)(`dd`,{children:Z.usage.totalRequests.toLocaleString(l)})]}),(0,J.jsxs)(`div`,{className:`awi-kv-row`,children:[(0,J.jsx)(`dt`,{children:W(`api.attribution.lastUsed`)}),(0,J.jsx)(`dd`,{children:Z.usage.lastUsedAt?d_(Z.usage.lastUsedAt,l):W(`api.attribution.neverUsed`)})]}),(0,J.jsxs)(`div`,{className:`awi-kv-row`,children:[(0,J.jsx)(`dt`,{children:W(r?`api.attribution.sinceAvailable`:`api.attribution.since`)}),(0,J.jsx)(`dd`,{children:d_(n,l)})]})]}):(0,J.jsx)(`p`,{className:`muted`,children:W(`api.attribution.unavailableDetail`)})]})]})]}):(0,J.jsx)(`div`,{className:`awi-overview`,children:(0,J.jsxs)(`div`,{className:`awi-overview-section`,children:[(0,J.jsxs)(`div`,{id:gp(`api`,`keys`),className:`awi-section-anchor`,children:[(0,J.jsx)(__,{keys:e,keysLoading:a,keysLoadFailed:o,newName:u,creating:d,newKey:f,copied:p,confirmDelete:null,localeTag:l,showKeyList:!1,onNewNameChange:D,onCreate:O,onDismissNewKey:k,onCopyKey:A,onConfirmDelete:()=>{},onCancelDelete:()=>{},onDelete:()=>{}}),(0,J.jsx)(C_,{keys:e,keysLoading:a,keysLoadFailed:o,attributionSince:n,localeTag:l,busy:ye,onSelect:e=>{G(e),Ce(),ae(!1),de(!1),pe(!1)}})]}),(0,J.jsx)(`div`,{id:gp(`api`,`connect`),className:`awi-section-anchor`,children:(0,J.jsx)(S_,{apiBase:t,baseUrl:s.baseUrl,hasKeys:e.length>0})}),(0,J.jsx)(`div`,{id:gp(`api`,`endpoints`),className:`awi-section-anchor`,children:(0,J.jsx)(g_,{endpoints:s,claudeCodeEnabled:c,authMatrix:i})}),(0,J.jsx)(`div`,{id:gp(`api`,`models`),className:`awi-section-anchor`,children:(0,J.jsx)(v_,{filteredModels:g,modelsLoading:v,modelsRefreshing:y,modelsLoadFailed:b,modelCount:x,hasModelData:S,modelQuery:C,copiedModelId:w,modelTests:T,claudeCodeEnabled:c,onModelQueryChange:R,onCopyModelId:z,onTestModel:B,onRetryModels:V,canTestModels:E,sourceLabel:H,protocolLabel:U})}),(0,J.jsx)(`div`,{id:gp(`api`,`examples`),className:`awi-section-anchor`,children:(0,J.jsx)(y_,{endpoints:s,claudeCodeEnabled:c})})]})})})})]})}var T_=[],E_=15e3;function D_(e){let t=e.replace(/\/$/,``);if(!t)return l_;try{return new URL(t).host?u_(`${t}/v1/responses`):l_}catch{return l_}}function O_(e){return!e||!c_(e.authMatrix)||!Array.isArray(e.keys)||e.keys.some(e=>!e||!o_(e.usage)||!k_(e.pendingRotation))?null:e}function k_(e){if(e===void 0)return!0;if(!e||typeof e!=`object`||Array.isArray(e))return!1;let t=e;return typeof t.id==`string`&&!!t.id&&typeof t.createdAt==`string`&&!Number.isNaN(Date.parse(t.createdAt))&&typeof t.expiresAt==`string`&&!Number.isNaN(Date.parse(t.expiresAt))}function A_({apiBase:e,active:t=!0}){let{t:n,locale:r}=ct(),i=et.find(e=>e.code===r)?.htmlLang,a=`ocx.apikeys.list.v2:${e}`,o=`ocx.apikeys.models.v1:${e}`,s=`api-keys:${e}`,c=`api-models:${e}`,l=vr(a),u=vr(o),d=O_(l?.data??null),f=u?.data??null,[p,m]=(0,_.useState)(null),[h,g]=(0,_.useState)(``),[v,y]=(0,_.useState)(null),[b,x]=(0,_.useState)({}),[S,C]=(0,_.useState)(``),[w,T]=(0,_.useState)(!1),[E,D]=(0,_.useState)(null),[O,k]=(0,_.useState)(!1),[A,j]=(0,_.useState)(null),[M,N]=(0,_.useState)(!1),P=(0,_.useRef)(!1),F=(0,_.useCallback)(async t=>{let r=await Ft(await fetch(`${e}/api/keys`,{signal:t}));if(!r||!c_(r.authMatrix))throw Error(n(`api.keysLoadFailed`));let i=r.keys??[];if(i.some(e=>!o_(e.usage)||!k_(e.pendingRotation)))throw Error(n(`api.keysLoadFailed`));let o=i,s=u_(r.endpoint??``),c={keys:o,endpoints:{baseUrl:r.baseUrl??s.baseUrl,responses:r.responsesEndpoint??r.endpoint??l_.responses,chatCompletions:r.chatCompletionsEndpoint??s.chatCompletions,messages:r.messagesEndpoint??s.messages,models:r.modelsEndpoint??s.models},claudeCodeEnabled:r.claudeCodeEnabled!==!1,...r.attributionSince?{attributionSince:r.attributionSince}:{},...r.historyTruncated===!0?{historyTruncated:!0}:{},authMatrix:r.authMatrix};return yr(a,c),c},[e,a,n]),I=(0,_.useCallback)(async t=>{let r=await fetch(`${e}/v1/models`,{signal:t});if(!r.ok)throw Error(n(`api.modelsLoadFailed`));let i=await r.json(),a=Array.isArray(i)?i:typeof i==`object`&&i&&Array.isArray(i.data)?i.data:null;if(!a)throw Error(n(`api.modelsLoadFailed`));let s=a.filter(e=>typeof e==`object`&&!!e&&typeof e.id==`string`).map(e=>i_(e)).sort((e,t)=>a_(e).localeCompare(a_(t)));return yr(o,s),s},[e,o,n]),L=ml(s,[e],F,{isEmpty:e=>e.keys.length===0,initialData:d??void 0,initialDataCachedAt:l?.cachedAt??null,staleAfterMs:6e4,enabled:t}),R=ml(c,[e],I,{isEmpty:e=>e.length===0,initialData:f??void 0,initialDataCachedAt:u?.cachedAt??null,staleAfterMs:6e4,enabled:t}),z=L.state,B=R.state,V=z.data??d,H=B.data??f??T_,U=V?.keys??[],W=V?.endpoints??D_(e),ee=V?.claudeCodeEnabled??!0,G=V?.attributionSince,K=V?.historyTruncated===!0,q=V?.authMatrix??[],Y=L.refresh,te=R.refresh,ne=(0,_.useMemo)(()=>{let e=h.trim().toLowerCase();return e?H.filter(t=>a_(t).toLowerCase().includes(e)||t.displayName.toLowerCase().includes(e)||t.provider.toLowerCase().includes(e)):H},[h,H]),re=async t=>{if(P.current)return!1;P.current=!0,T(!0),m(null);try{let r=t??S,i=await Pt(await fetch(`${e}/api/keys`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({name:r||`default`})}),n(`api.createFailed`));return typeof i?.key!=`string`||i.key.length===0?(m(n(`api.createFailed`)),!1):(D(i.key),C(``),Y(),!0)}catch{return m(n(`api.createFailed`)),!1}finally{P.current=!1,T(!1)}},ie=async t=>{m(null);let n=Vn(E_);try{return(await fetch(`${e}/api/keys`,{method:`DELETE`,headers:{"Content-Type":`application/json`},body:JSON.stringify({id:t}),signal:n.signal})).ok?(Y(),!0):!1}catch{return!1}finally{n.clear()}},ae=async(t,n)=>{m(null);let r=Vn(E_);try{return(await fetch(`${e}/api/keys`,{method:`PATCH`,headers:{"Content-Type":`application/json`},body:JSON.stringify({id:t,name:n}),signal:r.signal})).ok?(Y(),!0):!1}catch{return!1}finally{r.clear()}},oe=async t=>{m(null);let r=Vn(E_);try{let i=await Pt(await fetch(`${e}/api/keys/rotate`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({id:t}),signal:r.signal}),n(`api.rotation.startFailed`));return!i||typeof i.key!=`string`||!i.key||typeof i.rotationId!=`string`||!i.rotationId?!1:(j({id:t,key:i.key,rotationId:i.rotationId}),Y(),!0)}catch{return!1}finally{r.clear()}},se=async(t,n,r)=>{m(null);let i=Vn(E_);try{return(await fetch(`${e}${r===`commit`?`/api/keys/rotate/commit`:`/api/keys/rotate`}`,{method:r===`commit`?`POST`:`DELETE`,headers:{"Content-Type":`application/json`},body:JSON.stringify({id:t,rotationId:n}),signal:i.signal})).ok?(j(e=>e?.id===t?null:e),Y(),!0):!1}catch{return!1}finally{i.clear()}},ce=async()=>{if(A)try{await navigator.clipboard.writeText(A.key),N(!0),window.setTimeout(()=>N(!1),2e3)}catch{m(n(`api.key.copyFailed`))}},le=async()=>{if(E){m(null);try{await navigator.clipboard.writeText(E),k(!0),window.setTimeout(()=>k(!1),2e3)}catch{k(!1),m(n(`api.key.copyFailed`))}}},ue=async e=>{try{await navigator.clipboard.writeText(e),y(e),window.setTimeout(()=>y(t=>t===e?null:t),2e3)}catch{}},de=e=>e.native?n(`api.sourceNative`):e.provider===`combo`?n(`api.sourceCombo`):e.custom?n(`api.sourceCustom`):jn(e.provider,n),fe=e=>n(e===`responses`?`api.protocolResponses`:e===`messages`?`api.protocolMessages`:`api.protocolChatCompletions`),pe=(e,t)=>e===`responses`?{url:W.responses,body:{model:t,input:`ping`,max_output_tokens:1,stream:!1}}:e===`messages`?{url:W.messages,body:{model:t,max_tokens:1,messages:[{role:`user`,content:`ping`}]}}:{url:W.chatCompletions,body:{model:t,messages:[{role:`user`,content:`ping`}],max_tokens:1,stream:!1}},X=(e,t,n)=>x(r=>({...r,[e]:{...r[e],[t]:n}})),me=async(e,t)=>{if(!E)return;let r=a_(e),i=pe(t,r);X(r,t,{state:`testing`});try{let e=await fetch(i.url,{method:`POST`,headers:{"Content-Type":`application/json`,"x-opencodex-api-key":E},body:JSON.stringify(i.body)});if(!e.ok){let n=await e.text();X(r,t,{state:`error`,detail:n.slice(0,160)||String(e.status)});return}X(r,t,{state:`ok`})}catch(e){X(r,t,{state:`error`,detail:e instanceof Error?e.message:n(`api.testFailed`)})}},he=n(`api.subtitle`).split(`{authHeader}`);return(0,J.jsxs)(`section`,{className:`api-page`,"aria-busy":z.refreshing||B.refreshing||void 0,children:[(0,J.jsx)(`div`,{className:`page-head`,children:(0,J.jsx)(`h2`,{children:n(`api.title`)})}),(0,J.jsxs)(`p`,{className:`page-sub`,children:[he[0],(0,J.jsx)(`code`,{children:`x-opencodex-api-key`}),he[1]]}),p&&(0,J.jsx)($,{tone:`err`,children:p}),z.showError&&V&&(0,J.jsx)($,{tone:`err`,children:n(`api.keysLoadFailed`)}),z.showSkeleton&&!V?(0,J.jsx)(gl,{label:n(`api.activeKeysLoading`),rows:4}):z.kind===`failed-cold`&&!V?(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)($,{tone:`err`,children:z.error instanceof Error?z.error.message:n(`api.keysLoadFailed`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:()=>Y(),children:n(`common.retry`)})]}):(0,J.jsx)(J.Fragment,{children:(0,J.jsx)(w_,{keys:U,apiBase:e,attributionSince:G,historyTruncated:K,authMatrix:q,keysLoading:!1,keysLoadFailed:z.showError,endpoints:W,claudeCodeEnabled:ee,localeTag:i,newName:S,creating:w,newKey:E,copied:O,rotationSecret:A,rotationCopied:M,filteredModels:ne,modelsLoading:B.showSkeleton&&!B.data&&!f,modelsRefreshing:B.refreshing&&B.showError&&(B.data!==void 0||f!==null),modelsLoadFailed:B.showError,modelCount:H.length,hasModelData:B.data!==void 0||f!==null,modelQuery:h,copiedModelId:v,modelTests:b,onNewNameChange:C,onCreate:()=>{re()},onDismissNewKey:()=>D(null),onCopyKey:()=>{le()},onDelete:ie,onRename:ae,...qg()?{onRotationStart:oe,onRotationCommit:(e,t)=>se(e,t,`commit`),onRotationAbort:(e,t)=>se(e,t,`abort`),onCopyRotationSecret:()=>{ce()},onDismissRotationSecret:()=>j(null)}:{},onModelQueryChange:g,onCopyModelId:e=>{ue(e)},onTestModel:(e,t)=>{me(e,t)},onRetryModels:()=>{te({forceLoading:!0})},canTestModels:E!==null,sourceLabel:de,protocolLabel:fe})})]})}function j_(e,t){let n=(e??[]).map(e=>({value:e,label:fl(e)}));return[{value:``,label:t},...n]}function M_(e){let t=e.autoConnectSupported===!0;return{autoConnectSupported:t,systemEnv:t&&e.systemEnv===!0}}function N_(){if(typeof crypto<`u`&&typeof crypto.randomUUID==`function`)try{return crypto.randomUUID()}catch{}let e=new Uint8Array(16);if(typeof crypto<`u`&&typeof crypto.getRandomValues==`function`)crypto.getRandomValues(e);else for(let t=0;t<16;t++)e[t]=Math.floor(Math.random()*256);e[6]=e[6]&15|64,e[8]=e[8]&63|128;let t=Array.from(e,e=>e.toString(16).padStart(2,`0`)).join(``);return`${t.slice(0,8)}-${t.slice(8,12)}-${t.slice(12,16)}-${t.slice(16,20)}-${t.slice(20)}`}var P_=829800;function F_(e,t=`en`){if(e>=1e6){let n=e/1e6,r=n.toFixed(1).replace(/\.0$/,``);return Number.isInteger(n)||Number(r)*1e6===e?new Intl.NumberFormat(t,{notation:`compact`,compactDisplay:`short`,maximumFractionDigits:+!Number.isInteger(n)}).format(e):`${Math.round(e/1e3)}k`}return`${Math.round(e/1e3)}k`}var I_=[`ANTHROPIC_MODEL`,`ANTHROPIC_DEFAULT_OPUS_MODEL`,`ANTHROPIC_DEFAULT_SONNET_MODEL`,`ANTHROPIC_DEFAULT_HAIKU_MODEL`,`ANTHROPIC_DEFAULT_FABLE_MODEL`];function L_(e){let t=`http://127.0.0.1:${e.port}`,n=e.authMode===`auto`?e.markerMode??`subscription`:e.authMode,r=e.autoContext&&e.maxContextTokens===null,i=I_.filter(t=>e.effectiveModelEnv[t]).map(t=>`export ${t}=${e.effectiveModelEnv[t]}`);return[`export ANTHROPIC_BASE_URL=${t}`,...n===`proxy`?[`export ANTHROPIC_AUTH_TOKEN=opencodex-proxy`]:[`# no ANTHROPIC_AUTH_TOKEN: your claude.ai login (and connectors) stay active`],`export CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY=1`,...n===`proxy`?['[ -z "${CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST+x}" ] && export CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST=1']:[],...r?[`export CLAUDE_CODE_AUTO_COMPACT_WINDOW=${e.autoCompactWindow??829800}`]:[],...i,`claude`].join(` -`)}function R_(e){return e?e.backend??`auto`:`inherit`}function z_(e,t){if(t!==`inherit`)return t===`auto`?{...e,backend:void 0}:{...e,backend:t}}function B_(e,t){return{...e,model:t}}function V_(e){if(!e)return null;let t=(e.model??``).trim();return e.backend?{backend:e.backend,model:t}:t?{backend:null,model:t}:null}function H_({label:e,checked:t,onChange:n,disabled:r=!1,describedBy:i}){return(0,J.jsxs)(`label`,{className:`toggle`,children:[(0,J.jsx)(`input`,{type:`checkbox`,checked:t,disabled:r,"aria-label":e,"aria-describedby":i,onChange:e=>n(e.target.checked)}),(0,J.jsx)(`span`,{className:`slider`,"aria-hidden":`true`})]})}function U_({supported:e,checked:t,onChange:n}){let r=Q(),i=e?void 0:`claude-system-env-unsupported`;return(0,J.jsxs)(`div`,{className:`setting-row`,children:[(0,J.jsxs)(`div`,{className:`setting-label`,children:[(0,J.jsx)(`span`,{className:`title`,children:r(`claude.systemEnv`)}),e?(0,J.jsx)(`span`,{className:`desc`,children:r(`claude.systemEnvDesc`)}):(0,J.jsx)(`span`,{className:`desc`,id:i,children:(0,J.jsx)(ut,{k:`claude.systemEnvUnsupported`,cmd:`ocx claude`})}),e&&t&&(0,J.jsx)(`span`,{className:`desc`,style:{color:`var(--red)`},children:r(`claude.systemEnvWarn`)})]}),(0,J.jsx)(H_,{label:r(`claude.systemEnv`),checked:e&&t,disabled:!e,describedBy:i,onChange:n})]})}function W_({value:e,tierHaikuModel:t,options:n,onChange:r}){let i=Q(),a=t??e;return(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)(`p`,{className:`muted text-label`,style:{margin:`0 0 8px`},children:i(`claude.smallFastModelAccurateHint`)}),(0,J.jsx)(Dt,{value:e,options:n,onChange:r,label:i(`claude.smallFastModel`),style:{maxWidth:420}}),a===``&&(0,J.jsx)(`p`,{className:`notice-warn`,role:`status`,style:{marginTop:8},children:i(`claude.smallFastModelNativeWarning`)})]})}function G_(e,t){return e&&[`claude-json-oauth`,`claude-credentials-file`,`macos-keychain`,`exported-env`].includes(e)?t(`claude.authSource.${e}`):t(`claude.authSource.unknown`)}function K_({state:e,autoCompactOptions:t,availableModels:n,onStateChange:r}){let i=Q();return(0,J.jsxs)(`div`,{className:`card`,style:{overflow:`hidden`},children:[(0,J.jsxs)(`div`,{className:`setting-row`,children:[(0,J.jsxs)(`div`,{className:`setting-label`,children:[(0,J.jsx)(`span`,{className:`title`,children:i(`claude.authMode`)}),(0,J.jsx)(`span`,{className:`desc`,children:i(`claude.authModeHint`)})]}),(0,J.jsx)(`div`,{className:`setting-controls`,children:(0,J.jsx)(Dt,{value:e.authMode,options:[{value:`auto`,label:i(`claude.authModeAuto`)},{value:`subscription`,label:i(`claude.authModeSubscription`)},{value:`proxy`,label:i(`claude.authModeProxy`)}],onChange:t=>r({...e,authMode:t}),label:i(`claude.authMode`),style:{minWidth:220},align:`right`,portal:!0})})]}),e.authModeOrigin&&(0,J.jsxs)(`div`,{className:`claude-effective-auth${e.authModeOrigin===`auto-unknown`?` warn`:``}`,role:`status`,children:[(0,J.jsx)(`span`,{className:`claude-effective-auth-label`,children:i(`claude.effectiveMode.label`)}),(0,J.jsxs)(`span`,{children:[e.authModeOrigin===`manual`?i(`claude.effectiveMode.manual`,{mode:e.markerMode===`proxy`?i(`claude.authModeProxy`):i(`claude.authModeSubscription`)}):e.authModeOrigin===`auto-present`?i(`claude.effectiveMode.autoPresent`,{source:G_(e.authFoundBy,i)}):e.authModeOrigin===`auto-absent`?i(`claude.effectiveMode.autoAbsent`):i(`claude.effectiveMode.autoUnknown`),e.admissionKeyActive===!0?` ${i(`claude.effectiveMode.admissionKey`)}`:``]})]}),(0,J.jsx)(U_,{supported:e.autoConnectSupported,checked:e.systemEnv,onChange:t=>r({...e,systemEnv:t})}),(0,J.jsxs)(`div`,{className:`setting-row`,children:[(0,J.jsxs)(`div`,{className:`setting-label`,children:[(0,J.jsx)(`span`,{className:`title`,children:i(`claude.fastMode`)}),(0,J.jsx)(`span`,{className:`desc`,children:i(`claude.fastModeDesc`)})]}),(0,J.jsx)(`div`,{className:`setting-controls`,children:(0,J.jsx)(Dt,{value:e.fastMode===null?`auto`:e.fastMode?`on`:`off`,options:[{value:`auto`,label:i(`claude.fastAuto`)},{value:`on`,label:i(`claude.fastOn`)},{value:`off`,label:i(`claude.fastOff`)}],onChange:t=>r({...e,fastMode:t===`auto`?null:t===`on`}),label:i(`claude.fastMode`),style:{minWidth:140},align:`right`,portal:!0})})]}),(0,J.jsxs)(`div`,{className:`setting-row`,children:[(0,J.jsxs)(`div`,{className:`setting-label`,children:[(0,J.jsx)(`span`,{className:`title`,children:i(`claude.autoContext`)}),(0,J.jsx)(`span`,{className:`desc`,children:i(`claude.autoContextDesc`)}),e.maxContextTokens!==null&&(0,J.jsx)(`span`,{className:`desc`,style:{color:`var(--muted)`},children:i(`claude.autoContextInert`)})]}),(0,J.jsx)(H_,{label:i(`claude.autoContext`),checked:e.autoContext,onChange:t=>r({...e,autoContext:t})})]}),e.autoContext&&(0,J.jsxs)(`div`,{className:`setting-row`,children:[(0,J.jsxs)(`div`,{className:`setting-label`,children:[(0,J.jsx)(`span`,{className:`title`,children:i(`claude.autoCompactWindow`)}),(0,J.jsx)(`span`,{className:`desc`,children:i(`claude.autoCompactWindowDesc`)}),e.autoCompactWindow!==null&&(0,J.jsx)(`span`,{className:`desc`,style:{color:`var(--red)`},children:i(`claude.autoCompactWindowWarn`)})]}),(0,J.jsx)(`div`,{className:`setting-controls`,children:(0,J.jsx)(Dt,{value:e.autoCompactWindow===null?``:String(e.autoCompactWindow),options:t,onChange:t=>r({...e,autoCompactWindow:t===``?null:Number(t)}),label:i(`claude.autoCompactWindow`),style:{minWidth:130},align:`right`,portal:!0})})]}),(0,J.jsxs)(`div`,{className:`setting-row`,children:[(0,J.jsxs)(`div`,{className:`setting-label`,children:[(0,J.jsx)(`span`,{className:`title`,children:i(`claude.injectAgents`)}),(0,J.jsx)(`span`,{className:`desc`,children:i(`claude.injectAgentsDesc`)})]}),(0,J.jsx)(H_,{label:i(`claude.injectAgents`),checked:e.injectAgents,onChange:t=>r({...e,injectAgents:t})})]}),[`webSearchSidecar`,`visionSidecar`].map(t=>{let a=e[t],o=t===`webSearchSidecar`?`claude.webSearchSidecar`:`claude.visionSidecar`,s=t===`webSearchSidecar`?`claude.webSearchSidecarHint`:`claude.visionSidecarHint`,c=`claude-sidecar-models-${t}`;return(0,J.jsxs)(`div`,{className:`setting-row`,style:{alignItems:`flex-start`},children:[(0,J.jsxs)(`div`,{className:`setting-label setting-copy`,style:{flex:1},children:[(0,J.jsx)(`span`,{className:`title`,children:i(o)}),(0,J.jsx)(`span`,{className:`desc`,children:i(s)})]}),(0,J.jsxs)(`div`,{className:`setting-controls`,style:{display:`flex`,gap:8},children:[(0,J.jsx)(Dt,{value:R_(a),options:[{value:`inherit`,label:i(`claude.useMainSetting`)},{value:`auto`,label:i(`dash.backendAuto`)},{value:`openai`,label:i(`dash.backendOpenAI`)},{value:`anthropic`,label:i(`dash.backendAnthropic`)}],onChange:n=>{r({...e,[t]:z_(a,n)})},label:i(`dash.sidecarBackend`),portal:!0}),(0,J.jsx)(`input`,{className:`input mono`,value:a?.model??``,onChange:n=>{r({...e,[t]:B_(a,n.target.value)})},placeholder:i(`claude.sidecarModelPlaceholder`),disabled:!a,list:a?c:void 0,"aria-label":i(`dash.sidecarModel`),style:{minWidth:210},autoComplete:`off`}),a&&(0,J.jsx)(`datalist`,{id:c,children:n.map(e=>(0,J.jsx)(`option`,{value:e},e))})]})]},t)})]})}function q_({manualEnv:e}){let t=Q();return(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)(`p`,{className:`muted text-label`,style:{margin:`0 0 8px`},children:(0,J.jsx)(ut,{k:`claude.quickstartHint`,cmd:`ocx claude`})}),(0,J.jsx)(`pre`,{className:`mono card`,style:{padding:`10px 14px`,overflowX:`auto`,margin:0},children:`ocx claude`}),(0,J.jsxs)(`details`,{style:{margin:`10px 0 0`},children:[(0,J.jsx)(`summary`,{className:`muted text-label`,style:{cursor:`pointer`,padding:`2px 2px`},children:t(`claude.manualEnv`)}),(0,J.jsx)(`pre`,{className:`mono card text-label`,style:{padding:`10px 14px`,overflowX:`auto`,margin:`6px 0 0`},children:e})]})]})}function J_({rows:e,onRowsChange:t}){let n=Q();return(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)(`p`,{className:`muted text-label`,style:{margin:`0 0 8px`},children:n(`claude.modelMapHint`)}),(0,J.jsx)(`div`,{className:`stack`,style:{gap:8},children:e.map((r,i)=>(0,J.jsxs)(`div`,{className:`row`,style:{gap:8},children:[(0,J.jsx)(`input`,{className:`input mono`,value:r.from,placeholder:n(`claude.mapFrom`),"aria-label":n(`claude.mapFrom`),onChange:n=>t(e.map((e,t)=>t===i?{...e,from:n.target.value}:e)),style:{flex:1}}),(0,J.jsx)(`span`,{className:`muted`,"aria-hidden":!0,children:`→`}),(0,J.jsx)(`input`,{className:`input mono`,value:r.to,placeholder:n(`claude.mapTo`),"aria-label":n(`claude.mapTo`),onChange:n=>t(e.map((e,t)=>t===i?{...e,to:n.target.value}:e)),style:{flex:1}}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-icon btn-sm`,onClick:()=>t(e.filter((e,t)=>t!==i)),"aria-label":n(`claude.removeMapping`),style:{color:`var(--red)`},children:(0,J.jsx)(de,{})})]},r.id))}),(0,J.jsx)(`div`,{style:{marginTop:8},children:(0,J.jsxs)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:()=>t([...e,{id:N_(),from:``,to:``}]),children:[(0,J.jsx)(fe,{}),` `,n(`claude.addMapping`)]})})]})}var Y_=`etc`;function X_(e){let t=new Map;for(let n of e){let e=/\(([^)]+)\)\s*$/.exec(n.display_name),r=e?e[1]:Y_,i=t.get(r);i?i.push(n):t.set(r,[n])}return Array.from(t)}function Z_({aliases:e}){let t=Q();return(0,J.jsxs)(`div`,{className:`claude-aliases`,children:[(0,J.jsx)(`p`,{className:`muted text-label claude-aliases-hint`,children:t(`claude.aliasesHint`)}),e.length===0?(0,J.jsx)(`div`,{className:`muted text-label`,children:t(`claude.none`)}):(0,J.jsx)(`div`,{className:`claude-aliases-scroll`,children:X_(e).map(([e,n])=>(0,J.jsxs)(`div`,{className:`claude-aliases-group`,children:[(0,J.jsxs)(`div`,{className:`claude-aliases-group-label`,children:[e===Y_?t(`claude.aliasProviderOther`):e,(0,J.jsx)(`span`,{className:`claude-aliases-group-count`,children:n.length})]}),(0,J.jsx)(`div`,{className:`claude-aliases-chips`,children:n.map(e=>(0,J.jsxs)(`span`,{className:`claude-aliases-chip`,children:[(0,J.jsx)(`code`,{className:`claude-aliases-chip-id`,children:e.id}),e.display_name?(0,J.jsx)(`span`,{className:`claude-aliases-chip-name`,children:e.display_name}):null]},e.id))})]},e))})]})}function Q_({apiBase:e,active:t=!0}){let n=Q(),{locale:r}=ct(),i=et.find(e=>e.code===r)?.htmlLang??`en`,a=`ocx.claude-code.v1:${e}`,o=`claude-code:${e}`,s=(0,_.useMemo)(()=>vr(a),[a]),c=s?.data??null,[l,u]=(0,_.useState)(()=>c?.state??null),[d,f]=(0,_.useState)(()=>c?.rows??[]),[p,m]=(0,_.useState)(!!c),[h,g]=(0,_.useState)(``),[v,y]=(0,_.useState)(!1),[b,x]=(0,_.useState)(`settings`),[S,C]=(0,_.useState)(!1),w=(0,_.useRef)(!1),T=(0,_.useCallback)(async t=>{let r=await Pt(await fetch(`${e}/api/claude-code`,{signal:t}),n(`claude.loadFail`));if(!r)throw Error(n(`claude.loadFail`));let i={...r,authMode:r.authMode===`proxy`||r.authMode===`subscription`?r.authMode:`auto`,...M_(r),fastMode:r.fastMode??null,maxContextTokens:r.maxContextTokens??null,autoContext:r.autoContext!==!1,autoCompactWindow:r.autoCompactWindow??null,injectAgents:r.injectAgents!==!1,effectiveModelEnv:r.effectiveModelEnv??{}},o=Object.entries(r.modelMap??{}).map(([e,t])=>({id:N_(),from:e,to:String(t)})),s={state:i,rows:o};if(t.aborted)throw Error(`Claude Code request aborted`);return u(i),f(o),m(!0),yr(a,s),s},[e,a,n]),E=ml(o,[e],T,{isEmpty:()=>!1,enabled:t,initialData:c??void 0,initialDataCachedAt:s?.cachedAt??null,staleAfterMs:6e4}),D=E.state,O=D.data??c,k=l??O?.state??null,A=p?d:O?.rows??d,j=(0,_.useMemo)(()=>j_(k?.available,n(`claude.smallFastModelUnsetOption`)),[k?.available,n]),M=(0,_.useMemo)(()=>{let e=[1e5,2e5,25e4,3e5,35e4,4e5,5e5,6e5,75e4,P_,9e5,1e6].sort((e,t)=>e-t),t=k?.autoCompactWindow??null,r=t!==null&&!e.includes(t)?[...e,t].sort((e,t)=>e-t):e;return[{value:``,label:n(`claude.autoCompactDefault`,{value:F_(P_,i)})},...r.map(e=>({value:String(e),label:F_(e,i)}))]},[k?.autoCompactWindow,n,i]),N=async()=>{if(!k||w.current)return;w.current=!0,C(!0),g(``);let t=!k.enabled;try{await Pt(await fetch(`${e}/api/claude-code`,{method:`PUT`,headers:{"Content-Type":`application/json`},body:JSON.stringify({enabled:t})}),n(`claude.saveFailed`)),u({...k,enabled:t}),E.refresh()}catch(e){y(!1),g(e instanceof Error&&e.message?e.message:n(`claude.networkError`))}finally{w.current=!1,C(!1)}},P=async()=>{if(!k)return;g(``);let t={};for(let e of A)e.from.trim()&&e.to.trim()&&(t[e.from.trim()]=e.to.trim());try{await Pt(await fetch(`${e}/api/claude-code`,{method:`PUT`,headers:{"Content-Type":`application/json`},body:JSON.stringify({enabled:k.enabled,authMode:k.authMode,systemEnv:k.systemEnv,fastMode:k.fastMode,autoContext:k.autoContext,autoCompactWindow:k.autoCompactWindow,injectAgents:k.injectAgents,smallFastModel:k.smallFastModel,modelMap:t,webSearchSidecar:V_(k.webSearchSidecar),visionSidecar:V_(k.visionSidecar)})}),n(`claude.saveFailed`)),y(!0),g(n(`claude.saved`)),E.refresh()}catch(e){y(!1),g(e instanceof Error&&e.message?e.message:n(`claude.networkError`))}};if(D.kind===`disabled`&&!O)return null;if(D.showSkeleton&&!O)return(0,J.jsx)(gl,{label:n(`claude.loading`),rows:3});if(D.kind===`failed-cold`){let e=D.error instanceof Error?D.error.message:n(`claude.loadFail`);return(0,J.jsxs)(`div`,{className:`claudecode-workspace-shell`,children:[(0,J.jsx)($,{tone:`err`,children:e}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:()=>E.refresh(),children:n(`common.retry`)})]})}if(!k)return null;let F=[{id:`settings`,label:n(`claude.workspace.settings`),body:(0,J.jsx)(K_,{state:k,autoCompactOptions:M,availableModels:k.available??[],onStateChange:u})},{id:`quickstart`,label:n(`claude.quickstart`),body:(0,J.jsx)(q_,{manualEnv:L_(k)})},{id:`smallFast`,label:n(`claude.smallFastModel`),body:(0,J.jsx)(W_,{value:k.smallFastModel,tierHaikuModel:k.tierModels?.haiku,options:j,onChange:e=>u({...k,smallFastModel:e})})},{id:`modelMap`,label:n(`claude.modelMap`),meta:String(A.length),body:(0,J.jsx)(J_,{rows:A,onRowsChange:e=>{m(!0),f(e)}})},{id:`aliases`,label:n(`claude.aliases`),meta:String(k.aliases.length),body:(0,J.jsx)(Z_,{aliases:k.aliases})}],I=F.find(e=>e.id===b)??F[0],L=b===`settings`||b===`smallFast`||b===`modelMap`;return(0,J.jsxs)(`div`,{className:`claudecode-workspace-shell`,children:[h&&(0,J.jsx)($,{tone:v?`ok`:`err`,children:h}),D.showError&&(0,J.jsx)($,{tone:`err`,children:n(`claude.loadFail`)}),k&&(0,J.jsxs)(`div`,{className:`claudecode-connection-head`,children:[(0,J.jsx)(`span`,{id:`claudecode-connection-label`,children:n(`claude.enabledLabel`)}),(0,J.jsx)(Tt,{on:k.enabled,onClick:()=>void N(),disabled:S,label:n(`claude.toggleAria`)})]}),(0,J.jsxs)(`div`,{className:`claudecode-workspace-root`,children:[(0,J.jsx)(`aside`,{className:`claudecode-workspace-rail`,"aria-label":n(`claude.pageTitle`),children:(0,J.jsx)(`div`,{className:`claudecode-workspace-rail-list`,children:F.map(e=>(0,J.jsx)(`button`,{type:`button`,className:`claudecode-workspace-rail-row${b===e.id?` claudecode-workspace-rail-row--selected`:``}`,onClick:()=>x(e.id),"aria-current":b===e.id?`true`:void 0,children:(0,J.jsx)(`span`,{className:`claudecode-workspace-rail-name`,children:e.label})},e.id))})}),(0,J.jsxs)(`section`,{className:`claudecode-workspace-main`,"aria-label":I.label,children:[(0,J.jsxs)(`div`,{className:`ccw-main-head`,children:[(0,J.jsxs)(`h3`,{className:`ccw-main-title`,children:[I.label,I.meta==null?null:(0,J.jsx)(`span`,{className:`count`,children:I.meta})]}),(0,J.jsx)(`div`,{className:`claudecode-workspace-save`,"data-visible":L?`true`:`false`,children:(0,J.jsx)(`button`,{type:`button`,className:`btn btn-primary btn-sm`,disabled:!L,tabIndex:L?0:-1,"aria-hidden":!L,onClick:()=>{P()},children:n(`common.save`)})})]}),(0,J.jsx)(`div`,{className:`ccw-body`,children:I.body})]})]})]})}function $_(e,t,n){let r=t.trim().toLowerCase(),i=(r?e.filter(e=>e.label.toLowerCase().includes(r)||e.route.toLowerCase().includes(r)):e).toSorted((e,t)=>Number(!e.available)-Number(!t.available)),a=i.slice(0,n);return{total:e.length,showSearch:e.length>4,shown:a,hidden:i.length-a.length,noMatch:e.length>0&&i.length===0}}function ev(e){return new Set(Object.keys(e))}function tv(e,t){return t!==null&&e===t}function nv(e){return e||(typeof localStorage>`u`?void 0:localStorage)}function rv(e){return{read(t){let n=nv(t);if(!n)return null;try{let t=n.getItem(e);if(t===null)return null;let r=JSON.parse(t);return Array.isArray(r)?new Set(r.filter(e=>typeof e==`string`)):null}catch{return null}},write(t,n){let r=nv(n);if(r)try{r.setItem(e,JSON.stringify([...t]))}catch{}}}}function iv(e,t){let n=new Set(e);return n.has(t)?n.delete(t):n.add(t),n}var av=[`opus`,`fable`,`sonnet`,`haiku`],ov=rv(`ocx.claudeDesktop.collapsedFamilies.v2`),sv={opus:`claudeDesktop.family.opus`,fable:`claudeDesktop.family.fable`,sonnet:`claudeDesktop.family.sonnet`,haiku:`claudeDesktop.family.haiku`};function cv(e){return{version:1,assignments:Object.fromEntries(Object.entries(e.assignments).map(([e,t])=>[e,{...t}])),defaults:{...e.defaults},...e.appliedFingerprint===void 0?{}:{appliedFingerprint:e.appliedFingerprint},...e.appliedAt===void 0?{}:{appliedAt:e.appliedAt}}}function lv(e){let t={...e.profile.assignments};for(let n of e.models){let e=t[n.route]??n.assignment;t[n.route]={family:av.includes(e?.family)?e.family:`opus`,alias:typeof e?.alias==`string`?e.alias:``}}return{version:1,assignments:t,defaults:{opus:e.profile.defaults.opus??null,fable:e.profile.defaults.fable??null,sonnet:e.profile.defaults.sonnet??null,haiku:e.profile.defaults.haiku??null}}}function uv(e,t){return e&&typeof e==`object`&&`error`in e&&typeof e.error==`string`?e.error:t}function dv(e,t){return e?e>=1048576?t(`claudeDesktop.contextM`,{n:Math.round(e/1048576)}):e>=1e6?t(`claudeDesktop.contextM`,{n:e/1e6}):t(`claudeDesktop.contextK`,{n:Math.round(e/1e3)}):null}function fv(e){return vr(e)?.data??null}function pv(e){return vr(e)?.cachedAt??null}function mv(e){let t=fv(e);return{held:t,data:t?.data??null,profile:t?.profile??null,savedProfile:t?.profile?cv(t.profile):null,destinations:t?.data?Object.fromEntries(t.data.models.map(e=>[e.route,t.profile.assignments[e.route]?.family??`opus`])):{}}}function hv({apiBase:e,active:t=!0,onPortChange:n}){let{t:r,locale:i}=ct(),a=et.find(e=>e.code===i)?.htmlLang,o=`ocx.claude-desktop.v1:${e}`,s=`claude-desktop:${e}`,c=(0,_.useMemo)(()=>mv(o),[o]),[l,u]=(0,_.useState)(()=>c.profile),[d,f]=(0,_.useState)(()=>c.savedProfile),[p,m]=(0,_.useState)(()=>c.destinations),[h,g]=(0,_.useState)(null),[v,y]=(0,_.useState)(``),[b,x]=(0,_.useState)(null),[S,C]=(0,_.useState)({}),[w,T]=(0,_.useState)({}),[E,D]=(0,_.useState)(()=>ov.read()??new Set(av)),[O,k]=(0,_.useState)({}),A=(0,_.useRef)(null),j=(0,_.useCallback)(async t=>{let n=await Pt(await fetch(`${e}/api/claude-desktop`,{signal:t}),r(`claudeDesktop.loadFail`));if(!n||!(`profile`in n)||!(`models`in n))throw Error(uv(n,r(`claudeDesktop.loadFail`)));let i=lv(n),a={data:n,profile:i};if(t.aborted)throw Error(`Claude Desktop request aborted`);if(u(i),f(cv(i)),m(Object.fromEntries(n.models.map(e=>[e.route,i.assignments[e.route]?.family??`opus`]))),ov.read()===null){let e=Object.fromEntries(av.map(e=>[e,0]));for(let t of n.models)e[i.assignments[t.route]?.family??`opus`]+=1;D(ev(e))}return yr(o,a),a},[e,o,r,m,u,f]),M=ml(s,[e],j,{isEmpty:()=>!1,enabled:t,initialData:c.held??void 0,initialDataCachedAt:pv(o),staleAfterMs:6e4}),N=M.state,P=N.data??(c.data&&c.profile?{data:c.data,profile:c.profile}:null),F=P?.data??null,I=l??P?.profile??null,L=d??P?.profile??null,R=P?Object.fromEntries(P.data.models.map(e=>[e.route,P.profile.assignments[e.route]?.family??`opus`])):{},z=Object.keys(p).length>0?p:R;(0,_.useEffect)(()=>{if(n){if(typeof F?.port==`number`){n(F.port);return}N.kind===`failed-cold`&&n(null)}},[F?.port,N.kind,n]);let B=(0,_.useMemo)(()=>I!==null&&L!==null&&JSON.stringify(I)!==JSON.stringify(L),[I,L]),V=(0,_.useMemo)(()=>{let e=Object.fromEntries(av.map(e=>[e,[]]));if(!F||!I)return e;for(let t of F.models)e[I.assignments[t.route]?.family??`opus`].push(t);return e},[F,I]),H=(0,_.useMemo)(()=>{let e={};for(let t of av){let n=V[t].filter(e=>e.available).map(e=>e.route).sort(),r=I?.defaults[t]??null;e[t]=r&&n.includes(r)?r:n[0]??null}return e},[V,I]),U=`ocx.claude-desktop.status.v1:${e}`,W=`claude-desktop-status:${e}`,ee=vr(U),G=ml(W,[e],async t=>{let n=await Ft(await fetch(`${e}/api/claude-desktop/status`,{signal:t}));if(!n)throw Error(`Claude Desktop status unavailable`);return yr(U,n),n},{isEmpty:()=>!1,pollMs:5e3,enabled:t,initialData:ee?.data??void 0}),K=G.state,q=K.data??ee?.data??null,Y=K.showError,te=(e,t)=>{!I||I.assignments[e]?.family===t||(u(n=>{if(!n)return n;let r=n.assignments[e];if(!r||r.family===t)return n;let i={...n.assignments,[e]:{...r,family:t}},a={...n.defaults};return a[r.family]===e&&(a[r.family]=Object.keys(i).filter(t=>t!==e&&i[t].family===r.family).sort()[0]??null),a[t]===null&&(a[t]=e),{...n,assignments:i,defaults:a}}),m(n=>({...n,[e]:t})),y(r(`claudeDesktop.moved`,{route:e,family:r(sv[t])})))},ne=e=>{let t=iv(E,e);ov.write(t),D(t)},re=async t=>{if(!(!I||b)){x(`save`),g(null);try{await Pt(await fetch(`${e}/api/claude-desktop`,{method:`PUT`,headers:{"Content-Type":`application/json`},body:JSON.stringify({profile:I})}),r(`claudeDesktop.saveFailed`)),f(cv(I)),t?(x(`apply`),(await Pt(await fetch(`${e}/api/claude-desktop/apply`,{method:`POST`}),r(`claudeDesktop.applyFailed`)))?.saved===!1?(g({tone:`warn`,text:r(`claudeDesktop.appliedMarkerUnsaved`)}),y(r(`claudeDesktop.appliedMarkerUnsaved`))):(g({tone:`ok`,text:r(`claudeDesktop.savedApplied`)}),y(r(`claudeDesktop.savedAppliedAnnounce`)))):(g({tone:`ok`,text:r(`claudeDesktop.saved`)}),y(r(`claudeDesktop.savedAnnounce`))),G.refresh()}catch(e){let t=e instanceof Error?e.message:r(`claudeDesktop.updateFailed`);g({tone:`err`,text:t}),y(t)}finally{x(null)}}},ie=()=>{if(!I)return;let e=URL.createObjectURL(new Blob([`${JSON.stringify(I,null,2)}\n`],{type:`application/json`})),t=document.createElement(`a`);t.href=e,t.download=`claude-desktop-profile.json`,t.click(),URL.revokeObjectURL(e),y(r(`claudeDesktop.exported`))},ae=async e=>{let t=e.target.files?.[0];if(e.target.value=``,t)try{let e=JSON.parse(await t.text());if(e.version!==1||!e.assignments||!e.defaults)throw Error(r(`claudeDesktop.importExpected`));let n=lv({...F,profile:e});u(n),g({tone:`ok`,text:r(`claudeDesktop.importReady`)}),y(r(`claudeDesktop.importedAnnounce`))}catch(e){let t=e instanceof Error?e.message:r(`claudeDesktop.importInvalid`);g({tone:`err`,text:t}),y(r(`claudeDesktop.importFailed`,{error:t}))}},oe=(e,t)=>{e.preventDefault();let n=e.dataTransfer.getData(`text/plain`);n&&te(n,t)};if(N.kind===`disabled`&&!P)return null;if(N.showSkeleton&&!P)return(0,J.jsx)(gl,{label:r(`claudeDesktop.loading`),rows:4});if(N.kind===`failed-cold`){let e=N.error instanceof Error?N.error.message:r(`claudeDesktop.loadFail`);return(0,J.jsxs)(`div`,{className:`claude-desktop-error`,children:[(0,J.jsx)($,{tone:`err`,children:e}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost`,onClick:()=>M.refresh(),children:r(`claudeDesktop.retry`)})]})}return!F||!I?null:(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)(`div`,{className:`claude-desktop-toolbar`,children:(0,J.jsxs)(`div`,{className:`claude-profile-tools`,children:[(0,J.jsx)(`input`,{ref:A,type:`file`,accept:`application/json,.json`,hidden:!0,onChange:e=>void ae(e)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:()=>A.current?.click(),children:r(`claudeDesktop.importJson`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:ie,children:r(`claudeDesktop.exportJson`)})]})}),(0,J.jsxs)(`div`,{className:`claude-status-bar ${Y&&!q?`not-applied`:q?q.desiredEnabled?q.activeProfile===!1?`not-applied`:q.stale?`stale`:q.applied?`applied`:`not-applied`:`not-applied`:`pending`}`,"aria-busy":!q&&!Y||void 0,children:[(0,J.jsx)(`span`,{className:`claude-status-dot`}),(0,J.jsx)(`span`,{children:Y&&!q?r(`claudeDesktop.loadFail`):q?q.desiredEnabled?q.activeProfile===!1?r(`claudeDesktop.status.notActiveProfile`):q.stale?r(`claudeDesktop.status.stale`):q.applied?r(`claudeDesktop.status.applied`):r(`claudeDesktop.status.notApplied`):r(`claudeDesktop.status.disabled`):r(`claudeDesktop.loading`)}),q?.health.lastRequestAt&&(0,J.jsxs)(`span`,{className:`claude-status-health`,children:[r(`claudeDesktop.health.lastRequest`),`:`,` `,new Date(q.health.lastRequestAt).toLocaleTimeString(a)]}),q&&q.health.requestCount>0&&(0,J.jsx)(`span`,{className:`claude-status-health`,children:r(`claudeDesktop.health.stats`,{count:q.health.requestCount,errors:q.health.errorCount})})]}),(0,J.jsx)(`div`,{className:`sr-only`,"aria-live":`polite`,"aria-atomic":`true`,children:v}),h&&(0,J.jsx)($,{tone:h.tone,children:h.text}),N.showError&&(0,J.jsx)($,{tone:`err`,children:r(`claudeDesktop.loadFail`)}),Y&&q&&(0,J.jsx)($,{tone:`err`,children:r(`claudeDesktop.loadFail`)}),(0,J.jsxs)(`div`,{className:`claude-profile-bar`,children:[(0,J.jsx)(`span`,{className:`claude-dirty${B?` active`:``}`,children:r(B?`claudeDesktop.unsaved`:`claudeDesktop.upToDate`)}),(0,J.jsxs)(`div`,{className:`claude-save-actions`,children:[(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost`,disabled:!B||b!==null,onClick:()=>void re(!1),children:r(b===`save`?`claudeDesktop.saving`:`common.save`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-primary`,disabled:b!==null,onClick:()=>void re(!0),children:b===`apply`?r(`claudeDesktop.applying`):b===`save`?r(`claudeDesktop.saving`):q?.desiredEnabled===!1?r(`claudeDesktop.enableApply`):r(`claudeDesktop.saveApply`)})]})]}),F.models.length===0&&(0,J.jsx)(Ot,{title:r(`claudeDesktop.emptyTitle`),children:r(`claudeDesktop.emptyHint`)}),(0,J.jsx)(`div`,{className:`ocx-group-stack`,"aria-label":r(`claudeDesktop.assignmentsLabel`),children:av.map(e=>{let t=V[e],n=$_(t,S[e]??``,w[e]??6),i=E.has(e),a=H[e];return(0,J.jsxs)(`section`,{className:`ocx-group${i?` collapsed`:``}`,"aria-labelledby":`claude-lane-${e}`,onDragOver:e=>e.preventDefault(),onDrop:t=>oe(t,e),children:[(0,J.jsxs)(`header`,{className:`ocx-group-head${i?``:` open`}`,children:[(0,J.jsx)(`h3`,{id:`claude-lane-${e}`,className:`ocx-group-heading`,children:(0,J.jsxs)(`button`,{type:`button`,className:`ocx-group-toggle`,"aria-expanded":!i,"aria-controls":`claude-lane-body-${e}`,onClick:()=>ne(e),children:[(0,J.jsx)(Se,{className:`ocx-chevron`,width:14,height:14,"aria-hidden":`true`,style:{transform:i?`none`:`rotate(90deg)`}}),(0,J.jsx)(`span`,{className:`ocx-group-name`,children:r(sv[e])}),(0,J.jsx)(`span`,{className:`ocx-group-count`,children:r(t.length===1?`claudeDesktop.modelCountOne`:`claudeDesktop.modelCountMany`,{count:t.length})}),a&&(0,J.jsx)(`code`,{className:`claude-lane-default`,title:a,children:a})]})}),t.length>0&&I.defaults[e]===null&&(0,J.jsx)(`span`,{className:`claude-default-needed`,children:r(`claudeDesktop.chooseDefault`)}),a&&a!==I.defaults[e]&&(0,J.jsx)(`span`,{className:`claude-default-needed`,title:a,children:r(`claudeDesktop.temporaryDefault`)})]}),!i&&(0,J.jsxs)(`div`,{id:`claude-lane-body-${e}`,children:[n.showSearch&&(0,J.jsx)(`input`,{className:`input claude-lane-search`,type:`search`,placeholder:r(`models.search`),"aria-label":r(`models.search`),value:S[e]??``,onChange:t=>{let n=t.target.value;C(t=>({...t,[e]:n})),T(t=>({...t,[e]:6}))}}),(0,J.jsxs)(`div`,{className:`claude-lane-models`,children:[t.length===0?(0,J.jsx)(`div`,{className:`claude-lane-empty`,children:r(`claudeDesktop.laneEmpty`)}):n.noMatch?(0,J.jsx)(`div`,{className:`claude-lane-empty`,children:r(`claudeDesktop.laneNoMatch`)}):n.shown.map(t=>{let n=I.assignments[t.route],i=dv(t.contextWindow,r),a=z[t.route]??`opus`,o=O[t.route]??tv(t.route,H[e]);return(0,J.jsxs)(`article`,{className:`claude-model-card${o?` open`:``}`,draggable:t.available,onDragStart:e=>{e.dataTransfer.effectAllowed=`move`,e.dataTransfer.setData(`text/plain`,t.route)},children:[(0,J.jsxs)(`button`,{type:`button`,className:`claude-model-summary`,"aria-expanded":o,"aria-controls":`claude-model-body-${t.route}`,onClick:()=>k(e=>({...e,[t.route]:!o})),children:[(0,J.jsx)(Se,{className:`ocx-chevron`,width:12,height:12,"aria-hidden":`true`,style:{transform:o?`rotate(90deg)`:`none`}}),(0,J.jsxs)(`span`,{className:`claude-model-names`,children:[(0,J.jsx)(`strong`,{title:t.label,children:t.label}),(0,J.jsx)(`code`,{title:t.route,children:t.route})]}),i&&(0,J.jsx)(`span`,{className:`claude-model-context`,children:i}),!i&&(0,J.jsx)(`span`,{className:`claude-model-context claude-model-context-unknown`,children:r(`claudeDesktop.contextUnknown`)}),t.supports1m===!0&&(0,J.jsx)(`span`,{className:`claude-1m-chip`,children:r(`claudeDesktop.supports1m`)}),t.effortSupported===!1&&(0,J.jsx)(`span`,{className:`claude-effort-badge off`,children:r(`claudeDesktop.effort.displayOnly`)}),t.effortSupported===!0&&(0,J.jsx)(`span`,{className:`claude-effort-badge on`,children:r(`claudeDesktop.effort.supported`)}),I.defaults[e]===t.route&&(0,J.jsx)(`span`,{className:`claude-row-default`,children:r(`claudeDesktop.defaultBadge`)}),(0,J.jsx)(`span`,{className:`badge ${t.available?`badge-green`:`badge-muted`}`,children:t.available?r(`claudeDesktop.available`):r(`claudeDesktop.unavailable`)})]}),o&&(0,J.jsxs)(`div`,{className:`claude-model-body`,id:`claude-model-body-${t.route}`,children:[H[e]===t.route&&I.defaults[e]!==t.route&&(0,J.jsx)(`span`,{className:`claude-effective-default`,children:r(`claudeDesktop.temporaryDefault`)}),(0,J.jsxs)(`div`,{className:`claude-field`,children:[(0,J.jsx)(`span`,{children:r(`claudeDesktop.alias`)}),(0,J.jsx)(`code`,{className:`claude-alias`,title:n.alias,children:n.alias})]}),(0,J.jsxs)(`label`,{className:`claude-default-radio`,children:[(0,J.jsx)(`input`,{type:`radio`,name:`default-${e}`,checked:I.defaults[e]===t.route,disabled:!t.available,onChange:()=>u(n=>n&&{...n,defaults:{...n.defaults,[e]:t.route}})}),r(`claudeDesktop.useAsDefault`,{family:r(sv[e])})]}),(0,J.jsxs)(`div`,{className:`claude-move-row`,children:[(0,J.jsx)(`label`,{htmlFor:`move-${t.route}`,children:r(`claudeDesktop.moveTo`)}),(0,J.jsx)(`select`,{id:`move-${t.route}`,className:`input`,value:a,disabled:!t.available,onChange:e=>m(n=>({...n,[t.route]:e.target.value})),children:av.map(e=>(0,J.jsx)(`option`,{value:e,children:r(sv[e])},e))}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,disabled:!t.available||a===e,onClick:()=>te(t.route,a),children:r(`claudeDesktop.move`)})]})]})]},t.route)}),n.hidden>0&&(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm claude-lane-more`,onClick:()=>T(t=>({...t,[e]:(t[e]??6)+6})),children:r(`models.showMore`,{n:n.hidden})})]})]})]},e)})})]})}var gv=`integrations/claude`,_v=`integrations/claude/desktop`;function vv(e=typeof window<`u`?window.location.hash:``){return dt(e)===_v?`desktop`:`code`}function yv(e){let t=gr(`ocx.claude-desktop.v1:${e}`);return typeof t?.data?.port==`number`?t.data.port:null}function bv({apiBase:e,active:t=!0}){let[n,r]=(0,_.useState)(vv),i=Q(),a=(0,_.useRef)(null),o=(0,_.useRef)(null),s=yv(e),[c,l]=(0,_.useState)(null),u=c?.base===e?c.port:s,d=c?.base===e,f=(0,_.useCallback)(t=>{l(n=>n?.base===e&&n.port===t?n:{base:e,port:t})},[e]);(0,_.useEffect)(()=>{let e=()=>r(vv());return window.addEventListener(`hashchange`,e),window.addEventListener(`popstate`,e),()=>{window.removeEventListener(`hashchange`,e),window.removeEventListener(`popstate`,e)}},[]);let p=e=>{pt(e===`desktop`?_v:gv),r(e),window.requestAnimationFrame(()=>{(e===`code`?a:o).current?.focus({preventScroll:!0})})},m=e=>{e.key===`ArrowLeft`||e.key===`ArrowRight`?(e.preventDefault(),p(n===`code`?`desktop`:`code`)):e.key===`Home`?(e.preventDefault(),p(`code`)):e.key===`End`&&(e.preventDefault(),p(`desktop`))};return(0,J.jsxs)(`section`,{className:`claude-page`,children:[(0,J.jsxs)(`div`,{className:`claude-page-intro`,children:[(0,J.jsx)(`div`,{className:`page-head`,children:(0,J.jsx)(`h2`,{children:i(n===`code`?`claude.pageTitle`:`claudeDesktop.title`)})}),n===`code`?(0,J.jsx)(`p`,{className:`page-sub`,children:i(`claude.subtitle`)}):(0,J.jsx)(`p`,{className:`page-sub`,children:u==null?i(d?`claudeDesktop.loadFail`:`claudeDesktop.loading`):i(`claudeDesktop.subtitle`,{port:u})})]}),(0,J.jsxs)(`div`,{className:`claude-tabs`,role:`tablist`,"aria-label":i(`claude.tabsLabel`),children:[(0,J.jsx)(`button`,{type:`button`,role:`tab`,ref:a,"aria-selected":n===`code`,"aria-controls":`claude-code-panel`,id:`claude-code-tab`,className:n===`code`?`active`:``,tabIndex:n===`code`?0:-1,onKeyDown:m,onClick:()=>p(`code`),children:i(`claude.tabCode`)}),(0,J.jsx)(`button`,{type:`button`,role:`tab`,ref:o,"aria-selected":n===`desktop`,"aria-controls":`claude-desktop-panel`,id:`claude-desktop-tab`,className:n===`desktop`?`active`:``,tabIndex:n===`desktop`?0:-1,onKeyDown:m,onClick:()=>p(`desktop`),children:i(`claude.tabDesktop`)})]}),(0,J.jsx)(`div`,{id:`claude-code-panel`,role:`tabpanel`,"aria-labelledby":`claude-code-tab`,hidden:n!==`code`,children:(0,J.jsx)(Q_,{apiBase:e,active:t&&n===`code`},e)}),(0,J.jsx)(`div`,{id:`claude-desktop-panel`,role:`tabpanel`,"aria-labelledby":`claude-desktop-tab`,hidden:n!==`desktop`,children:(0,J.jsx)(hv,{apiBase:e,active:t&&n===`desktop`,onPortChange:f},e)})]})}function xv(e,t,n,r){let i=e.filter(e=>r===`native`===e.native).map(e=>({...e,alias:t.get(e.id)??null,enabled:!n.has(e.id)})).toSorted((e,t)=>Number(!e.enabled)-Number(!t.enabled));return{rows:i,total:i.length,enabled:i.filter(e=>e.enabled).length}}var Sv=rv(`ocx.grok.collapsedGroups.v2`),Cv=[{id:`native`,tkey:`grok.groupNative`},{id:`routed`,tkey:`grok.groupRouted`}],wv=new Set(Cv.map(e=>e.id));function Tv(e,t){return e?e>=1048576?t(`claudeDesktop.contextM`,{n:Math.round(e/1048576)}):e>=1e6?t(`claudeDesktop.contextM`,{n:e/1e6}):t(`claudeDesktop.contextK`,{n:Math.round(e/1e3)}):`—`}function Ev({apiBase:e,active:t=!0}){let n=Q(),r=`ocx.grok.status.v1:${e}`,i=vr(r),a=i?.data??null,[o,s]=(0,_.useState)(null),[c,l]=(0,_.useState)(()=>Sv.read()??new Set(wv)),[u,d]=(0,_.useState)(null),[f,p]=(0,_.useState)(null),[m,h]=(0,_.useState)(``),g=(0,_.useCallback)(async t=>{let i=await Pt(await fetch(`${e}/api/grok`,{signal:t}),n(`grok.loadFail`));if(!i)throw Error(n(`grok.loadFail`));let a={...i,candidates:i.candidates??[],excluded:i.excluded??[]};return yr(r,a),a},[e,r,n]),v=`grok-status:${e}`,y=ml(v,[e],g,{isEmpty:()=>!1,initialData:a??void 0,initialDataCachedAt:i?.cachedAt??null,staleAfterMs:6e4,enabled:t}),{state:b}=y,x=y.refresh,S=b.data??a,C=(0,_.useMemo)(()=>new Set(S?.excluded??[]),[S]),w=o??C,T=(0,_.useMemo)(()=>o!==null&&(o.size!==C.size||[...o].some(e=>!C.has(e))),[o,C]),E=(0,_.useMemo)(()=>new Map((S?.models??[]).map(e=>[e.id,e.alias])),[S]),D=e=>{let t=iv(c,e);Sv.write(t),l(t)},O=e=>{let t=e?new Set(Cv.map(e=>e.id)):new Set;Sv.write(t),l(t)},k=(e,t)=>{s(n=>{let r=new Set(n??C);return t?r.delete(e):r.add(e),r})},A=async t=>{if(!u){d(`save`),p(null);try{await Pt(await fetch(`${e}/api/grok/selection`,{method:`PUT`,headers:{"Content-Type":`application/json`},body:JSON.stringify({excluded:[...w]})}),n(`grok.saveFailed`));let i=[...w];if(S&&K(v,{...S,excluded:i}),s(null),t){d(`apply`);let t=await fetch(`${e}/api/grok/apply`,{method:`POST`});if(!t.ok){let e=await t.json().catch(()=>({}));throw Error(e.message??e.error??n(`grok.applyFailed`))}let r=await t.json().catch(()=>({}));r.skippedReason?(p({tone:`err`,text:r.message??n(`grok.applySkipped`)}),h(r.message??n(`grok.applySkipped`))):(p({tone:`ok`,text:n(`grok.savedApplied`)}),h(n(`grok.savedApplied`))),await x()}else p({tone:`ok`,text:n(`grok.saved`)}),h(n(`grok.saved`)),S&&yr(r,{...S,excluded:i})}catch(e){let t=e instanceof Error?e.message:n(`grok.saveFailed`);p({tone:`err`,text:t}),h(t)}finally{d(null)}}};if(b.showSkeleton&&!S)return(0,J.jsx)(`section`,{className:`grok-page`,children:(0,J.jsx)(gl,{label:n(`grok.loading`),rows:4})});if(b.kind===`failed-cold`){let e=b.error instanceof Error?b.error.message:n(`grok.loadFail`);return(0,J.jsxs)(`section`,{className:`grok-page`,children:[(0,J.jsx)(`div`,{className:`alert alert-err`,role:`alert`,children:e}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:()=>x(),children:n(`common.retry`)})]})}return(0,J.jsxs)(`section`,{className:`grok-page`,"aria-busy":b.refreshing||void 0,children:[(0,J.jsx)(`h2`,{className:`page-title`,children:n(`grok.title`)}),(0,J.jsx)(`p`,{className:`page-sub`,children:n(`grok.subtitle`)}),(0,J.jsx)(`div`,{className:`sr-only`,"aria-live":`polite`,"aria-atomic":`true`,children:m}),f&&(0,J.jsx)($,{tone:f.tone,children:f.text}),b.showError&&(0,J.jsx)($,{tone:`err`,children:n(`grok.loadFail`)}),S&&S.candidates.length>0&&(0,J.jsxs)(`div`,{className:`claude-profile-bar`,children:[(0,J.jsx)(`span`,{className:`claude-dirty${T?` active`:``}`,children:n(T?`grok.unsaved`:`grok.upToDate`)}),(0,J.jsxs)(`div`,{className:`claude-save-actions`,children:[(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost`,disabled:!T||u!==null,onClick:()=>void A(!1),children:n(u===`save`?`grok.saving`:`common.save`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-primary`,disabled:!T||u!==null,onClick:()=>void A(!0),children:n(u===`apply`?`grok.applying`:u===`save`?`grok.saving`:`grok.saveApply`)})]})]}),S?.present?(0,J.jsxs)(J.Fragment,{children:[(0,J.jsxs)(`div`,{className:`grok-endpoint`,children:[(0,J.jsx)(`span`,{children:n(`grok.endpoint`)}),(0,J.jsx)(`code`,{children:S.baseUrl??`—`})]}),(0,J.jsx)(`p`,{className:`page-sub`,children:(0,J.jsx)(`code`,{children:S.configPath})})]}):(0,J.jsxs)(Ot,{title:n(`grok.notConfiguredTitle`),children:[n(`grok.notConfiguredHint`),(0,J.jsx)(`br`,{}),(0,J.jsx)(`code`,{children:S?.configPath})]}),S&&S.candidates.length>0&&(0,J.jsxs)(`div`,{className:`ocx-group-stack`,children:[(0,J.jsxs)(`div`,{className:`row`,style:{gap:6,margin:`2px 0 10px`},children:[(0,J.jsxs)(`button`,{type:`button`,className:`btn btn-ghost btn-sm text-caption`,onClick:()=>O(!0),disabled:u!==null,children:[(0,J.jsx)(Se,{width:12,height:12,"aria-hidden":`true`}),` `,n(`models.collapseAll`)]}),(0,J.jsxs)(`button`,{type:`button`,className:`btn btn-ghost btn-sm text-caption`,onClick:()=>O(!1),disabled:u!==null,children:[(0,J.jsx)(Se,{width:12,height:12,"aria-hidden":`true`,style:{transform:`rotate(90deg)`}}),` `,n(`models.expandAll`)]})]}),Cv.map(e=>{let t=xv(S.candidates,E,w,e.id);if(t.total===0)return null;let r=c.has(e.id);return(0,J.jsxs)(`section`,{className:`ocx-group${r?` collapsed`:``}`,"aria-labelledby":`grok-group-${e.id}`,children:[(0,J.jsx)(`header`,{className:`ocx-group-head${r?``:` open`}`,children:(0,J.jsx)(`h3`,{id:`grok-group-${e.id}`,className:`ocx-group-heading`,children:(0,J.jsxs)(`button`,{type:`button`,className:`ocx-group-toggle`,"aria-expanded":!r,"aria-controls":`grok-group-body-${e.id}`,onClick:()=>D(e.id),children:[(0,J.jsx)(Se,{className:`ocx-chevron`,width:14,height:14,"aria-hidden":`true`,style:{transform:r?`none`:`rotate(90deg)`}}),(0,J.jsx)(`span`,{className:`ocx-group-name`,children:n(e.tkey)}),(0,J.jsx)(`span`,{className:`ocx-group-count`,children:n(`grok.enabledCount`,{on:t.enabled,total:t.total})})]})})}),!r&&(0,J.jsx)(`div`,{id:`grok-group-body-${e.id}`,className:`grok-model-list`,children:t.rows.map(e=>(0,J.jsxs)(`div`,{className:`grok-model-row`,children:[(0,J.jsx)(Tt,{on:e.enabled,onClick:()=>k(e.id,!e.enabled),disabled:u!==null,label:n(`grok.toggleModel`,{id:e.id})}),(0,J.jsxs)(`span`,{className:`grok-model-names`,children:[(0,J.jsx)(`strong`,{title:e.id,children:e.id}),(0,J.jsx)(`code`,{title:e.alias??void 0,children:e.alias??`—`})]}),(0,J.jsx)(`span`,{className:`claude-model-context`,children:Tv(e.contextWindow,n)})]},e.id))})]},e.id)})]})]})}async function Dv(e,t){try{let n=await fetch(`${e}/api/native-integrations/cursor`,{signal:t});if(!n.ok)return null;let r=await Ft(n);return!r||typeof r!=`object`||!r.gateway||!r.privateInference?null:r}catch{return null}}function Ov({value:e,label:t}){let n=Q(),[r,i]=(0,_.useState)(!1),a=(0,_.useRef)(null);(0,_.useEffect)(()=>()=>{a.current!==null&&window.clearTimeout(a.current)},[]);let o=async()=>{try{await navigator.clipboard.writeText(e),i(!0),a.current!==null&&window.clearTimeout(a.current),a.current=window.setTimeout(()=>i(!1),1500)}catch{i(!1)}};return(0,J.jsxs)(`div`,{className:`cursor-gateway-row`,children:[(0,J.jsx)(`span`,{className:`cursor-gateway-label`,children:t}),(0,J.jsx)(`code`,{className:`cursor-gateway-value`,children:e}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:()=>void o(),"aria-label":`${n(`integrations.cursor.copy`)} ${t}`,children:n(r?`integrations.cursor.copied`:`integrations.cursor.copy`)})]})}function kv({labelKey:e,installed:t,path:n,version:r}){let i=Q();return(0,J.jsxs)(`div`,{className:`cursor-detect-row`,"data-installed":t?`true`:`false`,children:[(0,J.jsx)(`span`,{className:`cursor-detect-name`,children:i(e)}),(0,J.jsx)(`span`,{className:`badge ${t?`badge-green`:`badge-muted`}`,children:i(t?`integrations.cursor.detected`:`integrations.cursor.notFound`)}),t&&n&&(0,J.jsxs)(`span`,{className:`cursor-detect-path muted`,children:[r?`${r} · `:``,n]})]})}function Av({apiBase:e,active:t}){let{t:n,locale:r}=ct(),[i,a]=(0,_.useState)(()=>Date.now()),o=(0,_.useCallback)(async t=>{let n=await Dv(e,t);if(!n)throw Error(`cursor status unavailable`);return a(Date.now()),n},[e]),s=ml(`integration-cursor-page:${e}`,[e],o,{isEmpty:()=>!1,enabled:t,pollMs:15e3,pauseWhenHidden:!0}),c=s.state.data??null,l=Ti(n);return(0,J.jsxs)(`section`,{className:`integration-native-page cursor-page`,"aria-labelledby":`cursor-integration-title`,children:[(0,J.jsx)(`h3`,{id:`cursor-integration-title`,children:n(`integrations.cursor.title`)}),(0,J.jsx)(`p`,{children:n(`integrations.cursor.intro`)}),s.state.showError&&(0,J.jsx)($,{tone:`err`,children:n(`integrations.cursor.unavailable`)}),!c&&!s.state.showError&&(0,J.jsx)(gl,{label:n(`integrations.cursor.loading`),rows:4}),c&&(0,J.jsxs)(J.Fragment,{children:[(0,J.jsxs)(`div`,{className:`cursor-card`,children:[(0,J.jsx)(`h4`,{children:n(`integrations.cursor.detection`)}),(0,J.jsx)(kv,{labelKey:`integrations.cursor.privateInference`,installed:c.privateInference.installed,path:c.privateInference.path,version:c.privateInference.version}),(0,J.jsx)(kv,{labelKey:`integrations.cursor.regular`,installed:c.regularCursor.installed,path:c.regularCursor.path,version:null}),!c.privateInference.installed&&(0,J.jsxs)($,{tone:`warn`,children:[n(c.regularCursor.installed?`integrations.cursor.regularOnly`:`integrations.cursor.nothingFound`),` `,(0,J.jsx)(`a`,{href:c.guideUrl,target:`_blank`,rel:`noreferrer`,"data-cursor-guide":`notice`,children:n(`integrations.cursor.guide`)})]})]}),(0,J.jsxs)(`div`,{className:`cursor-card`,children:[(0,J.jsx)(`h4`,{children:n(`integrations.cursor.gateway`)}),(0,J.jsx)(`p`,{className:`muted`,children:n(`integrations.cursor.gatewayHint`)}),(0,J.jsx)(Ov,{label:n(`integrations.cursor.baseUrl`),value:c.gateway.baseUrl}),c.gateway.apiKeyMode===`placeholder`?(0,J.jsx)(Ov,{label:n(`integrations.cursor.apiKey`),value:c.gateway.placeholder}):(0,J.jsxs)(`div`,{className:`cursor-gateway-row`,children:[(0,J.jsx)(`span`,{className:`cursor-gateway-label`,children:n(`integrations.cursor.apiKey`)}),(0,J.jsx)(`span`,{className:`cursor-gateway-value`,children:n(`integrations.cursor.apiKeyCredential`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:()=>pt(`integrations/keys`),children:n(`integrations.tab.keys`)})]})]}),(0,J.jsxs)(`div`,{className:`cursor-card`,"data-seen":c.lastSeen?`true`:`false`,children:[(0,J.jsx)(`h4`,{children:n(`integrations.cursor.connection`)}),c.lastSeen?(0,J.jsx)(`p`,{children:(0,J.jsx)(`span`,{className:`badge ${i-c.lastSeen.at<864e5?`badge-green`:`badge-muted`}`,children:n(`integrations.cursor.seen`,{time:wi(c.lastSeen.at,l,i),ua:c.lastSeen.userAgent})})}):(0,J.jsx)(`p`,{className:`muted`,children:n(`integrations.cursor.neverSeen`)})]}),(0,J.jsxs)(`div`,{className:`cursor-card`,children:[(0,J.jsx)(`h4`,{children:n(`integrations.cursor.models`)}),(0,J.jsx)(`p`,{className:`muted`,children:c.effortTable.source===`bundle`?n(`integrations.cursor.ladderFromBundle`,{version:c.effortTable.version??n(`integrations.cursor.unknownVersion`)}):n(`integrations.cursor.ladderFromStatic`)}),(0,J.jsxs)(`table`,{className:`cursor-model-table`,children:[(0,J.jsx)(`thead`,{children:(0,J.jsxs)(`tr`,{children:[(0,J.jsx)(`th`,{children:n(`integrations.cursor.colModel`)}),(0,J.jsx)(`th`,{children:n(`integrations.cursor.colReasoning`)}),(0,J.jsx)(`th`,{children:n(`integrations.cursor.colContext`)})]})}),(0,J.jsx)(`tbody`,{children:c.models.map(e=>(0,J.jsxs)(`tr`,{children:[(0,J.jsx)(`td`,{children:(0,J.jsx)(`code`,{children:e.id})}),(0,J.jsx)(`td`,{children:e.reasoning?e.reasoning.join(` · `):(0,J.jsxs)(J.Fragment,{children:[(0,J.jsx)(`span`,{className:`cursor-no-control`,title:n(`integrations.cursor.noControlTitle`),"aria-label":n(`integrations.cursor.noControlTitle`),children:n(`integrations.cursor.noControl`)}),e.effortRows.length>0?(0,J.jsx)(`span`,{className:`cursor-effort-rows`,children:n(e.effortRows.length===1?`integrations.cursor.effortRowsOne`:`integrations.cursor.effortRowsMany`,{n:e.effortRows.length})}):(0,J.jsx)(`span`,{className:`cursor-effort-rows muted`,children:n(`integrations.cursor.effortRowsOff`)})]})}),(0,J.jsx)(`td`,{children:e.context?`${Rn(e.context.defaultWindow,r)} · ${Rn(e.context.longWindow,r)}`:n(`integrations.cursor.singleWindow`)})]},e.id))})]}),c.models.some(e=>e.tableLess)&&(0,J.jsx)(`p`,{className:`muted`,"data-cursor-tableless-hint":!0,children:n(`integrations.cursor.tableLessHint`)})]}),(0,J.jsx)(`p`,{children:(0,J.jsx)(`a`,{href:c.guideUrl,target:`_blank`,rel:`noreferrer`,children:n(`integrations.cursor.guide`)})})]})]})}var jv={"not-installed":`integrations.state.notInstalled`,unknown:`integrations.state.unknown`,absent:`integrations.state.absent`,current:`integrations.state.current`,stale:`integrations.state.stale`,conflict:`integrations.state.conflict`,unsafe:`integrations.state.unsafe`},Mv={"not-installed":`badge badge-muted`,unknown:`badge badge-muted`,absent:`badge badge-muted`,current:`badge badge-green`,stale:`badge badge-amber`,conflict:`badge integration-badge--danger`,unsafe:`badge integration-badge--danger-outline`};function Nv({state:e,installed:t,id:n}){let r=Q(),i=e===`unknown`||e===`not-installed`||t?e:`not-installed`;return(0,J.jsx)(`span`,{id:n,className:Mv[i],"data-integration-state":i,children:r(jv[i])})}function Pv({copyKey:e,vars:t}){let n=Q()(e,t),r=t?.path;if(!r||!n.includes(r))return(0,J.jsx)(`p`,{children:n});let[i,...a]=n.split(r);return(0,J.jsxs)(`p`,{children:[i,(0,J.jsx)(`code`,{children:r}),a.join(r)]})}function Fv({copy:e,onConfirm:t,onClose:n}){let r=Q(),i=(0,_.useRef)(null),[a,o]=(0,_.useState)(!1),[s,c]=(0,_.useState)(null),l=`integration-consequence-dialog-title`;(0,_.useEffect)(()=>{let e=i.current;return e&&!e.open&&e.showModal(),()=>{e?.open&&e.close()}},[]);let u=(0,_.useCallback)(e=>{e.preventDefault(),a||n()},[n,a]),d=(0,_.useCallback)(async()=>{if(!a){o(!0),c(null);try{await t()}catch(e){c(e instanceof Error?e.message:r(`integrations.error.generic`)),o(!1)}}},[t,a,r]),f=[(0,J.jsx)(Pv,{copyKey:e.changesKey,vars:e.vars},`changes`),(0,J.jsx)(Pv,{copyKey:e.breakageKey,vars:e.vars},`breakage`),(0,J.jsx)(Pv,{copyKey:e.undoKey,vars:e.vars},`undo`)];return e.sideEffectKey&&f.push((0,J.jsx)(Pv,{copyKey:e.sideEffectKey,vars:e.vars},`side-effect`)),(0,J.jsxs)(`dialog`,{ref:i,className:`modal-overlay`,"aria-labelledby":l,onCancel:u,children:[(0,J.jsx)(`button`,{type:`button`,className:`modal-backdrop-dismiss`,"aria-label":r(`common.close`),tabIndex:-1,onClick:()=>{a||n()}}),(0,J.jsxs)(`div`,{className:`modal-card integration-consequence-dialog`,role:`document`,children:[(0,J.jsxs)(`div`,{className:`modal-head`,children:[(0,J.jsx)(`h3`,{id:l,children:r(e.titleKey,e.vars)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:n,disabled:a,children:r(`common.close`)})]}),(0,J.jsx)(`div`,{className:`integration-consequence-body`,children:f}),s&&(0,J.jsx)($,{tone:`err`,children:s}),(0,J.jsx)(`div`,{className:`modal-actions`,children:(0,J.jsx)(`button`,{type:`button`,className:`btn btn-primary`,onClick:()=>void d(),disabled:a,children:r(e.confirmKey)})})]})]})}var Iv=[`opencode`,`pi`,`omp`,`hermes`,`openclaw`,`kimi`,`gajae`,`dsh`,`mcode`,`zcode`,`prime`,`aside`],Lv=new Set([`not_installed`,`conflict`,`unsafe`,`non_loopback`,`drift_requires_confirm`,`snapshot_expired`,`write_failed`]),Rv=new Set([`integration_unsafe`,`integration_conflict`,`integration_drift_confirmation_required`,`integration_snapshot_expired`,`integration_mutation_failed`]),zv=new Set([`absent`,`current`,`stale`,`conflict`,`unsafe`]);function Bv(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function Vv(e){return!Bv(e)||!Lv.has(String(e.reason))?!1:typeof e.error==`string`&&Rv.has(String(e.code))&&Iv.includes(e.clientId)&&zv.has(String(e.state))&&typeof e.message==`string`}var Hv=class extends Error{refusal;status;body;constructor(e,t){let n=Vv(t)?t:null;super(n?.message??t.error??t.message??String(e)),this.name=`IntegrationApiError`,this.status=e,this.body=t,this.refusal=n}};async function Uv(e){try{let t=await e.json();return Bv(t)?t:{}}catch{return{}}}async function Wv(e){if(!e.ok)throw new Hv(e.status,await Uv(e));let t=await Ft(e);if(t==null)throw new Hv(e.status,{});return t}async function Gv(e,t){return Wv(await fetch(`${e}/api/client-integrations`,{signal:t}))}async function Kv(e,t,n){return Wv(await fetch(`${e}/api/client-integrations/${encodeURIComponent(t)}`,{signal:n}))}async function qv(e,t,n){let r=t?`?client=${encodeURIComponent(t)}`:``;return Wv(await fetch(`${e}/api/client-integrations/journal${r}`,{signal:n}))}async function Jv(e,t,n,r,i){return Wv(await fetch(`${e}/api/client-integrations/${encodeURIComponent(t)}`,{method:`PUT`,headers:{"Content-Type":`application/json`},body:JSON.stringify(i===!0?{enabled:n,overwriteConflict:!0}:{enabled:n}),signal:r}))}async function Yv(e,t,n=!1,r){return Wv(await fetch(`${e}/api/client-integrations/restore`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({opId:t,confirmDrift:n}),signal:r}))}async function Xv(e){try{let t=await e;return t.ok?await Ft(t)??null:null}catch{return null}}async function Zv(e,t){let n=await Xv(fetch(`${e}/api/startup-health`,{signal:t}));return n?{routingInjected:n.routingInjected===!0,status:typeof n.status==`string`?n.status:void 0,recommendedCommand:typeof n.recommendedCommand==`string`?n.recommendedCommand:null}:null}async function Qv(e,t){let n=await fetch(`${e}/api/keys`,{signal:t});if(!n.ok)throw Error(`/api/keys responded ${n.status}`);let r=await Ft(n);if(!r||!Array.isArray(r.keys))throw Error(`/api/keys returned an unexpected body`);return r.keys.length}async function $v(e,t){let n=await Xv(fetch(`${e}/api/claude-code`,{signal:t}));return n?{enabled:n.enabled===!0,authMode:typeof n.authMode==`string`?n.authMode:void 0}:null}async function ey(e,t){let n=await Xv(fetch(`${e}/api/claude-desktop/status`,{signal:t}));return!n||typeof n.desiredEnabled!=`boolean`||typeof n.installed!=`boolean`||typeof n.observedKind!=`string`?null:{desiredEnabled:n.desiredEnabled,installed:n.installed,observedKind:n.observedKind,applied:n.applied===!0,stale:n.stale===!0,drift:n.drift===!0,driftReason:typeof n.driftReason==`string`?n.driftReason:null,activeProfile:typeof n.activeProfile==`boolean`?n.activeProfile:null,appliedAt:typeof n.appliedAt==`string`?n.appliedAt:null}}async function ty(e,t){let n=await Xv(fetch(`${e}/api/grok`,{signal:t}));return n?{present:n.present===!0,models:Array.isArray(n.models)?n.models:[]}:null}var ny=new Set([`claude`,`grok`,`codex`,`claude-desktop`]),ry=new Set([`native_integration_refused`,`native_integration_failed`]),iy=new Set([`not_installed`,`orphaned_marker`,`home_mismatch`,`config_busy`,`write_failed`,`metadata_unreadable`,`cleanup_incomplete`,`desired_state_changed`]);function ay(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function oy(e){return ay(e)?typeof e.error==`string`&&ry.has(String(e.code))&&ny.has(String(e.clientId))&&iy.has(String(e.reason))&&typeof e.message==`string`:!1}var sy=class extends Error{refusal;status;body;constructor(e,t){let n=oy(t)?t:null;super(n?.message??t.error??t.message??String(e)),this.name=`NativeApiError`,this.status=e,this.body=t,this.refusal=n}};async function cy(e){try{let t=await e;return t.ok?await Ft(t)??null:null}catch{return null}}async function ly(e){try{let t=await e.json();return ay(t)?t:{}}catch{return{}}}async function uy(e){if(!e.ok)throw new sy(e.status,await ly(e));let t=await Ft(e);if(t==null)throw new sy(e.status,{});return t}function dy(e,t){return cy(fetch(`${e}/api/native-integrations`,{signal:t}))}async function fy(e,t,n,r){return uy(await fetch(`${e}/api/native-integrations/${encodeURIComponent(t)}`,{method:`PUT`,headers:{"Content-Type":`application/json`},body:JSON.stringify({enabled:n}),signal:r}))}var py={integration_mutation_busy:`integrations.error.busy`};function my(e){return e instanceof Hv?e.refusal:null}function hy(e){return e instanceof sy?e.refusal:null}function gy(e){return e===`conflict`?`integrations.error.conflict`:e===`unsafe`?`integrations.error.unsafe`:e===`non_loopback`?`integrations.error.nonLoopback`:`integrations.error.generic`}var _y=new Set([`non_loopback`]);function vy(e,t,n){return t.reason===`orphaned_marker`?e(`integrations.native.error.orphanedMarker`,{path:n??``}):t.reason===`home_mismatch`?`${e(`integrations.native.error.homeMismatch`)} ${t.message}`:t.reason===`not_installed`?e(`integrations.native.error.notInstalled`):t.reason===`config_busy`?e(`integrations.native.error.configBusy`):t.reason===`metadata_unreadable`?e(`integrations.native.error.desktopUnsafeMetadata`,{path:n??``}):t.reason===`cleanup_incomplete`?e(`integrations.native.error.desktopCleanupIncomplete`,{paths:(t.residualPaths??[]).join(`, `)}):t.message||e(`integrations.error.generic`)}function yy(e,t,n,r){let i=hy(t);if(i)return vy(e,i,r);let a=my(t);if(!a){let r=py[t instanceof Hv?String(t.body.code??``):``];return r?e(r):t instanceof Error&&t.message?t.message:n??e(`integrations.error.generic`)}let o=_y.has(a.reason)?e(gy(a.reason),{client:a.clientId}):a.message||e(gy(a.reason));return a.snapshotPath?e(a.residual?`integrations.error.residual`:`integrations.error.recover`,{message:o,path:a.snapshotPath}):a.reason===`conflict`||a.reason===`unsafe`?`${e(gy(a.reason))} ${o}`:o}function by({apiBase:e,row:t,onClose:n,onRestored:r}){let i=Q(),a=(0,_.useRef)(null),o=(0,_.useRef)(null),s=(0,_.useRef)(null),c=(0,_.useRef)(!1),[l,u]=(0,_.useState)(!1),[d,f]=(0,_.useState)(!1),[p,m]=(0,_.useState)(null);(0,_.useEffect)(()=>{let e=a.current,t=document.activeElement;return o.current=t?.tagName===`BUTTON`?t:null,s.current=t?.closest?.(`section, [role='region'], main`)??null,e&&!e.open&&e.showModal(),()=>{e?.open&&e.close();let t=s.current;if(c.current&&t?.isConnected){t.hasAttribute(`tabindex`)||t.setAttribute(`tabindex`,`-1`),t.focus?.();return}let n=o.current;if(n?.isConnected){n.focus?.();return}t?.isConnected&&(t.hasAttribute(`tabindex`)||t.setAttribute(`tabindex`,`-1`),t.focus?.())}},[]);let h=(0,_.useCallback)(e=>{e.preventDefault(),d||n()},[n,d]),g=async()=>{if(!d){f(!0),m(null);try{await Yv(e,t.opId,l),c.current=!0,r(),n()}catch(e){if(my(e)?.reason===`drift_requires_confirm`){u(!0),f(!1);return}m(yy(i,e)),f(!1)}}};return(0,J.jsxs)(`dialog`,{ref:a,className:`modal-overlay`,"aria-labelledby":`integration-restore-title`,onCancel:h,children:[(0,J.jsx)(`button`,{type:`button`,className:`modal-backdrop-dismiss`,"aria-label":i(`common.close`),tabIndex:-1,onClick:()=>{d||n()}}),(0,J.jsxs)(`div`,{className:`modal-card integration-restore-dialog`,role:`document`,children:[(0,J.jsx)(`div`,{className:`modal-head`,children:(0,J.jsx)(`h3`,{id:`integration-restore-title`,children:i(l?`integrations.restore.driftTitle`:`integrations.restore.title`)})}),(0,J.jsx)(`div`,{className:`modal-desc`,children:i(l?`integrations.restore.driftBody`:`integrations.restore.body`)}),(0,J.jsx)(`p`,{className:`integration-path`,children:t.configPath}),p&&(0,J.jsx)($,{tone:`err`,children:p}),(0,J.jsxs)(`div`,{className:`modal-actions`,children:[(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost`,onClick:n,disabled:d,children:i(`common.cancel`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-primary`,onClick:()=>void g(),disabled:d,children:i(d?`integrations.restore.pending`:l?`integrations.restore.confirmDrift`:`integrations.restore.confirm`)})]})]})]})}var xy={opencode:`integrations.tab.opencode`,pi:`integrations.tab.pi`,omp:`integrations.tab.omp`,hermes:`integrations.tab.hermes`,openclaw:`integrations.tab.openclaw`,kimi:`integrations.tab.kimi`,gajae:`integrations.tab.gajae`,dsh:`integrations.tab.dsh`,mcode:`integrations.tab.mcode`,zcode:`integrations.tab.zcode`,prime:`integrations.tab.prime`,aside:`integrations.tab.aside`},Sy={apply:`integrations.kind.apply`,disable:`integrations.kind.disable`,refresh:`integrations.kind.refresh`,restore:`integrations.kind.restore`,overwrite:`integrations.kind.overwrite`};function Cy(e){return e===`current`||e===`stale`}function wy(e){let t={id:`codex`,hash:`integrations/codex`,labelKey:`integrations.tab.codex`,toggle:`codex`,toggleBlocked:null,togglePath:null,status:null,detail:null,detailVars:null};return e?e.routingInjected===!0?{...t,state:e.status===`error`?`stale`:`current`,installed:!0,applied:!0,detailKey:`integrations.detail.codexRouted`}:{...t,state:`absent`,installed:!0,applied:!1,detail:e.recommendedCommand??null,detailKey:e.recommendedCommand?null:`integrations.detail.codexAbsent`}:{...t,state:`unknown`,installed:!1,applied:!1,detailKey:null}}function Ty(e,t){let n={hash:`integrations/keys`,labelKey:`integrations.tab.keys`};return e===`checking`?{...n,state:`checking`,detailKey:`integrations.detail.keyChecking`,detailVars:null}:e===`unavailable`||t===null?{...n,state:`unavailable`,detailKey:`integrations.detail.keyUnavailable`,detailVars:null}:{...n,state:t>0?`issued`:`none-issued`,detailKey:t>0?`integrations.detail.keyCount`:`integrations.detail.keyNone`,detailVars:t>0?{count:String(t)}:null}}function Ey(e){return e?e.enabled===!0?e.authMode===`subscription`?`claude.authModeSubscription`:e.authMode===`proxy`?`claude.authModeProxy`:e.authMode===`auto`?`claude.authModeAuto`:null:`integrations.detail.claudeOff`:null}function Dy(e,t,n){let r={id:`claude`,hash:`integrations/claude`,labelKey:`integrations.tab.claude`,toggle:`claude`,toggleBlocked:t?.disableBlocked??null,togglePath:t?.configPath??null,status:null,detail:null,detailVars:null},i=Ey(e);if(n===void 0){if(!e)return{...r,state:`unknown`,installed:!1,applied:!1,detailKey:i};let t=e.enabled===!0;return{...r,state:t?`current`:`absent`,installed:!0,applied:t,detailKey:i}}return n?t?{...r,state:t.state,installed:t.installed,applied:t.state===`current`,detailKey:i}:{...r,toggle:null,state:`unknown`,installed:!1,applied:!1,detailKey:i}:{...r,state:`unknown`,installed:!1,applied:!1,detailKey:i}}function Oy(e,t,n){let r={id:`claudeDesktop`,hash:`integrations/claude/desktop`,labelKey:`claudeDesktop.title`,toggle:`claude-desktop`,toggleBlocked:t?.disableBlocked??null,togglePath:t?.configPath??null,status:null,detail:null,detailVars:null};if(!e||!n||!t||typeof e.desiredEnabled!=`boolean`)return{...r,toggle:null,state:`unknown`,installed:!1,applied:!1,detailKey:null};let i=e.desiredEnabled;if(!i)return e.applied===!0||e.driftReason===`desired_off_gateway_selected`?{...r,state:`stale`,installed:e.installed===!0,applied:!0,toggleOn:!1,detailKey:`integrations.detail.desktopDesiredOffCleanupPending`}:{...r,state:`absent`,installed:e.installed===!0,applied:!1,toggleOn:!1,detailKey:`integrations.detail.desktopDesiredOff`};if(e.applied!==!0)return{...r,state:`absent`,installed:e.installed===!0,applied:!1,toggleOn:i,detailKey:`integrations.detail.desktopDesiredOnNotApplied`};let a=e.stale===!0||e.activeProfile===!1;return{...r,state:a?`stale`:`current`,installed:!0,applied:!0,toggleOn:i,detailKey:e.activeProfile===!1?`integrations.detail.desktopNotServed`:a?`integrations.detail.desktopStale`:`integrations.detail.desktopCurrent`}}function ky(e){if(!e)return{detailKey:null,detailVars:null};let t=e.present===!0;return{detailKey:t?`integrations.detail.grokModels`:`integrations.detail.grokAbsent`,detailVars:t?{count:String(e.models?.length??0)}:null}}function Ay(e,t,n){let r={id:`grok`,hash:`integrations/grok`,labelKey:`integrations.tab.grok`,toggle:`grok`,toggleBlocked:t?.disableBlocked??null,togglePath:t?.configPath??null,status:null,detail:null},i=ky(e);if(n===void 0){if(!e)return{...r,state:`unknown`,installed:!1,applied:!1,...i};let t=e.present===!0;return{...r,state:t?`current`:`absent`,installed:t,applied:t,...i}}return n?t?{...r,state:t.state,installed:t.installed,applied:t.state===`current`,...i}:{...r,toggle:null,state:`unknown`,installed:!1,applied:!1,...i}:{...r,state:`unknown`,installed:!1,applied:!1,...i}}function jy(e,t=Date.now()){let n={id:`cursor`,hash:`integrations/cursor`,labelKey:`integrations.tab.cursor`,toggle:null,toggleBlocked:null,togglePath:null,status:null,detail:null,detailVars:null};if(!e)return{...n,state:`unknown`,installed:!1,applied:!1,detailKey:null};if(!e.privateInference.installed)return{...n,state:`not-installed`,installed:!1,applied:!1,detailKey:`integrations.detail.cursorAbsent`};let r=e.lastSeen!==null&&t-e.lastSeen.at<864e5;return{...n,state:r?`current`:`absent`,installed:!0,applied:r,detailKey:r?`integrations.detail.cursorSeen`:`integrations.detail.cursorNeverSeen`}}function My(e){return{id:e.clientId,hash:`integrations/${e.clientId}`,labelKey:xy[e.clientId],state:e.installed?e.state:`not-installed`,installed:e.installed,applied:e.installed&&Cy(e.state),detail:e.configPath,detailKey:null,detailVars:null,toggle:e.clientId,toggleBlocked:null,togglePath:e.configPath,status:e}}function Ny(e){let t=e.native?.find(e=>e.clientId===`claude`),n=e.native?.find(e=>e.clientId===`grok`),r=new Map(e.clients.map(e=>[e.clientId,e])),i=[wy(e.codex),Dy(e.claude,t,e.nativeSettled),Oy(e.claudeDesktop,e.native?.find(e=>e.clientId===`claude-desktop`),e.nativeSettled),Ay(e.grok,n,e.nativeSettled),jy(e.cursor)];for(let t of Iv){let n=r.get(t);if(n){i.push(My(n));continue}e.clientsSettled||i.push({id:t,hash:`integrations/${t}`,labelKey:xy[t],state:`unknown`,installed:!1,applied:!1,detail:null,detailKey:null,detailVars:null,toggle:null,toggleBlocked:null,togglePath:null,status:null})}return{keysRow:Ty(e.keyPhase,e.keyCount),rows:i}}function Py(e){return{detected:e.filter(e=>e.installed).length,applied:e.filter(e=>e.applied).length,stale:e.filter(e=>e.state===`stale`).length,unknown:e.filter(e=>e.state===`unknown`).length}}var Fy=6;function Iy({row:e,showClient:t,onRestore:n}){let r=Q();return(0,J.jsxs)(`li`,{className:`integration-history-row`,children:[(0,J.jsx)(`span`,{className:`integration-history-kind`,children:r(Sy[e.kind])}),t&&(0,J.jsx)(`span`,{className:`integration-history-client`,children:e.clientId}),(0,J.jsx)(`span`,{className:`integration-history-at`,children:new Date(e.at).toLocaleString()}),e.snapshot===`expired`?(0,J.jsx)(`span`,{className:`badge badge-muted`,children:r(`integrations.action.snapshotExpired`)}):(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:()=>n(e),children:e.undoable?r(`integrations.action.undo`):r(`integrations.action.restorePoint`)})]})}function Ly({rows:e,showClient:t,onRestore:n}){let r=Q(),[i,a]=(0,_.useState)(Fy),[o,...s]=e;if(!o)return null;let c=s.slice(0,i),l=s.length-c.length;return(0,J.jsxs)(`div`,{className:`integration-history`,children:[(0,J.jsx)(`ul`,{className:`integration-history-list`,children:(0,J.jsx)(Iy,{row:o,showClient:t,onRestore:n})}),s.length>0&&(0,J.jsxs)(`details`,{className:`integration-history-older`,children:[(0,J.jsx)(`summary`,{children:r(`integrations.rollback.older`)}),(0,J.jsx)(`ul`,{className:`integration-history-list`,children:c.map(e=>(0,J.jsx)(Iy,{row:e,showClient:t,onRestore:n},e.opId))}),l>0&&(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm integration-history-more`,onClick:()=>a(e=>e+Fy),children:r(`integrations.rollback.showMore`,{n:String(Math.min(l,Fy))})})]})]})}var Ry={titleKey:`integrations.dialog.grok.title`,changesKey:`integrations.dialog.grok.changes`,breakageKey:`integrations.dialog.grok.breakage`,undoKey:`integrations.dialog.grok.undo`,confirmKey:`integrations.dialog.grok.confirm`},zy={titleKey:`integrations.dialog.desktop.title`,changesKey:`integrations.dialog.desktop.changes`,breakageKey:`integrations.dialog.desktop.breakage`,undoKey:`integrations.dialog.desktop.undo`,sideEffectKey:`integrations.dialog.desktop.restart`,confirmKey:`integrations.dialog.desktop.confirm`};function By(e){return e.state===`current`||e.state===`stale`}function Vy({row:e,pending:t,result:n,onOpen:r,onToggle:i,onOverwrite:a}){let o=Q(),s=e.detail??(e.detailKey?o(e.detailKey,e.detailVars??void 0):null),c=e.toggleBlocked!==null&&(e.applied||e.toggleBlocked.reason===`orphaned_marker`),l=c&&e.toggleBlocked&&(e.toggle===`claude`||e.toggle===`grok`)?yy(o,new sy(409,{error:`native integration change refused`,code:`native_integration_refused`,clientId:e.toggle,reason:e.toggleBlocked.reason,message:e.toggleBlocked.message}),void 0,e.togglePath??void 0):null;return(0,J.jsxs)(`li`,{className:`integration-card`,"data-client":e.id,children:[(0,J.jsxs)(`div`,{className:`integration-card-head`,children:[(0,J.jsx)(Gg,{src:Wg(e.id),label:o(e.labelKey),size:20}),(0,J.jsx)(`h4`,{children:(0,J.jsx)(`button`,{type:`button`,className:`integration-card-link`,onClick:r,children:o(e.labelKey)})}),(0,J.jsx)(Nv,{state:e.state,installed:e.installed})]}),s&&(0,J.jsx)(`p`,{className:e.detail?`integration-path`:`integration-meta`,children:s}),n?.tone===`err`&&(0,J.jsx)($,{tone:`err`,children:n.text}),n?.tone===`ok`&&(0,J.jsx)($,{tone:`ok`,children:n.text}),(0,J.jsxs)(`div`,{className:`integration-card-actions`,children:[e.toggle&&i&&(0,J.jsxs)(`div`,{className:`integration-toggle-control`,children:[(0,J.jsx)(Tt,{on:e.toggleOn??e.applied,onClick:i,disabled:e.state===`unknown`||!e.installed||e.state===`conflict`||e.state===`unsafe`||c||t,label:e.applied?o(`integrations.action.disable`):o(`integrations.action.apply`)}),l&&(0,J.jsx)(`p`,{className:`integration-toggle-blocked`,children:l})]}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost`,onClick:r,tabIndex:-1,children:o(`integrations.action.settings`)}),a&&(0,J.jsx)(`button`,{type:`button`,className:`btn btn-danger`,onClick:a,disabled:t,children:o(`integrations.action.overwrite`)})]})]})}function Hy({apiBase:e,active:t=!0}){let n=Q(),[r,i]=(0,_.useState)(!1),[a,o]=(0,_.useState)(null),[s,c]=(0,_.useState)(null),[l,u]=(0,_.useState)({}),[d,f]=(0,_.useState)(null),[p,m]=(0,_.useState)(null),h=(0,_.useRef)(null);(0,_.useEffect)(()=>{if(d!==null)return;let e=h.current;e&&(h.current=null,e.isConnected&&e.focus())},[d]);let g=(0,_.useCallback)(async t=>(await Gv(e,t)).clients,[e]),v=(0,_.useCallback)(async t=>(await qv(e,void 0,t)).operations,[e]),y=(0,_.useCallback)(t=>Zv(e,t),[e]),b=(0,_.useCallback)(t=>Qv(e,t),[e]),x=(0,_.useCallback)(t=>$v(e,t),[e]),S=(0,_.useCallback)(t=>ey(e,t),[e]),C=(0,_.useCallback)(t=>ty(e,t),[e]),w=(0,_.useCallback)(t=>Dv(e,t),[e]),T=(0,_.useCallback)(async t=>(await dy(e,t))?.clients??null,[e]),E=ml(`integration-states:${e}`,[e],g,{isEmpty:e=>e.length===0,enabled:t,sessionCacheKey:`ocx.integrations.states.v1:${e}`}),D=ml(`integration-journal-all:${e}`,[e],v,{isEmpty:e=>e.length===0,enabled:t,sessionCacheKey:`ocx.integrations.journal.v1:${e}`}),O=ml(`integration-codex:${e}`,[e],y,{isEmpty:e=>e===null,enabled:t,sessionCacheKey:`ocx.integrations.codex.v1:${e}`}),k=ml(`integration-keys:${e}`,[e],b,{isEmpty:()=>!1,enabled:t,sessionCacheKey:`ocx.integrations.keys.v1:${e}`}),A=ml(`integration-claude:${e}`,[e],x,{isEmpty:e=>e===null,enabled:t,sessionCacheKey:`ocx.integrations.claude.v1:${e}`}),j=ml(`integration-claude-desktop:${e}`,[e],S,{isEmpty:e=>e===null,enabled:t,sessionCacheKey:`ocx.integrations.claude-desktop.v1:${e}`}),M=ml(`integration-grok:${e}`,[e],C,{isEmpty:e=>e===null,enabled:t,sessionCacheKey:`ocx.integrations.grok.v1:${e}`}),N=ml(`integration-cursor:${e}`,[e],w,{isEmpty:e=>e===null,enabled:t,sessionCacheKey:`ocx.integrations.cursor.v1:${e}`}),P=ml(`integration-native:${e}`,[e],T,{isEmpty:e=>e===null,enabled:t,sessionCacheKey:`ocx.integrations.native.v1:${e}`}),F=E.state.data??[],I=D.state.data??[],L=F.filter(By),R=F.filter(e=>e.installed),z=E.state.kind!==`cold`&&E.state.kind!==`retrying-cold`,B=P.state.data??null,V=B!==null,H=k.state.kind===`cold`||k.state.kind===`retrying-cold`?`checking`:k.state.kind===`failed-cold`||k.state.kind===`failed-with-stale`?`unavailable`:`settled`,{keysRow:U,rows:W}=Ny({clients:F,clientsSettled:z,codex:O.state.data??null,keyCount:k.state.data??null,keyPhase:H,claude:A.state.data??null,claudeDesktop:j.state.data??null,grok:M.state.data??null,cursor:N.state.data??null,native:B,nativeSettled:V}),ee=Py(W),G=()=>{E.refresh(),D.refresh(),O.refresh(),k.refresh(),A.refresh(),j.refresh(),M.refresh(),P.refresh()},K=async()=>{if(r||L.length===0)return;let t=[n(`integrations.bulk.title`),n(`integrations.bulk.body`)].join(` - -`);if(!confirm(t))return;i(!0),o(null);let a=[];for(let t of L)try{await Jv(e,t.clientId,!1)}catch(e){a.push(`${t.clientId}: ${yy(n,e)}`)}let s=!1;try{s=(await Gv(e)).clients.some(By)}catch{a.push(n(`integrations.error.stale`))}s&&a.length===0&&a.push(n(`integrations.error.stale`)),G(),i(!1),o(a.length===0?{tone:`ok`,text:n(`integrations.bulk.success`)}:{tone:`err`,text:n(`integrations.bulk.partial`,{clients:a.join(`; `)})})},q=I[0]?.at,[Y,te]=(0,_.useState)(null),ne=()=>{P.refresh(),A.refresh(),M.refresh()},re=(e,t)=>{u(n=>{let r={...n};return t?r[e]=t:delete r[e],r})},ie=async(t,r)=>{if(!Y&&t.toggle){te(t.id),re(t.id,null);try{if(t.status)await Jv(e,t.status.clientId,r),G();else if(t.toggle===`claude`||t.toggle===`grok`||t.toggle===`codex`||t.toggle===`claude-desktop`){let i=await fy(e,t.toggle,r);i.reason===`non_loopback_removed`?re(t.id,{tone:`ok`,text:n(i.changed?`integrations.native.msg.nonLoopbackRemoved`:`integrations.native.msg.nonLoopbackRemovedNoop`)}):i.reason===`non_loopback_superseded`&&re(t.id,{tone:`ok`,text:n(`integrations.native.msg.nonLoopbackSuperseded`)}),ne()}}catch(e){re(t.id,{tone:`err`,text:yy(n,e,void 0,t.togglePath??void 0)}),(t.toggle===`claude`||t.toggle===`grok`||t.toggle===`codex`||t.toggle===`claude-desktop`)&&ne()}finally{te(null)}}},ae=(e,t)=>{if(e.status||t||e.id===`claude`||e.toggle===null){ie(e,t);return}let n=document.activeElement;h.current=n?.tagName===`BUTTON`?n:null,f(e)},oe=async t=>{if(t.status){te(t.id),re(t.id,null);try{await Jv(e,t.status.clientId,!0,void 0,!0),G()}catch(e){throw re(t.id,{tone:`err`,text:yy(n,e,void 0,t.togglePath??void 0)}),e}finally{te(null)}}};return(0,J.jsxs)(`section`,{className:`integrations-overview`,children:[(0,J.jsxs)(`div`,{className:`integration-summary`,children:[(0,J.jsxs)(`div`,{className:`integration-summary-cell`,children:[(0,J.jsx)(`span`,{className:`integration-summary-label`,children:n(`integrations.summary.detected`)}),(0,J.jsx)(`strong`,{children:ee.detected})]}),(0,J.jsxs)(`div`,{className:`integration-summary-cell`,children:[(0,J.jsx)(`span`,{className:`integration-summary-label`,children:n(`integrations.summary.applied`)}),(0,J.jsx)(`strong`,{children:ee.applied})]}),(0,J.jsxs)(`div`,{className:`integration-summary-cell`,children:[(0,J.jsx)(`span`,{className:`integration-summary-label`,children:n(`integrations.summary.stale`)}),(0,J.jsx)(`strong`,{children:ee.stale})]}),ee.unknown>0&&(0,J.jsxs)(`div`,{className:`integration-summary-cell`,children:[(0,J.jsx)(`span`,{className:`integration-summary-label`,children:n(`integrations.state.unknown`)}),(0,J.jsx)(`strong`,{children:ee.unknown})]}),(0,J.jsxs)(`div`,{className:`integration-summary-cell`,children:[(0,J.jsx)(`span`,{className:`integration-summary-label`,children:n(`integrations.summary.lastChange`)}),(0,J.jsx)(`strong`,{children:q?new Date(q).toLocaleString():n(`integrations.status.unknown`)})]}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost`,onClick:()=>void K(),disabled:r||L.length===0,children:n(`integrations.summary.disableAll`)})]}),(0,J.jsx)(`h3`,{children:n(`integrations.catalog.title`)}),(0,J.jsx)(Uy,{row:U}),(0,J.jsx)(`p`,{className:`page-sub`,children:n(`integrations.onboarding`)}),E.state.kind===`failed-cold`&&(0,J.jsx)($,{tone:`err`,children:n(`integrations.error.load`)}),E.state.kind===`failed-with-stale`&&(0,J.jsx)($,{tone:`err`,children:n(`integrations.error.stale`)}),a&&(0,J.jsx)($,{tone:a.tone,children:a.text}),W.length===0?E.state.kind===`failed-cold`?null:(0,J.jsx)(`p`,{className:`page-sub`,children:n(`common.loading`)}):(0,J.jsx)(`ul`,{className:`integration-cards`,children:W.map(e=>(0,J.jsx)(Vy,{row:e,pending:Y!==null,result:l[e.id]??null,onOpen:()=>pt(e.hash),onToggle:e.toggle?()=>ae(e,!(e.toggleOn??e.applied)):null,onOverwrite:e.status!==null&&e.status.state===`conflict`&&e.installed?()=>m(e):null},e.id))}),z&&R.length===0&&(0,J.jsxs)(`div`,{className:`integration-empty`,children:[(0,J.jsx)(`h4`,{children:n(`integrations.empty.title`)}),(0,J.jsx)(`p`,{children:n(`integrations.empty.body`)})]}),(0,J.jsx)(`h3`,{children:n(`integrations.rollback.title`)}),D.state.showSkeleton?(0,J.jsx)(gl,{label:n(`integrations.rollback.title`),rows:2}):D.state.kind===`failed-cold`?(0,J.jsxs)($,{tone:`err`,children:[n(`integrations.rollback.failed`),` `,(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:()=>void D.refresh(),children:n(`common.retry`)})]}):I.length===0?(0,J.jsxs)(`div`,{className:`integration-empty`,children:[(0,J.jsx)(`p`,{children:n(`integrations.rollback.empty`)}),(0,J.jsx)(`p`,{className:`page-sub`,children:n(`integrations.rollback.emptyBody`)})]}):(0,J.jsx)(Ly,{rows:I,showClient:!0,onRestore:c}),s&&(0,J.jsx)(by,{apiBase:e,row:s,onClose:()=>c(null),onRestored:G}),d&&(0,J.jsx)(Fv,{copy:{...d.toggle===`claude-desktop`?zy:Ry,vars:{path:d.togglePath??``}},onClose:()=>f(null),onConfirm:async()=>{await ie(d,!1),f(null)}}),p&&p.status&&(0,J.jsx)(Fv,{copy:{titleKey:`integrations.dialog.overwrite.title`,changesKey:p.status.reason===`foreign-edit`?`integrations.dialog.overwrite.changesForeign`:`integrations.dialog.overwrite.changesUnowned`,breakageKey:`integrations.dialog.overwrite.breakage`,undoKey:`integrations.dialog.overwrite.undo`,confirmKey:`integrations.dialog.overwrite.confirm`,vars:{path:p.status.configPath}},onClose:()=>m(null),onConfirm:async()=>{await oe(p),m(null)}})]})}function Uy({row:e}){let t=Q(),n=e.detailKey?t(e.detailKey,e.detailVars??void 0):null;return(0,J.jsxs)(`div`,{className:`integration-api-keys-row`,"data-client":`keys`,"data-key-state":e.state,children:[(0,J.jsxs)(`div`,{className:`integration-api-keys-copy`,children:[(0,J.jsx)(`h4`,{children:t(e.labelKey)}),n&&(0,J.jsx)(`p`,{className:`integration-meta`,children:n})]}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost`,onClick:()=>pt(e.hash),children:t(`integrations.action.manageKeys`)})]})}function Wy(e,t){return{titleKey:`integrations.dialog.overwrite.title`,changesKey:e===`foreign-edit`?`integrations.dialog.overwrite.changesForeign`:`integrations.dialog.overwrite.changesUnowned`,breakageKey:`integrations.dialog.overwrite.breakage`,undoKey:`integrations.dialog.overwrite.undo`,confirmKey:`integrations.dialog.overwrite.confirm`,vars:{path:t}}}var Gy={opencode:`integrations.semantics.opencode`,pi:`integrations.semantics.pi`,omp:`integrations.semantics.omp`,hermes:`integrations.semantics.hermes`,openclaw:`integrations.semantics.openclaw`,kimi:`integrations.semantics.kimi`,gajae:`integrations.semantics.gajae`,dsh:`integrations.semantics.dsh`,mcode:`integrations.semantics.mcode`,zcode:`integrations.semantics.zcode`,prime:`integrations.semantics.prime`,aside:`integrations.semantics.aside`},Ky={opencode:`integrations.tab.opencode`,pi:`integrations.tab.pi`,omp:`integrations.tab.omp`,hermes:`integrations.tab.hermes`,openclaw:`integrations.tab.openclaw`,kimi:`integrations.tab.kimi`,gajae:`integrations.tab.gajae`,dsh:`integrations.tab.dsh`,mcode:`integrations.tab.mcode`,zcode:`integrations.tab.zcode`,prime:`integrations.tab.prime`,aside:`integrations.tab.aside`};function qy({apiBase:e,client:t,active:n=!0}){let r=Q(),[i,a]=(0,_.useState)(!1),[o,s]=(0,_.useState)(null),[c,l]=(0,_.useState)(null),[u,d]=(0,_.useState)(!1),f=(0,_.useCallback)(n=>Kv(e,t,n),[e,t]),p=(0,_.useCallback)(async n=>(await qv(e,t,n)).operations,[e,t]),m=ml(`integration-state:${e}:${t}`,[e,t],f,{isEmpty:()=>!1,enabled:n,sessionCacheKey:`ocx.integrations.state.v1:${e}:${t}`}),h=ml(`integration-journal:${e}:${t}`,[e,t],p,{isEmpty:e=>e.length===0,enabled:n,sessionCacheKey:`ocx.integrations.client-journal.v1:${e}:${t}`}),g=m.state.data??null,v=h.state.data??[],y=()=>{m.refresh(),h.refresh()},b=async n=>{if(!(!g||i)){a(!0),s(null);try{await Jv(e,t,n),y()}catch(e){s(yy(r,e))}finally{a(!1)}}},x=async()=>{if(g){s(null);try{await Jv(e,t,!0,void 0,!0),y()}catch(e){throw s(yy(r,e)),e}}},S=()=>void b(!(g&&(g.state===`current`||g.state===`stale`)));if(!g)return(0,J.jsx)(`section`,{className:`integration-client-page`,children:m.state.kind===`failed-cold`?(0,J.jsx)($,{tone:`err`,children:r(`integrations.error.load`)}):(0,J.jsx)(`p`,{className:`page-sub`,children:r(`common.loading`)})});let C=g.state===`current`||g.state===`stale`,w=!g.installed||g.state===`conflict`||g.state===`unsafe`;return(0,J.jsxs)(`section`,{className:`integration-client-page`,children:[(0,J.jsxs)(`div`,{className:`integration-client-head`,children:[(0,J.jsx)(Gg,{src:Wg(t),label:r(Ky[t]),size:24}),(0,J.jsx)(`h3`,{children:r(Ky[t])}),(0,J.jsx)(Nv,{state:g.state,installed:g.installed,id:`integration-state-${t}`}),(0,J.jsx)(Tt,{on:C,onClick:S,disabled:w||i,label:r(C?`integrations.action.disable`:`integrations.action.apply`)})]}),g.state===`stale`&&(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost`,onClick:()=>void b(!0),disabled:i,children:r(`integrations.action.refresh`)}),g.installed&&g.state===`conflict`&&(0,J.jsx)(`button`,{type:`button`,className:`btn btn-danger`,onClick:()=>d(!0),disabled:i,children:r(`integrations.action.overwrite`)}),(0,J.jsx)(`p`,{className:`page-sub`,children:r(Gy[t])}),(0,J.jsx)(`p`,{className:`integration-path`,children:g.configPath}),g.appliedAt&&(0,J.jsxs)(`p`,{className:`integration-meta`,children:[r(`integrations.status.appliedAt`),`: `,new Date(g.appliedAt).toLocaleString()]}),g.retentionDegraded&&(0,J.jsx)($,{tone:`err`,children:r(`integrations.retention.degraded`)}),o&&(0,J.jsx)($,{tone:`err`,children:o}),(0,J.jsx)(`h4`,{children:r(`integrations.rollback.title`)}),h.state.showSkeleton?(0,J.jsx)(gl,{label:r(`integrations.rollback.title`),rows:2}):h.state.kind===`failed-cold`?(0,J.jsxs)($,{tone:`err`,children:[r(`integrations.rollback.failed`),` `,(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:()=>void h.refresh(),children:r(`common.retry`)})]}):v.length===0?(0,J.jsx)(`p`,{className:`page-sub`,children:r(`integrations.rollback.empty`)}):(0,J.jsx)(Ly,{rows:v,onRestore:l}),c&&(0,J.jsx)(by,{apiBase:e,row:c,onClose:()=>l(null),onRestored:y}),u&&(0,J.jsx)(Fv,{copy:Wy(g.reason,g.configPath),onClose:()=>d(!1),onConfirm:async()=>{await x(),d(!1)}})]})}var Jy=[{id:`overview`,hash:`integrations`,labelKey:`integrations.tab.overview`},{id:`keys`,hash:`integrations/keys`,labelKey:`integrations.tab.keys`},{id:`codex`,hash:`integrations/codex`,labelKey:`integrations.tab.codex`},{id:`claude`,hash:`integrations/claude`,labelKey:`integrations.tab.claude`},{id:`grok`,hash:`integrations/grok`,labelKey:`integrations.tab.grok`},{id:`cursor`,hash:`integrations/cursor`,labelKey:`integrations.tab.cursor`},{id:`opencode`,hash:`integrations/opencode`,labelKey:`integrations.tab.opencode`},{id:`pi`,hash:`integrations/pi`,labelKey:`integrations.tab.pi`},{id:`omp`,hash:`integrations/omp`,labelKey:`integrations.tab.omp`},{id:`hermes`,hash:`integrations/hermes`,labelKey:`integrations.tab.hermes`},{id:`openclaw`,hash:`integrations/openclaw`,labelKey:`integrations.tab.openclaw`},{id:`kimi`,hash:`integrations/kimi`,labelKey:`integrations.tab.kimi`},{id:`gajae`,hash:`integrations/gajae`,labelKey:`integrations.tab.gajae`},{id:`dsh`,hash:`integrations/dsh`,labelKey:`integrations.tab.dsh`},{id:`mcode`,hash:`integrations/mcode`,labelKey:`integrations.tab.mcode`},{id:`zcode`,hash:`integrations/zcode`,labelKey:`integrations.tab.zcode`},{id:`prime`,hash:`integrations/prime`,labelKey:`integrations.tab.prime`},{id:`aside`,hash:`integrations/aside`,labelKey:`integrations.tab.aside`}],Yy=new Set([`opencode`,`pi`,`omp`,`hermes`,`openclaw`,`kimi`,`gajae`,`dsh`,`mcode`,`zcode`,`prime`,`aside`]);function Xy(e=window.location.hash){let t=dt(e);return t===`integrations/claude/desktop`?`claude`:Jy.find(e=>e.hash===t)?.id??`overview`}function Zy(e){return`integrations-tab-${e}`}function Qy(e){return`integrations-panel-${e}`}function $y(e){return e===`overview`||e===`keys`?null:Vg[e]??null}function eb({apiBase:e,machineApiBase:t=e,connected:n=!1}){let r=Q(),[i,a]=(0,_.useState)(Xy),[o,s]=(0,_.useState)(()=>new Set([Xy()])),c=(0,_.useRef)(null),[l,u]=(0,_.useState)([]),[d,f]=(0,_.useState)(!1);c.current===null&&(c.current=new Map),(0,_.useEffect)(()=>{if(!n)return;let e=new AbortController;return fetch(`${t}/api/machine/clients`,{signal:e.signal}).then(e=>e.ok?e.json():null).then(t=>{!e.signal.aborted&&Array.isArray(t?.selectedClients)&&u(t.selectedClients.filter(e=>typeof e==`string`))}).catch(()=>{}),()=>e.abort()},[n,t]);let p=async()=>{f(!0);try{await fetch(`${t}/api/machine/sync`,{method:`POST`,headers:{"Content-Type":`application/json`},body:`{}`})}finally{f(!1)}},m=e=>{a(e),s(t=>t.has(e)?t:new Set([...t,e]))};(0,_.useEffect)(()=>{let e=()=>m(Xy());return window.addEventListener(`hashchange`,e),window.addEventListener(`popstate`,e),()=>{window.removeEventListener(`hashchange`,e),window.removeEventListener(`popstate`,e)}},[]);let h=(e,t)=>{let n=Jy.find(t=>t.id===e);n&&(pt(n.hash),m(e),t&&window.requestAnimationFrame(()=>{c.current.get(e)?.focus({preventScroll:!0})}))},g=e=>{let t=Jy.findIndex(e=>e.id===i),n=null;e.key===`ArrowLeft`?n=(t-1+Jy.length)%Jy.length:e.key===`ArrowRight`?n=(t+1)%Jy.length:e.key===`Home`?n=0:e.key===`End`&&(n=Jy.length-1),n!==null&&(e.preventDefault(),h(Jy[n].id,!0))};return(0,J.jsxs)(`section`,{className:`integrations-page`,children:[(0,J.jsx)(`div`,{className:`page-head`,children:(0,J.jsx)(`h2`,{children:r(`nav.integrations`)})}),(0,J.jsx)(`p`,{className:`page-sub`,children:r(`integrations.subtitle`)}),n&&(0,J.jsxs)(`section`,{className:`notice`,"aria-label":r(`connection.clients.title`),children:[(0,J.jsx)(`strong`,{children:r(`connection.clients.title`)}),(0,J.jsx)(`span`,{children:l.length>0?l.join(`, `):r(`connection.clients.none`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,disabled:d,onClick:()=>void p(),children:r(d?`connection.clients.syncing`:`connection.clients.sync`)})]}),(0,J.jsx)(`div`,{className:`page-tabs`,role:`tablist`,"aria-label":r(`integrations.tabsLabel`),children:Jy.map(e=>(0,J.jsxs)(`button`,{ref:t=>{t?c.current.set(e.id,t):c.current.delete(e.id)},type:`button`,role:`tab`,id:Zy(e.id),"aria-selected":i===e.id,"aria-controls":Qy(e.id),tabIndex:i===e.id?0:-1,className:`page-tab${i===e.id?` page-tab--active`:``}`,onClick:()=>h(e.id,!0),onKeyDown:g,children:[$y(e.id)&&(0,J.jsx)(Gg,{src:$y(e.id),label:r(e.labelKey),size:14}),r(e.labelKey)]},e.id))}),Jy.map(t=>{if(!o.has(t.id))return null;let n=i===t.id;return(0,J.jsxs)(`div`,{role:`tabpanel`,id:Qy(t.id),"aria-labelledby":Zy(t.id),hidden:!n,children:[t.id===`overview`&&(0,J.jsx)(Hy,{apiBase:e,active:n}),t.id===`keys`&&(0,J.jsx)(A_,{apiBase:e,active:n}),t.id===`codex`&&(0,J.jsxs)(`section`,{className:`integration-native-page`,"aria-labelledby":`codex-integration-title`,children:[(0,J.jsx)(`h3`,{id:`codex-integration-title`,children:r(`integrations.codex.title`)}),(0,J.jsx)(`p`,{children:r(`integrations.codex.body`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost`,onClick:()=>pt(`startup`),children:r(`integrations.codex.openService`)})]}),t.id===`claude`&&(0,J.jsx)(bv,{apiBase:e,active:n}),t.id===`grok`&&(0,J.jsx)(Ev,{apiBase:e,active:n}),t.id===`cursor`&&(0,J.jsx)(Av,{apiBase:e,active:n}),Yy.has(t.id)&&(0,J.jsx)(qy,{apiBase:e,client:t.id,active:n})]},t.id)})]})}function tb(e){if(!e||typeof e!=`object`)return!1;let t=e;return typeof t.supported==`boolean`&&typeof t.installed==`boolean`&&typeof t.running==`boolean`&&typeof t.stale==`boolean`&&typeof t.summary==`string`}var nb={native:`startup.status.native`,protected:`startup.status.protected`,"at-risk":`startup.status.atRisk`},rb={native:`startup.summary.native`,protected:`startup.summary.protected`,"at-risk":`startup.summary.atRisk`},ib={service:`startup.protection.service`,shim:`startup.protection.shim`,none:`startup.protection.none`};function ab({ok:e,yes:t,no:n}){return(0,J.jsx)(`span`,{className:`badge ${e?`badge-green`:`badge-amber`}`,children:e?t:n})}function ob({failed:e,data:t}){let{t:n}=ct(),r=e?`startup-hero--risk`:t.status===`protected`?`startup-hero--safe`:t.status===`at-risk`?`startup-hero--risk`:`startup-hero--native`,i=e||t.status===`at-risk`?_e:ue,a=t.routingKind===`opencodex-local`?`startup.routing.proxy`:t.routingKind===`custom-local`?`startup.routing.customLocal`:t.routingKind===`custom-remote`?`startup.routing.customRemote`:t.routingKind===`unknown`?`startup.routing.unknown`:`startup.routing.native`;return(0,J.jsx)(J.Fragment,{children:(0,J.jsxs)(`section`,{className:`panel startup-hero ${r}`,"aria-live":`polite`,children:[(0,J.jsx)(`div`,{className:`startup-hero-icon`,children:(0,J.jsx)(i,{})}),(0,J.jsxs)(`div`,{className:`startup-hero-copy`,children:[(0,J.jsx)(`span`,{className:`badge ${e||t.status===`at-risk`?`badge-amber`:`badge-green`}`,children:n(e?`startup.status.atRisk`:nb[t.status])}),(0,J.jsx)(`h3`,{children:n(e?`startup.error`:rb[t.status])}),(0,J.jsx)(`p`,{children:e?n(`startup.staleData`):t.status===`at-risk`?n(xr(t)):n(`startup.safeDetail`)}),(0,J.jsxs)(`p`,{className:`muted startup-state-line`,children:[n(a),` · `,n(ib[t.protection]),` · `,n(t.autostartEnabled?`startup.enabled`:`startup.disabled`)]}),(0,J.jsx)(`p`,{className:`muted text-label`,children:n(`startup.subtitle`)})]})]})})}function sb({data:e,failed:t,loading:n=!1,installBusy:r,installResult:i,onInstall:a}){let{t:o}=ct(),s=e.serviceSupported&&e.serviceInstalled&&e.serviceStale&&!e.serviceConflict,c=e.shimInstalled&&!e.shimHealthy,l=r!==null||t||n;return(0,J.jsxs)(`section`,{className:`panel startup-details`,children:[(0,J.jsxs)(`div`,{className:`panel-head`,children:[(0,J.jsx)(`h3`,{className:`panel-title`,children:o(`startup.details`)}),(0,J.jsx)(`span`,{className:`muted mono`,children:e.platform})]}),(0,J.jsxs)(`div`,{className:`startup-detail-row`,children:[(0,J.jsxs)(`div`,{children:[(0,J.jsx)(`strong`,{children:o(`startup.service`)}),(0,J.jsx)(`span`,{children:o(`startup.serviceHint`)})]}),(0,J.jsxs)(`div`,{className:`startup-detail-actions`,children:[(0,J.jsx)(ab,{ok:e.serviceViable,yes:o(`startup.viable`),no:o(e.serviceConflict?`startup.conflict`:e.serviceStale?`startup.stale`:e.serviceInstalled?`startup.unhealthy`:e.serviceSupported?`startup.notInstalled`:`startup.unsupported`)}),e.serviceSupported&&!e.serviceInstalled&&(0,J.jsx)(`button`,{type:`button`,className:`btn btn-primary btn-sm`,"aria-label":`${o(`startup.service`)} - ${o(`startup.install`)}`,disabled:l,onClick:()=>a(`install-service`),children:o(r===`install-service`?`startup.installing`:`startup.install`)}),s&&(0,J.jsx)(`button`,{type:`button`,className:`btn btn-primary btn-sm`,"aria-label":`${o(`startup.service`)} - ${o(`startup.repair`)}`,disabled:l,onClick:()=>a(`install-service`,{repair:!0}),children:o(r===`install-service`?`startup.repairing`:`startup.repair`)})]})]}),(0,J.jsxs)(`div`,{className:`startup-detail-row`,children:[(0,J.jsxs)(`div`,{children:[(0,J.jsx)(`strong`,{children:o(`startup.shim`)}),(0,J.jsx)(`span`,{children:o(`startup.shimHint`)})]}),(0,J.jsxs)(`div`,{className:`startup-detail-actions`,children:[(0,J.jsx)(ab,{ok:e.shimHealthy&&e.autostartEnabled,yes:o(e.shimCoverage===`cli-only`?`startup.cliOnly`:`startup.healthy`),no:o(e.shimInstalled?e.shimHealthy&&!e.autostartEnabled?`startup.installedDisabled`:`startup.stale`:`startup.notInstalled`)}),!e.shimInstalled&&(0,J.jsx)(`button`,{type:`button`,className:`btn btn-primary btn-sm`,"aria-label":`${o(`startup.shim`)} - ${o(`startup.install`)}`,disabled:l,onClick:()=>a(`install-shim`),children:o(r===`install-shim`?`startup.installing`:`startup.install`)}),c&&(0,J.jsx)(`button`,{type:`button`,className:`btn btn-primary btn-sm`,"aria-label":`${o(`startup.shim`)} - ${o(`startup.repair`)}`,disabled:l,onClick:()=>a(`install-shim`,{repair:!0}),children:o(r===`install-shim`?`startup.repairing`:`startup.repair`)})]})]}),i&&(0,J.jsx)(`div`,{className:`notice ${i.kind===`success`?`notice-ok`:`notice-warn`} startup-action-notice`,role:`status`,"aria-live":`polite`,children:i.kind===`success`?i.action===`install-service`?o(i.repair?`startup.serviceRepaired`:`startup.serviceInstalled`):o(i.repair?`startup.shimRepaired`:`startup.shimInstalled`):`${o(`startup.installFailed`)} ${i.detail??``}`})]})}function cb({tray:e,trayLoading:t,trayError:n,trayBusy:r,onTrayAction:i}){let{t:a}=ct();return(0,J.jsxs)(`section`,{className:`panel startup-actions`,children:[(0,J.jsxs)(`div`,{className:`panel-head`,children:[(0,J.jsx)(`h3`,{className:`panel-title`,children:a(`startup.tray.title`)}),(0,J.jsx)(we,{})]}),(0,J.jsx)(`p`,{className:`muted`,children:a(`startup.tray.hint`)}),(0,J.jsxs)(`div`,{className:`startup-detail-row`,children:[(0,J.jsxs)(`div`,{children:[(0,J.jsx)(`strong`,{children:a(`startup.tray.login`)}),(0,J.jsx)(`span`,{children:a(`startup.tray.notProtection`)})]}),t||n||!e?(0,J.jsx)(`span`,{className:`badge badge-amber`,children:a(t?`startup.tray.loading`:`startup.tray.unavailable`)}):(0,J.jsx)(ab,{ok:e.running&&!e.stale,yes:a(`startup.tray.running`),no:a(e.stale?`startup.tray.stale`:e.installed?`startup.tray.stopped`:`startup.tray.notInstalled`)})]}),(0,J.jsxs)(`div`,{className:`startup-tray-buttons`,children:[!t&&!n&&e&&!e.installed&&!e.stale&&(0,J.jsx)(`button`,{type:`button`,className:`btn btn-primary`,disabled:r,onClick:()=>i(`install`),children:a(`startup.tray.install`)}),!t&&!n&&e?.installed&&!e.stale&&!e.running&&(0,J.jsx)(`button`,{type:`button`,className:`btn btn-primary`,disabled:r,onClick:()=>i(`start`),children:a(`startup.tray.start`)}),!t&&!n&&e?.running&&!e.stale&&(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost`,disabled:r,onClick:()=>i(`stop`),children:a(`startup.tray.stop`)}),!t&&!n&&e&&(e.installed||e.stale)&&(0,J.jsx)(`button`,{type:`button`,className:`btn btn-danger`,disabled:r,onClick:()=>{window.confirm(a(`startup.tray.uninstall`))&&i(`uninstall`)},children:a(`startup.tray.uninstall`)})]}),(n||e?.stale)&&(0,J.jsx)(`div`,{className:`notice notice-warn startup-tray-error`,role:`alert`,children:a(`startup.tray.error`)})]})}function lb({data:e,copied:t,onCopy:n}){let{t:r}=ct(),i=e.serviceInstalled&&!e.serviceConflict?e.commands.repairService:e.commands.installService;return(0,J.jsxs)(`section`,{className:`panel startup-actions`,children:[(0,J.jsxs)(`div`,{className:`panel-head`,children:[(0,J.jsx)(`h3`,{className:`panel-title`,children:r(`startup.recovery`)}),(0,J.jsx)(se,{})]}),(0,J.jsxs)(`details`,{className:`startup-recovery-details`,open:e.status!==`protected`,children:[(0,J.jsx)(`summary`,{className:`muted`,children:r(`startup.recoveryHint`)}),(0,J.jsxs)(`div`,{className:`startup-command-list`,children:[e.serviceSupported&&(0,J.jsxs)(`div`,{className:`startup-command-row`,children:[(0,J.jsxs)(`div`,{children:[(0,J.jsx)(`strong`,{children:r(`startup.command.service`)}),(0,J.jsx)(`code`,{children:i})]}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:()=>n(i),children:r(t===i?`startup.copied`:`startup.copy`)})]}),(0,J.jsxs)(`div`,{className:`startup-command-row`,children:[(0,J.jsxs)(`div`,{children:[(0,J.jsx)(`strong`,{children:r(`startup.command.shim`)}),(0,J.jsx)(`code`,{children:e.commands.installShim})]}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:()=>n(e.commands.installShim),children:t===e.commands.installShim?r(`startup.copied`):r(`startup.copy`)})]}),(0,J.jsxs)(`div`,{className:`startup-command-row`,children:[(0,J.jsxs)(`div`,{children:[(0,J.jsx)(`strong`,{children:r(`startup.command.native`)}),(0,J.jsx)(`code`,{children:e.commands.restoreNative})]}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:()=>n(e.commands.restoreNative),children:t===e.commands.restoreNative?r(`startup.copied`):r(`startup.copy`)})]})]}),e.status===`at-risk`&&(0,J.jsxs)(`div`,{className:`notice notice-warn startup-action-notice`,role:`alert`,children:[(0,J.jsx)(we,{}),` `,r(`startup.recommended`,{cmd:e.recommendedCommand??e.commands.installService})]})]})]})}var ub=`ocx.startup.page.v1:`;function db(e,t){let n=t===`win32`?`; `:` && `;return e.join(n)}function fb(e,t,n){if(!e)return{warning:null,fix:null};let r=!!e.catalogClamp?.active,i=!!e.newerAvailable,a=(r?e.catalogClamp?.runtimeVersion:e.version)??e.version??`unknown`,o=(e.catalogClamp?.removedEfforts??[]).join(`, `),s=db([`ocx doctor --fix-codex-runtime`,`ocx sync`],n);return r?{warning:o?t(`startup.codexRuntime.clampHiddenWithEfforts`,{version:a,efforts:o}):t(`startup.codexRuntime.clampHidden`,{version:a}),fix:i?s:`ocx sync`}:i?{warning:t(`startup.codexRuntime.olderBinary`,{version:a}),fix:s}:{warning:null,fix:null}}function pb({apiBase:e,machineApiBase:t=e,connected:n=!1}){let{t:r}=ct(),i=`${ub}${e}`,a=(0,_.useMemo)(()=>gr(i),[i]),o=`startup-page:${e}`,[s,c]=(0,_.useState)(null),[l,u]=(0,_.useState)(()=>a?.tray??null),[d,f]=(0,_.useState)(()=>!a?.data),[p,m]=(0,_.useState)(!1),[h,g]=(0,_.useState)(!1),[v,y]=(0,_.useState)(null),[b,x]=(0,_.useState)(null),[S,C]=(0,_.useState)(()=>a?.warning??null),[w,T]=(0,_.useState)(()=>a?.fix??null),[E,D]=(0,_.useState)(()=>!a?.data),O=(0,_.useRef)(!!a?.data),k=(0,_.useRef)(0),[A,j]=(0,_.useState)(null),[M,N]=(0,_.useState)(!1);(0,_.useEffect)(()=>{if(!n)return;let e=new AbortController;return fetch(`${t}/api/machine/shim`,{signal:e.signal}).then(e=>e.ok?e.json():null).then(t=>{e.signal.aborted||j(t)}).catch(()=>{e.signal.aborted||j(null)}),()=>e.abort()},[n,t]);let P=async e=>{N(!0);try{let n=await fetch(`${t}/api/machine/shim`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({action:e})});if(n.ok){let e=await n.json();j(e.shim??null)}}finally{N(!1)}};(0,_.useEffect)(()=>()=>{k.current+=1},[e]);let F=(0,_.useCallback)(async t=>{let n=++k.current,a=O.current;a||(f(!0),D(!0));try{let a=fetch(`${e}/api/settings`,{signal:t}).then(async e=>e.ok?await e.json():null).catch(()=>null),o=await fetch(`${e}/api/startup-health`,{signal:t});if(!o.ok)throw Error(`fetch failed`);let s=await o.json();x(e=>e?.kind===`error`&&(s.status===`native`&&e.forLocalRouting===!0||(e.action===`install-service`?s.serviceViable:s.shimInstalled&&s.shimHealthy))?null:e),O.current=!0;let c=gr(i);br(i,{data:s,warning:c?.warning??null,fix:c?.fix??null,tray:c?.tray??null});let l=s.platform===`win32`?fetch(`${e}/api/windows-tray`,{signal:t}).then(async e=>{if(!e.ok)throw Error(`tray status failed`);let t=await e.json();if(!tb(t))throw Error(`invalid tray status`);return{tray:t,error:!1}}).catch(()=>({tray:null,error:!0})):Promise.resolve({tray:null,error:!1});return Promise.all([a,l]).then(([e,a])=>{if(t.aborted||n!==k.current)return;let o=s.platform===`win32`?a.tray:null;if(s.platform===`win32`?(u(o),g(a.error)):(u(null),g(!1)),f(!1),D(!1),e){let t=fb(e.codexRuntime,r,s.platform);C(t.warning),T(t.fix),br(i,{data:s,warning:t.warning,fix:t.fix,tray:o});return}let c=gr(i);br(i,{data:s,warning:c?.warning??null,fix:c?.fix??null,tray:o})}),s}catch(e){throw t.aborted?e:(a||(u(null),g(!0),C(null),T(null)),D(!1),f(!1),e)}},[e,i,r]),I=ml(o,[e],F,{isEmpty:()=>!1,initialData:a?.data??void 0}),L=I.state,R=I.refresh,z=L.data??a?.data??null,B=L.refreshing,V=!!z?.diagnosticStale||L.showError;(0,_.useEffect)(()=>{if(!z?.diagnosticStale)return;let e=window.setTimeout(R,2e3);return()=>window.clearTimeout(e)},[z,R]);let H=async e=>{try{await navigator.clipboard.writeText(e),c(e),window.setTimeout(()=>c(t=>t===e?null:t),1600)}catch{c(null)}},U=async t=>{m(!0),g(!1);try{let n=await fetch(`${e}/api/windows-tray`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({action:t})});if(!n.ok)throw Error(`tray action failed`);let r=await n.json();if(!tb(r.status))throw Error(`invalid tray action status`);u(r.status),g(!1)}catch{u(null),g(!0)}finally{m(!1)}},W=async(t,n)=>{y(t),x(null);try{let r=await fetch(`${e}/api/startup-action`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({action:t,repair:n?.repair===!0})});if(!r.ok){let e=await r.json().catch(()=>null);throw Error(typeof e?.error==`string`?e.error:`installation failed`)}x({kind:`success`,action:t,repair:n?.repair===!0}),R()}catch(e){x({kind:`error`,action:t,repair:n?.repair===!0,detail:e instanceof Error?e.message:String(e),forLocalRouting:z?.localRoutingDependency===!0})}finally{y(null)}};return(0,J.jsxs)(J.Fragment,{children:[(0,J.jsxs)(`div`,{className:`page-head`,children:[(0,J.jsx)(`h2`,{children:r(`startup.title`)}),(0,J.jsx)(`div`,{className:`startup-page-head-actions`,children:(0,J.jsxs)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:()=>R(),disabled:B,children:[(0,J.jsx)(pe,{}),` `,r(`startup.refresh`)]})})]}),n&&(0,J.jsxs)(`section`,{className:`notice startup-page-notice`,"aria-label":r(`connection.machine.title`),children:[(0,J.jsx)(`strong`,{children:r(`connection.machine.title`)}),(0,J.jsx)(`span`,{children:A?.healthy?r(`connection.machine.shimHealthy`):r(`connection.machine.shimNeedsAttention`)}),(0,J.jsxs)(`div`,{className:`startup-page-head-actions`,children:[(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,disabled:M,onClick:()=>void P(`repair`),children:r(`connection.machine.repairShim`)}),A?.installed&&(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,disabled:M,onClick:()=>void P(`uninstall`),children:r(`connection.machine.removeShim`)})]})]}),L.showSkeleton&&!z?(0,J.jsx)(gl,{label:r(`startup.loading`),rows:5}):L.kind===`failed-cold`?(0,J.jsxs)(`div`,{className:`startup-page-notice`,children:[(0,J.jsx)($,{tone:`err`,children:L.error instanceof Error?L.error.message:r(`startup.error`)}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:()=>R(),children:r(`common.retry`)})]}):z?(0,J.jsxs)(J.Fragment,{children:[L.showError&&(0,J.jsx)($,{tone:`err`,children:r(`startup.error`)}),V&&(0,J.jsx)(`div`,{className:`notice notice-warn startup-page-notice`,role:`alert`,children:r(`startup.staleData`)}),(E||S)&&(0,J.jsx)(`div`,{className:`startup-runtime-notice-slot${E&&!S?` startup-runtime-notice-slot--pending`:``}`,"aria-hidden":E&&!S?!0:void 0,children:S&&(0,J.jsxs)(`div`,{className:`notice notice-warn startup-page-notice startup-runtime-notice`,role:`status`,children:[(0,J.jsx)(`p`,{className:`startup-runtime-notice__text`,children:S}),w&&(0,J.jsxs)(`div`,{className:`startup-runtime-notice__fix`,children:[(0,J.jsx)(`code`,{children:w}),(0,J.jsx)(`button`,{type:`button`,className:`btn btn-ghost btn-sm`,onClick:()=>void H(w),children:r(s===w?`startup.copied`:`startup.copy`)})]})]})}),(0,J.jsx)(ob,{failed:V,data:z}),(0,J.jsx)(sb,{data:z,failed:V,loading:B,installBusy:v,installResult:b,onInstall:(e,t)=>{W(e,t)}}),z.platform===`win32`&&(0,J.jsx)(cb,{tray:l,trayLoading:d,trayError:h,trayBusy:p,onTrayAction:e=>{U(e)}}),(0,J.jsx)(lb,{data:z,copied:s,onCopy:e=>{H(e)}})]}):null]})}var mb=3e5,hb=6e5,gb=`https://github.com/lidge-jun/opencodex`;async function _b(e,t){let n=await fetch(e,{signal:t});return n.ok?await n.json():null}function vb({apiBase:e,onOpenUpdate:t}){let n=Q(),[r,i]=(0,_.useState)(!1),[a,o]=(0,_.useState)(null),s=G(`sidebar-star:${e}`,[e],t=>_b(`${e}/api/github/star`,t),{pollMs:mb}),c=G(`sidebar-update-badge:${e}`,[e],t=>_b(`${e}/api/update/badge`,t),{pollMs:hb}),l=s.data?.state??null,u=a!==null&&a.basedOn===l?a.state:l??`not-starred`,d=s.data?.url??gb,f=u===`starred`,p=c.data,m=p?.updateAvailable===!0,h=p?.latestVersion??null,g=()=>window.open(d,`_blank`,`noopener,noreferrer`),v=async()=>{if(!(f||r)){if(u===`unauthenticated`){g();return}i(!0);try{let t=await fetch(`${e}/api/github/star`,{method:`POST`}),n=t.ok?await t.json():null;if(n?.ok===!0){o({state:`starred`,basedOn:l});return}n?.state&&o({state:n.state,basedOn:l}),g()}catch{g()}finally{i(!1),s.refresh()}}},y=n(f?`sidebar.starred`:u===`unauthenticated`?`sidebar.starUnauthenticated`:`sidebar.star`),b=m&&h?n(`sidebar.updateAvailable`,{version:h}):n(`sidebar.checkUpdate`);return(0,J.jsxs)(`div`,{className:`sidebar-github-row`,children:[(0,J.jsxs)(`a`,{className:`sidebar-link sidebar-github-link`,href:d,target:`_blank`,rel:`noreferrer`,children:[(0,J.jsx)(Ce,{}),` `,n(`common.github`)]}),(0,J.jsxs)(`div`,{className:`sidebar-github-actions`,children:[(0,J.jsx)(`button`,{type:`button`,className:`sidebar-orb${f?` sidebar-orb--starred`:``}`,onClick:()=>{v()},disabled:r||f,"aria-label":y,"aria-pressed":f,title:y,children:(0,J.jsx)(Ie,{"aria-hidden":`true`,...f?{fill:`currentColor`}:{}})}),(0,J.jsxs)(`button`,{type:`button`,className:`sidebar-orb${m?` sidebar-orb--update`:``}`,onClick:t,"aria-label":b,title:b,children:[(0,J.jsx)(xe,{"aria-hidden":`true`}),m&&(0,J.jsx)(`span`,{className:`sidebar-orb-dot`,"aria-hidden":`true`})]})]})]})}var yb=`opencodex-admin-token-dialog`,bb=`OpenCodex`;function xb(e,t=it()){let n=Ze[t],r=n[`auth.adminTokenTitle`];return new Promise(t=>{let i=document.activeElement instanceof HTMLElement?document.activeElement:null,a=!1,o=document.createElement(`dialog`);o.id=yb,o.className=`modal-overlay`,o.setAttribute(`aria-labelledby`,`${yb}-title`);let s=document.createElement(`form`);s.className=`modal-card`,s.method=`post`,s.action=window.location.href,s.autocomplete=`on`;let c=document.createElement(`div`);c.className=`modal-head`;let l=document.createElement(`h3`);l.id=`${yb}-title`,l.textContent=r,c.append(l);let u=document.createElement(`div`),d=document.createElement(`label`);d.className=`field-label`,d.htmlFor=`${yb}-username`,d.textContent=n[`auth.adminAccountLabel`];let f=document.createElement(`input`);f.id=d.htmlFor,f.className=`input`,f.type=`text`,f.name=`username`,f.autocomplete=`username`,f.value=bb,f.readOnly=!0,u.append(d,f);let p=document.createElement(`div`);p.style.marginTop=`var(--space-4)`;let m=document.createElement(`label`);m.className=`field-label`,m.htmlFor=`${yb}-password`,m.textContent=n[`auth.adminTokenFieldLabel`];let h=document.createElement(`input`);h.id=m.htmlFor,h.className=`input`,h.type=`password`,h.name=`password`,h.autocomplete=`current-password`,h.required=!0,h.spellcheck=!1,h.autocapitalize=`none`,p.append(m,h);let g=document.createElement(`div`);g.className=`notice notice-err`,g.setAttribute(`role`,`alert`),g.hidden=!0;let _=document.createElement(`div`);_.className=`modal-actions`;let v=document.createElement(`button`);v.type=`button`,v.className=`btn btn-ghost`,v.textContent=n[`common.cancel`];let y=document.createElement(`button`);y.type=`submit`,y.className=`btn btn-primary`,y.textContent=n[`common.ok`],_.append(v,y),s.append(c,u,p,g,_),o.append(s);let b=e=>{a||(a=!0,o.open&&o.close(),o.remove(),i?.focus(),t(e))};s.addEventListener(`submit`,t=>{t.preventDefault();let r=h.value.trim();if(!r){h.value=``,h.reportValidity();return}h.disabled=!0,y.disabled=!0,g.hidden=!0,e(r).then(e=>{if(!a){if(e===`accepted`){b(r);return}h.value=``,h.disabled=!1,y.disabled=!1,g.textContent=e===`rejected`?n[`auth.adminTokenRejected`]:n[`auth.adminTokenUnavailable`],g.hidden=!1,h.focus()}}).catch(()=>{a||(h.value=``,h.disabled=!1,y.disabled=!1,g.textContent=n[`auth.adminTokenUnavailable`],g.hidden=!1,h.focus())})}),v.addEventListener(`click`,()=>b(null)),o.addEventListener(`cancel`,e=>{e.preventDefault(),b(null)}),document.body.append(o),typeof o.showModal==`function`?o.showModal():o.setAttribute(`open`,``),queueMicrotask(()=>h.focus())})}var Sb=`opencodex-api-token`,Cb=`/api/settings`,wb=1e4,Tb=15e3,Eb=`X-OpenCodex-Machine-Session`,Db=`X-OpenCodex-Machine-GUI-Origin`,Ob=`X-OpenCodex-Machine-CSRF-Token`,kb=!1,Ab=null,jb=null,Mb=xb,Nb=wb,Pb=Tb,Fb=new Map;function Ib(){return{token:null,csrfToken:null,browserOrigin:null,serverOrigin:null}}function Lb(){return jb||zb(Qg(``)),jb}function Rb(e,t){return e.baseUrl===t.baseUrl&&e.serverOrigin===t.serverOrigin&&e.transport===t.transport}function zb(e){jb=e;for(let t of[`machine`,`shared`]){let n=Fb.get(t);Fb.set(t,n&&Rb(n.target,e[t])?{...n,target:e[t]}:{target:e[t],session:Ib(),resolutionInFlight:null,promptCancelled:!1})}}function Bb(e){return Lb(),Fb.get(e)}function Vb(e,t){let n=Bb(e);t!==null&&n.session.token===t&&(n.session=Ib())}function Hb(e,t,n,r,i){let a=Bb(e);return!t?.startsWith(`ocx_session_`)||!n||r!==window.location.origin||i!==a.target.serverOrigin?(a.session=Ib(),!1):(a.session={token:t,csrfToken:n,browserOrigin:r,serverOrigin:i},a.promptCancelled=!1,!0)}function Ub(e){return!!Bb(e).session.token?.startsWith(`ocx_session_`)}async function Wb(e){let t=Bb(e);if(!t.session.token?.startsWith(`ocx_session_`))return!1;let n=Vn(wb);try{return(await window.fetch(`${t.target.baseUrl}/api/session/logout`,{method:`POST`,signal:n.signal})).ok?(t.session=Ib(),t.promptCancelled=!1,!0):!1}catch{return!1}finally{n.clear()}}function Gb(e){let t=document.querySelector(`meta[name="${e}"]`),n=t?.content.trim()||null;return t?.remove(),n}function Kb(){let e={token:Gb(`opencodex-session-token`),csrf:Gb(`opencodex-session-csrf`),browser:Gb(`opencodex-session-origin`),server:Gb(`opencodex-session-server-origin`)};for(let t of[`machine`,`shared`])Bb(t).target.serverOrigin===e.server&&Hb(t,e.token,e.csrf,e.browser,e.server)}function qb(e,t){for(let n of e.match(/]*>/gi)??[])if(n.match(/\bname=["']([^"']+)["']/i)?.[1]===t)return n.match(/\bcontent=["']([^"']*)["']/i)?.[1]?.trim()||null;return null}function Jb(e,t){return Hb(e,qb(t,`opencodex-session-token`),qb(t,`opencodex-session-csrf`),qb(t,`opencodex-session-origin`),qb(t,`opencodex-session-server-origin`))}function Yb(){try{sessionStorage.removeItem(Sb)}catch{}}function Xb(e){return new URL(e.baseUrl||`/`,window.location.href)}function Zb(e,t){let n=Xb(e);if(t.origin!==n.origin)return!1;let r=n.pathname.replace(/\/$/,``);return r===``||t.pathname===r||t.pathname.startsWith(`${r}/`)}function Qb(e,t){if(!Zb(e,t))return null;let n=Xb(e).pathname.replace(/\/$/,``);return t.pathname.slice(n.length)||`/`}function $b(e){let t;try{t=new URL(e instanceof Request?e.url:String(e),window.location.href)}catch{return null}let n=Lb();return t.href===new URL(n.shared.bootstrapPath,window.location.href).href?{plane:`shared`,bootstrap:!0}:n.shared.transport===`relay`&&Zb(n.shared,t)?{plane:`shared`,bootstrap:!1}:Qb(n.machine,t)?.startsWith(`/api/machine/`)?{plane:`machine`,bootstrap:!1}:Qb(n.shared,t)?.startsWith(`/api/`)?{plane:`shared`,bootstrap:!1}:t.href===new URL(n.machine.bootstrapPath,window.location.href).href?{plane:`machine`,bootstrap:!0}:null}function ex(e,t,n,r){let i=Bb(e),a=new Headers(n?.headers??(t instanceof Request?t.headers:void 0)),o=r===void 0?i.session.token:r,s=(n?.method??(t instanceof Request?t.method:`GET`)).toUpperCase();if(o&&a.set(`X-OpenCodex-API-Key`,o),o?.startsWith(`ocx_session_`)&&i.session.browserOrigin&&i.session.csrfToken&&(a.set(`X-OpenCodex-GUI-Origin`,i.session.browserOrigin),s!==`GET`&&s!==`HEAD`&&a.set(`X-OpenCodex-CSRF-Token`,i.session.csrfToken)),e===`shared`&&i.target.transport===`relay`){let e=Bb(`machine`).session;e.token&&a.set(Eb,e.token),e.browserOrigin&&a.set(Db,e.browserOrigin),s!==`GET`&&s!==`HEAD`&&e.csrfToken&&a.set(Ob,e.csrfToken)}return a}function tx(e,t,n,r){let i=ex(e,t,n,r);return t instanceof Request?[new Request(t,{headers:i}),n?{...n,headers:i}:void 0]:[t,{...n,headers:i}]}async function nx(e){if(!Ab)return{kind:`failed`};let t=Bb(e),n=Vn(Nb);try{let[r,i]=tx(e,t.target.bootstrapPath,{cache:`no-store`,signal:n.signal},null),a=await Ab(r,i);return a.ok?Jb(e,await a.text())?{kind:`minted`,token:Bb(e).session.token}:{kind:`unavailable`}:a.status>=400&&a.status<500?{kind:`unavailable`}:{kind:`failed`}}catch{return{kind:`failed`}}finally{n.clear()}}async function rx(e,t){if(!Ab)return`unavailable`;try{let[n,r]=tx(e,`${Bb(e).target.baseUrl}${Cb}`,{cache:`no-store`},t),i=await Ab(n,r);return i.status===401?`rejected`:i.ok?`accepted`:`unavailable`}catch{return`unavailable`}}async function ix(e,t,n){let r=Bb(e);if(r.promptCancelled||n?.aborted)return null;if(!r.resolutionInFlight){let n=(async()=>{let n=r.session.token;if(n&&n!==t)return n;let i,a=await Promise.race([nx(e),new Promise(e=>{i=setTimeout(()=>e({kind:`failed`}),Pb)})]).finally(()=>clearTimeout(i));if(a.kind===`minted`)return a.token;if(a.kind===`failed`)return null;let o=await Mb(t=>rx(e,t));return o?(r.session={token:o,csrfToken:null,browserOrigin:null,serverOrigin:r.target.serverOrigin},o):(r.promptCancelled=!0,null)})().finally(()=>{r.resolutionInFlight===n&&(r.resolutionInFlight=null)});r.resolutionInFlight=n}if(!n)return r.resolutionInFlight;let i,a=new Promise(e=>{i=()=>e(null),n.addEventListener(`abort`,i,{once:!0})});return Promise.race([r.resolutionInFlight,a]).finally(()=>{i&&n.removeEventListener(`abort`,i)})}function ax(){if(kb)return;kb=!0,Yb(),Lb(),Kb();let e=window.fetch.bind(window);Ab=e,window.fetch=async(t,n)=>{let r=$b(t);if(!r)return e(t,n);let i=Bb(r.plane),a=i.session.token,[o,s]=tx(r.plane,t,n),c=await e(o,s);if(r.bootstrap||c.status!==401)return c;let l=i.session.token;if(l&&l!==a){let[i,a]=tx(r.plane,t,n),o=await e(i,a);if(o.status!==401)return o;Vb(r.plane,l)}else Vb(r.plane,a);let u=n?.signal??(t instanceof Request?t.signal:void 0),d=await ix(r.plane,a,u??void 0);if(!d)return c;let[f,p]=tx(r.plane,t,n,d),m=await e(f,p);return m.status===401&&Vb(r.plane,d),m}}var ox=/^ocx_pair_[A-Za-z0-9_-]{43}$/;async function sx(e,t,n){let r=t.trim();if(!ox.test(r))throw Error(`pairing_code_invalid`);let i=await(n??((e,t)=>window.fetch(e,t)))(e.bootstrapPath,{method:`POST`,headers:{"Content-Type":`application/json`,Accept:`text/html`},body:JSON.stringify({grant:r})});if(!i.ok)throw Error(`pairing_refused`);if(!Jb(`shared`,await i.text()))throw Error(`pairing_response_invalid`);return!0}function cx({target:e,onConnected:t}){let n=Q(),[r,i]=(0,_.useState)(``),[a,o]=(0,_.useState)(!1),[s,c]=(0,_.useState)(!1);return(0,_.createElement)(`section`,{className:`card connect-pairing`,"aria-labelledby":`connect-pairing-title`},(0,_.createElement)(`h2`,{id:`connect-pairing-title`},n(`connection.pairing.title`)),(0,_.createElement)(`p`,null,n(e.transport===`relay`?`connection.pairing.relayWarning`:`connection.pairing.body`)),(0,_.createElement)(`form`,{onSubmit:async n=>{if(n.preventDefault(),!a){o(!0),c(!1);try{await sx(e,r),t()}catch{c(!0)}finally{o(!1)}}},className:`api-form-row`},(0,_.createElement)(`label`,{htmlFor:`connect-pairing-code`,className:`field-label`},n(`connection.pairing.code`)),(0,_.createElement)(`input`,{id:`connect-pairing-code`,name:`pairingCode`,value:r,onChange:e=>i(e.currentTarget.value),autoComplete:`off`,spellCheck:!1,disabled:a,className:`input mono`,"aria-invalid":s||void 0,"aria-describedby":s?`connect-pairing-error`:void 0}),(0,_.createElement)(`button`,{type:`submit`,className:`btn btn-primary`,disabled:a||!r.trim()},n(a?`connection.pairing.submitting`:`connection.pairing.submit`)),s?(0,_.createElement)(`p`,{id:`connect-pairing-error`,className:`alert alert-err`,role:`alert`},n(`connection.pairing.error`)):null))}var lx=new Set([`dashboard`,`startup`,`providers`,`models`,`subagents`,`logs`,`usage`,`storage`,`codex-set`,`integrations`]);function ux(e){let t=dt(e??(typeof window<`u`?window.location.hash:``)).split(`/`)[0];return t===`debug`?`logs`:t===`codex-auth`?`codex-set`:t===`combos`||t===`routing`||t===`lab`?`models`:t===`api`||t===`claude`||t===`grok`?`integrations`:lx.has(t)?t:`dashboard`}var dx=[`dashboard/providers`,`dashboard/models`],fx=[`models/combos`,`models/routing`,`models/compatibility`],px=[`integrations/keys`,`integrations/codex`,`integrations/claude`,`integrations/claude/desktop`,`integrations/grok`,`integrations/cursor`,`integrations/opencode`,`integrations/pi`,`integrations/omp`,`integrations/hermes`,`integrations/openclaw`,`integrations/kimi`,`integrations/gajae`,`integrations/dsh`,`integrations/mcode`,`integrations/zcode`,`integrations/prime`,`integrations/aside`];function mx(e,t){return e===t||t===`logs`&&e===`logs/debug`||t===`codex-set`&&e===`codex-set/prompt`||t===`models`&&fx.includes(e)||t===`dashboard`&&(e===`dashboard/update`||dx.includes(e))||t===`integrations`&&px.includes(e)}function hx(e){let t=ux(e);return e===`debug`||e.startsWith(`debug/`)?{page:`logs`,replaceTo:`logs/debug`}:e===`codex-auth`||e.startsWith(`codex-auth/`)?{page:`codex-set`,replaceTo:`codex-set`}:e===`combos`||e.startsWith(`combos/`)?{page:`models`,replaceTo:`models/combos`}:e===`routing`||e.startsWith(`routing/`)?{page:`models`,replaceTo:`models/routing`}:e===`lab`||e.startsWith(`lab/`)?{page:`models`,replaceTo:`models/compatibility`}:e===`api`?{page:`integrations`,replaceTo:`integrations/keys`}:e===`claude`?{page:`integrations`,replaceTo:`integrations/claude`}:e===`grok`?{page:`integrations`,replaceTo:`integrations/grok`}:e===`providers/workspace`?{page:`providers`,replaceTo:`providers`}:mx(e,t)?{page:t,replaceTo:null}:{page:t,replaceTo:t}}var gx=[`ocx-global-view`,`ocx-view`,`ocx-providers-view`,`ocx-subagents-view`,`ocx-storage-view`,`ocx-codexauth-view`,`ocx-apikeys-view`,`ocx-claudecode-view`,`ocx-usage-view`,`ocx-logs-view`,`ocx-models-view`,`ocx-dashboard-view`];function _x(){try{for(let e of gx)localStorage.removeItem(e)}catch{}}function vx(){let[e,t]=(0,_.useState)(ux);(0,_.useEffect)(()=>{_x()},[]);let n=(0,_.useCallback)(e=>{let n=hx(e);n.replaceTo&&ft(n.replaceTo),t(n.page)},[]);return(0,_.useEffect)(()=>{let e=()=>{n(dt(window.location.hash))};return window.addEventListener(`hashchange`,e),window.addEventListener(`popstate`,e),()=>{window.removeEventListener(`hashchange`,e),window.removeEventListener(`popstate`,e)}},[n]),(0,_.useEffect)(()=>{let n=hx(dt(window.location.hash));n.replaceTo&&ft(n.replaceTo),n.page!==e&&t(n.page)},[e]),{page:e,setPageState:t,navigateToPage:(e,n)=>{pt(n?`${e}/${n}`:e),t(e)}}}var yx=15e3;function bx(e,t,n){return typeof e?.message==`string`&&e.message.trim()?e.message:typeof e?.error==`string`&&e.error.trim()?e.error:n(t)}function xx(e){return(e instanceof DOMException||e instanceof Error)&&e.name===`AbortError`}async function Sx(e,t={}){let{fetchFn:n=fetch,timeoutMs:r=yx,formatFailure:i=e=>`Failed to stop proxy (HTTP ${e}).`,mode:a=`standalone`}=t,o;try{o=await n(`${e}${a===`client`?`/api/machine/disconnect`:`/api/stop`}`,{method:`POST`,...a===`client`?{headers:{"Content-Type":`application/json`},body:`{}`}:{},signal:AbortSignal.timeout(r)})}catch(e){return xx(e),{accepted:!0}}let s=await o.json().catch(()=>null);return!o.ok||s?.success===!1?{accepted:!1,message:bx(s,o.status,i)}:{accepted:!0}}var Cx={dashboard:`nav.dashboard`,startup:`nav.startup`,providers:`nav.providers`,models:`nav.models`,subagents:`nav.subagents`,logs:`nav.logs`,usage:`nav.usage`,storage:`nav.storage`,"codex-set":`nav.codexSet`,integrations:`nav.integrations`},wx=``,Tx=Qg(wx);zb(Tx),ax();var Ex=`ocx-theme`,Dx=[{id:`dashboard`,tkey:`nav.dashboard`,Icon:te},{id:`codex-set`,tkey:`nav.codexSet`,Icon:Ee},{id:`providers`,tkey:`nav.providers`,Icon:ne},{id:`models`,tkey:`nav.models`,Icon:re},{id:`subagents`,tkey:`nav.subagents`,Icon:ie},{id:`logs`,tkey:`nav.logs`,Icon:ae},{id:`usage`,tkey:`nav.usage`,Icon:ce},{id:`storage`,tkey:`nav.storage`,Icon:le},{id:`integrations`,tkey:`nav.integrations`,Icon:Ne}],Ox={light:Ae,dark:je,system:Me},kx={light:`theme.light`,dark:`theme.dark`,system:`theme.system`};function Ax(e){if(!e||typeof e!=`object`||!(`version`in e))return null;let t=e.version;return typeof t==`string`&&t.length>0?t:null}function jx(){let e=localStorage.getItem(Ex);return e===`light`||e===`dark`?e:`system`}function Mx(){let{page:e,navigateToPage:t}=vx(),[n,r]=(0,_.useState)(Nf);(0,_.useEffect)(()=>{let e=()=>r(Nf());return window.addEventListener(`hashchange`,e),window.addEventListener(`popstate`,e),()=>{window.removeEventListener(`hashchange`,e),window.removeEventListener(`popstate`,e)}},[]);let[i,a]=(0,_.useState)(jx),{locale:o,setLocale:s}=ct(),c=Q(),[l,u]=(0,_.useState)(Tx),[d,f]=(0,_.useState)(()=>!qg()),[p,m]=(0,_.useState)(!1),[h,g]=(0,_.useState)(()=>Ub(`shared`)),[v,y]=(0,_.useState)(!1);(0,_.useEffect)(()=>{let e=new AbortController;return n_(wx,e.signal).then(async t=>{if(zb(t),u(t),t.connected&&!Ub(`shared`))try{let n=await fetch(t.shared.bootstrapPath,{cache:`no-store`,signal:AbortSignal.any([e.signal,AbortSignal.timeout(5e3)])});n.ok&&Jb(`shared`,await n.text())}catch{}e.signal.aborted||(g(Ub(`shared`)),m(!1),f(!0))}).catch(()=>{e.signal.aborted||(m(!0),f(!0))}),()=>e.abort()},[]);let b=t_(`machine`,l),x=t_(`shared`,l),[S,C]=(0,_.useState)(!1),w=(0,_.useRef)(null),T=(0,_.useRef)(null),E=(0,_.useRef)(!1);(0,_.useEffect)(()=>{let e=()=>C(!1);return window.addEventListener(`hashchange`,e),window.addEventListener(`popstate`,e),()=>{window.removeEventListener(`hashchange`,e),window.removeEventListener(`popstate`,e)}},[]),(0,_.useEffect)(()=>{let e=document.documentElement;i===`system`?(e.removeAttribute(`data-theme`),localStorage.removeItem(Ex)):(e.setAttribute(`data-theme`,i),localStorage.setItem(Ex,i))},[i]);let D=G(`app-healthz:${b}`,[b,d],async e=>{let t=await fetch(`${b}/healthz`,{signal:e});return t.ok?Ax(await t.json()):null},{pollMs:3e4,enabled:d}),O=()=>a(e=>e===`light`?`dark`:e===`dark`?`system`:`light`),k=Ox[i],A=D.data??`2.42.0`,[j,M]=(0,_.useState)(!1);(0,_.useEffect)(()=>{if(!S)return;let e=e=>{e.key===`Escape`&&C(!1)};window.addEventListener(`keydown`,e);let t=document.body.style.overflow;return document.body.style.overflow=`hidden`,()=>{window.removeEventListener(`keydown`,e),document.body.style.overflow=t}},[S]),(0,_.useEffect)(()=>{if(S){E.current=!0;let e=setTimeout(()=>T.current?.focus(),200);return()=>clearTimeout(e)}E.current&&(E.current=!1,w.current?.focus())},[S]),(0,_.useEffect)(()=>{let e=window.matchMedia(`(min-width: 761px)`),t=()=>{e.matches&&C(!1)};return e.addEventListener(`change`,t),()=>e.removeEventListener(`change`,t)},[]);let[N,P]=(0,_.useState)(0),{restarting:F,restart:I}=sl(x,{onSettled:()=>P(e=>e+1)}),L=async()=>{if(!confirm(c(l.connected?`connection.disconnectConfirm`:`dash.stopConfirm`)))return;M(!0);let e=await Sx(b,{formatFailure:e=>c(`dash.stopFailed`,{status:String(e)}),mode:l.connected?`client`:`standalone`});e.accepted||(M(!1),alert(e.message))},R=async()=>{if(v)return;y(!0);let e=await Wb(`shared`);y(!1),e?g(!1):alert(c(`connection.sessionLogoutFailed`))},z=(0,J.jsxs)(`div`,{className:`brand`,children:[(0,J.jsx)(`span`,{className:`brand-logo`,role:`img`,"aria-label":c(`app.logoAria`)}),(0,J.jsx)(`span`,{className:`name`,children:`opencodex`}),(0,J.jsxs)(`span`,{className:`ver`,children:[`v`,A]})]});return(0,J.jsxs)(`div`,{className:`app`,children:[(0,J.jsxs)(`header`,{className:`mobile-topbar`,inert:S,children:[(0,J.jsx)(`button`,{ref:w,type:`button`,className:`menu-toggle`,onClick:()=>C(e=>!e),"aria-expanded":S,"aria-controls":`app-sidebar`,"aria-label":c(S?`nav.closeMenu`:`nav.openMenu`),title:c(S?`nav.closeMenu`:`nav.openMenu`),children:(0,J.jsx)(oe,{})}),z,(0,J.jsxs)(`div`,{className:`mobile-topbar-actions`,children:[l.connected&&h&&(0,J.jsx)(`button`,{type:`button`,className:`sidebar-orb`,onClick:()=>{R()},disabled:v,"aria-label":c(v?`connection.sessionLoggingOut`:`connection.sessionLogout`),title:c(`connection.sessionLogout`),children:(0,J.jsx)(de,{})}),(0,J.jsx)(`button`,{type:`button`,className:`sidebar-orb sidebar-orb--danger`,onClick:L,disabled:j,"aria-label":c(l.connected?`connection.disconnect`:`dash.stop`),title:c(l.connected?`connection.disconnect`:`dash.stop`),children:(0,J.jsx)(we,{})}),(0,J.jsx)(`button`,{type:`button`,className:`sidebar-orb`,onClick:()=>{I()},disabled:F,"aria-label":c(`dash.codexRestart`),title:c(`dash.codexRestart`),children:(0,J.jsx)(pe,{})})]})]}),S&&(0,J.jsx)(`div`,{className:`drawer-scrim`,onClick:()=>C(!1),"aria-hidden":`true`}),(0,J.jsxs)(`aside`,{id:`app-sidebar`,className:`sidebar${S?` open`:``}`,ref:T,tabIndex:-1,children:[(0,J.jsxs)(`div`,{className:`drawer-head`,children:[z,(0,J.jsx)(`button`,{type:`button`,className:`menu-toggle drawer-close`,onClick:()=>C(!1),"aria-label":c(`nav.closeMenu`),title:c(`nav.closeMenu`),children:(0,J.jsx)(de,{})})]}),(0,J.jsx)(`nav`,{children:Dx.map(n=>{let{id:r,tkey:i,Icon:a}=n,o=r===e;return(0,J.jsx)(`div`,{className:`nav-entry`,children:(0,J.jsxs)(`button`,{type:`button`,className:`nav-item${o?` active`:``}`,"data-page":r,onClick:()=>{t(r),C(!1)},"aria-current":o?`page`:void 0,children:[(0,J.jsx)(a,{}),` `,c(i)]})},r)})}),(0,J.jsxs)(`div`,{className:`sidebar-foot`,children:[(0,J.jsxs)(`div`,{className:`lang-toggle`,children:[(0,J.jsx)(Ne,{"aria-hidden":!0}),(0,J.jsx)(Dt,{value:o,options:et.map(e=>({value:e.code,label:Qe(e.code)})),onChange:e=>s(e),label:c(`lang.label`),placement:`right`,portal:!1,style:{flex:1,minWidth:0,width:`100%`}})]}),(0,J.jsxs)(`button`,{type:`button`,className:`theme-toggle`,onClick:O,"aria-label":`${c(`theme.label`)}: ${c(kx[i])}`,title:`${c(`theme.label`)}: ${c(kx[i])}`,children:[(0,J.jsx)(k,{}),` `,(0,J.jsx)(`span`,{className:`mode`,children:c(kx[i])})]}),(0,J.jsxs)(`div`,{className:`sidebar-action-row`,children:[(0,J.jsx)(`span`,{className:`sidebar-action-label`,children:c(`dash.actions`)}),(0,J.jsxs)(`div`,{className:`sidebar-action-orbs`,children:[l.connected&&h&&(0,J.jsx)(`button`,{type:`button`,className:`sidebar-orb`,onClick:()=>{R()},disabled:v,"aria-label":c(v?`connection.sessionLoggingOut`:`connection.sessionLogout`),title:c(`connection.sessionLogout`),children:(0,J.jsx)(de,{})}),(0,J.jsx)(`button`,{type:`button`,className:`sidebar-orb sidebar-orb--danger`,onClick:L,disabled:j,"aria-label":c(j?`dash.stopping`:l.connected?`connection.disconnect`:`dash.stop`),title:c(j?`dash.stopping`:l.connected?`connection.disconnect`:`dash.stop`),children:(0,J.jsx)(we,{})}),(0,J.jsx)(`button`,{type:`button`,className:`sidebar-orb`,onClick:()=>{I()},disabled:F,"aria-label":c(F?`dash.codexRestarting`:`dash.codexRestart`),title:c(F?`dash.codexRestarting`:`dash.codexRestart`),children:(0,J.jsx)(pe,{})})]})]}),(0,J.jsx)(vb,{apiBase:x,onOpenUpdate:()=>{C(!1),t(`dashboard`,`update`)}})]})]}),(0,J.jsx)(`main`,{className:`main`,inert:S,children:(0,J.jsx)(`div`,{className:`main-inner${e===`models`&&n===`combos`?` main-inner--combos`:``}`,children:(0,J.jsx)(vl,{pageName:c(Cx[e]),title:c(`errorBoundary.title`),message:c(`errorBoundary.message`),detailsLabel:c(`errorBoundary.details`),reloadLabel:c(`errorBoundary.reload`),children:d?(0,J.jsxs)(J.Fragment,{children:[p&&(0,J.jsx)(`div`,{className:`alert alert-err`,role:`alert`,children:c(`connection.machineUnavailable`)}),l.connected&&!h&&(0,J.jsx)(cx,{target:l.shared,onConnected:()=>g(!0)}),e===`dashboard`&&(0,J.jsx)(Xr,{apiBase:x}),e===`startup`&&(0,J.jsx)(pb,{apiBase:x,machineApiBase:b,connected:l.connected}),e===`providers`&&(0,J.jsx)(Jc,{apiBase:x}),e===`models`&&(0,J.jsx)(mp,{apiBase:x,restartEpoch:N},x),e===`subagents`&&(0,J.jsx)(Ep,{apiBase:x},x),e===`logs`&&(0,J.jsx)(th,{apiBase:x}),e===`usage`&&(0,J.jsx)(Ch,{apiBase:x,connected:l.connected,apiKeyId:l.apiKeyId}),e===`storage`&&(0,J.jsx)(Qh,{apiBase:x}),e===`codex-set`&&(0,J.jsx)(Fg,{apiBase:x}),e===`integrations`&&(0,J.jsx)(eb,{apiBase:x,machineApiBase:b,connected:l.connected})]}):(0,J.jsx)(`div`,{className:`alert`,children:c(`connection.discovering`)})},e)})})]})}g.createRoot(document.getElementById(`root`)).render((0,J.jsx)(_.StrictMode,{children:(0,J.jsx)(lt,{children:(0,J.jsx)(Mx,{})})})); \ No newline at end of file diff --git a/go/internal/embeddedui/static/assets/index-DL9-iS6J.css b/go/internal/embeddedui/static/assets/index-DL9-iS6J.css deleted file mode 100644 index 97242350bf..0000000000 --- a/go/internal/embeddedui/static/assets/index-DL9-iS6J.css +++ /dev/null @@ -1 +0,0 @@ -.provider-catalog{flex-direction:column;gap:10px;display:flex}.provider-catalog-tabs{border-bottom:1px solid var(--border);gap:4px;display:flex}.provider-catalog-tab{appearance:none;color:var(--muted);font:inherit;cursor:pointer;background:0 0;border:none;border-bottom:2px solid #0000;padding:8px 12px}.provider-catalog-tab.active{color:var(--fg);border-bottom-color:var(--accent)}.provider-catalog-accounts-hint{padding:2px 2px 0}.provider-catalog-search{width:100%}.provider-catalog-rows{flex-direction:column;gap:6px;max-height:360px;display:flex;overflow-y:auto}.provider-catalog-badges{flex-shrink:0;align-items:center;gap:4px;display:flex}.provider-catalog-empty{padding:8px}.provider-catalog-account-row .sub{text-overflow:ellipsis;overflow:hidden}.provider-catalog-rows .provider-icon{flex:none}.provider-catalog-rows .provider-icon+div{flex:auto;min-width:0}.provider-catalog-account-row-head{justify-content:space-between;align-items:center;gap:10px;width:100%;display:flex}.list-row.provider-catalog-account-row--waiting{cursor:default;flex-direction:column;align-items:stretch;gap:10px}.provider-catalog-footer{align-items:center;gap:8px;display:flex}.bar-warn{background:var(--amber)}.quota-row--warn .quota-label{color:var(--amber)}.quota-val--warn{color:var(--amber);align-items:center;gap:3px;display:inline-flex}.quota-stacked,.quota-stacked--pending{flex-direction:column;gap:10px;min-height:64px;display:flex}.quota-stacked-row--skeleton .quota-stacked-bar-row{min-height:18px}.quota-stacked-row{flex-direction:column;gap:4px;display:flex}.quota-stacked-head{justify-content:space-between;align-items:baseline;gap:8px;display:flex}.quota-stacked-limit{font-weight:600}.quota-stacked-limit-group{overflow-wrap:anywhere;flex-wrap:wrap;flex:auto;align-items:baseline;gap:4px;min-width:0;display:inline-flex}.quota-window-partial{border:1px solid color-mix(in srgb, var(--amber) 45%, transparent);color:var(--amber);white-space:nowrap;border-radius:999px;flex:none;padding:1px 5px;font-size:10px;font-weight:600;line-height:1.3}.quota-stacked-reset{overflow-wrap:anywhere;min-width:0;font-size:12px}.quota-stacked-bar-row{align-items:center;gap:8px;display:flex}.quota-stacked-bar{flex:1}.quota-stacked-used{font-variant-numeric:tabular-nums;white-space:nowrap;font-size:12px}.quota-stacked-used--warn{color:var(--amber)}.quota-stacked-limit-reached{color:var(--amber);align-items:center;gap:4px;font-size:12px;display:inline-flex}.main-inner:has(.pws-shell-container){max-width:1440px}.pws-shell-container{width:100%;min-width:0;container:provider-workspace/inline-size}.pws-root{gap:var(--space-4);grid-template-columns:minmax(240px,280px) minmax(0,1fr);width:100%;max-width:100%;min-height:480px;display:grid}.pws-rail{gap:var(--space-3);border-right:1px solid var(--border);padding-right:var(--space-3);flex-direction:column;min-width:0;display:flex}.pws-search-row{align-items:center;gap:6px;display:flex}.pws-search-wrap{flex:1;min-width:0;position:relative}.pws-search-icon{color:var(--muted);pointer-events:none;position:absolute;top:50%;left:8px;transform:translateY(-50%)}.pws-search-wrap .pws-search-input{width:100%;padding-left:32px}.pws-filter-wrap{position:relative}.pws-filter-btn{appearance:none;border:1px solid var(--border);border-radius:var(--radius-sm);cursor:pointer;color:var(--muted);min-width:var(--control-md);min-height:var(--control-md);background:0 0;padding:6px;position:relative}.pws-filter-btn--active{color:var(--text);border-color:var(--text)}.pws-filter-dot{background:var(--accent);border-radius:50%;width:6px;height:6px;position:absolute;top:3px;right:3px}.pws-filter-menu{z-index:30;background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-sm);flex-direction:column;gap:4px;min-width:230px;padding:10px;display:flex;position:absolute;top:calc(100% + 6px);right:0;box-shadow:0 8px 24px #0000001f}.pws-filter-title{margin-bottom:2px;font-weight:600}.pws-filter-head{text-transform:uppercase;letter-spacing:.04em;color:var(--muted);margin-top:8px;font-size:11px}.pws-filter-option{cursor:pointer;align-items:center;gap:8px;padding:3px 2px;display:flex}.pws-filter-label{flex:1}.pws-filter-count{color:var(--muted);font-variant-numeric:tabular-nums;font-size:12px}.pws-sort-grid{grid-template-columns:1fr 1fr;gap:4px;display:grid}.pws-sort-btn{appearance:none;border:1px solid var(--border);border-radius:var(--radius-sm);cursor:pointer;color:var(--muted);background:0 0;padding:4px 8px;font-size:12px}.pws-sort-btn--active{color:var(--text);border-color:var(--text)}.pws-filter-footer{justify-content:flex-end;margin-top:8px;display:flex}.pws-rail-list{flex-direction:column;gap:4px;min-height:0;display:flex;overflow-y:auto}.pws-rail-empty{padding:8px 4px;font-size:12px}.pws-rail-group{flex-direction:column;gap:2px;display:flex}.pws-rail-group-head{justify-content:space-between;align-items:center;gap:var(--space-2);font-size:var(--text-caption);font-weight:var(--weight-medium);color:var(--muted);padding:var(--space-2) var(--space-2) var(--space-1);display:flex}.pws-rail-group-label{text-overflow:ellipsis;white-space:nowrap;min-width:0;overflow:hidden}.pws-rail-group-count{font-variant-numeric:tabular-nums;flex-shrink:0}.providers-workspace-rail-row{grid-template-columns:var(--icon-lg) minmax(0, 1fr) max-content;align-items:center;gap:var(--space-2);width:100%;min-height:var(--pws-rail-row-height,46px);appearance:none;border-radius:var(--radius-sm);padding:var(--space-1-5) var(--space-2);cursor:pointer;text-align:left;color:inherit;font:inherit;transition:background var(--motion-fast), box-shadow var(--motion-fast);background:0 0;border:none;display:grid;overflow:hidden}.providers-workspace-rail-row:hover{background:var(--surface)}.providers-workspace-rail-row--selected{background:var(--accent-soft);box-shadow:inset 0 0 0 1px var(--border)}.pws-rail-row-wrap{position:relative}.pws-rail-row-remove{right:var(--space-2);appearance:none;border-radius:var(--radius-xs);background:var(--surface);width:24px;height:24px;color:var(--red);cursor:pointer;opacity:0;pointer-events:none;transition:opacity var(--motion-fast), background var(--motion-fast);border:none;justify-content:center;align-items:center;padding:0;display:inline-flex;position:absolute;top:50%;transform:translateY(-50%)}.pws-rail-row-wrap:hover .pws-rail-row-remove,.pws-rail-row-wrap:focus-within .pws-rail-row-remove{opacity:1;pointer-events:auto}.pws-rail-row-wrap:hover .providers-workspace-rail-row--selected~.pws-rail-row-remove{background:var(--raised)}.pws-rail-row-wrap:hover .providers-workspace-rail-trail{opacity:0}.providers-workspace-rail-trail{transition:opacity var(--motion-fast)}.pws-rail-row-remove:hover{background:var(--raised)}@media (hover:none){.pws-rail-row-remove{display:none}}.providers-workspace-rail-icon{width:var(--icon-lg);height:var(--icon-lg);flex-shrink:0;justify-content:center;align-items:center;display:inline-flex}.providers-workspace-rail-icon img,.providers-workspace-rail-icon .provider-icon-mask{width:var(--icon-lg);height:var(--icon-lg);display:block}.provider-icon-fallback{border-radius:var(--radius-xs);width:100%;height:100%;font-weight:var(--weight-semibold);-webkit-user-select:none;user-select:none;justify-content:center;align-items:center;font-size:13px;line-height:1;display:inline-flex}.provider-icon-mask{-webkit-mask-position:50%;mask-position:50%;-webkit-mask-size:contain;mask-size:contain;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}.providers-workspace-rail-copy{gap:var(--space-0-5);flex-direction:column;min-width:0;display:flex;overflow:hidden}.providers-workspace-rail-primary{align-items:center;gap:var(--space-1-5);min-width:0;display:flex}.providers-workspace-rail-name-label{white-space:nowrap;text-overflow:ellipsis;min-width:0;font-size:var(--text-body);font-weight:var(--weight-medium);line-height:var(--leading-ui);overflow:hidden}.providers-workspace-rail-secondary{white-space:nowrap;text-overflow:ellipsis;min-width:0;min-height:1.15em;font-size:var(--text-caption);line-height:var(--leading-ui);color:var(--muted);overflow:hidden}.pwi-rail-badge{border:1px solid var(--border);color:var(--muted);border-radius:999px;padding:1px 6px;font-size:10px}.pwi-rail-badge--free{color:var(--green);border-color:var(--green)}.pwi-rail-badge--local{color:var(--amber);border-color:var(--amber)}.providers-workspace-rail-trail{flex-shrink:0;align-items:center;gap:4px;display:inline-flex}.pwi-default-star{color:var(--amber);display:inline-flex}.providers-workspace-rail-status{border-radius:50%;width:8px;height:8px;display:inline-block}.providers-workspace-rail-status--active{background:var(--green)}.providers-workspace-rail-status--warning{background:var(--amber)}.providers-workspace-rail-status--inactive{background:var(--muted)}.pws-main{min-width:0;max-width:100%;padding:0 var(--space-2);overflow:hidden}.pws-detail-placeholder{flex-direction:column;align-items:flex-start;gap:8px;padding:24px 8px;display:flex}.pws-empty-root{justify-content:center;padding:48px 16px;display:flex}.pws-empty-hero{text-align:center;flex-direction:column;align-items:center;gap:16px;max-width:640px;display:flex}.pws-empty-tiles{grid-template-columns:repeat(3,minmax(0,1fr));gap:10px;width:100%;display:grid}.pws-empty-tile{appearance:none;border:1px solid var(--border);border-radius:var(--radius-sm);cursor:pointer;color:inherit;font:inherit;background:0 0;flex-direction:column;align-items:center;gap:6px;padding:14px 12px;display:flex}.pws-empty-tile:hover{background:var(--surface)}.pws-empty-tile-label{font-weight:600}.pws-empty-tile-desc{font-size:12px}@container provider-workspace (width<=920px){.pws-root{gap:var(--space-3);grid-template-columns:240px minmax(0,1fr)}}@container provider-workspace (width<=680px){.pws-root{grid-template-columns:minmax(0,1fr);min-height:auto}.pws-rail{--pws-rail-row-height:var(--control-touch);border-right:none;border-bottom:1px solid var(--border);padding-right:0;padding-bottom:var(--space-3);max-height:320px;overflow-y:auto}.pws-main{padding:var(--space-2) 0 0}}@media (width<=768px){.pws-root{grid-template-columns:1fr;min-height:auto}.pws-rail{border-right:none;border-bottom:1px solid var(--border);max-height:320px;padding-bottom:12px;padding-right:0;overflow-y:auto}.pws-main{padding-top:8px}.pws-empty-tiles{grid-template-columns:1fr}}@media (width<=360px){.main-inner:has(.pws-shell-container)>.page-head{flex-wrap:wrap;align-items:flex-start}.main-inner:has(.pws-shell-container)>.page-head .row{flex-wrap:wrap;justify-content:flex-start;width:100%}}.pws-detail-back-link{color:var(--accent,#3b82f6);font:inherit;cursor:pointer;background:0 0;border:none;align-items:center;gap:4px;margin-bottom:8px;padding:0;font-size:.82rem;display:inline-flex}.pws-detail-back-link:hover{text-decoration:underline}.pws-detail-back-chevron{width:12px;height:12px;transform:rotate(180deg)}.pws-detail-head-main{align-items:center;gap:10px;margin-bottom:12px;display:flex}.pws-detail-head-main .pws-detail-title{align-items:center;gap:8px;display:flex}.pws-detail-actions{align-items:center;gap:8px;margin-left:auto;display:flex}.pws-detail-toggle{align-items:center;gap:6px;display:flex}.pws-detail-toggle-label{color:var(--muted);font-size:.82rem}.btn-icon-only{padding:4px 6px}.pws-overview-layout{grid-template-columns:1fr 280px;align-items:start;gap:24px;display:grid}.pws-overview-main{flex-direction:column;gap:20px;min-width:0;display:flex}.pws-overview-sidebar{flex-direction:column;align-self:start;gap:20px;display:flex}@container provider-workspace (width<=920px){.pws-overview-layout{grid-template-columns:minmax(0,1fr)}}.pws-auth-summary{align-items:center;gap:8px;font-size:.88rem;display:flex}.pws-auth-summary--warn{border:1px solid color-mix(in srgb, var(--yellow,#eab308) 45%, transparent);background:color-mix(in srgb, var(--yellow,#eab308) 12%, transparent);color:inherit;border-radius:8px;align-items:flex-start;padding:10px 12px}.pws-auth-summary-body{flex-wrap:wrap;flex:1;align-items:center;gap:8px 10px;min-width:0;display:flex}.pws-auth-summary--warn svg{color:var(--yellow,#eab308);flex-shrink:0;margin-top:2px}.pws-auth-dot{background:var(--green,#22c55e);border-radius:50%;flex-shrink:0;width:8px;height:8px}.pws-notes-section{min-width:0}.pws-notes-display{text-align:left;width:100%;font:inherit;color:inherit;cursor:pointer;background:0 0;border:1px solid #0000;border-radius:6px;min-height:62px;padding:8px 10px;font-size:.85rem;transition:border-color .15s;display:block}.pws-notes-display:hover{border-color:var(--border)}.pws-notes-textarea{border:1px solid var(--accent,#3b82f6);width:100%;font:inherit;color:inherit;background:var(--bg);resize:vertical;border-radius:6px;min-height:62px;padding:8px 10px;font-size:.85rem}.pws-notes-textarea:focus{outline:none;box-shadow:0 0 0 2px #3b82f640}@media (width<=768px){.pws-overview-layout{grid-template-columns:1fr}.pws-overview-sidebar{position:static}}.pws-detail{flex-direction:column;gap:0;width:100%;min-width:0;max-width:960px;display:flex}.pws-detail-icon{flex-shrink:0;justify-content:center;align-items:center;width:32px;height:32px;display:flex}.pws-detail-icon img,.pws-detail-icon .provider-icon-mask{width:28px;height:28px}.pws-detail-title-wrap{flex:1;min-width:0}.pws-detail-title{margin:0;font-size:1.15rem;font-weight:600;line-height:1.3}.pws-detail-tabs{scrollbar-width:thin;border-bottom:1px solid var(--border);gap:0;margin-top:8px;margin-bottom:16px;display:flex;overflow-x:auto}.pws-detail-tab{appearance:none;font:inherit;color:var(--muted);cursor:pointer;background:0 0;border:none;border-bottom:2px solid #0000;padding:8px 14px;font-size:.82rem;font-weight:500;transition:color .15s,border-color .15s}.pws-detail-tab:hover{color:var(--text)}.pws-detail-tab--active{color:var(--text);border-bottom-color:var(--text)}.pws-detail-tab:focus-visible{outline:2px solid var(--accent-ring);outline-offset:-2px}.pws-detail-panel:focus{outline:none}.pws-section{margin-bottom:16px}.pws-section-title{text-transform:uppercase;letter-spacing:.04em;color:var(--muted);margin:0 0 8px;font-size:.7rem;font-weight:600}.pws-section--side{margin-bottom:12px}.pws-kv{flex-direction:column;gap:0;margin:0;display:flex}.pws-kv-row{gap:12px;padding:5px 0;font-size:.84rem;line-height:1.4;display:flex}.pws-kv-row dt{width:130px;color:var(--muted);flex-shrink:0;font-weight:400}.pws-kv-row dd{word-break:break-word;flex:1;min-width:0;margin:0}.pws-kv-row dd code{font-size:.8rem}.pws-kv-mono{font-variant-numeric:tabular-nums}.pws-status-ok{color:var(--green,#22c55e);align-items:center;gap:4px;display:flex}.pws-status-warn{color:var(--amber,#f59e0b);align-items:center;gap:4px;display:flex}.pws-stats-note{margin-top:6px;font-size:.75rem}.pws-edit-settings-link,.pws-view-usage-link{margin-top:8px;font-size:.8rem}.pws-section-head{justify-content:space-between;align-items:baseline;gap:12px;margin-bottom:12px;display:flex}.pws-section-head .pws-section-title{margin-bottom:0}.pws-model-search{width:100%;margin-bottom:18px}.pws-custom-model-label{margin-bottom:6px;display:block}.pws-custom-model-row{gap:8px;margin-bottom:12px}.pws-custom-model-row .input{flex:1;min-width:0}.pws-model-list{flex-wrap:wrap;align-items:center;gap:12px;margin:4px 0 0;padding:0;list-style:none;display:flex}.pws-model-expand{color:inherit;font:inherit;cursor:pointer;text-align:left;background:0 0;border:0;margin:0;padding:0;display:inline}.pws-model-chip{border:1px solid color-mix(in oklab, var(--border) 85%, transparent);background:color-mix(in oklab, var(--surface,var(--panel)) 92%, var(--text) 4%);border-radius:8px;align-items:center;gap:10px;max-width:100%;padding:8px 10px 8px 12px;display:inline-flex}.pws-model-chip-main{min-width:0;color:inherit;font:inherit;cursor:pointer;text-align:left;background:0 0;border:none;align-items:center;margin:0;padding:0;display:inline-flex}.pws-model-chip-main:hover .pws-model-id{color:var(--accent,var(--text))}.pws-model-chip-main:focus-visible{outline:2px solid var(--accent-ring);outline-offset:2px;border-radius:4px}.pws-model-id{font-family:var(--mono,ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);color:var(--text);white-space:nowrap;font-size:.8rem;font-weight:600;line-height:1.35}.pws-model-flag{flex-shrink:0}.pws-inline-error{align-items:center;gap:10px;margin-top:8px;display:flex}.pws-usage-block+.pws-usage-block{border-top:1px solid color-mix(in oklab, var(--border) 45%, transparent);margin-top:32px;padding-top:24px}.pws-usage-metrics{grid-template-columns:repeat(2,minmax(0,1fr));gap:20px 28px;margin-top:14px;display:grid}.pws-usage-metric{flex-direction:column;gap:8px;min-width:0;display:flex}.pws-usage-metric-value{font-variant-numeric:tabular-nums;letter-spacing:-.02em;color:var(--text);font-size:1.35rem;font-weight:650;line-height:1.15}.pws-usage-metric-label{font-size:.8rem;line-height:1.3}.pws-usage-meta{margin-top:16px}.pws-usage-metrics-3{grid-template-columns:repeat(3,minmax(0,1fr))}.pws-cost-disclaimer{margin-top:8px;font-size:.75rem}.pws-model-table{border-collapse:collapse;width:100%;font-size:.8rem}.pws-model-table th{text-align:left;border-bottom:1px solid var(--border);padding:6px 8px;font-weight:600}.pws-model-table td{border-bottom:1px solid var(--border-faint,#8080801a);padding:6px 8px}.pws-model-table .num{text-align:right;font-variant-numeric:tabular-nums}.pws-model-table .mono{font-family:var(--font-mono,monospace);font-size:.78rem}.pws-model-row:hover{background:var(--bg-subtle,#8080800d)}.pws-share-bar{background:var(--border-faint,#80808026);border-radius:3px;min-width:60px;height:6px}.pws-share-bar-fill{background:var(--green,#22c55e);border-radius:3px;height:100%}.pws-model-detail{background:var(--bg-subtle,#8080800d)}.pws-model-detail td{padding:8px 8px 8px 24px}.pws-model-detail-grid{grid-template-columns:repeat(2,1fr);gap:8px 24px;font-size:.8rem;display:grid}@media (width<=600px){.pws-usage-metrics-3{grid-template-columns:1fr}}@media (width<=640px){.pws-usage-metrics{grid-template-columns:1fr;gap:16px}}.pwi-auth-section{flex-direction:column;gap:12px;display:flex}.pwi-auth-body{flex-direction:column;gap:10px;display:flex}.pwi-auth-status-row{align-items:center;gap:8px;display:flex}.pwi-auth-dot{border-radius:var(--radius-round);background:var(--muted);flex-shrink:0;width:8px;height:8px}.pwi-auth-dot--ok{background:var(--green)}.pwi-auth-dot--off{background:var(--muted)}.pwi-auth-dot--warn{background:var(--amber)}.pwi-auth-status-text{font-size:var(--text-control);color:var(--text)}.pwi-auth-state{min-height:36px;font-size:var(--text-label);color:var(--muted);background:var(--raised);border-radius:var(--radius-xs);align-items:center;gap:8px;padding:8px 10px;display:flex}.pwi-auth-state--error{color:var(--red);background:var(--red-soft);justify-content:space-between}.pwi-auth-state--empty{justify-content:center}.pwi-auth-actions{flex-wrap:wrap;gap:8px;display:flex}.pwi-auth-optin-row{border:1px solid var(--border-soft);border-radius:var(--radius-xs);background:var(--raised);justify-content:space-between;align-items:center;gap:16px;padding:10px 12px;display:flex}.pwi-auth-optin-copy{flex-direction:column;gap:3px;min-width:0;display:flex}.pwi-auth-optin-label{font-size:var(--text-control);color:var(--text);font-weight:600}.pwi-auth-optin-mixed{color:var(--amber);font-weight:600}.pwi-auth-optin-error{color:var(--red);font-size:var(--text-caption)}.pwi-auth-wait{flex-direction:column;gap:6px;display:flex}.pwi-auth-wait-title{font-size:var(--text-control);color:var(--text);font-weight:600}.pwi-auth-wait-copy{font-size:var(--text-label);color:var(--muted);line-height:1.45}.pwi-device-code-wrap{border:1px solid var(--border);background:var(--surface);border-radius:10px;flex-wrap:wrap;align-items:center;gap:10px;margin:6px 0;padding:12px;display:flex}.pwi-device-code{letter-spacing:.14em;color:var(--text);-webkit-user-select:all;user-select:all;font-size:20px;font-weight:800}.pwi-auth-row-copy{flex-direction:column;align-items:flex-start;gap:2px;min-width:0;display:flex}.pwi-auth-row-secondary{text-overflow:ellipsis;max-width:100%;font-size:var(--text-label);color:var(--muted);overflow:hidden}.pwi-auth-list{flex-direction:column;gap:4px;margin:0;padding:0;list-style:none;display:flex}.pwi-auth-row{border-radius:var(--radius-xs);transition:background var(--motion-fast);align-items:center;gap:8px;padding:6px 8px;display:flex}.pwi-auth-row:hover{background:var(--hover)}.pwi-auth-row--active{background:var(--accent-soft)}.pwi-auth-acct{border-radius:var(--radius-xs);flex-direction:column;gap:2px;display:flex}.pwi-auth-acct--active{background:var(--accent-soft)}.pwi-auth-acct--active .pwi-auth-row--active{background:0 0}.pwi-auth-acct-quota{padding:0 8px 8px 26px}.pwi-auth-acct-quota-stale{margin:4px 0 0;font-size:.85em}.quota-observed{margin:0 0 4px;font-size:.85em}.pwi-auth-row-main{appearance:none;min-width:0;color:inherit;text-align:left;cursor:pointer;border-radius:var(--radius-2xs);background:0 0;border:0;flex:1;align-items:center;gap:8px;padding:2px;display:flex}.pwi-auth-row-main:disabled{cursor:default;opacity:.72}.pwi-auth-row-main:focus-visible,.pwi-auth-row-remove:focus-visible{outline:2px solid var(--accent-ring);outline-offset:2px}.pwi-auth-row-label{font-size:var(--text-control);color:var(--text);text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.pwi-auth-row-remove{cursor:pointer;color:var(--muted);border-radius:var(--radius-2xs);transition:color var(--motion-fast), background var(--motion-fast);background:0 0;border:none;padding:2px}.pwi-auth-row-remove:hover{color:var(--red);background:var(--red-soft)}.pwi-auth-add-key{font-size:var(--text-label);color:var(--accent);cursor:pointer;text-align:left;background:0 0;border:none;padding:4px 0}.pwi-auth-add-key:hover{text-decoration:underline}@keyframes pwi-spin{to{transform:rotate(360deg)}}.pwi-spin-inline{border:2px solid var(--border);border-top-color:var(--accent);border-radius:var(--radius-round);width:14px;height:14px;animation:.7s linear infinite pwi-spin;display:inline-block}.pwi-settings-form{flex-direction:column;gap:14px;display:flex;position:relative}.pwi-settings-field{flex-direction:column;gap:4px;display:flex}.pwi-settings-label{font-size:var(--text-label);color:var(--muted);letter-spacing:.01em;font-weight:500}.pwi-settings-textarea{width:100%;min-height:60px;font:inherit;font-size:var(--text-control);color:var(--text);background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-xs);resize:vertical;transition:border-color var(--motion-fast);padding:8px 10px}.pwi-settings-textarea:focus{border-color:var(--accent-ring);outline:none}.pwi-settings-hint{font-size:var(--text-caption);color:var(--muted);line-height:1.4}.pwi-settings-mode-msg{font-size:var(--text-caption);line-height:1.4}.pwi-settings-mode-msg--ok{color:var(--green)}.pwi-settings-mode-msg--err{color:var(--red)}.pwi-settings-sticky-bar{z-index:2;background:var(--bg);border-top:1px solid var(--border-soft);align-items:center;gap:8px;padding:10px 0;display:flex;position:sticky;bottom:0}.pwi-settings-sticky-bar-actions{gap:8px;margin-left:auto;display:flex}.pwi-settings-msg{font-size:var(--text-label);border-radius:var(--radius-xs);padding:6px 10px;line-height:1.4}.pwi-settings-msg--ok{background:var(--green-soft);color:var(--green)}.pwi-settings-msg--err{background:var(--red-soft);color:var(--red)}.pwi-settings-dirty{font-size:var(--text-caption);color:var(--amber)}.pwi-pacing-card{border:1px solid var(--border);border-radius:var(--radius-sm);background:color-mix(in srgb, var(--surface) 88%, var(--accent) 12%);flex-direction:column;gap:12px;padding:14px;display:flex}.pwi-pacing-head{justify-content:space-between;align-items:flex-start;gap:14px;display:flex}.pwi-pacing-head h3,.pwi-pacing-card h4{color:var(--text);font-size:var(--text-control);margin:0}.pwi-pacing-head p{color:var(--muted);font-size:var(--text-caption);margin:4px 0 0;line-height:1.45}.pwi-pacing-toggle{white-space:nowrap;font-size:var(--text-label);align-items:center;gap:6px;display:flex}.pwi-pacing-grid{grid-template-columns:repeat(2,minmax(0,1fr));align-items:end;gap:10px;display:grid}.pwi-pacing-grid--model{grid-template-columns:minmax(160px,2fr) repeat(2,minmax(110px,1fr)) auto}.pwi-pacing-status{grid-template-columns:repeat(3,minmax(0,1fr));gap:8px;display:grid}.pwi-pacing-status span{border:1px solid var(--border-soft);border-radius:var(--radius-xs);color:var(--muted);font-size:var(--text-caption);padding:8px 10px}.pwi-pacing-status strong{color:var(--text);font-size:var(--text-label);text-overflow:ellipsis;white-space:nowrap;display:block;overflow:hidden}.pwi-pacing-overrides{flex-direction:column;gap:6px;display:flex}.pwi-pacing-row{border-radius:var(--radius-xs);background:var(--bg);grid-template-columns:minmax(0,1fr) auto auto;align-items:center;gap:10px;padding:7px 8px;display:grid}.pwi-pacing-row code{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.pwi-pacing-row span{color:var(--muted);font-size:var(--text-caption)}@media (width<=760px){.pwi-pacing-head{flex-direction:column}.pwi-pacing-grid,.pwi-pacing-grid--model,.pwi-pacing-status{grid-template-columns:1fr}.pwi-pacing-grid--model .btn{width:100%}.pwi-pacing-row{grid-template-columns:minmax(0,1fr) auto}.pwi-pacing-row span{grid-area:2/1/auto/-1}}.pwi-json-panel{flex-direction:column;gap:8px;display:flex}.pwi-json-panel-header{justify-content:space-between;align-items:center;gap:8px;display:flex}.pwi-json-panel-title{font-size:var(--text-control);color:var(--text);font-weight:600}.pwi-json-panel-actions{gap:6px;display:flex}.pwi-json-panel-desc{font-size:var(--text-caption);color:var(--muted);line-height:1.4}.pwi-json-textarea{width:100%;min-height:180px;font-family:ui-monospace,SF Mono,Cascadia Code,Segoe UI Mono,Menlo,monospace;font-size:var(--text-label);tab-size:2;color:var(--text);background:var(--raised);border:1px solid var(--border);border-radius:var(--radius-xs);resize:vertical;transition:border-color var(--motion-fast);padding:10px 12px;line-height:1.5}.pwi-json-textarea:focus{border-color:var(--accent-ring);outline:none}.dialog-backdrop{z-index:900;background:#00000073;justify-content:center;align-items:center;display:flex;position:fixed;inset:0}.dialog{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);width:90%;max-width:420px;box-shadow:var(--shadow);flex-direction:column;gap:16px;padding:24px;display:flex}.dialog-actions{justify-content:flex-end;gap:8px;display:flex}.login-url-block{flex-direction:column;gap:6px;margin:6px 0;display:flex}.login-url-block-text{overflow-wrap:anywhere;border:1px solid var(--border);border-radius:var(--radius-xs);background:var(--surface);max-width:100%;font-size:var(--text-label);color:var(--text);-webkit-user-select:all;user-select:all;padding:8px 10px;display:block}.login-url-block-actions{flex-wrap:wrap;align-items:center;gap:10px;display:flex}.login-url-block-open{font-size:var(--text-label);color:var(--accent);cursor:pointer;text-decoration:underline}.login-hint{flex-direction:column;gap:8px;display:flex}.login-hint-device{border:1px solid var(--border);background:var(--surface);border-radius:10px;flex-wrap:wrap;align-items:center;gap:10px;padding:12px;display:flex}.login-hint-device-code{letter-spacing:.14em;color:var(--text);-webkit-user-select:all;user-select:all;font-size:20px;font-weight:800}.login-hint-paste{flex-direction:column;gap:6px;display:flex}.login-hint-paste-row{gap:8px;display:flex}.login-hint-paste-input{flex:1}.open-browser-pref{cursor:pointer;align-items:flex-start;gap:8px;margin:2px 0;display:flex}.open-browser-pref input{flex-shrink:0;margin-top:2px}.open-browser-pref-copy{flex-direction:column;gap:2px;display:flex}.pws-dashboard{--fg-muted:var(--muted);flex-direction:column;gap:14px;max-width:100%;padding:4px 0;display:flex}.pws-dashboard-header{flex-direction:row;justify-content:space-between;align-items:flex-start;gap:12px;display:flex}.pws-dashboard-header-text{flex-direction:column;gap:2px;min-width:0;display:flex}.pws-dashboard-title{margin:0;font-size:1.1rem;font-weight:600}.pws-dashboard-subtitle{margin:0;font-size:.82rem}.pws-dashboard-summary{gap:10px;display:flex}.pws-dashboard-card{border:1px solid var(--border);background:var(--bg-secondary,var(--bg));border-radius:8px;flex-direction:column;align-items:center;gap:2px;min-width:80px;padding:10px 20px;display:flex}.pws-dashboard-card-count{font-size:1.25rem;font-weight:700;line-height:1}.pws-dashboard-card--ok .pws-dashboard-card-count{color:var(--green,#22c55e)}.pws-dashboard-card--warn .pws-dashboard-card-count{color:var(--yellow,#eab308)}.pws-dashboard-card--muted .pws-dashboard-card-count{color:var(--fg-muted,#888)}.pws-dashboard-card-label{color:var(--fg-muted,#888);letter-spacing:.02em;font-size:.7rem}.pws-dashboard-columns{grid-template-columns:minmax(0,1.4fr) minmax(0,1fr);align-items:start;gap:20px;display:grid}.pws-dashboard-section{flex-direction:column;gap:4px;min-width:0;display:flex}.pws-dashboard-section-title{text-transform:uppercase;letter-spacing:.04em;color:var(--fg-muted,#888);margin:0;padding-bottom:2px;font-size:.68rem;font-weight:600}.pws-dashboard-attention .pws-dashboard-section-title{color:var(--yellow,#eab308);text-transform:none;letter-spacing:0;align-items:center;gap:6px;font-size:.82rem;display:inline-flex}.pws-dashboard-row--attention{border-color:color-mix(in srgb, var(--yellow,#eab308) 40%, var(--border));background:color-mix(in srgb, var(--yellow,#eab308) 8%, var(--bg-secondary,var(--bg)))}.pws-dashboard-rows{flex-direction:column;gap:0;display:flex}.pws-dashboard-row{cursor:pointer;text-align:left;color:inherit;font:inherit;background:0 0;border:none;border-radius:6px;grid-template-columns:22px 1fr auto auto;align-items:center;gap:8px;width:100%;padding:6px;transition:background .12s;display:grid}.pws-dashboard-row:hover{background:var(--bg-hover,#80808014)}.pws-dashboard-row-icon{justify-content:center;align-items:center;width:20px;height:20px;display:flex}.pws-dashboard-row-icon img,.pws-dashboard-row-icon .provider-icon-mask{width:18px;height:18px}.pws-dashboard-row-info{flex-direction:column;gap:1px;min-width:0;display:flex}.pws-dashboard-row-name{white-space:nowrap;text-overflow:ellipsis;font-size:.85rem;font-weight:500;overflow:hidden}.pws-dashboard-row-meta{font-size:.72rem}.pws-dashboard-row-count{white-space:nowrap;font-size:.8rem}.pws-dashboard-row-chevron{width:12px;height:12px;color:var(--fg-muted,#888);flex-shrink:0}.pws-dashboard-row-bars{grid-column:1/-1;min-height:64px;padding-top:1px;padding-left:30px}.pws-capacity-label,.pws-capacity-recovery,.pws-capacity-incomplete{font-size:.7rem}.pws-capacity-label{color:var(--fg-muted,#888);margin-bottom:3px}.pws-capacity-details{border-top:1px solid var(--border-soft);gap:7px;margin-top:8px;padding-top:7px;display:grid}.pws-capacity-recovery{color:var(--fg-muted,#888);flex-wrap:wrap;justify-content:space-between;align-items:flex-start;gap:12px;display:flex}.pws-capacity-recovery span{overflow-wrap:anywhere;min-width:0}.pws-capacity-recovery strong{color:var(--text);white-space:nowrap;margin-left:auto}@container (width<=520px){.pws-capacity-recovery{grid-template-columns:minmax(0,1fr);gap:3px;display:grid}.pws-capacity-recovery strong{white-space:normal;margin-left:0}}.pws-capacity-current{padding-top:2px}.pws-capacity-incomplete{color:var(--amber)}.pws-dashboard-section--rate-limits,.pws-dashboard-section--recent{min-height:180px}.pws-dashboard-empty{min-height:3rem;margin:8px 0 0}.pws-dashboard-row--skeleton{pointer-events:none;cursor:default}.pws-skel{border-radius:var(--radius-2xs,4px);background:linear-gradient(90deg, var(--raised,#eee) 0%, var(--surface,#f7f7f7) 50%, var(--raised,#eee) 100%);background-size:200% 100%;animation:1.2s ease-in-out infinite codex-auth-skeleton-shimmer;display:block}.pws-dashboard-row-icon.pws-skel{border-radius:6px;width:22px;height:22px}.pws-skel--name{width:7rem;height:.85rem}.pws-skel--meta{width:5rem;height:.7rem;margin-top:4px}.pws-skel--count{width:4rem;height:.75rem;margin-left:auto}.providers-workspace--boot{min-height:600px}.providers-workspace-rail--boot{opacity:.35;min-height:600px}@container provider-workspace (width<=920px){.pws-dashboard-columns{grid-template-columns:1fr;gap:14px}}@media (width<=900px){.pws-dashboard-columns{grid-template-columns:1fr;gap:14px}}@media (width<=600px){.pws-dashboard-summary{flex-direction:column}.pws-dashboard-card{flex-direction:row;justify-content:space-between;min-width:0}}.combos-workspace-root{grid-template-columns:minmax(340px,28vw) 1fr;align-items:stretch;width:100%;height:100%;min-height:100%;display:grid;overflow:hidden}.combos-workspace-shell{flex-direction:column;width:100%;height:100%;min-height:0;display:flex;overflow:hidden}.combos-workspace-shell-banner{flex-shrink:0;padding:10px 16px 0}.combos-workspace-shell-body{flex-direction:column;flex:auto;min-height:0;display:flex;overflow:hidden}.combos-workspace-rail{border-right:1px solid var(--border);background:var(--rail);flex-direction:column;height:100%;min-height:0;display:flex;position:sticky;top:0;overflow-y:auto}.combos-workspace-rail-header{border-bottom:1px solid var(--border-soft);flex-shrink:0;justify-content:space-between;align-items:center;gap:8px;padding:14px 16px 12px;display:flex}.combos-workspace-rail-title{font-size:var(--text-title);color:var(--text);font-weight:600}.combos-workspace-rail-count{font-family:var(--mono);font-size:var(--text-caption);color:var(--faint)}.combos-workspace-rail-list{flex-direction:column;flex:auto;display:flex;overflow-y:auto}.combos-workspace-rail-group+.combos-workspace-rail-group{margin-top:10px}.combos-workspace-rail-group-head{color:var(--muted);font-size:var(--text-caption);align-items:center;gap:7px;padding:8px 16px 6px;font-weight:600;display:flex}.combos-workspace-rail-row{border:none;border-bottom:1px solid var(--border-soft);cursor:pointer;min-height:40px;font:inherit;font-size:var(--text-control);color:var(--text);text-align:left;background:0 0;grid-template-columns:26px minmax(0,1fr) 3.5rem 14px;align-items:center;column-gap:8px;width:100%;padding:11px 12px 11px 14px;transition:background .12s;display:grid}.combos-workspace-rail-row:hover{background:var(--hover)}.combos-workspace-rail-row.combos-workspace-rail-row--selected{background:var(--accent-soft);box-shadow:inset 0 0 0 1px var(--border)}.combos-workspace-rail-row:focus-visible{outline:1px solid var(--accent);outline-offset:-1px}.combos-workspace-rail-icon{width:26px;height:26px;color:var(--text);flex-shrink:0;justify-content:center;align-items:center;display:flex}.combos-workspace-rail-name{text-overflow:ellipsis;white-space:nowrap;min-width:0;font-family:var(--mono);font-size:var(--text-label);overflow:hidden}.combos-workspace-rail-meta{font-family:var(--mono);font-size:var(--text-caption);color:var(--faint);text-align:right}.combos-workspace-rail-chevron{width:14px;height:14px;color:var(--faint)}.combos-workspace-main{background:var(--bg);flex-direction:column;min-width:0;height:100%;min-height:0;display:flex;overflow:hidden}.combos-workspace-overview,.combos-workspace-detail{flex:auto;min-height:0;padding:20px 24px 32px;overflow-y:auto}.combos-workspace-overview-head{justify-content:space-between;align-items:baseline;gap:12px;margin-bottom:16px;display:flex}.combos-workspace-overview-title{margin:0;font-size:20px;font-weight:600}.combos-workspace-detail-head{border-bottom:1px solid var(--border-soft);flex-wrap:wrap;align-items:center;gap:10px;margin-bottom:16px;padding-bottom:12px;display:flex}.combos-workspace-detail-title{font-size:var(--text-title);font-weight:600;font-family:var(--mono);min-width:0;margin:0}.combos-workspace-detail-actions{flex-wrap:wrap;align-items:center;gap:8px;margin-left:auto;display:flex}.combos-workspace-segmented{border:1px solid var(--border);border-radius:var(--radius-pill);background:var(--surface);gap:2px;margin-bottom:16px;padding:2px;display:inline-flex}.combos-workspace-segmented .btn{border-radius:var(--radius-pill);min-width:0;min-height:0;font-size:var(--text-label);line-height:inherit;border:none;padding:4px 12px}.combos-workspace-tab-content:not([hidden]){flex-direction:column;gap:16px;max-width:720px;display:flex}.cwi-search-row{border-bottom:1px solid var(--border-soft);flex-shrink:0;align-items:center;gap:8px;padding:10px 14px 12px;display:flex}.cwi-search-wrap{flex:1;min-width:0;position:relative}.cwi-search-wrap .cwi-search-icon{width:14px;height:14px;color:var(--faint);pointer-events:none;position:absolute;top:50%;left:10px;transform:translateY(-50%)}.cwi-search-input{width:100%;padding-left:32px!important}.cwi-count-strip{flex-wrap:wrap;gap:10px;margin-bottom:18px;display:flex}.cwi-count-pill{border:1px solid var(--border-soft);border-radius:var(--radius);background:var(--raised);align-items:baseline;gap:6px;padding:8px 12px;display:inline-flex}.cwi-count-pill strong{font-family:var(--mono);font-size:var(--text-subtitle)}.cwi-count-pill span{font-size:var(--text-label);color:var(--muted)}.cwi-field>p.muted{max-width:var(--prose-measure);overflow-wrap:anywhere}.cwi-capabilities{border:1px solid var(--border-soft);border-radius:var(--radius);background:var(--raised);flex-direction:column;gap:10px;padding:12px;display:flex}.cwi-capability-row{justify-content:space-between;align-items:center;gap:12px;display:flex}.cwi-capability-label{font-size:13px;font-weight:500}.cwi-capability-hint{margin:3px 0 0;font-size:12px}.cwi-target-list{flex-direction:column;gap:8px;display:flex}.cwi-target-row{grid-template-columns:28px auto minmax(0,1fr) minmax(0,1.2fr) 4.5rem auto auto;align-items:center;gap:8px;display:grid}.cwi-target-row--failover{grid-template-columns:28px auto minmax(0,1fr) minmax(0,1.2fr) auto auto}.cwi-target-row--dragging{opacity:.55}.cwi-target-row--drop{box-shadow:inset 0 0 0 1px var(--accent);border-radius:var(--radius)}.cwi-target-grip{border-radius:var(--radius);width:28px;height:32px;color:var(--faint);cursor:grab;touch-action:none;background:0 0;border:none;justify-content:center;align-items:center;padding:0;display:inline-flex}.cwi-target-grip:hover{color:var(--muted);background:var(--hover)}.cwi-target-grip:active{cursor:grabbing}.cwi-target-reorder{flex-direction:column;flex-shrink:0;gap:0;display:inline-flex}.cwi-target-reorder .btn{min-width:0;padding:2px 4px;line-height:1}.cwi-target-actions{flex-shrink:0;gap:2px;display:flex}.cwi-quota-badge{border:1px solid var(--border);border-radius:var(--radius-pill);background:var(--raised);min-height:24px;color:var(--muted);font-size:var(--text-caption);white-space:nowrap;justify-content:center;align-items:center;padding:3px 7px;font-weight:600;line-height:1.2;display:inline-flex}.cwi-quota-badge--available{border-color:color-mix(in srgb, var(--green) 36%, var(--border));background:var(--green-soft);color:var(--green)}.cwi-quota-badge--exhausted{border-color:color-mix(in srgb, var(--red) 36%, var(--border));background:var(--red-soft);color:var(--red)}.cwi-quota-banner{border:1px solid color-mix(in srgb, var(--red) 36%, var(--border));border-radius:var(--radius-sm);background:var(--red-soft);max-width:720px;color:var(--red);font-size:var(--text-control);margin-bottom:16px;padding:10px 12px;font-weight:550;line-height:1.4}.cwi-strategy-seg{border-radius:var(--radius-pill);background:var(--surface-soft,var(--raised));gap:2px;padding:3px;display:inline-flex}.cwi-strategy-seg .btn{border-radius:var(--radius-pill);border:none;min-width:88px;padding:5px 12px}.cwi-strategy-seg .btn-ghost{color:var(--muted);background:0 0}.cwi-copy-chip{font-family:var(--mono);font-size:var(--text-label);cursor:pointer}.cwi-form-grid{flex-direction:column;gap:12px;display:flex}.cwi-field label{font-size:var(--text-label);color:var(--muted);margin-bottom:4px;font-weight:600;display:block}.cwi-field .input,.cwi-field select.input{width:100%}.cwi-attention-list{flex-direction:column;gap:0;display:flex}.cwi-attention-row{border:none;border-bottom:1px solid var(--border-soft);width:100%;font:inherit;color:var(--text);text-align:left;cursor:pointer;background:0 0;align-items:center;gap:10px;padding:10px 0;display:flex}.cwi-attention-row:hover{color:var(--accent)}.cwi-modal-form{flex-direction:column;gap:14px;max-height:min(70vh,560px);padding-right:2px;display:flex;overflow-y:auto}.cwi-modal-actions{justify-content:flex-end;gap:8px;margin-top:16px;display:flex}@media (width<=939px){.combos-workspace-root{grid-template-rows:minmax(220px,38vh) 1fr;grid-template-columns:1fr}.combos-workspace-rail{border-right:none;border-bottom:1px solid var(--border);position:relative}.cwi-capability-row{align-items:flex-start}.cwi-target-row,.cwi-target-row--failover{grid-template-columns:28px auto 1fr auto}.cwi-target-row>.input:nth-child(3),.cwi-target-row>.input:nth-child(4){grid-column:3/-1}.cwi-target-row>.cwi-quota-badge{grid-column:3;justify-self:start}}.pwi-dot{background:var(--muted);border-radius:50%;flex-shrink:0;width:7px;height:7px;display:inline-block}.pwi-back-overview{white-space:nowrap;flex:none;gap:4px;margin-right:2px}.pwi-section{background:0 0;border:none;border-radius:0;min-width:0;padding:0}.pwi-section-title{font-size:var(--text-label);color:var(--muted);text-transform:uppercase;letter-spacing:.05em;margin:0 0 4px;font-weight:650}.pwi-empty-right-icon{color:var(--faint)}.pwi-empty-right-sub{font-size:var(--text-control);color:var(--muted);max-width:44ch;margin:0;line-height:1.5}.pwi-json-unsaved-card,.pwi-remove-confirm-card{width:min(420px,92vw);max-width:420px;padding:20px 22px 16px}.pwi-json-unsaved-title,.pwi-remove-confirm-title{font-size:var(--text-subtitle);color:var(--text);margin:0 0 8px;font-weight:650}.pwi-json-unsaved-desc,.pwi-remove-confirm-desc{font-size:var(--text-control);margin:0 0 18px;line-height:1.45}.pwi-json-unsaved-actions,.pwi-remove-confirm-actions{flex-wrap:wrap;justify-content:flex-end;gap:8px;display:flex}.pwi-remove-confirm-danger{background:var(--red)!important;color:#fff!important;border-color:#0000!important}.pwi-remove-confirm-danger:hover:not(:disabled){filter:brightness(1.08)}.main-inner:has(#models-panel-catalog:not([hidden])){max-width:1200px}.main-inner:has(#models-panel-routing:not([hidden])){max-width:1200px}.models-workspace-shell{width:100%;min-width:0;container:models-workspace/inline-size}.models-tab-panel{min-width:0}.models-workspace-root{gap:var(--space-4);grid-template-columns:minmax(240px,280px) minmax(0,1fr);width:100%;max-width:100%;min-height:480px;display:grid}.models-workspace-rail{gap:var(--space-3);border-right:1px solid var(--border);padding-right:var(--space-3);flex-direction:column;min-width:0;display:flex}.models-workspace-rail-header{justify-content:space-between;align-items:baseline;gap:8px;display:flex}.models-workspace-rail-title{font-size:var(--text-body);font-weight:var(--weight-semibold);color:var(--text)}.models-workspace-rail-count{font-family:var(--mono);color:var(--faint);font-variant-numeric:tabular-nums;font-size:11px}.models-workspace-rail-list{flex-direction:column;gap:2px;min-height:0;max-height:640px;display:flex;overflow-y:auto}.models-workspace-rail-row{gap:var(--space-0-5);appearance:none;border-radius:var(--radius-sm);width:100%;min-height:46px;padding:var(--space-1-5) var(--space-2);cursor:pointer;text-align:left;color:inherit;font:inherit;background:0 0;border:none;flex-direction:column;display:flex;overflow:hidden}.models-workspace-rail-row:hover{background:var(--surface-2)}.models-workspace-rail-row--selected{background:var(--surface-2);box-shadow:inset 0 0 0 1px var(--border)}.models-workspace-rail-name{font-size:var(--text-body);font-weight:var(--weight-medium);color:var(--text);white-space:nowrap;text-overflow:ellipsis;overflow:hidden}.models-workspace-rail-meta{font-size:var(--text-caption);color:var(--faint);white-space:nowrap;text-overflow:ellipsis;font-variant-numeric:tabular-nums;overflow:hidden}.models-workspace-main{gap:var(--space-3);flex-direction:column;min-width:0;display:flex}.models-control-top-row{align-items:center;column-gap:clamp(var(--space-3), 2vw, var(--space-6));row-gap:var(--space-2);margin-bottom:var(--space-3);grid-template-columns:minmax(0,1fr) auto;min-width:0;display:grid}.models-shadow-row{align-items:center;gap:var(--space-2);flex-wrap:nowrap;min-width:0;display:flex}.models-shadow-label,.models-shadow-warning{white-space:nowrap}.models-shadow-model-slot{flex:0 auto;min-width:9.5rem;max-width:12.5rem}.models-shadow-model-slot .custom-select{width:auto;min-width:100%;max-width:100%}.models-shadow-model-slot .select-trigger{justify-content:space-between;width:auto;min-width:100%;max-width:100%}.models-shadow-model-slot .select-trigger>span{text-overflow:ellipsis;white-space:nowrap;min-width:0;overflow:hidden}.models-v2-mode-row{align-items:center;gap:var(--space-2);flex-wrap:nowrap;flex:none;justify-self:end;min-width:0;min-height:28px;display:flex}.models-v2-detail-row{gap:var(--space-2);margin-bottom:var(--space-2);flex-wrap:wrap;align-items:center}.models-v2-keep-native-row{min-width:0;margin-top:calc(var(--space-1) * -1);grid-column:2;justify-content:flex-end;justify-self:end;display:flex}.models-v2-keep-native{align-items:center;gap:var(--space-2);min-width:0;display:inline-flex}.models-v2-keep-native-label{color:var(--muted);white-space:nowrap;text-overflow:ellipsis;min-width:0;overflow:hidden}.models-v2-keep-native-info{width:24px;height:24px;color:var(--muted);cursor:help;flex:0 0 24px;justify-content:center;align-items:center;display:inline-flex}.models-provider-head{row-gap:var(--space-2);column-gap:var(--space-2);flex-wrap:wrap;align-items:center;min-width:0}.models-provider-head>span.text-body,.models-provider-toggle>span.text-body{overflow-wrap:anywhere;min-width:0}.models-provider-toggle{min-width:0}.models-provider-toggle>*{text-overflow:ellipsis;white-space:nowrap;min-width:0;overflow:hidden}.models-provider-toggle>svg{flex:none}.models-provider-actions{justify-content:flex-end;align-items:center;gap:var(--space-2);flex-wrap:wrap;min-width:0;max-width:100%;margin-left:auto}.models-cap-cluster{align-items:center;gap:var(--space-1);flex-wrap:wrap;display:flex}.models-provider-actions .btn-ghost.models-alias-edit{opacity:.75;transition:opacity var(--motion-fast)}.models-provider-head:hover .models-provider-actions .btn-ghost.models-alias-edit,.models-provider-head:focus-within .models-provider-actions .btn-ghost.models-alias-edit,.models-provider-actions .btn-ghost.models-alias-edit:focus-visible{opacity:1}@media (hover:none){.models-provider-actions .btn-ghost.models-alias-edit{opacity:1}}@media (prefers-reduced-motion:reduce){.models-provider-actions .btn-ghost.models-alias-edit{transition:none}}.models-provider-list{gap:var(--space-2);flex-direction:column;display:flex}.models-provider-card{margin-bottom:0;overflow:hidden}.models-provider-body{padding:var(--space-3) var(--space-4)}.models-provider-body>.input{width:100%;margin-bottom:var(--space-2)}.models-provider-hint{margin:0 0 var(--space-2);max-width:var(--prose-measure)}.models-chip{padding:var(--space-0-5) var(--space-2);border:1px solid var(--border);border-radius:var(--radius-pill);display:inline-block}.models-chip--tip{margin-bottom:var(--space-1)}.models-model-row{padding:var(--space-2) 0}.models-show-more{margin-top:var(--space-2)}.models-segmented{border:1px solid var(--border);border-radius:var(--radius-pill);background:var(--surface);gap:2px;padding:2px;display:inline-flex}.models-segmented .btn{border-radius:var(--radius-pill);min-width:0;min-height:0;font-size:var(--text-label);line-height:inherit;border:none;padding:4px 12px}.models-cap-row{gap:var(--space-2);margin-bottom:var(--space-3);flex-wrap:wrap}.models-custom-summary{gap:var(--space-2);margin-bottom:var(--space-2)}.models-order-hint{align-items:flex-start;gap:var(--space-2);margin-bottom:var(--space-4);max-width:var(--prose-measure)}.models-order-hint>svg{margin-top:var(--space-0-5);flex-shrink:0}.models-collapse-controls{gap:var(--space-2);margin:0 0 var(--space-2)}.models-combos-card{margin-bottom:var(--space-3)}.models-combos-card--pending{min-height:var(--space-12)}.models-combos-empty-head{padding:var(--space-3);justify-content:space-between;gap:var(--space-2)}.models-combos-add{padding:var(--space-2) var(--space-3) var(--space-3) calc(var(--space-8) + var(--space-0-5));gap:var(--space-2);text-decoration:none}.models-help-link{margin-top:var(--space-3)}.models-context-fields,.models-field-stack{gap:var(--space-4);flex-direction:column;display:flex}.models-field{gap:var(--space-2);flex-direction:column;display:flex}.models-field-row{gap:var(--space-2)}.models-modality-option{gap:var(--space-2);cursor:pointer}.modal-card .models-modality-option input[type=checkbox]{flex:none;width:13px;height:13px;margin:0}.row.models-model-row,.row.models-cap-row,.row.models-custom-summary,.row.models-order-hint,.row.models-collapse-controls,.row.models-combos-empty-head,.row.models-combos-add,.row.models-field-row,.row.models-modality-option{gap:var(--space-2)}@media (width<=1160px){.models-control-top-row{grid-template-columns:1fr}.models-shadow-row{flex-wrap:wrap;flex:100%}.models-shadow-model-slot{flex:12rem;width:auto;min-width:min(100%,10rem);max-width:100%}.models-v2-mode-row{flex-wrap:wrap;justify-self:start}.models-v2-keep-native-row{grid-column:1;justify-content:flex-start;justify-self:start;margin-top:0}.models-v2-keep-native{white-space:normal}}@container models-workspace (width<=720px){.models-workspace-root{grid-template-columns:1fr}.models-workspace-rail{border-right:none;border-bottom:1px solid var(--border);padding-right:0;padding-bottom:var(--space-3)}.models-workspace-rail-list{max-height:240px}.models-provider-actions{justify-content:flex-start;width:100%;margin-left:0}}@media (width<=768px){.models-workspace-root{grid-template-columns:1fr}.models-workspace-rail{border-right:none;border-bottom:1px solid var(--border);padding-right:0;padding-bottom:var(--space-3)}.models-workspace-rail-list{max-height:240px}.models-provider-actions{justify-content:flex-start;width:100%;margin-left:0}}.main-inner:has(.dashboard-workspace-shell){max-width:1200px}.dashboard-workspace-shell{width:100%;min-width:0}.dashboard-workspace-main{gap:var(--space-4);flex-direction:column;min-width:0;display:flex}.dashboard-workspace-main .tbl-wrap{max-height:520px;padding:0 var(--space-2) var(--space-2);overflow-y:auto}.dashboard-workspace-main .tbl{margin-top:var(--space-3)}.dashboard-workspace-main .tbl thead th{z-index:1;background:var(--surface);position:sticky;top:0}.dash-overview-stack{gap:var(--space-4);flex-direction:column;display:flex}.dash-overview-tools{gap:var(--space-4);grid-template-columns:repeat(auto-fit,minmax(min(100%,21rem),1fr));align-items:stretch;display:grid}.dash-overview-tools>.panel{box-sizing:border-box;min-width:0;height:100%}.dash-sidecar-grid{gap:var(--space-4);grid-template-columns:repeat(auto-fit,minmax(min(100%,39rem),1fr));align-items:stretch;display:grid}.dash-sidecar-row-card .dash-delegation-controls .custom-select:nth-child(2){min-width:min(100%,6.5rem);max-width:9rem}.dash-sidecar-copy{flex:auto;min-width:0}.dash-sidecar-row-card .dash-sidecar-copy{overflow-wrap:break-word;flex:1 1 0;min-width:min(100%,14rem)}.dash-sidecar-row-card .dash-delegation-controls{flex-flow:column;flex:0 0 min(100%,26rem);justify-content:flex-start;align-items:stretch;gap:12px;min-height:3.6875rem}.dash-sidecar-row-card .dash-delegation-controls .custom-select:first-child{min-width:min(100%,10.5rem);max-width:14rem}.dash-sidecar-row-card .dash-delegation-controls .select-trigger{justify-content:space-between;width:100%;max-width:100%}.dash-sidecar-row-card .dash-delegation-controls .select-trigger>span{text-overflow:ellipsis;white-space:nowrap;min-width:0;overflow:hidden}.dash-sidecar-row-card{flex-wrap:wrap;align-content:start;min-width:0;container:sidecar-card/inline-size}.dash-sidecar-row-card .dash-sidecar-copy .setting-hint{min-height:3lh}.dash-vision-sidecar-card .dash-sidecar-copy{min-width:min(100%,14rem)}.dash-vision-sidecar-card .dash-delegation-controls{min-width:0;max-width:100%}.dash-sidecar-select-row{flex-wrap:wrap;justify-content:flex-start;align-items:center;gap:8px;width:100%;min-width:0;display:flex}.dash-sidecar-select-row .custom-select:first-child{flex:70%;min-width:min(100%,9rem);max-width:16rem}.dash-sidecar-select-row .custom-select:nth-child(2){flex:30%;min-width:min(100%,6rem);max-width:9rem}.dash-sidecar-select-row .custom-select .select-trigger{width:100%;max-width:100%}.dash-sidecar-trailing-row{justify-content:flex-end;align-items:center;gap:8px;width:100%;display:flex}.dash-sidecar-toggle-label{text-align:right;min-width:0}.dash-vision-advanced-trigger{appearance:none;color:var(--muted);font-size:var(--text-control);cursor:pointer;background:0 0;border:none;align-items:center;gap:6px;padding:0;display:inline-flex}.dash-vision-advanced-trigger:hover:not(:disabled){color:var(--text)}.dash-vision-advanced-trigger:disabled{opacity:.6;cursor:not-allowed}.dash-vision-advanced-trigger:focus-visible{outline:2px solid var(--accent-ring);outline-offset:2px;border-radius:var(--radius-2xs)}.dash-vision-number{flex-direction:column;gap:4px;min-width:0;display:flex}.dash-vision-number .codex-auto-switch-input-wrap{width:min(11.5rem,100%)}.dash-vision-number .codex-auto-switch-input{min-width:0}.dash-vision-advanced-popover{box-sizing:border-box;background:var(--raised);border:1px solid var(--border);border-radius:var(--radius);flex-direction:column;gap:12px;width:max-content;min-width:16rem;max-width:min(22rem,100vw - 2rem);padding:12px 14px;display:flex;overflow-y:auto;box-shadow:0 4px 24px #00000024}.dash-vision-advanced-popover-title{font-weight:var(--weight-semibold)}.dash-vision-advanced-popover .dash-vision-number .codex-auto-switch-input-wrap{width:100%}.dash-vision-advanced-popover .dash-vision-number .codex-auto-switch-input{flex:1 1 0}.dash-vision-advanced-popover .dash-vision-number{gap:4px}@container sidecar-card (width<=36rem){.dash-sidecar-row-card .dash-sidecar-copy,.dash-sidecar-row-card .dash-delegation-controls{flex:0 100%;min-width:0}.dash-sidecar-row-card .dash-delegation-controls{align-items:stretch}.dash-vision-sidecar-card .dash-sidecar-select-row{justify-content:flex-start}}@container sidecar-card (width<=22rem){.dash-sidecar-row-card .dash-sidecar-copy,.dash-sidecar-row-card .dash-delegation-controls{flex-basis:100%;min-width:0}.dash-sidecar-row-card .dash-delegation-controls{flex-wrap:wrap;justify-content:flex-start}}.dash-model-acc{gap:var(--space-2);flex-direction:column;display:flex}.dash-model-group{border:1px solid var(--border);border-radius:var(--radius);background:var(--surface);overflow:hidden}.dash-model-head{appearance:none;cursor:pointer;width:100%;font:inherit;color:var(--text);text-align:left;transition:background var(--motion-fast);background:0 0;border:none;align-items:center;gap:8px;padding:10px 14px;display:flex}.dash-model-head:hover{background:var(--hover)}.dash-model-head .count{font-family:var(--font-code);font-weight:var(--weight-medium);color:var(--faint);font-size:var(--text-label)}.dash-model-chips{border-top:1px solid var(--border-soft);flex-wrap:wrap;gap:6px;padding:12px 14px 14px;display:flex}.dash-model-chip{font-family:var(--font-code);font-size:var(--text-label);border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--raised);color:var(--text);white-space:nowrap;padding:3px 8px}.main-inner:has(.storage-workspace-root){max-width:1200px}.storage-workspace-root{gap:var(--space-4);grid-template-columns:minmax(0,1fr);align-items:start;width:100%;max-width:100%;display:grid}.storage-workspace-rail{gap:var(--space-2);border-bottom:1px solid var(--border);padding-bottom:var(--space-3);flex-direction:column;min-width:0;min-height:0;display:flex}.storage-workspace-rail-header{flex:none;justify-content:space-between;align-items:baseline;gap:8px;display:flex}.storage-workspace-rail-title{font-size:var(--text-body);font-weight:var(--weight-semibold);color:var(--text)}.storage-workspace-rail-count{font-family:var(--mono);color:var(--faint);font-variant-numeric:tabular-nums;font-size:11px}.storage-workspace-rail-list{overscroll-behavior:auto;scrollbar-gutter:stable;flex-direction:column;gap:2px;min-height:0;max-height:min(14rem,40vh);padding-right:10px;display:flex;overflow-y:auto}.storage-workspace-rail-row{justify-content:center;gap:var(--space-0-5);appearance:none;border-radius:var(--radius-sm);width:100%;height:3.25rem;min-height:3.25rem;padding:var(--space-1) var(--space-2);cursor:pointer;text-align:left;color:inherit;font:inherit;transition:background var(--motion-fast), box-shadow var(--motion-fast);background:0 0;border:none;flex-direction:column;flex:none;display:flex;overflow:hidden}.storage-workspace-rail-row:hover{background:var(--surface)}.storage-workspace-rail-row--selected{background:var(--accent-soft);box-shadow:inset 0 0 0 1px var(--border)}.storage-workspace-rail-row:focus-visible{outline:2px solid var(--accent-ring);outline-offset:-2px}.storage-workspace-rail-primary{justify-content:space-between;align-items:center;gap:var(--space-2);display:flex}.storage-workspace-rail-name{text-overflow:ellipsis;white-space:nowrap;min-width:0;font-size:var(--text-body);font-weight:var(--weight-medium);line-height:var(--leading-ui);overflow:hidden}.storage-workspace-rail-size{font-family:var(--mono);color:var(--muted);font-variant-numeric:tabular-nums;flex-shrink:0;padding-right:2px;font-size:11px}.storage-workspace-rail-meta{text-overflow:ellipsis;white-space:nowrap;min-width:0;font-size:var(--text-caption);line-height:var(--leading-ui);color:var(--muted);overflow:hidden}.storage-workspace-rail-empty{color:var(--muted);padding:8px 4px;font-size:12px}.storage-workspace-main{flex-direction:column;min-width:0;max-width:100%;min-height:0;padding:0;display:flex}.stw-overview,.stw-detail-body{min-height:0;overflow-x:hidden}.stw-detail-body{padding-top:14px}.storage-workspace-main .stw-summary{gap:var(--space-3);grid-template-columns:repeat(auto-fit,minmax(min(100%,11rem),1fr));margin-bottom:16px;display:grid}.stw-summary-card{border:1px solid var(--border);border-radius:var(--radius);background:var(--surface);flex-direction:column;gap:6px;min-width:0;padding:12px 14px;display:flex}.stw-summary-label{font-size:var(--text-label);font-weight:var(--weight-medium);color:var(--muted)}.stw-summary-value{font-size:var(--text-body);font-weight:var(--weight-semibold);font-variant-numeric:tabular-nums;color:var(--text);text-overflow:ellipsis;line-height:1.3;overflow:hidden}.stw-summary-value.mono,.stw-home-path{font-family:var(--mono);font-size:12px;font-weight:var(--weight-medium);text-overflow:ellipsis;white-space:nowrap;word-break:normal;overflow:hidden}.stw-section{margin-bottom:16px}.stw-section-title{text-transform:uppercase;letter-spacing:.04em;color:var(--muted);margin:0 0 8px;font-size:.7rem;font-weight:600}.stw-hint{color:var(--muted);padding:4px 0 12px;font-size:13px}.stw-file-row{align-items:center;gap:var(--space-3);border-bottom:1px solid var(--border-soft,var(--border));padding:6px 0;display:flex}.stw-file-row:last-child{border-bottom:none}.stw-file-path{text-overflow:ellipsis;white-space:nowrap;min-width:0;font-family:var(--mono);color:var(--text);flex:auto;font-size:12px;overflow:hidden}.stw-file-size{font-family:var(--mono);color:var(--muted);font-variant-numeric:tabular-nums;flex-shrink:0;font-size:12px}.stw-file-bucket{color:var(--faint);flex-shrink:0;font-size:11px}.stw-detail{flex-direction:column;max-width:760px;height:100%;min-height:0;display:flex}.stw-detail-toolbar{border-bottom:1px solid var(--border);background:0 0;flex:none;align-items:center;gap:8px;margin-bottom:0;padding:0 0 10px;display:flex}.stw-detail-back{appearance:none;border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--raised);color:var(--text);font:inherit;font-size:var(--text-control);font-weight:var(--weight-medium);line-height:var(--leading-ui);cursor:pointer;transition:background var(--motion-fast), border-color var(--motion-fast), color var(--motion-fast);align-items:center;gap:6px;margin:0;padding:6px 12px 6px 8px;display:inline-flex}.stw-detail-back:hover{background:var(--raised-hover,var(--surface));border-color:var(--faint);color:var(--text);text-decoration:none}.stw-detail-back:focus-visible{outline:2px solid var(--accent-ring);outline-offset:1px}.stw-detail-back-chevron{width:14px;height:14px;color:var(--muted);flex-shrink:0;transform:rotate(180deg)}.stw-detail-back:hover .stw-detail-back-chevron{color:var(--text)}.stw-detail-title{word-break:break-word;min-width:0;margin:0 0 12px;font-size:1.1rem;font-weight:600}.stw-kv{flex-direction:column;margin:0 0 16px;display:flex}.stw-kv-row{gap:12px;padding:4px 0;font-size:.84rem;line-height:1.4;display:flex}.stw-kv-row dt{width:110px;color:var(--muted);flex-shrink:0;font-weight:400}.stw-kv-row dd{word-break:break-word;flex:1;min-width:0;margin:0}.stw-kv-row dd code,.stw-kv-mono{font-family:var(--mono);font-variant-numeric:tabular-nums;font-size:.8rem}.storage-policy-help{font-size:var(--text-sm);line-height:var(--leading-body);margin:0}.storage-policy-enable{flex-wrap:wrap;align-items:center;gap:12px;display:flex}.storage-policy-enable-row{cursor:default;align-items:center;gap:10px;min-width:0;display:inline-flex}.storage-policy-fields{gap:10px;max-width:none;margin-top:0;display:grid}.storage-policy-fields .field{gap:4px;margin:0;display:grid}.storage-policy-fields .field-label{margin-bottom:0}.storage-policy-fields fieldset.field{border:none;min-width:0;padding:0}.storage-policy-trigger-row{flex-wrap:wrap;align-items:center;gap:10px;min-height:32px;display:flex}.storage-policy-trigger-hint{font-size:var(--text-control);line-height:var(--leading-ui);color:var(--text);flex:0 auto}.storage-policy-target{gap:6px;display:grid}.storage-policy-target>.field-label{padding:0}.storage-policy-target-row{flex-wrap:wrap;align-items:center;gap:10px;min-height:32px;display:flex}.storage-policy-target-label{font-size:var(--text-control);line-height:var(--leading-ui);color:var(--text);flex:0 auto}.storage-policy-target-row .codex-auto-switch-input-wrap,.storage-policy-trigger-row .codex-auto-switch-input-wrap{flex:none}.storage-policy-selects{grid-template-columns:repeat(2,minmax(0,1fr));gap:8px 12px;display:grid}.storage-policy-warn{font-size:var(--text-sm);line-height:var(--leading-body);margin:0}.storage-policy-meta{border-top:1px solid var(--border);grid-template-columns:repeat(2,minmax(0,1fr));gap:8px 12px;margin-top:6px;padding-top:12px;display:grid}.storage-policy-meta-item{min-width:0;font-size:var(--text-sm);line-height:var(--leading-body);align-items:baseline;gap:8px;display:flex}.storage-policy-meta-item>.muted{flex:none}.storage-policy-meta-value{font-variant-numeric:tabular-nums;min-width:0}.storage-policy-actions{flex-wrap:wrap;align-items:center;gap:8px;min-height:28px;margin-top:0;display:flex}.storage-policy-actions__status{min-width:7rem;font-size:var(--text-label);color:var(--muted);line-height:var(--leading-body)}.storage-policy-actions__status.is-error{color:var(--red)}.storage-page-head-actions{align-items:center;gap:10px;min-width:0;display:flex}.storage-page-head-feedback{text-align:right;min-width:7rem;max-width:16rem;font-size:var(--text-label);color:var(--muted);line-height:var(--leading-body)}.storage-page-meta{font-size:var(--text-sm);color:var(--muted);line-height:var(--leading-body);flex-wrap:wrap;align-items:baseline;gap:6px 10px;margin:-12px 0 16px;display:flex}.storage-page-meta__home{text-overflow:ellipsis;white-space:nowrap;max-width:min(100%,42rem);color:var(--text);font-size:inherit;overflow:hidden}.storage-page-meta__sep{color:var(--faint)}.storage-cleanup-card{justify-items:start;gap:10px;margin-top:16px;padding:14px 16px;display:grid}.storage-cleanup-card>.panel-title{width:100%;margin:0}.storage-cleanup-card__tabs{justify-self:start}.storage-cleanup-card__stack{width:100%;min-width:0;display:grid}.storage-cleanup-card__body{grid-area:1/1;width:100%;min-width:0}.storage-cleanup-card__body[data-active=false]{visibility:hidden;pointer-events:none}.storage-cleanup-policy-split{grid-template-columns:minmax(0,1fr);align-items:start;gap:12px 20px;display:grid}.storage-cleanup-pane{gap:10px;min-width:0;display:grid}.storage-cleanup-manual{border-top:1px solid var(--border);align-content:start;gap:8px;min-width:0;padding-top:12px;display:grid}.storage-cleanup-manual__title{font-size:var(--text-body);font-weight:var(--weight-semibold);line-height:var(--leading-ui);color:var(--text);margin:0}.storage-cleanup-manual .storage-cleanup-pane{gap:8px}.storage-cleanup-manual .storage-manual-panel__controls{flex-direction:column;align-items:stretch}.storage-cleanup-manual .storage-manual-panel__slider{flex:auto}.storage-cleanup-pane.storage-quarantine-pane{grid-template-rows:auto auto 1fr auto;align-content:start;min-height:8rem;display:grid}.storage-manual-panel{gap:10px;padding:14px 16px;display:grid}.storage-manual-panel>.panel-title{margin:0}.storage-manual-panel__help{font-size:var(--text-sm);line-height:var(--leading-body);margin:0}.storage-manual-panel__controls{flex-wrap:wrap;align-items:center;gap:10px;display:flex}.storage-manual-panel__slider{flex:220px;align-items:center;gap:8px;min-width:0;display:flex}.storage-manual-panel__presets{flex-wrap:wrap;gap:6px;display:flex}.storage-manual-panel__status{font-size:var(--text-sm);line-height:var(--leading-body);margin:0}.storage-manual-panel__table{margin:0}@media (width<=520px){.storage-cleanup-card__tabs{width:100%}.storage-cleanup-card__tabs.usage-segmented{width:100%;display:flex}.storage-cleanup-card__tabs .usage-segmented-btn{flex:1 1 0;min-width:0}.storage-policy-selects,.storage-policy-meta{grid-template-columns:minmax(0,1fr)}.storage-policy-enable{align-items:flex-start}}@media (width<=768px){.storage-workspace-rail{position:static}}.main-inner:has(.subagents-workspace-shell){max-width:1200px}.subagents-workspace-shell{width:100%;min-width:0;container:subagents-workspace/inline-size}.subagents-workspace-root{gap:var(--space-4);grid-template-columns:minmax(0,1fr);width:100%;max-width:100%;min-height:0;display:grid}.subagents-workspace-rail{gap:var(--space-3);border-bottom:1px solid var(--border);padding-bottom:var(--space-3);flex-direction:column;min-width:0;display:flex}.subagents-workspace-section{gap:var(--space-2);flex-direction:column;min-width:0;display:flex}.subagents-workspace-section+.subagents-workspace-section{margin-top:var(--space-3);padding-top:var(--space-4);border-top:1px solid var(--border)}.swi-delegation{border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--surface);flex-direction:column;display:flex}.swi-delegation-row{justify-content:space-between;align-items:flex-start;gap:var(--space-4);padding:var(--space-3);display:flex}.swi-delegation-row+.swi-delegation-row{border-top:1px solid var(--border-soft)}.swi-delegation-row .setting-copy{flex:auto;min-width:0}.swi-delegation-row .setting-hint{max-width:72ch;margin-top:3px;line-height:1.5}.swi-delegation-controls{align-items:center;gap:var(--space-2);flex-wrap:wrap;flex-shrink:0;justify-content:flex-end;display:flex}.swi-rail-icon{width:15px;height:15px;color:var(--faint);flex-shrink:0}.subagents-workspace-rail-header{justify-content:space-between;align-items:baseline;gap:8px;display:flex}.subagents-workspace-rail-title{font-size:var(--text-body);font-weight:var(--weight-semibold);color:var(--text)}.subagents-workspace-rail-count{font-family:var(--mono);color:var(--faint);font-variant-numeric:tabular-nums;font-size:11px}.swi-picker-box{gap:var(--space-2);border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--surface);min-height:0;padding:var(--space-2);flex-direction:column;display:flex}.swi-picker-box .subagents-workspace-rail-search{flex:none}.subagents-workspace-rail-list{flex-direction:column;gap:4px;min-height:0;display:flex}.swi-picker-box{--swi-row-step:46px}.swi-picker-box .subagents-workspace-rail-list{overscroll-behavior:auto;scrollbar-gutter:stable;max-height:min(456px,52vh);overflow-y:auto}@supports (height:round(down, 50vh, 46px)){.swi-picker-box .subagents-workspace-rail-list{max-height:calc(round(down, min(456px, 52vh) + 4px, var(--swi-row-step)) - 4px)}}.subagents-workspace-rail-group{flex-direction:column;gap:2px;min-width:0;display:flex}.subagents-workspace-rail-group+.subagents-workspace-rail-group{margin-top:8px}.subagents-workspace-rail-group-head{justify-content:space-between;align-items:center;gap:var(--space-2);font-size:var(--text-caption);font-weight:var(--weight-medium);color:var(--muted);padding:var(--space-2) var(--space-2) var(--space-1);display:flex}.subagents-workspace-rail-group-count{font-variant-numeric:tabular-nums;flex-shrink:0}.subagents-workspace-rail-row{min-width:0;max-width:100%;min-height:42px;padding:var(--space-1) var(--space-1-5);border-radius:var(--radius-sm);transition:background var(--motion-fast);align-items:center;gap:6px;display:flex}.subagents-workspace-rail-row:hover{background:var(--surface)}.subagents-workspace-rail-row--selected{background:var(--accent-soft);box-shadow:inset 0 0 0 1px var(--border)}.subagents-workspace-rail-row-main{appearance:none;cursor:pointer;min-width:0;font:inherit;font-size:var(--text-body);color:var(--text);text-align:left;border-radius:var(--radius-sm);background:0 0;border:none;flex:auto;align-items:center;gap:8px;padding:4px 6px;display:flex}.subagents-workspace-rail-row-main:focus-visible{outline:2px solid var(--accent-ring);outline-offset:-2px}.subagents-workspace-rail-name{text-overflow:ellipsis;white-space:nowrap;min-width:0;font-family:var(--mono);flex:auto;font-size:12.5px;overflow:hidden}.subagents-workspace-rail-toggle{appearance:none;border:1px solid var(--border);border-radius:var(--radius-sm);width:26px;height:26px;color:var(--faint);cursor:pointer;transition:background var(--motion-fast), border-color var(--motion-fast), color var(--motion-fast);background:0 0;flex-shrink:0;justify-content:center;align-items:center;display:inline-flex}.subagents-workspace-rail-toggle:hover{background:var(--surface);border-color:var(--muted)}.subagents-workspace-rail-toggle:focus-visible{outline:2px solid var(--accent-ring);outline-offset:1px}.subagents-workspace-rail-toggle--on{border-color:var(--accent);color:var(--accent);background:var(--accent-soft)}.subagents-workspace-rail-toggle--disabled{opacity:.4;cursor:not-allowed}.swi-rail-priority{font-family:var(--mono);color:var(--accent);text-align:center;font-variant-numeric:tabular-nums;flex-shrink:0;width:14px;font-size:10.5px;font-weight:700}.subagents-workspace-rail-empty{color:var(--muted);padding:8px 4px;font-size:12px}.subagents-workspace-main{min-width:0;max-width:100%;padding:0;overflow:hidden}.swi-featured-head{justify-content:space-between;align-items:baseline;gap:12px;margin-bottom:6px;display:flex}.swi-featured-title{color:var(--text);margin:0;font-size:1.15rem;font-weight:600}.swi-featured-count{font-family:var(--mono);color:var(--faint);font-variant-numeric:tabular-nums;font-size:12px}.swi-featured-hint{color:var(--muted);align-items:flex-start;gap:8px;max-width:80ch;margin:0 0 16px;font-size:12.5px;line-height:1.5;display:flex}.swi-featured-hint svg{flex-shrink:0;margin-top:2px}.swi-featured-list{flex-direction:column;gap:8px;margin-bottom:18px;display:flex}.swi-featured-row{border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--surface);align-items:center;gap:10px;padding:9px 12px;display:flex}.swi-featured-pos{font-family:var(--mono);color:var(--accent);font-variant-numeric:tabular-nums;flex-shrink:0;width:18px;font-size:13px;font-weight:700}.swi-featured-name{text-overflow:ellipsis;white-space:nowrap;min-width:0;font-family:var(--mono);color:var(--text);flex:auto;font-size:13px;overflow:hidden}.swi-featured-actions{flex-shrink:0;align-items:center;gap:4px;display:inline-flex}.swi-featured-empty{border:1px dashed var(--border);border-radius:var(--radius-sm);text-align:center;color:var(--muted);margin-bottom:18px;padding:24px 16px;font-size:13px}.swi-save-row{align-items:center;gap:10px;display:flex}.swi-detail{flex-direction:column;max-width:720px;display:flex}.swi-detail-back{color:var(--accent);font:inherit;cursor:pointer;background:0 0;border:none;align-items:center;gap:4px;margin-bottom:10px;padding:0;font-size:.82rem;display:inline-flex}.swi-detail-back:hover{text-decoration:underline}.swi-detail-back-chevron{width:12px;height:12px;transform:rotate(180deg)}.swi-detail-head{align-items:center;gap:10px;margin-bottom:16px;display:flex}.swi-detail-icon{width:32px;height:32px;color:var(--text);flex-shrink:0;justify-content:center;align-items:center;display:flex}.swi-detail-title{word-break:break-all;min-width:0;margin:0;font-size:1.15rem;font-weight:600;line-height:1.3}.swi-detail-section{margin-bottom:18px}.swi-detail-section-title{text-transform:uppercase;letter-spacing:.04em;color:var(--muted);margin:0 0 8px;font-size:.7rem;font-weight:600}.swi-detail-kv{flex-direction:column;margin:0;display:flex}.swi-detail-kv-row{gap:12px;padding:5px 0;font-size:.84rem;line-height:1.4;display:flex}.swi-detail-kv-row dt{width:130px;color:var(--muted);flex-shrink:0;font-weight:400}.swi-detail-kv-row dd{word-break:break-word;flex:1;min-width:0;margin:0}.swi-detail-kv-row dd code{font-size:.8rem}.swi-detail-actions{flex-wrap:wrap;align-items:center;gap:8px;margin-top:4px;display:flex}@container subagents-workspace (width<=720px){.subagents-workspace-root{min-height:auto}.swi-picker-box .subagents-workspace-rail-list{max-height:min(318px,46vh)}@supports (height:round(down, 50vh, 46px)){.swi-picker-box .subagents-workspace-rail-list{max-height:calc(round(down, min(318px, 46vh) + 4px, var(--swi-row-step)) - 4px)}}}@media (width<=768px){.subagents-workspace-root{min-height:auto}.swi-picker-box .subagents-workspace-rail-list{max-height:min(272px,44vh)}@supports (height:round(down, 50vh, 46px)){.swi-picker-box .subagents-workspace-rail-list{max-height:calc(round(down, min(272px, 44vh) + 4px, var(--swi-row-step)) - 4px)}}}.integration-badge--danger{background:var(--red-soft);color:var(--red)}.integration-badge--danger-outline{color:var(--red);border-color:var(--red);background:0 0}.integration-summary{border:1px solid var(--border);border-radius:var(--radius);background:var(--raised);flex-wrap:wrap;align-items:center;gap:18px;margin-bottom:14px;padding:14px 16px;display:flex}.integration-summary-cell{flex-direction:column;gap:2px;display:flex}.integration-summary-label{font-size:var(--text-caption);color:var(--muted)}.integration-summary .btn{margin-left:auto}.integration-cards{grid-template-columns:repeat(auto-fill,minmax(min(260px,100%),1fr));gap:12px;margin:14px 0;padding:0;list-style:none;display:grid}.integration-api-keys-row{border:1px solid var(--border);border-radius:var(--radius);background:var(--raised);flex-wrap:wrap;align-items:center;gap:10px;margin:14px 0 0;padding:12px 14px;display:flex}.integration-api-keys-copy{flex-direction:column;flex:220px;gap:2px;min-width:0;display:flex}.integration-api-keys-copy h4{margin:0}.integration-api-keys-row .integration-meta{min-height:0;margin:0}.integration-card{border:1px solid var(--border);border-radius:var(--radius);background:var(--raised);flex-direction:column;gap:8px;padding:14px;display:flex;position:relative}.integration-card-head{justify-content:space-between;align-items:center;gap:8px;display:flex}.integration-card-head h4{margin:0}.integration-card-link{appearance:none;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:none;margin:0;padding:0}.integration-card-link:after{content:"";border-radius:var(--radius);position:absolute;inset:0}.integration-card-link:focus-visible{outline:none}.integration-card-link:focus-visible:after{outline:2px solid var(--accent-ring);outline-offset:-2px}.integration-card:hover{border-color:var(--accent-ring)}.integration-card-actions{z-index:1;flex-wrap:wrap;align-items:center;gap:10px;min-width:0;margin-top:auto;display:flex;position:relative}.integration-card-actions .btn{margin-left:auto}.integration-empty{border:1px dashed var(--border);border-radius:var(--radius);text-align:center;color:var(--muted);padding:20px}.integration-client-head{align-items:center;gap:10px;display:flex}.integration-client-head h3{margin:0}.client-mark{width:var(--client-mark-size,20px);height:var(--client-mark-size,20px);flex:none;justify-content:center;align-items:center;display:inline-flex}.client-mark--img img{border-radius:2px;width:100%;height:100%}.client-mark--mask{background:var(--text);-webkit-mask-position:50%;mask-position:50%;-webkit-mask-size:contain;mask-size:contain;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}.client-mark--monogram{font-size:var(--text-label);font-weight:var(--weight-semibold);color:var(--muted);border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--raised);line-height:1}.integration-card-head h4{flex:auto;min-width:0}.page-tab>.client-mark{vertical-align:-2px;margin-right:6px}.integration-client-head .switch{margin-left:auto}.integration-path{font-family:var(--font-code);font-size:var(--text-caption);color:var(--muted);overflow-wrap:anywhere;margin:4px 0}.integration-meta{font-size:var(--text-caption);color:var(--muted)}.integration-card .integration-path,.integration-card .integration-meta{min-height:calc(var(--text-caption) * var(--leading-ui) * 2);margin:0}.integration-history{border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--raised);margin:8px 0;overflow:hidden}.integration-history-list{margin:0;padding:0;list-style:none}.integration-history-row{flex-wrap:wrap;align-items:center;gap:10px;padding:8px 12px;display:flex}.integration-history-list .integration-history-row+.integration-history-row,.integration-history-older{border-top:1px solid var(--border-soft)}.integration-history-older>summary{color:var(--muted);font-size:var(--text-caption);cursor:pointer;padding:8px 12px}.integration-history-older>summary:focus-visible{outline:2px solid var(--accent-ring);outline-offset:-2px}.integration-history-older .integration-history-row:first-child{border-top:1px solid var(--border-soft)}.integration-history-more{margin:8px 12px}.integration-history-kind{font-family:var(--font-code);font-size:var(--text-caption);font-weight:var(--weight-semibold)}.integration-history-client{font-size:var(--text-caption);color:var(--muted)}.integration-history-at{font-size:var(--text-caption);color:var(--muted);margin-right:auto}@media (width<=420px){.integration-history-at{flex:100%;margin-right:0}.integration-history-row .btn{margin-left:auto}}.integration-restore-dialog .integration-path{margin:10px 0}.integration-consequence-body{flex-direction:column;gap:12px;display:flex}.integration-consequence-body p{font-size:var(--text-caption);line-height:var(--leading-body);margin:0}.integration-consequence-body code{font-family:var(--font-code);font-size:inherit;overflow-wrap:anywhere}.claudecode-connection-head{align-items:center;gap:10px;padding:10px 0;display:flex}.claudecode-connection-head .switch{margin-left:auto}.cursor-page{flex-direction:column;gap:14px;display:flex}.cursor-card{border:1px solid var(--border);border-radius:var(--radius);background:var(--raised);flex-direction:column;gap:8px;padding:14px;display:flex}.cursor-card h4{margin:0}.cursor-detect-row{flex-wrap:wrap;align-items:center;gap:10px;display:flex}.cursor-detect-name{font-weight:var(--weight-semibold);min-width:12ch}.cursor-detect-path{font-family:var(--font-code);font-size:var(--text-caption);overflow-wrap:anywhere}.cursor-gateway-row{grid-template-columns:minmax(7ch,auto) 1fr auto;align-items:center;gap:10px;display:grid}.cursor-gateway-label{font-weight:var(--weight-semibold)}.cursor-gateway-value{font-family:var(--font-code);border-radius:var(--radius);background:var(--bg);border:1px solid var(--border);overflow-wrap:anywhere;padding:4px 8px}.cursor-model-table{border-collapse:collapse;width:100%;font-size:var(--text-caption)}.cursor-model-table th,.cursor-model-table td{text-align:left;border-bottom:1px solid var(--border);vertical-align:top;padding:6px 8px}.cursor-model-table th{font-weight:var(--weight-semibold);color:var(--muted)}.cursor-effort-rows{margin-left:.5rem;font-size:.85em}.main-inner:has(.usage-workspace-shell){max-width:1200px}.usage-workspace-shell{width:100%;min-width:0;container:usage-workspace/inline-size}.usage-workspace-root{grid-template-columns:minmax(0,1fr);gap:0;width:100%;max-width:100%;min-height:0;display:grid}.usw-section-block+.usw-section-block{margin-top:var(--space-5);padding-top:var(--space-4);border-top:1px solid var(--border)}.usage-workspace-rail{gap:var(--space-3);border-bottom:1px solid var(--border);padding-bottom:var(--space-3);flex-direction:column;min-width:0;display:flex}.usage-workspace-rail-header{justify-content:space-between;align-items:baseline;gap:8px;display:flex}.usage-workspace-rail-title{font-size:var(--text-body);font-weight:var(--weight-semibold);color:var(--text)}.usage-workspace-rail-list{flex-direction:column;gap:2px;min-height:0;display:flex}.usage-workspace-rail-row{gap:var(--space-0-5);appearance:none;border-radius:var(--radius-sm);min-width:0;max-width:100%;min-height:46px;padding:var(--space-1-5) var(--space-2);cursor:pointer;text-align:left;color:inherit;font:inherit;background:0 0;border:none;flex-direction:column;display:flex;overflow:hidden}.usage-workspace-rail-row:hover{background:var(--raised)}.usage-workspace-rail-row--selected{background:var(--raised);box-shadow:inset 0 0 0 1px var(--border)}.usage-workspace-rail-name{font-size:var(--text-body);font-weight:var(--weight-medium);color:var(--text);white-space:nowrap;text-overflow:ellipsis;overflow:hidden}.usage-workspace-rail-meta{font-size:var(--text-caption);color:var(--faint);white-space:nowrap;text-overflow:ellipsis;font-variant-numeric:tabular-nums;overflow:hidden}.usage-workspace-main,.usw-body{gap:var(--space-4);flex-direction:column;min-width:0;display:flex}.usw-section{flex-direction:column;gap:0;min-width:0;display:flex}.usw-section .h-section{margin:0 0 var(--space-3);font-size:var(--text-title);font-weight:var(--weight-semibold);color:var(--text);line-height:1.2}.usw-section-toolbar{margin:0 0 var(--space-5);max-width:220px}.usw-section .tbl-wrap{min-width:0;padding:var(--space-3);overscroll-behavior:auto;scrollbar-gutter:stable;max-height:min(574px,58vh);overflow-y:auto}.usw-section .tbl-wrap thead th{top:calc(-1 * var(--space-3));z-index:1;background:var(--surface);box-shadow:0 calc(-1 * var(--space-3)) 0 var(--surface), inset 0 -1px 0 var(--border);border-bottom-color:#0000;position:sticky}.usw-section .usage-cards{margin-top:0}@container usage-workspace (width<=720px){.usage-workspace-root{min-height:auto}}@media (width<=768px){.usage-workspace-root{min-height:auto}}.usage-source-row{color:var(--text-secondary);justify-content:space-between;align-items:center;gap:12px;margin:0 0 14px;display:flex}.usage-scope-control{gap:6px;display:inline-flex}@media (width<=640px){.usage-source-row{flex-direction:column;align-items:flex-start}}.main-inner:has(.claudecode-workspace-shell){max-width:1200px}.claudecode-workspace-shell{width:100%;min-width:0}.claudecode-workspace-root{gap:var(--space-4);grid-template-columns:minmax(240px,280px) minmax(0,1fr);width:100%;max-width:100%;min-height:0;display:grid}.claude-effective-auth{font-size:var(--text-caption);color:var(--muted);border-bottom:1px solid var(--border-soft);flex-direction:column;gap:2px;margin-top:-4px;padding:0 16px 12px;line-height:1.45;display:flex}.claude-effective-auth.warn{color:var(--amber)}.claude-effective-auth-label{font-weight:var(--weight-semibold);color:var(--faint);text-transform:uppercase;letter-spacing:.04em;font-size:10.5px}.claudecode-workspace-rail{gap:var(--space-3);border-right:1px solid var(--border);padding-right:var(--space-3);flex-direction:column;min-width:0;display:flex}.claudecode-workspace-rail-list{flex-direction:column;gap:2px;min-height:0;display:flex}.claudecode-workspace-rail-row{gap:var(--space-0-5);appearance:none;border-radius:var(--radius-sm);width:100%;min-height:46px;padding:var(--space-1-5) var(--space-2);cursor:pointer;text-align:left;color:inherit;font:inherit;background:0 0;border:none;flex-direction:column;display:flex;overflow:hidden}.claudecode-workspace-rail-row:hover{background:var(--raised)}.claudecode-workspace-rail-row--selected{background:var(--raised);box-shadow:inset 0 0 0 1px var(--border)}.claudecode-workspace-rail-name{font-size:var(--text-body);font-weight:var(--weight-medium);color:var(--text);white-space:nowrap;text-overflow:ellipsis;overflow:hidden}.claudecode-workspace-main{gap:var(--space-3);flex-direction:column;min-width:0;display:flex}.ccw-main-head{justify-content:space-between;align-items:center;gap:var(--space-3);min-width:0;min-height:32px;display:flex}.ccw-main-title{min-width:0;font-size:var(--text-control);font-weight:var(--weight-semibold);line-height:var(--leading-ui);color:var(--text);align-items:baseline;gap:8px;margin:0;display:flex}.ccw-main-title .count{color:var(--muted);font-weight:var(--weight-medium);font-family:var(--font-code);font-size:var(--text-label)}.claudecode-workspace-save[data-visible=false]{visibility:hidden;pointer-events:none}.ccw-body{gap:var(--space-3);flex-direction:column;min-width:0;display:flex}.claude-aliases{min-width:0}.claude-aliases-hint{margin:0 0 8px}.claude-aliases-scroll{overscroll-behavior:contain;scrollbar-gutter:stable;flex-direction:column;gap:10px;max-height:min(22rem,48vh);padding-right:2px;display:flex;overflow-y:auto}.claude-aliases-group{min-width:0}.claude-aliases-group-label{font-size:10.5px;font-weight:var(--weight-semibold);letter-spacing:.04em;text-transform:uppercase;color:var(--muted);align-items:baseline;gap:6px;margin:0 0 5px;line-height:1.2;display:flex}.claude-aliases-group-count{font-family:var(--font-code);font-weight:var(--weight-medium);color:var(--faint);letter-spacing:0;text-transform:none;font-size:10px}.claude-aliases-chips{flex-wrap:wrap;gap:4px;display:flex}.claude-aliases-chip{border:1px solid var(--border-soft,var(--border));border-radius:var(--radius-xs);background:var(--raised);max-width:100%;color:var(--text);flex-direction:column;align-items:flex-start;gap:1px;padding:3px 8px;line-height:1.35;display:inline-flex;overflow:hidden}.claude-aliases-chip-id{font-family:var(--font-code);white-space:nowrap;text-overflow:ellipsis;max-width:100%;font-size:11px;overflow:hidden}.claude-aliases-chip-name{color:var(--muted);white-space:nowrap;text-overflow:ellipsis;max-width:100%;font-size:10px;overflow:hidden}.claudecode-workspace-save{align-items:center;gap:var(--space-2);display:flex}@media (width<=768px){.claudecode-workspace-root{grid-template-columns:1fr}.claudecode-workspace-rail{border-right:none;border-bottom:1px solid var(--border);padding-right:0;padding-bottom:var(--space-3)}}:is(.main-inner:has(.api-page),.main-inner:has(.apikeys-workspace-shell)){box-sizing:border-box;max-width:1200px}.api-page{flex-direction:column;gap:0;min-width:0;display:flex}.api-page .page-head{margin-bottom:2px}.api-page .page-sub{margin:0 0 1.15rem}.api-page>.notice{margin-bottom:.85rem}.api-page .apikeys-workspace-shell{margin-top:.15rem}.apikeys-workspace-shell{width:100%;min-width:0;container:apikeys-workspace/inline-size}.apikeys-workspace-root{gap:var(--space-3);grid-template-columns:minmax(0,1fr);align-items:start;width:100%;max-width:100%;min-height:0;display:grid}.awi-section-anchor{gap:var(--space-3);flex-direction:column;min-width:0;display:flex}.awi-keylist-panel .awi-keylist-name{appearance:none;font:inherit;color:var(--accent-text,var(--text));font-weight:var(--weight-semibold);text-align:left;cursor:pointer;overflow-wrap:anywhere;background:0 0;border:none;padding:0}.awi-keylist-panel .awi-keylist-name:hover{text-decoration:underline}.awi-keylist-panel .awi-keylist-name:disabled{cursor:default;opacity:.6;text-decoration:none}.awi-keylist-panel .awi-keylist-name:focus-visible{outline:2px solid var(--accent-ring);outline-offset:2px;border-radius:var(--radius-sm)}.apikeys-workspace-main{min-width:0;max-width:100%;padding:0 var(--space-2);flex-direction:column;display:flex}.awi-detail{flex-direction:column;flex:1;min-height:0;display:flex}.awi-detail-toolbar{border-bottom:1px solid var(--border);background:0 0;flex:none;align-items:center;gap:8px;margin-bottom:0;padding:0 0 10px;display:flex}.awi-detail-body{min-height:0;padding-top:14px}.awi-overview{align-items:start;row-gap:var(--space-3);grid-template-columns:minmax(0,1fr);padding-top:0;display:grid}.awi-overview-section{gap:var(--space-3);flex-direction:column;min-width:0;display:flex}.awi-overview-section>.panel{flex:none;margin-top:0!important}.awi-overview .api-panel{gap:10px;padding:18px}.awi-overview .api-auth-list{gap:8px}.awi-overview .api-endpoints{margin-top:4px}.awi-overview-section>.panel>p.muted.small{margin:0;line-height:1.35}.awi-overview-section>.api-models-panel{flex-direction:column;flex:none;display:flex;overflow:visible}.awi-overview-section .api-models-panel>.input,.awi-overview-section .api-models-panel>.api-panel-head,.awi-overview-section .api-models-panel>.muted,.awi-overview-section .api-models-panel>.api-models-error{flex:none}.awi-overview-section .api-models-panel>.api-models-scroll{overscroll-behavior:auto;scrollbar-gutter:stable;min-width:0;max-height:min(574px,58vh);margin-top:.5rem;overflow:auto}.awi-overview-section .api-models-scroll>.tbl{table-layout:auto;width:100%}.awi-overview-section .api-models-scroll .api-model-cell{overflow-wrap:anywhere;min-width:0}.awi-overview-section .api-models-scroll thead th{z-index:1;background:var(--surface);box-shadow:inset 0 -1px 0 var(--border);border-bottom-color:#0000;position:sticky;top:0}.api-models-error{justify-content:space-between;align-items:center;gap:var(--space-2);flex-wrap:wrap;margin-top:.75rem;display:flex}.api-models-error p{min-width:0;margin:0}.api-models-empty{margin-top:.75rem}.api-example-copy-btn.ocx-tooltip,.api-example-copy-btn{width:100%;min-width:0;max-width:100%;display:block}.api-example-pre{overscroll-behavior:auto;white-space:pre;min-height:0;max-height:none;font-size:var(--text-label);line-height:var(--leading-relaxed);box-sizing:border-box;pointer-events:auto;padding:9px 11px;overflow:auto visible}.api-auth-matrix-block{margin-top:var(--space-3);gap:var(--space-2);flex-direction:column;min-width:0;display:flex}.api-auth-matrix-title{font-size:var(--text-label);margin:0;font-weight:600}.api-auth-matrix{border-collapse:collapse;width:100%;font-size:var(--text-label)}.api-auth-matrix-scroll{overscroll-behavior-x:contain;max-width:100%;overflow-x:auto}.api-auth-matrix th,.api-auth-matrix td{text-align:left;border-bottom:1px solid var(--border);white-space:nowrap;padding:4px 8px 4px 0}.api-auth-matrix th{color:var(--text-muted);font-weight:600}.api-auth-matrix code{font-size:inherit}.awi-rename{align-items:center;gap:var(--space-2);margin-bottom:var(--space-3);flex-wrap:wrap;min-width:0;display:flex}.awi-rename-error,.awi-delete-error{color:var(--danger,#c44);font-size:var(--text-label);flex-basis:100%;margin:0}.awi-rename-label{font-size:var(--text-label);color:var(--text-muted)}.awi-rename .input{flex:12rem;min-width:0}.api-model-test-chip{align-items:center;gap:4px;min-width:0;display:inline-flex}.api-model-actions{flex-wrap:nowrap;align-items:center;gap:4px;min-width:0;display:flex}.api-model-actions .btn{white-space:nowrap}.awi-usage-panel-body{gap:var(--space-3);flex-direction:column;min-width:0;display:flex}.awi-usage-example{gap:8px;min-width:0;padding-top:2px;display:grid}.awi-usage-example+.awi-usage-example{border-top:1px solid var(--border-soft,var(--border));padding-top:10px}.awi-usage-example-title{font-size:var(--text-control);font-weight:var(--weight-semibold);line-height:var(--leading-ui);color:var(--text);margin:0}.awi-overview-section>.api-generate-panel,.awi-overview-section>.api-newkey-panel{margin-top:0}.awi-overview-section>.awi-clientconfig-panel,.awi-clientconfig-panel.api-panel{overflow:visible}.awi-clientconfig-head{gap:var(--space-2);flex-wrap:wrap}.awi-clientconfig-rows{gap:var(--space-1);flex-direction:column;min-width:0;margin:0;padding:0;list-style:none;display:flex}.awi-clientconfig-row{align-items:center;gap:var(--space-2);min-width:0;padding:var(--space-2);border:1px solid var(--border);border-radius:var(--radius);background:var(--surface);display:flex}.awi-clientconfig-mark{border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--raised);flex:none;justify-content:center;align-items:center;width:28px;height:28px;display:inline-flex;overflow:hidden}.awi-clientconfig-mark:has(img){background:0 0;border-color:#0000}.awi-clientconfig-mark img{border-radius:var(--radius-sm);width:100%;height:100%}.awi-clientconfig-mark-mask{background:var(--text);width:20px;height:20px;-webkit-mask-position:50%;mask-position:50%;-webkit-mask-size:contain;mask-size:contain;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}.awi-clientconfig-mark:has(.awi-clientconfig-mark-mask){background:0 0;border-color:#0000}.awi-clientconfig-monogram{font-size:var(--text-label);font-weight:var(--weight-semibold);color:var(--muted);line-height:1}.awi-clientconfig-identity{flex-direction:column;flex:auto;gap:2px;min-width:0;display:flex}.awi-clientconfig-name{font-size:var(--text-control);font-weight:var(--weight-semibold);color:var(--text)}.awi-clientconfig-meta{text-overflow:ellipsis;white-space:nowrap;min-width:0;display:block;overflow:hidden}.awi-clientconfig-row-actions{align-items:center;gap:var(--space-1);flex:none;display:inline-flex}.awi-clientconfig-row-actions .btn{white-space:nowrap}.awi-clientconfig-dialog .awi-clientconfig-json{margin:0 0 var(--space-2)}@container apikeys-workspace (width<=560px){.awi-clientconfig-row{flex-wrap:wrap}.awi-clientconfig-row-actions{justify-content:flex-end;width:100%}}.awi-clientconfig-json{margin:0}.awi-clientconfig-json:focus-visible{outline:2px solid var(--accent-ring);outline-offset:1px}.awi-clientconfig-count,.awi-clientconfig-degraded,.awi-clientconfig-nokey,.awi-clientconfig-merge{margin:0;line-height:1.35}.awi-clientconfig-line{gap:4px;min-width:0;display:grid}.awi-clientconfig-where-title{margin:var(--space-2) 0 2px}.awi-section{margin-bottom:20px}.awi-section-title{text-transform:uppercase;letter-spacing:.04em;color:var(--muted);margin:0 0 8px;font-size:.7rem;font-weight:600}.awi-kv{flex-direction:column;margin:0;display:flex}.awi-kv-row{gap:12px;padding:5px 0;font-size:.84rem;line-height:1.4;display:flex}.awi-kv-row dt{width:130px;color:var(--muted);flex-shrink:0;font-weight:400}.awi-kv-row dd{overflow-wrap:anywhere;flex:1;min-width:0;margin:0}.awi-kv-row dd code{font-size:.8rem}.awi-detail-head{justify-content:space-between;align-items:center;gap:12px;margin-bottom:16px;display:flex}.awi-back{appearance:none;border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--raised);color:var(--text);font:inherit;font-size:var(--text-control);font-weight:var(--weight-medium);line-height:var(--leading-ui);cursor:pointer;transition:background var(--motion-fast), border-color var(--motion-fast), color var(--motion-fast);align-items:center;gap:6px;margin:0;padding:6px 12px 6px 8px;display:inline-flex}.awi-back:hover{background:var(--raised-hover,var(--surface));border-color:var(--faint);color:var(--text);text-decoration:none}.awi-back:focus-visible{outline:2px solid var(--accent-ring);outline-offset:1px}.awi-back-chevron{width:14px;height:14px;color:var(--muted);flex-shrink:0;transform:rotate(180deg)}.awi-detail-title{word-break:break-all;min-width:0;margin:0;font-size:1.15rem;font-weight:600}.awi-detail-actions{flex-shrink:0;align-items:center;gap:8px;display:inline-flex}@container apikeys-workspace (width<=720px){.apikeys-workspace-main{padding:var(--space-2) 0 0}}@media (width<=768px){.apikeys-workspace-main{padding:var(--space-2) 0 0}}.codex-set-prompt__rows{flex-direction:column;margin:12px 0 0;padding:0;list-style:none;display:flex}.codex-set-prompt__row{border-bottom:1px solid var(--border);align-items:center;gap:10px;padding:7px 0;display:flex}.codex-set-prompt__row:last-child{border-bottom:0}.codex-set-prompt__row[data-layer-class=config-toggle],.codex-set-prompt__row[data-layer-class=base]{border-left:3px solid var(--green);margin-left:-11px;padding-left:8px}.codex-set-prompt__name{text-align:left;flex:none}.codex-set-prompt__pos{text-align:right;font-variant-numeric:tabular-nums;width:18px;color:var(--faint);flex:none;font-size:11px}.codex-set-prompt__group{border-top:1px solid var(--border);margin-top:20px;padding-top:14px}.codex-set-prompt__group strong{font-size:var(--text-caption);text-transform:uppercase;letter-spacing:.05em;color:var(--faint)}.codex-set-prompt__row[data-layer-class=config-toggle] .codex-set-prompt__name{font-weight:500}.codex-set-prompt__bytes{text-align:right;font-variant-numeric:tabular-nums;min-width:48px;color:var(--faint);flex:none;font-size:11px}.codex-set-prompt__row[data-layer-class=config-toggle] .toggle,.codex-set-prompt__row[data-layer-class=base] .toggle{flex-shrink:0;margin-left:auto}.codex-set-prompt__key{text-overflow:ellipsis;white-space:nowrap;min-width:0;color:var(--muted);flex:auto;font-size:12px;overflow:hidden}.codex-set-prompt__row .codex-set-prompt__key{font-size:var(--text-caption);opacity:.6}.codex-set-prompt__note{color:var(--muted);white-space:nowrap;margin-left:auto;font-size:12px}.codex-set-prompt__extensions{margin-top:12px}.codex-set-layer-dialog{max-width:520px}.codex-set-layer-dialog__line{align-items:baseline;gap:8px;margin-top:8px;display:flex}.codex-set-layer-dialog__no-text{border-top:1px solid var(--border);margin-top:14px;padding-top:12px}.codex-set-layer-dialog__text.api-code{white-space:pre-wrap;overflow-wrap:anywhere;word-break:normal;max-height:320px;margin-top:8px;font-size:12px;line-height:1.5;overflow:hidden auto}.codex-set-custom{border-top:1px solid var(--border);margin-top:24px;padding-top:16px}.codex-set-custom__add{margin-left:auto}.codex-set-custom__reorder{gap:2px;margin-left:auto;display:inline-flex}.codex-set-custom__adopt{margin-top:12px}.codex-set-custom__adopt-preview{white-space:pre-wrap;word-break:break-word;max-height:200px;overflow:auto}.codex-set-custom__confirm{align-items:center;gap:8px;margin-top:12px;display:flex}.codex-set-custom-dialog{max-width:640px}.codex-set-custom-dialog .field{margin-top:12px;display:block}.codex-set-custom-dialog .field>span{margin-bottom:4px;display:block}.codex-set-custom-dialog .field>input{width:100%}.codex-set-custom-dialog textarea{width:100%;font-family:var(--mono,monospace);resize:vertical;font-size:13px}.codex-set-custom-dialog__lint{color:var(--muted);flex-direction:column;gap:6px;margin:12px 0 0;padding:0;font-size:12px;list-style:none;display:flex}.codex-set-custom-dialog__span{margin-left:6px}.codex-set-custom-dialog__discard{align-items:center;gap:8px}.codex-set-custom-dialog__nav{align-items:center;gap:4px;margin-left:auto;display:inline-flex}.codex-set-custom-dialog__nav-pos{text-align:center;font-variant-numeric:tabular-nums;min-width:44px;color:var(--muted);font-size:12px}.codex-set-prompt__drift{align-items:center;gap:10px;margin-top:12px;display:flex}.codex-set-prompt__drift button{flex:none;margin-left:auto}.codex-set-custom__adopt-refusal{margin-top:8px}.codex-set-preset{margin-left:auto;position:relative}.codex-set-preset>summary{cursor:pointer;list-style:none}.codex-set-preset>summary::-webkit-details-marker{display:none}.codex-set-preset__menu{z-index:10;border:1px solid var(--border);background:var(--bg);border-radius:8px;flex-direction:column;gap:2px;min-width:280px;max-width:380px;padding:6px;display:flex;position:absolute;right:0}.codex-set-preset__item{text-align:left;cursor:pointer;background:0 0;border:0;border-radius:6px;flex-direction:column;gap:2px;padding:8px;display:flex}.codex-set-preset__item:hover:not(:disabled){background:var(--bg-subtle,#7f7f7f14)}.codex-set-preset__desc,.codex-set-preset__provenance{color:var(--muted);font-size:12px}.codex-set-preset__preview{white-space:pre-wrap;max-height:120px;font-family:var(--mono,monospace);color:var(--muted);margin-top:4px;font-size:11px;overflow:auto}.codex-set-base-dialog__nav{align-items:center;gap:4px;margin-left:auto;display:inline-flex}.codex-set-base-dialog__pos{text-align:center;font-variant-numeric:tabular-nums;min-width:44px;color:var(--muted);font-size:12px}.codex-set-base-dialog__default{border:1px solid var(--border);background:var(--surface-2,transparent);border-radius:8px;flex-direction:column;gap:6px;padding:12px;display:flex}.codex-set-base-dialog{touch-action:pan-y}.codex-set-base-dialog__dots{justify-content:center;gap:6px;margin:8px 0 4px;display:flex}.codex-set-base-dialog__dot{border-radius:var(--radius-round);background:var(--border);width:6px;height:6px;transition:background .15s}.codex-set-base-dialog__dot.active{background:var(--green)}.main-inner:has(#models-panel-compatibility:not([hidden])){box-sizing:border-box;max-width:1200px}.lab-page{gap:var(--space-3);flex-direction:column;min-width:0;display:flex}.lab-page .page-head{margin-bottom:2px}.lab-page .page-sub{margin:0 0 .5rem}.lab-status-grid{gap:var(--space-2);grid-template-columns:repeat(auto-fit,minmax(9rem,1fr));display:grid}.lab-status-card{border:1px solid var(--border);border-radius:var(--radius);background:var(--surface);min-width:0;padding:.65rem .75rem}.lab-status-card .label{font-size:var(--text-label);color:var(--muted);margin-bottom:.2rem;display:block}.lab-status-card .value{font-variant-numeric:tabular-nums;font-weight:var(--weight-semibold)}.lab-filters{gap:var(--space-2);flex-wrap:wrap;align-items:flex-end;display:flex}.lab-filter-field{flex-direction:column;gap:.25rem;min-width:10rem;display:flex}.lab-filter-field label{font-size:var(--text-label);color:var(--muted)}.lab-filter-field input,.lab-filter-field select{border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--surface);min-height:2rem;color:var(--text);font:inherit;padding:.25rem .5rem}.lab-matrix-block{border:1px solid var(--border);border-radius:var(--radius);background:var(--surface);padding:.75rem}.lab-matrix-title{font-size:var(--text-control);font-weight:var(--weight-semibold);margin:0 0 .65rem}.lab-matrix-scroll{max-width:100%;overflow:auto}.lab-matrix{border-collapse:collapse;width:100%;font-size:var(--text-control)}.lab-matrix th,.lab-matrix td{border:1px solid var(--border-soft);text-align:left;vertical-align:top;padding:.45rem .55rem}.lab-matrix th{background:var(--raised);font-weight:var(--weight-semibold);white-space:nowrap}.lab-matrix td.subject{font-family:var(--font-code);font-size:var(--text-label);overflow-wrap:anywhere;max-width:14rem}.lab-matrix td.kind{color:var(--muted);font-size:var(--text-label);white-space:nowrap}.lab-verdict-stack{flex-direction:column;gap:.25rem;display:flex}.lab-verdict-badge{border-radius:var(--radius-pill);font:inherit;font-size:var(--text-label);font-weight:var(--weight-semibold);color:inherit;white-space:nowrap;border:1px solid #0000;align-items:center;gap:.35rem;padding:.1rem .45rem;display:inline-flex}button.lab-verdict-badge{appearance:none;cursor:pointer}.lab-verdict-badge .suite{font-weight:var(--weight-regular);color:var(--muted);font-family:var(--font-code)}.lab-verdict-badge[data-verdict=VERIFIED],.lab-verdict-badge[data-verdict=PROBED]{background:color-mix(in srgb, var(--green) 14%, transparent);border-color:color-mix(in srgb, var(--green) 35%, transparent)}.lab-verdict-badge[data-verdict=CLAIMED],.lab-verdict-badge[data-verdict=UNKNOWN]{background:color-mix(in srgb, var(--muted) 12%, transparent);border-color:color-mix(in srgb, var(--muted) 30%, transparent)}.lab-verdict-badge[data-verdict=DEGRADED]{background:#c47a0024;border-color:#c47a0059}.lab-verdict-badge[data-verdict=BLOCKED],.lab-verdict-badge[data-verdict=UNSUPPORTED]{background:color-mix(in srgb, var(--red) 12%, transparent);border-color:color-mix(in srgb, var(--red) 35%, transparent)}.lab-detail-table{border-collapse:collapse;width:100%;font-size:var(--text-control)}.lab-detail-table th,.lab-detail-table td{border-bottom:1px solid var(--border-soft);text-align:left;vertical-align:top;padding:.45rem .55rem}.lab-detail-table th{color:var(--muted);font-weight:var(--weight-semibold);font-size:var(--text-label)}.lab-detail-table td.mono{font-family:var(--font-code);font-size:var(--text-label);overflow-wrap:anywhere}.lab-toolbar{gap:var(--space-2);flex-wrap:wrap;justify-content:space-between;align-items:center;display:flex}.lab-toolbar .btn-ghost{align-items:center;gap:.35rem;display:inline-flex}.lab-layout{gap:var(--space-3);grid-template-columns:minmax(0,1fr) minmax(220px,320px);align-items:start;display:grid}.lab-main{min-width:0}.lab-detail-pane{border:1px solid var(--border);border-radius:var(--radius);padding:var(--space-3);background:var(--surface)}.lab-detail-head{justify-content:space-between;align-items:center;gap:var(--space-2);margin-bottom:var(--space-2);display:flex}.lab-detail-meta{gap:var(--space-1);margin:0 0 var(--space-3);display:grid}.lab-detail-meta dt{font-size:var(--text-label);color:var(--muted)}.lab-detail-section h4{margin:0 0 var(--space-1)}.lab-detail-list{gap:var(--space-1);margin:0;padding:0;list-style:none;display:grid}.lab-load-more{margin-top:var(--space-2);justify-content:center;display:flex}.lab-verdict-badge--selected{outline:2px solid var(--accent)}.lab-detail-table tbody tr.selected{background:var(--raised)}@media (width<=960px){.lab-layout{grid-template-columns:1fr}}@media (width<=720px){.lab-filters{flex-direction:column;align-items:stretch}.lab-filter-field{width:100%}}:root{--lightningcss-light:initial;--lightningcss-dark: ;color-scheme:light dark;--bg:var(--lightningcss-light,#fff)var(--lightningcss-dark,#212121);--rail:var(--lightningcss-light,#f9f9f9)var(--lightningcss-dark,#171717);--surface:var(--lightningcss-light,#fff)var(--lightningcss-dark,#262626);--raised:var(--lightningcss-light,#f4f4f4)var(--lightningcss-dark,#303030);--raised-hover:var(--lightningcss-light,#ececec)var(--lightningcss-dark,#3a3a3a);--border:var(--lightningcss-light,#e6e6e6)var(--lightningcss-dark,#3d3d3d);--border-soft:var(--lightningcss-light,#f0f0f0)var(--lightningcss-dark,#333);--hover:var(--lightningcss-light,#0d0d0d08)var(--lightningcss-dark,#ffffff08);--text:var(--lightningcss-light,#0d0d0d)var(--lightningcss-dark,#ececec);--muted:var(--lightningcss-light,#6e6e6e)var(--lightningcss-dark,#a6a6a6);--faint:var(--lightningcss-light,#707070)var(--lightningcss-dark,#9a9a9a);--accent:var(--lightningcss-light,#0d0d0d)var(--lightningcss-dark,#ececec);--accent-hover:var(--lightningcss-light,#3d3d3d)var(--lightningcss-dark,#fff);--accent-ink:var(--lightningcss-light,#fff)var(--lightningcss-dark,#0d0d0d);--accent-soft:var(--lightningcss-light,#0d0d0d0f)var(--lightningcss-dark,#ffffff17);--accent-ring:var(--lightningcss-light,#00000080)var(--lightningcss-dark,#ffffff61);--green:var(--lightningcss-light,#0a7d5c)var(--lightningcss-dark,#4ecb9d);--green-soft:var(--lightningcss-light,#10a37f1a)var(--lightningcss-dark,#4ecb9d21);--red:var(--lightningcss-light,#b91c1c)var(--lightningcss-dark,#f87171);--red-soft:var(--lightningcss-light,#b91c1c17)var(--lightningcss-dark,#f8717121);--amber:var(--lightningcss-light,#9a4a08)var(--lightningcss-dark,#fbbf24);--amber-soft:var(--lightningcss-light,#b453091a)var(--lightningcss-dark,#fbbf2421);--blue:var(--lightningcss-light,#1d4ed8)var(--lightningcss-dark,#7aa2ff);--blue-soft:var(--lightningcss-light,#1d4ed81a)var(--lightningcss-dark,#7aa2ff29);--space-0-5:2px;--space-1:4px;--space-1-5:6px;--space-2:8px;--space-3:12px;--space-4:16px;--space-5:20px;--space-6:24px;--space-8:32px;--space-10:40px;--space-12:48px;--space-16:64px;--prose-measure:70ch;--radius-2xs:4px;--radius:12px;--radius-sm:8px;--radius-xs:6px;--radius-lg:16px;--radius-round:50%;--radius-pill:999px;--font-ui:"OpenAI Sans", "Pretendard Variable", Pretendard, "Noto Sans KR", "Apple SD Gothic Neo", "Malgun Gothic", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, system-ui, sans-serif;--font-code:ui-monospace, "SFMono-Regular", "Cascadia Code", "JetBrains Mono", "Noto Sans Mono CJK KR", Menlo, Consolas, monospace;--font:var(--font-ui);--mono:var(--font-code);--text-micro:10px;--text-caption:11px;--text-label:12px;--text-control:13px;--text-body:14px;--text-subtitle:16px;--text-title:20px;--text-display:24px;--weight-regular:400;--weight-medium:500;--weight-semibold:600;--weight-bold:700;--leading-tight:1.2;--leading-ui:1.35;--leading-body:1.5;--leading-relaxed:1.6;--tracking-normal:0;--tracking-wide:.04em;--control-sm:28px;--control-md:34px;--control-lg:40px;--control-touch:44px;--icon-sm:14px;--icon-md:16px;--icon-lg:20px;--motion-fast:.12s;--motion-normal:.18s;--z-sticky:20;--z-overlay:30;--z-popover:40;--z-modal:50;--shadow:0 1px 2px var(--lightningcss-light,#1018280f)var(--lightningcss-dark,#00000080), 0 10px 28px var(--lightningcss-light,#10182812)var(--lightningcss-dark,#0000004d);--shadow-sm:0 1px 2px var(--lightningcss-light,#1018280f)var(--lightningcss-dark,#0006);--toggle-w:36px;--toggle-h:20px;--toggle-dot:14px;--toggle-off-bg:var(--lightningcss-light,#d4d4d4)var(--lightningcss-dark,#4a4a4a);--toggle-on-bg:var(--lightningcss-light,#0d0d0d)var(--lightningcss-dark,#4ecb9d);--toggle-dot-color:var(--lightningcss-light,#fff)var(--lightningcss-dark,#0d0d0d)}@media (prefers-color-scheme:dark){:root{--lightningcss-light: ;--lightningcss-dark:initial}}:root[data-theme=light]{--lightningcss-light:initial;--lightningcss-dark: ;color-scheme:light}:root[data-theme=dark]{--lightningcss-light: ;--lightningcss-dark:initial;color-scheme:dark}:root{--glass-rail:var(--lightningcss-light,#f9f9f9a8)var(--lightningcss-dark,#1717179e);--glass-panel:var(--lightningcss-light,#ffffffc7)var(--lightningcss-dark,#262626d1);--glass-blur:saturate(1.6) blur(22px)}*{box-sizing:border-box}html,body,#root{height:100%}html{background:var(--bg);overflow-x:hidden}body{color:var(--text);font-family:var(--font-ui);font-size:var(--text-body);line-height:var(--leading-body);-webkit-font-smoothing:antialiased;text-rendering:optimizelegibility;background:0 0;margin:0;overflow-x:hidden}body:before{content:"";z-index:-1;pointer-events:none;filter:blur(70px);background:radial-gradient(42% 38% at 12% 6%,var(--lightningcss-light,#a4c4ff6b)var(--lightningcss-dark,#6082c829),#0000 70%),radial-gradient(46% 42% at 88% 18%,var(--lightningcss-light,#a8e2c561)var(--lightningcss-dark,#58a08221),#0000 70%),radial-gradient(40% 36% at 70% 92%,var(--lightningcss-light,#ffe0c24d)var(--lightningcss-dark,#b48c6414),#0000 72%);position:fixed;inset:-20%}a{color:var(--text);text-decoration:underline;-webkit-text-decoration-color:var(--faint);text-decoration-color:var(--faint);text-underline-offset:2px}a:hover{-webkit-text-decoration-color:var(--text);text-decoration-color:var(--text)}code{font-family:var(--font-code);font-size:var(--text-label)}.mono{font-family:var(--font-code);font-variant-numeric:tabular-nums}.model-label{align-items:center;gap:var(--space-2);display:inline-flex}h1,h2,h3,h4{font-weight:var(--weight-semibold);letter-spacing:0;line-height:var(--leading-tight);margin:0}.text-micro{line-height:var(--leading-ui);font-size:var(--text-micro)!important}.text-caption{line-height:var(--leading-ui);font-size:var(--text-caption)!important}.text-label{line-height:var(--leading-ui);font-size:var(--text-label)!important}.text-control{line-height:var(--leading-ui);font-size:var(--text-control)!important}p.muted.text-label,p.muted.text-control{max-width:var(--prose-measure)}.text-body{line-height:var(--leading-body);font-size:var(--text-body)!important}.text-subtitle{line-height:var(--leading-tight);font-size:var(--text-subtitle)!important}.text-title{line-height:var(--leading-tight);font-size:var(--text-title)!important}.text-display{line-height:var(--leading-tight);font-size:var(--text-display)!important}.font-regular{font-weight:var(--weight-regular)!important}.font-medium{font-weight:var(--weight-medium)!important}.font-semibold{font-weight:var(--weight-semibold)!important}.font-bold{font-weight:var(--weight-bold)!important}.leading-tight{line-height:var(--leading-tight)!important}.leading-ui{line-height:var(--leading-ui)!important}.leading-body{line-height:var(--leading-body)!important}.leading-relaxed{line-height:var(--leading-relaxed)!important}::selection{background:var(--accent-soft)}input[type=checkbox],input[type=radio]{accent-color:var(--accent)}::-webkit-scrollbar{width:10px;height:10px}::-webkit-scrollbar-thumb{background:var(--border);border-radius:var(--radius-pill);border:2px solid var(--bg)}::-webkit-scrollbar-thumb:hover{background:var(--faint)}:focus-visible{outline:2px solid var(--accent-ring);outline-offset:2px;border-radius:var(--radius-2xs)}.app{grid-template-columns:232px 1fr;min-height:100dvh;display:grid}.sidebar{height:100dvh;z-index:var(--z-overlay);border-right:1px solid var(--border);background:var(--glass-rail);-webkit-backdrop-filter:var(--glass-blur);flex-direction:column;align-self:start;gap:4px;padding:18px 14px;display:flex;position:sticky;top:0}.brand{align-items:center;gap:10px;padding:6px 8px 14px;display:flex}.brand-logo{background:var(--text);flex-shrink:0;width:26px;height:26px;-webkit-mask:url(/logo.png) 50%/contain no-repeat;mask:url(/logo.png) 50%/contain no-repeat}.brand .name{font-weight:var(--weight-semibold);font-size:var(--text-subtitle);letter-spacing:0;line-height:26px}.brand .ver{font-family:var(--font-code);font-size:var(--text-micro);color:var(--muted);line-height:var(--leading-tight);background:var(--raised);border:1px solid var(--border);border-radius:var(--radius-pill);align-self:center;padding:2px 6px}.sidebar nav{flex-direction:column;gap:4px;display:flex}.nav-item{border-radius:var(--radius-sm);text-align:left;cursor:pointer;width:100%;color:var(--muted);font:inherit;font-size:var(--text-control);font-weight:var(--weight-medium);transition:background var(--motion-fast), color var(--motion-fast);background:0 0;border:none;align-items:center;gap:10px;padding:8px 10px;display:flex}.nav-item:hover,.nav-item.active{background:var(--accent-soft);color:var(--text)}.nav-item.active{font-weight:var(--weight-semibold)}.nav-item svg{width:17px;height:17px;color:var(--faint);flex-shrink:0}.nav-item.active svg{color:var(--text)}.nav-entry{border-radius:var(--radius-sm);align-items:center;min-width:0;display:flex}.nav-entry .nav-item{flex:auto;min-width:0}.nav-entry-claude{transition:background var(--motion-fast), color var(--motion-fast);padding-right:4px}.nav-entry-claude:hover,.nav-entry-claude.active{background:var(--accent-soft);color:var(--text)}.nav-entry-claude .nav-item,.nav-entry-claude .nav-item:hover,.nav-entry-claude .nav-item.active{background:0 0;width:auto}.sidebar-foot{flex-direction:column;gap:2px;margin-top:auto;padding-top:12px;display:flex}.drawer-head{align-items:flex-start;display:flex}.drawer-head .brand{flex:auto;min-width:0}.mobile-topbar{display:none}.menu-toggle{border-radius:var(--radius-sm);min-width:44px;min-height:44px;color:var(--muted);cursor:pointer;transition:background var(--motion-fast), color var(--motion-fast);background:0 0;border:none;justify-content:center;align-items:center;padding:8px;display:none}.menu-toggle:hover{background:var(--accent-soft);color:var(--text)}.menu-toggle svg{width:20px;height:20px}.drawer-scrim{z-index:var(--z-overlay);background:var(--lightningcss-light,#14141452)var(--lightningcss-dark,#00000085);display:none;position:fixed;inset:0}.sidebar-link{color:var(--muted);font-size:var(--text-control);border-radius:var(--radius-sm);align-items:center;gap:9px;padding:8px 10px;text-decoration:none;display:flex}.sidebar-link:hover{background:var(--accent-soft);color:var(--text);text-decoration:none}.sidebar-link svg{width:16px;height:16px}.sidebar-github-row{align-items:center;gap:4px;min-width:0;padding-right:10px;display:flex}.sidebar-github-link{flex:auto;min-width:0}.sidebar-github-actions{flex:none;align-items:center;gap:4px;display:flex}.sidebar-orb{border:1px solid var(--border);border-radius:var(--radius-pill);background:var(--raised);width:28px;height:28px;color:var(--muted);cursor:pointer;transition:background var(--motion-fast), color var(--motion-fast), border-color var(--motion-fast);flex:0 0 28px;justify-content:center;align-items:center;padding:0;display:inline-flex;position:relative}.sidebar-orb svg{width:14px;height:14px}.sidebar-orb:hover:not(:disabled){background:var(--accent-soft);color:var(--text);border-color:var(--accent-ring)}.sidebar-orb:focus-visible{outline:2px solid var(--accent-ring);outline-offset:2px}.sidebar-orb--starred{color:var(--amber);cursor:default;opacity:1}.sidebar-orb--update{color:var(--blue);border-color:var(--blue);background:var(--blue-soft)}.sidebar-orb--update:hover:not(:disabled){color:var(--blue);border-color:var(--blue);background:var(--blue-soft);filter:brightness(1.06)}.sidebar-orb-dot{background:var(--blue);border:1.5px solid var(--rail);border-radius:50%;width:7px;height:7px;position:absolute;top:1px;right:1px}.lang-toggle{width:100%;color:var(--muted);font-size:var(--text-control);border-radius:var(--radius-sm);transition:background var(--motion-fast), color var(--motion-fast);align-items:center;gap:9px;padding:8px 10px;display:flex;position:relative}.lang-toggle:hover{background:var(--accent-soft);color:var(--text)}.lang-toggle svg{flex-shrink:0;width:16px;height:16px}.lang-toggle .custom-select{width:100%;position:static!important}.lang-toggle .select-trigger{width:100%;color:inherit;font-size:var(--text-control);min-height:auto;box-shadow:none;-webkit-backdrop-filter:none;background:0 0;border:none;justify-content:space-between;padding:0}.lang-toggle .select-trigger:hover:not(:disabled){color:inherit;box-shadow:none;background:0 0;border:none}.lang-toggle .select-dropdown{background:var(--glass-rail);-webkit-backdrop-filter:var(--glass-blur);border:1px solid var(--border);box-shadow:var(--shadow-sm)}.lang-toggle .select-dropdown-beside{min-width:10rem;max-height:min(60vh,20rem);inset:auto auto 0 calc(100% + 20px);overflow-y:auto}.theme-toggle{text-align:left;cursor:pointer;width:100%;color:var(--muted);font:inherit;font-size:var(--text-control);border-radius:var(--radius-sm);transition:background var(--motion-fast), color var(--motion-fast);background:0 0;border:none;align-items:center;gap:9px;padding:8px 10px;display:flex}.theme-toggle:hover{background:var(--accent-soft);color:var(--text)}.theme-toggle svg{flex-shrink:0;width:16px;height:16px}.theme-toggle .mode{text-transform:capitalize}.stop-toggle{color:var(--red)}.stop-toggle:hover{background:var(--red-soft);color:var(--red)}.stop-toggle:disabled{opacity:.5;cursor:default}.sidebar-action-row{align-items:center;gap:4px;min-width:0;padding-right:10px;display:flex}.sidebar-action-label{min-width:0;color:var(--muted);font-size:var(--text-control);flex:auto;padding:8px 10px 8px 35px}.sidebar-action-orbs{flex:none;align-items:center;gap:4px;display:flex}.sidebar-orb--danger{color:var(--red)}.sidebar-orb--danger:hover:not(:disabled){background:var(--red-soft);color:var(--red);border-color:var(--red)}.sidebar-orb:disabled{opacity:.5;cursor:default}.main{min-width:0}.main-inner{max-width:980px;margin:0 auto;padding:32px 36px 64px;container-type:inline-size}.main-inner.main-inner--combos{flex-direction:column;max-width:none;height:100dvh;min-height:100dvh;margin:0;padding:0;display:flex;overflow:hidden}.main-inner.main-inner--combos>.models-tab-panel--fill:not([hidden]),.main-inner.main-inner--combos>.models-tab-panel--fill:not([hidden])>.combos-workspace-shell{flex-direction:column;flex:auto;height:100%;min-height:0;display:flex}.main-inner.main-inner--combos>.page-head,.main-inner.main-inner--combos>.page-tabs,.main-inner.main-inner--combos>.codex-stale-banner,.main-inner.main-inner--combos>.page-sub{flex-shrink:0;padding-inline:36px}.main-inner.main-inner--combos>.page-sub{margin-bottom:10px}.main-inner.main-inner--combos>.page-tabs{margin-inline:36px;padding-inline:0}.main-inner.main-inner--combos:not(:has(.combos-workspace-shell)){max-width:1200px;height:auto;min-height:0;margin:0 auto;padding:32px 0 64px;display:block;overflow:visible}.main-inner.main-inner--combos:not(:has(.combos-workspace-shell))>.models-tab-panel--fill:not([hidden]){flex:0 auto;height:auto;padding-inline:36px;display:block}.page-head{justify-content:space-between;align-items:center;gap:16px;margin-bottom:6px;display:flex}.page-head h2{font-size:var(--text-title)}.page-head-actions{flex:none;align-items:center;gap:6px;display:flex}.codex-stale-banner{border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--raised);color:var(--text);align-items:center;gap:10px;margin:8px 0 4px;padding:10px 12px;display:flex}.codex-stale-banner-text{min-width:0;font-size:var(--text-control);flex:auto}.page-sub{color:var(--muted);font-size:var(--text-body);max-width:var(--prose-measure);margin:4px 0 22px}.page-tabs{border-bottom:1px solid var(--border);flex-wrap:wrap;gap:2px;margin:2px 0 14px;display:flex;overflow:visible}.page-tab{white-space:nowrap;appearance:none;color:var(--muted);cursor:pointer;font:inherit;font-size:var(--text-control);background:0 0;border:none;border-bottom:2px solid #0000;flex:none;margin-bottom:-1px;padding:8px 12px}.page-tab:hover{color:var(--text)}.page-tab--active{color:var(--text);border-bottom-color:var(--accent);font-weight:var(--weight-semibold)}.page-tab:focus-visible{outline:2px solid var(--accent-ring);outline-offset:-2px}.section-tabs{z-index:3;background:var(--bg);margin-top:0;padding-top:6px;position:sticky;top:0}.section-tab-meta{color:var(--muted);font-size:var(--text-label);font-weight:var(--weight-regular);font-variant-numeric:tabular-nums;margin-left:6px}.page-tab--active>.section-tab-meta{color:var(--text)}[id*=-section-]{scroll-margin-top:56px}.page-sub b{color:var(--text);font-weight:var(--weight-semibold)}.api-page h2 svg{vertical-align:-.16em;width:1em;height:1em}.api-page .page-sub code{font-size:var(--text-label);color:var(--text);background:var(--raised);border:1px solid var(--border-soft);border-radius:var(--radius-xs);padding:1px 4px}.api-endpoints{grid-template-columns:repeat(2,minmax(0,1fr));gap:8px 12px;margin-top:8px;display:grid}.api-endpoints>div{flex-direction:column;align-items:stretch;gap:4px;min-width:0;display:flex}.api-endpoints>div>.muted{flex:none;line-height:1.3}.api-endpoints .ocx-tooltip,.api-endpoints .api-endpoint-url-btn{text-align:left;z-index:1;appearance:none;width:100%;max-width:100%;color:inherit;font:inherit;cursor:pointer;background:0 0;border:0;margin:0;padding:0;position:relative;display:block!important}.api-endpoints .ocx-tooltip:focus-within,.api-endpoints .ocx-tooltip:hover,.api-endpoints .api-endpoint-url-btn:focus-within,.api-endpoints .api-endpoint-url-btn:hover{z-index:5}.api-endpoints .api-endpoint-url{box-sizing:border-box;white-space:nowrap;text-overflow:ellipsis;pointer-events:none;width:100%;max-width:100%;display:block;overflow:hidden}.api-endpoints .api-endpoint-url-btn:hover .api-endpoint-url,.api-endpoints .api-endpoint-url-btn:focus-visible .api-endpoint-url{border-color:color-mix(in srgb, var(--accent) 40%, var(--border))}.api-example-copy-btn,.ocx-tooltip.api-example-copy-btn{appearance:none;width:100%;max-width:100%;color:inherit;font:inherit;text-align:left;cursor:pointer;z-index:1;background:0 0;border:0;align-items:stretch;margin:0;padding:0;display:block;position:relative}.api-example-copy-btn:hover,.api-example-copy-btn:focus-within,.ocx-tooltip.api-example-copy-btn:hover,.ocx-tooltip.api-example-copy-btn:focus-within{z-index:5}.api-example-copy-btn .api-example-pre{box-sizing:border-box;pointer-events:auto;width:100%;max-width:100%;margin:0;display:block}.api-example-copy-btn:hover .api-example-pre,.api-example-copy-btn:focus-visible .api-example-pre{border-color:color-mix(in srgb, var(--accent) 40%, var(--border))}.ocx-tooltip-bubble.api-copy-tip-fixed{z-index:var(--z-popover,40);pointer-events:none;white-space:nowrap;width:max-content;max-width:min(320px,90vw);position:fixed;inset:0 auto auto 0;transform:translate(-50%,-100%)}@media (width<=720px){.api-endpoints{grid-template-columns:minmax(0,1fr)}}.api-auth-list{gap:8px;margin:0;padding-left:1.1rem;display:grid}.api-auth-list li{color:var(--muted);font-size:var(--text-label)}.api-model-actions{align-items:center;gap:6px;display:inline-flex}.api-test-note{font-size:var(--text-label)}.api-test-note--ok{color:var(--green,#22c55e)}.api-test-note--error{color:var(--red)}.btn{border-radius:var(--radius-pill);font:inherit;font-size:var(--text-control);font-weight:var(--weight-medium);line-height:var(--leading-ui);cursor:pointer;transition:background var(--motion-fast), border-color var(--motion-fast), opacity var(--motion-fast);white-space:nowrap;border:1px solid #0000;justify-content:center;align-items:center;gap:7px;padding:8px 16px;display:inline-flex}a.btn,a.btn:hover{text-decoration:none}.btn svg{width:15px;height:15px}.btn:disabled{opacity:.55;cursor:default}.btn-primary{background:var(--accent);color:var(--accent-ink)}.btn-primary:hover:not(:disabled){background:var(--accent-hover)}.btn-ghost{background:var(--bg);color:var(--text);border-color:var(--border)}.btn-ghost:hover:not(:disabled){background:var(--raised)}.btn-danger{color:var(--red);background:0 0;border-color:#f871714d}.btn-danger:hover:not(:disabled){background:var(--red-soft)}.btn-sm{font-size:var(--text-label);border-radius:var(--radius-pill);padding:4px 12px}.btn-icon{appearance:none;font:inherit;cursor:pointer;width:28px;height:28px;color:var(--muted);border-radius:var(--radius-sm);transition:background var(--motion-fast), color var(--motion-fast);background:0 0;border:none;justify-content:center;align-items:center;padding:0;display:inline-flex}.btn-icon:hover{background:var(--raised-hover);color:var(--text)}.btn-icon:focus-visible{outline:2px solid var(--accent-ring);outline-offset:1px}.btn-icon svg{width:16px;height:16px;display:block}.btn.btn-ghost.btn-icon{width:28px;height:28px;padding:0}.card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);min-width:0}.panel{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:18px}.panel-accent{border-color:color-mix(in srgb, var(--accent) 28%, var(--border));background:color-mix(in srgb, var(--accent) 5%, var(--surface))}.api-panel{flex-direction:column;gap:10px;display:flex;overflow:hidden}.api-panel .panel-title,.api-panel .muted{margin:0}.api-panel-head{justify-content:space-between;align-items:baseline;gap:12px;min-width:0;display:flex}.api-panel-head .panel-title{min-width:0}.api-panel-head .muted{text-align:right;flex:none}.api-form-row{align-items:center;gap:8px;min-width:0;display:flex}.api-form-row .input{flex:1;min-width:0}.api-code{min-width:0;color:var(--text);background:var(--raised);border:1px solid var(--border-soft);border-radius:var(--radius-sm);font-family:var(--font-code);font-size:var(--text-label);line-height:var(--leading-relaxed);white-space:pre;margin:0;padding:9px 11px;display:block;overflow-x:auto}.api-code-inline{white-space:nowrap}.api-actions{justify-content:flex-end;align-items:center;gap:4px;display:flex}.dash-sync-summary{justify-content:space-between;align-items:center;gap:16px;display:flex}.dash-sync-copy{flex:auto;min-width:0}.dash-sync-hint{-webkit-line-clamp:2;-webkit-box-orient:vertical;margin:2px 0 0;display:-webkit-box;overflow:hidden}.maintenance-actions{flex:none;justify-content:flex-end;align-items:center;gap:8px;display:flex}.maintenance-update-anchor{background:0 0;border:0;width:0;height:0;padding:0;overflow:hidden}.maintenance-notice{align-items:flex-start;margin:14px 0 0}.action-toast{right:var(--space-6);bottom:var(--space-6);z-index:var(--z-modal);max-width:min(480px,100vw - 48px);box-shadow:var(--shadow);animation:sync-toast-in var(--motion-normal) ease-out;align-items:flex-start;margin:0;position:fixed}@keyframes sync-toast-in{0%{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}.spin-icon{animation:.9s linear infinite spin}.action-toast.notice{max-width:min(480px,100vw - 48px)}.action-toast-dismiss{border-radius:var(--radius-sm);min-width:24px;min-height:24px;color:var(--muted);cursor:pointer;transition:color var(--motion-fast), background var(--motion-fast);background:0 0;border:none;flex:none;justify-content:center;align-self:flex-start;align-items:center;margin:-4px -6px 0 2px;padding:4px;display:inline-flex}.action-toast-dismiss:hover{color:var(--text);background:color-mix(in srgb, var(--muted) 14%, transparent)}.action-toast-dismiss:focus-visible{outline:2px solid var(--accent);outline-offset:1px}.update-row{justify-content:space-between;align-items:center;gap:12px;margin-bottom:14px;display:flex}.update-row .field-label{margin:0}.update-empty{margin-bottom:14px;padding:18px}.update-box{flex-direction:column;gap:12px;display:flex}.update-command{font-size:var(--text-label);flex-wrap:wrap;align-items:flex-start;gap:6px;display:flex}.update-command .chip{white-space:pre-wrap;word-break:break-all;overflow-wrap:anywhere;flex:auto;min-width:0;max-width:100%;line-height:1.5}.update-recheck{flex-wrap:wrap;align-items:center;gap:10px;display:flex}.update-recheck .btn{flex:none}.update-recheck-reason{min-width:0;font-size:var(--text-label);color:var(--muted);flex:200px}.update-restart{border-top:1px solid var(--border-soft);padding-top:12px}.injection-head{flex-wrap:wrap;align-items:center;gap:10px;display:flex}.injection-label{font-weight:var(--weight-semibold)}@media (width<=800px){.injection-head{gap:8px}.injection-label{flex:1 0 calc(100% - 70px);order:1}.injection-head>.badge{order:1;margin-left:auto}.injection-head>.custom-select{order:2}}.stat-row{grid-template-columns:repeat(6,1fr);gap:12px;margin-bottom:28px;display:grid}.stat-row>.stat{flex-direction:column;justify-content:center;min-height:80px;display:flex}@container (width<=820px){.stat-row{grid-template-columns:repeat(3,1fr)}}@container (width<=480px){.stat-row{grid-template-columns:repeat(2,1fr)}}.stat{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);transition:border-color var(--motion-fast);padding:14px 16px}.stat:hover{border-color:var(--faint)}.stat .label{font-size:var(--text-label);color:var(--muted);font-weight:var(--weight-medium);align-items:center;gap:6px;margin-bottom:6px;display:flex}.stat .label svg{width:14px;height:14px}.stat .value{font-size:var(--text-title);font-weight:var(--weight-semibold);letter-spacing:0;line-height:var(--leading-tight)}.stat .value.mono{font-family:var(--font-code);font-size:var(--text-subtitle)}.startup-health-bar{box-sizing:border-box;width:100%;min-height:var(--control-lg);margin:calc(-1 * var(--space-3)) 0 var(--space-6);padding:0 var(--space-3);align-items:center;gap:var(--space-2);min-width:0;color:var(--muted);background:var(--hover);border:none;border-block:1px solid var(--border-soft);font:inherit;font-size:var(--text-control);line-height:var(--leading-ui);text-align:start;cursor:pointer;transition:background var(--motion-fast), color var(--motion-fast);text-decoration:none;display:flex}.startup-health-bar:hover{background:var(--raised);color:var(--text)}.startup-health-bar:active{transform:translateY(1px)}.startup-health-bar:focus-visible{outline:2px solid var(--accent-ring);outline-offset:2px}.startup-health-bar__summary{text-overflow:ellipsis;white-space:nowrap;min-width:0;overflow:hidden}.startup-health-slot{min-height:var(--control-lg)}.startup-health-bar--pending{pointer-events:none;color:var(--faint)}.dash-stat-coverage{min-height:1.25em;margin-top:2px;line-height:1.25}.dash-overview-head{flex-direction:column;margin-bottom:0;display:flex}.mem-head{flex-wrap:wrap;justify-content:space-between;align-items:center;gap:12px;margin-bottom:12px;display:flex}.mem-head-title{align-items:center;gap:8px;display:flex}.mem-head-actions{flex-wrap:wrap;align-items:center;gap:10px;display:flex}.mem-inflight{align-items:baseline;gap:6px;display:inline-flex}.mem-inflight-label{font-size:var(--text-label);color:var(--muted)}.mem-inflight-value{font-size:var(--text-control);font-weight:var(--weight-semibold)}.mem-status{flex-wrap:wrap;align-items:center;gap:10px;margin-top:10px;display:flex}.mem-status:empty{display:none}.mem-stats{grid-template-columns:repeat(4,minmax(0,1fr));margin-bottom:0}@container (width<=660px){.mem-stats{grid-template-columns:repeat(2,minmax(0,1fr))}}@container (width<=380px){.mem-stats{grid-template-columns:minmax(0,1fr)}}.mem-stats .stat-sub{font-size:var(--text-caption);color:var(--faint);margin-top:4px}.stat .value--warn{color:var(--amber)}.stat .value--danger{color:var(--red)}.mem-pressure{border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--raised);margin-bottom:12px;padding:12px 14px}.mem-pressure--warn{border-color:color-mix(in srgb, var(--amber) 42%, var(--border));background:var(--amber-soft)}.mem-pressure--over{border-color:color-mix(in srgb, var(--red) 42%, var(--border));background:var(--red-soft)}.mem-pressure-head{flex-wrap:wrap;justify-content:space-between;align-items:baseline;gap:12px;display:flex}.mem-pressure-label{font-size:var(--text-label);font-weight:var(--weight-medium);color:var(--muted);align-items:baseline;gap:6px;display:inline-flex}.mem-pressure-metric{font-size:var(--text-caption);color:var(--faint)}.mem-pressure-figure{font-size:var(--text-subtitle);font-weight:var(--weight-semibold);line-height:var(--leading-tight);white-space:nowrap}.mem-pressure-limit{color:var(--faint);font-weight:var(--weight-regular)}.mem-pressure-track{border-radius:var(--radius-2xs);background:color-mix(in srgb, var(--muted) 20%, transparent);height:5px;margin:8px 0 6px;overflow:hidden}.mem-pressure-fill{border-radius:var(--radius-2xs);background:var(--green);width:100%;height:100%;transform:scaleX(var(--mem-scale,0));transform-origin:0;transition:transform var(--motion-normal);display:block}.mem-pressure--warn .mem-pressure-fill{background:var(--amber)}.mem-pressure--over .mem-pressure-fill{background:var(--red)}.mem-pressure-foot{font-size:var(--text-caption);color:var(--muted)}.dash-overview-head .stat-row{margin-bottom:0}.dash-overview-head .startup-health-bar,.dash-overview-head .startup-health-bar--pending{margin:var(--space-4) 0 0}.model-group-head{font-size:var(--text-control);font-weight:var(--weight-semibold);color:var(--text);align-items:baseline;gap:8px;margin:0 0 8px;display:flex}.model-group-head .count{font-family:var(--font-code);font-weight:var(--weight-medium);color:var(--faint);font-size:var(--text-label)}.group-head{cursor:pointer;background:var(--surface);transition:background var(--motion-fast);padding:10px 12px}.group-head:hover{background:var(--hover)}.group-head.open{border-bottom:1px solid var(--border-soft)}.models-combos-card{overflow:hidden}.models-combo-row{min-height:var(--control-lg);padding:0 var(--space-3) 0 calc(var(--space-8) + var(--space-0-5));gap:var(--space-2);align-items:center}.model-grid{grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:8px;display:grid}.model-card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-sm);transition:border-color var(--motion-fast), background var(--motion-fast);padding:10px 12px}.model-card:hover{border-color:var(--faint);background:var(--hover)}.model-card .id{font-family:var(--font-code);font-weight:var(--weight-semibold);font-size:var(--text-control);letter-spacing:0;color:var(--text)}.badge{font-size:var(--text-caption);font-weight:var(--weight-semibold);line-height:var(--leading-ui);border-radius:var(--radius-pill);font-family:var(--font-code);letter-spacing:0;border:1px solid #0000;align-items:center;gap:5px;padding:2px 8px;display:inline-flex}.badge-accent{background:var(--accent-soft);color:var(--text)}.badge-green{background:var(--green-soft);color:var(--green)}.badge-amber{background:var(--amber-soft);color:var(--amber)}.badge-muted{background:var(--raised);color:var(--muted);border:1px solid var(--border)}.badge-clickable{cursor:pointer;transition:filter var(--motion-fast);appearance:none}.badge-clickable:hover{filter:brightness(1.1)}.badge-disabled{opacity:.5;cursor:default}.card-badges{flex-wrap:wrap;align-items:center;gap:8px;min-width:0;display:inline-flex}.card-badges .badge{flex-shrink:0}.confirm-icon{border-radius:var(--radius-round);background:var(--amber-soft);width:44px;height:44px;color:var(--amber);justify-content:center;align-items:center;margin:0 auto 12px;display:flex}.credit-list{flex-direction:column;gap:6px;display:flex}.credit-item{border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--raised);transition:border-color var(--motion-fast);padding:8px 10px}.credit-next{border-color:var(--amber);background:var(--amber-soft)}.credit-item-head{color:var(--text);font-size:var(--text-label);font-weight:var(--weight-semibold);align-items:center;gap:6px;margin-bottom:3px;display:flex}.credit-item-head svg{color:var(--amber);flex-shrink:0}.credit-item-dates{font-size:var(--text-caption);font-family:var(--font-code);color:var(--muted);justify-content:space-between;display:flex}.credit-urgent{color:var(--red);font-weight:var(--weight-semibold)}.dot{border-radius:var(--radius-round);flex-shrink:0;width:7px;height:7px}.dot-green{background:var(--green);box-shadow:0 0 0 3px var(--green-soft)}.dot-red{background:var(--red);box-shadow:0 0 0 3px var(--red-soft)}.tbl{border-collapse:collapse;width:100%;font-size:var(--text-control)}.tbl thead th{text-align:left;color:var(--muted);font-weight:var(--weight-medium);font-size:var(--text-label);border-bottom:1px solid var(--border);padding:9px 12px}.tbl tbody td{border-bottom:1px solid var(--border-soft);padding:10px 12px}.tbl tbody tr:last-child td{border-bottom:none}.tbl tbody tr:hover td{background:var(--hover)}.checkbox{cursor:pointer;align-items:center;gap:8px;display:flex}.tbl .num{text-align:right;font-family:var(--font-code);font-variant-numeric:tabular-nums}.tbl-wrap{border:1px solid var(--border);border-radius:var(--radius);background:var(--surface);padding:var(--space-3);overflow-x:auto}.api-models-scroll{overscroll-behavior:contain;min-height:0;max-height:min(360px,50vh);margin-top:.75rem;overflow:auto}.api-models-scroll thead th{z-index:1;background:var(--panel,var(--raised));position:sticky;top:0}.api-models-panel{min-height:0}.awi-overview-section .api-models-panel>.input,.awi-overview-section .api-models-panel>.api-panel-head,.awi-overview-section .api-models-panel>.muted{flex:none}.input,textarea.input{border-radius:var(--radius-sm);background:var(--raised);border:1px solid var(--border);width:100%;color:var(--text);font:inherit;font-size:var(--text-control);line-height:var(--leading-ui);transition:border-color var(--motion-fast);padding:8px 11px}.input::placeholder{color:var(--faint)}.input:focus{border-color:var(--faint);box-shadow:0 0 0 3px var(--accent-soft);outline:none}textarea.input{resize:vertical;font-family:var(--font-code);line-height:var(--leading-relaxed)}.field-label{font-size:var(--text-label);color:var(--muted);font-weight:var(--weight-medium);margin-bottom:5px;display:block}select.input{appearance:none}.select-sm{border-radius:var(--radius-pill);background:color-mix(in oklab, canvas 75%, transparent);-webkit-backdrop-filter:blur(12px)saturate(1.3);border:1px solid var(--border);color:var(--text);font:inherit;font-size:var(--text-control);line-height:var(--leading-ui);cursor:pointer;transition:border-color var(--motion-fast), box-shadow var(--motion-fast);padding:6px 12px;box-shadow:0 2px 8px #0000000f}.select-sm:focus{border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-soft);outline:none}.select-sm:hover:not(:disabled){border-color:var(--faint);box-shadow:0 2px 12px #0000001a}.select-sm:disabled{opacity:.5;cursor:default;-webkit-backdrop-filter:none}.select-trigger{border-radius:var(--radius-pill);background:color-mix(in oklab, canvas 75%, transparent);-webkit-backdrop-filter:blur(12px)saturate(1.3);border:1px solid var(--border);color:var(--text);font:inherit;font-size:var(--text-control);line-height:var(--leading-ui);cursor:pointer;transition:border-color var(--motion-fast), box-shadow var(--motion-fast);white-space:nowrap;align-items:center;gap:6px;padding:6px 12px;display:inline-flex;box-shadow:0 2px 8px #0000000f}.select-trigger:hover:not(:disabled){border-color:var(--faint);box-shadow:0 2px 12px #0000001a}.select-trigger:disabled{opacity:.5;cursor:default}.select-dropdown{z-index:var(--z-popover);background:var(--surface);-webkit-backdrop-filter:blur(20px)saturate(1.4);border:1px solid var(--border);border-radius:var(--radius);min-width:100%;max-height:280px;padding:4px;position:absolute;top:calc(100% + 4px);left:0;overflow-y:auto;box-shadow:0 8px 32px #00000029}.select-dropdown-portal{position:fixed;inset:auto}.select-dropdown-right{right:0;left:auto!important}.select-dropdown-beside{min-width:10rem;max-height:min(60vh,20rem);inset:auto auto 0 calc(100% + 12px);overflow-y:auto}.select-option{text-align:left;border-radius:var(--radius-sm);width:100%;color:var(--text);font:inherit;font-size:var(--text-control);line-height:var(--leading-ui);cursor:pointer;transition:background var(--motion-fast), box-shadow var(--motion-fast);white-space:nowrap;background:0 0;border:none;padding:7px 12px;display:block}.select-option:hover:not(.select-option-active){background:var(--hover)}.select-option.active{background:var(--accent-soft);color:var(--accent);font-weight:var(--weight-semibold)}.select-option-active{background:var(--hover)}.select-option.active.select-option-active{background:var(--accent-soft);box-shadow:inset 0 0 0 1px var(--accent)}.switch{border-radius:var(--radius-pill);border:1px solid var(--border);cursor:pointer;background:var(--raised);vertical-align:middle;width:34px;height:20px;transition:background var(--motion-normal), border-color var(--motion-normal);appearance:none;flex-shrink:0;align-items:center;padding:2px;line-height:0;display:inline-flex}.switch.on{background:var(--toggle-on-bg);border-color:var(--toggle-on-bg)}.switch.mixed{background:var(--amber-soft);border-color:var(--amber)}.switch:disabled{opacity:.6;cursor:default}.switch.switch-labeled{border-radius:var(--radius-sm);gap:var(--space-1);background:0 0;border:0;flex:none;width:auto;height:auto;padding:0;line-height:normal}.switch.switch-labeled:before{content:"";border-radius:var(--radius-pill);border:1px solid var(--border);background:var(--raised);box-sizing:border-box;width:34px;height:20px;transition:background var(--motion-normal), border-color var(--motion-normal);flex-shrink:0}.switch.switch-labeled.on:before{background:var(--toggle-on-bg);border-color:var(--toggle-on-bg)}.switch.switch-labeled.mixed:before{background:var(--amber-soft);border-color:var(--amber)}.switch.switch-labeled{position:relative}.switch.switch-labeled .knob{position:absolute;left:3px}.switch.switch-labeled.on .knob{transform:translate(14px)}.switch.switch-labeled.mixed .knob{transform:translate(7px)}.switch-labeled-text{white-space:nowrap}.switch .knob{border-radius:var(--radius-round);width:14px;height:14px;transition:transform var(--motion-normal), background var(--motion-normal);background:var(--lightningcss-light,#fff)var(--lightningcss-dark,#ececec);transform:translate(0);box-shadow:0 0 0 1px #10182814,0 1px 1px #1018282e}.switch.on .knob{background:var(--toggle-dot-color);transform:translate(14px);box-shadow:0 0 0 1px #0000001f}.switch.mixed .knob{transform:translate(7px)}.muted{color:var(--muted)}.faint{color:var(--faint)}.row{align-items:center;gap:10px;display:flex}.spread{justify-content:space-between;align-items:center;gap:12px;display:flex}.setting-hint{font-size:var(--text-control);line-height:var(--leading-body);max-width:var(--prose-measure);margin-top:3px}.stack{flex-direction:column;display:flex}.chip{font-family:var(--font-code);font-size:var(--text-label);line-height:var(--leading-ui);background:var(--raised);border:1px solid var(--border);border-radius:var(--radius-xs);color:var(--text);padding:1px 7px}.empty{text-align:center;border:1px dashed var(--border);border-radius:var(--radius);color:var(--muted);padding:56px 20px}.empty svg{width:30px;height:30px;color:var(--faint);margin-bottom:12px}.empty .title{color:var(--text);font-weight:var(--weight-semibold);margin-bottom:6px}.notice{font-size:var(--text-control);line-height:var(--leading-body);border-radius:var(--radius-sm);max-width:var(--prose-measure);align-items:center;gap:8px;margin-bottom:14px;padding:9px 12px;display:flex}.notice svg{flex-shrink:0;width:15px;height:15px}.toast-notice-host{z-index:var(--z-modal);pointer-events:none;justify-content:center;align-items:flex-start;padding:12vh 16px 16px;display:flex;position:fixed;inset:0}.toast-notice{pointer-events:auto;max-width:min(var(--prose-measure), calc(100vw - 32px));box-shadow:var(--shadow);animation:toast-notice-in var(--motion-normal) ease-out;margin-bottom:0}.toast-notice-copy{flex:1;min-width:0}.toast-notice-dismiss{color:inherit;opacity:.7;cursor:pointer;background:0 0;border:0;flex-shrink:0;padding:0 2px;font-size:1.1rem;line-height:1}.toast-notice-dismiss:hover{opacity:1}@keyframes toast-notice-in{0%{opacity:0;transform:translateY(-8px)}to{opacity:1;transform:translateY(0)}}@media (prefers-reduced-motion:reduce){.toast-notice{animation:none}}.notice-ok{background:var(--lightningcss-light,#ecfdf5)var(--lightningcss-dark,color-mix(in oklab, var(--green) 18%, var(--surface)));color:var(--lightningcss-light,#065f46)var(--lightningcss-dark,#d1fae5);border:1px solid color-mix(in srgb, var(--green) 32%, transparent)}.notice-ok svg{color:var(--green)}.notice-err{background:var(--lightningcss-light,#fef2f2)var(--lightningcss-dark,color-mix(in oklab, var(--red) 18%, var(--surface)));color:var(--lightningcss-light,#991b1b)var(--lightningcss-dark,#fee2e2);border:1px solid color-mix(in srgb, var(--red) 32%, transparent)}.notice-err svg{color:var(--red)}.h-section{font-size:var(--text-control);font-weight:var(--weight-semibold);line-height:var(--leading-ui);color:var(--text);align-items:center;gap:8px;margin:30px 0 12px;display:flex}.h-section .count{color:var(--muted);font-weight:var(--weight-medium);font-family:var(--font-code);font-size:var(--text-label)}.spin{border:2px solid var(--border);border-top-color:var(--accent);border-radius:var(--radius-round);width:14px;height:14px;animation:.7s linear infinite spin;display:inline-block}@keyframes spin{to{transform:rotate(360deg)}}.data-surface-status{align-items:center;gap:var(--space-2);min-height:var(--control-md);color:var(--muted);font-size:var(--text-control);line-height:var(--leading-body);display:flex}.data-surface-skeleton{gap:var(--space-2);display:grid}.data-surface-skeleton__row{min-height:var(--control-lg)}.data-surface-skeleton__block{min-height:var(--control-lg);border:1px solid var(--border-soft);border-radius:var(--radius-sm);background:linear-gradient(90deg, var(--raised) 0%, var(--surface) 50%, var(--raised) 100%);background-size:200% 100%;width:100%;animation:1.2s ease-in-out infinite codex-auth-skeleton-shimmer;display:block}@media (prefers-reduced-motion:reduce){*{transition:none!important;animation:none!important}}@supports not ((-webkit-backdrop-filter:blur(1px)) or (backdrop-filter:blur(1px))){.sidebar,.lang-toggle .select-dropdown{background:var(--rail)}.select-dropdown,.modal-card{background:var(--surface)}}@media (prefers-reduced-transparency:reduce){body:before{display:none}.sidebar,.lang-toggle .select-dropdown{background:var(--rail);-webkit-backdrop-filter:none}.select-dropdown,.modal-card{background:var(--surface);-webkit-backdrop-filter:none}.modal-overlay{-webkit-backdrop-filter:none}}.modal-overlay{-webkit-backdrop-filter:blur(40px)saturate(1.2);z-index:var(--z-modal);background:var(--lightningcss-light,#11131cc7)var(--lightningcss-dark,#000000d1);justify-content:center;align-items:flex-start;padding:8vh 16px;display:flex;position:fixed;inset:0}dialog.modal-overlay{width:100%;max-width:none;height:100%;max-height:none;color:inherit;border:none;margin:0}dialog.modal-overlay::backdrop{background:0 0}.modal-card{z-index:1;background:color-mix(in oklab, canvas 92%, transparent);-webkit-backdrop-filter:blur(20px)saturate(1.4);border:1px solid var(--border);border-radius:var(--radius-lg);width:100%;max-width:520px;box-shadow:var(--shadow-sm);max-height:84vh;padding:20px;position:relative;overflow-y:auto}.modal-head{justify-content:space-between;align-items:center;margin-bottom:16px;display:flex}.modal-head h3{font-size:var(--text-subtitle)}.card-head{flex-wrap:wrap;align-items:center;gap:8px;min-width:0;padding:10px 16px 4px;display:flex}.card-head strong{overflow-wrap:anywhere;min-width:0}.card-sub{font-size:var(--text-label);line-height:var(--leading-body);color:var(--muted);overflow-wrap:anywhere;min-width:0;padding:0 16px 8px}.card-active{border-color:var(--accent-ring)}.card-right{font-size:var(--text-caption);color:var(--faint);align-items:center;gap:4px;margin-left:auto;display:flex}.btn-icon-danger.card-right{appearance:none;color:var(--red);cursor:pointer;background:0 0;border:1px solid #0000;justify-content:center}.btn-icon-danger.card-right:hover{background:var(--red-soft);border-color:var(--red-soft);color:var(--red)}.card-row{justify-content:space-between;align-items:center;padding:14px 16px;display:flex}.badge-primary{background:var(--accent-soft);color:var(--accent-hover)}.dot-blue{background:var(--accent);box-shadow:0 0 0 3px var(--accent-soft)}.dot-muted{background:var(--muted)}.dot-amber{background:var(--amber);box-shadow:0 0 0 3px var(--amber-soft)}.section-sep{align-items:center;gap:10px;margin:20px 0 12px;display:flex}.section-label{font-size:var(--text-label);color:var(--muted);font-weight:var(--weight-medium);white-space:nowrap}.sep-line{background:var(--border);flex:1;height:1px}.quota-compact{gap:4px;padding:0 16px 10px;display:grid}.codex-account-quota-slot{box-sizing:border-box;min-height:22px}.codex-auth-load-skeleton{display:contents}.codex-auth-load-skeleton__main{border-color:var(--border)}.codex-auth-load-skeleton__line{vertical-align:middle;background:linear-gradient(90deg, var(--raised) 0%, var(--surface) 50%, var(--raised) 100%);background-size:200% 100%;border-radius:4px;height:.9em;animation:1.2s ease-in-out infinite codex-auth-skeleton-shimmer;display:inline-block}.codex-auth-load-skeleton__line--sub{width:12rem;position:absolute;top:.35em;left:16px}.codex-auth-load-skeleton__main .card-sub{min-height:calc(var(--leading-body) * 1em + 10px);position:relative}.codex-auth-load-skeleton__strut{visibility:hidden;white-space:nowrap;display:inline-block}.codex-auth-load-skeleton__empty{pointer-events:none}.codex-auth-pool-empty{box-sizing:border-box}.openai-account-mode-banner__desc{margin:6px 0 0;padding-left:0;padding-right:0}.openai-account-mode-banner__desc--pending{visibility:hidden;min-height:1.35em}.openai-account-mode-banner__badge-slot{justify-content:center;min-width:4.5rem}.codex-ticket-badge-slot{visibility:hidden;pointer-events:none;justify-content:center;min-width:2.25rem}.quota-compact--pending{box-sizing:border-box;height:22px;min-height:22px;max-height:22px;overflow:hidden}.quota-compact--pending .quota-row--skeleton{height:18px;min-height:0}.quota-skel{border-radius:var(--radius-2xs);background:linear-gradient(90deg, var(--raised) 0%, var(--surface) 50%, var(--raised) 100%);background-size:200% 100%;height:8px;animation:1.2s ease-in-out infinite codex-auth-skeleton-shimmer;display:block}.quota-skel--reset{width:34px}.quota-skel--bar{align-self:center;width:100%;height:5px}.quota-skel--val{justify-self:end;width:28px}.quota-row{grid-template-columns:minmax(34px,max-content) max-content minmax(34px,max-content) minmax(38px,max-content) minmax(58px,1fr) minmax(30px,max-content);align-items:center;gap:8px;min-width:0;display:grid}.quota-label{font-size:var(--text-caption);color:var(--muted);font-weight:var(--weight-semibold)}.quota-val{font-size:var(--text-caption);font-family:var(--font-code);color:var(--text);text-align:right}.quota-reset-label{text-overflow:ellipsis;white-space:nowrap;font-size:var(--text-caption);color:var(--faint);overflow:hidden}.quota-reset-day,.quota-reset-time{font-size:var(--text-caption);color:var(--muted);white-space:nowrap}.quota-reset-time{font-family:var(--font-code)}.bar{background:var(--raised);border-radius:var(--radius-2xs);min-width:0;height:5px;overflow:hidden}.bar-fill{border-radius:var(--radius-2xs);width:100%;height:100%;transform:scaleX(var(--bar-scale,0));transform-origin:0;transition:transform var(--motion-normal)}.bar-green{background:var(--green)}.bar-amber{background:var(--amber)}.quota-row--skeleton{min-height:18px}.quota-skel{border-radius:var(--radius-2xs);background:color-mix(in srgb, var(--muted) 22%, transparent);min-height:8px;display:inline-block}.quota-skel--label,.quota-skel--reset{width:34px}.quota-skel--day{width:36px}.quota-skel--time{width:38px}.quota-skel--bar{width:100%;min-height:5px}.quota-skel--val{width:30px}.openai-account-mode-banner__badge-slot--pending{visibility:hidden}.toggle{width:var(--toggle-w);height:var(--toggle-h);border-radius:var(--radius-pill);background:var(--toggle-off-bg);border:1px solid var(--border);cursor:pointer;transition:background var(--motion-normal), border-color var(--motion-normal);flex-shrink:0;position:relative}.toggle.on{background:var(--toggle-on-bg);border-color:var(--toggle-on-bg)}.toggle-knob{border-radius:var(--radius-round);width:16px;height:16px;transition:left var(--motion-normal), background var(--motion-normal);background:var(--lightningcss-light,#fff)var(--lightningcss-dark,#ececec);position:absolute;top:50%;left:1px;transform:translateY(-50%)}.toggle.on .toggle-knob{background:var(--toggle-dot-color);left:17px}.toggle:disabled{cursor:not-allowed;opacity:.55}.codex-auto-switch-card{flex-wrap:wrap;gap:16px}.codex-auto-switch-copy{flex:auto;min-width:0}.codex-auto-switch-copy .card-sub{padding:2px 0 0}.codex-auto-switch-controls{flex:none;align-items:flex-end;gap:12px;margin-left:auto;display:flex}.codex-request-user-input-card{flex-wrap:wrap;gap:16px}.codex-request-user-input-copy{flex:auto;min-width:0}.codex-request-user-input-copy .card-sub{padding:2px 0 0}.codex-request-user-input-config{overflow-wrap:anywhere;font-size:var(--text-label);color:var(--muted);margin-top:6px;display:block}.codex-request-user-input-controls{flex:none;align-items:flex-end;gap:12px;margin-left:auto;display:flex}.codex-request-user-input-controls>.toggle{margin-left:auto}.codex-request-user-input-feedback{color:var(--muted);font-size:var(--text-label);line-height:var(--leading-body);text-align:right;flex:1 0 100%;margin-top:-8px}.codex-request-user-input-feedback.is-error{color:var(--red)}.codex-account-picker-card{flex-wrap:wrap;gap:16px;margin-top:16px}.codex-auth-advanced{margin-top:8px}.codex-auth-advanced__toggle{width:100%;color:var(--muted);font-size:var(--text-label);font-weight:var(--weight-medium);cursor:pointer;text-align:left;background:0 0;border:0;align-items:center;gap:6px;padding:8px 2px;display:flex}.codex-auth-advanced__toggle:hover{color:var(--text)}.codex-auth-advanced__chevron{transition:transform var(--motion-fast,.12s)}.codex-auth-advanced__chevron.is-open{transform:rotate(90deg)}.codex-auth-advanced__boxes{gap:0;display:grid}.codex-account-picker-copy{flex:34rem;min-width:0}.codex-account-picker-copy .card-sub{padding:4px 0 0}.codex-account-picker-controls{flex:none;align-items:center;gap:12px;margin-left:auto;display:flex}.codex-account-picker-feedback{font-size:var(--text-label);line-height:var(--leading-body);text-align:right;flex:1 0 100%;margin-top:-8px}.codex-account-picker-feedback.is-ok{color:var(--green)}.codex-account-picker-feedback.is-warn{color:var(--amber)}.codex-account-picker-feedback.is-err{color:var(--red)}.codex-auto-switch-toggle-slot{flex:none;justify-content:flex-end;align-items:center;min-height:36px;margin-left:auto;display:inline-flex}.codex-auto-switch-threshold{flex-direction:column;align-items:flex-start;margin:0;display:flex}.codex-auto-switch-threshold .field-label{white-space:nowrap;margin-bottom:4px}.codex-auto-switch-input-wrap{border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--surface);align-items:stretch;gap:0;width:max-content;max-width:100%;min-height:32px;display:inline-flex;overflow:hidden}.codex-auto-switch-input-wrap:focus-within{border-color:var(--accent-ring);box-shadow:0 0 0 1px var(--accent-ring)}.codex-auto-switch-input-wrap .input,.codex-auto-switch-input-wrap .codex-auto-switch-input{box-shadow:none;background:0 0;border:none;border-radius:0;align-self:stretch;height:auto;min-height:0}.codex-auto-switch-input-wrap .input:focus{box-shadow:none;border-color:#0000}.codex-auto-switch-input{text-align:right;font-variant-numeric:tabular-nums;width:84px}.codex-auto-switch-input[readonly]{cursor:progress;opacity:.7}.codex-auto-switch-input::-webkit-outer-spin-button{-webkit-appearance:none;margin:0}.codex-auto-switch-input::-webkit-inner-spin-button{-webkit-appearance:none;margin:0}.codex-auto-switch-input[type=number]{appearance:textfield}.codex-auto-switch-unit{color:var(--muted);font-size:var(--text-control);flex:none;align-items:center;padding-right:8px;display:inline-flex}.codex-auto-switch-feedback{color:var(--muted);font-size:var(--text-label);line-height:var(--leading-body);text-align:right;flex:1 0 100%;margin-top:-8px}.codex-auto-switch-feedback.is-error{color:var(--red)}.ocx-stepper{flex-direction:column;flex:none;gap:2px;display:inline-flex}.ocx-stepper__btn{appearance:none;border:1px solid var(--border);border-radius:var(--radius-2xs);background:var(--raised);width:22px;height:16px;color:var(--muted);cursor:pointer;justify-content:center;align-items:center;padding:0;display:inline-flex}.ocx-stepper__btn:hover:not(:disabled){color:var(--text);border-color:var(--faint)}.ocx-stepper__btn:disabled{opacity:.45;cursor:not-allowed}.ocx-stepper__btn:focus-visible{outline:2px solid var(--accent-ring);outline-offset:1px}.ocx-stepper__btn svg{display:block}.codex-auto-switch-input-wrap .ocx-stepper{border-left:1px solid var(--border);align-self:stretch;gap:0}.codex-auto-switch-input-wrap .ocx-stepper__btn{background:var(--raised);border:none;border-radius:0;flex:1 1 0;width:26px;height:auto;min-height:0}.codex-auto-switch-input-wrap .ocx-stepper__btn+.ocx-stepper__btn{border-top:1px solid var(--border)}.codex-auto-switch-input-wrap .ocx-stepper__btn:hover:not(:disabled){background:var(--surface);border-color:#0000}.codex-auto-switch-input-wrap .ocx-stepper__btn:focus-visible{outline-offset:-2px;z-index:1}.codex-auth-page-head{align-items:flex-start}.codex-auth-page-head__actions{align-items:center;gap:10px;min-width:0;display:flex}.codex-auth-spark-toggle{white-space:nowrap;align-items:center;gap:8px;display:inline-flex}.codex-auth-spark-toggle__label{color:var(--muted);font-size:12px}.codex-auth-action-btn:hover:not(:disabled){background:var(--raised-hover);border-color:var(--faint);color:var(--text)}.codex-auth-action-btn.btn-primary:hover:not(:disabled){background:var(--accent-hover);color:var(--accent-ink);border-color:#0000}.codex-auth-action-btn:focus-visible{outline:2px solid var(--accent-ring);outline-offset:1px}.codex-auth-page-head__feedback{min-width:8rem;max-width:18rem;min-height:calc(var(--leading-body) * 1em);text-align:right;font-size:var(--text-label);line-height:var(--leading-body);color:var(--muted);text-overflow:ellipsis;white-space:nowrap;justify-content:flex-end;align-items:center;display:inline-flex;overflow:hidden}.codex-auth-page-head__feedback.is-ok{color:var(--green)}.codex-auth-page-head__feedback.is-warn{color:var(--amber);text-overflow:clip;white-space:normal;overflow:visible}.codex-auth-page-head__feedback.is-err{color:var(--red)}.codex-auth-pause-label{text-align:center;display:inline-block}@keyframes codex-auth-skeleton-shimmer{0%{background-position:100% 0}to{background-position:-100% 0}}.account-pool-strategy-card{gap:8px;margin-top:16px;padding:14px 16px;display:grid}.account-pool-strategy-card>strong{margin:0;display:block}.account-pool-strategy-card>.card-sub,.account-pool-strategy-controls>.card-sub,.account-pool-strategy-controls .field .card-sub,.anthropic-pool-card__field .card-sub{margin:0;padding:0}.account-pool-strategy-card__retry{justify-self:start}.account-pool-strategy-card__error{color:var(--danger,#c44)}.account-pool-strategy-controls{gap:8px;margin:0;display:grid}.account-pool-strategy-controls .field{gap:6px;margin:0;display:grid}.account-pool-strategy-controls .field-label{margin-bottom:0}.account-pool-strategy-controls .custom-select{width:100%;max-width:100%}.account-pool-strategy-controls .select-trigger{border-radius:var(--radius-sm);justify-content:space-between;width:100%;max-width:100%}.account-pool-strategy-controls .select-trigger>span{text-overflow:ellipsis;white-space:nowrap;min-width:0;overflow:hidden}.anthropic-pool-card{margin-top:12px}.anthropic-pool-card__notice{border:1px solid var(--border,#c9a227);background:color-mix(in srgb, var(--warn,#c9a227) 12%, transparent);border-radius:6px;margin:10px 16px 0;padding:10px 16px}.anthropic-pool-card__field{margin-top:12px;padding:0 16px;display:block}.anthropic-pool-card__field--quota-window{padding-bottom:var(--space-4)}.api-active-keys-skeleton{border:1px solid var(--border-soft);border-radius:var(--radius);background:linear-gradient(90deg, var(--raised) 0%, var(--surface) 50%, var(--raised) 100%);background-size:200% 100%;min-height:96px;animation:1.2s ease-in-out infinite codex-auth-skeleton-shimmer}.notice-warn{font-size:var(--text-label);line-height:var(--leading-body);border-radius:var(--radius-sm);background:var(--lightningcss-light,#fffbeb)var(--lightningcss-dark,color-mix(in oklab, var(--amber) 20%, var(--surface)));color:var(--lightningcss-light,#92400e)var(--lightningcss-dark,#fef3c7);border:1px solid color-mix(in srgb, var(--amber) 32%, transparent);max-width:var(--prose-measure);align-items:center;gap:6px;margin-bottom:12px;padding:8px 10px;display:flex}.notice-warn svg{width:14px;height:14px;color:var(--amber);flex-shrink:0}.startup-runtime-notice__text,.startup-runtime-notice__fix code{color:inherit}.codex-pool-strategy-card{padding:14px 16px}.codex-pool-strategy-card .card-sub{padding:0}.codex-account-more{flex-wrap:wrap;align-items:center;display:inline-flex}.codex-account-more>summary{cursor:pointer;justify-content:center;min-width:28px;list-style:none}.codex-account-more>summary::-webkit-details-marker{display:none}.codex-account-more-body{flex-wrap:wrap;flex-basis:100%;align-items:center;gap:8px;padding-top:6px;display:flex}.codex-account-identity{justify-content:space-between;align-items:center;gap:8px;min-width:0;padding:0 16px 6px;display:flex}.codex-account-identity-copy{font-size:var(--text-label);line-height:var(--leading-body);color:var(--muted);overflow-wrap:anywhere;min-width:0}.codex-account-priority{flex-wrap:wrap;flex:none;align-items:center;gap:8px;min-width:0;padding:0;display:flex}.codex-account-priority-label{font-size:var(--text-label);color:var(--muted);font-weight:var(--weight-medium);white-space:nowrap}.codex-account-priority .select-trigger{max-width:100%;font-size:var(--text-label);padding:4px 9px}.startup-page-head-actions{flex-shrink:0;align-items:center;gap:8px;display:flex}.startup-runtime-notice-slot{min-height:52px;margin-bottom:12px}.startup-runtime-notice-slot--pending{border-radius:var(--radius-sm);background:color-mix(in srgb, var(--amber-soft) 45%, transparent)}.startup-runtime-notice-slot .startup-runtime-notice{margin-bottom:0}.notice.notice-warn.startup-page-notice{box-sizing:border-box;width:100%;max-width:none}.notice.notice-warn.startup-runtime-notice{flex-direction:column;align-items:stretch;gap:8px;display:flex}.startup-runtime-notice__text{max-width:none;margin:0}.startup-runtime-notice__fix{justify-content:space-between;align-items:center;gap:12px;min-width:0;display:flex}.startup-runtime-notice__fix code{overflow-wrap:anywhere;flex:auto;min-width:0;margin:0}.startup-runtime-notice__fix .btn{flex:none}.startup-hero{align-items:flex-start;gap:16px;margin-bottom:16px;display:flex}.startup-hero--safe{border-color:color-mix(in srgb, var(--green) 34%, var(--border));background:color-mix(in srgb, var(--green-soft) 64%, var(--surface))}.startup-hero--risk{border-color:color-mix(in srgb, var(--amber) 40%, var(--border));background:color-mix(in srgb, var(--amber-soft) 72%, var(--surface))}.startup-hero--native{border-color:color-mix(in srgb, var(--accent) 20%, var(--border))}.startup-hero-icon{border-radius:var(--radius);background:var(--raised);flex:none;place-items:center;width:42px;height:42px;display:grid}.startup-hero-icon svg{width:21px;height:21px}.startup-hero-copy h3{font-size:var(--text-title);margin:10px 0 4px}.startup-hero-copy p{color:var(--muted);line-height:var(--leading-body);max-width:var(--prose-measure);margin:0}.startup-state-line{font-size:var(--text-control);margin:8px 0 0}.startup-recovery-details>summary{cursor:pointer;align-items:center;gap:6px;list-style:none;display:inline-flex}.startup-recovery-details>summary::-webkit-details-marker{display:none}.startup-recovery-details>summary:before{content:"";border-left:5px solid var(--muted);width:0;height:0;transition:transform var(--motion-fast);border-top:4px solid #0000;border-bottom:4px solid #0000}.startup-recovery-details[open]>summary:before{transform:rotate(90deg)}.startup-recovery-details[open]>summary{margin-bottom:8px}.startup-details,.startup-actions{margin-bottom:16px}.startup-actions .panel-head>svg{flex:none;width:18px;height:18px}.startup-detail-row{border-top:1px solid var(--border-soft);justify-content:space-between;align-items:center;gap:16px;padding:12px 0;display:flex}.startup-detail-row>div{flex-direction:column;gap:3px;min-width:0;display:flex}.startup-detail-row>.startup-detail-actions{flex-direction:row;flex:none;justify-content:flex-end;align-items:center;gap:8px}.startup-detail-row span:not(.badge){color:var(--muted);font-size:var(--text-label);line-height:var(--leading-body)}.startup-actions>.muted{font-size:var(--text-control);max-width:var(--prose-measure);margin:-4px 0 14px}.startup-tray-buttons{flex-wrap:wrap;gap:10px;min-height:36px;margin-top:16px;display:flex}.startup-tray-error{margin-top:12px}.startup-command-list{border:1px solid var(--border-soft);border-radius:var(--radius-sm);overflow:hidden}.startup-command-row{justify-content:space-between;align-items:center;gap:16px;padding:12px;display:flex}.startup-command-row+.startup-command-row{border-top:1px solid var(--border-soft)}.startup-command-row>div{flex-direction:column;gap:5px;min-width:0;display:flex}.startup-command-row strong{font-size:var(--text-control)}.startup-command-row code{color:var(--muted);font-size:var(--text-label);overflow-wrap:anywhere}.startup-action-notice{margin:14px 0 0}@media (width<=700px){.startup-command-row,.startup-detail-row{align-items:flex-start}.startup-detail-row>.startup-detail-actions{flex-direction:column;align-items:flex-end}}.modal-desc{font-size:var(--text-control);line-height:var(--leading-body);color:var(--muted);max-width:var(--prose-measure);margin-bottom:14px}.modal-actions{gap:8px;margin-top:16px;display:flex}.modal-actions .btn{flex:1}.log-reqid{-webkit-line-clamp:2;word-break:break-all;-webkit-box-orient:vertical;max-width:14ch;display:-webkit-box;overflow:hidden}.main-inner:has(.logs-page){max-width:1200px}.log-col-model{overflow-wrap:break-word;word-break:normal;max-width:16ch}.log-col-tokens{min-width:10ch}.log-col-time{white-space:nowrap;vertical-align:middle}.log-col-duration{white-space:nowrap}table.logs-table{table-layout:fixed;width:100%;min-width:1100px}.logs-table col.logs-col-time{width:12%}.logs-table col.logs-col-tokens{width:9%}.logs-table col.logs-col-rate{width:7%}.logs-table col.logs-col-cost{width:8%}.logs-table col.logs-col-model{width:15%}.logs-table col.logs-col-effort{width:9%}.logs-table col.logs-col-provider{width:13%}.logs-table col.logs-col-status{width:8%}.logs-table col.logs-col-request{width:11%}.logs-table col.logs-col-duration{width:8%}.log-col-rate{white-space:nowrap;min-width:7ch}.log-col-cost{white-space:nowrap;min-width:10ch}.logs-table tbody td{overflow:hidden}.log-reasoning-cell{overflow-wrap:anywhere}.log-status-cell{align-items:flex-start;gap:var(--space-0-5);min-width:7ch;line-height:var(--leading-tight);flex-direction:column;display:inline-flex}.log-detail-btn{cursor:pointer;color:var(--accent-hover);font:inherit;font-size:var(--text-caption);white-space:normal;text-align:left;background:0 0;border:none;padding:0;text-decoration:underline}.log-detail-btn:focus-visible{outline-offset:-2px}.logs-auto-refresh{align-items:center;gap:var(--space-2);cursor:pointer;display:inline-flex}.logs-toolbar{align-items:center;gap:var(--space-2);margin-bottom:var(--space-3);flex-wrap:wrap;display:flex}.logs-segmented{border-radius:var(--radius-pill);background:var(--surface-soft,var(--raised));padding:var(--space-1);gap:var(--space-1);display:inline-flex}.logs-segmented .btn{border-radius:var(--radius-pill);min-width:64px;padding:var(--space-1-5) var(--space-3);border:none}.logs-filter-field{align-items:center;gap:var(--space-2);display:inline-flex}.logs-filter-field .input{min-width:220px;max-width:360px}.logs-conversation-totals{margin-bottom:var(--space-3)}.logs-table-wrap{overflow-anchor:none;scrollbar-gutter:stable;max-height:calc(100dvh - 260px);overflow-y:auto}.logs-table thead{z-index:1;background:var(--surface);position:sticky;top:0}.logs-virtual-spacer{border:0;padding:0}.logs-stack-end{align-items:flex-end;gap:var(--space-0-5);flex-direction:column;display:inline-flex}.logs-stack-start{align-items:flex-start;gap:var(--space-0-5);flex-direction:column;display:inline-flex}.logs-model-cell{align-items:center;gap:var(--space-2);flex-wrap:wrap;display:inline-flex}.logs-detail-info{margin-left:var(--space-2)}.log-detail-grid{gap:var(--space-2) var(--space-3);font-size:var(--text-control);grid-template-columns:max-content minmax(0,1fr);display:grid}.log-detail-break{word-break:break-all}.log-detail-card{max-width:760px}.log-detail-section{border-top:1px solid var(--border-soft);padding:14px 0}.log-detail-section:first-of-type{border-top:0;padding-top:0}.log-detail-section-title{font-size:var(--text-label);font-weight:var(--weight-semibold);color:var(--text);margin:0 0 10px}.log-detail-request-row{justify-content:space-between;align-items:center;gap:8px;min-width:0;display:flex}.log-detail-request-row>span{min-width:0}.log-detail-notes{color:var(--muted);font-size:var(--text-label);margin:10px 0 0;padding-left:18px}.log-detail-notes-line{font-size:var(--text-label);margin:8px 0 0}.log-detail-attempts-wrap{border:1px solid var(--border-soft);border-radius:var(--radius-sm);overflow-x:auto}.log-detail-attempts{min-width:680px}.log-detail-attempts th,.log-detail-attempts td{vertical-align:top}.log-detail-raw{margin-top:14px}.log-detail-raw>summary{cursor:pointer;color:var(--muted);font-size:var(--text-label)}.log-detail-raw[open]>summary{margin-bottom:6px}.usage-cost-row{border:1px solid var(--border-soft);border-radius:var(--radius-sm);background:var(--raised);align-items:baseline;gap:10px;margin:10px 0 4px;padding:10px 14px;display:flex}.usage-cost-value{font-size:var(--text-lg,1.15em)}@media (width<=760px){.modal-overlay{padding:16px 10px}.log-detail-card{max-height:calc(100dvh - 32px);padding:16px}.log-detail-grid{grid-template-columns:minmax(7rem,max-content) minmax(0,1fr);gap:8px 10px}.log-detail-request-row{flex-direction:column;align-items:flex-start}}.log-detail-json{background:var(--raised);border:1px solid var(--border-soft);border-radius:var(--radius-sm);font-family:var(--font-code);font-size:var(--text-caption);line-height:var(--leading-body);white-space:pre-wrap;word-break:break-all;max-height:40vh;margin:0;padding:10px 12px;overflow:auto}.setup-guide{font-size:var(--text-control);line-height:var(--leading-body);border:1px solid var(--border);border-radius:var(--radius-sm);margin-bottom:4px;padding:8px 12px}.setup-guide summary{cursor:pointer;color:var(--accent-hover);font-weight:var(--weight-medium)}.setup-guide summary:hover{text-decoration:underline}.setup-guide a{color:var(--accent-hover)}.list-row{text-align:left;border-radius:var(--radius-sm);border:1px solid var(--border);background:var(--raised);cursor:pointer;width:100%;color:var(--text);font:inherit;transition:background var(--motion-fast), border-color var(--motion-fast);justify-content:space-between;align-items:center;gap:10px;padding:11px 13px;display:flex}.list-row:hover{background:var(--raised-hover);border-color:var(--accent-ring)}.list-row .title{font-weight:var(--weight-semibold);font-size:var(--text-body)}.list-row .sub{font-size:var(--text-label);color:var(--muted);margin-top:2px}.prov-card{overflow:hidden}.prov-card-main{flex-wrap:wrap;justify-content:space-between;align-items:flex-start;gap:12px;padding:15px 16px;display:flex}.prov-card-disabled{opacity:.62}.prov-card-info{flex:360px;align-items:flex-start;gap:11px;min-width:0;display:flex}.prov-card-copy{flex:1;min-width:0}.prov-title{flex-wrap:wrap;align-items:center;gap:8px;min-width:0;margin-bottom:5px;display:flex}.provider-icon{border-radius:var(--radius-xs);background:var(--raised);border:1px solid var(--border-soft);width:31px;height:31px;color:var(--text);flex:none;justify-content:center;align-items:center;display:inline-flex}.provider-icon img{object-fit:contain;width:19px;height:19px;display:block}.provider-icon-mask{background:currentColor;width:19px;height:19px;display:block;-webkit-mask-position:50%;mask-position:50%;-webkit-mask-size:contain;mask-size:contain;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}.provider-icon-sm .provider-icon-mask{width:15px;height:15px}.provider-icon--plate{background:#f4f4f4;border-color:#0000001f}.provider-icon--plate-dark{background:#1c1c1c;border-color:#ffffff24}.prov-meta{flex-wrap:wrap;align-items:center;gap:5px;min-width:0;display:flex}.prov-meta>span{text-overflow:ellipsis;white-space:nowrap;min-width:0;max-width:100%;overflow:hidden}.provider-quota{padding-left:58px}.provider-actions{flex-shrink:0;align-items:center;gap:8px;margin-left:auto;display:flex}.openai-mode-row{flex-wrap:wrap;align-items:center;gap:10px;margin-top:10px;display:flex}.openai-mode-control .usage-segmented-btn{min-width:76px}.link-btn{color:var(--accent-hover);font:inherit;font-size:var(--text-control);cursor:pointer;background:0 0;border:none;padding:6px 2px;text-decoration:underline}.link-btn svg{flex-shrink:0;width:14px;height:14px}.prov-accounts-toggle{border:none;border-top:1px solid var(--border-soft);width:100%;color:var(--muted);font:inherit;font-size:var(--text-label);cursor:pointer;background:0 0;justify-content:center;align-items:center;gap:6px;min-height:24px;padding:4px 0;display:flex}.prov-accounts-toggle:hover{color:var(--text);background:var(--raised)}.prov-accounts-toggle .chev{transition:transform var(--motion-normal) ease;display:inline-flex}.prov-accounts-toggle .chev svg{width:12px;height:12px;transform:rotate(90deg)}.prov-accounts-toggle.open .chev svg{transform:rotate(-90deg)}.prov-accounts-list{border-top:1px solid var(--border-soft);flex-direction:column;gap:2px;padding:6px 16px 10px;display:flex}.prov-account-row{border-radius:var(--radius-xs);align-items:center;gap:8px;width:100%;min-height:32px;padding:6px 8px;display:flex}button.prov-account-row{text-align:left;color:var(--text);font:inherit;font-size:var(--text-control);line-height:var(--leading-ui);cursor:pointer;background:0 0;border:none}button.prov-account-row:hover,.prov-account-row:hover{background:var(--raised)}button.prov-account-row.active{cursor:default}.prov-account-row-main{appearance:none;min-width:0;color:inherit;text-align:left;cursor:pointer;font:inherit;font-size:var(--text-control);line-height:var(--leading-ui);background:0 0;border:0;flex:auto;align-items:center;gap:8px;padding:0;display:flex}.prov-account-row-main:disabled{cursor:default;opacity:.72}.prov-account-row .prov-account-email{text-overflow:ellipsis;white-space:nowrap;flex:auto;min-width:0;overflow:hidden}.prov-account-row .badge{flex:none}.prov-account-reauth{color:var(--accent);cursor:pointer;font:inherit;font-size:var(--text-label);border-radius:var(--radius-xs);background:0 0;border:none;flex:none;padding:4px 6px}.prov-account-reauth:hover:not(:disabled){color:var(--accent-hover);background:var(--raised)}.prov-account-reauth:disabled{opacity:.6;cursor:default}.prov-account-remove{color:var(--muted);cursor:pointer;border-radius:var(--radius-xs);background:0 0;border:none;flex:none;justify-content:center;align-items:center;padding:4px;display:inline-flex}.prov-account-remove:hover{color:var(--red);background:var(--red-soft)}.prov-account-add{color:var(--muted);font-size:var(--text-label)}.prov-account-add:hover{color:var(--accent-hover)}.prov-account-keyform{cursor:default;gap:6px}.prov-account-keyform:hover{background:0 0}.prov-account-keyform .input-sm{min-width:0;font-size:var(--text-label);height:var(--control-sm);flex:auto;padding:4px 8px}.oauth-grid{grid-template-columns:minmax(140px,max-content) minmax(0,1fr) max-content;align-items:center;gap:10px 16px;display:grid}.oauth-row{grid-column:1/-1;grid-template-columns:subgrid;align-items:center;min-height:30px;display:grid}.oauth-name{font-size:var(--text-control);font-weight:var(--weight-semibold);align-items:center;gap:8px;min-width:0;display:inline-flex}.oauth-name-text{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.provider-icon-sm{border-radius:var(--radius-xs);width:24px;height:24px}.provider-icon-sm img{width:15px;height:15px}.oauth-status{font-size:var(--text-control);align-items:center;gap:7px;min-width:0;display:inline-flex}.oauth-email{text-overflow:ellipsis;white-space:nowrap;min-width:0;overflow:hidden}.oauth-actions{justify-content:flex-end;align-items:center;gap:8px;min-width:0;display:inline-flex}.oauth-login-hint{font-size:var(--text-label);line-height:var(--leading-body);flex-direction:column;grid-column:1/-1;align-items:stretch;gap:8px;display:flex}.oauth-login-hint-links{flex-wrap:wrap;align-items:center;gap:8px;display:inline-flex}.oauth-device-code-wrap{border:1px solid var(--border);background:var(--surface);border-radius:10px;flex-wrap:wrap;align-items:center;gap:10px;padding:12px;display:flex}.oauth-device-code-label{font-size:var(--text-label);color:var(--text);font-weight:600}.oauth-device-code{letter-spacing:.14em;color:var(--text);-webkit-user-select:all;user-select:all;font-size:20px;font-weight:800}.oauth-login-paste{align-items:center;gap:8px;width:100%;display:flex}.oauth-login-paste .input{min-width:0;font-size:var(--text-label);flex:1;padding:6px 10px}@media (width<=760px){.app{grid-template-rows:auto 1fr;grid-template-columns:1fr}.mobile-topbar{z-index:var(--z-sticky);border-bottom:1px solid var(--border);background:var(--glass-rail);min-width:0;-webkit-backdrop-filter:var(--glass-blur);align-items:center;gap:2px;padding:4px 10px;display:flex;position:sticky;top:0}.mobile-topbar .brand{flex:auto;min-width:0;padding:4px}.mobile-topbar .brand .name{text-overflow:ellipsis;white-space:nowrap;min-width:0;overflow:hidden}.mobile-topbar .brand .ver{flex-shrink:0}.mobile-topbar .stop-toggle{justify-content:center;width:auto;min-width:44px;min-height:44px;padding:8px}.mobile-topbar-actions{flex:none;align-items:center;gap:6px;display:flex}.mobile-topbar-actions .sidebar-orb{flex:0 0 44px;width:44px;min-width:44px;height:44px;min-height:44px}.mobile-topbar-actions .sidebar-orb svg{width:18px;height:18px}.menu-toggle{display:inline-flex}.sidebar{z-index:var(--z-popover);width:min(280px,84vw);height:100dvh;padding-bottom:calc(18px + env(safe-area-inset-bottom));visibility:hidden;transition:transform var(--motion-normal) ease, visibility var(--motion-normal);background:var(--lightningcss-light,#f9f9f9f7)var(--lightningcss-dark,#171717f5);outline:none;position:fixed;top:0;bottom:0;left:0;overflow-y:auto;transform:translate(-100%)}.sidebar.open{visibility:visible;transform:translate(0);box-shadow:0 12px 40px var(--lightningcss-light,#14141438)var(--lightningcss-dark,#00000080)}.drawer-scrim{display:block}.main-inner{padding:22px 18px 48px}.main-inner.main-inner--combos{height:100%;min-height:0;padding:0;overflow:hidden}.main-inner.main-inner--combos>.page-head,.main-inner.main-inner--combos>.page-tabs,.main-inner.main-inner--combos>.page-sub{padding-inline:18px}.main-inner.main-inner--combos>.page-tabs{margin-inline:18px;padding-inline:0}.main-inner.main-inner--combos:not(:has(.combos-workspace-shell))>.models-tab-panel--fill:not([hidden]){padding-inline:0}.main-inner.main-inner--combos:not(:has(.combos-workspace-shell)){padding:22px 18px 48px}.main-inner.main-inner--combos:not(:has(.combos-workspace-shell))>.page-head{padding-inline:0}.main-inner.main-inner--combos:not(:has(.combos-workspace-shell))>.page-tabs{padding-inline:0}.main-inner.main-inner--combos:not(:has(.combos-workspace-shell))>.page-sub{padding-inline:0}.main-inner.main-inner--combos:not(:has(.combos-workspace-shell))>.page-tabs{margin-inline:0}.setting-row{flex-wrap:wrap}.setting-row .setting-copy{flex:100%!important}.setting-row .setting-controls{width:100%}.setting-row .setting-controls .select-sm{flex:1 1 0;min-width:0}.setting-row>.select-sm{width:100%}.api-form-row{flex-direction:column;align-items:stretch}.api-form-row .btn{width:100%;min-height:40px}.codex-auto-switch-card{flex-direction:column;align-items:stretch}.codex-auto-switch-controls{justify-content:space-between;align-items:flex-end;width:100%}.codex-auto-switch-feedback{text-align:left;margin-top:-6px}.stat-row>.stat{min-width:100px}.tbl{min-width:460px}.usage-cards{grid-template-columns:repeat(2,minmax(0,1fr))!important}.provider-quota{padding-left:16px}.prov-meta{grid-template-columns:max-content minmax(0,1fr);width:100%;display:grid}.prov-meta .chip~span:not(:last-child){display:none}.oauth-grid{grid-template-columns:minmax(0,auto) minmax(0,1fr) max-content;column-gap:8px}.sidebar .lang-toggle .select-dropdown-beside{inset:auto auto calc(100% + 6px) 0}}.usage-cards{grid-template-columns:repeat(3,minmax(0,1fr));gap:12px;margin-top:8px;display:grid}.usage-cards .stat-value{font-size:var(--text-title);font-weight:var(--weight-semibold);margin-top:4px}.usage-head{flex-wrap:wrap;align-items:flex-start}.usage-filters{flex-wrap:wrap;justify-content:flex-end;align-items:center;gap:8px;display:flex}.usage-segmented{border:1px solid var(--border);border-radius:var(--radius-pill);background:var(--surface);gap:2px;padding:2px;display:inline-flex}.usage-segmented-btn{color:var(--muted);border-radius:var(--radius-pill);cursor:pointer;font:inherit;white-space:nowrap;background:0 0;border:none;justify-content:center;align-items:center;gap:6px;padding:4px 12px;display:inline-flex}.usage-segmented-btn.active{background:var(--raised);color:var(--text);font-weight:var(--weight-semibold)}.usage-source-mark{width:var(--icon-sm);height:var(--icon-sm);object-fit:contain;flex:none}:root[data-theme=dark] .usage-source-mark--mono{filter:invert()}@media (prefers-color-scheme:dark){:root:not([data-theme=light]) .usage-source-mark--mono{filter:invert()}}@media (width<=760px){.usage-segmented-btn{min-height:var(--control-touch)}.openai-mode-row{flex-direction:column;align-items:stretch;gap:6px}.openai-mode-control{width:100%}.openai-mode-control .usage-segmented-btn{flex:1 1 0;min-width:0}}@media (width<=640px){.usage-source-btn .usage-source-label-collapsible{display:none}}@media (width<=360px){.mobile-topbar .brand .ver{display:none}.usage-filters{flex-direction:column;align-items:stretch;width:100%}.usage-segmented{width:100%}.usage-segmented-btn{flex:1 1 0}}.panel-title{font-size:var(--text-body);font-weight:var(--weight-semibold);color:var(--text);margin:0 0 12px}.panel-head{justify-content:space-between;align-items:center;gap:12px;margin-bottom:12px;display:flex}.panel-head .panel-title{margin:0}.panel-head .input{max-width:220px}.heatmap{--hm-cell:11px;--hm-gap:3px;flex-direction:column;gap:6px;padding-bottom:4px;display:flex;overflow-x:auto}.heatmap-months{font-size:var(--text-caption);color:var(--muted);width:max-content;margin-bottom:-2px;display:grid}.heatmap-day-spacer{grid-column:1}.heatmap-month{white-space:nowrap}.heatmap-body{gap:var(--hm-gap);width:max-content;display:flex}.heatmap-days{grid-template-rows:repeat(7, var(--hm-cell));row-gap:var(--hm-gap);font-size:var(--text-micro);color:var(--muted);background:var(--surface);z-index:1;flex-shrink:0;align-items:center;width:25px;display:grid;position:sticky;left:0}.heatmap-grid{gap:var(--hm-gap);display:grid}.heatmap-week{grid-template-rows:repeat(7, var(--hm-cell));gap:var(--hm-gap);display:grid}.heatmap-cell{width:var(--hm-cell);height:var(--hm-cell);border-radius:var(--radius-2xs);background:var(--border)}.heatmap-cell-0{background:var(--border)}.heatmap-cell-1{background:color-mix(in oklch, var(--green) 25%, var(--surface))}.heatmap-cell-2{background:color-mix(in oklch, var(--green) 50%, var(--surface))}.heatmap-cell-3{background:color-mix(in oklch, var(--green) 75%, var(--surface))}.heatmap-cell-4{background:var(--green)}.heatmap-legend{font-size:var(--text-label);align-self:flex-end;align-items:center;gap:4px;display:inline-flex;position:sticky;right:0}.heatmap-legend .heatmap-cell{width:10px;height:10px}.heatmap-tip{z-index:10;pointer-events:none;background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-sm);box-shadow:var(--shadow-sm);white-space:nowrap;font-size:var(--text-label);padding:6px 10px;position:fixed;transform:translate(-50%,-100%)translateY(-8px)}.heatmap-tip-date{font-weight:var(--weight-semibold);color:var(--text);margin-bottom:2px}.heatmap-tip-val{color:var(--text);font-variant-numeric:tabular-nums}.heatmap-tip-req{font-size:var(--text-caption)}.usage-bar{background:var(--border);border-radius:var(--radius-pill);width:100%;min-width:60px;height:6px;overflow:hidden}.usage-bar-fill{background:var(--green);border-radius:var(--radius-pill);height:100%}.daybars{grid-template-columns:repeat(7,1fr);align-items:end;gap:10px;height:180px;padding-top:8px;display:grid}.daybar{flex-direction:column;justify-content:flex-end;align-items:center;gap:6px;height:100%;display:flex;position:relative}.daybar-track{background:var(--border);border-radius:var(--radius-xs);flex:1;align-items:flex-end;width:100%;max-width:48px;display:flex;overflow:hidden}.daybar-stack{border-radius:var(--radius-xs) var(--radius-xs) 0 0;width:100%;height:100%;min-height:2px;transform:scaleY(var(--daybar-scale,0));transform-origin:bottom;transition:transform var(--motion-normal) ease;flex-direction:column-reverse;display:flex;overflow:hidden}.daybar-seg{width:100%;min-height:1px}.daybar-count{font-size:var(--text-label);font-weight:var(--weight-semibold);color:var(--text)}.daybar-label{font-size:var(--text-caption);white-space:nowrap}.daybar:hover .daybar-track{outline:1px solid var(--border)}.daybar-tip{z-index:5;background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-sm);min-width:160px;box-shadow:var(--shadow-sm);pointer-events:none;padding:8px 10px;position:absolute;bottom:calc(100% + 6px);left:50%;transform:translate(-50%)}.daybar-tip-date{font-size:var(--text-label);font-weight:var(--weight-semibold);color:var(--text);white-space:nowrap;margin-bottom:6px}.daybar-tip-row{font-size:var(--text-label);line-height:var(--leading-relaxed);align-items:center;gap:8px;display:flex}.daybar-tip-swatch{border-radius:var(--radius-2xs);flex-shrink:0;width:10px;height:10px}.daybar-tip-name{color:var(--text);white-space:nowrap;text-overflow:ellipsis;flex:1;max-width:160px;overflow:hidden}.daybar-tip-val{color:var(--muted);font-variant-numeric:tabular-nums}.toggle{width:var(--toggle-w);height:var(--toggle-h);flex-shrink:0;display:inline-block;position:relative}.toggle input{opacity:0;width:0;height:0;position:absolute}.toggle .slider{background:var(--toggle-off-bg);border-radius:var(--radius-pill);cursor:pointer;transition:background var(--motion-normal) ease;position:absolute;inset:0}.toggle .slider:after{content:"";width:var(--toggle-dot);height:var(--toggle-dot);background:var(--toggle-dot-color);border-radius:var(--radius-round);transition:transform var(--motion-normal) ease;position:absolute;top:50%;left:3px;transform:translateY(-50%)}.toggle input:checked+.slider{background:var(--toggle-on-bg)}.toggle input:checked+.slider:after{transform:translate(calc(var(--toggle-w) - var(--toggle-dot) - 6px), -50%)}.toggle input:focus-visible+.slider{outline:2px solid var(--accent-ring);outline-offset:2px}.toggle input:disabled+.slider{opacity:.5;cursor:not-allowed}.setting-row{justify-content:space-between;align-items:center;gap:16px;padding:12px 16px;display:flex}.setting-row+.setting-row{border-top:1px solid var(--border-soft)}.dash-delegation-summary{justify-content:space-between;align-items:center;gap:16px;display:flex}.dash-delegation-controls{flex-wrap:wrap;justify-content:flex-end;align-items:center;gap:8px;min-width:0;display:flex}.setting-label{flex-direction:column;gap:2px;min-width:0;display:flex}.setting-label .title{font-size:var(--text-body);font-weight:var(--weight-semibold);color:var(--text)}.setting-label .desc{font-size:var(--text-label);color:var(--muted);line-height:var(--leading-body)}.model-row-wrap{position:relative}.model-tip{z-index:10;background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);pointer-events:none;min-width:320px;max-width:480px;max-height:360px;font-size:var(--text-control);line-height:var(--leading-relaxed);white-space:nowrap;padding:12px 16px;overflow-y:auto;box-shadow:0 6px 20px #00000059}.model-tip.has-actions{pointer-events:auto}.model-tip-id{font-family:var(--mono);font-size:var(--text-body);font-weight:var(--weight-semibold);color:var(--text);white-space:normal;word-break:break-all;margin-bottom:2px}.model-tip-display{color:var(--muted);margin-bottom:6px}.model-tip-grid{grid-template-columns:auto 1fr;gap:4px 16px;margin-bottom:6px;display:grid}.model-tip-key{color:var(--muted)}.model-tip-val{color:var(--text);font-family:var(--mono);font-size:var(--text-label)}.model-tip-actions{border-top:1px solid var(--border-soft);gap:6px;margin-top:8px;padding-top:8px;display:flex}.ocx-tooltip{align-items:center;display:inline-flex;position:relative}.ocx-tooltip-bubble{z-index:var(--z-popover);background:color-mix(in oklab, canvas 84%, transparent);-webkit-backdrop-filter:blur(20px)saturate(1.4);border:1px solid var(--border);border-radius:var(--radius-sm);width:max-content;color:var(--text);font-size:var(--text-label);line-height:var(--leading-body);white-space:normal;pointer-events:none;animation:ocx-tooltip-in var(--motion-fast) ease-out;padding:8px 12px;position:absolute;box-shadow:0 8px 32px #00000029}.ocx-tooltip-bubble--top{bottom:calc(100% + 8px);left:50%;transform:translate(-50%)}.ocx-tooltip-bubble--bottom{top:calc(100% + 8px);left:50%;transform:translate(-50%)}.ocx-tooltip-bubble--left{top:50%;right:calc(100% + 8px);transform:translateY(-50%)}.ocx-tooltip-bubble--right{top:50%;left:calc(100% + 8px);transform:translateY(-50%)}@keyframes ocx-tooltip-in{0%{opacity:0}to{opacity:1}}.modal-backdrop-dismiss{z-index:0;cursor:pointer;background:0 0;border:0;margin:0;padding:0;position:fixed;inset:0}.claude-page{flex-direction:column;min-width:0;display:flex}.claude-page-intro .page-head{margin-bottom:6px}.claude-page-intro .page-sub{margin:4px 0 14px}.claude-tabs{border:1px solid var(--border);border-radius:var(--radius-pill);background:var(--surface);box-sizing:border-box;flex:none;align-items:stretch;gap:2px;min-height:42px;margin-bottom:22px;padding:3px;display:inline-flex}.claude-desktop-toolbar{justify-content:flex-end;margin-bottom:12px;display:flex}.claude-tabs button{border-radius:var(--radius-pill);min-width:88px;min-height:34px;color:var(--muted);font:inherit;cursor:pointer;background:0 0;border:0;padding:6px 14px;font-size:13px;font-weight:550;line-height:1.2}.claude-tabs button:hover{color:var(--text);background:var(--hover)}.claude-tabs button.active{color:var(--text);background:var(--raised);box-shadow:var(--shadow-sm)}.claude-desktop-loading{color:var(--muted);padding:28px 4px}.claude-desktop-error{flex-direction:column;align-items:flex-start;gap:12px;display:flex}.claude-desktop-head{align-items:flex-start}.claude-desktop-head .page-sub{margin-bottom:0}.claude-profile-tools,.claude-save-actions{flex-wrap:wrap;align-items:center;gap:8px;display:flex}.claude-profile-bar{z-index:8;border:1px solid var(--border);border-radius:var(--radius);background:var(--surface);box-shadow:var(--shadow-sm);justify-content:space-between;align-items:center;gap:12px;margin:18px 0 16px;padding:10px 12px;display:flex;position:sticky;top:12px}.claude-dirty{color:var(--muted);font-size:12.5px;font-weight:550}.claude-dirty.active{color:var(--amber)}.ocx-group-stack{flex-direction:column;gap:10px;display:flex}.ocx-group{border:1px solid var(--border);border-radius:var(--radius);background:var(--surface);min-width:0;overflow:hidden}.ocx-group-head{background:var(--raised);justify-content:space-between;align-items:center;gap:12px;padding:12px 14px;display:flex}.ocx-group-head.open{border-bottom:1px solid var(--border-soft)}.ocx-group-heading{flex:1;min-width:0;margin:0;font-size:14px}.ocx-group-toggle{width:100%;min-width:0;color:inherit;cursor:pointer;text-align:left;background:0 0;border:0;align-items:baseline;gap:10px;padding:0;display:flex}.ocx-group-name{font-size:14px;font-weight:600}.ocx-group-count{color:var(--muted);flex-shrink:0;font-size:11.5px}.ocx-chevron{color:var(--muted);transition:transform var(--motion-fast);flex-shrink:0;align-self:center}.claude-model-names{flex-direction:column;flex:1;gap:1px;min-width:0;display:flex}.claude-model-names strong{color:var(--text);text-overflow:ellipsis;white-space:nowrap;font-size:13px;display:block;overflow:hidden}.claude-model-names code{color:var(--text);font-family:var(--font-code);font-weight:var(--weight-semibold);font-size:var(--text-control);letter-spacing:0;text-overflow:ellipsis;white-space:nowrap;display:block;overflow:hidden}.claude-lane-default{min-width:0;color:var(--faint);font-family:var(--font-code);font-size:11px;font-weight:var(--weight-semibold);text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.claude-default-radio{color:var(--muted);cursor:pointer;align-items:center;gap:6px;font-size:11.5px;display:inline-flex}.claude-default-needed{color:var(--amber);font-size:11.5px;font-weight:550}.claude-effective-default{color:var(--amber);margin-top:6px;font-size:11px;font-weight:550;display:inline-block}.claude-lane-models{flex-direction:column;gap:9px;min-height:104px;padding:10px;display:flex}.claude-lane-search{width:calc(100% - 20px);min-height:32px;margin:10px 10px 0;font-size:12.5px}.claude-lane-more{justify-content:center;align-self:stretch}.grok-endpoint{border:1px solid var(--border);border-radius:var(--radius);background:var(--surface);font-size:var(--text-control);align-items:center;gap:10px;margin:14px 0 6px;padding:10px 14px;display:flex}.grok-endpoint>span{color:var(--muted)}.grok-model-list{flex-direction:column;display:flex}.grok-model-row{border-top:1px solid var(--border-soft);align-items:center;gap:12px;padding:7px 14px;display:flex}.grok-model-row:first-child{border-top:0}.grok-model-names{flex-direction:column;flex:1;gap:1px;min-width:0;display:flex}.grok-model-names strong{color:var(--text);text-overflow:ellipsis;white-space:nowrap;font-size:12.5px;overflow:hidden}.grok-model-names code{color:var(--muted);text-overflow:ellipsis;white-space:nowrap;font-size:10.5px;overflow:hidden}.claude-lane-empty{border:1px dashed var(--border);border-radius:var(--radius-sm);min-height:82px;color:var(--muted);text-align:center;place-items:center;padding:12px;font-size:12px;display:grid}.claude-model-card{border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--bg);box-shadow:var(--shadow-sm)}.claude-model-card[draggable=true]{cursor:grab}.claude-model-card[draggable=true]:active{cursor:grabbing}.claude-model-summary{width:100%;color:inherit;cursor:pointer;text-align:left;background:0 0;border:0;align-items:center;gap:10px;padding:9px 12px;display:flex}.claude-model-summary:hover{background:var(--hover)}.claude-model-context{color:var(--muted);flex-shrink:0;font-size:11px}.claude-model-context-unknown{color:var(--faint);font-style:italic}.claude-row-default{color:var(--green);flex-shrink:0;font-size:10.5px;font-weight:600}.claude-1m-chip{border-radius:var(--radius-xs);background:color-mix(in srgb, var(--accent) 15%, transparent);color:var(--accent);letter-spacing:.02em;flex-shrink:0;padding:1px 6px;font-size:10px;font-weight:700}.claude-model-body{border-top:1px solid var(--border-soft);padding:10px 12px 12px}.claude-field{margin-top:10px;display:block}.claude-model-body>.claude-field:first-child{margin-top:0}.claude-field>span,.claude-move-row>label{color:var(--muted);margin-bottom:4px;font-size:11.5px;font-weight:550;display:block}.claude-alias{border:1px solid var(--border);border-radius:var(--radius-xs);background:var(--raised);width:100%;min-height:34px;color:var(--text);text-overflow:ellipsis;white-space:nowrap;padding:7px 10px;font-size:11.5px;display:block;overflow:hidden}.claude-default-radio{color:var(--text);margin-top:10px}.claude-move-row{grid-template-columns:minmax(0,1fr) auto;align-items:end;gap:6px;margin-top:10px;display:grid}.claude-move-row>label{grid-column:1/-1;margin:0}.claude-move-row .input{min-width:0;height:34px;padding-block:4px}.sr-only{clip:rect(0, 0, 0, 0);white-space:nowrap;border:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}@media (width<=760px){.claude-tabs{width:100%;display:flex}.claude-tabs button{flex:1;min-height:44px}.claude-desktop-head{flex-direction:column}.claude-profile-tools{width:100%}.claude-profile-tools .btn{flex:1;min-height:44px}.claude-profile-bar{flex-direction:column;align-items:stretch;top:8px}.claude-save-actions .btn{flex:1;min-height:44px}.ocx-group-head{align-items:flex-start}.claude-move-row .input,.claude-move-row .btn{min-height:44px}}.claude-status-bar{border:1px solid var(--border);border-radius:var(--radius);background:var(--surface);color:var(--muted);align-items:center;gap:10px;margin-bottom:14px;padding:8px 14px;font-size:12.5px;display:flex}.claude-status-bar.applied{border-color:var(--green)}.claude-status-bar.stale{border-color:var(--amber)}.claude-status-bar.not-applied,.claude-status-bar.pending{border-color:var(--border)}.claude-status-dot{background:var(--muted);border-radius:50%;flex-shrink:0;width:8px;height:8px}.claude-status-bar.applied .claude-status-dot{background:var(--green)}.claude-status-bar.stale .claude-status-dot{background:var(--amber)}.claude-status-health{margin-left:auto;font-size:11.5px}.claude-effort-badge{border-radius:var(--radius-xs);letter-spacing:.02em;flex-shrink:0;padding:1px 6px;font-size:10px;font-weight:600;display:inline-block}.claude-effort-badge.on{background:color-mix(in srgb, var(--green) 15%, transparent);color:var(--green)}.claude-effort-badge.off{background:color-mix(in srgb, var(--muted) 12%, transparent);color:var(--muted)} diff --git a/go/internal/embeddedui/static/index.html b/go/internal/embeddedui/static/index.html index 999f63007d..45c4fa01a8 100644 --- a/go/internal/embeddedui/static/index.html +++ b/go/internal/embeddedui/static/index.html @@ -2,24 +2,77 @@ - opencodex · proxy dashboard + + + +
+

opencodex proxy dashboard

+

The full dashboard build is not available in this standalone runtime.

+

Status: checking…

+

+ +

Release builds embed the full Vite dashboard; this page is the + offline fallback when no GUI build ships with the runtime. Management API: + http://127.0.0.1:/api/

+
- - - - -
diff --git a/scripts/sync-go-embedded-dashboard.sh b/scripts/sync-go-embedded-dashboard.sh index af5197afe2..e296b97e48 100755 --- a/scripts/sync-go-embedded-dashboard.sh +++ b/scripts/sync-go-embedded-dashboard.sh @@ -1,7 +1,14 @@ #!/usr/bin/env bash -# Refresh the Go release binary's go:embed tree from the Vite dashboard build. -# This runs only on a release build host. A small checked-in snapshot remains so -# go build works for source users and CI without Bun installed. +# Refresh the embedded dashboard build for a release ocx binary. The Vite +# output is staged into go/internal/embeddedui/static/assets/, which is +# gitignored: generated build output is never committed (matching the +# repository-wide gui/dist convention), and the release build of ./cmd/ocx +# picks the staged build up through go:embed. +# +# Checked-in static/ content stays source-only (the thin fallback page and the +# gui/public icon mirrors), so `go build` keeps working offline for source +# users and CI without Bun: the binary serves the staged build when present and +# the embedded fallback page otherwise. set -euo pipefail repo_root="$(cd "$(dirname "$0")/.." && pwd)" @@ -15,7 +22,12 @@ cd "$repo_root/gui" "$bun_bin" install --frozen-lockfile "$bun_bin" run build [ -f dist/index.html ] || { echo "sync-go-embedded-dashboard: gui/dist/index.html missing after build" >&2; exit 1; } -target="$repo_root/go/internal/embeddedui/static" -find "$target" -mindepth 1 -delete -cp -R dist/. "$target/" -printf 'embedded dashboard refreshed from %s\n' "$repo_root/gui/dist" +# Overlay only the generated bundle into the embed tree. The static root keeps +# its tracked source files; assets/ is replaced wholesale because Vite hashes +# every filename on each build. +target="$repo_root/go/internal/embeddedui/static/assets" +rm -rf "$target" +mkdir -p "$target" +cp dist/assets/* "$target/" +cp dist/index.html "$repo_root/go/internal/embeddedui/static/index.html" +printf 'embedded dashboard refreshed from %s (assets staged in gitignored static/assets)\n' "$repo_root/gui/dist" From 3944e83bf12de3f48f765418cb91db7ff04d70dd Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Mon, 7 Sep 2026 18:49:59 +0800 Subject: [PATCH 112/165] test(go): live-gateway hot-path differential harness (#34) The fixture-upstream suites prove byte parity against replies this repo fabricates. Add the missing slice: the same TS-vs-Go-sidecar differential run against a real provider gateway serving the OpenAI Responses wire format (non-stream and SSE streaming). The gateway is pointed at by OCX_LIVE_GATEWAY_URL (default the LAN gateway on 127.0.0.1:20100) with OCX_LIVE_GATEWAY_MODEL (default glm-5.3-flash) and no key by default. Both opencodex listeners reach the gateway through a local passthrough fixture whose User-Agent log proves path ownership: the TypeScript oracle must never present Go's client UA, and relay-admitted requests must. Real responses are non-deterministic where fixture replies are not, so the differential compares captures after normalising exactly the declared-volatile set: gateway-minted ids, timestamps, token usage, encrypted reasoning blobs, and sequence counters. Scoped synthetic ids (`msg_ocx__`) also fold, but that branch is defensive shape normalisation: the plain backfill path mints deterministic, scope-less ids on both sides and this suite never arms stateful item-id repair (stream-time repairs are gated to the Bun bridge), so it only fires for a future gateway that serves scoped ids while armed; the stateful-repair scope RNG parity (TS randomUUID vs Go crypto/rand hex) is owned by deepseek-responses-item-id-repair.test.ts and the go-hotpath-relay tools case, not this suite (#31 note). Output-text values fold to lowercase because the model occasionally answers a fixed instruction with a case variant. Event-type sequence, item ordering, structure, and status are asserted exactly; SSE streams must terminate on response.completed with identical event order. The suite skips cleanly when the gateway or the Go toolchain is unavailable, so CI environments without the LAN gateway lose nothing. Verified: 3/3 pass against the live gateway after review close-out (deleted the dead header-comparison helpers); typecheck clean; unreachable-gateway skip path exercised. Co-Authored-By: Claude Code --- tests/go-hotpath-live-gateway.test.ts | 388 ++++++++++++++++++++++++++ 1 file changed, 388 insertions(+) create mode 100644 tests/go-hotpath-live-gateway.test.ts diff --git a/tests/go-hotpath-live-gateway.test.ts b/tests/go-hotpath-live-gateway.test.ts new file mode 100644 index 0000000000..45ceceac10 --- /dev/null +++ b/tests/go-hotpath-live-gateway.test.ts @@ -0,0 +1,388 @@ +import { afterAll, beforeAll, describe, expect, test } from "bun:test"; +import { mkdtempSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { SERVER_BUDGET_MS } from "./helpers/test-budget"; +import { saveConfig } from "../src/config"; +import { startServer } from "../src/server"; +import { + GO_SIDECAR_BIN_ENV, + activeGoSidecarBaseUrl, + resetGoSidecarForTests, +} from "../src/server/go-sidecar"; +import { HOT_PATH_RELAY_ENV, HOT_PATH_SEAM_ENV } from "../src/server/hot-path-seam"; +import { removeTreeWithRetry } from "./helpers/remove-tree"; + +/** + * Live-provider hot-path differential (ticket #34). + * + * The fixture-upstream suites (go-hotpath-relay, go-hotpath-relay-streaming) + * prove byte parity against replies this repo fabricates. This suite closes + * the remaining gap: the same differential run against a REAL provider + * gateway, so both implementations are exercised on responses neither was + * tuned to. The gateway serves the OpenAI Responses wire format + * (`/v1/responses`, SSE streaming included). + * + * Real responses are non-deterministic where fixture replies are not: + * response/item ids, timestamps, token usage, and encrypted reasoning blobs + * differ on every call. The differential therefore compares each capture + * after normalising exactly those declared-volatile fields — everything else + * (event types, item ordering, output text, status, content types) must be + * identical between the TypeScript oracle and the armed Go relay. + * + * The id fold is defensive rather than evidence-bearing: the plain backfill + * path mints deterministic, scope-less `msg_ocx_` ids on both sides, + * and this suite never arms stateful item-id repair (stream-time repairs are + * gated to the Bun bridge, so arming would break the UA path-ownership claim), + * so the scoped-id branch (`msg_ocx__` → `msg_ocx_SCOPE_`) + * only fires for a future Responses-compatible gateway that serves scoped ids + * while armed. The stateful-repair scope bytes themselves (TypeScript randomUUID + * vs Go crypto/rand hex) are contractually compared elsewhere — by + * deepseek-responses-item-id-repair.test.ts and the go-hotpath-relay tools + * case — not by this suite (#31 note). + * + * The upstream User-Agent still proves path ownership: the TS oracle must + * never present Go's client UA, and relay-admitted requests must. + * + * Opt-in by reachability: the suite skips entirely when the gateway (or the + * Go toolchain) is unavailable, so CI environments without the LAN gateway + * lose nothing. Point OCX_LIVE_GATEWAY_URL at any Responses-compatible + * upstream to run it elsewhere. + */ + +const gatewayBase = (process.env.OCX_LIVE_GATEWAY_URL ?? "http://127.0.0.1:20100").replace(/\/+$/, ""); +const gatewayModel = process.env.OCX_LIVE_GATEWAY_MODEL ?? "glm-5.3-flash"; +const gatewayApiKey = process.env.OCX_LIVE_GATEWAY_KEY ?? ""; +const GO_UA = "Go-http-client/1.1"; + +async function gatewayReachable(): Promise { + try { + const response = await fetch(new URL("/v1/models", gatewayBase), { + signal: AbortSignal.timeout(3_000), + headers: gatewayApiKey ? { authorization: `Bearer ${gatewayApiKey}` } : {}, + }); + return response.ok; + } catch { + return false; + } +} + +function goToolchainAvailable(): boolean { + return Bun.spawnSync(["go", "version"], { stdout: "ignore", stderr: "ignore" }).success; +} + +function buildSidecarBinary(): string { + const dir = mkdtempSync(join(tmpdir(), "ocx-go-sidecar-live-")); + const binPath = join(dir, process.platform === "win32" ? "ocx-sidecar.exe" : "ocx-sidecar"); + const build = Bun.spawnSync( + ["go", "build", "-o", binPath, "./cmd/ocx-sidecar"], + { + cwd: join(import.meta.dir, "..", "go"), + env: { ...process.env, CGO_ENABLED: "0" }, + stdout: "pipe", + stderr: "pipe", + }, + ); + if (build.exitCode !== 0) { + throw new Error( + `go build ./cmd/ocx-sidecar failed (${build.exitCode}):\n${new TextDecoder().decode(build.stderr)}`, + ); + } + return binPath; +} + +const goAvailable = goToolchainAvailable(); +const live = goAvailable ? await gatewayReachable() : false; +const sidecarBinary = goAvailable && live ? buildSidecarBinary() : null; + +interface UpstreamLog { + ua: string; + method: string; + path: string; + contentType: string | null; + body: string; +} + +interface ResponseCapture { + status: number; + contentType: string | null; + body: string; +} + +const upstreamLogs: UpstreamLog[] = []; +let upstream: ReturnType | null = null; + +// Deterministic instruction prompts: the gateway answers these identically on +// every call, so the non-volatile bytes of both runs are comparable. +const liveCases = [ + { name: "single word", input: "Reply with exactly one word: blue" }, + { name: "two words", input: "Reply with exactly two words: hot dog" }, + { name: "markdown shape", input: "Reply with exactly: **bold**" }, +] as const; + +const streamCase = { name: "stream single word", input: "Reply with exactly one word: red" } as const; + +/** + * Declared-volatile normalisation for a live capture. + * + * - ids: every gateway-minted id is fresh per call. Ids share the `_` + * shape (resp_/msg_/rs_/fc_ + opaque token) and are normalised to `_VOLATILE`. + * Scoped synthetic ids (`msg_ocx_<32hex>_`) fold to + * `msg_ocx_SCOPE_` so the index position stays comparable. Defensive: + * this suite never arms stateful repair, so only a future gateway serving + * scoped ids would exercise it (see the file header for where the repair RNG + * bytes ARE compared). + * - timestamps / usage counts / encrypted reasoning: recomputed per call. + * - `sequence_number`: both relays replay the upstream order, but the two + * gateway calls are independent streams, so the absolute counters ride the + * same volatile set (the ORDER of event types is still asserted exactly). + * - output text: the model occasionally answers a fixed instruction with a + * case variant ("Hot dog" vs "hot dog") even when repeated runs are stable, + * so output_text/delta/text payload VALUES fold to lowercase. Everything + * around them (event types, item structure, annotations, ordering) is kept. + */ +function normalise(raw: string): string { + return raw + .replace(/"(resp|msg|rs|fc)_[0-9a-f]{16,}"/g, '"$1_VOLATILE"') + .replace(/"(msg|rs|fc)_ocx_[0-9a-f]+_(\d+)"/g, '"$1_ocx_SCOPE_$2"') + .replace(/"(created_at|created)":\s*\d+/g, '"$1":0') + .replace(/"(input_tokens|output_tokens|total_tokens|cached_tokens|reasoning_tokens)":\s*\d+/g, '"$1":0') + .replace(/"(encrypted_content|reasoning_content)":\s*"[^"]*"/g, '"$1":"VOLATILE"') + .replace(/"sequence_number":\s*\d+/g, '"sequence_number":0') + .replace(/"(text|delta)":\s*"([^"]*)"/g, (_, field: string, value: string) => `"${field}":"${value.toLowerCase()}"`); +} + +function configFixture(upstreamPort: number): Record { + return { + port: 0, + hostname: "127.0.0.1", + defaultProvider: "live", + providers: { + live: { + adapter: "openai-responses", + baseUrl: `http://127.0.0.1:${upstreamPort}/v1`, + allowPrivateNetwork: true, + disabled: false, + models: [gatewayModel], + ...(gatewayApiKey ? { apiKey: gatewayApiKey } : {}), + }, + }, + }; +} + +async function postResponses(server: { url: URL }, token: string, body: unknown): Promise { + const response = await fetch(new URL("/v1/responses", server.url), { + method: "POST", + headers: { "content-type": "application/json", "x-opencodex-api-key": token }, + body: JSON.stringify(body), + }); + return { + status: response.status, + contentType: response.headers.get("content-type"), + body: await response.text(), + }; +} + +/** Event-type sequence of an SSE stream: the ordering contract, volatile-free. */ +function sseEventTypes(raw: string): string[] { + return [...raw.matchAll(/^event:\s*(.+)$/gm)].map((match) => match[1]!.trim()); +} + +const previousEnv: Record = {}; +let testHome = ""; + +function captureEnv(): void { + for (const name of [GO_SIDECAR_BIN_ENV, HOT_PATH_SEAM_ENV, HOT_PATH_RELAY_ENV, "OPENCODEX_HOME", "OPENCODEX_API_AUTH_TOKEN"]) { + previousEnv[name] = process.env[name]; + } +} + +function setUpFixture(upstreamPort: number): void { + testHome = mkdtempSync(join(tmpdir(), "ocx-hotpath-live-")); + process.env.OPENCODEX_HOME = testHome; + process.env.OPENCODEX_API_AUTH_TOKEN = "data-secret"; + saveConfig(configFixture(upstreamPort) as Parameters[0]); +} + +function tearDownFixture(): void { + resetGoSidecarForTests(); + for (const [name, value] of Object.entries(previousEnv)) { + if (value === undefined) delete process.env[name]; + else process.env[name] = value; + } + if (testHome) { + removeTreeWithRetry(testHome); + testHome = ""; + } +} + +async function waitFor(probe: () => T | null | undefined, timeoutMs: number): Promise { + const deadline = Date.now() + timeoutMs; + for (;;) { + const value = probe(); + if (value !== null && value !== undefined) return value; + if (Date.now() >= deadline) throw new Error(`condition not met within ${timeoutMs}ms`); + await Bun.sleep(50); + } +} + +function runLiveTest(name: string, fn: () => Promise): void { + test( + name, + async () => { + captureEnv(); + try { + await fn(); + } finally { + tearDownFixture(); + } + }, + // Two full server lifetimes plus real-gateway inference round-trips per + // case are intrinsic waits (test-budget rule 1), so double the server + // budget rather than sizing to a local timing. + SERVER_BUDGET_MS * 2, + ); +} + +// A minimal passthrough to the real gateway. The opencodex listeners connect +// to THIS fixture, the fixture forwards to the gateway, and the logs prove +// which implementation performed the upstream hop. +describe.skipIf(!goAvailable || sidecarBinary === null || !live)("ocx-sidecar live-gateway hot-path differential (ADR-0008, ticket #34)", () => { + beforeAll(() => { + upstream = Bun.serve({ + hostname: "127.0.0.1", + port: 0, + async fetch(req) { + const incoming = new URL(req.url); + const headers = new Headers(); + for (const name of ["content-type", "accept", "authorization", "api-key"]) { + const value = req.headers.get(name); + if (value) headers.set(name, value); + } + const body = await req.text(); + upstreamLogs.push({ + ua: req.headers.get("user-agent") ?? "", + method: req.method, + path: incoming.pathname, + contentType: req.headers.get("content-type"), + body, + }); + const upstreamResponse = await fetch(new URL(incoming.pathname + incoming.search, gatewayBase), { + method: req.method, + headers, + body, + }); + return new Response(upstreamResponse.body, { + status: upstreamResponse.status, + headers: { "content-type": upstreamResponse.headers.get("content-type") ?? "application/json" }, + }); + }, + }); + }); + + afterAll(() => { + upstream?.stop(true); + upstream = null; + }); + + test("gateway is reachable", () => { + expect(live).toBe(true); + }); + + runLiveTest("non-streaming responses match the TS oracle after volatile normalisation", async () => { + const token = "data-secret"; + const port = upstream!.port; + + setUpFixture(port); + const serverA = startServer(0); + const tsCaptures: ResponseCapture[] = []; + try { + for (const c of liveCases) { + tsCaptures.push(await postResponses(serverA, token, { model: gatewayModel, input: c.input })); + } + } finally { + await serverA.stop(true); + } + const oracleLogs = upstreamLogs.slice(0, liveCases.length); + for (const log of oracleLogs) { + expect(log.ua, "TS oracle must never present the Go client UA").not.toBe(GO_UA); + expect(log.path).toBe("/v1/responses"); + } + + process.env[GO_SIDECAR_BIN_ENV] = sidecarBinary!; + process.env[HOT_PATH_SEAM_ENV] = "1"; + process.env[HOT_PATH_RELAY_ENV] = "1"; + const serverB = startServer(0); + const goCaptures: ResponseCapture[] = []; + try { + await waitFor(() => activeGoSidecarBaseUrl(), 15_000); + for (const c of liveCases) { + goCaptures.push(await postResponses(serverB, token, { model: gatewayModel, input: c.input })); + } + } finally { + await serverB.stop(true); + } + const goLogs = upstreamLogs.slice(liveCases.length); + expect(goLogs).toHaveLength(liveCases.length); + for (const log of goLogs) { + expect(log.ua, "relay-admitted live request must be Go-owned").toBe(GO_UA); + expect(log.path).toBe("/v1/responses"); + } + + for (let i = 0; i < liveCases.length; i++) { + const c = liveCases[i]!; + const ts = tsCaptures[i]!; + const go = goCaptures[i]!; + expect(go.status, `${c.name} status`).toBe(ts.status); + expect((go.contentType ?? "").split(";")[0], `${c.name} content type`).toBe((ts.contentType ?? "").split(";")[0]); + expect(normalise(go.body), `${c.name} normalised body must match the TS oracle`).toBe(normalise(ts.body)); + // Non-vacuous: the normaliser must not erase the response payload. + expect(normalise(go.body), `${c.name} keeps output text`).toContain("output_text"); + } + expect(activeGoSidecarBaseUrl()).toBeNull(); + }); + + runLiveTest("streaming responses match the TS oracle event-for-event after normalisation", async () => { + const token = "data-secret"; + const port = upstream!.port; + const body = { model: gatewayModel, input: streamCase.input, stream: true }; + + setUpFixture(port); + const serverA = startServer(0); + let tsCapture: ResponseCapture; + try { + tsCapture = await postResponses(serverA, token, body); + } finally { + await serverA.stop(true); + } + const oracleLog = upstreamLogs[upstreamLogs.length - 1]!; + expect(oracleLog.ua, "TS oracle stream must never present the Go client UA").not.toBe(GO_UA); + + process.env[GO_SIDECAR_BIN_ENV] = sidecarBinary!; + process.env[HOT_PATH_SEAM_ENV] = "1"; + process.env[HOT_PATH_RELAY_ENV] = "1"; + const serverB = startServer(0); + let goCapture: ResponseCapture; + try { + await waitFor(() => activeGoSidecarBaseUrl(), 15_000); + goCapture = await postResponses(serverB, token, body); + } finally { + await serverB.stop(true); + } + const goLog = upstreamLogs[upstreamLogs.length - 1]!; + expect(goLog.ua, "relay-admitted live stream must be Go-owned").toBe(GO_UA); + + expect(goCapture.status, "stream status").toBe(tsCapture.status); + expect((goCapture.contentType ?? "").split(";")[0], "stream content type").toBe((tsCapture.contentType ?? "").split(";")[0]); + + const tsEvents = sseEventTypes(tsCapture.body); + const goEvents = sseEventTypes(goCapture.body); + expect(goEvents, "SSE event-type sequence must match the TS oracle").toEqual(tsEvents); + expect(tsEvents.length, "live stream carries a full event sequence").toBeGreaterThan(2); + expect(tsEvents[tsEvents.length - 1], "stream terminates on response.completed").toBe("response.completed"); + + expect(normalise(goCapture.body), "normalised SSE bytes must match the TS oracle").toBe(normalise(tsCapture.body)); + expect(normalise(goCapture.body), "stream keeps the output text").toContain("output_text.delta"); + expect(activeGoSidecarBaseUrl()).toBeNull(); + }); +}); From cd529df1e78221d6df87f90f5cec6c1ec417ffde Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Mon, 7 Sep 2026 22:20:30 +0800 Subject: [PATCH 113/165] feat(ci): activate Go release artifact gate with release-path verification (#42) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Go binary is the release runtime (ADR-0008 increment 7): release.yml attaches static cross-platform ocx binaries built by scripts/build-go-release-artifact.sh, so the release path must not trust an unverified producer. The #42 scaffold workflow (dispatch-only until #40 closed) is now the release-artifact gate that keeps that producer honest. - go-release-artifacts.yml: active on pull_request and push to main/preview/dev (paths: package.json — the -ldflags stamp authority, go/**, both artifact scripts, both workflows) plus manual dispatch. A changes job (dorny/paths-filter) gates the expensive jobs so a skipped job reports success rather than leaving a check pending forever. The verify job runs go build/vet/test under CGO_ENABLED=0 and smokes the linux/amd64 candidate exactly as a release consumes it: static ELF plus --version printed from a directory with no package.json proving the -ldflags stamp. A build-release-artifact matrix cross-compiles all five release targets through the same script and asserts each artifact's format/arch (ELF 64-bit x86-64/aarch64 statically linked, Mach-O x86_64/arm64, PE32+ x86-64). - release.yml: now requires a successful go-release-artifacts push run for the exact GITHUB_SHA before publishing, mirroring the existing ci.yml gate, so a release only attaches binaries CI has proven. Artifact build/attach steps (added with the #41 flip) unchanged; comments tie them to #42. - build-go-release-artifact.sh: header updated from staging-only helper to the single artifact builder shared by the gate workflow and release.yml. - tests/ci-workflows.test.ts: pins the gate's triggers, permissions, immutable action refs, allowlist equality on trigger and filter, matrix targets, smoke assertions, and release.yml's build/create/attach ordering plus the new gate lookup shape. - go/README.md and structure/06_docs-and-release.md document the gate; devlog 043 records the increment. Co-Authored-By: Claude Code --- .github/workflows/go-release-artifacts.yml | 157 ++++++++++++++++-- .github/workflows/release.yml | 30 +++- .../043_release_pipeline_go_artifacts.md | 48 ++++++ go/README.md | 10 +- scripts/build-go-release-artifact.sh | 9 +- structure/06_docs-and-release.md | 1 + tests/ci-workflows.test.ts | 137 +++++++++++++++ 7 files changed, 373 insertions(+), 19 deletions(-) create mode 100644 devlog/_plan/260905_go_sidecar_takeover/043_release_pipeline_go_artifacts.md diff --git a/.github/workflows/go-release-artifacts.yml b/.github/workflows/go-release-artifacts.yml index 3638f9f073..157c602a0b 100644 --- a/.github/workflows/go-release-artifacts.yml +++ b/.github/workflows/go-release-artifacts.yml @@ -1,8 +1,34 @@ -name: Go release artifact scaffold +name: Go release artifact gate +# Ticket #42: the Go binary is the release runtime (ADR-0008 increment 7), so +# the release path must build the same static cross-platform artifact CI has +# verified. #40 proved the artifact embeds every runtime asset (dashboard, CLI, +# identity); this workflow is that verification. It builds the exact release +# artifact via scripts/build-go-release-artifact.sh — the same script release.yml +# runs when it attaches ocx binaries to a release tag — and smokes the result. +# release.yml's own artifact build/attach steps (added with the #41 flip) remain +# the producer; this workflow is the gate that keeps the producer honest. on: - # This workflow deliberately stays outside the release path until #40 proves - # that the Go artifact embeds every runtime asset required by a clean host. + # No base-branch filter on purpose (mirrors ci.yml): GitHub matches + # `branches:` against the BASE ref, which would silently exclude stacked child + # PRs. The `changes` job below is the real scope gate; a skipped job reports + # success, where a skipped *workflow* would leave its check pending forever. + pull_request: {} + # Pinned to the integration lines (mirrors ci.yml): the release path lives on + # main/preview, and dev is where the work queues before promotion. + push: + branches: [main, preview, dev] + paths: + # package.json is the version authority the artifact's -ldflags stamp + # reads, so a release commit (which bumps it) must re-run the gate even + # when no Go file changed — the same reason ci.yml's push allowlist + # carries package.json. + - "package.json" + - "go/**" + - "scripts/build-go-release-artifact.sh" + - "scripts/sync-go-embedded-dashboard.sh" + - ".github/workflows/go-release-artifacts.yml" + - ".github/workflows/release.yml" workflow_dispatch: permissions: @@ -13,10 +39,70 @@ concurrency: cancel-in-progress: true jobs: + # Same paths-filter shape as ci.yml's `changes` job: on a pull request the + # filter decides whether the expensive verification jobs need to run, and the + # validation step fails the job (rather than producing a malformed output) + # when the filter misbehaves. + changes: + name: changes + runs-on: ubuntu-latest + timeout-minutes: 5 + permissions: + contents: read + pull-requests: read + outputs: + go: ${{ steps.scope.outputs.go }} + steps: + - name: Checkout + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + with: + persist-credentials: false + + - name: Detect changed Go release surface + id: filter + uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2 + with: + # Mirrors the push trigger's path allowlist above; keep the two in + # sync. On `pull_request` the action uses the PR's own file list; on a + # branch push it means "compare against the previous commit on this + # branch". + base: ${{ github.ref }} + filters: | + go: + - 'package.json' + - 'go/**' + - 'scripts/build-go-release-artifact.sh' + - 'scripts/sync-go-embedded-dashboard.sh' + - '.github/workflows/go-release-artifacts.yml' + - '.github/workflows/release.yml' + + - name: Assert the scope output is usable + id: scope + shell: bash + env: + GO_SCOPE: ${{ steps.filter.outputs.go }} + run: | + set -euo pipefail + case "$GO_SCOPE" in + true|false) + printf 'go=%s\n' "$GO_SCOPE" >> "$GITHUB_OUTPUT" + ;; + *) + printf '::error::changes.outputs.go was %q, expected true or false\n' "$GO_SCOPE" + exit 1 + ;; + esac + verify-go-runtime: name: verify Go runtime + needs: changes + if: github.event_name != 'pull_request' || needs.changes.outputs.go == 'true' runs-on: ubuntu-latest - timeout-minutes: 10 + timeout-minutes: 20 + env: + # The release artifact is built static per ADR-0008; the build script sets + # the same flag, and this job keeps go build/vet/test consistent with it. + CGO_ENABLED: "0" steps: - name: Checkout uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 @@ -36,35 +122,61 @@ jobs: run: | set -euo pipefail cd go - CGO_ENABLED=0 go build -buildvcs=false ./... + go build -buildvcs=false ./... go vet ./... go test ./... - - name: Smoke test Linux release candidate + - name: Build and smoke-test the Linux release artifact + # The release path stamps the package version with -ldflags, so the + # smoke runs the built candidate from a directory with no package.json: + # only a real stamp prints the exact version. The file check asserts + # the binary is genuinely static — CGO_ENABLED=0 plus a cgo-free + # platform surface is what makes the release artifact self-contained. run: | set -euo pipefail scripts/build-go-release-artifact.sh linux/amd64 .tmp/go-release/linux-amd64 - .tmp/go-release/linux-amd64/ocx-linux-amd64 --version + candidate="$GITHUB_WORKSPACE/.tmp/go-release/linux-amd64/ocx-linux-amd64" + test -x "$candidate" || { echo "::error::release artifact is not executable: $candidate"; exit 1; } + file "$candidate" | tee /dev/stderr | grep -qE "ELF 64-bit.*statically linked" || { echo "::error::linux/amd64 artifact is not a static ELF binary"; exit 1; } + + expected="opencodex $(sed -n 's/^[[:space:]]*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$GITHUB_WORKSPACE/package.json" | head -n 1)" + actual="$(cd /tmp && exec "$candidate" --version)" + if [ "$actual" != "$expected" ]; then + echo "::error::artifact --version printed ${actual@Q}, expected ${expected@Q} (ldflags stamp missing or stale)" + exit 1 + fi + echo "release artifact identity: $actual" build-release-artifact: name: build ${{ matrix.target }} - needs: verify-go-runtime + needs: [changes, verify-go-runtime] + if: github.event_name != 'pull_request' || needs.changes.outputs.go == 'true' runs-on: ubuntu-latest - timeout-minutes: 10 + timeout-minutes: 15 strategy: fail-fast: false matrix: include: - target: linux/amd64 artifact: ocx-linux-amd64 + format: ELF 64-bit + arch: x86-64 - target: linux/arm64 artifact: ocx-linux-arm64 + format: ELF 64-bit + arch: aarch64 - target: darwin/amd64 artifact: ocx-darwin-amd64 + format: Mach-O + arch: x86_64 - target: darwin/arm64 artifact: ocx-darwin-arm64 + format: Mach-O + arch: arm64 - target: windows/amd64 artifact: ocx-windows-amd64 + format: PE32+ + arch: x86-64 steps: - name: Checkout uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 @@ -80,8 +192,31 @@ jobs: - name: Setup project Bun for embedded dashboard uses: ./.github/actions/setup-project-bun - - name: Cross-compile static ocx candidate - run: scripts/build-go-release-artifact.sh '${{ matrix.target }}' dist + # Matrix values reach shell via env (repo convention: generated command + # input is a trust boundary — ci.yml passes TEST_SHARD the same way). + - name: Cross-compile static ocx release candidate + env: + TARGET: ${{ matrix.target }} + run: scripts/build-go-release-artifact.sh "$TARGET" dist + + - name: Verify the artifact format + # A platform-specific dependency that leaks cgo (or a build tag + # mistake) would surface here as a dynamic binary or a missing file, + # exactly where the release path would have shipped it. ELF rows also + # assert the static link; `file` describes Mach-O/PE32+ consistently + # enough that the format tag plus architecture is the reliable check. + env: + EXPECTED_FORMAT: ${{ matrix.format }} + EXPECTED_ARCH: ${{ matrix.arch }} + run: | + set -euo pipefail + description="$(file dist/ocx-*)" + echo "$description" + echo "$description" | grep -F "$EXPECTED_FORMAT" >/dev/null || { echo "::error::artifact format mismatch, wanted ${EXPECTED_FORMAT}"; exit 1; } + echo "$description" | grep -F "$EXPECTED_ARCH" >/dev/null || { echo "::error::artifact architecture mismatch, wanted ${EXPECTED_ARCH}"; exit 1; } + case "$EXPECTED_FORMAT" in + ELF*) echo "$description" | grep -F "statically linked" >/dev/null || { echo "::error::ELF artifact is not statically linked"; exit 1; } ;; + esac - name: Upload candidate uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5fa1c37687..50c4883944 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -244,6 +244,28 @@ jobs: echo "Cross-platform CI passed for ${GITHUB_SHA}: ${ci_url}" + # ADR-0008 increment 7 (#42): the Go binary is the release runtime, + # and the go-release-artifacts gate verifies the exact binaries this + # workflow attaches. Require its successful push run for this SHA the + # same way ci.yml is required above — a release must never attach + # artifacts CI has not proven. + go_gate_url="$( + gh run list \ + --workflow go-release-artifacts.yml \ + --branch "${GITHUB_REF#refs/heads/}" \ + --commit "$GITHUB_SHA" \ + --event push \ + --limit 10 \ + --json conclusion,url \ + --jq '[.[] | select(.conclusion == "success")][0].url // ""' + )" + if [ -z "$go_gate_url" ]; then + echo "::error::No successful Go release artifact gate run found for ${GITHUB_SHA} on ${GITHUB_REF#refs/heads/} (push event). Wait for the promotion run to pass before releasing." + gh run list --workflow go-release-artifacts.yml --commit "$GITHUB_SHA" --limit 10 || true + exit 1 + fi + echo "Go release artifact gate passed for ${GITHUB_SHA}: ${go_gate_url}" + # Service baseline (lineage-relative): merged tags only, so the # changed-files gate compares against the last release actually # reachable from this commit. The release-notes baseline below uses the @@ -387,9 +409,11 @@ jobs: npm view @bitkyc08/opencodex versions dist-tags --json || true exit 1 - # ADR-0008 increment 7 (#41): the Go binary is the release runtime. Build the - # static cross-platform artifacts and attach them to the GitHub Release; the - # npm package remains the source distribution channel, but every release tag + # ADR-0008 increment 7: the Go binary is the release runtime. #41 flipped + # the runtime to Go and added these steps; #42 added the go-release-artifacts + # gate above that verifies every release target through the same script, so + # a release only attaches binaries CI has proven build static. The npm + # package remains the source distribution channel, but every release tag # now ships a TypeScript-free single-binary artifact. - name: Setup Go for single-binary artifacts uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5 diff --git a/devlog/_plan/260905_go_sidecar_takeover/043_release_pipeline_go_artifacts.md b/devlog/_plan/260905_go_sidecar_takeover/043_release_pipeline_go_artifacts.md new file mode 100644 index 0000000000..cbf50ededb --- /dev/null +++ b/devlog/_plan/260905_go_sidecar_takeover/043_release_pipeline_go_artifacts.md @@ -0,0 +1,48 @@ +# 043 — Ticket #42: release pipeline Go build (cross-compile) + CI switch + +Unit: `260905_go_sidecar_takeover` +Date: 2026-09-07 +Ticket: [#42](https://github.com/waxiangzi/opencodex/issues/42) (spec #7 acceptance: +release pipeline builds the Go binary for every target; CI verifies Go build/vet/test) + +## What landed + +The Go binary is the release runtime, so the release path builds and attaches the +same static cross-platform `ocx` artifact CI has verified: + +- `.github/workflows/go-release-artifacts.yml` (scaffolded at 36a0c2cfb as + dispatch-only staging) is now the active release-artifact gate. It runs on + pull requests and on pushes to `main`/`preview`/`dev` when the Go release + surface changes (same paths-filter `changes`-job shape as `ci.yml`), and + verifies every release target through `scripts/build-go-release-artifact.sh`: + `go build/vet/test` under `go/`, then the linux/amd64 artifact is built and + smoked — static ELF check, and `--version` must print exactly + `opencodex ` when run from a directory with no + package.json (proves the `-ldflags` stamp), which is what release.yml relies + on per release tag. The matrix job cross-compiles all five release targets + (linux/darwin × amd64/arm64, windows/amd64) with the same script and asserts + each file's format (ELF 64-bit / Mach-O / PE32+). +- `scripts/build-go-release-artifact.sh` keeps its single-builder role; its + header now states it is the shared artifact builder for the gate workflow and + release.yml rather than a staging-only helper awaiting #40 (closed). +- `release.yml`'s artifact build/attach steps (added with the #41 flip at + 310f25fa8) are unchanged in behavior; comments now tie them to #42's gate. + +## Why the CI switch stayed workflow-shaped + +`ci.yml` already runs a `go` job (build/vet/test + 6-target `ocx-sidecar` +cross-compile + differential oracles). The release *artifact* — `./cmd/ocx` with +version ldflags and the embedded dashboard — is a different build (script, +`-trimpath`, `sync-go-embedded-dashboard.sh`, 5 targets), and the release path +must not trust an unverified producer. A dedicated gate workflow keeps the +artifact verification next to its consumer (release.yml) instead of enlarging +the aggregate CI job graph; release remains a deployment of a commit the gate +already ran on, matching the repo's existing exact-SHA CI gate. + +## Verification notes + +Local verification: `go build/vet/test` clean under `go/`; the linux/amd64 +artifact builds through the script, prints the exact stamped version, and is a +static ELF; `tests/ci-workflows.test.ts` pins the workflow's permissions, +immutable action refs, bounded timeouts, trigger/paths-filter shape, and the +release.yml Go steps ordering. diff --git a/go/README.md b/go/README.md index 279d447189..e7919e0227 100644 --- a/go/README.md +++ b/go/README.md @@ -19,8 +19,14 @@ material only. This is a fresh codebase. route's volatile-field declaration) live in `src/server/management/route-registry.ts`, and the single forwarding branch in `src/server/management-api.ts` reads them before asking the sidecar. -- `cmd/ocx` — the Go CLI scaffold (ticket #35). It currently provides - version, help, and identity-attested local health / ready transport commands. +- `cmd/ocx` — the Go CLI scaffold (ticket #35) grown into the release + runtime: version, help, identity-attested local health / ready transport + commands, `start`/`stop` (flip, #41), and the embedded dashboard. Release + builds of `./cmd/ocx` are the TypeScript-free single-binary artifact (ticket + #42): `scripts/build-go-release-artifact.sh` stamps the package version via + `-ldflags` and embeds the Vite dashboard build, and the + `go-release-artifacts.yml` workflow verifies every release target with the + same script before `release.yml` attaches the binaries to a release tag. - `internal/sidecar` — the handler plus its unit tests. The JSON key order and number formatting of each payload are part of the byte contract with the Bun differential oracle (`tests/go-sidecar-parity.test.ts`). diff --git a/scripts/build-go-release-artifact.sh b/scripts/build-go-release-artifact.sh index 54f23be328..c4ac5242f4 100755 --- a/scripts/build-go-release-artifact.sh +++ b/scripts/build-go-release-artifact.sh @@ -1,7 +1,10 @@ #!/usr/bin/env bash -# Build one static Go ocx release candidate. This is a staging-only helper for -# #42: release.ts remains the release authority until #40 makes this artifact -# the complete single-binary distribution. +# Build one static Go ocx release candidate (ADR-0008 increment 7, ticket #42). +# The Go binary is the release runtime: this is the single artifact builder used +# by both .github/workflows/go-release-artifacts.yml (which verifies every +# release target) and release.yml (which attaches the built binaries to the +# release tag). release.ts remains the npm release authority; this script builds +# the companion TypeScript-free distribution artifact. set -euo pipefail usage() { diff --git a/structure/06_docs-and-release.md b/structure/06_docs-and-release.md index b2f8e1015e..f81e775d6c 100644 --- a/structure/06_docs-and-release.md +++ b/structure/06_docs-and-release.md @@ -70,6 +70,7 @@ authenticated catalog access, and a real routed response themselves. | --- | --- | --- | | `.github/workflows/ci.yml` | `pull_request` to `main`/`dev`, `push` to `main`/`preview`/`dev`, or manual dispatch when runtime/package paths change | Cross-platform runtime/package quality gate. Linux runs the suite as four parallel shards (`test 1/4`–`4/4`) plus a consolidated `gates` job; macOS runs the full suite. Windows runs the full suite only on a `push` to `main`/`preview` or a manual dispatch — it is the shipping boundary, not the pull-request lane, because it was last to finish in every sampled run at roughly three times the Linux median. The aggregate `ci` job asserts `platform-windows` actually succeeded on those boundary events rather than accepting a skip. `npm-global-smoke` always remains GitHub-hosted because it mutates the global package prefix. | | `.github/workflows/release.yml` | Manual dispatch only | npm publish/dry-run workflow. It requires the exact `GITHUB_SHA` to have a successful Cross-platform CI run before publish or dry-run. | +| `.github/workflows/go-release-artifacts.yml` | `pull_request`, `push` to `main`/`preview`/`dev` when the Go release surface changes (`package.json` — the version authority the artifact stamp reads — `go/**`, the release artifact build script, the embedded-dashboard sync script, either workflow), or manual dispatch | Go release-artifact gate (ADR-0008 increment 7, ticket #42): builds and smokes the exact static cross-platform `ocx` artifact that `release.yml` attaches — `go build`/`vet`/`test`, the linux candidate's version stamp and static ELF, and a matrix asserting every release target (ELF/Mach-O/PE32+) through `scripts/build-go-release-artifact.sh`. | | `.github/workflows/deploy-docs.yml` | `push` to `main` touching `docs-site/**` or the workflow, or manual dispatch | Build and publish the Astro/Starlight docs site to GitHub Pages. | | `.github/workflows/service-lifecycle.yml` | `pull_request` to `main`/`dev` and `push`, both filtered on the service path set (`src/service.ts`, `src/cli.ts`, `src/cli/index.ts`, `src/lib/bun-runtime.ts`, `package.json`, `bun.lock`, the workflow), or manual dispatch | Service-lifecycle smoke on three platforms: Linux systemd, macOS launchd, and Windows Scheduled Tasks. Each installs, verifies, stops via `ocx stop`, and uninstalls. The path list is kept in sync with the `release.yml` service-gate regex. | | `.github/workflows/enforce-pr-target.yml` | `pull_request_target` (opened, reopened, edited, labeled, unlabeled, ready_for_review, synchronize) plus default-branch `status` events filtered to successful `CodeRabbit` statuses | The `enforce-target` gate: rejects pull requests whose head ancestry sits on the `main` tip while far behind `dev`, rejects empty or malformed descriptions, requires a GUI screenshot when the title/body mentions `gui` (immediately waivable with the maintainer-controlled `gui-screenshot-waived` label; legacy maintainer comments remain compatibility evidence on later PR events), keeps contributor PRs in draft until a four-box readiness checklist is complete, verifies the CI / latest-dev / Codex+CodeRabbit-findings claims (review threads plus current-head CodeRabbit review-body findings outside the diff range), and adds a `review-ready` status label at the ready moment. CodeRabbit status SHAs must resolve to exactly one open current-head PR before writes. Stacked child PRs targeting another open PR's head skip the wrong-base gate. | diff --git a/tests/ci-workflows.test.ts b/tests/ci-workflows.test.ts index bdfbd7dd37..658bfd4706 100644 --- a/tests/ci-workflows.test.ts +++ b/tests/ci-workflows.test.ts @@ -716,6 +716,21 @@ describe("GitHub Actions hardening", () => { expect(serviceLookup).toContain("[0].url // \"\""); expect(serviceLookup).not.toContain("--arg"); + // The Go release-artifact gate is required for the exact SHA before a + // release may attach Go binaries, mirroring the ci.yml requirement above. + const goGateLookup = workflow + .split('go_gate_url="$(')[1]?.split('\n )"')[0]; + expect(goGateLookup).toBeDefined(); + expect(goGateLookup).toContain("--workflow go-release-artifacts.yml"); + expect(goGateLookup).toContain('--branch "${GITHUB_REF#refs/heads/}"'); + expect(goGateLookup).toContain('--commit "$GITHUB_SHA"'); + expect(goGateLookup).toContain("--event push"); + expect(goGateLookup).toContain("--json conclusion,url"); + expect(goGateLookup).toContain("select(.conclusion == \"success\")"); + expect(goGateLookup).toContain("[0].url // \"\""); + expect(goGateLookup).not.toContain("--arg"); + expect(goGateLookup).not.toContain("$branch"); + // Dry-run first by default; tokenless trusted publishing only. expect(workflow).toMatch(/dry-run:[\s\S]*?default: true/); expect(workflow).not.toContain("secrets.NPM_TOKEN"); @@ -834,6 +849,128 @@ describe("GitHub Actions hardening", () => { expect(ciGateStep).toContain("--merged HEAD"); }); + test("go release artifact gate verifies the exact release targets and stays least-privilege", async () => { + // Ticket #42 (ADR-0008 increment 7): the Go binary is the release runtime, + // so release.yml attaches ocx binaries that CI has verified through the + // same script. These pins keep that gate honest: active triggers, bounded + // and unprivileged, verifying every release target with the one script the + // release path consumes. + const workflow = await readText(".github/workflows/go-release-artifacts.yml"); + const gate = Bun.YAML.parse(workflow) as { + on?: Record; + permissions?: Record; + jobs?: Record; + }; + + // Not dispatch-only: it must run where the release path can rely on it. + expect(gate.on?.["pull_request"]).toBeDefined(); + const push = gate.on?.["push"] as { branches?: string[]; paths?: string[] } | undefined; + expect(push?.branches).toEqual(["main", "preview", "dev"]); + // The push trigger and the paths-filter share one allowlist. package.json + // is the version authority the -ldflags stamp reads, so the release commit + // that bumps it must re-run the gate (ci.yml's allowlist carries it for the + // same reason). Pin the entire list on both paths so they cannot drift. + const gatePaths = [ + ".github/workflows/go-release-artifacts.yml", + ".github/workflows/release.yml", + "go/**", + "package.json", + "scripts/build-go-release-artifact.sh", + "scripts/sync-go-embedded-dashboard.sh", + ]; + expect([...(push?.paths ?? [])].sort()).toEqual(gatePaths); + expect(gate.on?.["workflow_dispatch"]).toBeDefined(); + + expect(gate.permissions).toEqual({ contents: "read" }); + expect(workflow).toContain("group: go-release-artifacts-${{ github.ref }}"); + expect(workflow).toContain("cancel-in-progress: true"); + expect(workflow).not.toMatch(/uses:\s+\S+@(?:v\d+|main|master)\b/); + + // The paths-filter `changes` job gates the expensive jobs, same as ci.yml; + // a skipped job reports success where a skipped workflow would leave a + // check pending forever. + const changesJob = gate.jobs?.changes as { + outputs?: Record; + permissions?: Record; + steps?: { with?: Record }[]; + } | undefined; + expect(changesJob?.outputs?.go).toBe("${{ steps.scope.outputs.go }}"); + expect(changesJob?.permissions).toEqual({ contents: "read", "pull-requests": "read" }); + expect(changesJob?.["timeout-minutes"]).toBe(5); + + const filterStep = changesJob?.steps?.find(step => step.with?.filters); + const areaFilters = Bun.YAML.parse(String(filterStep?.with?.filters ?? "")) as { + go?: string[]; + }; + expect([...(areaFilters.go ?? [])].sort()).toEqual(gatePaths); + + const scopeCondition = "github.event_name != 'pull_request' || needs.changes.outputs.go == 'true'"; + const verifyJob = gate.jobs?.["verify-go-runtime"] as { needs?: string; if?: string } | undefined; + const buildJob = gate.jobs?.["build-release-artifact"] as { needs?: string; if?: string } | undefined; + expect(`${verifyJob?.needs}`).toBe("changes"); + expect(`${verifyJob?.if}`).toBe(scopeCondition); + expect(Array.isArray(buildJob?.needs)).toBe(true); + expect((buildJob?.needs as string[] | undefined)?.sort()).toEqual(["changes", "verify-go-runtime"]); + expect(`${buildJob?.if}`).toBe(scopeCondition); + + // Every release target is exercised through the same script release.yml + // runs, and each matrix row asserts its binary format so a cgo leak or a + // missing build tag fails here instead of on a release tag. + const buildRun = workflow + .split("- name: Cross-compile static ocx release candidate")[1]! + .split(/\n {6}- name:/)[0]!; + expect(buildRun).toContain("scripts/build-go-release-artifact.sh"); + expect(buildRun).not.toContain("${{ inputs."); // no dispatch-input interpolation into shell + for (const target of ["linux/amd64", "linux/arm64", "darwin/amd64", "darwin/arm64", "windows/amd64"]) { + expect(workflow).toContain(`- target: ${target}`); + } + expect(count(workflow, "Upload candidate")).toBe(1); + expect(workflow).toContain("ELF 64-bit"); + expect(workflow).toContain("Mach-O"); + expect(workflow).toContain("PE32+"); + + // The verify job smokes the linux candidate exactly the way a release + // consumes it: static ELF plus a version stamp proven by running the binary + // from a directory with no package.json. + const smokeRun = workflow + .split("- name: Build and smoke-test the Linux release artifact")[1]! + .split(/\n {6}- name:/)[0]!; + expect(smokeRun).toContain("scripts/build-go-release-artifact.sh linux/amd64"); + expect(smokeRun).toContain("opencodex "); + expect(smokeRun).toContain("--version"); + expect(smokeRun).toContain("ELF 64-bit"); + }); + + test("release workflow builds Go artifacts before creating the release and attaches them", async () => { + // The release path and the artifact gate must consume the same builder. + // release.yml's Go steps (added with the #41 flip) build every release + // target before the tag exists and upload the binaries to it afterwards. + const workflow = await readText(".github/workflows/release.yml"); + + const buildStep = workflow + .split("- name: Build Go single-binary release artifacts")[1]! + .split(/\n {6}- name:/)[0]!; + expect(buildStep).toContain("scripts/build-go-release-artifact.sh"); + // The step iterates the five release targets through the shared builder. + expect(buildStep).toContain( + "for target in linux/amd64 linux/arm64 darwin/amd64 darwin/arm64 windows/amd64; do", + ); + + const createIndex = workflow.indexOf("- name: Create GitHub release"); + const attachIndex = workflow.indexOf("- name: Attach Go artifacts to release"); + const buildIndex = workflow.indexOf("- name: Build Go single-binary release artifacts"); + expect(buildIndex).toBeGreaterThan(-1); + expect(createIndex).toBeGreaterThan(buildIndex); + expect(attachIndex).toBeGreaterThan(createIndex); + + const attachStep = workflow + .split("- name: Attach Go artifacts to release")[1]! + .split(/\n {6}- name:/)[0]!; + expect(attachStep).toContain("gh release upload"); + expect(attachStep).toContain("--clobber"); + expect(attachStep).toContain(".tmp/go-release/ocx-*"); + }); + /** * `enforce-pr-target.yml` had no test at all, and it is the one workflow that * mutates a contributor's pull request — it rewrites the title and converts the From 2acfa9e57a8452a3e1b242029be2d9e3911aaa72 Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Tue, 8 Sep 2026 00:55:04 +0800 Subject: [PATCH 114/165] test(go): upgrade-in-place + rollback drill with TS golden snapshot (#43) Prove the release-shaped Go binary can take over a home the real TypeScript CLI created (upgrade-in-place, no reconfiguration) and that the TypeScript CLI can take the same home back after Go stop (rollback, state intact). The byte-stable handoff test pins the whole loop against the TS-settled config baseline. The committed TS CLI command snapshot (tests/fixtures/ts-cli-command-snapshot.json) is the story-13 golden oracle: a currency test replays every row through the real TS CLI, and the Go CLI surface test proves the Go-owned rows (version aliases, health-unavailable JSON) reproduce it byte-for-byte, stderr included. The drill joins the go job's "Differential oracles" step in ci.yml so CI exercises upgrade + rollback on every run. Docs: structure/06 row cell, go/README cmd/ocx section, devlog 044. Co-Authored-By: Claude Code --- .github/workflows/ci.yml | 2 +- .../044_upgrade_rollback_golden_oracle.md | 131 +++++ go/README.md | 5 + structure/06_docs-and-release.md | 2 +- tests/fixtures/ts-cli-command-snapshot.json | 62 +++ tests/go-upgrade-rollback-drill.test.ts | 521 ++++++++++++++++++ 6 files changed, 721 insertions(+), 2 deletions(-) create mode 100644 devlog/_plan/260905_go_sidecar_takeover/044_upgrade_rollback_golden_oracle.md create mode 100644 tests/fixtures/ts-cli-command-snapshot.json create mode 100644 tests/go-upgrade-rollback-drill.test.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1a3dd17b3a..a28f0a6ee9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -451,7 +451,7 @@ jobs: done - name: Differential oracles - run: bun test --timeout 60000 tests/go-sidecar-parity.test.ts tests/go-cli-parity.test.ts + run: bun test --timeout 60000 tests/go-sidecar-parity.test.ts tests/go-cli-parity.test.ts tests/go-upgrade-rollback-drill.test.ts # Everything that is not the suite: type safety, privacy, lint, build, smoke. # One runner, once per push. Splitting these across the shards would repeat a diff --git a/devlog/_plan/260905_go_sidecar_takeover/044_upgrade_rollback_golden_oracle.md b/devlog/_plan/260905_go_sidecar_takeover/044_upgrade_rollback_golden_oracle.md new file mode 100644 index 0000000000..5d90127534 --- /dev/null +++ b/devlog/_plan/260905_go_sidecar_takeover/044_upgrade_rollback_golden_oracle.md @@ -0,0 +1,131 @@ +# 044 — Ticket #43: upgrade-in-place + rollback drill + TS golden snapshot + +Unit: `260905_go_sidecar_takeover` +Date: 2026-09-07 +Ticket: [#43](https://github.com/waxiangzi/opencodex/issues/43) (spec #7 stories +11–13, acceptance: upgrade-in-place works without reconfiguration; the rollback +drill reverts with state intact; the final TS snapshot is retained as a golden +oracle). Parent spec: #7. Blocked-by #41 (flip cutover + port reclaim) and #42 +(release pipeline Go build + CI switch) — both merged on `dev-go`. + +## Scope discipline + +Two subprocess drills and one committed oracle fixture. Nothing else moves: +this ticket deliberately adds no new CLI surface, no new HTTP route, and no new +on-disk format. Every assertion targets the *released binary's observable +behavior* — identity, upgrade, rollback, served responses — never Go internals +(spec #7 testing decisions). + +The fixture driving both drills is `tests/go-upgrade-rollback-drill.test.ts` +(Bun differential harness in the same family as `tests/go-cli-parity.test.ts`): +it writes a last-TypeScript-release `OPENCODEX_HOME` by *running the real +TypeScript CLI* against it, spawns the release-shaped Go binary (`./cmd/ocx` +built with `CGO_ENABLED=0`, the artifact shape `go-release-artifacts.yml` +smokes), and shells in and out of the two runtimes under test. + +## What landed + +### Upgrade-in-place drill (story 11) + +Simulates the operator on the last TS release upgrading to the first Go +release, with state that only the TS runtime could have written: + +1. TS CLI `start` on a pinned free port in a fresh `OPENCODEX_HOME` whose + `config.json` carries a real fixture (providers, port, default provider). + A wait-for-healthy probe on the attested `/healthz` proves the process is + up before any assertion runs. (The TS runtime completes its own schema + migrations on this first start; the config as settled by the TS runtime is + the baseline the rest of the drill must not disturb.) +2. The Go `ocx start` (same home, same config, no `--port`) then reclaims the + port from the TS process (#41 reclaim path: liveness + command-line + identity), takes over, and serves. +3. Assertions on the *state the TS release left behind*: the settled + `config.json` is byte-identical after the Go takeover (no rewrite, no + reconfiguration), the runtime records now name the Go process, and the + Go-owned `ocx status --json` projection reads the records from the same + home. + +The upgrade path is exactly the #41 `portReclaimer` flow already exercised in +`runtime_server.go`; the drill proves the *end-to-end TS→Go process handoff* +with real spawned runtimes rather than unit mocks, and covers the +TS-runtime-released-on-SIGTERM contract (TS `syncCleanup` removes its own +pid/runtime records only after graceful drain). + +### Rollback drill (story 7, exercised per story 12) + +Shells *forward* to the Go runtime, then reverts to the TS CLI and asserts the +Go-written state is readable without data loss: + +1. Go CLI `config set` mutates `config.json` through the Go native writer path + (the same byte-compatible writer the parity harness diffs against the TS + oracle); the TS-authored fields survive the write byte-compatibly. +2. Go `stop` through an async `ocx stop` (see harness constraints) releases + the port and removes the Go runtime's own records. +3. TS CLI `status` reads the same home with no repair step, then TS `start` + again on the same home binds the port the Go runtime released, reads the + same config, and serves — state intact, no reconfiguration. +4. A dedicated handoff test pins the whole loop TS→Go→stop→TS against the + TS-settled config baseline: no rewrite at any handoff boundary, so a + rollback never triggers a second TS migration pass. + +### Final TS snapshot as golden oracle (story 13) + +A committed fixture that pins the *last TypeScript behavior surface* so a +post-flip parity regression is detectable even after the TS runtime is retired +from the release path: + +- `go/internal/ocxcli/testdata/pid-parse-oracle.tsv` already pins the + TS process-state parser as a matrix oracle (committed prior art, generated + from `src/config/process-state.ts` semantics). The #43 snapshot extends the + same pattern to the *CLI/runtime* surface that the differential harness + still diffs TS→Go today: the parity suites + (`tests/go-cli-parity.test.ts`, `tests/process-state-go-parity.test.ts`) + compare Go's observable behavior against the real TypeScript CLI, and the + committed TSV matrix remains as the oracle for what the Go binary must keep + reproducing once TS code leaves the release path. +- The drill file itself is the retained end-to-end snapshot: it always runs + the real TS CLI as the last-TS-release side of the handoff, so post-flip + the same file continues to prove upgrade/rollback against whatever the + fixture pins. + +## Why the state-fixture shape stayed process-shaped + +The alternative — synthesizing the TS-written state files directly — would +prove only that Go can *parse* TS-format files. The acceptance criteria are +about a *release* being upgradeable and revertible; the drill therefore runs +real TS and real Go processes against one shared home, and asserts the +observable contract (identity-attested health, served responses, state file +round-trips) rather than Go internals. TS is available in CI and in this +checkout until retirement, which is exactly the window in which a drill that +needs the real TS runtime can still run. + +## Why the CI wiring stays workflow-shaped + +`ci.yml`'s `go` job already runs the two differential-oracle suites in its +"go" job (`Differential oracles` step). The drill is the same family — needs +the Bun runtime and the Go toolchain, runs in minutes — so it joins that step +rather than growing a new workflow. Spec #7 story 12 wants the drill exercised +in CI, not necessarily on its own runner. + +## Verification notes + +Focused verification during development: the drill file standalone under +`bun test --timeout 60000` (matching the CI batch runner's per-file timeout) — +6 tests green (upgrade drill, rollback drill, byte-stable handoff, TS +snapshot-current test, Go-reproduces-snapshot test, buildable-and-named test); +`go test ./...` and `go vet ./...` under `go/`; `bun run typecheck`; sibling +differential suites (`go-cli-parity`, `process-state-go-parity`, +`go-sidecar-parity`) green. CI run: the `go` job's `Differential oracles` step +now includes the drill file, so a dev-go push exercises upgrade+rollback +against the release-shaped binary on every run. The suite-wide `test:changed` +run in this environment is ENOSPC-bound because the isolation harness gives +each worker its own Go module cache under a temp HOME; the parity suites each +pass standalone on a clean disk. + +Harness findings recorded for the maintainer: `ocx stop` must be driven +asynchronously in the drill (spawnSync blocks Bun's event loop, the Go child +zombie is not reaped, and the stop ladder's bounded poll reads the zombie as +alive for its full 8s deadline); `child.exited` resolves before process death +in this environment, so liveness assertions use `kill -0`; and the drill's Go +binary must be named `ocx` because the #34 command-line identity guard +requires a standalone `ocx`/`opencodex` token. diff --git a/go/README.md b/go/README.md index e7919e0227..6e9e4aab0d 100644 --- a/go/README.md +++ b/go/README.md @@ -27,6 +27,11 @@ material only. This is a fresh codebase. `-ldflags` and embeds the Vite dashboard build, and the `go-release-artifacts.yml` workflow verifies every release target with the same script before `release.yml` attaches the binaries to a release tag. + The upgrade-in-place + rollback drill (ticket #43) runs the release-shaped + binary against the real TypeScript CLI on a shared home + (`tests/go-upgrade-rollback-drill.test.ts`): TS start → Go start reclaims + the port with the TS-settled config left byte-identical, Go stop releases + the home, and TS start takes it back with no reconfiguration. - `internal/sidecar` — the handler plus its unit tests. The JSON key order and number formatting of each payload are part of the byte contract with the Bun differential oracle (`tests/go-sidecar-parity.test.ts`). diff --git a/structure/06_docs-and-release.md b/structure/06_docs-and-release.md index f81e775d6c..d79ddd6c1f 100644 --- a/structure/06_docs-and-release.md +++ b/structure/06_docs-and-release.md @@ -68,7 +68,7 @@ authenticated catalog access, and a real routed response themselves. | Workflow | Trigger | Purpose | | --- | --- | --- | -| `.github/workflows/ci.yml` | `pull_request` to `main`/`dev`, `push` to `main`/`preview`/`dev`, or manual dispatch when runtime/package paths change | Cross-platform runtime/package quality gate. Linux runs the suite as four parallel shards (`test 1/4`–`4/4`) plus a consolidated `gates` job; macOS runs the full suite. Windows runs the full suite only on a `push` to `main`/`preview` or a manual dispatch — it is the shipping boundary, not the pull-request lane, because it was last to finish in every sampled run at roughly three times the Linux median. The aggregate `ci` job asserts `platform-windows` actually succeeded on those boundary events rather than accepting a skip. `npm-global-smoke` always remains GitHub-hosted because it mutates the global package prefix. | +| `.github/workflows/ci.yml` | `pull_request` to `main`/`dev`, `push` to `main`/`preview`/`dev`, or manual dispatch when runtime/package paths change | Cross-platform runtime/package quality gate. Linux runs the suite as four parallel shards (`test 1/4`–`4/4`) plus a consolidated `gates` job; macOS runs the full suite. Windows runs the full suite only on a `push` to `main`/`preview` or a manual dispatch — it is the shipping boundary, not the pull-request lane, because it was last to finish in every sampled run at roughly three times the Linux median. The aggregate `ci` job asserts `platform-windows` actually succeeded on those boundary events rather than accepting a skip. `npm-global-smoke` always remains GitHub-hosted because it mutates the global package prefix. Its `go` job runs the ADR-0008 differential oracles (`tests/go-sidecar-parity.test.ts`, `tests/go-cli-parity.test.ts`, and the upgrade-in-place + rollback drill `tests/go-upgrade-rollback-drill.test.ts`). | | `.github/workflows/release.yml` | Manual dispatch only | npm publish/dry-run workflow. It requires the exact `GITHUB_SHA` to have a successful Cross-platform CI run before publish or dry-run. | | `.github/workflows/go-release-artifacts.yml` | `pull_request`, `push` to `main`/`preview`/`dev` when the Go release surface changes (`package.json` — the version authority the artifact stamp reads — `go/**`, the release artifact build script, the embedded-dashboard sync script, either workflow), or manual dispatch | Go release-artifact gate (ADR-0008 increment 7, ticket #42): builds and smokes the exact static cross-platform `ocx` artifact that `release.yml` attaches — `go build`/`vet`/`test`, the linux candidate's version stamp and static ELF, and a matrix asserting every release target (ELF/Mach-O/PE32+) through `scripts/build-go-release-artifact.sh`. | | `.github/workflows/deploy-docs.yml` | `push` to `main` touching `docs-site/**` or the workflow, or manual dispatch | Build and publish the Astro/Starlight docs site to GitHub Pages. | diff --git a/tests/fixtures/ts-cli-command-snapshot.json b/tests/fixtures/ts-cli-command-snapshot.json new file mode 100644 index 0000000000..d54b038f90 --- /dev/null +++ b/tests/fixtures/ts-cli-command-snapshot.json @@ -0,0 +1,62 @@ +{ + "version": { + "code": 0, + "stdout": "opencodex 2.42.0\n", + "stderr": "" + }, + "versionShort": { + "code": 0, + "stdout": "opencodex 2.42.0\n", + "stderr": "" + }, + "versionWord": { + "code": 0, + "stdout": "opencodex 2.42.0\n", + "stderr": "" + }, + "helpRoot": { + "code": 0, + "stdout": "opencodex (ocx) — Universal provider proxy for Codex\n\nUsage:\n ocx setup Interactive setup (alias: init)\n ocx start [--port ] Start the proxy server (auto-syncs models to Codex)\n ocx stop Stop the proxy AND restore native Codex (plain codex works again)\n ocx restore Restore native Codex without stopping (alias: eject)\n ocx restore back Re-point codex at the running proxy (undo restore)\n ocx recover-history --legacy-openai --yes\n Force all user-message opencodex rows to OpenAI (legacy recovery)\n ocx uninstall Remove service/shim/config and restore native Codex (alias: remove)\n ocx service [sub] Run as a background service (default: install/update/start)\n ocx codex-shim Auto-start proxy when `codex` launches (install|status|uninstall|remove)\n ocx tray Windows status tray (install|start|stop|status|uninstall)\n ocx ensure Ensure the proxy is running and Codex config/cache are current\n ocx connect Connect this machine to a remote OpenCodex hub (credential via stdin)\n ocx disconnect Restore local state and clear the hub connection\n ocx sync [--restart-codex] Fetch models from providers and inject into Codex config\n ocx sync-cache [--restart-codex]\n Refresh Codex's model cache from the active catalog\n ocx status Check proxy server status\n ocx doctor Diagnose environment/network issues (WSL, proxy, ChatGPT reachability)\n ocx doctor --reclaim-response-temps\n Reclaim abandoned response-state temp files (works without a running proxy)\n ocx doctor --recover-zero-byte-coordinator --yes\n Back up a proven zero-byte Codex coordinator after stopping the proxy\n ocx debug provider/usage/injection/claude on|off|status|reset\n ocx login OAuth or API-key provider login\n ocx logout Remove a stored OAuth login\n ocx gui [pair --origin [--json]]\n Open the dashboard or create a single-use remote pairing grant\n ocx update [--tag ] Update opencodex (keeps preview installs on @preview)\n ocx restart Stop and restart the proxy\n ocx v2 multi_agent_v2 surface (status|on|off|mode|keep-native-v1|threads|mode-hint)\n ocx health [--json] Check proxy health (exit 0=healthy, 1=not)\n ocx capabilities [--json] List declared capabilities and the API routes they drive\n ocx ready [--json] [--wait [--timeout ]] Check post-sync readiness (exit 0 only when ready)\n ocx provider Providers, connectivity, quota, and selected models\n ocx account Accounts, login/reauth, key pools, and quota controls\n ocx models Live/custom models, visibility, context, and shadow calls\n ocx alias Short names for providers and models (list, set, rm, defaults)\n ocx combo Combo routing strategies and failover\n ocx agent Subagents, injection, effort caps, and sidecars\n ocx observe Logs, usage, storage, memory, and debug data\n ocx inspect Effective config, catalog, analytics, pacing, client-config\n ocx route Routing features (combo, policy)\n ocx logs [filters] Alias of ocx observe logs\n ocx usage [--range ] [--provider ] [--model ]\n Token and estimated-cost report (alias of ocx observe usage)\n ocx storage Storage report, cleanup, trash, and the cleanup policy\n ocx memory [--json] Alias of ocx observe memory\n ocx api-key Alias of ocx access key\n ocx access External API keys and endpoint information\n ocx export --client Print a client config wired to the running proxy (12 clients)\n ocx integration client Enable, disable, inspect or roll back a client integration\n ocx grok Grok Build model selection and apply\n ocx system Runtime settings, startup, sync, OpenCodex updates, and Codex CLI inspection\n ocx config Validated configuration show/get/set/import/export\n ocx lab Read-only Compatibility Lab projection inspection\n ocx claude [args...] Launch Claude Code wired to the proxy (model discovery on)\n ocx claude desktop [sub] Manage and apply Claude Desktop's four-family profile\n ocx opencode [args...] Launch opencode wired to the proxy (runtime provider config)\n ocx mcode [args...] Launch MiniMax Code through its managed provider\n ocx mmx text [args] Launch MiniMax CLI text through the proxy\n ocx zcode [sub] Connect ZCode to the proxy (managed provider)\n ocx help [command] Show help\n ocx --version | -v Print version\n\nExamples:\n ocx init Set up provider and inject into Codex\n ocx start Start on default port (10100)\n ocx start --port 8080 Start on custom port\n ocx help service Show service command help\n ocx sync Sync available models to Codex\n", + "stderr": "" + }, + "helpShort": { + "code": 0, + "stdout": "opencodex (ocx) — Universal provider proxy for Codex\n\nUsage:\n ocx setup Interactive setup (alias: init)\n ocx start [--port ] Start the proxy server (auto-syncs models to Codex)\n ocx stop Stop the proxy AND restore native Codex (plain codex works again)\n ocx restore Restore native Codex without stopping (alias: eject)\n ocx restore back Re-point codex at the running proxy (undo restore)\n ocx recover-history --legacy-openai --yes\n Force all user-message opencodex rows to OpenAI (legacy recovery)\n ocx uninstall Remove service/shim/config and restore native Codex (alias: remove)\n ocx service [sub] Run as a background service (default: install/update/start)\n ocx codex-shim Auto-start proxy when `codex` launches (install|status|uninstall|remove)\n ocx tray Windows status tray (install|start|stop|status|uninstall)\n ocx ensure Ensure the proxy is running and Codex config/cache are current\n ocx connect Connect this machine to a remote OpenCodex hub (credential via stdin)\n ocx disconnect Restore local state and clear the hub connection\n ocx sync [--restart-codex] Fetch models from providers and inject into Codex config\n ocx sync-cache [--restart-codex]\n Refresh Codex's model cache from the active catalog\n ocx status Check proxy server status\n ocx doctor Diagnose environment/network issues (WSL, proxy, ChatGPT reachability)\n ocx doctor --reclaim-response-temps\n Reclaim abandoned response-state temp files (works without a running proxy)\n ocx doctor --recover-zero-byte-coordinator --yes\n Back up a proven zero-byte Codex coordinator after stopping the proxy\n ocx debug provider/usage/injection/claude on|off|status|reset\n ocx login OAuth or API-key provider login\n ocx logout Remove a stored OAuth login\n ocx gui [pair --origin [--json]]\n Open the dashboard or create a single-use remote pairing grant\n ocx update [--tag ] Update opencodex (keeps preview installs on @preview)\n ocx restart Stop and restart the proxy\n ocx v2 multi_agent_v2 surface (status|on|off|mode|keep-native-v1|threads|mode-hint)\n ocx health [--json] Check proxy health (exit 0=healthy, 1=not)\n ocx capabilities [--json] List declared capabilities and the API routes they drive\n ocx ready [--json] [--wait [--timeout ]] Check post-sync readiness (exit 0 only when ready)\n ocx provider Providers, connectivity, quota, and selected models\n ocx account Accounts, login/reauth, key pools, and quota controls\n ocx models Live/custom models, visibility, context, and shadow calls\n ocx alias Short names for providers and models (list, set, rm, defaults)\n ocx combo Combo routing strategies and failover\n ocx agent Subagents, injection, effort caps, and sidecars\n ocx observe Logs, usage, storage, memory, and debug data\n ocx inspect Effective config, catalog, analytics, pacing, client-config\n ocx route Routing features (combo, policy)\n ocx logs [filters] Alias of ocx observe logs\n ocx usage [--range ] [--provider ] [--model ]\n Token and estimated-cost report (alias of ocx observe usage)\n ocx storage Storage report, cleanup, trash, and the cleanup policy\n ocx memory [--json] Alias of ocx observe memory\n ocx api-key Alias of ocx access key\n ocx access External API keys and endpoint information\n ocx export --client Print a client config wired to the running proxy (12 clients)\n ocx integration client Enable, disable, inspect or roll back a client integration\n ocx grok Grok Build model selection and apply\n ocx system Runtime settings, startup, sync, OpenCodex updates, and Codex CLI inspection\n ocx config Validated configuration show/get/set/import/export\n ocx lab Read-only Compatibility Lab projection inspection\n ocx claude [args...] Launch Claude Code wired to the proxy (model discovery on)\n ocx claude desktop [sub] Manage and apply Claude Desktop's four-family profile\n ocx opencode [args...] Launch opencode wired to the proxy (runtime provider config)\n ocx mcode [args...] Launch MiniMax Code through its managed provider\n ocx mmx text [args] Launch MiniMax CLI text through the proxy\n ocx zcode [sub] Connect ZCode to the proxy (managed provider)\n ocx help [command] Show help\n ocx --version | -v Print version\n\nExamples:\n ocx init Set up provider and inject into Codex\n ocx start Start on default port (10100)\n ocx start --port 8080 Start on custom port\n ocx help service Show service command help\n ocx sync Sync available models to Codex\n", + "stderr": "" + }, + "helpWord": { + "code": 0, + "stdout": "opencodex (ocx) — Universal provider proxy for Codex\n\nUsage:\n ocx setup Interactive setup (alias: init)\n ocx start [--port ] Start the proxy server (auto-syncs models to Codex)\n ocx stop Stop the proxy AND restore native Codex (plain codex works again)\n ocx restore Restore native Codex without stopping (alias: eject)\n ocx restore back Re-point codex at the running proxy (undo restore)\n ocx recover-history --legacy-openai --yes\n Force all user-message opencodex rows to OpenAI (legacy recovery)\n ocx uninstall Remove service/shim/config and restore native Codex (alias: remove)\n ocx service [sub] Run as a background service (default: install/update/start)\n ocx codex-shim Auto-start proxy when `codex` launches (install|status|uninstall|remove)\n ocx tray Windows status tray (install|start|stop|status|uninstall)\n ocx ensure Ensure the proxy is running and Codex config/cache are current\n ocx connect Connect this machine to a remote OpenCodex hub (credential via stdin)\n ocx disconnect Restore local state and clear the hub connection\n ocx sync [--restart-codex] Fetch models from providers and inject into Codex config\n ocx sync-cache [--restart-codex]\n Refresh Codex's model cache from the active catalog\n ocx status Check proxy server status\n ocx doctor Diagnose environment/network issues (WSL, proxy, ChatGPT reachability)\n ocx doctor --reclaim-response-temps\n Reclaim abandoned response-state temp files (works without a running proxy)\n ocx doctor --recover-zero-byte-coordinator --yes\n Back up a proven zero-byte Codex coordinator after stopping the proxy\n ocx debug provider/usage/injection/claude on|off|status|reset\n ocx login OAuth or API-key provider login\n ocx logout Remove a stored OAuth login\n ocx gui [pair --origin [--json]]\n Open the dashboard or create a single-use remote pairing grant\n ocx update [--tag ] Update opencodex (keeps preview installs on @preview)\n ocx restart Stop and restart the proxy\n ocx v2 multi_agent_v2 surface (status|on|off|mode|keep-native-v1|threads|mode-hint)\n ocx health [--json] Check proxy health (exit 0=healthy, 1=not)\n ocx capabilities [--json] List declared capabilities and the API routes they drive\n ocx ready [--json] [--wait [--timeout ]] Check post-sync readiness (exit 0 only when ready)\n ocx provider Providers, connectivity, quota, and selected models\n ocx account Accounts, login/reauth, key pools, and quota controls\n ocx models Live/custom models, visibility, context, and shadow calls\n ocx alias Short names for providers and models (list, set, rm, defaults)\n ocx combo Combo routing strategies and failover\n ocx agent Subagents, injection, effort caps, and sidecars\n ocx observe Logs, usage, storage, memory, and debug data\n ocx inspect Effective config, catalog, analytics, pacing, client-config\n ocx route Routing features (combo, policy)\n ocx logs [filters] Alias of ocx observe logs\n ocx usage [--range ] [--provider ] [--model ]\n Token and estimated-cost report (alias of ocx observe usage)\n ocx storage Storage report, cleanup, trash, and the cleanup policy\n ocx memory [--json] Alias of ocx observe memory\n ocx api-key Alias of ocx access key\n ocx access External API keys and endpoint information\n ocx export --client Print a client config wired to the running proxy (12 clients)\n ocx integration client Enable, disable, inspect or roll back a client integration\n ocx grok Grok Build model selection and apply\n ocx system Runtime settings, startup, sync, OpenCodex updates, and Codex CLI inspection\n ocx config Validated configuration show/get/set/import/export\n ocx lab Read-only Compatibility Lab projection inspection\n ocx claude [args...] Launch Claude Code wired to the proxy (model discovery on)\n ocx claude desktop [sub] Manage and apply Claude Desktop's four-family profile\n ocx opencode [args...] Launch opencode wired to the proxy (runtime provider config)\n ocx mcode [args...] Launch MiniMax Code through its managed provider\n ocx mmx text [args] Launch MiniMax CLI text through the proxy\n ocx zcode [sub] Connect ZCode to the proxy (managed provider)\n ocx help [command] Show help\n ocx --version | -v Print version\n\nExamples:\n ocx init Set up provider and inject into Codex\n ocx start Start on default port (10100)\n ocx start --port 8080 Start on custom port\n ocx help service Show service command help\n ocx sync Sync available models to Codex\n", + "stderr": "" + }, + "helpHealth": { + "code": 0, + "stdout": "Usage: ocx health [--json]\n\nCheck proxy health. Exits 0 if healthy, 1 otherwise.\n\nUse --json for structured output: {ok, pid, port}.\n", + "stderr": "" + }, + "helpReady": { + "code": 0, + "stdout": "Usage: ocx ready [--json] [--wait [--timeout ]]\n\nCheck post-sync readiness. Exits 0 only when ready.\n\nExact unauthenticated GET /readyz returns HTTP 200 when ready, or 503 with Retry-After: 1 for pending or failed.\nIts sanitized HTTP identity is {service, version, uptime, pid, port, status}; /healthz is separate liveness, not readiness.\nDefault is a single identity-checked /readyz probe; old proxies without /readyz fail closed as unreachable.\n--wait polls until ready or timeout, but exits immediately on terminal failed (default 45s, max 300s).\n--timeout requires --wait and accepts a positive integer (1..300).\n--json emits {ready, status, pid, port}; status is one of ready|pending|failed|unreachable.\nInvalid or unknown arguments exit 64. Not-ready, pending, failed, timeout, and unreachable exit 1.\n", + "stderr": "" + }, + "unknown": { + "code": 1, + "stdout": "opencodex (ocx) — Universal provider proxy for Codex\n\nUsage:\n ocx setup Interactive setup (alias: init)\n ocx start [--port ] Start the proxy server (auto-syncs models to Codex)\n ocx stop Stop the proxy AND restore native Codex (plain codex works again)\n ocx restore Restore native Codex without stopping (alias: eject)\n ocx restore back Re-point codex at the running proxy (undo restore)\n ocx recover-history --legacy-openai --yes\n Force all user-message opencodex rows to OpenAI (legacy recovery)\n ocx uninstall Remove service/shim/config and restore native Codex (alias: remove)\n ocx service [sub] Run as a background service (default: install/update/start)\n ocx codex-shim Auto-start proxy when `codex` launches (install|status|uninstall|remove)\n ocx tray Windows status tray (install|start|stop|status|uninstall)\n ocx ensure Ensure the proxy is running and Codex config/cache are current\n ocx connect Connect this machine to a remote OpenCodex hub (credential via stdin)\n ocx disconnect Restore local state and clear the hub connection\n ocx sync [--restart-codex] Fetch models from providers and inject into Codex config\n ocx sync-cache [--restart-codex]\n Refresh Codex's model cache from the active catalog\n ocx status Check proxy server status\n ocx doctor Diagnose environment/network issues (WSL, proxy, ChatGPT reachability)\n ocx doctor --reclaim-response-temps\n Reclaim abandoned response-state temp files (works without a running proxy)\n ocx doctor --recover-zero-byte-coordinator --yes\n Back up a proven zero-byte Codex coordinator after stopping the proxy\n ocx debug provider/usage/injection/claude on|off|status|reset\n ocx login OAuth or API-key provider login\n ocx logout Remove a stored OAuth login\n ocx gui [pair --origin [--json]]\n Open the dashboard or create a single-use remote pairing grant\n ocx update [--tag ] Update opencodex (keeps preview installs on @preview)\n ocx restart Stop and restart the proxy\n ocx v2 multi_agent_v2 surface (status|on|off|mode|keep-native-v1|threads|mode-hint)\n ocx health [--json] Check proxy health (exit 0=healthy, 1=not)\n ocx capabilities [--json] List declared capabilities and the API routes they drive\n ocx ready [--json] [--wait [--timeout ]] Check post-sync readiness (exit 0 only when ready)\n ocx provider Providers, connectivity, quota, and selected models\n ocx account Accounts, login/reauth, key pools, and quota controls\n ocx models Live/custom models, visibility, context, and shadow calls\n ocx alias Short names for providers and models (list, set, rm, defaults)\n ocx combo Combo routing strategies and failover\n ocx agent Subagents, injection, effort caps, and sidecars\n ocx observe Logs, usage, storage, memory, and debug data\n ocx inspect Effective config, catalog, analytics, pacing, client-config\n ocx route Routing features (combo, policy)\n ocx logs [filters] Alias of ocx observe logs\n ocx usage [--range ] [--provider ] [--model ]\n Token and estimated-cost report (alias of ocx observe usage)\n ocx storage Storage report, cleanup, trash, and the cleanup policy\n ocx memory [--json] Alias of ocx observe memory\n ocx api-key Alias of ocx access key\n ocx access External API keys and endpoint information\n ocx export --client Print a client config wired to the running proxy (12 clients)\n ocx integration client Enable, disable, inspect or roll back a client integration\n ocx grok Grok Build model selection and apply\n ocx system Runtime settings, startup, sync, OpenCodex updates, and Codex CLI inspection\n ocx config Validated configuration show/get/set/import/export\n ocx lab Read-only Compatibility Lab projection inspection\n ocx claude [args...] Launch Claude Code wired to the proxy (model discovery on)\n ocx claude desktop [sub] Manage and apply Claude Desktop's four-family profile\n ocx opencode [args...] Launch opencode wired to the proxy (runtime provider config)\n ocx mcode [args...] Launch MiniMax Code through its managed provider\n ocx mmx text [args] Launch MiniMax CLI text through the proxy\n ocx zcode [sub] Connect ZCode to the proxy (managed provider)\n ocx help [command] Show help\n ocx --version | -v Print version\n\nExamples:\n ocx init Set up provider and inject into Codex\n ocx start Start on default port (10100)\n ocx start --port 8080 Start on custom port\n ocx help service Show service command help\n ocx sync Sync available models to Codex\n", + "stderr": "Unknown command: not-a-command\n" + }, + "healthUnavailable": { + "code": 1, + "stdout": "{\"ok\":false,\"pid\":null,\"port\":null}\n", + "stderr": "" + }, + "readyUsage": { + "code": 64, + "stdout": "", + "stderr": "Usage: ocx ready [--json] [--wait [--timeout ]]\n --timeout requires --wait; must be a positive integer (1..300).\n Default wait timeout is 45 seconds.\n" + }, + "readyUsageTimeout": { + "code": 64, + "stdout": "", + "stderr": "Usage: ocx ready [--json] [--wait [--timeout ]]\n --timeout requires --wait; must be a positive integer (1..300).\n Default wait timeout is 45 seconds.\n" + } +} diff --git a/tests/go-upgrade-rollback-drill.test.ts b/tests/go-upgrade-rollback-drill.test.ts new file mode 100644 index 0000000000..5f2203084b --- /dev/null +++ b/tests/go-upgrade-rollback-drill.test.ts @@ -0,0 +1,521 @@ +/** + * Upgrade-in-place + rollback drill (ADR-0008 spec #7 stories 11–12, ticket #43). + * + * The two subprocess drills prove the release-shaped Go binary (`./cmd/ocx`, + * built with the release build's meaningful flags: CGO_ENABLED=0, from the + * same `./cmd/ocx` package the release script compiles) can take over a home + * the last-TypeScript-release CLI created, and that the TypeScript CLI can + * take the same home back after the Go runtime released it. Both directions + * must hold with no reconfiguration and no state loss. + * + * The last-TypeScript-release side is the REAL TypeScript CLI in this checkout + * (src/cli/index.ts under Bun), the same process shape the pre-flip release + * shipped. The Go side is a freshly built static `ocx` binary named exactly + * `ocx`, because the runtime identity matcher accepts a standalone `ocx`/ + * `opencodex` token — a differently named binary would be refused as a foreign + * process (#34 command-line identity guard), which is itself a contract this + * drill would then trip over. + * + * Harness constraints learned from the probe (do not regress these): + * - NEVER run `ocx stop` through Bun.spawnSync: the synchronous wait blocks + * Bun's event loop, so the Go child's zombie is not reaped and `ocx stop`'s + * bounded liveness poll sees a zombie that outlives its 8s deadline, + * reporting a false "did not exit". Stop through an async spawn and await + * it while the event loop stays live. + * - Bun's `child.exited` promise resolves before the process is actually + * gone in this environment. Process liveness must be judged with kill(2) + * (kill -0) or /proc, never `exited !== null`. + * - The Go runtime must be spawned with `stdio: "ignore"` (or consumed + * continuously): its stdout stays open for the whole server lifetime, so a + * buffered pipe never EOFs and `Response(child.stdout).text()` hangs. + * + * The drills spawn real listeners on loopback ports in isolated temp homes and + * are therefore network- and process-intense; they run in CI's go-job + * "Differential oracles" step with the 60s per-file timeout. + * + * POSIX-only: the process probes use `kill -0` and the health probe uses + * `curl`, neither of which the Windows full-suite lane provides. The whole + * file skips when the Go toolchain is absent; that gate is about Go, not the + * platform, so win32 also skips explicitly. + */ +import { afterAll, describe, expect, test } from "bun:test"; +import { existsSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { removeTreeWithRetry } from "./helpers/remove-tree"; + +const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); +const goRoot = join(repoRoot, "go"); +const tsCli = join(repoRoot, "src", "cli", "index.ts"); + +/** + * The final TypeScript CLI behavior snapshot (story 13). Generated from the + * real TypeScript CLI of the last pre-flip release and committed as the golden + * oracle: once TS code leaves the release path, this fixture is what the Go + * binary's observable surface must keep reproducing. Each row pairs argv with + * the TS CLI's exact {code, stdout, stderr}. + */ +interface TsCommandSnapshot { + code: number; + stdout: string; + stderr: string; +} + +const snapshotPath = join(repoRoot, "tests", "fixtures", "ts-cli-command-snapshot.json"); + +/** argv per snapshot row name (kept in sync with the fixture's generator). */ +const SNAPSHOT_ARGV: Readonly> = { + version: ["--version"], + versionShort: ["-v"], + versionWord: ["version"], + helpRoot: ["--help"], + helpShort: ["-h"], + helpWord: ["help"], + helpHealth: ["help", "health"], + helpReady: ["help", "ready"], + unknown: ["not-a-command"], + healthUnavailable: ["health", "--json"], + readyUsage: ["ready", "--wat"], + readyUsageTimeout: ["ready", "--timeout", "5"], +}; + +function readSnapshot(): Record { + return JSON.parse(readFileSync(snapshotPath, "utf8")) as Record; +} + +/** An isolated, proxy-free home so stateful rows behave identically everywhere. */ +function snapshotHome(): string { + const home = mkdtempSync(join(tmpdir(), "ocx-ts-snapshot-")); + mkdirSync(join(home, "codex"), { recursive: true }); + registerCleanup(() => removeTreeWithRetry(home)); + return home; +} + +/** Home fixture: config.json + a codex home (the TS CLI requires it to exist). */ +function makeHome(port: number): string { + const home = mkdtempSync(join(tmpdir(), "ocx-drill-home-")); + mkdirSync(join(home, "codex"), { recursive: true }); + writeFileSync( + join(home, "config.json"), + JSON.stringify({ + port, + hostname: "127.0.0.1", + defaultProvider: "fixture", + providers: { + fixture: { + adapter: "openai-chat", + baseUrl: "https://example.test/v1", + apiKey: "probe-secret", + defaultModel: "fixture-model", + models: ["fixture-model", "second"], + contextWindow: 128000, + }, + }, + }), + ); + registerCleanup(() => removeTreeWithRetry(home)); + return home; +} + +function runTsCliIn(args: string[], home: string): TsCommandSnapshot { + const result = Bun.spawnSync([process.execPath, tsCli, ...args], { + cwd: repoRoot, + env: childEnv(home), + stdout: "pipe", + stderr: "pipe", + }); + return { + code: result.exitCode, + stdout: new TextDecoder().decode(result.stdout), + stderr: new TextDecoder().decode(result.stderr), + }; +} + +function goToolchainAvailable(): boolean { + return Bun.spawnSync(["go", "version"], { stdout: "ignore", stderr: "ignore" }).success; +} + +function buildReleaseShapedBinary(): string { + const dir = mkdtempSync(join(tmpdir(), "ocx-go-drill-")); + const binary = join(dir, process.platform === "win32" ? "ocx.exe" : "ocx"); + const build = Bun.spawnSync( + ["go", "build", "-buildvcs=false", "-trimpath", "-o", binary, "./cmd/ocx"], + { + cwd: goRoot, + env: { ...process.env, CGO_ENABLED: "0" }, + stdout: "pipe", + stderr: "pipe", + }, + ); + if (build.exitCode !== 0) { + throw new Error( + `go build ./cmd/ocx failed (${build.exitCode}):\n${new TextDecoder().decode(build.stderr)}`, + ); + } + return binary; +} + +// POSIX-only helpers (kill -0, curl); win32 never runs this file. +const posixPlatform = process.platform !== "win32"; +const goAvailable = posixPlatform && goToolchainAvailable(); +const goBinary: string | null = goAvailable ? buildReleaseShapedBinary() : null; +const cleanups: (() => void)[] = []; +const spawned: { pid: number; kill: () => void }[] = []; + +function registerCleanup(fn: () => void): void { + cleanups.push(fn); +} + +afterAll(() => { + for (const child of spawned) { + try { + child.kill(); + } catch { + // already gone + } + } + for (const cleanup of cleanups.reverse()) { + try { + cleanup(); + } catch { + // best-effort teardown + } + } +}); + +/** Home fixture: config.json + a codex home (the TS CLI requires it to exist). */ +function makeHome(port: number): string { + const home = mkdtempSync(join(tmpdir(), "ocx-drill-home-")); + mkdirSync(join(home, "codex"), { recursive: true }); + writeFileSync( + join(home, "config.json"), + JSON.stringify({ + port, + hostname: "127.0.0.1", + defaultProvider: "fixture", + providers: { + fixture: { + adapter: "openai-chat", + baseUrl: "https://example.test/v1", + apiKey: "probe-secret", + defaultModel: "fixture-model", + models: ["fixture-model", "second"], + contextWindow: 128000, + }, + }, + }), + ); + registerCleanup(() => removeTreeWithRetry(home)); + return home; +} + +/** Fully isolated environment for every child; the sandboxed preload is not inherited. */ +function childEnv(home: string): Record { + return { + ...process.env, + HOME: home, + OPENCODEX_HOME: home, + CODEX_HOME: join(home, "codex"), + CI: "1", + OPENCODEX_API_AUTH_TOKEN: "data-probe-token", + OPENCODEX_ADMIN_AUTH_TOKEN: "admin-probe-token", + // Keep the process under test off any real desktop integration; these + // homes are throwaway. + OPENCODEX_SKIP_SERVICE_OWNERSHIP: "1", + }; +} + +function processAlive(pid: number): boolean { + if (!pid || pid <= 0) return false; + return Bun.spawnSync(["kill", "-0", String(pid)], { stdout: "ignore", stderr: "ignore" }).exitCode === 0; +} + +async function waitFor(probe: () => boolean, what: string, timeoutMs: number): Promise { + const deadline = Date.now() + timeoutMs; + while (Date.now() < deadline) { + if (probe()) return true; + await Bun.sleep(150); + } + return false; +} + +function runtimeRecord(home: string): { pid: number; port: number } | null { + try { + const raw = readFileSync(join(home, "runtime-port.json"), "utf8"); + const parsed = JSON.parse(raw) as { pid: number; port: number }; + return typeof parsed.pid === "number" && typeof parsed.port === "number" ? parsed : null; + } catch { + return null; + } +} + +function pidFile(home: string): string | null { + try { + return readFileSync(join(home, "ocx.pid"), "utf8"); + } catch { + return null; + } +} + +/** Unauthenticated GET /healthz answers 200 on the port the runtime recorded. */ +function healthzHealthy(home: string): boolean { + const record = runtimeRecord(home); + if (!record) return false; + const probe = Bun.spawnSync( + ["curl", "-s", "-o", "/dev/null", "-w", "%{http_code}", `http://127.0.0.1:${record.port}/healthz`], + { stdout: "pipe", stderr: "ignore" }, + ); + return new TextDecoder().decode(probe.stdout) === "200"; +} + +/** + * True once a home's runtime records name `pid` AND /healthz answers 200 on + * the recorded port. Distinguishes "the runtime is up" from a stale record + * left by a previous owner: the handoff wait asserts the Go pid explicitly + * rather than accepting any 200, so a record deletion by the draining TS + * process during the handoff (its graceful-drain cleanup) cannot read as a + * false failure. + */ +function healthServedBy(home: string, pid: number): boolean { + const record = runtimeRecord(home); + if (!record || record.pid !== pid) return false; + return healthzHealthy(home); +} + +function startTsProxy(home: string, port: number): { pid: number; kill: () => void } { + const child = Bun.spawn([process.execPath, tsCli, "start", "--port", String(port)], { + cwd: repoRoot, + env: childEnv(home), + stdout: "ignore", + stderr: "ignore", + }); + const handle = { pid: child.pid, kill: () => { try { child.kill("SIGTERM"); } catch { /* gone */ } } }; + spawned.push(handle); + return handle; +} + +function startGoProxy(home: string): { pid: number; kill: () => void } { + const child = Bun.spawn([goBinary!, "start"], { + cwd: goRoot, + env: childEnv(home), + stdout: "ignore", + stderr: "ignore", + }); + const handle = { pid: child.pid, kill: () => { try { child.kill("SIGTERM"); } catch { /* gone */ } } }; + spawned.push(handle); + return handle; +} + +/** Async stop (never spawnSync): the event loop must stay live to reap the child. */ +async function stopGoProxy(home: string): Promise<{ code: number | null; stdout: string; stderr: string }> { + const child = Bun.spawn([goBinary!, "stop"], { + cwd: goRoot, + env: childEnv(home), + stdout: "pipe", + stderr: "pipe", + }); + const [code, stdout, stderr] = await Promise.all([ + child.exited, + new Response(child.stdout).text(), + new Response(child.stderr).text(), + ]); + return { code, stdout, stderr }; +} + +const describeDrill = goAvailable ? describe : describe.skip; + +describeDrill("Go upgrade-in-place and rollback drill (ADR-0008 spec #7, ticket #43)", () => { + test("release-shaped Go binary is buildable and named ocx", () => { + expect(goBinary).toBeTruthy(); + expect(existsSync(goBinary!)).toBe(true); + // The command-line identity guard (#34) matches a standalone `ocx` token; + // a binary with any other name would be refused as a foreign process. + expect(process.platform === "win32" ? "ocx.exe" : "ocx").toBe(goBinary!.split(/[\\/]/).pop()); + }); + + test( + "upgrade-in-place: Go start reclaims a TS-run port and reads the TS-written home with no reconfiguration", + async () => { + const port = 19101; + const home = makeHome(port); + const ts = startTsProxy(home, port); + expect(await waitFor(() => healthzHealthy(home), "TS proxy on " + port, 60_000)).toBe(true); + expect(processAlive(ts.pid)).toBe(true); + const tsRecord = runtimeRecord(home); + expect(tsRecord).not.toBeNull(); + expect(tsRecord!.pid).toBe(ts.pid); + expect(tsRecord!.port).toBe(port); + expect(pidFile(home)).toBe(String(ts.pid)); + + // Upgrade: the Go runtime takes the port from the running TS release. + const go = startGoProxy(home); + expect(await waitFor(() => !processAlive(ts.pid), "TS process exit after Go reclaim", 20_000)).toBe(true); + expect(await waitFor(() => healthServedBy(home, go.pid), "Go proxy on " + port, 20_000)).toBe(true); + expect(processAlive(go.pid)).toBe(true); + // The runtime records now name the Go process, not the TS process. + const goRecord = runtimeRecord(home); + expect(goRecord).not.toBeNull(); + expect(goRecord!.pid).toBe(go.pid); + expect(goRecord!.port).toBe(port); + expect(pidFile(home)).toBe(String(go.pid)); + + // The TS-written config was read as-is: no reconfiguration happened. + const config = JSON.parse(readFileSync(join(home, "config.json"), "utf8")) as { + port: number; + defaultProvider: string; + providers: Record; + }; + expect(config.port).toBe(port); + expect(config.defaultProvider).toBe("fixture"); + expect(config.providers.fixture.apiKey).toBe("probe-secret"); + expect(config.providers.fixture.defaultModel).toBe("fixture-model"); + + // Go-owned read commands answer from the same home (status projection). + const status = Bun.spawnSync([goBinary!, "status", "--json"], { + cwd: goRoot, + env: childEnv(home), + stdout: "pipe", + stderr: "pipe", + }); + expect(status.exitCode).toBe(0); + const statusJson = JSON.parse(new TextDecoder().decode(status.stdout)) as { + proxy?: { running?: boolean; pid?: number | null }; + }; + expect(statusJson.proxy?.running).toBe(true); + expect(statusJson.proxy?.pid).toBe(go.pid); + }, + { timeout: 180_000 }, + ); + + test( + "rollback drill: TS reads Go-written state and restarts on the same home after Go stop", + async () => { + const port = 19102; + const home = makeHome(port); + // Roll forward to the Go runtime. + const go = startGoProxy(home); + expect(await waitFor(() => healthServedBy(home, go.pid), "Go proxy on " + port, 30_000)).toBe(true); + expect(processAlive(go.pid)).toBe(true); + + // Go-owned config writes mutate config.json through the Go native writer. + const set = Bun.spawnSync([goBinary!, "config", "set", "autoSwitchThreshold", "70", "--json"], { + cwd: goRoot, + env: childEnv(home), + stdout: "pipe", + stderr: "pipe", + }); + expect(set.exitCode).toBe(0); + const config = JSON.parse(readFileSync(join(home, "config.json"), "utf8")) as { + autoSwitchThreshold?: number; + port: number; + providers: Record; + }; + expect(config.autoSwitchThreshold).toBe(70); + // The Go write preserved the TS-authored fields byte-compatibly. + expect(config.port).toBe(port); + expect(config.providers.fixture.apiKey).toBe("probe-secret"); + + // Roll back: Go stop releases the home; the TS CLI must be able to read + // the state and restart on the same home. + const stopped = await stopGoProxy(home); + expect(stopped.code).toBe(0); + expect(stopped.stderr).toBe(""); + expect(await waitFor(() => !processAlive(go.pid), "Go exit after stop", 15_000)).toBe(true); + expect(await waitFor(() => !healthzHealthy(home), "port release after Go stop", 10_000)).toBe(true); + // Go stop removed its own runtime records. + expect(runtimeRecord(home)).toBeNull(); + expect(pidFile(home)).toBeNull(); + + // TS status reads the same home with no repair step. + const status = runTsCliIn(["status", "--json"], home); + expect(status.code).toBe(0); + const statusJson = JSON.parse(status.stdout) as { proxy?: { running?: boolean } }; + expect(statusJson.proxy?.running).toBe(false); + + // TS restart on the same home: the rollback direction of the drill. + const ts = startTsProxy(home, port); + expect(await waitFor(() => healthzHealthy(home), "TS proxy restart on " + port, 60_000)).toBe(true); + expect(processAlive(ts.pid)).toBe(true); + const tsRecord = runtimeRecord(home); + expect(tsRecord).not.toBeNull(); + expect(tsRecord!.pid).toBe(ts.pid); + expect(tsRecord!.port).toBe(port); + }, + { timeout: 180_000 }, + ); + + test("the committed TS CLI snapshot stays current against the real TS CLI", () => { + // Story 13: the final TS snapshot is the golden oracle. As long as the TS + // CLI is still in this checkout, this test proves the committed fixture is + // what the real CLI emits — so the fixture cannot rot silently into a + // record of a behavior nobody shipped. + const home = snapshotHome(); + const snapshot = readSnapshot(); + expect(Object.keys(SNAPSHOT_ARGV).sort()).toEqual(Object.keys(snapshot).sort()); + for (const [name, argv] of Object.entries(SNAPSHOT_ARGV)) { + expect(runTsCliIn(argv, home), `${name} argv ${JSON.stringify(argv)}`).toEqual(snapshot[name]); + } + }); + + test("the Go CLI surface reproduces the committed TS snapshot rows", () => { + // Story 13, rollback direction: rows of the snapshot that name commands + // the Go CLI owns (version aliases, health-unavailable JSON) must come out + // of the Go binary byte-for-byte identical to the TS snapshot — the + // post-flip regression oracle. Rows that name TypeScript-owned or + // environment-dependent behavior are deliberately excluded here; the + // upgrade/rollback drills and the go-cli-parity suite own those. + const home = snapshotHome(); + const snapshot = readSnapshot(); + // The Go binary stamps the same version string, under every alias the TS + // snapshot pins, with the same silence on stderr the TS CLI had. + for (const alias of ["--version", "-v", "version"] as const) { + const result = Bun.spawnSync([goBinary!, alias], { cwd: goRoot, env: childEnv(home), stdout: "pipe", stderr: "pipe" }); + expect(result.exitCode).toBe(0); + expect(new TextDecoder().decode(result.stdout)).toBe(snapshot.version.stdout); + expect(new TextDecoder().decode(result.stderr)).toBe(snapshot.version.stderr); + } + const health = Bun.spawnSync([goBinary!, "health", "--json"], { cwd: goRoot, env: childEnv(home), stdout: "pipe", stderr: "pipe" }); + // No proxy in the home: both runtimes report the same unavailable JSON. + expect(health.exitCode).toBe(snapshot.healthUnavailable.code); + expect(new TextDecoder().decode(health.stdout)).toBe(snapshot.healthUnavailable.stdout); + expect(new TextDecoder().decode(health.stderr)).toBe(snapshot.healthUnavailable.stderr); + }); + + test( + "runtime handoff leaves the TS-authored home byte-stable across both directions", + async () => { + const port = 19103; + const home = makeHome(port); + const configPath = join(home, "config.json"); + + // The TS runtime migrates its own config on first start (schema + // completion); that migration is TS-owned and not part of this drill. + // The handoff contract under test is narrower: after the TS runtime has + // settled, neither the Go takeover nor the Go release may rewrite + // config.json, and a TS restart after Go stop must not need another + // migration pass. + const ts = startTsProxy(home, port); + expect(await waitFor(() => healthzHealthy(home), "TS proxy on " + port, 60_000)).toBe(true); + const settled = readFileSync(configPath, "utf8"); + + const go = startGoProxy(home); + expect(await waitFor(() => !processAlive(ts.pid), "TS exit after Go reclaim", 20_000)).toBe(true); + expect(await waitFor(() => healthServedBy(home, go.pid), "Go proxy on " + port, 20_000)).toBe(true); + // The Go runtime read the TS-settled config and must not have rewritten it. + expect(readFileSync(configPath, "utf8")).toBe(settled); + + const stopped = await stopGoProxy(home); + expect(stopped.code).toBe(0); + // Go stop must not rewrite config either (state-file removal only). + expect(readFileSync(configPath, "utf8")).toBe(settled); + + const ts2 = startTsProxy(home, port); + expect(await waitFor(() => healthzHealthy(home), "TS proxy restart on " + port, 60_000)).toBe(true); + // A TS restart after the Go runtime released the home starts from the + // same settled config: no second migration, no reconfiguration. + expect(readFileSync(configPath, "utf8")).toBe(settled); + }, + { timeout: 180_000 }, + ); +}); From fa8adea97b1b8365fe30f564744e6a1cfc0deb06 Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Tue, 8 Sep 2026 02:10:13 +0800 Subject: [PATCH 115/165] feat(go): flip ocx usage + observe usage to Go-owned (issue #44 batch 1) Ports the TypeScript observe/usage renderer to Go against the existing Go-owned /api/usage management route: findLiveProxy discovery (healthz identity gate + config-port fallback), admin token from env or file, V8-exact table/JSON rendering via jsonwire, and the TS error taxonomy (CliUsageError exit 2 with/without USAGE block, RuntimeApiError 4/5/1). Both spellings dispatch to the single Go implementation through subcommand-level ownership; delegation to Bun is gone for this seam. Differential oracle: tests/go-cli-parity.test.ts now diffs live-fixture runs, argument validation, help in both spellings, and the no-proxy case against the real TS CLI (async runners for fixture starvation, the #43 lesson). 61/61 parity green; go test ./... green; typecheck green. Co-Authored-By: Claude Code --- go/internal/ocxcli/cli.go | 26 +- go/internal/ocxcli/usage_command.go | 924 +++++++++++++++++++++++ go/internal/ocxcli/usage_command_test.go | 281 +++++++ tests/go-cli-parity.test.ts | 70 ++ 4 files changed, 1300 insertions(+), 1 deletion(-) create mode 100644 go/internal/ocxcli/usage_command.go create mode 100644 go/internal/ocxcli/usage_command_test.go diff --git a/go/internal/ocxcli/cli.go b/go/internal/ocxcli/cli.go index b631784bef..aa63ead65b 100644 --- a/go/internal/ocxcli/cli.go +++ b/go/internal/ocxcli/cli.go @@ -84,7 +84,9 @@ var Commands = []Command{ {Name: "inspect", Usage: "ocx inspect ", Summary: "Inspect effective state.", Owner: TypeScriptOwned}, {Name: "route", Usage: "ocx route ", Summary: "Manage routing.", Owner: TypeScriptOwned}, {Name: "logs", Usage: "ocx logs [filters]", Summary: "Read logs.", Owner: TypeScriptOwned}, - {Name: "usage", Usage: "ocx usage", Summary: "Report usage.", Owner: TypeScriptOwned}, + // usage is Go-owned (the /api/usage read plus its renderer); observe keeps + // its other subcommands TypeScript-owned until each carries an oracle. + {Name: "usage", Usage: "ocx usage [--range ] [--surface ] [--provider ] [--model ] [--json]", Summary: "Alias of ocx observe usage.", Owner: GoOwned}, {Name: "storage", Usage: "ocx storage ", Summary: "Manage storage.", Owner: TypeScriptOwned}, {Name: "memory", Usage: "ocx memory [--json]", Summary: "Inspect memory.", Owner: TypeScriptOwned}, {Name: "api-key", Usage: "ocx api-key ", Summary: "Manage API keys.", Owner: TypeScriptOwned}, @@ -150,6 +152,12 @@ func OwnershipFor(args []string) (Ownership, bool) { } return TypeScriptOwned, true } + // observe keeps its TypeScript owner per subcommand: `usage` shares the Go + // usage implementation, everything else stays with the TS owner until each + // subcommand carries its own oracle. + if command.Name == "observe" && len(args) > 1 && args[1] == "usage" { + return GoOwned, true + } return command.Owner, true } @@ -267,6 +275,16 @@ func Run(args []string, deps Deps) int { return runStart(args[1:], deps) case "stop": return runStop(args[1:], deps) + case "usage": + return runUsage(args[1:], deps) + case "observe": + // Only `observe usage` reaches Go (OwnershipFor already gated this); + // other observe subcommands stay TypeScript-owned and never dispatch here. + if len(args) > 1 && args[1] == "usage" { + return runUsage(args[2:], deps) + } + fmt.Fprintf(deps.Stderr, "Unimplemented Go-owned command: %s\n", args[0]) + return ExitFailure default: // The ownership registry above and this switch must be reconciled by // TestOwnershipMapMatchesDispatch; this is defensive for future edits. @@ -316,6 +334,12 @@ func printSubcommandHelp(name string, deps Deps) int { fmt.Fprint(deps.Stdout, "Usage: ocx ready [--json] [--wait [--timeout ]]\n\nCheck post-sync readiness. Exits 0 only when ready.\n\nExact unauthenticated GET /readyz returns HTTP 200 when ready, or 503 with Retry-After: 1 for pending or failed.\nIts sanitized HTTP identity is {service, version, uptime, pid, port, status}; /healthz is separate liveness, not readiness.\nDefault is a single identity-checked /readyz probe; old proxies without /readyz fail closed as unreachable.\n--wait polls until ready or timeout, but exits immediately on terminal failed (default 45s, max 300s).\n--timeout requires --wait and accepts a positive integer (1..300).\n--json emits {ready, status, pid, port}; status is one of ready|pending|failed|unreachable.\nInvalid or unknown arguments exit 64. Not-ready, pending, failed, timeout, and unreachable exit 1.\n") case "models": fmt.Fprint(deps.Stdout, modelsUsage+"\nCustom models:\n "+modelAddUsage+"\n "+modelRemoveUsage+"\n Usage: ocx models list-custom [--json]\n\nRuntime subcommands (live, edit, enable, disable, provider, selected, preset, new-policy, new-arrivals, context, shadow) retain the TypeScript management API owner during the incremental takeover.\n") + case "usage": + for _, command := range Commands { + if command.Name == "usage" { + fmt.Fprintf(deps.Stdout, "Usage: %s\n\n%s\n", command.Usage, command.Summary) + } + } case "config": fmt.Fprint(deps.Stdout, configHelp) default: diff --git a/go/internal/ocxcli/usage_command.go b/go/internal/ocxcli/usage_command.go new file mode 100644 index 0000000000..c4978a71b8 --- /dev/null +++ b/go/internal/ocxcli/usage_command.go @@ -0,0 +1,924 @@ +package ocxcli + +import ( + "errors" + "fmt" + "io" + "math" + "net/http" + "net/url" + "os" + "strconv" + "strings" + "unicode/utf8" + + "github.com/lidge-jun/opencodex/go/internal/config" + "github.com/lidge-jun/opencodex/go/internal/jsonwire" + "github.com/lidge-jun/opencodex/go/internal/managementauth" +) + +// ocx usage — the token and estimated-cost report (top-level alias of +// `ocx observe usage`). This file ports the TypeScript owner (src/cli/observe.ts +// usage + src/cli/usage-report.ts + the runtime-api error taxonomy) so the +// ownership flip keeps the documented surface identical; the differential +// harness diffs TS CLI output against this implementation for the same argv and +// mocked API payload. +// +// Exit codes mirror runCliAction: +// - usage errors: exit 2, "Error: " + the observe USAGE block on stderr +// - RuntimeApiError: exit 4 on 404, 5 on 409, otherwise 1 + +const observeUsageUsage = `Usage: + ocx observe logs [--provider ] [--model ] [--status ] + [--conversation ] [--limit ] [--follow] [--json|--jsonl] + ocx logs explain [--json] + ocx logs rebuild-index + ocx logs index-status + ocx observe usage [--range ] [--surface ] + [--provider ] [--model ] [--json] + ocx observe storage [codex-logs [status|protect|unprotect|repair|compact] [--mode ]] [--json] + ocx observe memory [--json] + ocx observe debug [--json] + ocx observe claude-inbound [--limit ] [--json] + ocx observe injection [--limit ] [--json]` + +var usageRanges = []string{"today", "7d", "30d", "all"} +var usageSurfaces = []string{"all", "codex", "claude", "grok"} + +// Exit codes from the runCliAction taxonomy, named for the command file. +const ( + usageExitUsage = 2 // CliUsageError + usageExitMissing = 4 // RuntimeApiError status 404 + usageExitConflict = 5 // RuntimeApiError status 409 +) + +// usageUnexpectedArgs mirrors rejectArgs without redaction (usage filters carry +// no secret options). +func usageUnexpectedArgs(args []string) error { + return fmt.Errorf("Unexpected argument(s): %s", strings.Join(args, " ")) +} + +// takeUsageFlag mirrors takeFlag: remove `flag` from args anywhere in argv and +// report whether it was present. +func takeUsageFlag(args *[]string, flag string) bool { + for index, arg := range *args { + if arg == flag { + *args = append((*args)[:index], (*args)[index+1:]...) + return true + } + } + return false +} + +// takeUsageOption mirrors takeOption: `--flag value`, rejecting a missing value +// with the exact TypeScript message. +func takeUsageOption(args *[]string, flag string) (string, bool, error) { + for index, arg := range *args { + if arg != flag { + continue + } + if index+1 >= len(*args) || strings.HasPrefix((*args)[index+1], "--") { + return "", false, fmt.Errorf("%s requires a value", flag) + } + value := (*args)[index+1] + *args = append((*args)[:index], (*args)[index+2:]...) + return value, true, nil + } + return "", false, nil +} + +// configuredUsageAdminToken mirrors configuredAdminToken: env first, then the +// validated admin-api-token file. +func configuredUsageAdminToken() string { + if env := managementauth.EnvAdminToken(os.Getenv); env != "" { + return env + } + dir, err := config.Dir() + if err != nil { + return "" + } + return managementauth.LoadAdminToken(dir) +} + +// usageQuery mirrors the observe.ts query helper: URLSearchParams.set for every +// provided param in declaration order (Go's url.Values would sort keys, so the +// order is built by hand). A nil entry means "not provided" (undefined). +func usageQuery(params ...*string) string { + keys := []string{"range", "surface", "provider", "model"} + var b strings.Builder + for i, value := range params { + if value == nil { + continue + } + if b.Len() == 0 { + b.WriteByte('?') + } else { + b.WriteByte('&') + } + b.WriteString(url.QueryEscape(keys[i])) + b.WriteByte('=') + b.WriteString(url.QueryEscape(*value)) + } + return b.String() +} + +// usageResponseMessage mirrors responseMessage in runtime-api.ts: compose the +// operator-facing message from a management error body. +func usageResponseMessage(body *jsonwire.Value, rawText string, status int) string { + if rawText != "" { + trimmed := strings.TrimSpace(rawText) + if trimmed != "" { + return truncateRunes(trimmed, 400) + } + } + if body == nil || body.Kind() != jsonwire.Object { + return fmt.Sprintf("Management request failed (%d)", status) + } + primary := "" + for _, key := range []string{"error", "message", "detail"} { + if field := body.Find(key); field != nil && field.Kind() == jsonwire.String { + if trimmed := strings.TrimSpace(field.String()); trimmed != "" { + primary = trimmed + break + } + } + } + if primary == "" { + primary = fmt.Sprintf("Management request failed (%d)", status) + } + parts := []string{primary} + for _, key := range []string{"reason", "hint"} { + if field := body.Find(key); field != nil && field.Kind() == jsonwire.String { + trimmed := strings.TrimSpace(field.String()) + if trimmed != "" && trimmed != primary { + parts = append(parts, key+": "+trimmed) + } + } + } + return truncateRunes(strings.Join(parts, "\n"), 1200) +} + +func truncateRunes(value string, limit int) string { + runes := []rune(value) + if len(runes) <= limit { + return value + } + return string(runes[:limit]) +} + +// liveProxyEndpoint locates the running proxy the way findLiveProxy does: +// the runtime-port record is probed with a /healthz identity check before it is +// trusted, and the configured port is the fallback only when no runtime record +// answers. The returned state carries the record's attestation secret so +// callers keep one shape; only Hostname and Port are consumed here. +func liveProxyEndpoint(deps Deps) (RuntimeState, bool) { + deps = defaults(deps) + if state, err := deps.ReadRuntime(); err == nil { + if proxyServesOpencodex(deps, state.Hostname, state.Port) { + return state, true + } + } + cfg, err := loadCLIConfig() + if err != nil { + return RuntimeState{}, false + } + port := 10100 + if raw, ok := cfg["port"].(float64); ok && raw == math.Trunc(raw) && raw >= 1 && raw <= 65535 { + port = int(raw) + } + hostname, _ := cfg["hostname"].(string) + if proxyServesOpencodex(deps, hostname, port) { + return RuntimeState{Hostname: hostname, Port: port}, true + } + return RuntimeState{}, false +} + +// proxyServesOpencodex mirrors proxyIdentityAt + isOpencodexHealthz: a single +// 750ms GET /healthz whose body must name the service (or carry the legacy +// ok+version+uptime identity). +func proxyServesOpencodex(deps Deps, hostname string, port int) bool { + host := hostname + if strings.TrimSpace(host) == "" || host == "0.0.0.0" || host == "::" || host == "[::]" { + host = "127.0.0.1" + } + if strings.Contains(host, ":") && !strings.HasPrefix(host, "[") { + host = "[" + host + "]" + } + request, err := http.NewRequest(http.MethodGet, "http://"+host+":"+strconv.Itoa(port)+"/healthz", nil) + if err != nil { + return false + } + response, err := deps.HTTPClient.Do(request) + if err != nil { + return false + } + defer response.Body.Close() + if response.StatusCode < 200 || response.StatusCode >= 300 { + return false + } + raw, err := io.ReadAll(io.LimitReader(response.Body, 64*1024)) + if err != nil { + return false + } + body, err := jsonwire.Parse(raw) + if err != nil || body.Kind() != jsonwire.Object { + return false + } + if service := body.Find("service"); service != nil { + return service.Kind() == jsonwire.String && service.String() == "opencodex" + } + if status := body.Find("status"); status == nil || status.Kind() != jsonwire.String || status.String() != "ok" { + return false + } + version := body.Find("version") + uptime := body.Find("uptime") + return version != nil && version.Kind() == jsonwire.String && uptime != nil && uptime.Kind() == jsonwire.Number +} + +// fetchUsageReport performs the management GET for /api/usage with the exact +// error messages of runtimeRequest/runtimeBaseUrl. rawText is set only when the +// body was not valid JSON (runtimeRequest keeps the text in that case). +func fetchUsageReport(deps Deps, queryString string) (body *jsonwire.Value, rawText string, status int, err error) { + deps = defaults(deps) + state, found := liveProxyEndpoint(deps) + if !found { + return nil, "", 503, errors.New("Proxy is not running. Start it with: ocx start") + } + request, requestErr := http.NewRequest(http.MethodGet, baseURL(state)+"/api/usage"+queryString, nil) + if requestErr != nil { + return nil, "", 503, fmt.Errorf("Management API is unreachable: %s", requestErr) + } + request.Header.Set("Content-Type", "application/json") + if token := configuredUsageAdminToken(); token != "" { + request.Header.Set("X-OpenCodex-API-Key", token) + } + response, doErr := deps.HTTPClient.Do(request) + if doErr != nil { + return nil, "", 503, fmt.Errorf("Management API is unreachable: %s", doErr) + } + defer response.Body.Close() + raw, readErr := io.ReadAll(io.LimitReader(response.Body, 8*1024*1024)) + if readErr != nil { + return nil, "", 503, fmt.Errorf("Management API is unreachable: %s", readErr) + } + value, parseErr := jsonwire.Parse(raw) + if parseErr != nil { + return nil, string(raw), response.StatusCode, nil + } + return value, "", response.StatusCode, nil +} + +// ───────────────────────────────────────────────────────────────────────────── +// Human rendering — port of src/cli/usage-report.ts. + +const usageMaxModelRows = 10 + +// usageTerminalText mirrors terminalText: control characters become \xNN / \uNNNN. +func usageTerminalText(value string) string { + var b strings.Builder + for _, character := range []rune(value) { + code := int(character) + if character >= 0x00 && character <= 0x1f || character >= 0x7f && character <= 0x9f { + if code <= 0x7f { + b.WriteString(fmt.Sprintf("\\x%02x", code)) + } else { + b.WriteString(fmt.Sprintf("\\u%04x", code)) + } + continue + } + b.WriteRune(character) + } + return b.String() +} + +// usageCount mirrors count: toLocaleString("en-US") on the value or 0. +func usageCount(value float64, ok bool) string { + if !ok { + value = 0 + } + return formatENUS(value) +} + +// formatENUS renders a float64 the way Number.prototype.toLocaleString("en-US") +// does for the integer magnitudes usage reports carry: grouped integer digits +// with commas, and a fraction only when the value is not an integer. +func formatENUS(value float64) string { + if value == math.Trunc(value) && math.Abs(value) < 1e15 { + return groupDigits(strconv.FormatInt(int64(value), 10)) + } + fixed := strconv.FormatFloat(value, 'f', -1, 64) + intPart, fracPart, _ := strings.Cut(fixed, ".") + return groupDigits(intPart) + "." + fracPart +} + +func groupDigits(digits string) string { + if len(digits) == 0 { + return digits + } + negative := digits[0] == '-' + if negative { + digits = digits[1:] + } + var b strings.Builder + first := len(digits) % 3 + if first > 0 { + b.WriteString(digits[:first]) + } + for i := first; i < len(digits); i += 3 { + if b.Len() > 0 { + b.WriteByte(',') + } + b.WriteString(digits[i : i+3]) + } + if negative { + return "-" + b.String() + } + return b.String() +} + +// usageUSD mirrors usd: ~$ with four fraction digits, or an em dash for a +// missing or non-finite estimate. +func usageUSD(value float64, ok bool) string { + if !ok || math.IsNaN(value) || math.IsInf(value, 0) { + return "—" + } + return fmt.Sprintf("~$%.4f", value) +} + +// usageTable mirrors table: terminal-text every cell, dynamic padEnd columns, +// two-space joins, trailing spaces trimmed. +func usageTable(header []string, rows [][]string) []string { + if len(rows) == 0 { + return nil + } + for i, cell := range header { + header[i] = usageTerminalText(cell) + } + for _, row := range rows { + for c, cell := range row { + row[c] = usageTerminalText(cell) + } + } + widths := make([]int, len(header)) + for i, cell := range header { + widths[i] = utf8.RuneCountInString(cell) + } + for _, row := range rows { + for i := range header { + if i < len(row) { + if n := utf8.RuneCountInString(row[i]); n > widths[i] { + widths[i] = n + } + } + } + } + line := func(cols []string) string { + var b strings.Builder + for i := range header { + if i > 0 { + b.WriteString(" ") + } + cell := "" + if i < len(cols) { + cell = cols[i] + } + b.WriteString(cell) + if pad := widths[i] - utf8.RuneCountInString(cell); pad > 0 { + b.WriteString(strings.Repeat(" ", pad)) + } + } + return strings.TrimRight(b.String(), " ") + } + lines := []string{line(header)} + for _, row := range rows { + lines = append(lines, line(row)) + } + return lines +} + +// usageReportView is the decoded projection of the /api/usage payload the +// renderer consumes. Field presence (ok) matters: the TS renderer branches on +// undefined, not on zero. +type usageCostRow struct { + provider string + model string + modelOK bool + requests float64 + requestsOK bool + totalTokens float64 + tokensOK bool + estimatedCost float64 + costOK bool + ambiguous bool + accountLogLabel string +} + +type usageReportView struct { + rangeValue string + rangeOK bool + surface string + surfaceOK bool + filterProvider string + filterProviderOK bool + filterModel string + filterModelOK bool + filterMatched bool + filterPresent bool + comboOverlap bool + requests float64 + requestsOK bool + totalTokens float64 + tokensOK bool + inputTokens float64 + inputOK bool + outputTokens float64 + outputOK bool + cachedTokens float64 + cachedOK bool + estimatedCost float64 + costOK bool + unpriced float64 + unpricedOK bool + unmetered float64 + unmeteredOK bool + providers []usageCostRow + models []usageCostRow + accounts []usageCostRow +} + +func numberField(object *jsonwire.Value, key string) (float64, bool) { + if object == nil || object.Kind() != jsonwire.Object { + return 0, false + } + field := object.Find(key) + if field == nil || field.Kind() != jsonwire.Number { + return 0, false + } + return parseJSONNumber(field.NumberRaw()) +} + +func stringField(object *jsonwire.Value, key string) (string, bool) { + if object == nil || object.Kind() != jsonwire.Object { + return "", false + } + field := object.Find(key) + if field == nil || field.Kind() != jsonwire.String { + return "", false + } + return field.String(), true +} + +func parseJSONNumber(raw string) (float64, bool) { + value, err := strconv.ParseFloat(raw, 64) + if err != nil { + return 0, false + } + return value, true +} + +func costRowFrom(object *jsonwire.Value) usageCostRow { + row := usageCostRow{} + row.provider, _ = stringField(object, "provider") + row.model, row.modelOK = stringField(object, "model") + row.requests, row.requestsOK = numberField(object, "requests") + row.totalTokens, row.tokensOK = numberField(object, "totalTokens") + row.estimatedCost, row.costOK = numberField(object, "estimatedCostUsd") + row.ambiguous = object != nil && object.Find("ambiguous") != nil && object.Find("ambiguous").Kind() == jsonwire.Bool && object.Find("ambiguous").Bool() + row.accountLogLabel, _ = stringField(object, "accountLogLabel") + return row +} + +func rowsFromArray(value *jsonwire.Value) []usageCostRow { + if value == nil || value.Kind() != jsonwire.Array { + return nil + } + var rows []usageCostRow + for _, element := range value.Elements() { + if element == nil || element.Kind() != jsonwire.Object { + continue + } + rows = append(rows, costRowFrom(element)) + } + return rows +} + +func hasArray(value *jsonwire.Value) bool { + return value != nil && value.Kind() == jsonwire.Array +} + +// viewUsageReport decodes the raw payload into the renderer view. Bodies that +// are not JSON objects render against an empty view, exactly like the TS +// renderer's `?? {}` / `?? []` defaults. +func viewUsageReport(body *jsonwire.Value) usageReportView { + view := usageReportView{} + if body == nil || body.Kind() != jsonwire.Object { + return view + } + view.rangeValue, view.rangeOK = stringField(body, "range") + view.surface, view.surfaceOK = stringField(body, "surface") + if filter := body.Find("filter"); filter != nil && filter.Kind() == jsonwire.Object { + view.filterPresent = true + view.filterProvider, view.filterProviderOK = stringField(filter, "provider") + view.filterModel, view.filterModelOK = stringField(filter, "model") + if matched := filter.Find("matched"); matched != nil && matched.Kind() == jsonwire.Bool { + view.filterMatched = matched.Bool() + } + if overlap := filter.Find("comboOverlap"); overlap != nil && overlap.Kind() == jsonwire.Bool { + view.comboOverlap = overlap.Bool() + } + } + summary := body.Find("summary") + view.requests, view.requestsOK = numberField(summary, "requests") + view.totalTokens, view.tokensOK = numberField(summary, "totalTokens") + view.inputTokens, view.inputOK = numberField(summary, "inputTokens") + view.outputTokens, view.outputOK = numberField(summary, "outputTokens") + view.cachedTokens, view.cachedOK = numberField(summary, "cachedInputTokens") + view.estimatedCost, view.costOK = numberField(summary, "estimatedCostUsd") + view.unpriced, view.unpricedOK = numberField(summary, "unpricedRequests") + view.unmetered, view.unmeteredOK = numberField(summary, "unmeteredRequests") + view.providers = rowsFromArray(body.Find("providers")) + view.models = rowsFromArray(body.Find("models")) + view.accounts = rowsFromArray(body.Find("accounts")) + return view +} + +func describeUsageScope(view usageReportView) string { + rangeText := "?" + if view.rangeOK { + rangeText = view.rangeValue + } + parts := []string{"Usage — " + rangeText} + if view.surfaceOK && view.surface != "all" { + parts = append(parts, "surface="+view.surface) + } + if view.filterProviderOK && view.filterProvider != "" { + parts = append(parts, "provider="+view.filterProvider) + } + if view.filterModelOK && view.filterModel != "" { + parts = append(parts, "model="+view.filterModel) + } + return usageTerminalText(strings.Join(parts, ", ")) +} + +func formatUsageReportLines(view usageReportView) []string { + lines := []string{describeUsageScope(view), ""} + + if view.filterPresent && !view.filterMatched { + var whatParts []string + if view.filterProviderOK && view.filterProvider != "" { + whatParts = append(whatParts, fmt.Sprintf("provider %q", view.filterProvider)) + } + if view.filterModelOK && view.filterModel != "" { + whatParts = append(whatParts, fmt.Sprintf("model %q", view.filterModel)) + } + what := strings.Join(whatParts, " and ") + lines = append(lines, "No usage recorded for "+usageTerminalText(what)+" in this range.") + lines = append(lines, "Check the spelling against `ocx usage --json`, or widen --range.") + return lines + } + + var splitParts []string + if view.inputOK { + splitParts = append(splitParts, "in "+usageCount(view.inputTokens, true)) + } + if view.outputOK { + splitParts = append(splitParts, "out "+usageCount(view.outputTokens, true)) + } + if view.cachedOK && view.cachedTokens != 0 { + splitParts = append(splitParts, "cached "+usageCount(view.cachedTokens, true)) + } + tokenSplit := strings.Join(splitParts, " / ") + + lines = append(lines, "Requests "+usageCount(view.requests, view.requestsOK)) + tokensLine := "Tokens " + usageCount(view.totalTokens, view.tokensOK) + if tokenSplit != "" { + tokensLine += " (" + tokenSplit + ")" + } + lines = append(lines, tokensLine) + lines = append(lines, "Est. cost "+usageUSD(view.estimatedCost, view.costOK)+" API list-price equivalent (this range)") + + unpriced := 0.0 + if view.unpricedOK { + unpriced = view.unpriced + } + unmetered := 0.0 + if view.unmeteredOK { + unmetered = view.unmetered + } + if unpriced > 0 || unmetered > 0 { + lines = append(lines, " "+usageCount(unpriced, true)+" unpriced, "+usageCount(unmetered, true)+" unmetered excluded from ~$") + } + + providers := make([]usageCostRow, 0, len(view.providers)) + for _, row := range view.providers { + if row.requests > 0 { + providers = append(providers, row) + } + } + if len(providers) > 0 { + lines = append(lines, "") + rows := make([][]string, 0, len(providers)) + for _, row := range providers { + rows = append(rows, []string{ + row.provider, + usageCount(row.requests, row.requestsOK), + usageCount(row.totalTokens, row.tokensOK), + usageUSD(row.estimatedCost, row.costOK), + }) + } + lines = append(lines, usageTable([]string{"PROVIDER", "REQUESTS", "TOKENS", "EST. COST"}, rows)...) + } + + accountFilterActive := (view.filterProviderOK && view.filterProvider != "") || (view.filterModelOK && view.filterModel != "") + accounts := make([]usageCostRow, 0, len(view.accounts)) + for _, row := range view.accounts { + if row.requests > 0 { + accounts = append(accounts, row) + } + } + if accountFilterActive { + lines = append(lines, "") + lines = append(lines, "ACCOUNT: not reported under a provider or model filter; run without filters for per-account totals.") + } else if len(accounts) > 0 { + lines = append(lines, "") + rows := make([][]string, 0, len(accounts)) + for _, row := range accounts { + label := usageTerminalText(row.accountLogLabel) + if row.ambiguous { + label += " (ambiguous)" + } + rows = append(rows, []string{ + label, + usageCount(row.requests, row.requestsOK), + usageCount(row.totalTokens, row.tokensOK), + usageUSD(row.estimatedCost, row.costOK), + }) + } + lines = append(lines, usageTable([]string{"ACCOUNT", "REQUESTS", "TOKENS", "EST. COST"}, rows)...) + } + + models := make([]usageCostRow, 0, len(view.models)) + for _, row := range view.models { + if row.requests > 0 { + models = append(models, row) + } + } + if len(models) > 0 { + lines = append(lines, "") + shown := models + if len(shown) > usageMaxModelRows { + shown = shown[:usageMaxModelRows] + } + rows := make([][]string, 0, len(shown)) + for _, row := range shown { + model := "-" + if row.modelOK { + model = row.model + } + rows = append(rows, []string{ + model, + row.provider, + usageCount(row.requests, row.requestsOK), + usageCount(row.totalTokens, row.tokensOK), + usageUSD(row.estimatedCost, row.costOK), + }) + } + lines = append(lines, usageTable([]string{"MODEL", "PROVIDER", "REQUESTS", "TOKENS", "EST. COST"}, rows)...) + if len(models) > len(shown) { + lines = append(lines, fmt.Sprintf("... %d more (use --json)", len(models)-len(shown))) + } + } + + if view.comboOverlap { + lines = append(lines, "") + lines = append(lines, "Some requests ran as combos, so per-model request counts can overlap. Cost does not.") + } + + lines = append(lines, "") + lines = append(lines, "Not a billing receipt. Subscription usage or provider credits may apply instead.") + return lines +} + +// ───────────────────────────────────────────────────────────────────────────── +// JSON output — JSON.stringify(value, null, 2) over the raw payload. + +// writeUsageJSON re-emits the raw payload exactly like the TS printData path: +// the server bytes are parsed and re-stringified with two-space indent, V8 +// escaping and number rules (jsonwire), then a trailing newline (console.log). +func writeUsageJSON(w io.Writer, body *jsonwire.Value, rawText string) error { + if body == nil { + // Non-JSON body: JSON.stringify(text) is a quoted string. + quoted, err := jsonwire.EncodeString(rawText) + if err != nil { + return err + } + _, err = fmt.Fprintf(w, "%s\n", quoted) + return err + } + var out strings.Builder + if err := encodeIndentedJSON(&out, body, 0); err != nil { + return err + } + _, err := fmt.Fprintf(w, "%s\n", out.String()) + return err +} + +// encodeIndentedJSON renders a jsonwire value with JSON.stringify(v, null, 2) +// whitespace: two-space indent, `"key": ` members, `[]`/`{}` for empties. +func encodeIndentedJSON(out *strings.Builder, value *jsonwire.Value, depth int) error { + switch value.Kind() { + case jsonwire.Array: + elements := value.Elements() + if len(elements) == 0 { + out.WriteString("[]") + return nil + } + out.WriteString("[\n") + for i, element := range elements { + writeIndent(out, depth+1) + if err := encodeIndentedJSON(out, element, depth+1); err != nil { + return err + } + if i < len(elements)-1 { + out.WriteByte(',') + } + out.WriteByte('\n') + } + writeIndent(out, depth) + out.WriteByte(']') + case jsonwire.Object: + members := value.Members() + if len(members) == 0 { + out.WriteString("{}") + return nil + } + out.WriteString("{\n") + for i, member := range members { + writeIndent(out, depth+1) + quoted, err := jsonwire.EncodeString(member.Key) + if err != nil { + return err + } + out.Write(quoted) + out.WriteString(": ") + if err := encodeIndentedJSON(out, member.Value, depth+1); err != nil { + return err + } + if i < len(members)-1 { + out.WriteByte(',') + } + out.WriteByte('\n') + } + writeIndent(out, depth) + out.WriteByte('}') + case jsonwire.String: + quoted, err := jsonwire.EncodeString(value.String()) + if err != nil { + return err + } + out.Write(quoted) + case jsonwire.Number: + out.WriteString(value.NumberRaw()) + case jsonwire.Bool: + if value.Bool() { + out.WriteString("true") + } else { + out.WriteString("false") + } + default: + out.WriteString("null") + } + return nil +} + +func writeIndent(out *strings.Builder, depth int) { + for i := 0; i < depth; i++ { + out.WriteString(" ") + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// Command entry — port of observe.ts usage(). + +// runUsage implements `ocx usage` (and `ocx observe usage`). It assumes the +// caller validated ownership; argv carries only this command's own arguments. +func runUsage(args []string, deps Deps) int { + rest := append([]string(nil), args...) + jsonOutput := takeUsageFlag(&rest, "--json") + rangeValue, rangeGiven, err := takeUsageOption(&rest, "--range") + if err != nil { + // takeOption throws its CliUsageError without the USAGE block; only the + // range/surface validation and rejectArgs carry it. + return usageFailureNoUsage(deps, err) + } + if !rangeGiven { + rangeValue = "30d" + } + surfaceValue, surfaceGiven, err := takeUsageOption(&rest, "--surface") + if err != nil { + return usageFailureNoUsage(deps, err) + } + if !surfaceGiven { + surfaceValue = "all" + } + providerValue, _, err := takeUsageOption(&rest, "--provider") + if err != nil { + return usageFailureNoUsage(deps, err) + } + modelValue, _, err := takeUsageOption(&rest, "--model") + if err != nil { + return usageFailureNoUsage(deps, err) + } + // `1d` is accepted here as well as server-side so the CLI does not reject an + // alias the API would have understood. + allowedRanges := append([]string(nil), usageRanges...) + allowedRanges = append(allowedRanges, "1d") + if !containsString(allowedRanges, rangeValue) { + return usageFailure(deps, errors.New("--range must be one of " + strings.Join(usageRanges, ", ") + " (1d aliases today)")) + } + if !containsString(usageSurfaces, surfaceValue) { + return usageFailure(deps, errors.New("--surface must be one of " + strings.Join(usageSurfaces, ", "))) + } + if len(rest) > 0 { + return usageFailure(deps, usageUnexpectedArgs(rest)) + } + + var providerParam, modelParam *string + if providerValue != "" { + provider := providerValue + providerParam = &provider + } + if modelValue != "" { + model := modelValue + modelParam = &model + } + queryString := usageQuery(&rangeValue, &surfaceValue, providerParam, modelParam) + + body, rawText, status, fetchErr := fetchUsageReport(deps, queryString) + if fetchErr != nil { + apiErr := usageAPIError{message: fetchErr.Error(), status: status} + return reportUsageAPIError(deps, apiErr) + } + if status < 200 || status >= 300 { + return reportUsageAPIError(deps, usageAPIError{ + message: usageResponseMessage(body, rawText, status), + status: status, + }) + } + + if jsonOutput { + if err := writeUsageJSON(deps.Stdout, body, rawText); err != nil { + fmt.Fprintln(deps.Stderr, "Error: "+err.Error()) + return 1 + } + return 0 + } + lines := formatUsageReportLines(viewUsageReport(body)) + for _, line := range lines { + fmt.Fprintln(deps.Stdout, line) + } + return 0 +} + +// usageAPIError mirrors RuntimeApiError's observable shape: a message and an +// HTTP status that selects the exit code. +type usageAPIError struct { + message string + status int +} + +func reportUsageAPIError(deps Deps, apiErr usageAPIError) int { + fmt.Fprintln(deps.Stderr, "Error: "+apiErr.message) + switch apiErr.status { + case 404: + return usageExitMissing + case 409: + return usageExitConflict + default: + return 1 + } +} + +// usageFailure mirrors a CliUsageError carrying the USAGE block reaching +// runCliAction: message plus the observe USAGE block on stderr, exit 2. +func usageFailure(deps Deps, err error) int { + fmt.Fprintln(deps.Stderr, "Error: "+err.Error()) + fmt.Fprintln(deps.Stderr, observeUsageUsage) + return usageExitUsage +} + +// usageFailureNoUsage mirrors a CliUsageError constructed without a USAGE +// block (takeOption's `--flag requires a value`): only the Error line prints. +func usageFailureNoUsage(deps Deps, err error) int { + fmt.Fprintln(deps.Stderr, "Error: "+err.Error()) + return usageExitUsage +} + +func containsString(values []string, want string) bool { + for _, value := range values { + if value == want { + return true + } + } + return false +} diff --git a/go/internal/ocxcli/usage_command_test.go b/go/internal/ocxcli/usage_command_test.go new file mode 100644 index 0000000000..0628afabfa --- /dev/null +++ b/go/internal/ocxcli/usage_command_test.go @@ -0,0 +1,281 @@ +package ocxcli + +import ( + "bytes" + "errors" + "fmt" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/lidge-jun/opencodex/go/internal/jsonwire" +) + +// The fixture payloads mirror tests/cli-usage-report.test.ts so the Go renderer +// is diffed against the same shapes the TypeScript oracle pins. + +func usageFixturePayload() string { + return `{ + "range": "today", + "surface": "all", + "since": 1756000000000, + "summary": { + "requests": 1447, + "totalTokens": 178521375, + "inputTokens": 4489102, + "outputTokens": 1283441, + "cachedInputTokens": 172748832, + "estimatedCostUsd": 12.3456, + "unpricedRequests": 0, + "unmeteredRequests": 0 + }, + "providers": [{"provider": "xai", "requests": 1447, "totalTokens": 178521375, "estimatedCostUsd": 12.3456}], + "models": [{"provider": "xai", "model": "grok-4.6", "requests": 1447, "totalTokens": 178521375, "estimatedCostUsd": 12.3456}], + "days": [{"date": "2026-08-22", "requests": 1447, "totalTokens": 178521375, "estimatedCostUsd": 12.3456}], + "accounts": [] +}` +} + +// usageFixtureServer serves an attested /healthz identity probe (the +// findLiveProxy gate) plus a canned /api/usage response. +func usageFixtureServer(t *testing.T, handler http.HandlerFunc) *httptest.Server { + t.Helper() + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/healthz" { + w.Header().Set("Content-Type", "application/json") + fmt.Fprint(w, `{"status":"ok","service":"opencodex","version":"test","uptime":1,"pid":1}`) + return + } + handler(w, r) + })) + t.Cleanup(server.Close) + return server +} + +func atoi(t *testing.T, value string) int { + t.Helper() + number := 0 + for _, digit := range value { + if digit < '0' || digit > '9' { + t.Fatalf("non-numeric port %q", value) + } + number = number*10 + int(digit-'0') + } + return number +} + +func TestUsageRendererMatchesTypeScriptFixture(t *testing.T) { + body, err := jsonwire.Parse([]byte(usageFixturePayload())) + if err != nil { + t.Fatal(err) + } + lines := formatUsageReportLines(viewUsageReport(body)) + out := strings.Join(lines, "\n") + for _, want := range []string{ + "Usage — today", + "Requests 1,447", + "Tokens 178,521,375 (in 4,489,102 / out 1,283,441 / cached 172,748,832)", + "Est. cost ~$12.3456 API list-price equivalent (this range)", + "PROVIDER ", + "grok-4.6", + "Not a billing receipt. Subscription usage or provider credits may apply instead.", + } { + if !strings.Contains(out, want) { + t.Fatalf("usage rendering missing %q:\n%s", want, out) + } + } + if strings.Contains(out, "item(s)") { + t.Fatalf("usage rendering regressed to the flattener:\n%s", out) + } +} + +func TestUsageRendererTerminalTextIsInert(t *testing.T) { + control := "demo-\\u001b]52;c;SGVsbG8=\\u0007-after\\nnext\\u007f-\\u0080" + payload := fmt.Sprintf(`{"range":"today","summary":{"requests":1,"totalTokens":2,"estimatedCostUsd":0},"providers":[{"provider":"%s","requests":1,"totalTokens":2}],"models":[{"provider":"%s","model":"%s","requests":1,"totalTokens":2}],"filter":{"provider":"%s","model":"%s","matched":true,"comboOverlap":false}}`, control, control, control, control, control) + body, err := jsonwire.Parse([]byte(payload)) + if err != nil { + t.Fatal(err) + } + out := strings.Join(formatUsageReportLines(viewUsageReport(body)), "\n") + // U+0080 is matched by the TS [\x7f-\x9f] class and must render as \u0080; + // compare it separately because a raw 0x80 byte is not valid UTF-8 in Go. + escaped := usageTerminalText("demo-\x1b]52;c;SGVsbG8=\x07-after\nnext\x7f") + if !strings.Contains(out, escaped) { + t.Fatalf("rendering lost the escaped control text %q:\n%s", escaped, out) + } + if !strings.Contains(out, `\u0080`) { + t.Fatalf("U+0080 must render as \\u0080 like the TS oracle:\n%s", out) + } + if !strings.Contains(out, `\x0anext`) { + t.Fatalf("LF inside a label must be escaped like the TS oracle (\\x0a):\n%s", out) + } + for _, banned := range []string{"\x1b", "\x07", "\x7f"} { + if strings.Contains(out, banned) { + t.Fatalf("rendering emitted raw control character %q:\n%s", banned, out) + } + } +} + +func TestUsageUnmatchedFilterMessage(t *testing.T) { + payload := `{"range":"today","summary":{"requests":0,"totalTokens":0,"estimatedCostUsd":0},"providers":[],"models":[],"days":[],"filter":{"provider":"nope","model":null,"matched":false,"comboOverlap":false}}` + body, err := jsonwire.Parse([]byte(payload)) + if err != nil { + t.Fatal(err) + } + out := strings.Join(formatUsageReportLines(viewUsageReport(body)), "\n") + want := "No usage recorded for provider \"nope\" in this range." + if !strings.Contains(out, want) { + t.Fatalf("missing unmatched-filter message %q:\n%s", want, out) + } +} + +func TestUsageAccountsWithheldUnderFilter(t *testing.T) { + payload := `{"range":"today","summary":{"requests":2,"totalTokens":10,"estimatedCostUsd":1},"providers":[{"provider":"p","requests":2,"totalTokens":10,"estimatedCostUsd":1}],"accounts":[{"accountLogLabel":"acct@example","requests":2,"totalTokens":10,"estimatedCostUsd":1}],"filter":{"provider":"p","model":null,"matched":true,"comboOverlap":false}}` + body, err := jsonwire.Parse([]byte(payload)) + if err != nil { + t.Fatal(err) + } + out := strings.Join(formatUsageReportLines(viewUsageReport(body)), "\n") + if !strings.Contains(out, "ACCOUNT: not reported under a provider or model filter") { + t.Fatalf("missing withheld-account note:\n%s", out) + } +} + +func TestUsageAmbiguousAccountMarked(t *testing.T) { + payload := `{"range":"today","summary":{"requests":2,"totalTokens":10,"estimatedCostUsd":1},"accounts":[{"accountLogLabel":"label","ambiguous":true,"requests":2,"totalTokens":10,"estimatedCostUsd":1}]}` + body, err := jsonwire.Parse([]byte(payload)) + if err != nil { + t.Fatal(err) + } + out := strings.Join(formatUsageReportLines(viewUsageReport(body)), "\n") + if !strings.Contains(out, "label (ambiguous)") { + t.Fatalf("missing ambiguous marker:\n%s", out) + } +} + +func TestUsageJSONMatchesStringifyIndent(t *testing.T) { + body, err := jsonwire.Parse([]byte(usageFixturePayload())) + if err != nil { + t.Fatal(err) + } + var out bytes.Buffer + deps := Deps{Version: "test", Stdout: &out, Stderr: &bytes.Buffer{}, HTTPClient: http.DefaultClient} + if err := writeUsageJSON(deps.Stdout, body, ""); err != nil { + t.Fatal(err) + } + text := out.String() + if !strings.HasSuffix(text, "\n") { + t.Fatal("JSON output must end with a newline (console.log)") + } + for _, want := range []string{ + "\"estimatedCostUsd\": 12.3456", + "\"totalTokens\": 178521375", + " \"providers\": [", + " \"provider\": \"xai\"", + } { + if !strings.Contains(text, want) { + t.Fatalf("JSON output missing %q:\n%s", want, text) + } + } +} + +func TestUsageArgumentValidationExits2WithUsage(t *testing.T) { + cases := []struct { + args []string + message string + wantUsage bool + }{ + {args: []string{"--range", "nope"}, message: "--range must be one of today, 7d, 30d, all (1d aliases today)", wantUsage: true}, + {args: []string{"--surface", "beard"}, message: "--surface must be one of all, codex, claude, grok", wantUsage: true}, + {args: []string{"--range"}, message: "--range requires a value"}, + {args: []string{"extra"}, message: "Unexpected argument(s): extra", wantUsage: true}, + } + for _, testCase := range cases { + var stdout, stderr bytes.Buffer + deps := Deps{Version: "test", Stdout: &stdout, Stderr: &stderr, ReadRuntime: func() (RuntimeState, error) { return RuntimeState{}, errors.New("unused") }} + code := runUsage(testCase.args, deps) + if code != usageExitUsage { + t.Fatalf("args %v exit = %d, want %d (stderr: %s)", testCase.args, code, usageExitUsage, stderr.String()) + } + if !strings.Contains(stderr.String(), "Error: "+testCase.message) { + t.Fatalf("args %v stderr missing %q:\n%s", testCase.args, testCase.message, stderr.String()) + } + if testCase.wantUsage && !strings.Contains(stderr.String(), "ocx observe usage [--range") { + t.Fatalf("args %v stderr missing USAGE block:\n%s", testCase.args, stderr.String()) + } + if !testCase.wantUsage && strings.Contains(stderr.String(), "Usage:") { + t.Fatalf("args %v stderr must not carry the USAGE block:\n%s", testCase.args, stderr.String()) + } + } +} + +func TestUsageWithoutRuntimeReportsStartHint(t *testing.T) { + var stdout, stderr bytes.Buffer + deps := Deps{Version: "test", Stdout: &stdout, Stderr: &stderr, ReadRuntime: func() (RuntimeState, error) { return RuntimeState{}, os.ErrNotExist }} + code := runUsage(nil, deps) + if code != 1 { + t.Fatalf("exit = %d, want 1", code) + } + if strings.TrimSpace(stderr.String()) != "Error: Proxy is not running. Start it with: ocx start" { + t.Fatalf("unexpected stderr: %s", stderr.String()) + } +} + +func TestUsageHTTPErrorBodyComposesReasonAndHint(t *testing.T) { + server := usageFixtureServer(t, func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusUnauthorized) + fmt.Fprint(w, `{"error":"opencodex admin token required","reason":"no token","hint":"Set OPENCODEX_ADMIN_AUTH_TOKEN to bypass file-backed admin token ACL hardening"}`) + }) + port := server.URL[len("http://127.0.0.1:"):] + runtimeState := RuntimeState{PID: 1, Port: atoi(t, port), Hostname: "127.0.0.1", AttestationSecret: "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG"} + var stdout, stderr bytes.Buffer + deps := Deps{Version: "test", Stdout: &stdout, Stderr: &stderr, ReadRuntime: func() (RuntimeState, error) { return runtimeState, nil }, HTTPClient: server.Client()} + t.Setenv("OPENCODEX_ADMIN_AUTH_TOKEN", "") + code := runUsage(nil, deps) + if code != 1 { + t.Fatalf("exit = %d, want 1", code) + } + stderrText := stderr.String() + for _, want := range []string{"Error: opencodex admin token required", "reason: no token", "hint: Set OPENCODEX_ADMIN_AUTH_TOKEN"} { + if !strings.Contains(stderrText, want) { + t.Fatalf("stderr missing %q:\n%s", want, stderrText) + } + } +} + +func TestUsageAdminTokenHeaderFromEnvAndFile(t *testing.T) { + var gotToken string + server := usageFixtureServer(t, func(w http.ResponseWriter, r *http.Request) { + gotToken = r.Header.Get("X-OpenCodex-API-Key") + fmt.Fprint(w, usageFixturePayload()) + }) + port := server.URL[len("http://127.0.0.1:"):] + runtimeState := RuntimeState{PID: 1, Port: atoi(t, port), Hostname: "127.0.0.1", AttestationSecret: "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG"} + + t.Setenv("OPENCODEX_ADMIN_AUTH_TOKEN", "ocx_admin_envtoken") + var stdout bytes.Buffer + deps := Deps{Version: "test", Stdout: &stdout, Stderr: &bytes.Buffer{}, ReadRuntime: func() (RuntimeState, error) { return runtimeState, nil }, HTTPClient: server.Client()} + if code := runUsage(nil, deps); code != 0 { + t.Fatalf("env-token run exit = %d", code) + } + if gotToken != "ocx_admin_envtoken" { + t.Fatalf("env token not sent, got %q", gotToken) + } + + t.Setenv("OPENCODEX_ADMIN_AUTH_TOKEN", "") + home := t.TempDir() + t.Setenv("OPENCODEX_HOME", home) + tokenFile := filepath.Join(home, "admin-api-token") + if err := os.WriteFile(tokenFile, []byte("ocx_admin_"+strings.Repeat("a", 43)+"\n"), 0o600); err != nil { + t.Fatal(err) + } + if code := runUsage(nil, deps); code != 0 { + t.Fatalf("file-token run exit = %d", code) + } + if gotToken != "ocx_admin_"+strings.Repeat("a", 43) { + t.Fatalf("file token not sent, got %q", gotToken) + } +} diff --git a/tests/go-cli-parity.test.ts b/tests/go-cli-parity.test.ts index 6b4794072a..0ac65bbb56 100644 --- a/tests/go-cli-parity.test.ts +++ b/tests/go-cli-parity.test.ts @@ -198,4 +198,74 @@ describe.skipIf(!goAvailable || goCLI === null)("Go CLI parity (ADR-0008, ticket testHome = mkdtempSync(join(tmpdir(), "ocx-go-cli-parity-")); expectParity(args); }); + + // usage is Go-owned (ADR-0008 post-flip seam batch 1); the TS CLI still runs + // its own implementation, so the differential compares both against the same + // mocked /api/usage payload. The fixture server answers /healthz with the + // attested identity both runtimes' live-proxy discovery probes before they + // trust runtime-port.json, and /api/usage with a canned payload. + function startUsageFixture(payload: string, status = 200): void { + testHome = mkdtempSync(join(tmpdir(), "ocx-go-usage-parity-")); + testServer = Bun.serve({ port: 0, fetch(request) { + const path = new URL(request.url).pathname; + if (path === "/healthz") { + const challenge = request.headers.get("x-opencodex-attestation-challenge") ?? ""; + const headers = challenge ? { "x-opencodex-attestation-proof": createLocalAttestationProof(secret, challenge, process.pid, testServer!.port) } : {}; + return Response.json({ status: "ok", service: "opencodex", version: "2.42.0", uptime: 1, pid: process.pid, port: testServer!.port }, { headers }); + } + return new Response(payload, { status, headers: { "content-type": "application/json" } }); + }}); + writeFileSync(join(testHome, "runtime-port.json"), JSON.stringify({ pid: process.pid, port: testServer.port, hostname: "127.0.0.1", attestationSecret: secret })); + } + const usagePayload = JSON.stringify({ + range: "today", + surface: "all", + since: 1756000000000, + summary: { requests: 1447, totalTokens: 178521375, inputTokens: 4489102, outputTokens: 1283441, cachedInputTokens: 172748832, estimatedCostUsd: 12.3456, unpricedRequests: 0, unmeteredRequests: 0 }, + providers: [{ provider: "xai", requests: 1447, totalTokens: 178521375, estimatedCostUsd: 12.3456 }], + models: [{ provider: "xai", model: "grok-4.6", requests: 1447, totalTokens: 178521375, estimatedCostUsd: 12.3456 }], + days: [{ date: "2026-08-22", requests: 1447, totalTokens: 178521375, estimatedCostUsd: 12.3456 }], + accounts: [], + }); + test.each([ + { args: ["usage"] }, + { args: ["usage", "--json"] }, + { args: ["usage", "--range", "7d"] }, + { args: ["usage", "--provider", "xai", "--json"] }, + { args: ["observe", "usage", "--json"] }, + ])("diffs Go-owned usage output and exit code for $args", async ({ args }) => { + startUsageFixture(usagePayload); + // spawnSync blocks Bun's event loop, which starves the fixture server the + // same way the #43 drill hit; live-fixture rows drive both CLIs async. + const ts = await runTsAsync(args); + const go = await runGoAsync(args); + expect(go).toEqual(ts); + expect(ts).toMatchObject({ code: 0, stderr: "" }); + }); + test.each([ + { args: ["usage", "--range", "nope"] }, + { args: ["usage", "--surface", "beard"] }, + { args: ["usage", "--range"] }, + { args: ["usage", "extra"] }, + ])("diffs usage argument validation for $args", ({ args }) => { + testHome = mkdtempSync(join(tmpdir(), "ocx-go-usage-parity-")); + expect(expectParity(args)).toMatchObject({ code: 2 }); + }); + test("diffs usage against a non-JSON error body and its exit code", async () => { + startUsageFixture("nope", 500); + const ts = await runTsAsync(["usage"]); + const go = await runGoAsync(["usage"]); + expect(go).toEqual(ts); + expect(ts).toMatchObject({ code: 1 }); + }); + test("diffs usage help in both spellings", () => { + testHome = mkdtempSync(join(tmpdir(), "ocx-go-usage-parity-")); + expect(expectParity(["help", "usage"])); + expect(expectParity(["usage", "--help"])); + }); + test("diffs usage when no proxy is running", () => { + testHome = mkdtempSync(join(tmpdir(), "ocx-go-usage-parity-")); + expect(expectParity(["usage"])).toMatchObject({ code: 1 }); + expect(expectParity(["observe", "usage"])).toMatchObject({ code: 1 }); + }); }); From f437f8db9f1119b7a6099e12fbb5d3166fbc54dc Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Tue, 8 Sep 2026 06:32:20 +0800 Subject: [PATCH 116/165] feat(go): flip ocx logout to Go-owned (issue #51) Ports the TypeScript logout dispatch (src/cli/dispatch.ts) and the auth-store mutation (src/oauth/store.ts removeCredential) to Go: argument parsing with the exact usage taxonomy (exit 2 naming the problem), the not-found disposition (exit 4, stderr line or --json envelope), and read-normalise-remove-persist on auth.json so the on-disk credential store matches what TypeScript would leave (JSON.stringify(store,null,2)+newline, legacy rows upgraded to multiauth). Adds jsonwire.EncodePretty, the V8-exact JSON.stringify(value,null,2) renderer the Go CLI needs for every 2-space JSON surface. Differential oracle: tests/go-cli-parity.test.ts diffs the real TS CLI against the Go binary for help in both spellings, six argument-validation rows, not-found text and JSON, last-account removal, promotion after active removal, and legacy row normalization - stdout/stderr/exit code and the resulting auth.json bytes all match. 76/76 parity green; go test ./... green; typecheck green. Co-Authored-By: Claude Code --- go/internal/jsonwire/pretty.go | 96 +++++++ go/internal/jsonwire/pretty_test.go | 65 +++++ go/internal/ocxcli/auth_store.go | 366 +++++++++++++++++++++++++++ go/internal/ocxcli/cli.go | 12 +- go/internal/ocxcli/logout_command.go | 201 +++++++++++++++ tests/go-cli-parity.test.ts | 98 +++++++ 6 files changed, 836 insertions(+), 2 deletions(-) create mode 100644 go/internal/jsonwire/pretty.go create mode 100644 go/internal/jsonwire/pretty_test.go create mode 100644 go/internal/ocxcli/auth_store.go create mode 100644 go/internal/ocxcli/logout_command.go diff --git a/go/internal/jsonwire/pretty.go b/go/internal/jsonwire/pretty.go new file mode 100644 index 0000000000..7f1cdad76c --- /dev/null +++ b/go/internal/jsonwire/pretty.go @@ -0,0 +1,96 @@ +package jsonwire + +import "bytes" + +// EncodePretty emits the value exactly like ECMAScript JSON.stringify(value, +// null, 2): the same compact member/element order and string/number encoding +// as Encode, but with each object member and array element on its own line, +// indented two spaces per nesting level, and `": "` after each object key. +// +// The CLI parity surface needs this because the TypeScript command layer +// reports many management DTOs with console.log(JSON.stringify(x, null, 2)), +// and re-encoding through Go's encoding/json would differ in key order, number +// literals, and string escaping. Empty objects and arrays stay on one line, +// exactly as V8 emits them. +func (v *Value) EncodePretty() ([]byte, error) { + var out bytes.Buffer + if err := v.encodePretty(&out, 0); err != nil { + return nil, err + } + return out.Bytes(), nil +} + +func (v *Value) encodePretty(out *bytes.Buffer, depth int) error { + switch v.kind { + case Null: + out.WriteString("null") + case Bool: + if v.b { + out.WriteString("true") + } else { + out.WriteString("false") + } + case Number: + out.WriteString(v8NumberString(v.num)) + case String: + raw, err := EncodeString(v.str) + if err != nil { + return err + } + out.Write(raw) + case Array: + if len(v.arr) == 0 { + out.WriteString("[]") + return nil + } + out.WriteByte('[') + for i, member := range v.arr { + if i > 0 { + out.WriteByte(',') + } + out.WriteByte('\n') + writePrettyIndent(out, depth+1) + if err := member.encodePretty(out, depth+1); err != nil { + return err + } + } + out.WriteByte('\n') + writePrettyIndent(out, depth) + out.WriteByte(']') + case Object: + members := orderedObjectMembers(v.obj) + if len(members) == 0 { + out.WriteString("{}") + return nil + } + out.WriteByte('{') + for i, member := range members { + if i > 0 { + out.WriteByte(',') + } + out.WriteByte('\n') + writePrettyIndent(out, depth+1) + rawKey, err := EncodeString(member.Key) + if err != nil { + return err + } + out.Write(rawKey) + out.WriteString(": ") + if err := member.Value.encodePretty(out, depth+1); err != nil { + return err + } + } + out.WriteByte('\n') + writePrettyIndent(out, depth) + out.WriteByte('}') + default: + out.WriteString("null") + } + return nil +} + +func writePrettyIndent(out *bytes.Buffer, depth int) { + for i := 0; i < depth; i++ { + out.WriteString(" ") + } +} diff --git a/go/internal/jsonwire/pretty_test.go b/go/internal/jsonwire/pretty_test.go new file mode 100644 index 0000000000..64d216c27d --- /dev/null +++ b/go/internal/jsonwire/pretty_test.go @@ -0,0 +1,65 @@ +package jsonwire_test + +import ( + "strings" + "testing" + + "github.com/lidge-jun/opencodex/go/internal/jsonwire" +) + +// TestEncodePrettyMatchesV8 pins EncodePretty against JSON.stringify(value, +// null, 2) as emitted by Node/V8 for the same parsed document: two-space +// indent, each object member and array element on its own line, ": " after +// object keys, inline empty containers, canonical re-encoded numbers, and +// unchanged string escaping. +func TestEncodePrettyMatchesV8(t *testing.T) { + cases := map[string]string{ + `{"a":{"b":1,"c":[1,2,{"x":"y"}],"e":[]},"d":2,"n":1.0,"big":1e21}`: + "{\n \"a\": {\n \"b\": 1,\n \"c\": [\n 1,\n 2,\n {\n \"x\": \"y\"\n }\n ],\n \"e\": []\n },\n \"d\": 2,\n \"n\": 1,\n \"big\": 1e+21\n}", + `{"only":{}}`: + "{\n \"only\": {}\n}", + `[[],{"z":null}]`: + "[\n [],\n {\n \"z\": null\n }\n]", + `{"s":"a\"b\\c\nd\u2028e"}`: + "{\n \"s\": \"a\\\"b\\\\c\\nd\u2028e\"\n}", + `{"-0":-0.0,"x":-1.5e-7,"y":0.000001}`: + "{\n \"-0\": 0,\n \"x\": -1.5e-7,\n \"y\": 0.000001\n}", + } + for payload, want := range cases { + value, err := jsonwire.Parse([]byte(payload)) + if err != nil { + t.Fatalf("parse %q: %v", payload, err) + } + got, err := value.EncodePretty() + if err != nil { + t.Fatalf("encode %q: %v", payload, err) + } + if string(got) != want { + t.Fatalf("pretty mismatch for %s\n got: %s\nwant: %s", payload, got, want) + } + } +} + +// TestEncodePrettyArrayIndexOrdering confirms pretty output reuses the same +// array-index-first member ordering as compact Encode (V8 own-property order). +func TestEncodePrettyArrayIndexOrdering(t *testing.T) { + payload := `{"b":1,"2":"two","a":3,"10":"ten"}` + value, err := jsonwire.Parse([]byte(payload)) + if err != nil { + t.Fatal(err) + } + got, err := value.EncodePretty() + if err != nil { + t.Fatal(err) + } + compact, err := value.Encode() + if err != nil { + t.Fatal(err) + } + if !strings.Contains(string(got), "\"2\": \"two\"") || !strings.Contains(string(got), "\"10\": \"ten\"") { + t.Fatalf("pretty output lost array-index ordering: %s", got) + } + if string(compact) != `{"2":"two","10":"ten","b":1,"a":3}` { + t.Fatalf("compact reference drifted: %s", compact) + } +} diff --git a/go/internal/ocxcli/auth_store.go b/go/internal/ocxcli/auth_store.go new file mode 100644 index 0000000000..df73347dbf --- /dev/null +++ b/go/internal/ocxcli/auth_store.go @@ -0,0 +1,366 @@ +// Auth-store reads and rewrites for the Go-owned credential commands +// (ocx logout today; ocx login OAuth persistence joins it on the same seam). +// +// The store lives at /auth.json and is owned by TypeScript's +// src/oauth/store.ts: multiauth values are `{ activeAccountId, accounts: [...] }` +// per provider, each account is `{ id, credential, alias?, needsReauth?, +// addedAt? }`, and credentials are `{ access, refresh, expires, email?, +// accountId?, source?, projectId?, apiBaseUrl?, kiro? }`. +// +// Every Go write must reproduce what store.ts's mutateStore would persist: +// the file is normalised on load (legacy single-credential values become +// multiauth sets, unknown or invalid rows are dropped, key order is rebuilt), +// and the result is written as JSON.stringify(store, null, 2) + "\n". The +// differential oracle pins both the stdout contract and the resulting auth.json +// bytes, so this module mirrors the normalisation exactly rather than only +// mutating the JSON tree. +package ocxcli + +import ( + "crypto/sha256" + "encoding/hex" + "errors" + "net" + "net/url" + "os" + "path/filepath" + "strconv" + "strings" + + "github.com/lidge-jun/opencodex/go/internal/config" + "github.com/lidge-jun/opencodex/go/internal/jsonwire" +) + +// authStorePath mirrors getAuthStorePath in src/oauth/store.ts. +func authStorePath() (string, error) { + dir, err := config.Dir() + if err != nil { + return "", err + } + return filepath.Join(dir, "auth.json"), nil +} + +// readAuthStore loads and normalises auth.json the way loadAuthStoreInternal +// does: a missing file yields an empty store, and a file that is not valid +// JSON yields an empty store too (TypeScript additionally backs the invalid +// file up; the Go CLI must not move user files, so it only logs and proceeds). +// The returned object mirrors the normalised AuthStore: one member per +// provider in file order. +func readAuthStore() (*jsonwire.Value, error) { + path, err := authStorePath() + if err != nil { + return nil, err + } + raw, err := os.ReadFile(path) + if err != nil { + if errors.Is(err, os.ErrNotExist) { + return jsonwire.ObjectValue(), nil + } + return nil, err + } + parsed, parseErr := jsonwire.Parse(raw) + if parseErr != nil || parsed.Kind() != jsonwire.Object { + return jsonwire.ObjectValue(), nil + } + return normalizeAuthStore(parsed), nil +} + +// writeAuthStore persists the normalised store exactly like store.ts persist(): +// JSON.stringify(store, null, 2) followed by a trailing newline. +func writeAuthStore(store *jsonwire.Value) error { + path, err := authStorePath() + if err != nil { + return err + } + pretty, err := store.EncodePretty() + if err != nil { + return err + } + pretty = append(pretty, '\n') + if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil { + return err + } + return os.WriteFile(path, pretty, 0o600) +} + +// normalizeAuthStore mirrors normalizeAuthStore in src/oauth/store.ts. Rows +// that do not normalise (no accounts, invalid credential) are dropped; legacy +// single-credential values are upgraded to a one-account multiauth set whose +// account id is sha256(accountId ?? email ?? refresh)[:32]. +func normalizeAuthStore(parsed *jsonwire.Value) *jsonwire.Value { + out := jsonwire.ObjectValue() + for _, member := range parsed.Members() { + set := normalizeAccountSet(member.Value) + if set == nil { + continue + } + out.Set(member.Key, set) + } + return out +} + +// normalizeAccountSet mirrors normalizeAccountSet: a value with an `accounts` +// array normalises each row and repins the active id; any other object is +// treated as a legacy single credential and upgraded. +func normalizeAccountSet(raw *jsonwire.Value) *jsonwire.Value { + if raw == nil || raw.Kind() != jsonwire.Object { + return nil + } + accountsArray := raw.Find("accounts") + if accountsArray != nil && accountsArray.Kind() == jsonwire.Array { + set := jsonwire.ObjectValue() + accounts := jsonwire.EmptyArray() + activeAccountId := "" + if active := raw.Find("activeAccountId"); active != nil && active.Kind() == jsonwire.String { + activeAccountId = active.String() + } + firstID := "" + for _, element := range accountsArray.Elements() { + account := normalizeAccount(element) + if account == nil { + continue + } + accounts.AppendArray(account) + if firstID == "" { + if id := account.Find("id"); id != nil && id.Kind() == jsonwire.String { + firstID = id.String() + } + } + } + if len(accounts.Elements()) == 0 { + return nil + } + active := firstID + if activeAccountId != "" { + for _, element := range accounts.Elements() { + if id := element.Find("id"); id != nil && id.Kind() == jsonwire.String && id.String() == activeAccountId { + active = activeAccountId + break + } + } + } + set.Set("activeAccountId", jsonwire.StringValue(active)) + set.Set("accounts", accounts) + return set + } + // Legacy single-credential value. + credential := normalizeCredential(raw) + if credential == nil { + return nil + } + id := newAccountID(credential) + set := jsonwire.ObjectValue() + account := jsonwire.ObjectValue() + account.Set("id", jsonwire.StringValue(id)) + account.Set("credential", credential) + accounts := jsonwire.EmptyArray() + accounts.AppendArray(account) + set.Set("activeAccountId", jsonwire.StringValue(id)) + set.Set("accounts", accounts) + return set +} + +// normalizeAccount mirrors normalizeAccount in src/oauth/store.ts. +func normalizeAccount(raw *jsonwire.Value) *jsonwire.Value { + if raw == nil || raw.Kind() != jsonwire.Object { + return nil + } + id := raw.Find("id") + if id == nil || id.Kind() != jsonwire.String || id.String() == "" { + return nil + } + credential := normalizeCredential(raw.Find("credential")) + if credential == nil { + return nil + } + account := jsonwire.ObjectValue() + account.Set("id", jsonwire.StringValue(id.String())) + account.Set("credential", credential) + if alias := raw.Find("alias"); alias != nil && alias.Kind() == jsonwire.String { + if trimmed := strings.TrimSpace(alias.String()); trimmed != "" { + account.Set("alias", jsonwire.StringValue(trimmed)) + } + } + if needsReauth := raw.Find("needsReauth"); needsReauth != nil && needsReauth.Kind() == jsonwire.Bool && needsReauth.Bool() { + account.Set("needsReauth", jsonwire.BoolValue(true)) + } + if addedAt := raw.Find("addedAt"); addedAt != nil && addedAt.Kind() == jsonwire.Number { + if number, err := numberAsFloat(addedAt); err == nil && number == number { // not NaN + account.Set("addedAt", jsonwire.NumberFrom(number)) + } + } + return account +} + +// normalizeCredential mirrors normalizeCredential in src/oauth/store.ts: only +// access/refresh/expires survive unconditionally, followed by the optional +// identity, source, project, and (allowlisted) apiBaseUrl fields. +func normalizeCredential(raw *jsonwire.Value) *jsonwire.Value { + if raw == nil || raw.Kind() != jsonwire.Object { + return nil + } + access := raw.Find("access") + refresh := raw.Find("refresh") + expires := raw.Find("expires") + if access == nil || access.Kind() != jsonwire.String || + refresh == nil || refresh.Kind() != jsonwire.String || + expires == nil || expires.Kind() != jsonwire.Number { + return nil + } + out := jsonwire.ObjectValue() + out.Set("access", jsonwire.StringValue(access.String())) + out.Set("refresh", jsonwire.StringValue(refresh.String())) + number, err := numberAsFloat(expires) + if err != nil { + return nil + } + out.Set("expires", jsonwire.NumberFrom(number)) + setStringIfPresent := func(key string, out *jsonwire.Value) { + if field := raw.Find(key); field != nil && field.Kind() == jsonwire.String && field.String() != "" { + out.Set(key, jsonwire.StringValue(field.String())) + } + } + setStringIfPresent("email", out) + setStringIfPresent("accountId", out) + if source := raw.Find("source"); source != nil && source.Kind() == jsonwire.String { + switch source.String() { + case "oauth", "local-cli", "credential-file", "environment", "manual": + out.Set("source", jsonwire.StringValue(source.String())) + } + } + setStringIfPresent("projectId", out) + if apiBaseURL := raw.Find("apiBaseUrl"); apiBaseURL != nil && apiBaseURL.Kind() == jsonwire.String { + if validated := validateCopilotAPIBaseURL(apiBaseURL.String()); validated != "" { + out.Set("apiBaseUrl", jsonwire.StringValue(validated)) + } + } + if kiro := normalizeKiro(raw.Find("kiro")); kiro != nil { + out.Set("kiro", kiro) + } + return out +} + +// normalizeKiro mirrors the kiro block of normalizeCredential: string fields +// survive trimmed when non-empty, within bounds, and free of control chars. +func normalizeKiro(raw *jsonwire.Value) *jsonwire.Value { + if raw == nil || raw.Kind() != jsonwire.Object { + return nil + } + clean := func(key string, max int) string { + field := raw.Find(key) + if field == nil || field.Kind() != jsonwire.String { + return "" + } + value := strings.TrimSpace(field.String()) + if value == "" || len(value) > max { + return "" + } + for _, r := range value { + if r < 0x20 || r == 0x7f { + return "" + } + } + return value + } + out := jsonwire.ObjectValue() + profileARN := clean("profileArn", 1024) + ssoRegion := clean("ssoRegion", 64) + apiRegion := clean("apiRegion", 64) + clientID := clean("clientId", 4096) + clientSecret := clean("clientSecret", 4096) + if profileARN == "" && ssoRegion == "" && apiRegion == "" && clientID == "" && clientSecret == "" { + return nil + } + if profileARN != "" { + out.Set("profileArn", jsonwire.StringValue(profileARN)) + } + if ssoRegion != "" { + out.Set("ssoRegion", jsonwire.StringValue(ssoRegion)) + } + if apiRegion != "" { + out.Set("apiRegion", jsonwire.StringValue(apiRegion)) + } + if clientID != "" { + out.Set("clientId", jsonwire.StringValue(clientID)) + } + if clientSecret != "" { + out.Set("clientSecret", jsonwire.StringValue(clientSecret)) + } + return out +} + +// newAccountID mirrors newAccountId: sha256 of accountId ?? email ?? refresh, +// hex, first 32 characters. +func newAccountID(credential *jsonwire.Value) string { + identity := "" + if field := credential.Find("accountId"); field != nil && field.Kind() == jsonwire.String && field.String() != "" { + identity = field.String() + } else if field := credential.Find("email"); field != nil && field.Kind() == jsonwire.String && field.String() != "" { + identity = field.String() + } else if field := credential.Find("refresh"); field != nil && field.Kind() == jsonwire.String { + identity = field.String() + } + sum := sha256.Sum256([]byte(identity)) + return hex.EncodeToString(sum[:])[:32] +} + +// numberAsFloat reads a jsonwire Number as float64 (V8-style parse). +func numberAsFloat(value *jsonwire.Value) (float64, error) { + return strconv.ParseFloat(value.NumberRaw(), 64) +} + +// validateCopilotAPIBaseURL mirrors validateCopilotApiBaseUrl in +// src/oauth/github-copilot.ts: only https origins under githubcopilot.com +// (or bare api.githubcopilot.com) survive a credential rewrite; everything +// else is dropped so auth.json cannot become an SSRF springboard. +func validateCopilotAPIBaseURL(raw string) string { + trimmed := strings.TrimSpace(raw) + if trimmed == "" { + return "" + } + parsed, err := url.Parse(trimmed) + if err != nil { + return "" + } + if parsed.Scheme != "https" { + return "" + } + if parsed.User != nil { + return "" + } + if parsed.Port() != "" && parsed.Port() != "443" { + return "" + } + host := strings.ToLower(parsed.Hostname()) + if host == "localhost" || host == "127.0.0.1" || host == "::1" || strings.HasSuffix(host, ".localhost") { + return "" + } + if isNumericIPv4(host) || strings.Contains(host, ":") { + return "" + } + if host != "api.githubcopilot.com" && !strings.HasSuffix(host, ".githubcopilot.com") { + return "" + } + return "https://" + host +} + +func isNumericIPv4(host string) bool { + if net.ParseIP(host) == nil { + return false + } + parts := strings.Split(host, ".") + if len(parts) != 4 { + return false + } + for _, part := range parts { + if part == "" || len(part) > 3 { + return false + } + for _, r := range part { + if r < '0' || r > '9' { + return false + } + } + } + return true +} diff --git a/go/internal/ocxcli/cli.go b/go/internal/ocxcli/cli.go index aa63ead65b..5727f34309 100644 --- a/go/internal/ocxcli/cli.go +++ b/go/internal/ocxcli/cli.go @@ -65,8 +65,8 @@ var Commands = []Command{ {Name: "status", Usage: "ocx status", Summary: "Check proxy status.", Owner: GoOwned}, {Name: "doctor", Usage: "ocx doctor", Summary: "Diagnose the environment.", Owner: GoOwned}, {Name: "debug", Usage: "ocx debug ", Summary: "Manage debug settings.", Owner: TypeScriptOwned}, - {Name: "login", Usage: "ocx login ", Summary: "Log in to a provider.", Owner: TypeScriptOwned}, - {Name: "logout", Usage: "ocx logout ", Summary: "Log out from a provider.", Owner: TypeScriptOwned}, + {Name: "login", Usage: "ocx login ", Summary: "OAuth or API-key login for a provider.", Owner: TypeScriptOwned}, + {Name: "logout", Usage: "ocx logout ", Summary: "Remove a stored provider login.", Owner: GoOwned}, {Name: "gui", Usage: "ocx gui", Summary: "Open the dashboard.", Owner: TypeScriptOwned}, {Name: "update", Usage: "ocx update [--tag ]", Summary: "Update OpenCodex.", Owner: TypeScriptOwned}, {Name: "restart", Usage: "ocx restart", Summary: "Restart the proxy.", Owner: TypeScriptOwned}, @@ -259,6 +259,8 @@ func Run(args []string, deps Deps) int { return runCodexShim(args[1:], deps) case "health": return runHealth(args[1:], deps) + case "logout": + return runLogout(args[1:], deps) case "ready": return runReady(args[1:], deps) case "models": @@ -340,6 +342,12 @@ func printSubcommandHelp(name string, deps Deps) int { fmt.Fprintf(deps.Stdout, "Usage: %s\n\n%s\n", command.Usage, command.Summary) } } + case "logout": + for _, command := range Commands { + if command.Name == "logout" { + fmt.Fprintf(deps.Stdout, "Usage: %s\n\n%s\n", command.Usage, command.Summary) + } + } case "config": fmt.Fprint(deps.Stdout, configHelp) default: diff --git a/go/internal/ocxcli/logout_command.go b/go/internal/ocxcli/logout_command.go new file mode 100644 index 0000000000..18c3c95eeb --- /dev/null +++ b/go/internal/ocxcli/logout_command.go @@ -0,0 +1,201 @@ +// ocx logout — Go-native port of the TypeScript dispatch handler in +// src/cli/dispatch.ts plus removeCredential in src/oauth/store.ts. +// +// The differential oracle diffs this implementation against the TypeScript CLI +// for the same argv and home: argument validation (exit 2, usage line naming +// the problem), the not-found disposition (exit 4, JSON envelope or stderr +// line), the success path (exit 0, "Logged out of ."), and the resulting +// auth.json bytes. Every mutation runs read-normalise-remove-persist exactly +// like mutateStore so the on-disk store matches what TypeScript would leave. +package ocxcli + +import ( + "fmt" + "strings" + + "github.com/lidge-jun/opencodex/go/internal/jsonwire" +) + +const logoutUsagePrefix = "Usage: ocx logout [--json]" + +// isValidOAuthProviderName mirrors isValidProviderName in +// src/config/provider-name.ts: trim-stable, alphanumeric start/end with +// internal ._- allowed, max 64 chars, and none of the reserved object keys. +func isValidOAuthProviderName(name string) bool { + if name != strings.TrimSpace(name) { + return false + } + switch strings.ToLower(name) { + case "__proto__", "prototype", "constructor", "policy": + return false + } + if len(name) == 0 || len(name) > 64 { + return false + } + runes := []rune(name) + valid := func(r rune) bool { + return r >= 'a' && r <= 'z' || r >= 'A' && r <= 'Z' || r >= '0' && r <= '9' || r == '.' || r == '_' || r == '-' + } + if !valid(runes[0]) || !valid(runes[len(runes)-1]) { + return false + } + for _, r := range runes { + if !valid(r) { + return false + } + } + return true +} + +// runLogout parses argv the way the TypeScript dispatch does and returns the +// process code. provider is the only positional; any leading-dash token is an +// option, --json is the only recognised one. +func runLogout(args []string, deps Deps) int { + logoutArgs := args + wantsJSON := false + var unknownFlags []string + var positionals []string + for _, arg := range logoutArgs { + if strings.HasPrefix(arg, "-") { + if arg != "--json" { + unknownFlags = append(unknownFlags, arg) + } else { + wantsJSON = true + } + continue + } + positionals = append(positionals, arg) + } + name := "" + if len(positionals) > 0 { + name = strings.TrimSpace(positionals[0]) + } + name = strings.ToLower(name) + malformed := name != "" && !isValidOAuthProviderName(name) + + if len(unknownFlags) > 0 || len(positionals) > 1 || name == "" || malformed { + problem := "" + switch { + case len(unknownFlags) > 0: + problem = "unknown option " + unknownFlags[0] + case len(positionals) > 1: + problem = "too many arguments" + case malformed: + problem = "not a valid provider name: " + name + default: + problem = "missing provider" + } + deps = defaults(deps) + reportLogoutUsage(deps, problem) + return 2 + } + + outcome, ioErr := logoutRemoveCredential(deps, name) + if ioErr != nil { + deps = defaults(deps) + fmt.Fprintln(deps.Stderr, "Error: could not update the auth store: "+ioErr.Error()) + return 1 + } + if outcome == "not-found" { + if wantsJSON { + reportLogoutJSON(deps, logoutEnvelope(false, name, "not_found")) + } else { + deps = defaults(deps) + reportLogoutStderr(deps, "No stored credential for '"+name+"'.") + } + return 4 + } + if wantsJSON { + reportLogoutJSON(deps, logoutEnvelope(true, name, "")) + } else { + deps = defaults(deps) + reportLogoutStdout(deps, "Logged out of "+name+".") + } + return 0 +} + +func reportLogoutUsage(deps Deps, problem string) { + deps = defaults(deps) + fmt.Fprintln(deps.Stderr, logoutUsagePrefix+" ("+problem+")") +} + +func reportLogoutStderr(deps Deps, line string) { + deps = defaults(deps) + fmt.Fprintln(deps.Stderr, line) +} + +func reportLogoutStdout(deps Deps, line string) { + deps = defaults(deps) + fmt.Fprintln(deps.Stdout, line) +} + +// logoutEnvelope mirrors the --json result objects: +// {schemaVersion:1, ok:false, provider, removed:false, reason:"not_found"} or +// {schemaVersion:1, ok:true, provider, removed:true}. +func logoutEnvelope(ok bool, name string, reason string) *jsonwire.Value { + out := jsonwire.ObjectValue() + out.Set("schemaVersion", jsonwire.NumberFrom(1)) + out.Set("ok", jsonwire.BoolValue(ok)) + out.Set("provider", jsonwire.StringValue(name)) + out.Set("removed", jsonwire.BoolValue(ok)) + if reason != "" { + out.Set("reason", jsonwire.StringValue(reason)) + } + return out +} + +func reportLogoutJSON(deps Deps, value *jsonwire.Value) { + deps = defaults(deps) + pretty, err := value.EncodePretty() + if err != nil { + fmt.Fprintln(deps.Stderr, "Error: "+err.Error()) + return + } + deps.Stdout.Write(pretty) + fmt.Fprintln(deps.Stdout, "") +} + +// logoutRemoveCredential mirrors removeCredential: remove the ACTIVE account +// of the named provider; promote the first remaining account; drop the +// provider when none remain; always persist the normalised store (even for a +// not-found provider, exactly like mutateStore). A second return value carries +// a store IO error (read or write) that the caller reports as exit 1. +func logoutRemoveCredential(deps Deps, provider string) (string, error) { + store, err := readAuthStore() + if err != nil { + return "not-found", err + } + set := store.Find(provider) + if set == nil || set.Kind() != jsonwire.Object { + return "not-found", writeAuthStore(store) + } + accounts := set.Find("accounts") + if accounts == nil || accounts.Kind() != jsonwire.Array { + return "not-found", writeAuthStore(store) + } + active := "" + if activeField := set.Find("activeAccountId"); activeField != nil && activeField.Kind() == jsonwire.String { + active = activeField.String() + } + remaining := jsonwire.EmptyArray() + for _, element := range accounts.Elements() { + id := "" + if idField := element.Find("id"); idField != nil && idField.Kind() == jsonwire.String { + id = idField.String() + } + if id != active { + remaining.AppendArray(element) + } + } + if len(remaining.Elements()) == 0 { + store.Delete(provider) + } else { + firstID := "" + if idField := remaining.Elements()[0].Find("id"); idField != nil && idField.Kind() == jsonwire.String { + firstID = idField.String() + } + set.Set("activeAccountId", jsonwire.StringValue(firstID)) + set.Set("accounts", remaining) + } + return "removed", writeAuthStore(store) +} diff --git a/tests/go-cli-parity.test.ts b/tests/go-cli-parity.test.ts index 0ac65bbb56..210f8f0ffd 100644 --- a/tests/go-cli-parity.test.ts +++ b/tests/go-cli-parity.test.ts @@ -268,4 +268,102 @@ describe.skipIf(!goAvailable || goCLI === null)("Go CLI parity (ADR-0008, ticket expect(expectParity(["usage"])).toMatchObject({ code: 1 }); expect(expectParity(["observe", "usage"])).toMatchObject({ code: 1 }); }); + + // logout is Go-owned (issue #51 slice). The TS CLI still runs its own + // handler, so the differential runs the same argv through both CLIs on the + // same fresh home and compares stdout/stderr/exit code plus the resulting + // auth.json bytes (the on-disk credential store both owners rewrite). + const authFixtureJSON = (extra: Record = {}) => JSON.stringify({ + openai: { + activeAccountId: "acct-1", + accounts: [ + { id: "acct-1", credential: { access: "tok-1", refresh: "ref-1", expires: 1756000000000 } }, + { id: "acct-2", credential: { access: "tok-2", refresh: "ref-2", expires: 1756000000000 } }, + ], + }, + xai: { + activeAccountId: "one", + accounts: [{ id: "one", credential: { access: "xa", refresh: "xr", expires: 1756000000000 } }], + }, + ...extra, + }); + function writeAuthFixture(home: string, extra: Record = {}): void { + writeFileSync(join(home, "auth.json"), authFixtureJSON(extra) + "\n"); + } + // Both CLIs rewrite auth.json on a logout, so compare the byte state each + // owner leaves behind from the identical fixture. + async function logoutParity(args: string[], extra: Record = {}): Promise { + const tsHome = mkdtempSync(join(tmpdir(), "ocx-go-logout-ts-")); + const goHome = mkdtempSync(join(tmpdir(), "ocx-go-logout-go-")); + try { + writeAuthFixture(tsHome, extra); + writeAuthFixture(goHome, extra); + const ts = await runTsAsync(args, tsHome); + const go = await runGoAsync(args, goHome); + expect(go).toEqual(ts); + const tsStore = await Bun.file(join(tsHome, "auth.json")).text(); + const goStore = await Bun.file(join(goHome, "auth.json")).text(); + expect(goStore).toBe(tsStore); + return ts; + } finally { + if (existsSync(tsHome)) removeTreeWithRetry(tsHome); + if (existsSync(goHome)) removeTreeWithRetry(goHome); + } + } + test.each([ + { args: ["help", "logout"] }, + { args: ["logout", "--help"] }, + ])("diffs logout help contracts for $args", ({ args }) => { + testHome = mkdtempSync(join(tmpdir(), "ocx-go-logout-parity-")); + expect(expectParity(args)).toMatchObject({ code: 0, stderr: "" }); + }); + test.each([ + { args: ["logout"], reason: "missing provider" }, + { args: ["logout", "--bogus"], reason: "unknown option --bogus" }, + { args: ["logout", "openai", "xai"], reason: "too many arguments" }, + { args: ["logout", "-j"], reason: "unknown option -j" }, + { args: ["logout", "bad provider"], reason: "not a valid provider name: bad provider" }, + { args: ["logout", "constructor"], reason: "not a valid provider name: constructor" }, + ])("diffs logout argument validation for $args", async ({ args }) => { + testHome = mkdtempSync(join(tmpdir(), "ocx-go-logout-parity-")); + const ts = await runTsAsync(args); + const go = await runGoAsync(args); + expect(go).toEqual(ts); + expect(ts).toMatchObject({ code: 2, stdout: "" }); + }); + test.each([ + { args: ["logout", "nonexistent"] }, + { args: ["logout", "nonexistent", "--json"] }, + ])("diffs logout not-found for $args", async ({ args }) => { + testHome = mkdtempSync(join(tmpdir(), "ocx-go-logout-parity-")); + const result = await logoutParity(args); + expect(result).toMatchObject({ code: 4 }); + }); + test("diffs logout not-found on a store-less home", async () => { + testHome = mkdtempSync(join(tmpdir(), "ocx-go-logout-parity-")); + const ts = await runTsAsync(["logout", "nonexistent"]); + const go = await runGoAsync(["logout", "nonexistent"]); + expect(go).toEqual(ts); + }); + test("diffs logout of a non-active account promoting the next one", async () => { + const result = await logoutParity(["logout", "openai"]); + expect(result).toMatchObject({ code: 0, stderr: "" }); + expect(result.stdout).toBe("Logged out of openai.\n"); + }); + test.each([ + { args: ["logout", "xai"] }, + { args: ["logout", "xai", "--json"] }, + ])("diffs logout of the last account for $args", async ({ args }) => { + const result = await logoutParity(args); + expect(result).toMatchObject({ code: 0 }); + }); + test("diffs logout normalizing a legacy single-credential row", async () => { + const extra = { + anthropic: { access: "legacy-access", refresh: "legacy-refresh", expires: 1756000000000, email: "Legacy@Example.com" }, + }; + const result = await logoutParity(["logout", "anthropic"], extra); + expect(result).toMatchObject({ code: 0, stdout: "Logged out of anthropic.\n" }); + // The whole file is normalised on the rewrite: untouched openai/xai rows + // survive in the same normalised shape from both owners. + }); }); From 61179e846e99b24336c271bf81b2d02d53a21ae9 Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Tue, 8 Sep 2026 07:00:04 +0800 Subject: [PATCH 117/165] feat(go): flip alias, combo, and route to Go-owned (issue #49) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flip the config-routing command families (`ocx alias`, `ocx combo`, `ocx route combo|policy`) from TypeScript delegation to native Go dispatch, following the fa8adea97 usage flip as the migration template. - cli.go: surface map marks alias/combo/route GoOwned; Run dispatches natively; printSubcommandHelp carries the exact registry texts for all three help spellings (help , --help, help). - routing_client.go: shared management-plane client. These commands never edit config.json directly — they discover the identity-checked live proxy, send the admin token, and call the same /api/aliases, /api/default-aliases, /api/providers/:name/alias(+model-aliases), /api/combos, /api/routing-profiles routes the GUI writes, mirroring runtime-api.ts (encodeURIComponent, takeOption/integer, rejectArgs, printData/V8-exact JSON, and the CliUsageError/RuntimeApiError taxonomy: usage exit 2, 404 exit 4, 409 exit 5, else 1). - alias_command.go / combo_command.go / route_command.go: native ports of src/cli/alias.ts, combo.ts, route-policy.ts and the route gate. combo set keeps the GET-before-PUT round trip (imageInput preservation) and weight/sticky/strategy validation; route policy renders the policy/ model fallback and dry-run decisions. - src/cli/alias.ts: wrap handleAliasCommand in runCliAction so the TS reference side carries the same error taxonomy as combo/route policy (previously every alias usage error crashed with a Bun stack dump at exit 1, which was not byte-comparable). Success output unchanged. - tests/go-cli-parity.test.ts: extend the byte-diff oracle with a routing fixture (attested /healthz + canned alias/combo/profile/dry-run management plane). Rows cover normal list/show/set/defaults/rm output (human and --json), help contracts in both spellings, ~40 argument- validation failures, unknown-id usage errors, write refusals (409 exit 5, 404 exit 4), and no-proxy failures — stdout/stderr/exit byte-identical between `bun src/cli/index.ts` and the Go binary. - routing_command_test.go: Go unit tests mirroring usage_command_test (validation exits 2 with/without the USAGE block, no-proxy hint, and httptest read/write flows incl. 409/404 exit mapping). go vet ./... and bun run typecheck stay green; ownership and Lab-boundary tests (go-ownership-plumbing, read-surface-diff-matrix, core-lab-boundary) pass unchanged. Co-Authored-By: Claude Code --- go/internal/ocxcli/alias_command.go | 234 ++++++++++++ go/internal/ocxcli/cli.go | 21 +- go/internal/ocxcli/combo_command.go | 423 +++++++++++++++++++++ go/internal/ocxcli/route_command.go | 241 ++++++++++++ go/internal/ocxcli/routing_client.go | 220 +++++++++++ go/internal/ocxcli/routing_command_test.go | 413 ++++++++++++++++++++ src/cli/alias.ts | 106 +++--- tests/go-cli-parity.test.ts | 204 ++++++++++ 8 files changed, 1809 insertions(+), 53 deletions(-) create mode 100644 go/internal/ocxcli/alias_command.go create mode 100644 go/internal/ocxcli/combo_command.go create mode 100644 go/internal/ocxcli/route_command.go create mode 100644 go/internal/ocxcli/routing_client.go create mode 100644 go/internal/ocxcli/routing_command_test.go diff --git a/go/internal/ocxcli/alias_command.go b/go/internal/ocxcli/alias_command.go new file mode 100644 index 0000000000..c70ecdfc6b --- /dev/null +++ b/go/internal/ocxcli/alias_command.go @@ -0,0 +1,234 @@ +package ocxcli + +// ocx alias — the short-name surface for providers and models. This file ports +// the TypeScript owner (src/cli/alias.ts + the runtime-api error taxonomy) so +// the ownership flip keeps the documented surface identical. Both runtimes +// talk to the same management routes (/api/aliases reads, /api/default-aliases +// and /api/providers/:name/alias + /model-aliases writes), so a live proxy +// validates and persists exactly what the GUI writes. + +import ( + "fmt" + "net/http" + "strings" + + "github.com/lidge-jun/opencodex/go/internal/jsonwire" +) + +const aliasUsage = `Usage: + ocx alias list [--json] + ocx alias set + ocx alias set / + ocx alias rm [/] + ocx alias defaults [--provider ]` + +// aliasSelector mirrors the alias.ts selector(): the provider is everything +// before the first slash; a model is only present when a slash exists. ok is +// false when the parsed target cannot name a provider/model (empty provider or +// an empty model from a trailing slash). +func aliasSelector(value string) (provider string, model string, hasModel bool, ok bool) { + slash := strings.Index(value, "/") + if slash < 0 { + return value, "", false, value != "" + } + return value[:slash], value[slash+1:], true, value[:slash] != "" && value[slash+1:] != "" +} + +// runAlias implements `ocx alias`. It assumes the caller validated ownership; +// argv carries only this command's own arguments. +func runAlias(args []string, deps Deps) int { + rest := append([]string(nil), args...) + action := "list" + if len(rest) > 0 { + action = strings.ToLower(rest[0]) + rest = rest[1:] + } + jsonOutput := takeFlag(&rest, "--json") + if action == "list" { + return runAliasList(rest, jsonOutput, deps) + } + if action == "defaults" { + return runAliasDefaults(rest, jsonOutput, deps) + } + target := "" + if len(rest) > 0 { + target = strings.TrimSpace(rest[0]) + rest = rest[1:] + } + if target == "" { + return routingUsageError(deps, "alias target is required", aliasUsage) + } + provider, model, hasModel, ok := aliasSelector(target) + if !ok { + return routingUsageError(deps, "target must be provider or provider/native-model-id", aliasUsage) + } + if action == "set" { + return runAliasSet(rest, jsonOutput, target, provider, model, hasModel, deps) + } + if action == "rm" { + return runAliasRemove(rest, jsonOutput, target, provider, model, hasModel, deps) + } + return routingUsageError(deps, "unknown alias action '"+action+"'", aliasUsage) +} + +func runAliasList(args []string, jsonOutput bool, deps Deps) int { + rest := append([]string(nil), args...) + if len(rest) != 0 { + return routingUsageError(deps, routingUnexpectedArgs(rest), aliasUsage) + } + value, rawText, status, err := routingRoundTrip(deps, http.MethodGet, "/api/aliases", nil) + if err == nil { + err = routingErrorFromRoundTrip(value, rawText, status) + } + if err != nil { + return routingReportError(deps, err.(routingAPIError)) + } + lines := aliasLines(value) + if len(lines) == 0 { + lines = []string{"No aliases configured."} + } + routingPrintData(deps, value, rawText, jsonOutput, lines) + return ExitOK +} + +// aliasLines builds the human table rows in document order: one `provider` +// row per provider alias, then one `model` row per effective model alias. +func aliasLines(value *jsonwire.Value) []string { + var lines []string + if value == nil || value.Kind() != jsonwire.Object { + return lines + } + if providers := value.Find("providers"); providers != nil && providers.Kind() == jsonwire.Object { + for _, member := range providers.Members() { + if member.Value == nil || member.Value.Kind() != jsonwire.String { + continue + } + lines = append(lines, fmt.Sprintf("provider %s %s user", member.Key, member.Value.String())) + } + } + if models := value.Find("models"); models != nil && models.Kind() == jsonwire.Object { + for _, providerRow := range models.Members() { + if providerRow.Value == nil || providerRow.Value.Kind() != jsonwire.Object { + continue + } + for _, modelRow := range providerRow.Value.Members() { + alias, ok := stringField(modelRow.Value, "alias") + if !ok { + continue + } + source, _ := stringField(modelRow.Value, "source") + lines = append(lines, fmt.Sprintf("model %s/%s %s %s", providerRow.Key, modelRow.Key, alias, source)) + } + } + } + return lines +} + +func runAliasDefaults(args []string, jsonOutput bool, deps Deps) int { + rest := append([]string(nil), args...) + state := "" + if len(rest) > 0 { + state = strings.ToLower(rest[0]) + rest = rest[1:] + } + provider, providerGiven, err := routingTakeOption(&rest, "--provider") + if err != nil { + return routingUsageError(deps, err.Error(), "") + } + if len(rest) != 0 { + return routingUsageError(deps, routingUnexpectedArgs(rest), aliasUsage) + } + if state != "on" && state != "off" { + return routingUsageError(deps, "defaults requires on or off", aliasUsage) + } + body := jsonwire.ObjectValue() + body.Set("enabled", jsonwire.BoolValue(state == "on")) + if providerGiven { + body.Set("provider", jsonwire.StringValue(provider)) + } + encoded, encodeErr := body.Encode() + if encodeErr != nil { + fmt.Fprintln(deps.Stderr, "Error: "+encodeErr.Error()) + return ExitFailure + } + value, rawText, status, roundErr := routingRoundTrip(deps, http.MethodPut, "/api/default-aliases", encoded) + if roundErr == nil { + roundErr = routingErrorFromRoundTrip(value, rawText, status) + } + if roundErr != nil { + return routingReportError(deps, roundErr.(routingAPIError)) + } + scope := " globally" + if providerGiven { + scope = " for " + provider + } + routingPrintData(deps, value, rawText, jsonOutput, []string{fmt.Sprintf("Default aliases %s%s.", state, scope)}) + return ExitOK +} + +func runAliasSet(args []string, jsonOutput bool, target, provider, model string, hasModel bool, deps Deps) int { + rest := append([]string(nil), args...) + alias := "" + if len(rest) > 0 { + alias = strings.TrimSpace(rest[0]) + rest = rest[1:] + } + if len(rest) != 0 { + return routingUsageError(deps, routingUnexpectedArgs(rest), aliasUsage) + } + if alias == "" { + return routingUsageError(deps, "alias value is required", aliasUsage) + } + body := jsonwire.ObjectValue() + if hasModel { + set := jsonwire.ObjectValue() + set.Set(model, jsonwire.StringValue(alias)) + body.Set("set", set) + } else { + body.Set("alias", jsonwire.StringValue(alias)) + } + path := aliasWritePath(provider, hasModel) + return routingAliasWrite(deps, path, body, jsonOutput, target+" → "+alias) +} + +func runAliasRemove(args []string, jsonOutput bool, target, provider, model string, hasModel bool, deps Deps) int { + rest := append([]string(nil), args...) + if len(rest) != 0 { + return routingUsageError(deps, routingUnexpectedArgs(rest), aliasUsage) + } + body := jsonwire.ObjectValue() + if hasModel { + remove := jsonwire.EmptyArray() + remove.AppendArray(jsonwire.StringValue(model)) + body.Set("remove", remove) + } else { + body.Set("alias", jsonwire.NullValue()) + } + path := aliasWritePath(provider, hasModel) + return routingAliasWrite(deps, path, body, jsonOutput, "Removed alias for "+target+".") +} + +func aliasWritePath(provider string, hasModel bool) string { + encoded := routingEncodePathComponent(provider) + if hasModel { + return "/api/providers/" + encoded + "/model-aliases" + } + return "/api/providers/" + encoded + "/alias" +} + +func routingAliasWrite(deps Deps, path string, body *jsonwire.Value, jsonOutput bool, line string) int { + encoded, encodeErr := body.Encode() + if encodeErr != nil { + fmt.Fprintln(deps.Stderr, "Error: "+encodeErr.Error()) + return ExitFailure + } + value, rawText, status, err := routingRoundTrip(deps, http.MethodPut, path, encoded) + if err == nil { + err = routingErrorFromRoundTrip(value, rawText, status) + } + if err != nil { + return routingReportError(deps, err.(routingAPIError)) + } + routingPrintData(deps, value, rawText, jsonOutput, []string{line}) + return ExitOK +} diff --git a/go/internal/ocxcli/cli.go b/go/internal/ocxcli/cli.go index aa63ead65b..e6d6ff8a7a 100644 --- a/go/internal/ocxcli/cli.go +++ b/go/internal/ocxcli/cli.go @@ -77,12 +77,15 @@ var Commands = []Command{ {Name: "provider", Usage: "ocx provider ", Summary: "Inspect configured providers.", Owner: GoOwned}, {Name: "account", Usage: "ocx account ", Summary: "Manage accounts.", Owner: TypeScriptOwned}, {Name: "models", Usage: "ocx models [--provider ] [--json]", Summary: "List configured models.", Owner: GoOwned}, - {Name: "alias", Usage: "ocx alias ", Summary: "Manage aliases.", Owner: TypeScriptOwned}, - {Name: "combo", Usage: "ocx combo ", Summary: "Manage combo routing.", Owner: TypeScriptOwned}, + // alias/combo/route form the config-routing slice (issue #49): Go owns the + // management-plane clients that read and write the routing subsections of + // the config file (aliases, combos, routing profiles) against a live proxy. + {Name: "alias", Usage: "ocx alias ", Summary: "Manage aliases.", Owner: GoOwned}, + {Name: "combo", Usage: "ocx combo ", Summary: "Manage combo routing.", Owner: GoOwned}, {Name: "agent", Usage: "ocx agent ", Summary: "Manage agents.", Owner: TypeScriptOwned}, {Name: "observe", Usage: "ocx observe ", Summary: "Inspect runtime observations.", Owner: TypeScriptOwned}, {Name: "inspect", Usage: "ocx inspect ", Summary: "Inspect effective state.", Owner: TypeScriptOwned}, - {Name: "route", Usage: "ocx route ", Summary: "Manage routing.", Owner: TypeScriptOwned}, + {Name: "route", Usage: "ocx route ", Summary: "Manage routing.", Owner: GoOwned}, {Name: "logs", Usage: "ocx logs [filters]", Summary: "Read logs.", Owner: TypeScriptOwned}, // usage is Go-owned (the /api/usage read plus its renderer); observe keeps // its other subcommands TypeScript-owned until each carries an oracle. @@ -277,6 +280,12 @@ func Run(args []string, deps Deps) int { return runStop(args[1:], deps) case "usage": return runUsage(args[1:], deps) + case "alias": + return runAlias(args[1:], deps) + case "combo": + return runCombo(args[1:], deps) + case "route": + return runRoute(args[1:], deps) case "observe": // Only `observe usage` reaches Go (OwnershipFor already gated this); // other observe subcommands stay TypeScript-owned and never dispatch here. @@ -340,6 +349,12 @@ func printSubcommandHelp(name string, deps Deps) int { fmt.Fprintf(deps.Stdout, "Usage: %s\n\n%s\n", command.Usage, command.Summary) } } + case "alias": + fmt.Fprint(deps.Stdout, "Usage: ocx alias ...\n\nManage short provider and model names.\n") + case "combo": + fmt.Fprint(deps.Stdout, "Usage: ocx combo ...\n\nManage combo virtual models and routing strategies.\n\nAlias hierarchy: ocx route combo ...\nUse --targets provider/model[:weight],provider/model[:weight].\n") + case "route": + fmt.Fprint(deps.Stdout, "Usage: ocx route combo ...\n\nManage routing features; combo is currently the supported routing resource.\n") case "config": fmt.Fprint(deps.Stdout, configHelp) default: diff --git a/go/internal/ocxcli/combo_command.go b/go/internal/ocxcli/combo_command.go new file mode 100644 index 0000000000..de38a1f9dd --- /dev/null +++ b/go/internal/ocxcli/combo_command.go @@ -0,0 +1,423 @@ +package ocxcli + +// ocx combo — the virtual-model routing surface (and the `ocx route combo` +// spelling, which shares this implementation). Ports src/cli/combo.ts plus the +// runtime-api error taxonomy; both runtimes read GET /api/combos and write PUT +// /api/combos / DELETE /api/combos through the live proxy's management plane. + +import ( + "fmt" + "net/http" + "strconv" + "strings" + + "github.com/lidge-jun/opencodex/go/internal/jsonwire" +) + +const comboUsage = `Usage: + ocx combo [list] [--json] + ocx combo show [--json] + ocx combo set --targets + [--strategy ] [--sticky <1-100>] + [--effort ] [--alias ] + [--native-alias] [--display-name ] + [--rename-from ] [--json] + ocx combo remove --yes [--json]` + +const comboStrategiesError = "--strategy must be failover, round-robin, random, least-used, or reset-window" + +type comboRow struct { + id string + idOK bool + model string + modelOK bool + imageInputDisabled bool +} + +// comboListRows decodes the /api/combos `combos` array into the projection the +// renderer consumes (document order preserved). +func comboListRows(value *jsonwire.Value) []comboRow { + var rows []comboRow + combos := value.Find("combos") + if combos == nil || combos.Kind() != jsonwire.Array { + return rows + } + for _, element := range combos.Elements() { + if element == nil || element.Kind() != jsonwire.Object { + continue + } + row := comboRow{} + if id := element.Find("id"); id != nil { + row.id, row.idOK = comboRowID(id) + } + if model := element.Find("model"); model != nil && model.Kind() == jsonwire.String { + row.model, row.modelOK = model.String(), true + } + if imageInput := element.Find("imageInput"); imageInput != nil && imageInput.Kind() == jsonwire.String { + row.imageInputDisabled = imageInput.String() == "disabled" + } + rows = append(rows, row) + } + return rows +} + +// comboRowID mirrors String(row.id): a string stays as-is, other kinds go +// through JS String() semantics (only strings are exercised by the oracle). +func comboRowID(value *jsonwire.Value) (string, bool) { + if value.Kind() == jsonwire.String { + return value.String(), true + } + if value.Kind() == jsonwire.Number { + if number, ok := parseJSONNumber(value.NumberRaw()); ok { + return jsonwire.FormatV8Number(number), true + } + } + return "", false +} + +// runCombo implements `ocx combo` and the `ocx route combo ` spelling. +func runCombo(args []string, deps Deps) int { + sub := "list" + rest := append([]string(nil), args...) + if len(rest) > 0 { + sub = rest[0] + rest = rest[1:] + } + switch sub { + case "list": + return runComboList(rest, deps) + case "show": + return runComboShow(rest, deps) + case "set", "create", "update": + return runComboSet(rest, deps) + case "remove", "delete": + return runComboRemove(rest, deps) + default: + return routingUsageError(deps, "unknown combo command "+sub, comboUsage) + } +} + +func runComboList(args []string, deps Deps) int { + rest := append([]string(nil), args...) + jsonOutput := takeFlag(&rest, "--json") + if len(rest) != 0 { + return routingUsageError(deps, routingUnexpectedArgs(rest), comboUsage) + } + value, rawText, status, err := routingRoundTrip(deps, http.MethodGet, "/api/combos", nil) + if err == nil { + err = routingErrorFromRoundTrip(value, rawText, status) + } + if err != nil { + return routingReportError(deps, err.(routingAPIError)) + } + rows := comboListRows(value) + var lines []string + for _, row := range rows { + display := row.model + if !row.modelOK { + // String(row.model ?? `combo/${row.id}`): only a missing/null model + // falls back to the synthesized public id. + display = "combo/" + row.id + } + lines = append(lines, row.id+" "+display) + } + if len(lines) == 0 { + lines = []string{"No combos configured."} + } + routingPrintData(deps, value, rawText, jsonOutput, lines) + return ExitOK +} + +func runComboShow(args []string, deps Deps) int { + rest := append([]string(nil), args...) + id := "" + if len(rest) > 0 { + id = rest[0] + rest = rest[1:] + } + jsonOutput := takeFlag(&rest, "--json") + if id == "" { + return routingUsageError(deps, "combo id is required", comboUsage) + } + if len(rest) != 0 { + return routingUsageError(deps, routingUnexpectedArgs(rest), comboUsage) + } + value, rawText, status, err := routingRoundTrip(deps, http.MethodGet, "/api/combos", nil) + if err == nil { + err = routingErrorFromRoundTrip(value, rawText, status) + } + if err != nil { + return routingReportError(deps, err.(routingAPIError)) + } + if row := comboRowForID(value, id); row != nil { + // printData without human lines prints the row as JSON even without + // --json, exactly like the TS show path. + routingPrintData(deps, row, "", jsonOutput, nil) + return ExitOK + } + return routingUsageError(deps, "unknown combo "+id, "") +} + +// comboRowForID re-finds the raw /api/combos element whose id equals want so +// the JSON echo prints the server bytes (document order and number literals), +// not a decoded projection. +func comboRowForID(value *jsonwire.Value, want string) *jsonwire.Value { + combos := value.Find("combos") + if combos == nil || combos.Kind() != jsonwire.Array { + return nil + } + for _, element := range combos.Elements() { + if element == nil || element.Kind() != jsonwire.Object { + continue + } + id := element.Find("id") + if id != nil { + if text, ok := comboRowID(id); ok && text == want { + return element + } + } + } + return nil +} + +func runComboSet(args []string, deps Deps) int { + rest := append([]string(nil), args...) + id := "" + if len(rest) > 0 { + id = strings.TrimSpace(rest[0]) + rest = rest[1:] + } + jsonOutput := takeFlag(&rest, "--json") + if id == "" { + return routingUsageError(deps, "combo id is required", comboUsage) + } + targetsRaw, targetsGiven, err := routingTakeOption(&rest, "--targets") + if err != nil { + return routingUsageError(deps, err.Error(), "") + } + if !targetsGiven || targetsRaw == "" { + return routingUsageError(deps, "--targets is required", comboUsage) + } + strategy, strategyGiven, err := routingTakeOption(&rest, "--strategy") + if err != nil { + return routingUsageError(deps, err.Error(), "") + } + if !strategyGiven { + strategy = "failover" + } + if strategy != "failover" && strategy != "round-robin" && strategy != "random" && strategy != "least-used" && strategy != "reset-window" { + return routingUsageError(deps, comboStrategiesError, comboUsage) + } + sticky, stickyGiven, err := routingTakeIntegerOption(&rest, "--sticky", 1) + if err != nil { + return routingUsageError(deps, err.Error(), "") + } + if stickyGiven { + if sticky > 100 { + return routingUsageError(deps, "--sticky must be <= 100", comboUsage) + } + if strategy != "round-robin" { + return routingUsageError(deps, "--sticky applies only to round-robin", comboUsage) + } + } + effort, effortGiven, err := routingTakeOption(&rest, "--effort") + if err != nil { + return routingUsageError(deps, err.Error(), "") + } + alias, aliasGiven, err := routingTakeOption(&rest, "--alias") + if err != nil { + return routingUsageError(deps, err.Error(), "") + } + nativeAlias := takeFlag(&rest, "--native-alias") + displayName, displayNameGiven, err := routingTakeOption(&rest, "--display-name") + if err != nil { + return routingUsageError(deps, err.Error(), "") + } + renameFrom, renameFromGiven, err := routingTakeOption(&rest, "--rename-from") + if err != nil { + return routingUsageError(deps, err.Error(), "") + } + if len(rest) != 0 { + return routingUsageError(deps, routingUnexpectedArgs(rest), comboUsage) + } + targets, parseErr := parseComboTargets(targetsRaw) + if parseErr != nil { + return routingUsageError(deps, parseErr.Error(), comboUsage) + } + combo := comboBody(strategy, sticky, stickyGiven, targets, effort, effortGiven, alias, aliasGiven, nativeAlias, displayName, displayNameGiven) + putBody := jsonwire.ObjectValue() + putBody.Set("id", jsonwire.StringValue(id)) + putBody.Set("combo", combo) + if renameFromGiven { + putBody.Set("renameFrom", jsonwire.StringValue(renameFrom)) + } + // imageInput is preserved only when the existing combo disabled image input; + // a newly-created or auto-mode combo never echoes the default back. The GET + // runs before the PUT exactly like combo.ts, and its failure is fatal. + existingKey := id + if renameFromGiven { + existingKey = renameFrom + } + current, rawText, status, getErr := routingRoundTrip(deps, http.MethodGet, "/api/combos", nil) + if getErr == nil { + getErr = routingErrorFromRoundTrip(current, rawText, status) + } + if getErr != nil { + return routingReportError(deps, getErr.(routingAPIError)) + } + for _, row := range comboListRows(current) { + if row.id == existingKey && row.imageInputDisabled { + combo.Set("imageInput", jsonwire.StringValue("disabled")) + } + } + encoded, encodeErr := putBody.Encode() + if encodeErr != nil { + fmt.Fprintln(deps.Stderr, "Error: "+encodeErr.Error()) + return ExitFailure + } + value, rawText, status, putErr := routingRoundTrip(deps, http.MethodPut, "/api/combos", encoded) + if putErr == nil { + putErr = routingErrorFromRoundTrip(value, rawText, status) + } + if putErr != nil { + return routingReportError(deps, putErr.(routingAPIError)) + } + routingPrintData(deps, value, rawText, jsonOutput, []string{"Saved combo " + id + "."}) + return ExitOK +} + +// comboBody mirrors the combo object construction order in combo.ts so the PUT +// payload key order matches the TS client for the same argv. +func comboBody(strategy string, sticky int, stickyGiven bool, targets []comboTarget, effort string, effortGiven bool, alias string, aliasGiven bool, nativeAlias bool, displayName string, displayNameGiven bool) *jsonwire.Value { + combo := jsonwire.ObjectValue() + combo.Set("strategy", jsonwire.StringValue(strategy)) + stickyValue := 1 + if stickyGiven { + stickyValue = sticky + } + combo.Set("stickyLimit", jsonwire.NumberFrom(float64(stickyValue))) + targetsArray := jsonwire.EmptyArray() + for _, target := range targets { + element := jsonwire.ObjectValue() + element.Set("provider", jsonwire.StringValue(target.provider)) + element.Set("model", jsonwire.StringValue(target.model)) + if target.hasWeight { + element.Set("weight", jsonwire.NumberFrom(float64(target.weight))) + } + targetsArray.AppendArray(element) + } + combo.Set("targets", targetsArray) + if effortGiven { + if effort == "-" { + combo.Set("defaultEffort", jsonwire.NullValue()) + } else { + combo.Set("defaultEffort", jsonwire.StringValue(effort)) + } + } + if aliasGiven { + if alias == "-" { + combo.Set("alias", jsonwire.StringValue("")) + } else { + combo.Set("alias", jsonwire.StringValue(alias)) + } + } + if nativeAlias { + combo.Set("nativeAlias", jsonwire.BoolValue(true)) + } + if displayNameGiven { + if displayName == "-" { + combo.Set("displayName", jsonwire.StringValue("")) + } else { + combo.Set("displayName", jsonwire.StringValue(displayName)) + } + } + return combo +} + +type comboTarget struct { + provider string + model string + weight int + hasWeight bool +} + +// parseComboTargets mirrors parseTargets in combo.ts. The error messages echo +// the ORIGINAL trimmed comma-segment, exactly like TypeScript. +func parseComboTargets(value string) ([]comboTarget, error) { + var targets []comboTarget + for _, part := range strings.Split(value, ",") { + part = strings.TrimSpace(part) + if part == "" { + continue + } + colon := strings.LastIndex(part, ":") + selector := part + var weight int + hasWeight := false + if colon > strings.Index(part, "/") { + if parsed, ok := parseIntLiteral(part[colon+1:]); ok { + selector = part[:colon] + weight = parsed + hasWeight = true + } + } + slash := strings.Index(selector, "/") + if slash <= 0 || slash == len(selector)-1 { + return nil, fmt.Errorf("invalid target \"%s\"; use provider/model[:weight]", part) + } + target := comboTarget{provider: selector[:slash], model: selector[slash+1:], weight: weight, hasWeight: hasWeight} + if hasWeight && (weight < 1 || weight > 10_000) { + return nil, fmt.Errorf("target weight must be 1-10000: %s", part) + } + targets = append(targets, target) + } + if len(targets) == 0 { + return nil, fmt.Errorf("--targets requires at least one provider/model") + } + return targets, nil +} + +// parseIntLiteral mirrors Number(raw) + Number.isInteger for the decimal +// targets the CLI documents: commas and underscores are not Number()-valid in +// JS and hex prefixes are not exercised by the documented surface. +func parseIntLiteral(raw string) (int, bool) { + cleaned := strings.TrimSpace(raw) + if cleaned == "" { + return 0, true // Number("") === 0 + } + value, err := strconv.ParseFloat(cleaned, 64) + if err != nil || value != float64(int64(value)) { + return 0, false + } + return int(value), true +} + +func runComboRemove(args []string, deps Deps) int { + rest := append([]string(nil), args...) + id := "" + if len(rest) > 0 { + id = strings.TrimSpace(rest[0]) + rest = rest[1:] + } + jsonOutput := takeFlag(&rest, "--json") + yes := takeFlag(&rest, "--yes") + if id == "" { + return routingUsageError(deps, "combo id is required", comboUsage) + } + if !yes { + return routingUsageError(deps, "remove requires --yes", comboUsage) + } + if len(rest) != 0 { + return routingUsageError(deps, routingUnexpectedArgs(rest), comboUsage) + } + path := "/api/combos?id=" + routingEncodePathComponent(id) + value, rawText, status, err := routingRoundTrip(deps, http.MethodDelete, path, nil) + if err == nil { + err = routingErrorFromRoundTrip(value, rawText, status) + } + if err != nil { + return routingReportError(deps, err.(routingAPIError)) + } + routingPrintData(deps, value, rawText, jsonOutput, []string{"Removed combo " + id + "."}) + return ExitOK +} diff --git a/go/internal/ocxcli/route_command.go b/go/internal/ocxcli/route_command.go new file mode 100644 index 0000000000..4ec057c550 --- /dev/null +++ b/go/internal/ocxcli/route_command.go @@ -0,0 +1,241 @@ +package ocxcli + +// ocx route — the routing-features gate (`ocx route +// `) plus the `route policy` family. The gate mirrors the dispatch +// check in src/cli/dispatch.ts before it fans out to the shared combo and +// policy implementations; route policy itself is the port of +// src/cli/route-policy.ts against GET/POST /api/routing-profiles. + +import ( + "fmt" + "net/http" + "strings" + + "github.com/lidge-jun/opencodex/go/internal/jsonwire" +) + +const routeUsageLine = "Usage: ocx route " + +const routePolicyUsage = `Usage: + ocx route policy list [--json] + ocx route policy show [--json] + ocx route policy dry-run [--model-context ] [--tools] + [--image] [--structured-output] [--json] + ocx route policy evaluate [--model-context ] [--tools] + [--image] [--structured-output] [--json]` + +// runRoute implements the `ocx route` gate. It assumes the caller validated +// ownership; argv carries only this command's own arguments. +func runRoute(args []string, deps Deps) int { + if len(args) == 0 || (args[0] != "combo" && args[0] != "policy") { + fmt.Fprintln(deps.Stderr, routeUsageLine) + return routingExitUsage + } + if args[0] == "combo" { + return runCombo(args[1:], deps) + } + return runRoutePolicy(args[1:], deps) +} + +// runRoutePolicy mirrors handleRoutePolicyCommand. Subcommand names are +// compared literally, exactly like combo. +func runRoutePolicy(args []string, deps Deps) int { + sub := "" + rest := append([]string(nil), args...) + if len(rest) > 0 { + sub = rest[0] + rest = rest[1:] + } + if sub == "" { + return routingUsageError(deps, "route policy requires a subcommand (list, show, dry-run, evaluate)", routePolicyUsage) + } + switch sub { + case "list": + return runRoutePolicyList(rest, deps) + case "show": + return runRoutePolicyShow(rest, deps) + case "dry-run", "evaluate": + return runRoutePolicyDryRun(rest, deps) + default: + return routingUsageError(deps, "unknown route policy command: "+sub, routePolicyUsage) + } +} + +// profileRow is the projection of one /api/routing-profiles entry the +// renderer consumes. +type profileRow struct { + id string + idOK bool + model string + modelOK bool + revision *jsonwire.Value + raw *jsonwire.Value +} + +func profileRows(value *jsonwire.Value) []profileRow { + var rows []profileRow + profiles := value.Find("profiles") + if profiles == nil || profiles.Kind() != jsonwire.Array { + return rows + } + for _, element := range profiles.Elements() { + if element == nil || element.Kind() != jsonwire.Object { + continue + } + row := profileRow{raw: element} + if id := element.Find("id"); id != nil { + row.id, row.idOK = profileRowID(id) + } + if model := element.Find("model"); model != nil && model.Kind() == jsonwire.String { + row.model, row.modelOK = model.String(), true + } + if revision := element.Find("revision"); revision != nil { + row.revision = revision + } + rows = append(rows, row) + } + return rows +} + +// profileRowID mirrors String(row.id) for the id field. +func profileRowID(value *jsonwire.Value) (string, bool) { + if value.Kind() == jsonwire.String { + return value.String(), true + } + if value.Kind() == jsonwire.Number { + if number, ok := parseJSONNumber(value.NumberRaw()); ok { + return jsonwire.FormatV8Number(number), true + } + } + return "", false +} + +// profileRevisionText mirrors String(row.revision ?? "-"): null/absent become +// "-", numbers render through V8 number-to-string, strings verbatim. +func profileRevisionText(revision *jsonwire.Value) string { + if revision == nil || revision.Kind() == jsonwire.Null { + return "-" + } + if revision.Kind() == jsonwire.String { + return revision.String() + } + if revision.Kind() == jsonwire.Number { + if number, ok := parseJSONNumber(revision.NumberRaw()); ok { + return jsonwire.FormatV8Number(number) + } + } + return fmt.Sprint(revision.NumberRaw()) +} + +func runRoutePolicyList(args []string, deps Deps) int { + rest := append([]string(nil), args...) + jsonOutput := takeFlag(&rest, "--json") + if len(rest) != 0 { + return routingUsageError(deps, routingUnexpectedArgs(rest), routePolicyUsage) + } + value, rawText, status, err := routingRoundTrip(deps, http.MethodGet, "/api/routing-profiles", nil) + if err == nil { + err = routingErrorFromRoundTrip(value, rawText, status) + } + if err != nil { + return routingReportError(deps, err.(routingAPIError)) + } + rows := profileRows(value) + var lines []string + for _, row := range rows { + display := row.model + if !row.modelOK { + display = "policy/" + row.id + } + lines = append(lines, fmt.Sprintf("%s %s rev:%s", row.id, display, profileRevisionText(row.revision))) + } + if len(lines) == 0 { + lines = []string{"No routing profiles configured."} + } + routingPrintData(deps, value, rawText, jsonOutput, lines) + return ExitOK +} + +func runRoutePolicyShow(args []string, deps Deps) int { + rest := append([]string(nil), args...) + id := "" + if len(rest) > 0 { + id = rest[0] + rest = rest[1:] + } + jsonOutput := takeFlag(&rest, "--json") + if id == "" || strings.HasPrefix(id, "-") { + return routingUsageError(deps, "profile id is required", routePolicyUsage) + } + if len(rest) != 0 { + return routingUsageError(deps, routingUnexpectedArgs(rest), routePolicyUsage) + } + value, rawText, status, err := routingRoundTrip(deps, http.MethodGet, "/api/routing-profiles", nil) + if err == nil { + err = routingErrorFromRoundTrip(value, rawText, status) + } + if err != nil { + return routingReportError(deps, err.(routingAPIError)) + } + for _, row := range profileRows(value) { + if row.id == id { + routingPrintData(deps, row.raw, "", jsonOutput, nil) + return ExitOK + } + } + return routingUsageError(deps, "unknown routing profile: "+id, routePolicyUsage) +} + +func runRoutePolicyDryRun(args []string, deps Deps) int { + rest := append([]string(nil), args...) + id := "" + if len(rest) > 0 { + id = rest[0] + rest = rest[1:] + } + jsonOutput := takeFlag(&rest, "--json") + if id == "" || strings.HasPrefix(id, "-") { + return routingUsageError(deps, "profile id is required", routePolicyUsage) + } + modelContext, modelContextGiven, err := routingTakeIntegerOption(&rest, "--model-context", 1) + if err != nil { + return routingUsageError(deps, err.Error(), "") + } + tools := takeFlag(&rest, "--tools") + image := takeFlag(&rest, "--image") + structuredOutput := takeFlag(&rest, "--structured-output") + if len(rest) != 0 { + return routingUsageError(deps, routingUnexpectedArgs(rest), routePolicyUsage) + } + body := jsonwire.ObjectValue() + body.Set("profile", jsonwire.StringValue(id)) + evidence := jsonwire.ObjectValue() + if modelContextGiven { + evidence.Set("contextWindow", jsonwire.NumberFrom(float64(modelContext))) + } + if tools { + evidence.Set("toolsRequired", jsonwire.BoolValue(true)) + } + if image { + evidence.Set("imageInputRequired", jsonwire.BoolValue(true)) + } + if structuredOutput { + evidence.Set("structuredOutputRequired", jsonwire.BoolValue(true)) + } + body.Set("evidence", evidence) + encoded, encodeErr := body.Encode() + if encodeErr != nil { + fmt.Fprintln(deps.Stderr, "Error: "+encodeErr.Error()) + return ExitFailure + } + value, rawText, status, roundErr := routingRoundTrip(deps, http.MethodPost, "/api/routing-profiles/dry-run", encoded) + if roundErr == nil { + roundErr = routingErrorFromRoundTrip(value, rawText, status) + } + if roundErr != nil { + return routingReportError(deps, roundErr.(routingAPIError)) + } + // The policy commands always print the decision as JSON, --json or not. + routingPrintData(deps, value, rawText, jsonOutput, nil) + return ExitOK +} diff --git a/go/internal/ocxcli/routing_client.go b/go/internal/ocxcli/routing_client.go new file mode 100644 index 0000000000..a25c11072d --- /dev/null +++ b/go/internal/ocxcli/routing_client.go @@ -0,0 +1,220 @@ +package ocxcli + +// Shared management-plane client for the config-routing command families +// (ocx alias, ocx combo, ocx route combo|policy). Mirrors +// src/cli/runtime-api.ts: these CLIs never edit config.json directly. They +// find the live proxy (identity-checked, exactly like findLiveProxy), send the +// admin token like runningProxyUpdateHeaders, and talk to the same /api/* +// management routes the GUI uses, so validation and live-config refresh cannot +// diverge between surfaces. +// +// The error taxonomy mirrors runCliAction in runtime-api.ts: +// - usage errors (CliUsageError): "Error: " plus the USAGE block when +// the error carried one, exit 2 +// - management failures (RuntimeApiError): "Error: ", exit 4 on 404, +// 5 on 409, otherwise 1 + +import ( + "bytes" + "fmt" + "io" + "math" + "net/http" + "strconv" + "strings" + "unicode/utf8" + + "github.com/lidge-jun/opencodex/go/internal/jsonwire" +) + +// Exit codes from the runCliAction taxonomy, shared by the routing families. +const ( + routingExitUsage = 2 // CliUsageError + routingExitMissing = 4 // RuntimeApiError status 404 + routingExitConflict = 5 // RuntimeApiError status 409 +) + +// routingAPIError mirrors RuntimeApiError's observable shape: a message and an +// HTTP status that selects the exit code. +type routingAPIError struct { + message string + status int +} + +func (e routingAPIError) Error() string { return e.message } + +// routingEncodePathComponent mirrors encodeURIComponent: percent-encode the +// UTF-8 bytes of every character outside the JS unescaped set +// (A-Z a-z 0-9 - _ . ! ~ * ' ( )), with uppercase hex escapes. Go's +// url.QueryEscape would encode spaces as '+' and url.PathEscape leaves +// '$&+,;=:@' raw, so neither reproduces a path TypeScript builds. +func routingEncodePathComponent(value string) string { + var b strings.Builder + for index := 0; index < len(value); { + character, size := utf8.DecodeRuneInString(value[index:]) + if size == 1 && character == utf8.RuneError { + // Invalid UTF-8 byte: encode the raw byte, not the replacement rune. + b.WriteString(fmt.Sprintf("%%%02X", value[index])) + index++ + continue + } + unescaped := character >= 'a' && character <= 'z' || + character >= 'A' && character <= 'Z' || + character >= '0' && character <= '9' || + character == '-' || character == '_' || character == '.' || + character == '!' || character == '~' || character == '*' || + character == '\'' || character == '(' || character == ')' + if unescaped { + b.WriteRune(character) + } else { + for offset := 0; offset < size; offset++ { + b.WriteString(fmt.Sprintf("%%%02X", value[index+offset])) + } + } + index += size + } + return b.String() +} + +// routingTakeOption mirrors takeOption: remove the first `flag value` pair from +// args and report whether it was present. A missing value or a value that +// starts with "--" fails with the exact TypeScript message (no USAGE block). +func routingTakeOption(args *[]string, flag string) (string, bool, error) { + for index, arg := range *args { + if arg != flag { + continue + } + if index+1 >= len(*args) || strings.HasPrefix((*args)[index+1], "--") { + return "", false, fmt.Errorf("%s requires a value", flag) + } + value := (*args)[index+1] + *args = append((*args)[:index], (*args)[index+2:]...) + return value, true, nil + } + return "", false, nil +} + +// routingTakeIntegerOption mirrors takeIntegerOption: parse the option value +// with JS Number semantics for the separators it strips (commas/underscores), +// requiring an integer >= min with the exact TypeScript error message. +func routingTakeIntegerOption(args *[]string, flag string, min int) (int, bool, error) { + raw, given, err := routingTakeOption(args, flag) + if err != nil || !given { + return 0, given, err + } + cleaned := strings.NewReplacer(",", "", "_", "").Replace(raw) + value, parseErr := strconv.ParseFloat(cleaned, 64) + if parseErr != nil || math.Trunc(value) != value || value < float64(min) { + return 0, true, fmt.Errorf("%s must be an integer >= %d", flag, min) + } + return int(value), true, nil +} + +// routingUnexpectedArgs mirrors rejectArgs without secret redaction: the +// config-routing families parse no credential-bearing options. +func routingUnexpectedArgs(args []string) string { + return fmt.Sprintf("Unexpected argument(s): %s", strings.Join(args, " ")) +} + +func bytesReader(data []byte) io.Reader { + if data == nil { + return nil + } + return bytes.NewReader(data) +} + +// routingRoundTrip performs one management request. It mirrors +// runtimeRequest/fetchUsageReport: identity-checked live-proxy discovery +// first, admin token from env or file, then the response body parsed into a +// jsonwire tree (rawText is set only when the body is not valid JSON, exactly +// like the TS parse-or-text branch). Transport and discovery failures carry +// the 503 RuntimeApiError message; a non-2xx status surfaces through the +// returned error so the status selects the exit code. +func routingRoundTrip(deps Deps, method, path string, body []byte) (*jsonwire.Value, string, int, error) { + deps = defaults(deps) + state, found := liveProxyEndpoint(deps) + if !found { + return nil, "", 503, routingAPIError{message: "Proxy is not running. Start it with: ocx start", status: 503} + } + request, requestErr := http.NewRequest(method, baseURL(state)+path, bytesReader(body)) + if requestErr != nil { + return nil, "", 503, routingAPIError{message: "Management API is unreachable: " + requestErr.Error(), status: 503} + } + request.Header.Set("Content-Type", "application/json") + if token := configuredUsageAdminToken(); token != "" { + request.Header.Set("X-OpenCodex-API-Key", token) + } + response, doErr := deps.HTTPClient.Do(request) + if doErr != nil { + return nil, "", 503, routingAPIError{message: "Management API is unreachable: " + doErr.Error(), status: 503} + } + defer response.Body.Close() + raw, readErr := io.ReadAll(io.LimitReader(response.Body, 8*1024*1024)) + if readErr != nil { + return nil, "", 503, routingAPIError{message: "Management API is unreachable: " + readErr.Error(), status: 503} + } + value, parseErr := jsonwire.Parse(raw) + if parseErr != nil { + return nil, string(raw), response.StatusCode, nil + } + return value, "", response.StatusCode, nil +} + +// routingResponseMessage composes the operator-facing message from a failed +// management response, mirroring responseMessage in runtime-api.ts (via the +// usage_command.go port of the same helper). +func routingResponseMessage(body *jsonwire.Value, rawText string, status int) string { + return usageResponseMessage(body, rawText, status) +} + +// routingReportError mirrors a RuntimeApiError reaching runCliAction: the +// Error line on stderr and the status-selected exit code. +func routingReportError(deps Deps, err routingAPIError) int { + fmt.Fprintln(deps.Stderr, "Error: "+err.message) + switch err.status { + case http.StatusNotFound: + return routingExitMissing + case http.StatusConflict: + return routingExitConflict + default: + return ExitFailure + } +} + +// routingUsageError mirrors a CliUsageError carrying the USAGE block reaching +// runCliAction: the Error line plus the block on stderr, exit 2. +func routingUsageError(deps Deps, message, usage string) int { + fmt.Fprintln(deps.Stderr, "Error: "+message) + if usage != "" { + fmt.Fprintln(deps.Stderr, usage) + } + return routingExitUsage +} + +// routingPrintData mirrors printData in runtime-api.ts: +// - wantsJSON true, or no human lines supplied: JSON.stringify(value, null, 2) +// plus the console.log newline (rawText quoted when the body was not JSON) +// - otherwise one console.log line per human line +func routingPrintData(deps Deps, value *jsonwire.Value, rawText string, wantsJSON bool, lines []string) { + if wantsJSON || lines == nil { + if err := writeUsageJSON(deps.Stdout, value, rawText); err != nil { + // The raw-text branch only triggers for non-JSON bodies; EncodeString + // cannot fail for arbitrary UTF-8 input. + fmt.Fprintln(deps.Stderr, "Error: "+err.Error()) + } + return + } + for _, line := range lines { + fmt.Fprintln(deps.Stdout, line) + } +} + +// routingErrorFromRoundTrip inspects a routingRoundTrip outcome and, for a +// non-2xx response, converts it into the RuntimeApiError taxonomy error the +// caller reports (mirroring the runtimeRequest non-ok branch). +func routingErrorFromRoundTrip(value *jsonwire.Value, rawText string, status int) error { + if status >= 200 && status < 300 { + return nil + } + return routingAPIError{message: routingResponseMessage(value, rawText, status), status: status} +} diff --git a/go/internal/ocxcli/routing_command_test.go b/go/internal/ocxcli/routing_command_test.go new file mode 100644 index 0000000000..d2c56502e2 --- /dev/null +++ b/go/internal/ocxcli/routing_command_test.go @@ -0,0 +1,413 @@ +package ocxcli + +// Config-routing families (ocx alias/combo/route, issue #49) — Go unit tests +// mirroring the usage_command_test pattern. Argument validation runs before +// discovery (ReadRuntime errors "unused"), the no-proxy hint uses +// os.ErrNotExist, and live flows run against a canned httptest management +// plane keyed on the same routes the parity fixture pins. + +import ( + "bytes" + "errors" + "fmt" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "strings" + "testing" +) + +func routingRuntimeState(t *testing.T, server *httptest.Server) RuntimeState { + t.Helper() + port := atoi(t, server.URL[len("http://127.0.0.1:"):]) + return RuntimeState{PID: 1, Port: port, Hostname: "127.0.0.1", AttestationSecret: "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG"} +} + +func routingFixtureServer(t *testing.T, handler http.HandlerFunc) *httptest.Server { + t.Helper() + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/healthz" { + w.Header().Set("Content-Type", "application/json") + fmt.Fprint(w, `{"status":"ok","service":"opencodex","version":"test","uptime":1,"pid":1}`) + return + } + handler(w, r) + })) + t.Cleanup(server.Close) + return server +} + +func routingDeps(t *testing.T, server *httptest.Server, stdout, stderr *bytes.Buffer) Deps { + t.Helper() + return Deps{ + Version: "test", + Stdout: stdout, + Stderr: stderr, + HTTPClient: server.Client(), + ReadRuntime: func() (RuntimeState, error) { + return routingRuntimeState(t, server), nil + }, + } +} + +func TestRoutingArgumentValidationExits2WithUsage(t *testing.T) { + cases := []struct { + name string + run func(args []string, deps Deps) int + args []string + message string + wantUsage bool + }{ + {name: "alias set missing target", run: runAlias, args: []string{"set"}, message: "alias target is required", wantUsage: true}, + {name: "alias set trailing slash", run: runAlias, args: []string{"set", "alpha/"}, message: "target must be provider or provider/native-model-id", wantUsage: true}, + {name: "alias set leading slash", run: runAlias, args: []string{"set", "/m"}, message: "target must be provider or provider/native-model-id", wantUsage: true}, + {name: "alias set empty value", run: runAlias, args: []string{"set", "alpha"}, message: "alias value is required", wantUsage: true}, + {name: "alias set extra arg", run: runAlias, args: []string{"set", "alpha", "a", "extra"}, message: "Unexpected argument(s): extra", wantUsage: true}, + {name: "alias rm missing target", run: runAlias, args: []string{"rm"}, message: "alias target is required", wantUsage: true}, + {name: "alias defaults missing state", run: runAlias, args: []string{"defaults"}, message: "defaults requires on or off", wantUsage: true}, + {name: "alias defaults bad state", run: runAlias, args: []string{"defaults", "maybe"}, message: "defaults requires on or off", wantUsage: true}, + {name: "alias defaults empty provider", run: runAlias, args: []string{"defaults", "on", "--provider"}, message: "--provider requires a value", wantUsage: false}, + {name: "alias defaults extra arg", run: runAlias, args: []string{"defaults", "on", "extra"}, message: "Unexpected argument(s): extra", wantUsage: true}, + {name: "alias list extra arg", run: runAlias, args: []string{"list", "extra"}, message: "Unexpected argument(s): extra", wantUsage: true}, + {name: "alias unknown action", run: runAlias, args: []string{"frobnicate", "x"}, message: "unknown alias action 'frobnicate'", wantUsage: true}, + {name: "alias unknown action without target", run: runAlias, args: []string{"frobnicate"}, message: "alias target is required", wantUsage: true}, + {name: "combo unknown subcommand", run: runCombo, args: []string{"nope"}, message: "unknown combo command nope", wantUsage: true}, + {name: "combo show missing id", run: runCombo, args: []string{"show"}, message: "combo id is required", wantUsage: true}, + {name: "combo show extra arg", run: runCombo, args: []string{"show", "x", "extra"}, message: "Unexpected argument(s): extra", wantUsage: true}, + {name: "combo set missing id", run: runCombo, args: []string{"set"}, message: "combo id is required", wantUsage: true}, + {name: "combo set missing targets", run: runCombo, args: []string{"set", "x"}, message: "--targets is required", wantUsage: true}, + {name: "combo set empty targets", run: runCombo, args: []string{"set", "x", "--targets", ""}, message: "--targets is required", wantUsage: true}, + {name: "combo set dangling targets", run: runCombo, args: []string{"set", "x", "--targets"}, message: "--targets requires a value", wantUsage: false}, + {name: "combo set bad strategy", run: runCombo, args: []string{"set", "x", "--targets", "a/b", "--strategy", "bad"}, message: comboStrategiesError, wantUsage: true}, + {name: "combo set sticky without round-robin", run: runCombo, args: []string{"set", "x", "--targets", "a/b", "--sticky", "3"}, message: "--sticky applies only to round-robin", wantUsage: true}, + {name: "combo set sticky too large", run: runCombo, args: []string{"set", "x", "--targets", "a/b", "--strategy", "round-robin", "--sticky", "200"}, message: "--sticky must be <= 100", wantUsage: true}, + {name: "combo set sticky not integer", run: runCombo, args: []string{"set", "x", "--targets", "a/b", "--sticky", "abc"}, message: "--sticky must be an integer >= 1", wantUsage: false}, + {name: "combo set plain target", run: runCombo, args: []string{"set", "x", "--targets", "plain"}, message: "invalid target \"plain\"; use provider/model[:weight]", wantUsage: true}, + {name: "combo set trailing slash target", run: runCombo, args: []string{"set", "x", "--targets", "a/"}, message: "invalid target \"a/\"; use provider/model[:weight]", wantUsage: true}, + {name: "combo set zero weight", run: runCombo, args: []string{"set", "x", "--targets", "a/b:0"}, message: "target weight must be 1-10000: a/b:0", wantUsage: true}, + {name: "combo set extra arg", run: runCombo, args: []string{"set", "x", "--targets", "a/b", "extra"}, message: "Unexpected argument(s): extra", wantUsage: true}, + {name: "combo remove missing id", run: runCombo, args: []string{"remove"}, message: "combo id is required", wantUsage: true}, + {name: "combo remove without yes", run: runCombo, args: []string{"remove", "x"}, message: "remove requires --yes", wantUsage: true}, + {name: "route policy missing subcommand", run: runRoutePolicy, args: []string{}, message: "route policy requires a subcommand (list, show, dry-run, evaluate)", wantUsage: true}, + {name: "route policy unknown subcommand", run: runRoutePolicy, args: []string{"nope"}, message: "unknown route policy command: nope", wantUsage: true}, + {name: "route policy show needs id", run: runRoutePolicy, args: []string{"show", "--json"}, message: "profile id is required", wantUsage: true}, + {name: "route policy dry-run needs id", run: runRoutePolicy, args: []string{"evaluate", "--json"}, message: "profile id is required", wantUsage: true}, + {name: "route policy dry-run bad context", run: runRoutePolicy, args: []string{"dry-run", "x", "--model-context", "abc"}, message: "--model-context must be an integer >= 1", wantUsage: false}, + {name: "route policy dry-run dangling context", run: runRoutePolicy, args: []string{"dry-run", "x", "--model-context"}, message: "--model-context requires a value", wantUsage: false}, + {name: "route policy dry-run extra arg", run: runRoutePolicy, args: []string{"dry-run", "x", "extra"}, message: "Unexpected argument(s): extra", wantUsage: true}, + {name: "route policy show extra arg", run: runRoutePolicy, args: []string{"show", "x", "extra"}, message: "Unexpected argument(s): extra", wantUsage: true}, + {name: "route policy list extra arg", run: runRoutePolicy, args: []string{"list", "extra"}, message: "Unexpected argument(s): extra", wantUsage: true}, + } + for _, testCase := range cases { + t.Run(testCase.name, func(t *testing.T) { + var stdout, stderr bytes.Buffer + deps := Deps{Version: "test", Stdout: &stdout, Stderr: &stderr, ReadRuntime: func() (RuntimeState, error) { return RuntimeState{}, errors.New("unused") }} + code := testCase.run(testCase.args, deps) + if code != routingExitUsage { + t.Fatalf("args %v exit = %d, want %d (stderr: %s)", testCase.args, code, routingExitUsage, stderr.String()) + } + if !strings.Contains(stderr.String(), "Error: "+testCase.message) { + t.Fatalf("args %v stderr missing %q:\n%s", testCase.args, "Error: "+testCase.message, stderr.String()) + } + hasUsage := strings.Contains(stderr.String(), "ocx alias list [--json]") || + strings.Contains(stderr.String(), "ocx combo [list] [--json]") || + strings.Contains(stderr.String(), "ocx route policy list [--json]") + if testCase.wantUsage && !hasUsage { + t.Fatalf("args %v stderr missing USAGE block:\n%s", testCase.args, stderr.String()) + } + if !testCase.wantUsage && strings.Contains(stderr.String(), "Usage:\n") { + t.Fatalf("args %v stderr must not carry the USAGE block:\n%s", testCase.args, stderr.String()) + } + if stdout.Len() != 0 { + t.Fatalf("args %v stdout must stay empty:\n%s", testCase.args, stdout.String()) + } + }) + } +} + +func TestRoutingGateExits2OnUnknownFamily(t *testing.T) { + for _, args := range [][]string{{}, {"comboize"}, {"nope"}} { + var stdout, stderr bytes.Buffer + deps := Deps{Version: "test", Stdout: &stdout, Stderr: &stderr, ReadRuntime: func() (RuntimeState, error) { return RuntimeState{}, errors.New("unused") }} + if code := runRoute(args, deps); code != routingExitUsage { + t.Fatalf("route %v exit = %d, want 2", args, code) + } + if !strings.Contains(stderr.String(), "Usage: ocx route ") { + t.Fatalf("route %v stderr missing gate line:\n%s", args, stderr.String()) + } + } +} + +func TestRoutingWithoutRuntimeReportsStartHint(t *testing.T) { + // Isolate from any developer proxy: discovery falls back to the configured + // port when no runtime record answers, so make the config fallback abort by + // pointing OPENCODEX_HOME at an invalid config.json (Load errors instead of + // probing a possibly-live 10100). + home := t.TempDir() + t.Setenv("OPENCODEX_HOME", home) + if err := os.WriteFile(filepath.Join(home, "config.json"), []byte("{not-json"), 0o600); err != nil { + t.Fatal(err) + } + noRuntime := func() Deps { + return Deps{Version: "test", Stdout: &bytes.Buffer{}, Stderr: &bytes.Buffer{}, ReadRuntime: func() (RuntimeState, error) { return RuntimeState{}, os.ErrNotExist }} + } + aliasCases := [][]string{{"list"}, {"defaults", "on"}, {"set", "alpha", "x"}, {"rm", "alpha"}} + for _, args := range aliasCases { + var stdout, stderr bytes.Buffer + deps := noRuntime() + deps.Stdout, deps.Stderr = &stdout, &stderr + if code := runAlias(args, deps); code != 1 { + t.Fatalf("alias %v exit = %d, want 1", args, code) + } + if !strings.Contains(stderr.String(), "Error: Proxy is not running. Start it with: ocx start") { + t.Fatalf("alias %v unexpected stderr: %s", args, stderr.String()) + } + } + comboCases := [][]string{{"show", "fast"}, {"set", "fast", "--targets", "alpha/m1"}, {"remove", "fast", "--yes"}, {"list", "--json"}} + for _, args := range comboCases { + var stdout, stderr bytes.Buffer + deps := noRuntime() + deps.Stdout, deps.Stderr = &stdout, &stderr + if code := runCombo(args, deps); code != 1 { + t.Fatalf("combo %v exit = %d, want 1", args, code) + } + if !strings.Contains(stderr.String(), "Error: Proxy is not running. Start it with: ocx start") { + t.Fatalf("combo %v unexpected stderr: %s", args, stderr.String()) + } + } + policyCases := [][]string{{"list"}, {"show", "p1"}, {"dry-run", "p1"}, {"evaluate", "p1", "--model-context", "9000", "--json"}} + for _, args := range policyCases { + var stdout, stderr bytes.Buffer + deps := noRuntime() + deps.Stdout, deps.Stderr = &stdout, &stderr + if code := runRoutePolicy(args, deps); code != 1 { + t.Fatalf("route policy %v exit = %d, want 1", args, code) + } + if !strings.Contains(stderr.String(), "Error: Proxy is not running. Start it with: ocx start") { + t.Fatalf("route policy %v unexpected stderr: %s", args, stderr.String()) + } + } + // `ocx route combo list` shares the combo discovery path. + var stdout, stderr bytes.Buffer + deps := noRuntime() + deps.Stdout, deps.Stderr = &stdout, &stderr + if code := runCombo([]string{"list"}, deps); code != 1 { + t.Fatalf("route combo list exit = %d, want 1", code) + } +} + +func routingManagementPlane(t *testing.T) *httptest.Server { + t.Helper() + return routingFixtureServer(t, func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + switch r.Method + " " + r.URL.Path { + case "GET /api/aliases": + fmt.Fprint(w, `{"providers":{"alpha":"a","beta":"b"},"models":{"alpha":{"m1":{"alias":"one","source":"user"}}},"defaults":{"global":true,"providers":{"alpha":false}}}`) + case "GET /api/combos": + fmt.Fprint(w, `{"combos":[{"id":"fast","model":"combo/fast","strategy":"failover","stickyLimit":1,"targets":[{"provider":"alpha","model":"m1","weight":2},{"provider":"beta","model":"b1"}]},{"id":"smart","model":"combo/smart","strategy":"round-robin","stickyLimit":3,"targets":[{"provider":"alpha","model":"m2"}],"alias":"smartie"}]}`) + case "GET /api/routing-profiles": + fmt.Fprint(w, `{"profiles":[{"id":"p1","model":"policy/p1","revision":3,"strategy":"cost"},{"id":"p2","revision":1}]}`) + case "POST /api/routing-profiles/dry-run": + fmt.Fprint(w, `{"profile":"p1","matched":true,"model":"policy/p1","reasons":["compatibility matched"],"evidence":{},"candidates":[]}`) + case "PUT /api/default-aliases": + fmt.Fprint(w, `{"ok":true,"catalogRefresh":{"status":"noop","ok":true}}`) + case "PUT /api/combos": + fmt.Fprint(w, `{"ok":true,"id":"fast"}`) + case "DELETE /api/combos": + if r.URL.Query().Get("id") == "gone" { + w.WriteHeader(http.StatusNotFound) + fmt.Fprint(w, `{"error":"unknown combo: gone"}`) + return + } + fmt.Fprint(w, `{"ok":true,"id":"fast"}`) + case "PUT /api/providers/beta/alias": + w.WriteHeader(http.StatusConflict) + fmt.Fprint(w, `{"error":"alias conflicts with 'beta'"}`) + case "PUT /api/providers/ghost/alias": + w.WriteHeader(http.StatusNotFound) + fmt.Fprint(w, `{"error":"provider 'ghost' not found"}`) + case "PUT /api/providers/alpha/alias": + fmt.Fprint(w, `{"ok":true,"provider":"alpha","alias":"x","catalogRefresh":{"status":"noop","ok":true}}`) + case "PUT /api/providers/alpha/model-aliases": + fmt.Fprint(w, `{"ok":true,"aliases":{"m1":"one"},"catalogRefresh":{"status":"noop","ok":true}}`) + default: + http.NotFound(w, r) + } + }) +} + +func TestRoutingReadsAgainstFixture(t *testing.T) { + server := routingManagementPlane(t) + + var stdout, stderr bytes.Buffer + if code := runAlias([]string{"list"}, routingDeps(t, server, &stdout, &stderr)); code != 0 { + t.Fatalf("alias list exit = %d (stderr: %s)", code, stderr.String()) + } + for _, want := range []string{"provider alpha a user", "provider beta b user", "model alpha/m1 one user"} { + if !strings.Contains(stdout.String(), want) { + t.Fatalf("alias list stdout missing %q:\n%s", want, stdout.String()) + } + } + stdout.Reset() + stderr.Reset() + if code := runAlias([]string{"list", "--json"}, routingDeps(t, server, &stdout, &stderr)); code != 0 { + t.Fatalf("alias list --json exit = %d (stderr: %s)", code, stderr.String()) + } + for _, want := range []string{"\"providers\": {", "\"alpha\": \"a\"", "\"models\": {"} { + if !strings.Contains(stdout.String(), want) { + t.Fatalf("alias list --json stdout missing %q:\n%s", want, stdout.String()) + } + } + + stdout.Reset() + stderr.Reset() + if code := runCombo([]string{"list"}, routingDeps(t, server, &stdout, &stderr)); code != 0 { + t.Fatalf("combo list exit = %d (stderr: %s)", code, stderr.String()) + } + for _, want := range []string{"fast combo/fast", "smart combo/smart"} { + if !strings.Contains(stdout.String(), want) { + t.Fatalf("combo list stdout missing %q:\n%s", want, stdout.String()) + } + } + + stdout.Reset() + stderr.Reset() + if code := runCombo([]string{"show", "smart"}, routingDeps(t, server, &stdout, &stderr)); code != 0 { + t.Fatalf("combo show exit = %d (stderr: %s)", code, stderr.String()) + } + for _, want := range []string{"\"id\": \"smart\"", "\"alias\": \"smartie\"", "\"stickyLimit\": 3"} { + if !strings.Contains(stdout.String(), want) { + t.Fatalf("combo show stdout missing %q:\n%s", want, stdout.String()) + } + } + stdout.Reset() + stderr.Reset() + if code := runCombo([]string{"show", "missing"}, routingDeps(t, server, &stdout, &stderr)); code != routingExitUsage { + t.Fatalf("combo show missing exit = %d, want 2 (stderr: %s)", code, stderr.String()) + } + if !strings.Contains(stderr.String(), "Error: unknown combo missing") { + t.Fatalf("combo show missing stderr missing error:\n%s", stderr.String()) + } + + stdout.Reset() + stderr.Reset() + if code := runRoutePolicy([]string{"list"}, routingDeps(t, server, &stdout, &stderr)); code != 0 { + t.Fatalf("route policy list exit = %d (stderr: %s)", code, stderr.String()) + } + for _, want := range []string{"p1 policy/p1 rev:3", "p2 policy/p2 rev:1"} { + if !strings.Contains(stdout.String(), want) { + t.Fatalf("route policy list stdout missing %q:\n%s", want, stdout.String()) + } + } + + stdout.Reset() + stderr.Reset() + if code := runRoutePolicy([]string{"show", "p2"}, routingDeps(t, server, &stdout, &stderr)); code != 0 { + t.Fatalf("route policy show exit = %d (stderr: %s)", code, stderr.String()) + } + if !strings.Contains(stdout.String(), "\"id\": \"p2\"") || strings.Contains(stdout.String(), "\"model\"") { + t.Fatalf("route policy show p2 must echo the model-less row raw:\n%s", stdout.String()) + } + stdout.Reset() + stderr.Reset() + if code := runRoutePolicy([]string{"dry-run", "p1", "--json"}, routingDeps(t, server, &stdout, &stderr)); code != 0 { + t.Fatalf("route policy dry-run exit = %d (stderr: %s)", code, stderr.String()) + } + for _, want := range []string{"\"profile\": \"p1\"", "\"matched\": true", "\"compatibility matched\""} { + if !strings.Contains(stdout.String(), want) { + t.Fatalf("route policy dry-run stdout missing %q:\n%s", want, stdout.String()) + } + } +} + +func TestRoutingWritesAgainstFixture(t *testing.T) { + server := routingManagementPlane(t) + + var stdout, stderr bytes.Buffer + if code := runAlias([]string{"defaults", "on"}, routingDeps(t, server, &stdout, &stderr)); code != 0 { + t.Fatalf("alias defaults on exit = %d (stderr: %s)", code, stderr.String()) + } + if stdout.String() != "Default aliases on globally.\n" { + t.Fatalf("alias defaults on stdout = %q", stdout.String()) + } + stdout.Reset() + stderr.Reset() + if code := runAlias([]string{"defaults", "off", "--provider", "alpha", "--json"}, routingDeps(t, server, &stdout, &stderr)); code != 0 { + t.Fatalf("alias defaults off --json exit = %d (stderr: %s)", code, stderr.String()) + } + if !strings.Contains(stdout.String(), "\"catalogRefresh\": {") { + t.Fatalf("alias defaults off --json stdout missing PUT echo:\n%s", stdout.String()) + } + + stdout.Reset() + stderr.Reset() + if code := runAlias([]string{"set", "alpha", "fast-a"}, routingDeps(t, server, &stdout, &stderr)); code != 0 { + t.Fatalf("alias set exit = %d (stderr: %s)", code, stderr.String()) + } + if stdout.String() != "alpha → fast-a\n" { + t.Fatalf("alias set stdout = %q", stdout.String()) + } + stdout.Reset() + stderr.Reset() + if code := runAlias([]string{"set", "alpha/m1", "one-a", "--json"}, routingDeps(t, server, &stdout, &stderr)); code != 0 { + t.Fatalf("alias set model-aliases exit = %d (stderr: %s)", code, stderr.String()) + } + if !strings.Contains(stdout.String(), "\"aliases\": {") { + t.Fatalf("alias set model-aliases --json stdout missing PUT echo:\n%s", stdout.String()) + } + stdout.Reset() + stderr.Reset() + if code := runAlias([]string{"rm", "alpha", "--json"}, routingDeps(t, server, &stdout, &stderr)); code != 0 { + t.Fatalf("alias rm exit = %d (stderr: %s)", code, stderr.String()) + } + for _, want := range []string{"\"provider\": \"alpha\"", "\"catalogRefresh\": {"} { + if !strings.Contains(stdout.String(), want) { + t.Fatalf("alias rm --json stdout missing %q:\n%s", want, stdout.String()) + } + } + + // combo set performs the GET-before-PUT round trip; the human line matches. + stdout.Reset() + stderr.Reset() + if code := runCombo([]string{"set", "fast", "--targets", "alpha/m1:2,beta/b1"}, routingDeps(t, server, &stdout, &stderr)); code != 0 { + t.Fatalf("combo set exit = %d (stderr: %s)", code, stderr.String()) + } + if stdout.String() != "Saved combo fast.\n" { + t.Fatalf("combo set stdout = %q", stdout.String()) + } + stdout.Reset() + stderr.Reset() + if code := runCombo([]string{"remove", "fast", "--yes"}, routingDeps(t, server, &stdout, &stderr)); code != 0 { + t.Fatalf("combo remove exit = %d (stderr: %s)", code, stderr.String()) + } + if stdout.String() != "Removed combo fast.\n" { + t.Fatalf("combo remove stdout = %q", stdout.String()) + } + + // Write refusals select exit 5 (409) and 4 (404) with the body error. + stdout.Reset() + stderr.Reset() + if code := runAlias([]string{"set", "beta", "b2"}, routingDeps(t, server, &stdout, &stderr)); code != routingExitConflict { + t.Fatalf("alias set beta exit = %d, want 5 (stderr: %s)", code, stderr.String()) + } + if stdout.Len() != 0 || !strings.Contains(stderr.String(), "Error: alias conflicts with 'beta'") { + t.Fatalf("alias set beta stdout/stderr = %q / %q", stdout.String(), stderr.String()) + } + stdout.Reset() + stderr.Reset() + if code := runAlias([]string{"set", "ghost", "g"}, routingDeps(t, server, &stdout, &stderr)); code != routingExitMissing { + t.Fatalf("alias set ghost exit = %d, want 4 (stderr: %s)", code, stderr.String()) + } + if !strings.Contains(stderr.String(), "Error: provider 'ghost' not found") { + t.Fatalf("alias set ghost stderr = %q", stderr.String()) + } + stdout.Reset() + stderr.Reset() + if code := runCombo([]string{"remove", "gone", "--yes"}, routingDeps(t, server, &stdout, &stderr)); code != routingExitMissing { + t.Fatalf("combo remove gone exit = %d, want 4 (stderr: %s)", code, stderr.String()) + } + if !strings.Contains(stderr.String(), "Error: unknown combo: gone") { + t.Fatalf("combo remove gone stderr = %q", stderr.String()) + } +} diff --git a/src/cli/alias.ts b/src/cli/alias.ts index 462d98e229..95c27c10aa 100644 --- a/src/cli/alias.ts +++ b/src/cli/alias.ts @@ -1,4 +1,4 @@ -import { CliUsageError, printData, rejectArgs, runtimeRequest, takeFlag, takeOption, type RuntimeApiDeps } from "./runtime-api"; +import { CliUsageError, printData, rejectArgs, runCliAction, runtimeRequest, takeFlag, takeOption, type RuntimeApiDeps } from "./runtime-api"; const USAGE = `Usage: ocx alias list [--json] @@ -13,54 +13,60 @@ function selector(value: string): { provider: string; model?: string } { } export async function handleAliasCommand(argv: string[], deps: RuntimeApiDeps = {}): Promise { - const args = [...argv]; - const action = (args.shift() ?? "list").toLowerCase(); - const wantsJson = takeFlag(args, "--json"); - if (action === "list") { - rejectArgs(args, USAGE); - const result = await runtimeRequest>("/api/aliases", {}, deps); - const lines: string[] = []; - for (const [target, alias] of Object.entries((result.providers ?? {}) as Record)) lines.push(`provider ${target} ${alias} user`); - for (const [provider, rows] of Object.entries((result.models ?? {}) as Record>)) { - for (const [model, value] of Object.entries(rows)) lines.push(`model ${provider}/${model} ${value.alias} ${value.source}`); + // runCliAction owns the error taxonomy (usage errors exit 2 with the USAGE + // block, RuntimeApiError 4/5/1) exactly like the sibling combo and route + // policy families; without it every CliUsageError above crashed the CLI with + // a Bun stack dump at exit 1 instead of a classified error. + return runCliAction(async () => { + const args = [...argv]; + const action = (args.shift() ?? "list").toLowerCase(); + const wantsJson = takeFlag(args, "--json"); + if (action === "list") { + rejectArgs(args, USAGE); + const result = await runtimeRequest>("/api/aliases", {}, deps); + const lines: string[] = []; + for (const [target, alias] of Object.entries((result.providers ?? {}) as Record)) lines.push(`provider ${target} ${alias} user`); + for (const [provider, rows] of Object.entries((result.models ?? {}) as Record>)) { + for (const [model, value] of Object.entries(rows)) lines.push(`model ${provider}/${model} ${value.alias} ${value.source}`); + } + printData(result, wantsJson, lines.length ? lines : ["No aliases configured."]); + return; } - printData(result, wantsJson, lines.length ? lines : ["No aliases configured."]); - return 0; - } - if (action === "defaults") { - const state = args.shift()?.toLowerCase(); - const provider = takeOption(args, "--provider"); - rejectArgs(args, USAGE); - if (state !== "on" && state !== "off") throw new CliUsageError("defaults requires on or off", USAGE); - const result = await runtimeRequest("/api/default-aliases", { method: "PUT", body: JSON.stringify({ enabled: state === "on", ...(provider ? { provider } : {}) }) }, deps); - printData(result, wantsJson, [`Default aliases ${state}${provider ? ` for ${provider}` : " globally"}.`]); - return 0; - } - const target = args.shift()?.trim(); - if (!target) throw new CliUsageError("alias target is required", USAGE); - const parsed = selector(target); - if (!parsed.provider || parsed.model === "") throw new CliUsageError("target must be provider or provider/native-model-id", USAGE); - if (action === "set") { - const alias = args.shift()?.trim(); - rejectArgs(args, USAGE); - if (!alias) throw new CliUsageError("alias value is required", USAGE); - const path = parsed.model === undefined - ? `/api/providers/${encodeURIComponent(parsed.provider)}/alias` - : `/api/providers/${encodeURIComponent(parsed.provider)}/model-aliases`; - const body = parsed.model === undefined ? { alias } : { set: { [parsed.model]: alias } }; - const result = await runtimeRequest(path, { method: "PUT", body: JSON.stringify(body) }, deps); - printData(result, wantsJson, [`${target} → ${alias}`]); - return 0; - } - if (action === "rm") { - rejectArgs(args, USAGE); - const path = parsed.model === undefined - ? `/api/providers/${encodeURIComponent(parsed.provider)}/alias` - : `/api/providers/${encodeURIComponent(parsed.provider)}/model-aliases`; - const body = parsed.model === undefined ? { alias: null } : { remove: [parsed.model] }; - const result = await runtimeRequest(path, { method: "PUT", body: JSON.stringify(body) }, deps); - printData(result, wantsJson, [`Removed alias for ${target}.`]); - return 0; - } - throw new CliUsageError(`unknown alias action '${action}'`, USAGE); + if (action === "defaults") { + const state = args.shift()?.toLowerCase(); + const provider = takeOption(args, "--provider"); + rejectArgs(args, USAGE); + if (state !== "on" && state !== "off") throw new CliUsageError("defaults requires on or off", USAGE); + const result = await runtimeRequest("/api/default-aliases", { method: "PUT", body: JSON.stringify({ enabled: state === "on", ...(provider ? { provider } : {}) }) }, deps); + printData(result, wantsJson, [`Default aliases ${state}${provider ? ` for ${provider}` : " globally"}.`]); + return; + } + const target = args.shift()?.trim(); + if (!target) throw new CliUsageError("alias target is required", USAGE); + const parsed = selector(target); + if (!parsed.provider || parsed.model === "") throw new CliUsageError("target must be provider or provider/native-model-id", USAGE); + if (action === "set") { + const alias = args.shift()?.trim(); + rejectArgs(args, USAGE); + if (!alias) throw new CliUsageError("alias value is required", USAGE); + const path = parsed.model === undefined + ? `/api/providers/${encodeURIComponent(parsed.provider)}/alias` + : `/api/providers/${encodeURIComponent(parsed.provider)}/model-aliases`; + const body = parsed.model === undefined ? { alias } : { set: { [parsed.model]: alias } }; + const result = await runtimeRequest(path, { method: "PUT", body: JSON.stringify(body) }, deps); + printData(result, wantsJson, [`${target} → ${alias}`]); + return; + } + if (action === "rm") { + rejectArgs(args, USAGE); + const path = parsed.model === undefined + ? `/api/providers/${encodeURIComponent(parsed.provider)}/alias` + : `/api/providers/${encodeURIComponent(parsed.provider)}/model-aliases`; + const body = parsed.model === undefined ? { alias: null } : { remove: [parsed.model] }; + const result = await runtimeRequest(path, { method: "PUT", body: JSON.stringify(body) }, deps); + printData(result, wantsJson, [`Removed alias for ${target}.`]); + return; + } + throw new CliUsageError(`unknown alias action '${action}'`, USAGE); + }); } diff --git a/tests/go-cli-parity.test.ts b/tests/go-cli-parity.test.ts index 0ac65bbb56..dc43acfbeb 100644 --- a/tests/go-cli-parity.test.ts +++ b/tests/go-cli-parity.test.ts @@ -268,4 +268,208 @@ describe.skipIf(!goAvailable || goCLI === null)("Go CLI parity (ADR-0008, ticket expect(expectParity(["usage"])).toMatchObject({ code: 1 }); expect(expectParity(["observe", "usage"])).toMatchObject({ code: 1 }); }); + + // alias/combo/route are Go-owned (ADR-0008 config-routing slice, issue #49); + // both CLIs read and write the routing subsections of the config file through + // the same live-proxy management routes, so the fixture serves the attested + // identity probe plus canned /api/aliases, /api/combos, /api/routing-profiles + // and their write endpoints. + function startRoutingFixture(): void { + testHome = mkdtempSync(join(tmpdir(), "ocx-go-routing-parity-")); + const aliasesPayload = { + providers: { alpha: "a", beta: "b" }, + models: { + alpha: { + m1: { alias: "one", source: "user" }, + "gpt-5.4": { alias: "five", source: "builtin" }, + stale: { alias: "gone", source: "user", stale: true }, + }, + }, + defaults: { global: true, providers: { alpha: false } }, + }; + const combosPayload = { + combos: [ + { id: "fast", model: "combo/fast", strategy: "failover", stickyLimit: 1, targets: [{ provider: "alpha", model: "m1", weight: 2 }, { provider: "beta", model: "b1" }] }, + { id: "smart", model: "combo/smart", strategy: "round-robin", stickyLimit: 3, targets: [{ provider: "alpha", model: "m2" }], alias: "smartie", defaultEffort: "high" }, + ], + }; + const profilesPayload = { + profiles: [ + { id: "p1", model: "policy/p1", revision: 3, strategy: "cost" }, + // A profile without a public model exercises the `policy/` fallback. + { id: "p2", revision: 1, strategy: "cost" }, + ], + }; + const decisionPayload = { + profile: "p1", + matched: true, + model: "policy/p1", + reasons: ["compatibility matched"], + evidence: {}, + candidates: [], + }; + testServer = Bun.serve({ port: 0, fetch: async request => { + const url = new URL(request.url); + const path = url.pathname; + if (path === "/healthz") { + const challenge = request.headers.get("x-opencodex-attestation-challenge") ?? ""; + const headers = challenge ? { "x-opencodex-attestation-proof": createLocalAttestationProof(secret, challenge, process.pid, testServer!.port) } : {}; + return Response.json({ status: "ok", service: "opencodex", version: "2.42.0", uptime: 1, pid: process.pid, port: testServer!.port }, { headers }); + } + if (path === "/api/aliases" && request.method === "GET") return Response.json(aliasesPayload); + if (path === "/api/combos" && request.method === "GET") return Response.json(combosPayload); + if (path === "/api/combos" && request.method === "PUT") { + const body = await request.json().catch(() => ({})); + if ((body as { id?: string })?.id === "conflict") return Response.json({ error: "alias conflicts with 'combo/smart'" }, { status: 409 }); + return Response.json({ ok: true, id: (body as { id?: string })?.id ?? null }); + } + if (path === "/api/combos" && request.method === "DELETE") { + const id = url.searchParams.get("id"); + if (id === "gone") return Response.json({ error: "unknown combo: gone" }, { status: 404 }); + return Response.json({ ok: true, id }); + } + if (path === "/api/routing-profiles" && request.method === "GET") return Response.json(profilesPayload); + if (path === "/api/routing-profiles/dry-run" && request.method === "POST") return Response.json(decisionPayload); + if (path === "/api/default-aliases" && request.method === "PUT") { + return Response.json({ ok: true, catalogRefresh: { status: "noop", ok: true } }); + } + const providerAlias = path.match(/^\/api\/providers\/([^/]+)\/alias$/); + if (providerAlias && request.method === "PUT") { + const name = decodeURIComponent(providerAlias[1]!); + if (name === "beta") return Response.json({ error: "alias conflicts with 'beta'" }, { status: 409 }); + if (name === "ghost") return Response.json({ error: `provider '${name}' not found` }, { status: 404 }); + return Response.json({ ok: true, provider: name, alias: "x", catalogRefresh: { status: "noop", ok: true } }); + } + const modelAlias = path.match(/^\/api\/providers\/([^/]+)\/model-aliases$/); + if (modelAlias && request.method === "PUT") { + const name = decodeURIComponent(modelAlias[1]!); + if (name === "ghost") return Response.json({ error: `provider '${name}' not found` }, { status: 404 }); + return Response.json({ ok: true, aliases: { m1: "one" }, catalogRefresh: { status: "noop", ok: true } }); + } + return new Response("not found", { status: 404 }); + } }); + writeFileSync(join(testHome, "runtime-port.json"), JSON.stringify({ pid: process.pid, port: testServer.port, hostname: "127.0.0.1", attestationSecret: secret })); + } + test.each([ + { args: ["alias", "list"] }, + { args: ["alias", "list", "--json"] }, + { args: ["alias", "defaults", "on"] }, + { args: ["alias", "defaults", "off", "--provider", "alpha"] }, + { args: ["alias", "defaults", "on", "--json"] }, + { args: ["alias", "set", "alpha", "fast-a"] }, + { args: ["alias", "set", "alpha", "fast-a", "--json"] }, + { args: ["alias", "set", "alpha/m1", "one-a", "--json"] }, + { args: ["alias", "rm", "alpha"] }, + { args: ["alias", "rm", "alpha/m1", "--json"] }, + { args: ["combo", "list"] }, + { args: ["combo", "list", "--json"] }, + { args: ["combo", "show", "fast"] }, + { args: ["combo", "show", "smart", "--json"] }, + { args: ["combo", "set", "fast", "--targets", "alpha/m1:2,beta/b1", "--json"] }, + { args: ["combo", "set", "smart", "--targets", "alpha/m2", "--strategy", "round-robin", "--sticky", "3", "--effort", "high", "--alias", "smartie", "--display-name", "Smart Combo", "--rename-from", "old-smart"] }, + { args: ["combo", "set", "dash", "--targets", "alpha/m1", "--effort", "-", "--alias", "-", "--display-name", "-"] }, + { args: ["combo", "remove", "fast", "--yes"] }, + { args: ["combo", "remove", "fast", "--yes", "--json"] }, + { args: ["route", "combo", "list"] }, + { args: ["route", "combo", "show", "fast", "--json"] }, + { args: ["route", "policy", "list"] }, + { args: ["route", "policy", "list", "--json"] }, + { args: ["route", "policy", "show", "p1"] }, + { args: ["route", "policy", "show", "p2", "--json"] }, + { args: ["route", "policy", "dry-run", "p1"] }, + { args: ["route", "policy", "dry-run", "p1", "--model-context", "9000", "--tools", "--image", "--structured-output", "--json"] }, + { args: ["route", "policy", "evaluate", "p1", "--model-context", "128000"] }, + ])("diffs Go-owned config-routing output and exit code for $args", async ({ args }) => { + startRoutingFixture(); + // spawnSync blocks Bun's event loop, which starves the fixture server; live + // management-plane rows drive both CLIs async like the usage oracle above. + const ts = await runTsAsync(args); + const go = await runGoAsync(args); + expect(go).toEqual(ts); + expect(ts).toMatchObject({ code: 0, stderr: "" }); + }); + test.each([ + { args: ["combo", "show", "missing"] }, + { args: ["route", "policy", "show", "missing"] }, + ])("diffs config-routing unknown-id usage output for $args", async ({ args }) => { + startRoutingFixture(); + const ts = await runTsAsync(args); + const go = await runGoAsync(args); + expect(go).toEqual(ts); + expect(ts).toMatchObject({ code: 2, stdout: "" }); + }); + test.each([ + // Write refusals: 409 (collision) and 404 (unknown target) selection the + // fixture keys on the provider/combo id both CLIs send. + { args: ["alias", "set", "beta", "b2"], code: 5 }, + { args: ["alias", "set", "ghost", "g"], code: 4 }, + { args: ["alias", "set", "ghost/m", "g", "--json"], code: 4 }, + { args: ["combo", "set", "conflict", "--targets", "alpha/m1"], code: 5 }, + { args: ["combo", "remove", "gone", "--yes"], code: 4 }, + ])("diffs config-routing write refusals for $args", async ({ args, code }) => { + startRoutingFixture(); + const ts = await runTsAsync(args); + const go = await runGoAsync(args); + expect(go).toEqual(ts); + expect(ts).toMatchObject({ code, stdout: "" }); + }); + test.each([ + { args: ["alias", "defaults", "maybe"] }, + { args: ["alias", "defaults", "on", "extra"] }, + { args: ["alias", "defaults", "--provider"] }, + { args: ["alias", "defaults", "--provider", "x", "on"] }, + { args: ["alias", "set"] }, + { args: ["alias", "set", "alpha"] }, + { args: ["alias", "set", "alpha/"] }, + { args: ["alias", "set", "/m"] }, + { args: ["alias", "set", "alpha", "m1", "extra"] }, + { args: ["alias", "rm"] }, + { args: ["alias", "rm", "alpha", "extra"] }, + { args: ["alias", "list", "extra"] }, + { args: ["alias", "frobnicate", "x"] }, + { args: ["combo", "frobnicate"] }, + { args: ["combo", "set"] }, + { args: ["combo", "set", "x"] }, + { args: ["combo", "set", "x", "--targets"] }, + { args: ["combo", "set", "x", "--targets", "plain"] }, + { args: ["combo", "set", "x", "--targets", "a/b:0"] }, + { args: ["combo", "set", "x", "--targets", "a/"] }, + { args: ["combo", "set", "x", "--targets", "a/b", "--strategy", "bad"] }, + { args: ["combo", "set", "x", "--targets", "a/b", "--sticky", "3"] }, + { args: ["combo", "set", "x", "--targets", "a/b", "--strategy", "random", "--sticky", "5"] }, + { args: ["combo", "set", "x", "--targets", "a/b", "--sticky", "200"] }, + { args: ["combo", "set", "x", "--targets", "a/b", "--sticky", "abc"] }, + { args: ["combo", "remove", "x"] }, + { args: ["combo", "remove", "x", "--yes", "extra"] }, + { args: ["route"] }, + { args: ["route", "frobnicate"] }, + { args: ["route", "policy"] }, + { args: ["route", "policy", "show", "--json"] }, + { args: ["route", "policy", "dry-run", "x", "--model-context", "abc"] }, + { args: ["route", "policy", "dry-run", "x", "extra"] }, + { args: ["route", "policy", "frobnicate"] }, + ])("diffs config-routing argument validation for $args", ({ args }) => { + testHome = mkdtempSync(join(tmpdir(), "ocx-go-routing-parity-")); + expect(expectParity(args)).toMatchObject({ code: 2 }); + }); + test.each([ + { args: ["alias", "list"] }, + { args: ["combo"] }, + { args: ["route", "combo"] }, + { args: ["route", "combo", "list"] }, + { args: ["route", "policy", "list"] }, + { args: ["alias", "set", "alpha", "x"] }, + ])("diffs config-routing when no proxy is running for $args", ({ args }) => { + testHome = mkdtempSync(join(tmpdir(), "ocx-go-routing-parity-")); + expect(expectParity(args)).toMatchObject({ code: 1, stdout: "" }); + }); + test.each([ + { args: ["help", "alias"] }, { args: ["alias", "--help"] }, { args: ["alias", "help"] }, + { args: ["help", "combo"] }, { args: ["combo", "--help"] }, + { args: ["help", "route"] }, { args: ["route", "--help"] }, + { args: ["route", "combo", "--help"] }, { args: ["route", "policy", "--help"] }, + ])("diffs config-routing help contracts for $args", ({ args }) => { + testHome = mkdtempSync(join(tmpdir(), "ocx-go-routing-parity-")); + expect(expectParity(args)); + }); }); From a2ace110b2507e1eacb369fd3ab9668a924ed57a Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Tue, 8 Sep 2026 07:04:24 +0800 Subject: [PATCH 118/165] feat(go): flip debug, system, api-key, and access to Go-owned (issue #47) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The four management-command families now run natively in the Go binary as management-API clients (findLiveProxy + /api/* + /v1/*), byte-identical to the TypeScript owner. New native ports share one management client ported from src/cli/runtime-api.ts (live-proxy discovery, admin token from OPENCODEX_ADMIN_AUTH_TOKEN or the admin-api-token file, RuntimeApiError message composition, exit taxonomy 2/4/5/1) and one V8-exact JSON/line renderer (jsonwire key order, canonical numbers, summaryLines flattening). - go/internal/ocxcli/mgmt_command.go — management client + failures. - go/internal/ocxcli/mgmt_print.go — JSON.stringify(v,null,2) and summaryLines renderers ported from runtime-api.ts printData. - access_command.go — access + api-key alias (key list/create/rotate start|commit|abort/remove, endpoints, models, test), rotation and remove bodies, key-table rendering with ambiguous column and attribution footer. - debug_command.go — provider/usage/injection/claude on|off|status|reset and provider/usage logs [-f], env-default help for a stopped proxy, and the debug.ts failure wording (exit 1, no Error: prefix). - system_command.go — status/settings/startup/diagnostics/sync/ codex-app-server/codex-restart/update with the system USAGE block. - cli.go/help.go — ownership flip, per-command Run dispatch and family help blocks that byte-match the registry entries; `system codex-cli-update` stays a TypeScript-owned subcommand seam (read-only local Codex install inspection, no management plane), matching the models/observe pattern. Oracle coverage in tests/go-cli-parity.test.ts against a management-plane fixture: family help contracts (help X and X --help), argument validation exit codes, live read/write rows (debug status/toggle/reset/logs, access key list/create/rotate commit/abort/remove/endpoints/models/test, api-key, system settings/startup/diagnostics/sync/codex-app-server/codex-restart/ update check|run|status, JSON spellings), and denied (401) write rows. Go unit coverage in mgmt_command_test.go pins the wire bodies and the exact human renderings captured from the TS CLI on the same payloads. Co-Authored-By: Claude Code --- go/internal/ocxcli/access_command.go | 491 ++++++++++++++++++++++++ go/internal/ocxcli/cli.go | 39 +- go/internal/ocxcli/debug_command.go | 434 +++++++++++++++++++++ go/internal/ocxcli/help.go | 10 + go/internal/ocxcli/mgmt_command.go | 176 +++++++++ go/internal/ocxcli/mgmt_command_test.go | 348 +++++++++++++++++ go/internal/ocxcli/mgmt_print.go | 248 ++++++++++++ go/internal/ocxcli/system_command.go | 397 +++++++++++++++++++ tests/go-cli-parity.test.ts | 181 ++++++++- 9 files changed, 2319 insertions(+), 5 deletions(-) create mode 100644 go/internal/ocxcli/access_command.go create mode 100644 go/internal/ocxcli/debug_command.go create mode 100644 go/internal/ocxcli/mgmt_command.go create mode 100644 go/internal/ocxcli/mgmt_command_test.go create mode 100644 go/internal/ocxcli/mgmt_print.go create mode 100644 go/internal/ocxcli/system_command.go diff --git a/go/internal/ocxcli/access_command.go b/go/internal/ocxcli/access_command.go new file mode 100644 index 0000000000..077c420511 --- /dev/null +++ b/go/internal/ocxcli/access_command.go @@ -0,0 +1,491 @@ +// ocx access + ocx api-key — the admission-key and endpoint family. This file +// ports the TypeScript owner (src/cli/access.ts, shared by both spellings: +// `api-key` dispatches as `access key`) against the same management endpoints +// (/api/keys, /api/keys/rotate[/commit], /v1/models, /v1/chat/completions, +// /v1/responses, /v1/messages) with byte-identical parsing, rendering, and +// exit-code taxonomy. +package ocxcli + +import ( + "fmt" + "strconv" + "strings" + + "github.com/lidge-jun/opencodex/go/internal/jsonwire" +) + +// accessUsage mirrors ACCESS_USAGE in src/cli/access.ts; the api-key alias +// reuses it verbatim because the TS owner routes `api-key` through +// handleAccessCommand(["key", ...argv]). +const accessUsage = `Usage: + ocx access key [list] [--json] + ocx access key create [name] [--json] + ocx access key rotate [--json] + ocx access key rotate commit [--json] + ocx access key rotate abort [--json] + ocx access key remove --yes [--json] + ocx access endpoints [--json] + ocx access models [--json] + ocx access test [--protocol ] [--json]` + +// takeMgmtFlag mirrors takeFlag: remove `flag` from args anywhere and report +// whether it was present. +func takeMgmtFlag(args *[]string, flag string) bool { + for i, arg := range *args { + if arg == flag { + *args = append((*args)[:i], (*args)[i+1:]...) + return true + } + } + return false +} + +// takeMgmtOption mirrors takeOption: `--flag value`, rejecting a missing value +// with the exact TypeScript message. +func takeMgmtOption(args *[]string, flag string) (string, bool, error) { + for i, arg := range *args { + if arg != flag { + continue + } + if i+1 >= len(*args) || strings.HasPrefix((*args)[i+1], "--") { + return "", false, managementCliUsage(fmt.Sprintf("%s requires a value", flag), "") + } + value := (*args)[i+1] + *args = append((*args)[:i], (*args)[i+2:]...) + return value, true, nil + } + return "", false, nil +} + +// rejectMgmtArgs mirrors rejectArgs: a leftover positional is a usage error +// carrying the command's USAGE block. +func rejectMgmtArgs(args []string, usage string) error { + if len(args) == 0 { + return nil + } + return managementCliUsage("Unexpected argument(s): "+strings.Join(args, " "), usage) +} + +// runAccess implements `ocx access` (and, through runApiKey, `ocx api-key`). +// argv carries only this command's own arguments. +func runAccess(args []string, deps Deps) int { + err := handleMgmtAccess(args, deps) + if err != nil { + return reportManagementFailure(deps, err) + } + return ExitOK +} + +// runApiKey mirrors the TypeScript dispatch alias: `api-key ` is +// exactly `access key `. +func runApiKey(args []string, deps Deps) int { + return runAccess(append([]string{"key"}, args...), deps) +} + +func handleMgmtAccess(argv []string, deps Deps) error { + sub := "key" + rest := argv + if len(argv) > 0 { + sub = argv[0] + rest = argv[1:] + } + switch sub { + case "key", "keys": + return mgmtAccessKey(rest, deps) + case "endpoints": + return mgmtAccessEndpoints(rest, deps) + case "models": + return mgmtAccessModels(rest, deps) + case "test": + return mgmtAccessTest(rest, deps) + default: + return managementCliUsage("unknown access command "+sub, accessUsage) + } +} + +func mgmtAccessKey(argv []string, deps Deps) error { + args := append([]string(nil), argv...) + action := "list" + if len(args) > 0 { + action = strings.ToLower(args[0]) + args = args[1:] + } + wantsJSON := takeMgmtFlag(&args, "--json") + switch action { + case "list": + if err := rejectMgmtArgs(args, accessUsage); err != nil { + return err + } + body, rawText, _, err := managementRequest(deps, "GET", "/api/keys", "") + if err != nil { + return err + } + lines := []string{"No API access keys configured."} + keys := mgmtKeyRows(body) + if len(keys) > 0 { + lines = mgmtFormatKeyRows(body, keys) + } + printManagementData(deps, body, rawText, wantsJSON, lines) + return nil + case "create": + name := "default" + if len(args) > 0 { + name = args[0] + args = args[1:] + } + if err := rejectMgmtArgs(args, accessUsage); err != nil { + return err + } + result, _, _, err := managementRequest(deps, "POST", "/api/keys", `{"name":`+quoteJSONString(name)+`}`) + if err != nil { + return err + } + resultName := fieldString(result, "name") + if resultName == "" { + resultName = name + } + printManagementData(deps, result, "", wantsJSON, []string{ + fmt.Sprintf("Created API key %s (%s).", resultName, fieldString(result, "id")), + fmt.Sprintf("Key (shown once): %s", fieldString(result, "key")), + }) + return nil + case "rotate": + return mgmtAccessKeyRotate(args, wantsJSON, deps) + case "remove", "delete": + id := "" + if len(args) > 0 { + id = args[0] + args = args[1:] + } + yes := takeMgmtFlag(&args, "--yes") + if id == "" { + return managementCliUsage("key id is required", accessUsage) + } + if !yes { + return managementCliUsage("remove requires --yes", accessUsage) + } + if err := rejectMgmtArgs(args, accessUsage); err != nil { + return err + } + result, _, _, err := managementRequest(deps, "DELETE", "/api/keys", `{"id":`+quoteJSONString(id)+`}`) + if err != nil { + return err + } + printManagementData(deps, result, "", wantsJSON, []string{fmt.Sprintf("Removed API key %s.", id)}) + return nil + default: + return managementCliUsage("unknown key command "+action, accessUsage) + } +} + +func mgmtAccessKeyRotate(args []string, wantsJSON bool, deps Deps) error { + operation := "start" + if len(args) > 0 && (args[0] == "commit" || args[0] == "abort") { + operation = args[0] + args = args[1:] + } + id := "" + if len(args) > 0 { + id = args[0] + args = args[1:] + } + if id == "" { + return managementCliUsage("key id is required", accessUsage) + } + if operation == "start" { + if err := rejectMgmtArgs(args, accessUsage); err != nil { + return err + } + result, _, _, err := managementRequest(deps, "POST", "/api/keys/rotate", `{"id":`+quoteJSONString(id)+`}`) + if err != nil { + return err + } + printManagementData(deps, result, "", wantsJSON, []string{ + fmt.Sprintf("Started rotation for API key %s.", id), + fmt.Sprintf("New key (shown once): %s", fieldString(result, "key")), + fmt.Sprintf("After the client accepts it, commit with rotation id %s.", fieldString(result, "rotationId")), + }) + return nil + } + rotationID := "" + if len(args) > 0 { + rotationID = args[0] + args = args[1:] + } + if rotationID == "" { + return managementCliUsage("rotation id is required", accessUsage) + } + if err := rejectMgmtArgs(args, accessUsage); err != nil { + return err + } + body := `{"id":` + quoteJSONString(id) + `,"rotationId":` + quoteJSONString(rotationID) + `}` + method, path := "POST", "/api/keys/rotate/commit" + if operation == "abort" { + method, path = "DELETE", "/api/keys/rotate" + } + result, _, _, err := managementRequest(deps, method, path, body) + if err != nil { + return err + } + verb := "Committed" + if operation == "abort" { + verb = "Aborted" + } + printManagementData(deps, result, "", wantsJSON, []string{fmt.Sprintf("%s rotation for API key %s.", verb, id)}) + return nil +} + +func mgmtAccessEndpoints(argv []string, deps Deps) error { + args := append([]string(nil), argv...) + wantsJSON := takeMgmtFlag(&args, "--json") + if err := rejectMgmtArgs(args, accessUsage); err != nil { + return err + } + result, _, _, err := managementRequest(deps, "GET", "/api/keys", "") + if err != nil { + return err + } + filtered := jsonwire.ObjectValue() + if result != nil && result.Kind() == jsonwire.Object { + for _, member := range result.Members() { + if strings.HasSuffix(member.Key, "Endpoint") || member.Key == "baseUrl" || member.Key == "endpoint" { + filtered.Set(member.Key, member.Value) + } + } + } + lines := []string{} + for _, member := range filtered.Members() { + lines = append(lines, fmt.Sprintf("%s: %s", member.Key, jsString(member.Value))) + } + printManagementData(deps, filtered, "", wantsJSON, lines) + return nil +} + +func mgmtAccessModels(argv []string, deps Deps) error { + args := append([]string(nil), argv...) + wantsJSON := takeMgmtFlag(&args, "--json") + if err := rejectMgmtArgs(args, accessUsage); err != nil { + return err + } + result, _, _, err := managementRequest(deps, "GET", "/v1/models", "") + if err != nil { + return err + } + lines := []string{} + if data := fieldArray(result, "data"); data != nil { + for _, row := range data.Elements() { + if row == nil || row.Kind() != jsonwire.Object { + continue + } + id := fieldString(row, "id") + ownedBy := "" + if field := row.Find("owned_by"); field != nil { + if field.Kind() == jsonwire.String { + ownedBy = field.String() + } else if field.Kind() == jsonwire.Null { + ownedBy = "" + } + } + line := id + " " + ownedBy + lines = append(lines, strings.TrimRight(line, " ")) + } + } + printManagementData(deps, result, "", wantsJSON, lines) + return nil +} + +func mgmtAccessTest(argv []string, deps Deps) error { + args := append([]string(nil), argv...) + model := "" + if len(args) > 0 { + model = args[0] + args = args[1:] + } + wantsJSON := takeMgmtFlag(&args, "--json") + protocol, _, err := takeMgmtOption(&args, "--protocol") + if err != nil { + return err + } + if protocol == "" { + protocol = "chat" + } + if model == "" { + return managementCliUsage("model is required", accessUsage) + } + if protocol != "chat" && protocol != "responses" && protocol != "messages" { + return managementCliUsage("--protocol must be chat, responses, or messages", accessUsage) + } + if err := rejectMgmtArgs(args, accessUsage); err != nil { + return err + } + path := "/v1/chat/completions" + requestBody := `{"model":` + quoteJSONString(model) + `,"messages":[{"role":"user","content":"Reply with OK."}],"max_tokens":16,"stream":false}` + if protocol == "responses" { + path = "/v1/responses" + requestBody = `{"model":` + quoteJSONString(model) + `,"input":"Reply with OK.","max_output_tokens":16}` + } else if protocol == "messages" { + path = "/v1/messages" + requestBody = `{"model":` + quoteJSONString(model) + `,"messages":[{"role":"user","content":"Reply with OK."}],"max_tokens":16}` + } + result, _, _, reqErr := managementRequest(deps, "POST", path, requestBody) + if reqErr != nil { + return reqErr + } + printManagementData(deps, result, "", wantsJSON, []string{fmt.Sprintf("%s: %s request succeeded.", model, protocol)}) + return nil +} + +// ───────────────────────────────────────────────────────────────────────────── +// key-table rendering — port of formatKeyRows in src/cli/access.ts. + +func mgmtKeyRows(body *jsonwire.Value) []*jsonwire.Value { + if body == nil || body.Kind() != jsonwire.Object { + return nil + } + array := fieldArray(body, "keys") + if array == nil { + return nil + } + var rows []*jsonwire.Value + for _, element := range array.Elements() { + if element != nil && element.Kind() == jsonwire.Object { + rows = append(rows, element) + } + } + return rows +} + +// mgmtFormatKeyRows renders the key table with usage-column semantics: an +// ambiguous key prints one marker spanning both numeric columns; numeric cells +// use toLocaleString("en-US"); the data-set footer prints once below a blank +// line. +func mgmtFormatKeyRows(body *jsonwire.Value, keys []*jsonwire.Value) []string { + cells := [][]string{{"ID", "NAME", "PREFIX", "REQ 7D", "TOTAL", "LAST USED"}} + for _, entry := range keys { + usage := entry.Find("usage") + if usage == nil || usage.Kind() != jsonwire.Object { + usage = jsonwire.ObjectValue() + } + ambiguous := false + if field := usage.Find("ambiguous"); field != nil && field.Kind() == jsonwire.Bool { + ambiguous = field.Bool() + } + requests7d := usage.Find("requests7d") + totalRequests := usage.Find("totalRequests") + lastUsedAt := usage.Find("lastUsedAt") + cells = append(cells, []string{ + fieldString(entry, "id"), + fieldString(entry, "name"), + fieldString(entry, "prefix"), + ambiguousOrNumber(ambiguous, requests7d), + ambiguousOrEmptyNumber(ambiguous, totalRequests), + ambiguousOrLastUsed(ambiguous, lastUsedAt), + }) + } + widths := make([]int, len(cells[0])) + for _, row := range cells { + for i, cell := range row { + if len(cell) > widths[i] { + widths[i] = len(cell) + } + } + } + lines := []string{} + for _, row := range cells { + parts := make([]string, 0, len(row)) + for i, cell := range row { + parts = append(parts, cell+strings.Repeat(" ", widths[i]-len(cell))) + } + lines = append(lines, strings.TrimRight(strings.Join(parts, " "), " ")) + } + footer := []string{} + if attribution := fieldString(body, "attributionSince"); attribution != "" { + footer = append(footer, "attribution since "+attribution) + } + if field := body.Find("historyTruncated"); field != nil && field.Kind() == jsonwire.Bool && field.Bool() { + footer = append(footer, "older history truncated") + } + for _, entry := range keys { + usage := entry.Find("usage") + if usage == nil || usage.Kind() != jsonwire.Object { + continue + } + if field := usage.Find("ambiguous"); field != nil && field.Kind() == jsonwire.Bool && field.Bool() { + footer = append(footer, "ambiguous: two configured keys share an id, so per-key totals do not exist") + break + } + } + if len(footer) > 0 { + lines = append(lines, "") + lines = append(lines, footer...) + } + return lines +} + +func ambiguousOrNumber(ambiguous bool, value *jsonwire.Value) string { + if ambiguous { + return "ambiguous" + } + return usageNumberCell(value) +} + +func ambiguousOrEmptyNumber(ambiguous bool, value *jsonwire.Value) string { + if ambiguous { + return "" + } + return usageNumberCell(value) +} + +func ambiguousOrLastUsed(ambiguous bool, value *jsonwire.Value) string { + if ambiguous { + return "" + } + if value != nil && value.Kind() == jsonwire.String { + return value.String() + } + return "never" +} + +func usageNumberCell(value *jsonwire.Value) string { + if value != nil && value.Kind() == jsonwire.Number { + number, err := strconv.ParseFloat(value.NumberRaw(), 64) + if err == nil { + return formatENUSNumber(number) + } + } + return "-" +} + +// quoteJSONString encodes one string as a JSON literal via the V8 rules. +func quoteJSONString(value string) string { + raw, err := jsonwire.EncodeString(value) + if err != nil { + return `""` + } + return string(raw) +} + +// fieldString returns an object member's string payload, or "" when absent or +// not a string. +func fieldString(object *jsonwire.Value, key string) string { + if object == nil || object.Kind() != jsonwire.Object { + return "" + } + field := object.Find(key) + if field == nil || field.Kind() != jsonwire.String { + return "" + } + return field.String() +} + +// fieldArray returns an object member's array, or nil when absent/not an array. +func fieldArray(object *jsonwire.Value, key string) *jsonwire.Value { + if object == nil || object.Kind() != jsonwire.Object { + return nil + } + field := object.Find(key) + if field == nil || field.Kind() != jsonwire.Array { + return nil + } + return field +} diff --git a/go/internal/ocxcli/cli.go b/go/internal/ocxcli/cli.go index aa63ead65b..f42adb0bcd 100644 --- a/go/internal/ocxcli/cli.go +++ b/go/internal/ocxcli/cli.go @@ -64,7 +64,9 @@ var Commands = []Command{ {Name: "sync-cache", Usage: "ocx sync-cache [--restart-codex]", Summary: "Refresh the model cache.", Owner: TypeScriptOwned}, {Name: "status", Usage: "ocx status", Summary: "Check proxy status.", Owner: GoOwned}, {Name: "doctor", Usage: "ocx doctor", Summary: "Diagnose the environment.", Owner: GoOwned}, - {Name: "debug", Usage: "ocx debug ", Summary: "Manage debug settings.", Owner: TypeScriptOwned}, + // The debug family is Go-owned: every scope writes /api/debug settings and + // reads the buffered /api/debug log streams through the running proxy. + {Name: "debug", Usage: "ocx debug ", Summary: "Show or toggle runtime provider, usage, injection, and Claude debug capture.", Owner: GoOwned}, {Name: "login", Usage: "ocx login ", Summary: "Log in to a provider.", Owner: TypeScriptOwned}, {Name: "logout", Usage: "ocx logout ", Summary: "Log out from a provider.", Owner: TypeScriptOwned}, {Name: "gui", Usage: "ocx gui", Summary: "Open the dashboard.", Owner: TypeScriptOwned}, @@ -89,12 +91,18 @@ var Commands = []Command{ {Name: "usage", Usage: "ocx usage [--range ] [--surface ] [--provider ] [--model ] [--json]", Summary: "Alias of ocx observe usage.", Owner: GoOwned}, {Name: "storage", Usage: "ocx storage ", Summary: "Manage storage.", Owner: TypeScriptOwned}, {Name: "memory", Usage: "ocx memory [--json]", Summary: "Inspect memory.", Owner: TypeScriptOwned}, - {Name: "api-key", Usage: "ocx api-key ", Summary: "Manage API keys.", Owner: TypeScriptOwned}, - {Name: "access", Usage: "ocx access ", Summary: "Manage external access.", Owner: TypeScriptOwned}, + // access/api-key share one Go implementation (api-key dispatches as + // `access key`); both mutate admission keys through /api/keys and read the + // external endpoint surface. + {Name: "access", Usage: "ocx access ...", Summary: "Manage OpenCodex admission API keys and inspect external endpoints.", Owner: GoOwned}, + {Name: "api-key", Usage: "ocx api-key ...", Summary: "Alias of ocx access key.", Owner: GoOwned}, {Name: "export", Usage: "ocx export --client ", Summary: "Export client configuration.", Owner: TypeScriptOwned}, {Name: "integration", Usage: "ocx integration client ", Summary: "Manage integrations.", Owner: TypeScriptOwned}, {Name: "grok", Usage: "ocx grok ", Summary: "Manage Grok Build.", Owner: TypeScriptOwned}, - {Name: "system", Usage: "ocx system ", Summary: "Manage runtime settings.", Owner: TypeScriptOwned}, + // The full system family is Go-owned except codex-cli-update, which stays a + // TypeScript-owned subcommand seam: it is a read-only local Codex install + // inspection (no management API, no proxy), unlike every other system verb. + {Name: "system", Usage: "ocx system ...", Summary: "Manage headless runtime settings, startup, sync, diagnostics, OpenCodex updates, and read-only Codex CLI inspection.", Owner: GoOwned}, // The full config family is Go-owned: reads project through the schema // normalizer and writes share the SQLite generation transaction. {Name: "config", Usage: "ocx config ", Summary: "Manage configuration.", Owner: GoOwned}, @@ -152,6 +160,13 @@ func OwnershipFor(args []string) (Ownership, bool) { } return TypeScriptOwned, true } + // system owns every subcommand except codex-cli-update, which is a read-only + // local Codex install inspection outside the management plane (no proxy, no + // API); it stays with the TypeScript owner until that inspection carries its + // own oracle. + if command.Name == "system" && len(args) > 1 && args[1] == "codex-cli-update" { + return TypeScriptOwned, true + } // observe keeps its TypeScript owner per subcommand: `usage` shares the Go // usage implementation, everything else stays with the TS owner until each // subcommand carries its own oracle. @@ -285,6 +300,14 @@ func Run(args []string, deps Deps) int { } fmt.Fprintf(deps.Stderr, "Unimplemented Go-owned command: %s\n", args[0]) return ExitFailure + case "debug": + return runDebug(args[1:], deps) + case "access": + return runAccess(args[1:], deps) + case "api-key": + return runApiKey(args[1:], deps) + case "system": + return runSystem(args[1:], deps) default: // The ownership registry above and this switch must be reconciled by // TestOwnershipMapMatchesDispatch; this is defensive for future edits. @@ -340,6 +363,14 @@ func printSubcommandHelp(name string, deps Deps) int { fmt.Fprintf(deps.Stdout, "Usage: %s\n\n%s\n", command.Usage, command.Summary) } } + case "debug": + fmt.Fprint(deps.Stdout, debugFamilyHelp) + case "access": + fmt.Fprint(deps.Stdout, accessFamilyHelp) + case "api-key": + fmt.Fprint(deps.Stdout, apiKeyFamilyHelp) + case "system": + fmt.Fprint(deps.Stdout, systemFamilyHelp) case "config": fmt.Fprint(deps.Stdout, configHelp) default: diff --git a/go/internal/ocxcli/debug_command.go b/go/internal/ocxcli/debug_command.go new file mode 100644 index 0000000000..f04401ab20 --- /dev/null +++ b/go/internal/ocxcli/debug_command.go @@ -0,0 +1,434 @@ +// ocx debug — the runtime debug-flag family (provider/usage/injection/claude +// with on|off|status|reset|logs [-f]). This file ports the TypeScript owner +// (src/cli/debug.ts) exactly: it reads and writes the running proxy's +// /api/debug settings and buffered log endpoints with the TS failure wording +// (no "Error:" prefix, exit 1) and the env-default help block for a stopped +// proxy. +package ocxcli + +import ( + "fmt" + "io" + "net/http" + "os" + "strings" + "time" + + "github.com/lidge-jun/opencodex/go/internal/jsonwire" +) + +// debugFlags mirrors the scopes of DEBUG_ENV plus the legacy provider env in +// src/lib/debug-settings.ts. overallKey names the DebugSettingsView member +// that reports the scope's effective state. +type debugFlag struct { + name string // scope key used on the wire and in messages + title string // "Provider", "Usage", "Injection", "Claude inbound" + envName string // DEBUG_ENV. + legacyEnv string // legacy provider env (OCX_DEBUG_FRAMES) + overallKey string + hasLogs bool +} + +var debugFlags = map[string]debugFlag{ + "provider": {name: "provider", title: "Provider", envName: "OCX_DEBUG", legacyEnv: "OCX_DEBUG_FRAMES", overallKey: "enabled", hasLogs: true}, + "usage": {name: "usage", title: "Usage", envName: "OPENCODEX_USAGE_DEBUG", overallKey: "usage", hasLogs: true}, + "injection": {name: "injection", title: "Injection", envName: "OCX_INJECTION_DEBUG", overallKey: "injection"}, + "claude": {name: "claude", title: "Claude inbound", envName: "OCX_CLAUDE_DEBUG", overallKey: "claude"}, +} + +// runDebug implements `ocx debug`. argv carries only this command's arguments; +// Run already handled --help/-h/help tokens at dispatch time. +func runDebug(args []string, deps Deps) int { + sub := "" + if len(args) > 0 { + sub = strings.ToLower(strings.TrimSpace(args[0])) + } + if flag, ok := debugFlags[sub]; ok { + return debugHandleScope(flag, args[1:], deps) + } + if sub == "" || sub == "help" || sub == "--help" || sub == "-h" { + debugPrintTopLevelHelp(deps, !liveProxyAvailable(deps)) + return ExitOK + } + debugPrintTopLevelHelp(deps, false) + return ExitFailure +} + +// liveProxyAvailable mirrors findLiveProxy() != null in debug.ts (used only to +// decide whether the env-default block precedes the top-level help). +func liveProxyAvailable(deps Deps) bool { + deps = defaults(deps) + _, found := liveProxyEndpoint(deps) + return found +} + +// debugPrintTopLevelHelp mirrors printTopLevelHelp plus the stopped-proxy +// env-default block of handleDebugCommand. +func debugPrintTopLevelHelp(deps Deps, envDefaults bool) { + if envDefaults { + fmt.Fprintln(deps.Stdout, "Proxy is not running — env defaults for the next start:") + fmt.Fprintf(deps.Stdout, " provider → OCX_DEBUG = %s\n", boolWord(envOn("OCX_DEBUG") || envOn("OCX_DEBUG_FRAMES"))) + fmt.Fprintf(deps.Stdout, " usage → %s = %s\n", debugFlags["usage"].envName, boolWord(envOn(debugFlags["usage"].envName))) + fmt.Fprintf(deps.Stdout, " injection→ %s = %s\n", debugFlags["injection"].envName, boolWord(envOn(debugFlags["injection"].envName))) + fmt.Fprintf(deps.Stdout, " claude → %s = %s\n", debugFlags["claude"].envName, boolWord(envOn(debugFlags["claude"].envName))) + fmt.Fprintln(deps.Stdout) + } + fmt.Fprintln(deps.Stdout, "Debug commands (proxy must be running):") + fmt.Fprintln(deps.Stdout) + fmt.Fprintln(deps.Stdout, " ocx debug provider on|off|status|reset|logs [-f]") + fmt.Fprintln(deps.Stdout, " ocx debug usage on|off|status|reset|logs [-f]") + fmt.Fprintln(deps.Stdout, " ocx debug injection on|off|status|reset") + fmt.Fprintln(deps.Stdout, " ocx debug claude on|off|status|reset") + fmt.Fprintln(deps.Stdout) + fmt.Fprintln(deps.Stdout, "Env defaults on start:") + fmt.Fprintf(deps.Stdout, " provider → %s=1 (legacy %s still works)\n", debugFlags["provider"].envName, debugFlags["provider"].legacyEnv) + fmt.Fprintf(deps.Stdout, " usage → %s=1\n", debugFlags["usage"].envName) + fmt.Fprintf(deps.Stdout, " injection→ %s=1\n", debugFlags["injection"].envName) + fmt.Fprintf(deps.Stdout, " claude → %s=1\n", debugFlags["claude"].envName) +} + +func envOn(name string) bool { return os.Getenv(name) == "1" } +func boolWord(value bool) string { + if value { + return "on" + } + return "off" +} + +func debugHandleScope(flag debugFlag, actionArgv []string, deps Deps) int { + scope := flag.name + action := "status" + if len(actionArgv) > 0 { + action = strings.ToLower(strings.TrimSpace(actionArgv[0])) + } + switch action { + case "on", "off": + enabled := action == "on" + view, code := debugPutSettings(deps, fmt.Sprintf(`{"%s":%t}`, debugWireKey(scope), enabled)) + if code != ExitOK { + return code + } + debugPrintScopeStatus(scope, view, deps) + if enabled { + fmt.Fprintf(deps.Stdout, "\n%s debug is now enabled.\n", scope) + } else { + fmt.Fprintf(deps.Stdout, "\n%s debug is now disabled.\n", scope) + } + return ExitOK + case "status": + view, code := debugGetSettings(deps) + if code != ExitOK { + return code + } + debugPrintScopeStatus(scope, view, deps) + return ExitOK + case "reset": + resetKey := scope + if scope == "provider" { + resetKey = "provider" + } + view, code := debugPutSettings(deps, fmt.Sprintf(`{"reset":%s}`, quoteJSONString(resetKey))) + if code != ExitOK { + return code + } + debugPrintScopeStatus(scope, view, deps) + fmt.Fprintf(deps.Stdout, "\nRuntime override cleared for %s; effective value follows env again.\n", scope) + return ExitOK + case "logs": + if scope == "injection" || scope == "claude" { + if scope == "claude" { + fmt.Fprintln(deps.Stderr, "Use: ocx observe claude-inbound") + } else { + fmt.Fprintln(deps.Stderr, "Injection debug has no buffered log stream; use: ocx observe injection") + } + return ExitFailure + } + follow := false + for _, arg := range actionArgv[1:] { + if arg == "-f" || arg == "--follow" { + follow = true + } + } + if scope == "provider" { + return debugPrintProviderLogs(follow, deps) + } + return debugPrintUsageLogs(follow, deps) + default: + if scope == "injection" || scope == "claude" { + fmt.Fprintf(deps.Stderr, "Usage: ocx debug %s on|off|status|reset\n", scope) + } else { + fmt.Fprintf(deps.Stderr, "Usage: ocx debug %s on|off|status|reset|logs [-f]\n", scope) + } + return ExitFailure + } +} + +// debugWireKey mirrors the PUT body key for each scope: only provider maps to +// the legacy `debug` flag. +func debugWireKey(scope string) string { + if scope == "provider" { + return "debug" + } + return scope +} + +// debugLive mirrors requireLiveProxy: discover the live proxy or fail with the +// exact TS message and exit code 1. +func debugLive(deps Deps) (RuntimeState, bool) { + deps = defaults(deps) + if state, ok := liveProxyEndpoint(deps); ok { + return state, true + } + fmt.Fprintln(deps.Stderr, "Proxy is not running. Start it with: ocx start") + return RuntimeState{}, false +} + +// debugRequest performs one raw authenticated management fetch. doErr is the +// transport/read error, or nil when the server answered. +func debugRequest(deps Deps, state RuntimeState, method, path string, body string) (raw []byte, status int, doErr error) { + request, err := http.NewRequest(method, baseURL(state)+path, nil) + if err != nil { + return nil, 0, err + } + request.Header.Set("Content-Type", "application/json") + if body != "" { + request.Body = io.NopCloser(strings.NewReader(body)) + request.ContentLength = int64(len(body)) + } + if token := configuredUsageAdminToken(); token != "" { + request.Header.Set("X-OpenCodex-API-Key", token) + } + response, doErr := deps.HTTPClient.Do(request) + if doErr != nil { + return nil, 0, doErr + } + defer response.Body.Close() + raw, readErr := io.ReadAll(io.LimitReader(response.Body, 8*1024*1024)) + if readErr != nil { + return nil, 0, readErr + } + return raw, response.StatusCode, nil +} + +// debugGetSettings mirrors fetchDebugSettings: GET /api/debug, "Failed to read +// debug settings ()" on non-OK, and the unreachable wording on a +// transport or JSON failure. +func debugGetSettings(deps Deps) (*jsonwire.Value, int) { + state, ok := debugLive(deps) + if !ok { + return nil, ExitFailure + } + raw, status, err := debugRequest(deps, state, "GET", "/api/debug", "") + if err != nil { + fmt.Fprintf(deps.Stderr, "Proxy is running but /api/debug is unreachable: %s\n", err) + return nil, ExitFailure + } + if status < 200 || status >= 300 { + fmt.Fprintf(deps.Stderr, "Failed to read debug settings (%d)\n", status) + return nil, ExitFailure + } + view, parseErr := jsonwire.Parse(raw) + if parseErr != nil { + fmt.Fprintf(deps.Stderr, "Proxy is running but /api/debug is unreachable: %s\n", parseErr) + return nil, ExitFailure + } + return view, ExitOK +} + +// debugPutSettings mirrors putDebugSettings: PUT /api/debug. The non-OK text +// appends the first 200 characters of the body when one exists. +func debugPutSettings(deps Deps, body string) (*jsonwire.Value, int) { + state, ok := debugLive(deps) + if !ok { + return nil, ExitFailure + } + raw, status, err := debugRequest(deps, state, "PUT", "/api/debug", body) + if err != nil { + // TS putDebugSettings has no catch: a transport failure rejects the + // command promise. The differential never exercises this path (the + // fixture server is reachable), so fail with code 1 and no invented + // message. + return nil, ExitFailure + } + if status < 200 || status >= 300 { + suffix := "" + text := string(raw) + if text != "" { + suffix = ": " + firstRunes(text, 200) + } + fmt.Fprintf(deps.Stderr, "Failed to update debug settings (%d)%s\n", status, suffix) + return nil, ExitFailure + } + view, parseErr := jsonwire.Parse(raw) + if parseErr != nil { + return nil, ExitFailure + } + return view, ExitOK +} + +func firstRunes(value string, limit int) string { + runes := []rune(value) + if len(runes) <= limit { + return value + } + return string(runes[:limit]) +} + +// debugPrintScopeStatus mirrors printScopeStatus in src/cli/debug.ts. +func debugPrintScopeStatus(scope string, view *jsonwire.Value, deps Deps) { + flag := debugFlags[scope] + fmt.Fprintf(deps.Stdout, "%s debug: %s\n", flag.title, boolUpper(debugBoolField(view, flag.overallKey))) + // env and runtimeOverride use the settings member key (provider → `debug`), + // not the CLI scope name. + key := debugWireKey(scope) + envValue := debugNestedBool(view, "env", key) + fmt.Fprintf(deps.Stdout, " env=%s, runtime=%s\n", boolWord(envValue), debugRuntimeText(view, key)) + switch scope { + case "provider": + fmt.Fprintln(deps.Stdout, " Tail: ocx debug provider logs [-f]") + case "usage": + fmt.Fprintln(deps.Stdout, " Tail: ocx debug usage logs [-f] (via running proxy API)") + case "injection": + fmt.Fprintln(deps.Stdout, " Lines appear on the proxy console when multi-agent guidance is injected.") + case "claude": + fmt.Fprintln(deps.Stdout, " View: ocx observe claude-inbound") + } +} + +func boolUpper(value bool) string { + if value { + return "ON" + } + return "off" +} + +func debugBoolField(view *jsonwire.Value, key string) bool { + if view == nil { + return false + } + field := view.Find(key) + return field != nil && field.Kind() == jsonwire.Bool && field.Bool() +} + +func debugNestedBool(view *jsonwire.Value, section, key string) bool { + if view == nil { + return false + } + container := view.Find(section) + if container == nil || container.Kind() != jsonwire.Object { + return false + } + field := container.Find(key) + return field != nil && field.Kind() == jsonwire.Bool && field.Bool() +} + +// debugRuntimeText mirrors the runtimeOverride tri-state: an absent member is +// "env/default", a boolean is on/off. +func debugRuntimeText(view *jsonwire.Value, key string) string { + if view == nil { + return "env/default" + } + container := view.Find("runtimeOverride") + if container == nil || container.Kind() != jsonwire.Object { + return "env/default" + } + field := container.Find(key) + if field == nil { + return "env/default" + } + if field.Kind() != jsonwire.Bool { + return "env/default" + } + return boolWord(field.Bool()) +} + +// ───────────────────────────────────────────────────────────────────────────── +// log-stream rendering — port of printProviderLogs/printUsageLogs. + +func debugPrintProviderLogs(follow bool, deps Deps) int { + state, ok := debugLive(deps) + if !ok { + return ExitFailure + } + after, code := debugPrintLogBatch(deps, state, "/api/debug/logs", 0) + if code != ExitOK { + return code + } + for follow { + time.Sleep(time.Second) + newAfter, _ := debugPrintLogBatch(deps, state, "/api/debug/logs", after) + if newAfter > after { + after = newAfter + } + } + return ExitOK +} + +func debugPrintUsageLogs(follow bool, deps Deps) int { + state, ok := debugLive(deps) + if !ok { + return ExitFailure + } + after, code := debugPrintLogBatch(deps, state, "/api/debug/usage-logs", 0) + if code != ExitOK { + return code + } + for follow { + time.Sleep(time.Second) + newAfter, _ := debugPrintLogBatch(deps, state, "/api/debug/usage-logs", after) + if newAfter > after { + after = newAfter + } + } + return ExitOK +} + +// debugPrintLogBatch fetches one batch and prints each entry line, returning +// the last seq for the follow loop. The first usage poll prints the empty hint +// when no entries exist; follow-loop polls swallow failures like the TS loop. +func debugPrintLogBatch(deps Deps, state RuntimeState, path string, after int) (int, int) { + query := "?limit=500" + if after > 0 { + query = fmt.Sprintf("?after=%d&limit=500", after) + } + raw, status, err := debugRequest(deps, state, "GET", path+query, "") + if err != nil { + if after == 0 { + fmt.Fprintf(deps.Stderr, "Failed to read debug logs: %s\n", err) + } + return after, ExitFailure + } + if status < 200 || status >= 300 { + if after == 0 { + fmt.Fprintf(deps.Stderr, "Failed to read debug logs (%d)\n", status) + } + return after, ExitFailure + } + entries, parseErr := jsonwire.Parse(raw) + if parseErr != nil { + if after == 0 { + fmt.Fprintf(deps.Stderr, "Failed to read debug logs: %s\n", parseErr) + } + return after, ExitFailure + } + last := after + count := 0 + if entries != nil && entries.Kind() == jsonwire.Array { + for _, element := range entries.Elements() { + if element == nil || element.Kind() != jsonwire.Object { + continue + } + fmt.Fprintln(deps.Stdout, fieldString(element, "line")) + count++ + if seqField := element.Find("seq"); seqField != nil && seqField.Kind() == jsonwire.Number { + if number, ok := parseJSONNumber(seqField.NumberRaw()); ok { + last = int(number) + } + } + } + } + if count == 0 && after == 0 && strings.HasSuffix(path, "usage-logs") { + fmt.Fprintln(deps.Stdout, "(empty — enable with: ocx debug usage on)") + } + return last, ExitOK +} diff --git a/go/internal/ocxcli/help.go b/go/internal/ocxcli/help.go index 8955144f1d..740564e6de 100644 --- a/go/internal/ocxcli/help.go +++ b/go/internal/ocxcli/help.go @@ -3,3 +3,13 @@ package ocxcli const fullUsage = "opencodex (ocx) — Universal provider proxy for Codex\n\nUsage:\n ocx setup Interactive setup (alias: init)\n ocx start [--port ] Start the proxy server (auto-syncs models to Codex)\n ocx stop Stop the proxy AND restore native Codex (plain codex works again)\n ocx restore Restore native Codex without stopping (alias: eject)\n ocx restore back Re-point codex at the running proxy (undo restore)\n ocx recover-history --legacy-openai --yes\n Force all user-message opencodex rows to OpenAI (legacy recovery)\n ocx uninstall Remove service/shim/config and restore native Codex (alias: remove)\n ocx service [sub] Run as a background service (default: install/update/start)\n ocx codex-shim Auto-start proxy when `codex` launches (install|status|uninstall|remove)\n ocx tray Windows status tray (install|start|stop|status|uninstall)\n ocx ensure Ensure the proxy is running and Codex config/cache are current\n ocx connect Connect this machine to a remote OpenCodex hub (credential via stdin)\n ocx disconnect Restore local state and clear the hub connection\n ocx sync [--restart-codex] Fetch models from providers and inject into Codex config\n ocx sync-cache [--restart-codex]\n Refresh Codex's model cache from the active catalog\n ocx status Check proxy server status\n ocx doctor Diagnose environment/network issues (WSL, proxy, ChatGPT reachability)\n ocx doctor --reclaim-response-temps\n Reclaim abandoned response-state temp files (works without a running proxy)\n ocx doctor --recover-zero-byte-coordinator --yes\n Back up a proven zero-byte Codex coordinator after stopping the proxy\n ocx debug provider/usage/injection/claude on|off|status|reset\n ocx login OAuth or API-key provider login\n ocx logout Remove a stored OAuth login\n ocx gui [pair --origin [--json]]\n Open the dashboard or create a single-use remote pairing grant\n ocx update [--tag ] Update opencodex (keeps preview installs on @preview)\n ocx restart Stop and restart the proxy\n ocx v2 multi_agent_v2 surface (status|on|off|mode|keep-native-v1|threads|mode-hint)\n ocx health [--json] Check proxy health (exit 0=healthy, 1=not)\n ocx capabilities [--json] List declared capabilities and the API routes they drive\n ocx ready [--json] [--wait [--timeout ]] Check post-sync readiness (exit 0 only when ready)\n ocx provider Providers, connectivity, quota, and selected models\n ocx account Accounts, login/reauth, key pools, and quota controls\n ocx models Live/custom models, visibility, context, and shadow calls\n ocx alias Short names for providers and models (list, set, rm, defaults)\n ocx combo Combo routing strategies and failover\n ocx agent Subagents, injection, effort caps, and sidecars\n ocx observe Logs, usage, storage, memory, and debug data\n ocx inspect Effective config, catalog, analytics, pacing, client-config\n ocx route Routing features (combo, policy)\n ocx logs [filters] Alias of ocx observe logs\n ocx usage [--range ] [--provider ] [--model ]\n Token and estimated-cost report (alias of ocx observe usage)\n ocx storage Storage report, cleanup, trash, and the cleanup policy\n ocx memory [--json] Alias of ocx observe memory\n ocx api-key Alias of ocx access key\n ocx access External API keys and endpoint information\n ocx export --client Print a client config wired to the running proxy (12 clients)\n ocx integration client Enable, disable, inspect or roll back a client integration\n ocx grok Grok Build model selection and apply\n ocx system Runtime settings, startup, sync, OpenCodex updates, and Codex CLI inspection\n ocx config Validated configuration show/get/set/import/export\n ocx lab Read-only Compatibility Lab projection inspection\n ocx claude [args...] Launch Claude Code wired to the proxy (model discovery on)\n ocx claude desktop [sub] Manage and apply Claude Desktop's four-family profile\n ocx opencode [args...] Launch opencode wired to the proxy (runtime provider config)\n ocx mcode [args...] Launch MiniMax Code through its managed provider\n ocx mmx text [args] Launch MiniMax CLI text through the proxy\n ocx zcode [sub] Connect ZCode to the proxy (managed provider)\n ocx help [command] Show help\n ocx --version | -v Print version\n\nExamples:\n ocx init Set up provider and inject into Codex\n ocx start Start on default port (10100)\n ocx start --port 8080 Start on custom port\n ocx help service Show service command help\n ocx sync Sync available models to Codex\n" + +// Family help blocks for the Go-owned management families, mirroring +// src/cli/registry.ts entries byte-for-byte (printSubcommandUsage prints +// "Usage: \n\n" plus "\n" + details when present). +const ( + debugFamilyHelp = "Usage: ocx debug \n\nShow or toggle runtime provider, usage, injection, and Claude debug capture.\n\nProvider: ocx debug provider on | off | status | reset | logs [-f]\nUsage JSONL: ocx debug usage on | off | status | reset | logs [-f]\nEnv default: OCX_DEBUG=1 (legacy OCX_DEBUG_FRAMES still works)\n" + accessFamilyHelp = "Usage: ocx access ...\n\nManage OpenCodex admission API keys and inspect external endpoints.\n\nKey rotation start uses POST /api/keys/rotate and returns the replacement secret once.\nCommit uses POST /api/keys/rotate/commit; abort uses DELETE /api/keys/rotate with the returned rotation id.\n" + apiKeyFamilyHelp = "Usage: ocx api-key ...\n\nAlias of ocx access key.\n" + systemFamilyHelp = "Usage: ocx system ...\n\nManage headless runtime settings, startup, sync, diagnostics, OpenCodex updates, and read-only Codex CLI inspection.\n\nsystem update manages OpenCodex itself.\nocx system codex-cli-update check [--json]\nThe Codex CLI inspection command makes no package-registry request, does not execute Codex or npm, install or repair software, control a process, or write configuration or cache state.\n" +) diff --git a/go/internal/ocxcli/mgmt_command.go b/go/internal/ocxcli/mgmt_command.go new file mode 100644 index 0000000000..5ec51b6959 --- /dev/null +++ b/go/internal/ocxcli/mgmt_command.go @@ -0,0 +1,176 @@ +// Shared management-plane client for the Go-owned headless command families +// (debug, access/api-key, system). This file ports the exact wire and error +// semantics of src/cli/runtime-api.ts so a flipped command keeps the same +// requests, messages, and exit codes as the TypeScript owner: +// +// - live-proxy discovery reuses liveProxyEndpoint (runtime-port first, then +// the configured listen port), so writes never bypass the management API +// into config files unless the TS owner did (none of these families do). +// - A request failure surfaces as a typed failure: apiFailure mirrors +// RuntimeApiError (message + HTTP status -> exit 4 on 404, 5 on 409, +// otherwise 1) and cliUsageFailure mirrors CliUsageError (message plus an +// optional USAGE block on stderr, exit 2). +// - Bodies are re-printed exactly like console.log(JSON.stringify(value, +// null, 2)): jsonwire preserves document key order and V8 number/string +// rules, with a trailing newline from the console.log. +// +// The differential harness diffs every Go-owned row against the real TS CLI +// for the same argv and fixture server, so a drift here fails loudly. +package ocxcli + +import ( + "errors" + "fmt" + "io" + "net/http" + "strings" + + "github.com/lidge-jun/opencodex/go/internal/jsonwire" +) + +// Exit codes from the runCliAction taxonomy (src/cli/runtime-api.ts), shared by +// every management-plane family. +const ( + mgmtExitUsage = 2 // CliUsageError + mgmtExitMissing = 4 // RuntimeApiError status 404 + mgmtExitConflict = 5 // RuntimeApiError status 409 +) + +// cliUsageFailure mirrors CliUsageError: a message and an optional USAGE block +// printed to stderr before exit 2. +type cliUsageFailure struct { + message string + usage string +} + +func (e cliUsageFailure) Error() string { return e.message } + +// apiFailure mirrors RuntimeApiError: a message and the HTTP status that +// selects the exit code. +type apiFailure struct { + message string + status int +} + +func (e apiFailure) Error() string { return e.message } + +// reportManagementFailure prints the operator-facing line and USAGE block the +// way runCliAction does, and returns the matching exit code. +func reportManagementFailure(deps Deps, err error) int { + fmt.Fprintln(deps.Stderr, "Error: "+err.Error()) + var usageErr cliUsageFailure + if errors.As(err, &usageErr) { + if usageErr.usage != "" { + fmt.Fprint(deps.Stderr, usageErr.usage) + if !strings.HasSuffix(usageErr.usage, "\n") { + fmt.Fprintln(deps.Stderr) + } + } + return mgmtExitUsage + } + var api apiFailure + if errors.As(err, &api) { + switch api.status { + case 404: + return mgmtExitMissing + case 409: + return mgmtExitConflict + default: + return 1 + } + } + return 1 +} + +// managementCliUsage builds the CliUsageError-equivalent failure. +func managementCliUsage(message, usage string) error { + return cliUsageFailure{message: message, usage: usage} +} + +// managementAPIError builds the RuntimeApiError-equivalent failure from the +// response body and status, composing responseMessage exactly like runtime-api. +func managementAPIError(body *jsonwire.Value, rawText string, status int) error { + return apiFailure{message: managementResponseMessage(body, rawText, status), status: status} +} + +// managementResponseMessage mirrors responseMessage in runtime-api.ts: compose +// the operator-facing message from a management error body. The primary string +// comes from error|message|detail; reason and hint append as separate lines. +func managementResponseMessage(body *jsonwire.Value, rawText string, status int) string { + if rawText != "" { + trimmed := strings.TrimSpace(rawText) + if trimmed != "" { + return truncateRunes(trimmed, 400) + } + } + if body == nil || body.Kind() != jsonwire.Object { + return fmt.Sprintf("Management request failed (%d)", status) + } + primary := "" + for _, key := range []string{"error", "message", "detail"} { + if field := body.Find(key); field != nil && field.Kind() == jsonwire.String { + if trimmed := strings.TrimSpace(field.String()); trimmed != "" { + primary = trimmed + break + } + } + } + if primary == "" { + primary = fmt.Sprintf("Management request failed (%d)", status) + } + parts := []string{primary} + for _, key := range []string{"reason", "hint"} { + if field := body.Find(key); field != nil && field.Kind() == jsonwire.String { + trimmed := strings.TrimSpace(field.String()) + if trimmed != "" && trimmed != primary { + parts = append(parts, key+": "+trimmed) + } + } + } + return truncateRunes(strings.Join(parts, "\n"), 1200) +} + +// managementRequest mirrors runtimeRequest: resolve the live proxy, send one +// authenticated management request, and classify the response. A nil body with +// a non-empty rawText means the response was not valid JSON (runtimeRequest +// keeps the text verbatim in that case). Non-2xx responses surface as an +// apiFailure whose message is composed from the body. +func managementRequest(deps Deps, method, path, body string) (*jsonwire.Value, string, int, error) { + deps = defaults(deps) + state, found := liveProxyEndpoint(deps) + if !found { + return nil, "", 503, apiFailure{message: "Proxy is not running. Start it with: ocx start", status: 503} + } + request, requestErr := http.NewRequest(method, baseURL(state)+path, nil) + if requestErr != nil { + return nil, "", 503, apiFailure{message: fmt.Sprintf("Management API is unreachable: %s", requestErr), status: 503} + } + request.Header.Set("Content-Type", "application/json") + if body != "" { + request.Body = io.NopCloser(strings.NewReader(body)) + request.ContentLength = int64(len(body)) + } + if token := configuredUsageAdminToken(); token != "" { + request.Header.Set("X-OpenCodex-API-Key", token) + } + response, doErr := deps.HTTPClient.Do(request) + if doErr != nil { + return nil, "", 503, apiFailure{message: fmt.Sprintf("Management API is unreachable: %s", doErr), status: 503} + } + defer response.Body.Close() + raw, readErr := io.ReadAll(io.LimitReader(response.Body, 8*1024*1024)) + if readErr != nil { + return nil, "", 503, apiFailure{message: fmt.Sprintf("Management API is unreachable: %s", readErr), status: 503} + } + value, parseErr := jsonwire.Parse(raw) + if parseErr != nil { + if response.StatusCode < 200 || response.StatusCode >= 300 { + return nil, string(raw), response.StatusCode, managementAPIError(nil, string(raw), response.StatusCode) + } + return nil, string(raw), response.StatusCode, nil + } + if response.StatusCode < 200 || response.StatusCode >= 300 { + return value, "", response.StatusCode, managementAPIError(value, "", response.StatusCode) + } + return value, "", response.StatusCode, nil +} diff --git a/go/internal/ocxcli/mgmt_command_test.go b/go/internal/ocxcli/mgmt_command_test.go new file mode 100644 index 0000000000..3d1ea32cbc --- /dev/null +++ b/go/internal/ocxcli/mgmt_command_test.go @@ -0,0 +1,348 @@ +package ocxcli + +import ( + "bytes" + "fmt" + "io" + "net/http" + "net/http/httptest" + "strings" + "testing" +) + +// Go-owned management families (debug/access/api-key/system) talk to the +// proxy's management plane through the same fixture-shaped servers as the +// differential oracle in tests/go-cli-parity.test.ts, so these tests pin the +// exact wire requests, human renderings, and exit taxonomy in Go without +// spawning the TS CLI. Golden strings below were captured from the TS CLI run +// against the identical fixture payloads. + +const mgmtFixtureToken = "ocx_admin_go-unittest-token-abcdefghijklmnopqrstuvwxyz0123" + +const mgmtKeysEnvelope = `{ + "keys": [ + {"id": "key-1", "name": "default", "prefix": "ocx_data_ab12", "createdAt": "2026-08-01T00:00:00.000Z", "usage": {"requests7d": 1447, "totalRequests": 9033}}, + {"id": "key-2", "name": "deploy", "prefix": "ocx_data_cd34", "createdAt": "2026-08-02T00:00:00.000Z", "usage": {"ambiguous": true}}, + {"id": "key-3", "name": "unused", "prefix": "ocx_data_ef56", "createdAt": "2026-08-03T00:00:00.000Z", "usage": {"requests7d": 0, "totalRequests": 0, "lastUsedAt": "2026-09-01T10:00:00.000Z"}} + ], + "attributionSince": "2026-08-01T00:00:00.000Z", + "authMatrix": {"admin": ["GET", "POST"]}, + "baseUrl": "http://127.0.0.1:1/v1", + "endpoint": "http://127.0.0.1:1/v1/responses" +}` + +const mgmtSettingsPayload = `{"codexAutoStart":false,"streamMode":"auto","codexDesktopAuthless":false,"managementPort":10100,"desired":{"enabled":true}}` + +// mgmtFixtureServer serves the /healthz discovery probe (service/pid gate like +// usageFixtureServer) and routes every other request to the handler. +func mgmtFixtureServer(t *testing.T, handler http.HandlerFunc) (*httptest.Server, func() RuntimeState) { + t.Helper() + server := usageFixtureServer(t, handler) + state := func() RuntimeState { + port := atoi(t, strings.TrimPrefix(server.URL, "http://127.0.0.1:")) + return RuntimeState{PID: 1, Port: port, Hostname: "127.0.0.1", AttestationSecret: "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG"} + } + return server, state +} + +func mgmtDeps(t *testing.T, server *httptest.Server, state func() RuntimeState, stdout, stderr *bytes.Buffer) Deps { + t.Helper() + t.Setenv("OPENCODEX_ADMIN_AUTH_TOKEN", mgmtFixtureToken) + return Deps{Version: "test", Stdout: stdout, Stderr: stderr, ReadRuntime: func() (RuntimeState, error) { return state(), nil }, HTTPClient: server.Client()} +} + +func readRequestBody(t *testing.T, r *http.Request) string { + t.Helper() + raw, err := io.ReadAll(r.Body) + if err != nil { + t.Fatal(err) + } + return string(raw) +} + +func TestAccessKeyListHumanMatchesTSGolden(t *testing.T) { + server, state := mgmtFixtureServer(t, func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/api/keys" || r.Method != http.MethodGet { + t.Fatalf("unexpected request %s %s", r.Method, r.URL.Path) + } + fmt.Fprint(w, mgmtKeysEnvelope) + }) + var stdout, stderr bytes.Buffer + code := runAccess([]string{"key"}, mgmtDeps(t, server, state, &stdout, &stderr)) + if code != ExitOK { + t.Fatalf("exit = %d, stderr:\n%s", code, stderr.String()) + } + want := "ID NAME PREFIX REQ 7D TOTAL LAST USED\n" + + "key-1 default ocx_data_ab12 1,447 9,033 never\n" + + "key-2 deploy ocx_data_cd34 ambiguous\n" + + "key-3 unused ocx_data_ef56 0 0 2026-09-01T10:00:00.000Z\n" + + "\n" + + "attribution since 2026-08-01T00:00:00.000Z\n" + + "ambiguous: two configured keys share an id, so per-key totals do not exist\n" + if stdout.String() != want { + t.Fatalf("key list stdout mismatch:\n--- want ---\n%s\n--- got ---\n%s", want, stdout.String()) + } +} + +func TestApiKeyAliasRunsAccessKeyList(t *testing.T) { + server, state := mgmtFixtureServer(t, func(w http.ResponseWriter, r *http.Request) { + fmt.Fprint(w, mgmtKeysEnvelope) + }) + var stdout, stderr bytes.Buffer + code := runApiKey([]string{"list"}, mgmtDeps(t, server, state, &stdout, &stderr)) + if code != ExitOK { + t.Fatalf("exit = %d, stderr:\n%s", code, stderr.String()) + } + if !strings.Contains(stdout.String(), "ID NAME PREFIX") || !strings.Contains(stdout.String(), "attribution since") { + t.Fatalf("api-key list did not delegate to access key rendering:\n%s", stdout.String()) + } +} + +func TestAccessKeyCreateSendsNameAndReprintsEnvelope(t *testing.T) { + var gotBody string + server, state := mgmtFixtureServer(t, func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + t.Fatalf("method = %s", r.Method) + } + gotBody = readRequestBody(t, r) + fmt.Fprint(w, `{"id":"key-new","name":"deploy","key":"ocx_data_newsecret","createdAt":"2026-09-05T00:00:00.000Z"}`) + }) + var stdout, stderr bytes.Buffer + code := runAccess([]string{"key", "create", "deploy"}, mgmtDeps(t, server, state, &stdout, &stderr)) + if code != ExitOK { + t.Fatalf("exit = %d, stderr:\n%s", code, stderr.String()) + } + if gotBody != `{"name":"deploy"}` { + t.Fatalf("create body = %s", gotBody) + } + want := "Created API key deploy (key-new).\nKey (shown once): ocx_data_newsecret\n" + if stdout.String() != want { + t.Fatalf("create stdout mismatch:\n--- want ---\n%s\n--- got ---\n%s", want, stdout.String()) + } +} + +func TestAccessRotationBodyAndAbortMethod(t *testing.T) { + var startBody, commitBody string + var aborted bool + server, state := mgmtFixtureServer(t, func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/api/keys/rotate": + if r.Method == http.MethodPost { + startBody = readRequestBody(t, r) + fmt.Fprint(w, `{"id":"key-1","rotationId":"rot-1","key":"ocx_data_rotsecret","createdAt":"2026-09-05T00:00:00.000Z"}`) + } else if r.Method == http.MethodDelete { + aborted = true + commitBody = readRequestBody(t, r) + fmt.Fprint(w, `{"ok":true}`) + } + case "/api/keys/rotate/commit": + commitBody = readRequestBody(t, r) + fmt.Fprint(w, `{"ok":true}`) + default: + t.Fatalf("unexpected request %s %s", r.Method, r.URL.Path) + } + }) + var stdout, stderr bytes.Buffer + deps := mgmtDeps(t, server, state, &stdout, &stderr) + if code := runAccess([]string{"key", "rotate", "key-1"}, deps); code != ExitOK { + t.Fatalf("rotate start exit = %d, stderr:\n%s", code, stderr.String()) + } + if startBody != `{"id":"key-1"}` { + t.Fatalf("rotate start body = %s", startBody) + } + stdout.Reset() + if code := runAccess([]string{"key", "rotate", "commit", "key-1", "rot-1"}, deps); code != ExitOK { + t.Fatalf("rotate commit exit = %d, stderr:\n%s", code, stderr.String()) + } + if commitBody != `{"id":"key-1","rotationId":"rot-1"}` { + t.Fatalf("rotate commit body = %s", commitBody) + } + stdout.Reset() + if code := runAccess([]string{"key", "rotate", "abort", "key-1", "rot-1"}, deps); code != ExitOK { + t.Fatalf("rotate abort exit = %d, stderr:\n%s", code, stderr.String()) + } + if !aborted { + t.Fatal("abort did not DELETE /api/keys/rotate") + } +} + +func TestDebugProviderStatusReadsEnvAndRuntimeOverride(t *testing.T) { + server, state := mgmtFixtureServer(t, func(w http.ResponseWriter, r *http.Request) { + fmt.Fprint(w, `{"enabled":false,"usage":false,"injection":false,"claude":false,"runtimeOverride":{},"env":{"debug":true,"usage":false,"injection":false,"claude":false}}`) + }) + var stdout, stderr bytes.Buffer + code := runDebug([]string{"provider", "status"}, mgmtDeps(t, server, state, &stdout, &stderr)) + if code != ExitOK { + t.Fatalf("exit = %d, stderr:\n%s", code, stderr.String()) + } + want := "Provider debug: off\n" + + " env=on, runtime=env/default\n" + + " Tail: ocx debug provider logs [-f]\n" + if stdout.String() != want { + t.Fatalf("status stdout mismatch:\n--- want ---\n%s\n--- got ---\n%s", want, stdout.String()) + } +} + +func TestDebugProviderToggleAndResetWireBodies(t *testing.T) { + var gotBody string + server, state := mgmtFixtureServer(t, func(w http.ResponseWriter, r *http.Request) { + gotBody = readRequestBody(t, r) + fmt.Fprint(w, `{"enabled":true,"usage":false,"injection":false,"claude":false,"runtimeOverride":{"debug":true},"env":{"debug":true,"usage":false,"injection":false,"claude":false}}`) + }) + var stdout, stderr bytes.Buffer + deps := mgmtDeps(t, server, state, &stdout, &stderr) + if code := runDebug([]string{"provider", "on"}, deps); code != ExitOK { + t.Fatalf("on exit = %d, stderr:\n%s", code, stderr.String()) + } + if gotBody != `{"debug":true}` { + t.Fatalf("toggle body = %s", gotBody) + } + want := "Provider debug: ON\n env=on, runtime=on\n Tail: ocx debug provider logs [-f]\n\nprovider debug is now enabled.\n" + if stdout.String() != want { + t.Fatalf("on stdout mismatch:\n--- want ---\n%s\n--- got ---\n%s", want, stdout.String()) + } + stdout.Reset() + if code := runDebug([]string{"provider", "reset"}, deps); code != ExitOK { + t.Fatalf("reset exit = %d, stderr:\n%s", code, stderr.String()) + } + if gotBody != `{"reset":"provider"}` { + t.Fatalf("reset body = %s", gotBody) + } +} + +func TestSystemSettingsHumanMatchesTSGolden(t *testing.T) { + server, state := mgmtFixtureServer(t, func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + t.Fatalf("method = %s", r.Method) + } + fmt.Fprint(w, mgmtSettingsPayload) + }) + var stdout, stderr bytes.Buffer + code := runSystem([]string{"settings"}, mgmtDeps(t, server, state, &stdout, &stderr)) + if code != ExitOK { + t.Fatalf("exit = %d, stderr:\n%s", code, stderr.String()) + } + want := "codexAutoStart: false\n" + + "streamMode: auto\n" + + "codexDesktopAuthless: false\n" + + "managementPort: 10100\n" + + "desired.enabled: true\n" + if stdout.String() != want { + t.Fatalf("settings stdout mismatch:\n--- want ---\n%s\n--- got ---\n%s", want, stdout.String()) + } +} + +func TestSystemCodexAppServerDefaultsToJSONReprint(t *testing.T) { + // No human summary lines exist for codex-app-server, so printData falls back + // to console.log(JSON.stringify(body, null, 2)) even without --json. + server, state := mgmtFixtureServer(t, func(w http.ResponseWriter, r *http.Request) { + fmt.Fprint(w, `{"reachable":true,"pid":4242}`) + }) + var stdout, stderr bytes.Buffer + code := runSystem([]string{"codex-app-server"}, mgmtDeps(t, server, state, &stdout, &stderr)) + if code != ExitOK { + t.Fatalf("exit = %d, stderr:\n%s", code, stderr.String()) + } + want := "{\n \"reachable\": true,\n \"pid\": 4242\n}\n" + if stdout.String() != want { + t.Fatalf("codex-app-server stdout mismatch:\n--- want ---\n%s\n--- got ---\n%s", want, stdout.String()) + } +} + +func TestManagementUsageFailuresExit2WithAccessUsage(t *testing.T) { + cases := []struct { + command string + args []string + message string + }{ + {command: "access", args: []string{"bogus"}, message: "unknown access command bogus"}, + {command: "access", args: []string{"key", "remove", "key-1"}, message: "remove requires --yes"}, + {command: "system", args: []string{"startup", "bogus"}, message: "startup action must be health, install-service, or install-shim"}, + {command: "system", args: []string{"update", "bogus"}, message: "unknown update action bogus"}, + {command: "system", args: []string{"codex-restart"}, message: "system codex-restart requires --yes"}, + } + for _, testCase := range cases { + var stdout, stderr bytes.Buffer + deps := Deps{Version: "test", Stdout: &stdout, Stderr: &stderr, ReadRuntime: func() (RuntimeState, error) { return RuntimeState{}, fmt.Errorf("unused") }} + var code int + switch testCase.command { + case "access": + code = runAccess(testCase.args, deps) + case "system": + code = runSystem(testCase.args, deps) + } + if code != mgmtExitUsage { + t.Fatalf("%s %v exit = %d, want %d (stderr: %s)", testCase.command, testCase.args, code, mgmtExitUsage, stderr.String()) + } + if !strings.Contains(stderr.String(), "Error: "+testCase.message) { + t.Fatalf("%s %v stderr missing %q:\n%s", testCase.command, testCase.args, "Error: "+testCase.message, stderr.String()) + } + } +} + +func TestManagementUnknownSystemAndDebugScope(t *testing.T) { + var stdout, stderr bytes.Buffer + deps := Deps{Version: "test", Stdout: &stdout, Stderr: &stderr, ReadRuntime: func() (RuntimeState, error) { return RuntimeState{}, fmt.Errorf("unused") }} + if code := runSystem([]string{"bogus"}, deps); code != mgmtExitUsage { + t.Fatalf("system bogus exit = %d, want %d", code, mgmtExitUsage) + } + if !strings.Contains(stderr.String(), "Error: unknown system command bogus") { + t.Fatalf("system bogus stderr:\n%s", stderr.String()) + } + stdout.Reset() + stderr.Reset() + // Debug with an unknown scope prints the family help on stdout and exits 1. + if code := runDebug([]string{"bogus"}, deps); code != ExitFailure { + t.Fatalf("debug bogus exit = %d, want 1", code) + } + if !strings.Contains(stdout.String(), "Debug commands (proxy must be running):") { + t.Fatalf("debug bogus stdout:\n%s", stdout.String()) + } +} + +func TestManagementAPIErrorComposesEnvelopeAndExitTaxonomy(t *testing.T) { + server, state := mgmtFixtureServer(t, func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusUnauthorized) + fmt.Fprint(w, `{"error":"opencodex admin token required","reason":"no matching credential","hint":"Set OPENCODEX_ADMIN_AUTH_TOKEN to the token written to OPENCODEX_HOME/admin-api-token"}`) + }) + var stdout, stderr bytes.Buffer + code := runSystem([]string{"sync"}, mgmtDeps(t, server, state, &stdout, &stderr)) + if code != ExitFailure { + t.Fatalf("401 exit = %d, want 1", code) + } + for _, want := range []string{"Error: opencodex admin token required", "reason: no matching credential", "hint: Set OPENCODEX_ADMIN_AUTH_TOKEN"} { + if !strings.Contains(stderr.String(), want) { + t.Fatalf("stderr missing %q:\n%s", want, stderr.String()) + } + } + + // 404 maps to exit 4, 409 to exit 5, exactly like the TS RuntimeApiError + // taxonomy used by the parity oracle's non-2xx rows. + codes := map[int]int{http.StatusNotFound: mgmtExitMissing, http.StatusConflict: mgmtExitConflict, http.StatusBadGateway: ExitFailure} + for status, wantCode := range codes { + server, state := mgmtFixtureServer(t, func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(status) + fmt.Fprint(w, `{"error":"denied by fixture","reason":"taxonomy probe"}`) + }) + var stdout, stderr bytes.Buffer + code := runSystem([]string{"sync"}, mgmtDeps(t, server, state, &stdout, &stderr)) + if code != wantCode { + t.Fatalf("status %d exit = %d, want %d (stderr: %s)", status, code, wantCode, stderr.String()) + } + } +} + +func TestManagementAdminTokenSentFromEnv(t *testing.T) { + var gotToken string + server, state := mgmtFixtureServer(t, func(w http.ResponseWriter, r *http.Request) { + gotToken = r.Header.Get("X-OpenCodex-API-Key") + fmt.Fprint(w, mgmtSettingsPayload) + }) + var stdout, stderr bytes.Buffer + if code := runSystem([]string{"settings"}, mgmtDeps(t, server, state, &stdout, &stderr)); code != ExitOK { + t.Fatalf("exit = %d, stderr:\n%s", code, stderr.String()) + } + if gotToken != mgmtFixtureToken { + t.Fatalf("token header = %q, want %q", gotToken, mgmtFixtureToken) + } +} diff --git a/go/internal/ocxcli/mgmt_print.go b/go/internal/ocxcli/mgmt_print.go new file mode 100644 index 0000000000..3628b811c4 --- /dev/null +++ b/go/internal/ocxcli/mgmt_print.go @@ -0,0 +1,248 @@ +package ocxcli + +import ( + "fmt" + "math" + "strconv" + "strings" + + "github.com/lidge-jun/opencodex/go/internal/jsonwire" +) + +// printManagementData mirrors printData: JSON.stringify(value, null, 2) when +// json output was requested or no human lines exist; otherwise the lines, each +// on its own stdout row via console.log semantics. +func printManagementData(deps Deps, body *jsonwire.Value, rawText string, wantsJSON bool, lines []string) { + if wantsJSON || lines == nil { + value := body + if value == nil { + if rawText == "" { + // Empty 2xx body: runtimeRequest keeps body = null, so + // JSON.stringify(null) prints null. + fmt.Fprintln(deps.Stdout, "null") + return + } + // Non-JSON body: JSON.stringify(text) is a quoted string. + quoted, err := jsonwire.EncodeString(rawText) + if err != nil { + fmt.Fprintln(deps.Stderr, err) + return + } + fmt.Fprintln(deps.Stdout, string(quoted)) + return + } + var out strings.Builder + if err := indentJSONWire(&out, value, 0); err != nil { + fmt.Fprintln(deps.Stderr, err) + return + } + fmt.Fprintln(deps.Stdout, out.String()) + return + } + for _, line := range lines { + fmt.Fprintln(deps.Stdout, line) + } +} + +// indentJSONWire renders a jsonwire value with JSON.stringify(v, null, 2) +// whitespace and V8 number canonicalisation (jsonwire.FormatV8Number), not the +// raw literal, so parsed exponent/duplicate forms re-emit exactly like V8. +func indentJSONWire(out *strings.Builder, value *jsonwire.Value, depth int) error { + switch value.Kind() { + case jsonwire.Array: + elements := value.Elements() + if len(elements) == 0 { + out.WriteString("[]") + return nil + } + out.WriteString("[\n") + for i, element := range elements { + writeJSONIndent(out, depth+1) + if err := indentJSONWire(out, element, depth+1); err != nil { + return err + } + if i < len(elements)-1 { + out.WriteByte(',') + } + out.WriteByte('\n') + } + writeJSONIndent(out, depth) + out.WriteByte(']') + case jsonwire.Object: + members := value.Members() + if len(members) == 0 { + out.WriteString("{}") + return nil + } + out.WriteString("{\n") + for i, member := range members { + writeJSONIndent(out, depth+1) + quoted, err := jsonwire.EncodeString(member.Key) + if err != nil { + return err + } + out.Write(quoted) + out.WriteString(": ") + if err := indentJSONWire(out, member.Value, depth+1); err != nil { + return err + } + if i < len(members)-1 { + out.WriteByte(',') + } + out.WriteByte('\n') + } + writeJSONIndent(out, depth) + out.WriteByte('}') + case jsonwire.String: + quoted, err := jsonwire.EncodeString(value.String()) + if err != nil { + return err + } + out.Write(quoted) + case jsonwire.Number: + out.WriteString(canonicalJSONNumber(value.NumberRaw())) + case jsonwire.Bool: + if value.Bool() { + out.WriteString("true") + } else { + out.WriteString("false") + } + default: + out.WriteString("null") + } + return nil +} + +func writeJSONIndent(out *strings.Builder, depth int) { + for i := 0; i < depth; i++ { + out.WriteString(" ") + } +} + +// canonicalJSONNumber renders a raw JSON number literal the way V8 +// JSON.stringify does after JSON.parse (shortest round-trip decimal). Raw +// literals that are already canonical pass through unchanged. +func canonicalJSONNumber(raw string) string { + value, err := strconv.ParseFloat(raw, 64) + if err != nil { + return raw + } + return jsonwire.FormatV8Number(value) +} + +// summaryLinesFor renders the compact human view of a request result the way +// the TS renderer sees it: a parsed JSON object flattens; a non-JSON body is a +// JS string ("value: "); an empty body is null ("value: null"). +func summaryLinesFor(body *jsonwire.Value, rawText string) []string { + if body == nil { + if rawText != "" { + return []string{"value: " + rawText} + } + return []string{"value: null"} + } + return summaryLines(body) +} + +// summaryLines is the compact human view port (src/cli/runtime-api.ts +// summaryLines): flatten an object to "label: value" rows, recursing one level +// into objects, rendering arrays as joined scalars or "N item(s)", and dashes +// for null/undefined/empty leaves. Document key order is preserved. +func summaryLines(value *jsonwire.Value) []string { + if value == nil || value.Kind() != jsonwire.Object { + return []string{prefixLeaf("", value)} + } + var lines []string + for _, member := range value.Members() { + lines = append(lines, summarizeMember(member.Key, member.Value, 0)...) + } + return lines +} + +func prefixLeaf(prefix string, value *jsonwire.Value) string { + name := prefix + if name == "" { + name = "value" + } + return name + ": " + jsString(value) +} + +func summarizeMember(key string, child *jsonwire.Value, depth int) []string { + if child != nil && child.Kind() == jsonwire.Array { + scalar := true + for _, item := range child.Elements() { + if item == nil || (item.Kind() != jsonwire.String && item.Kind() != jsonwire.Number && item.Kind() != jsonwire.Bool && item.Kind() != jsonwire.Null) { + scalar = false + break + } + } + text := fmt.Sprintf("%d item(s)", len(child.Elements())) + if scalar { + joined := jsArrayJoin(child) + if joined != "" { + text = joined + } else { + text = "none" + } + } + return []string{key + ": " + text} + } + if child != nil && child.Kind() == jsonwire.Object && depth < 1 { + var lines []string + for _, nested := range child.Members() { + lines = append(lines, summarizeMember(key+"."+nested.Key, nested.Value, depth+1)...) + } + return lines + } + // Leaf: null/undefined/empty string become a dash. + if child == nil || child.Kind() == jsonwire.Null || (child.Kind() == jsonwire.String && child.String() == "") { + return []string{key + ": -"} + } + return []string{key + ": " + jsString(child)} +} + +// jsString mirrors String(value) in JS for the value kinds these DTOs carry: +// strings verbatim, booleans true/false, numbers in V8 form, objects as +// [object Object], and null as "null". +func jsString(value *jsonwire.Value) string { + if value == nil || value.Kind() == jsonwire.Null { + return "null" + } + switch value.Kind() { + case jsonwire.String: + return value.String() + case jsonwire.Bool: + return strconv.FormatBool(value.Bool()) + case jsonwire.Number: + return canonicalJSONNumber(value.NumberRaw()) + case jsonwire.Object: + return "[object Object]" + default: + return fmt.Sprint(value) + } +} + +// jsArrayJoin mirrors Array.prototype.join(", "): null/undefined items become +// the empty string, strings verbatim, numbers and booleans via String(). +func jsArrayJoin(value *jsonwire.Value) string { + parts := make([]string, 0, len(value.Elements())) + for _, item := range value.Elements() { + if item == nil || item.Kind() == jsonwire.Null { + parts = append(parts, "") + continue + } + parts = append(parts, jsString(item)) + } + return strings.Join(parts, ", ") +} + +// formatENUSNumber renders a float64 the way Number.prototype.toLocaleString +// ("en-US") does for the integer magnitudes these DTOs carry: grouped integer +// digits with commas, fraction only for non-integers. +func formatENUSNumber(value float64) string { + if value == math.Trunc(value) && math.Abs(value) < 1e15 { + return groupDigits(strconv.FormatInt(int64(value), 10)) + } + fixed := strconv.FormatFloat(value, 'f', -1, 64) + intPart, fracPart, _ := strings.Cut(fixed, ".") + return groupDigits(intPart) + "." + fracPart +} diff --git a/go/internal/ocxcli/system_command.go b/go/internal/ocxcli/system_command.go new file mode 100644 index 0000000000..0396c49aec --- /dev/null +++ b/go/internal/ocxcli/system_command.go @@ -0,0 +1,397 @@ +// ocx system — the headless runtime-settings family (status/settings/startup/ +// diagnostics/sync/codex-app-server/codex-restart/update). This file ports the +// TypeScript owner (src/cli/system-command.ts) against the same management +// routes (/api/settings, /api/startup-health, /api/system/memory, +// /api/startup-action, /api/diagnostics/project-config, /api/sync, +// /api/system/codex-app-server, /api/system/codex-restart, +// /api/update/check|run|status) with byte-identical rendering (summaryLines +// for the compact views, JSON.stringify re-emission for the envelopes) and +// the runCliAction exit-code taxonomy. +// +// `system codex-cli-update` stays TypeScript-owned behind a subcommand-level +// seam: it is a read-only local Codex install inspection that never touches +// the management plane, unlike every other subcommand here (see OwnershipFor). +package ocxcli + +import ( + "fmt" + "strings" + + "github.com/lidge-jun/opencodex/go/internal/jsonwire" +) + +// systemUsage mirrors SYSTEM_USAGE in src/cli/system-command.ts. +const systemUsage = `Usage: + ocx system [status] [--json] + ocx system settings [--auto-start ] [--stream-mode ] + [--desktop-authless ] [--json] + ocx system startup [--json] + ocx system diagnostics [--json] + ocx system sync [--json] + ocx system codex-app-server [--json] + ocx system codex-restart --yes [--json] + ocx system codex-cli-update check [--json] + ocx system update check [--channel ] [--json] + ocx system update run [--channel ] [--restart ] --yes [--json] + ocx system update status [--json]` + +// runSystem implements `ocx system`. argv carries only this command's own +// arguments; codex-cli-update never reaches here (OwnershipFor delegates it). +func runSystem(args []string, deps Deps) int { + sub := "status" + rest := args + if len(args) > 0 { + sub = args[0] + rest = args[1:] + } + var err error + switch sub { + case "status": + err = systemStatus(rest, deps) + case "settings": + err = systemSettings(rest, deps) + case "startup": + err = systemStartup(rest, deps) + case "diagnostics": + err = systemDiagnostics(rest, deps) + case "sync": + err = systemSync(rest, deps) + case "codex-app-server": + err = systemCodexAppServer(rest, deps) + case "codex-restart": + err = systemCodexRestart(rest, deps) + case "update": + err = systemUpdate(rest, deps) + default: + err = managementCliUsage("unknown system command "+sub, systemUsage) + } + if err != nil { + return reportManagementFailure(deps, err) + } + return ExitOK +} + +// takeBoolean mirrors takeBooleanOption: --flag value must read as an on/off +// word, else the exact CliUsageError (no USAGE block). +func takeBoolean(args *[]string, flag string) (value string, present bool, err error) { + raw, found, takeErr := takeMgmtOption(args, flag) + if takeErr != nil { + return "", false, takeErr + } + if !found { + return "", false, nil + } + lower := strings.ToLower(raw) + switch lower { + case "on", "true", "yes", "1", "enabled": + return raw, true, nil + case "off", "false", "no", "0", "disabled": + return raw, true, nil + } + return "", false, managementCliUsage(fmt.Sprintf("%s must be on or off", flag), "") +} + +func systemStatus(argv []string, deps Deps) error { + args := append([]string(nil), argv...) + wantsJSON := takeMgmtFlag(&args, "--json") + if err := rejectMgmtArgs(args, systemUsage); err != nil { + return err + } + settings, rawSettings, _, err := managementRequest(deps, "GET", "/api/settings", "") + if err != nil { + return err + } + startup, rawStartup, _, err := managementRequest(deps, "GET", "/api/startup-health", "") + if err != nil { + return err + } + memory, rawMemory, _, err := managementRequest(deps, "GET", "/api/system/memory", "") + if err != nil { + return err + } + result := jsonwire.ObjectValue() + result.Set("settings", mgmtBodyValue(settings, rawSettings)) + result.Set("startup", mgmtBodyValue(startup, rawStartup)) + result.Set("memory", mgmtBodyValue(memory, rawMemory)) + if wantsJSON { + printManagementData(deps, result, "", true, nil) + return nil + } + _ = rawSettings + _ = rawStartup + _ = rawMemory + lines := summaryLines(result) + printManagementData(deps, nil, "", false, lines) + return nil +} + +func systemSettings(argv []string, deps Deps) error { + args := append([]string(nil), argv...) + wantsJSON := takeMgmtFlag(&args, "--json") + autoStart, autoStartSet, err := takeBoolean(&args, "--auto-start") + if err != nil { + return err + } + streamMode, streamModeSet, err := takeMgmtOption(&args, "--stream-mode") + if err != nil { + return err + } + desktopAuthless, desktopAuthlessSet, err := takeBoolean(&args, "--desktop-authless") + if err != nil { + return err + } + if err := rejectMgmtArgs(args, systemUsage); err != nil { + return err + } + if !autoStartSet && !streamModeSet && !desktopAuthlessSet { + result, rawText, _, readErr := managementRequest(deps, "GET", "/api/settings", "") + if readErr != nil { + return readErr + } + printManagementData(deps, result, rawText, wantsJSON, summaryLinesFor(result, rawText)) + return nil + } + var body strings.Builder + body.WriteByte('{') + first := true + if autoStartSet { + body.WriteString(`"codexAutoStart":`) + body.WriteString(onOffJSON(autoStart)) + first = false + } + if streamModeSet { + if !first { + body.WriteByte(',') + } + body.WriteString(`"streamMode":`) + body.WriteString(quoteJSONString(streamMode)) + first = false + } + if desktopAuthlessSet { + if !first { + body.WriteByte(',') + } + body.WriteString(`"codexDesktopAuthless":`) + body.WriteString(onOffJSON(desktopAuthless)) + } + body.WriteByte('}') + result, _, _, writeErr := managementRequest(deps, "PUT", "/api/settings", body.String()) + if writeErr != nil { + return writeErr + } + printManagementData(deps, result, "", wantsJSON, []string{"System settings updated."}) + return nil +} + +// onOffJSON mirrors the TS boolean conversion of an on/off option value. +func onOffJSON(value string) string { + lower := strings.ToLower(value) + switch lower { + case "true", "yes", "1", "enabled": + return "true" + } + return "false" +} + +func systemStartup(argv []string, deps Deps) error { + args := append([]string(nil), argv...) + action := "health" + if len(args) > 0 { + action = strings.ToLower(args[0]) + args = args[1:] + } + wantsJSON := takeMgmtFlag(&args, "--json") + if err := rejectMgmtArgs(args, systemUsage); err != nil { + return err + } + if action == "health" || action == "status" { + result, rawText, _, err := managementRequest(deps, "GET", "/api/startup-health", "") + if err != nil { + return err + } + printManagementData(deps, result, rawText, wantsJSON, summaryLinesFor(result, rawText)) + return nil + } + if action != "install-service" && action != "install-shim" { + return managementCliUsage("startup action must be health, install-service, or install-shim", systemUsage) + } + body := `{"action":` + quoteJSONString(action) + `}` + result, _, _, err := managementRequest(deps, "POST", "/api/startup-action", body) + if err != nil { + return err + } + message := action + " complete." + if member := result.Find("message"); member != nil && member.Kind() != jsonwire.Null { + message = jsString(member) + } + printManagementData(deps, result, "", wantsJSON, []string{message}) + return nil +} + +func systemDiagnostics(argv []string, deps Deps) error { + args := append([]string(nil), argv...) + wantsJSON := takeMgmtFlag(&args, "--json") + if err := rejectMgmtArgs(args, systemUsage); err != nil { + return err + } + result, rawText, _, err := managementRequest(deps, "GET", "/api/diagnostics/project-config", "") + if err != nil { + return err + } + printManagementData(deps, result, rawText, wantsJSON, nil) + return nil +} + +func systemSync(argv []string, deps Deps) error { + args := append([]string(nil), argv...) + wantsJSON := takeMgmtFlag(&args, "--json") + if err := rejectMgmtArgs(args, systemUsage); err != nil { + return err + } + result, rawText, _, err := managementRequest(deps, "POST", "/api/sync", "") + if err != nil { + return err + } + printManagementData(deps, result, rawText, wantsJSON, nil) + return nil +} + +func systemCodexAppServer(argv []string, deps Deps) error { + args := append([]string(nil), argv...) + wantsJSON := takeMgmtFlag(&args, "--json") + if err := rejectMgmtArgs(args, systemUsage); err != nil { + return err + } + result, rawText, _, err := managementRequest(deps, "GET", "/api/system/codex-app-server", "") + if err != nil { + return err + } + printManagementData(deps, result, rawText, wantsJSON, nil) + return nil +} + +func systemCodexRestart(argv []string, deps Deps) error { + args := append([]string(nil), argv...) + wantsJSON := takeMgmtFlag(&args, "--json") + yes := takeMgmtFlag(&args, "--yes") + if !yes { + return managementCliUsage("system codex-restart requires --yes", systemUsage) + } + if err := rejectMgmtArgs(args, systemUsage); err != nil { + return err + } + result, _, _, err := managementRequest(deps, "POST", "/api/system/codex-restart", "") + if err != nil { + return err + } + printManagementData(deps, result, "", wantsJSON, []string{"Codex app-server restart requested."}) + return nil +} + +func systemUpdate(argv []string, deps Deps) error { + args := append([]string(nil), argv...) + action := "check" + if len(args) > 0 { + action = strings.ToLower(args[0]) + args = args[1:] + } + wantsJSON := takeMgmtFlag(&args, "--json") + if action == "status" { + jobID := "" + if len(args) > 0 { + jobID = args[0] + args = args[1:] + } + if jobID == "" { + return managementCliUsage("update job id is required", systemUsage) + } + if err := rejectMgmtArgs(args, systemUsage); err != nil { + return err + } + result, rawText, _, err := managementRequest(deps, "GET", "/api/update/status?jobId="+encodeURIComponent(jobID), "") + if err != nil { + return err + } + printManagementData(deps, result, rawText, wantsJSON, nil) + return nil + } + channel, channelSet, err := takeMgmtOption(&args, "--channel") + if err != nil { + return err + } + if !channelSet { + channel = "latest" + } + if channel != "latest" && channel != "preview" { + return managementCliUsage("--channel must be latest or preview", systemUsage) + } + if action == "check" { + if err := rejectMgmtArgs(args, systemUsage); err != nil { + return err + } + result, rawText, _, err := managementRequest(deps, "GET", "/api/update/check?tag="+channel, "") + if err != nil { + return err + } + printManagementData(deps, result, rawText, wantsJSON, nil) + return nil + } + if action != "run" { + return managementCliUsage("unknown update action "+action, systemUsage) + } + restartValue, restartSet, err := takeBoolean(&args, "--restart") + if err != nil { + return err + } + restart := "true" + if restartSet { + restart = onOffJSON(restartValue) + } + yes := takeMgmtFlag(&args, "--yes") + if !yes { + return managementCliUsage("update run requires --yes", systemUsage) + } + if err := rejectMgmtArgs(args, systemUsage); err != nil { + return err + } + body := `{"tag":` + quoteJSONString(channel) + `,"restart":` + restart + `}` + result, _, _, err := managementRequest(deps, "POST", "/api/update/run", body) + if err != nil { + return err + } + printManagementData(deps, result, "", wantsJSON, []string{fmt.Sprintf("Update started (%s).", channel)}) + return nil +} + +// mgmtBodyValue projects a request result the way the TS runtimeRequest type +// does: a parsed JSON body stays as-is, a non-JSON body is the raw text as a +// JS string, and an empty body is null. +func mgmtBodyValue(body *jsonwire.Value, rawText string) *jsonwire.Value { + if body != nil { + return body + } + if rawText == "" { + return jsonwire.NullValue() + } + return jsonwire.StringValue(rawText) +} + +// encodeURIComponent mirrors the JavaScript helper for the one value the +// system family interpolates into a query string (update job ids). +func encodeURIComponent(value string) string { + var b strings.Builder + for i := 0; i < len(value); i++ { + char := value[i] + if char >= 'a' && char <= 'z' || char >= 'A' && char <= 'Z' || char >= '0' && char <= '9' || + char == '-' || char == '_' || char == '.' || char == '!' || char == '~' || char == '*' || + char == '\'' || char == '(' || char == ')' { + b.WriteByte(char) + continue + } + const hex = "0123456789ABCDEF" + b.WriteByte('%') + b.WriteByte(hex[char>>4]) + b.WriteByte(hex[char&0xf]) + } + return b.String() +} diff --git a/tests/go-cli-parity.test.ts b/tests/go-cli-parity.test.ts index 0ac65bbb56..90221a6e3a 100644 --- a/tests/go-cli-parity.test.ts +++ b/tests/go-cli-parity.test.ts @@ -46,7 +46,7 @@ function normalizeHealthPid(result: Result): Result { if (!result.stdout.startsWith("Proxy healthy") && !result.stdout.startsWith("{\"ok\":true")) return result; return { ...result, stdout: result.stdout.replace(/PID (?:null|\d+)/, "PID ").replace(/\"pid\":(?:null|\d+)/, '"pid":') }; } -afterEach(async () => { testServer?.stop(true); testServer = undefined; delete process.env.OPENCODEX_HOME; if (testHome && existsSync(testHome)) removeTreeWithRetry(testHome); testHome = ""; }); +afterEach(async () => { testServer?.stop(true); testServer = undefined; delete process.env.OPENCODEX_HOME; delete process.env.OPENCODEX_ADMIN_AUTH_TOKEN; if (testHome && existsSync(testHome)) removeTreeWithRetry(testHome); testHome = ""; }); function startAttestedFixture(status: "ready" | "pending" | "failed"): void { testHome = mkdtempSync(join(tmpdir(), "ocx-go-cli-parity-")); testServer = Bun.serve({ port: 0, fetch(request) { @@ -268,4 +268,183 @@ describe.skipIf(!goAvailable || goCLI === null)("Go CLI parity (ADR-0008, ticket expect(expectParity(["usage"])).toMatchObject({ code: 1 }); expect(expectParity(["observe", "usage"])).toMatchObject({ code: 1 }); }); + + // debug/system/api-key/access are Go-owned (ADR-0008, issue #47 batch 1 + // management families). The fixture models the management plane: attested + // /healthz, then stateless canned responses derived only from the request + // (so the TS and Go runs never perturb each other). deny=true makes every + // /api and /v1 request 401 like a missing admin token. + function startMgmtFixture(deny: boolean): void { + testHome = mkdtempSync(join(tmpdir(), "ocx-go-mgmt-parity-")); + testServer = Bun.serve({ port: 0, async fetch(request) { + const url = new URL(request.url); + const path = url.pathname; + if (path === "/healthz") { + const challenge = request.headers.get("x-opencodex-attestation-challenge") ?? ""; + const headers = challenge ? { "x-opencodex-attestation-proof": createLocalAttestationProof(secret, challenge, process.pid, testServer!.port) } : {}; + return Response.json({ status: "ok", service: "opencodex", version: "2.42.0", uptime: 1, pid: process.pid, port: testServer!.port }, { headers }); + } + const management = path.startsWith("/api/") || path.startsWith("/v1/"); + if (management && (deny || request.headers.get("x-opencodex-api-key") !== process.env.OPENCODEX_ADMIN_AUTH_TOKEN)) { + return Response.json({ error: "opencodex admin token required", reason: "no matching credential", hint: "Set OPENCODEX_ADMIN_AUTH_TOKEN to the token written to OPENCODEX_HOME/admin-api-token" }, { status: 401 }); + } + const text = await request.text(); + let entry: any = null; + if (text) { try { entry = JSON.parse(text); } catch { entry = null; } } + const env = { debug: true, usage: false, injection: false, claude: false }; + const debugView = () => { + const override: Record = {}; + const out = { enabled: false, usage: false, injection: false, claude: false }; + if (entry && typeof entry === "object") { + if (entry.reset === undefined) { + for (const key of ["debug", "usage", "injection", "claude"] as const) { + if (typeof entry[key] === "boolean") { override[key] = entry[key]; if (key === "debug") out.enabled = entry[key]; else (out as any)[key] = entry[key]; } + } + } + } + return { ...out, runtimeOverride: override, env }; + }; + if (path === "/api/debug" && request.method === "GET") return Response.json(debugView()); + if (path === "/api/debug" && request.method === "PUT") return Response.json(debugView()); + if (path === "/api/debug/logs") return Response.json([{ seq: 5, line: "provider debug line one" }, { seq: 6, line: "provider debug line two" }]); + if (path === "/api/debug/usage-logs") return Response.json([]); + if (path === "/api/keys" && request.method === "GET") return Response.json({ + keys: [ + { id: "key-1", name: "default", prefix: "ocx_data_ab12", createdAt: "2026-08-01T00:00:00.000Z", usage: { requests7d: 1447, totalRequests: 9033 } }, + { id: "key-2", name: "deploy", prefix: "ocx_data_cd34", createdAt: "2026-08-02T00:00:00.000Z", usage: { ambiguous: true } }, + { id: "key-3", name: "unused", prefix: "ocx_data_ef56", createdAt: "2026-08-03T00:00:00.000Z", usage: { requests7d: 0, totalRequests: 0, lastUsedAt: "2026-09-01T10:00:00.000Z" } }, + ], + attributionSince: "2026-08-01T00:00:00.000Z", + authMatrix: { admin: ["GET", "POST"] }, + baseUrl: `http://127.0.0.1:${testServer!.port}/v1`, + responsesEndpoint: `http://127.0.0.1:${testServer!.port}/v1/responses`, + chatCompletionsEndpoint: `http://127.0.0.1:${testServer!.port}/v1/chat/completions`, + messagesEndpoint: `http://127.0.0.1:${testServer!.port}/v1/messages`, + modelsEndpoint: `http://127.0.0.1:${testServer!.port}/v1/models`, + endpoint: `http://127.0.0.1:${testServer!.port}/v1/responses`, + }); + if (path === "/api/keys" && request.method === "POST") return Response.json({ id: "key-new", name: entry?.name ?? "default", key: "ocx_data_newsecret", createdAt: "2026-09-05T00:00:00.000Z" }, { status: 201 }); + if (path === "/api/keys" && request.method === "DELETE") return Response.json({ success: true }); + if (path === "/api/keys/rotate" && request.method === "POST") return Response.json({ id: entry?.id, rotationId: "rot-1", key: "ocx_data_rotsecret", createdAt: "2026-09-05T00:00:00.000Z" }, { status: 201 }); + if (path === "/api/keys/rotate/commit") return Response.json({ ok: true }); + if (path === "/api/keys/rotate" && request.method === "DELETE") return Response.json({ ok: true }); + if (path === "/v1/models") return Response.json({ object: "list", data: [{ id: "grok-4.6", owned_by: "xai" }, { id: "claude-haiku-4-5", owned_by: null }] }); + if (path === "/v1/chat/completions") return Response.json({ id: "chatcmpl-1", object: "chat.completion", model: entry?.model, choices: [{ index: 0, message: { role: "assistant", content: "OK" }, finish_reason: "stop" }] }); + if (path === "/v1/responses") return Response.json({ id: "resp-1", object: "response", model: entry?.model, output: [] }); + if (path === "/v1/messages") return Response.json({ id: "msg-1", type: "message", model: entry?.model, content: [{ type: "text", text: "OK" }] }); + if (path === "/api/settings" && request.method === "GET") return Response.json({ codexAutoStart: false, streamMode: "auto", codexDesktopAuthless: false, managementPort: 10100, desired: { enabled: true } }); + if (path === "/api/settings" && request.method === "PUT") return Response.json({ ok: true, saved: entry }); + if (path === "/api/startup-health") return Response.json({ status: "ok", autoStart: false, checks: [{ name: "port", ok: true }, { name: "config", ok: false }], service: { installed: true, name: "opencodex" } }); + if (path === "/api/system/memory") return Response.json({ rssBytes: 123456789, heapUsed: 2097152, heapTotal: 4194304, responseState: { activeTurns: 0, draining: false } }); + if (path === "/api/startup-action" && request.method === "POST") return Response.json({ message: `startup ${entry?.action} accepted` }); + if (path === "/api/diagnostics/project-config") return Response.json({ ok: true, file: "/tmp/none.json", issues: [] }); + if (path === "/api/sync" && request.method === "POST") return Response.json({ ok: true, catalogWritten: true, message: "Catalog refreshed." }); + if (path === "/api/system/codex-app-server") return Response.json({ reachable: true, pid: 4242 }); + if (path === "/api/system/codex-restart" && request.method === "POST") return Response.json({ requested: true }); + if (path === "/api/update/check") return Response.json({ available: false, current: "2.42.0", latest: "2.42.0", channel: url.searchParams.get("tag") ?? "latest" }); + if (path === "/api/update/status") return Response.json({ jobId: url.searchParams.get("jobId"), status: "done", ok: true }); + if (path === "/api/update/run" && request.method === "POST") return Response.json({ started: true, channel: entry?.tag, restart: entry?.restart }); + return Response.json({ error: "not found", path }, { status: 404 }); + }}); + writeFileSync(join(testHome, "runtime-port.json"), JSON.stringify({ pid: process.pid, port: testServer.port, hostname: "127.0.0.1", attestationSecret: secret })); + } + function withMgmtToken(value: string | undefined): void { + if (value === undefined) delete process.env.OPENCODEX_ADMIN_AUTH_TOKEN; + else process.env.OPENCODEX_ADMIN_AUTH_TOKEN = value; + } + test.each([ + { args: ["help", "debug"] }, { args: ["debug", "--help"] }, + { args: ["help", "access"] }, { args: ["access", "--help"] }, + { args: ["help", "api-key"] }, { args: ["api-key", "--help"] }, + { args: ["help", "system"] }, { args: ["system", "--help"] }, + ])("diffs management family help contracts for $args", ({ args }) => { + testHome = mkdtempSync(join(tmpdir(), "ocx-go-mgmt-parity-")); + expect(expectParity(args)).toMatchObject({ code: 0 }); + }); + test.each([ + { args: ["access", "bogus"], code: 2 }, + { args: ["access", "key", "bogus"], code: 2 }, + { args: ["access", "key", "remove", "key-1"], code: 2 }, + { args: ["access", "key", "rotate", "commit", "key-1"], code: 2 }, + { args: ["access", "test"], code: 2 }, + { args: ["access", "test", "m", "--protocol", "nope"], code: 2 }, + { args: ["api-key", "bogus"], code: 2 }, + { args: ["system", "bogus"], code: 2 }, + { args: ["system", "startup", "bogus"], code: 2 }, + { args: ["system", "update", "bogus"], code: 2 }, + { args: ["system", "codex-restart"], code: 2 }, + { args: ["debug", "bogus"], code: 1 }, + { args: ["debug", "provider", "bogus"], code: 1 }, + ])("diffs management family argument validation for $args", ({ args, code }) => { + testHome = mkdtempSync(join(tmpdir(), "ocx-go-mgmt-parity-")); + expect(expectParity(args)).toMatchObject({ code }); + }); + test.each([ + { args: ["debug", "provider", "status"] }, + { args: ["debug", "provider", "on"] }, + { args: ["debug", "provider", "reset"] }, + { args: ["debug", "usage", "status"] }, + { args: ["debug", "injection", "off"] }, + { args: ["debug", "provider", "logs"] }, + { args: ["debug", "usage", "logs"] }, + { args: ["access", "key"] }, + { args: ["access", "key", "list"] }, + { args: ["access", "key", "list", "--json"] }, + { args: ["access", "key", "create"] }, + { args: ["access", "key", "create", "deploy", "--json"] }, + { args: ["access", "key", "rotate", "key-1"] }, + { args: ["access", "key", "rotate", "key-1", "--json"] }, + { args: ["access", "key", "rotate", "commit", "key-1", "rot-1"] }, + { args: ["access", "key", "rotate", "abort", "key-1", "rot-1"] }, + { args: ["access", "key", "remove", "key-1", "--yes"] }, + { args: ["access", "endpoints"] }, + { args: ["access", "endpoints", "--json"] }, + { args: ["access", "models"] }, + { args: ["access", "models", "--json"] }, + { args: ["access", "test", "grok-4.6"] }, + { args: ["access", "test", "grok-4.6", "--json"] }, + { args: ["api-key"] }, + { args: ["api-key", "list"] }, + { args: ["api-key", "create", "deploy", "--json"] }, + { args: ["system", "settings"] }, + { args: ["system", "settings", "--json"] }, + { args: ["system", "settings", "--auto-start", "off", "--json"] }, + { args: ["system", "startup"] }, + { args: ["system", "diagnostics"] }, + { args: ["system", "diagnostics", "--json"] }, + { args: ["system", "sync"] }, + { args: ["system", "sync", "--json"] }, + { args: ["system", "codex-app-server"] }, + { args: ["system", "codex-app-server", "--json"] }, + { args: ["system", "codex-restart", "--yes"] }, + { args: ["system", "status"] }, + { args: ["system", "status", "--json"] }, + { args: ["system", "update", "check"] }, + { args: ["system", "update", "check", "--channel", "latest"] }, + { args: ["system", "update", "check", "--channel", "latest", "--json"] }, + { args: ["system", "update", "run", "--channel", "latest", "--restart", "off", "--yes"] }, + { args: ["system", "update", "status", "update-1"] }, + ])("diffs Go-owned management family output and exit code for $args", async ({ args }) => { + startMgmtFixture(false); + withMgmtToken("ocx_admin_testtokenforissue47abcdefghijklmnopqrstuvwxyz"); + const ts = await runTsAsync(args); + const go = await runGoAsync(args); + expect(go).toEqual(ts); + expect(ts).toMatchObject({ code: 0, stderr: "" }); + }, 20000); + test.each([ + { args: ["access", "key"] }, + { args: ["access", "key", "create", "denied", "--json"] }, + { args: ["api-key", "create", "denied", "--json"] }, + { args: ["debug", "provider", "on"] }, + { args: ["system", "settings"] }, + { args: ["system", "sync", "--json"] }, + ])("diffs denied management writes across families for $args", async ({ args }) => { + startMgmtFixture(true); + withMgmtToken(undefined); + const ts = await runTsAsync(args); + const go = await runGoAsync(args); + expect(go).toEqual(ts); + expect(ts).toMatchObject({ code: 1 }); + }, 20000); }); From 1e4c437acab97bf20d2a56461834e21574e20562 Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Tue, 8 Sep 2026 07:33:31 +0800 Subject: [PATCH 119/165] feat(go): flip capabilities, observe, and export to Go-owned (issue #46) Ports the remaining observe slice from TS to native Go dispatch: the whole capabilities surface, the observe family's non-usage subcommands (logs with all renderers/filters/explain/follow, storage codex-logs actions, memory, debug, claude-inbound, injection), and export for all twelve clients. Oracle coverage in tests/go-cli-parity.test.ts diffs byte-for-byte output, stderr, and exit code against the real TS CLI for: - capabilities: human rows, --json envelope, --mutating-only, --route inverse lookup (matched, unmatched exit 4, empty exit 64), and help spellings. - observe: live-fixture logs (json/jsonl/human, provider/status/conversation/ limit filters), logs explain, memory/debug/claude-inbound/injection summaries, storage codex-logs status/protect/repair/compact (--mode gating), unknown-command and value-validation failures, and help spellings. - export: --json and human output for opencode/pi/omp/hermes/openclaw/kimi/ gajae/dsh/mcode/zcode/prime, aside under a fixture account home, --out write plus the no-force clobber refusal, missing-value/unknown-client/ leftover-argument failures, offline proxy exit, and help spellings. - Ownership stays GoOwned for every dispatch path; only the observe request-history indexer actions (logs rebuild-index / index-status) keep the TypeScript owner, since they read/write the Bun:sqlite index with no management route, and delegate through the existing seam. V8-exact rendering via go/internal/jsonwire (ordered keys, raw numbers, indent) throughout; JSON5 export mirrors Bun's trailing-comma style. Help text and the CliUsageError/RuntimeApiError taxonomy (2 with/without USAGE, 4, 64, 1) match the TS registry byte for byte. go test ./... green (only the pre-existing embeddedui env failure), go vet ./... green, bun run typecheck green, parity suite 131/132 with the one pre-existing config-writes timeout. Co-Authored-By: Claude Code --- go/internal/ocxcli/capabilities_command.go | 231 +++++++ go/internal/ocxcli/capabilities_data.go | 488 +++++++++++++ go/internal/ocxcli/cli.go | 56 +- go/internal/ocxcli/export_build.go | 644 ++++++++++++++++++ go/internal/ocxcli/export_command.go | 675 ++++++++++++++++++ go/internal/ocxcli/export_models.go | 389 +++++++++++ go/internal/ocxcli/export_serialize.go | 418 ++++++++++++ go/internal/ocxcli/observe_command.go | 751 +++++++++++++++++++++ go/internal/ocxcli/takeover46_test.go | 98 +++ tests/go-cli-parity.test.ts | 187 ++++- 10 files changed, 3919 insertions(+), 18 deletions(-) create mode 100644 go/internal/ocxcli/capabilities_command.go create mode 100644 go/internal/ocxcli/capabilities_data.go create mode 100644 go/internal/ocxcli/export_build.go create mode 100644 go/internal/ocxcli/export_command.go create mode 100644 go/internal/ocxcli/export_models.go create mode 100644 go/internal/ocxcli/export_serialize.go create mode 100644 go/internal/ocxcli/observe_command.go create mode 100644 go/internal/ocxcli/takeover46_test.go diff --git a/go/internal/ocxcli/capabilities_command.go b/go/internal/ocxcli/capabilities_command.go new file mode 100644 index 0000000000..d43ef236df --- /dev/null +++ b/go/internal/ocxcli/capabilities_command.go @@ -0,0 +1,231 @@ +package ocxcli + +import ( + "fmt" + "strings" + + "github.com/lidge-jun/opencodex/go/internal/jsonwire" +) + +// ocx capabilities — the declared CLI capability index and its inverse route +// lookup. This file ports src/cli/capabilities-command.ts (plus the leaf data +// table src/cli/capabilities.ts, kept in capabilities_data.go) so the ownership +// flip keeps the documented surface identical: human rows, the --json envelope, +// --mutating-only filtering, and --route inverse lookup share one V8-exact +// JSON renderer with the rest of the Go CLI. +// +// Like the TypeScript owner, argv that names no known flag is ignored rather than +// rejected: `ocx capabilities` is the surface index an agent reads first, and the +// parser only ever recognizes --json, --mutating-only, and --route. + +const capabilitiesUsage = "Usage: ocx capabilities --route " + +// capabilitiesInvocation renders `ocx ` like capabilityInvocation. +func capabilitiesInvocation(cap capability) string { + return "ocx " + strings.Join(cap.command, " ") +} + +// capabilitiesForRoute returns the capabilities that drive a route path, in +// declaration order (capabilitiesForRoute in capabilities.ts). +func capabilitiesForRoute(path string) []capability { + var selected []capability + for _, cap := range capabilitiesTable { + for _, route := range cap.routes { + if route.path == path { + selected = append(selected, cap) + break + } + } + } + return selected +} + +// takeCapabilitiesValueFlag mirrors takeValueFlag: remove `--route` from args +// anywhere in argv and report its value ("" for a missing or flag-shaped one). +func takeCapabilitiesValueFlag(args []string, flag string) (rest []string, value string, present bool) { + for index, arg := range args { + if arg != flag { + continue + } + present = true + if index+1 < len(args) && !strings.HasPrefix(args[index+1], "-") { + value = args[index+1] + rest = append(append([]string(nil), args[:index]...), args[index+2:]...) + return rest, value, true + } + rest = append(append([]string(nil), args[:index]...), args[index+1:]...) + return rest, "", true + } + return args, "", false +} + +// capabilityEnvelope builds the --json envelope as an ordered jsonwire tree so +// the pretty printer emits exactly what JSON.stringify(value, null, 2) does. +func capabilityEnvelope(route string, routeGiven bool, selected []capability, mutatingOnly bool) *jsonwire.Value { + envelope := jsonwire.ObjectValue() + envelope.Set("schemaVersion", jsonwire.NumberFrom(1)) + if routeGiven { + envelope.Set("route", jsonwire.StringValue(route)) + } + caps := jsonwire.EmptyArray() + for _, cap := range selected { + entry := jsonwire.ObjectValue() + command := jsonwire.EmptyArray() + for _, part := range cap.command { + command.AppendArray(jsonwire.StringValue(part)) + } + entry.Set("command", command) + entry.Set("invocation", jsonwire.StringValue(capabilitiesInvocation(cap))) + entry.Set("summary", jsonwire.StringValue(cap.summary)) + routes := jsonwire.EmptyArray() + for _, routeEntry := range cap.routes { + object := jsonwire.ObjectValue() + object.Set("method", jsonwire.StringValue(routeEntry.method)) + object.Set("path", jsonwire.StringValue(routeEntry.path)) + routes.AppendArray(object) + } + entry.Set("routes", routes) + flags := jsonwire.EmptyArray() + for _, flag := range cap.flags { + object := jsonwire.ObjectValue() + object.Set("name", jsonwire.StringValue(flag.name)) + if flag.value != "" { + object.Set("value", jsonwire.StringValue(flag.value)) + } + if flag.required { + object.Set("required", jsonwire.BoolValue(true)) + } + object.Set("summary", jsonwire.StringValue(flag.summary)) + flags.AppendArray(object) + } + entry.Set("flags", flags) + entry.Set("mutates", jsonwire.BoolValue(cap.mutates)) + entry.Set("json", jsonwire.StringValue(cap.json)) + if len(cap.details) > 0 { + details := jsonwire.EmptyArray() + for _, detail := range cap.details { + details.AppendArray(jsonwire.StringValue(detail)) + } + entry.Set("details", details) + } + caps.AppendArray(entry) + } + envelope.Set("capabilities", caps) + if !routeGiven && !mutatingOnly { + head := jsonwire.EmptyArray() + for _, entry := range headCapabilitiesTable { + object := jsonwire.ObjectValue() + invocations := jsonwire.EmptyArray() + for _, invocation := range entry.invocations { + invocations.AppendArray(jsonwire.StringValue(invocation)) + } + object.Set("invocations", invocations) + object.Set("summary", jsonwire.StringValue(entry.summary)) + object.Set("bannerLine", jsonwire.StringValue(entry.bannerLine)) + head.AppendArray(object) + } + envelope.Set("headCapabilities", head) + } + return envelope +} + +// renderCapabilitiesHuman mirrors renderHuman in capabilities-command.ts. +func renderCapabilitiesHuman(deps Deps, selected []capability, includeHead bool) { + for _, cap := range selected { + marker := " " + if cap.mutates { + marker = "!" + } + fmt.Fprintf(deps.Stdout, "%s %s\n", marker, capabilitiesInvocation(cap)) + fmt.Fprintf(deps.Stdout, " %s\n", cap.summary) + if len(cap.routes) > 0 { + parts := make([]string, 0, len(cap.routes)) + for _, route := range cap.routes { + parts = append(parts, route.method+" "+route.path) + } + fmt.Fprintf(deps.Stdout, " routes: %s\n", strings.Join(parts, ", ")) + } + if len(cap.flags) > 0 { + names := make([]string, 0, len(cap.flags)) + for _, flag := range cap.flags { + names = append(names, flag.name) + } + fmt.Fprintf(deps.Stdout, " flags: %s\n", strings.Join(names, " ")) + } + } + if !includeHead { + return + } + for _, head := range headCapabilitiesTable { + fmt.Fprintf(deps.Stdout, " ocx %s\n", head.invocations[0]) + fmt.Fprintf(deps.Stdout, " %s\n", head.summary) + } +} + +// runCapabilities implements `ocx capabilities`. +func runCapabilities(args []string, deps Deps) int { + jsonOutput := false + mutatingOnly := false + rest := append([]string(nil), args...) + for index := 0; index < len(rest); index++ { + switch rest[index] { + case "--json": + jsonOutput = true + case "--mutating-only": + mutatingOnly = true + } + } + var route string + routeGiven := false + rest, route, routeGiven = takeCapabilitiesValueFlag(rest, "--route") + _ = rest + if routeGiven && route == "" { + fmt.Fprintln(deps.Stderr, capabilitiesUsage) + return 64 + } + + var selected []capability + if routeGiven { + selected = capabilitiesForRoute(route) + } else { + selected = append([]capability(nil), capabilitiesTable...) + } + if mutatingOnly { + filtered := selected[:0:0] + for _, cap := range selected { + if cap.mutates { + filtered = append(filtered, cap) + } + } + selected = filtered + } + + if routeGiven && len(selected) == 0 { + if jsonOutput { + envelope := capabilityEnvelope(route, true, nil, mutatingOnly) + var out strings.Builder + if err := encodeIndentedJSON(&out, envelope, 0); err != nil { + fmt.Fprintln(deps.Stderr, "Error: "+err.Error()) + return 1 + } + fmt.Fprintln(deps.Stdout, out.String()) + } else { + fmt.Fprintf(deps.Stderr, "No CLI capability drives %s.\n", route) + } + return 4 + } + + if jsonOutput { + envelope := capabilityEnvelope(route, routeGiven, selected, mutatingOnly) + var out strings.Builder + if err := encodeIndentedJSON(&out, envelope, 0); err != nil { + fmt.Fprintln(deps.Stderr, "Error: "+err.Error()) + return 1 + } + fmt.Fprintln(deps.Stdout, out.String()) + return 0 + } + + renderCapabilitiesHuman(deps, selected, !routeGiven && !mutatingOnly) + return 0 +} diff --git a/go/internal/ocxcli/capabilities_data.go b/go/internal/ocxcli/capabilities_data.go new file mode 100644 index 0000000000..b5b0e1e8fe --- /dev/null +++ b/go/internal/ocxcli/capabilities_data.go @@ -0,0 +1,488 @@ +package ocxcli + +// Code generated from src/cli/capabilities.ts by the issue-46 flip; keep in +// lockstep with the TypeScript capability table (the parity oracle diffs the +// TS CLI against this data at runtime). + +// capabilityRoute is one management route a capability drives. +type capabilityRoute struct{ method, path string } + +type capabilityFlag struct { + name string + value string // "" when the flag takes no value; else "string"|"number"|"boolean" + required bool + summary string +} + +type capability struct { + command []string + summary string + routes []capabilityRoute + flags []capabilityFlag + mutates bool + json string // "payload" | "envelope" | "none" + details []string +} + +type headCapability struct { + invocations []string + summary string + bannerLine string +} + +// capabilitiesTable mirrors CAPABILITIES in src/cli/capabilities.ts, in order. +var capabilitiesTable = []capability{ + { + command: []string{"status"}, + summary: "Proxy status, injection state, and version skew between this CLI and the running proxy.", + routes: []capabilityRoute{}, + flags: []capabilityFlag{ + {name: "--json", value: "boolean", summary: "Emit the status envelope as JSON."}, + }, + mutates: false, + json: "envelope", + details: []string{"Reads /healthz plus local config; drives no management API route."}, + }, + { + command: []string{"connect", "rotate"}, + summary: "Rotate the connected client's data key against the hub, with commit and abort.", + routes: []capabilityRoute{ + {method: "POST", path: "/api/keys/rotate"}, + {method: "POST", path: "/api/keys/rotate/commit"}, + {method: "DELETE", path: "/api/keys/rotate"}, + }, + flags: []capabilityFlag{ + {name: "--pairing-code-stdin", value: "boolean", summary: "Read a one-time pairing code from stdin as the rotation authority."}, + {name: "--admin-token-stdin", value: "boolean", summary: "Read the hub admin token from stdin as the rotation authority."}, + {name: "--json", value: "boolean", summary: "Emit the rotation result as JSON."}, + }, + mutates: true, + json: "payload", + details: []string{"Requires transient authority on stdin; the credential is never persisted or echoed.", "A rotation left pending by a crash is resumed here — startup and status stop rather than guess which key generation is live."}, + }, + { + command: []string{"capabilities"}, + summary: "List the declared CLI capabilities and the management routes they drive.", + routes: []capabilityRoute{}, + flags: []capabilityFlag{ + {name: "--json", value: "boolean", summary: "Emit the full capability table as JSON."}, + {name: "--mutating-only", value: "boolean", summary: "Restrict output to capabilities that mutate state."}, + {name: "--route", value: "string", summary: "Show which capabilities drive a management route."}, + }, + mutates: false, + json: "envelope", + details: []string{"Start here when driving ocx programmatically: it is the declared surface index, not a complete verb list."}, + }, + { + command: []string{"provider", "list"}, + summary: "Configured providers with connectivity and selected models.", + routes: []capabilityRoute{}, + flags: []capabilityFlag{ + {name: "--json", value: "boolean", summary: "Emit the provider list as JSON."}, + }, + mutates: false, + json: "envelope", + details: []string{"Reads local config; drives no management API route."}, + }, + { + command: []string{"provider", "keychain"}, + summary: "Move a provider's API key into the OS keychain, restore it, or report where it lives.", + routes: []capabilityRoute{ + {method: "GET", path: "/api/providers/keychain"}, + {method: "POST", path: "/api/providers/keychain"}, + }, + flags: []capabilityFlag{ + {name: "--json", value: "boolean", summary: "Emit the keychain status or result as JSON."}, + }, + mutates: true, + json: "payload", + details: []string{"`store` verifies every keychain write by read-back before config.json is rewritten with keychain: references; an unavailable keychain refuses with 503 and leaves the file untouched.", "Headless services usually have no unlocked keychain session; prefer ${ENV_VAR} references there."}, + }, + { + command: []string{"account", "list"}, + summary: "Codex OAuth accounts with pool priority and pause state.", + routes: []capabilityRoute{ + {method: "GET", path: "/api/codex-auth/accounts"}, + }, + flags: []capabilityFlag{ + {name: "--json", value: "boolean", summary: "Emit the account list as JSON."}, + }, + mutates: false, + json: "payload", + details: []string{"STATUS names `paused` alongside `selected`: a paused-but-selected account still receives requests.", "`--quota` shows cached Codex windows (including 5h); `--refresh` bypasses the server TTL."}, + }, + { + command: []string{"usage"}, + summary: "Token and estimated-cost report over a time range.", + routes: []capabilityRoute{ + {method: "GET", path: "/api/usage"}, + }, + flags: []capabilityFlag{ + {name: "--range", value: "string", summary: "today | 1d | 7d | 30d | all"}, + {name: "--provider", value: "string", summary: "Restrict to one provider."}, + {name: "--model", value: "string", summary: "Restrict to one model id."}, + {name: "--json", value: "boolean", summary: "Emit the usage report as JSON."}, + }, + mutates: false, + json: "payload", + details: []string{"Per-account totals are withheld under `--provider` or `--model`: account rows cannot be honestly re-partitioned by provider, so the report says so rather than printing an empty table.", "An `(ambiguous)` account row aggregates several accounts; do not read it as one identity."}, + }, + { + command: []string{"account", "pause"}, + summary: "Stop routing new requests to one account in the Codex pool.", + routes: []capabilityRoute{ + {method: "PUT", path: "/api/codex-auth/accounts/pause"}, + }, + flags: []capabilityFlag{ + {name: "--json", value: "boolean", summary: "Emit the pause result as JSON."}, + }, + mutates: true, + json: "envelope", + details: []string{"Pausing also unbinds threads pinned to the account and selects a fallback if it was active -- side effects of the route, not of the word `pause`.", "The issue that requested this reported the route as POST; it is PUT."}, + }, + { + command: []string{"account", "resume"}, + summary: "Return a paused account to the Codex pool.", + routes: []capabilityRoute{ + {method: "PUT", path: "/api/codex-auth/accounts/pause"}, + }, + flags: []capabilityFlag{ + {name: "--json", value: "boolean", summary: "Emit the resume result as JSON."}, + }, + mutates: true, + json: "envelope", + }, + { + command: []string{"account", "pause-exhausted"}, + summary: "Pause every Codex account whose quota is spent.", + routes: []capabilityRoute{ + {method: "PUT", path: "/api/codex-auth/accounts/pause-exhausted"}, + }, + flags: []capabilityFlag{ + {name: "--json", value: "boolean", summary: "Emit paused ids and the checked/failed counts as JSON."}, + }, + mutates: true, + json: "envelope", + details: []string{"The route refreshes quota per account and can partially fail; a non-zero failed count exits 1 and sets ok:false, because silence would read as `none were exhausted`."}, + }, + { + command: []string{"account", "strategy"}, + summary: "Show or set how an account pool picks the next account.", + routes: []capabilityRoute{ + {method: "GET", path: "/api/codex-auth/active"}, + {method: "PUT", path: "/api/codex-auth/pool-strategy"}, + {method: "GET", path: "/api/oauth/accounts/pool"}, + {method: "PUT", path: "/api/oauth/accounts/pool"}, + }, + flags: []capabilityFlag{ + {name: "--json", value: "boolean", summary: "Emit the applied strategy and sticky limit as JSON."}, + }, + mutates: true, + json: "envelope", + details: []string{"A bare invocation reads and never writes.", "The APPLIED value is echoed, not the requested one, so a server-side normalization stays visible.", "Values are not re-validated in the CLI: the server owns the strategy names and the 1-100 sticky bound.", "`anthropic` owns the full pool contract. Other OAuth providers reach the same endpoint with a generic subset (enabled/strategy/autoSwitchThreshold) whose settings persist but do not yet steer selection; `sticky` and `quotaWindow` are refused for them."}, + }, + { + command: []string{"account", "sticky"}, + summary: "Show or set how many consecutive requests stay on one account.", + routes: []capabilityRoute{ + {method: "GET", path: "/api/codex-auth/active"}, + {method: "PUT", path: "/api/codex-auth/pool-strategy"}, + {method: "GET", path: "/api/oauth/accounts/pool"}, + {method: "PUT", path: "/api/oauth/accounts/pool"}, + }, + flags: []capabilityFlag{ + {name: "--json", value: "boolean", summary: "Emit the applied strategy and sticky limit as JSON."}, + }, + mutates: true, + json: "envelope", + details: []string{"Only meaningful under the sticky-capable strategies; the pool strategy is the other half of this setting."}, + }, + { + command: []string{"logs"}, + summary: "Recent request log rows, filterable by provider, model, conversation, and status.", + routes: []capabilityRoute{ + {method: "GET", path: "/api/logs"}, + }, + flags: []capabilityFlag{ + {name: "--provider", value: "string", summary: "Restrict to one provider, matching failover attempts too."}, + {name: "--model", value: "string", summary: "Restrict to one model id, matching failover attempts too."}, + {name: "--conversation", value: "string", summary: "Restrict to one conversation id (`--conversationId` is accepted too)."}, + {name: "--status", value: "string", summary: "An exact code (429) or a class (5xx)."}, + {name: "--limit", value: "number", summary: "Row cap; defaults to 200."}, + {name: "--follow", value: "boolean", summary: "Poll for new rows; add --jsonl to emit JSONL."}, + {name: "--json", value: "boolean", summary: "Emit the server payload as JSON."}, + {name: "--jsonl", value: "boolean", summary: "Emit one row per line."}, + }, + mutates: false, + json: "payload", + details: []string{"`--provider` and `--model` both match a failover attempt, so a request is findable by what actually served it, not only by what was asked for.", "Rows print `conv=` when the entry carries one, so a conversation filter can be told apart from an empty result.", "`--follow` deduplicates by row id and cannot be combined with `--json`."}, + }, + { + command: []string{"storage", "report"}, + summary: "Disk usage under CODEX_HOME, with the log-guard protection report.", + routes: []capabilityRoute{ + {method: "GET", path: "/api/storage"}, + }, + flags: []capabilityFlag{ + {name: "--json", value: "boolean", summary: "Emit the storage report as JSON."}, + }, + mutates: false, + json: "payload", + }, + { + command: []string{"storage", "cleanup"}, + summary: "Preview or delete the oldest archived sessions by percentage.", + routes: []capabilityRoute{ + {method: "POST", path: "/api/storage/cleanup/preview"}, + {method: "POST", path: "/api/storage/cleanup"}, + }, + flags: []capabilityFlag{ + {name: "--percent", value: "number", summary: "Portion of the oldest archived sessions to target (0-100)."}, + {name: "--mode", value: "string", summary: "quarantine (recoverable from trash) or permanent."}, + {name: "--yes", value: "boolean", summary: "Required to actually delete; without it this is a preview."}, + {name: "--json", value: "boolean", summary: "Emit the preview or result as JSON."}, + }, + mutates: true, + json: "payload", + details: []string{"Without `--yes` it prints what WOULD be freed and exits 0 having changed nothing.", "There is no interactive confirmation: a prompt an agent can answer is not a safety boundary.", "`--mode quarantine` moves files to trash, so `storage trash restore` can undo it; `permanent` cannot be undone."}, + }, + { + command: []string{"storage", "trash"}, + summary: "List quarantined cleanup batches, or restore one.", + routes: []capabilityRoute{ + {method: "GET", path: "/api/storage/trash"}, + {method: "POST", path: "/api/storage/trash/restore"}, + }, + flags: []capabilityFlag{ + {name: "--yes", value: "boolean", summary: "Required for restore, which moves files and reconciles database rows."}, + {name: "--json", value: "boolean", summary: "Emit the trash list or restore result as JSON."}, + }, + mutates: true, + json: "payload", + details: []string{"Restore fails with a named 409 when the destination already exists, rather than overwriting it."}, + }, + { + command: []string{"storage", "policy"}, + summary: "Show, change, or run the automatic archived-session cleanup policy.", + routes: []capabilityRoute{ + {method: "GET", path: "/api/storage/cleanup-policy"}, + {method: "PUT", path: "/api/storage/cleanup-policy"}, + {method: "POST", path: "/api/storage/cleanup-policy/run"}, + }, + flags: []capabilityFlag{ + {name: "--enabled", value: "string", summary: "true or false."}, + {name: "--percent", value: "number", summary: "Portion of oldest archived sessions each run targets."}, + {name: "--mode", value: "string", summary: "quarantine or permanent."}, + {name: "--schedule", value: "string", summary: "startup, daily, weekly, or manual."}, + {name: "--yes", value: "boolean", summary: "Required for `policy run`, which deletes immediately."}, + {name: "--json", value: "boolean", summary: "Emit the policy or run state as JSON."}, + }, + mutates: true, + json: "payload", + details: []string{"`policy set` never enables implicitly: omitting `--enabled` keeps the stored value.", "`policy run` forces a run regardless of schedule, so it needs `--yes`."}, + }, + { + command: []string{"inspect", "config"}, + summary: "The effective merged configuration the proxy is running.", + routes: []capabilityRoute{ + {method: "GET", path: "/api/config"}, + }, + flags: []capabilityFlag{ + {name: "--json", value: "boolean", summary: "Emit the config as JSON."}, + }, + mutates: false, + json: "payload", + }, + { + command: []string{"inspect", "catalog"}, + summary: "The generated model catalog served to clients.", + routes: []capabilityRoute{ + {method: "GET", path: "/api/catalog"}, + }, + flags: []capabilityFlag{ + {name: "--json", value: "boolean", summary: "Emit the catalog as JSON."}, + }, + mutates: false, + json: "payload", + }, + { + command: []string{"inspect", "routing-analytics"}, + summary: "Aggregate routing outcomes per provider and model.", + routes: []capabilityRoute{ + {method: "GET", path: "/api/routing-analytics"}, + }, + flags: []capabilityFlag{ + {name: "--json", value: "boolean", summary: "Emit the analytics payload as JSON."}, + }, + mutates: false, + json: "payload", + }, + { + command: []string{"inspect", "pacing"}, + summary: "Request-pacing state for one provider or all of them.", + routes: []capabilityRoute{ + {method: "GET", path: "/api/provider-request-pacing"}, + }, + flags: []capabilityFlag{ + {name: "--name", value: "string", summary: "Restrict to one provider; omitted means every provider."}, + {name: "--json", value: "boolean", summary: "Emit the pacing state as JSON."}, + }, + mutates: false, + json: "payload", + details: []string{"An unknown provider name is a 404 rather than an empty result."}, + }, + { + command: []string{"inspect", "key-providers"}, + summary: "Providers that authenticate with an API key rather than OAuth.", + routes: []capabilityRoute{ + {method: "GET", path: "/api/key-providers"}, + }, + flags: []capabilityFlag{ + {name: "--json", value: "boolean", summary: "Emit the provider list as JSON."}, + }, + mutates: false, + json: "payload", + }, + { + command: []string{"inspect", "codex-prompt"}, + summary: "The Codex system prompt state, or the prompt text itself.", + routes: []capabilityRoute{ + {method: "GET", path: "/api/codex-prompt"}, + {method: "GET", path: "/api/codex-prompt/text"}, + }, + flags: []capabilityFlag{ + {name: "--text", value: "boolean", summary: "Print the prompt body verbatim instead of its metadata."}, + {name: "--json", value: "boolean", summary: "Emit the prompt metadata as JSON."}, + }, + mutates: false, + json: "payload", + details: []string{"Read-only by design: the six mutating prompt routes require a dashboard session."}, + }, + { + command: []string{"inspect", "client-config"}, + summary: "The generated configuration snippet for a supported client.", + routes: []capabilityRoute{ + {method: "GET", path: "/api/client-config"}, + }, + flags: []capabilityFlag{ + {name: "--client", value: "string", summary: "Required client id; the route names every accepted value on error."}, + {name: "--json", value: "boolean", summary: "Emit the snippet payload as JSON."}, + }, + mutates: false, + json: "payload", + }, + { + command: []string{"inspect", "star"}, + summary: "Whether this repository is starred by the signed-in GitHub account.", + routes: []capabilityRoute{ + {method: "GET", path: "/api/github/star"}, + }, + flags: []capabilityFlag{ + {name: "--json", value: "boolean", summary: "Emit the star status as JSON."}, + }, + mutates: false, + json: "payload", + details: []string{"Starring is never available from the CLI; the verb says so rather than offering a flag that cannot work."}, + }, + { + command: []string{"inspect", "windows-tray"}, + summary: "Windows tray helper state.", + routes: []capabilityRoute{ + {method: "GET", path: "/api/windows-tray"}, + }, + flags: []capabilityFlag{ + {name: "--json", value: "boolean", summary: "Emit the tray state as JSON."}, + }, + mutates: false, + json: "payload", + }, + { + command: []string{"system", "codex-app-server"}, + summary: "Codex app-server reachability and process state, as the dashboard sees it.", + routes: []capabilityRoute{ + {method: "GET", path: "/api/system/codex-app-server"}, + }, + flags: []capabilityFlag{ + {name: "--json", value: "boolean", summary: "Emit the app-server state as JSON."}, + }, + mutates: false, + json: "payload", + details: []string{"The GUI reads this state directly; without a verb an agent could not tell whether the Codex app-server was reachable at all."}, + }, + { + command: []string{"system", "codex-cli-update", "check"}, + summary: "Inspect a configured Codex CLI candidate and its ownership provenance.", + routes: []capabilityRoute{}, + flags: []capabilityFlag{ + {name: "--json", value: "boolean", summary: "Emit the redacted provenance report as JSON."}, + }, + mutates: false, + json: "envelope", + details: []string{"Proof-bound published-launcher context authenticates the configured candidate snapshot, not successful Codex execution; this check does not attest or admit a selected runtime.", "On Windows this first slice performs no candidate or configuration filesystem I/O: only a proof-captured absolute environment candidate can receive lexical app-bundle or version-manager labels; every other Windows candidate fails closed.", "Makes no package-registry request.", "Does not execute Codex or npm, install or repair software, control a process, or write configuration or cache state."}, + }, + { + command: []string{"system", "codex-restart"}, + summary: "Restart the Codex app-server.", + routes: []capabilityRoute{ + {method: "POST", path: "/api/system/codex-restart"}, + }, + flags: []capabilityFlag{ + {name: "--yes", value: "boolean", summary: "Required: restarts the operator's running Codex app-server."}, + {name: "--json", value: "boolean", summary: "Emit the restart result as JSON."}, + }, + mutates: true, + json: "payload", + details: []string{"`sync --restart-codex` is not a substitute: it restarts only as a side effect after a catalog or cache write, so it cannot restart a healthy install on request.", "--yes is mandatory because this interrupts a running editor session, which must never happen because an agent guessed a subcommand."}, + }, + { + command: []string{"claude", "desktop", "status"}, + summary: "Applied-vs-desired Claude Desktop state, including staleness, drift, and health.", + routes: []capabilityRoute{ + {method: "GET", path: "/api/claude-desktop/status"}, + }, + flags: []capabilityFlag{ + {name: "--json", value: "boolean", summary: "Emit the live status as JSON."}, + }, + mutates: false, + json: "payload", + details: []string{"Distinct from `claude desktop show`, which reports what this machine WOULD write; this reports what is actually in effect, which only the running proxy knows."}, + }, + { + command: []string{"integration", "native"}, + summary: "Show or toggle the native Claude, Claude Desktop, Codex, and Grok integrations, and read the Cursor status (which builds are installed, gateway values, last request seen).", + routes: []capabilityRoute{ + {method: "GET", path: "/api/native-integrations"}, + {method: "PUT", path: "/api/native-integrations/claude"}, + {method: "PUT", path: "/api/native-integrations/claude-desktop"}, + {method: "PUT", path: "/api/native-integrations/codex"}, + {method: "PUT", path: "/api/native-integrations/grok"}, + {method: "GET", path: "/api/native-integrations/cursor"}, + }, + flags: []capabilityFlag{ + {name: "--json", value: "boolean", summary: "Emit the client rows or toggle result as JSON."}, + }, + mutates: true, + json: "payload", + details: []string{"The list renders per-client state, installed, and desired columns; a blocked disable is named rather than left silent.", "Each client has its own route because a toggle rewrites that client's own config file."}, + }, + { + command: []string{"agent", "request-user-input"}, + summary: "Show or set whether default mode may ask the operator a question mid-task.", + routes: []capabilityRoute{ + {method: "GET", path: "/api/codex-auth/features/default-mode-request-user-input"}, + {method: "PUT", path: "/api/codex-auth/features/default-mode-request-user-input"}, + }, + flags: []capabilityFlag{ + {name: "--json", value: "boolean", summary: "Emit the feature state as JSON."}, + }, + mutates: true, + json: "payload", + details: []string{"A bare invocation reads and never writes."}, + }, +} + +// headCapabilitiesTable mirrors HEAD_CAPABILITIES in src/cli/capabilities.ts. +var headCapabilitiesTable = []headCapability{ + {invocations: []string{"--version", "-v", "version"}, summary: "Print the CLI version and exit.", bannerLine: "ocx --version | -v Print version"}, + {invocations: []string{"help", "--help", "-h"}, summary: "Print the command list, or one command's usage with `ocx help `.", bannerLine: "ocx help [command] Show help for a command"}, +} diff --git a/go/internal/ocxcli/cli.go b/go/internal/ocxcli/cli.go index aa63ead65b..1e21b30015 100644 --- a/go/internal/ocxcli/cli.go +++ b/go/internal/ocxcli/cli.go @@ -72,7 +72,7 @@ var Commands = []Command{ {Name: "restart", Usage: "ocx restart", Summary: "Restart the proxy.", Owner: TypeScriptOwned}, {Name: "v2", Usage: "ocx v2 ", Summary: "Manage the v2 surface.", Owner: TypeScriptOwned}, {Name: "health", Usage: "ocx health [--json]", Summary: "Verify the local proxy identity and report health.", Owner: GoOwned}, - {Name: "capabilities", Usage: "ocx capabilities [--json]", Summary: "List declared capabilities.", Owner: TypeScriptOwned}, + {Name: "capabilities", Usage: "ocx capabilities [--json] [--mutating-only] [--route ]", Summary: "List the declared CLI capabilities and the management routes they drive.", Owner: GoOwned}, {Name: "ready", Usage: "ocx ready [--json] [--wait [--timeout ]]", Summary: "Verify readiness.", Owner: GoOwned}, {Name: "provider", Usage: "ocx provider ", Summary: "Inspect configured providers.", Owner: GoOwned}, {Name: "account", Usage: "ocx account ", Summary: "Manage accounts.", Owner: TypeScriptOwned}, @@ -80,18 +80,19 @@ var Commands = []Command{ {Name: "alias", Usage: "ocx alias ", Summary: "Manage aliases.", Owner: TypeScriptOwned}, {Name: "combo", Usage: "ocx combo ", Summary: "Manage combo routing.", Owner: TypeScriptOwned}, {Name: "agent", Usage: "ocx agent ", Summary: "Manage agents.", Owner: TypeScriptOwned}, - {Name: "observe", Usage: "ocx observe ", Summary: "Inspect runtime observations.", Owner: TypeScriptOwned}, + {Name: "observe", Usage: "ocx observe ...", Summary: "Inspect proxy requests, usage, storage, memory, and debug data.", Owner: GoOwned}, {Name: "inspect", Usage: "ocx inspect ", Summary: "Inspect effective state.", Owner: TypeScriptOwned}, {Name: "route", Usage: "ocx route ", Summary: "Manage routing.", Owner: TypeScriptOwned}, {Name: "logs", Usage: "ocx logs [filters]", Summary: "Read logs.", Owner: TypeScriptOwned}, - // usage is Go-owned (the /api/usage read plus its renderer); observe keeps - // its other subcommands TypeScript-owned until each carries an oracle. + // usage is Go-owned (the /api/usage read plus its renderer); observe logs + // rebuild-index / index-status stay TypeScript-owned because they read the + // Bun:sqlite request-history index directly (no management route). {Name: "usage", Usage: "ocx usage [--range ] [--surface ] [--provider ] [--model ] [--json]", Summary: "Alias of ocx observe usage.", Owner: GoOwned}, {Name: "storage", Usage: "ocx storage ", Summary: "Manage storage.", Owner: TypeScriptOwned}, {Name: "memory", Usage: "ocx memory [--json]", Summary: "Inspect memory.", Owner: TypeScriptOwned}, {Name: "api-key", Usage: "ocx api-key ", Summary: "Manage API keys.", Owner: TypeScriptOwned}, {Name: "access", Usage: "ocx access ", Summary: "Manage external access.", Owner: TypeScriptOwned}, - {Name: "export", Usage: "ocx export --client ", Summary: "Export client configuration.", Owner: TypeScriptOwned}, + {Name: "export", Usage: "ocx export --client [--json] [--out ] [--force]", Summary: "Print a client config (OpenCode, Pi, OMP, Hermes, OpenClaw, Kimi Code, Gajae Code, DeepSeek Harness, MiniMax Code, ZCode, Prime Agent, Aside) wired to the running proxy.", Owner: GoOwned}, {Name: "integration", Usage: "ocx integration client ", Summary: "Manage integrations.", Owner: TypeScriptOwned}, {Name: "grok", Usage: "ocx grok ", Summary: "Manage Grok Build.", Owner: TypeScriptOwned}, {Name: "system", Usage: "ocx system ", Summary: "Manage runtime settings.", Owner: TypeScriptOwned}, @@ -152,11 +153,25 @@ func OwnershipFor(args []string) (Ownership, bool) { } return TypeScriptOwned, true } - // observe keeps its TypeScript owner per subcommand: `usage` shares the Go - // usage implementation, everything else stays with the TS owner until each - // subcommand carries its own oracle. - if command.Name == "observe" && len(args) > 1 && args[1] == "usage" { - return GoOwned, true + // observe dispatches natively for the whole family; only the request-history + // indexer actions (`logs rebuild-index` / `logs index-status`) keep the TS + // owner, because they read/write the Bun:sqlite index with no management route. + if command.Name == "observe" { + if len(args) == 1 { + return GoOwned, true + } + switch args[1] { + case "usage", "storage", "memory", "debug", "claude-inbound", "injection": + return GoOwned, true + case "logs": + if len(args) > 2 && (args[2] == "rebuild-index" || args[2] == "index-status") { + return TypeScriptOwned, true + } + return GoOwned, true + default: + // Unknown observe subcommands reproduce the TS CliUsageError natively. + return GoOwned, true + } } return command.Owner, true } @@ -277,14 +292,15 @@ func Run(args []string, deps Deps) int { return runStop(args[1:], deps) case "usage": return runUsage(args[1:], deps) + case "capabilities": + return runCapabilities(args[1:], deps) case "observe": - // Only `observe usage` reaches Go (OwnershipFor already gated this); - // other observe subcommands stay TypeScript-owned and never dispatch here. - if len(args) > 1 && args[1] == "usage" { - return runUsage(args[2:], deps) - } - fmt.Fprintf(deps.Stderr, "Unimplemented Go-owned command: %s\n", args[0]) - return ExitFailure + // rebuild-index / index-status never dispatch here (OwnershipFor gates + // them to TypeScriptOwned and delegates first); the indexer actions stay + // behind the Bun:sqlite owner seam while every other subcommand runs natively. + return runObserve(args[1:], deps) + case "export": + return runExport(args[1:], deps) default: // The ownership registry above and this switch must be reconciled by // TestOwnershipMapMatchesDispatch; this is defensive for future edits. @@ -340,6 +356,12 @@ func printSubcommandHelp(name string, deps Deps) int { fmt.Fprintf(deps.Stdout, "Usage: %s\n\n%s\n", command.Usage, command.Summary) } } + case "capabilities": + fmt.Fprint(deps.Stdout, "Usage: ocx capabilities [--json] [--mutating-only] [--route ]\n\nList the declared CLI capabilities and the management routes they drive.\n\nThe machine-readable surface index: start here when driving ocx programmatically instead of parsing help text.\n--route answers the inverse question: which commands drive this management route.\n") + case "observe": + fmt.Fprint(deps.Stdout, "Usage: ocx observe ...\n\nInspect proxy requests, usage, storage, memory, and debug data.\n") + case "export": + fmt.Fprint(deps.Stdout, "Usage: ocx export --client [--json] [--out ] [--force]\n\nPrint a client config (OpenCode, Pi, OMP, Hermes, OpenClaw, Kimi Code, Gajae Code, DeepSeek Harness, MiniMax Code, ZCode, Prime Agent, Aside) wired to the running proxy.\n\n--json prints the generated document as JSON on stdout; use --out for the client's native format.\n--out writes the native config there and refuses to replace an existing file without --force.\nThe config never contains a real key; it carries a documented env reference or a non-secret loopback placeholder.\nThe destination path is printed for merging by hand — ocx never writes your real client config.\n") case "config": fmt.Fprint(deps.Stdout, configHelp) default: diff --git a/go/internal/ocxcli/export_build.go b/go/internal/ocxcli/export_build.go new file mode 100644 index 0000000000..4593dbf8fe --- /dev/null +++ b/go/internal/ocxcli/export_build.go @@ -0,0 +1,644 @@ +package ocxcli + +import ( + "strings" + + "github.com/lidge-jun/opencodex/go/internal/jsonwire" +) + +// export_build.go — per-client document builders (src/clients/config-export.ts). +// Every builder mirrors the TypeScript object-literal key order so jsonwire's +// indented JSON (and the YAML/TOML/JSON5 renderers walking the same tree) emit +// byte-identical documents. No secret is serialized: env references and the +// loopback placeholder only. + +const ( + exportProviderID = "opencodex" + exportProviderSchema = "https://opencode.ai/config.json" + exportProviderNPM = "@ai-sdk/openai-compatible" + exportProviderV2Package = "@opencode-ai/ai/providers/openai-compatible" + exportProviderName = "OpenCodex" + exportOpenCodeAPIKeyEnv = "OPENCODEX_OPENCODE_API_KEY" + exportOpenCodeAPIKeyEnvRef = "{env:OPENCODEX_OPENCODE_API_KEY}" + exportHermesAPIKeyEnv = "OPENCODEX_HERMES_API_KEY" + exportOpenClawAPIKeyEnv = "OPENCODEX_OPENCLAW_API_KEY" + exportGajaeAPIKeyEnv = "OPENCODEX_GAJAE_API_KEY" + exportLoopbackPlaceholder = "opencodex-loopback" + exportPiAPIDialect = "openai-completions" +) + +func exportNumber(value float64) *jsonwire.Value { return jsonwire.NumberFrom(value) } +func exportStr(value string) *jsonwire.Value { return jsonwire.StringValue(value) } + +// exportContext bundles what the builders read. +type exportContext struct { + baseURL string + models []exportModel + config exportConfigView +} + +type exportConfigView struct { + codexDirect bool + hostname string + unauthLoopback bool + combos *jsonwire.Value // providers.combos raw object, if any +} + +// isLoopbackExportHostname mirrors isLoopbackHostname. +func isLoopbackExportHostname(hostname string) bool { + normalized := strings.ToLower(strings.TrimSpace(hostname)) + if normalized == "" { + return true + } + switch normalized { + case "localhost", "127.0.0.1", "::1", "[::1]": + return true + } + return false +} + +// shouldInjectExportAPIHeader mirrors shouldInjectApiAuthHeader. +func shouldInjectExportAPIHeader(config exportConfigView) bool { + if config.unauthLoopback { + return false + } + return !isLoopbackExportHostname(config.hostname) +} + +// ───────────────────────────────────────────────────────────────────────────── +// OpenCode. + +type exportProviderBlocks struct { + v1, v2 *jsonwire.Value +} + +func exportEffortVariants(model exportModel) *jsonwire.Value { + if model.reasoningEfforts == nil { + return nil + } + efforts := []string{} + for _, effort := range canonicalizeExportReasoningEfforts(model.reasoningEfforts) { + if effort != "none" { + efforts = append(efforts, effort) + } + } + if len(efforts) == 0 { + return nil + } + variants := jsonwire.EmptyArray() + for _, effort := range efforts { + variant := jsonwire.ObjectValue() + variant.Set("id", exportStr(effort)) + settings := jsonwire.ObjectValue() + settings.Set("reasoningEffort", exportStr(effort)) + variant.Set("settings", settings) + variants.AppendArray(variant) + } + return variants +} + +func exportOpenCodeConnection(baseURL string, config exportConfigView) *jsonwire.Value { + options := jsonwire.ObjectValue() + options.Set("baseURL", exportStr(baseURL)) + if shouldInjectExportAPIHeader(config) { + headers := jsonwire.ObjectValue() + headers.Set("x-opencodex-api-key", exportStr(exportOpenCodeAPIKeyEnvRef)) + options.Set("headers", headers) + return options + } + options.Set("apiKey", exportStr(exportOpenCodeAPIKeyEnvRef)) + return options +} + +func exportOpenCodeProviderBlocks(baseURL string, models []exportModel, config exportConfigView) exportProviderBlocks { + v1Models := jsonwire.ObjectValue() + v2Models := jsonwire.ObjectValue() + for _, model := range models { + key := model.namespaced + label := exportModelLabel(model) + entry := jsonwire.ObjectValue() + entry.Set("name", exportStr(label)) + if context, ok := authoritativeExportContextWindow(model); ok { + limit := jsonwire.ObjectValue() + limit.Set("context", exportNumber(context)) + limit.Set("output", exportNumber(outputBudgetForExport(context))) + entry.Set("limit", limit) + } + v1Models.Set(key, entry) + v2Entry := jsonwire.ObjectValue() + v2Entry.Set("name", exportStr(label)) + if context, ok := authoritativeExportContextWindow(model); ok { + limit := jsonwire.ObjectValue() + limit.Set("context", exportNumber(context)) + limit.Set("output", exportNumber(outputBudgetForExport(context))) + v2Entry.Set("limit", limit) + } + if variants := exportEffortVariants(model); variants != nil { + v2Entry.Set("variants", variants) + } + v2Models.Set(key, v2Entry) + } + v1 := jsonwire.ObjectValue() + v1.Set("npm", exportStr(exportProviderNPM)) + v1.Set("name", exportStr(exportProviderName)) + v1.Set("options", exportOpenCodeConnection(baseURL, config)) + v1.Set("models", v1Models) + v2 := jsonwire.ObjectValue() + v2.Set("package", exportStr(exportProviderV2Package)) + v2.Set("name", exportStr(exportProviderName)) + v2.Set("settings", exportOpenCodeConnection(baseURL, config)) + v2.Set("models", v2Models) + return exportProviderBlocks{v1: v1, v2: v2} +} + +func buildExportOpenCodeClientConfig(ctx exportContext) *jsonwire.Value { + models := normalizeExportModels(ctx.models) + blocks := exportOpenCodeProviderBlocks(ctx.baseURL, models, ctx.config) + doc := jsonwire.ObjectValue() + doc.Set("$schema", exportStr(exportProviderSchema)) + legacy := jsonwire.ObjectValue() + legacy.Set(exportProviderID, blocks.v1) + doc.Set("provider", legacy) + v2 := jsonwire.ObjectValue() + v2.Set(exportProviderID, blocks.v2) + doc.Set("providers", v2) + return doc +} + +// ───────────────────────────────────────────────────────────────────────────── +// Pi-shaped clients (pi, omp, prime, aside) and OMP's extra metadata. + +func buildExportPiClientConfig(ctx exportContext) *jsonwire.Value { + models := jsonwire.EmptyArray() + for _, model := range normalizeExportModels(ctx.models) { + input := inputModalitiesForExportClient(model.inputModalities) + if input == nil { + continue + } + entry := jsonwire.ObjectValue() + entry.Set("id", exportStr(model.namespaced)) + entry.Set("name", exportStr(exportModelLabel(model))) + inputArray := jsonwire.EmptyArray() + for _, modality := range input { + inputArray.AppendArray(exportStr(modality)) + } + entry.Set("input", inputArray) + if len(model.reasoningEfforts) > 0 { + entry.Set("reasoning", jsonwire.BoolValue(true)) + levelMap := jsonwire.ObjectValue() + levels := []string{"off", "minimal", "low", "medium", "high", "xhigh", "max"} + for _, level := range levels { + var mapped string + hasValue := false + if level == "max" { + if exportContains(model.reasoningEfforts, "max") { + mapped, hasValue = "max", true + } else if exportContains(model.reasoningEfforts, "ultra") { + mapped, hasValue = "ultra", true + } + } else if exportContains(model.reasoningEfforts, level) { + mapped, hasValue = level, true + } + if hasValue { + levelMap.Set(level, exportStr(mapped)) + } else { + levelMap.Set(level, jsonwire.NullValue()) + } + } + entry.Set("thinkingLevelMap", levelMap) + } + if context, ok := authoritativeExportContextWindow(model); ok { + entry.Set("contextWindow", exportNumber(context)) + entry.Set("maxTokens", exportNumber(outputBudgetForExport(context))) + } + models.AppendArray(entry) + } + provider := jsonwire.ObjectValue() + provider.Set("baseUrl", exportStr(ctx.baseURL)) + provider.Set("api", exportStr(exportPiAPIDialect)) + provider.Set("apiKey", exportStr(exportLoopbackPlaceholder)) + provider.Set("models", models) + providers := jsonwire.ObjectValue() + providers.Set(exportProviderID, provider) + doc := jsonwire.ObjectValue() + doc.Set("providers", providers) + return doc +} + +var exportOMPEffortVocabulary = map[string]bool{"minimal": true, "low": true, "medium": true, "high": true, "xhigh": true, "max": true} + +func exportOMPEfforts(model exportModel) []string { + efforts := []string{} + for _, effort := range model.reasoningEfforts { + normalized := strings.ToLower(strings.TrimSpace(effort)) + if exportOMPEffortVocabulary[normalized] && !exportContains(efforts, normalized) { + efforts = append(efforts, normalized) + } + } + return efforts +} + +func buildExportOmpClientConfig(ctx exportContext) *jsonwire.Value { + models := jsonwire.EmptyArray() + for _, model := range normalizeExportModels(ctx.models) { + input := inputModalitiesForExportClient(model.inputModalities) + if input == nil { + continue + } + entry := jsonwire.ObjectValue() + entry.Set("id", exportStr(model.namespaced)) + entry.Set("name", exportStr(exportModelLabel(model))) + inputArray := jsonwire.EmptyArray() + for _, modality := range input { + inputArray.AppendArray(exportStr(modality)) + } + entry.Set("input", inputArray) + if model.native && model.provider == "openai" { + entry.Set("api", exportStr("openai-responses")) + } + if context, ok := authoritativeExportContextWindow(model); ok { + entry.Set("contextWindow", exportNumber(context)) + entry.Set("maxTokens", exportNumber(outputBudgetForExport(context))) + } + efforts := exportOMPEfforts(model) + if len(efforts) > 0 { + defaultLevel := strings.ToLower(strings.TrimSpace(model.defaultReasoningEffort)) + entry.Set("reasoning", jsonwire.BoolValue(true)) + thinking := jsonwire.ObjectValue() + thinking.Set("mode", exportStr("effort")) + effortArray := jsonwire.EmptyArray() + for _, effort := range efforts { + effortArray.AppendArray(exportStr(effort)) + } + thinking.Set("efforts", effortArray) + if defaultLevel != "" && exportContains(efforts, defaultLevel) { + thinking.Set("defaultLevel", exportStr(defaultLevel)) + } + entry.Set("thinking", thinking) + } + models.AppendArray(entry) + } + provider := jsonwire.ObjectValue() + provider.Set("baseUrl", exportStr(ctx.baseURL)) + provider.Set("api", exportStr(exportPiAPIDialect)) + provider.Set("apiKey", exportStr(exportLoopbackPlaceholder)) + provider.Set("models", models) + providers := jsonwire.ObjectValue() + providers.Set(exportProviderID, provider) + doc := jsonwire.ObjectValue() + doc.Set("providers", providers) + return doc +} + +// ───────────────────────────────────────────────────────────────────────────── +// Hermes / OpenClaw. + +func buildExportHermesClientConfig(ctx exportContext) *jsonwire.Value { + models := jsonwire.ObjectValue() + for _, model := range normalizeExportModels(ctx.models) { + if len(model.inputModalities) > 0 { + entry := jsonwire.ObjectValue() + entry.Set("supports_vision", jsonwire.BoolValue(exportContains(model.inputModalities, "image"))) + models.Set(model.namespaced, entry) + } else { + models.Set(model.namespaced, jsonwire.ObjectValue()) + } + } + provider := jsonwire.ObjectValue() + provider.Set("api", exportStr(ctx.baseURL)) + provider.Set("api_key", exportStr("${"+exportHermesAPIKeyEnv+"}")) + provider.Set("api_mode", exportStr("chat_completions")) + provider.Set("discover_models", jsonwire.BoolValue(false)) + provider.Set("models", models) + if shouldInjectExportAPIHeader(ctx.config) { + headers := jsonwire.ObjectValue() + headers.Set("x-opencodex-api-key", exportStr("${"+exportHermesAPIKeyEnv+"}")) + provider.Set("extra_headers", headers) + } + providers := jsonwire.ObjectValue() + providers.Set(exportProviderID, provider) + doc := jsonwire.ObjectValue() + doc.Set("providers", providers) + return doc +} + +func buildExportOpenclawClientConfig(ctx exportContext) *jsonwire.Value { + models := jsonwire.EmptyArray() + for _, model := range normalizeExportModels(ctx.models) { + entry := jsonwire.ObjectValue() + entry.Set("id", exportStr(model.namespaced)) + entry.Set("name", exportStr(exportModelLabel(model))) + if context, ok := authoritativeExportContextWindow(model); ok { + entry.Set("contextWindow", exportNumber(context)) + } + models.AppendArray(entry) + } + provider := jsonwire.ObjectValue() + provider.Set("baseUrl", exportStr(ctx.baseURL)) + provider.Set("apiKey", exportStr("${"+exportOpenClawAPIKeyEnv+"}")) + provider.Set("api", exportStr("openai-completions")) + provider.Set("models", models) + if shouldInjectExportAPIHeader(ctx.config) { + headers := jsonwire.ObjectValue() + headers.Set("x-opencodex-api-key", exportStr("${"+exportOpenClawAPIKeyEnv+"}")) + provider.Set("headers", headers) + } + providers := jsonwire.ObjectValue() + providers.Set(exportProviderID, provider) + merge := jsonwire.ObjectValue() + merge.Set("mode", exportStr("merge")) + merge.Set("providers", providers) + doc := jsonwire.ObjectValue() + doc.Set("models", merge) + return doc +} + +// ───────────────────────────────────────────────────────────────────────────── +// Kimi / Gajae. + +func exportKimiModelAlias(namespaced string) string { + return exportProviderID + "/" + namespaced +} + +func buildExportKimiClientConfig(ctx exportContext) *jsonwire.Value { + models := jsonwire.ObjectValue() + for _, model := range normalizeExportModels(ctx.models) { + context, ok := authoritativeExportContextWindow(model) + if !ok { + continue + } + block := jsonwire.ObjectValue() + block.Set("provider", exportStr(exportProviderID)) + block.Set("model", exportStr(model.namespaced)) + block.Set("max_context_size", exportNumber(context)) + if model.displayName != "" { + block.Set("display_name", exportStr(model.displayName)) + } + models.Set(exportKimiModelAlias(model.namespaced), block) + } + provider := jsonwire.ObjectValue() + provider.Set("type", exportStr("openai")) + provider.Set("base_url", exportStr(ctx.baseURL)) + provider.Set("api_key", exportStr(exportLoopbackPlaceholder)) + providers := jsonwire.ObjectValue() + providers.Set(exportProviderID, provider) + doc := jsonwire.ObjectValue() + doc.Set("providers", providers) + doc.Set("models", models) + return doc +} + +func buildExportGajaeClientConfig(ctx exportContext) *jsonwire.Value { + models := jsonwire.EmptyArray() + for _, model := range normalizeExportModels(ctx.models) { + input := inputModalitiesForExportClient(model.inputModalities) + if input == nil { + continue + } + entry := jsonwire.ObjectValue() + entry.Set("id", exportStr(model.namespaced)) + entry.Set("name", exportStr(exportModelLabel(model))) + inputArray := jsonwire.EmptyArray() + for _, modality := range input { + inputArray.AppendArray(exportStr(modality)) + } + entry.Set("input", inputArray) + if context, ok := authoritativeExportContextWindow(model); ok { + entry.Set("contextWindow", exportNumber(context)) + entry.Set("maxTokens", exportNumber(outputBudgetForExport(context))) + } + models.AppendArray(entry) + } + provider := jsonwire.ObjectValue() + provider.Set("baseUrl", exportStr(ctx.baseURL)) + provider.Set("apiKeyEnv", exportStr(exportGajaeAPIKeyEnv)) + provider.Set("api", exportStr("openai-completions")) + provider.Set("models", models) + providers := jsonwire.ObjectValue() + providers.Set(exportProviderID, provider) + doc := jsonwire.ObjectValue() + doc.Set("providers", providers) + return doc +} + +// ───────────────────────────────────────────────────────────────────────────── +// DSH. + +var exportDSHEffortOrder = []string{"low", "medium", "high", "xhigh", "max"} + +func exportDshReasoningEfforts(model exportModel) *jsonwire.Value { + offered := map[string]bool{} + for _, raw := range model.reasoningEfforts { + effort := strings.ToLower(strings.TrimSpace(raw)) + if effort == "ultra" || exportContains(exportDSHEffortOrder, effort) { + offered[effort] = true + } + } + if len(offered) == 0 { + return nil + } + out := jsonwire.ObjectValue() + for _, effort := range exportDSHEffortOrder { + if effort != "max" { + if offered[effort] { + out.Set(effort, exportStr(effort)) + } + continue + } + if offered["max"] { + out.Set("max", exportStr("max")) + } else if offered["ultra"] { + out.Set("max", exportStr("ultra")) + } + } + return out +} + +// isKnownSafeExportDshCombo mirrors isKnownSafeDshCombo: the config's combos +// entry for model.id must name only non-openai providers/models with trimmed +// non-empty strings. +func isKnownSafeExportDshCombo(model exportModel, combos *jsonwire.Value) bool { + if combos == nil || combos.Kind() != jsonwire.Object { + return false + } + combo := combos.Find(model.id) + if combo == nil || combo.Kind() != jsonwire.Object { + return false + } + targets := combo.Find("targets") + if targets == nil || targets.Kind() != jsonwire.Array || len(targets.Elements()) == 0 { + return false + } + for _, target := range targets.Elements() { + if target == nil || target.Kind() != jsonwire.Object { + return false + } + provider := target.Find("provider") + modelID := target.Find("model") + if provider == nil || provider.Kind() != jsonwire.String { + return false + } + if modelID == nil || modelID.Kind() != jsonwire.String { + return false + } + providerValue := provider.String() + modelValue := modelID.String() + if providerValue == "" || providerValue != strings.TrimSpace(providerValue) || providerValue == "openai" { + return false + } + if modelValue == "" || modelValue != strings.TrimSpace(modelValue) { + return false + } + } + return true +} + +func buildExportDshClientConfig(ctx exportContext) *jsonwire.Value { + direct := ctx.config.codexDirect + models := jsonwire.EmptyArray() + for _, model := range normalizeExportModels(ctx.models) { + if direct && (model.native || model.provider == "openai") { + continue + } + if direct && model.provider == "combo" && !isKnownSafeExportDshCombo(model, ctx.config.combos) { + continue + } + input := dshExportInputModalities(model.inputModalities) + if input == nil { + continue + } + entry := jsonwire.ObjectValue() + entry.Set("id", exportStr(model.namespaced)) + entry.Set("name", exportStr(exportModelLabel(model))) + inputArray := jsonwire.EmptyArray() + for _, modality := range input { + inputArray.AppendArray(exportStr(modality)) + } + entry.Set("input", inputArray) + if context, ok := authoritativeExportContextWindow(model); ok { + entry.Set("contextWindow", exportNumber(context)) + } + if efforts := exportDshReasoningEfforts(model); efforts != nil { + entry.Set("reasoningEfforts", efforts) + } + models.AppendArray(entry) + } + provider := jsonwire.ObjectValue() + provider.Set("displayName", exportStr("OpenCodex")) + provider.Set("api", exportStr("openai-responses")) + provider.Set("baseURL", exportStr(ctx.baseURL)) + headers := jsonwire.ObjectValue() + headers.Set("Authorization", exportStr("Bearer ocx_data_dsh")) + provider.Set("headers", headers) + provider.Set("models", models) + providers := jsonwire.ObjectValue() + providers.Set(exportProviderID, provider) + llm := jsonwire.ObjectValue() + llm.Set("providers", providers) + top := jsonwire.ObjectValue() + top.Set("llm-pi-ai", llm) + return top +} + +// ───────────────────────────────────────────────────────────────────────────── +// MiniMax Code / ZCode. + +func buildExportMcodeClientConfig(ctx exportContext) *jsonwire.Value { + models := jsonwire.ObjectValue() + for _, model := range normalizeExportModels(ctx.models) { + entry := jsonwire.ObjectValue() + if context, ok := authoritativeExportContextWindow(model); ok { + limit := jsonwire.ObjectValue() + limit.Set("context", exportNumber(context)) + entry.Set("limit", limit) + } + efforts := sanitizeExportReasoningEfforts(model.reasoningEfforts) + filtered := []string{} + for _, effort := range efforts { + if effort != "none" { + filtered = append(filtered, effort) + } + } + if len(filtered) > 0 { + thinking := jsonwire.ObjectValue() + options := jsonwire.EmptyArray() + for _, effort := range filtered { + options.AppendArray(exportStr(effort)) + } + thinking.Set("effortOptions", options) + entry.Set("thinking", thinking) + } + models.Set(model.namespaced, entry) + } + provider := jsonwire.ObjectValue() + provider.Set("name", exportStr("OpenCodex")) + provider.Set("kind", exportStr("custom")) + provider.Set("enabled", jsonwire.BoolValue(true)) + provider.Set("api", exportStr("anthropic-messages")) + options := jsonwire.ObjectValue() + options.Set("apiKey", exportStr(exportLoopbackPlaceholder)) + options.Set("baseURL", exportStr(exportTrimV1Suffix(ctx.baseURL))) + options.Set("authMode", exportStr("api-key")) + provider.Set("options", options) + provider.Set("models", models) + custom := jsonwire.ObjectValue() + custom.Set(exportProviderID, provider) + doc := jsonwire.ObjectValue() + doc.Set("custom_provider", custom) + return doc +} + +// exportTrimV1Suffix mirrors ctx.baseUrl.replace(/\/v1\/?$/, ""). +func exportTrimV1Suffix(baseURL string) string { + if strings.HasSuffix(baseURL, "/v1") { + return baseURL[:len(baseURL)-3] + } + if strings.HasSuffix(baseURL, "/v1/") { + return baseURL[:len(baseURL)-4] + } + return baseURL +} + +func buildExportZcodeClientConfig(ctx exportContext) *jsonwire.Value { + models := jsonwire.ObjectValue() + for _, model := range normalizeExportModels(ctx.models) { + input := inputModalitiesForExportClient(model.inputModalities) + if input == nil { + continue + } + entry := jsonwire.ObjectValue() + entry.Set("name", exportStr(exportModelLabel(model))) + modalities := jsonwire.ObjectValue() + inputArray := jsonwire.EmptyArray() + for _, modality := range input { + inputArray.AppendArray(exportStr(modality)) + } + modalities.Set("input", inputArray) + output := jsonwire.EmptyArray() + output.AppendArray(exportStr("text")) + modalities.Set("output", output) + entry.Set("modalities", modalities) + if context, ok := authoritativeExportContextWindow(model); ok { + limit := jsonwire.ObjectValue() + limit.Set("context", exportNumber(context)) + entry.Set("limit", limit) + } + models.Set(model.namespaced, entry) + } + provider := jsonwire.ObjectValue() + provider.Set("name", exportStr("OpenCodex")) + provider.Set("kind", exportStr("openai-compatible")) + provider.Set("enabled", jsonwire.BoolValue(true)) + provider.Set("source", exportStr("custom")) + options := jsonwire.ObjectValue() + options.Set("apiKey", exportStr(exportLoopbackPlaceholder)) + options.Set("baseURL", exportStr(exportTrimV1Suffix(ctx.baseURL)+"/v1")) + options.Set("apiKeyRequired", jsonwire.BoolValue(true)) + provider.Set("options", options) + provider.Set("models", models) + providers := jsonwire.ObjectValue() + providers.Set(exportProviderID, provider) + doc := jsonwire.ObjectValue() + doc.Set("provider", providers) + return doc +} diff --git a/go/internal/ocxcli/export_command.go b/go/internal/ocxcli/export_command.go new file mode 100644 index 0000000000..c050818494 --- /dev/null +++ b/go/internal/ocxcli/export_command.go @@ -0,0 +1,675 @@ +package ocxcli + +import ( + "errors" + "fmt" + "io" + "net/http" + "os" + "path/filepath" + "strings" + + "github.com/lidge-jun/opencodex/go/internal/config" + "github.com/lidge-jun/opencodex/go/internal/jsonwire" +) + +// ocx export — print a client config (OpenCode, Pi, OMP, Hermes, OpenClaw, +// Kimi Code, Gajae Code, DeepSeek Harness, MiniMax Code, ZCode, Prime Agent, +// Aside) wired to the running proxy. This file ports src/cli/export-command.ts +// and the destination/path helpers of src/clients/config-export.ts. +// +// --json stdout is exactly the client config as JSON (every client — the flag +// is about machine readability, not the client's native format); the native +// format leads the human path. The command never writes the user's real config +// path: --out is an explicit target and refuses to clobber without --force. + +const exportUsageUsage = `Usage: + ocx export --client [--json] [--out ] [--force]` + +var exportClientIDs = []string{"opencode", "pi", "omp", "hermes", "openclaw", "kimi", "gajae", "dsh", "mcode", "zcode", "prime", "aside"} + +func exportIsClientID(value string) bool { + for _, id := range exportClientIDs { + if id == value { + return true + } + } + return false +} + +type exportClientSpec struct { + id string + filename string + apiKeyEnv string + exportHint string + format exportFormat + build func(ctx exportContext) *jsonwire.Value +} + +func exportSpecs() map[string]exportClientSpec { + specs := map[string]exportClientSpec{} + add := func(spec exportClientSpec) { specs[spec.id] = spec } + add(exportClientSpec{id: "opencode", filename: "opencode.json", apiKeyEnv: exportOpenCodeAPIKeyEnv, + exportHint: "export " + exportOpenCodeAPIKeyEnv + "=", format: exportFormatJSON, build: buildExportOpenCodeClientConfig}) + add(exportClientSpec{id: "pi", filename: "pi-models.json", exportHint: "Pi reads a non-secret placeholder from models.json; loopback needs no key.", format: exportFormatJSON, build: buildExportPiClientConfig}) + add(exportClientSpec{id: "omp", filename: "omp-models.yaml", exportHint: "OMP reads a non-secret placeholder from models.yml; loopback needs no key.", format: exportFormatYAML, build: buildExportOmpClientConfig}) + add(exportClientSpec{id: "hermes", filename: "hermes-config.yaml", apiKeyEnv: exportHermesAPIKeyEnv, + exportHint: "export " + exportHermesAPIKeyEnv + "=", format: exportFormatYAML, build: buildExportHermesClientConfig}) + add(exportClientSpec{id: "openclaw", filename: "openclaw.json5", apiKeyEnv: exportOpenClawAPIKeyEnv, + exportHint: "export " + exportOpenClawAPIKeyEnv + "=", format: exportFormatJSON5, build: buildExportOpenclawClientConfig}) + add(exportClientSpec{id: "kimi", filename: "kimi-config.toml", exportHint: "Kimi Code reads credentials from its config file; loopback needs no key.", format: exportFormatTOML, build: buildExportKimiClientConfig}) + add(exportClientSpec{id: "gajae", filename: "gajae-models.yaml", apiKeyEnv: exportGajaeAPIKeyEnv, + exportHint: "export " + exportGajaeAPIKeyEnv + "=", format: exportFormatYAML, build: buildExportGajaeClientConfig}) + add(exportClientSpec{id: "dsh", filename: "settings.yaml", exportHint: "DSH uses a non-secret loopback bearer placeholder in settings.yaml; loopback needs no key.", format: exportFormatYAML, build: buildExportDshClientConfig}) + add(exportClientSpec{id: "mcode", filename: "mcode-config.yaml", exportHint: "MiniMax Code reads a non-secret placeholder from config.yaml; loopback needs no key.", format: exportFormatYAML, build: buildExportMcodeClientConfig}) + add(exportClientSpec{id: "zcode", filename: "config.json", exportHint: "ZCode reads a non-secret placeholder from v2/config.json; loopback needs no key.", format: exportFormatJSON, build: buildExportZcodeClientConfig}) + add(exportClientSpec{id: "prime", filename: "prime-models.json", exportHint: "Prime Agent reads a non-secret placeholder from models.json; loopback needs no key.", format: exportFormatJSON, build: buildExportPiClientConfig}) + add(exportClientSpec{id: "aside", filename: "aside-models.json", exportHint: "Aside reads a non-secret placeholder from models.json; loopback needs no key.", format: exportFormatJSON, build: buildExportPiClientConfig}) + return specs +} + +// ───────────────────────────────────────────────────────────────────────────── +// Destination path resolution (port of the config-export.ts path helpers). + +// exportHome mirrors homedir() through the process environment. +func exportHome() string { + if home := os.Getenv("HOME"); home != "" { + return home + } + if home, err := os.UserHomeDir(); err == nil { + return home + } + return "." +} + +func exportJoinPath(parts ...string) string { return filepath.Join(parts...) } + +func exportFileExists(path string) bool { + info, err := os.Stat(path) + return err == nil && !info.IsDir() +} + +func exportEnvValue(env map[string]string, key string) string { return env[key] } + +// exportAbsoluteClientPath mirrors absoluteClientPath: expand ~ against home, +// refuse anything still relative. +func exportAbsoluteClientPath(raw, home, variable string) (string, error) { + trimmed := strings.TrimSpace(raw) + if trimmed == "~" { + return home, nil + } + if strings.HasPrefix(trimmed, "~/") || strings.HasPrefix(trimmed, `~\`) { + return exportJoinPath(home, trimmed[2:]), nil + } + if !filepath.IsAbs(trimmed) { + return "", errors.New(variable + " must be an absolute path or start with ~; \"" + trimmed + "\" depends on the working directory, so opencodex and the client would disagree about which file it names.") + } + return trimmed, nil +} + +func exportOmpProfileName(env map[string]string) (string, error) { + raw, hasOMP := env["OMP_PROFILE"] + if !hasOMP { + raw = env["PI_PROFILE"] + } + profile := strings.TrimSpace(raw) + if profile == "" || profile == "default" { + return "", nil + } + valid := len(profile) >= 1 && len(profile) <= 64 && isASCIILowerAlnumDotDashUnderscore(profile) + reserved := regexpExportWindowsReserved(profile) + if profile == "." || profile == ".." || strings.HasSuffix(profile, ".") || !valid || reserved { + return "", errors.New("Invalid OMP profile \"" + raw + "\"") + } + return profile, nil +} + +func isASCIILowerAlnumDotDashUnderscore(value string) bool { + for index, character := range value { + if index == 0 { + if !(character >= 'a' && character <= 'z' || character >= '0' && character <= '9') { + return false + } + continue + } + ok := character >= 'a' && character <= 'z' || + character >= '0' && character <= '9' || + character == '.' || character == '_' || character == '-' + if !ok { + return false + } + } + return true +} + +var exportWindowsReserved = []string{"CON", "PRN", "AUX", "NUL", "COM0", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9", "LPT0", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9"} + +func regexpExportWindowsReserved(value string) bool { + upper := strings.ToUpper(value) + for _, reserved := range exportWindowsReserved { + if upper == reserved || strings.HasPrefix(upper, reserved+".") { + return true + } + } + return false +} + +func exportPiAgentDir(env map[string]string, home string) (string, error) { + if override := strings.TrimSpace(env["PI_CODING_AGENT_DIR"]); override != "" { + return exportAbsoluteClientPath(override, home, "PI_CODING_AGENT_DIR") + } + return exportJoinPath(home, ".pi", "agent"), nil +} + +func exportPiConfigPath(env map[string]string, home string) (string, error) { + dir, err := exportPiAgentDir(env, home) + if err != nil { + return "", err + } + return exportJoinPath(dir, "models.json"), nil +} + +func exportOmpAgentDir(env map[string]string, home string) (string, error) { + profile, err := exportOmpProfileName(env) + if err != nil { + return "", err + } + if profile == "" { + if override := strings.TrimSpace(env["PI_CODING_AGENT_DIR"]); override != "" { + return exportAbsoluteClientPath(override, home, "PI_CODING_AGENT_DIR") + } + } + rootName := env["PI_CONFIG_DIR"] + if rootName == "" { + rootName = ".omp" + } + root := exportJoinPath(home, rootName) + if profile != "" { + return exportJoinPath(root, "profiles", profile, "agent"), nil + } + return exportJoinPath(root, "agent"), nil +} + +func exportOmpModelsConfigPath(env map[string]string, home string) (string, error) { + agentDir, err := exportOmpAgentDir(env, home) + if err != nil { + return "", err + } + yamlFallback := exportJoinPath(agentDir, "models.yaml") + canonical := exportJoinPath(agentDir, "models.yml") + if !exportFileExists(canonical) && exportFileExists(yamlFallback) { + return yamlFallback, nil + } + return canonical, nil +} + +func exportOpenCodeGlobalConfigPath(env map[string]string, home string) string { + xdg := env["XDG_CONFIG_HOME"] + if xdg == "" { + xdg = exportJoinPath(home, ".config") + } + return exportJoinPath(xdg, "opencode", "opencode.json") +} + +func exportHermesHomeDir(env map[string]string, home string) string { + if override := strings.TrimSpace(env["HERMES_HOME"]); override != "" { + return override + } + if os.PathSeparator == '\\' { + local := strings.TrimSpace(env["LOCALAPPDATA"]) + if local == "" { + local = exportJoinPath(home, "AppData", "Local") + } + return exportJoinPath(local, "hermes") + } + return exportJoinPath(home, ".hermes") +} + +func exportHermesConfigPath(env map[string]string, home string) string { + return exportJoinPath(exportHermesHomeDir(env, home), "config.yaml") +} + +func exportOpenclawEffectiveHome(env map[string]string, home string) (string, error) { + if override := strings.TrimSpace(env["OPENCLAW_HOME"]); override != "" { + return exportAbsoluteClientPath(override, home, "OPENCLAW_HOME") + } + return home, nil +} + +func exportOpenclawHomeDir(env map[string]string, home string) (string, error) { + effectiveHome, err := exportOpenclawEffectiveHome(env, home) + if err != nil { + return "", err + } + if stateDir := strings.TrimSpace(env["OPENCLAW_STATE_DIR"]); stateDir != "" { + return exportAbsoluteClientPath(stateDir, effectiveHome, "OPENCLAW_STATE_DIR") + } + profile := strings.TrimSpace(env["OPENCLAW_PROFILE"]) + if profile != "" && !strings.EqualFold(profile, "default") { + return exportJoinPath(effectiveHome, ".openclaw-"+profile), nil + } + modern := exportJoinPath(effectiveHome, ".openclaw") + if exportFileExists(modern) { + return modern, nil + } + legacy := exportJoinPath(effectiveHome, ".clawdbot") + if exportFileExists(legacy) { + return legacy, nil + } + return modern, nil +} + +func exportOpenclawConfigPath(env map[string]string, home string) (string, error) { + effectiveHome, err := exportOpenclawEffectiveHome(env, home) + if err != nil { + return "", err + } + if explicit := strings.TrimSpace(env["OPENCLAW_CONFIG_PATH"]); explicit != "" { + return exportAbsoluteClientPath(explicit, effectiveHome, "OPENCLAW_CONFIG_PATH") + } + stateOverride := strings.TrimSpace(env["OPENCLAW_STATE_DIR"]) + profile := strings.TrimSpace(env["OPENCLAW_PROFILE"]) + scoped := stateOverride != "" || (profile != "" && !strings.EqualFold(profile, "default")) + stateDir, err := exportOpenclawHomeDir(env, home) + if err != nil { + return "", err + } + var candidates []string + if scoped { + candidates = []string{exportJoinPath(stateDir, "openclaw.json"), exportJoinPath(stateDir, "clawdbot.json")} + } else { + candidates = []string{ + exportJoinPath(effectiveHome, ".openclaw", "openclaw.json"), + exportJoinPath(effectiveHome, ".openclaw", "clawdbot.json"), + exportJoinPath(effectiveHome, ".clawdbot", "openclaw.json"), + exportJoinPath(effectiveHome, ".clawdbot", "clawdbot.json"), + } + } + for _, candidate := range candidates { + if exportFileExists(candidate) { + return candidate, nil + } + } + return candidates[0], nil +} + +func exportKimiConfigPath(env map[string]string, home string) string { + override := strings.TrimSpace(env["KIMI_CODE_HOME"]) + if override != "" { + return exportJoinPath(override, "config.toml") + } + return exportJoinPath(home, ".kimi-code", "config.toml") +} + +func exportGajaeConfigPath(env map[string]string, home string) string { + return exportJoinPath(home, ".gjc", "agent", "models.yml") +} + +func exportDshHomeDir(env map[string]string, home string) (string, error) { + raw, present := env["DSH_HOME"] + if !present || strings.TrimSpace(raw) == "" { + return exportJoinPath(home, ".dsh"), nil + } + if raw == "~" { + return home, nil + } + if strings.HasPrefix(raw, "~/") || strings.HasPrefix(raw, `~\`) { + return exportJoinPath(home, raw[2:]), nil + } + if !filepath.IsAbs(raw) { + return "", errors.New("DSH_HOME must be an absolute path or start with ~; \"" + raw + "\" depends on the working directory, so opencodex and DSH would disagree about which settings file it names.") + } + abs, err := filepath.Abs(raw) + if err != nil { + return "", err + } + return abs, nil +} + +func exportDshConfigPath(env map[string]string, home string) (string, error) { + dir, err := exportDshHomeDir(env, home) + if err != nil { + return "", err + } + return exportJoinPath(dir, "settings.yaml"), nil +} + +func exportMcodeHomeDir(env map[string]string, home string) (string, error) { + if primary := strings.TrimSpace(env["MINIMAX_DATA_DIR"]); primary != "" { + return exportAbsoluteClientPath(primary, home, "MINIMAX_DATA_DIR") + } + if legacy := strings.TrimSpace(env["MAVIS_DATA_DIR"]); legacy != "" { + return exportAbsoluteClientPath(legacy, home, "MAVIS_DATA_DIR") + } + return exportJoinPath(home, ".minimax"), nil +} + +func exportMcodeConfigPath(env map[string]string, home string) (string, error) { + dir, err := exportMcodeHomeDir(env, home) + if err != nil { + return "", err + } + return exportJoinPath(dir, "config.yaml"), nil +} + +func exportZcodeConfigPath(env map[string]string, home string) (string, error) { + var dir string + if override := strings.TrimSpace(env["ZCODE_DATA_DIR"]); override != "" { + expanded, err := exportAbsoluteClientPath(override, home, "ZCODE_DATA_DIR") + if err != nil { + return "", err + } + dir = expanded + } else { + dir = exportJoinPath(home, ".zcode") + } + return exportJoinPath(dir, "v2", "config.json"), nil +} + +func exportPrimeConfigPath(env map[string]string, home string) (string, error) { + var dir string + if override := strings.TrimSpace(env["PRIME_AGENT_CODING_AGENT_DIR"]); override != "" { + expanded, err := exportAbsoluteClientPath(override, home, "PRIME_AGENT_CODING_AGENT_DIR") + if err != nil { + return "", err + } + dir = expanded + } else { + dir = exportJoinPath(home, ".prime", "agent") + } + return exportJoinPath(dir, "models.json"), nil +} + +func exportAsideCurrentAccountID(root string) (int, error) { + manifest := exportJoinPath(root, "accounts.json") + raw, err := os.ReadFile(manifest) + if err != nil { + return 0, errors.New("Aside's account manifest is missing or unreadable at " + manifest + ", so opencodex cannot tell which account's model catalog to write. Launch Aside once to create it.") + } + value, parseErr := jsonwire.Parse(raw) + if parseErr != nil || value.Kind() != jsonwire.Object { + return 0, errors.New("Aside's account manifest at " + manifest + " is not readable JSON, so the account it names cannot be trusted. Writing a guessed account would target a different account's catalog.") + } + idField := value.Find("currentAccountId") + if idField == nil || idField.Kind() != jsonwire.Number { + return 0, errors.New("Aside's account manifest at " + manifest + " declares no usable currentAccountId, so opencodex cannot tell which account is current.") + } + id := idField.NumberRaw() + parsed, err := strconvExportAtoi(id) + if err != nil || parsed < 0 { + return 0, errors.New("Aside's account manifest at " + manifest + " declares no usable currentAccountId, so opencodex cannot tell which account is current.") + } + return parsed, nil +} + +func strconvExportAtoi(raw string) (int, error) { + var out int + for _, character := range raw { + if character < '0' || character > '9' { + return 0, errors.New("not an integer") + } + out = out*10 + int(character-'0') + } + return out, nil +} + +func exportAsideConfigPath(env map[string]string, home string) (string, error) { + root := exportJoinPath(home, ".aside") + id, err := exportAsideCurrentAccountID(root) + if err != nil { + return "", err + } + return exportJoinPath(root, "u", fmt.Sprintf("%d", id), "models.json"), nil +} + +// exportClientDestination mirrors spec.destination(env). +func exportClientDestination(id string, env map[string]string, home string) (string, error) { + switch id { + case "opencode": + return exportOpenCodeGlobalConfigPath(env, home), nil + case "pi": + return exportPiConfigPath(env, home) + case "omp": + return exportOmpModelsConfigPath(env, home) + case "hermes": + return exportHermesConfigPath(env, home), nil + case "openclaw": + return exportOpenclawConfigPath(env, home) + case "kimi": + return exportKimiConfigPath(env, home), nil + case "gajae": + return exportGajaeConfigPath(env, home), nil + case "dsh": + return exportDshConfigPath(env, home) + case "mcode": + return exportMcodeConfigPath(env, home) + case "zcode": + return exportZcodeConfigPath(env, home) + case "prime": + return exportPrimeConfigPath(env, home) + case "aside": + return exportAsideConfigPath(env, home) + } + return "", errors.New("unknown export client " + id) +} + +// ───────────────────────────────────────────────────────────────────────────── +// Command entry. + +func exportEnv() map[string]string { + out := map[string]string{} + for _, entry := range os.Environ() { + if index := strings.Index(entry, "="); index >= 0 { + out[entry[:index]] = entry[index+1:] + } + } + return out +} + +func readExportConfig() (exportConfigView, *jsonwire.Value, error) { + view := exportConfigView{} + dir, err := config.Dir() + if err != nil { + return view, nil, nil + } + path := filepath.Join(dir, "config.json") + raw, readErr := os.ReadFile(path) + if readErr != nil { + if os.IsNotExist(readErr) { + return view, nil, nil + } + return view, nil, errors.New("Could not load opencodex config at " + path + ": " + readErr.Error()) + } + value, parseErr := jsonwire.Parse(raw) + if parseErr != nil { + return view, nil, errors.New("Could not load opencodex config at " + path + ": " + parseErr.Error()) + } + if value.Kind() != jsonwire.Object { + return view, value, nil + } + if hostname := value.Find("hostname"); hostname != nil && hostname.Kind() == jsonwire.String { + view.hostname = hostname.String() + } + if providers := value.Find("providers"); providers != nil && providers.Kind() == jsonwire.Object { + if openai := providers.Find("openai"); openai != nil && openai.Kind() == jsonwire.Object { + if mode := openai.Find("codexAccountMode"); mode != nil && mode.Kind() == jsonwire.String { + view.codexDirect = codexAccountModeDirect(mode.String()) + } + } + } + if combos := value.Find("combos"); combos != nil { + view.combos = combos + } + if listener := value.Find("unauthenticatedLoopbackListener"); listener != nil && listener.Kind() == jsonwire.Object { + if enabled := listener.Find("enabled"); enabled != nil && enabled.Kind() == jsonwire.Bool { + view.unauthLoopback = enabled.Bool() + } + } + return view, value, nil +} + +// exportHasContextLimit mirrors hasContextLimit for the degraded-count line. +func exportHasContextLimit(model exportModel) bool { + return model.hasContextWindow && model.contextWindow > 0 +} + +// exportModelsFromProxyRowsRaw decodes a /api/models array. +func exportModelsFromProxyRowsRaw(rowsValue *jsonwire.Value, config exportConfigView) []exportModel { + if rowsValue == nil || rowsValue.Kind() != jsonwire.Array { + return nil + } + rows := make([]exportModelRow, 0, len(rowsValue.Elements())) + for _, element := range rowsValue.Elements() { + rows = append(rows, decodeExportModelRow(element)) + } + return exportModelsFromProxyRows(rows, config.codexDirect) +} + +// exportWriteFile mirrors writeExport: `wx` refusal without --force. +func exportWriteFile(path, text string, force bool) error { + flags := os.O_WRONLY | os.O_CREATE + if !force { + flags |= os.O_EXCL + } + file, err := os.OpenFile(path, flags, 0o666) + if err != nil { + if os.IsExist(err) { + return errors.New(path + " already exists. Re-run with --force to replace it, or print the config and merge it yourself.") + } + return err + } + defer file.Close() + if _, err := io.WriteString(file, text); err != nil { + return err + } + return nil +} + +func runExport(argv []string, deps Deps) int { + rest := append([]string(nil), argv...) + // takeOption mirrors (missing flag is distinct from a present flag with no + // value): the former reports `--client is required (...)`, the latter + // `--client requires a value` without a usage block. + client, clientGiven, err := takeUsageOption(&rest, "--client") + clientFlagPresent := containsString(rest, "--client") + if err != nil && clientFlagPresent { + return exportUsageError(deps, err, false) + } + if !clientGiven { + return exportUsageError(deps, errors.New("--client is required ("+strings.Join(exportClientIDs, ", ")+")"), true) + } + client = strings.ToLower(strings.TrimSpace(client)) + if !exportIsClientID(client) { + return exportUsageError(deps, errors.New("--client must be one of: "+strings.Join(exportClientIDs, ", ")), true) + } + wantsJSON := takeUsageFlag(&rest, "--json") + force := takeUsageFlag(&rest, "--force") + out, outGiven, err := takeUsageOption(&rest, "--out") + outFlagPresent := containsString(rest, "--out") + if err != nil && outFlagPresent { + return exportUsageError(deps, err, false) + } + if ok, code := exportRejectArgs(deps, rest); !ok { + return code + } + spec := exportSpecs()[client] + + configView, _, configErr := readExportConfig() + if configErr != nil { + fmt.Fprintln(deps.Stderr, "Error: "+configErr.Error()) + return 1 + } + state, found := liveProxyEndpoint(deps) + if !found { + fmt.Fprintln(deps.Stderr, "Error: Proxy is not running. Start it with: ocx start") + return 1 + } + root := baseURL(state) + body, rawText, responseStatus, fetchErr := fetchManagementJSON(deps, http.MethodGet, "/api/models", nil) + if fetchErr != nil { + return observeReportAPIError(deps, fetchErr.Error(), responseStatus) + } + if responseStatus < 200 || responseStatus >= 300 { + return observeReportAPIError(deps, usageResponseMessage(body, rawText, responseStatus), responseStatus) + } + rowsValue := body + if rowsValue == nil || rowsValue.Kind() != jsonwire.Array { + message := "Management API returned an unexpected /api/models payload." + fmt.Fprintln(deps.Stderr, "Error: "+message) + return 1 + } + models := exportModelsFromProxyRowsRaw(rowsValue, configView) + ctx := exportContext{baseURL: root + "/v1", models: models, config: configView} + document := spec.build(ctx) + text, serializeErr := serializeExportDocument(document, spec.format) + if serializeErr != nil { + fmt.Fprintln(deps.Stderr, "Error: "+serializeErr.Error()) + return 1 + } + if outGiven { + if writeErr := exportWriteFile(out, text, force); writeErr != nil { + if exportIsCliUsage(writeErr) { + return exportUsageError(deps, writeErr, true) + } + fmt.Fprintln(deps.Stderr, "Error: "+writeErr.Error()) + return 1 + } + if wantsJSON { + fmt.Fprintln(deps.Stderr, "Wrote "+out) + } + } + degraded := 0 + for _, model := range models { + if !exportHasContextLimit(model) { + degraded++ + } + } + if wantsJSON { + if err := observePrintPayload(deps, document, ""); err != nil { + fmt.Fprintln(deps.Stderr, "Error: "+err.Error()) + return 1 + } + return 0 + } + fmt.Fprintln(deps.Stdout, strings.TrimRight(text, "\n")) + fmt.Fprintln(deps.Stdout, "") + if outGiven { + fmt.Fprintln(deps.Stdout, "Wrote "+out) + } + destination, destErr := exportClientDestination(client, exportEnv(), exportHome()) + if destErr != nil { + fmt.Fprintln(deps.Stderr, "Error: "+destErr.Error()) + return 1 + } + fmt.Fprintln(deps.Stdout, "Destination: "+destination) + fmt.Fprintln(deps.Stdout, "Merge this generated configuration into that file; do not replace it.") + fmt.Fprintln(deps.Stdout, "Before launching: "+spec.exportHint) + count := len(models) + plural := "models" + if count == 1 { + plural = "model" + } + fmt.Fprintf(deps.Stdout, "%d %s; %d omit context limits (the client applies its own defaults).\n", count, plural, degraded) + return 0 +} + +func exportIsCliUsage(err error) bool { + return strings.Contains(err.Error(), "already exists") +} + +func exportUsageError(deps Deps, err error, withUsage bool) int { + fmt.Fprintln(deps.Stderr, "Error: "+err.Error()) + if withUsage { + fmt.Fprintln(deps.Stderr, exportUsageUsage) + } + return usageExitUsage +} + +// exportRejectArgs mirrors the export command's rejectArgs(args, USAGE): +// leftover positional arguments print the export USAGE block, not observe's. +func exportRejectArgs(deps Deps, rest []string) (ok bool, code int) { + if len(rest) == 0 { + return true, 0 + } + return false, exportUsageError(deps, errors.New("Unexpected argument(s): "+strings.Join(rest, " ")), true) +} diff --git a/go/internal/ocxcli/export_models.go b/go/internal/ocxcli/export_models.go new file mode 100644 index 0000000000..d40382b3fd --- /dev/null +++ b/go/internal/ocxcli/export_models.go @@ -0,0 +1,389 @@ +package ocxcli + +import ( + "math" + "sort" + "strconv" + "strings" + + "github.com/lidge-jun/opencodex/go/internal/jsonwire" +) + +// export_models.go — the /api/models row → client model pipeline behind +// `ocx export`. Ports the catalog visibility rules (opencodeCatalogFromProxyRows +// in src/cli/opencode.ts), the ExportModel projection and per-client gating in +// src/clients/config-export.ts, and the shared reasoning-effort helpers. + +// codexReasoningLadder is the canonical Codex order (low..ultra). +var codexReasoningLadder = []string{"low", "medium", "high", "xhigh", "max", "ultra"} + +// canonicalizeExportReasoningEfforts mirrors canonicalizeReasoningEfforts: +// `none`/`minimal` sentinels first (that order), then ladder members in +// canonical order, duplicates dropped. +func canonicalizeExportReasoningEfforts(values []string) []string { + seen := map[string]bool{} + for _, value := range values { + seen[value] = true + } + out := []string{} + for _, sentinel := range []string{"none", "minimal"} { + if seen[sentinel] { + out = append(out, sentinel) + } + } + for _, effort := range codexReasoningLadder { + if seen[effort] { + out = append(out, effort) + } + } + return out +} + +// sanitizeExportReasoningEfforts mirrors sanitizeCodexReasoningEfforts: keep +// only sentinels + ladder members, drop duplicates, order by ladder rank with +// sentinels first (stable among equal ranks, like V8 sort). +func sanitizeExportReasoningEfforts(efforts []string) []string { + if efforts == nil { + return nil + } + seen := map[string]bool{} + rank := map[string]int{} + for index, effort := range codexReasoningLadder { + rank[effort] = index + } + kept := make([]string, 0, len(efforts)) + position := map[string]int{} + for _, effort := range efforts { + valid := effort == "none" || effort == "minimal" + if !valid { + if _, ok := rank[effort]; !ok { + continue + } + } + if seen[effort] { + continue + } + seen[effort] = true + position[effort] = len(kept) + kept = append(kept, effort) + } + out := append([]string(nil), kept...) + sort.SliceStable(out, func(i, j int) bool { + return exportEffortRank(out[i]) < exportEffortRank(out[j]) + }) + _ = position + return out +} + +func exportEffortRank(effort string) int { + if effort == "none" || effort == "minimal" { + return -1 + } + for index, candidate := range codexReasoningLadder { + if candidate == effort { + return index + } + } + return -1 +} + +// exportModelRow is one /api/models row decoded into the fields the catalog +// pipeline reads. +type exportModelRow struct { + namespaced string + provider string + id string + native bool + disabled bool + displayName string + displayNameSource string + contextWindow *jsonwire.Value + reasoningEfforts []string + defaultReasoningEffort string + inputModalities []string +} + +func decodeExportModelRow(row *jsonwire.Value) exportModelRow { + var out exportModelRow + if row == nil || row.Kind() != jsonwire.Object { + return out + } + field := func(key string) *jsonwire.Value { return row.Find(key) } + out.namespaced = exportMemberString(field("namespaced")) + out.provider = exportMemberString(field("provider")) + out.id = exportMemberString(field("id")) + out.native = exportMemberBool(field("native")) + out.disabled = exportMemberBool(field("disabled")) + out.displayName = exportMemberString(field("displayName")) + out.displayNameSource = exportMemberString(field("displayNameSource")) + out.contextWindow = field("contextWindow") + out.defaultReasoningEffort = exportMemberString(field("defaultReasoningEffort")) + out.reasoningEfforts = exportMemberStringArray(field("reasoningEfforts")) + out.inputModalities = exportMemberStringArray(field("inputModalities")) + return out +} + +func exportMemberString(value *jsonwire.Value) string { + if value == nil || value.Kind() != jsonwire.String { + return "" + } + return value.String() +} + +func exportMemberBool(value *jsonwire.Value) bool { + if value == nil || value.Kind() != jsonwire.Bool { + return false + } + return value.Bool() +} + +func exportMemberStringArray(value *jsonwire.Value) []string { + if value == nil || value.Kind() != jsonwire.Array { + return nil + } + out := []string{} + for _, element := range value.Elements() { + if element != nil && element.Kind() == jsonwire.String { + out = append(out, element.String()) + } + } + return out +} + +// exportCatalogModel mirrors OpencodeCatalogModel (the deduped catalog entry). +type exportCatalogModel struct { + namespaced string + native bool + provider string + id string + contextWindow float64 + hasContextWindow bool + displayName string + reasoningEfforts []string + defaultReasoningEffort string +} + +// exportModel mirrors ExportModel (provider/id/… rows after the projection). +type exportModel struct { + namespaced string + provider string + id string + native bool + displayName string + contextWindow float64 + hasContextWindow bool + inputModalities []string + reasoningEfforts []string + defaultReasoningEffort string +} + +// codexAccountModeDirect mirrors providerCodexAccountMode("openai", …): +// persisted pool/direct wins; the canonical openai registry mode is "pool". +func codexAccountModeDirect(mode string) bool { + return mode == "direct" +} + +// opencodeCatalogFromProxyRows ports src/cli/opencode.ts: +// omitNative only under Codex Direct; disabled rows drop; first namespaced +// wins; displayName is dropped when displayNameSource is "fallback". +func opencodeCatalogFromProxyRows(rows []exportModelRow, codexDirect bool) []exportCatalogModel { + omitNative := codexDirect + seen := map[string]bool{} + catalog := []exportCatalogModel{} + for _, row := range rows { + namespaced := strings.TrimSpace(row.namespaced) + if namespaced == "" || row.disabled { + continue + } + if omitNative && row.native { + continue + } + if seen[namespaced] { + continue + } + seen[namespaced] = true + entry := exportCatalogModel{ + namespaced: namespaced, + native: row.native, + provider: row.provider, + id: row.id, + displayName: row.displayName, + reasoningEfforts: row.reasoningEfforts, + defaultReasoningEffort: row.defaultReasoningEffort, + } + if row.displayNameSource == "fallback" { + entry.displayName = "" + } + if row.contextWindow != nil && row.contextWindow.Kind() == jsonwire.Number { + if parsed, err := strconv.ParseFloat(row.contextWindow.NumberRaw(), 64); err == nil { + entry.contextWindow = parsed + entry.hasContextWindow = true + } + } + catalog = append(catalog, entry) + } + return catalog +} + +// exportModelsFromProxyRows ports exportModelsFromProxyRows: joins modalities +// by namespaced and projects the catalog entry into an ExportModel. +func exportModelsFromProxyRows(rows []exportModelRow, codexDirect bool) []exportModel { + modalities := map[string][]string{} + for _, row := range rows { + namespaced := strings.TrimSpace(row.namespaced) + if namespaced == "" { + continue + } + if _, exists := modalities[namespaced]; exists { + continue + } + if len(row.inputModalities) > 0 { + modalities[namespaced] = append([]string(nil), row.inputModalities...) + } + } + catalog := opencodeCatalogFromProxyRows(rows, codexDirect) + out := make([]exportModel, 0, len(catalog)) + for _, entry := range catalog { + model := exportModel{ + namespaced: entry.namespaced, + id: entry.id, + displayName: entry.displayName, + hasContextWindow: entry.hasContextWindow, + contextWindow: entry.contextWindow, + reasoningEfforts: entry.reasoningEfforts, + defaultReasoningEffort: entry.defaultReasoningEffort, + } + if entry.provider != "" { + model.provider = entry.provider + } else if entry.native { + model.provider = "openai" + } else { + model.provider = "routed" + } + if entry.native { + model.native = true + } + if entry.id == "" { + model.id = entry.namespaced + } + if input := modalities[entry.namespaced]; input != nil { + model.inputModalities = append([]string(nil), input...) + } + out = append(out, model) + } + return out +} + +// normalizeExportModels mirrors normalizeExportModels: dedupe by namespaced +// (first wins) then sort by namespaced ascending (JS string order). +func normalizeExportModels(models []exportModel) []exportModel { + seen := map[string]bool{} + unique := []exportModel{} + for _, model := range models { + if seen[model.namespaced] { + continue + } + seen[model.namespaced] = true + unique = append(unique, model) + } + sort.Slice(unique, func(i, j int) bool { return unique[i].namespaced < unique[j].namespaced }) + return unique +} + +// exportModelLabel mirrors exportModelLabel: " (native|provider|routed)". +func exportModelLabel(model exportModel) string { + providerLabel := "routed" + if model.native { + providerLabel = "native" + } else if model.provider != "" { + providerLabel = model.provider + } + id := model.id + if id == "" { + id = model.namespaced + } + if model.displayName != "" { + return model.displayName + " (" + providerLabel + ")" + } + return id + " (" + providerLabel + ")" +} + +// authoritativeExportContextWindow mirrors authoritativeContextWindow: +// a missing/non-finite/non-positive window has no authoritative value; floors +// positive windows (integer floor > 0 required). +func authoritativeExportContextWindow(model exportModel) (float64, bool) { + if !model.hasContextWindow || model.contextWindow <= 0 { + return 0, false + } + integer := math.Floor(model.contextWindow) + if integer <= 0 { + return 0, false + } + return integer, true +} + +const exportSchemaRequiredOutputBudget = 32_000.0 + +// outputBudgetForExport mirrors outputBudgetFor: min(32000, context). +func outputBudgetForExport(context float64) float64 { + if context < exportSchemaRequiredOutputBudget { + return context + } + return exportSchemaRequiredOutputBudget +} + +// inputModalitiesForExportClient mirrors inputModalitiesForClient (pi/gajae/ +// zcode accepted enum text|image): unknown→["text"], incompatible (no kept)→nil. +func inputModalitiesForExportClient(modalities []string) []string { + declared := modalities + if len(declared) == 0 { + return []string{"text"} + } + kept := []string{} + for _, value := range declared { + if (value == "text" || value == "image") && !exportContains(kept, value) { + kept = append(kept, value) + } + } + if len(kept) > 0 { + return kept + } + return nil +} + +// dshExportInputModalities mirrors dshInputModalities. +func dshExportInputModalities(modalities []string) []string { + declared := modalities + if len(declared) == 0 { + return []string{"text"} + } + kept := []string{} + for _, value := range declared { + if (value == "text" || value == "image") && !exportContains(kept, value) { + kept = append(kept, value) + } + } + if len(kept) > 0 { + return kept + } + allAudio := true + for _, value := range declared { + if value != "audio" { + allAudio = false + break + } + } + if allAudio { + return nil + } + return []string{"text"} +} + +func exportContains(values []string, want string) bool { + for _, value := range values { + if value == want { + return true + } + } + return false +} diff --git a/go/internal/ocxcli/export_serialize.go b/go/internal/ocxcli/export_serialize.go new file mode 100644 index 0000000000..a9700f78e6 --- /dev/null +++ b/go/internal/ocxcli/export_serialize.go @@ -0,0 +1,418 @@ +package ocxcli + +import ( + "errors" + "fmt" + "regexp" + "strings" + + "github.com/lidge-jun/opencodex/go/internal/jsonwire" +) + +// export_serialize.go — port of src/integrations/serialize.ts. Client export +// text formats: JSON (reuses encodeIndentedJSON), block-style YAML, and TOML +// render exactly like the TypeScript hand-renderers, plus the JSON5 spelling +// Bun.JSON5.stringify produces for the shallow identifier-keyed shapes the +// openclaw exporter emits (single-quoted strings, bare identifier keys). + +type exportFormat string + +const ( + exportFormatJSON exportFormat = "json" + exportFormatYAML exportFormat = "yaml" + exportFormatTOML exportFormat = "toml" + exportFormatJSON5 exportFormat = "json5" +) + +func serializeExportDocument(document *jsonwire.Value, format exportFormat) (string, error) { + switch format { + case exportFormatJSON: + var out strings.Builder + if err := encodeIndentedJSON(&out, document, 0); err != nil { + return "", err + } + return out.String() + "\n", nil + case exportFormatJSON5: + return renderJSON5(document, 0) + case exportFormatYAML: + lines, err := yamlExportLines(document, 0) + if err != nil { + return "", err + } + return strings.Join(lines, "\n") + "\n", nil + case exportFormatTOML: + if document.Kind() != jsonwire.Object { + return "", errors.New("TOML root must be a table") + } + return renderExportToml(document, ""), nil + } + return "", fmt.Errorf("unknown export format %s", format) +} + +// ───────────────────────────────────────────────────────────────────────────── +// JSON5 — mirrors Bun.JSON5.stringify(value, null, 2) for the identifier-keyed +// shapes the openclaw document emits. + +var exportIdentifierKeyRe = regexp.MustCompile(`^[A-Za-z_$][A-Za-z0-9_$]*$`) + +func json5String(value string) string { + var b strings.Builder + b.WriteByte('\'') + for _, character := range value { + switch character { + case '\\': + b.WriteString(`\\`) + case '\'': + b.WriteString(`\'`) + case '\b': + b.WriteString(`\b`) + case '\f': + b.WriteString(`\f`) + case '\n': + b.WriteString(`\n`) + case '\r': + b.WriteString(`\r`) + case '\t': + b.WriteString(`\t`) + default: + if character < 0x20 { + b.WriteString(fmt.Sprintf(`\u%04x`, character)) + } else { + b.WriteRune(character) + } + } + } + b.WriteByte('\'') + return b.String() +} + +func renderJSON5(value *jsonwire.Value, depth int) (string, error) { + switch value.Kind() { + case jsonwire.Null: + return "null", nil + case jsonwire.Bool: + if value.Bool() { + return "true", nil + } + return "false", nil + case jsonwire.Number: + return value.NumberRaw(), nil + case jsonwire.String: + return json5String(value.String()), nil + case jsonwire.Array: + elements := value.Elements() + if len(elements) == 0 { + return "[]", nil + } + var b strings.Builder + b.WriteString("[\n") + for _, element := range elements { + rendered, err := renderJSON5(element, depth+1) + if err != nil { + return "", err + } + b.WriteString(strings.Repeat(" ", depth+1)) + b.WriteString(rendered + ",\n") + } + b.WriteString(strings.Repeat(" ", depth)) + b.WriteString("]") + return b.String(), nil + case jsonwire.Object: + members := value.Members() + if len(members) == 0 { + return "{}", nil + } + var b strings.Builder + b.WriteString("{\n") + for _, member := range members { + key := member.Key + if !exportIdentifierKeyRe.MatchString(key) { + key = json5String(key) + } + rendered, err := renderJSON5(member.Value, depth+1) + if err != nil { + return "", err + } + b.WriteString(strings.Repeat(" ", depth+1)) + b.WriteString(key + ": " + rendered + ",\n") + } + b.WriteString(strings.Repeat(" ", depth)) + b.WriteString("}") + return b.String(), nil + } + return "", errors.New("cannot render json5 value") +} + +// ───────────────────────────────────────────────────────────────────────────── +// YAML — block style port of renderYaml. + +var exportYamlPlainRe = regexp.MustCompile(`^[A-Za-z_./][A-Za-z0-9_./-]*$`) +var exportYamlReservedRe = regexp.MustCompile(`(?i)^(?:null|true|false|yes|no|on|off|~|\.nan|[-+]?\.inf)$`) + +func yamlExportString(value string) string { + if value != "" && strings.TrimSpace(value) == value && + exportYamlPlainRe.MatchString(value) && !exportYamlReservedRe.MatchString(value) { + return value + } + quoted, err := jsonwire.EncodeString(value) + if err != nil { + return json5String(value) // unreachable for finite strings + } + return string(quoted) +} + +func yamlExportScalar(value *jsonwire.Value) (string, error) { + switch value.Kind() { + case jsonwire.Null: + return "null", nil + case jsonwire.String: + return yamlExportString(value.String()), nil + case jsonwire.Bool: + if value.Bool() { + return "true", nil + } + return "false", nil + case jsonwire.Number: + raw := value.NumberRaw() + return raw, nil + } + return "", errors.New("yaml cannot represent container value in scalar position") +} + +func yamlExportIsScalar(value *jsonwire.Value) bool { + if value == nil { + return false + } + switch value.Kind() { + case jsonwire.Null, jsonwire.String, jsonwire.Number, jsonwire.Bool: + return true + } + return false +} + +func yamlExportEmptyCollection(value *jsonwire.Value) (string, bool) { + if value == nil { + return "", false + } + if value.Kind() == jsonwire.Array && len(value.Elements()) == 0 { + return "[]", true + } + if value.Kind() == jsonwire.Object && len(value.Members()) == 0 { + return "{}", true + } + return "", false +} + +// yamlExportMapEntryLines renders `key: ` inside a map at indent. +func yamlExportMapEntryLines(key string, value *jsonwire.Value, indent int) ([]string, error) { + padding := strings.Repeat(" ", indent) + renderedKey := yamlExportString(key) + if yamlExportIsScalar(value) { + scalar, err := yamlExportScalar(value) + if err != nil { + return nil, err + } + return []string{padding + renderedKey + ": " + scalar}, nil + } + if empty, ok := yamlExportEmptyCollection(value); ok { + return []string{padding + renderedKey + ": " + empty}, nil + } + lines := []string{padding + renderedKey + ":"} + child, err := yamlExportLines(value, indent+2) + if err != nil { + return nil, err + } + return append(lines, child...), nil +} + +// yamlExportArrayMapLines renders a record as a `- key: value` sequence item. +func yamlExportArrayMapLines(value *jsonwire.Value, indent int) ([]string, error) { + members := value.Members() + padding := strings.Repeat(" ", indent) + if len(members) == 0 { + return []string{padding + "- {}"}, nil + } + first := members[0] + rest := members[1:] + renderedFirstKey := yamlExportString(first.Key) + var lines []string + if yamlExportIsScalar(first.Value) { + scalar, err := yamlExportScalar(first.Value) + if err != nil { + return nil, err + } + lines = append(lines, padding+"- "+renderedFirstKey+": "+scalar) + } else if empty, ok := yamlExportEmptyCollection(first.Value); ok { + lines = append(lines, padding+"- "+renderedFirstKey+": "+empty) + } else { + lines = append(lines, padding+"- "+renderedFirstKey+":") + child, err := yamlExportLines(first.Value, indent+4) + if err != nil { + return nil, err + } + lines = append(lines, child...) + } + for _, member := range rest { + entry, err := yamlExportMapEntryLines(member.Key, member.Value, indent+2) + if err != nil { + return nil, err + } + lines = append(lines, entry...) + } + return lines, nil +} + +func yamlExportLines(value *jsonwire.Value, indent int) ([]string, error) { + padding := strings.Repeat(" ", indent) + if yamlExportIsScalar(value) { + scalar, err := yamlExportScalar(value) + if err != nil { + return nil, err + } + return []string{padding + scalar}, nil + } + if value.Kind() == jsonwire.Array { + elements := value.Elements() + if len(elements) == 0 { + return []string{padding + "[]"}, nil + } + var lines []string + for _, item := range elements { + if yamlExportIsScalar(item) { + scalar, err := yamlExportScalar(item) + if err != nil { + return nil, err + } + lines = append(lines, padding+"- "+scalar) + continue + } + if item.Kind() == jsonwire.Object { + entry, err := yamlExportArrayMapLines(item, indent) + if err != nil { + return nil, err + } + lines = append(lines, entry...) + continue + } + if item.Kind() == jsonwire.Array { + if len(item.Elements()) == 0 { + lines = append(lines, padding+"- []") + } else { + lines = append(lines, padding+"-") + child, err := yamlExportLines(item, indent+2) + if err != nil { + return nil, err + } + lines = append(lines, child...) + } + continue + } + return nil, errors.New("yaml cannot represent this sequence item") + } + return lines, nil + } + if value.Kind() == jsonwire.Object { + members := value.Members() + if len(members) == 0 { + return []string{padding + "{}"}, nil + } + var lines []string + for _, member := range members { + entry, err := yamlExportMapEntryLines(member.Key, member.Value, indent) + if err != nil { + return nil, err + } + lines = append(lines, entry...) + } + return lines, nil + } + return nil, errors.New("yaml cannot represent this value") +} + +// ───────────────────────────────────────────────────────────────────────────── +// TOML — port of renderToml + tomlString + quoteTomlKey. + +var exportTomlBareKeyRe = regexp.MustCompile(`^[A-Za-z0-9_-]+$`) + +func exportTomlString(value string) string { + quoted, err := jsonwire.EncodeString(value) + if err != nil { + return value + } + return string(quoted) +} + +func exportQuoteTomlKey(key string) string { + if exportTomlBareKeyRe.MatchString(key) { + return key + } + return exportTomlString(key) +} + +func exportTomlScalar(value *jsonwire.Value) (string, error) { + switch value.Kind() { + case jsonwire.String: + return exportTomlString(value.String()), nil + case jsonwire.Bool: + if value.Bool() { + return "true", nil + } + return "false", nil + case jsonwire.Number: + return value.NumberRaw(), nil + case jsonwire.Array: + parts := make([]string, 0, len(value.Elements())) + for _, element := range value.Elements() { + rendered, err := exportTomlScalar(element) + if err != nil { + return "", err + } + parts = append(parts, rendered) + } + return "[" + strings.Join(parts, ", ") + "]", nil + case jsonwire.Object: + parts := make([]string, 0, len(value.Members())) + for _, member := range value.Members() { + rendered, err := exportTomlScalar(member.Value) + if err != nil { + return "", err + } + parts = append(parts, exportQuoteTomlKey(member.Key)+" = "+rendered) + } + return "{ " + strings.Join(parts, ", ") + " }", nil + } + return "", errors.New("toml cannot represent this value") +} + +// renderExportToml mirrors renderToml(document, prefix): scalars first in +// document order, then each nested table as a `[path]` section whose own +// scalars and tables follow recursively; sections join with blank lines and +// the whole document ends with one newline. +func renderExportToml(document *jsonwire.Value, prefix string) string { + var scalars []string + var tables []string + for _, member := range document.Members() { + path := exportQuoteTomlKey(member.Key) + if prefix != "" { + path = prefix + "." + exportQuoteTomlKey(member.Key) + } + if member.Value.Kind() == jsonwire.Object { + body := renderExportToml(member.Value, path) + tables = append(tables, "["+path+"]\n"+strings.TrimRight(body, "\n")) + } else { + rendered, err := exportTomlScalar(member.Value) + if err != nil { + return "" + } + scalars = append(scalars, exportQuoteTomlKey(member.Key)+" = "+rendered) + } + } + var sections []string + if len(scalars) > 0 { + sections = append(sections, strings.Join(scalars, "\n")) + } + if len(tables) > 0 { + sections = append(sections, strings.Join(tables, "\n\n")) + } + return strings.Join(sections, "\n\n") + "\n" +} diff --git a/go/internal/ocxcli/observe_command.go b/go/internal/ocxcli/observe_command.go new file mode 100644 index 0000000000..28b0114b95 --- /dev/null +++ b/go/internal/ocxcli/observe_command.go @@ -0,0 +1,751 @@ +package ocxcli + +import ( + "errors" + "fmt" + "io" + "math" + "net/http" + "net/url" + "strconv" + "strings" + "time" + + "github.com/lidge-jun/opencodex/go/internal/jsonwire" +) + +// ocx observe — the runtime-observation family (logs, storage, memory, debug, +// claude-inbound, injection) plus the already-flipped usage subcommand. This +// file ports the TypeScript owner (src/cli/observe.ts + the runtime-api shared +// taxonomy) so `ocx observe ` dispatches natively in the Go binary. +// +// `ocx observe` with no subcommand is `observe logs`, exactly like +// handleObserveCommand's `sub = "logs"` default. Unknown subcommands reproduce +// the TypeScript CliUsageError (exit 2, message + the observe USAGE block). +// +// The request-history indexer actions under `logs` (rebuild-index / +// index-status) read and write the Bun:sqlite-derived index directly with no +// management route, so they stay TypeScript-owned at the action level while the +// rest of the logs surface dispatches natively — the same seam `models live` +// and `codex-shim install` keep. +// +// Exit codes mirror runCliAction: usage errors exit 2 ("Error: " plus the +// observe USAGE block when the throw carried one); RuntimeApiError exits 4 on +// 404, 5 on 409, otherwise 1. + +// runObserve implements `ocx observe [ ...]`. argv is the slice BELOW the +// observe command itself (subcommand first), like handleObserveCommand's argv. +func runObserve(argv []string, deps Deps) int { + sub := "logs" + rest := argv + if len(argv) > 0 { + sub, rest = argv[0], argv[1:] + } + switch sub { + case "logs": + return observeLogs(rest, deps) + case "usage": + return runUsage(rest, deps) + case "storage": + return observeStorage(rest, deps) + case "memory": + return observeSimple("/api/system/memory", rest, deps) + case "debug": + return observeSimple("/api/debug", rest, deps) + case "claude-inbound": + return observeSimple("/api/claude/inbound-debug", rest, deps) + case "injection": + return observeSimple("/api/debug/injection-logs", rest, deps) + default: + return observeUsageError(deps, errors.New("unknown observe command "+sub), true) + } +} + +// observeUsageError mirrors a CliUsageError reaching runCliAction: message plus +// the observe USAGE block on stderr (when withUsage), exit 2. +func observeUsageError(deps Deps, err error, withUsage bool) int { + fmt.Fprintln(deps.Stderr, "Error: "+err.Error()) + if withUsage { + fmt.Fprintln(deps.Stderr, observeUsageUsage) + } + return usageExitUsage +} + +// observeRejectArgs mirrors rejectArgs without redaction (observe filters carry +// no secret options). ok is false when leftovers remain. +func observeRejectArgs(deps Deps, rest []string) (ok bool, code int) { + if len(rest) == 0 { + return true, 0 + } + return false, observeUsageError(deps, errors.New("Unexpected argument(s): "+strings.Join(rest, " ")), true) +} + +// takeUsageIntegerOption mirrors takeIntegerOption: the value is +// Number(raw.replace(/[_,]/g, "")) and must be an integer >= min. +func takeUsageIntegerOption(args *[]string, flag string, min int) (int, bool, error) { + raw, given, err := takeUsageOption(args, flag) + if err != nil || !given { + return 0, given, err + } + value := jsNumber(raw) + if value != math.Trunc(value) || math.IsInf(value, 0) || math.IsNaN(value) || value < float64(min) { + return 0, true, errors.New(flag + " must be an integer >= " + strconv.Itoa(min)) + } + if value >= math.MaxInt64 { + return math.MaxInt64, true, nil + } + if value <= math.MinInt64 { + return math.MinInt64, true, nil + } + return int(value), true, nil +} + +// jsNumber mirrors Number(raw) for the domain takeIntegerOption feeds it: +// commas/underscores stripped, surrounding whitespace tolerated, hex prefixes +// accepted, empty input is 0, trailing garbage is NaN. +func jsNumber(raw string) float64 { + cleaned := strings.ReplaceAll(strings.ReplaceAll(raw, ",", ""), "_", "") + trimmed := strings.TrimSpace(cleaned) + if trimmed == "" { + return 0 + } + if strings.HasPrefix(trimmed, "0x") || strings.HasPrefix(trimmed, "0X") { + if parsed, err := strconv.ParseInt(trimmed[2:], 16, 64); err == nil { + return float64(parsed) + } + return math.NaN() + } + parsed, err := strconv.ParseFloat(trimmed, 64) + if err != nil { + return math.NaN() + } + return parsed +} + +// ───────────────────────────────────────────────────────────────────────────── +// Shared management fetch — port of runtimeRequest in runtime-api.ts (same +// shape as fetchUsageReport in usage_command.go, parameterized for method and +// body so the codex-logs actions can POST). + +// fetchManagementJSON performs one management request through liveProxyEndpoint +// with the runtime admin-token header, exactly like runtimeRequest. On success +// the parsed body is returned (nil + rawText when the body is not valid JSON — +// runtimeRequest keeps the text in that case). Non-2xx responses are returned +// with their status; transport failures return the exact RuntimeApiError +// messages and a 503 status. +func fetchManagementJSON(deps Deps, method, path string, body []byte) (*jsonwire.Value, string, int, error) { + deps = defaults(deps) + state, found := liveProxyEndpoint(deps) + if !found { + return nil, "", 503, errors.New("Proxy is not running. Start it with: ocx start") + } + request, requestErr := http.NewRequest(method, baseURL(state)+path, strings.NewReader(string(body))) + if requestErr != nil { + return nil, "", 503, fmt.Errorf("Management API is unreachable: %s", requestErr) + } + request.Header.Set("Content-Type", "application/json") + if token := configuredUsageAdminToken(); token != "" { + request.Header.Set("X-OpenCodex-API-Key", token) + } + response, doErr := deps.HTTPClient.Do(request) + if doErr != nil { + return nil, "", 503, fmt.Errorf("Management API is unreachable: %s", doErr) + } + defer response.Body.Close() + raw, readErr := io.ReadAll(io.LimitReader(response.Body, 8*1024*1024)) + if readErr != nil { + return nil, "", 503, fmt.Errorf("Management API is unreachable: %s", readErr) + } + value, parseErr := jsonwire.Parse(raw) + if parseErr != nil { + return nil, string(raw), response.StatusCode, nil + } + return value, "", response.StatusCode, nil +} + +// observeReportAPIError reports a RuntimeApiError the way runCliAction does: +// message on stderr, exit 4 on 404, 5 on 409, otherwise 1. +func observeReportAPIError(deps Deps, message string, status int) int { + fmt.Fprintln(deps.Stderr, "Error: "+message) + switch status { + case 404: + return usageExitMissing + case 409: + return usageExitConflict + default: + return 1 + } +} + +// observeURLQuery mirrors the observe.ts query() helper: URLSearchParams.set for +// every provided param in declaration order (url.Values would sort keys). +func observeURLQuery(params []observeQueryParam) string { + var b strings.Builder + for _, param := range params { + if !param.present { + continue + } + if b.Len() == 0 { + b.WriteByte('?') + } else { + b.WriteByte('&') + } + b.WriteString(url.QueryEscape(param.key)) + b.WriteByte('=') + b.WriteString(url.QueryEscape(param.value)) + } + return b.String() +} + +type observeQueryParam struct { + key string + present bool + value string +} + +// observePrintPayload emits `--json` output: the server payload re-encoded with +// JSON.stringify(value, null, 2) (V8 rules) plus one newline, or a quoted +// string when the body was not JSON. +func observePrintPayload(deps Deps, body *jsonwire.Value, rawText string) error { + return writeUsageJSON(deps.Stdout, body, rawText) +} + +// ───────────────────────────────────────────────────────────────────────────── +// Human summary rendering — port of summaryLines in runtime-api.ts. + +// observeStringify mirrors String(value) for JSON payload values: V8 number +// spelling, container spellings for objects/arrays, the literal text for +// strings. An ABSENT jsonwire member reads as nil and stringifies to +// "undefined" exactly like an undefined property in TypeScript. +func observeStringify(value *jsonwire.Value) string { + if value == nil { + return "undefined" + } + switch value.Kind() { + case jsonwire.Null: + return "null" + case jsonwire.Bool: + if value.Bool() { + return "true" + } + return "false" + case jsonwire.Number: + return value.NumberRaw() + case jsonwire.String: + return value.String() + case jsonwire.Array: + elements := value.Elements() + parts := make([]string, 0, len(elements)) + for _, element := range elements { + if element == nil || element.Kind() == jsonwire.Null { + parts = append(parts, "") + } else { + parts = append(parts, observeStringify(element)) + } + } + return strings.Join(parts, ",") + default: + return "[object Object]" + } +} + +// observeSummaryLines mirrors summaryLines: compact human view for safe +// management DTOs. Depth caps at two object levels; arrays of scalars join +// with ", " ("none" when empty); deeper values stringify like String(value). +func observeSummaryLines(value *jsonwire.Value) []string { + return observeSummaryLinesAt(value, "", 0) +} + +// observeSummaryLinesAt mirrors summaryLines(value, prefix, depth): the depth>1 +// early exit stringifies the WHOLE value with String() (arrays join with ","); +// object entries iterate in document order; a root array iterates its index +// keys exactly like Object.entries([...]). +func observeSummaryLinesAt(value *jsonwire.Value, prefix string, depth int) []string { + if value == nil || depth > 1 || (value.Kind() != jsonwire.Object && value.Kind() != jsonwire.Array) { + return []string{observeSummaryLabel(prefix) + ": " + observeStringify(value)} + } + lines := []string{} + if value.Kind() == jsonwire.Array { + for index, element := range value.Elements() { + lines = append(lines, observeSummaryMember(strconv.Itoa(index), element, prefix, depth)...) + } + return lines + } + for _, member := range value.Members() { + lines = append(lines, observeSummaryMember(member.Key, member.Value, prefix, depth)...) + } + return lines +} + +// observeSummaryMember renders one Object.entries row the way summaryLines +// does: arrays of scalars join with ", " ("none" when empty), object children +// recurse one level, null/undefined/"" leaves become "-". +func observeSummaryMember(key string, child *jsonwire.Value, prefix string, depth int) []string { + label := key + if prefix != "" { + label = prefix + "." + key + } + if child != nil && child.Kind() == jsonwire.Array { + elements := child.Elements() + allScalars := true + for _, element := range elements { + if !observeIsScalar(element) { + allScalars = false + break + } + } + if allScalars { + joined := observeArrayJoin(elements) + if joined == "" { + joined = "none" + } + return []string{label + ": " + joined} + } + return []string{label + ": " + strconv.Itoa(len(elements)) + " item(s)"} + } + if child != nil && child.Kind() == jsonwire.Object { + if depth < 1 { + return observeSummaryLinesAt(child, label, depth+1) + } + return []string{label + ": " + observeStringify(child)} + } + scalar := observeStringify(child) + if child == nil || child.Kind() == jsonwire.Null || (child.Kind() == jsonwire.String && child.String() == "") { + scalar = "-" + } + return []string{label + ": " + scalar} +} + +func observeSummaryLabel(prefix string) string { + if prefix == "" { + return "value" + } + return prefix +} + +func observeIsScalar(value *jsonwire.Value) bool { + if value == nil { + return true // JSON null is a scalar member of the TS scalar set + } + switch value.Kind() { + case jsonwire.String, jsonwire.Number, jsonwire.Bool, jsonwire.Null: + return true + } + return false +} + +// observeArrayJoin mirrors Array.prototype.join(", "): null/undefined elements +// become empty strings, everything else String(element). +func observeArrayJoin(elements []*jsonwire.Value) string { + parts := make([]string, 0, len(elements)) + for _, element := range elements { + if element == nil || element.Kind() == jsonwire.Null { + parts = append(parts, "") + continue + } + parts = append(parts, observeStringify(element)) + } + return strings.Join(parts, ", ") +} + +// ───────────────────────────────────────────────────────────────────────────── +// Logs — port of logs()/logRows()/formatLog() in observe.ts. + +// observeLogField reads one member of a log row object, nil when absent. +type observeLogRow struct { + id, timestamp, createdAt, provider, model, status, statusCode, durationMs *jsonwire.Value +} + +func decodeLogRow(row *jsonwire.Value) observeLogRow { + var view observeLogRow + if row == nil || row.Kind() != jsonwire.Object { + return view + } + view.id = row.Find("id") + view.timestamp = row.Find("timestamp") + view.createdAt = row.Find("createdAt") + view.provider = row.Find("provider") + view.model = row.Find("model") + view.status = row.Find("status") + view.statusCode = row.Find("statusCode") + view.durationMs = row.Find("durationMs") + return view +} + +// observeFalsy mirrors Boolean(value): absent members, JSON null, empty +// strings, 0/-0, and false are falsy; everything else is truthy. +func observeFalsy(value *jsonwire.Value) bool { + if value == nil { + return true + } + switch value.Kind() { + case jsonwire.Null: + return true + case jsonwire.String: + return value.String() == "" + case jsonwire.Number: + raw := value.NumberRaw() + return raw == "0" || raw == "-0" + case jsonwire.Bool: + return !value.Bool() + } + return false +} + +// observeLogFormat mirrors formatLog: one human log line from a request row. +func observeLogFormat(row *jsonwire.Value) string { + view := decodeLogRow(row) + timeValue := "" + switch { + case view.timestamp != nil && view.timestamp.Kind() != jsonwire.Null: + timeValue = observeStringify(view.timestamp) + case view.createdAt != nil && view.createdAt.Kind() != jsonwire.Null: + timeValue = observeStringify(view.createdAt) + } + // `row.createdAt ?? ""` only when timestamp is absent/null: an explicitly + // null createdAt also lands here and stringifies to "". + route := "" + { + parts := make([]string, 0, 2) + for _, part := range []*jsonwire.Value{view.provider, view.model} { + if observeFalsy(part) { + continue + } + parts = append(parts, observeStringify(part)) + } + route = strings.Join(parts, "/") + } + // `row.status ?? row.statusCode ?? "?"`: nullish coalescing skips only + // absent/null members — 0 and "" and false are kept and stringified. + statusValue := "?" + for _, candidate := range []*jsonwire.Value{view.status, view.statusCode} { + if candidate == nil || candidate.Kind() == jsonwire.Null { + continue + } + statusValue = observeStringify(candidate) + break + } + duration := "" + if view.durationMs != nil { + duration = observeStringify(view.durationMs) + "ms" + } + conversation := "" + if row != nil && row.Kind() == jsonwire.Object { + if member := row.Find("conversationId"); member != nil && member.Kind() == jsonwire.String && member.String() != "" { + conversation = "conv=" + member.String() + } + } + parts := make([]string, 0, 5) + for _, part := range []string{timeValue, statusValue, route, duration, conversation} { + if part != "" { + parts = append(parts, part) + } + } + return strings.Join(parts, " ") +} + +// observeLogKey mirrors the follow dedup key in observe.ts logs(): +// String(row.id ?? `${timestamp}:${provider}:${model}:${status}`) — absent +// members read "undefined", null members read "null". +func observeLogKey(row *jsonwire.Value) string { + view := decodeLogRow(row) + if view.id != nil && view.id.Kind() != jsonwire.Null { + return observeStringify(view.id) + } + parts := make([]string, 0, 4) + for _, part := range []*jsonwire.Value{view.timestamp, view.provider, view.model, view.status} { + parts = append(parts, observeStringify(part)) + } + return strings.Join(parts, ":") +} + +// observeLogRows mirrors logRows: the payload's array, or the first array +// under logs/entries/requests. +func observeLogRows(data *jsonwire.Value) []*jsonwire.Value { + if data == nil { + return nil + } + if data.Kind() == jsonwire.Array { + return data.Elements() + } + if data.Kind() == jsonwire.Object { + for _, key := range []string{"logs", "entries", "requests"} { + if record := data.Find(key); record != nil && record.Kind() == jsonwire.Array { + return record.Elements() + } + } + } + return nil +} + +func observeLogs(argv []string, deps Deps) int { + rest := append([]string(nil), argv...) + // `ocx observe logs `: explain reads the management route natively; + // rebuild-index / index-status touch the Bun:sqlite index with no + // management route, so OwnershipFor keeps them TypeScript-owned and they + // delegate before this dispatch ever runs. The branch is defensive for + // direct (test) callers of runObserve. + if len(rest) > 0 { + switch rest[0] { + case "explain": + return observeLogsExplain(rest[1:], deps) + case "rebuild-index", "index-status": + return runDelegated(append([]string{"observe", "logs"}, rest...), deps) + } + } + + wantsJSON := takeUsageFlag(&rest, "--json") + wantsJSONL := takeUsageFlag(&rest, "--jsonl") + follow := takeUsageFlag(&rest, "--follow") || takeUsageFlag(&rest, "-f") + provider, providerGiven, err := takeUsageOption(&rest, "--provider") + if err != nil { + return observeUsageError(deps, err, false) + } + model, modelGiven, err := takeUsageOption(&rest, "--model") + if err != nil { + return observeUsageError(deps, err, false) + } + status, statusGiven, err := takeUsageOption(&rest, "--status") + if err != nil { + return observeUsageError(deps, err, false) + } + // Both spellings, because the server accepts both and an operator should + // not have to remember which one this surface wanted. + conversation, conversationGiven, err := takeUsageOption(&rest, "--conversation") + if err != nil { + return observeUsageError(deps, err, false) + } + if !conversationGiven { + conversation, conversationGiven, err = takeUsageOption(&rest, "--conversationId") + if err != nil { + return observeUsageError(deps, err, false) + } + } + limit := 200 + if limitRaw, limitGiven, err := takeUsageIntegerOption(&rest, "--limit", 1); err != nil { + return observeUsageError(deps, err, false) + } else if limitGiven { + limit = limitRaw + } + if ok, code := observeRejectArgs(deps, rest); !ok { + return code + } + if wantsJSON && wantsJSONL { + return observeUsageError(deps, errors.New("--json and --jsonl cannot be combined"), true) + } + if follow && wantsJSON { + return observeUsageError(deps, errors.New("--follow cannot be combined with --json; use --jsonl for streaming JSONL"), true) + } + + params := []observeQueryParam{ + {key: "provider", present: providerGiven, value: provider}, + {key: "model", present: modelGiven, value: model}, + {key: "status", present: statusGiven, value: status}, + {key: "conversationId", present: conversationGiven, value: conversation}, + {key: "limit", present: true, value: strconv.Itoa(limit)}, + } + query := observeURLQuery(params) + + // follow keeps insertion order so the size cap below mirrors + // `seen = new Set([...seen].slice(-2_500))` (map order would prune the + // wrong tail and change which rows a long-lived follow re-prints). + seenOrder := make([]string, 0, 64) + seen := map[string]bool{} + for { + body, rawText, responseStatus, fetchErr := fetchManagementJSON(deps, http.MethodGet, "/api/logs"+query, nil) + if fetchErr != nil { + return observeReportAPIError(deps, fetchErr.Error(), responseStatus) + } + if responseStatus < 200 || responseStatus >= 300 { + return observeReportAPIError(deps, usageResponseMessage(body, rawText, responseStatus), responseStatus) + } + rows := observeLogRows(body) + if !follow && wantsJSON { + if err := observePrintPayload(deps, body, rawText); err != nil { + fmt.Fprintln(deps.Stderr, "Error: "+err.Error()) + return 1 + } + } else { + for _, row := range rows { + key := observeLogKey(row) + if follow && seen[key] { + continue + } + if wantsJSONL { + encoded, err := row.Encode() + if err != nil { + fmt.Fprintln(deps.Stderr, "Error: "+err.Error()) + return 1 + } + fmt.Fprintln(deps.Stdout, string(encoded)) + } else { + fmt.Fprintln(deps.Stdout, observeLogFormat(row)) + } + if follow { + seen[key] = true + seenOrder = append(seenOrder, key) + } + } + } + if !follow { + return 0 + } + if len(seen) > 5_000 { + tail := seenOrder[len(seenOrder)-2_500:] + pruned := make(map[string]bool, len(tail)) + for _, key := range tail { + pruned[key] = true + } + seenOrder = append([]string(nil), tail...) + seen = pruned + } + time.Sleep(1 * time.Second) + } +} + +// observeLogsExplain mirrors explain(): GET the route-decision payload for one +// request id and print the JSON.stringify(result, null, 2) document — both +// --json and human spellings print the same pretty document. +func observeLogsExplain(argv []string, deps Deps) int { + rest := append([]string(nil), argv...) + requestID := "" + if len(rest) > 0 { + requestID = rest[0] + rest = rest[1:] + } + _ = takeUsageFlag(&rest, "--json") + if requestID == "" { + return observeUsageError(deps, errors.New("request id is required"), true) + } + if ok, code := observeRejectArgs(deps, rest); !ok { + return code + } + body, rawText, responseStatus, fetchErr := fetchManagementJSON(deps, http.MethodGet, "/api/request-history/"+url.PathEscape(requestID)+"/route-decision", nil) + if fetchErr != nil { + return observeReportAPIError(deps, fetchErr.Error(), responseStatus) + } + if responseStatus < 200 || responseStatus >= 300 { + return observeReportAPIError(deps, usageResponseMessage(body, rawText, responseStatus), responseStatus) + } + if err := observePrintPayload(deps, body, rawText); err != nil { + fmt.Fprintln(deps.Stderr, "Error: "+err.Error()) + return 1 + } + return 0 +} + +// ───────────────────────────────────────────────────────────────────────────── +// Simple management reads — port of simple() + printData. + +// observeSimple implements memory/debug/claude-inbound/injection: one GET with +// an optional --limit, then either the payload as pretty JSON (--json) or the +// compact summaryLines view. +func observeSimple(path string, argv []string, deps Deps) int { + rest := append([]string(nil), argv...) + wantsJSON := takeUsageFlag(&rest, "--json") + var limitParam *observeQueryParam + if limitRaw, limitGiven, err := takeUsageIntegerOption(&rest, "--limit", 1); err != nil { + return observeUsageError(deps, err, false) + } else if limitGiven { + limitParam = &observeQueryParam{key: "limit", present: true, value: strconv.Itoa(limitRaw)} + } + if ok, code := observeRejectArgs(deps, rest); !ok { + return code + } + params := []observeQueryParam{} + if limitParam != nil { + params = append(params, *limitParam) + } + body, rawText, responseStatus, fetchErr := fetchManagementJSON(deps, http.MethodGet, path+observeURLQuery(params), nil) + if fetchErr != nil { + return observeReportAPIError(deps, fetchErr.Error(), responseStatus) + } + if responseStatus < 200 || responseStatus >= 300 { + return observeReportAPIError(deps, usageResponseMessage(body, rawText, responseStatus), responseStatus) + } + if wantsJSON { + if err := observePrintPayload(deps, body, rawText); err != nil { + fmt.Fprintln(deps.Stderr, "Error: "+err.Error()) + return 1 + } + return 0 + } + for _, line := range observeSummaryLines(body) { + fmt.Fprintln(deps.Stdout, line) + } + return 0 +} + +// ───────────────────────────────────────────────────────────────────────────── +// Storage — port of storage() in observe.ts. + +func observeStorage(argv []string, deps Deps) int { + if len(argv) == 0 || argv[0] != "codex-logs" { + return observeSimple("/api/storage", argv, deps) + } + rest := append([]string(nil), argv[1:]...) + action := "status" + if len(rest) > 0 && !strings.HasPrefix(rest[0], "-") { + action = rest[0] + rest = rest[1:] + } + wantsJSON := takeUsageFlag(&rest, "--json") + mode, modeGiven, err := takeUsageOption(&rest, "--mode") + if err != nil { + return observeUsageError(deps, err, false) + } + if ok, code := observeRejectArgs(deps, rest); !ok { + return code + } + + var body *jsonwire.Value + var rawText string + var responseStatus int + var fetchErr error + switch action { + case "status": + if modeGiven { + return observeUsageError(deps, errors.New("--mode is only valid with codex-logs protect"), true) + } + body, rawText, responseStatus, fetchErr = fetchManagementJSON(deps, http.MethodGet, "/api/storage/codex-logs", nil) + case "protect": + requestedMode := "compat" + if modeGiven { + requestedMode = mode + } + if requestedMode != "compat" && requestedMode != "quiet" { + return observeUsageError(deps, errors.New("--mode must be compat or quiet"), true) + } + payload := jsonwire.ObjectValue() + payload.Set("mode", jsonwire.StringValue(requestedMode)) + encoded, encodeErr := payload.Encode() + if encodeErr != nil { + return observeUsageError(deps, encodeErr, false) + } + body, rawText, responseStatus, fetchErr = fetchManagementJSON(deps, http.MethodPost, "/api/storage/codex-logs/protect", encoded) + case "unprotect", "repair", "compact": + if modeGiven { + return observeUsageError(deps, errors.New("--mode is only valid with codex-logs protect"), true) + } + body, rawText, responseStatus, fetchErr = fetchManagementJSON(deps, http.MethodPost, "/api/storage/codex-logs/"+action, nil) + default: + return observeUsageError(deps, errors.New("unknown codex-logs action "+action), true) + } + if fetchErr != nil { + return observeReportAPIError(deps, fetchErr.Error(), responseStatus) + } + if responseStatus < 200 || responseStatus >= 300 { + return observeReportAPIError(deps, usageResponseMessage(body, rawText, responseStatus), responseStatus) + } + if wantsJSON { + if err := observePrintPayload(deps, body, rawText); err != nil { + fmt.Fprintln(deps.Stderr, "Error: "+err.Error()) + return 1 + } + return 0 + } + for _, line := range observeSummaryLines(body) { + fmt.Fprintln(deps.Stdout, line) + } + return 0 +} diff --git a/go/internal/ocxcli/takeover46_test.go b/go/internal/ocxcli/takeover46_test.go new file mode 100644 index 0000000000..497e5ae0dc --- /dev/null +++ b/go/internal/ocxcli/takeover46_test.go @@ -0,0 +1,98 @@ +package ocxcli + +import ( + "bytes" + "strings" + "testing" +) + +// The capabilities/observe/export families flipped to Go-owned dispatch in the +// same ticket; the parity suite locks byte-for-byte output against TypeScript, +// and these fast unit tests lock ownership routing and exit taxonomy without a +// live proxy (ADR-0008 issue #46). +func TestObserveAndExportOwnershipMap(t *testing.T) { + if got, known := OwnershipFor([]string{"capabilities"}); !known || got != GoOwned { + t.Fatalf("capabilities owner = %q, %t", got, known) + } + if got, known := OwnershipFor([]string{"export"}); !known || got != GoOwned { + t.Fatalf("export owner = %q, %t", got, known) + } + for verb, owner := range map[string]Ownership{ + "logs": GoOwned, + "usage": GoOwned, + "storage": GoOwned, + "memory": GoOwned, + "debug": GoOwned, + "claude-inbound": GoOwned, + "injection": GoOwned, + "wat": GoOwned, + } { + if got, known := OwnershipFor([]string{"observe", verb}); !known || got != owner { + t.Fatalf("observe %s owner = %q, %t", verb, got, known) + } + } + // The request-history indexer reads/writes the Bun:sqlite index directly + // (no management route), so it keeps the TypeScript owner at the action. + for _, action := range []string{"logs rebuild-index", "logs index-status"} { + args := strings.Split(action, " ") + if got, known := OwnershipFor(append([]string{"observe"}, args...)); !known || got != TypeScriptOwned { + t.Fatalf("observe %s owner = %q, %t (want TypeScriptOwned seam)", action, got, known) + } + } +} + +func TestCapabilitiesNativeStaticExitCodes(t *testing.T) { + var out, stderr bytes.Buffer + deps := depsFor(RuntimeState{}, &out, &stderr) + deps.Delegate = func([]string) (int, error) { t.Fatal("capabilities delegated"); return 0, nil } + if got := Run([]string{"capabilities"}, deps); got != ExitOK || out.Len() == 0 { + t.Fatalf("capabilities exit = %d stderr %q", got, stderr.String()) + } + out.Reset() + stderr.Reset() + if got := Run([]string{"capabilities", "--route", "/api/nope"}, deps); got != 4 { + t.Fatalf("unmatched --route exit = %d (want 4)", got) + } + stderr.Reset() + if got := Run([]string{"capabilities", "--route"}, deps); got != 64 { + t.Fatalf("empty --route exit = %d (want 64)", got) + } + if !strings.Contains(stderr.String(), "Usage: ocx capabilities --route ") { + t.Fatalf("empty --route stderr = %q", stderr.String()) + } +} + +func TestObserveUnknownSubcommandIsUsageErrorTwo(t *testing.T) { + var out, stderr bytes.Buffer + deps := depsFor(RuntimeState{}, &out, &stderr) + deps.Delegate = func([]string) (int, error) { t.Fatal("observe wat delegated"); return 0, nil } + if got := Run([]string{"observe", "wat"}, deps); got != usageExitUsage { + t.Fatalf("observe wat exit = %d stderr %q", got, stderr.String()) + } + if !strings.Contains(stderr.String(), "Error: unknown observe command wat") { + t.Fatalf("stderr = %q", stderr.String()) + } +} + +func TestExportArgumentErrorsAndOfflineProxy(t *testing.T) { + home := t.TempDir() + t.Setenv("OPENCODEX_HOME", home) + run := func(args ...string) (int, string) { + var out, stderr bytes.Buffer + deps := depsFor(RuntimeState{}, &out, &stderr) + deps.Delegate = func([]string) (int, error) { t.Fatal("export delegated"); return 0, nil } + return Run(args, deps), stderr.String() + } + if code, message := run("export", "--client", "nope"); code != usageExitUsage || + !strings.Contains(message, "--client must be one of:") || !strings.Contains(message, "Usage:") { + t.Fatalf("bad client = %d %q", code, message) + } + if code, message := run("export", "--client"); code != usageExitUsage || + !strings.Contains(message, "--client requires a value") || strings.Contains(message, "Usage:") { + t.Fatalf("missing value = %d %q", code, message) + } + if code, message := run("export", "--client", "pi"); code != ExitFailure || + !strings.Contains(message, "Proxy is not running. Start it with: ocx start") { + t.Fatalf("offline export = %d %q", code, message) + } +} diff --git a/tests/go-cli-parity.test.ts b/tests/go-cli-parity.test.ts index 0ac65bbb56..4b75c510c5 100644 --- a/tests/go-cli-parity.test.ts +++ b/tests/go-cli-parity.test.ts @@ -1,5 +1,5 @@ import { afterEach, describe, expect, test } from "bun:test"; -import { existsSync, mkdtempSync, writeFileSync } from "node:fs"; +import { existsSync, mkdirSync, mkdtempSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { dirname, join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; @@ -268,4 +268,189 @@ describe.skipIf(!goAvailable || goCLI === null)("Go CLI parity (ADR-0008, ticket expect(expectParity(["usage"])).toMatchObject({ code: 1 }); expect(expectParity(["observe", "usage"])).toMatchObject({ code: 1 }); }); + + // capabilities is Go-owned static data (ADR-0008 issue #46): the whole + // surface — human rows, --json envelope, --mutating-only, --route inverse + // lookup and its empty/missing-value exit — needs no live proxy. + test.each([ + { args: ["capabilities"] }, + { args: ["capabilities", "--json"] }, + { args: ["capabilities", "--mutating-only"] }, + { args: ["capabilities", "--mutating-only", "--json"] }, + { args: ["capabilities", "--route", "/api/status"] }, + { args: ["capabilities", "--route", "/api/nope"] }, + { args: ["capabilities", "--route", "/api/nope", "--json"] }, + { args: ["capabilities", "--route"] }, + { args: ["help", "capabilities"] }, + { args: ["capabilities", "--help"] }, + ])("diffs Go-owned capabilities output and exit code for $args", ({ args }) => { + testHome = mkdtempSync(join(tmpdir(), "ocx-go-cli-parity-")); + expectParity(args); + }); + + // observe dispatches natively per subcommand (ADR-0008 issue #46); the + // rebuild-index / index-status actions read the Bun:sqlite index directly, so + // they keep the TypeScript owner at the action level and delegate. + test.each([ + { args: ["observe", "logs", "rebuild-index"] }, + { args: ["observe", "logs", "index-status"] }, + ])("diffs TypeScript-owned observe indexer delegation for $args", ({ args }) => { + testHome = mkdtempSync(join(tmpdir(), "ocx-go-cli-parity-")); + expectParity(args); + }); + test.each([ + { args: ["observe", "wat"] }, + { args: ["observe", "logs", "--limit", "0"] }, + { args: ["observe", "logs", "--json", "--jsonl"] }, + { args: ["observe", "logs", "--follow", "--json"] }, + { args: ["observe", "storage", "codex-logs", "protect", "--mode", "wat"] }, + ])("diffs observe usage validation for $args", ({ args }) => { + testHome = mkdtempSync(join(tmpdir(), "ocx-go-cli-parity-")); + expect(expectParity(args)).toMatchObject({ code: 2 }); + }); + test("diffs observe and export help in both spellings", () => { + testHome = mkdtempSync(join(tmpdir(), "ocx-go-cli-parity-")); + for (const args of [ + ["help", "observe"], ["observe", "--help"], ["observe", "logs", "--help"], + ["help", "export"], ["export", "--help"], ["export", "--client", "pi", "--help"], + ]) { + expectParity(args); + } + }); + + // observe + export against one management fixture: logs (all renderers and + // filters), explain, memory/debug/claude-inbound/injection summaries, storage + // codex-logs actions, and export's twelve client documents. Both CLIs run + // async because spawnSync starves the fixture server (the #43 lesson). + function startExportFixture(): void { + testHome = mkdtempSync(join(tmpdir(), "ocx-go-export-parity-")); + testServer = Bun.serve({ port: 0, fetch(request) { + const u = new URL(request.url); + if (u.pathname === "/healthz") { + const challenge = request.headers.get("x-opencodex-attestation-challenge") ?? ""; + const headers = challenge ? { "x-opencodex-attestation-proof": createLocalAttestationProof(secret, challenge, process.pid, testServer!.port) } : {}; + return Response.json({ status: "ok", service: "opencodex", version: "2.42.0", uptime: 1, pid: process.pid, port: testServer!.port }, { headers }); + } + if (u.pathname === "/api/models") { + return Response.json([ + { namespaced: "openai/gpt-5.1-codex", provider: "openai", id: "gpt-5.1-codex", native: true, displayName: "GPT-5.1 Codex", displayNameSource: "provider", contextWindow: 400000, reasoningEfforts: ["minimal", "low", "medium", "high", "xhigh", "max", "ultra"], defaultReasoningEffort: "medium" }, + { namespaced: "anthropic/claude-sonnet-4-5", provider: "anthropic", id: "claude-sonnet-4-5", displayName: "Claude Sonnet 4.5", displayNameSource: "provider", contextWindow: 200000, inputModalities: ["text", "image"], reasoningEfforts: ["low", "medium", "high"] }, + { namespaced: "fixture/audio-model", provider: "fixture", id: "audio-model", displayName: "Audio Only", displayNameSource: "fallback", contextWindow: 0, inputModalities: ["audio"], reasoningEfforts: ["none"] }, + { namespaced: "fixture/plain", provider: "fixture", id: "plain", displayName: "Plain", displayNameSource: "provider" }, + ]); + } + if (u.pathname === "/api/logs") { + const all = [ + { id: "req-1", timestamp: "2026-09-07T10:00:00.000Z", provider: "anthropic", model: "claude-sonnet-4-5", status: 200, durationMs: 1234.5, conversationId: "convA" }, + { id: "req-2", timestamp: "2026-09-07T10:00:01.000Z", provider: "openai", model: null, status: 429, conversationId: "" }, + { id: 3, timestamp: null, createdAt: "2026-09-07T09:00:00.000Z", statusCode: 500, durationMs: 2 }, + { timestamp: "2026-09-07T08:00:00.000Z", status: 200 }, + ]; + const provider = u.searchParams.get("provider"); + const status = u.searchParams.get("status"); + const filtered = all.filter(row => (!provider || row.provider === provider) && (!status || String(row.status) === status || String(row.statusCode) === status)); + return Response.json({ timeZone: "UTC", total: filtered.length, logs: filtered }); + } + if (u.pathname === "/api/request-history/req-1/route-decision") { + return Response.json({ requestId: "req-1", route: { provider: "anthropic", model: "claude-sonnet-4-5", reason: "match" }, usedFallback: false }); + } + if (u.pathname === "/api/system/memory") return Response.json({ heap: 123456789, heapPeak: 200000000, gc: { count: 42, durationMs: 3.5 }, items: 7 }); + if (u.pathname === "/api/debug") return Response.json({ debug: true, usage: false, injection: null, claude: { enabled: false }, reset: false }); + if (u.pathname === "/api/claude/inbound-debug") return Response.json({ enabled: true, entries: [{ ts: 1756000000000, method: "POST", path: "/api/claude/inbound" }] }); + if (u.pathname === "/api/debug/injection-logs") return Response.json({ after: 0, entries: [{ ts: 1756000000000, kind: "prompt", bytes: 128 }, { ts: 1756000000001, kind: "response", bytes: null }] }); + if (u.pathname === "/api/storage") return Response.json({ codexLogs: { present: true, files: 12, sizeBytes: 4096 }, sessions: { archived: 3 } }); + if (u.pathname === "/api/storage/codex-logs") return Response.json({ mode: "compat", protected: true, files: 12 }); + if (u.pathname === "/api/storage/codex-logs/protect") return Response.json({ mode: "quiet", protected: true }); + if (u.pathname === "/api/storage/codex-logs/repair" || u.pathname === "/api/storage/codex-logs/compact" || u.pathname === "/api/storage/codex-logs/unprotect") { + return Response.json({ ok: true }); + } + return new Response("not found", { status: 404 }); + }}); + writeFileSync(join(testHome, "runtime-port.json"), JSON.stringify({ pid: process.pid, port: testServer.port, hostname: "127.0.0.1", attestationSecret: secret })); + writeFileSync(join(testHome, "config.json"), JSON.stringify({ + port: testServer.port, + hostname: "127.0.0.1", + defaultProvider: "fixture", + providers: { + openai: { adapter: "openai-responses", baseUrl: "https://chatgpt.com/backend-api/codex" }, + anthropic: { adapter: "anthropic", baseUrl: "https://api.anthropic.com" }, + fixture: { adapter: "openai-chat", baseUrl: "https://example.test/v1", apiKey: "k", defaultModel: "plain" }, + }, + })); + } + test.each([ + { args: ["observe", "logs"] }, { args: ["observe", "logs", "--json"] }, { args: ["observe", "logs", "--jsonl"] }, + { args: ["observe", "logs", "--provider", "anthropic"] }, { args: ["observe", "logs", "--status", "200"] }, + { args: ["observe", "logs", "--status", "429", "--conversation", "x"] }, { args: ["observe", "logs", "--limit", "2"] }, + { args: ["observe", "logs", "explain", "req-1"] }, { args: ["observe", "logs", "explain", "req-1", "--json"] }, + { args: ["observe", "memory"] }, { args: ["observe", "memory", "--json"] }, { args: ["observe", "debug"] }, { args: ["observe", "debug", "--json"] }, + { args: ["observe", "claude-inbound"] }, { args: ["observe", "injection"] }, { args: ["observe", "injection", "--limit", "1"] }, + { args: ["observe", "storage"] }, { args: ["observe", "storage", "--json"] }, { args: ["observe", "storage", "codex-logs"] }, + { args: ["observe", "storage", "codex-logs", "status", "--json"] }, { args: ["observe", "storage", "codex-logs", "protect"] }, + { args: ["observe", "storage", "codex-logs", "protect", "--mode", "quiet", "--json"] }, { args: ["observe", "storage", "codex-logs", "repair"] }, + { args: ["observe", "storage", "codex-logs", "compact", "--json"] }, { args: ["observe", "storage", "codex-logs", "unprotect", "--json"] }, + ])("diffs Go-owned observe output and exit code for $args", async ({ args }) => { + startExportFixture(); + const ts = await runTsAsync(args); + const go = await runGoAsync(args); + expect(go).toEqual(ts); + expect(ts).toMatchObject({ code: 0, stderr: "" }); + }); + const exportClientIds = ["opencode", "pi", "omp", "hermes", "openclaw", "kimi", "gajae", "dsh", "mcode", "zcode", "prime"]; + test.each(exportClientIds.flatMap(id => [ + { args: ["export", "--client", id, "--json"] }, + { args: ["export", "--client", id] }, + ]))("diffs Go-owned export output and exit code for $args", async ({ args }) => { + startExportFixture(); + const ts = await runTsAsync(args); + const go = await runGoAsync(args); + expect(go).toEqual(ts); + expect(ts).toMatchObject({ code: 0, stderr: "" }); + }); + test("diffs export --json --out and the refusal to clobber", async () => { + startExportFixture(); + const outPath = join(testHome, "written.json"); + const ts = await runTsAsync(["export", "--client", "pi", "--json", "--out", outPath]); + expect(ts).toMatchObject({ code: 0 }); + // Give Go the same clean slate the TypeScript run just had. + if (existsSync(outPath)) removeTreeWithRetry(outPath); + const go = await runGoAsync(["export", "--client", "pi", "--json", "--out", outPath]); + expect(go).toEqual(ts); + const tsRefusal = await runTsAsync(["export", "--client", "pi", "--out", outPath]); + const goRefusal = await runGoAsync(["export", "--client", "pi", "--out", outPath]); + expect(goRefusal).toEqual(tsRefusal); + expect(tsRefusal).toMatchObject({ code: 2 }); + }); + test("diffs export when no proxy is running", () => { + testHome = mkdtempSync(join(tmpdir(), "ocx-go-export-parity-")); + expect(expectParity(["export", "--client", "pi"])).toMatchObject({ code: 1 }); + }); + test("diffs aside export under a fixture home with an account manifest", async () => { + startExportFixture(); + const asideHome = mkdtempSync(join(tmpdir(), "ocx-aside-home-")); + mkdirSync(join(asideHome, ".aside", "u", "0"), { recursive: true }); + writeFileSync(join(asideHome, ".aside", "accounts.json"), JSON.stringify({ currentAccountId: 0 })); + const previousHome = process.env.HOME; + process.env.HOME = asideHome; + try { + const ts = await runTsAsync(["export", "--client", "aside", "--json"]); + const go = await runGoAsync(["export", "--client", "aside", "--json"]); + expect(go).toEqual(ts); + expect(ts).toMatchObject({ code: 0, stderr: "" }); + } finally { + if (previousHome === undefined) delete process.env.HOME; + else process.env.HOME = previousHome; + removeTreeWithRetry(asideHome); + } + }); + test.each([ + { args: ["export", "--client"] }, + { args: ["export", "--client", "nope"] }, + { args: ["export", "extra"] }, + { args: ["export", "--client", "pi", "--json", "--force", "extra"] }, + ])("diffs export argument validation for $args", ({ args }) => { + testHome = mkdtempSync(join(tmpdir(), "ocx-go-cli-parity-")); + expect(expectParity(args)).toMatchObject({ code: 2 }); + }); + }); From e4b7a807115943d289ec76fe0801e8e85ca28f4e Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Tue, 8 Sep 2026 07:33:47 +0800 Subject: [PATCH 120/165] feat(go): flip the ocx account API-routing surface to Go-owned (issue #51) Flips the account subcommands that speak the management API to Go-owned with per-subcommand ownership routing (the observe/config seam model): list, current, use, refresh, auto-switch, alias/rename, priority, pause, resume, pause-exhausted, strategy, sticky, remove and clear-cooldown are now native Go in cli.go; add-key/import (stdin), main (native CODEX_HOME staging) and the login/reauth/code/cancel/reset-credits device flows keep their TypeScript owner until each surface carries its own oracle. Go implementation mirrors src/cli/account.ts and account-extended.ts through a shared data-access layer (account_runtime.go) over account-api.ts semantics: live-proxy discovery with the attested /healthz identity probe, the transport-sentinel apiJson client with its admin-token header, the classifyAccount taxonomy (codex pool/direct, local and forward-auth refusals, key providers, the ten public OAuth ids), the apiError 4/5/1 exit vocabulary, and V8-exact JSON via jsonwire.EncodePretty. fetchRows keeps TS presence semantics (absent vs empty optional fields) so tables, --json output and quota projection cannot drift. Differential oracle: tests/go-cli-parity.test.ts runs the real TS CLI and the Go binary against an identical fresh attested fixture proxy per side and diffs stdout/stderr and the exit code across 75 rows: every subcommand family in plain and --json form, argument validation, provider classification errors, quota/refresh reports, selection-order and alias mutations, remove with the promotion and main-login guards, and clear-cooldown. 151/151 parity green; go vet ./... and go test ./internal/ocxcli/ green; typecheck green. Co-Authored-By: Claude Code --- go/internal/ocxcli/account_command.go | 653 ++++++++++++++++++ go/internal/ocxcli/account_extended_cmd.go | 386 +++++++++++ go/internal/ocxcli/account_pool_cmd.go | 622 +++++++++++++++++ go/internal/ocxcli/account_refresh_helpers.go | 189 +++++ go/internal/ocxcli/account_runtime.go | 581 ++++++++++++++++ go/internal/ocxcli/cli.go | 13 + go/internal/ocxcli/families.go | 12 + tests/go-cli-parity.test.ts | 336 +++++++++ 8 files changed, 2792 insertions(+) create mode 100644 go/internal/ocxcli/account_command.go create mode 100644 go/internal/ocxcli/account_extended_cmd.go create mode 100644 go/internal/ocxcli/account_pool_cmd.go create mode 100644 go/internal/ocxcli/account_refresh_helpers.go create mode 100644 go/internal/ocxcli/account_runtime.go diff --git a/go/internal/ocxcli/account_command.go b/go/internal/ocxcli/account_command.go new file mode 100644 index 0000000000..bee67138ae --- /dev/null +++ b/go/internal/ocxcli/account_command.go @@ -0,0 +1,653 @@ +// ocx account — Go-native port of src/cli/account.ts (the command switch and +// the list/current/use handlers) over the account_runtime data layer. The +// differential oracle feeds one attested fixture proxy and the same +// config.json to the TypeScript CLI and this binary and requires identical +// stdout/stderr and exit codes for every subcommand. +package ocxcli + +import ( + "fmt" + "net/http" + "strings" + + "github.com/lidge-jun/opencodex/go/internal/jsonwire" +) + +// accountFamilyRows is the FamilyRows shape account-api.ts returns. +type accountFamilyRows struct { + rows []*accountRow + activeID string + hasActiveID bool + hasAutoSwitch bool + autoSwitch float64 + status int + errorBody *jsonwire.Value + networkDown bool + transportErr string +} + +func accountRowObject(row *accountRow, includeQuota bool) *jsonwire.Value { + out := jsonwire.ObjectValue() + out.Set("provider", jsonwire.StringValue(row.provider)) + out.Set("type", jsonwire.StringValue(string(row.rowType))) + out.Set("id", jsonwire.StringValue(row.id)) + if row.hasLabel { + out.Set("label", jsonwire.StringValue(row.label)) + } + if row.hasEmail { + out.Set("email", jsonwire.StringValue(row.email)) + } + if row.hasPlan { + out.Set("plan", jsonwire.StringValue(row.plan)) + } + if row.hasMasked { + out.Set("masked", jsonwire.StringValue(row.masked)) + } + out.Set("active", jsonwire.BoolValue(row.active)) + if row.needsReauthSet { + out.Set("needsReauth", jsonwire.BoolValue(row.needsReauth)) + } + if row.rowType == accountTypeCodex { + out.Set("priority", jsonwire.NumberFrom(row.priority)) + out.Set("paused", jsonwire.BoolValue(row.paused)) + if includeQuota { + out.Set("quota", row.quota) + } + } + if row.rowType == accountTypeOAuth { + if row.hasQuota { + out.Set("quota", row.quota) + } + if row.quotaUnavailable { + out.Set("quotaUnavailable", jsonwire.BoolValue(true)) + } + } + return out +} + +func projectQuota(quota *jsonwire.Value) *jsonwire.Value { + if quota == nil || quota.Kind() != jsonwire.Object { + return jsonwire.NullValue() + } + keys := []string{"fiveHourPercent", "fiveHourResetAt", "weeklyPercent", "monthlyPercent", "weeklyResetAt", "monthlyResetAt", "shortPercent", "shortResetAt", "shortWindowSeconds"} + out := jsonwire.ObjectValue() + for _, key := range keys { + field := quota.Find(key) + if field == nil || field.Kind() != jsonwire.Number { + continue + } + if number, err := numberAsFloat(field); err == nil && number == number { + out.Set(key, field) + } + } + return out +} + +// fetchCodexRows mirrors fetchCodexRows in account-api.ts (two parallel GETs). +func fetchCodexRows(client *http.Client, baseURL string, forceRefresh, includeQuota bool) accountFamilyRows { + accountsPath := "/api/codex-auth/accounts" + if forceRefresh { + accountsPath += "?refresh=1" + } + accounts := accountHTTP(client, baseURL, "GET", accountsPath, nil) + active := accountHTTP(client, baseURL, "GET", "/api/codex-auth/active", nil) + rows := accountFamilyRows{status: 200} + if accounts.status != 0 && accounts.status != 200 { + return accountFamilyRows{status: accounts.status, errorBody: accounts.body} + } + if active.status != 0 && active.status != 200 { + return accountFamilyRows{status: active.status, errorBody: active.body} + } + if accounts.status == 0 || active.status == 0 { + transportErr := accounts.transportError + if transportErr == "" { + transportErr = active.transportError + } + return accountFamilyRows{status: 0, networkDown: true, transportErr: transportErr} + } + if field := active.body.Find("activeCodexAccountId"); field != nil && field.Kind() == jsonwire.String { + rows.activeID = field.String() + rows.hasActiveID = true + } + if number, ok := accountNumber(active.body, "autoSwitchThreshold"); ok { + rows.autoSwitch = number + rows.hasAutoSwitch = true + } + accountsArray := activeOrEmptyArray(accounts.body, "accounts") + for _, account := range accountsArray { + row := &accountRow{provider: "openai", rowType: accountTypeCodex} + row.id = objectString(account, "id") + // label = a.alias ?? a.plan ?? a.email (nullish, not empty-coalescing). + aliasValue, aliasSet := objectField(account, "alias") + planValue, planSet := objectField(account, "plan") + emailValue, emailSet := objectField(account, "email") + row.hasPlan = planSet + row.plan = planValue + row.hasEmail = emailSet + row.email = emailValue + if aliasSet { + row.hasLabel = true + row.label = aliasValue + } else if planSet { + row.hasLabel = true + row.label = planValue + } else if emailSet { + row.hasLabel = true + row.label = emailValue + } + row.active = row.id != "" && row.id == rows.activeID + row.needsReauthSet, row.needsReauth = objectBool(account, "needsReauth") + row.hasPriority = true + if number, ok := accountNumber(account, "priority"); ok { + row.priority = number + } + if paused, present := objectBool(account, "paused"); present { + row.paused = paused + } + if includeQuota { + row.quota = projectQuota(account.Find("quota")) + row.hasQuota = true + } + rows.rows = append(rows.rows, row) + } + return rows +} + +// fetchOAuthRows mirrors fetchOAuthRows. +func fetchOAuthRows(client *http.Client, baseURL, name string, withQuota, refreshQuota bool) accountFamilyRows { + query := "" + if withQuota { + query = "?provider=" + urlQueryEscape(name) + ""a=1" + if refreshQuota { + query += "&refresh=1" + } + } else { + query = "?provider=" + urlQueryEscape(name) + } + response := accountHTTP(client, baseURL, "GET", "/api/oauth/accounts"+query, nil) + if response.status == 0 { + return accountFamilyRows{status: 0, networkDown: true, transportErr: response.transportError} + } + if response.status != 200 { + return accountFamilyRows{status: response.status, errorBody: response.body} + } + rows := accountFamilyRows{status: 200} + if field := response.body.Find("activeAccountId"); field != nil && field.Kind() == jsonwire.String { + rows.activeID = field.String() + rows.hasActiveID = true + } + for index, account := range activeOrEmptyArray(response.body, "accounts") { + row := &accountRow{provider: name, rowType: accountTypeOAuth} + row.id = objectString(account, "id") + aliasValue, aliasSet := objectField(account, "alias") + emailValue, emailSet := objectField(account, "email") + row.hasEmail = emailSet + row.email = emailValue + if aliasSet { + row.hasLabel = true + row.label = aliasValue + } else if emailSet { + row.hasLabel = true + row.label = emailValue + } else { + row.hasLabel = true + row.label = fmt.Sprintf("Account %d", index+1) + } + row.active = false + if activeField := account.Find("active"); activeField != nil && activeField.Kind() == jsonwire.Bool { + row.active = activeField.Bool() + } else if row.id != "" { + row.active = row.id == rows.activeID + } + row.needsReauthSet, row.needsReauth = objectBool(account, "needsReauth") + if quota := account.Find("quota"); quota != nil { + row.quota = quota + row.hasQuota = true + } + if unavailable := account.Find("quotaUnavailable"); unavailable != nil && unavailable.Kind() == jsonwire.Bool { + row.quotaUnavailable = unavailable.Bool() + } + rows.rows = append(rows.rows, row) + } + return rows +} + +// fetchKeyRows mirrors fetchKeyRows. +func fetchKeyRows(client *http.Client, baseURL, name string) accountFamilyRows { + response := accountHTTP(client, baseURL, "GET", "/api/providers/keys?name="+urlQueryEscape(name), nil) + if response.status == 0 { + return accountFamilyRows{status: 0, networkDown: true, transportErr: response.transportError} + } + if response.status != 200 { + return accountFamilyRows{status: response.status, errorBody: response.body} + } + rows := accountFamilyRows{status: 200} + if field := response.body.Find("activeId"); field != nil && field.Kind() == jsonwire.String { + rows.activeID = field.String() + rows.hasActiveID = true + } + for _, key := range activeOrEmptyArray(response.body, "keys") { + row := &accountRow{provider: name, rowType: accountTypeAPIKey} + row.id = objectString(key, "id") + labelValue, labelSet := objectField(key, "label") + maskedValue, maskedSet := objectField(key, "masked") + row.hasMasked = maskedSet + row.masked = maskedValue + // row.label = k.label ?? k.masked (nullish). + if labelSet { + row.hasLabel = true + row.label = labelValue + } else if maskedSet { + row.hasLabel = true + row.label = maskedValue + } + row.active = false + if activeField := key.Find("active"); activeField != nil && activeField.Kind() == jsonwire.Bool { + row.active = activeField.Bool() + } else if row.id != "" { + row.active = row.id == rows.activeID + } + rows.rows = append(rows.rows, row) + } + return rows +} + +func fetchAccountRows(client *http.Client, baseURL, name string, rowType AccountType, withQuota, refreshQuota bool) accountFamilyRows { + switch rowType { + case accountTypeCodex: + return fetchCodexRows(client, baseURL, refreshQuota, withQuota) + case accountTypeOAuth: + return fetchOAuthRows(client, baseURL, name, withQuota, refreshQuota) + default: + return fetchKeyRows(client, baseURL, name) + } +} + +// familyFailure maps a FamilyRows error to an exit code, mirroring the +// familyFailure helper in account-extended.ts; nil means no failure. +func accountFamilyFailure(deps Deps, result accountFamilyRows, fallback string) *int { + if result.networkDown { + code := reportProxyUnreachable(deps, result.transportErr) + return &code + } + if result.errorBody != nil { + code := accountAPIError(deps, result.errorBody, fallback, result.status) + return &code + } + return nil +} + +func objectField(object *jsonwire.Value, key string) (value string, present bool) { + if object == nil || object.Kind() != jsonwire.Object { + return "", false + } + field := object.Find(key) + if field == nil || field.Kind() != jsonwire.String { + return "", false + } + return field.String(), true +} + +func objectBool(object *jsonwire.Value, key string) (value bool, present bool) { + if object == nil || object.Kind() != jsonwire.Object { + return false, false + } + field := object.Find(key) + if field == nil || field.Kind() != jsonwire.Bool { + return false, false + } + return field.Bool(), true +} + +func objectString(object *jsonwire.Value, key string) string { + if object == nil || object.Kind() != jsonwire.Object { + return "" + } + field := object.Find(key) + if field == nil || field.Kind() != jsonwire.String { + return "" + } + return field.String() +} + +func activeOrEmptyArray(object *jsonwire.Value, key string) []*jsonwire.Value { + if object == nil || object.Kind() != jsonwire.Object { + return nil + } + field := object.Find(key) + if field == nil || field.Kind() != jsonwire.Array { + return nil + } + return field.Elements() +} + +func firstPresent(values ...string) string { + for _, value := range values { + if value != "" { + return value + } + } + return "" +} + +// urlQueryEscape mirrors encodeURIComponent: every byte outside the RFC 3986 +// unreserved set (A-Z a-z 0-9 - _ . ~) becomes an uppercase %XX escape. +func urlQueryEscape(value string) string { + const hex = "0123456789ABCDEF" + var b strings.Builder + for i := 0; i < len(value); i++ { + c := value[i] + if c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c >= '0' && c <= '9' || c == '-' || c == '_' || c == '.' || c == '~' { + b.WriteByte(c) + continue + } + b.WriteByte('%') + b.WriteByte(hex[c>>4]) + b.WriteByte(hex[c&0xf]) + } + return b.String() +} + +// consumeAccountPositionals strips a leading positional (returned as the +// trimmed name) after all flags were consumed. +func accountShift(args *[]string) string { + if len(*args) == 0 { + return "" + } + value := (*args)[0] + *args = (*args)[1:] + return value +} + +// runAccountList mirrors cmdList in account.ts. +func runAccountList(rest []string, deps accountDeps) int { + wantsJSON := consumeAccountFlag(&rest, "--json") + showAll := consumeAccountFlag(&rest, "--all") + wantsQuota := consumeAccountFlag(&rest, "--quota") + refreshQuota := consumeAccountFlag(&rest, "--refresh") + name := accountShift(&rest) + if leftover := accountLeftoverError(rest); leftover != "" { + reportAccountStderrLine(deps, leftover) + reportAccountUsage(deps, accountUsage) + return 1 + } + raw := loadAccountConfigRaw() + baseURL := resolveAccountBaseURL(deps) + if baseURL == "" { + return reportProxyUnreachable(deps.deps, "") + } + + type target struct { + name string + rowType AccountType + } + var targets []target + if name != "" { + errorText, rowType, ok := classifyAccount(raw, name) + if !ok { + reportAccountStderrLine(deps, fmt.Sprintf("Error: %s. Known candidates: %s", errorText, candidateNames(raw))) + return 1 + } + targets = append(targets, target{name: name, rowType: rowType}) + } else { + seen := map[string]bool{} + push := func(n string) { + if seen[n] { + return + } + seen[n] = true + _, rowType, ok := classifyAccount(raw, n) + if !ok { + return + } + targets = append(targets, target{name: n, rowType: rowType}) + } + push("openai") + providersRes := accountHTTP(deps.httpClientOr(), baseURL, "GET", "/api/oauth/providers", nil) + if providersRes.status == 0 { + return reportProxyUnreachable(deps.deps, providersRes.transportError) + } + if providersRes.status != 200 { + return accountAPIError(deps.deps, providersRes.body, "failed to list OAuth providers", providersRes.status) + } + if providers := activeOrEmptyArray(providersRes.body, "providers"); providers != nil { + for _, provider := range providers { + if provider.Kind() == jsonwire.String { + push(provider.String()) + } + } + } + providersSection, _ := raw["providers"].(map[string]any) + for providerName := range providersSection { + push(providerName) + } + } + + var rows []*accountRow + var notes []string + for _, target := range targets { + var result accountFamilyRows + if wantsQuota { + result = fetchAccountRows(deps.httpClientOr(), baseURL, target.name, target.rowType, true, refreshQuota) + } else { + result = fetchAccountRows(deps.httpClientOr(), baseURL, target.name, target.rowType, false, false) + } + if result.networkDown { + return reportProxyUnreachable(deps.deps, result.transportErr) + } + if result.errorBody != nil { + if name != "" { + return accountAPIError(deps.deps, result.errorBody, fmt.Sprintf("failed to list %s", target.name), result.status) + } + errorText := objectString(result.errorBody, "error") + skipUnknownKey := target.rowType == accountTypeAPIKey && result.status == 404 && strings.Contains(errorText, "unknown provider") + skipConfigOAuth := target.rowType == accountTypeOAuth && result.status == 400 && strings.Contains(errorText, "unknown oauth provider") + if skipUnknownKey || skipConfigOAuth { + continue + } + return accountAPIError(deps.deps, result.errorBody, fmt.Sprintf("failed to list %s", target.name), result.status) + } + if len(result.rows) == 0 { + if showAll { + notes = append(notes, fmt.Sprintf("%s: no stored accounts or keys", target.name)) + } + continue + } + rows = append(rows, result.rows...) + if target.rowType == accountTypeCodex { + if !result.hasActiveID { + notes = append(notes, "openai: auto (no pin — lowest-usage account is selected per request)") + } + providerRow, _ := configProviderMode(raw, "openai") + if codexAccountModeFor("openai", providerRow) == "direct" { + notes = append(notes, "openai is in direct mode — the selection takes effect when pool mode is enabled") + } + } + } + + if wantsJSON { + value := jsonwire.ObjectValue() + accounts := jsonwire.EmptyArray() + for _, row := range rows { + accounts.AppendArray(accountRowObject(row, wantsQuota)) + } + value.Set("accounts", accounts) + noteArray := jsonwire.EmptyArray() + for _, note := range notes { + noteArray.AppendArray(jsonwire.StringValue(note)) + } + value.Set("notes", noteArray) + printPrettyJSON(deps.deps, value) + return 0 + } + if len(rows) > 0 { + fmt.Fprintln(deps.deps.Stdout, formatAccountTable(rows, wantsQuota)) + } + for _, note := range notes { + fmt.Fprintln(deps.deps.Stdout, note) + } + if len(rows) == 0 && len(notes) == 0 { + fmt.Fprintln(deps.deps.Stdout, "No stored accounts or keys.") + } + return 0 +} + +// runAccountCurrent mirrors cmdCurrent in account.ts. +func runAccountCurrent(rest []string, deps accountDeps) int { + wantsJSON := consumeAccountFlag(&rest, "--json") + name := accountShift(&rest) + leftover := accountLeftoverError(rest) + if name == "" || leftover != "" { + if leftover != "" { + reportAccountStderrLine(deps, leftover) + } + reportAccountUsage(deps, accountUsage) + return 1 + } + raw := loadAccountConfigRaw() + errorText, rowType, ok := classifyAccount(raw, name) + if !ok { + reportAccountStderrLine(deps, fmt.Sprintf("Error: %s. Known candidates: %s", errorText, candidateNames(raw))) + return 1 + } + baseURL := resolveAccountBaseURL(deps) + if baseURL == "" { + return reportProxyUnreachable(deps.deps, "") + } + result := fetchAccountRows(deps.httpClientOr(), baseURL, name, rowType, false, false) + if result.networkDown { + return reportProxyUnreachable(deps.deps, result.transportErr) + } + if result.errorBody != nil { + return accountAPIError(deps.deps, result.errorBody, fmt.Sprintf("failed to read %s", name), result.status) + } + var activeRow *accountRow + for _, row := range result.rows { + if row.active { + activeRow = row + break + } + } + if wantsJSON { + value := jsonwire.ObjectValue() + value.Set("provider", jsonwire.StringValue(name)) + value.Set("type", jsonwire.StringValue(string(rowType))) + if result.hasActiveID { + value.Set("activeId", jsonwire.StringValue(result.activeID)) + } else { + value.Set("activeId", jsonwire.NullValue()) + } + if result.hasAutoSwitch { + value.Set("autoSwitchThreshold", jsonwire.NumberFrom(result.autoSwitch)) + } + if activeRow != nil { + value.Set("account", accountRowObject(activeRow, false)) + } else { + value.Set("account", jsonwire.NullValue()) + } + printPrettyJSON(deps.deps, value) + return 0 + } + if activeRow != nil { + fmt.Fprintln(deps.deps.Stdout, formatAccountTable([]*accountRow{activeRow}, false)) + } else if rowType == accountTypeCodex && !result.hasActiveID { + fmt.Fprintln(deps.deps.Stdout, "openai: auto (no pin — lowest-usage account is selected per request)") + } else { + fmt.Fprintln(deps.deps.Stdout, fmt.Sprintf("%s: no active account or key", name)) + } + return 0 +} + +// runAccountUse mirrors cmdUse in account.ts. +func runAccountUse(rest []string, deps accountDeps) int { + wantsJSON := consumeAccountFlag(&rest, "--json") + name := accountShift(&rest) + id := accountShift(&rest) + leftover := accountLeftoverError(rest) + if name == "" || id == "" || leftover != "" { + if leftover != "" { + reportAccountStderrLine(deps, leftover) + } + reportAccountUsage(deps, accountUsage) + return 1 + } + raw := loadAccountConfigRaw() + errorText, rowType, ok := classifyAccount(raw, name) + if !ok { + reportAccountStderrLine(deps, fmt.Sprintf("Error: %s. Known candidates: %s", errorText, candidateNames(raw))) + return 1 + } + baseURL := resolveAccountBaseURL(deps) + if baseURL == "" { + return reportProxyUnreachable(deps.deps, "") + } + var response accountAPIResult + activeID := "" + switch rowType { + case accountTypeCodex: + activeID = id + if id == mainAlias { + activeID = mainAccountID + } + body := jsonwire.ObjectValue() + body.Set("accountId", jsonwire.StringValue(activeID)) + response = accountHTTP(deps.httpClientOr(), baseURL, "PUT", "/api/codex-auth/active", body) + case accountTypeOAuth: + activeID = id + body := jsonwire.ObjectValue() + body.Set("provider", jsonwire.StringValue(name)) + body.Set("accountId", jsonwire.StringValue(id)) + response = accountHTTP(deps.httpClientOr(), baseURL, "PUT", "/api/oauth/accounts/active", body) + default: + activeID = id + body := jsonwire.ObjectValue() + body.Set("name", jsonwire.StringValue(name)) + body.Set("id", jsonwire.StringValue(id)) + response = accountHTTP(deps.httpClientOr(), baseURL, "PUT", "/api/providers/keys/active", body) + } + if response.status == 0 { + return reportProxyUnreachable(deps.deps, response.transportError) + } + if response.status != 200 { + return accountAPIError(deps.deps, response.body, fmt.Sprintf("failed to switch %s", name), response.status) + } + if wantsJSON { + value := jsonwire.ObjectValue() + value.Set("ok", jsonwire.BoolValue(true)) + value.Set("provider", jsonwire.StringValue(name)) + value.Set("type", jsonwire.StringValue(string(rowType))) + value.Set("activeId", jsonwire.StringValue(activeID)) + printPrettyJSON(deps.deps, value) + } else { + kind := "account" + if rowType == accountTypeAPIKey { + kind = "key" + } + fmt.Fprintln(deps.deps.Stdout, fmt.Sprintf("%s: active %s is now %s", name, kind, displayID(activeID))) + } + if rowType == accountTypeCodex { + reportAccountStderrLine(deps, "Takes effect immediately; running threads move on their next request, and in-flight requests keep the account they captured.") + active := accountHTTP(deps.httpClientOr(), baseURL, "GET", "/api/codex-auth/active", nil) + if active.status == 200 { + if number, ok := accountNumber(active.body, "autoSwitchThreshold"); ok && number > 0 { + reportAccountStderrLine(deps, fmt.Sprintf("Note: auto-switch (threshold %s%%) may override this pin.", jsonwire.FormatV8Number(number))) + } + } + } + return 0 +} + +func reportAccountStderrLine(deps accountDeps, line string) { + fmt.Fprintln(deps.deps.Stderr, line) +} + +func reportAccountUsage(deps accountDeps, usage string) { + fmt.Fprintln(deps.deps.Stderr, usage) +} + +func (d accountDeps) httpClientOr() *http.Client { + if d.httpClient != nil { + return d.httpClient + } + return defaults(d.deps).HTTPClient +} diff --git a/go/internal/ocxcli/account_extended_cmd.go b/go/internal/ocxcli/account_extended_cmd.go new file mode 100644 index 0000000000..406f3861e0 --- /dev/null +++ b/go/internal/ocxcli/account_extended_cmd.go @@ -0,0 +1,386 @@ +// ocx account command switch and the API-mutation subcommands — Go-native port +// of src/cli/account.ts (cmdAccount switch) and the refresh/auto-switch/remove +// handlers in src/cli/account-extended.ts. All of these speak the management +// API through account_runtime; the differential oracle pins their bytes. +package ocxcli + +import ( + "fmt" + "math" + "strconv" + + "github.com/lidge-jun/opencodex/go/internal/jsonwire" +) + +// runAccount is cmdAccount in src/cli/account.ts. +func runAccount(args []string, deps Deps) int { + acc := accountDeps{deps: deps} + sub := "" + if len(args) > 0 { + sub = args[0] + } + rest := args[1:] + switch sub { + case "list": + return runAccountList(rest, acc) + case "current": + return runAccountCurrent(rest, acc) + case "use": + return runAccountUse(rest, acc) + case "refresh": + return runAccountRefresh(rest, acc) + case "auto-switch": + return runAccountAutoSwitch(rest, acc) + case "alias", "rename": + return runAccountAlias(rest, acc) + case "priority": + return runAccountPriority(rest, acc) + case "pause": + return runAccountPause(rest, acc, true) + case "resume": + return runAccountPause(rest, acc, false) + case "pause-exhausted": + return runAccountPauseExhausted(rest, acc) + case "strategy": + return runAccountStrategy(rest, acc) + case "sticky": + return runAccountSticky(rest, acc) + case "remove": + return runAccountRemove(rest, acc) + case "clear-cooldown": + return runAccountClearCooldown(rest, acc) + default: + // account add-key/import/main/login/reauth/code/cancel/reset-credits + // stay TypeScript-owned: cli.go gates them before dispatch (they need + // stdin, the native-profile staging home, or the device-flow runtime). + reportAccountUsage(acc, accountUsage) + return 1 + } +} + +// accountExtendedUsageError mirrors usage() in account-extended.ts. +func accountExtendedUsageError(acc accountDeps, message string) int { + if message != "" { + reportAccountStderrLine(acc, message) + } + reportAccountUsage(acc, accountExtendedUsage) + return 1 +} + +func runAccountRefresh(rest []string, acc accountDeps) int { + wantsJSON := consumeAccountFlag(&rest, "--json") + name := accountShift(&rest) + if name == "" || len(rest) > 0 { + return accountExtendedUsageError(acc, "") + } + raw := loadAccountConfigRaw() + errorText, rowType, ok := classifyAccount(raw, name) + if !ok { + return accountExtendedUsageError(acc, "Error: "+errorText) + } + baseURL := resolveAccountBaseURL(acc) + if baseURL == "" { + return reportProxyUnreachable(acc.deps, "") + } + if rowType != accountTypeCodex { + status, report, errorBody, transportErr := fetchProviderQuotaReport(acc.httpClientOr(), baseURL, name) + if status == 0 { + return reportProxyUnreachable(acc.deps, transportErr) + } + if status != 200 { + return accountAPIError(acc.deps, errorBody, fmt.Sprintf("failed to refresh %s", name), status) + } + if wantsJSON { + value := jsonwire.ObjectValue() + value.Set("provider", jsonwire.StringValue(name)) + if report != nil { + value.Set("report", report) + } else { + value.Set("report", jsonwire.NullValue()) + } + printPrettyJSON(acc.deps, value) + } else if report != nil { + fmt.Fprintln(acc.deps.Stdout, accountProviderQuotaLine(name, report)) + } else if accountHasPassiveQuota(name) { + fmt.Fprintln(acc.deps.Stdout, name+" reports usage only during a streaming response; there is nothing to refresh. Run a request through this provider to update it, then see `ocx account list "+name+"`.") + } else { + fmt.Fprintln(acc.deps.Stdout, "no quota report available for "+name) + } + return 0 + } + result := fetchCodexRows(acc.httpClientOr(), baseURL, true, true) + if failed := accountFamilyFailure(acc.deps, result, fmt.Sprintf("failed to refresh %s", name)); failed != nil { + return *failed + } + if wantsJSON { + value := jsonwire.ObjectValue() + accounts := jsonwire.EmptyArray() + for _, row := range result.rows { + accounts.AppendArray(accountRowObject(row, true)) + } + value.Set("accounts", accounts) + printPrettyJSON(acc.deps, value) + } else { + for _, row := range result.rows { + fmt.Fprintln(acc.deps.Stdout, accountRefreshLine(row)) + } + } + return 0 +} + +func allDigits(value string) bool { + if value == "" { + return false + } + for _, r := range value { + if r < '0' || r > '9' { + return false + } + } + return true +} + +func accountIsInteger(value float64) bool { + return value == math.Trunc(value) +} + +func hasNumberField(object *jsonwire.Value, key string) bool { + _, ok := accountNumber(object, key) + return ok +} + +func runAccountAutoSwitch(rest []string, acc accountDeps) int { + wantsJSON := consumeAccountFlag(&rest, "--json") + name := accountShift(&rest) + action := accountShift(&rest) + if name == "" || action == "" { + return accountExtendedUsageError(acc, "") + } + raw := loadAccountConfigRaw() + _, rowType, ok := classifyAccount(raw, name) + genericPool := rowType == accountTypeOAuth + if !ok || rowType == accountTypeAPIKey || name == "anthropic" { + return accountExtendedUsageError(acc, "Error: auto-switch only applies to the openai Codex account pool or a generic OAuth provider pool") + } + var threshold *float64 + switch { + case action == "on" && len(rest) == 0: + value := 80.0 + threshold = &value + case action == "off" && len(rest) == 0: + value := 0.0 + threshold = &value + case action == "threshold" && len(rest) == 1 && allDigits(rest[0]): + if number, err := strconv.ParseFloat(rest[0], 64); err == nil { + threshold = &number + } + case action != "status" || len(rest) != 0: + return accountExtendedUsageError(acc, "") + } + if threshold != nil && (!accountIsInteger(*threshold) || *threshold < 0 || *threshold > 100) { + return accountExtendedUsageError(acc, "Error: threshold must be an integer 0-100") + } + baseURL := resolveAccountBaseURL(acc) + if baseURL == "" { + return reportProxyUnreachable(acc.deps, "") + } + if action == "status" { + var response accountAPIResult + if genericPool { + response = accountHTTP(acc.httpClientOr(), baseURL, "GET", "/api/oauth/accounts/pool?provider="+urlQueryEscape(name), nil) + } else { + response = accountHTTP(acc.httpClientOr(), baseURL, "GET", "/api/codex-auth/active", nil) + } + if response.status == 0 { + return reportProxyUnreachable(acc.deps, response.transportError) + } + if response.status != 200 || (!genericPool && !hasNumberField(response.body, "autoSwitchThreshold")) { + return accountAPIError(acc.deps, response.body, "failed to read auto-switch status", response.status) + } + value, _ := accountNumber(response.body, "autoSwitchThreshold") + threshold = &value + } else { + var body *jsonwire.Value + var path string + if genericPool { + body = jsonwire.ObjectValue() + body.Set("provider", jsonwire.StringValue(name)) + body.Set("autoSwitchThreshold", jsonwire.NumberFrom(*threshold)) + path = "/api/oauth/accounts/pool" + } else { + body = jsonwire.ObjectValue() + body.Set("threshold", jsonwire.NumberFrom(*threshold)) + path = "/api/codex-auth/auto-switch" + } + response := accountHTTP(acc.httpClientOr(), baseURL, "PUT", path, body) + if response.status == 0 { + return reportProxyUnreachable(acc.deps, response.transportError) + } + if response.status != 200 { + return accountAPIError(acc.deps, response.body, "failed to update auto-switch", response.status) + } + } + enabled := *threshold > 0 + if wantsJSON { + value := jsonwire.ObjectValue() + value.Set("provider", jsonwire.StringValue(name)) + value.Set("autoSwitchThreshold", jsonwire.NumberFrom(*threshold)) + value.Set("enabled", jsonwire.BoolValue(enabled)) + printPrettyJSON(acc.deps, value) + } else if enabled { + fmt.Fprintln(acc.deps.Stdout, fmt.Sprintf("auto-switch: on (threshold %s%%)", jsonwire.FormatV8Number(*threshold))) + } else { + fmt.Fprintln(acc.deps.Stdout, "auto-switch: off") + } + return 0 +} + +// accountDeletePath mirrors deletePath() in account-extended.ts. +func accountDeletePath(rowType AccountType, name, id string) string { + switch rowType { + case accountTypeCodex: + return "/api/codex-auth/accounts?id=" + urlQueryEscape(id) + case accountTypeOAuth: + return "/api/oauth/accounts?provider=" + urlQueryEscape(name) + "&id=" + urlQueryEscape(id) + default: + return "/api/providers/keys?name=" + urlQueryEscape(name) + "&id=" + urlQueryEscape(id) + } +} + +func errorTextOf(json *jsonwire.Value, fallback string) string { + text := objectString(json, "error") + if text == "" { + return fallback + } + return text +} + +func runAccountRemove(rest []string, acc accountDeps) int { + wantsJSON := consumeAccountFlag(&rest, "--json") + confirmed := consumeAccountFlag(&rest, "--yes") + fail := func(message string) int { + if wantsJSON { + value := jsonwire.ObjectValue() + value.Set("error", jsonwire.StringValue(message)) + printPrettyJSON(acc.deps, value) + } else { + fmt.Fprintln(acc.deps.Stderr, "Error: "+message) + } + return 1 + } + name := accountShift(&rest) + requestedID := accountShift(&rest) + if name == "" || requestedID == "" || len(rest) > 0 { + if wantsJSON { + return fail("provider and account id are required") + } + return accountExtendedUsageError(acc, "") + } + if !confirmed { + message := fmt.Sprintf("Confirmation required. Re-run: ocx account remove %s %s --yes", name, requestedID) + if wantsJSON { + return fail(message) + } + return accountExtendedUsageError(acc, message) + } + raw := loadAccountConfigRaw() + errorText, rowType, ok := classifyAccount(raw, name) + if !ok { + if wantsJSON { + return fail(errorText) + } + return accountExtendedUsageError(acc, "Error: "+errorText) + } + id := requestedID + if rowType == accountTypeCodex && requestedID == mainAlias { + id = mainAccountID + } + if rowType == accountTypeCodex && id == mainAccountID { + if wantsJSON { + return fail("the main Codex App login cannot be removed") + } + return accountExtendedUsageError(acc, "Error: the main Codex App login cannot be removed") + } + baseURL := resolveAccountBaseURL(acc) + if baseURL == "" { + return fail("Proxy not reachable. Start it with 'ocx start' or 'ocx ensure'.") + } + before := fetchAccountRows(acc.httpClientOr(), baseURL, name, rowType, false, false) + if before.networkDown { + return fail("Proxy not reachable. Start it with 'ocx start' or 'ocx ensure'.") + } + if before.errorBody != nil { + return fail(errorTextOf(before.errorBody, fmt.Sprintf("failed to verify %s before removal", name))) + } + found := false + for _, row := range before.rows { + if row.id == id { + found = true + break + } + } + if !found { + if wantsJSON { + return fail(fmt.Sprintf("account or key %q was not found", requestedID)) + } + return accountExtendedUsageError(acc, fmt.Sprintf("Error: account or key %q was not found", requestedID)) + } + response := accountHTTP(acc.httpClientOr(), baseURL, "DELETE", accountDeletePath(rowType, name, id), nil) + if response.status == 0 { + return fail("Proxy not reachable. Start it with 'ocx start' or 'ocx ensure'.") + } + if response.status != 200 { + return fail(errorTextOf(response.body, fmt.Sprintf("failed to remove %s", requestedID))) + } + catalogRefreshPending := rowType == accountTypeCodex && codexCatalogRefreshPending(response.body) + after := fetchAccountRows(acc.httpClientOr(), baseURL, name, rowType, false, false) + if after.networkDown || after.errorBody != nil { + detail := "unknown error" + if after.networkDown { + detail = "proxy not reachable" + } else if after.errorBody != nil { + detail = objectString(after.errorBody, "error") + if detail == "" { + detail = "unknown error" + } + } + return fail(fmt.Sprintf("post-delete verification failed; delete may have succeeded: %s", detail)) + } + removedActive := before.hasActiveID && before.activeID == id + value := jsonwire.ObjectValue() + value.Set("ok", jsonwire.BoolValue(true)) + value.Set("provider", jsonwire.StringValue(name)) + value.Set("id", jsonwire.StringValue(id)) + value.Set("removedActive", jsonwire.BoolValue(removedActive)) + if after.hasActiveID { + value.Set("promotedActiveId", jsonwire.StringValue(after.activeID)) + } else { + value.Set("promotedActiveId", jsonwire.NullValue()) + } + if rowType == accountTypeCodex { + value.Set("catalogRefreshPending", jsonwire.BoolValue(catalogRefreshPending)) + } + if wantsJSON { + printPrettyJSON(acc.deps, value) + } else if rowType == accountTypeCodex && removedActive && !after.hasActiveID { + fmt.Fprintln(acc.deps.Stdout, "openai: auto (no pin — lowest-usage account is selected per request)") + } else if rowType == accountTypeOAuth { + if len(after.rows) > 0 { + fmt.Fprintln(acc.deps.Stdout, name+": active account is now "+after.activeID) + } else { + fmt.Fprintln(acc.deps.Stdout, name+": no accounts remaining") + } + } else if rowType == accountTypeAPIKey { + if len(after.rows) > 0 { + fmt.Fprintln(acc.deps.Stdout, name+": active key is now "+after.activeID) + } else { + fmt.Fprintln(acc.deps.Stdout, name+": no keys remaining") + } + } else { + fmt.Fprintln(acc.deps.Stdout, fmt.Sprintf("%s: removed account %s", name, requestedID)) + } + if !wantsJSON && catalogRefreshPending { + warnIfCodexCatalogRefreshPending(acc, response.body) + } + return 0 +} diff --git a/go/internal/ocxcli/account_pool_cmd.go b/go/internal/ocxcli/account_pool_cmd.go new file mode 100644 index 0000000000..d4717e54be --- /dev/null +++ b/go/internal/ocxcli/account_pool_cmd.go @@ -0,0 +1,622 @@ +// ocx account selection-order subcommands (priority, pause/resume, +// pause-exhausted, strategy, sticky and alias) — byte-faithful Go-native port +// of the matching handlers in src/cli/account-extended.ts. +package ocxcli + +import ( + "fmt" + "strconv" + "strings" + + "github.com/lidge-jun/opencodex/go/internal/jsonwire" +) + +// accountPriorityPresets mirrors PRIORITY_PRESETS. +var accountPriorityPresets = map[string]float64{ + "first": 2, "earlier": 1, "normal": 0, "later": -1, "last": -2, +} + +// accountPriorityNames is the canonical key order for preset messages. +var accountPriorityNames = []string{"first", "earlier", "normal", "later", "last"} + +func accountPriorityPresetName(priority float64) string { + for name, value := range accountPriorityPresets { + if value == priority { + return name + } + } + return "" +} + +func accountFormatPriority(priority float64) string { + preset := accountPriorityPresetName(priority) + signed := strconv.FormatFloat(priority, 'f', -1, 64) + if priority > 0 { + signed = "+" + signed + } + if preset != "" { + return signed + " (" + preset + ")" + } + return signed +} + +// accountParsePriorityArgument mirrors parsePriorityArgument: null = reset, +// nil-ok = unparseable. +func accountParsePriorityArgument(raw string) (*float64, bool) { + word := strings.ToLower(strings.TrimSpace(raw)) + if word == "reset" { + return nil, true + } + if _, ok := accountPriorityPresets[word]; ok { + value := accountPriorityPresets[word] + return &value, true + } + if !signedIntegerString(word) { + return nil, false + } + number, err := strconv.ParseFloat(word, 64) + if err != nil { + return nil, false + } + // parseAccountPriority returns null outside [-100,100]; that null maps to + // "unparseable" in parsePriorityArgument. + if number < -100 || number > 100 { + return nil, false + } + return &number, true +} + +func accountPriorityJSON(provider, id string, priority float64) *jsonwire.Value { + value := jsonwire.ObjectValue() + value.Set("ok", jsonwire.BoolValue(true)) + value.Set("provider", jsonwire.StringValue(provider)) + value.Set("id", jsonwire.StringValue(id)) + value.Set("priority", jsonwire.NumberFrom(priority)) + preset := accountPriorityPresetName(priority) + if preset != "" { + value.Set("preset", jsonwire.StringValue(preset)) + } else { + value.Set("preset", jsonwire.NullValue()) + } + return value +} + +// runAccountPriority mirrors cmdPriority. +func runAccountPriority(rest []string, acc accountDeps) int { + wantsJSON := consumeAccountFlag(&rest, "--json") + name := accountShift(&rest) + requestedID := accountShift(&rest) + requestedPriority := accountShift(&rest) + if name == "" || requestedID == "" || len(rest) > 0 { + return accountExtendedUsageError(acc, "") + } + raw := loadAccountConfigRaw() + errorText, rowType, ok := classifyAccount(raw, name) + if !ok { + return accountExtendedUsageError(acc, "Error: "+errorText) + } + if rowType != accountTypeCodex { + return accountExtendedUsageError(acc, "Error: selection order only applies to the openai Codex account pool") + } + id := requestedID + if requestedID == mainAlias { + id = mainAccountID + } + // Validate before touching the network so a typo never reaches the proxy. + var priority *float64 + parseable := true + if requestedPriority != "" { + priority, parseable = accountParsePriorityArgument(requestedPriority) + if !parseable { + return accountExtendedUsageError(acc, "Error: selection order must be an integer -100..100, one of first/earlier/normal/later/last, or reset") + } + } + baseURL := resolveAccountBaseURL(acc) + if baseURL == "" { + return reportProxyUnreachable(acc.deps, "") + } + // No value means "show" — a read must not rewrite what it is reporting. + if requestedPriority == "" { + result := fetchCodexRows(acc.httpClientOr(), baseURL, false, false) + if failed := accountFamilyFailure(acc.deps, result, fmt.Sprintf("failed to read %s accounts", name)); failed != nil { + return *failed + } + var row *accountRow + for _, candidate := range result.rows { + if candidate.id == id { + row = candidate + break + } + } + if row == nil { + return accountExtendedUsageError(acc, "Error: no "+name+" account "+requestedID) + } + current := 0.0 + if row.hasPriority { + current = row.priority + } + if wantsJSON { + printPrettyJSON(acc.deps, accountPriorityJSON(name, id, current)) + } else { + fmt.Fprintln(acc.deps.Stdout, fmt.Sprintf("%s: %s selection order is %s", name, requestedID, accountFormatPriority(current))) + } + return 0 + } + // "reset" parses to nil and is sent as JSON null (the API's default). + body := jsonwire.ObjectValue() + body.Set("id", jsonwire.StringValue(id)) + if priority != nil { + body.Set("priority", jsonwire.NumberFrom(*priority)) + } else { + body.Set("priority", jsonwire.NullValue()) + } + applied := 0.0 + if priority != nil { + applied = *priority + } + response := accountHTTP(acc.httpClientOr(), baseURL, "PUT", "/api/codex-auth/accounts/priority", body) + if response.status == 0 { + return reportProxyUnreachable(acc.deps, response.transportError) + } + if response.status != 200 { + return accountAPIError(acc.deps, response.body, fmt.Sprintf("failed to set selection order for %s", requestedID), response.status) + } + appliedPriority := applied + if number, ok := accountNumber(response.body, "priority"); ok { + appliedPriority = number + } + if wantsJSON { + printPrettyJSON(acc.deps, accountPriorityJSON(name, id, appliedPriority)) + } else { + fmt.Fprintln(acc.deps.Stdout, fmt.Sprintf("%s: %s selection order is now %s", name, requestedID, accountFormatPriority(appliedPriority))) + } + reportAccountStderrLine(acc, "Takes effect from the next unbound request; running threads keep their current account until drained.") + reportAccountStderrLine(acc, `Also releases any manual "use this account now" pin, on any account.`) + return 0 +} + +// runAccountPause mirrors cmdPause. +func runAccountPause(rest []string, acc accountDeps, paused bool) int { + wantsJSON := consumeAccountFlag(&rest, "--json") + verb := "pause" + if !paused { + verb = "resume" + } + name := accountShift(&rest) + requestedID := accountShift(&rest) + if name == "" || requestedID == "" || len(rest) > 0 { + return accountExtendedUsageError(acc, "") + } + raw := loadAccountConfigRaw() + errorText, rowType, ok := classifyAccount(raw, name) + if !ok { + return accountExtendedUsageError(acc, "Error: "+errorText) + } + if rowType != accountTypeCodex { + return accountExtendedUsageError(acc, "Error: "+verb+" applies to the openai Codex account pool") + } + id := requestedID + if requestedID == mainAlias { + id = mainAccountID + } + baseURL := resolveAccountBaseURL(acc) + if baseURL == "" { + return reportProxyUnreachable(acc.deps, "") + } + body := jsonwire.ObjectValue() + body.Set("id", jsonwire.StringValue(id)) + body.Set("paused", jsonwire.BoolValue(paused)) + response := accountHTTP(acc.httpClientOr(), baseURL, "PUT", "/api/codex-auth/accounts/pause", body) + if response.status == 0 { + return reportProxyUnreachable(acc.deps, response.transportError) + } + if response.status != 200 { + return accountAPIError(acc.deps, response.body, fmt.Sprintf("failed to %s %s", verb, requestedID), response.status) + } + if wantsJSON { + value := jsonwire.ObjectValue() + value.Set("ok", jsonwire.BoolValue(true)) + value.Set("provider", jsonwire.StringValue(name)) + value.Set("id", jsonwire.StringValue(id)) + value.Set("paused", jsonwire.BoolValue(paused)) + printPrettyJSON(acc.deps, value) + } else { + fmt.Fprintln(acc.deps.Stdout, fmt.Sprintf("%s: %s %s", name, requestedID, verb+"d")) + } + if paused { + reportAccountStderrLine(acc, "Threads bound to this account are unbound, and a fallback account is selected if this one was active.") + } + return 0 +} + +// runAccountPauseExhausted mirrors cmdPauseExhausted. +func runAccountPauseExhausted(rest []string, acc accountDeps) int { + wantsJSON := consumeAccountFlag(&rest, "--json") + name := accountShift(&rest) + if name == "" || len(rest) > 0 { + return accountExtendedUsageError(acc, "") + } + raw := loadAccountConfigRaw() + errorText, rowType, classified := classifyAccount(raw, name) + if !classified { + return accountExtendedUsageError(acc, "Error: "+errorText) + } + if rowType != accountTypeCodex { + return accountExtendedUsageError(acc, "Error: pause-exhausted applies to the openai Codex account pool") + } + baseURL := resolveAccountBaseURL(acc) + if baseURL == "" { + return reportProxyUnreachable(acc.deps, "") + } + body := jsonwire.ObjectValue() + response := accountHTTP(acc.httpClientOr(), baseURL, "PUT", "/api/codex-auth/accounts/pause-exhausted", body) + if response.status == 0 { + return reportProxyUnreachable(acc.deps, response.transportError) + } + if response.status != 200 { + return accountAPIError(acc.deps, response.body, "failed to pause exhausted accounts", response.status) + } + var pausedIDs []string + if field := response.body.Find("pausedAccountIds"); field != nil && field.Kind() == jsonwire.Array { + for _, value := range field.Elements() { + if value.Kind() == jsonwire.String { + pausedIDs = append(pausedIDs, value.String()) + } + } + } + checked, hasChecked := accountNumber(response.body, "checkedAccountCount") + failed, hasFailed := accountNumber(response.body, "failedAccountCount") + complete := !hasFailed || failed == 0 + ok := complete + if hasFailed && failed > 0 { + reportAccountStderrLine(acc, fmt.Sprintf("Quota refresh failed for %s account(s); those were not evaluated.", jsonwire.FormatV8Number(failed))) + } + if wantsJSON { + value := jsonwire.ObjectValue() + value.Set("ok", jsonwire.BoolValue(ok)) + value.Set("complete", jsonwire.BoolValue(complete)) + value.Set("provider", jsonwire.StringValue(name)) + ids := jsonwire.EmptyArray() + for _, id := range pausedIDs { + ids.AppendArray(jsonwire.StringValue(id)) + } + value.Set("pausedAccountIds", ids) + if hasChecked { + value.Set("checkedAccountCount", jsonwire.NumberFrom(checked)) + } else { + value.Set("checkedAccountCount", jsonwire.NullValue()) + } + if hasFailed { + value.Set("failedAccountCount", jsonwire.NumberFrom(failed)) + } else { + value.Set("failedAccountCount", jsonwire.NullValue()) + } + printPrettyJSON(acc.deps, value) + if !ok { + return 1 + } + return 0 + } + if len(pausedIDs) > 0 { + fmt.Fprintln(acc.deps.Stdout, fmt.Sprintf("%s: paused %d exhausted account(s): %s", name, len(pausedIDs), strings.Join(pausedIDs, ", "))) + } else { + fmt.Fprintln(acc.deps.Stdout, fmt.Sprintf("%s: no exhausted accounts to pause", name)) + } + if !ok { + return 1 + } + return 0 +} + +// runAccountStrategy mirrors cmdStrategy, runAccountSticky mirrors cmdSticky; +// both delegate to poolSetting. +func runAccountStrategy(rest []string, acc accountDeps) int { + return accountPoolSetting(rest, acc, "strategy") +} + +func runAccountSticky(rest []string, acc accountDeps) int { + return accountPoolSetting(rest, acc, "stickyLimit") +} + +func accountPoolSetting(rest []string, acc accountDeps, field string) int { + wantsJSON := consumeAccountFlag(&rest, "--json") + label := "pool strategy" + if field == "stickyLimit" { + label = "sticky limit" + } + name := accountShift(&rest) + requested := accountShift(&rest) + if name == "" || len(rest) > 0 { + return accountExtendedUsageError(acc, "") + } + raw := loadAccountConfigRaw() + errorText, rowType, ok := classifyAccount(raw, name) + if !ok { + return accountExtendedUsageError(acc, "Error: "+errorText) + } + // poolTransportFor: codex → codex transport; oauth → anthropic transport; + // api-key → refusal string. + if rowType == accountTypeAPIKey { + return accountExtendedUsageError(acc, fmt.Sprintf("Error: pool settings apply to OAuth account pools, not the API-key provider %q", name)) + } + baseURL := resolveAccountBaseURL(acc) + if baseURL == "" { + return reportProxyUnreachable(acc.deps, "") + } + readPath := "/api/codex-auth/active" + writePath := "/api/codex-auth/pool-strategy" + strategyKey := "accountPoolStrategy" + stickyKey := "accountPoolStickyLimit" + writeBody := func(value *jsonwire.Value) *jsonwire.Value { + body := jsonwire.ObjectValue() + body.Set(field, value) + return body + } + if rowType == accountTypeOAuth { + readPath = "/api/oauth/accounts/pool?provider=" + urlQueryEscape(name) + writePath = "/api/oauth/accounts/pool" + strategyKey = "strategy" + stickyKey = "stickyLimit" + writeBody = func(value *jsonwire.Value) *jsonwire.Value { + body := jsonwire.ObjectValue() + body.Set("provider", jsonwire.StringValue(name)) + body.Set(field, value) + return body + } + } + // No value means show. + if requested == "" { + response := accountHTTP(acc.httpClientOr(), baseURL, "GET", readPath, nil) + if response.status == 0 { + return reportProxyUnreachable(acc.deps, response.transportError) + } + if response.status != 200 { + return accountAPIError(acc.deps, response.body, fmt.Sprintf("failed to read %s", label), response.status) + } + strategyField := response.body.Find(strategyKey) + stickyField := response.body.Find(stickyKey) + if wantsJSON { + value := jsonwire.ObjectValue() + value.Set("ok", jsonwire.BoolValue(true)) + value.Set("provider", jsonwire.StringValue(name)) + if strategyField != nil { + value.Set("strategy", strategyField) + } + if stickyField != nil { + value.Set("stickyLimit", stickyField) + } + printPrettyJSON(acc.deps, value) + } else { + shown := strategyField + if field == "stickyLimit" { + shown = stickyField + } + fmt.Fprintln(acc.deps.Stdout, fmt.Sprintf("%s: %s is %s", name, label, jsonwireWireText(shown))) + } + return 0 + } + // Sent as a number when it parses as one so the server sees the type it + // validates; a non-numeric string still goes through. + var value *jsonwire.Value + if field == "strategy" { + value = jsonwire.StringValue(requested) + } else { + number, err := strconv.ParseFloat(requested, 64) + if err != nil { + value = jsonwire.StringValue(requested) + } else { + value = jsonwire.NumberFrom(number) + } + } + response := accountHTTP(acc.httpClientOr(), baseURL, "PUT", writePath, writeBody(value)) + if response.status == 0 { + return reportProxyUnreachable(acc.deps, response.transportError) + } + if response.status != 200 { + return accountAPIError(acc.deps, response.body, fmt.Sprintf("failed to set %s", label), response.status) + } + if wantsJSON { + out := jsonwire.ObjectValue() + out.Set("ok", jsonwire.BoolValue(true)) + out.Set("provider", jsonwire.StringValue(name)) + if fieldResp := response.body.Find(strategyKey); fieldResp != nil { + out.Set("strategy", fieldResp) + } + if stickyResp := response.body.Find(stickyKey); stickyResp != nil { + out.Set("stickyLimit", stickyResp) + } + printPrettyJSON(acc.deps, out) + } else { + appliedField := response.body.Find(strategyKey) + if field == "stickyLimit" { + appliedField = response.body.Find(stickyKey) + } + fmt.Fprintln(acc.deps.Stdout, fmt.Sprintf("%s: %s is now %s", name, label, jsonwireWireText(appliedField))) + } + return 0 +} + +// jsonwireWireText renders a json value the way String(jsValue) does for the +// keys poolSetting prints: an absent key is "undefined", JSON null is "null", +// numbers use the V8 spelling. +func jsonwireWireText(value *jsonwire.Value) string { + if value == nil { + return "undefined" + } + switch value.Kind() { + case jsonwire.String: + return value.String() + case jsonwire.Number: + number, err := numberAsFloat(value) + if err != nil { + return "" + } + return jsonwire.FormatV8Number(number) + case jsonwire.Bool: + if value.Bool() { + return "true" + } + return "false" + case jsonwire.Null: + return "null" + default: + return "" + } +} + +// runAccountAlias mirrors cmdAlias. An alias of "-" (or an explicitly empty +// positional) clears the alias. +func runAccountAlias(rest []string, acc accountDeps) int { + wantsJSON := consumeAccountFlag(&rest, "--json") + if len(rest) < 3 || len(rest) > 3 { + return accountExtendedUsageError(acc, "") + } + name := rest[0] + requestedID := rest[1] + requestedAlias := rest[2] + raw := loadAccountConfigRaw() + errorText, rowType, ok := classifyAccount(raw, name) + if !ok { + return accountExtendedUsageError(acc, "Error: "+errorText) + } + id := requestedID + if rowType == accountTypeCodex && requestedID == mainAlias { + id = mainAccountID + } + if id == mainAccountID { + return accountExtendedUsageError(acc, "Error: the main Codex App login cannot be renamed") + } + alias := requestedAlias + if alias == "-" { + alias = "" + } else { + alias = strings.TrimSpace(alias) + } + if len([]rune(alias)) > 80 || accountHasControlChars(alias) { + return accountExtendedUsageError(acc, "Error: alias must be at most 80 printable characters") + } + baseURL := resolveAccountBaseURL(acc) + if baseURL == "" { + return reportProxyUnreachable(acc.deps, "") + } + path := "/api/codex-auth/accounts/alias" + body := jsonwire.ObjectValue() + if rowType == accountTypeCodex { + body.Set("id", jsonwire.StringValue(id)) + body.Set("alias", jsonwire.StringValue(alias)) + } else if rowType == accountTypeOAuth { + path = "/api/oauth/accounts/alias" + body.Set("provider", jsonwire.StringValue(name)) + body.Set("accountId", jsonwire.StringValue(id)) + body.Set("alias", jsonwire.StringValue(alias)) + } else { + path = "/api/providers/keys/alias" + body.Set("name", jsonwire.StringValue(name)) + body.Set("id", jsonwire.StringValue(id)) + body.Set("alias", jsonwire.StringValue(alias)) + } + response := accountHTTP(acc.httpClientOr(), baseURL, "PUT", path, body) + if response.status == 0 { + return reportProxyUnreachable(acc.deps, response.transportError) + } + if response.status != 200 { + return accountAPIError(acc.deps, response.body, fmt.Sprintf("failed to rename %s", requestedID), response.status) + } + value := jsonwire.ObjectValue() + value.Set("ok", jsonwire.BoolValue(true)) + value.Set("provider", jsonwire.StringValue(name)) + value.Set("id", jsonwire.StringValue(id)) + if alias != "" { + value.Set("alias", jsonwire.StringValue(alias)) + } else { + value.Set("alias", jsonwire.NullValue()) + } + if wantsJSON { + printPrettyJSON(acc.deps, value) + } else if alias != "" { + fmt.Fprintln(acc.deps.Stdout, fmt.Sprintf("%s: %s is now \u201c%s\u201d", name, requestedID, alias)) + } else { + fmt.Fprintln(acc.deps.Stdout, fmt.Sprintf("%s: cleared alias for %s", name, requestedID)) + } + return 0 +} + +func accountHasControlChars(value string) bool { + for _, r := range value { + if r < 0x20 || r == 0x7f { + return true + } + } + return false +} + +// runAccountClearCooldown mirrors cmdClearCooldown in account-extended.ts. +func runAccountClearCooldown(rest []string, acc accountDeps) int { + wantsJSON := consumeAccountFlag(&rest, "--json") + name := accountShift(&rest) + requestedID := accountShift(&rest) + if name == "" || requestedID == "" || len(rest) > 0 { + return accountExtendedUsageError(acc, "") + } + raw := loadAccountConfigRaw() + errorText, rowType, ok := classifyAccount(raw, name) + if !ok { + return accountExtendedUsageError(acc, "Error: "+errorText) + } + if rowType != accountTypeCodex { + return accountExtendedUsageError(acc, "Error: "+name+" is not a Codex account pool; cooldown clearing applies to Codex accounts only") + } + id := requestedID + if requestedID == mainAlias { + id = mainAccountID + } + baseURL := resolveAccountBaseURL(acc) + if baseURL == "" { + return reportProxyUnreachable(acc.deps, "") + } + body := jsonwire.ObjectValue() + body.Set("id", jsonwire.StringValue(id)) + response := accountHTTP(acc.httpClientOr(), baseURL, "POST", "/api/codex-auth/accounts/clear-cooldown", body) + if response.status == 0 { + return reportProxyUnreachable(acc.deps, response.transportError) + } + if response.status != 200 { + return accountAPIError(acc.deps, response.body, fmt.Sprintf("failed to clear cooldown for %s", requestedID), response.status) + } + cleared := false + if field := response.body.Find("cleared"); field != nil && field.Kind() == jsonwire.Bool { + cleared = field.Bool() + } + if wantsJSON { + value := jsonwire.ObjectValue() + value.Set("ok", jsonwire.BoolValue(true)) + value.Set("provider", jsonwire.StringValue(name)) + value.Set("id", jsonwire.StringValue(id)) + value.Set("cleared", jsonwire.BoolValue(cleared)) + printPrettyJSON(acc.deps, value) + } else if cleared { + fmt.Fprintln(acc.deps.Stdout, fmt.Sprintf("%s: cooldown lifted for %s", name, requestedID)) + } else { + fmt.Fprintln(acc.deps.Stdout, fmt.Sprintf("%s: no active cooldown for %s", name, requestedID)) + } + return 0 +} + +func signedIntegerString(value string) bool { + if value == "" { + return false + } + start := 0 + if value[0] == '+' || value[0] == '-' { + start = 1 + } + if start >= len(value) { + return false + } + for i := start; i < len(value); i++ { + if value[i] < '0' || value[i] > '9' { + return false + } + } + return true +} diff --git a/go/internal/ocxcli/account_refresh_helpers.go b/go/internal/ocxcli/account_refresh_helpers.go new file mode 100644 index 0000000000..cb7a132b18 --- /dev/null +++ b/go/internal/ocxcli/account_refresh_helpers.go @@ -0,0 +1,189 @@ +// Refresh-line and quota-parts renderers shared by the Go-owned `ocx account +// refresh` handler, mirroring refreshLine/quotaParts/providerQuotaLine and the +// passive-provider note in src/cli/account-extended.ts. +package ocxcli + +import ( + "fmt" + "math" + "net/http" + "strings" + + "github.com/lidge-jun/opencodex/go/internal/jsonwire" +) + +// accountHasPassiveQuota mirrors hasPassiveAccountQuota in src/providers/quota.ts: +// only meta-muse reports quota in-band and has no probe to refresh. +func accountHasPassiveQuota(name string) bool { + return name == "meta-muse" +} + +// codexCatalogRefreshPending mirrors codexCatalogRefreshPending in +// src/cli/account-catalog-refresh.ts (own-property descriptor check, no +// prototype walks). +func codexCatalogRefreshPending(json *jsonwire.Value) bool { + if json == nil || json.Kind() != jsonwire.Object { + return false + } + field := json.Find("catalogRefreshPending") + return field != nil && field.Kind() == jsonwire.Bool && field.Bool() +} + +const codexCatalogRefreshPendingWarning = "Warning: the account change was saved, but the Codex model catalog refresh is pending. Run 'ocx sync' to retry." + +func warnIfCodexCatalogRefreshPending(acc accountDeps, json *jsonwire.Value) { + if codexCatalogRefreshPending(json) { + reportAccountStderrLine(acc, codexCatalogRefreshPendingWarning) + } +} + +// isoResetState mirrors resetIso: it reports a reset timestamp string only for +// finite numbers; seconds (<1e10) are promoted to millis. +func accountResetISO(value float64, ok bool) (string, bool) { + if !ok { + return "", false + } + text := isoReset(value, true) + if text == "" { + return "", false + } + return text, true +} + +// accountQuotaParts mirrors quotaParts in account-extended.ts. +func accountQuotaParts(quota *jsonwire.Value) []string { + if quota == nil || quota.Kind() != jsonwire.Object { + return nil + } + var parts []string + add := func(label string, percent float64, ok bool, resetAt float64, hasReset bool) { + if !ok { + return + } + parts = append(parts, fmt.Sprintf("%s %s%%", label, jsonwire.FormatV8Number(percent))) + if reset, ok := accountResetISO(resetAt, hasReset); ok { + parts = append(parts, "resets "+reset) + } + } + percent, ok := accountNumber(quota, "fiveHourPercent") + resetAt, hasReset := accountNumber(quota, "fiveHourResetAt") + add("5h", percent, ok, resetAt, hasReset) + percent, ok = accountNumber(quota, "weeklyPercent") + resetAt, hasReset = accountNumber(quota, "weeklyResetAt") + add("weekly", percent, ok, resetAt, hasReset) + percent, ok = accountNumber(quota, "monthlyPercent") + resetAt, hasReset = accountNumber(quota, "monthlyResetAt") + add("monthly", percent, ok, resetAt, hasReset) + if windows := activeOrEmptyArray(quota, "customWindows"); windows != nil { + for _, window := range windows { + label := objectString(window, "label") + percent, ok = accountNumber(window, "percent") + resetAt, hasReset = accountNumber(window, "resetAt") + add(label, percent, ok, resetAt, hasReset) + } + } + return parts +} + +// accountProviderQuotaLine mirrors providerQuotaLine. +func accountProviderQuotaLine(name string, report *jsonwire.Value) string { + parts := []string{name} + if report != nil && report.Kind() == jsonwire.Object { + if quota := report.Find("quota"); quota != nil { + parts = append(parts, accountQuotaParts(quota)...) + } + } + return strings.Join(parts, " ") +} + +// accountRefreshLine mirrors refreshLine for a codex row: email/plan are +// dropped when empty, quota parts or the quota: unknown marker, then +// needs-reauth. +func accountRefreshLine(row *accountRow) string { + var parts []string + parts = append(parts, displayID(row.id)) + if row.hasEmail && row.email != "" { + parts = append(parts, row.email) + } + if row.hasPlan && row.plan != "" { + parts = append(parts, row.plan) + } + if row.paused { + parts = append(parts, "paused") + } + quotaParts := accountQuotaParts(row.quota) + if len(quotaParts) == 0 { + parts = append(parts, "quota: unknown") + } else { + parts = append(parts, strings.Join(quotaParts, " ")) + } + if row.needsReauthSet && row.needsReauth { + parts = append(parts, "needs-reauth") + } + var kept []string + for _, part := range parts { + if part != "" { + kept = append(kept, part) + } + } + return strings.Join(kept, " ") +} + +// accountQuotaPercent reads fiveHourPercent ?? shortPercent off a quota object. +func accountQuotaPercent(quota *jsonwire.Value) (float64, bool) { + for _, key := range []string{"fiveHourPercent", "shortPercent"} { + if number, ok := accountNumber(quota, key); ok { + return number, true + } + } + return 0, false +} + +// accountQuotaText mirrors quotaText in account.ts for the QUOTA table column. +func accountQuotaText(row *accountRow) string { + if row.quotaUnavailable { + return "unavailable" + } + if !row.hasQuota || row.quota == nil { + return "-" + } + var parts []string + if percent, ok := accountQuotaPercent(row.quota); ok { + parts = append(parts, "5h "+jsonwire.FormatV8Number(percent)+"%") + } + if percent, ok := accountNumber(row.quota, "weeklyPercent"); ok { + parts = append(parts, "wk "+jsonwire.FormatV8Number(percent)+"%") + } + if percent, ok := accountNumber(row.quota, "monthlyPercent"); ok { + parts = append(parts, "mo "+jsonwire.FormatV8Number(mathRound(percent))+"%") + } + if len(parts) == 0 { + return "-" + } + return strings.Join(parts, " ") +} + +func mathRound(value float64) float64 { + return math.Floor(value + 0.5) +} + +// fetchProviderQuotaReport mirrors fetchProviderQuotaReport in account-api.ts: +// a refreshed quota report list is filtered for the requested provider. A +// missing report is not an error (report == nil, status 200). +func fetchProviderQuotaReport(client *http.Client, baseURL, name string) (int, *jsonwire.Value, *jsonwire.Value, string) { + response := accountHTTP(client, baseURL, "GET", "/api/provider-quotas?refresh=1", nil) + if response.status == 0 { + return 0, nil, response.body, response.transportError + } + if response.status != 200 { + return response.status, nil, response.body, "" + } + var report *jsonwire.Value + for _, candidate := range activeOrEmptyArray(response.body, "reports") { + if objectString(candidate, "provider") == name { + report = candidate + break + } + } + return 200, report, nil, "" +} diff --git a/go/internal/ocxcli/account_runtime.go b/go/internal/ocxcli/account_runtime.go new file mode 100644 index 0000000000..89c646e6c6 --- /dev/null +++ b/go/internal/ocxcli/account_runtime.go @@ -0,0 +1,581 @@ +// Shared data-access layer for the Go-owned `ocx account` command family. +// +// This mirrors src/cli/account-api.ts (live-proxy discovery, the /api/* +// management client and its transport sentinel, the account classification +// rules and the apiError taxonomy) plus the pieces of src/cli/account.ts and +// src/cli/account-extended.ts that every subcommand shares (the usage blocks, +// the account table renderer, candidate names). The differential oracle feeds +// the same fixture config + management payloads to the TypeScript CLI and this +// binary and requires byte-identical stdout/stderr and exit codes. +package ocxcli + +import ( + "fmt" + "math" + "net/http" + "strings" + "time" + + "github.com/lidge-jun/opencodex/go/internal/config" + "github.com/lidge-jun/opencodex/go/internal/jsonwire" +) + +// Exit codes from the account CLI taxonomy (account-api.ts apiError and the +// runtime-api runCliAction used by the account-auth subcommands). +const ( + accountExitMissing = 4 // RuntimeApiError 404 + accountExitConflict = 5 // RuntimeApiError 409 + mainAccountID = "__main__" + mainAlias = "main" +) + +// accountUsage is ACCOUNT_USAGE in src/cli/account.ts (the top-level usage the +// bare command and its list/current/use handlers print on stderr). +const accountUsage = `Usage: + ocx account list [provider] [--json] [--all] [--quota [--refresh]] + ocx account current [--json] + ocx account use [--json] + ocx account refresh [--json] + ocx account auto-switch > [--json] + ocx account alias [--json] + ocx account priority [<-100..100|first|earlier|normal|later|last|reset>] [--json] + ocx account pause [--json] + ocx account resume [--json] + ocx account pause-exhausted [--json] + ocx account strategy [] [--json] + ocx account sticky [<1-100>] [--json] + ocx account remove --yes [--json] + ocx account clear-cooldown [--json] + ocx account add-key [--label